diff --git a/src/app/testing/page.tsx b/src/app/testing/page.tsx deleted file mode 100644 index 24ec643..0000000 --- a/src/app/testing/page.tsx +++ /dev/null @@ -1,222 +0,0 @@ -// page.tsx -"use client"; - -import React from "react"; -import Image from "next/image"; -import { ChevronDown } from "lucide-react"; -import GlowingLine from "@/components/decorations/GlowingLine"; - -const Website = () => { - return ( -
- {/* Hero Section with Background Image */} -
- {/* Hero Content */} -
- {/* Content Container with Grid */} -
- {/* Logo Section - Left Side */} -
- {/* Outer Glow */} -
- {/* Inner Glow */} -
- {/* Logo */} -
- SPCB Logo -
-
- - {/* Text Section - Right Side */} -
-

- The Society of PC Building -

- - {/* Social Stats */} -
- {/*TODO: Import discord and instagram api to get follower count*/} -

Discord count: 150 members

-

Instagram count: 200 followers

-
- - {/* Join Button */} - -
-
- - {/* Scroll Indicator */} -
- -
-
-
- - {/* WORK FROM HERE */} -
- {/* Background Lines */} -
- {/* Vertical line in the middle */} - - {/* Horizontal lines */} - - - -
- - {/* Content Cards */} -
-
- {" "} - {/* Adjust spacing between cards */} - {/* Socials Card */} -
-
-
- Socials -
-
-

Socials

-
-
-
- {/* GBMs Card */} -
-
-
- GBMs -
-
-

GBMs

-
-
-
- {/* PC Builds Card */} -
-
-
- PC Builds -
-
-

PC Builds

-
-
-
-
-
-
- {/* WORK UNTIL HERE */} - {/* Footer */} -
-
-

- © {new Date().getFullYear()} The Society of PC Building. All - rights reserved. -

