initial commit
This commit is contained in:
13
src/routes/+layout.svelte
Normal file
13
src/routes/+layout.svelte
Normal file
@@ -0,0 +1,13 @@
|
||||
<script>
|
||||
let { children } = $props();
|
||||
import '../app.css';
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="font-mono bg-background text-foreground dark:bg-background-dark dark:text-shadow-foreground-dark"
|
||||
>
|
||||
<a href="#main-content" class="sr-only focus:not-sr-only focus:absolute">
|
||||
Skip to main content
|
||||
</a>
|
||||
{@render children()}
|
||||
</div>
|
||||
24
src/routes/+page.server.ts
Normal file
24
src/routes/+page.server.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { PageServerLoad } from './$types.js';
|
||||
import { loadAllContent } from '$lib/content/loader.js';
|
||||
import { fetchGitHubProjects, fetchContributedRepos } from '$lib/github.js';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
const content = loadAllContent();
|
||||
|
||||
// Fetch GitHub projects and contributions in parallel
|
||||
const [ownProjects, contributions] = await Promise.all([
|
||||
fetchGitHubProjects(content.profile.github),
|
||||
fetchContributedRepos(content.profile.github),
|
||||
]);
|
||||
|
||||
return {
|
||||
profile: content.profile,
|
||||
experience: content.experience,
|
||||
education: content.education,
|
||||
skills: content.skills,
|
||||
ownProjects: ownProjects.slice(0, 6),
|
||||
contributions: contributions.slice(0, 5),
|
||||
};
|
||||
};
|
||||
71
src/routes/+page.svelte
Normal file
71
src/routes/+page.svelte
Normal file
@@ -0,0 +1,71 @@
|
||||
<script lang="ts">
|
||||
import Header from '$lib/components/Header.svelte';
|
||||
import Experience from '$lib/components/Experience.svelte';
|
||||
import Education from '$lib/components/Education.svelte';
|
||||
import Skills from '$lib/components/Skills.svelte';
|
||||
import Projects from '$lib/components/Projects.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.profile.name} — {data.profile.title}</title>
|
||||
<meta name="description" content={data.profile.summary.substring(0, 160)} />
|
||||
</svelte:head>
|
||||
|
||||
<main id="main-content" class="min-h-screen bg-bg">
|
||||
<div class="max-w-5xl mx-auto px-6 py-12">
|
||||
<Header profile={data.profile} />
|
||||
|
||||
<Experience experience={data.experience} />
|
||||
|
||||
<Education education={data.education} />
|
||||
|
||||
<Skills skills={data.skills} />
|
||||
|
||||
<Projects
|
||||
ownProjects={data.ownProjects}
|
||||
contributions={data.contributions}
|
||||
username={data.profile.github}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="/joakim-repomaa-cv.pdf"
|
||||
download
|
||||
class="no-print fixed top-0 right-0 w-24 h-24 z-50 group"
|
||||
style="clip-path: polygon(0 0, 100% 0, 100% 100%);"
|
||||
data-sveltekit-preload-data="off"
|
||||
aria-label="Download PDF"
|
||||
>
|
||||
<div
|
||||
class="absolute top-0 right-0 w-0 h-0 border-t-80 border-t-accent border-l-80 border-l-transparent group-hover:opacity-80 transition-opacity"
|
||||
></div>
|
||||
<span
|
||||
class="absolute inset-0 flex items-start justify-end p-4 text-bg font-mono text-sm font-bold"
|
||||
>
|
||||
<span class="inline-block rotate-45 origin-center group-focus:ring ring-fg">PDF</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<footer class="max-w-5xl mx-auto mt-16 py-8 border-t border-fg/20 text-center text-sm text-muted">
|
||||
<p class="font-mono">
|
||||
<span class="text-accent">$</span>
|
||||
Built with SvelteKit + Sveltia CMS
|
||||
</p>
|
||||
<p class="mt-2">
|
||||
<a href="/admin/index.html" class="text-accent hover:text-accent/80 transition-colors">
|
||||
Edit Content →
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
/* Ensure proper print styling */
|
||||
@media print {
|
||||
main {
|
||||
background: white !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1
src/routes/admin/+page.server.ts
Normal file
1
src/routes/admin/+page.server.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const ssr = false;
|
||||
105
src/routes/admin/+page.svelte
Normal file
105
src/routes/admin/+page.svelte
Normal file
@@ -0,0 +1,105 @@
|
||||
<script lang="ts">
|
||||
import CMS from '@sveltia/cms';
|
||||
|
||||
CMS.init({
|
||||
config: {
|
||||
load_config_file: false,
|
||||
media_folder: 'src/lib/media',
|
||||
backend: {
|
||||
name: 'gitea',
|
||||
app_id: 'a046b53c-787a-4b76-bd3a-633221a38954',
|
||||
repo: 'repomaa/cv',
|
||||
},
|
||||
collections: [
|
||||
{
|
||||
name: 'profile',
|
||||
label: 'Profile',
|
||||
format: 'json',
|
||||
files: [
|
||||
{
|
||||
name: 'profile',
|
||||
label: 'Profile',
|
||||
file: 'src/lib/content/profile.json',
|
||||
fields: [
|
||||
{ name: 'name', label: 'Name', widget: 'string' },
|
||||
{ name: 'title', label: 'Title', widget: 'string' },
|
||||
{ name: 'email', label: 'Email', widget: 'string' },
|
||||
{ name: 'phone', label: 'Phone', widget: 'string', required: false },
|
||||
{ name: 'location', label: 'Location', widget: 'string' },
|
||||
{ name: 'website', label: 'Website', widget: 'string', required: false },
|
||||
{ name: 'github', label: 'GitHub', widget: 'string' },
|
||||
{ name: 'summary', label: 'Summary', widget: 'text' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'experience',
|
||||
label: 'Experience',
|
||||
format: 'json',
|
||||
folder: 'src/lib/content/experience',
|
||||
fields: [
|
||||
{ name: 'company', label: 'Company', widget: 'string' },
|
||||
{ name: 'position', label: 'Position', widget: 'string' },
|
||||
{ name: 'location', label: 'Location', widget: 'string', required: false },
|
||||
{ name: 'startDate', label: 'Start Date', widget: 'datetime', time_format: false },
|
||||
{
|
||||
name: 'endDate',
|
||||
label: 'End Date',
|
||||
widget: 'datetime',
|
||||
time_format: false,
|
||||
required: false,
|
||||
},
|
||||
{ name: 'description', label: 'Description', widget: 'markdown' },
|
||||
{
|
||||
name: 'technologies',
|
||||
label: 'Technologies',
|
||||
widget: 'list',
|
||||
field: { label: 'Technology', widget: 'string' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'education',
|
||||
label: 'Education',
|
||||
format: 'json',
|
||||
folder: 'src/lib/content/education',
|
||||
fields: [
|
||||
{ name: 'institution', label: 'Institution', widget: 'string' },
|
||||
{ name: 'degree', label: 'Degree', widget: 'string' },
|
||||
{ name: 'field', label: 'Field', widget: 'string', required: false },
|
||||
{ name: 'location', label: 'Location', widget: 'string', required: false },
|
||||
{ name: 'startDate', label: 'Start Date', widget: 'datetime', time_format: false },
|
||||
{
|
||||
name: 'endDate',
|
||||
label: 'End Date',
|
||||
widget: 'datetime',
|
||||
time_format: false,
|
||||
required: false,
|
||||
},
|
||||
{ name: 'description', label: 'Description', widget: 'markdown', required: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'skills',
|
||||
label: 'Skills',
|
||||
format: 'json',
|
||||
folder: 'src/lib/content/skills',
|
||||
fields: [
|
||||
{ name: 'category', label: 'Category', widget: 'string' },
|
||||
{
|
||||
name: 'items',
|
||||
label: 'Skills',
|
||||
widget: 'list',
|
||||
field: { label: 'Skill', widget: 'string' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Content Manager</title>
|
||||
</svelte:head>
|
||||
111
src/routes/joakim-repomaa-cv.pdf/+server.ts
Normal file
111
src/routes/joakim-repomaa-cv.pdf/+server.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { RequestHandler } from './$types.js';
|
||||
import type { LaunchOptions } from 'puppeteer';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
// This is a dynamic endpoint - not prerenderable
|
||||
export const prerender = false;
|
||||
|
||||
// PDF generation configuration
|
||||
const PDF_CONFIG = {
|
||||
format: 'A4' as const,
|
||||
printBackground: true,
|
||||
preferCSSPageSize: true,
|
||||
margin: {
|
||||
top: '20mm',
|
||||
right: '20mm',
|
||||
bottom: '20mm',
|
||||
left: '20mm',
|
||||
},
|
||||
};
|
||||
|
||||
// Browser launch options for different environments
|
||||
const getLaunchOptions = (): LaunchOptions => {
|
||||
const chromePath = process.env.PUPPETEER_EXECUTABLE_PATH;
|
||||
const options: LaunchOptions = {
|
||||
headless: true,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-accelerated-2d-canvas',
|
||||
'--disable-gpu',
|
||||
],
|
||||
};
|
||||
if (chromePath) {
|
||||
options.executablePath = chromePath;
|
||||
}
|
||||
return options;
|
||||
};
|
||||
|
||||
export const GET: RequestHandler = async ({ url }: { url: URL }) => {
|
||||
// Check if Chrome/Puppeteer is available
|
||||
const chromePath = process.env.PUPPETEER_EXECUTABLE_PATH;
|
||||
const isCI = process.env.CI || process.env.CONTINUOUS_INTEGRATION;
|
||||
|
||||
if (!chromePath && !isCI) {
|
||||
// Return 503 if Chrome is not available
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'PDF generation not available',
|
||||
message:
|
||||
'Chrome/Chromium is not configured. Set PUPPETEER_EXECUTABLE_PATH environment variable.',
|
||||
}),
|
||||
{
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the base URL for the current request
|
||||
const baseUrl = `${url.protocol}//${url.host}`;
|
||||
const pdfUrl = `${baseUrl}/print/`;
|
||||
|
||||
// Launch browser
|
||||
const browser = await puppeteer.launch(getLaunchOptions());
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Navigate to the PDF page with increased timeout
|
||||
// waitUntil: 'networkidle2' waits for 2 network connections to be idle
|
||||
// This is more lenient than 'networkidle0' which waits for 0 connections
|
||||
await page.goto(pdfUrl, {
|
||||
waitUntil: 'networkidle2',
|
||||
timeout: 120000, // Increased from 30s to 120s to handle slow GitHub API calls
|
||||
});
|
||||
|
||||
// Wait for fonts to load
|
||||
await page.evaluateHandle('document.fonts.ready');
|
||||
|
||||
// Generate PDF
|
||||
const pdfBuffer = await page.pdf(PDF_CONFIG);
|
||||
|
||||
// Close browser
|
||||
await browser.close();
|
||||
|
||||
// Create Blob from buffer for Response
|
||||
const pdfBlob = new Blob([pdfBuffer as unknown as ArrayBuffer], { type: 'application/pdf' });
|
||||
|
||||
// Return PDF with appropriate headers
|
||||
return new Response(pdfBlob, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/pdf',
|
||||
'Cache-Control': 'public, max-age=3600', // Cache for 1 hour
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('PDF generation error:', error);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Failed to generate PDF',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
24
src/routes/print/+page.server.ts
Normal file
24
src/routes/print/+page.server.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { PageServerLoad } from './$types.js';
|
||||
import { loadAllContent } from '$lib/content/loader.js';
|
||||
import { fetchGitHubProjects, fetchContributedRepos } from '$lib/github.js';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
const content = loadAllContent();
|
||||
|
||||
// Fetch GitHub projects and contributions at build time
|
||||
const [ownProjects, contributions] = await Promise.all([
|
||||
fetchGitHubProjects(content.profile.github),
|
||||
fetchContributedRepos(content.profile.github),
|
||||
]);
|
||||
|
||||
return {
|
||||
profile: content.profile,
|
||||
experience: content.experience,
|
||||
education: content.education,
|
||||
skills: content.skills,
|
||||
ownProjects: ownProjects.slice(0, 6),
|
||||
contributions: contributions.slice(0, 5),
|
||||
};
|
||||
};
|
||||
50
src/routes/print/+page.svelte
Normal file
50
src/routes/print/+page.svelte
Normal file
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import '../../app.css';
|
||||
import PDFContent from '$lib/components/PDFContent.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.profile.name} — CV (Print Version)</title>
|
||||
<meta name="description" content="Printable CV for {data.profile.name}" />
|
||||
|
||||
<!-- Print-specific styles -->
|
||||
<style>
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 15mm;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
background: white;
|
||||
color: black;
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
|
||||
* {
|
||||
-webkit-print-color-adjust: exact !important;
|
||||
print-color-adjust: exact !important;
|
||||
}
|
||||
|
||||
/* Explicitly hide skip link in PDF */
|
||||
.skip-link,
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</svelte:head>
|
||||
|
||||
<main class="min-h-screen bg-pdf-bg">
|
||||
<PDFContent
|
||||
profile={data.profile}
|
||||
experience={data.experience}
|
||||
education={data.education}
|
||||
skills={data.skills}
|
||||
ownProjects={data.ownProjects}
|
||||
contributions={data.contributions}
|
||||
/>
|
||||
</main>
|
||||
Reference in New Issue
Block a user