mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-24 08:54:05 +10:00
b404dbd42a
* feat(applications): job application tracker with AI copilot Add an Applications module at /dashboard/applications: pipeline board (dnd-kit), table view with bulk actions, Insights (fit tiles, funnel, sources, shareable funnel-flow SVG), campaigns, tags, CSV import, and Add/Edit/Detail slide-overs. Each application links a live Reactive Resume. AI "Application Copilot" (applications.ai.*): job-posting autofill, resume↔job match score (fit ring), resume tailoring, and cover-letter / follow-up drafting — via the user's configured provider. Board cards + table rows get context menus (edit / move / archive / delete). Charts are CSS/SVG (no new chart dep); adds a UI Checkbox. Also includes local TanStack devtools setup and toolchain bumps. Claude-Session: https://claude.ai/code/session_01TEeRHnEayw2MFCShFRyL5f * feat(applications): close follow-up gaps + squash migrations Finish the deferred/open items on the applications tracker: - Cover-letter upload re-enabled. Fix the storage blocker by deriving the key extension from content type (buildFileKey/EXTENSION_BY_CONTENT_TYPE) instead of hardcoding .jpeg, so PDFs serve correctly and non-JPEG image avatars keep working under FLAG_DISABLE_IMAGE_PROCESSING. Add coverLetterUrl/coverLetterName columns + Documents-section upload/remove. - Contacts editor in the detail sheet (add/edit/remove, keyed per app). - Board caps rendered cards per column (COLUMN_PAGE_SIZE=50 + "Show more"). - Extract new Lingui messages across locales. - Guard coverLetterUrl to http(s)/relative at the API boundary. Squash the five branch-only application-table migrations (create -> +tags -> +cover-letter -> drop -> re-add) into a single clean CREATE TABLE via drizzle-kit generate. Claude-Session: https://claude.ai/code/session_01TEeRHnEayw2MFCShFRyL5f * chore: update dependencies * fix(web): address React Doctor findings — compiler, purity, query, component structure prefer-module-scope-pure-function: hoist buildSubtitle, getDecimalPlaces, handleLocaleChange, onLocaleChange, stop, listContent/groupedListContent to module scope so they aren't rebuilt on every render. react-compiler-todo (??=): rewrite draft.metadata.styleRules ??= [] to the non-assignment form to unblock auto-memoization. set-state-in-effect: derive updatedAtLabel at render time instead of syncing it through useState + useEffect. query-destructure-result: destructure useQuery results at call site in resume-analysis and resume-thumbnail to follow TanStack Query v5 convention. only-export-components: extract non-component exports to sibling .ts files so Fast Refresh can preserve component state: - getNextWeights → typography/get-next-weights.ts - detectJsonImportType + ImportType → dialogs/resume/import.utils.ts - getLocaleOptions → features/locale/locale-options.tsx - preview helpers + DEFAULT_PDF_PAGE_SIZE → preview.shared.utils.ts - resolveHighlightToolbarState + defaultHighlightColor → rich-input.utils.ts - computeDelta + getSparklinePoints → statistics.utils.ts no-multi-comp: split multi-component files into focused companions: - ResumePane + ToolbarButton → routes/agent/-components/resume-pane.tsx - DesktopBuilderShell → builder/$resumeId/-components/desktop-builder-shell.tsx - MobileBuilderShell + helpers → builder/$resumeId/-components/mobile-builder-shell.tsx - setBuilderLayout/getBuilderLayout moved to -store/sidebar.ts fix(tests): add Resume type import to section-builder mocks and cast partial mock data as unknown as Resume to satisfy stricter type checking; fix noExplicitAny Biome errors in the same mocks. * feat(applications): improve performance * chore: fix knip issues * perf(builder): halve per-keystroke render cost Section-form fields called `form.handleSubmit()` on every keystroke, which re-validated the whole form and toggled submit state — firing the render cascade twice per character (~6809 renders/keystroke, FPS dropping to 9). Persist via a form-level `listeners.onChange` instead and drop the per-field `handleSubmit()` (basics, custom-fields, design). Narrow header/dock resume subscriptions to metadata slices so they no longer re-render on content edits. Cuts renders 6809 -> 3403 per keystroke (50%), 0 frame drops. Save, preview, and design controls verified working; 449/449 web tests pass. * perf(home): eliminate hero CLS from unreserved video box The hero <section> is `flex items-center` (shrink-to-fit), so the video wrapper's width depended on the video's intrinsic size, which only resolves after the media loads. aspect-ratio couldn't reserve height without a definite width, so the video grew from ~190px to ~563px after first paint and shoved the centered hero text down ~373px (CLS ~0.095). Give the wrapper a definite width (w-full + mx-auto on the CometCard) and set an explicit aspect ratio + width/height on the video so its box is reserved before load. CLS 0.095 -> 0; hero stays visually centered at max-w-4xl. * docs: add application tracker guides * chore(db): squash application migrations * fix(email): import React in auth template for server-side rendering compatibility * chore(release): v5.2.1 * Refactor resume rendering and builder workflows * fix: address application tracker review findings
111 lines
3.7 KiB
TypeScript
111 lines
3.7 KiB
TypeScript
import type { Icon } from "@phosphor-icons/react";
|
|
import type { ReactNode } from "react";
|
|
import { t } from "@lingui/core/macro";
|
|
import { Trans } from "@lingui/react/macro";
|
|
import { EyeIcon, NotePencilIcon, PaletteIcon } from "@phosphor-icons/react";
|
|
import { Outlet } from "@tanstack/react-router";
|
|
import { useEffect, useState } from "react";
|
|
import { cn } from "@reactive-resume/utils/style";
|
|
import { usePreviewPausedStore } from "@/features/resume/builder/draft";
|
|
import { BuilderSidebarLeft } from "../-sidebar/left";
|
|
import { BuilderSidebarRight } from "../-sidebar/right";
|
|
import { BuilderHeader } from "./header";
|
|
|
|
type MobileBuilderTab = "edit" | "preview" | "design";
|
|
|
|
const MOBILE_BUILDER_TABS = [
|
|
{ value: "edit", icon: NotePencilIcon },
|
|
{ value: "preview", icon: EyeIcon },
|
|
{ value: "design", icon: PaletteIcon },
|
|
] as const satisfies readonly { value: MobileBuilderTab; icon: Icon }[];
|
|
|
|
type MobileSidebarPanelProps = {
|
|
children: ReactNode;
|
|
};
|
|
|
|
function MobileSidebarPanel({ children }: MobileSidebarPanelProps) {
|
|
// Sits below the header (top-14) and above the tab bar (bottom-16). The sidebar's ScrollArea hardcodes
|
|
// `h-[calc(100svh-3.5rem)]`; override it to fill this panel so its last item isn't hidden under the tab bar.
|
|
return (
|
|
<div className="fixed inset-x-0 top-14 bottom-16 z-40 bg-background [&_[data-slot=scroll-area]]:h-full">
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
type MobileBuilderTabBarProps = {
|
|
activeTab: MobileBuilderTab;
|
|
onTabChange: (tab: MobileBuilderTab) => void;
|
|
};
|
|
|
|
function MobileBuilderTabBar({ activeTab, onTabChange }: MobileBuilderTabBarProps) {
|
|
const labels: Record<MobileBuilderTab, string> = { edit: t`Edit`, preview: t`Preview`, design: t`Design` };
|
|
|
|
return (
|
|
<nav className="fixed inset-x-0 bottom-0 z-50 flex border-t bg-popover pb-[env(safe-area-inset-bottom)]">
|
|
{MOBILE_BUILDER_TABS.map(({ value, icon: Icon }) => {
|
|
const isActive = value === activeTab;
|
|
|
|
return (
|
|
<button
|
|
key={value}
|
|
type="button"
|
|
aria-current={isActive ? "page" : undefined}
|
|
onClick={() => onTabChange(value)}
|
|
className={cn(
|
|
"flex min-h-16 flex-1 flex-col items-center justify-center gap-1 text-xs transition-colors",
|
|
isActive ? "text-primary" : "text-muted-foreground hover:text-foreground",
|
|
)}
|
|
>
|
|
<Icon className="size-5" weight={isActive ? "fill" : "regular"} />
|
|
<span>{labels[value]}</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</nav>
|
|
);
|
|
}
|
|
|
|
export function MobileBuilderShell() {
|
|
// Local state is enough — mobile view mode is shell-scoped and doesn't need to persist.
|
|
const [tab, setTab] = useState<MobileBuilderTab>("edit");
|
|
const setPreviewPaused = usePreviewPausedStore((state) => state.setPaused);
|
|
|
|
// The preview stays mounted under the Edit/Design overlay; pause its render while it's covered.
|
|
useEffect(() => {
|
|
setPreviewPaused(tab !== "preview");
|
|
return () => setPreviewPaused(false);
|
|
}, [tab, setPreviewPaused]);
|
|
|
|
return (
|
|
<div className="flex min-h-[100dvh] flex-col">
|
|
<a
|
|
href="#main-content"
|
|
className="sr-only rounded-md bg-popover px-4 py-2 text-sm ring-2 ring-ring focus:not-sr-only focus:absolute focus:inset-s-2 focus:top-2 focus:z-[100]"
|
|
>
|
|
<Trans>Skip to main content</Trans>
|
|
</a>
|
|
|
|
<BuilderHeader />
|
|
|
|
{/* The preview (fixed inset-0) stays mounted so zoom/pan state survives tab switches. */}
|
|
<main id="main-content" className="flex-1">
|
|
<Outlet />
|
|
</main>
|
|
|
|
{tab === "edit" && (
|
|
<MobileSidebarPanel>
|
|
<BuilderSidebarLeft />
|
|
</MobileSidebarPanel>
|
|
)}
|
|
{tab === "design" && (
|
|
<MobileSidebarPanel>
|
|
<BuilderSidebarRight />
|
|
</MobileSidebarPanel>
|
|
)}
|
|
|
|
<MobileBuilderTabBar activeTab={tab} onTabChange={setTab} />
|
|
</div>
|
|
);
|
|
}
|