Repository: BartoszJarocki/cv Branch: main Commit: b9c9c2bacf53 Files: 43 Total size: 73.6 KB Directory structure: gitextract_05bs3w52/ ├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── biome.json ├── components.json ├── docker-compose.yaml ├── next.config.js ├── package.json ├── postcss.config.js ├── public/ │ └── robots.txt ├── src/ │ ├── app/ │ │ ├── components/ │ │ │ ├── education.tsx │ │ │ ├── header.tsx │ │ │ ├── projects.tsx │ │ │ ├── skills.tsx │ │ │ ├── summary.tsx │ │ │ └── work-experience.tsx │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── loading.tsx │ │ ├── opengraph-image.tsx │ │ ├── page.tsx │ │ └── sitemap.ts │ ├── components/ │ │ ├── avatar.tsx │ │ ├── command-menu.tsx │ │ ├── error-boundary.tsx │ │ ├── icons/ │ │ │ ├── github-icon.tsx │ │ │ ├── index.ts │ │ │ ├── linkedin-icon.tsx │ │ │ └── x-icon.tsx │ │ └── ui/ │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── command.tsx │ │ ├── dialog.tsx │ │ └── section.tsx │ ├── data/ │ │ └── resume-data.ts │ └── lib/ │ ├── structured-data.ts │ ├── types.ts │ └── utils.ts ├── tailwind.config.js └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ node_modules .next .git .gitignore .claude .idea .vscode *.md Dockerfile docker-compose.yaml .env* ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js .yarn/install-state.gz # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* # local env files .env*.local # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts # ide .idea ================================================ FILE: Dockerfile ================================================ FROM node:22-slim AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable && corepack prepare pnpm@latest-10 --activate FROM base AS deps WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ pnpm install --frozen-lockfile FROM base AS build WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . ENV NEXT_TELEMETRY_DISABLED=1 ENV NODE_ENV=production RUN pnpm build FROM node:22-slim AS runner WORKDIR /app ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 ENV HOSTNAME="0.0.0.0" ENV PORT=3000 RUN addgroup --system --gid 1001 nodejs && \ adduser --system --uid 1001 nextjs COPY --from=build /app/public ./public COPY --from=build --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=build --chown=nextjs:nodejs /app/.next/static ./.next/static USER nextjs EXPOSE 3000 CMD ["node", "server.js"] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 Bartosz Jarocki Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ![cv](https://github.com/BartoszJarocki/cv/assets/1017620/79bdb9fc-0b20-4d2c-aafe-0526ad4a71d2)

minimalist cv Deploy with Vercel

