This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
<script>
|
||||
let { children } = $props();
|
||||
import '../../app.css';
|
||||
</script>
|
||||
|
||||
<div class="font-mono bg-background text-foreground">
|
||||
@@ -34,7 +34,7 @@
|
||||
<a
|
||||
href="/joakim-repomaa-cv.pdf"
|
||||
download
|
||||
class="no-print fixed top-0 right-0 w-24 h-24 z-50 group"
|
||||
class="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"
|
||||
@@ -51,12 +51,3 @@
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
|
||||
<style>
|
||||
/* Ensure proper print styling */
|
||||
@media print {
|
||||
main {
|
||||
background: white !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
21
src/routes/(content)/+layout.server.ts
Normal file
21
src/routes/(content)/+layout.server.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { loadAllContent } from '$lib/content/loader';
|
||||
import { fetchContributedRepos, fetchGitHubProjects } from '$lib/github';
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
|
||||
export const load: LayoutServerLoad = async () => {
|
||||
const content = await loadAllContent();
|
||||
|
||||
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,
|
||||
contributions,
|
||||
};
|
||||
};
|
||||
14
src/routes/(content)/print/+layout.svelte
Normal file
14
src/routes/(content)/print/+layout.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script>
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<main class="min-h-screen bg-pdf-bg text-pdf-fg">
|
||||
{@render children()}
|
||||
</main>
|
||||
|
||||
<style>
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 15mm;
|
||||
}
|
||||
</style>
|
||||
18
src/routes/(content)/print/+page.svelte
Normal file
18
src/routes/(content)/print/+page.svelte
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import PDFContent from '$lib/components/PDFContent.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.profile.name} — CV</title>
|
||||
</svelte:head>
|
||||
|
||||
<PDFContent
|
||||
profile={data.profile}
|
||||
experience={data.experience}
|
||||
education={data.education}
|
||||
skills={data.skills}
|
||||
ownProjects={data.ownProjects}
|
||||
contributions={data.contributions}
|
||||
/>
|
||||
@@ -1,24 +0,0 @@
|
||||
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),
|
||||
};
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
<script>
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
@@ -1,113 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import config from '$lib/content/config';
|
||||
|
||||
onMount(async () => {
|
||||
const CMS = await import('@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: 'avatar', label: 'Avatar', widget: 'image', required: false },
|
||||
{
|
||||
name: 'profilePicture',
|
||||
label: 'Profile Picture (for PDF)',
|
||||
widget: 'image',
|
||||
required: false,
|
||||
},
|
||||
{ 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' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
CMS.init({ config });
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { RequestHandler } from './$types.js';
|
||||
import type { LaunchOptions } from 'puppeteer';
|
||||
import { dev } from '$app/environment';
|
||||
import puppeteer from 'puppeteer';
|
||||
import * as cheerio from 'cheerio';
|
||||
import path from 'path';
|
||||
@@ -37,7 +38,22 @@ const getLaunchOptions = (): LaunchOptions => {
|
||||
return options;
|
||||
};
|
||||
|
||||
export const GET: RequestHandler = async ({ url, fetch }) => {
|
||||
/**
|
||||
* Prepares the print page HTML and returns the URL to use for PDF generation.
|
||||
* During dev server, returns the dev server URL directly.
|
||||
* During build time, copies assets to temp dir and returns file:// URL.
|
||||
*/
|
||||
const preparePrintPageUrl = async (
|
||||
requestUrl: URL,
|
||||
fetch: typeof globalThis.fetch
|
||||
): Promise<string> => {
|
||||
// In dev mode, just use the dev server URL
|
||||
if (dev) {
|
||||
const devUrl = new URL('/print/', requestUrl);
|
||||
return devUrl.toString();
|
||||
}
|
||||
|
||||
// Build time: prepare files in temp directory
|
||||
const tmpDir = await mkdtemp(path.join(tmpdir(), 'cv-pdf-genration-'));
|
||||
const tmpFile = (url: string) => {
|
||||
const filename = path.basename(url);
|
||||
@@ -45,63 +61,70 @@ export const GET: RequestHandler = async ({ url, fetch }) => {
|
||||
return tempFile;
|
||||
};
|
||||
|
||||
try {
|
||||
// Launch browser
|
||||
const browser = await puppeteer.launch(getLaunchOptions());
|
||||
const page = await browser.newPage();
|
||||
const printResponse = await fetch('/print/');
|
||||
const html = await printResponse.text();
|
||||
const $ = cheerio.load(html);
|
||||
const printResponse = await fetch('/print/');
|
||||
const html = await printResponse.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const fileDownloads: Record<string, string> = {};
|
||||
const fileDownloads: Record<string, string> = {};
|
||||
|
||||
$('script[src], link[rel="stylesheet"], img[src]').each((i, el) => {
|
||||
if (el.tagName === 'link') {
|
||||
const href = $(el).attr('href');
|
||||
if (href) {
|
||||
const tempFile = (fileDownloads[href] ||= tmpFile(href));
|
||||
$(el).attr('href', `file://${tempFile}`);
|
||||
}
|
||||
} else {
|
||||
const src = $(el).attr('src');
|
||||
if (src) {
|
||||
const tempFile = (fileDownloads[src] ||= tmpFile(src));
|
||||
$(el).attr('src', `file://${tempFile}`);
|
||||
}
|
||||
$('script[src], link[rel="stylesheet"], img[src]').each((i, el) => {
|
||||
if (el.tagName === 'link') {
|
||||
const href = $(el).attr('href');
|
||||
if (href) {
|
||||
const tempFile = (fileDownloads[href] ||= tmpFile(href));
|
||||
$(el).attr('href', `file://${tempFile}`);
|
||||
}
|
||||
});
|
||||
|
||||
$('style[src]').each((i, el) => {
|
||||
} else {
|
||||
const src = $(el).attr('src');
|
||||
if (src) {
|
||||
const tempFile = (fileDownloads[src] ||= tmpFile(src));
|
||||
$(el).attr('src', `file://${tempFile}`);
|
||||
}
|
||||
});
|
||||
|
||||
$('style:not([src])').each((i, el) => {
|
||||
const content = $(el).text();
|
||||
$(el).text(
|
||||
content.replaceAll(/(?<=url\(".+?)(?=")/g, (match) => {
|
||||
const url = match[0];
|
||||
const tempFile = (fileDownloads[url] ||= tmpFile(url));
|
||||
return `file://${tempFile}`;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const dir = await opendir('.svelte-kit/output/client/_app/immutable/assets');
|
||||
for await (const file of dir) {
|
||||
await copyFile(path.join(file.parentPath, file.name), tmpFile(file.name));
|
||||
}
|
||||
});
|
||||
|
||||
const htmlFile = path.join(tmpDir, 'index.html');
|
||||
await writeFile(htmlFile, $.root().html() ?? '');
|
||||
$('style[src]').each((i, el) => {
|
||||
const src = $(el).attr('src');
|
||||
if (src) {
|
||||
const tempFile = (fileDownloads[src] ||= tmpFile(src));
|
||||
$(el).attr('src', `file://${tempFile}`);
|
||||
}
|
||||
});
|
||||
|
||||
$('style:not([src])').each((i, el) => {
|
||||
const content = $(el).text();
|
||||
$(el).text(
|
||||
content.replaceAll(/(?<=url\(".+?)(?=")/g, (match) => {
|
||||
const url = match[0];
|
||||
const tempFile = (fileDownloads[url] ||= tmpFile(url));
|
||||
return `file://${tempFile}`;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const dir = await opendir('.svelte-kit/output/client/_app/immutable/assets');
|
||||
for await (const file of dir) {
|
||||
await copyFile(path.join(file.parentPath, file.name), tmpFile(file.name));
|
||||
}
|
||||
|
||||
const htmlFile = path.join(tmpDir, 'index.html');
|
||||
await writeFile(htmlFile, $.root().html() ?? '');
|
||||
|
||||
return `file://${htmlFile}`;
|
||||
};
|
||||
|
||||
export const GET: RequestHandler = async ({ url, fetch }) => {
|
||||
try {
|
||||
// Launch browser
|
||||
const browser = await puppeteer.launch(getLaunchOptions());
|
||||
const page = await browser.newPage();
|
||||
|
||||
const pageUrl = await preparePrintPageUrl(url, fetch);
|
||||
|
||||
// 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(`file://${htmlFile}`, { waitUntil: 'networkidle2' });
|
||||
await page.goto(pageUrl, { waitUntil: 'networkidle2' });
|
||||
// Wait for fonts to load
|
||||
await page.evaluateHandle('document.fonts.ready');
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
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),
|
||||
};
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
<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