feat: add projects page and rework home page
This commit is contained in:
+23
-23
@@ -1,5 +1,5 @@
|
||||
:root {
|
||||
--text: oklch(38.30% 0.029 266.48);
|
||||
--text: oklch(38.3% 0.029 266.48);
|
||||
--text-50: oklch(95.82% 0.004 271.37);
|
||||
--text-100: oklch(91.83% 0.009 264.52);
|
||||
--text-200: oklch(83.53% 0.016 266.26);
|
||||
@@ -8,30 +8,30 @@
|
||||
--text-500: oklch(57.02% 0.047 267.31);
|
||||
--text-600: oklch(48.66% 0.039 268.21);
|
||||
--text-700: oklch(40.13% 0.031 265.23);
|
||||
--text-800: oklch(30.90% 0.021 265.90);
|
||||
--text-800: oklch(30.9% 0.021 265.9);
|
||||
--text-900: oklch(20.86% 0.013 264.25);
|
||||
--text-950: oklch(15.46% 0.007 270.96);
|
||||
|
||||
--background: oklch(95.13% 0.007 260.73);
|
||||
--background-50: oklch(95.80% 0.007 268.55);
|
||||
--background-50: oklch(95.8% 0.007 268.55);
|
||||
--background-100: oklch(91.74% 0.012 259.82);
|
||||
--background-200: oklch(83.07% 0.027 262.33);
|
||||
--background-300: oklch(74.46% 0.041 261.48);
|
||||
--background-400: oklch(65.63% 0.058 260.56);
|
||||
--background-500: oklch(56.42% 0.075 261.41);
|
||||
--background-600: oklch(48.32% 0.062 260.40);
|
||||
--background-600: oklch(48.32% 0.062 260.4);
|
||||
--background-700: oklch(39.64% 0.048 261.18);
|
||||
--background-800: oklch(30.43% 0.036 261.92);
|
||||
--background-900: oklch(20.77% 0.018 259.72);
|
||||
--background-950: oklch(15.29% 0.010 255.44);
|
||||
--background-950: oklch(15.29% 0.01 255.44);
|
||||
|
||||
--primary: oklch(77.15% 0.062 217.48);
|
||||
--primary-50: oklch(96.50% 0.009 222.06);
|
||||
--primary-50: oklch(96.5% 0.009 222.06);
|
||||
--primary-100: oklch(93.16% 0.019 213.42);
|
||||
--primary-200: oklch(86.07% 0.039 217.46);
|
||||
--primary-300: oklch(79.25% 0.057 216.55);
|
||||
--primary-400: oklch(72.48% 0.075 217.32);
|
||||
--primary-500: oklch(65.88% 0.089 218.00);
|
||||
--primary-500: oklch(65.88% 0.089 218);
|
||||
--primary-600: oklch(55.99% 0.075 218.52);
|
||||
--primary-700: oklch(45.64% 0.059 218.22);
|
||||
--primary-800: oklch(34.67% 0.043 219.39);
|
||||
@@ -41,7 +41,7 @@
|
||||
--secondary: oklch(69.65% 0.059 248.69);
|
||||
--secondary-50: oklch(95.95% 0.008 253.85);
|
||||
--secondary-100: oklch(92.05% 0.015 244.73);
|
||||
--secondary-200: oklch(83.76% 0.030 248.19);
|
||||
--secondary-200: oklch(83.76% 0.03 248.19);
|
||||
--secondary-300: oklch(75.31% 0.048 249.46);
|
||||
--secondary-400: oklch(66.99% 0.065 248.83);
|
||||
--secondary-500: oklch(58.35% 0.083 249.96);
|
||||
@@ -51,16 +51,16 @@
|
||||
--secondary-900: oklch(20.99% 0.022 251.79);
|
||||
--secondary-950: oklch(15.56% 0.012 241.97);
|
||||
|
||||
--accent: oklch(59.38% 0.078 253.40);
|
||||
--accent-50: oklch(95.93% 0.007 247.90);
|
||||
--accent: oklch(59.38% 0.078 253.4);
|
||||
--accent-50: oklch(95.93% 0.007 247.9);
|
||||
--accent-100: oklch(91.85% 0.015 251.16);
|
||||
--accent-200: oklch(83.39% 0.030 254.70);
|
||||
--accent-200: oklch(83.39% 0.03 254.7);
|
||||
--accent-300: oklch(74.95% 0.046 253.67);
|
||||
--accent-400: oklch(66.37% 0.064 253.29);
|
||||
--accent-500: oklch(57.47% 0.081 254.47);
|
||||
--accent-600: oklch(49.19% 0.068 253.56);
|
||||
--accent-700: oklch(40.31% 0.053 254.02);
|
||||
--accent-800: oklch(30.91% 0.038 255.00);
|
||||
--accent-800: oklch(30.91% 0.038 255);
|
||||
--accent-900: oklch(20.99% 0.022 251.79);
|
||||
--accent-950: oklch(15.35% 0.012 260.39);
|
||||
}
|
||||
@@ -68,7 +68,7 @@
|
||||
--text: oklch(76.63% 0.024 266.86);
|
||||
--text-50: oklch(15.46% 0.007 270.96);
|
||||
--text-100: oklch(20.86% 0.013 264.25);
|
||||
--text-200: oklch(30.90% 0.021 265.90);
|
||||
--text-200: oklch(30.9% 0.021 265.9);
|
||||
--text-300: oklch(40.13% 0.031 265.23);
|
||||
--text-400: oklch(48.66% 0.039 268.21);
|
||||
--text-500: oklch(57.02% 0.047 267.31);
|
||||
@@ -79,17 +79,17 @@
|
||||
--text-950: oklch(95.82% 0.004 271.37);
|
||||
|
||||
--background: oklch(16.29% 0.012 260.61);
|
||||
--background-50: oklch(15.29% 0.010 255.44);
|
||||
--background-50: oklch(15.29% 0.01 255.44);
|
||||
--background-100: oklch(20.77% 0.018 259.72);
|
||||
--background-200: oklch(30.43% 0.036 261.92);
|
||||
--background-300: oklch(39.64% 0.048 261.18);
|
||||
--background-400: oklch(48.32% 0.062 260.40);
|
||||
--background-400: oklch(48.32% 0.062 260.4);
|
||||
--background-500: oklch(56.42% 0.075 261.41);
|
||||
--background-600: oklch(65.63% 0.058 260.56);
|
||||
--background-700: oklch(74.46% 0.041 261.48);
|
||||
--background-800: oklch(83.07% 0.027 262.33);
|
||||
--background-900: oklch(91.74% 0.012 259.82);
|
||||
--background-950: oklch(95.80% 0.007 268.55);
|
||||
--background-950: oklch(95.8% 0.007 268.55);
|
||||
|
||||
--primary: oklch(48.89% 0.064 217.48);
|
||||
--primary-50: oklch(16.48% 0.015 212.62);
|
||||
@@ -97,12 +97,12 @@
|
||||
--primary-200: oklch(34.67% 0.043 219.39);
|
||||
--primary-300: oklch(45.64% 0.059 218.22);
|
||||
--primary-400: oklch(55.99% 0.075 218.52);
|
||||
--primary-500: oklch(65.88% 0.089 218.00);
|
||||
--primary-500: oklch(65.88% 0.089 218);
|
||||
--primary-600: oklch(72.48% 0.075 217.32);
|
||||
--primary-700: oklch(79.25% 0.057 216.55);
|
||||
--primary-800: oklch(86.07% 0.039 217.46);
|
||||
--primary-900: oklch(93.16% 0.019 213.42);
|
||||
--primary-950: oklch(96.50% 0.009 222.06);
|
||||
--primary-950: oklch(96.5% 0.009 222.06);
|
||||
|
||||
--secondary: oklch(47.12% 0.064 249.33);
|
||||
--secondary-50: oklch(15.56% 0.012 241.97);
|
||||
@@ -113,20 +113,20 @@
|
||||
--secondary-500: oklch(58.35% 0.083 249.96);
|
||||
--secondary-600: oklch(66.99% 0.065 248.83);
|
||||
--secondary-700: oklch(75.31% 0.048 249.46);
|
||||
--secondary-800: oklch(83.76% 0.030 248.19);
|
||||
--secondary-800: oklch(83.76% 0.03 248.19);
|
||||
--secondary-900: oklch(92.05% 0.015 244.73);
|
||||
--secondary-950: oklch(95.95% 0.008 253.85);
|
||||
|
||||
--accent: oklch(55.80% 0.080 254.61);
|
||||
--accent: oklch(55.8% 0.08 254.61);
|
||||
--accent-50: oklch(15.35% 0.012 260.39);
|
||||
--accent-100: oklch(20.99% 0.022 251.79);
|
||||
--accent-200: oklch(30.91% 0.038 255.00);
|
||||
--accent-200: oklch(30.91% 0.038 255);
|
||||
--accent-300: oklch(40.31% 0.053 254.02);
|
||||
--accent-400: oklch(49.19% 0.068 253.56);
|
||||
--accent-500: oklch(57.47% 0.081 254.47);
|
||||
--accent-600: oklch(66.37% 0.064 253.29);
|
||||
--accent-700: oklch(74.95% 0.046 253.67);
|
||||
--accent-800: oklch(83.39% 0.030 254.70);
|
||||
--accent-800: oklch(83.39% 0.03 254.7);
|
||||
--accent-900: oklch(91.85% 0.015 251.16);
|
||||
--accent-950: oklch(95.93% 0.007 247.90);
|
||||
--accent-950: oklch(95.93% 0.007 247.9);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
@import './colors.css';
|
||||
@import './ui/index.css';
|
||||
@import './tailwind.css';
|
||||
@import './typography.css';
|
||||
|
||||
@source "../../../content/**/*";
|
||||
|
||||
|
||||
@@ -80,6 +80,6 @@
|
||||
Noto Sans, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
--font-title:
|
||||
Wittgenstein, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
Montserrat, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
@layer base {
|
||||
body {
|
||||
font-family:
|
||||
'Noto Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
font-family:
|
||||
'Montserrat', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.hero-image {
|
||||
max-width: 7rem;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.centered-image {
|
||||
margin: 0 auto;
|
||||
}
|
||||
@@ -2,3 +2,4 @@
|
||||
@import './text.css';
|
||||
@import './background.css';
|
||||
@import './border.css';
|
||||
@import './images.css';
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { NavigationMenuItem } from '@nuxt/ui';
|
||||
|
||||
const route = useRoute();
|
||||
const items = computed<NavigationMenuItem[]>(() => [
|
||||
{
|
||||
@@ -20,7 +22,7 @@ const items = computed<NavigationMenuItem[]>(() => [
|
||||
to: '/',
|
||||
active: route.path == '/',
|
||||
},
|
||||
...['resume', 'vocal-synthesis', 'languages', 'contact'].map((page) => ({
|
||||
...['resume', 'projects', 'vocal-synthesis', 'languages', 'contact'].map((page) => ({
|
||||
label: $t(`pages.${page}.name`),
|
||||
to: `/${page}`,
|
||||
active: route.path.startsWith(`/${page}`),
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<UButton :icon="icon" target="_blank" :to="link.url">
|
||||
<span>{{ link.display }}</span>
|
||||
<span v-if="link.type === 'cargo'"><UIcon name="i-mdi-download" /> {{ crateDownloads }}</span>
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ProjectLink, ProjectLinkType } from '~/types/project';
|
||||
|
||||
const crates = useCrates();
|
||||
|
||||
const iconDict: Record<ProjectLinkType, string> = {
|
||||
github: 'mdi:github',
|
||||
gitea: 'simple-icons:gitea',
|
||||
cargo: 'simple-icons:rust',
|
||||
melpa: 'file-icons:melpa',
|
||||
};
|
||||
|
||||
const icon = computed(() => iconDict[link.type]);
|
||||
|
||||
const { link } = defineProps<{
|
||||
link: ProjectLink;
|
||||
}>();
|
||||
|
||||
const cratesData = computed(() => (link.type === 'cargo' ? crates.getCrateInfo(link.display) : null));
|
||||
const crateDownloads = computed(
|
||||
() => cratesData.value?.data?.value?.crate.downloads ?? (cratesData.value?.loading ? 'loading' : 'error'),
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<UPageCard class="w-md bg-background-100">
|
||||
<div class="text-2xl font-title">{{ project.name }}</div>
|
||||
<p>
|
||||
{{ project.description }}
|
||||
</p>
|
||||
<div class="flex flex-row flex-wrap gap-2 items-baseline">
|
||||
<UiProjectLink v-for="link in project.links" :key="link.url" :link="link" />
|
||||
</div>
|
||||
<div class="flex flex-row gap-4 items-baseline">
|
||||
<div>Tools</div>
|
||||
<UiBadgeList :tools="project.tools" />
|
||||
</div>
|
||||
</UPageCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Project } from '~/types/project';
|
||||
|
||||
const { project } = defineProps<{
|
||||
project: Project;
|
||||
}>();
|
||||
</script>
|
||||
@@ -4,7 +4,7 @@
|
||||
{{ $t('pages.vocal-synthesis.projects') }}
|
||||
</p>
|
||||
<div class="flex flex-col max-w gap-10">
|
||||
<div v-for="project in data?.projects" :key="project.title" class="flex flex-row max-w gap-5">
|
||||
<div v-for="project in data?.projects" :key="project.title" class="flex flex-row max-w gap-5 font-title">
|
||||
<div>
|
||||
<div
|
||||
class="bg-primary text-text-50 dark:bg-primary p-1 rounded-md min-w-13 w-13 h-13 min-h-13 flex justify-center my-2"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
type Theme = 'light' | 'dark' | 'system';
|
||||
const icons: Dictionary<Theme, string> = {
|
||||
const icons: Record<Theme, string> = {
|
||||
light: 'material-symbols:light-mode',
|
||||
dark: 'material-symbols:dark-mode',
|
||||
system: 'material-symbols:computer-outline',
|
||||
|
||||
+21
-16
@@ -13,12 +13,17 @@ export interface UseApi {
|
||||
patch: <T, B = unknown>(path: string, opts?: FetchOptions, immediate?: boolean, body?: B) => UseApiResponse<T, B>;
|
||||
}
|
||||
|
||||
export interface CreateRequestOptions<PayloadT> {
|
||||
host?: string;
|
||||
path: string;
|
||||
immediate?: boolean;
|
||||
method: HttpMethod;
|
||||
body?: PayloadT;
|
||||
opts?: FetchOptions;
|
||||
}
|
||||
|
||||
const createRequest = <ResponseT = unknown, PayloadT = unknown>(
|
||||
method: HttpMethod,
|
||||
url: string,
|
||||
opts?: FetchOptions,
|
||||
immediate: boolean = true,
|
||||
body?: PayloadT,
|
||||
options: CreateRequestOptions<PayloadT>,
|
||||
): QueryResult<ResponseT, PayloadT> => {
|
||||
const response = new QueryResult<ResponseT, PayloadT>();
|
||||
const { apiBase } = useRuntimeConfig().public;
|
||||
@@ -28,10 +33,10 @@ const createRequest = <ResponseT = unknown, PayloadT = unknown>(
|
||||
response.error.value = null;
|
||||
|
||||
try {
|
||||
const res = await $fetch<ResponseT>(url, {
|
||||
baseURL: apiBase,
|
||||
...opts,
|
||||
method,
|
||||
const res = await $fetch<ResponseT>(options.path, {
|
||||
...options.opts,
|
||||
baseURL: options.host ?? apiBase,
|
||||
method: options.method,
|
||||
body: requestBody ?? undefined,
|
||||
});
|
||||
response.data.value = res;
|
||||
@@ -48,22 +53,22 @@ const createRequest = <ResponseT = unknown, PayloadT = unknown>(
|
||||
};
|
||||
response.run = run;
|
||||
|
||||
if (immediate) run(body);
|
||||
if (options.immediate) run(options.body);
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
export const useApi = (): UseApi => {
|
||||
export const useApi = (host?: string): UseApi => {
|
||||
const get = <T>(path: string, opts?: FetchOptions, immediate: boolean = true) =>
|
||||
createRequest<T>('GET', path, opts, immediate);
|
||||
createRequest<T>({ method: 'GET', path, opts, immediate, host });
|
||||
const del = <T>(path: string, opts?: FetchOptions, immediate: boolean = true) =>
|
||||
createRequest<T>('DELETE', path, opts, immediate);
|
||||
createRequest<T>({ method: 'DELETE', path, opts, immediate, host });
|
||||
const post = <T, B = unknown>(path: string, opts?: FetchOptions, immediate: boolean = true, body?: B) =>
|
||||
createRequest<T, B>('POST', path, opts, immediate, body);
|
||||
createRequest<T, B>({ method: 'POST', path, opts, immediate, body, host });
|
||||
const put = <T, B = unknown>(path: string, opts?: FetchOptions, immediate: boolean = true, body?: B) =>
|
||||
createRequest<T, B>('PUT', path, opts, immediate, body);
|
||||
createRequest<T, B>({ method: 'PUT', path, opts, immediate, body, host });
|
||||
const patch = <T, B = unknown>(path: string, opts?: FetchOptions, immediate: boolean = true, body?: B) =>
|
||||
createRequest<T, B>('PATCH', path, opts, immediate, body);
|
||||
createRequest<T, B>({ method: 'PATCH', path, opts, immediate, body, host });
|
||||
|
||||
return { get, post, put, patch, del };
|
||||
};
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { CrateMeta } from '~/types/crate';
|
||||
|
||||
export const useCrates = () => {
|
||||
const api = useApi('https://crates.io/api/v1/');
|
||||
const getCrateInfo = (name: string): UseApiResponse<CrateMeta> => api.get<CrateMeta>(`crates/${name}`);
|
||||
|
||||
return { getCrateInfo };
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { GithubRepository } from '~/types/github-repository';
|
||||
|
||||
export const useGithub = () => {
|
||||
const api = useApi('https://api.github.com');
|
||||
|
||||
const getRepoMeta = (owner: string, name: string): UseApiResponse<GithubRepository> =>
|
||||
api.get<GithubRepository>(`repos/${owner}/${name}`);
|
||||
|
||||
return { getRepoMeta };
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<NuxtLayout name="default">
|
||||
<h1 class="text-4xl text-highlighted font-bold mb-8">
|
||||
{{ $t('pages.projects.name') }}
|
||||
</h1>
|
||||
<div class="flex flex-wrap gap-4 justify-center">
|
||||
<UiSingleProject v-for="project in projects" :key="project.name" :project="project" />
|
||||
</div>
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Project, ProjectsWrapper } from '~/types/project';
|
||||
|
||||
const { getJsonData } = useDataJson('projects');
|
||||
const projects$ = await getJsonData<ProjectsWrapper>();
|
||||
const projects: ComputedRef<Project[]> = computed(() => (projects$?.value ? projects$.value.projects : []));
|
||||
</script>
|
||||
@@ -7,7 +7,7 @@
|
||||
<p>
|
||||
{{ $t('pages.resume.experience') }}
|
||||
</p>
|
||||
<UTimeline v-model="valueExp" reverse :items="resumeContent?.experience" class="w-full">
|
||||
<UTimeline v-model="valueExp" reverse :items="experiences" class="w-full">
|
||||
<template #description="{ item }">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p>
|
||||
@@ -22,7 +22,7 @@
|
||||
<p>
|
||||
{{ $t('pages.resume.education') }}
|
||||
</p>
|
||||
<UTimeline v-model="valueEd" reverse :items="resumeContent?.education" class="w-full" />
|
||||
<UTimeline v-model="valueEd" reverse :items="education" class="w-full" />
|
||||
</UPageCard>
|
||||
<UiBadgeListCard :tools="resumeContent?.otherTools">{{ $t('pages.resume.tools') }}</UiBadgeListCard>
|
||||
<UiBadgeListCard :tools="resumeContent?.devops">{{ $t('pages.resume.devops') }}</UiBadgeListCard>
|
||||
@@ -35,7 +35,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ResumeContent } from '~/types/resume';
|
||||
import { EducationTimelineItem, ResumeContent, ResumeExperienceTimelineItem } from '~/types/resume';
|
||||
|
||||
useMeta({
|
||||
title: $t('pages.resume.name'),
|
||||
@@ -44,6 +44,8 @@ useMeta({
|
||||
const { getJsonData } = useDataJson('resume');
|
||||
const resumeContent$ = await getJsonData<ResumeContent>();
|
||||
const resumeContent = computed(() => (resumeContent$.value ? resumeContent$.value : new ResumeContent()));
|
||||
const experiences = computed(() => resumeContent.value.experience.map((e) => new ResumeExperienceTimelineItem(e)));
|
||||
const education = computed(() => resumeContent.value.education.map((e) => new EducationTimelineItem(e)));
|
||||
const arrLength = <T,>(array?: T[]) => (array ? array.length - 1 : 0);
|
||||
const valueExp = computed(() => arrLength(resumeContent.value?.experience));
|
||||
const valueEd = computed(() => arrLength(resumeContent.value?.education));
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
export interface CrateMeta {
|
||||
crate: Crate;
|
||||
}
|
||||
|
||||
export interface Crate {
|
||||
id: string;
|
||||
name: string;
|
||||
updated_at: string;
|
||||
versions: number[];
|
||||
keywords: string[];
|
||||
created_at: string;
|
||||
downloads: number;
|
||||
recent_downloads: number;
|
||||
default_version: string;
|
||||
num_versions: number;
|
||||
yanked: boolean;
|
||||
max_version: string;
|
||||
newest_version: string;
|
||||
max_stable_version: string;
|
||||
description: string;
|
||||
homepage: string;
|
||||
repository: string;
|
||||
exact_match: boolean;
|
||||
trustpub_only: boolean;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
export interface GithubRepository {
|
||||
id: number;
|
||||
node_id: string;
|
||||
name: string;
|
||||
full_name: string;
|
||||
private: boolean;
|
||||
owner: Owner;
|
||||
html_url: string;
|
||||
description: string;
|
||||
fork: boolean;
|
||||
url: string;
|
||||
forks_url: string;
|
||||
keys_url: string;
|
||||
collaborators_url: string;
|
||||
teams_url: string;
|
||||
hooks_url: string;
|
||||
issue_events_url: string;
|
||||
events_url: string;
|
||||
assignees_url: string;
|
||||
branches_url: string;
|
||||
tags_url: string;
|
||||
blobs_url: string;
|
||||
git_tags_url: string;
|
||||
git_refs_url: string;
|
||||
trees_url: string;
|
||||
statuses_url: string;
|
||||
languages_url: string;
|
||||
stargazers_url: string;
|
||||
contributors_url: string;
|
||||
subscribers_url: string;
|
||||
subscription_url: string;
|
||||
commits_url: string;
|
||||
git_commits_url: string;
|
||||
comments_url: string;
|
||||
issue_comment_url: string;
|
||||
contents_url: string;
|
||||
compare_url: string;
|
||||
merges_url: string;
|
||||
archive_url: string;
|
||||
downloads_url: string;
|
||||
issues_url: string;
|
||||
pulls_url: string;
|
||||
milestones_url: string;
|
||||
notifications_url: string;
|
||||
labels_url: string;
|
||||
releases_url: string;
|
||||
deployments_url: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
pushed_at: string;
|
||||
git_url: string;
|
||||
ssh_url: string;
|
||||
clone_url: string;
|
||||
svn_url: string;
|
||||
homepage: string;
|
||||
size: number;
|
||||
stargazers_count: number;
|
||||
watchers_count: number;
|
||||
language: string;
|
||||
has_issues: boolean;
|
||||
has_projects: boolean;
|
||||
has_downloads: boolean;
|
||||
has_wiki: boolean;
|
||||
has_pages: boolean;
|
||||
has_discussions: boolean;
|
||||
forks_count: number;
|
||||
mirror_url: any;
|
||||
archived: boolean;
|
||||
disabled: boolean;
|
||||
open_issues_count: number;
|
||||
license: any;
|
||||
allow_forking: boolean;
|
||||
is_template: boolean;
|
||||
web_commit_signoff_required: boolean;
|
||||
has_pull_requests: boolean;
|
||||
pull_request_creation_policy: string;
|
||||
topics: any[];
|
||||
visibility: string;
|
||||
forks: number;
|
||||
open_issues: number;
|
||||
watchers: number;
|
||||
default_branch: string;
|
||||
temp_clone_token: any;
|
||||
network_count: number;
|
||||
subscribers_count: number;
|
||||
}
|
||||
|
||||
export interface Owner {
|
||||
login: string;
|
||||
id: number;
|
||||
node_id: string;
|
||||
avatar_url: string;
|
||||
gravatar_id: string;
|
||||
url: string;
|
||||
html_url: string;
|
||||
followers_url: string;
|
||||
following_url: string;
|
||||
gists_url: string;
|
||||
starred_url: string;
|
||||
subscriptions_url: string;
|
||||
organizations_url: string;
|
||||
repos_url: string;
|
||||
events_url: string;
|
||||
received_events_url: string;
|
||||
type: string;
|
||||
user_view_type: string;
|
||||
site_admin: boolean;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { TimePeriod } from './time';
|
||||
import type { Tool } from './tool';
|
||||
|
||||
export type ProjectLinkType = 'github' | 'gitea' | 'cargo' | 'melpa';
|
||||
|
||||
export interface ProjectLink {
|
||||
url: string;
|
||||
display: string;
|
||||
type: ProjectLinkType;
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
name: string;
|
||||
description: string;
|
||||
role: string;
|
||||
dates: TimePeriod;
|
||||
links: ProjectLink[];
|
||||
tools: Tool[];
|
||||
}
|
||||
|
||||
export interface ProjectsWrapper {
|
||||
projects: Project[];
|
||||
}
|
||||
+47
-3
@@ -1,14 +1,58 @@
|
||||
import type { TimelineItem } from '@nuxt/ui';
|
||||
import type { Tool } from './tool';
|
||||
import { timeToString, type TimePeriod } from './time';
|
||||
|
||||
export class ResumeExperience implements TimelineItem {
|
||||
tools: Tool[] = [];
|
||||
export interface ResumeExperience {
|
||||
dates: TimePeriod;
|
||||
title: string;
|
||||
company: string;
|
||||
description: string;
|
||||
tools: Tool[];
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export class ResumeExperienceTimelineItem implements TimelineItem {
|
||||
tools: Tool[];
|
||||
description?: string;
|
||||
date: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
|
||||
constructor(from: ResumeExperience) {
|
||||
this.date = timeToString(from.dates);
|
||||
this.title = `${from.title} — ${from.company}`;
|
||||
this.description = from.description;
|
||||
this.icon = from.icon;
|
||||
this.tools = from.tools;
|
||||
}
|
||||
}
|
||||
|
||||
export interface Education {
|
||||
dates: TimePeriod;
|
||||
degree: string;
|
||||
institution: string;
|
||||
location: string;
|
||||
description?: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export class EducationTimelineItem implements TimelineItem {
|
||||
date: string;
|
||||
description?: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
|
||||
constructor(from: Education) {
|
||||
this.date = timeToString(from.dates);
|
||||
this.description = from.description;
|
||||
this.title = `${from.degree} — ${from.institution}, ${from.location}`;
|
||||
this.icon = from.icon;
|
||||
}
|
||||
}
|
||||
|
||||
export class ResumeContent {
|
||||
experience: ResumeExperience[] = [];
|
||||
education: TimelineItem[] = [];
|
||||
education: Education[] = [];
|
||||
otherTools: Tool[] = [];
|
||||
devops: Tool[] = [];
|
||||
os: Tool[] = [];
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { t } from '../utils/i18n';
|
||||
|
||||
export interface TimePeriod {
|
||||
start: string;
|
||||
end?: string;
|
||||
}
|
||||
|
||||
export const timeToString = (time: TimePeriod): string =>
|
||||
time.end ? `${time.start} — ${time.end}` : `${t('misc.since')} ${time.start}`;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { tryUseNuxtApp } from '#app';
|
||||
|
||||
/**
|
||||
* Synchronous translation helper usable outside of Vue components.
|
||||
*
|
||||
* It resuses the request-scoped `@nuxtjs/i18n` instance exposed on the Nuxt app
|
||||
* as `$i18n`, so translations stay in sync with the active locale (including
|
||||
* server-side rendering and language switches). Falls back to the raw key when
|
||||
* no Nuxt context is available (e.g. in unit tests).
|
||||
*
|
||||
* @param key Translation key
|
||||
* @param named Named/list arguments forwarded to vue-i18n
|
||||
* @returns The translated string, or `key` when no i18n instance is found
|
||||
*/
|
||||
export const t = <T extends string = string>(key: T, named?: Record<string, unknown> | unknown[]): string => {
|
||||
const i18n = tryUseNuxtApp()?.$i18n;
|
||||
if (!i18n) {
|
||||
return key;
|
||||
}
|
||||
return i18n.t(key, named as never);
|
||||
};
|
||||
Reference in New Issue
Block a user