diff --git a/.github/workflows/check-code-quality.yml b/.github/workflows/check-code-quality.yml
index 5f35be4..74f4dbe 100644
--- a/.github/workflows/check-code-quality.yml
+++ b/.github/workflows/check-code-quality.yml
@@ -34,3 +34,17 @@ jobs:
run: npm ci
- name: Run TypeScript types check
run: npm run ts:check
+
+ astro:
+ name: Run Astro check
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
+ with:
+ node-version: 16
+ cache: npm
+ - name: Install dependencies
+ run: npm ci
+ - name: Run Astro check
+ run: npm run astro:check
diff --git a/astro.config.ts b/astro.config.ts
index c94120b..f1785cb 100644
--- a/astro.config.ts
+++ b/astro.config.ts
@@ -1,8 +1,8 @@
import image from '@astrojs/image';
import tailwind from '@astrojs/tailwind';
+import compress from 'astro-compress';
import { defineConfig } from 'astro/config';
import { visualizer } from 'rollup-plugin-visualizer';
-import compress from 'astro-compress';
// https://astro.build/config
export default defineConfig({
diff --git a/package-lock.json b/package-lock.json
index 3ec2a97..1f2ef54 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,7 +19,9 @@
"astro": "1.9.2",
"astro-compress": "1.1.27",
"concurrently": "7.6.0",
+ "date-fns": "2.29.3",
"iconify-icon-names": "1.1.0",
+ "immer": "9.0.18",
"locales-ts": "1.0.0",
"postcss": "8.4.21",
"prettier": "2.8.2",
@@ -29,6 +31,7 @@
"puppeteer-report": "3.1.0",
"rollup-plugin-visualizer": "5.9.0",
"tailwindcss": "3.2.4",
+ "type-fest": "3.5.2",
"typescript": "4.9.4"
},
"engines": {
@@ -3976,6 +3979,16 @@
"node": ">=14.0.0"
}
},
+ "node_modules/immer": {
+ "version": "9.0.18",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.18.tgz",
+ "integrity": "sha512-eAPNpsj7Ax1q6Y/3lm2PmlwRcFzpON7HSNQ3ru5WQH1/PSpnyed/HpNOELl2CxLKoj4r+bAHgdyKqW5gc2Se1A==",
+ "dev": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -7918,6 +7931,18 @@
"node": "*"
}
},
+ "node_modules/type-fest": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.5.2.tgz",
+ "integrity": "sha512-Ph7S4EhXzWy0sbljEuZo0tTNoLl+K2tPauGrQpcwUWrOVneLePTuhVzcuzVJJ6RU5DsNwQZka+8YtkXXU4z9cA==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/typescript": {
"version": "4.9.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz",
@@ -11971,6 +11996,12 @@
"queue": "6.0.2"
}
},
+ "immer": {
+ "version": "9.0.18",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.18.tgz",
+ "integrity": "sha512-eAPNpsj7Ax1q6Y/3lm2PmlwRcFzpON7HSNQ3ru5WQH1/PSpnyed/HpNOELl2CxLKoj4r+bAHgdyKqW5gc2Se1A==",
+ "dev": true
+ },
"import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -14725,6 +14756,12 @@
"safe-buffer": "^5.0.1"
}
},
+ "type-fest": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.5.2.tgz",
+ "integrity": "sha512-Ph7S4EhXzWy0sbljEuZo0tTNoLl+K2tPauGrQpcwUWrOVneLePTuhVzcuzVJJ6RU5DsNwQZka+8YtkXXU4z9cA==",
+ "dev": true
+ },
"typescript": {
"version": "4.9.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz",
diff --git a/package.json b/package.json
index 89b886e..50904af 100644
--- a/package.json
+++ b/package.json
@@ -13,9 +13,10 @@
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
- "generate-cv": "node scripts/generate-cv.cjs --with-clause",
+ "generate-cv": "node scripts/generate-cv.cjs",
"prettier:check": "prettier --check --ignore-path .gitignore .",
"prettier:write": "prettier --write --ignore-path .gitignore .",
+ "astro:check": "astro check",
"ts:check": "tsc --jsx preserve --skipLibCheck",
"check": "concurrently npm:*:check"
},
@@ -31,7 +32,9 @@
"astro": "1.9.2",
"astro-compress": "1.1.27",
"concurrently": "7.6.0",
+ "date-fns": "2.29.3",
"iconify-icon-names": "1.1.0",
+ "immer": "9.0.18",
"locales-ts": "1.0.0",
"postcss": "8.4.21",
"prettier": "2.8.2",
@@ -41,6 +44,7 @@
"puppeteer-report": "3.1.0",
"rollup-plugin-visualizer": "5.9.0",
"tailwindcss": "3.2.4",
+ "type-fest": "3.5.2",
"typescript": "4.9.4"
}
}
diff --git a/public/cv.pdf b/public/cv.pdf
index ac7c0ae..f50b9f2 100644
Binary files a/public/cv.pdf and b/public/cv.pdf differ
diff --git a/scripts/generate-cv.cjs b/scripts/generate-cv.cjs
index 52b4a1d..31332ad 100644
--- a/scripts/generate-cv.cjs
+++ b/scripts/generate-cv.cjs
@@ -25,10 +25,6 @@ const config = {
margin: { top: '10mm', right: '10mm', bottom: '10mm', left: '10mm' },
};
-const hasClause = process.argv.includes('--with-clause');
-
-const url = hasClause ? 'http://localhost:3000/pdf?clause' : 'http://localhost:3000/pdf';
-
const main = async () => {
const child = exec('npm run dev');
@@ -39,7 +35,7 @@ const main = async () => {
await page.setViewport({ width: 794, height: 1122, deviceScaleFactor: 2 });
await retry({
- promise: () => page.goto(url, { waitUntil: 'networkidle0' }),
+ promise: () => page.goto('http://localhost:3000/pdf', { waitUntil: 'networkidle0' }),
retries: 5,
retryTime: 1000,
});
diff --git a/src/components/divider.astro b/src/components/divider.astro
deleted file mode 100644
index cefb78b..0000000
--- a/src/components/divider.astro
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/src/components/icon-button.astro b/src/components/icon-button.astro
deleted file mode 100644
index 4775de1..0000000
--- a/src/components/icon-button.astro
+++ /dev/null
@@ -1,32 +0,0 @@
----
-import type { IconName } from '@/types/icon';
-
-import Icon from './icon.astro';
-
-type IconButtonSize = 'small' | 'large';
-
-export interface Props {
- icon: IconName;
- target?: astroHTML.JSX.AnchorHTMLAttributes['target'];
- href: string;
- size: IconButtonSize;
- 'aria-label'?: astroHTML.JSX.AnchorHTMLAttributes['aria-label'];
-}
-
-const sizeMap: Record = {
- small: 'w-7 h-7',
- large: 'w-9 h-9',
-};
-
-const { icon, href, target, size, ...rest } = Astro.props;
-
-const classes = /* tw */ {
- main: 'flex items-center justify-center rounded text-gray-400 bg-gray-100 dark:bg-gray-600 dark:text-gray-200',
- active: 'active:translate-y-px',
- focus: 'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
-};
----
-
-
-
-
diff --git a/src/components/labelled-value.astro b/src/components/labelled-value.astro
deleted file mode 100644
index c1c5fda..0000000
--- a/src/components/labelled-value.astro
+++ /dev/null
@@ -1,13 +0,0 @@
----
-export interface Props {
- label: string;
- value: string | string[];
-}
-
-const { label, value } = Astro.props;
----
-
-
- {label}:
- {value}
-
diff --git a/src/components/photo.astro b/src/components/photo.astro
deleted file mode 100644
index c471ecc..0000000
--- a/src/components/photo.astro
+++ /dev/null
@@ -1,28 +0,0 @@
----
-import { Image } from '@astrojs/image/components';
-
-import type { Photo } from '@/types/common';
-
-export interface Props {
- src: Photo;
- alt: string;
- class?: string;
- loading?: 'eager' | 'lazy';
- width?: number;
- height?: number;
-}
-
-const { src, loading, ...props } = Astro.props;
-
-const className = Astro.props.class ?? '';
-
-const isRemoteImage = typeof src === 'string';
----
-
-{
- isRemoteImage ? (
-
- ) : (
-
- )
-}
diff --git a/src/components/section-card.astro b/src/components/section-card.astro
deleted file mode 100644
index 698904c..0000000
--- a/src/components/section-card.astro
+++ /dev/null
@@ -1,47 +0,0 @@
----
-import type { SectionKey } from '@/types/data';
-
-import Typography from './typography.astro';
-
-export interface Props {
- section: SectionKey;
- title?: string;
- className?: string;
-}
-
-const { section, title, className } = Astro.props;
----
-
-
- {
- title && (
-
- {title}
-
- )
- }
-
-
-
-
diff --git a/src/components/tag.astro b/src/components/tag.astro
deleted file mode 100644
index 66bfbdd..0000000
--- a/src/components/tag.astro
+++ /dev/null
@@ -1,19 +0,0 @@
----
-import type { IconName } from '@/types/icon';
-
-import Icon from './icon.astro';
-
-export interface Props {
- name?: IconName;
- color?: string;
-}
-
-const { name, color } = Astro.props;
----
-
-
- {name && }
-
-
diff --git a/src/components/tags-list.astro b/src/components/tags-list.astro
deleted file mode 100644
index bccf614..0000000
--- a/src/components/tags-list.astro
+++ /dev/null
@@ -1,29 +0,0 @@
----
-import type { Tag } from '@/types/common';
-
-import TagComponent from './tag.astro';
-
-export interface Props {
- tags: Tag[];
-}
-
-const { tags } = Astro.props;
----
-
-
- {
- tags.map(({ name: tagName, icon, iconColor, url }) =>
- url ? (
-
-
- {tagName}
-
-
- ) : (
-
- {tagName}
-
- )
- )
- }
-
diff --git a/src/components/timestamp.astro b/src/components/timestamp.astro
deleted file mode 100644
index c67bc1d..0000000
--- a/src/components/timestamp.astro
+++ /dev/null
@@ -1,17 +0,0 @@
----
-import getDateFormatter from '@/utils/date-formatter';
-import Typography from './typography.astro';
-
-export interface Props {
- startDate: Date;
- endDate: Date | null;
- locale: string;
- translationForNow: string;
-}
-const { startDate, endDate, locale, translationForNow } = Astro.props;
-const getFormattedDate = getDateFormatter(locale);
----
-
-
- {getFormattedDate(startDate).concat(' - ', endDate ? getFormattedDate(endDate) : translationForNow)}
-
diff --git a/src/data/config.ts b/src/data/config.ts
new file mode 100644
index 0000000..463b18c
--- /dev/null
+++ b/src/data/config.ts
@@ -0,0 +1,25 @@
+import type { Config } from '@/types/data';
+import enUS from 'date-fns/locale/en-US/index.js';
+import type { ReadonlyDeep } from 'type-fest';
+
+const config = {
+ i18n: {
+ locale: enUS,
+ dateFormat: 'MMMM yyyy',
+ translations: {
+ now: 'now',
+ },
+ },
+ seo: {
+ title: 'Mark Freeman - Senior React Developer',
+ description:
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. In sodales ac dui at vestibulum. In condimentum metus id dui tincidunt, in blandit mi vehicula.',
+ favicon: '/favicon.svg',
+ },
+ pdf: {
+ footer:
+ 'I hereby give consent for my personal data included in my application to be processed for the purposes of the recruitment process.',
+ },
+} as const satisfies ReadonlyDeep;
+
+export default config;
diff --git a/src/data/cv.ts b/src/data/cv.ts
new file mode 100644
index 0000000..997a3db
--- /dev/null
+++ b/src/data/cv.ts
@@ -0,0 +1,11 @@
+import type { ReadonlyDeep } from 'type-fest';
+import type { Data } from '@/types/data';
+import transformData from './transformers/transform-data';
+import configData from './config';
+import sectionsData from './sections';
+
+const data = { config: configData, sections: sectionsData } as const satisfies ReadonlyDeep;
+
+export type PreciseData = typeof data;
+
+export const cv = transformData(data);
diff --git a/src/data/helpers/links.ts b/src/data/helpers/links.ts
new file mode 100644
index 0000000..2b2380e
--- /dev/null
+++ b/src/data/helpers/links.ts
@@ -0,0 +1,146 @@
+// GENERAL
+
+import createLinkFactory from '@/utils/create-link-factory';
+
+export const facebook = createLinkFactory({
+ name: 'Facebook',
+ icon: 'fa6-brands:facebook-f',
+});
+
+export const linkedin = createLinkFactory({
+ name: 'LinkedIn',
+ icon: 'fa6-brands:linkedin-in',
+});
+
+export const twitter = createLinkFactory({
+ name: 'Twitter',
+ icon: 'fa6-brands:twitter',
+});
+
+export const pinterest = createLinkFactory({
+ name: 'Pinterest',
+ icon: 'fa6-brands:pinterest',
+});
+
+// CODE
+
+export const github = createLinkFactory({
+ name: 'GitHub',
+ icon: 'fa6-brands:github',
+});
+
+export const codepen = createLinkFactory({
+ name: 'CodePen',
+ icon: 'fa6-brands:codepen',
+});
+
+export const stackblitz = createLinkFactory({
+ name: 'StackBlitz',
+ icon: 'simple-icons:stackblitz',
+});
+
+export const codesandbox = createLinkFactory({
+ name: 'CodeSandbox',
+ icon: 'simple-icons:codesandbox',
+});
+
+// BLOG
+
+export const dev = createLinkFactory({
+ name: 'Dev',
+ icon: 'fa6-brands:dev',
+});
+
+export const medium = createLinkFactory({
+ name: 'Medium',
+ icon: 'fa6-brands:medium',
+});
+
+// FORUM / CHAT
+
+export const reddit = createLinkFactory({
+ name: 'Reddit',
+ icon: 'fa6-brands:reddit',
+});
+
+export const quora = createLinkFactory({
+ name: 'Quora',
+ icon: 'fa6-brands:quora',
+});
+
+export const stackoverflow = createLinkFactory({
+ name: 'Stack Overflow',
+ icon: 'fa6-brands:stack-overflow',
+});
+
+// DESIGN
+
+export const instagram = createLinkFactory({
+ name: 'Instagram',
+ icon: 'fa6-brands:instagram',
+});
+export const behance = createLinkFactory({
+ name: 'Behance',
+ icon: 'fa6-brands:behance',
+});
+
+export const dribbble = createLinkFactory({
+ name: 'Dribbble',
+ icon: 'fa6-brands:dribbble',
+});
+
+export const figma = createLinkFactory({
+ name: 'Figma',
+ icon: 'fa6-brands:figma',
+});
+
+// MUSIC
+
+export const spotify = createLinkFactory({
+ name: 'Spotify',
+ icon: 'fa6-brands:spotify',
+});
+
+export const soundcloud = createLinkFactory({
+ name: 'SoundCloud',
+ icon: 'fa6-brands:soundcloud',
+});
+
+// VIDEO
+
+export const youtube = createLinkFactory({
+ name: 'YouTube',
+ icon: 'fa6-brands:youtube',
+});
+
+export const twitch = createLinkFactory({
+ name: 'Twitch',
+ icon: 'fa6-brands:twitch',
+});
+
+export const vimeo = createLinkFactory({
+ name: 'Vimeo',
+ icon: 'fa6-brands:vimeo',
+});
+
+// PROJECT TYPE
+
+export const website = createLinkFactory({
+ name: 'Website',
+ icon: 'fa6-solid:globe',
+});
+
+export const demo = createLinkFactory({
+ name: 'App demo',
+ icon: 'fa6-solid:desktop',
+});
+
+export const mockups = createLinkFactory({
+ name: 'Mockups',
+ icon: 'fa6-solid:image',
+});
+
+export const repository = createLinkFactory({
+ name: 'Repository',
+ icon: 'fa6-solid:code-branch',
+});
diff --git a/src/data/skills.ts b/src/data/helpers/skills.ts
similarity index 66%
rename from src/data/skills.ts
rename to src/data/helpers/skills.ts
index 3294c72..26c3126 100644
--- a/src/data/skills.ts
+++ b/src/data/helpers/skills.ts
@@ -1,159 +1,146 @@
-import type { LevelledSkill } from '@/types/skills-section';
+import createSkillFactory from '@/utils/create-skill-factory';
-import type { Tag } from '../types/common';
-
-interface SkillFactory {
- (data: Partial & { level: LevelledSkill['level'] }): LevelledSkill;
- (data?: Partial): Tag;
-}
-
-const createSkill = (defaultData: Omit) =>
- ((data?: Partial & { level?: LevelledSkill['level'] }): Tag | LevelledSkill => ({
- ...defaultData,
- ...data,
- })) as SkillFactory;
-
-export const apolloGraphql = createSkill({
+export const apolloGraphql = createSkillFactory({
name: 'Apollo GraphQL',
icon: 'simple-icons:apollographql',
iconColor: '#311C87',
url: 'https://www.apollographql.com/',
});
-export const astro = createSkill({
+export const astro = createSkillFactory({
name: 'Astro',
icon: 'simple-icons:astro',
iconColor: '#FF5D01',
url: 'https://astro.build/',
});
-export const chakraUi = createSkill({
+export const chakraUi = createSkillFactory({
name: 'Chakra UI',
icon: 'simple-icons:chakraui',
iconColor: '#319795',
url: 'https://chakra-ui.com/',
});
-export const cypress = createSkill({
+export const cypress = createSkillFactory({
name: 'Cypress',
icon: 'simple-icons:cypress',
iconColor: '#17202C',
url: 'https://www.cypress.io/',
});
-export const eslint = createSkill({
+export const eslint = createSkillFactory({
name: 'ESLint',
icon: 'simple-icons:eslint',
iconColor: '#4B32C3',
url: 'https://eslint.org/',
});
-export const firebase = createSkill({
+export const firebase = createSkillFactory({
name: 'Firebase',
icon: 'simple-icons:firebase',
iconColor: '#FFCA28',
url: 'https://firebase.google.com/',
});
-export const jest = createSkill({
+export const jest = createSkillFactory({
name: 'Jest',
icon: 'simple-icons:jest',
iconColor: '#C21325',
url: 'https://jestjs.io/',
});
-export const mongoDb = createSkill({
+export const mongoDb = createSkillFactory({
name: 'MongoDB',
icon: 'simple-icons:mongodb',
iconColor: '#47A248',
url: 'https://www.mongodb.com/',
});
-export const nestJs = createSkill({
+export const nestJs = createSkillFactory({
name: 'NestJS',
icon: 'simple-icons:nestjs',
iconColor: '#E0234E',
url: 'https://nestjs.com/',
});
-export const nextJs = createSkill({
+export const nextJs = createSkillFactory({
name: 'Next.js',
icon: 'simple-icons:nextdotjs',
iconColor: '#000000',
url: 'https://nextjs.org/',
});
-export const nx = createSkill({
+export const nx = createSkillFactory({
name: 'Nx',
icon: 'simple-icons:nx',
iconColor: '#143055',
url: 'https://nx.dev/',
});
-export const pnpm = createSkill({
+export const pnpm = createSkillFactory({
name: 'pnpm',
icon: 'simple-icons:pnpm',
iconColor: '#F69220',
url: 'https://pnpm.io/',
});
-export const postgreSql = createSkill({
+export const postgreSql = createSkillFactory({
name: 'PostgreSQL',
icon: 'simple-icons:postgresql',
iconColor: '#4169E1',
url: 'https://www.postgresql.org/',
});
-export const prettier = createSkill({
+export const prettier = createSkillFactory({
name: 'Prettier',
icon: 'simple-icons:prettier',
iconColor: '#F7B93E',
url: 'https://prettier.io/',
});
-export const react = createSkill({
+export const react = createSkillFactory({
name: 'React.js',
icon: 'simple-icons:react',
iconColor: '#61DAFB',
url: 'https://reactjs.org/',
});
-export const reactQuery = createSkill({
+export const reactQuery = createSkillFactory({
name: 'React Query',
icon: 'simple-icons:reactquery',
iconColor: '#FF4154',
url: 'https://tanstack.com/query',
});
-export const sass = createSkill({
+export const sass = createSkillFactory({
name: 'SASS',
icon: 'simple-icons:sass',
iconColor: '#CC6699',
url: 'https://sass-lang.com/',
});
-export const supabase = createSkill({
+export const supabase = createSkillFactory({
name: 'Supabase',
icon: 'simple-icons:supabase',
iconColor: '#3ECF8E',
url: 'https://supabase.io/',
});
-export const tailwindCss = createSkill({
+export const tailwindCss = createSkillFactory({
name: 'Tailwind CSS',
icon: 'simple-icons:tailwindcss',
iconColor: '#06B6D4',
url: 'https://tailwindcss.com/',
});
-export const typescript = createSkill({
+export const typescript = createSkillFactory({
name: 'TypeScript',
icon: 'simple-icons:typescript',
iconColor: '#3178C6',
url: 'https://www.typescriptlang.org/',
});
-export const vue = createSkill({
+export const vue = createSkillFactory({
name: 'Vue.js',
icon: 'simple-icons:vuedotjs',
iconColor: '#4FC08D',
diff --git a/src/data/index.ts b/src/data/index.ts
deleted file mode 100644
index 2cb21b7..0000000
--- a/src/data/index.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import type { EducationSection } from '@/types/education-section';
-import type { ExperienceSection } from '@/types/experience-section';
-import type { FavoritesSection } from '@/types/favorites-section';
-import type { Pdf } from '@/types/pdf';
-import type { I18n } from '@/types/i18n';
-import type { MainSection } from '@/types/main-section';
-import type { PortfolioSection } from '@/types/portfolio-section';
-import type { Seo } from '@/types/seo';
-import type { SkillsSection } from '@/types/skills-section';
-import type { TestimonialsSection } from '@/types/testimonials-section';
-
-import educationData from './sections/education';
-import experienceData from './sections/experience';
-import favoritesData from './sections/favorites';
-import mainData from './sections/main';
-import portfolioData from './sections/portfolio';
-import skillsData from './sections/skills';
-import testimonialsData from './sections/testimonials';
-
-export interface Data {
- i18n: I18n;
- seo: Seo;
- pdf: Pdf;
- main: MainSection;
- skills?: SkillsSection;
- experience?: ExperienceSection;
- portfolio?: PortfolioSection;
- education?: EducationSection;
- testimonials?: TestimonialsSection;
- favorites?: FavoritesSection;
-}
-
-const data: Data = {
- i18n: {
- locale: 'en-US',
- translations: {
- now: 'now',
- },
- },
- seo: {
- title: 'Mark Freeman - Senior React Developer',
- description:
- 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. In sodales ac dui at vestibulum. In condimentum metus id dui tincidunt, in blandit mi vehicula.',
- },
- pdf: {
- footer:
- 'I hereby give consent for my personal data included in my application to be processed for the purposes of the recruitment process.',
- },
- main: mainData,
- skills: skillsData,
- experience: experienceData,
- portfolio: portfolioData,
- education: educationData,
- testimonials: testimonialsData,
- favorites: favoritesData,
-};
-
-export default data;
diff --git a/src/data/sections/education-section.data.ts b/src/data/sections/education-section.data.ts
new file mode 100644
index 0000000..ca6af21
--- /dev/null
+++ b/src/data/sections/education-section.data.ts
@@ -0,0 +1,30 @@
+import type { EducationSection } from '@/types/sections/education-section.types';
+import type { ReadonlyDeep } from 'type-fest';
+import { website } from '../helpers/links';
+
+const educationSectionData = {
+ config: {
+ title: 'Education',
+ slug: 'education',
+ icon: 'fa6-solid:graduation-cap',
+ visible: true,
+ },
+ diplomas: [
+ {
+ title: 'Information Technology',
+ institution: 'Wrocław University of Science and Technology',
+ dates: [new Date('2014.10'), new Date('2016.07')],
+ description: 'Master degree. Specialization in software development.',
+ links: [website({ url: '#' })],
+ },
+ {
+ title: 'Information Technology',
+ institution: 'Wrocław University of Science and Technology',
+ dates: [new Date('2011.10'), new Date('2014.07')],
+ description: "Bachelor's degree. Specialization in application development.",
+ links: [website({ url: '#' })],
+ },
+ ],
+} as const satisfies ReadonlyDeep;
+
+export default educationSectionData;
diff --git a/src/data/sections/education.ts b/src/data/sections/education.ts
deleted file mode 100644
index 33bceec..0000000
--- a/src/data/sections/education.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import type { EducationSection } from '@/types/education-section';
-
-import { website } from '../socials';
-
-const educationData: EducationSection = {
- config: {
- title: 'Education',
- icon: 'fa6-solid:graduation-cap',
- },
- educationItems: [
- {
- title: 'Information Technology',
- institution: 'Wrocław University of Science and Technology',
- startDate: new Date('2014.10'),
- endDate: new Date('2016.07'),
- description: 'Master degree. Specialization in software development.',
- socials: [website('#')],
- },
- {
- title: 'Information Technology',
- institution: 'Wrocław University of Science and Technology',
- startDate: new Date('2011.10'),
- endDate: new Date('2014.07'),
- description: "Bachelor's degree. Specialization in application development.",
- socials: [website('#')],
- },
- ],
-};
-
-export default educationData;
diff --git a/src/data/sections/experience.ts b/src/data/sections/experience-section.data.ts
similarity index 62%
rename from src/data/sections/experience.ts
rename to src/data/sections/experience-section.data.ts
index 079fcb2..46d2f39 100644
--- a/src/data/sections/experience.ts
+++ b/src/data/sections/experience-section.data.ts
@@ -1,5 +1,6 @@
-import type { ExperienceSection } from '@/types/experience-section';
-
+import type { ExperienceSection } from '@/types/sections/experience-section.types';
+import type { ReadonlyDeep } from 'type-fest';
+import { facebook, github, instagram, linkedin, twitter, website } from '../helpers/links';
import {
chakraUi,
eslint,
@@ -12,20 +13,20 @@ import {
tailwindCss,
typescript,
vue,
-} from '../skills';
-import { facebook, github, instagram, linkedin, twitter, website } from '../socials';
+} from '../helpers/skills';
-const experienceData: ExperienceSection = {
+const experienceSectionData = {
config: {
title: 'Work experience',
+ slug: 'experience',
icon: 'fa6-solid:suitcase',
+ visible: true,
},
jobs: [
{
role: 'Senior front-end developer',
company: 'Google',
- startDate: new Date('2020-02'),
- endDate: null,
+ dates: [new Date('2020-02'), null],
description: [
'In tristique vulputate augue vel egestas.',
'Quisque ac imperdiet tortor, at lacinia ex.',
@@ -34,38 +35,45 @@ const experienceData: ExperienceSection = {
'Nunc malesuada leo et est iaculis facilisis.',
'Fusce eu urna ut magna malesuada fringilla.',
],
- tags: [react(), nextJs(), typescript(), nx(), firebase()],
- socials: [facebook('#'), linkedin('#')],
+ tagsList: {
+ title: 'Technologies',
+ tags: [react(), nextJs(), typescript(), nx(), firebase()],
+ },
+ links: [facebook({ url: '#' }), linkedin({ url: '#' })],
},
{
role: 'React.js developer',
company: 'Facebook',
- startDate: new Date('2019-04'),
- endDate: new Date('2020-02'),
+ dates: [new Date('2019-04'), new Date('2020-02')],
description: [
'Aenean eget ultricies felis. Pellentesque dictum massa ut tellus eleifend, sed posuere massa mattis.',
'Ut posuere massa lacus, eleifend molestie tortor auctor vel.',
'Sed sed sollicitudin eros, id ultricies mi. Aliquam sodales elit vel ante tempor, non vehicula nibh facilisis.',
'Cras feugiat ultricies maximus. Aliquam tristique ex odio, ac semper urna accumsan a.',
],
- tags: [react(), reactQuery(), chakraUi(), eslint()],
- socials: [website('#'), instagram('#')],
+ tagsList: {
+ title: 'Technologies',
+ tags: [react(), reactQuery(), chakraUi(), eslint()],
+ },
+ links: [website({ url: '#' }), instagram({ url: '#' })],
},
{
role: 'Junior front-end developer',
company: 'GitLab',
- startDate: new Date('2016-09'),
- endDate: new Date('2019-04'),
+ dates: [new Date('2016-09'), new Date('2019-04')],
description: [
'Nulla volutpat justo ante, rhoncus posuere massa egestas in.',
'Quisque pellentesque, dolor nec sollicitudin iaculis, sem velit consequat ligula, eget tempus ligula leo et est.',
'Maecenas ut elit sit amet nibh maximus condimentum in nec lorem. Pellentesque tincidunt odio vel leo suscipit, in interdum mi gravida.',
'Donec non vulputate augue.',
],
- tags: [vue(), tailwindCss(), pnpm()],
- socials: [twitter('#'), github('#')],
+ tagsList: {
+ title: 'Technologies',
+ tags: [vue(), tailwindCss(), pnpm()],
+ },
+ links: [twitter({ url: '#' }), github({ url: '#' })],
},
],
-};
+} as const satisfies ReadonlyDeep;
-export default experienceData;
+export default experienceSectionData;
diff --git a/src/data/sections/favorites.ts b/src/data/sections/favorites-section.data.ts
similarity index 83%
rename from src/data/sections/favorites.ts
rename to src/data/sections/favorites-section.data.ts
index bcb6a6c..503869f 100644
--- a/src/data/sections/favorites.ts
+++ b/src/data/sections/favorites-section.data.ts
@@ -1,33 +1,36 @@
-import type { FavoritesSection } from '@/types/favorites-section';
+import type { FavoritesSection } from '@/types/sections/favorites-section.types';
+import type { ReadonlyDeep } from 'type-fest';
-const favoritesData: FavoritesSection = {
+const favoritesSectionData = {
config: {
title: 'My favorites',
+ slug: 'favorites',
icon: 'fa6-solid:star',
+ visible: true,
},
books: {
title: 'Books I read',
data: [
{
- cover: import('@/assets/favorites/books/book-1.jpeg'),
+ image: import('@/assets/favorites/books/book-1.jpeg'),
title: 'The Pragmatic Programmer: From Journeyman to Master',
author: 'Andy Hunt, Dave Thomas',
url: 'https://www.goodreads.com/book/show/4099.The_Pragmatic_Programmer',
},
{
- cover: 'https://m.media-amazon.com/images/I/61aFldsgAmL._SY344_BO1,204,203,200_QL70_ML2_.jpg',
+ image: 'https://m.media-amazon.com/images/I/61aFldsgAmL._SY344_BO1,204,203,200_QL70_ML2_.jpg',
title: 'Domain-Driven Design: Tackling Complexity in the Heart of Software',
author: 'Eric Evans',
url: 'https://www.goodreads.com/book/show/179133.Domain_Driven_Design',
},
{
- cover: import('@/assets/favorites/books/book-3.jpeg'),
+ image: import('@/assets/favorites/books/book-3.jpeg'),
title: 'Clean Code: A Handbook of Agile Software Craftsmanship',
author: 'Robert C. Martin',
url: 'https://www.goodreads.com/book/show/3735293-clean-code',
},
{
- cover: import('@/assets/favorites/books/book-4.jpeg'),
+ image: import('@/assets/favorites/books/book-4.jpeg'),
title: 'The Clean Coder: A Code of Conduct for Professional Programmers',
author: 'Robert C. Martin',
url: 'https://www.goodreads.com/book/show/10284614-the-clean-coder',
@@ -73,17 +76,17 @@ const favoritesData: FavoritesSection = {
title: 'Videos I watched',
data: [
{
- thumbnail: import('@/assets/favorites/videos/video-1.jpeg'),
+ image: import('@/assets/favorites/videos/video-1.jpeg'),
title: 'Building Resilient Frontend Architecture • Monica Lent • GOTO 2019',
url: 'https://youtu.be/TqfbAXCCVwE',
},
{
- thumbnail: import('@/assets/favorites/videos/video-2.jpeg'),
+ image: import('@/assets/favorites/videos/video-2.jpeg'),
title: 'Scaling Yourself • Scott Hanselman • GOTO 2012',
url: 'https://youtu.be/FS1mnISoG7U',
},
{
- thumbnail: import('@/assets/favorites/videos/video-3.jpeg'),
+ image: import('@/assets/favorites/videos/video-3.jpeg'),
title: "Why Isn't Functional Programming the Norm? - Richard Feldman",
url: 'https://youtu.be/QyJZzq0v7Z4',
},
@@ -130,6 +133,6 @@ const favoritesData: FavoritesSection = {
},
],
},
-};
+} as const satisfies ReadonlyDeep;
-export default favoritesData;
+export default favoritesSectionData;
diff --git a/src/data/sections/index.ts b/src/data/sections/index.ts
new file mode 100644
index 0000000..7b2e539
--- /dev/null
+++ b/src/data/sections/index.ts
@@ -0,0 +1,21 @@
+import type { Sections } from '@/types/data';
+import type { ReadonlyDeep } from 'type-fest';
+import educationData from './education-section.data';
+import experienceData from './experience-section.data';
+import favoritesData from './favorites-section.data';
+import mainData from './main-section.data';
+import portfolioData from './portfolio-section.data';
+import skillsData from './skills-section.data';
+import testimonialsData from './testimonials-section.data';
+
+export const sections = {
+ main: mainData,
+ skills: skillsData,
+ experience: experienceData,
+ portfolio: portfolioData,
+ education: educationData,
+ testimonials: testimonialsData,
+ favorites: favoritesData,
+} as const satisfies ReadonlyDeep;
+
+export default sections;
diff --git a/src/data/sections/main.ts b/src/data/sections/main-section.data.ts
similarity index 75%
rename from src/data/sections/main.ts
rename to src/data/sections/main-section.data.ts
index adf2f41..f5097ea 100644
--- a/src/data/sections/main.ts
+++ b/src/data/sections/main-section.data.ts
@@ -1,11 +1,13 @@
-import type { MainSection } from '@/types/main-section';
+import type { MainSection } from '@/types/sections/main-section.types';
+import type { ReadonlyDeep } from 'type-fest';
+import { facebook, github, linkedin, twitter } from '../helpers/links';
-import { facebook, github, linkedin, twitter } from '../socials';
-
-const mainData: MainSection = {
+const mainSectionData = {
config: {
icon: 'fa6-solid:user',
title: 'Profile',
+ slug: 'profile',
+ visible: true,
},
image: import('@/assets/my-image.jpeg'),
fullName: 'Mark Freeman',
@@ -29,8 +31,9 @@ const mainData: MainSection = {
action: {
label: 'Download CV',
url: '/cv.pdf',
+ downloadedFileName: 'CV-Mark_Freeman.pdf',
},
- socials: [facebook('#'), github('#'), linkedin('#'), twitter('#')],
-};
+ links: [facebook({ url: '#' }), github({ url: '#' }), linkedin({ url: '#' }), twitter({ url: '#' })],
+} as const satisfies ReadonlyDeep;
-export default mainData;
+export default mainSectionData;
diff --git a/src/data/sections/portfolio.ts b/src/data/sections/portfolio-section.data.ts
similarity index 74%
rename from src/data/sections/portfolio.ts
rename to src/data/sections/portfolio-section.data.ts
index f098878..ab25217 100644
--- a/src/data/sections/portfolio.ts
+++ b/src/data/sections/portfolio-section.data.ts
@@ -1,5 +1,6 @@
-import type { PortfolioSection } from '@/types/portfolio-section';
-
+import type { PortfolioSection } from '@/types/sections/portfolio-section.types';
+import type { ReadonlyDeep } from 'type-fest';
+import { demo, github, mockups, website } from '../helpers/links';
import {
chakraUi,
eslint,
@@ -15,20 +16,20 @@ import {
sass,
tailwindCss,
typescript,
-} from '../skills';
-import { demo, github, mockups, website } from '../socials';
+} from '../helpers/skills';
-const portfolioData: PortfolioSection = {
+const portfolioSectionData = {
config: {
title: 'Projects',
+ slug: 'projects',
icon: 'fa6-solid:rocket',
+ visible: true,
},
projects: [
{
name: 'Golden Bulls',
image: import('@/assets/portfolio/project-1.jpeg'),
- startDate: new Date('2020-03'),
- endDate: null,
+ dates: [new Date('2020-03'), null],
details: [
{ label: 'Team size', value: '1 person' },
{ label: 'My role', value: ['Front-end Developer', 'Designer'] },
@@ -41,14 +42,16 @@ const portfolioData: PortfolioSection = {
],
description:
'In tristique vulputate augue vel egestas. Quisque ac imperdiet tortor, at lacinia ex. Duis vel ex hendrerit, commodo odio sed, aliquam enim. Ut arcu nulla, tincidunt eget arcu eget, molestie vulputate nisi. Nunc malesuada leo et est iaculis facilisis.',
- tags: [nextJs(), sass(), pnpm(), eslint(), prettier()],
- socials: [mockups('#'), demo('#')],
+ tagsList: {
+ title: 'Technologies',
+ tags: [nextJs(), sass(), pnpm(), eslint(), prettier()],
+ },
+ links: [mockups({ url: '#' }), demo({ url: '#' })],
},
{
name: 'TruQuest',
image: import('@/assets/portfolio/project-2.jpeg'),
- startDate: new Date('2019-06'),
- endDate: new Date('2020-02'),
+ dates: [new Date('2019-06'), new Date('2020-02')],
details: [
{ label: 'Team size', value: '7 people' },
{ label: 'My role', value: ['Front-end Developer', 'Mobile Developer', 'Designer'] },
@@ -61,14 +64,16 @@ const portfolioData: PortfolioSection = {
],
description:
'Ut ultricies tortor at sodales aliquam. Vivamus metus ante, fringilla nec ligula in, suscipit rhoncus mauris. Praesent hendrerit velit odio, at accumsan urna faucibus convallis. Nunc at massa eget ligula volutpat dictum a sit amet libero. Vestibulum iaculis molestie maximus. In hac habitasse platea dictumst.',
- tags: [react(), tailwindCss(), nestJs(), postgreSql()],
- socials: [mockups('#'), demo('#')],
+ tagsList: {
+ title: 'Technologies',
+ tags: [react(), tailwindCss(), nestJs(), postgreSql()],
+ },
+ links: [mockups({ url: '#' }), demo({ url: '#' })],
},
{
name: 'Software Chasers',
image: import('@/assets/portfolio/project-3.jpeg'),
- startDate: new Date('2018-01'),
- endDate: new Date('2020-12'),
+ dates: [new Date('2018-01'), new Date('2020-12')],
details: [
{ label: 'Team size', value: '3 people' },
{ label: 'My role', value: ['Front-end Developer', 'Designer'] },
@@ -81,14 +86,16 @@ const portfolioData: PortfolioSection = {
],
description:
'Quisque id consectetur eros. In hac habitasse platea dictumst. Sed eu pulvinar orci. Mauris consequat, est in dignissim varius, neque nisl commodo mauris, id blandit risus justo eu nulla.',
- tags: [react(), chakraUi(), typescript(), nx(), pnpm()],
- socials: [website('#'), github('#')],
+ tagsList: {
+ title: 'Technologies',
+ tags: [react(), chakraUi(), typescript(), nx(), pnpm()],
+ },
+ links: [website({ url: '#' }), github({ url: '#' })],
},
{
name: 'Disco Ninjas',
image: import('@/assets/portfolio/project-4.jpeg'),
- startDate: new Date('2016-05'),
- endDate: new Date('2018-07'),
+ dates: [new Date('2016-05'), new Date('2018-07')],
details: [
{ label: 'Team size', value: '11 people' },
{ label: 'My role', value: 'Front-end Developer' },
@@ -101,10 +108,13 @@ const portfolioData: PortfolioSection = {
],
description:
'Praesent eu neque tortor. Vestibulum ac magna nisl. Vivamus massa sem, feugiat in pharetra non, convallis egestas purus. Ut consequat ullamcorper sem, in euismod nibh posuere ut. ',
- tags: [typescript(), jest(), firebase()],
- socials: [mockups('#'), github('#')],
+ tagsList: {
+ title: 'Technologies',
+ tags: [typescript(), jest(), firebase()],
+ },
+ links: [mockups({ url: '#' }), github({ url: '#' })],
},
],
-};
+} as const satisfies ReadonlyDeep;
-export default portfolioData;
+export default portfolioSectionData;
diff --git a/src/data/sections/skills.ts b/src/data/sections/skills-section.data.ts
similarity index 85%
rename from src/data/sections/skills.ts
rename to src/data/sections/skills-section.data.ts
index 1fcf4aa..52289b4 100644
--- a/src/data/sections/skills.ts
+++ b/src/data/sections/skills-section.data.ts
@@ -1,5 +1,5 @@
-import type { SkillsSection } from '@/types/skills-section';
-
+import type { SkillsSection } from '@/types/sections/skills-section.types';
+import type { ReadonlyDeep } from 'type-fest';
import {
apolloGraphql,
astro,
@@ -17,17 +17,18 @@ import {
supabase,
tailwindCss,
typescript,
-} from '../skills';
+} from '../helpers/skills';
-const skillsData: SkillsSection = {
+const skillsSectionData = {
config: {
title: 'Skills',
+ slug: 'skills',
icon: 'fa6-solid:bars-progress',
+ visible: true,
},
skillSets: [
{
title: 'I already know',
- pdfTitle: 'Technologies',
skills: [
react({
level: 5,
@@ -63,12 +64,10 @@ const skillsData: SkillsSection = {
},
{
title: 'I want to learn',
- excludeFromPdf: true,
skills: [apolloGraphql(), astro(), supabase(), cypress()],
},
{
title: 'I speak',
- pdfTitle: 'Languages',
skills: [
{ icon: 'circle-flags:pl', name: 'Polish - native' },
{ icon: 'circle-flags:us', name: 'English - C1' },
@@ -76,6 +75,6 @@ const skillsData: SkillsSection = {
],
},
],
-};
+} as const satisfies ReadonlyDeep;
-export default skillsData;
+export default skillsSectionData;
diff --git a/src/data/sections/testimonials.ts b/src/data/sections/testimonials-section.data.ts
similarity index 71%
rename from src/data/sections/testimonials.ts
rename to src/data/sections/testimonials-section.data.ts
index bedbd5c..a73c6f6 100644
--- a/src/data/sections/testimonials.ts
+++ b/src/data/sections/testimonials-section.data.ts
@@ -1,11 +1,13 @@
-import type { TestimonialsSection } from '@/types/testimonials-section';
+import type { TestimonialsSection } from '@/types/sections/testimonials-section.types';
+import type { ReadonlyDeep } from 'type-fest';
+import { github, linkedin, website } from '../helpers/links';
-import { github, linkedin, website } from '../socials';
-
-const testimonialsData: TestimonialsSection = {
+const testimonialsSectionData = {
config: {
title: 'Testimonials',
+ slug: 'testimonials',
icon: 'fa6-solid:comment',
+ visible: true,
},
testimonials: [
{
@@ -14,7 +16,7 @@ const testimonialsData: TestimonialsSection = {
relation: 'We work together as front-end developers at Google',
content:
'In nec mattis sem. Morbi purus lorem, euismod ac varius at, aliquet vitae augue. Pellentesque ut facilisis felis. In sed dui blandit, aliquet odio eu, elementum leo. In facilisis dapibus tortor ac volutpat. Cras cursus nec odio maximus elementum.',
- socials: [github('#'), linkedin('#')],
+ links: [github({ url: '#' }), linkedin({ url: '#' })],
},
{
image: import('@/assets/testimonials/testimonial-2.jpeg'),
@@ -22,7 +24,7 @@ const testimonialsData: TestimonialsSection = {
relation: 'My project manager at GitLab',
content:
'Praesent nec congue elit. Vestibulum lobortis congue ipsum, a gravida mi tempus ac. Mauris aliquet purus nibh, vel varius turpis tempus non. Nullam eget ultricies orci. Quisque nulla ante, auctor eget varius ac, imperdiet nec magna.',
- socials: [linkedin('#')],
+ links: [linkedin({ url: '#' })],
},
{
image: import('@/assets/testimonials/testimonial-3.jpeg'),
@@ -30,9 +32,9 @@ const testimonialsData: TestimonialsSection = {
relation: 'My customer for sidewing.com website',
content:
'Mauris tincidunt at purus vehicula porta. Mauris eget mollis turpis. Sed iaculis rutrum pharetra. Vivamus risus quam, suscipit et semper ut, aliquet ut tellus. Donec quis auctor nunc.',
- socials: [github('#'), website('#')],
+ links: [github({ url: '#' }), website({ url: '#' })],
},
],
-};
+} as const satisfies ReadonlyDeep;
-export default testimonialsData;
+export default testimonialsSectionData;
diff --git a/src/data/socials.ts b/src/data/socials.ts
deleted file mode 100644
index a0383a5..0000000
--- a/src/data/socials.ts
+++ /dev/null
@@ -1,200 +0,0 @@
-import type { Social } from '../types/common';
-
-type SocialWithoutUrl = Omit;
-
-// GENERAL
-
-export const facebook = (url: string, override?: Partial): Social => ({
- name: 'Facebook',
- icon: 'fa6-brands:facebook-f',
- url,
- ...override,
-});
-
-export const linkedin = (url: string, override?: Partial): Social => ({
- name: 'LinkedIn',
- icon: 'fa6-brands:linkedin-in',
- url,
- ...override,
-});
-
-export const twitter = (url: string, override?: Partial): Social => ({
- name: 'Twitter',
- icon: 'fa6-brands:twitter',
- url,
- ...override,
-});
-
-export const pinterest = (url: string, override?: Partial): Social => ({
- name: 'Pinterest',
- icon: 'fa6-brands:pinterest',
- url,
- ...override,
-});
-
-// CODE
-
-export const github = (url: string, override?: Partial): Social => ({
- name: 'GitHub',
- icon: 'fa6-brands:github',
- url,
- ...override,
-});
-
-export const codepen = (url: string, override?: Partial): Social => ({
- name: 'CodePen',
- icon: 'fa6-brands:codepen',
- url,
- ...override,
-});
-
-export const stackblitz = (url: string, override?: Partial): Social => ({
- name: 'StackBlitz',
- icon: 'simple-icons:stackblitz',
- url,
- ...override,
-});
-
-export const codesandbox = (url: string, override?: Partial): Social => ({
- name: 'CodeSandbox',
- icon: 'simple-icons:codesandbox',
- url,
- ...override,
-});
-
-// BLOG
-
-export const dev = (url: string, override?: Partial): Social => ({
- name: 'Dev',
- icon: 'fa6-brands:dev',
- url,
- ...override,
-});
-
-export const medium = (url: string, override?: Partial): Social => ({
- name: 'Medium',
- icon: 'fa6-brands:medium',
- url,
- ...override,
-});
-
-// FORUM / CHAT
-
-export const reddit = (url: string, override?: Partial): Social => ({
- name: 'Reddit',
- icon: 'fa6-brands:reddit',
- url,
- ...override,
-});
-
-export const quora = (url: string, override?: Partial): Social => ({
- name: 'Quora',
- icon: 'fa6-brands:quora',
- url,
- ...override,
-});
-
-export const stackoverflow = (url: string, override?: Partial): Social => ({
- name: 'Stack Overflow',
- icon: 'fa6-brands:stack-overflow',
- url,
- ...override,
-});
-
-// DESIGN
-
-export const instagram = (url: string, override?: Partial): Social => ({
- name: 'Instagram',
- icon: 'fa6-brands:instagram',
- url,
- ...override,
-});
-export const behance = (url: string, override?: Partial): Social => ({
- name: 'Behance',
- icon: 'fa6-brands:behance',
- url,
- ...override,
-});
-
-export const dribbble = (url: string, override?: Partial): Social => ({
- name: 'Dribbble',
- icon: 'fa6-brands:dribbble',
- url,
- ...override,
-});
-
-export const figma = (url: string, override?: Partial): Social => ({
- name: 'Figma',
- icon: 'fa6-brands:figma',
- url,
- ...override,
-});
-
-// MUSIC
-
-export const spotify = (url: string, override?: Partial): Social => ({
- name: 'Spotify',
- icon: 'fa6-brands:spotify',
- url,
- ...override,
-});
-
-export const soundcloud = (url: string, override?: Partial): Social => ({
- name: 'SoundCloud',
- icon: 'fa6-brands:soundcloud',
- url,
- ...override,
-});
-
-// VIDEO
-
-export const youtube = (url: string, override?: Partial): Social => ({
- name: 'YouTube',
- icon: 'fa6-brands:youtube',
- url,
- ...override,
-});
-
-export const twitch = (url: string, override?: Partial): Social => ({
- name: 'Twitch',
- icon: 'fa6-brands:twitch',
- url,
- ...override,
-});
-
-export const vimeo = (url: string, override?: Partial): Social => ({
- name: 'Vimeo',
- icon: 'fa6-brands:vimeo',
- url,
- ...override,
-});
-
-// PROJECT TYPE
-
-export const website = (url: string, override?: Partial): Social => ({
- name: 'Website',
- icon: 'fa6-solid:globe',
- url,
- ...override,
-});
-
-export const demo = (url: string, override?: Partial): Social => ({
- name: 'App demo',
- icon: 'fa6-solid:desktop',
- url,
- ...override,
-});
-
-export const mockups = (url: string, override?: Partial): Social => ({
- name: 'Mockups',
- icon: 'fa6-solid:image',
- url,
- ...override,
-});
-
-export const repository = (url: string, override?: Partial): Social => ({
- name: 'Repository',
- icon: 'fa6-solid:code-branch',
- url,
- ...override,
-});
diff --git a/src/data/transformers/index.ts b/src/data/transformers/index.ts
new file mode 100644
index 0000000..845cc62
--- /dev/null
+++ b/src/data/transformers/index.ts
@@ -0,0 +1 @@
+export * from './transformers';
diff --git a/src/data/transformers/transform-data.ts b/src/data/transformers/transform-data.ts
new file mode 100644
index 0000000..2b10336
--- /dev/null
+++ b/src/data/transformers/transform-data.ts
@@ -0,0 +1,14 @@
+import type { Data } from '@/types/data';
+import produce from 'immer';
+import type { PreciseData } from '../cv';
+import type { DataTransformer } from './transformers';
+
+const transformData =
+ (data: PreciseData) =>
+ (...callbacks: DataTransformer[]): Data =>
+ // @ts-ignore -- waiting for https://github.com/sindresorhus/type-fest/pull/540 to be merged
+ produce(data, (draft) => {
+ callbacks.forEach((callback) => callback(draft));
+ });
+
+export default transformData;
diff --git a/src/data/transformers/transformers.ts b/src/data/transformers/transformers.ts
new file mode 100644
index 0000000..44b7ecf
--- /dev/null
+++ b/src/data/transformers/transformers.ts
@@ -0,0 +1,92 @@
+import type { Data } from '@/types/data';
+import type { Draft } from 'immer';
+import type { PreciseData } from '../cv';
+
+export type DraftData = Draft;
+
+export type DataTransformer = (draft: DraftData) => void;
+
+type Sections = PreciseData['sections'];
+
+type SectionKey = keyof Sections;
+
+type ProjectName = Sections['portfolio']['projects'][number]['name'];
+
+type JobRole = Sections['experience']['jobs'][number]['role'];
+
+type JobCompany = Sections['experience']['jobs'][number]['company'];
+
+type DiplomaTitle = Sections['education']['diplomas'][number]['title'];
+
+type DiplomaInstitution = Sections['education']['diplomas'][number]['institution'];
+
+type SkillSets = Sections['skills']['skillSets'];
+
+type SkillSetTitle = SkillSets[number]['title'];
+
+type Filter, P> = T extends Readonly<[infer A, ...infer Rest]>
+ ? [...(A extends P ? [A] : []), ...Filter]
+ : [];
+
+type SkillsBySkillSet = Filter<
+ SkillSets,
+ { title: SkillSet }
+>[number]['skills'][number]['name'];
+
+export const hideSection =
+ (section: SectionKey): DataTransformer =>
+ (draft) => {
+ draft.sections[section].config.visible = false;
+ };
+
+export const hideJob =
+ (role: JobRole, company?: JobCompany): DataTransformer =>
+ (draft) => {
+ draft.sections.experience.jobs = draft.sections.experience.jobs.filter(
+ (job) => job.role !== role && job.company !== company
+ );
+ };
+
+export const hideDiploma =
+ (title: DiplomaTitle, institution?: DiplomaInstitution): DataTransformer =>
+ (draft) => {
+ draft.sections.education.diplomas = draft.sections.education.diplomas.filter(
+ (diploma) => diploma.title === title && diploma.institution === institution
+ );
+ };
+
+export const hideProject =
+ (name: ProjectName): DataTransformer =>
+ (draft) => {
+ draft.sections.portfolio.projects = draft.sections.portfolio.projects.filter((project) => project.name !== name);
+ };
+
+export const hideSkillSet =
+ (name: SkillSetTitle): DataTransformer =>
+ (draft) => {
+ draft.sections.skills.skillSets = draft.sections.skills.skillSets.filter((skillSet) => skillSet.title !== name);
+ };
+
+export const renameSkillSet =
+ (from: SkillSetTitle, to: string): DataTransformer =>
+ (draft) => {
+ draft.sections.skills.skillSets = draft.sections.skills.skillSets.map((skillSet) =>
+ skillSet.title === from ? { ...skillSet, title: to } : skillSet
+ );
+ };
+
+export const hideSkills =
+ (
+ skillSetTitle: SkillSetTitle,
+ skills: SkillsBySkillSet[]
+ ): DataTransformer =>
+ (draft) => {
+ draft.sections.skills.skillSets = draft.sections.skills.skillSets.map((skillSet) => {
+ if (skillSet.title !== skillSetTitle) return skillSet;
+
+ return {
+ ...skillSet,
+ skills: skillSet.skills.filter((skill) => !skills.includes(skill.name as (typeof skills)[number])),
+ };
+ });
+ };
diff --git a/src/pages/_playground/button.astro b/src/pages/_playground/button.astro
deleted file mode 100644
index 72f6b84..0000000
--- a/src/pages/_playground/button.astro
+++ /dev/null
@@ -1,7 +0,0 @@
----
-import Button from '@/components/button.astro';
----
-
-
- Button text
-
diff --git a/src/pages/_playground/favorites.astro b/src/pages/_playground/favorites.astro
deleted file mode 100644
index 594808e..0000000
--- a/src/pages/_playground/favorites.astro
+++ /dev/null
@@ -1,51 +0,0 @@
----
-import Typography from '@/components/typography.astro';
-import BookTile from '@/sections/favorites/book-tile.astro';
-import MediaTile from '@/sections/favorites/media-tile.astro';
-import PersonTile from '@/sections/favorites/person-tile.astro';
-import VideoTile from '@/sections/favorites/video-tile.astro';
-import type { Book, Media, Person, Video } from '@/types/favorites-section';
-
-const book: Book = {
- cover: import('@/assets/favorites/books/book-1.jpeg'),
- title: 'The Pragmatic Programmer: From Journeyman to Master',
- author: 'Andy Hunt, Dave Thomas',
- url: 'https://www.goodreads.com/book/show/4099.The_Pragmatic_Programmer',
-};
-
-const person: Person = {
- image: import('@/assets/favorites/people/person-1.jpg'),
- name: 'Kent C. Dodds',
- url: 'https://kentcdodds.com/',
-};
-
-const video: Video = {
- thumbnail: import('@/assets/favorites/videos/video-1.jpeg'),
- title: 'Building Resilient Frontend Architecture - Monica Lent - GOTO 2019',
- url: 'https://youtu.be/TqfbAXCCVwE',
-};
-
-const media: Media = {
- image: import('@/assets/favorites/media/media-1.jpeg'),
- title: 'Fireship.io',
- type: 'YouTube channel',
- url: 'https://www.youtube.com/c/Fireship',
-};
----
-
-Favourite Book
-
-
-
-Favourite Person
-
-Favourite Video
-
-
-
-Favourite Media
-
-
-
diff --git a/src/pages/_playground/icon-button.astro b/src/pages/_playground/icon-button.astro
deleted file mode 100644
index 6d5f3e6..0000000
--- a/src/pages/_playground/icon-button.astro
+++ /dev/null
@@ -1,10 +0,0 @@
----
-import IconButton from '@/components/icon-button.astro';
----
-
-
-
-
-
-
-
diff --git a/src/pages/_playground/icon.astro b/src/pages/_playground/icon.astro
deleted file mode 100644
index c54f32b..0000000
--- a/src/pages/_playground/icon.astro
+++ /dev/null
@@ -1,9 +0,0 @@
----
-import Icon from '@/components/icon.astro';
----
-
-
-
-
-
-
diff --git a/src/pages/_playground/image.astro b/src/pages/_playground/image.astro
deleted file mode 100644
index 893d99a..0000000
--- a/src/pages/_playground/image.astro
+++ /dev/null
@@ -1,7 +0,0 @@
----
-import { Image } from '@astrojs/image/components';
----
-
-
-
-
diff --git a/src/pages/_playground/labelled-value.astro b/src/pages/_playground/labelled-value.astro
deleted file mode 100644
index 56f1abf..0000000
--- a/src/pages/_playground/labelled-value.astro
+++ /dev/null
@@ -1,7 +0,0 @@
----
-import LabelledValue from '@/components/labelled-value.astro';
----
-
-
-
-
diff --git a/src/pages/_playground/main-section.astro b/src/pages/_playground/main-section.astro
deleted file mode 100644
index 6b63a60..0000000
--- a/src/pages/_playground/main-section.astro
+++ /dev/null
@@ -1,41 +0,0 @@
----
-import MainSection from '@/sections/main/main-section.astro';
-import type { MainSection as MainSectionData } from '@/types/main-section';
-
-const mainSectionData: MainSectionData = {
- config: {
- icon: 'fa6-solid:user',
- title: 'About me',
- },
- image: import('@/assets/my-image.jpeg'),
- fullName: 'Mark Freeman',
- role: 'Senior React Developer',
- details: [
- { label: 'Phone', value: '+48 604 343 212' },
- { label: 'Email', value: 'veeeery.long.email.address@gmail.com' },
- { label: 'From', value: 'Warsaw, Poland' },
- { label: 'Salary range', value: '18 000 - 25 000 PLN' },
- ],
- description:
- 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. In sodales ac dui at vestibulum. In condimentum metus id dui tincidunt, in blandit mi vehicula. Nulla lacinia, erat sit amet elementum vulputate, lectus mauris volutpat mi, vitae accumsan metus elit ut nunc. Vestibulum lacinia enim eget eros fermentum scelerisque. Proin augue leo, posuere ut imperdiet vitae, fermentum eu ipsum. Sed sed neque sagittis, posuere urna nec, commodo leo. Pellentesque posuere justo vitae massa volutpat maximus.',
- tags: [{ name: 'Open for freelance' }, { name: 'Available for mentoring' }, { name: 'Working on side project' }],
- action: {
- label: 'Download CV',
- url: '#',
- },
- socials: [
- { name: 'Facebook', icon: 'fa6-brands:facebook-f', url: '#' },
- { name: 'GitHub', icon: 'fa6-brands:github', url: '#' },
- { name: 'LinkedIn', icon: 'fa6-brands:linkedin-in', url: '#' },
- { name: 'Twitter', icon: 'fa6-brands:twitter', url: '#' },
- ],
-};
----
-
-
-
-
-
-
-
-
diff --git a/src/pages/_playground/projects-timeline.astro b/src/pages/_playground/projects-timeline.astro
deleted file mode 100644
index cf544df..0000000
--- a/src/pages/_playground/projects-timeline.astro
+++ /dev/null
@@ -1,67 +0,0 @@
----
-import ProjectTimelineItem from '@/sections/portfolio/project-timeline-item.astro';
-import type { I18n } from '@/types/i18n';
-import type { Project } from '@/types/portfolio-section';
-
-const project: Project = {
- name: 'Golden Bulls',
- image: import('@/assets/portfolio/project-1.jpeg'),
- startDate: new Date('2020-03'),
- endDate: null,
- details: [
- { label: 'Team size', value: '1 person' },
- { label: 'Company', value: 'None' },
- { label: 'My role', value: ['Front-end Developer', 'Designer'] },
- { label: 'Category', value: ['Web app', 'Open source'] },
- ],
- description:
- 'In tristique vulputate augue vel egestas. Quisque ac imperdiet tortor, at lacinia ex. Duis vel ex hendrerit, commodo odio sed, aliquam enim. Ut arcu nulla, tincidunt eget arcu eget, molestie vulputate nisi. Nunc malesuada leo et est iaculis facilisis.',
- tags: [
- {
- icon: 'simple-icons:nextdotjs',
- iconColor: '#000000',
- name: 'Next.js',
- url: 'https://nextjs.org/',
- },
- {
- icon: 'simple-icons:sass',
- iconColor: '#CC6699',
- name: 'SASS',
- url: 'https://sass-lang.com/',
- },
- {
- icon: 'simple-icons:pnpm',
- iconColor: '#F69220',
- name: 'pnpm',
- url: 'https://pnpm.io/',
- },
- {
- icon: 'simple-icons:eslint',
- iconColor: '#4B32C3',
- name: 'ESLint',
- url: 'https://eslint.org/',
- },
- {
- icon: 'simple-icons:prettier',
- iconColor: '#F7B93E',
- name: 'Prettier',
- url: 'https://prettier.io/',
- },
- ],
- socials: [
- { name: 'Mockups', icon: 'fa6-solid:image', url: '#' },
- { name: 'App demo', icon: 'fa6-solid:desktop', url: '#' },
- ],
-};
-
-const i18nData: I18n = {
- locale: 'en-US',
- translations: {
- now: 'now',
- },
-};
----
-
-
diff --git a/src/pages/_playground/section-card.astro b/src/pages/_playground/section-card.astro
deleted file mode 100644
index 33b3150..0000000
--- a/src/pages/_playground/section-card.astro
+++ /dev/null
@@ -1,7 +0,0 @@
----
-import SectionCard from '@/components/section-card.astro';
----
-
-
- SectionCard text
-
diff --git a/src/pages/_playground/sidebar-item.astro b/src/pages/_playground/sidebar-item.astro
deleted file mode 100644
index 63149b6..0000000
--- a/src/pages/_playground/sidebar-item.astro
+++ /dev/null
@@ -1,7 +0,0 @@
----
-import SidebarItem from '@/components/sidebar-item.astro';
----
-
-
-
-
diff --git a/src/pages/_playground/skill.astro b/src/pages/_playground/skill.astro
deleted file mode 100644
index eaae15e..0000000
--- a/src/pages/_playground/skill.astro
+++ /dev/null
@@ -1,14 +0,0 @@
----
-import Skill from '@/sections/skills/skill.astro';
-import type { LevelledSkill } from '@/types/skills-section';
-
-const levelledSkill: LevelledSkill = {
- icon: 'simple-icons:react',
- iconColor: '#61DAFB',
- name: 'React.js',
- level: 3,
- url: 'https://reactjs.org/',
-};
----
-
-
diff --git a/src/pages/_playground/skills-section.astro b/src/pages/_playground/skills-section.astro
deleted file mode 100644
index ea124eb..0000000
--- a/src/pages/_playground/skills-section.astro
+++ /dev/null
@@ -1,160 +0,0 @@
----
-import SkillsSection from '@/sections/skills/skills-section.astro';
-import type { SkillsSection as SkillsSectionData } from '@/types/skills-section';
-
-const skills: SkillsSectionData = {
- config: {
- title: 'Skills',
- icon: 'fa6-solid:bars-progress',
- },
- skillSets: [
- {
- title: 'I already know',
- skills: [
- {
- icon: 'simple-icons:react',
- iconColor: '#61DAFB',
- name: 'React.js',
- level: 5,
- url: 'https://reactjs.org/',
- description:
- 'Proin ut erat sed massa tempus suscipit. Mauris efficitur nunc sem, nec scelerisque ligula bibendum ut.',
- },
- {
- icon: 'simple-icons:typescript',
- iconColor: '#3178C6',
- name: 'TypeScript',
- level: 4,
- url: 'https://www.typescriptlang.org/',
- description: 'Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.',
- },
- {
- icon: 'simple-icons:sass',
- iconColor: '#CC6699',
- name: 'SASS',
- level: 4,
- url: 'https://sass-lang.com/',
- description: 'Nulla interdum pellentesque ultricies. Ut id eros commodo, ultrices ligula eu, elementum ante.',
- },
- {
- icon: 'simple-icons:chakraui',
- iconColor: '#319795',
- name: 'Chakra UI',
- level: 5,
- url: 'https://chakra-ui.com/',
- },
- {
- icon: 'simple-icons:tailwindcss',
- iconColor: '#06B6D4',
- name: 'Tailwind CSS',
- level: 2,
- url: 'https://tailwindcss.com/',
- },
- {
- icon: 'simple-icons:prettier',
- iconColor: '#F7B93E',
- name: 'Prettier',
- level: 5,
- url: 'https://prettier.io/',
- },
- {
- icon: 'simple-icons:eslint',
- iconColor: '#4B32C3',
- name: 'ESLint',
- level: 4,
- url: 'https://eslint.org/',
- description:
- 'Nulla tempor turpis at vehicula pharetra. Vestibulum tellus tortor, commodo et suscipit id, lobortis id nunc.',
- },
- {
- icon: 'simple-icons:nestjs',
- iconColor: '#E0234E',
- name: 'NestJS',
- level: 2,
- url: 'https://nestjs.com/',
- description:
- 'Praesent feugiat ultricies iaculis. In posuere vehicula odio, sed consequat velit porta viverra.',
- },
- {
- icon: 'simple-icons:postgresql',
- iconColor: '#4169E1',
- name: 'PostgreSQL',
- level: 2,
- url: 'https://www.postgresql.org/',
- },
- {
- icon: 'simple-icons:mongodb',
- iconColor: '#47A248',
- name: 'MongoDB',
- level: 1,
- url: 'https://www.mongodb.com/',
- },
- {
- icon: 'simple-icons:firebase',
- iconColor: '#FFCA28',
- name: 'Firebase',
- level: 1,
- url: 'https://firebase.google.com/',
- },
- {
- icon: 'simple-icons:pnpm',
- iconColor: '#F69220',
- name: 'pnpm',
- level: 3,
- url: 'https://pnpm.io/',
- },
- ],
- },
- {
- title: 'I want to learn',
- skills: [
- {
- icon: 'simple-icons:apollographql',
- iconColor: '#311C87',
- name: 'Apollo GraphQL',
- },
- {
- icon: 'simple-icons:astro',
- iconColor: '#FF5D01',
- name: 'Astro',
- },
- {
- icon: 'simple-icons:supabase',
- iconColor: '#3ECF8E',
- name: 'Supabase',
- },
- {
- icon: 'simple-icons:cypress',
- iconColor: '#17202C',
- name: 'Cypress',
- },
- ],
- },
- {
- title: 'I speak',
- skills: [
- {
- icon: 'circle-flags:pl',
- name: 'Polish - native',
- },
- {
- icon: 'circle-flags:us',
- name: 'English - C1',
- },
- {
- icon: 'circle-flags:es-variant',
- name: 'Spanish - B1',
- },
- ],
- },
- ],
-};
----
-
-
-
-
-
-
-
-
diff --git a/src/pages/_playground/tag.astro b/src/pages/_playground/tag.astro
deleted file mode 100644
index f0e0d90..0000000
--- a/src/pages/_playground/tag.astro
+++ /dev/null
@@ -1,11 +0,0 @@
----
-import Tag from '@/components/tag.astro';
----
-
-
- Tag text
-
-
-
- Tag text
-
diff --git a/src/pages/_playground/testimonial.astro b/src/pages/_playground/testimonial.astro
deleted file mode 100644
index d82c23e..0000000
--- a/src/pages/_playground/testimonial.astro
+++ /dev/null
@@ -1,20 +0,0 @@
----
-import Testimonial from '@/sections/testimonials/testimonial.astro';
-import type { Testimonial as TestimonialData } from '@/types/testimonials-section';
-
-const testimonial: TestimonialData = {
- author: 'Howard Stewart',
- relation: 'We work together as front-end developers at Google',
- content:
- 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, nisl vel tincidunt aliquam, nunc nisl aliquet nisl, eget aliquet nunc nisl euismod nisl. Sed euismod, nisl vel tincidunt aliquam, nunc nisl aliquet nisl, eget aliquet nunc nisl euismod nisl.',
- image: import('@/assets/testimonials/testimonial-1.jpeg'),
- socials: [
- { name: 'GitHub', icon: 'fa6-brands:github', url: '#' },
- { name: 'LinkedIn', icon: 'fa6-brands:linkedin-in', url: '#' },
- ],
-};
----
-
-
-
-
diff --git a/src/pages/_playground/testimonials-section.astro b/src/pages/_playground/testimonials-section.astro
deleted file mode 100644
index 19bdfef..0000000
--- a/src/pages/_playground/testimonials-section.astro
+++ /dev/null
@@ -1,45 +0,0 @@
----
-import TestimonialsSection from '@/sections/testimonials/testimonials-section.astro';
-import type { Testimonial } from '@/types/testimonials-section';
-
-const testimonials: Testimonial[] = [
- {
- image: import('@/assets/testimonials/testimonial-1.jpeg'),
- author: 'Howard Stewart',
- relation: 'We work together as front-end developers at Google',
- content:
- 'In nec mattis sem. Morbi purus lorem, euismod ac varius at, aliquet vitae augue. Pellentesque ut facilisis felis. In sed dui blandit, aliquet odio eu, elementum leo. In facilisis dapibus tortor ac volutpat. Cras cursus nec odio maximus elementum.',
- socials: [
- { name: 'GitHub', icon: 'fa6-brands:github', url: '#' },
- { name: 'LinkedIn', icon: 'fa6-brands:linkedin-in', url: '#' },
- ],
- },
- {
- image: import('@/assets/testimonials/testimonial-2.jpeg'),
- author: 'Jean Richards',
- relation: 'My project manager at GitLab',
- content:
- 'Praesent nec congue elit. Vestibulum lobortis congue ipsum, a gravida mi tempus ac. Mauris aliquet purus nibh, vel varius turpis tempus non. Nullam eget ultricies orci. Quisque nulla ante, auctor eget varius ac, imperdiet nec magna.',
- socials: [{ name: 'LinkedIn', icon: 'fa6-brands:linkedin-in', url: '#' }],
- },
- {
- image: import('@/assets/testimonials/testimonial-3.jpeg'),
- author: 'Jason Fisher',
- relation: 'My customer for sidewing.com website',
- content:
- 'Mauris tincidunt at purus vehicula porta. Mauris eget mollis turpis. Sed iaculis rutrum pharetra. Vivamus risus quam, suscipit et semper ut, aliquet ut tellus. Donec quis auctor nunc.',
- socials: [
- { name: 'GitHub', icon: 'fa6-brands:github', url: '#' },
- { name: 'Website', icon: 'fa6-solid:globe', url: '#' },
- ],
- },
-];
----
-
-
-
-
-
-
-
-
diff --git a/src/pages/_playground/typography.astro b/src/pages/_playground/typography.astro
deleted file mode 100644
index 24978a7..0000000
--- a/src/pages/_playground/typography.astro
+++ /dev/null
@@ -1,48 +0,0 @@
----
-import Typography from '@/components/typography.astro';
-
-const text = 'A quick brown fox jumps over the lazy dog';
----
-
-
-
-
paragraph (default)
-
{text}
-
-
-
-
main-subtitle
-
{text}
-
-
-
section-title
-
{text}
-
-
-
section-subtitle
-
{text}
-
-
-
-
item-title-suffix
-
{text}
-
-
-
item-subtitle
-
{text}
-
-
-
-
tile-subtitle
-
{text}
-
-
diff --git a/src/pages/_playground/work-timeline.astro b/src/pages/_playground/work-timeline.astro
deleted file mode 100644
index 73c2e96..0000000
--- a/src/pages/_playground/work-timeline.astro
+++ /dev/null
@@ -1,67 +0,0 @@
----
-import WorkTimelineItem from '@/sections/experience/work-timeline-item.astro';
-import type { Job } from '@/types/experience-section';
-import type { I18n } from '@/types/i18n';
-
-const job: Job = {
- role: 'Senior front-end developer',
- company: 'Google',
- startDate: new Date('2020-02'),
- endDate: null,
- description: [
- 'In tristique vulputate augue vel egestas.',
- 'Quisque ac imperdiet tortor, at lacinia ex.',
- 'Duis vel ex hendrerit, commodo odio sed, aliquam enim.',
- 'Ut arcu nulla, tincidunt eget arcu eget, molestie vulputate nisi.',
- 'Nunc malesuada leo et est iaculis facilisis.',
- 'Fusce eu urna ut magna malesuada fringilla.',
- ],
- tags: [
- {
- icon: 'simple-icons:react',
- iconColor: '#61DAFB',
- name: 'React.js',
- url: 'https://reactjs.org/',
- },
- {
- icon: 'simple-icons:nextdotjs',
- iconColor: '#000000',
- name: 'Next.js',
- url: 'https://nextjs.org/',
- },
- {
- icon: 'simple-icons:typescript',
- iconColor: '#3178C6',
- name: 'TypeScript',
- url: 'https://www.typescriptlang.org/',
- },
- {
- icon: 'simple-icons:nx',
- iconColor: '#143055',
- name: 'Nx',
- url: 'https://nx.dev/',
- },
- {
- icon: 'simple-icons:firebase',
- iconColor: '#FFCA28',
- name: 'Firebase',
- url: 'https://firebase.google.com/',
- },
- ],
- socials: [
- { name: 'Facebook', icon: 'fa6-brands:facebook-f', url: '#' },
- { name: 'LinkedIn', icon: 'fa6-brands:linkedin-in', url: '#' },
- ],
-};
-
-const i18nData: I18n = {
- locale: 'en-US',
- translations: {
- now: 'now',
- },
-};
----
-
-
-
-
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 9f4a53e..ce2707f 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -1,61 +1,30 @@
---
-import Sidebar from '@/components/sidebar.astro';
-import ThemeToggle from '@/components/theme-toggle.astro';
-import EducationSection from '@/sections/education/education-section.astro';
-import ExperienceSection from '@/sections/experience/experience-section.astro';
-import FavoritesSection from '@/sections/favorites/favorites-section.astro';
-import MainSection from '@/sections/main/main-section.astro';
-import PortfolioSection from '@/sections/portfolio/portfolio-section.astro';
-import SkillsSection from '@/sections/skills/skills-section.astro';
-import TestimonialsSection from '@/sections/testimonials/testimonials-section.astro';
+import Layout from '@/web/components/layout.astro';
+import Sidebar from '@/web/components/sidebar.astro';
+import ThemeToggle from '@/web/components/theme-toggle.astro';
+import MainSection from '@/web/sections/main/main-section.web.astro';
+import SkillsSection from '@/web/sections/skills/skills-section.web.astro';
+import ExperienceSection from '@/web/sections/experience/experience-section.web.astro';
+import PortfolioSection from '@/web/sections/portfolio/portfolio-section.web.astro';
+import EducationSection from '@/web/sections/education/education-section.web.astro';
+import TestimonialsSection from '@/web/sections/testimonials/testimonials-section.web.astro';
+import FavoritesSection from '@/web/sections/favorites/favorites-section.web.astro';
+import { cv } from '@/data/cv';
-import data from '../data';
-
-const { seo, i18n } = data;
-const seoImage = seo.image ? seo.image : '/favicon.svg';
+const { config, sections } = cv();
---
-
-
-
-
-
-
- {seo.title}
-
-
-
-
-
-
-
-
-
-
-
- {data.skills && }
- {data.experience && }
- {data.portfolio && }
- {data.education && }
- {data.testimonials && }
- {data.favorites && }
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/pdf.astro b/src/pages/pdf.astro
index 5f3d98d..0d9e112 100644
--- a/src/pages/pdf.astro
+++ b/src/pages/pdf.astro
@@ -1,29 +1,34 @@
---
-import Footer from '@/pdf/footer.astro';
+import Footer from '@/pdf/components/footer.astro';
import EducationSection from '@/pdf/sections/education-section.pdf.astro';
import ExperienceSection from '@/pdf/sections/experience-section.pdf.astro';
import MainSection from '@/pdf/sections/main-section.pdf.astro';
import PortfolioSection from '@/pdf/sections/portfolio-section.pdf.astro';
import SkillsSection from '@/pdf/sections/skills-section.pdf.astro';
-import data from '../data';
+import { cv } from '@/data/cv';
+import { hideProject, hideSkillSet, renameSkillSet } from '@/data/transformers';
-const { i18n } = data;
+const { config, sections } = cv(
+ hideSkillSet('I want to learn'),
+ renameSkillSet('I speak', 'Languages'),
+ hideProject('Disco Ninjas')
+);
---
-
+
PDF preview
-
- {data.skills && }
- {data.experience && }
- {data.portfolio && }
- {data.education && }
-
+
+ {sections.skills && }
+ {sections.experience && }
+ {sections.portfolio && }
+ {sections.education && }
+ {config.pdf?.footer && }
diff --git a/src/pdf/components/date-range-tag.astro b/src/pdf/components/date-range-tag.astro
index 1806324..6505e6c 100644
--- a/src/pdf/components/date-range-tag.astro
+++ b/src/pdf/components/date-range-tag.astro
@@ -1,17 +1,13 @@
---
-import type { I18n } from '@/types/i18n';
-import getDateFormatter from '@/utils/date-formatter';
+import type { DateRange } from '@/types/shared';
+import formatDateRange from '@/utils/format-date-range';
export interface Props {
- i18n: I18n;
- startDate: Date;
- endDate: Date | null;
class?: string;
+ dates: DateRange;
}
-const { startDate, endDate, i18n, ...props } = Astro.props;
-
-const getFormattedDate = getDateFormatter(i18n.locale);
+const { dates, ...props } = Astro.props;
---
- {getFormattedDate(startDate)} -{' '}
- {endDate ? getFormattedDate(endDate) : i18n.translations.now}
+ {formatDateRange(dates)}
diff --git a/src/pdf/components/description.astro b/src/pdf/components/description.astro
index 848d145..a8bef5c 100644
--- a/src/pdf/components/description.astro
+++ b/src/pdf/components/description.astro
@@ -1,6 +1,8 @@
---
+import type { Description } from '@/types/shared';
+
export interface Props {
- content: string | string[];
+ content: Description;
}
const { content } = Astro.props;
diff --git a/src/pdf/components/footer.astro b/src/pdf/components/footer.astro
new file mode 100644
index 0000000..dcfe372
--- /dev/null
+++ b/src/pdf/components/footer.astro
@@ -0,0 +1,6 @@
+
diff --git a/src/pdf/components/labelled-value.astro b/src/pdf/components/labelled-value.astro
index fdd620a..b090aa2 100644
--- a/src/pdf/components/labelled-value.astro
+++ b/src/pdf/components/labelled-value.astro
@@ -1,11 +1,13 @@
---
-import type { PdfDetail } from '@/types/common';
+import type { LabelledValue } from '@/types/shared';
-export interface Props extends PdfDetail {
+export interface Props extends LabelledValue {
class?: string;
}
const { label, value, url, ...props } = Astro.props;
+
+const parsedValue = Array.isArray(value) ? value.join(', ') : value;
---
@@ -13,10 +15,10 @@ const { label, value, url, ...props } = Astro.props;
{
url ? (
- {value}
+ {parsedValue}
) : (
-
{value}
+
{parsedValue}
)
}
diff --git a/src/pdf/components/list-item-heading.astro b/src/pdf/components/list-item-heading.astro
index 20a9f1f..13af7cf 100644
--- a/src/pdf/components/list-item-heading.astro
+++ b/src/pdf/components/list-item-heading.astro
@@ -1,22 +1,20 @@
---
-import type { I18n } from '@/types/i18n';
+import type { DateRange } from '@/types/shared';
import DateRangeTag from './date-range-tag.astro';
export interface Props {
title: string;
subtitle?: string;
- startDate: Date;
- endDate: Date | null;
- i18n: I18n;
+ dates: DateRange;
}
-const { title, subtitle, i18n, startDate, endDate } = Astro.props;
+const { title, subtitle, dates } = Astro.props;
---
-
+
{subtitle &&
{subtitle}
}
diff --git a/src/pdf/components/photo.astro b/src/pdf/components/photo.astro
new file mode 100644
index 0000000..0121945
--- /dev/null
+++ b/src/pdf/components/photo.astro
@@ -0,0 +1,24 @@
+---
+import type { Photo } from '@/types/shared';
+import { Image } from '@astrojs/image/components';
+
+export interface Props {
+ src: Photo;
+ alt: string;
+ class?: string;
+ width?: number;
+ height?: number;
+}
+
+const { src, ...props } = Astro.props;
+
+const isRemoteImage = typeof src === 'string';
+---
+
+{
+ isRemoteImage ? (
+
+ ) : (
+
+ )
+}
diff --git a/src/pdf/components/tags-list.astro b/src/pdf/components/tags-list.astro
index 963c4ae..c787d0a 100644
--- a/src/pdf/components/tags-list.astro
+++ b/src/pdf/components/tags-list.astro
@@ -1,15 +1,12 @@
---
-import type { Tag } from '@/types/common';
+import type { TagsList } from '@/types/shared';
-export interface Props {
- tags: Tag[];
- label: string;
-}
+export interface Props extends TagsList {}
-const { tags, label } = Astro.props;
+const { tags, title } = Astro.props;
---
- {label}:
+ {title}:
{tags.map((t) => t.name).join(', ')}
diff --git a/src/pdf/footer.astro b/src/pdf/footer.astro
deleted file mode 100644
index 5827b81..0000000
--- a/src/pdf/footer.astro
+++ /dev/null
@@ -1,24 +0,0 @@
----
-import type { Pdf } from '@/types/pdf';
-
-export interface Props {
- footer: Pdf['footer'];
-}
-
-const { footer } = Astro.props;
----
-
-
-
diff --git a/src/pdf/sections/education-section.pdf.astro b/src/pdf/sections/education-section.pdf.astro
index 0a3aa81..7995de8 100644
--- a/src/pdf/sections/education-section.pdf.astro
+++ b/src/pdf/sections/education-section.pdf.astro
@@ -1,30 +1,23 @@
---
-import type { EducationSection } from '@/types/education-section';
-import type { I18n } from '@/types/i18n';
+import type { EducationSection } from '@/types/sections/education-section.types';
import DashedDivider from '../components/dashed-divider.astro';
import Description from '../components/description.astro';
import ListItemHeading from '../components/list-item-heading.astro';
import SectionHeading from '../components/section-heading.astro';
-export interface Props extends EducationSection {
- i18n: I18n;
-}
+export interface Props extends EducationSection {}
-const {
- config: { title },
- educationItems,
- i18n,
-} = Astro.props;
+const { config, diplomas } = Astro.props;
---
-
{title}
+
{config.title}
{
- educationItems.map(({ title, description, institution, startDate, endDate }) => () => (
+ diplomas.map(({ title, description, institution, dates }) => () => (
<>
-
+
diff --git a/src/pdf/sections/experience-section.pdf.astro b/src/pdf/sections/experience-section.pdf.astro
index e522c7b..dc22277 100644
--- a/src/pdf/sections/experience-section.pdf.astro
+++ b/src/pdf/sections/experience-section.pdf.astro
@@ -1,34 +1,26 @@
---
-import type { ExperienceSection, Job } from '@/types/experience-section';
-import type { I18n } from '@/types/i18n';
+import type { ExperienceSection } from '@/types/sections/experience-section.types';
import DashedDivider from '../components/dashed-divider.astro';
import Description from '../components/description.astro';
import ListItemHeading from '../components/list-item-heading.astro';
import SectionHeading from '../components/section-heading.astro';
import TagsList from '../components/tags-list.astro';
-export interface Props extends ExperienceSection {
- jobs: Job[];
- i18n: I18n;
-}
+export interface Props extends ExperienceSection {}
-const {
- config: { title },
- i18n,
- jobs,
-} = Astro.props;
+const { config, jobs } = Astro.props;
---
-
{title}
+
{config.title}
{
- jobs.map(({ company, role, description, tags, startDate, endDate }) => () => (
+ jobs.map(({ company, role, description, tagsList, dates }) => () => (
<>
-
+
-
+
>
diff --git a/src/pdf/sections/main-section.pdf.astro b/src/pdf/sections/main-section.pdf.astro
index 2149173..14c647f 100644
--- a/src/pdf/sections/main-section.pdf.astro
+++ b/src/pdf/sections/main-section.pdf.astro
@@ -1,24 +1,23 @@
---
-import Photo from '@/components/photo.astro';
-import type { PdfDetail } from '@/types/common';
-import type { MainSection } from '@/types/main-section';
-import Description from '../components/description.astro';
-import LabelledValue from '../components/labelled-value.astro';
+import type { MainSection } from '@/types/sections/main-section.types';
+import Photo from '@/pdf/components/photo.astro';
+import Description from '@/pdf/components/description.astro';
+import LabelledValue from '@/pdf/components/labelled-value.astro';
export interface Props extends MainSection {}
-const { image, fullName, role, pdfDetails, details, description } = Astro.props;
+const { image, fullName, role, details, pdfDetails, description } = Astro.props;
---
-
+
{role}
{
- (pdfDetails || details).map((detail: PdfDetail) => (
+ (pdfDetails ?? details).map((detail) => (
))
}
diff --git a/src/pdf/sections/portfolio-section.pdf.astro b/src/pdf/sections/portfolio-section.pdf.astro
index 25d164c..7c2750e 100644
--- a/src/pdf/sections/portfolio-section.pdf.astro
+++ b/src/pdf/sections/portfolio-section.pdf.astro
@@ -1,7 +1,5 @@
---
-import type { PdfDetail } from '@/types/common';
-import type { I18n } from '@/types/i18n';
-import type { PortfolioSection } from '@/types/portfolio-section';
+import type { PortfolioSection } from '@/types/sections/portfolio-section.types';
import DashedDivider from '../components/dashed-divider.astro';
import Description from '../components/description.astro';
import LabelledValue from '../components/labelled-value.astro';
@@ -9,32 +7,26 @@ import ListItemHeading from '../components/list-item-heading.astro';
import SectionHeading from '../components/section-heading.astro';
import TagsList from '../components/tags-list.astro';
-export interface Props extends PortfolioSection {
- i18n: I18n;
-}
+export interface Props extends PortfolioSection {}
-const {
- config: { title },
- projects,
- i18n,
-} = Astro.props;
+const { config, projects } = Astro.props;
---
-
{title}
+
{config.title}
{
- projects.map(({ name, description, pdfDetails, details, tags, startDate, endDate }) => () => (
+ projects.map(({ name, description, details, pdfDetails, tagsList, dates }) => () => (
<>
-
+
-
- {(pdfDetails || details).map((detail: PdfDetail) => (
+
+ {(pdfDetails ?? details).map((detail) => (
))}
-
+
>
diff --git a/src/pdf/sections/skills-section.pdf.astro b/src/pdf/sections/skills-section.pdf.astro
index 2c0019d..dd32e7e 100644
--- a/src/pdf/sections/skills-section.pdf.astro
+++ b/src/pdf/sections/skills-section.pdf.astro
@@ -1,56 +1,48 @@
---
-import type { SkillsSection } from '@/types/skills-section';
+import type { SkillsSection } from '@/types/sections/skills-section.types';
import SectionHeading from '../components/section-heading.astro';
export interface Props extends SkillsSection {}
-const {
- config: { title },
- skillSets,
-} = Astro.props;
+const { config, skillSets } = Astro.props;
---
-
{title}
+
{config.title}
{
- skillSets.map(
- (skillSet) =>
- !skillSet.excludeFromPdf && (
-
-
-
- {skillSet.skills.map((skill) => {
- if ('level' in skill) {
- return (
-
-
{skill.name}
-
{skill.level}/5
-
- );
- }
+ skillSets.map((skillSet) => (
+
+
+
+ {skillSet.skills.map((skill) => {
+ if ('level' in skill) {
+ return (
+
+
{skill.name}
+
{skill.level}/5
+
+ );
+ }
- if (skill.name.includes(' - ')) {
- return (
-
-
- {skill.name.split(' - ')[0]}
-
-
- {skill.name.split(' - ')[1]}
-
-
- );
- }
+ if (skill.name.includes(' - ')) {
+ return (
+
+
+ {skill.name.split(' - ')[0]}
+
+
+ {skill.name.split(' - ')[1]}
+
+
+ );
+ }
- return (
-
{skill.name}
- );
- })}
-
-
- )
- )
+ return
{skill.name}
;
+ })}
+
+
+ ))
}
diff --git a/src/sections/education/education-item.astro b/src/sections/education/education-item.astro
deleted file mode 100644
index f7ea6e5..0000000
--- a/src/sections/education/education-item.astro
+++ /dev/null
@@ -1,42 +0,0 @@
----
-import IconButton from '@/components/icon-button.astro';
-import Timestamp from '@/components/timestamp.astro';
-import Typography from '@/components/typography.astro';
-import type { EducationItem } from '@/types/education-section';
-import type { I18n } from '@/types/i18n';
-
-export interface Props {
- educationItem: EducationItem;
- i18n: I18n;
-}
-
-const {
- educationItem: { title, institution, startDate, endDate, description, socials },
- i18n,
-} = Astro.props;
----
-
-
-
-
- {title}
- {institution}
-
-
- {
- socials.length > 0 && (
-
- {socials.map(({ icon, url: iconUrl, name }) => (
-
- ))}
-
- )
- }
-
-
{description}
-
diff --git a/src/sections/education/education-section.astro b/src/sections/education/education-section.astro
deleted file mode 100644
index f0eefbd..0000000
--- a/src/sections/education/education-section.astro
+++ /dev/null
@@ -1,30 +0,0 @@
----
-import Divider from '@/components/divider.astro';
-import SectionCard from '@/components/section-card.astro';
-import type { EducationSection } from '@/types/education-section';
-import type { I18n } from '@/types/i18n';
-import removeLast from '@/utils/remove-last';
-
-import EducationItem from './education-item.astro';
-
-export interface Props extends EducationSection {
- i18n: I18n;
-}
-
-const {
- config: { title },
- educationItems,
- i18n,
-} = Astro.props;
----
-
-
- {
- removeLast(
- educationItems.flatMap((educationItem) => [
- ,
- ,
- ])
- )
- }
-
diff --git a/src/sections/experience/experience-section.astro b/src/sections/experience/experience-section.astro
deleted file mode 100644
index 1c236df..0000000
--- a/src/sections/experience/experience-section.astro
+++ /dev/null
@@ -1,30 +0,0 @@
----
-import DividedList from '@/components/divided-list.astro';
-import Divider from '@/components/divider.astro';
-import SectionCard from '@/components/section-card.astro';
-import type { SectionKey } from '@/types/data';
-import type { ExperienceSection, Job } from '@/types/experience-section';
-import type { I18n } from '@/types/i18n';
-import removeLast from '@/utils/remove-last';
-
-import WorkTimelineItem from './work-timeline-item.astro';
-
-export interface Props extends ExperienceSection {
- jobs: Job[];
- i18n: I18n;
-}
-
-const {
- config: { title },
- i18n,
- jobs,
-} = Astro.props;
-
-const section: SectionKey = 'experience';
----
-
-
-
- {removeLast(jobs.flatMap((job) => [ , ]))}
-
-
diff --git a/src/sections/experience/work-timeline-item.astro b/src/sections/experience/work-timeline-item.astro
deleted file mode 100644
index 809e5a1..0000000
--- a/src/sections/experience/work-timeline-item.astro
+++ /dev/null
@@ -1,54 +0,0 @@
----
-import IconButton from '@/components/icon-button.astro';
-import TagsList from '@/components/tags-list.astro';
-import Timestamp from '@/components/timestamp.astro';
-import Typography from '@/components/typography.astro';
-import type { Job } from '@/types/experience-section';
-import type { I18n } from '@/types/i18n';
-
-export interface Props {
- job: Job;
- i18n: I18n;
-}
-const { job, i18n } = Astro.props;
----
-
-
-
-
-
- {job.role}
- — {job.company}
-
-
-
- {
- job.socials?.map(({ icon, url, name }) => (
-
- ))
- }
-
-
-
-
- {
- Array.isArray(job.description) ? (
- job.description.map((d) => (
-
- {d}
-
- ))
- ) : (
-
- {job.description}
-
- )
- }
-
-
-
diff --git a/src/sections/favorites/favorites-section.astro b/src/sections/favorites/favorites-section.astro
deleted file mode 100644
index 263d034..0000000
--- a/src/sections/favorites/favorites-section.astro
+++ /dev/null
@@ -1,87 +0,0 @@
----
-import type { ComponentInstance } from 'astro';
-
-import SectionCard from '@/components/section-card.astro';
-import Typography from '@/components/typography.astro';
-import type { SectionKey } from '@/types/data';
-import type { Book, FavoritesSection, Media, Person, Video } from '@/types/favorites-section';
-
-import BookTile from './book-tile.astro';
-import MediaTile from './media-tile.astro';
-import PersonTile from './person-tile.astro';
-import VideoTile from './video-tile.astro';
-
-export interface Props extends FavoritesSection {}
-
-const {
- config: { title },
- books,
- medias,
- people,
- videos,
-} = Astro.props;
-
-type Subsection = 'books' | 'medias' | 'people' | 'videos';
-type SubsectionData = Book | Media | Person | Video;
-// eslint-disable-next-line @typescript-eslint/no-explicit-any -- required to avoid type casting
-type SubsectionComponent = (_props: { value: any }) => ComponentInstance;
-
-interface FavoritesSubsection
{
- name: Subsection;
- data: T[];
- title: string;
- columnsLayout: string;
- Component: SubsectionComponent;
-}
-
-const booksSubsection: FavoritesSubsection = {
- name: 'books',
- columnsLayout: 'grid-cols-fluid200',
- Component: BookTile,
- ...books,
-};
-
-const mediasSubsection: FavoritesSubsection = {
- name: 'medias',
- columnsLayout: 'grid-cols-fluid120',
- Component: MediaTile,
- ...medias,
-};
-
-const peopleSubsection: FavoritesSubsection = {
- name: 'people',
- columnsLayout: 'grid-cols-fluid120',
- Component: PersonTile,
- ...people,
-};
-
-const videosSubsection: FavoritesSubsection = {
- name: 'videos',
- columnsLayout: 'grid-cols-fluid240',
- Component: VideoTile,
- ...videos,
-};
-
-const subsections = [booksSubsection, peopleSubsection, videosSubsection, mediasSubsection];
-
-const section: SectionKey = 'favorites';
----
-
-
-
- {
- subsections.map(({ Component, data, name, columnsLayout, title: subsectionTitle }) => (
-
-
- {subsectionTitle}
-
-
- {data.map((value) => (
-
- ))}
-
-
- ))
- }
-
-
diff --git a/src/sections/favorites/video-tile.astro b/src/sections/favorites/video-tile.astro
deleted file mode 100644
index 335caeb..0000000
--- a/src/sections/favorites/video-tile.astro
+++ /dev/null
@@ -1,23 +0,0 @@
----
-import Photo from '@/components/photo.astro';
-import Typography from '@/components/typography.astro';
-import type { Video } from '@/types/favorites-section';
-
-export interface Props {
- value: Video;
-}
-
-const {
- value: { title, url },
-} = Astro.props;
-
-const id = url.split('/').pop();
-const thumbnail = `https://img.youtube.com/vi/${id}/0.jpg`;
----
-
-
-
-
- {title}
-
-
diff --git a/src/sections/portfolio/portfolio-section.astro b/src/sections/portfolio/portfolio-section.astro
deleted file mode 100644
index 1413786..0000000
--- a/src/sections/portfolio/portfolio-section.astro
+++ /dev/null
@@ -1,29 +0,0 @@
----
-import DividedList from '@/components/divided-list.astro';
-import Divider from '@/components/divider.astro';
-import SectionCard from '@/components/section-card.astro';
-import type { SectionKey } from '@/types/data';
-import type { I18n } from '@/types/i18n';
-import type { PortfolioSection } from '@/types/portfolio-section';
-import removeLast from '@/utils/remove-last';
-
-import ProjectTimelineItem from './project-timeline-item.astro';
-
-export interface Props extends PortfolioSection {
- i18n: I18n;
-}
-
-const {
- config: { title },
- projects,
- i18n,
-} = Astro.props;
-
-const section: SectionKey = 'portfolio';
----
-
-
-
- {removeLast(projects.flatMap((project) => [ , ]))}
-
-
diff --git a/src/sections/portfolio/project-timeline-item.astro b/src/sections/portfolio/project-timeline-item.astro
deleted file mode 100644
index cdbb204..0000000
--- a/src/sections/portfolio/project-timeline-item.astro
+++ /dev/null
@@ -1,68 +0,0 @@
----
-import IconButton from '@/components/icon-button.astro';
-import LabelledValue from '@/components/labelled-value.astro';
-import Photo from '@/components/photo.astro';
-import TagsList from '@/components/tags-list.astro';
-import Timestamp from '@/components/timestamp.astro';
-import Typography from '@/components/typography.astro';
-import type { I18n } from '@/types/i18n';
-import type { Project } from '@/types/portfolio-section';
-
-export interface Props {
- project: Project;
- i18n: I18n;
-}
-const { project, i18n } = Astro.props;
-const { description, details, endDate, name, socials, startDate, tags, image } = project;
-
-const alt = `Thumbnail for ${name} project`;
----
-
-
-
-
-
-
-
-
- {name}
-
-
-
- {
- socials?.map(({ icon, url, name: socialName }) => (
-
- ))
- }
-
-
-
-
- {
- details.map(({ label: detailLabel, value: detailValue }) => (
-
- ))
- }
-
-
-
-
- {description}
-
-
-
-
diff --git a/src/sections/skills/levelled-skill-subsection.astro b/src/sections/skills/levelled-skill-subsection.astro
deleted file mode 100644
index 7ebc86a..0000000
--- a/src/sections/skills/levelled-skill-subsection.astro
+++ /dev/null
@@ -1,15 +0,0 @@
----
-import type { LevelledSkill } from '@/types/skills-section';
-
-import Skill from './skill.astro';
-
-export interface Props {
- skills: LevelledSkill[];
-}
-
-const { skills } = Astro.props;
----
-
-
- {skills.map((skill) => )}
-
diff --git a/src/sections/skills/skill-subsection.astro b/src/sections/skills/skill-subsection.astro
deleted file mode 100644
index c608fdd..0000000
--- a/src/sections/skills/skill-subsection.astro
+++ /dev/null
@@ -1,28 +0,0 @@
----
-import TagsList from '@/components/tags-list.astro';
-import Typography from '@/components/typography.astro';
-import type { Tag } from '@/types/common';
-import type { LevelledSkill, SkillSet } from '@/types/skills-section';
-
-import LevelledSkillSubsection from './levelled-skill-subsection.astro';
-
-export interface Props {
- skillSet: SkillSet | SkillSet;
-}
-
-const {
- skillSet: { skills, title },
-} = Astro.props;
-
-const isLevelledSkillSection = (skillsSectionData: Tag[] | LevelledSkill[]): skillsSectionData is LevelledSkill[] => {
- const firstSkill = skillsSectionData[0];
- if (!firstSkill) return false;
-
- return 'level' in firstSkill && firstSkill.level !== undefined;
-};
----
-
-
- {title}
- {isLevelledSkillSection(skills) ? : }
-
diff --git a/src/sections/skills/skills-section.astro b/src/sections/skills/skills-section.astro
deleted file mode 100644
index 931c716..0000000
--- a/src/sections/skills/skills-section.astro
+++ /dev/null
@@ -1,22 +0,0 @@
----
-import SectionCard from '@/components/section-card.astro';
-import type { SectionKey } from '@/types/data';
-import type { SkillsSection } from '@/types/skills-section';
-
-import SkillSubsection from './skill-subsection.astro';
-
-export interface Props extends SkillsSection {}
-
-const {
- config: { title },
- skillSets,
-} = Astro.props;
-
-const section: SectionKey = 'skills';
----
-
-
-
- {skillSets.map((skillSet) => )}
-
-
diff --git a/src/sections/testimonials/testimonials-section.astro b/src/sections/testimonials/testimonials-section.astro
deleted file mode 100644
index 4f62778..0000000
--- a/src/sections/testimonials/testimonials-section.astro
+++ /dev/null
@@ -1,25 +0,0 @@
----
-import DividedList from '@/components/divided-list.astro';
-import Divider from '@/components/divider.astro';
-import SectionCard from '@/components/section-card.astro';
-import type { SectionKey } from '@/types/data';
-import type { TestimonialsSection } from '@/types/testimonials-section';
-import removeLast from '@/utils/remove-last';
-
-import Testimonial from './testimonial.astro';
-
-export interface Props extends TestimonialsSection {}
-
-const {
- testimonials,
- config: { title },
-} = Astro.props;
-
-const section: SectionKey = 'testimonials';
----
-
-
-
- {removeLast(testimonials.flatMap((testimonial) => [ , ]))}
-
-
diff --git a/src/types/common.ts b/src/types/common.ts
deleted file mode 100644
index cc55459..0000000
--- a/src/types/common.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import type { IconName } from './icon';
-
-export type Photo = Promise<{ default: ImageMetadata }> | string;
-
-export interface Detail {
- label: string;
- value: string | string[];
-}
-
-export interface PdfDetail extends Detail {
- url?: string;
- fullRow?: boolean;
-}
-
-export interface Social {
- name: string;
- icon: IconName;
- url: string;
-}
-
-export interface Tag {
- name: string;
- icon?: IconName;
- iconColor?: string;
- url?: string;
- description?: string;
-}
-
-export interface SectionConfig {
- title: string;
- icon: IconName;
-}
diff --git a/src/types/config/i18n-config.types.ts b/src/types/config/i18n-config.types.ts
new file mode 100644
index 0000000..a90540d
--- /dev/null
+++ b/src/types/config/i18n-config.types.ts
@@ -0,0 +1,9 @@
+import type { Locale } from 'date-fns';
+
+export interface I18nConfig {
+ locale: Locale;
+ dateFormat: string;
+ translations: {
+ now: string;
+ };
+}
diff --git a/src/types/config/pdf-config.types.ts b/src/types/config/pdf-config.types.ts
new file mode 100644
index 0000000..9112955
--- /dev/null
+++ b/src/types/config/pdf-config.types.ts
@@ -0,0 +1,3 @@
+export interface PdfConfig {
+ footer?: string;
+}
diff --git a/src/types/config/seo-config.types.ts b/src/types/config/seo-config.types.ts
new file mode 100644
index 0000000..d1a7be8
--- /dev/null
+++ b/src/types/config/seo-config.types.ts
@@ -0,0 +1,8 @@
+export interface SeoConfig {
+ title: string;
+ description: string;
+ favicon: string;
+ ogTitle?: string;
+ ogDescription?: string;
+ ogImage?: string;
+}
diff --git a/src/types/data.ts b/src/types/data.ts
index 383c5e2..b42d387 100644
--- a/src/types/data.ts
+++ b/src/types/data.ts
@@ -1,23 +1,31 @@
-import type { EducationSection } from './education-section';
-import type { ExperienceSection } from './experience-section';
-import type { FavoritesSection } from './favorites-section';
-import type { I18n } from './i18n';
-import type { MainSection } from './main-section';
-import type { PortfolioSection } from './portfolio-section';
-import type { Seo } from './seo';
-import type { SkillsSection } from './skills-section';
-import type { TestimonialsSection } from './testimonials-section';
+import type { I18nConfig } from './config/i18n-config.types';
+import type { PdfConfig } from './config/pdf-config.types';
+import type { SeoConfig } from './config/seo-config.types';
+import type { EducationSection } from './sections/education-section.types';
+import type { ExperienceSection } from './sections/experience-section.types';
+import type { FavoritesSection } from './sections/favorites-section.types';
+import type { MainSection } from './sections/main-section.types';
+import type { PortfolioSection } from './sections/portfolio-section.types';
+import type { SkillsSection } from './sections/skills-section.types';
+import type { TestimonialsSection } from './sections/testimonials-section.types';
-export interface Data {
- i18n: I18n;
- seo: Seo;
- main: MainSection;
- skills?: SkillsSection;
- experience?: ExperienceSection;
- portfolio?: PortfolioSection;
- education?: EducationSection;
- testimonials?: TestimonialsSection;
- favorites?: FavoritesSection;
+export interface Config {
+ seo: SeoConfig;
+ i18n: I18nConfig;
+ pdf?: PdfConfig;
}
-export type SectionKey = Exclude;
+export interface Sections {
+ main: MainSection;
+ skills: SkillsSection;
+ experience: ExperienceSection;
+ portfolio: PortfolioSection;
+ education: EducationSection;
+ testimonials: TestimonialsSection;
+ favorites: FavoritesSection;
+}
+
+export interface Data {
+ config: Config;
+ sections: Sections;
+}
diff --git a/src/types/education-section.ts b/src/types/education-section.ts
deleted file mode 100644
index 5959a43..0000000
--- a/src/types/education-section.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import type { SectionConfig, Social } from './common';
-
-export interface EducationItem {
- title: string;
- institution: string;
- startDate: Date;
- endDate: Date | null;
- description: string;
- socials: Social[];
-}
-
-export interface EducationSection {
- educationItems: EducationItem[];
- config: SectionConfig;
-}
diff --git a/src/types/experience-section.ts b/src/types/experience-section.ts
deleted file mode 100644
index 2ec54c1..0000000
--- a/src/types/experience-section.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { SectionConfig, Social, Tag } from './common';
-
-export interface Job {
- role: string;
- company: string;
- startDate: Date;
- endDate: Date | null;
- description: string | string[];
- tags: Tag[];
- socials: Social[];
-}
-
-export interface ExperienceSection {
- jobs: Job[];
- config: SectionConfig;
-}
diff --git a/src/types/i18n.ts b/src/types/i18n.ts
deleted file mode 100644
index 5b4979f..0000000
--- a/src/types/i18n.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import type { Locales } from 'locales-ts/types';
-
-export interface I18n {
- locale: Locales;
- translations: {
- now: string;
- };
-}
diff --git a/src/types/icon.ts b/src/types/icon.ts
deleted file mode 100644
index 91aa389..0000000
--- a/src/types/icon.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import type { CircleFlags, Fa6Brands, Fa6Solid, Ri, SimpleIcons } from 'iconify-icon-names';
-
-export type IconName = Fa6Brands | Fa6Solid | SimpleIcons | CircleFlags | Ri;
diff --git a/src/types/main-section.ts b/src/types/main-section.ts
deleted file mode 100644
index a998d5c..0000000
--- a/src/types/main-section.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import type { Detail, PdfDetail, Photo, SectionConfig, Social, Tag } from './common';
-
-export interface MainSection {
- image: Photo;
- fullName: string;
- role: string;
- details: Detail[];
- pdfDetails?: PdfDetail[];
- description: string;
- tags: Tag[];
- action: {
- label: string;
- url: string;
- downloadedFileName?: string;
- };
- socials: Social[];
- config: SectionConfig;
-}
diff --git a/src/types/pdf.ts b/src/types/pdf.ts
deleted file mode 100644
index 7627364..0000000
--- a/src/types/pdf.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export interface Pdf {
- footer: string;
-}
diff --git a/src/types/portfolio-section.ts b/src/types/portfolio-section.ts
deleted file mode 100644
index 363d039..0000000
--- a/src/types/portfolio-section.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { Detail, PdfDetail, Photo, SectionConfig, Social, Tag } from './common';
-
-export interface Project {
- name: string;
- image: Photo;
- startDate: Date;
- endDate: Date | null;
- details: Detail[];
- pdfDetails?: PdfDetail[];
- description: string;
- tags: Tag[];
- socials: Social[];
-}
-export interface PortfolioSection {
- projects: Project[];
- config: SectionConfig;
-}
diff --git a/src/types/sections/education-section.types.ts b/src/types/sections/education-section.types.ts
new file mode 100644
index 0000000..d08f836
--- /dev/null
+++ b/src/types/sections/education-section.types.ts
@@ -0,0 +1,13 @@
+import type { DateRange, Description, LinkButton, Section } from '../shared';
+
+export interface Diploma {
+ title: string;
+ institution: string;
+ dates: DateRange;
+ description: Description;
+ links: LinkButton[];
+}
+
+export interface EducationSection extends Section {
+ diplomas: Diploma[];
+}
diff --git a/src/types/sections/experience-section.types.ts b/src/types/sections/experience-section.types.ts
new file mode 100644
index 0000000..b1bb4cc
--- /dev/null
+++ b/src/types/sections/experience-section.types.ts
@@ -0,0 +1,14 @@
+import type { DateRange, Description, LinkButton, Section, TagsList } from '../shared';
+
+export interface Job {
+ role: string;
+ company: string;
+ dates: DateRange;
+ description: Description;
+ tagsList: TagsList;
+ links: LinkButton[];
+}
+
+export interface ExperienceSection extends Section {
+ jobs: Job[];
+}
diff --git a/src/types/favorites-section.ts b/src/types/sections/favorites-section.types.ts
similarity index 51%
rename from src/types/favorites-section.ts
rename to src/types/sections/favorites-section.types.ts
index 75ee21a..5351c0c 100644
--- a/src/types/favorites-section.ts
+++ b/src/types/sections/favorites-section.types.ts
@@ -1,10 +1,10 @@
-import type { Photo, SectionConfig } from './common';
+import type { Photo, Section } from '../shared';
export interface Book {
title: string;
- cover: Photo;
+ image: Photo;
author: string;
- url?: string;
+ url: string;
}
export interface Person {
@@ -15,7 +15,7 @@ export interface Person {
export interface Video {
title: string;
- thumbnail: Photo;
+ image: Photo;
url: string;
}
@@ -26,15 +26,14 @@ export interface Media {
url: string;
}
-interface SubSection {
+export interface SubSection {
title: string;
data: Data[];
}
-export interface FavoritesSection {
- config: SectionConfig;
- books: SubSection;
- people: SubSection;
- videos: SubSection;
- medias: SubSection;
+export interface FavoritesSection extends Section {
+ books?: SubSection;
+ people?: SubSection;
+ videos?: SubSection;
+ medias?: SubSection;
}
diff --git a/src/types/sections/main-section.types.ts b/src/types/sections/main-section.types.ts
new file mode 100644
index 0000000..64ff592
--- /dev/null
+++ b/src/types/sections/main-section.types.ts
@@ -0,0 +1,13 @@
+import type { DownloadButton, Photo, LabelledValue, LinkButton, Section, Tag } from '../shared';
+
+export interface MainSection extends Section {
+ image: Photo;
+ fullName: string;
+ role: string;
+ details: LabelledValue[];
+ pdfDetails?: LabelledValue[];
+ description: string;
+ tags: Tag[];
+ action: DownloadButton;
+ links: LinkButton[];
+}
diff --git a/src/types/sections/portfolio-section.types.ts b/src/types/sections/portfolio-section.types.ts
new file mode 100644
index 0000000..23f525d
--- /dev/null
+++ b/src/types/sections/portfolio-section.types.ts
@@ -0,0 +1,16 @@
+import type { DateRange, Photo, LabelledValue, LinkButton, Section, TagsList, Description } from '../shared';
+
+export interface Project {
+ name: string;
+ image: Photo;
+ dates: DateRange;
+ details: LabelledValue[];
+ pdfDetails?: LabelledValue[];
+ description: Description;
+ tagsList: TagsList;
+ links: LinkButton[];
+}
+
+export interface PortfolioSection extends Section {
+ projects: Project[];
+}
diff --git a/src/types/sections/skills-section.types.ts b/src/types/sections/skills-section.types.ts
new file mode 100644
index 0000000..6dffbcd
--- /dev/null
+++ b/src/types/sections/skills-section.types.ts
@@ -0,0 +1,18 @@
+import type { Section, Tag } from '../shared';
+
+export interface Skill extends Tag {}
+
+export type SkillLevel = 1 | 2 | 3 | 4 | 5;
+
+export interface LevelledSkill extends Skill {
+ level: SkillLevel;
+}
+
+export interface SkillSet {
+ title: string;
+ skills: Skill[] | LevelledSkill[];
+}
+
+export interface SkillsSection extends Section {
+ skillSets: SkillSet[];
+}
diff --git a/src/types/sections/testimonials-section.types.ts b/src/types/sections/testimonials-section.types.ts
new file mode 100644
index 0000000..cf8fe9e
--- /dev/null
+++ b/src/types/sections/testimonials-section.types.ts
@@ -0,0 +1,13 @@
+import type { Photo, LinkButton, Section } from '../shared';
+
+export interface Testimonial {
+ image: Photo;
+ author: string;
+ relation: string;
+ content: string;
+ links: LinkButton[];
+}
+
+export interface TestimonialsSection extends Section {
+ testimonials: Testimonial[];
+}
diff --git a/src/types/seo.ts b/src/types/seo.ts
deleted file mode 100644
index 3d1cba6..0000000
--- a/src/types/seo.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-export interface Seo {
- /**
- * Title that will be displayed in Google search results and as tab name. To be fully visible in search results, it should be no longer than 60 characters.
- */
- title: string;
-
- /**
- * Description that will be displayed in Google search results. To be fully visible in search results, it should be no longer than 160 characters.
- */
- description: string;
- /**
- * Path to the image inside the public folder that will be used as page icon and as a preview image in site links.
- */
- image?: string;
-}
diff --git a/src/types/shared.ts b/src/types/shared.ts
new file mode 100644
index 0000000..85fab3d
--- /dev/null
+++ b/src/types/shared.ts
@@ -0,0 +1,52 @@
+import type { CircleFlags, Fa6Brands, Fa6Solid, Ri, SimpleIcons } from 'iconify-icon-names';
+
+export type IconName = Fa6Brands | Fa6Solid | SimpleIcons | CircleFlags | Ri;
+
+export type Photo = Promise<{ default: ImageMetadata }> | string;
+
+export type DateRange = [from: Date, to: Date | null];
+
+export type Description = string | string[];
+
+export interface SectionConfig {
+ title: string;
+ slug: string;
+ icon: IconName;
+ visible: boolean;
+}
+
+export interface Section {
+ config: SectionConfig;
+}
+
+export interface LabelledValue {
+ label: string;
+ value: string | string[];
+ url?: string;
+ fullRow?: boolean;
+}
+
+export interface Tag {
+ name: string;
+ icon?: IconName;
+ iconColor?: string;
+ url?: string;
+ description?: string;
+}
+
+export interface TagsList {
+ title: string;
+ tags: Tag[];
+}
+
+export interface DownloadButton {
+ label: string;
+ url: string;
+ downloadedFileName?: string;
+}
+
+export interface LinkButton {
+ name: string;
+ icon: IconName;
+ url: string;
+}
diff --git a/src/types/skills-section.ts b/src/types/skills-section.ts
deleted file mode 100644
index ade3e83..0000000
--- a/src/types/skills-section.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { SectionConfig, Tag } from './common';
-
-export interface LevelledSkill extends Tag {
- level: 1 | 2 | 3 | 4 | 5;
-}
-
-export interface SkillSet {
- title: string;
- skills: SkillType[];
- pdfTitle?: string;
- excludeFromPdf?: boolean;
-}
-
-export interface SkillsSection {
- skillSets: (SkillSet | SkillSet)[];
- config: SectionConfig;
-}
diff --git a/src/types/testimonials-section.ts b/src/types/testimonials-section.ts
deleted file mode 100644
index 539de41..0000000
--- a/src/types/testimonials-section.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import type { Photo, SectionConfig, Social } from './common';
-
-export interface Testimonial {
- image: Photo;
- author: string;
- relation: string;
- content: string;
- socials: Social[];
-}
-
-export interface TestimonialsSection {
- testimonials: Testimonial[];
- config: SectionConfig;
-}
diff --git a/src/utils/create-link-factory.ts b/src/utils/create-link-factory.ts
new file mode 100644
index 0000000..f755ca6
--- /dev/null
+++ b/src/utils/create-link-factory.ts
@@ -0,0 +1,15 @@
+import type { LinkButton } from '@/types/shared';
+import type { Merge } from 'type-fest';
+
+type LinkWithoutUrl = Omit;
+type PartialLinkWithUrl = Partial & { url: string };
+
+const createLinkFactory =
+ (defaultData: Readonly ) =>
+ (override: Readonly) =>
+ ({
+ ...defaultData,
+ ...override,
+ } as Readonly>);
+
+export default createLinkFactory;
diff --git a/src/utils/create-skill-factory.ts b/src/utils/create-skill-factory.ts
new file mode 100644
index 0000000..930dc12
--- /dev/null
+++ b/src/utils/create-skill-factory.ts
@@ -0,0 +1,16 @@
+import type { LevelledSkill, Skill, SkillLevel } from '@/types/sections/skills-section.types';
+import type { Merge } from 'type-fest';
+
+type SkillWithoutDescription = Omit;
+
+interface SkillFactory {
+ >(data: Readonly): Readonly>;
+ | undefined = undefined>(data?: Readonly): T extends undefined
+ ? Readonly
+ : Readonly>;
+}
+
+const createSkillFactory = (defaultData: Readonly) =>
+ ((data: Record) => ({ ...defaultData, ...data })) as SkillFactory;
+
+export default createSkillFactory;
diff --git a/src/utils/date-formatter.ts b/src/utils/date-formatter.ts
deleted file mode 100644
index 2cdf6ba..0000000
--- a/src/utils/date-formatter.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-const getDateFormatter =
- (locale: string) =>
- (date: Date): string =>
- new Intl.DateTimeFormat(locale, { month: 'long' }).format(date).concat(' ', date.getFullYear().toString());
-
-export default getDateFormatter;
diff --git a/src/utils/format-date-range.ts b/src/utils/format-date-range.ts
new file mode 100644
index 0000000..e8b58b1
--- /dev/null
+++ b/src/utils/format-date-range.ts
@@ -0,0 +1,10 @@
+import { format } from 'date-fns';
+import type { DateRange } from '@/types/shared';
+import config from '@/data/config';
+
+const { locale, dateFormat, translations } = config.i18n;
+
+const formatDateRange = ([from, to]: DateRange): string =>
+ format(from, dateFormat, { locale }).concat(' - ', to ? format(to, dateFormat, { locale }) : translations.now);
+
+export default formatDateRange;
diff --git a/src/utils/is-section-key.ts b/src/utils/is-section-key.ts
deleted file mode 100644
index ab4c8b7..0000000
--- a/src/utils/is-section-key.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import type { SectionKey } from '@/types/data';
-
-const sectionsMap: Record = {
- main: 'main',
- skills: 'skills',
- experience: 'experience',
- portfolio: 'portfolio',
- testimonials: 'testimonials',
- favorites: 'favorites',
- education: 'education',
-};
-
-const isSectionKey = (key: string): key is SectionKey => Object.keys(sectionsMap).includes(key as SectionKey);
-
-export default isSectionKey;
diff --git a/src/utils/remove-last.ts b/src/utils/remove-last.ts
deleted file mode 100644
index da7d670..0000000
--- a/src/utils/remove-last.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-/**
- * Get all elements of array except last one.
- */
-const removeLast = (arr: T[]): T[] => arr.slice(0, -1);
-
-export default removeLast;
diff --git a/src/web/components/description.astro b/src/web/components/description.astro
new file mode 100644
index 0000000..5880c01
--- /dev/null
+++ b/src/web/components/description.astro
@@ -0,0 +1,29 @@
+---
+import type { Description } from '@/types/shared';
+import Typography from './typography.astro';
+
+export interface Props {
+ content: Description;
+ class?: string;
+}
+
+const { content, ...props } = Astro.props;
+---
+
+
+ {
+ Array.isArray(content) ? (
+
+ {content.map((line) => (
+
+ {line}
+
+ ))}
+
+ ) : (
+
+ {content}
+
+ )
+ }
+
diff --git a/src/components/divided-list.astro b/src/web/components/divided-list.astro
similarity index 100%
rename from src/components/divided-list.astro
rename to src/web/components/divided-list.astro
diff --git a/src/web/components/divider.astro b/src/web/components/divider.astro
new file mode 100644
index 0000000..3b05f3f
--- /dev/null
+++ b/src/web/components/divider.astro
@@ -0,0 +1 @@
+
diff --git a/src/components/button.astro b/src/web/components/download-button.astro
similarity index 54%
rename from src/components/button.astro
rename to src/web/components/download-button.astro
index 5904464..9c58632 100644
--- a/src/components/button.astro
+++ b/src/web/components/download-button.astro
@@ -1,10 +1,9 @@
---
-export interface Props {
- href: string;
- download?: astroHTML.JSX.AnchorHTMLAttributes['download'];
-}
+import type { DownloadButton } from '@/types/shared';
-const { href, download } = Astro.props;
+export interface Props extends DownloadButton {}
+
+const { url, downloadedFileName, label } = Astro.props;
const classes = /* tw */ {
main: 'inline-flex items-center px-4 h-10 text-base font-medium rounded-md shadow-sm text-white bg-primary-600 select-none cursor-pointer',
@@ -14,6 +13,6 @@ const classes = /* tw */ {
};
---
-
-
+
+ {label}
diff --git a/src/components/icon.astro b/src/web/components/icon.astro
similarity index 88%
rename from src/components/icon.astro
rename to src/web/components/icon.astro
index 2886c5b..dcbd461 100644
--- a/src/components/icon.astro
+++ b/src/web/components/icon.astro
@@ -1,5 +1,5 @@
---
-import type { IconName } from '@/types/icon';
+import type { IconName } from '@/types/shared';
export interface Props {
name: IconName;
diff --git a/src/web/components/labelled-value.astro b/src/web/components/labelled-value.astro
new file mode 100644
index 0000000..e68ea5a
--- /dev/null
+++ b/src/web/components/labelled-value.astro
@@ -0,0 +1,22 @@
+---
+import type { LabelledValue } from '@/types/shared';
+
+export interface Props extends LabelledValue {}
+
+const { label, value, url } = Astro.props;
+
+const parsedValue = Array.isArray(value) ? value.join(', ') : value;
+---
+
+
+
{label}:
+ {
+ url ? (
+
+ {parsedValue}
+
+ ) : (
+
{parsedValue}
+ )
+ }
+
diff --git a/src/web/components/layout.astro b/src/web/components/layout.astro
new file mode 100644
index 0000000..ec8948d
--- /dev/null
+++ b/src/web/components/layout.astro
@@ -0,0 +1,41 @@
+---
+import type { Data } from '@/types/data';
+
+export interface Props extends Pick {}
+
+const { seo, i18n } = Astro.props;
+---
+
+
+
+
+
+
+
+ {seo.title}
+
+
+
+
+ {seo.ogImage && }
+
+
+
+
+
+
diff --git a/src/web/components/link-button.astro b/src/web/components/link-button.astro
new file mode 100644
index 0000000..69c2463
--- /dev/null
+++ b/src/web/components/link-button.astro
@@ -0,0 +1,18 @@
+---
+import type { LinkButton } from '@/types/shared';
+import Icon from './icon.astro';
+
+export interface Props extends LinkButton {}
+
+const { name, icon, url } = Astro.props;
+
+const classes = /* tw */ {
+ main: 'flex items-center justify-center w-7 h-7 rounded text-gray-400 bg-gray-100 dark:bg-gray-600 dark:text-gray-200',
+ active: 'active:translate-y-px',
+ focus: 'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
+};
+---
+
+
+
+
diff --git a/src/web/components/photo.astro b/src/web/components/photo.astro
new file mode 100644
index 0000000..51ff4a4
--- /dev/null
+++ b/src/web/components/photo.astro
@@ -0,0 +1,18 @@
+---
+import type { Photo } from '@/types/shared';
+import { Image } from '@astrojs/image/components';
+
+export interface Props {
+ src: Photo;
+ alt: string;
+ class?: string;
+ width?: number;
+ height?: number;
+}
+
+const { src, ...props } = Astro.props;
+
+const isRemoteImage = typeof src === 'string';
+---
+
+{isRemoteImage ? : }
diff --git a/src/web/components/section-card.astro b/src/web/components/section-card.astro
new file mode 100644
index 0000000..a933419
--- /dev/null
+++ b/src/web/components/section-card.astro
@@ -0,0 +1,43 @@
+---
+import type { SectionConfig } from '@/types/shared';
+import Typography from './typography.astro';
+
+export interface Props extends SectionConfig {
+ className?: string;
+ hideTitle?: boolean;
+}
+
+const { title, slug, hideTitle, className, visible } = Astro.props;
+---
+
+{
+ visible && (
+
+ {!hideTitle && {title} }
+
+
+ )
+}
+
+
diff --git a/src/components/sidebar-item.astro b/src/web/components/sidebar-item.astro
similarity index 50%
rename from src/components/sidebar-item.astro
rename to src/web/components/sidebar-item.astro
index dcd8174..fc1c717 100644
--- a/src/components/sidebar-item.astro
+++ b/src/web/components/sidebar-item.astro
@@ -1,24 +1,18 @@
---
-import type { SectionKey } from '@/types/data';
-import type { IconName } from '@/types/icon';
+import type { SectionConfig } from '@/types/shared';
import Icon from './icon.astro';
-export interface Props {
- section: SectionKey;
- icon: IconName;
- title?: string;
-}
+export interface Props extends SectionConfig {}
-const { section, icon, title = '', ...props } = Astro.props;
+const { icon, slug, title } = Astro.props;
---
diff --git a/src/components/sidebar.astro b/src/web/components/sidebar.astro
similarity index 59%
rename from src/components/sidebar.astro
rename to src/web/components/sidebar.astro
index 3c9c547..1815122 100644
--- a/src/components/sidebar.astro
+++ b/src/web/components/sidebar.astro
@@ -1,25 +1,15 @@
---
-import type { Data } from '@/data';
-import isSectionKey from '@/utils/is-section-key';
+import type { Data } from '@/types/data';
import SidebarItem from './sidebar-item.astro';
export interface Props {
className?: string;
- data: Data;
+ sections: Data['sections'];
}
-const { className, data } = Astro.props;
-
-const sections = Object.keys(data).flatMap((key) => {
- if (!isSectionKey(key)) return [];
-
- const section = data[key];
-
- if (!section) return [];
-
- return [{ title: section.config.title, icon: section.config.icon, section: key }];
-});
+const { className, sections } = Astro.props;
+const sectionConfigs = Object.values(sections).map((section) => section.config);
---
{
className,
]}
>
- {sections.map((section) => )}
+ {sectionConfigs.filter((config) => config.visible).map((config) => )}