Procházet zdrojové kódy

Create the currency page to display locations for user to click on

Lee Morgan před 2 týdny
rodič
revize
8616b170b5

+ 6 - 0
src/lib/currency.js

@@ -0,0 +1,6 @@
+export function currencyImageSrc(value) {
+	const raw = String(value ?? '');
+	if (!raw) return '';
+	if (raw.startsWith('/currencyimages/')) return raw;
+	return `/currencyimages/${raw.replace(/^\/+/, '')}`;
+}

+ 14 - 3
src/routes/+layout.svelte

@@ -10,6 +10,7 @@
 	const isGalleryMap = $derived(page.url.pathname === '/gallery');
 	const isGallery = $derived(isGalleryMap || page.url.pathname.startsWith('/gallery/'));
 	const isResume = $derived(page.url.pathname === '/resume');
+	const isCurrency = $derived(page.url.pathname === '/currency' || page.url.pathname.startsWith('/currency/'));
 </script>
 
 <svelte:head>
@@ -44,14 +45,24 @@
 					<li>
 						<a href="/gallery" aria-current={isGallery ? 'page' : undefined}>Gallery</a>
 					</li>
-					<li><a href="/">Currency</a></li>
+					<li>
+						<a href="/currency" data-sveltekit-reload aria-current={isCurrency ? 'page' : undefined}
+							>Currency</a
+						>
+					</li>
 					<li>
 						<a href="/resume" data-sveltekit-reload aria-current={isResume ? 'page' : undefined}
 							>Resume/CV</a
 						>
 					</li>
-					<li><a href="/">Git</a></li>
-					<li><a href="/">LinkedIn</a></li>
+					<li>
+						<a href="https://git.leemorgan.dev/lee" rel="noopener noreferrer" target="_blank">Git</a>
+					</li>
+					<li>
+						<a href="https://www.linkedin.com/in/leemorganio" rel="noopener noreferrer" target="_blank"
+							>LinkedIn</a
+						>
+					</li>
 				</ul>
 			</nav>
 			<a class="btn" href="mailto:lee@leemorgan.dev">Get in touch</a>

+ 32 - 0
src/routes/currency/+page.server.js

@@ -0,0 +1,32 @@
+import { currencyImageSrc } from '#lib/currency.js';
+import { getDb } from '#lib/server/mongo.js';
+
+export const prerender = false;
+
+export async function load() {
+	let locations = [];
+
+	try {
+		const db = await getDb();
+		const docs = await db
+			.collection('currencies')
+			.find({})
+			.project({ location: 1, frontImage: 1 })
+			.toArray();
+
+		const groups = new Map();
+		for (const doc of docs) {
+			const name = doc.location?.trim() || 'Unknown';
+			const group = groups.get(name) ?? { name, count: 0, image: '' };
+			group.count += 1;
+			if (!group.image) group.image = currencyImageSrc(doc.frontImage);
+			groups.set(name, group);
+		}
+
+		locations = [...groups.values()].sort((a, b) => a.name.localeCompare(b.name));
+	} catch (error) {
+		console.error('Failed to load currencies', error);
+	}
+
+	return { locations };
+}

+ 101 - 0
src/routes/currency/+page.svelte

@@ -0,0 +1,101 @@
+<script>
+	let { data } = $props();
+</script>
+
+<svelte:head>
+	<title>Currency — Lee Morgan</title>
+	<meta name="description" content="Currency collection grouped by location." />
+</svelte:head>
+
+<section class="currency-page">
+	<h1>Currency</h1>
+	{#if data.locations.length}
+		<ul class="places">
+			{#each data.locations as place}
+				<li>
+					<div class="card place">
+						{#if place.image}
+							<img src={place.image} alt="" />
+						{/if}
+						<div class="copy">
+							<h2>{place.name}</h2>
+							<p>{place.count} {place.count === 1 ? 'note' : 'notes'}</p>
+						</div>
+					</div>
+				</li>
+			{/each}
+		</ul>
+	{:else}
+		<p class="empty">No locations yet.</p>
+	{/if}
+</section>
+
+<style>
+	.currency-page {
+		max-width: 52rem;
+	}
+
+	h1 {
+		font-size: 1.25rem;
+		font-weight: 500;
+		letter-spacing: 0.02em;
+		margin-bottom: 1.5rem;
+	}
+
+	.places {
+		list-style: none;
+		margin: 0;
+		padding: 0;
+		display: grid;
+		grid-template-columns: 1fr;
+		grid-auto-rows: 1fr;
+		gap: 0.85rem;
+	}
+
+	.places li {
+		display: flex;
+		min-height: 0;
+	}
+
+	.place {
+		display: flex;
+		flex-direction: column;
+		width: 100%;
+		overflow: hidden;
+		padding: 0;
+	}
+
+	.place img {
+		display: block;
+		width: 100%;
+		aspect-ratio: 16 / 9;
+		object-fit: cover;
+		background: var(--bg);
+	}
+
+	.copy {
+		padding: 1.25rem 1.4rem 1.35rem;
+	}
+
+	.place h2 {
+		font-size: 1.125rem;
+		font-weight: 600;
+		letter-spacing: 0.02em;
+	}
+
+	.place p {
+		margin-top: 0.35rem;
+		color: var(--muted);
+		font-size: 0.9375rem;
+	}
+
+	.empty {
+		color: var(--muted);
+	}
+
+	@media (min-width: 800px) {
+		.places {
+			grid-template-columns: repeat(2, 1fr);
+		}
+	}
+</style>

+ 42 - 0
src/routes/currencyimages/[file]/+server.js

@@ -0,0 +1,42 @@
+import { createReadStream } from 'node:fs';
+import { stat } from 'node:fs/promises';
+import { homedir } from 'node:os';
+import { join } from 'node:path';
+import { Readable } from 'node:stream';
+import { error } from '@sveltejs/kit';
+
+export const prerender = false;
+
+const ROOT = join(homedir(), 'srv/leemorgan_dev/currencyimages');
+
+const TYPES = {
+	jpg: 'image/jpeg',
+	jpeg: 'image/jpeg',
+	png: 'image/png',
+	webp: 'image/webp',
+	gif: 'image/gif'
+};
+
+export async function GET({ params }) {
+	const file = params.file;
+	if (!/^[A-Za-z0-9._-]+$/.test(file)) error(400, 'Invalid file');
+
+	const path = join(ROOT, file);
+
+	try {
+		const info = await stat(path);
+		if (!info.isFile()) error(404, 'Not found');
+	} catch {
+		error(404, 'Not found');
+	}
+
+	const ext = file.split('.').pop()?.toLowerCase() ?? '';
+	const body = Readable.toWeb(createReadStream(path));
+
+	return new Response(body, {
+		headers: {
+			'content-type': TYPES[ext] ?? 'application/octet-stream',
+			'cache-control': 'public, max-age=31536000, immutable'
+		}
+	});
+}