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
================================================

minimalist cv
[](https://nextjs.org/)
[](https://www.typescriptlang.org/)
[](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 (
{location}
);
}
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 (
{personalWebsiteUrl && (
)}
{contact.email && (
)}
{contact.tel && (
)}
{contact.social.map((social) => (
))}
);
}
interface PrintContactProps {
contact: typeof RESUME_DATA.contact;
personalWebsiteUrl?: string;
}
function PrintContact({ contact, personalWebsiteUrl }: PrintContactProps) {
return (
);
}
/**
* 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}
{link.replace("https://", "").replace("www.", "").replace("/", "")}
>
);
}
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 (
{tags.map((tag) => (
{tag}
))}
);
}
interface ProjectCardProps {
title: string;
description: string;
tags: ProjectTags;
link?: string;
}
/**
* Card component displaying project information
*/
function ProjectCard({ title, description, tags, link }: ProjectCardProps) {
return (
);
}
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 (
{skills.map((skill) => (
{skill}
))}
);
}
interface SkillsProps {
skills: Skills;
className?: string;
}
/**
* Skills section component
* Displays a list of professional skills as badges
*/
export function Skills({ skills, className }: SkillsProps) {
return (
);
}
================================================
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 (
);
}
================================================
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 (
{badges.map((badge) => (
{badge}
))}
);
}
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.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 (
<>
{RESUME_DATA.name}'s Resume
>
);
}
================================================
FILE: src/app/sitemap.ts
================================================
import type { MetadataRoute } from "next";
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = "https://cv.jarocki.me";
return [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 1,
},
];
}
================================================
FILE: src/components/avatar.tsx
================================================
"use client";
import Image from "next/image";
import * as React from "react";
import { cn } from "@/lib/utils";
interface OptimizedAvatarProps {
src: string;
alt: string;
fallback: string;
className?: string;
}
export function Avatar({
src,
alt,
fallback,
className,
}: OptimizedAvatarProps) {
const [error, setError] = React.useState(false);
return (
{!error && src ? (
setError(true)}
priority={true}
/>
) : (
{fallback}
)}
);
}
================================================
FILE: src/components/command-menu.tsx
================================================
"use client";
import { CommandIcon } from "lucide-react";
import * as React from "react";
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/ui/command";
import { Button } from "./ui/button";
interface Props {
links: { url: string; title: string }[];
}
export const CommandMenu = ({ links }: Props) => {
const [open, setOpen] = React.useState(false);
const [isMac, setIsMac] = React.useState(false);
React.useEffect(() => {
setIsMac(window.navigator.userAgent.includes("Mac"));
const down = (e: KeyboardEvent) => {
if (e.key === "j" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((open) => !open);
}
};
document.addEventListener("keydown", down);
return () => document.removeEventListener("keydown", down);
}, []);
return (
<>
Press{" "}
{isMac ? "⌘" : "Ctrl"} +J
{" "}
to open the command menu
setOpen((open) => !open)}
variant="outline"
size="icon"
className="fixed bottom-4 right-4 flex rounded-full shadow-2xl xl:hidden print:hidden"
>
No results found.
{
setOpen(false);
window.print();
}}
>
Print
{links.map(({ url, title }) => (
{
setOpen(false);
window.open(url, "_blank");
}}
>
{title}
))}
>
);
};
================================================
FILE: src/components/error-boundary.tsx
================================================
"use client";
import { Component, type ErrorInfo, type ReactNode } from "react";
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component {
public state: State = {
hasError: false,
};
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error("Uncaught error:", error, errorInfo);
}
public render() {
if (this.state.hasError) {
return (
this.props.fallback || (
Something went wrong
We apologize for the inconvenience. Please try refreshing the
page.
this.setState({ hasError: false })}
className="rounded-md bg-primary px-4 py-2 text-primary-foreground hover:bg-primary/90"
>
Try again
)
);
}
return this.props.children;
}
}
================================================
FILE: src/components/icons/github-icon.tsx
================================================
export const GitHubIcon = (props: React.SVGProps) => {
return (
GitHub
);
};
================================================
FILE: src/components/icons/index.ts
================================================
import { GitHubIcon } from "./github-icon";
import { LinkedInIcon } from "./linkedin-icon";
export { GitHubIcon, LinkedInIcon };
================================================
FILE: src/components/icons/linkedin-icon.tsx
================================================
export const LinkedInIcon = (props: React.SVGProps) => {
return (
LinkedIn
);
};
================================================
FILE: src/components/icons/x-icon.tsx
================================================
export const XIcon = (props: React.SVGProps) => {
return (
X
);
};
================================================
FILE: src/components/ui/badge.tsx
================================================
import { cva, type VariantProps } from "class-variance-authority";
import type * as React from "react";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-semibold font-mono transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 text-nowrap",
{
variants: {
variant: {
default:
"border-transparent bg-primary/80 text-primary-foreground hover:bg-primary/60",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/60",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
);
export interface BadgeProps
extends React.HTMLAttributes,
VariantProps {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
);
}
export { Badge, badgeVariants };
================================================
FILE: src/components/ui/button.tsx
================================================
"use client";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes,
VariantProps {
asChild?: boolean;
}
const Button = React.forwardRef(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
);
}
);
Button.displayName = "Button";
export { Button, buttonVariants };
================================================
FILE: src/components/ui/card.tsx
================================================
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
));
CardFooter.displayName = "CardFooter";
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};
================================================
FILE: src/components/ui/command.tsx
================================================
"use client";
import type { DialogProps } from "@radix-ui/react-dialog";
import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
import * as React from "react";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
const Command = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
Command.displayName = CommandPrimitive.displayName;
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
{children}
);
};
const CommandInput = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>((props, ref) => (
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes) => {
return (
);
};
CommandShortcut.displayName = "CommandShortcut";
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};
================================================
FILE: src/components/ui/dialog.tsx
================================================
"use client";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
Close
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes) => (
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes) => (
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};
================================================
FILE: src/components/ui/section.tsx
================================================
import type React from "react";
import { cn } from "@/lib/utils";
export interface SectionProps extends React.HTMLAttributes {}
export function Section({ className, ...props }: SectionProps) {
return (
);
}
================================================
FILE: src/data/resume-data.ts
================================================
import type { ResumeData } from "@/lib/types";
export const RESUME_DATA: ResumeData = {
name: "Bartosz Jarocki",
initials: "BJ",
location: "Wrocław, Poland, CET",
locationLink: "https://www.google.com/maps/place/Wrocław",
about: "Full Stack Engineer building products from the ground up.",
summary:
"Full Stack Engineer with 15+ years of experience and 10+ years working remotely. Building high-performance web applications, leading distributed teams, and creating open source tools used by thousands of developers.",
avatarUrl: "https://avatars.githubusercontent.com/u/1017620?v=4",
personalWebsiteUrl: "https://jarocki.me",
contact: {
email: "bartosz.jarocki@hey.com",
tel: "+48530213401",
social: [
{
name: "GitHub",
url: "https://github.com/BartoszJarocki",
icon: "github",
},
{
name: "LinkedIn",
url: "https://www.linkedin.com/in/bjarocki/",
icon: "linkedin",
},
{
name: "X",
url: "https://x.com/BartoszJarocki",
icon: "x",
},
],
},
education: [
{
school: "Wrocław University of Technology",
degree: "Bachelor's Degree in Control systems engineering and Robotics",
start: "2007",
end: "2010",
},
],
work: [
{
company: "Motion",
link: "https://motionapp.com/",
badges: ["Remote", "AI", "React", "Next.js", "TypeScript", "AdonisJS"],
title: "Senior Software Engineer",
start: "2025",
end: null,
description:
"Building an internal AI agents platform that enables marketing teams to create and manage AI-powered workflows.",
},
{
company: "Film.io",
link: "https://film.io",
badges: ["Remote", "React", "Next.js", "TypeScript", "Node.js"],
title: "Software Architect",
start: "2024",
end: "2025",
description:
"Led technical architecture of a blockchain-based film funding platform.",
highlights: [
"Architected migration from CRA to Next.js for improved performance, SEO, and DX",
"Established release process enabling faster deployments and reliable rollbacks",
"Implemented system-wide monitoring and security improvements",
],
},
{
company: "Parabol",
link: "https://parabol.co",
badges: [
"Remote",
"React",
"TypeScript",
"Node.js",
"GraphQL",
"Tailwind CSS",
],
title: "Senior Full Stack Developer",
start: "2021",
end: "2024",
description:
"Led a product squad building an enterprise agile meeting platform.",
highlights: [
"Built design system with Tailwind CSS, improving development speed and time to market",
"Implemented engineering practices: PR automation, code review guidelines, and workflows",
"Open source contributions to Relay DevTools and React i18n tooling",
],
},
{
company: "Clevertech",
link: "https://clevertech.biz",
badges: ["Remote", "React", "TypeScript", "Node.js", "Android", "Kotlin"],
title: "Lead Android Developer → Full Stack Developer",
start: "2015",
end: "2021",
description:
"Transitioned from mobile to full-stack development while leading distributed teams across multiple client projects.",
highlights: [
"Led frontend team at Evercast, building real-time platform supporting 30+ users per room with HD streaming and collaboration tools",
"Developed offline-first Android app for DKMS, improving donor registration process",
"Led development teams across multiple successful client projects",
],
},
{
company: "Jojo Mobile",
link: "https://bsgroup.eu/",
badges: ["On Site", "Android", "Java", "Kotlin"],
title: "Android Developer → Lead Android Developer",
start: "2012",
end: "2015",
description:
"First Android developer, grew and led a team of 15+ engineers. Established mobile engineering culture and delivery processes.",
highlights: [
"Developed apps for major Polish companies including LOT, Polskie Radio, and Agora",
"Built and mentored high-performing mobile development team",
],
},
{
company: "Nokia Siemens Networks",
link: "https://www.nokia.com",
badges: ["On Site", "C/C++", "LTE", "Agile"],
title: "C/C++ Developer",
start: "2010",
end: "2012",
description:
"Developed software for LTE base stations at enterprise scale. Built strong foundations in software architecture, testing practices, and cross-team collaboration.",
},
],
skills: [
"React/Next.js",
"TypeScript",
"Node.js",
"AI/LLMs",
"Tailwind CSS",
"Design Systems",
"WebRTC",
"WebSockets",
"GraphQL",
"System Architecture",
"Remote Team Leadership",
],
projects: [
{
title: "Monito",
techStack: ["TypeScript", "Next.js", "AI", "Browser Extension"],
description: "Autonomous QA AI agent for web applications",
link: {
label: "monito.dev",
href: "https://monito.dev/",
},
},
{
title: "43frames",
techStack: ["TypeScript", "Next.js", "AI"],
description: "AI-powered image and video generation studio",
link: {
label: "43frames.com",
href: "https://43frames.com/",
},
},
{
title: "Minimalist CV",
techStack: ["TypeScript", "Next.js", "Tailwind CSS"],
description:
"Open source, print-friendly CV template. 9,600+ stars on GitHub",
link: {
label: "Minimalist CV",
href: "https://github.com/BartoszJarocki/cv",
},
},
],
} as const;
================================================
FILE: src/lib/structured-data.ts
================================================
import { RESUME_DATA } from "@/data/resume-data";
export function generatePersonStructuredData() {
return {
"@context": "https://schema.org",
"@type": "Person",
name: RESUME_DATA.name,
alternateName: RESUME_DATA.initials,
description: RESUME_DATA.about,
url: RESUME_DATA.personalWebsiteUrl,
image: RESUME_DATA.avatarUrl,
sameAs: RESUME_DATA.contact.social.map((social) => social.url),
address: {
"@type": "Place",
name: RESUME_DATA.location,
},
contactPoint: {
"@type": "ContactPoint",
email: RESUME_DATA.contact.email,
telephone: RESUME_DATA.contact.tel,
contactType: "personal",
},
jobTitle: "Full Stack Engineer",
worksFor:
RESUME_DATA.work.length > 0
? {
"@type": "Organization",
name: RESUME_DATA.work[0].company,
url: RESUME_DATA.work[0].link,
}
: undefined,
alumniOf: RESUME_DATA.education.map((edu) => ({
"@type": "EducationalOrganization",
name: edu.school,
})),
hasOccupation: RESUME_DATA.work.map((job) => ({
"@type": "Occupation",
name: job.title,
occupationLocation: {
"@type": "Place",
name: RESUME_DATA.location,
},
occupationalCategory: "Software Engineering",
estimatedSalary: {
"@type": "MonetaryAmountDistribution",
name: "Professional software engineer",
},
})),
knowsAbout: RESUME_DATA.skills,
};
}
export function generateWebPageStructuredData() {
return {
"@context": "https://schema.org",
"@type": "WebPage",
name: `${RESUME_DATA.name} - Resume`,
description: RESUME_DATA.about,
url: "https://cv.jarocki.me",
inLanguage: "en-US",
isPartOf: {
"@type": "WebSite",
name: `${RESUME_DATA.name}'s Professional Resume`,
url: "https://cv.jarocki.me",
},
about: {
"@type": "Person",
name: RESUME_DATA.name,
},
mainEntity: generatePersonStructuredData(),
};
}
export function generateResumeStructuredData() {
const person = generatePersonStructuredData();
return {
"@context": "https://schema.org",
"@type": "ProfilePage",
dateCreated: new Date().toISOString(),
dateModified: new Date().toISOString(),
mainEntity: person,
about: person,
name: `${RESUME_DATA.name} - Professional Resume`,
description: `Professional resume and portfolio of ${RESUME_DATA.name}, ${RESUME_DATA.about}`,
url: "https://cv.jarocki.me",
};
}
================================================
FILE: src/lib/types.ts
================================================
import type { StaticImageData } from "next/image";
export type ResumeIcon =
| React.ComponentType>
| StaticImageData;
export type IconType = "github" | "linkedin" | "x" | "globe" | "mail" | "phone";
export interface ResumeData {
name: string;
initials: string;
location: string;
locationLink: string;
about: string;
summary: string;
avatarUrl: string;
personalWebsiteUrl: string;
contact: {
email: string;
tel: string;
social: Array<{
name: string;
url: string;
icon: IconType;
}>;
};
education: Array<{
school: string;
degree: string;
start: string;
end: string;
}>;
work: Array<{
company: string;
link: string;
badges: string[];
title: string;
start: string;
end: string | null;
description: string;
highlights?: readonly string[];
}>;
skills: string[];
projects: Array<{
title: string;
techStack: string[];
description: string;
link?: {
label: string;
href: string;
};
}>;
}
================================================
FILE: src/lib/utils.ts
================================================
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
================================================
FILE: tailwind.config.js
================================================
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
fontFamily: {
sans: ["var(--font-geist-sans)", "system-ui", "sans-serif"],
mono: ["var(--font-geist-mono)", "ui-monospace", "monospace"],
},
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
plugins: [require("tailwindcss-animate")],
}
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "es2021",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"src/images/logos/*.*",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}