Data structure refactor (#164)
This commit is contained in:
parent
5afdfe9e16
commit
b2650d771d
146 changed files with 1501 additions and 2169 deletions
14
.github/workflows/check-code-quality.yml
vendored
14
.github/workflows/check-code-quality.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
37
package-lock.json
generated
37
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
public/cv.pdf
BIN
public/cv.pdf
Binary file not shown.
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
<div class="h-px w-full bg-gray-200 dark:bg-gray-600"></div>
|
||||
|
|
@ -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<IconButtonSize, string> = {
|
||||
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',
|
||||
};
|
||||
---
|
||||
|
||||
<a href={href} target={target} class:list={[classes.main, classes.active, classes.focus, sizeMap[size]]} {...rest}>
|
||||
<Icon name={icon} size={16} />
|
||||
</a>
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
---
|
||||
export interface Props {
|
||||
label: string;
|
||||
value: string | string[];
|
||||
}
|
||||
|
||||
const { label, value } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="text-base">
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300">{label}:</span>
|
||||
<span class="font-normal text-gray-500 dark:text-gray-400">{value}</span>
|
||||
</div>
|
||||
|
|
@ -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 ? (
|
||||
<img class={className} src={src} {...props} />
|
||||
) : (
|
||||
<Image format="webp" fit="cover" class={className} src={src} loading={loading} {...props} />
|
||||
)
|
||||
}
|
||||
|
|
@ -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;
|
||||
---
|
||||
|
||||
<div
|
||||
data-type="section"
|
||||
id={section}
|
||||
class:list={['flex flex-col gap-6 rounded-2xl bg-white p-8 shadow-md dark:bg-gray-800', className]}
|
||||
>
|
||||
{
|
||||
title && (
|
||||
<Typography variant="section-title" id={`${section}-heading`}>
|
||||
{title}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import hashState from '@/utils/hash-state';
|
||||
import throttle from '@/utils/throttle';
|
||||
|
||||
const sections = [...document.querySelectorAll('[data-type="section"]')];
|
||||
|
||||
const isInUpperView = (section: Element) => section.getBoundingClientRect().bottom >= window.innerHeight / 3;
|
||||
|
||||
const updateHash = () => {
|
||||
const currentSection = sections.find(isInUpperView);
|
||||
|
||||
if (currentSection) {
|
||||
hashState.updateHash(currentSection.id);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('scroll', throttle(updateHash, 200));
|
||||
</script>
|
||||
|
|
@ -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;
|
||||
---
|
||||
|
||||
<div
|
||||
class="flex h-6 w-fit items-center gap-x-1.5 rounded bg-gray-100 px-2.5 text-sm font-medium tracking-wide text-gray-700 dark:bg-gray-700 dark:text-gray-100"
|
||||
>
|
||||
{name && <Icon name={name} color={color} size={16} />}
|
||||
<slot />
|
||||
</div>
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
---
|
||||
import type { Tag } from '@/types/common';
|
||||
|
||||
import TagComponent from './tag.astro';
|
||||
|
||||
export interface Props {
|
||||
tags: Tag[];
|
||||
}
|
||||
|
||||
const { tags } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
{
|
||||
tags.map(({ name: tagName, icon, iconColor, url }) =>
|
||||
url ? (
|
||||
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||
<TagComponent name={icon} color={iconColor}>
|
||||
{tagName}
|
||||
</TagComponent>
|
||||
</a>
|
||||
) : (
|
||||
<TagComponent name={icon} color={iconColor}>
|
||||
{tagName}
|
||||
</TagComponent>
|
||||
)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
|
@ -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);
|
||||
---
|
||||
|
||||
<Typography variant="item-subtitle">
|
||||
{getFormattedDate(startDate).concat(' - ', endDate ? getFormattedDate(endDate) : translationForNow)}
|
||||
</Typography>
|
||||
25
src/data/config.ts
Normal file
25
src/data/config.ts
Normal file
|
|
@ -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<Config>;
|
||||
|
||||
export default config;
|
||||
11
src/data/cv.ts
Normal file
11
src/data/cv.ts
Normal file
|
|
@ -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<Data>;
|
||||
|
||||
export type PreciseData = typeof data;
|
||||
|
||||
export const cv = transformData(data);
|
||||
146
src/data/helpers/links.ts
Normal file
146
src/data/helpers/links.ts
Normal file
|
|
@ -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',
|
||||
});
|
||||
|
|
@ -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<Tag> & { level: LevelledSkill['level'] }): LevelledSkill;
|
||||
(data?: Partial<Tag>): Tag;
|
||||
}
|
||||
|
||||
const createSkill = (defaultData: Omit<Tag, 'description'>) =>
|
||||
((data?: Partial<Tag> & { 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',
|
||||
|
|
@ -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;
|
||||
30
src/data/sections/education-section.data.ts
Normal file
30
src/data/sections/education-section.data.ts
Normal file
|
|
@ -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<EducationSection>;
|
||||
|
||||
export default educationSectionData;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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.',
|
||||
],
|
||||
tagsList: {
|
||||
title: 'Technologies',
|
||||
tags: [react(), nextJs(), typescript(), nx(), firebase()],
|
||||
socials: [facebook('#'), linkedin('#')],
|
||||
},
|
||||
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.',
|
||||
],
|
||||
tagsList: {
|
||||
title: 'Technologies',
|
||||
tags: [react(), reactQuery(), chakraUi(), eslint()],
|
||||
socials: [website('#'), instagram('#')],
|
||||
},
|
||||
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.',
|
||||
],
|
||||
tagsList: {
|
||||
title: 'Technologies',
|
||||
tags: [vue(), tailwindCss(), pnpm()],
|
||||
socials: [twitter('#'), github('#')],
|
||||
},
|
||||
links: [twitter({ url: '#' }), github({ url: '#' })],
|
||||
},
|
||||
],
|
||||
};
|
||||
} as const satisfies ReadonlyDeep<ExperienceSection>;
|
||||
|
||||
export default experienceData;
|
||||
export default experienceSectionData;
|
||||
|
|
@ -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<FavoritesSection>;
|
||||
|
||||
export default favoritesData;
|
||||
export default favoritesSectionData;
|
||||
21
src/data/sections/index.ts
Normal file
21
src/data/sections/index.ts
Normal file
|
|
@ -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<Sections>;
|
||||
|
||||
export default sections;
|
||||
|
|
@ -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<MainSection>;
|
||||
|
||||
export default mainData;
|
||||
export default mainSectionData;
|
||||
|
|
@ -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.',
|
||||
tagsList: {
|
||||
title: 'Technologies',
|
||||
tags: [nextJs(), sass(), pnpm(), eslint(), prettier()],
|
||||
socials: [mockups('#'), demo('#')],
|
||||
},
|
||||
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.',
|
||||
tagsList: {
|
||||
title: 'Technologies',
|
||||
tags: [react(), tailwindCss(), nestJs(), postgreSql()],
|
||||
socials: [mockups('#'), demo('#')],
|
||||
},
|
||||
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.',
|
||||
tagsList: {
|
||||
title: 'Technologies',
|
||||
tags: [react(), chakraUi(), typescript(), nx(), pnpm()],
|
||||
socials: [website('#'), github('#')],
|
||||
},
|
||||
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. ',
|
||||
tagsList: {
|
||||
title: 'Technologies',
|
||||
tags: [typescript(), jest(), firebase()],
|
||||
socials: [mockups('#'), github('#')],
|
||||
},
|
||||
links: [mockups({ url: '#' }), github({ url: '#' })],
|
||||
},
|
||||
],
|
||||
};
|
||||
} as const satisfies ReadonlyDeep<PortfolioSection>;
|
||||
|
||||
export default portfolioData;
|
||||
export default portfolioSectionData;
|
||||
|
|
@ -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<SkillsSection>;
|
||||
|
||||
export default skillsData;
|
||||
export default skillsSectionData;
|
||||
|
|
@ -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<TestimonialsSection>;
|
||||
|
||||
export default testimonialsData;
|
||||
export default testimonialsSectionData;
|
||||
|
|
@ -1,200 +0,0 @@
|
|||
import type { Social } from '../types/common';
|
||||
|
||||
type SocialWithoutUrl = Omit<Social, 'url'>;
|
||||
|
||||
// GENERAL
|
||||
|
||||
export const facebook = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'Facebook',
|
||||
icon: 'fa6-brands:facebook-f',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
|
||||
export const linkedin = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'LinkedIn',
|
||||
icon: 'fa6-brands:linkedin-in',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
|
||||
export const twitter = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'Twitter',
|
||||
icon: 'fa6-brands:twitter',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
|
||||
export const pinterest = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'Pinterest',
|
||||
icon: 'fa6-brands:pinterest',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
|
||||
// CODE
|
||||
|
||||
export const github = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'GitHub',
|
||||
icon: 'fa6-brands:github',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
|
||||
export const codepen = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'CodePen',
|
||||
icon: 'fa6-brands:codepen',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
|
||||
export const stackblitz = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'StackBlitz',
|
||||
icon: 'simple-icons:stackblitz',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
|
||||
export const codesandbox = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'CodeSandbox',
|
||||
icon: 'simple-icons:codesandbox',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
|
||||
// BLOG
|
||||
|
||||
export const dev = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'Dev',
|
||||
icon: 'fa6-brands:dev',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
|
||||
export const medium = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'Medium',
|
||||
icon: 'fa6-brands:medium',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
|
||||
// FORUM / CHAT
|
||||
|
||||
export const reddit = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'Reddit',
|
||||
icon: 'fa6-brands:reddit',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
|
||||
export const quora = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'Quora',
|
||||
icon: 'fa6-brands:quora',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
|
||||
export const stackoverflow = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'Stack Overflow',
|
||||
icon: 'fa6-brands:stack-overflow',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
|
||||
// DESIGN
|
||||
|
||||
export const instagram = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'Instagram',
|
||||
icon: 'fa6-brands:instagram',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
export const behance = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'Behance',
|
||||
icon: 'fa6-brands:behance',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
|
||||
export const dribbble = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'Dribbble',
|
||||
icon: 'fa6-brands:dribbble',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
|
||||
export const figma = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'Figma',
|
||||
icon: 'fa6-brands:figma',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
|
||||
// MUSIC
|
||||
|
||||
export const spotify = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'Spotify',
|
||||
icon: 'fa6-brands:spotify',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
|
||||
export const soundcloud = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'SoundCloud',
|
||||
icon: 'fa6-brands:soundcloud',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
|
||||
// VIDEO
|
||||
|
||||
export const youtube = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'YouTube',
|
||||
icon: 'fa6-brands:youtube',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
|
||||
export const twitch = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'Twitch',
|
||||
icon: 'fa6-brands:twitch',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
|
||||
export const vimeo = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'Vimeo',
|
||||
icon: 'fa6-brands:vimeo',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
|
||||
// PROJECT TYPE
|
||||
|
||||
export const website = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'Website',
|
||||
icon: 'fa6-solid:globe',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
|
||||
export const demo = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'App demo',
|
||||
icon: 'fa6-solid:desktop',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
|
||||
export const mockups = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'Mockups',
|
||||
icon: 'fa6-solid:image',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
|
||||
export const repository = (url: string, override?: Partial<SocialWithoutUrl>): Social => ({
|
||||
name: 'Repository',
|
||||
icon: 'fa6-solid:code-branch',
|
||||
url,
|
||||
...override,
|
||||
});
|
||||
1
src/data/transformers/index.ts
Normal file
1
src/data/transformers/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './transformers';
|
||||
14
src/data/transformers/transform-data.ts
Normal file
14
src/data/transformers/transform-data.ts
Normal file
|
|
@ -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;
|
||||
92
src/data/transformers/transformers.ts
Normal file
92
src/data/transformers/transformers.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import type { Data } from '@/types/data';
|
||||
import type { Draft } from 'immer';
|
||||
import type { PreciseData } from '../cv';
|
||||
|
||||
export type DraftData = Draft<Data>;
|
||||
|
||||
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<T extends Readonly<unknown[]>, P> = T extends Readonly<[infer A, ...infer Rest]>
|
||||
? [...(A extends P ? [A] : []), ...Filter<Rest, P>]
|
||||
: [];
|
||||
|
||||
type SkillsBySkillSet<SkillSet extends SkillSetTitle> = 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 =
|
||||
<SkillSet extends SkillSetTitle>(
|
||||
skillSetTitle: SkillSetTitle,
|
||||
skills: SkillsBySkillSet<SkillSet>[]
|
||||
): 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])),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
import Button from '@/components/button.astro';
|
||||
---
|
||||
|
||||
<div class="p-5">
|
||||
<Button href="#">Button text</Button>
|
||||
</div>
|
||||
|
|
@ -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',
|
||||
};
|
||||
---
|
||||
|
||||
<Typography class="m-2 font-mono" variant="item-title">Favourite Book</Typography>
|
||||
<div class="m-4 grid grid-cols-8">
|
||||
<BookTile value={book} />
|
||||
</div>
|
||||
<Typography class="m-2 font-mono" variant="item-title">Favourite Person</Typography>
|
||||
<div class="m-4 grid grid-cols-12">
|
||||
<PersonTile value={person} />
|
||||
</div>
|
||||
<Typography class="m-2 font-mono" variant="item-title">Favourite Video</Typography>
|
||||
<div class="m-4 grid grid-cols-6">
|
||||
<VideoTile value={video} />
|
||||
</div>
|
||||
<Typography class="m-2 font-mono" variant="item-title">Favourite Media</Typography>
|
||||
<div class="m-4 grid grid-cols-12">
|
||||
<MediaTile value={media} />
|
||||
</div>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
---
|
||||
import IconButton from '@/components/icon-button.astro';
|
||||
---
|
||||
|
||||
<div class="flex flex-col gap-2 p-5">
|
||||
<IconButton icon="fa6-brands:facebook-f" size="small" href="#" />
|
||||
<IconButton icon="fa6-brands:github" size="small" href="#" />
|
||||
<IconButton icon="fa6-brands:linkedin-in" size="large" href="#" />
|
||||
<IconButton icon="fa6-brands:twitter" size="large" href="#" />
|
||||
</div>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
import Icon from '@/components/icon.astro';
|
||||
---
|
||||
|
||||
<div class="p-5">
|
||||
<!-- Available icon names here: https://icon-sets.iconify.design -->
|
||||
<!-- Colors for simple icons here: https://simpleicons.org -->
|
||||
<Icon name="simple-icons:react" size={24} color="#61DAFB" />
|
||||
</div>
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
import { Image } from '@astrojs/image/components';
|
||||
---
|
||||
|
||||
<div class="p-5">
|
||||
<Image src={import('@/assets/my-image.jpeg')} alt="My image" width={120} height={120} format="webp" />
|
||||
</div>
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
import LabelledValue from '@/components/labelled-value.astro';
|
||||
---
|
||||
|
||||
<div class="p-5">
|
||||
<LabelledValue label="Label" value="value" />
|
||||
</div>
|
||||
|
|
@ -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: '#' },
|
||||
],
|
||||
};
|
||||
---
|
||||
|
||||
<body class="flex justify-center bg-gray-50">
|
||||
<div class="flex w-full max-w-6xl gap-8 px-2 py-3 sm:px-8 sm:py-12 lg:ml-22 lg:px-16 lg:py-20">
|
||||
<main class="w-full space-y-4 sm:space-y-6 lg:space-y-8">
|
||||
<MainSection {...mainSectionData} />
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
|
|
@ -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',
|
||||
},
|
||||
};
|
||||
---
|
||||
|
||||
<div class="p-5">
|
||||
<ProjectTimelineItem project={project} i18n={i18nData} />
|
||||
</div>
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
import SectionCard from '@/components/section-card.astro';
|
||||
---
|
||||
|
||||
<div class="p-5">
|
||||
<SectionCard section="main">SectionCard text</SectionCard>
|
||||
</div>
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
import SidebarItem from '@/components/sidebar-item.astro';
|
||||
---
|
||||
|
||||
<div class="p-5">
|
||||
<SidebarItem icon="fa6-solid:bars-progress" section="experience" />
|
||||
</div>
|
||||
|
|
@ -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/',
|
||||
};
|
||||
---
|
||||
|
||||
<Skill {...levelledSkill} />
|
||||
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
---
|
||||
|
||||
<body class="flex justify-center bg-gray-50">
|
||||
<div class="flex w-full max-w-6xl gap-8 px-2 py-3 sm:px-8 sm:py-12 lg:ml-22 lg:px-16 lg:py-20">
|
||||
<main class="w-full space-y-4 sm:space-y-6 lg:space-y-8">
|
||||
<SkillsSection config={skills.config} skillSets={skills.skillSets} />
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
---
|
||||
import Tag from '@/components/tag.astro';
|
||||
---
|
||||
|
||||
<div class="p-5">
|
||||
<Tag>Tag text</Tag>
|
||||
</div>
|
||||
|
||||
<div class="p-5">
|
||||
<Tag name="simple-icons:react" color="#61DAFB">Tag text</Tag>
|
||||
</div>
|
||||
|
|
@ -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: '#' },
|
||||
],
|
||||
};
|
||||
---
|
||||
|
||||
<div class="flex max-w-[896px] flex-col gap-2">
|
||||
<Testimonial testimonial={testimonial} />
|
||||
</div>
|
||||
|
|
@ -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: '#' },
|
||||
],
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<body class="flex justify-center bg-gray-50">
|
||||
<div class="flex w-full max-w-6xl gap-8 px-2 py-3 sm:px-8 sm:py-12 lg:ml-22 lg:px-16 lg:py-20">
|
||||
<main class="w-full space-y-4 sm:space-y-6 lg:space-y-8">
|
||||
<TestimonialsSection testimonials={testimonials} config={{ title: 'Testimonials', icon: 'fa6-solid:comment' }} />
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
---
|
||||
import Typography from '@/components/typography.astro';
|
||||
|
||||
const text = 'A quick brown fox jumps over the lazy dog';
|
||||
---
|
||||
|
||||
<div class="space-y-10 p-5">
|
||||
<div>
|
||||
<p class="mb-2 font-mono">paragraph (default)</p>
|
||||
<Typography>{text}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 font-mono">main-title</p>
|
||||
<Typography variant="main-title">{text}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 font-mono">main-subtitle</p>
|
||||
<Typography variant="main-subtitle">{text}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 font-mono">section-title</p>
|
||||
<Typography variant="section-title">{text}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 font-mono">section-subtitle</p>
|
||||
<Typography variant="section-subtitle">{text}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 font-mono">item-title</p>
|
||||
<Typography variant="item-title">{text}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 font-mono">item-title-suffix</p>
|
||||
<Typography variant="item-title-suffix">{text}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 font-mono">item-subtitle</p>
|
||||
<Typography variant="item-subtitle">{text}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 font-mono">tile-title</p>
|
||||
<Typography variant="tile-title">{text}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 font-mono">tile-subtitle</p>
|
||||
<Typography variant="tile-subtitle">{text}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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',
|
||||
},
|
||||
};
|
||||
---
|
||||
|
||||
<div class="p-5">
|
||||
<WorkTimelineItem job={job} i18n={i18nData} />
|
||||
</div>
|
||||
|
|
@ -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();
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="scroll-smooth">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{seo.title}</title>
|
||||
<meta name="description" content={seo.description} />
|
||||
<link rel="icon" type={seo.image ? 'image/jpeg' : 'image/svg+xml'} href={seoImage} />
|
||||
<meta property="og:title" content={seo.title} />
|
||||
<meta property="og:description" content={seo.description} />
|
||||
<meta property="og:image" content={seoImage} />
|
||||
<script is:inline>
|
||||
const theme = (() => {
|
||||
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
|
||||
return localStorage.getItem('theme');
|
||||
}
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
})();
|
||||
|
||||
if (theme === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
|
||||
window.localStorage.setItem('theme', theme);
|
||||
</script>
|
||||
</head>
|
||||
<body class="flex justify-center overflow-x-hidden bg-gray-50 dark:bg-gray-900 xl:relative xl:left-7">
|
||||
<Layout {...config}>
|
||||
<ThemeToggle />
|
||||
<main class="w-full max-w-5xl space-y-4 px-2 py-3 sm:space-y-6 sm:px-8 sm:py-12 lg:space-y-8 lg:py-20">
|
||||
<MainSection {...data.main} />
|
||||
{data.skills && <SkillsSection {...data.skills} />}
|
||||
{data.experience && <ExperienceSection i18n={i18n} {...data.experience} />}
|
||||
{data.portfolio && <PortfolioSection i18n={i18n} {...data.portfolio} />}
|
||||
{data.education && <EducationSection i18n={i18n} {...data.education} />}
|
||||
{data.testimonials && <TestimonialsSection {...data.testimonials} />}
|
||||
{data.favorites && <FavoritesSection {...data.favorites} />}
|
||||
<MainSection {...sections.main} />
|
||||
<SkillsSection {...sections.skills} />
|
||||
<ExperienceSection {...sections.experience} />
|
||||
<PortfolioSection {...sections.portfolio} />
|
||||
<EducationSection {...sections.education} />
|
||||
<TestimonialsSection {...sections.testimonials} />
|
||||
<FavoritesSection {...sections.favorites} />
|
||||
</main>
|
||||
<Sidebar data={data} className="sticky top-8 mt-20" />
|
||||
<script src="../scripts/initialize-tooltips.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
<Sidebar sections={sections} className="sticky top-8 mt-20" />
|
||||
<script src="../web/scripts/initialize-tooltips.ts"></script>
|
||||
</Layout>
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
);
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang={config.i18n.locale.code}>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>PDF preview</title>
|
||||
</head>
|
||||
<body class="flex flex-col bg-white p-[10mm] print:p-0">
|
||||
<MainSection {...data.main} />
|
||||
{data.skills && <SkillsSection {...data.skills} />}
|
||||
{data.experience && <ExperienceSection i18n={i18n} {...data.experience} />}
|
||||
{data.portfolio && <PortfolioSection i18n={i18n} {...data.portfolio} />}
|
||||
{data.education && <EducationSection i18n={i18n} {...data.education} />}
|
||||
<Footer footer={data.pdf.footer} />
|
||||
<MainSection {...sections.main} />
|
||||
{sections.skills && <SkillsSection {...sections.skills} />}
|
||||
{sections.experience && <ExperienceSection {...sections.experience} />}
|
||||
{sections.portfolio && <PortfolioSection {...sections.portfolio} />}
|
||||
{sections.education && <EducationSection {...sections.education} />}
|
||||
{config.pdf?.footer && <Footer>{config.pdf.footer}</Footer>}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
---
|
||||
|
||||
<div
|
||||
|
|
@ -20,6 +16,5 @@ const getFormattedDate = getDateFormatter(i18n.locale);
|
|||
props.class,
|
||||
]}
|
||||
>
|
||||
{getFormattedDate(startDate)} -{' '}
|
||||
{endDate ? getFormattedDate(endDate) : i18n.translations.now}
|
||||
{formatDateRange(dates)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
---
|
||||
import type { Description } from '@/types/shared';
|
||||
|
||||
export interface Props {
|
||||
content: string | string[];
|
||||
content: Description;
|
||||
}
|
||||
|
||||
const { content } = Astro.props;
|
||||
|
|
|
|||
6
src/pdf/components/footer.astro
Normal file
6
src/pdf/components/footer.astro
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<div
|
||||
id="footer"
|
||||
class="mt-4 flex w-full justify-center rounded border border-gray-100 bg-gray-50 px-2 py-1 text-justify text-[11px]"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
|
@ -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;
|
||||
---
|
||||
|
||||
<div class:list={['flex gap-1 text-base font-normal text-gray-500', props.class]}>
|
||||
|
|
@ -13,10 +15,10 @@ const { label, value, url, ...props } = Astro.props;
|
|||
{
|
||||
url ? (
|
||||
<a href={url} class="underline">
|
||||
{value}
|
||||
{parsedValue}
|
||||
</a>
|
||||
) : (
|
||||
<div>{value}</div>
|
||||
<div>{parsedValue}</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
---
|
||||
|
||||
<div>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="text-lg font-extrabold text-gray-900">{title}</div>
|
||||
<DateRangeTag class="mt-0.5" i18n={i18n} startDate={startDate} endDate={endDate} />
|
||||
<DateRangeTag class="mt-0.5" dates={dates} />
|
||||
</div>
|
||||
{subtitle && <div class="text-md -mt-0.5 font-medium text-gray-700">{subtitle}</div>}
|
||||
</div>
|
||||
|
|
|
|||
24
src/pdf/components/photo.astro
Normal file
24
src/pdf/components/photo.astro
Normal file
|
|
@ -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 ? (
|
||||
<img src={src} {...props} />
|
||||
) : (
|
||||
<Image format="webp" fit="cover" loading="eager" src={src} {...props} />
|
||||
)
|
||||
}
|
||||
|
|
@ -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;
|
||||
---
|
||||
|
||||
<div class="text-base">
|
||||
<span class="font-medium text-gray-700">{label}:</span>
|
||||
<span class="font-medium text-gray-700">{title}:</span>
|
||||
<span class="font-normal text-gray-500">{tags.map((t) => t.name).join(', ')}</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
---
|
||||
import type { Pdf } from '@/types/pdf';
|
||||
|
||||
export interface Props {
|
||||
footer: Pdf['footer'];
|
||||
}
|
||||
|
||||
const { footer } = Astro.props;
|
||||
---
|
||||
|
||||
<div
|
||||
id="footer"
|
||||
class="mt-4 flex w-full justify-center rounded border border-gray-100 bg-gray-50 px-2 py-1 text-justify text-[11px]"
|
||||
>
|
||||
{footer}
|
||||
</div>
|
||||
<script>
|
||||
const footer = document.getElementById('footer')!;
|
||||
const withClause = window.location.search.includes('clause');
|
||||
|
||||
if (!withClause) {
|
||||
footer.remove();
|
||||
}
|
||||
</script>
|
||||
|
|
@ -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;
|
||||
---
|
||||
|
||||
<div>
|
||||
<SectionHeading>{title}</SectionHeading>
|
||||
<SectionHeading>{config.title}</SectionHeading>
|
||||
<div class="flex flex-col">
|
||||
{
|
||||
educationItems.map(({ title, description, institution, startDate, endDate }) => () => (
|
||||
diplomas.map(({ title, description, institution, dates }) => () => (
|
||||
<>
|
||||
<div class="flex flex-col gap-2">
|
||||
<ListItemHeading title={title} subtitle={institution} startDate={startDate} endDate={endDate} i18n={i18n} />
|
||||
<ListItemHeading title={title} subtitle={institution} dates={dates} />
|
||||
<Description content={description} />
|
||||
</div>
|
||||
<DashedDivider />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
---
|
||||
|
||||
<div>
|
||||
<SectionHeading>{title}</SectionHeading>
|
||||
<SectionHeading>{config.title}</SectionHeading>
|
||||
<div class="flex flex-col">
|
||||
{
|
||||
jobs.map(({ company, role, description, tags, startDate, endDate }) => () => (
|
||||
jobs.map(({ company, role, description, tagsList, dates }) => () => (
|
||||
<>
|
||||
<div class="flex flex-col gap-2">
|
||||
<ListItemHeading title={role} subtitle={company} startDate={startDate} endDate={endDate} i18n={i18n} />
|
||||
<ListItemHeading title={role} subtitle={company} dates={dates} />
|
||||
<Description content={description} />
|
||||
<TagsList label="Technologies" tags={tags} />
|
||||
<TagsList {...tagsList} />
|
||||
</div>
|
||||
<DashedDivider />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
---
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex gap-6">
|
||||
<Photo src={image} alt={fullName} loading="eager" class="w-40 h-40 max-w-[160px] max-h-[160px] rounded-2xl" />
|
||||
<Photo src={image} alt={fullName} class="w-40 h-40 max-w-[160px] max-h-[160px] rounded-2xl" />
|
||||
<div>
|
||||
<div class="text-3xl font-extrabold text-gray-900">{fullName}</div>
|
||||
<div class="text-lg font-medium text-gray-700">{role}</div>
|
||||
<div class="grid grid-cols-[auto_auto] gap-x-4 gap-y-1 pt-4">
|
||||
{
|
||||
(pdfDetails || details).map((detail: PdfDetail) => (
|
||||
(pdfDetails ?? details).map((detail) => (
|
||||
<LabelledValue {...detail} class={detail.fullRow ? 'col-span-2' : undefined} />
|
||||
))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
---
|
||||
|
||||
<div>
|
||||
<SectionHeading>{title}</SectionHeading>
|
||||
<SectionHeading>{config.title}</SectionHeading>
|
||||
<div class="flex flex-col">
|
||||
{
|
||||
projects.map(({ name, description, pdfDetails, details, tags, startDate, endDate }) => () => (
|
||||
projects.map(({ name, description, details, pdfDetails, tagsList, dates }) => () => (
|
||||
<>
|
||||
<div class="flex flex-col gap-2">
|
||||
<ListItemHeading title={name} startDate={startDate} endDate={endDate} i18n={i18n} />
|
||||
<ListItemHeading title={name} dates={dates} />
|
||||
<Description content={description} />
|
||||
<div>
|
||||
{(pdfDetails || details).map((detail: PdfDetail) => (
|
||||
<div class="grid grid-cols-[auto_auto] gap-x-4 gap-y-1">
|
||||
{(pdfDetails ?? details).map((detail) => (
|
||||
<LabelledValue {...detail} />
|
||||
))}
|
||||
</div>
|
||||
<TagsList label="Technologies" tags={tags} />
|
||||
<TagsList {...tagsList} />
|
||||
</div>
|
||||
<DashedDivider />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,19 @@
|
|||
---
|
||||
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;
|
||||
---
|
||||
|
||||
<div>
|
||||
<SectionHeading>{title}</SectionHeading>
|
||||
<SectionHeading>{config.title}</SectionHeading>
|
||||
<div class="flex flex-col gap-5">
|
||||
{
|
||||
skillSets.map(
|
||||
(skillSet) =>
|
||||
!skillSet.excludeFromPdf && (
|
||||
skillSets.map((skillSet) => (
|
||||
<div>
|
||||
<div class="text-base font-extrabold text-gray-900">{skillSet.pdfTitle || skillSet.title}</div>
|
||||
<div class="text-base font-extrabold text-gray-900">{skillSet.title}</div>
|
||||
<div class="mt-2 flex flex-wrap gap-3.5 text-sm text-gray-700">
|
||||
{skillSet.skills.map((skill) => {
|
||||
if ('level' in skill) {
|
||||
|
|
@ -43,14 +38,11 @@ const {
|
|||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex h-6 w-fit items-center rounded bg-gray-100 px-2.5 font-medium">{skill.name}</div>
|
||||
);
|
||||
return <div class="flex h-6 w-fit items-center rounded bg-gray-100 px-2.5 font-medium">{skill.name}</div>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
---
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex w-full justify-between gap-2">
|
||||
<div class="flex flex-col">
|
||||
<Typography variant="item-title">{title}</Typography>
|
||||
<Typography variant="main-subtitle">{institution}</Typography>
|
||||
<Timestamp
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
locale={i18n.locale}
|
||||
translationForNow={i18n.translations.now}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
socials.length > 0 && (
|
||||
<div class="flex flex-wrap gap-3 sm:flex-nowrap">
|
||||
{socials.map(({ icon, url: iconUrl, name }) => (
|
||||
<IconButton href={iconUrl} icon={icon} size="small" target="_blank" aria-label={name} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<Typography variant="paragraph">{description}</Typography>
|
||||
</div>
|
||||
|
|
@ -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;
|
||||
---
|
||||
|
||||
<SectionCard section="education" title={title}>
|
||||
{
|
||||
removeLast(
|
||||
educationItems.flatMap((educationItem) => [
|
||||
<EducationItem i18n={i18n} educationItem={educationItem} />,
|
||||
<Divider />,
|
||||
])
|
||||
)
|
||||
}
|
||||
</SectionCard>
|
||||
|
|
@ -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';
|
||||
---
|
||||
|
||||
<SectionCard section={section} title={title}>
|
||||
<DividedList>
|
||||
{removeLast(jobs.flatMap((job) => [<WorkTimelineItem job={job} i18n={i18n} />, <Divider />]))}
|
||||
</DividedList>
|
||||
</SectionCard>
|
||||
|
|
@ -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;
|
||||
---
|
||||
|
||||
<div class="flex flex-col gap-2 md:gap-0">
|
||||
<div class="flex w-full flex-row justify-between">
|
||||
<div>
|
||||
<Typography variant="item-title">
|
||||
{job.role}
|
||||
<span class="font-medium"> — {job.company}</span>
|
||||
</Typography>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{
|
||||
job.socials?.map(({ icon, url, name }) => (
|
||||
<IconButton icon={icon} href={url} target="_blank" size="small" aria-label={name} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<Timestamp
|
||||
startDate={job.startDate}
|
||||
endDate={job.endDate}
|
||||
locale={i18n.locale}
|
||||
translationForNow={i18n.translations.now}
|
||||
/>
|
||||
<ul class="ml-[18px] list-disc pt-3 pb-6 text-white">
|
||||
{
|
||||
Array.isArray(job.description) ? (
|
||||
job.description.map((d) => (
|
||||
<li>
|
||||
<Typography variant="paragraph">{d}</Typography>
|
||||
</li>
|
||||
))
|
||||
) : (
|
||||
<li>
|
||||
<Typography variant="paragraph">{job.description}</Typography>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
<TagsList tags={job.tags} />
|
||||
</div>
|
||||
|
|
@ -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<T extends SubsectionData> {
|
||||
name: Subsection;
|
||||
data: T[];
|
||||
title: string;
|
||||
columnsLayout: string;
|
||||
Component: SubsectionComponent;
|
||||
}
|
||||
|
||||
const booksSubsection: FavoritesSubsection<Book> = {
|
||||
name: 'books',
|
||||
columnsLayout: 'grid-cols-fluid200',
|
||||
Component: BookTile,
|
||||
...books,
|
||||
};
|
||||
|
||||
const mediasSubsection: FavoritesSubsection<Media> = {
|
||||
name: 'medias',
|
||||
columnsLayout: 'grid-cols-fluid120',
|
||||
Component: MediaTile,
|
||||
...medias,
|
||||
};
|
||||
|
||||
const peopleSubsection: FavoritesSubsection<Person> = {
|
||||
name: 'people',
|
||||
columnsLayout: 'grid-cols-fluid120',
|
||||
Component: PersonTile,
|
||||
...people,
|
||||
};
|
||||
|
||||
const videosSubsection: FavoritesSubsection<Video> = {
|
||||
name: 'videos',
|
||||
columnsLayout: 'grid-cols-fluid240',
|
||||
Component: VideoTile,
|
||||
...videos,
|
||||
};
|
||||
|
||||
const subsections = [booksSubsection, peopleSubsection, videosSubsection, mediasSubsection];
|
||||
|
||||
const section: SectionKey = 'favorites';
|
||||
---
|
||||
|
||||
<SectionCard section={section} title={title}>
|
||||
<div class="flex flex-col gap-16">
|
||||
{
|
||||
subsections.map(({ Component, data, name, columnsLayout, title: subsectionTitle }) => (
|
||||
<div class="flex flex-col gap-6">
|
||||
<Typography variant="section-subtitle" id={`${section}-${name}-heading`}>
|
||||
{subsectionTitle}
|
||||
</Typography>
|
||||
<div class:list={['grid gap-8', columnsLayout]}>
|
||||
{data.map((value) => (
|
||||
<Component value={value} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
|
@ -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`;
|
||||
---
|
||||
|
||||
<a href={url} class="flex w-full max-w-[240px] flex-col gap-3 transition duration-300 hover:translate-y-2">
|
||||
<Photo class="rounded-lg shadow-md aspect-video object-cover" src={thumbnail} alt={title} width={480} height={270} />
|
||||
<Typography variant="tile-title">
|
||||
{title}
|
||||
</Typography>
|
||||
</a>
|
||||
|
|
@ -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';
|
||||
---
|
||||
|
||||
<SectionCard section={section} title={title}>
|
||||
<DividedList>
|
||||
{removeLast(projects.flatMap((project) => [<ProjectTimelineItem project={project} i18n={i18n} />, <Divider />]))}
|
||||
</DividedList>
|
||||
</SectionCard>
|
||||
|
|
@ -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`;
|
||||
---
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex gap-6">
|
||||
<Photo
|
||||
src={image}
|
||||
alt={alt}
|
||||
width={200}
|
||||
height={200}
|
||||
class="rounded-lg object-cover max-w-[120px] overflow-hidden sm:block hidden"
|
||||
/>
|
||||
<div class="flex w-full flex-col gap-4">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<Typography variant="item-title">{name}</Typography>
|
||||
<Timestamp
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
locale={i18n.locale}
|
||||
translationForNow={i18n.translations.now}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
{
|
||||
socials?.map(({ icon, url, name: socialName }) => (
|
||||
<IconButton icon={icon} href={url} target="_blank" size="small" aria-label={socialName} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<Photo class="rounded-lg object-cover max-w-[120px] sm:hidden" src={image} alt={alt} />
|
||||
<div class="inline-grid w-full xl:grid-cols-[auto_auto]">
|
||||
{
|
||||
details.map(({ label: detailLabel, value: detailValue }) => (
|
||||
<LabelledValue
|
||||
label={detailLabel}
|
||||
value={typeof detailValue === 'object' ? detailValue.join(', ') : detailValue}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-3 col-start-1">
|
||||
<Typography variant="paragraph">{description}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<TagsList tags={tags} />
|
||||
</div>
|
||||
|
|
@ -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;
|
||||
---
|
||||
|
||||
<div class="flex flex-wrap gap-8">
|
||||
{skills.map((skill) => <Skill {...skill} />)}
|
||||
</div>
|
||||
|
|
@ -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<Tag> | SkillSet<LevelledSkill>;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
---
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<Typography variant="section-subtitle">{title}</Typography>
|
||||
{isLevelledSkillSection(skills) ? <LevelledSkillSubsection skills={skills} /> : <TagsList tags={skills} />}
|
||||
</div>
|
||||
|
|
@ -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';
|
||||
---
|
||||
|
||||
<SectionCard section={section} title={title}>
|
||||
<div class="flex flex-col gap-10">
|
||||
{skillSets.map((skillSet) => <SkillSubsection skillSet={skillSet} />)}
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
|
@ -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';
|
||||
---
|
||||
|
||||
<SectionCard section={section} title={title}>
|
||||
<DividedList>
|
||||
{removeLast(testimonials.flatMap((testimonial) => [<Testimonial testimonial={testimonial} />, <Divider />]))}
|
||||
</DividedList>
|
||||
</SectionCard>
|
||||
|
|
@ -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;
|
||||
}
|
||||
9
src/types/config/i18n-config.types.ts
Normal file
9
src/types/config/i18n-config.types.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import type { Locale } from 'date-fns';
|
||||
|
||||
export interface I18nConfig {
|
||||
locale: Locale;
|
||||
dateFormat: string;
|
||||
translations: {
|
||||
now: string;
|
||||
};
|
||||
}
|
||||
3
src/types/config/pdf-config.types.ts
Normal file
3
src/types/config/pdf-config.types.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export interface PdfConfig {
|
||||
footer?: string;
|
||||
}
|
||||
8
src/types/config/seo-config.types.ts
Normal file
8
src/types/config/seo-config.types.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export interface SeoConfig {
|
||||
title: string;
|
||||
description: string;
|
||||
favicon: string;
|
||||
ogTitle?: string;
|
||||
ogDescription?: string;
|
||||
ogImage?: string;
|
||||
}
|
||||
|
|
@ -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<keyof Data, 'seo' | 'i18n'>;
|
||||
export interface Sections {
|
||||
main: MainSection;
|
||||
skills: SkillsSection;
|
||||
experience: ExperienceSection;
|
||||
portfolio: PortfolioSection;
|
||||
education: EducationSection;
|
||||
testimonials: TestimonialsSection;
|
||||
favorites: FavoritesSection;
|
||||
}
|
||||
|
||||
export interface Data {
|
||||
config: Config;
|
||||
sections: Sections;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import type { Locales } from 'locales-ts/types';
|
||||
|
||||
export interface I18n {
|
||||
locale: Locales;
|
||||
translations: {
|
||||
now: string;
|
||||
};
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import type { CircleFlags, Fa6Brands, Fa6Solid, Ri, SimpleIcons } from 'iconify-icon-names';
|
||||
|
||||
export type IconName = Fa6Brands | Fa6Solid | SimpleIcons | CircleFlags | Ri;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export interface Pdf {
|
||||
footer: string;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
13
src/types/sections/education-section.types.ts
Normal file
13
src/types/sections/education-section.types.ts
Normal file
|
|
@ -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[];
|
||||
}
|
||||
14
src/types/sections/experience-section.types.ts
Normal file
14
src/types/sections/experience-section.types.ts
Normal file
|
|
@ -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[];
|
||||
}
|
||||
|
|
@ -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<Data> {
|
||||
export interface SubSection<Data = unknown> {
|
||||
title: string;
|
||||
data: Data[];
|
||||
}
|
||||
|
||||
export interface FavoritesSection {
|
||||
config: SectionConfig;
|
||||
books: SubSection<Book>;
|
||||
people: SubSection<Person>;
|
||||
videos: SubSection<Video>;
|
||||
medias: SubSection<Media>;
|
||||
export interface FavoritesSection extends Section {
|
||||
books?: SubSection<Book>;
|
||||
people?: SubSection<Person>;
|
||||
videos?: SubSection<Video>;
|
||||
medias?: SubSection<Media>;
|
||||
}
|
||||
13
src/types/sections/main-section.types.ts
Normal file
13
src/types/sections/main-section.types.ts
Normal file
|
|
@ -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[];
|
||||
}
|
||||
16
src/types/sections/portfolio-section.types.ts
Normal file
16
src/types/sections/portfolio-section.types.ts
Normal file
|
|
@ -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[];
|
||||
}
|
||||
18
src/types/sections/skills-section.types.ts
Normal file
18
src/types/sections/skills-section.types.ts
Normal file
|
|
@ -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[];
|
||||
}
|
||||
13
src/types/sections/testimonials-section.types.ts
Normal file
13
src/types/sections/testimonials-section.types.ts
Normal file
|
|
@ -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[];
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
52
src/types/shared.ts
Normal file
52
src/types/shared.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import type { SectionConfig, Tag } from './common';
|
||||
|
||||
export interface LevelledSkill extends Tag {
|
||||
level: 1 | 2 | 3 | 4 | 5;
|
||||
}
|
||||
|
||||
export interface SkillSet<SkillType> {
|
||||
title: string;
|
||||
skills: SkillType[];
|
||||
pdfTitle?: string;
|
||||
excludeFromPdf?: boolean;
|
||||
}
|
||||
|
||||
export interface SkillsSection {
|
||||
skillSets: (SkillSet<Tag> | SkillSet<LevelledSkill>)[];
|
||||
config: SectionConfig;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
15
src/utils/create-link-factory.ts
Normal file
15
src/utils/create-link-factory.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import type { LinkButton } from '@/types/shared';
|
||||
import type { Merge } from 'type-fest';
|
||||
|
||||
type LinkWithoutUrl = Omit<LinkButton, 'url'>;
|
||||
type PartialLinkWithUrl = Partial<LinkButton> & { url: string };
|
||||
|
||||
const createLinkFactory =
|
||||
<Link extends LinkWithoutUrl>(defaultData: Readonly<Link>) =>
|
||||
<Override extends PartialLinkWithUrl>(override: Readonly<Override>) =>
|
||||
({
|
||||
...defaultData,
|
||||
...override,
|
||||
} as Readonly<Merge<Link, Override>>);
|
||||
|
||||
export default createLinkFactory;
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue