From dd39369e69550bb560cdd92f1d1eb72e16aa5b50 Mon Sep 17 00:00:00 2001 From: Tobi Akere Date: Thu, 29 Jan 2026 16:10:20 -0600 Subject: [PATCH 1/3] Refactored Mobile View and Implemented Section Manager Modal --- app/builder/layout.tsx | 4 +- app/builder/page.tsx | 285 ++++++--------- app/dashboard/page.tsx | 335 ++++++++++-------- app/globals.css | 10 + .../elements/builder/BuilderHeaderBar.tsx | 34 +- components/elements/builder/MobileSidebar.tsx | 220 ++++++++++++ .../elements/resume/SectionManagerModal.tsx | 266 ++++++++++++++ .../elements/resume/SectionOrderModal.tsx | 159 --------- .../elements/resume/SectionReorderButton.tsx | 14 +- .../elements/templates/CreateResumeModal.tsx | 8 +- lib/resumeService.ts | 60 +++- store/useGuideStore.ts | 9 +- store/useResumeStore.tsx | 46 +++ 13 files changed, 932 insertions(+), 518 deletions(-) create mode 100644 components/elements/builder/MobileSidebar.tsx create mode 100644 components/elements/resume/SectionManagerModal.tsx delete mode 100644 components/elements/resume/SectionOrderModal.tsx diff --git a/app/builder/layout.tsx b/app/builder/layout.tsx index 7b6e795..86944d4 100644 --- a/app/builder/layout.tsx +++ b/app/builder/layout.tsx @@ -37,7 +37,7 @@ export default function BuildLayout({ {/* Main content (form sections) */} -
+
{children}
{/* Resume Preview */} @@ -52,7 +52,7 @@ export default function BuildLayout({
{/* Resume Preview Controls */} -
+
diff --git a/app/builder/page.tsx b/app/builder/page.tsx index 513c242..a1cc202 100644 --- a/app/builder/page.tsx +++ b/app/builder/page.tsx @@ -1,18 +1,31 @@ "use client"; -import { useState, useEffect, Suspense } from "react"; +import { useState, useEffect, Suspense, useMemo } from "react"; import { useSearchParams, useRouter } from "next/navigation"; 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 { ChevronLeft, ChevronRight, Loader2 } from "lucide-react"; +import { Loader2, Settings2 } from "lucide-react"; import { Button } from "../../components/ui/button"; import { Fade } from "react-awesome-reveal"; -import { useGuideStore, SectionId } from "../../store/useGuideStore"; -import { useResumeStore } from "../../store/useResumeStore"; +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" }, + "technical-skills": { Component: TechnicalSkillsSection, label: "Skills" }, + projects: { Component: ProjectsSection, label: "Projects" }, + experience: { Component: ExperienceSection, label: "Experience" }, +}; function BuilderPageContent() { const searchParams = useSearchParams(); @@ -23,7 +36,7 @@ function BuilderPageContent() { currentResumeId, setCurrentResumeId, setResumeFromDatabase, - resetResume, + sectionOrder, } = useResumeStore(); // Loading and error states @@ -34,6 +47,9 @@ function BuilderPageContent() { const [currentSectionIndex, setCurrentSectionIndex] = useState(0); const [isTransitioning, setIsTransitioning] = useState(false); + // Section manager modal state + const [isSectionManagerOpen, setIsSectionManagerOpen] = useState(false); + // Enable auto-save when resume is loaded useAutoSave(!!currentResumeId); @@ -78,7 +94,7 @@ function BuilderPageContent() { } catch (error) { console.error("Failed to load resume:", error); setLoadError( - error instanceof Error ? error.message : "Failed to load resume" + error instanceof Error ? error.message : "Failed to load resume", ); setIsLoading(false); } @@ -93,14 +109,23 @@ function BuilderPageContent() { }; }, [resumeId, router, setCurrentResumeId, setResumeFromDatabase]); - // Section configuration: defines order and IDs for navigation/header tracking - const sections = [ - { Component: PersonalInfoSection, id: "personal-info" as SectionId }, - { Component: EducationSection, id: "education" as SectionId }, - { Component: TechnicalSkillsSection, id: "technical-skills" as SectionId }, - { Component: ProjectsSection, id: "projects" as SectionId }, - { Component: ExperienceSection, id: "experience" as SectionId }, - ]; + // Build dynamic sections array from sectionOrder + const sections = useMemo(() => { + return sectionOrder + .filter((id) => SECTION_CONFIG[id]) // Only include sections that have a config + .map((id) => ({ + Component: SECTION_CONFIG[id].Component, + id: id as SectionId, + label: SECTION_CONFIG[id].label, + })); + }, [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(() => { @@ -108,31 +133,8 @@ function BuilderPageContent() { if (sectionId) { setCurrentSection(sectionId); } - }, [currentSectionIndex, setCurrentSection]); + }, [currentSectionIndex, sections, setCurrentSection]); - const goToPrevious = () => { - if (currentSectionIndex > 0 && !isTransitioning) { - setIsTransitioning(true); - setTimeout(() => { - setCurrentSectionIndex((prev) => prev - 1); - // Fade in new section after a brief delay to allow React to render - setTimeout(() => { - setIsTransitioning(false); - }, 50); - }, 250); - } - }; - const goToNext = () => { - if (currentSectionIndex < sections.length - 1 && !isTransitioning) { - setIsTransitioning(true); - setTimeout(() => { - setCurrentSectionIndex((prev) => prev + 1); - setTimeout(() => { - setIsTransitioning(false); - }, 50); - }, 250); - } - }; const goToSection = (targetIndex: number) => { if ( targetIndex >= 0 && @@ -150,15 +152,7 @@ function BuilderPageContent() { } }; - const sectionIds = [ - "personal-info", - "education", - "technical-skills", - "projects", - "experience", - ]; - - const activeSection = sectionIds[currentSectionIndex] || "personal-info"; + const activeSection = sections[currentSectionIndex]?.id || "personal-info"; // Loading state if (isLoading) { @@ -188,133 +182,84 @@ function BuilderPageContent() { ); } - const CurrentSection = sections[currentSectionIndex].Component; + // Handle empty sections state + if (sections.length === 0) { + return ( +
+
+
+ +

+ No Sections Added +

+

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

+ +
+
+ +
+ ); + } + + const CurrentSection = + sections[currentSectionIndex]?.Component || PersonalInfoSection; return ( -
-
+
+
{/* Navigation controls */} -
-

Navigation

-
- {/* Left navigation arrow */} - {currentSectionIndex > 0 && ( - - )} - - {/* Navigation steps */} -
+ + {/* Section Manager Modal */} +
); } diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 1a27f75..f6a1f31 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -11,6 +11,7 @@ import { ChevronDown, FileText, ExternalLink, + ClipboardCheck, } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -251,10 +252,10 @@ export default function DashboardPage() { const displayName = user?.user_metadata?.full_name || user?.email?.split("@")[0] || "there"; return ( -
+
{/* Welcome Section */}
-

+

Welcome back, {displayName}

@@ -262,161 +263,191 @@ export default function DashboardPage() {

- {/* Quick Actions */} -
-

Your Resumes

- - - -
- - {/* Resume Table */} -
- - - - - - - - - - - {/* Loading State */} - {isLoadingResumes && ( - <> - - - - - )} - - {/* Error State */} - {loadError && !isLoadingResumes && ( - - - - )} - - {/* Empty State */} - {!isLoadingResumes && !loadError && resumes.length === 0 && ( - - - - )} - - {/* Resume Rows */} - {!isLoadingResumes && - !loadError && - sortedResumes.map((resume) => ( - handleRowClick(resume.id)} - className="border-b border-[#2d313a]/50 hover:bg-[#1a1c22]/50 cursor-pointer transition-colors group" - > - - - - + + )} + + {/* Empty State */} + {!isLoadingResumes && !loadError && resumes.length === 0 && ( + + + + )} + + {/* Resume Rows */} + {!isLoadingResumes && + !loadError && + sortedResumes.map((resume) => ( + handleRowClick(resume.id)} + className="border-b border-[#2d313a]/50 hover:bg-[#1a1c22]/50 cursor-pointer transition-colors group" + > + + + + + + ))} + +
- Actions -
-

{loadError}

- -
- -

No resumes yet

- - - -
-
-
- -
- - {resume.name} - -
-
- - {formatTemplateType(resume.template_type)} - - - - {formatDate(resume.updated_at)} - - -
- + {/* Main Content */} +
+
+ {/* Quick Actions */} +
+

Your Resumes

+ + + +
+ + {/* Resume Table */} +
+ + + + + + + + + + + {/* Loading State */} + {isLoadingResumes && ( + <> + + + + + )} + + {/* Error State */} + {loadError && !isLoadingResumes && ( + + - - ))} - -
+ Actions +
+

{loadError}

- -
+
+ +

No resumes yet

+ + + +
+
+
+ +
+ + {resume.name} + +
+
+ + {formatTemplateType(resume.template_type)} + + + + {formatDate(resume.updated_at)} + + +
+ + +
+
+
+ + {/* Resume count */} + {!isLoadingResumes && !loadError && resumes.length > 0 && ( +

+ {resumes.length} resume{resumes.length !== 1 ? "s" : ""} +

+ )} +
+ + {/* 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 count */} - {!isLoadingResumes && !loadError && resumes.length > 0 && ( -

- {resumes.length} resume{resumes.length !== 1 ? "s" : ""} -

- )}
); } diff --git a/app/globals.css b/app/globals.css index cc6027e..08fbff6 100644 --- a/app/globals.css +++ b/app/globals.css @@ -146,3 +146,13 @@ a { @apply bg-background text-foreground; } } + +/* Utility class to hide scrollbar while keeping scroll functionality */ +.scrollbar-hide { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + +.scrollbar-hide::-webkit-scrollbar { + display: none; /* Chrome, Safari, Opera */ +} diff --git a/components/elements/builder/BuilderHeaderBar.tsx b/components/elements/builder/BuilderHeaderBar.tsx index b2a66fe..e65d9b9 100644 --- a/components/elements/builder/BuilderHeaderBar.tsx +++ b/components/elements/builder/BuilderHeaderBar.tsx @@ -2,6 +2,7 @@ import React from "react"; import { Loader2, Check, AlertCircle, Cloud } from "lucide-react"; import { MobileResumePreviewDrawer } from "../resume/MobileResumePreviewDrawer"; +import { MobileSidebar } from "./MobileSidebar"; import { RateLimitStatus } from "../refinement/RateLimitStatus"; import { useResumeStore } from "@/store/useResumeStore"; @@ -44,10 +45,10 @@ const SaveStatusIndicator: React.FC = () => { return (
{config.icon} - {config.text} + {config.text}
); }; @@ -55,25 +56,36 @@ const SaveStatusIndicator: React.FC = () => { export const BuilderHeaderBar = () => { return (
-
-
+
+ {/* Left section: Mobile menu + Logo */} +
+ {/* Mobile hamburger menu */} +
+ +
+ + {/* Logo */}
RESUMEBUILDER
- +
+ + {/* Center/Right section: Status indicators + Mobile preview */} +
{/* Save Status Indicator */} -
+
-
- {/* Mobile Preview Button */} -
- + + {/* Mobile Preview Button */} +
+ +
diff --git a/components/elements/builder/MobileSidebar.tsx b/components/elements/builder/MobileSidebar.tsx new file mode 100644 index 0000000..1346181 --- /dev/null +++ b/components/elements/builder/MobileSidebar.tsx @@ -0,0 +1,220 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { FaGithub, FaDiscord, FaGraduationCap } from "react-icons/fa"; +import { FileText, Menu, X } from "lucide-react"; +import { IoMdHelpCircle } from "react-icons/io"; +import { IoLogIn } from "react-icons/io5"; + +import { signOut } from "@/lib/auth"; +import { useSessionStore } from "@/store/useSessionStore"; +import { useSessionSync } from "@/lib/hooks/useSessionSync"; +import { Button } from "@/components/ui/button"; +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerTrigger, +} from "@/components/ui/drawer"; + +interface SidebarLink { + href: string; + label: string; + icon: React.ComponentType<{ className?: string }>; + ariaLabel: string; + external?: boolean; +} + +const sidebarLinks: SidebarLink[] = [ + { + href: "/templates", + label: "Templates", + icon: FileText, + ariaLabel: "Choose resume template", + external: false, + }, + { + href: "https://github.com/acmuta/mavresume", + label: "GitHub", + icon: FaGithub, + ariaLabel: "GitHub repository", + external: true, + }, + { + href: "https://www.acmuta.com", + label: "ACM @ UTA", + icon: FaGraduationCap, + ariaLabel: "ACM at UTA website", + external: true, + }, + { + href: "https://discord.gg/WjrDwNn5es", + label: "ACM Discord", + icon: FaDiscord, + ariaLabel: "Discord server", + external: true, + }, + { + href: "https://github.com/acmuta/mavresume#readme", + label: "Help", + icon: IoMdHelpCircle, + ariaLabel: "Documentation", + external: true, + }, +]; + +function getInitials(user: { + user_metadata?: { full_name?: string }; + email?: string; +}): string { + const name = user.user_metadata?.full_name?.trim(); + if (name) { + const parts = name.split(/\s+/); + if (parts.length >= 2) + return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); + return (parts[0].slice(0, 2) || "?").toUpperCase(); + } + const local = user.email?.split("@")[0] ?? ""; + return (local.slice(0, 2) || "?").toUpperCase(); +} + +/** + * Mobile sidebar component with slide-in drawer from the left. + * Triggered by a hamburger menu icon in the header. + */ +export const MobileSidebar = () => { + const router = useRouter(); + const { user } = useSessionStore(); + const [isSigningOut, setIsSigningOut] = useState(false); + const [isRedirecting, setIsRedirecting] = useState(false); + const [isOpen, setIsOpen] = useState(false); + + useSessionSync(); + + return ( + + + + + + {/* Header with logo and close button */} + + setIsOpen(false)} + > + MAVRESUME + + + + + + + {/* Navigation Links */} +
+ {sidebarLinks.map((link) => { + const IconComponent = link.icon; + return ( + !link.external && setIsOpen(false)} + className="flex items-center gap-3 px-4 py-3 + rounded-xl border border-[#2b3242] bg-[#10121a] + text-[#cfd3e1] hover:border-[#3f4a67] hover:text-white + transition-all duration-200" + aria-label={link.ariaLabel} + > + + {link.label} + + ); + })} +
+ + {/* Profile Section */} +
+ {user ? ( +
+
+ + {getInitials(user)} + +
+

+ {user.user_metadata?.full_name || + user.email?.split("@")[0] || + "User"} +

+

{user.email}

+
+
+ +
+ ) : ( +
+
+ +

+ Sign in to save your progress. +

+
+ setIsOpen(false)} + className="w-full rounded-xl border border-dashed border-[#2f323a] + bg-transparent py-2.5 text-sm font-medium text-white text-center + hover:border-[#4b4f5c] hover:bg-[#161920] transition-colors" + > + Sign in + +
+ )} +
+
+
+ ); +}; diff --git a/components/elements/resume/SectionManagerModal.tsx b/components/elements/resume/SectionManagerModal.tsx new file mode 100644 index 0000000..0c28075 --- /dev/null +++ b/components/elements/resume/SectionManagerModal.tsx @@ -0,0 +1,266 @@ +"use client"; +import React, { useState, useEffect } from "react"; +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, +} from "@dnd-kit/core"; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + verticalListSortingStrategy, + useSortable, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { GripVertical, Plus, X } from "lucide-react"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../../ui/dialog"; +import { Button } from "../../ui/button"; +import { useResumeStore, AVAILABLE_SECTIONS } from "../../../store/useResumeStore"; + +/** + * Section display names mapping + */ +const sectionDisplayNames: Record = { + "personal-info": "Personal Info", + "education": "Education", + "technical-skills": "Technical Skills", + "projects": "Projects", + "experience": "Experience", +}; + +interface SortableSectionItemProps { + id: string; + label: string; + onRemove: (id: string) => void; +} + +/** + * Individual sortable section item component with remove button + */ +const SortableSectionItem: React.FC = ({ + id, + label, + onRemove, +}) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + + return ( +
+
+ +
+ {label} + +
+ ); +}; + +/** + * Available section item component with add button + */ +const AvailableSectionItem: React.FC<{ + id: string; + label: string; + onAdd: (id: string) => void; +}> = ({ id, label, onAdd }) => { + return ( +
+ {label} + +
+ ); +}; + +/** + * Modal component for managing resume sections. + * Features: + * - Add sections from available list + * - Remove sections from selected list + * - Reorder selected sections via drag-and-drop + * - Personal Info is always present and cannot be removed + */ +export const SectionManagerModal: React.FC<{ + open: boolean; + onOpenChange: (open: boolean) => void; +}> = ({ open, onOpenChange }) => { + const { sectionOrder, addSection, removeSection, updateSectionOrder } = useResumeStore(); + + // Filter out personal-info for the reorderable list + const selectedSections = sectionOrder.filter((id) => id !== "personal-info"); + + // Track local state for drag-and-drop + const [items, setItems] = useState(selectedSections); + + // Sync local state when store changes + useEffect(() => { + const selected = sectionOrder.filter((id) => id !== "personal-info"); + setItems(selected); + }, [sectionOrder]); + + // Get available sections that are not yet added + const availableSections = AVAILABLE_SECTIONS.filter( + (section) => !sectionOrder.includes(section.id) + ); + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + + if (active.id !== over?.id) { + const oldIndex = items.indexOf(active.id as string); + const newIndex = items.indexOf(over!.id as string); + const newItems = arrayMove(items, oldIndex, newIndex); + + setItems(newItems); + + // Update store with new order (personal-info always first) + const newSectionOrder = ["personal-info", ...newItems]; + updateSectionOrder(newSectionOrder); + } + }; + + const handleAddSection = (sectionId: string) => { + addSection(sectionId); + }; + + const handleRemoveSection = (sectionId: string) => { + removeSection(sectionId); + }; + + return ( + + + + + Manage Resume Sections + + + +
+

+ Add, remove, and reorder sections. Personal Info always appears first and cannot be removed. +

+ +
+ {/* Available Sections Column */} +
+

+ Available Sections +

+
+ {availableSections.length > 0 ? ( + availableSections.map((section) => ( + + )) + ) : ( +

+ All sections added +

+ )} +
+
+ + {/* Selected Sections Column */} +
+

+ Your Sections +

+ + {/* Fixed Personal Info section */} +
+
{/* Spacer for alignment */} + Personal Info + Fixed +
+ + {/* Sortable sections */} + + +
+ {items.length > 0 ? ( + items.map((id) => ( + + )) + ) : ( +

+ Add sections from the left +

+ )} +
+
+
+
+
+
+ +
+ ); +}; + +// Re-export with old name for backward compatibility during migration +export { SectionManagerModal as SectionOrderModal }; diff --git a/components/elements/resume/SectionOrderModal.tsx b/components/elements/resume/SectionOrderModal.tsx deleted file mode 100644 index 7bba259..0000000 --- a/components/elements/resume/SectionOrderModal.tsx +++ /dev/null @@ -1,159 +0,0 @@ -"use client"; -import React, { useState } from "react"; -import { - DndContext, - closestCenter, - KeyboardSensor, - PointerSensor, - useSensor, - useSensors, - DragEndEvent, -} from "@dnd-kit/core"; -import { - arrayMove, - SortableContext, - sortableKeyboardCoordinates, - verticalListSortingStrategy, - useSortable, -} from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; -import { GripVertical } from "lucide-react"; -import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../../ui/dialog"; -import { useResumeStore } from "../../../store/useResumeStore"; - -/** - * Section display names mapping - */ -const sectionDisplayNames: Record = { - "education": "Education", - "technical-skills": "Technical Skills", - "projects": "Projects", - "experience": "Experience", -}; - -interface SortableSectionItemProps { - id: string; - label: string; -} - -/** - * Individual sortable section item component - */ -const SortableSectionItem: React.FC = ({ - id, - label, -}) => { - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging, - } = useSortable({ id }); - - const style = { - transform: CSS.Transform.toString(transform), - transition, - opacity: isDragging ? 0.5 : 1, - }; - - return ( -
- - {label} -
- ); -}; - -/** - * Modal component for reordering resume sections using drag-and-drop. - * Excludes "personal-info" from reordering (always stays first). - * Auto-saves order to store on drag end. - */ -export const SectionOrderModal: React.FC<{ - open: boolean; - onOpenChange: (open: boolean) => void; -}> = ({ open, onOpenChange }) => { - const { sectionOrder, updateSectionOrder } = useResumeStore(); - - // Filter out personal-info and get reorderable sections - const reorderableSections = sectionOrder.filter( - (id) => id !== "personal-info" - ); - - const [items, setItems] = useState(reorderableSections); - - // Update local state when store sectionOrder changes - React.useEffect(() => { - const reorderable = sectionOrder.filter((id) => id !== "personal-info"); - setItems(reorderable); - }, [sectionOrder]); - - const sensors = useSensors( - useSensor(PointerSensor), - useSensor(KeyboardSensor, { - coordinateGetter: sortableKeyboardCoordinates, - }) - ); - - const handleDragEnd = (event: DragEndEvent) => { - const { active, over } = event; - - if (active.id !== over?.id) { - const oldIndex = items.indexOf(active.id as string); - const newIndex = items.indexOf(over!.id as string); - const newItems = arrayMove(items, oldIndex, newIndex); - - setItems(newItems); - - // Update store with new order (personal-info always first) - const newSectionOrder = ["personal-info", ...newItems]; - updateSectionOrder(newSectionOrder); - } - }; - - return ( - - - - - Reorder Sections - - -
-

- Drag and drop to reorder sections. Personal Info always appears - first. -

- - -
- {items.map((id) => ( - - ))} -
-
-
-
-
-
- ); -}; diff --git a/components/elements/resume/SectionReorderButton.tsx b/components/elements/resume/SectionReorderButton.tsx index e880190..469a3a1 100644 --- a/components/elements/resume/SectionReorderButton.tsx +++ b/components/elements/resume/SectionReorderButton.tsx @@ -1,12 +1,12 @@ "use client"; import React, { useState } from "react"; -import { ListOrdered } from "lucide-react"; +import { Settings2 } from "lucide-react"; import { Button } from "../../ui/button"; -import { SectionOrderModal } from "./SectionOrderModal"; +import { SectionManagerModal } from "./SectionManagerModal"; import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/tooltip"; /** - * Button component that opens the section reordering modal. + * Button component that opens the section management modal. * Matches styling of other header buttons (ResumeDocPreview, ResumeDocDownloadButton). */ export const SectionReorderButton = () => { @@ -20,17 +20,17 @@ export const SectionReorderButton = () => { variant="ghost" size="icon-sm" className="rounded-full px-5 text-[#6d7895] hover:text-white hover:bg-white/10 transition-all" - aria-label="Reorder sections" + aria-label="Manage sections" onClick={() => setIsOpen(true)} > - + -

Reorder sections

+

Manage sections

- + ); }; diff --git a/components/elements/templates/CreateResumeModal.tsx b/components/elements/templates/CreateResumeModal.tsx index 2772aab..34aa4f9 100644 --- a/components/elements/templates/CreateResumeModal.tsx +++ b/components/elements/templates/CreateResumeModal.tsx @@ -145,12 +145,6 @@ export const CreateResumeModal: React.FC = ({
)} - {/* Divider for future settings */} -
-