-
-
-
- ); -}; - -export default Website; diff --git a/src/components/admin/projects/AddForm.tsx b/src/components/admin/projects/AddForm.tsx index f08abca..13c551f 100644 --- a/src/components/admin/projects/AddForm.tsx +++ b/src/components/admin/projects/AddForm.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useState } from "react"; -import { addDoc, collection } from "firebase/firestore"; +import { addDoc, collection, Timestamp } from "firebase/firestore"; import { ref, uploadBytes, getDownloadURL } from "firebase/storage"; import { db, storage } from "@/lib/firebase/firebase"; import { ProjectFormData, Parts } from "@/types/project"; @@ -12,8 +12,11 @@ interface AddFormProps { isEditing?: boolean; } +const DEFAULT_YOUTUBE = "https://www.youtube.com/@pcbuildinguf"; +const DEFAULT_PHOTOS = "https://photos.app.goo.gl/Mua121F4n2MVZ9wn8"; + const emptyFormState: ProjectFormData = { - Youtube: "", + Youtube: DEFAULT_YOUTUBE, Description: "", Parts: { RAM: "", @@ -26,8 +29,9 @@ const emptyFormState: ProjectFormData = { CPU: "", }, Title: "", - Photos: "", + Photos: DEFAULT_PHOTOS, Image: "", + buildDate: Timestamp.fromDate(new Date()), Builders: [""], semester: { term: "Fall", @@ -58,8 +62,21 @@ export default function AddForm({ >, ) => { const { name, value } = e.target; + + if (name === "buildDate") { + // Create date from input value and adjust for timezone + const date = new Date(value); + const timezoneOffset = date.getTimezoneOffset() * 60000; + const adjustedDate = new Date(date.getTime() + timezoneOffset); + + setFormData((prev) => ({ + ...prev, + buildDate: Timestamp.fromDate(adjustedDate), + })); + return; + } + if (name.includes(".")) { - // Handle nested objects (Parts and semester) const [parent, child] = name.split("."); if (parent === "Parts") { setFormData((prev) => ({ @@ -79,10 +96,22 @@ export default function AddForm({ })); } } else { - setFormData((prev) => ({ - ...prev, - [name]: value, - })); + if (name === "Youtube" && value.trim() === "") { + setFormData((prev) => ({ + ...prev, + [name]: DEFAULT_YOUTUBE, + })); + } else if (name === "Photos" && value.trim() === "") { + setFormData((prev) => ({ + ...prev, + [name]: DEFAULT_PHOTOS, + })); + } else { + setFormData((prev) => ({ + ...prev, + [name]: value, + })); + } } }; @@ -109,6 +138,42 @@ export default function AddForm({ })); }; + const handleImageChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + if (!file.type.startsWith("image/")) { + setError("Please upload an image file"); + return; + } + + if (file.size > 5 * 1024 * 1024) { + setError("Image must be less than 5MB"); + return; + } + + setSelectedImage(file); + const reader = new FileReader(); + reader.onloadend = () => { + setImagePreview(reader.result as string); + }; + reader.readAsDataURL(file); + } + }; + + const uploadImage = async (file: File): Promise => { + const fileName = `${Date.now()}-${file.name}`; + const storageRef = ref(storage, `project-images/${fileName}`); + + try { + const snapshot = await uploadBytes(storageRef, file); + const downloadURL = await getDownloadURL(snapshot.ref); + return downloadURL; + } catch (error) { + console.error("Error uploading image:", error); + throw new Error("Failed to upload image"); + } + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsSubmitting(true); @@ -118,86 +183,43 @@ export default function AddForm({ try { let imageUrl = formData.Image; - // Upload new image if selected if (selectedImage) { imageUrl = await uploadImage(selectedImage); } - // Clean the data const cleanedData: ProjectFormData = { ...formData, Image: imageUrl, + Youtube: formData.Youtube || DEFAULT_YOUTUBE, + Photos: formData.Photos || DEFAULT_PHOTOS, Builders: formData.Builders.filter((builder) => builder.trim() !== ""), }; if (onSubmit) { - // If onSubmit prop exists (editing mode), use it await onSubmit(cleanedData); } else { - // Direct submission to Firebase - console.log("Submitting to Firebase:", cleanedData); // Debug log const docRef = await addDoc(collection(db, "Projects"), cleanedData); console.log("Document written with ID: ", docRef.id); } setSuccess(true); - // Only reset form if not editing if (!isEditing) { setFormData(emptyFormState); setSelectedImage(null); setImagePreview(""); } } catch (e) { - console.error("Error in submission:", e); // Debug log + console.error("Error in submission:", e); setError(e instanceof Error ? e.message : "An error occurred"); } finally { setIsSubmitting(false); } }; - const handleImageChange = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (file) { - if (!file.type.startsWith("image/")) { - setError("Please upload an image file"); - return; - } - - if (file.size > 5 * 1024 * 1024) { - setError("Image must be less than 5MB"); - return; - } - - setSelectedImage(file); - console.log(file); //REMOVE - - const reader = new FileReader(); - reader.onloadend = () => { - setImagePreview(reader.result as string); - }; - reader.readAsDataURL(file); - } - }; - - const uploadImage = async (file: File): Promise => { - const fileName = `${Date.now()}-${file.name}`; - const storageRef = ref(storage, `project-images/${fileName}`); - - try { - const snapshot = await uploadBytes(storageRef, file); - const downloadURL = await getDownloadURL(snapshot.ref); - return downloadURL; - } catch (error) { - console.error("Error uploading image:", error); - throw new Error("Failed to upload image"); - } - }; - return (
- {/* Basic Information */}
@@ -215,7 +237,28 @@ export default function AddForm({ />
- {/* Add Semester Selection */} +
+ + +
+
@@ -303,8 +346,7 @@ export default function AddForm({ value={formData.Photos} onChange={handleChange} className="mt-1 w-full rounded-md border p-2" - placeholder="Url to photo folder" - required + placeholder={DEFAULT_PHOTOS} />
diff --git a/src/components/admin/projects/EditForm.tsx b/src/components/admin/projects/EditForm.tsx index a3c30b5..1be53fc 100644 --- a/src/components/admin/projects/EditForm.tsx +++ b/src/components/admin/projects/EditForm.tsx @@ -5,39 +5,66 @@ import { doc, updateDoc, deleteDoc, + Timestamp, } from "firebase/firestore"; import { db } from "@/lib/firebase/firebase"; import { Search, Trash2 } from "lucide-react"; -import { Project, ProjectFormData, Parts } from "@/types/project"; +import { Project, ProjectFormData } from "@/types/project"; import AddForm from "./AddForm"; import { LuLoader2, LuX } from "react-icons/lu"; +const DEFAULT_YOUTUBE = "https://www.youtube.com/@pcbuildinguf"; +const DEFAULT_PHOTOS = "https://photos.app.goo.gl/Mua121F4n2MVZ9wn8"; + export default function EditForm() { - const [projects, setProjects] = useState([]); // Store all projects - const [searchQuery, setSearchQuery] = useState(""); // Store search input - const [selectedProject, setSelectedProject] = useState(null); //current selected project for editing - const [isModalOpen, setIsModalOpen] = useState(false); // Control modal visibility - const [loading, setLoading] = useState(true); // Loading state for initial data fetch - const [error, setError] = useState(null); // Error handling - const [isDeleting, setIsDeleting] = useState(false); // Loading state for delete operation - const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); // Control delete confirmation modal - const [projectToDelete, setProjectToDelete] = useState(null); // Store project to delete + const [projects, setProjects] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedProject, setSelectedProject] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [isDeleting, setIsDeleting] = useState(false); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [projectToDelete, setProjectToDelete] = useState(null); useEffect(() => { fetchProjects(); }, []); - // Function to fetch all projects from Firestore const fetchProjects = async () => { try { const querySnapshot = await getDocs(collection(db, "Projects")); - const projectsData = querySnapshot.docs.map((doc) => ({ - id: doc.id, - ...doc.data(), - })) as Project[]; + const projectsData = querySnapshot.docs.map((doc) => { + const data = doc.data(); + + // Ensure buildDate is a Timestamp + let buildDate; + if (data.buildDate instanceof Timestamp) { + buildDate = data.buildDate; + } else if (data.buildDate) { + // If it's a different date format, convert to Timestamp + buildDate = Timestamp.fromDate(new Date(data.buildDate)); + } else { + // If no date exists, use current time + buildDate = Timestamp.fromDate(new Date()); + } + + return { + id: doc.id, + ...data, + buildDate, + // Ensure default values for links + Youtube: data.Youtube || DEFAULT_YOUTUBE, + Photos: data.Photos || DEFAULT_PHOTOS, + }; + }) as Project[]; + + // Sort projects by date, most recent first + const sortedProjects = projectsData.sort( + (a, b) => b.buildDate.toMillis() - a.buildDate.toMillis(), + ); - console.log("Projects:", projectsData); - setProjects(projectsData); + setProjects(sortedProjects); setLoading(false); } catch (err) { setError("Failed to fetch projects"); @@ -46,18 +73,11 @@ export default function EditForm() { } }; - // Filter projects based on search query - const filteredProjects = projects.filter((project) => { - return project.Title.toLowerCase().includes(searchQuery.toLowerCase()); - }); - - // Handler for clicking edit button const handleEditClick = (project: Project) => { setSelectedProject(project); setIsModalOpen(true); }; - //handler for updating a project const handleUpdate = async (updatedData: ProjectFormData) => { if (!selectedProject) return; @@ -65,11 +85,12 @@ export default function EditForm() { const projectRef = doc(db, "Projects", selectedProject.id); const firestoreData = { - Youtube: updatedData.Youtube, + Youtube: updatedData.Youtube || DEFAULT_YOUTUBE, Description: updatedData.Description, Title: updatedData.Title, - Photos: updatedData.Photos, + Photos: updatedData.Photos || DEFAULT_PHOTOS, Image: updatedData.Image, + buildDate: updatedData.buildDate, Builders: updatedData.Builders, semester: { term: updatedData.semester.term, @@ -89,12 +110,15 @@ export default function EditForm() { await updateDoc(projectRef, firestoreData); - // Update local state - setProjects((prevProjects) => - prevProjects.map((proj) => + // Update local state and sort + setProjects((prevProjects) => { + const updatedProjects = prevProjects.map((proj) => proj.id === selectedProject.id ? { ...proj, ...updatedData } : proj, - ), - ); + ); + return updatedProjects.sort( + (a, b) => b.buildDate.toMillis() - a.buildDate.toMillis(), + ); + }); setIsModalOpen(false); setSelectedProject(null); @@ -122,7 +146,6 @@ export default function EditForm() { setShowDeleteConfirm(false); setProjectToDelete(null); - //alert('Project deleted successfully'); } catch (err) { console.error("Error deleting project:", err); setError("Failed to delete project"); @@ -131,7 +154,6 @@ export default function EditForm() { } }; - // Render loading state if (loading) { return (
@@ -142,119 +164,120 @@ export default function EditForm() { ); } - // Render error state if (error) { - return
Error: {error}
; + return
Error: {error}
; } return ( - <> -
- {/* Search input */} -
- - setSearchQuery(e.target.value)} - className="w-full rounded border p-2 pl-11 text-sm sm:text-base" - /> -
+
+ {/* Search input */} +
+ + setSearchQuery(e.target.value)} + className="w-full rounded border p-2 pl-11 text-sm sm:text-base" + /> +
- {/* Projects List */} + {/* Projects List */} +
{projects - .filter( - ( - proj, // Changed from project to proj - ) => proj.Title.toLowerCase().includes(searchQuery.toLowerCase()), + .filter((project) => + project.Title.toLowerCase().includes(searchQuery.toLowerCase()), ) - .map( - ( - proj, // Changed from project to proj - ) => ( -
-

{proj.Title}

-
- - -
+ .map((project) => ( +
+
+

{project.Title}

+

+ Built on:{" "} + {new Date( + project.buildDate.toDate().setHours(12), + ).toLocaleDateString()} +

- ), - )} - - {/* Edit Modal */} - {isModalOpen && selectedProject && ( -
-
-
-

Edit Project

- setIsModalOpen(false)} - > - - -
- -
-
- )} - - {/* Delete Confirmation Modal */} - {showDeleteConfirm && ( -
-
-

Confirm Delete

-

- Are you sure you want to delete "{projectToDelete?.Title}"? This - action cannot be undone. -

-
+
-
- )} + ))}
- + + {/* Edit Modal */} + {isModalOpen && selectedProject && ( +
+
+
+

Edit Project

+ +
+ +
+
+ )} + + {/* Delete Confirmation Modal */} + {showDeleteConfirm && ( +
+
+

Confirm Delete

+

+ Are you sure you want to delete "{projectToDelete?.Title}"? This + action cannot be undone. +

+
+ + +
+
+
+ )} +
); } diff --git a/src/components/projects/ProjectModal.tsx b/src/components/projects/ProjectModal.tsx index e073787..3a9f7f3 100644 --- a/src/components/projects/ProjectModal.tsx +++ b/src/components/projects/ProjectModal.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useRef, useEffect } from "react"; import { Project } from "@/types/project"; import { X } from "lucide-react"; @@ -12,9 +12,33 @@ export default function ProjectModal({ project, onClose }: ProjectModalProps) { "description" | "specs" | "builders" >("description"); + const modalRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + modalRef.current && + !modalRef.current.contains(event.target as Node) + ) { + onClose(); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [onClose]); + return ( -
-
+
onClose()} + > +
e.stopPropagation()} + > {/* Noise background */}
@@ -26,9 +50,9 @@ export default function ProjectModal({ project, onClose }: ProjectModalProps) { -
+
{/* Left side - Image */} -
+
{project.Title} -

- {project.semester.term} {project.semester.year} -

+
+

+ {project.semester.term} {project.semester.year} +

+

+ Built on{" "} + {new Date( + project.buildDate.toDate().getTime() - + project.buildDate.toDate().getTimezoneOffset() * 60000, + ).toLocaleDateString()} +

+
{/* Tabs */}
diff --git a/src/components/projects/ProjectsPage.tsx b/src/components/projects/ProjectsPage.tsx index 0eb8485..4ace09f 100644 --- a/src/components/projects/ProjectsPage.tsx +++ b/src/components/projects/ProjectsPage.tsx @@ -15,7 +15,14 @@ const sortProjects = (projects: Project[]) => { } // If years are equal, compare terms - return termOrder[b.semester.term] - termOrder[a.semester.term]; + const termComparison = + termOrder[b.semester.term] - termOrder[a.semester.term]; + if (termComparison !== 0) { + return termComparison; + } + + // If terms are equal, compare dates + return b.buildDate.toMillis() - a.buildDate.toMillis(); }); }; @@ -44,7 +51,7 @@ export default function ProjectsPage() { }, []); return ( -
+
{/* Applied noise background for consistency */}
@@ -110,7 +117,7 @@ export default function ProjectsPage() {
setSelectedProject(project)} - className="shadow-white-glow group relative cursor-pointer overflow-hidden rounded-lg bg-gray-950 p-4 transition-transform hover:scale-105" + className="group relative cursor-pointer overflow-hidden rounded-lg bg-gray-950 p-4 shadow-white-glow transition-transform hover:scale-105" > {/* Project Image */}
diff --git a/src/types/project.ts b/src/types/project.ts index 7dc77c0..1649cf3 100644 --- a/src/types/project.ts +++ b/src/types/project.ts @@ -1,3 +1,5 @@ +import { Timestamp } from "firebase/firestore"; + export interface Parts { RAM: string; Cooling: string; @@ -17,6 +19,7 @@ export interface ProjectFormData { Photos: string; Image: string; Builders: string[]; + buildDate: Timestamp; // Added buildDate field semester: { term: "Spring" | "Summer" | "Fall"; year: number;