diff --git a/app/builder/layout.tsx b/app/builder/layout.tsx index 51c8615..a09eef3 100644 --- a/app/builder/layout.tsx +++ b/app/builder/layout.tsx @@ -1,4 +1,3 @@ -import { Fade } from "react-awesome-reveal"; import { ResumePreview } from "../../components/elements/resume/ResumePreview"; import { BuilderHeaderBar } from "../../components/elements/builder/BuilderHeaderBar"; import { BuilderSidebar } from "../../components/elements/builder/BuilderSidebar"; @@ -6,19 +5,6 @@ import { TooltipProvider } from "../../components/ui/tooltip"; import { ResumePreviewControls } from "../../components/elements/resume/ResumePreviewControls"; import { BuilderClientWrapper } from "../../components/elements/builder/BuilderClientWrapper"; -/** - * Builder layout creates a split-screen experience: - * - Left side (50vw): Form sections (children) - scrollable - * - Right side (fixed): Live preview panel with PDF preview/download buttons - * - * The preview panel is fixed position and remains visible while scrolling through forms. - * All preview components read from Zustand store and update reactively. - * - * Also includes: - * - Floating help widget for contextual assistance - * - Guided tour for first-time users - */ - export const metadata = { title: "Builder", }; @@ -29,41 +15,55 @@ export default function BuildLayout({ children: React.ReactNode; }) { return ( -
- {/* Builder sidebar (fixed overlay on left side) */} +
+
+
+
+
+ - {/* Main content (form sections) */} -
- {children} -
- {/* Resume Preview */} -
-
-
- RESUMEPREVIEW -
- {/* Resume Preview Controls */} - -
- -
-
-
+
+
+
{children}
-
- - - +
-
+
- {/* Client-side components: Help widget and Guided tour */}
); diff --git a/app/builder/page.tsx b/app/builder/page.tsx index d5ecb92..0f9162c 100644 --- a/app/builder/page.tsx +++ b/app/builder/page.tsx @@ -1,24 +1,23 @@ "use client"; -import { useState, useEffect, Suspense, useMemo } from "react"; + +import { Suspense, useEffect, useMemo, useState } from "react"; import { useSearchParams, useRouter } from "next/navigation"; +import { motion, AnimatePresence } from "framer-motion"; +import { ClipboardCheck, Edit3, Loader2, Plus, Settings2 } from "lucide-react"; + import { PersonalInfoSection } from "../../components/sections/personalInfo"; import { TechnicalSkillsSection } from "../../components/sections/technicalSkills"; import { EducationSection } from "../../components/sections/education"; import { ExperienceSection } from "../../components/sections/experience"; import { ProjectsSection } from "../../components/sections/projects"; -import { Loader2, Plus, Settings2, Edit } from "lucide-react"; import { Button } from "../../components/ui/button"; -import { Fade } from "react-awesome-reveal"; +import { SubmitReviewModal } from "../../components/elements/resume/SubmitReviewModal"; import { useGuideStore } from "../../store/useGuideStore"; import { useResumeStore, type SectionId } from "../../store/useResumeStore"; import { getResumeWithData } from "../../lib/resumeService"; import { useAutoSave } from "../../lib/hooks/useAutoSave"; import { SectionManagerModal } from "../../components/elements/resume/SectionManagerModal"; -/** - * Section configuration map. - * Maps section IDs to their components and display labels. - */ const SECTION_CONFIG: Record = { "personal-info": { Component: PersonalInfoSection, label: "Personal Info" }, education: { Component: EducationSection, label: "Education" }, @@ -39,24 +38,21 @@ function BuilderPageContent() { sectionOrder, } = useResumeStore(); - // Loading and error states const [isLoading, setIsLoading] = useState(true); const [loadError, setLoadError] = useState(null); - - // Track current section index for single-section display const [currentSectionIndex, setCurrentSectionIndex] = useState(0); const [isTransitioning, setIsTransitioning] = useState(false); - - // Section manager modal state const [isSectionManagerOpen, setIsSectionManagerOpen] = useState(false); + const [resumeName, setResumeName] = useState("Resume"); + const [showSubmitReviewModal, setShowSubmitReviewModal] = useState(false); + const [submitSuccessMessage, setSubmitSuccessMessage] = useState< + string | null + >(null); - // Enable auto-save when resume is loaded useAutoSave(!!currentResumeId); - // Load resume from database on mount useEffect(() => { async function loadResume() { - // If no resume ID, redirect to templates if (!resumeId) { router.replace("/templates"); return; @@ -70,15 +66,13 @@ function BuilderPageContent() { if (!resumeWithData) { setLoadError("Resume not found"); - // Redirect to dashboard after a short delay setTimeout(() => router.replace("/dashboard"), 2000); return; } - // Set the current resume ID setCurrentResumeId(resumeId); + setResumeName(resumeWithData.name || "Resume"); - // Hydrate the store with database data if (resumeWithData.resume_data) { setResumeFromDatabase({ personal_info: resumeWithData.resume_data.personal_info, @@ -101,18 +95,11 @@ function BuilderPageContent() { } loadResume(); - - // Cleanup: reset store when leaving builder - return () => { - // Only reset if we're actually navigating away (not just re-rendering) - // This is handled by the component unmount - }; }, [resumeId, router, setCurrentResumeId, setResumeFromDatabase]); - // Build dynamic sections array from sectionOrder const sections = useMemo(() => { return sectionOrder - .filter((id) => SECTION_CONFIG[id]) // Only include sections that have a config + .filter((id) => SECTION_CONFIG[id]) .map((id) => ({ Component: SECTION_CONFIG[id].Component, id: id as SectionId, @@ -120,14 +107,12 @@ function BuilderPageContent() { })); }, [sectionOrder]); - // Clamp currentSectionIndex if sections are removed useEffect(() => { if (currentSectionIndex >= sections.length && sections.length > 0) { setCurrentSectionIndex(sections.length - 1); } }, [sections.length, currentSectionIndex]); - // Update guide store when section changes (for contextual help) useEffect(() => { const sectionId = sections[currentSectionIndex]?.id; if (sectionId) { @@ -145,35 +130,38 @@ function BuilderPageContent() { setIsTransitioning(true); setTimeout(() => { setCurrentSectionIndex(targetIndex); - setTimeout(() => { - setIsTransitioning(false); - }, 50); - }, 250); + setTimeout(() => setIsTransitioning(false), 60); + }, 180); } }; const activeSection = sections[currentSectionIndex]?.id || "personal-info"; + const CurrentSection = + sections[currentSectionIndex]?.Component || PersonalInfoSection; + const builderFileName = `${resumeName || "Resume"}.pdf`; - // Loading state if (isLoading) { return ( -
-
- -

Loading your resume...

+
+
+
+ +
+

+ Loading builder +

); } - // Error state if (loadError) { return ( -
-
-
-

