From d59f528c84d1e35101dea603bd223eed46c01918 Mon Sep 17 00:00:00 2001 From: Claudio Sciotto Date: Sun, 19 Jan 2025 00:26:26 -0500 Subject: [PATCH] Added Projects page * Missing: UI Fixes --- src/app/projects/page.tsx | 37 +----- src/components/admin/projects/AddForm.tsx | 94 ++++++++++--- src/components/admin/projects/EditForm.tsx | 40 +++--- src/components/projects/ProjectModal.tsx | 128 ++++++++++++++++++ src/components/projects/ProjectsPage.tsx | 148 +++++++++++++++++++++ src/types/project.ts | 49 +++---- tailwind.config.ts | 4 + 7 files changed, 411 insertions(+), 89 deletions(-) create mode 100644 src/components/projects/ProjectModal.tsx create mode 100644 src/components/projects/ProjectsPage.tsx diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx index 160966c..b7c6a22 100644 --- a/src/app/projects/page.tsx +++ b/src/app/projects/page.tsx @@ -1,41 +1,12 @@ "use client"; -import React, { useState } from "react"; +import React from "react"; +import ProjectsPage from "@/components/projects/ProjectsPage"; export default function Projects() { - const [isHovered, setIsHovered] = useState(false); - return ( -
-
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - {/* Main text */} -

- COMING -

-

- SOON! -

- - {/* Animated underline */} -
-
-
- - {/* Subtitle */} -

- We're building something awesome. -
- Stay tuned! -

-
+
+
); } diff --git a/src/components/admin/projects/AddForm.tsx b/src/components/admin/projects/AddForm.tsx index 08dbe94..f08abca 100644 --- a/src/components/admin/projects/AddForm.tsx +++ b/src/components/admin/projects/AddForm.tsx @@ -1,8 +1,10 @@ +"use client"; + import React, { useState } from "react"; import { addDoc, collection } from "firebase/firestore"; import { ref, uploadBytes, getDownloadURL } from "firebase/storage"; import { db, storage } from "@/lib/firebase/firebase"; -import { ProjectFormData, Parts, Project } from "@/types/project"; +import { ProjectFormData, Parts } from "@/types/project"; interface AddFormProps { initialData?: ProjectFormData; @@ -27,6 +29,10 @@ const emptyFormState: ProjectFormData = { Photos: "", Image: "", Builders: [""], + semester: { + term: "Fall", + year: new Date().getFullYear(), + }, }; export default function AddForm({ @@ -40,7 +46,6 @@ export default function AddForm({ const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); - const [selectedImage, setSelectedImage] = useState(null); const [imagePreview, setImagePreview] = useState( formData.Image || "", @@ -48,23 +53,33 @@ export default function AddForm({ const [uploadProgress, setUploadProgress] = useState(0); const handleChange = ( - e: React.ChangeEvent, + e: React.ChangeEvent< + HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement + >, ) => { const { name, value } = e.target; if (name.includes(".")) { - // Handle nested Parts object + // Handle nested objects (Parts and semester) const [parent, child] = name.split("."); if (parent === "Parts") { - setFormData((prev: ProjectFormData) => ({ + setFormData((prev) => ({ ...prev, Parts: { ...prev.Parts, [child]: value, }, })); + } else if (parent === "semester") { + setFormData((prev) => ({ + ...prev, + semester: { + ...prev.semester, + [child]: child === "year" ? Number(value) : value, + }, + })); } } else { - setFormData((prev: ProjectFormData) => ({ + setFormData((prev) => ({ ...prev, [name]: value, })); @@ -103,33 +118,39 @@ export default function AddForm({ try { let imageUrl = formData.Image; + // Upload new image if selected if (selectedImage) { imageUrl = await uploadImage(selectedImage); } - // Filter out empty builders - const cleanedData = { + // Clean the data + const cleanedData: ProjectFormData = { ...formData, Image: imageUrl, 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 setError(e instanceof Error ? e.message : "An error occurred"); - console.error("Error adding document: ", e); } finally { setIsSubmitting(false); } @@ -174,11 +195,11 @@ export default function AddForm({ }; return ( -
+
{/* Basic Information */}
-
+
+ {/* Add Semester Selection */} +
+
+ + +
+ +
+ + +
+
+
)} @@ -329,7 +394,7 @@ export default function AddForm({ @@ -339,7 +404,7 @@ export default function AddForm({ {error &&
{error}
} - {success && (
{isEditing diff --git a/src/components/admin/projects/EditForm.tsx b/src/components/admin/projects/EditForm.tsx index 44b1485..a3c30b5 100644 --- a/src/components/admin/projects/EditForm.tsx +++ b/src/components/admin/projects/EditForm.tsx @@ -64,7 +64,6 @@ export default function EditForm() { try { const projectRef = doc(db, "Projects", selectedProject.id); - // Convert ProjectFormData to a plain object that Firestore can handle const firestoreData = { Youtube: updatedData.Youtube, Description: updatedData.Description, @@ -72,6 +71,10 @@ export default function EditForm() { Photos: updatedData.Photos, Image: updatedData.Image, Builders: updatedData.Builders, + semester: { + term: updatedData.semester.term, + year: updatedData.semester.year, + }, Parts: { RAM: updatedData.Parts.RAM, Cooling: updatedData.Parts.Cooling, @@ -88,16 +91,13 @@ export default function EditForm() { // Update local state setProjects((prevProjects) => - prevProjects.map((project) => - project.id === selectedProject.id - ? { ...project, ...updatedData } - : project, + prevProjects.map((proj) => + proj.id === selectedProject.id ? { ...proj, ...updatedData } : proj, ), ); setIsModalOpen(false); setSelectedProject(null); - //alert('Project updated successfully'); } catch (err) { console.error("Error updating project:", err); setError("Failed to update project"); @@ -163,26 +163,30 @@ export default function EditForm() {
{/* Projects List */} -
- {projects - .filter((project) => - project.Title.toLowerCase().includes(searchQuery.toLowerCase()), - ) - .map((project) => ( + {projects + .filter( + ( + proj, // Changed from project to proj + ) => proj.Title.toLowerCase().includes(searchQuery.toLowerCase()), + ) + .map( + ( + proj, // Changed from project to proj + ) => (
-

{project.Title}

+

{proj.Title}

- ))} -
+ ), + )} {/* Edit Modal */} {isModalOpen && selectedProject && ( diff --git a/src/components/projects/ProjectModal.tsx b/src/components/projects/ProjectModal.tsx new file mode 100644 index 0000000..e073787 --- /dev/null +++ b/src/components/projects/ProjectModal.tsx @@ -0,0 +1,128 @@ +import { useState } from "react"; +import { Project } from "@/types/project"; +import { X } from "lucide-react"; + +interface ProjectModalProps { + project: Project; + onClose: () => void; +} + +export default function ProjectModal({ project, onClose }: ProjectModalProps) { + const [activeTab, setActiveTab] = useState< + "description" | "specs" | "builders" + >("description"); + + return ( +
+
+ {/* Noise background */} +
+ + {/* Close button */} + + +
+ {/* Left side - Image */} +
+
+ {project.Title} + {/* Gradient overlay */} +
+
+
+ + {/* Right side - Content */} +
+

+ {project.Title} +

+

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

+ + {/* Tabs */} +
+ {(["description", "specs", "builders"] as const).map((tab) => ( + + ))} +
+ + {/* Tab Content */} +
+ {activeTab === "description" && ( +

+ {project.Description} +

+ )} + + {activeTab === "specs" && ( +
+ {Object.entries(project.Parts).map(([part, value]) => ( +
+ {part} + {value} +
+ ))} +
+ )} + + {activeTab === "builders" && ( +
+ {project.Builders.map((builder, index) => ( +
+ {builder} +
+ ))} +
+ )} +
+ + {/* Links */} + +
+
+
+
+ ); +} diff --git a/src/components/projects/ProjectsPage.tsx b/src/components/projects/ProjectsPage.tsx new file mode 100644 index 0000000..0eb8485 --- /dev/null +++ b/src/components/projects/ProjectsPage.tsx @@ -0,0 +1,148 @@ +import GlowingLine from "@/components/decorations/GlowingLine"; +import { useEffect, useState } from "react"; +import { collection, getDocs } from "firebase/firestore"; +import { db } from "@/lib/firebase/firebase"; +import { Project } from "@/types/project"; +import ProjectModal from "./ProjectModal"; + +const sortProjects = (projects: Project[]) => { + const termOrder = { Fall: 3, Summer: 2, Spring: 1 }; + + return [...projects].sort((a, b) => { + // First compare years + if (a.semester.year !== b.semester.year) { + return b.semester.year - a.semester.year; // Descending order + } + + // If years are equal, compare terms + return termOrder[b.semester.term] - termOrder[a.semester.term]; + }); +}; + +export default function ProjectsPage() { + const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(true); + const [selectedProject, setSelectedProject] = useState(null); + + useEffect(() => { + const fetchProjects = async () => { + try { + const querySnapshot = await getDocs(collection(db, "Projects")); + const projectsData = querySnapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })) as Project[]; + setProjects(projectsData); + } catch (error) { + console.error("Error fetching projects:", error); + } finally { + setLoading(false); + } + }; + + fetchProjects(); + }, []); + + return ( +
+ {/* Applied noise background for consistency */} +
+ + {/* Hero Section with Title */} +
+ {/* Decorative Lines */} + + + + + + + {/* Title Content */} +
+

+ Our Projects +

+

+ Discover our latest custom PC builds +

+
+
+ + {/* Projects Grid */} +
+ {loading ? ( +
+
+
+ ) : ( +
+ {sortProjects(projects).map((project) => ( +
setSelectedProject(project)} + className="shadow-white-glow group relative cursor-pointer overflow-hidden rounded-lg bg-gray-950 p-4 transition-transform hover:scale-105" + > + {/* Project Image */} +
+ {project.Title} +
+ + {/* Project Info */} +
+

+ {project.Title} +

+

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

+
+
+ ))} +
+ )} +
+ + {/* Project Modal */} + {selectedProject && ( + setSelectedProject(null)} + /> + )} +
+ ); +} diff --git a/src/types/project.ts b/src/types/project.ts index 9871a46..7dc77c0 100644 --- a/src/types/project.ts +++ b/src/types/project.ts @@ -1,25 +1,28 @@ export interface Parts { - RAM: string; - Cooling: string; - Case: string; - Motherboard: string; - PSU: string; - GPU: string; - Storage: string; - CPU: string; - } - - export interface ProjectFormData { - Youtube: string; - Description: string; - Parts: Parts; - Title: string; - Photos: string; - Image: string; - Builders: string[]; - } - - export interface Project extends ProjectFormData { - id: string; - } + RAM: string; + Cooling: string; + Case: string; + Motherboard: string; + PSU: string; + GPU: string; + Storage: string; + CPU: string; +} +export interface ProjectFormData { + Youtube: string; + Description: string; + Parts: Parts; + Title: string; + Photos: string; + Image: string; + Builders: string[]; + semester: { + term: "Spring" | "Summer" | "Fall"; + year: number; + }; +} + +export interface Project extends ProjectFormData { + id: string; +} diff --git a/tailwind.config.ts b/tailwind.config.ts index 1ba8981..3f5cfbf 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -37,6 +37,10 @@ const config = { fontFamily: { Michroma: ["Michroma", "sans-serif"], }, + boxShadow: { + "white-glow": "0 0 15px rgba(255,255,255,0.15)", + "white-glow-hover": "0 0 30px rgba(255,255,255,0.3)", + }, }, }, plugins: [require("tailwindcss-animate")],