diff --git a/apps/client/src/contexts/ProfileProvider.tsx b/apps/client/src/contexts/ProfileProvider.tsx index cbf4b1a..dc53874 100644 --- a/apps/client/src/contexts/ProfileProvider.tsx +++ b/apps/client/src/contexts/ProfileProvider.tsx @@ -2,29 +2,15 @@ import { raise } from "@cv/utils"; import { createContext, type ReactNode, - useCallback, useContext, useMemo, useState, } from "react"; -import { useMyProfilesQuery } from "@/generated/graphql"; +import { type MyProfilesQuery, useMyProfilesQuery } from "@/generated/graphql"; -interface Profile { - id: string; - name: string; - fullName: string | null; - headline: string | null; - phone: string | null; - address: string | null; - postalCode: string | null; - website: string | null; - linkedInUrl: string | null; - summary: string | null; - createdAt: string; - updatedAt: string; -} +export type Profile = MyProfilesQuery["myProfiles"][number]; -interface ProfileContextType { +interface ProfileContextValue { profiles: Profile[]; activeProfile: Profile | null; activeProfileId: string | null; @@ -36,81 +22,52 @@ interface ProfileContextType { const STORAGE_KEY = "cv-generator:active-profile-id"; -const ProfileContext = createContext(null); +const ProfileContext = createContext(null); -export function ProfileProvider({ children }: { children: ReactNode }) { +export const ProfileProvider = ({ children }: { children: ReactNode }) => { const [selectedProfileId, setSelectedProfileId] = useState( () => localStorage.getItem(STORAGE_KEY), ); const { data, isPending, refetch } = useMyProfilesQuery( {}, - { - retry: false, - refetchOnWindowFocus: false, - }, - ); - - const profiles = useMemo( - () => (data?.myProfiles as Profile[] | undefined) ?? [], - [data], + { retry: false, refetchOnWindowFocus: false }, ); - const activeProfile = useMemo(() => { - if (profiles.length === 0) return null; - if (selectedProfileId) { - const found = profiles.find((p) => p.id === selectedProfileId); - if (found) return found; - } - return profiles[0] ?? null; - }, [profiles, selectedProfileId]); - - const activeProfileId = activeProfile?.id ?? null; + const profiles = data?.myProfiles ?? []; + const activeProfile = + profiles.find((p) => p.id === selectedProfileId) ?? profiles[0] ?? null; - const setActiveProfileId = useCallback((id: string) => { - setSelectedProfileId(id); - localStorage.setItem(STORAGE_KEY, id); - }, []); - - const handleRefetch = useCallback(() => { - refetch(); - }, [refetch]); - - const value = useMemo( + const value = useMemo( () => ({ profiles, activeProfile, - activeProfileId, - setActiveProfileId, + activeProfileId: activeProfile?.id ?? null, + setActiveProfileId: (id) => { + setSelectedProfileId(id); + localStorage.setItem(STORAGE_KEY, id); + }, isLoading: isPending, hasProfiles: profiles.length > 0, - refetch: handleRefetch, + refetch, }), - [ - profiles, - activeProfile, - activeProfileId, - setActiveProfileId, - isPending, - handleRefetch, - ], + [profiles, activeProfile, isPending, refetch], ); return ( {children} ); -} +}; -export function useProfile() { - return ( - useContext(ProfileContext) ?? - raise("useProfile must be used within a ProfileProvider") - ); -} +export const useProfile = (): ProfileContextValue => + useContext(ProfileContext) ?? + raise("useProfile must be used within a ProfileProvider"); -export function useActiveProfileId() { - const { activeProfileId } = useProfile(); - return ( - activeProfileId ?? raise("No active profile — user needs to create one") - ); -} +/** + * Throws if no profile is active. Use in screens that already gate on + * `useProfile().hasProfiles`; otherwise prefer reading `activeProfileId` + * directly from `useProfile()`. + */ +export const useActiveProfileId = (): string => + useProfile().activeProfileId ?? + raise("No active profile - user needs to create one"); diff --git a/apps/client/src/features/cv-templates/components/EditSidebar.tsx b/apps/client/src/features/cv-templates/components/EditSidebar.tsx index 1616d97..831c38b 100644 --- a/apps/client/src/features/cv-templates/components/EditSidebar.tsx +++ b/apps/client/src/features/cv-templates/components/EditSidebar.tsx @@ -1,66 +1,32 @@ import { Select, TextInput, Textarea } from "@cv/ui"; +import type { ReactNode } from "react"; import type { SaveStatus } from "../hooks/useAutosave"; +import type { CVEditorPatch, CVEditorState } from "../hooks/useCVEditor"; +import { SaveStatusLabel } from "./SaveStatusLabel"; interface TemplateOption { id: string; name: string; } -interface ProfileFields { - fullName: string; - headline: string; - summary: string; +interface SaveState { + status: SaveStatus; + error: string | null; } interface EditSidebarProps { - title: string; - introduction: string; - templateId: string; - templateOptions: TemplateOption[]; - profile: ProfileFields; - onTitleChange: (value: string) => void; - onIntroductionChange: (value: string) => void; - onTemplateChange: (id: string) => void; - onProfileFieldChange: (field: keyof ProfileFields, value: string) => void; - saveStatus: SaveStatus; - saveError: string | null; + state: CVEditorState; + onPatch: (patch: CVEditorPatch) => void; + templates: TemplateOption[]; + save: SaveState; } -const statusLabel = (status: SaveStatus, error: string | null): string => { - if (status === "error") { - return error ? `Error: ${error}` : "Save failed"; - } - if (status === "saving") { - return "Saving..."; - } - if (status === "saved") { - return "Saved"; - } - if (status === "pending") { - return "Unsaved changes"; - } - return "All changes saved"; -}; - -const statusClass = (status: SaveStatus): string => { - if (status === "error") { - return "text-ctp-red"; - } - if (status === "saving" || status === "pending") { - return "text-ctp-yellow"; - } - if (status === "saved") { - return "text-ctp-green"; - } - return "text-ctp-subtext0"; -}; - const Section = ({ title, children, }: { title: string; - children: React.ReactNode; + children: ReactNode; }) => (

@@ -71,65 +37,60 @@ const Section = ({ ); export const EditSidebar = ({ - title, - introduction, - templateId, - templateOptions, - profile, - onTitleChange, - onIntroductionChange, - onTemplateChange, - onProfileFieldChange, - saveStatus, - saveError, + state, + onPatch, + templates, + save, }: EditSidebarProps) => { + const setCv = (cv: Partial) => onPatch({ cv }); + const setProfile = (profile: Partial) => + onPatch({ profile }); + return (