{loadError}

-

+

+
+
+

{loadError}

+

Redirecting to dashboard...

@@ -182,25 +170,26 @@ function BuilderPageContent() { ); } - // Handle empty sections state if (sections.length === 0) { return ( -
-
-
- -

- No Sections Added +
+
+
+
+ +
+

+ Start by adding sections

-

- Your resume doesn't have any sections yet. Add sections to - start building your resume. +

+ Your resume is empty right now. Add the sections you want, then + start filling them in.

@@ -213,64 +202,116 @@ function BuilderPageContent() { ); } - const CurrentSection = - sections[currentSectionIndex]?.Component || PersonalInfoSection; - return ( -
-
- {/* Navigation controls */} - - -
- {/* background glow effects */} -
-
-
+
+
+
+
+

+ {sections[currentSectionIndex]?.label} +

+
+

+ Edit this section and watch the final document update beside + the form. +

+
+ +
+ + +
- {/* Section content with fade transition */} -
- + {submitSuccessMessage && ( +
+ {submitSuccessMessage} +
+ )} + +
+ {sections.map((section, index) => ( + + ))}
-
-
+
+ + + + + + +
- {/* Section Manager Modal */} + {showSubmitReviewModal && ( + setShowSubmitReviewModal(false)} + onSubmitted={() => { + setShowSubmitReviewModal(false); + setSubmitSuccessMessage( + "Review request submitted from your current builder resume.", + ); + }} + /> + )}
); } diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index f6a1f31..6b42eec 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,8 +1,9 @@ "use client"; -import { useState, useEffect, useMemo } from "react"; +import { useState, useEffect, useMemo, useCallback } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; +import { SubmitReviewModal } from '@/components/elements/resume/SubmitReviewModal' import { Loader2, Plus, @@ -23,6 +24,7 @@ import { deleteResume, type ResumeMetadata, } from "@/lib/resumeService"; +import { getStudentReviewRequests, type StudentReviewRequest } from "@/lib/review/getStudentReviewRequests"; // Sort configuration type type SortColumn = "name" | "template_type" | "updated_at"; @@ -88,6 +90,44 @@ const TableRowSkeleton: React.FC = () => ( ); +/** + * Skeleton loader for review table rows + */ +const ReviewTableRowSkeleton: React.FC = () => ( + + + + + + + + + + + + + + +); + +const statusLabels: Record = { + pending: { + label: "Pending Review", + color: + "border border-[#f59e0b]/30 bg-[#f59e0b]/12 text-[#f5c76b]", + }, + accepted: { + label: "In Review", + color: + "border border-[#274cbc]/40 bg-[#274cbc]/15 text-[#8fa5ff]", + }, + completed: { + label: "Completed", + color: + "border border-[#58f5c3]/30 bg-[#58f5c3]/12 text-[#58f5c3]", + }, +}; + /** * Sortable column header component */ @@ -117,18 +157,16 @@ const SortableHeader: React.FC = ({ {label}

@@ -152,6 +190,14 @@ export default function DashboardPage() { direction: "desc", }); + const [showSubmitReviewModal, setShowSubmitReviewModal] = useState(false) + + // Review list state + const [reviews, setReviews] = useState([]); + const [isLoadingReviews, setIsLoadingReviews] = useState(true); + const [loadReviewsError, setLoadReviewsError] = useState(null); + + // Sync session store with Supabase useSessionSync(); @@ -179,6 +225,28 @@ export default function DashboardPage() { fetchResumes(); }, [user?.id]); + // Fetch user's review requests on mount and refetch helper + const fetchReviews = useCallback(async () => { + if (!user?.id) return; + setIsLoadingReviews(true); + setLoadReviewsError(null); + try { + const userReviews = await getStudentReviewRequests(user.id); + setReviews(userReviews); + } catch (error) { + console.error("Failed to fetch reviews:", error); + setLoadReviewsError( + error instanceof Error ? error.message : "Failed to load reviews" + ); + } finally { + setIsLoadingReviews(false); + } + }, [user?.id]); + + useEffect(() => { + fetchReviews(); + }, [fetchReviews]); + // Sort resumes based on current sort config const sortedResumes = useMemo(() => { const sorted = [...resumes].sort((a, b) => { @@ -425,27 +493,159 @@ export default function DashboardPage() { {/* Right Column - Resume Reviews (Under Development) */}
-
-