[![Next.js](https://img.shields.io/badge/Next.js-16-black?logo=next.js)](https://nextjs.org/) [![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue?logo=typescript)](https://www.typescriptlang.org/) [![Tailwind CSS](https://img.shields.io/badge/Tailwind%20CSS-3.4-38B2AC?logo=tailwind-css)](https://tailwindcss.com/) simple web app that renders a minimalist CV with print-friendly layout. ## getting started ```bash git clone https://github.com/BartoszJarocki/cv.git cd cv pnpm install pnpm dev # open http://localhost:3000 # edit src/data/resume-data.ts to customize ``` ## scripts ```bash pnpm dev # start development server pnpm build # build for production pnpm start # start production server pnpm lint # run biome linting checks pnpm lint:fix # run biome linting with auto-fix pnpm format # check code formatting with biome pnpm format:fix # format code with biome pnpm check # run both linting and formatting checks pnpm check:fix # run both linting and formatting with auto-fix ``` ## project structure ``` src/ ├── app/ # next.js app router │ ├── components/ # page-level components │ │ ├── education.tsx │ │ ├── header.tsx │ │ ├── projects.tsx │ │ ├── skills.tsx │ │ ├── summary.tsx │ │ └── work-experience.tsx │ ├── layout.tsx # root layout with metadata │ └── page.tsx # main resume page ├── components/ # shared components │ ├── icons/ # social icon components │ └── ui/ # shadcn/ui components ├── data/ # resume data configuration │ └── resume-data.ts └── lib/ # utilities and types ├── structured-data.ts ├── types.ts └── utils.ts ``` ## customization all resume content lives in a single file: ```typescript // src/data/resume-data.ts export const RESUME_DATA = { name: "Your Name", initials: "YN", location: "Your City, Country", about: "Brief description", summary: "Professional summary", // ... more fields } ``` styling uses tailwind css — customize colors in `tailwind.config.js` and global styles in `src/app/globals.css`. ## docker ```bash docker compose build # build the container docker compose up -d # run the container docker compose down # stop the container ``` ## license MIT ================================================ FILE: biome.json ================================================ { "$schema": "https://biomejs.dev/schemas/2.0.6/schema.json", "vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false }, "files": { "ignoreUnknown": false, "includes": ["src/**/*"] }, "formatter": { "enabled": true, "formatWithErrors": false, "indentStyle": "space", "indentWidth": 2, "lineEnding": "lf", "lineWidth": 80, "attributePosition": "auto" }, "linter": { "enabled": true, "rules": { "recommended": true, "a11y": { "recommended": true }, "complexity": { "recommended": true, "noExtraBooleanCast": "error", "noUselessCatch": "error", "noUselessTypeConstraint": "error" }, "correctness": { "recommended": true, "noChildrenProp": "error", "noConstAssign": "error", "noConstantCondition": "error", "noEmptyCharacterClassInRegex": "error", "noEmptyPattern": "error", "noGlobalObjectCalls": "error", "noInvalidConstructorSuper": "error", "noSetterReturn": "error", "noSwitchDeclarations": "error", "noUndeclaredVariables": "error", "noUnreachable": "error", "noUnreachableSuper": "error", "useIsNan": "error", "useValidForDirection": "error", "useYield": "error" }, "security": { "recommended": true, "noDangerouslySetInnerHtml": "warn" }, "style": { "recommended": true, "noImplicitBoolean": "error", "noInferrableTypes": "error", "noNamespace": "error", "noNegationElse": "off", "noNonNullAssertion": "warn", "noParameterAssign": "error", "noRestrictedGlobals": "error", "noUselessElse": "error", "useAsConstAssertion": "error", "useBlockStatements": "off", "useCollapsedElseIf": "error", "useConst": "error", "useDefaultParameterLast": "error", "useEnumInitializers": "error", "useExponentiationOperator": "error", "useFilenamingConvention": { "level": "error", "options": { "filenameCases": ["kebab-case", "PascalCase"] } }, "useForOf": "error", "useFragmentSyntax": "error", "useImportType": "error", "useNodejsImportProtocol": "error", "useNumberNamespace": "error", "useSelfClosingElements": "error", "useShorthandAssign": "error", "useSingleVarDeclarator": "error", "useTemplate": "error" }, "suspicious": { "recommended": true, "noApproximativeNumericConstant": "error", "noArrayIndexKey": "warn", "noAssignInExpressions": "error", "noAsyncPromiseExecutor": "error", "noCatchAssign": "error", "noClassAssign": "error", "noCompareNegZero": "error", "noControlCharactersInRegex": "error", "noDebugger": "error", "noDoubleEquals": "error", "noDuplicateCase": "error", "noDuplicateClassMembers": "error", "noDuplicateObjectKeys": "error", "noDuplicateParameters": "error", "noEmptyBlockStatements": "error", "noFallthroughSwitchClause": "error", "noFunctionAssign": "error", "noGlobalAssign": "error", "noImportAssign": "error", "noMisleadingCharacterClass": "error", "noPrototypeBuiltins": "error", "noRedeclare": "error", "noShadowRestrictedNames": "error", "noUnsafeNegation": "error", "useGetterReturn": "error" } } }, "javascript": { "formatter": { "jsxQuoteStyle": "double", "quoteProperties": "asNeeded", "trailingCommas": "es5", "semicolons": "always", "arrowParentheses": "always", "bracketSpacing": true, "bracketSameLine": false, "quoteStyle": "double", "attributePosition": "auto" } }, "json": { "formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2, "lineEnding": "lf", "lineWidth": 80, "trailingCommas": "none" } } } ================================================ FILE: components.json ================================================ { "$schema": "https://ui.shadcn.com/schema.json", "style": "default", "rsc": true, "tsx": true, "tailwind": { "config": "tailwind.config.js", "css": "app/globals.css", "baseColor": "gray", "cssVariables": true, "prefix": "" }, "aliases": { "components": "@/components", "utils": "@/lib/utils" } } ================================================ FILE: docker-compose.yaml ================================================ services: app: build: . ports: - '3000:3000' environment: - NODE_ENV=production restart: unless-stopped healthcheck: test: ['CMD-SHELL', 'curl -f http://localhost:3000 || exit 1'] interval: 30s timeout: 10s retries: 3 start_period: 10s deploy: resources: limits: cpus: '1' memory: 512M ================================================ FILE: next.config.js ================================================ /** @type {import('next').NextConfig} */ const nextConfig = { output: 'standalone', // Enable React strict mode for better development experience reactStrictMode: true, // Optimize images images: { remotePatterns: [ { protocol: 'https', hostname: 'avatars.githubusercontent.com', }, ], formats: ['image/avif', 'image/webp'], minimumCacheTTL: 60, }, // Compress output compress: true, // Headers for security and performance async headers() { return [ { source: '/:path*', headers: [ { key: 'X-DNS-Prefetch-Control', value: 'on' }, { key: 'X-Content-Type-Options', value: 'nosniff' }, { key: 'X-Frame-Options', value: 'SAMEORIGIN' }, { key: 'X-XSS-Protection', value: '1; mode=block' }, { key: 'Referrer-Policy', value: 'origin-when-cross-origin' }, { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' } ] }, { source: '/(.*).svg', headers: [ { key: 'Cache-Control', value: 'public, max-age=31536000, immutable' } ] }, { source: '/(.*).png', headers: [ { key: 'Cache-Control', value: 'public, max-age=31536000, immutable' } ] } ]; }, // Reduce bundle size by excluding source maps in production productionBrowserSourceMaps: false, // PoweredByHeader removes the X-Powered-By header poweredByHeader: false, } module.exports = nextConfig ================================================ FILE: package.json ================================================ { "name": "web-cv", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "biome lint ./src", "lint:fix": "biome lint --write ./src", "format": "biome format ./src", "format:fix": "biome format --write ./src", "check": "biome check ./src", "check:fix": "biome check --write ./src" }, "dependencies": { "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-slot": "^1.1.2", "@vercel/analytics": "^1.5.0", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "cmdk": "^1.0.0", "geist": "^1.7.0", "lucide-react": "^0.474.0", "next": "^16.1.0", "react": "^19", "react-dom": "^19", "tailwind-merge": "^2.2.0", "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "@biomejs/biome": "2.0.6", "@types/node": "^22", "@types/react": "^19", "@types/react-dom": "^19", "autoprefixer": "^10.0.1", "postcss": "^8", "tailwindcss": "^3.4.0", "typescript": "^5" } } ================================================ FILE: postcss.config.js ================================================ module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, } ================================================ FILE: public/robots.txt ================================================ # Robots.txt for https://cv.jarocki.me User-agent: * Allow: / Disallow: /api/ Disallow: /_next/ Disallow: /graphql # Sitemap location Sitemap: https://cv.jarocki.me/sitemap.xml # Crawl-delay for responsible crawling Crawl-delay: 1 ================================================ FILE: src/app/components/education.tsx ================================================ import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Section } from "@/components/ui/section"; import type { RESUME_DATA } from "@/data/resume-data"; type Education = (typeof RESUME_DATA)["education"][number]; interface EducationPeriodProps { start: Education["start"]; end: Education["end"]; } /** * Displays the education period in a consistent format */ function EducationPeriod({ start, end }: EducationPeriodProps) { return (
{start} - {end}
); } interface EducationItemProps { education: Education; } /** * Individual education card component */ function EducationItem({ education }: EducationItemProps) { const { school, start, end, degree } = education; const schoolId = `education-${school.toLowerCase().replace(/\s+/g, "-")}`; return (

{school}

{degree}
); } interface EducationListProps { education: readonly Education[]; } /** * Main education section component * Renders a list of education experiences */ export function Education({ education }: EducationListProps) { return (

Education

{education.map((item) => (
))}
); } ================================================ FILE: src/app/components/header.tsx ================================================ import { GlobeIcon, MailIcon, PhoneIcon } from "lucide-react"; import type React from "react"; import { Avatar } from "@/components/avatar"; import { GitHubIcon, LinkedInIcon } from "@/components/icons"; import { XIcon } from "@/components/icons/x-icon"; import { Button } from "@/components/ui/button"; import { RESUME_DATA } from "@/data/resume-data"; import type { IconType } from "@/lib/types"; // Type-safe icon mapping const ICON_MAP: Record< IconType, React.ComponentType> > = { github: GitHubIcon, linkedin: LinkedInIcon, x: XIcon, globe: GlobeIcon, mail: MailIcon, phone: PhoneIcon, } as const; interface LocationLinkProps { location: typeof RESUME_DATA.location; locationLink: typeof RESUME_DATA.locationLink; } function LocationLink({ location, locationLink }: LocationLinkProps) { return (

); } interface SocialButtonProps { href: string; iconType: IconType; label: string; } function SocialButton({ href, iconType, label }: SocialButtonProps) { const IconComponent = ICON_MAP[iconType]; return ( ); } interface ContactButtonsProps { contact: typeof RESUME_DATA.contact; personalWebsiteUrl?: string; } function ContactButtons({ contact, personalWebsiteUrl }: ContactButtonsProps) { return ( ); } interface PrintContactProps { contact: typeof RESUME_DATA.contact; personalWebsiteUrl?: string; } function PrintContact({ contact, personalWebsiteUrl }: PrintContactProps) { return (
{personalWebsiteUrl && ( <> {new URL(personalWebsiteUrl).hostname} )} {contact.email && ( <> {contact.email} )} {contact.tel && ( {contact.tel} )}
); } /** * Header component displaying personal information and contact details */ export function Header() { return (

{RESUME_DATA.name}

{RESUME_DATA.about}

); } ================================================ FILE: src/app/components/projects.tsx ================================================ import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Section } from "@/components/ui/section"; import type { RESUME_DATA } from "@/data/resume-data"; type ProjectTags = readonly string[]; interface ProjectLinkProps { title: string; link?: string; } /** * Renders project title with optional link and status indicator */ function ProjectLink({ title, link }: ProjectLinkProps) { if (!link) { return {title}; } return ( <> {title} ); } interface ProjectTagsProps { tags: ProjectTags; } /** * Renders a list of technology tags used in the project */ function ProjectTags({ tags }: ProjectTagsProps) { if (tags.length === 0) return null; return ( ); } interface ProjectCardProps { title: string; description: string; tags: ProjectTags; link?: string; } /** * Card component displaying project information */ function ProjectCard({ title, description, tags, link }: ProjectCardProps) { return (
{description}
); } interface ProjectsProps { projects: (typeof RESUME_DATA)["projects"]; } /** * Section component displaying all side projects */ export function Projects({ projects }: ProjectsProps) { return (

Side projects

{projects.map((project) => (
))}
); } ================================================ FILE: src/app/components/skills.tsx ================================================ import { Badge } from "@/components/ui/badge"; import { Section } from "@/components/ui/section"; import { cn } from "@/lib/utils"; type Skills = readonly string[]; interface SkillsListProps { skills: Skills; className?: string; } /** * Renders a list of skills as badges */ function SkillsList({ skills, className }: SkillsListProps) { return ( ); } interface SkillsProps { skills: Skills; className?: string; } /** * Skills section component * Displays a list of professional skills as badges */ export function Skills({ skills, className }: SkillsProps) { return (

Skills

); } ================================================ FILE: src/app/components/summary.tsx ================================================ import { Section } from "../../components/ui/section"; interface AboutProps { summary: string; className?: string; } /** * Summary section component * Displays a summary of professional experience and goals */ export function Summary({ summary, className }: AboutProps) { return (

About

{summary}
); } ================================================ FILE: src/app/components/work-experience.tsx ================================================ import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Section } from "@/components/ui/section"; import type { RESUME_DATA } from "@/data/resume-data"; import { cn } from "@/lib/utils"; type WorkExperience = (typeof RESUME_DATA)["work"][number]; type WorkBadges = readonly string[]; interface BadgeListProps { className?: string; badges: WorkBadges; } /** * Renders a list of badges for work experience * Handles both mobile and desktop layouts through className prop */ function BadgeList({ className, badges }: BadgeListProps) { if (badges.length === 0) return null; return ( ); } interface WorkPeriodProps { start: WorkExperience["start"]; end?: WorkExperience["end"]; } /** * Displays the work period in a consistent format */ function WorkPeriod({ start, end }: WorkPeriodProps) { return (
{start} - {end ?? "Present"}
); } interface CompanyLinkProps { company: WorkExperience["company"]; link: WorkExperience["link"]; } /** * Renders company name with optional link */ function CompanyLink({ company, link }: CompanyLinkProps) { return ( {company} ); } interface WorkExperienceItemProps { work: WorkExperience; } /** * Individual work experience card component * Handles responsive layout for badges (mobile/desktop) */ function WorkExperienceItem({ work }: WorkExperienceItemProps) { const { company, link, badges, title, start, end, description, highlights } = work; return (

{title}

{description} {highlights && highlights.length > 0 && (
    {highlights.map((highlight) => (
  • {highlight}
  • ))}
)}
); } interface WorkExperienceProps { work: (typeof RESUME_DATA)["work"]; } /** * Main work experience section component * Renders a list of work experiences in chronological order */ export function WorkExperience({ work }: WorkExperienceProps) { return (

Work Experience

{work.map((item) => (
))}
); } ================================================ FILE: src/app/globals.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { --background: 0 0% 100%; --foreground: 224 71.4% 4.1%; --card: 0 0% 100%; --card-foreground: 224 71.4% 4.1%; --popover: 0 0% 100%; --popover-foreground: 224 71.4% 4.1%; --primary: 220.9 39.3% 11%; --primary-foreground: 210 20% 98%; --secondary: 220 14.3% 95.9%; --secondary-foreground: 220.9 39.3% 11%; --muted: 220 14.3% 95.9%; --muted-foreground: 220 8.9% 46.1%; --accent: 220 14.3% 95.9%; --accent-foreground: 220.9 39.3% 11%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 20% 98%; --border: 220 13% 91%; --input: 220 13% 91%; --ring: 224 71.4% 4.1%; --radius: 0.5rem; } .dark { --background: 224 71.4% 4.1%; --foreground: 210 20% 98%; --card: 224 71.4% 4.1%; --card-foreground: 210 20% 98%; --popover: 224 71.4% 4.1%; --popover-foreground: 210 20% 98%; --primary: 210 20% 98%; --primary-foreground: 220.9 39.3% 11%; --secondary: 215 27.9% 16.9%; --secondary-foreground: 210 20% 98%; --muted: 215 27.9% 16.9%; --muted-foreground: 217.9 10.6% 64.9%; --accent: 215 27.9% 16.9%; --accent-foreground: 210 20% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 20% 98%; --border: 215 27.9% 16.9%; --input: 215 27.9% 16.9%; --ring: 216 12.2% 83.9%; } } @layer base { * { @apply border-border; } body { @apply bg-background text-foreground font-sans; } } .print-force-new-page { page-break-before: always; } @keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } } .animate-fade-in { animation: fadeIn 400ms ease-out both; } @media print { .animate-fade-in { animation: none !important; opacity: 1 !important; transform: none !important; } } ================================================ FILE: src/app/layout.tsx ================================================ import { Analytics } from "@vercel/analytics/react"; import { GeistMono } from "geist/font/mono"; import { GeistSans } from "geist/font/sans"; import type { Metadata, Viewport } from "next"; import "./globals.css"; import type React from "react"; import { ErrorBoundary } from "@/components/error-boundary"; import { RESUME_DATA } from "@/data/resume-data"; export const metadata: Metadata = { metadataBase: new URL("https://cv.jarocki.me"), title: { default: `${RESUME_DATA.name} - ${RESUME_DATA.about}`, template: `%s | ${RESUME_DATA.name}`, }, description: RESUME_DATA.about, keywords: [ "resume", "cv", "portfolio", RESUME_DATA.name, "software engineer", "full stack developer", "react", "next.js", "typescript", ], authors: [{ name: RESUME_DATA.name }], creator: RESUME_DATA.name, publisher: RESUME_DATA.name, formatDetection: { email: false, address: false, telephone: false, }, openGraph: { type: "website", locale: "en_US", url: RESUME_DATA.personalWebsiteUrl, siteName: `${RESUME_DATA.name}'s CV`, title: `${RESUME_DATA.name} - ${RESUME_DATA.about}`, description: RESUME_DATA.about, }, robots: { index: true, follow: true, googleBot: { index: true, follow: true, "max-video-preview": -1, "max-image-preview": "large", "max-snippet": -1, }, }, twitter: { card: "summary_large_image", title: `${RESUME_DATA.name} - ${RESUME_DATA.about}`, description: RESUME_DATA.about, creator: "@BartoszJarocki", }, alternates: { canonical: RESUME_DATA.personalWebsiteUrl, }, }; export const viewport: Viewport = { themeColor: [ { media: "(prefers-color-scheme: light)", color: "white" }, { media: "(prefers-color-scheme: dark)", color: "black" }, ], width: "device-width", initialScale: 1, maximumScale: 5, }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( {children} ); } ================================================ FILE: src/app/loading.tsx ================================================ export default function Loading() { return (
{/* Header skeleton */}
{/* Content sections skeleton */}
{[...Array(4)].map((_, i) => (
))}
); } ================================================ FILE: src/app/opengraph-image.tsx ================================================ import { ImageResponse } from "next/og"; import { RESUME_DATA } from "../data/resume-data"; export const runtime = "edge"; export const alt = "Minimalist Resume"; export const size = { width: 1200, height: 630, }; export const contentType = "image/png"; export default async function Image() { return new ImageResponse(
{/* biome-ignore lint/performance/noImgElement: ImageResponse context requires img element */} {RESUME_DATA.name}
{RESUME_DATA.name}
{RESUME_DATA.about}
{RESUME_DATA.personalWebsiteUrl && (
{RESUME_DATA.personalWebsiteUrl}
)}
, { ...size, } ); } ================================================ FILE: src/app/page.tsx ================================================ import type { Metadata } from "next"; import { CommandMenu } from "@/components/command-menu"; import { RESUME_DATA } from "@/data/resume-data"; import { generateResumeStructuredData } from "@/lib/structured-data"; import { Education } from "./components/education"; import { Header } from "./components/header"; import { Projects } from "./components/projects"; import { Skills } from "./components/skills"; import { Summary } from "./components/summary"; import { WorkExperience } from "./components/work-experience"; export const metadata: Metadata = { title: `${RESUME_DATA.name} - Resume`, description: RESUME_DATA.about, openGraph: { title: `${RESUME_DATA.name} - Resume`, description: RESUME_DATA.about, type: "profile", locale: "en_US", images: [ { url: "https://cv.jarocki.me/opengraph-image", width: 1200, height: 630, alt: `${RESUME_DATA.name}'s profile picture`, }, ], }, twitter: { card: "summary_large_image", title: `${RESUME_DATA.name} - Resume`, description: RESUME_DATA.about, images: ["https://cv.jarocki.me/opengraph-image"], }, }; /** * Transform social links for command menu */ function getCommandMenuLinks() { const links = []; if (RESUME_DATA.personalWebsiteUrl) { links.push({ url: RESUME_DATA.personalWebsiteUrl, title: "Personal Website", }); } return [ ...links, ...RESUME_DATA.contact.social.map((socialMediaLink) => ({ url: socialMediaLink.url, title: socialMediaLink.name, })), ]; } export default function ResumePage() { const structuredData = generateResumeStructuredData(); return ( <>