diff --git a/apps/studio/app/(main)/(dashboard)/components/RecentCard.tsx b/apps/studio/app/(main)/(dashboard)/components/RecentCard.tsx index 1c40147..9598ad3 100644 --- a/apps/studio/app/(main)/(dashboard)/components/RecentCard.tsx +++ b/apps/studio/app/(main)/(dashboard)/components/RecentCard.tsx @@ -7,14 +7,16 @@ import { FileText } from "lucide-react"; import { Badge } from "@veriworkly/ui"; import { getDocumentDefinition } from "@/features/documents/core/registry"; +import { getDocumentEditorPath } from "@/features/documents/core/routes"; import { type DocumentLibraryItem } from "@/features/documents/services/document-library"; const RecentCard = ({ doc }: { doc: DocumentLibraryItem }) => { const definition = getDocumentDefinition(doc.type); + const editorPath = getDocumentEditorPath(doc.type, doc.id); return (
diff --git a/apps/studio/app/(main)/(dashboard)/documents/components/DocumentActionsMenu.tsx b/apps/studio/app/(main)/(dashboard)/documents/components/DocumentActionsMenu.tsx index ed151e2..14ff7ed 100644 --- a/apps/studio/app/(main)/(dashboard)/documents/components/DocumentActionsMenu.tsx +++ b/apps/studio/app/(main)/(dashboard)/documents/components/DocumentActionsMenu.tsx @@ -16,6 +16,7 @@ import { useRouter } from "next/navigation"; import { Menu, MenuItem, MenuSeparator } from "@veriworkly/ui"; import type { DocumentLibraryItem } from "@/features/documents/services/document-library"; +import { getDocumentEditorPath } from "@/features/documents/core/routes"; export interface DocumentActionsMenuProps { doc: DocumentLibraryItem; @@ -37,6 +38,7 @@ export function DocumentActionsMenu({ onSyncDetailsAction, }: DocumentActionsMenuProps) { const router = useRouter(); + const editorPath = getDocumentEditorPath(doc.type, doc.id); return ( { close(); - router.push(`/editor/${doc.type.toLowerCase()}/${doc.id}`); + router.push(editorPath); }} > @@ -122,9 +124,7 @@ export function DocumentActionsMenu({ className="h-8 rounded-lg text-xs" onClick={() => { close(); - void navigator.clipboard.writeText( - `${window.location.origin}/editor/${doc.type.toLowerCase()}/${doc.id}`, - ); + void navigator.clipboard.writeText(`${window.location.origin}${editorPath}`); toast.success("Document link copied"); }} > diff --git a/apps/studio/app/(main)/(dashboard)/documents/components/DocumentListRow.tsx b/apps/studio/app/(main)/(dashboard)/documents/components/DocumentListRow.tsx index df663cf..6894202 100644 --- a/apps/studio/app/(main)/(dashboard)/documents/components/DocumentListRow.tsx +++ b/apps/studio/app/(main)/(dashboard)/documents/components/DocumentListRow.tsx @@ -8,6 +8,7 @@ import type { SyncTelemetry } from "@/features/documents/services/document-sync" import type { DocumentLibraryItem } from "@/features/documents/services/document-library"; import { getDocumentDefinition } from "@/features/documents/core/registry"; +import { getDocumentEditorPath } from "@/features/documents/core/routes"; import { formatRelative } from "@/features/documents/services/document-library"; import { DocumentActionsMenu } from "./DocumentActionsMenu"; @@ -35,6 +36,7 @@ export function DocumentListRow({ onSyncDetailsAction, }: DocumentListRowProps) { const Icon = docIconMap[doc.type]; + const editorPath = getDocumentEditorPath(doc.type, doc.id); return (
@@ -67,7 +69,7 @@ export function DocumentListRow({
@@ -79,7 +82,7 @@ export function DocumentPreviewCard({
diff --git a/apps/studio/app/(main)/editor/[type]/[id]/preview/PreviewClient.tsx b/apps/studio/app/(main)/editor/[type]/[id]/preview/PreviewClient.tsx index ece57fa..7cae30b 100644 --- a/apps/studio/app/(main)/editor/[type]/[id]/preview/PreviewClient.tsx +++ b/apps/studio/app/(main)/editor/[type]/[id]/preview/PreviewClient.tsx @@ -15,6 +15,7 @@ import { loadResumeById } from "@/features/resume/services/resume-service"; import { loadDocumentById } from "@/features/documents/services/document-workspace-service"; import type { DocumentType } from "@/features/documents/core/document-types"; import type { CoverLetterContent } from "@/features/cover-letter/types"; +import { getDocumentEditorPath } from "@/features/documents/core/routes"; import { CoverLetterPreview } from "@/templates/cover-letter/web"; interface PreviewClientProps { @@ -58,7 +59,7 @@ export function PreviewClient({ documentId, type }: PreviewClientProps) { const found = type === "RESUME" ? Boolean(routeResume) : Boolean(routeDocument); const title = type === "RESUME" ? resume.basics.fullName || "Untitled Resume" : routeDocument?.title; - const routeType = type.toLowerCase(); + const editorPath = getDocumentEditorPath(type, documentId); const debugType = type === "COVER_LETTER" ? "cover-letter" : type.toLowerCase(); const debugTemplateId = type === "RESUME" ? resume.templateId : routeDocument?.templateId; const canDebugPdf = type === "RESUME" || type === "COVER_LETTER"; @@ -87,7 +88,7 @@ export function PreviewClient({ documentId, type }: PreviewClientProps) { ) : null} Back to editor diff --git a/apps/studio/app/(main)/editor/components/EditorContentPanel.tsx b/apps/studio/app/(main)/editor/components/EditorContentPanel.tsx deleted file mode 100644 index 70549c2..0000000 --- a/apps/studio/app/(main)/editor/components/EditorContentPanel.tsx +++ /dev/null @@ -1,168 +0,0 @@ -"use client"; - -import { useState } from "react"; -import type { DragEvent } from "react"; - -import type { ResumeSectionId } from "@/types/resume"; - -import { useResume } from "@/features/resume/hooks/use-resume"; - -import LinksSection from "./content/sections/LinksSection"; -import AwardsSection from "./content/sections/AwardsSection"; -import BasicsSection from "./content/sections/BasicsSection"; -import CustomSection from "./content/sections/CustomSection"; -import SkillsSection from "./content/sections/SkillsSection"; -import SummarySection from "./content/sections/SummarySection"; -import ProjectsSection from "./content/sections/ProjectsSection"; -import LanguagesSection from "./content/sections/LanguagesSection"; -import InterestsSection from "./content/sections/InterestsSection"; -import VolunteerSection from "./content/sections/VolunteerSection"; -import EducationSection from "./content/sections/EducationSection"; -import ReferencesSection from "./content/sections/ReferencesSection"; -import ExperienceSection from "./content/sections/ExperienceSection"; -import AchievementsSection from "./content/sections/AchievementsSection"; -import PublicationsSection from "./content/sections/PublicationsSection"; -import CertificationsSection from "./content/sections/CertificationsSection"; - -const EditorContentPanel = () => { - const { reorderSections, resume } = useResume(); - - const [draggedSectionId, setDraggedSectionId] = useState(null); - const [openSectionId, setOpenSectionId] = useState("basics"); - - function handleToggleSection(sectionId: ResumeSectionId) { - setOpenSectionId((currentSectionId) => (currentSectionId === sectionId ? null : sectionId)); - } - - return ( -
-
-

- Resume Content -

- -

Content editor

-
- -
- {resume.sections - .slice() - .sort((left, right) => left.order - right.order) - .map((section, sectionIndex) => { - const handleDragStart = (event: DragEvent) => { - setDraggedSectionId(section.id); - - if (event.dataTransfer) { - event.dataTransfer.effectAllowed = "move"; - } - }; - - const handleDragOver = (event: DragEvent) => { - event.preventDefault(); - - if (event.dataTransfer) { - event.dataTransfer.dropEffect = "move"; - } - }; - - const handleDrop = (event: DragEvent) => { - event.preventDefault(); - - if (draggedSectionId && draggedSectionId !== section.id) { - const draggedIndex = resume.sections.findIndex( - (item) => item.id === draggedSectionId, - ); - - if (draggedIndex !== -1) { - reorderSections(draggedIndex, sectionIndex); - } - } - - setDraggedSectionId(null); - }; - - const handleDragEnd = () => { - setDraggedSectionId(null); - }; - - const sectionProps = { - isOpen: openSectionId === section.id, - onDragEnd: handleDragEnd, - onDragOver: handleDragOver, - onDragStart: handleDragStart, - onDrop: handleDrop, - onToggle: handleToggleSection, - }; - - if (section.id === "basics") { - return ; - } - - if (section.id === "links") { - return ; - } - - if (section.id === "summary") { - return ; - } - - if (section.id === "experience") { - return ; - } - - if (section.id === "education") { - return ; - } - - if (section.id === "projects") { - return ; - } - - if (section.id === "skills") { - return ; - } - - if (section.id === "certifications") { - return ; - } - - if (section.id === "awards") { - return ; - } - - if (section.id === "publications") { - return ; - } - - if (section.id === "languages") { - return ; - } - - if (section.id === "interests") { - return ; - } - - if (section.id === "volunteer") { - return ; - } - - if (section.id === "references") { - return ; - } - - if (section.id === "achievements") { - return ; - } - - if (section.id === "custom") { - return ; - } - - return null; - })} -
-
- ); -}; - -export default EditorContentPanel; diff --git a/apps/studio/app/(main)/editor/components/EditorLayout.tsx b/apps/studio/app/(main)/editor/components/EditorLayout.tsx deleted file mode 100644 index 1c013bd..0000000 --- a/apps/studio/app/(main)/editor/components/EditorLayout.tsx +++ /dev/null @@ -1,317 +0,0 @@ -"use client"; - -import { useRouter, useSearchParams } from "next/navigation"; -import { useDeferredValue, useEffect, useRef, useState } from "react"; - -import type { TemplateComponent } from "@/types/template"; - -import { Card } from "@veriworkly/ui"; - -import { - startDocumentSyncWorker, - hydrateCloudDocumentByIdToLocalStorage, -} from "@/features/documents/services/document-sync"; -import { - loadResumeById, - createResumeWithTemplate, -} from "@/features/resume/services/resume-service"; -import { useResume } from "@/features/resume/hooks/use-resume"; -import { loadWorkspaceSettingsFromLocalStorage } from "@/features/documents/services/workspace-settings"; - -import { loadTemplateComponentById } from "@/templates"; - -import Toolbar from "./Toolbar"; -import EditorModals from "./EditorModals"; -import EditorContentPanel from "./EditorContentPanel"; -import EditorSettingsPanel from "./EditorSettingsPanel"; - -import { useUserStore } from "@/store/useUserStore"; - -interface EditorLayoutProps { - resumeId: string; -} - -const EditorLayout = ({ resumeId }: EditorLayoutProps) => { - const router = useRouter(); - const searchParams = useSearchParams(); - - const hasHydratedRef = useRef(false); - - const isLoggedIn = useUserStore((state) => state.isLoggedIn); - - const { hydrateFromStorage, resume, saveToStorage, setResume } = useResume(); - - const [panelOpen, setPanelOpen] = useState(true); - const [activeTab, setActiveTab] = useState<"editor" | "preview">("editor"); - const [activePanel, setActivePanel] = useState<"content" | "settings">("content"); - - const [templateComponent, setTemplateComponent] = useState(null); - - const [shareModalOpen, setShareModalOpen] = useState(false); - const [deleteModalOpen, setDeleteModalOpen] = useState(false); - - const deferredResume = useDeferredValue(resume); - - const resumePreviewId = `resume-preview-${resume.id}`; - - const stagePaddingClass = resume.customization.pagePadding === 0 ? "p-0" : "p-3 md:p-6"; - - useEffect(() => { - let cancelled = false; - - const hydrate = async () => { - if (resumeId === "new") { - const template = searchParams.get("template") || "executive-clarity"; - const newResume = createResumeWithTemplate(template); - - router.replace(`/editor/resume/${newResume.id}`); - - return; - } - - if (resumeId) { - const routeResume = loadResumeById(resumeId); - - if (routeResume) { - setResume(routeResume); - hasHydratedRef.current = true; - - return; - } - - const cloudResult = await hydrateCloudDocumentByIdToLocalStorage("RESUME", resumeId); - - if (!cancelled && cloudResult.ok) { - const hydratedResume = loadResumeById(resumeId); - - if (hydratedResume) { - setResume(hydratedResume); - hasHydratedRef.current = true; - return; - } - } - } - - if (!cancelled) { - hydrateFromStorage(); - hasHydratedRef.current = true; - } - }; - - void hydrate(); - - return () => { - cancelled = true; - }; - }, [hydrateFromStorage, resumeId, router, searchParams, setResume]); - - useEffect(() => { - if (!hasHydratedRef.current) { - return; - } - - saveToStorage({ debounceMs: 300 }); - }, [resume, saveToStorage]); - - useEffect(() => { - if (!hasHydratedRef.current) { - return; - } - - if (!isLoggedIn) { - return; - } - - const workspaceSettings = loadWorkspaceSettingsFromLocalStorage(); - - startDocumentSyncWorker("RESUME", { - enabled: isLoggedIn && workspaceSettings.autoSyncEnabled, - idleDelayMs: 12_000, - }); - }, [isLoggedIn, resume.updatedAt]); - - useEffect(() => { - let cancelled = false; - - const loadTemplate = async () => { - const nextTemplate = await loadTemplateComponentById(deferredResume.templateId); - - if (!cancelled) { - setTemplateComponent(() => nextTemplate); - } - }; - - void loadTemplate(); - - return () => { - cancelled = true; - }; - }, [deferredResume.templateId]); - - return ( -
-
- setShareModalOpen(true)} - onOpenDelete={() => setDeleteModalOpen(true)} - /> -
- - setShareModalOpen(false)} - deleteModalOpen={deleteModalOpen} - onDeleteModalClose={() => setDeleteModalOpen(false)} - /> - -
- - - -
- -
- {panelOpen ? ( -
- -
-
- - - -
- - -
- -
- {activePanel === "content" ? : } -
-
-
- ) : null} - - {!panelOpen ? ( -
- -

- Panels -

- - - - -
-
- ) : null} - - -
-
-
-

- Live Preview -

- -

- {deferredResume.basics.fullName || "Untitled Resume"} -

-
-
- -
-
-
- {templateComponent - ? (() => { - const TemplateComponent = templateComponent; - return ; - })() - : null} -
-
-
-
-
-
-
- ); -}; - -export default EditorLayout; diff --git a/apps/studio/app/(main)/editor/components/content/sections/EducationSection.tsx b/apps/studio/app/(main)/editor/components/content/sections/EducationSection.tsx deleted file mode 100644 index c2ef61a..0000000 --- a/apps/studio/app/(main)/editor/components/content/sections/EducationSection.tsx +++ /dev/null @@ -1,181 +0,0 @@ -"use client"; - -import { useMemo, useState } from "react"; - -import type { BaseSectionProps } from "./section-types"; - -import { Input } from "@veriworkly/ui"; -import { Button } from "@veriworkly/ui"; - -import { useResume } from "@/features/resume/hooks/use-resume"; -import { validateEducation } from "@/features/resume/utils/validation"; - -import DraggableSection from "./DraggableSection"; -import { Field, invalidClass, TextArea } from "../EditorFormPrimitives"; - -const EducationSection = ({ - isOpen, - onDragEnd, - onDragOver, - onDragStart, - onDrop, - onToggle, -}: BaseSectionProps) => { - const { addEducation, removeEducation, resume, updateEducation } = useResume(); - - const [educationIndex, setEducationIndex] = useState(0); - - const safeEducationIndex = Math.min(educationIndex, Math.max(0, resume.education.length - 1)); - - const activeEducation = resume.education[safeEducationIndex]; - - const educationErrors = useMemo( - () => (activeEducation ? validateEducation(activeEducation) : {}), - [activeEducation], - ); - - if (!activeEducation) { - return null; - } - - return ( - -
- - - - - -
- -
- - - updateEducation(safeEducationIndex, { - school: event.target.value, - }) - } - value={activeEducation.school} - /> - - - - - updateEducation(safeEducationIndex, { - degree: event.target.value, - }) - } - value={activeEducation.degree} - /> - - - - - updateEducation(safeEducationIndex, { - field: event.target.value, - }) - } - value={activeEducation.field} - /> - - - - - updateEducation(safeEducationIndex, { - startDate: event.target.value.replace(/\D/g, "").slice(0, 4), - }) - } - pattern="[0-9]*" - placeholder="2019" - value={activeEducation.startDate} - /> - - - - - updateEducation(safeEducationIndex, { - endDate: event.target.value.replace(/\D/g, "").slice(0, 4), - }) - } - pattern="[0-9]*" - placeholder="2023" - value={activeEducation.endDate} - /> - - - -
- -
- -