Resume Reviews

- - Under Development - -
- - {/* Under Development Card */} -
-
-
- -
-

- Resume Reviews Coming Soon -

-

- Submit your resumes for expert feedback and track review status all in one place. +

+
+

Resume Reviews

+

+ Submit a PDF for feedback and track review progress here.

+ + {showSubmitReviewModal && ( + setShowSubmitReviewModal(false)} + onSubmitted={() => { + setShowSubmitReviewModal(false); + fetchReviews(); + }} + /> + )} +
+ + {/* Resume Review Table */} +
+ + + + + + + + + + + {/* Loading State */} + {isLoadingReviews && ( + <> + + + + + )} + + {/* Error State */} + {loadReviewsError && !isLoadingReviews && ( + + + + )} + + {/* Empty State */} + {!isLoadingReviews && !loadReviewsError && reviews.length === 0 && ( + + + + )} + + {/* Review Rows */} + {!isLoadingReviews && + !loadReviewsError && + reviews.map((request) => { + // resume_versions is returned as object (many-to-one FK), not array + const version = Array.isArray(request.resume_versions) + ? request.resume_versions[0] + : request.resume_versions; + const displayName = version?.label ?? version?.file_name ?? "Untitled"; + const statusConfig = statusLabels[request.status] ?? statusLabels.pending; + return ( + router.push(`/review/${request.id}`) + : undefined + } + > + + + + + + ); + })} + +
+ Name + + Status + + Submitted + + Actions +
+

{loadReviewsError}

+ +
+ +

No reviews yet

+

+ Submit a resume when you want reviewer feedback. +

+
+
+
+ +
+ + {displayName} + +
+
+ + {statusConfig.label} + + + + {formatDate(request.created_at)} + + + {request.status === "completed" ? ( + e.stopPropagation()} + > + View Feedback + + ) : ( + + )} +
+ + {/* Review count */} + {!isLoadingReviews && !loadReviewsError && reviews.length > 0 && ( +

+ {reviews.length} review{reviews.length !== 1 ? "s" : ""} +

+ )}
diff --git a/app/layout.tsx b/app/layout.tsx index 886c815..f9c61f8 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,11 @@ import type { Metadata } from "next"; import "./globals.css"; import { Montserrat } from "next/font/google"; +import 'react-pdf-highlighter-extended/dist/esm/style/pdf_viewer.css' +import 'react-pdf-highlighter-extended/dist/esm/style/PdfHighlighter.css' +import 'react-pdf-highlighter-extended/dist/esm/style/TextHighlight.css' +import 'react-pdf-highlighter-extended/dist/esm/style/AreaHighlight.css' +import 'react-pdf-highlighter-extended/dist/esm/style/MouseSelection.css' import { DeferredAnalytics } from "@/components/DeferredAnalytics"; diff --git a/app/page.tsx b/app/page.tsx index 337baa4..c8ab3a6 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,15 +1,611 @@ -import { HomeHeaderBarClient } from "@/components/elements/home/HomeHeaderBarClient"; -import { HomePageClientWrapper } from "@/components/sections/HomePageClientWrapper"; -import { HeroSection } from "@/components/sections/HeroSection"; +"use client"; + +import Link from "next/link"; +import type { LucideIcon } from "lucide-react"; +import { + ArrowRight, + CheckCircle2, + FileText, + Layers3, + MessageSquareText, + ScanSearch, + Sparkles, + Wand2, +} from "lucide-react"; +import { motion, useReducedMotion } from "framer-motion"; + +import { Button } from "@/components/ui/button"; + +const capabilityRail = [ + "Guided resume builder", + "PDF review annotations", + "One shared workflow", +] as const; + +const dualSystem = [ + { + icon: Wand2, + label: "Builder", + title: "Build the resume", + items: [ + "Guided prompts help students turn coursework and projects into resume sections.", + "Bullet refinement helps make weak lines clearer and stronger.", + "The finished PDF is ready to send straight into review.", + ], + }, + { + icon: MessageSquareText, + label: "Review", + title: "Review the PDF", + items: [ + "Reviewers pick up pending and active work from one place.", + "Comments attach to exact lines and highlighted PDF regions.", + "A review summary gives the student clear next steps.", + ], + }, +] as const; + +const workflowSteps = [ + { + number: "01", + icon: Sparkles, + title: "Build", + description: "Create the resume with prompts, structure, and stronger bullet writing.", + }, + { + number: "02", + icon: FileText, + title: "Submit", + description: "Send the finished PDF with notes so the reviewer has context.", + }, + { + number: "03", + icon: ScanSearch, + title: "Review", + description: "Get comments directly on the PDF plus a final written summary.", + }, +] as const; export default function Home() { return ( -
-
- - - - +
+ + +
+
+
+
+

+ MAVRESUME +

+

+ Built with ACM @ UTA +

+
+ + +
+ +
+
+
+ Builder + review system +
+ +

+ Draft with guidance. +
+ Review in context. +

+ +

+ Build the resume in one guided flow, then get feedback on that + same PDF from the review system. +

+ +
+ +

+ Built for UTA Mavericks, designed like a modern product. +

+
+ +
+ {capabilityRail.map((item) => ( +
+ + {item} +
+ ))} +
+
+ + +
+ + +
+ + + +
+
+
+
+ +
+
+ + +
+ + +
+
+ + +
+
+
+ +
+
+ + +
+
+ {workflowSteps.map((step, index) => ( + + ))} +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+

+ Ready to start +

+

+ Login to start building or reviewing. +

+
+ + +
+ + +
+

MavResume is product-first, with ACM @ UTA still in the DNA.

+

Built for UTA Mavericks.

+
+
+
+
+ ); +} + +function BackgroundAtmosphere() { + return ( + <> +
+
+
+
+
+ + ); +} + +function HeroSystemVisual() { + const prefersReducedMotion = useReducedMotion(); + + return ( +
+