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
159 lines
5.1 KiB
TypeScript
159 lines
5.1 KiB
TypeScript
import { t } from "@lingui/core/macro";
|
|
import { Trans } from "@lingui/react/macro";
|
|
import { ArrowRightIcon, BookIcon, SparkleIcon } from "@phosphor-icons/react";
|
|
import { Link } from "@tanstack/react-router";
|
|
import { m } from "motion/react";
|
|
import { Badge } from "@reactive-resume/ui/components/badge";
|
|
import { Button } from "@reactive-resume/ui/components/button";
|
|
import { CometCard } from "@/components/animation/comet-card";
|
|
import { Spotlight } from "@/components/animation/spotlight";
|
|
|
|
export function Hero() {
|
|
return (
|
|
<section
|
|
id="hero"
|
|
className="relative flex min-h-svh w-full flex-col items-center justify-center overflow-hidden border-b py-24"
|
|
>
|
|
<Spotlight />
|
|
|
|
<m.div
|
|
className="w-full will-change-[transform,opacity]"
|
|
initial={{ opacity: 0, y: 100 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 1.1, ease: "easeOut" }}
|
|
>
|
|
<CometCard
|
|
glareOpacity={0}
|
|
className="relative mx-auto -mb-12 3xl:max-w-7xl max-w-4xl px-8 md:-mb-24 md:px-12 lg:px-0"
|
|
>
|
|
<video
|
|
loop
|
|
muted
|
|
autoPlay
|
|
playsInline
|
|
width={1146}
|
|
height={720}
|
|
src="/videos/timelapse.mp4"
|
|
aria-label={t`Timelapse demonstration of building a resume with Reactive Resume`}
|
|
// Reserve the intrinsic aspect ratio so the box height is known before the video
|
|
// metadata loads — otherwise it reflows the centered hero column and causes CLS (~0.10).
|
|
className="pointer-events-none aspect-[1146/720] w-full rounded-md border object-cover"
|
|
/>
|
|
|
|
<div
|
|
aria-hidden="true"
|
|
className="pointer-events-none absolute inset-0 bg-linear-to-b from-transparent via-40% via-transparent to-background"
|
|
/>
|
|
</CometCard>
|
|
</m.div>
|
|
|
|
<div className="relative z-10 flex max-w-2xl flex-col items-center gap-y-6 px-4 xs:px-0 text-center">
|
|
{/* Badge */}
|
|
<m.a
|
|
className="will-change-[transform,opacity]"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.45, delay: 0.55 }}
|
|
whileHover={{ y: -2, scale: 1.01 }}
|
|
whileTap={{ scale: 0.985 }}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
href="https://docs.rxresu.me/getting-started"
|
|
>
|
|
<Badge variant="secondary" className="h-auto gap-1.5 px-3 py-0.5">
|
|
<SparkleIcon aria-hidden="true" className="size-3.5" weight="fill" />
|
|
<Trans>What's new in the latest version?</Trans>
|
|
</Badge>
|
|
</m.a>
|
|
|
|
{/* Headline */}
|
|
<m.div
|
|
className="will-change-[transform,opacity]"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.45, delay: 0.7 }}
|
|
>
|
|
<Trans>
|
|
<p className="font-medium text-muted-foreground tracking-tight md:text-lg">Finally,</p>
|
|
<h1 className="mt-1 font-semibold text-4xl tracking-tight md:text-5xl lg:text-6xl">
|
|
A free and open-source resume builder
|
|
</h1>
|
|
</Trans>
|
|
</m.div>
|
|
|
|
{/* Description */}
|
|
<m.p
|
|
className="max-w-xl text-base text-muted-foreground leading-relaxed will-change-[transform,opacity] md:text-lg"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.45, delay: 0.82 }}
|
|
>
|
|
<Trans>
|
|
Reactive Resume is a free and open-source resume builder that simplifies the process of creating, updating,
|
|
and sharing your resume.
|
|
</Trans>
|
|
</m.p>
|
|
|
|
{/* CTA Buttons */}
|
|
<m.div
|
|
className="flex flex-col items-center gap-3 will-change-[transform,opacity] sm:flex-row sm:gap-4"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.45, delay: 0.95 }}
|
|
>
|
|
<Button
|
|
size="lg"
|
|
nativeButton={false}
|
|
className="group relative overflow-hidden px-4"
|
|
render={
|
|
<Link to="/dashboard">
|
|
<span className="relative z-10 flex items-center gap-2">
|
|
<Trans>Get Started</Trans>
|
|
<ArrowRightIcon
|
|
aria-hidden="true"
|
|
className="size-4 transition-transform group-hover:translate-x-0.5"
|
|
/>
|
|
</span>
|
|
</Link>
|
|
}
|
|
/>
|
|
|
|
<Button
|
|
size="lg"
|
|
variant="ghost"
|
|
className="gap-2 px-4"
|
|
nativeButton={false}
|
|
render={
|
|
<a href="https://docs.rxresu.me" target="_blank" rel="noopener noreferrer">
|
|
<BookIcon aria-hidden="true" className="size-4" />
|
|
<Trans>Learn More</Trans>
|
|
<span className="sr-only">
|
|
<Trans>(opens in new tab)</Trans>
|
|
</span>
|
|
</a>
|
|
}
|
|
/>
|
|
</m.div>
|
|
</div>
|
|
|
|
{/* Scroll indicator - decorative */}
|
|
<m.div
|
|
aria-hidden="true"
|
|
role="presentation"
|
|
className="absolute inset-s-1/2 bottom-8 -translate-x-1/2"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ delay: 1.25, duration: 0.7 }}
|
|
>
|
|
<m.div
|
|
className="flex h-8 w-5 items-start justify-center rounded-full border border-muted-foreground/30 p-1.5 will-change-transform"
|
|
animate={{ y: [0, 5, 0] }}
|
|
transition={{ duration: 1.5, repeat: Number.POSITIVE_INFINITY, ease: "easeInOut" }}
|
|
>
|
|
<m.div className="h-1.5 w-1 rounded-full bg-muted-foreground/50" />
|
|
</m.div>
|
|
</m.div>
|
|
</section>
|
|
);
|
|
}
|