diff --git a/src/app/globals.css b/src/app/globals.css index f9dafdd..07ec9bd 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,11 +2,11 @@ @theme inline { --color-black: #000000; - --color-white: #FFFFFF; - --color-gray-100: #F5F5F5; - --color-gray-200: #E5E5E5; - --color-gray-300: #D4D4D4; - --color-gray-400: #A3A3A3; + --color-white: #ffffff; + --color-gray-100: #f5f5f5; + --color-gray-200: #e5e5e5; + --color-gray-300: #d4d4d4; + --color-gray-400: #a3a3a3; --color-gray-500: #737373; --color-gray-600: #525252; --color-gray-700: #404040; @@ -33,12 +33,14 @@ } html { - background-color: #FFFFFF; + background-color: #ffffff; color: #000000; scroll-behavior: smooth; -webkit-text-size-adjust: 100%; text-size-adjust: 100%; - font-feature-settings: "kern" 1, "liga" 1; + font-feature-settings: + "kern" 1, + "liga" 1; text-rendering: optimizeLegibility; } @@ -46,14 +48,14 @@ body { font-family: var(--font-sans), system-ui, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - background-color: #FFFFFF; + background-color: #ffffff; color: #000000; } /* Brutalist selection */ ::selection { background-color: #000000; - color: #FFFFFF; + color: #ffffff; } /* Brutalist focus states */ @@ -69,7 +71,7 @@ body { } ::-webkit-scrollbar-track { - background: #FFFFFF; + background: #ffffff; border-left: 1px solid #000000; } @@ -78,7 +80,9 @@ body { } /* Brutalist input reset */ -input, textarea, select { +input, +textarea, +select { border: 1px solid #000000; background: transparent; color: #000000; @@ -87,13 +91,18 @@ input, textarea, select { transition: all 0.15s ease; } -input:focus, textarea:focus, select:focus { - background-color: #000000; - color: #FFFFFF; +input:focus, +textarea:focus, +select:focus { + background-color: #ffffff; + color: #000000; + border-color: #000000; + box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1); outline: none; } -input::placeholder, textarea::placeholder { +input::placeholder, +textarea::placeholder { color: #737373; text-transform: uppercase; font-size: 0.75rem; @@ -123,8 +132,9 @@ textarea::-moz-selection { color: #000000 !important; } -input:focus::placeholder, textarea:focus::placeholder { - color: #A3A3A3; +input:focus::placeholder, +textarea:focus::placeholder { + color: #a3a3a3; } input[type="text"]:focus::selection, @@ -177,15 +187,17 @@ select { background-position: calc(100% - 16px) calc(50% - 2px), calc(100% - 10px) calc(50% - 2px); - background-size: 6px 6px, 6px 6px; + background-size: + 6px 6px, + 6px 6px; background-repeat: no-repeat; padding-right: 2.25rem; } select:focus { background-image: - linear-gradient(45deg, transparent 50%, #FFFFFF 50%), - linear-gradient(135deg, #FFFFFF 50%, transparent 50%); + linear-gradient(45deg, transparent 50%, #ffffff 50%), + linear-gradient(135deg, #ffffff 50%, transparent 50%); } /* macOS/Safari: custom checkbox styling (Safari ignores border-radius without appearance reset) */ @@ -214,7 +226,7 @@ input[type="checkbox"]:checked::after { top: 0px; width: 5px; height: 9px; - border: 2px solid #FFFFFF; + border: 2px solid #ffffff; border-top: none; border-left: none; transform: rotate(45deg); @@ -250,7 +262,7 @@ button { button:hover { background-color: #000000; - color: #FFFFFF; + color: #ffffff; } /* Monospace label utility */ diff --git a/src/components/builder/builder-form.tsx b/src/components/builder/builder-form.tsx index 09cafc6..3f82b49 100644 --- a/src/components/builder/builder-form.tsx +++ b/src/components/builder/builder-form.tsx @@ -11,7 +11,7 @@ import { ProjectsForm } from "./forms/projects-form"; import { CertificationsForm } from "./forms/certifications-form"; import { CompetitionsForm } from "./forms/competitions-form"; import { LanguagesForm } from "./forms/languages-form"; -import { ResumeScorePanel } from "./resume-score"; +import { ResumeScorePanel, scoreResume } from "./resume-score"; import { AISuggestionsPanel } from "./ai-suggestions"; import { VersionHistoryPanel } from "./version-history-panel"; @@ -21,7 +21,11 @@ const SECTIONS = [ { id: "education", label: "Education", component: EducationForm }, { id: "skills", label: "Skills", component: SkillsForm }, { id: "projects", label: "Projects", component: ProjectsForm }, - { id: "certifications", label: "Certifications", component: CertificationsForm }, + { + id: "certifications", + label: "Certifications", + component: CertificationsForm, + }, { id: "competitions", label: "Competitions", component: CompetitionsForm }, { id: "languages", label: "Languages", component: LanguagesForm }, { id: "versions", label: "Versions", component: VersionHistoryPanel }, @@ -31,12 +35,15 @@ const SECTIONS = [ export function BuilderForm() { const searchParams = useSearchParams(); + const data = useResumeStore((state) => state.data); const activeSection = useResumeStore((state) => state.activeSection); const setActiveSection = useResumeStore((state) => state.setActiveSection); const hiddenSections = useResumeStore((state) => state.hiddenSections); - const toggleSectionVisibility = useResumeStore((state) => state.toggleSectionVisibility); + const toggleSectionVisibility = useResumeStore( + (state) => state.toggleSectionVisibility, + ); const [sectionOrder, setSectionOrder] = useState(() => - SECTIONS.map((s) => s.id) + SECTIONS.map((s) => s.id), ); const tabsContainerRef = useRef(null); const [showSectionManager, setShowSectionManager] = useState(false); @@ -46,7 +53,10 @@ export function BuilderForm() { useEffect(() => { if (!showSectionManager) return; const handleClick = (e: MouseEvent) => { - if (sectionManagerRef.current && !sectionManagerRef.current.contains(e.target as Node)) { + if ( + sectionManagerRef.current && + !sectionManagerRef.current.contains(e.target as Node) + ) { setShowSectionManager(false); } }; @@ -55,10 +65,14 @@ export function BuilderForm() { }, [showSectionManager]); // Sections that the user has *not* hidden - const visibleSectionIds = sectionOrder.filter((id) => !hiddenSections.includes(id)); + const visibleSectionIds = sectionOrder.filter( + (id) => !hiddenSections.includes(id), + ); const visibleSections = visibleSectionIds .map((id) => SECTIONS.find((s) => s.id === id)) - .filter((section): section is (typeof SECTIONS)[number] => Boolean(section)); + .filter((section): section is (typeof SECTIONS)[number] => + Boolean(section), + ); // If the active section gets hidden, jump to the first visible section useEffect(() => { @@ -91,7 +105,7 @@ export function BuilderForm() { useEffect(() => { if (!tabsContainerRef.current) return; const activeTab = tabsContainerRef.current.querySelector( - `[data-section-id="${activeSection}"]` + `[data-section-id="${activeSection}"]`, ) as HTMLElement | null; if (!activeTab) return; @@ -139,11 +153,14 @@ export function BuilderForm() { setRecentlyMovedId(movedSection.id); const newPosition = target + 1; const arrow = direction === "left" ? "←" : "→"; - setMoveFeedback(`${arrow} Moved "${movedSection.label}" to position ${newPosition}`); + setMoveFeedback( + `${arrow} Moved "${movedSection.label}" to position ${newPosition}`, + ); }; const ActiveComponent = - visibleSections.find((s) => s.id === activeSection)?.component || PersonalInfoForm; + visibleSections.find((s) => s.id === activeSection)?.component || + PersonalInfoForm; const hasPrev = activeIndex > 0; const hasNext = activeIndex >= 0 && activeIndex < visibleSections.length - 1; @@ -158,20 +175,245 @@ export function BuilderForm() { const hiddenCount = hiddenSections.length; const activeSectionLabel = visibleSections[activeIndex]?.label ?? "Section"; + const getSectionBadge = (id: string) => { + switch (id) { + case "personal": + const hasContact = Boolean( + data.personalInfo.fullName.trim() && data.personalInfo.email.trim(), + ); + return hasContact ? ( + + OK + + ) : ( + + Required + + ); + case "experience": + return data.experience.length > 0 ? ( + + {data.experience.length} + + ) : null; + case "education": + return data.education.length > 0 ? ( + + {data.education.length} + + ) : null; + case "skills": + const skillCount = data.skills.reduce( + (acc, cat) => acc + cat.items.length, + 0, + ); + return skillCount > 0 ? ( + + {skillCount} + + ) : null; + case "projects": + return data.projects.length > 0 ? ( + + {data.projects.length} + + ) : null; + case "certifications": + return data.certifications.length > 0 ? ( + + {data.certifications.length} + + ) : null; + case "competitions": + return (data.competitions ?? []).length > 0 ? ( + + {(data.competitions ?? []).length} + + ) : null; + case "languages": + return data.languages.length > 0 ? ( + + {data.languages.length} + + ) : null; + case "score": + const { total } = scoreResume(data); + const scoreColor = + total >= 85 + ? "bg-green-100 text-green-800 border-green-200" + : total >= 50 + ? "bg-yellow-100 text-yellow-800 border-yellow-200" + : "bg-red-100 text-red-800 border-red-200"; + return ( + + {total}% + + ); + default: + return null; + } + }; + return ( -
- {/* Section tabs */} -
+
+ {/* LEFT SIDEBAR - Desktop (md and up) */} +
+
+
+
+

NAVIGATION

+

+ Section {Math.max(activeIndex + 1, 1)} of{" "} + {visibleSections.length} +

+
+ {/* Sections toggle */} +
+ + + {showSectionManager && ( +
+
+

+ TOGGLE SECTIONS +

+

+ Click to show/hide +

+
+
+ {SECTIONS.map((section) => { + const isHidden = hiddenSections.includes(section.id); + const isPersonal = section.id === "personal"; + return ( + + ); + })} +
+
+ )} +
+
+
+ +
+ {visibleSections.map((section, index) => { + const isJustMoved = recentlyMovedId === section.id; + const isActive = activeSection === section.id; + return ( + + ); + })} +
+ + {/* Sidebar Footer with reorder buttons */} +
+
+ + +
+
+ Reordering: {activeSectionLabel} +
+
+
+ + {/* MOBILE LAYOUT TABS - Sticky at the top */} +

SECTION NAVIGATION

- Section {Math.max(activeIndex + 1, 1)} of {visibleSections.length} - {hiddenCount > 0 && · {hiddenCount} hidden} + Section {Math.max(activeIndex + 1, 1)} of{" "} + {visibleSections.length} + {hiddenCount > 0 && ( + · {hiddenCount} hidden + )}

-
+
-
-
-
+
+
+
{visibleSections.map((section, index) => { const isJustMoved = recentlyMovedId === section.id; return ( @@ -204,52 +449,30 @@ export function BuilderForm() { key={section.id} data-section-id={section.id} onClick={() => setActiveSection(section.id)} - className={`snap-start px-3 py-2 text-[11px] sm:text-xs font-bold uppercase tracking-wider whitespace-nowrap transition-all duration-300 border ${ + className={`snap-start px-3 py-2 text-[11px] sm:text-xs font-bold uppercase tracking-wider whitespace-nowrap transition-all duration-300 border flex items-center gap-1.5 ${ isJustMoved ? "bg-yellow-400 text-black border-yellow-500 ring-2 ring-yellow-300 scale-105" : activeSection === section.id - ? "bg-black text-white border-black" - : "bg-white text-gray-700 border-gray-300 hover:bg-gray-100" + ? "bg-black text-white border-black" + : "bg-white text-gray-700 border-gray-300 hover:bg-gray-100" }`} - aria-current={activeSection === section.id ? "page" : undefined} + aria-current={ + activeSection === section.id ? "page" : undefined + } > - {String(index + 1).padStart(2, "0")} - {section.label} + + {String(index + 1).padStart(2, "0")} + + {section.label} + {getSectionBadge(section.id)} ); })}
- {/* Reorder controls — clearly labeled with the active section name */} -
- - - {activeSectionLabel} - - -
- - {/* Manage Sections toggle */} -
+ {/* Manage Sections toggle - Mobile */} +
{showSectionManager && ( -
+
-

TOGGLE SECTIONS

-

Click to show/hide

+

+ TOGGLE SECTIONS +

+

+ Click to show/hide +

{SECTIONS.map((section) => { @@ -277,23 +504,35 @@ export function BuilderForm() { ); })} @@ -302,43 +541,25 @@ export function BuilderForm() { )}
+
- {/* Move feedback toast — appears inside the nav area */} -
-
- - {moveFeedback} + {/* Main Content Area */} +
+ {/* Move feedback toast — appears inside the main area top */} + {moveFeedback && ( +
+
+ + + {moveFeedback} + +
+ )} +
+
- -
- - -
- - {/* Form content */} -
- -
); } diff --git a/src/components/builder/builder-header.tsx b/src/components/builder/builder-header.tsx index 02522b3..cb6f433 100644 --- a/src/components/builder/builder-header.tsx +++ b/src/components/builder/builder-header.tsx @@ -26,12 +26,28 @@ export function BuilderHeader({ saveValidationError, isGuest = false, }: BuilderHeaderProps) { - const { title, setTitle, templateId, setTemplateId, fontFamily, setFontFamily, pdfLocale, setPdfLocale, isSaving, isDirty, lastSavedAt, data, setData } = - useResumeStore(); + const { + title, + setTitle, + templateId, + setTemplateId, + fontFamily, + setFontFamily, + pdfLocale, + setPdfLocale, + isSaving, + isDirty, + lastSavedAt, + data, + setData, + } = useResumeStore(); const [isExporting, setIsExporting] = useState(false); const [isCheckingWriting, setIsCheckingWriting] = useState(false); - const [writingStatus, setWritingStatus] = useState<{ type: "success" | "error"; message: string } | null>(null); + const [writingStatus, setWritingStatus] = useState<{ + type: "success" | "error"; + message: string; + } | null>(null); const [previewPdfUrl, setPreviewPdfUrl] = useState(null); const [previewFilename, setPreviewFilename] = useState("resume.pdf"); const [exportError, setExportError] = useState(null); @@ -47,7 +63,12 @@ export function BuilderHeader({ ) { setExportError(null); } - }, [exportError, templateId, data.personalInfo.fullName, data.personalInfo.email]); + }, [ + exportError, + templateId, + data.personalInfo.fullName, + data.personalInfo.email, + ]); useEffect(() => { if (!writingStatus) return; @@ -80,7 +101,9 @@ export function BuilderHeader({ setIsExporting(true); try { - const response = await fetch(`/api/export/${resumeId}?locale=${pdfLocale}`); + const response = await fetch( + `/api/export/${resumeId}?locale=${pdfLocale}`, + ); if (!response.ok) { throw new Error("Export failed"); } @@ -130,7 +153,9 @@ export function BuilderHeader({ console.error("Failed to check grammar/spelling:", error); setWritingStatus({ type: "error", - message: (error as Error).message || "Failed to check writing. Please try again.", + message: + (error as Error).message || + "Failed to check writing. Please try again.", }); } finally { setIsCheckingWriting(false); @@ -153,7 +178,10 @@ export function BuilderHeader({ }); }; - const activeBulletSymbol = (data.personalInfo.bulletSymbol || "•").slice(0, 3); + const activeBulletSymbol = (data.personalInfo.bulletSymbol || "•").slice( + 0, + 3, + ); return ( <> @@ -168,7 +196,9 @@ export function BuilderHeader({ > ← {isGuest ? "Home" : "Back"} - {isGuest ? "GUEST_MODE" : "BUILDER_MODE"} + + {isGuest ? "GUEST_MODE" : "BUILDER_MODE"} +
@@ -177,17 +207,23 @@ export function BuilderHeader({ {isGuest ? ( <> - NOT SAVED · GUEST + + NOT SAVED · GUEST + ) : saveValidationError ? ( <> - {saveValidationError} + + {saveValidationError} + ) : isSaving ? ( <> - SAVING... + + SAVING... + ) : isDirty ? ( <> @@ -216,7 +252,10 @@ export function BuilderHeader({
{isGuest ? (
- + {isCheckingWriting ? "CHECKING..." : "CHECK GRAMMAR"}
{writingStatus && (

{writingStatus.message}

@@ -277,7 +321,7 @@ export function BuilderHeader({ value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Resume Title" - className="w-full border border-gray-300 bg-transparent px-3 py-2 text-lg font-bold focus:border-black focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-gray-300 bg-transparent px-3 py-2 text-lg font-bold focus:border-black transition-all duration-150" />
@@ -291,7 +335,7 @@ export function BuilderHeader({ title="Font family" value={fontFamily} onChange={(e) => setFontFamily(e.target.value)} - className="border border-black bg-transparent px-3 py-2 text-xs font-bold tracking-wider focus:bg-black focus:text-white transition-all duration-150" + className="border border-black bg-transparent px-3 py-2 text-xs font-bold tracking-wider transition-all duration-150" > {CV_FONTS.map((font) => (
diff --git a/src/components/builder/forms/certifications-form.tsx b/src/components/builder/forms/certifications-form.tsx index bb51f91..f594bdd 100644 --- a/src/components/builder/forms/certifications-form.tsx +++ b/src/components/builder/forms/certifications-form.tsx @@ -27,12 +27,12 @@ export function CertificationsForm() { const updateCertification = ( id: string, - updates: Partial + updates: Partial, ) => { setData({ ...data, certifications: certifications.map((c) => - c.id === id ? { ...c, ...updates } : c + c.id === id ? { ...c, ...updates } : c, ), }); }; @@ -40,8 +40,12 @@ export function CertificationsForm() { return (
- SECTION_06 // CERTIFICATIONS -

Certifications

+ + SECTION_06 // CERTIFICATIONS + +

+ Certifications +

Professional certifications, licenses, and credentials

@@ -72,7 +76,7 @@ export function CertificationsForm() { updateCertification(cert.id, { name: e.target.value }) } placeholder="AWS Solutions Architect Professional" - className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black transition-all duration-150" />
@@ -86,7 +90,7 @@ export function CertificationsForm() { updateCertification(cert.id, { issuer: e.target.value }) } placeholder="Amazon Web Services" - className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black transition-all duration-150" />
@@ -98,7 +102,7 @@ export function CertificationsForm() { updateCertification(cert.id, { date: value }) } title="Certification date" - className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black transition-all duration-150" />
@@ -112,7 +116,7 @@ export function CertificationsForm() { updateCertification(cert.id, { url: e.target.value }) } placeholder="https://www.credly.com/badges/..." - className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black transition-all duration-150" />
diff --git a/src/components/builder/forms/competitions-form.tsx b/src/components/builder/forms/competitions-form.tsx index f443d6f..e3217a7 100644 --- a/src/components/builder/forms/competitions-form.tsx +++ b/src/components/builder/forms/competitions-form.tsx @@ -10,21 +10,25 @@ export function CompetitionsForm() { const { data, setData } = useResumeStore(); const { competitions = [] } = data; // Default fallback for older resumes const [improvingBullet, setImprovingBullet] = useState(null); - const [improvingDescription, setImprovingDescription] = useState(null); + const [improvingDescription, setImprovingDescription] = useState< + string | null + >(null); const handleImproveBullet = async ( compId: string, idx: number, text: string, compName: string, - role: string + role: string, ) => { if (!text.trim() || improvingBullet) return; const key = `${compId}-${idx}`; setImprovingBullet(key); try { const positionContext = role ? `${role} at ${compName}` : compName; - const { result } = await improveBullet(text, { position: positionContext }); + const { result } = await improveBullet(text, { + position: positionContext, + }); updateHighlight(compId, idx, result); } catch (err: unknown) { alert((err as Error).message || "AI improvement failed."); @@ -37,14 +41,18 @@ export function CompetitionsForm() { compId: string, text: string, compName: string, - role: string + role: string, ) => { if (!text.trim() || improvingDescription) return; setImprovingDescription(compId); try { const titleContext = role ? `${role} at ${compName}` : compName; - const { result } = await improveDescription(text, { title: titleContext }); - updateCompetition(compId, { description: result.slice(0, MAX_DESCRIPTION_LENGTH) }); + const { result } = await improveDescription(text, { + title: titleContext, + }); + updateCompetition(compId, { + description: result.slice(0, MAX_DESCRIPTION_LENGTH), + }); } catch (err: unknown) { alert((err as Error).message || "AI improvement failed."); } finally { @@ -89,12 +97,12 @@ export function CompetitionsForm() { const updateCompetition = ( id: string, - updates: Partial + updates: Partial, ) => { setData({ ...data, competitions: competitions.map((comp) => - comp.id === id ? { ...comp, ...updates } : comp + comp.id === id ? { ...comp, ...updates } : comp, ), }); }; @@ -127,8 +135,12 @@ export function CompetitionsForm() { return (
- SECTION_OPT // COMPETITIONS -

Competitions

+ + SECTION_OPT // COMPETITIONS + +

+ Competitions +

Hackathons, case studies, academic or professional competitions

@@ -170,7 +182,9 @@ export function CompetitionsForm() {
- +
@@ -190,7 +204,7 @@ export function CompetitionsForm() { updateCompetition(comp.id, { role: e.target.value }) } placeholder="1st Place, Team Lead" - className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black transition-all duration-150" />
@@ -205,7 +219,7 @@ export function CompetitionsForm() { updateCompetition(comp.id, { date: newDate }); }} required - className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black transition-all duration-150" />
@@ -217,7 +231,7 @@ export function CompetitionsForm() { updateCompetition(comp.id, { location: e.target.value }) } placeholder="San Francisco, CA" - className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black transition-all duration-150" />
@@ -229,7 +243,7 @@ export function CompetitionsForm() { updateCompetition(comp.id, { url: e.target.value }) } placeholder="https://devpost.com/..." - className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black transition-all duration-150" />
@@ -239,8 +253,17 @@ export function CompetitionsForm() {
@@ -167,7 +169,7 @@ export function EducationForm() { updateEducation(edu.id, { degree: e.target.value }) } placeholder="Bachelor of Science" - className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black transition-all duration-150" />
@@ -179,7 +181,7 @@ export function EducationForm() { updateEducation(edu.id, { field: e.target.value }) } placeholder="Computer Science" - className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black transition-all duration-150" />
@@ -194,7 +196,7 @@ export function EducationForm() { updateEducation(edu.id, { location: e.target.value }) } placeholder="Berkeley, CA" - className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black transition-all duration-150" />
@@ -206,7 +208,7 @@ export function EducationForm() { updateEducation(edu.id, { gpa: e.target.value }) } placeholder="3.8" - className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black transition-all duration-150" />
@@ -221,7 +223,7 @@ export function EducationForm() { updateEducation(edu.id, { startDate: value }) } required - className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black transition-all duration-150" />
@@ -233,7 +235,7 @@ export function EducationForm() { updateEducation(edu.id, { endDate: value }) } disabled={edu.current} - className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150 disabled:opacity-50" + className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black transition-all duration-150 disabled:opacity-50" />
@@ -260,11 +262,15 @@ export function EducationForm() {
- +
{(edu.coursework ?? []).length === 0 && ( - No coursework added yet. + + No coursework added yet. + )} {(edu.coursework ?? []).map((course, idx) => ( handleCourseworkKeyDown(e, edu.id)} placeholder="Add a course (e.g. Data Structures)" - className="flex-1 border border-gray-400 bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150" + className="flex-1 border border-gray-400 bg-transparent px-3 py-2 focus:border-black transition-all duration-150" />
@@ -243,7 +250,7 @@ export function ExperienceForm() { updateExperience(exp.id, { company: e.target.value }) } placeholder="Tech Corp Inc." - className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black transition-all duration-150" />
@@ -257,7 +264,7 @@ export function ExperienceForm() { updateExperience(exp.id, { location: e.target.value }) } placeholder="New York, NY" - className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black transition-all duration-150" />
@@ -275,12 +282,16 @@ export function ExperienceForm() { [exp.id]: "End date cannot be before start date.", })); } else { - setDateErrors((prev) => { const next = { ...prev }; delete next[exp.id]; return next; }); + setDateErrors((prev) => { + const next = { ...prev }; + delete next[exp.id]; + return next; + }); } updateExperience(exp.id, { startDate: newStart }); }} required - className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black transition-all duration-150" />
@@ -296,11 +307,15 @@ export function ExperienceForm() { })); return; // block storing an invalid end date } - setDateErrors((prev) => { const next = { ...prev }; delete next[exp.id]; return next; }); + setDateErrors((prev) => { + const next = { ...prev }; + delete next[exp.id]; + return next; + }); updateExperience(exp.id, { endDate: newEnd }); }} disabled={exp.current} - className={`w-full border bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150 disabled:opacity-50 ${ + className={`w-full border bg-transparent px-3 py-2 focus:border-black transition-all duration-150 disabled:opacity-50 ${ dateErrors[exp.id] ? "border-red-500" : "border-gray-400" }`} /> @@ -340,7 +355,14 @@ export function ExperienceForm() {
@@ -98,7 +96,7 @@ export function LanguagesForm() { .value as ResumeData["languages"][0]["proficiency"], }) } - className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black transition-all duration-150" > {PROFICIENCY_LEVELS.map((level) => (
- {/* ── Import Profile ─────────────────────────────── */}
- {t("importSectionTitle")} + + {t("importSectionTitle")} +

{t("importSectionHint")}

{/* LinkedIn ZIP */}
- LINKEDIN + + LINKEDIN + - {t("linkedinZipHint")} + + {t("linkedinZipHint")} +
setGithubInput(e.target.value)} placeholder={t("githubPlaceholder")} - className="flex-1 min-w-0 border border-black bg-transparent px-3 py-1.5 text-sm focus:bg-black focus:text-white transition-all duration-150" + className="flex-1 min-w-0 border border-black bg-transparent px-3 py-1.5 text-sm focus:border-black transition-all duration-150" />
-

- {t("forgDevNotice")} -

+

{t("forgDevNotice")}

{forgOptions.length > 1 && (
@@ -765,9 +829,7 @@ export function PersonalInfoForm() { {isImportingForg ? t("importing") : t("forgUseSelected")}
-

- {t("forgPickerHint")} -

+

{t("forgPickerHint")}

)} @@ -811,12 +873,17 @@ export function PersonalInfoForm() {
{errors.fullName ? ( - {errors.fullName} + + {errors.fullName} + ) : ( )} @@ -869,7 +938,9 @@ export function PersonalInfoForm() { className={inputClass("email")} /> {errors.email && ( - {errors.email} + + {errors.email} + )}
@@ -891,7 +962,9 @@ export function PersonalInfoForm() { className={inputClass("phone")} /> {errors.phone ? ( - {errors.phone} + + {errors.phone} + ) : ( Digits, spaces, +, -, or parentheses @@ -908,7 +981,7 @@ export function PersonalInfoForm() { onChange={(e) => updateField("location", e.target.value)} placeholder="San Francisco, CA" maxLength={100} - className="w-full border border-black bg-transparent px-4 py-3 focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-black bg-transparent px-4 py-3 focus:border-black transition-all duration-150" />
@@ -920,21 +993,25 @@ export function PersonalInfoForm() { value={personalInfo.website || ""} onChange={(e) => updateField("website", e.target.value)} placeholder="https://example.com" - className="w-full border border-black bg-transparent px-4 py-3 focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-black bg-transparent px-4 py-3 focus:border-black transition-all duration-150" />
{/* LinkedIn */} {/* LinkedIn URL — shown in CV header */}
- -

{t("linkedinUrlHint")}

+ +

+ {t("linkedinUrlHint")} +

updateField("linkedin", e.target.value)} placeholder={t("linkedinPlaceholder")} - className="w-full border border-black bg-transparent px-4 py-3 focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-black bg-transparent px-4 py-3 focus:border-black transition-all duration-150" />
@@ -948,7 +1025,7 @@ export function PersonalInfoForm() { value={personalInfo.github || ""} onChange={(e) => updateField("github", e.target.value)} placeholder={t("githubPlaceholder")} - className="w-full border border-black bg-transparent px-4 py-3 focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-black bg-transparent px-4 py-3 focus:border-black transition-all duration-150" /> @@ -974,10 +1051,11 @@ export function PersonalInfoForm() { placeholder="A brief professional summary or objective statement..." rows={6} maxLength={MAX_SUMMARY_LENGTH} - className="w-full border border-black bg-transparent px-4 py-3 focus:bg-black focus:text-white transition-all duration-150 resize-none" + className="w-full border border-black bg-transparent px-4 py-3 focus:border-black transition-all duration-150 resize-none" /> - {personalInfo.summary?.length || 0} / {MAX_SUMMARY_LENGTH} characters + {personalInfo.summary?.length || 0} / {MAX_SUMMARY_LENGTH}{" "} + characters @@ -986,10 +1064,12 @@ export function PersonalInfoForm() { updateField("bulletSymbol", e.target.value.slice(0, 3))} + onChange={(e) => + updateField("bulletSymbol", e.target.value.slice(0, 3)) + } placeholder="•" maxLength={3} - className="w-full border border-black bg-transparent px-4 py-3 focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-black bg-transparent px-4 py-3 focus:border-black transition-all duration-150" /> Use any symbol for CV bullets, for example: •, -, ▪, or → diff --git a/src/components/builder/forms/projects-form.tsx b/src/components/builder/forms/projects-form.tsx index c8ad330..ca78c07 100644 --- a/src/components/builder/forms/projects-form.tsx +++ b/src/components/builder/forms/projects-form.tsx @@ -280,7 +280,7 @@ export function ProjectsForm() { updateProject(project.id, { name: e.target.value }) } placeholder="E-Commerce Platform" - className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black transition-all duration-150" /> @@ -294,7 +294,7 @@ export function ProjectsForm() { updateProject(project.id, { url: e.target.value }) } placeholder="https://myproject.com" - className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black transition-all duration-150" />
@@ -306,7 +306,7 @@ export function ProjectsForm() { updateProject(project.id, { githubUrl: e.target.value }) } placeholder="https://github.com/you/repo" - className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black transition-all duration-150" />
@@ -318,7 +318,7 @@ export function ProjectsForm() { updateProject(project.id, { websiteUrl: e.target.value }) } placeholder="https://myproject.vercel.app" - className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150" + className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black transition-all duration-150" />
@@ -355,7 +355,7 @@ export function ProjectsForm() { placeholder="A full-stack e-commerce platform built with modern technologies..." rows={3} maxLength={MAX_DESCRIPTION_LENGTH} - className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150 resize-none" + className="w-full border border-gray-400 bg-transparent px-3 py-2 focus:border-black transition-all duration-150 resize-none" /> {project.description.length}/{MAX_DESCRIPTION_LENGTH} @@ -430,7 +430,7 @@ export function ProjectsForm() { ) } placeholder="Built scalable microservices architecture..." - className="flex-1 border border-gray-400 bg-transparent px-3 py-2 focus:border-black focus:bg-black focus:text-white transition-all duration-150" + className="flex-1 border border-gray-400 bg-transparent px-3 py-2 focus:border-black transition-all duration-150" />