- More settings coming soon -

-
@@ -158,7 +152,7 @@ export const CreateResumeModal: React.FC = ({ variant="outline" onClick={() => handleOpenChange(false)} disabled={isCreating} - className="border-[#2d313a] bg-transparent text-white hover:bg-[#1c1d21] hover:border-[#3d4353]" + className="hover:text-white bg-[#151618] border border-[#2d313a] hover:bg-[#1c1d21]" > Cancel diff --git a/lib/resumeService.ts b/lib/resumeService.ts index 76c255f..af6e781 100644 --- a/lib/resumeService.ts +++ b/lib/resumeService.ts @@ -11,6 +11,55 @@ import type { PersonalInfo, Education, Project, Experience, Skills } from "@/sto * All operations respect Row Level Security (RLS) policies configured in Supabase. */ +/** + * Map template section display names to internal section IDs. + * Used to initialize section_order based on template type. + */ +const sectionNameToId: Record = { + "Education": "education", + "Technical Skills": "technical-skills", + "Projects": "projects", + "Experience": "experience", +}; + +/** + * Get section order based on template type. + * - Custom template: Only personal-info (user adds sections manually) + * - Predefined templates: Map template sections to IDs + * - Default/unknown: All sections in default order + */ +function getSectionOrderForTemplate(templateType?: string): string[] { + // Custom template starts with only personal-info + if (templateType === "custom" || !templateType) { + return ["personal-info"]; + } + + // Map predefined template sections + // Currently only computer-science is available + const templateSections: Record = { + "computer-science": ["Education", "Technical Skills", "Projects", "Experience"], + "data-science": ["Education", "Technical Skills", "Projects", "Experience"], + "cybersecurity": ["Education", "Technical Skills", "Projects", "Experience"], + }; + + const sections = templateSections[templateType]; + if (sections) { + const sectionIds = sections + .map((name) => sectionNameToId[name]) + .filter((id): id is string => id !== undefined); + return ["personal-info", ...sectionIds]; + } + + // Fallback: default section order for unknown templates + return [ + "personal-info", + "education", + "technical-skills", + "projects", + "experience", + ]; +} + // Types matching Supabase table schemas export interface ResumeMetadata { @@ -76,6 +125,9 @@ export async function createResume( throw new Error(`Failed to create resume: ${resumeError.message}`); } + // Get section order based on template type + const sectionOrder = getSectionOrderForTemplate(templateType); + // Create empty resume data linked to the new resume const { data: resumeData, error: dataError } = await supabase .from("resume_data") @@ -111,13 +163,7 @@ export async function createResume( customLanguages: [], customTechnologies: [], }, - section_order: [ - "personal-info", - "education", - "technical-skills", - "projects", - "experience", - ], + section_order: sectionOrder, }) .select() .single(); diff --git a/store/useGuideStore.ts b/store/useGuideStore.ts index 92b373d..1d8d925 100644 --- a/store/useGuideStore.ts +++ b/store/useGuideStore.ts @@ -1,5 +1,6 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; +import type { SectionId } from "./useResumeStore"; /** * State management for info guides and help widget. @@ -10,12 +11,8 @@ import { persist } from "zustand/middleware"; * - Persists to localStorage via zustand/persist middleware */ -export type SectionId = - | "personal-info" - | "education" - | "technical-skills" - | "projects" - | "experience"; +// Re-export SectionId for backward compatibility +export type { SectionId }; export interface GuideState { // Help widget state diff --git a/store/useResumeStore.tsx b/store/useResumeStore.tsx index e012268..87ea9be 100644 --- a/store/useResumeStore.tsx +++ b/store/useResumeStore.tsx @@ -59,6 +59,17 @@ export interface Experience { // Type for save status indicator export type SaveStatus = "idle" | "saving" | "saved" | "error"; +// Available sections that can be added to a resume (excluding personal-info which is always present) +export const AVAILABLE_SECTIONS = [ + { id: "education", label: "Education" }, + { id: "technical-skills", label: "Technical Skills" }, + { id: "projects", label: "Projects" }, + { id: "experience", label: "Experience" }, +] as const; + +// Section IDs type +export type SectionId = "personal-info" | "education" | "technical-skills" | "projects" | "experience"; + // Type for database resume data (used by setResumeFromDatabase) export interface DatabaseResumeData { personal_info?: PersonalInfo; @@ -117,6 +128,8 @@ export interface ResumeState { removeProject: (index: number) => void; removeExperience: (index: number) => void; updateSectionOrder: (order: string[]) => void; + addSection: (sectionId: string) => void; + removeSection: (sectionId: string) => void; } // Default initial state for a new/reset resume @@ -350,4 +363,37 @@ export const useResumeStore = create()((set) => ({ : validOrder; return { sectionOrder: personalInfoFirst }; }), + + /** + * Add a section to the resume. + * Section is added at the end of the list (after personal-info). + */ + addSection: (sectionId) => + set((state) => { + // Don't add if section already exists or is invalid + const validIds = ["education", "technical-skills", "projects", "experience"]; + if (!validIds.includes(sectionId) || state.sectionOrder.includes(sectionId)) { + return state; + } + // Ensure personal-info stays first + const newOrder = state.sectionOrder.includes("personal-info") + ? [...state.sectionOrder, sectionId] + : ["personal-info", ...state.sectionOrder, sectionId]; + return { sectionOrder: newOrder }; + }), + + /** + * Remove a section from the resume. + * Cannot remove personal-info. + */ + removeSection: (sectionId) => + set((state) => { + // Don't allow removing personal-info + if (sectionId === "personal-info") { + return state; + } + return { + sectionOrder: state.sectionOrder.filter((id) => id !== sectionId), + }; + }), })); From d64f47f205aea7f862a293fb3f6edde355bd61bc Mon Sep 17 00:00:00 2001 From: Tobi Akere Date: Tue, 3 Feb 2026 17:18:52 -0600 Subject: [PATCH 2/3] Edited Navigation Layout --- app/builder/layout.tsx | 18 ++--- app/builder/page.tsx | 70 +++++++------------ .../elements/accordion/EducationAccordion.tsx | 2 +- .../accordion/ExperienceAccordion.tsx | 2 +- .../elements/accordion/ProjectAccordion.tsx | 2 +- .../accordion/TechnicalSkillsAccordion.tsx | 2 +- components/elements/resume/ResumePreview.tsx | 2 +- 7 files changed, 40 insertions(+), 58 deletions(-) diff --git a/app/builder/layout.tsx b/app/builder/layout.tsx index 86944d4..51c8615 100644 --- a/app/builder/layout.tsx +++ b/app/builder/layout.tsx @@ -21,15 +21,13 @@ import { BuilderClientWrapper } from "../../components/elements/builder/BuilderC export const metadata = { title: "Builder", -} +}; export default function BuildLayout({ children, }: { children: React.ReactNode; }) { - - return (
{/* Builder sidebar (fixed overlay on left side) */} @@ -37,12 +35,12 @@ export default function BuildLayout({ {/* Main content (form sections) */} -
+
{children}
{/* Resume Preview */} -
-
+
+
- - +
+ + + +
{/* Client-side components: Help widget and Guided tour */} diff --git a/app/builder/page.tsx b/app/builder/page.tsx index a1cc202..d5ecb92 100644 --- a/app/builder/page.tsx +++ b/app/builder/page.tsx @@ -6,7 +6,7 @@ import { TechnicalSkillsSection } from "../../components/sections/technicalSkill import { EducationSection } from "../../components/sections/education"; import { ExperienceSection } from "../../components/sections/experience"; import { ProjectsSection } from "../../components/sections/projects"; -import { Loader2, Settings2 } from "lucide-react"; +import { Loader2, Plus, Settings2, Edit } from "lucide-react"; import { Button } from "../../components/ui/button"; import { Fade } from "react-awesome-reveal"; import { useGuideStore } from "../../store/useGuideStore"; @@ -218,51 +218,33 @@ function BuilderPageContent() { return (
-
+
{/* Navigation controls */} - -
- {/* Header with title and manage button */} -
-

Navigation

- {/* Manage Sections button - visible on all screen sizes */} - -
- - {/* Horizontal scrollable navigation */} -
- -
+ +
{/* background glow effects */} diff --git a/components/elements/accordion/EducationAccordion.tsx b/components/elements/accordion/EducationAccordion.tsx index 7ce7952..94f91a6 100644 --- a/components/elements/accordion/EducationAccordion.tsx +++ b/components/elements/accordion/EducationAccordion.tsx @@ -7,7 +7,7 @@ export const EducationAccordion = () => { const { education } = useResumeStore(); return ( - + {education.map((_, index) => { return ( { const { experience } = useResumeStore(); return ( - + {experience.map((_, index) => { return ( { const { projects } = useResumeStore(); return ( - + {projects.map((_, index) => { return ( diff --git a/components/elements/accordion/TechnicalSkillsAccordion.tsx b/components/elements/accordion/TechnicalSkillsAccordion.tsx index 36da1e6..58d56d2 100644 --- a/components/elements/accordion/TechnicalSkillsAccordion.tsx +++ b/components/elements/accordion/TechnicalSkillsAccordion.tsx @@ -64,7 +64,7 @@ export const TechnicalSkillsAccordion = () => { ]; return ( - + Languages{" "} diff --git a/components/elements/resume/ResumePreview.tsx b/components/elements/resume/ResumePreview.tsx index 591f94b..2dcd6c3 100644 --- a/components/elements/resume/ResumePreview.tsx +++ b/components/elements/resume/ResumePreview.tsx @@ -52,7 +52,7 @@ export const ResumePreview = () => { ); return ( -
+
Date: Tue, 3 Feb 2026 17:23:37 -0600 Subject: [PATCH 3/3] Edited BuilderHeaderBar.tsx --- components/elements/builder/BuilderHeaderBar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/elements/builder/BuilderHeaderBar.tsx b/components/elements/builder/BuilderHeaderBar.tsx index e65d9b9..0db3c71 100644 --- a/components/elements/builder/BuilderHeaderBar.tsx +++ b/components/elements/builder/BuilderHeaderBar.tsx @@ -56,7 +56,7 @@ const SaveStatusIndicator: React.FC = () => { export const BuilderHeaderBar = () => { return (
-
+
{/* Left section: Mobile menu + Logo */}
{/* Mobile hamburger menu */} @@ -77,7 +77,7 @@ export const BuilderHeaderBar = () => { {/* Center/Right section: Status indicators + Mobile preview */}
{/* Save Status Indicator */} -
+