From e2ea60e3d3b13e0660d9f76daf2e549563bc38d6 Mon Sep 17 00:00:00 2001
From: Wolfgang Müller
Date: Thu, 20 Feb 2025 19:33:55 +0100
Subject: frontend: Introduce ComicCard and ArchiveCard

Instead of repeatedly supplying Card content in all the places it is
required, it makes more sense to create dedicated ComicCard and
ArchiveCard components. These wrap around Card itself and can be used in
a more straightforward and consistent fashion.

Whilst we are here, simplify and streamline the display of Comic and
Archive metadata by introducing a Card footer. The footer is used for
information on page count, release date, and archive size.
---
 frontend/src/lib/components/ArchiveCard.svelte | 39 ++++++++++++++
 frontend/src/lib/components/Card.svelte        | 39 ++++++--------
 frontend/src/lib/components/ComicCard.svelte   | 75 ++++++++++++++++++++++++++
 3 files changed, 130 insertions(+), 23 deletions(-)
 create mode 100644 frontend/src/lib/components/ArchiveCard.svelte
 create mode 100644 frontend/src/lib/components/ComicCard.svelte

(limited to 'frontend/src/lib/components')

diff --git a/frontend/src/lib/components/ArchiveCard.svelte b/frontend/src/lib/components/ArchiveCard.svelte
new file mode 100644
index 0000000..c9d283b
--- /dev/null
+++ b/frontend/src/lib/components/ArchiveCard.svelte
@@ -0,0 +1,39 @@
+<script lang="ts">
+	import type { ArchiveFragment } from '$gql/graphql';
+	import FooterPill from '$lib/pills/FooterPill.svelte';
+	import { filesize } from 'filesize';
+	import { type Snippet } from 'svelte';
+	import Card from './Card.svelte';
+
+	interface Props {
+		archive: ArchiveFragment;
+		overlay?: Snippet;
+		onclick?: (event: MouseEvent) => void;
+	}
+
+	let { archive, overlay, onclick }: Props = $props();
+
+	let details = $derived({
+		title: archive.name,
+		cover: archive.cover
+	});
+	let href = $derived(`/archives/${archive.id.toString()}`);
+</script>
+
+<Card {details} {href} {onclick} {overlay}>
+	{#snippet footer()}
+		<div class="flex flex-wrap gap-1">
+			<FooterPill text={`${archive.pageCount} pages`}>
+				{#snippet icon()}
+					<span class="icon-[material-symbols--description] mr-0.5 text-sm"></span>
+				{/snippet}
+			</FooterPill>
+			<div class="flex grow"></div>
+			<FooterPill text={filesize(archive.size, { base: 2 })}>
+				{#snippet icon()}
+					<span class="icon-[material-symbols--hard-drive] mr-0.5 text-sm"></span>
+				{/snippet}
+			</FooterPill>
+		</div>
+	{/snippet}
+</Card>
diff --git a/frontend/src/lib/components/Card.svelte b/frontend/src/lib/components/Card.svelte
index b8e8ecf..8cfe34d 100644
--- a/frontend/src/lib/components/Card.svelte
+++ b/frontend/src/lib/components/Card.svelte
@@ -1,5 +1,8 @@
-<script lang="ts" module>
-	import type { ComicFragment, ImageFragment } from '$gql/graphql';
+<script lang="ts">
+	import type { ImageFragment } from '$gql/graphql';
+	import { src } from '$lib/Utils';
+	import Star from '$lib/icons/Star.svelte';
+	import type { Snippet } from 'svelte';
 
 	interface CardDetails {
 		title: string;
@@ -8,24 +11,6 @@
 		cover?: ImageFragment;
 	}
 
-	export function comicCard(comic: ComicFragment) {
-		return {
-			href: `/comics/${comic.id.toString()}`,
-			details: {
-				title: comic.title,
-				subtitle: comic.originalTitle,
-				favourite: comic.favourite,
-				cover: comic.cover
-			}
-		};
-	}
-</script>
-
-<script lang="ts">
-	import { src } from '$lib/Utils';
-	import Star from '$lib/icons/Star.svelte';
-	import type { Snippet } from 'svelte';
-
 	interface Props {
 		href: string;
 		details: CardDetails;
@@ -33,6 +18,7 @@
 		coverOnly?: boolean;
 		overlay?: Snippet;
 		children?: Snippet;
+		footer?: Snippet;
 		onclick?: (event: MouseEvent) => void;
 	}
 
@@ -43,6 +29,7 @@
 		coverOnly = false,
 		overlay,
 		children,
+		footer,
 		onclick
 	}: Props = $props();
 </script>
@@ -66,8 +53,8 @@
 		/>
 	{/if}
 	{#if !coverOnly}
-		<article class="flex h-full flex-col gap-2 p-2">
-			<header>
+		<article class="p flex h-full flex-col p-2 pb-1">
+			<header class="mb-2">
 				<h2 class="self-center text-sm font-medium [grid-area:title]" title={details.title}>
 					{details.title}
 				</h2>
@@ -86,9 +73,15 @@
 				{/if}
 			</header>
 
-			<section class="max-h-full grow overflow-auto border-t border-slate-800/80 pt-2 text-xs">
+			<section class="max-h-full grow overflow-auto border-y border-slate-800/80 pt-2 text-xs">
 				{@render children?.()}
 			</section>
+
+			{#if footer}
+				<div class="mt-1 text-xs">
+					{@render footer()}
+				</div>
+			{/if}
 		</article>
 	{/if}
 </a>
diff --git a/frontend/src/lib/components/ComicCard.svelte b/frontend/src/lib/components/ComicCard.svelte
new file mode 100644
index 0000000..cb73e97
--- /dev/null
+++ b/frontend/src/lib/components/ComicCard.svelte
@@ -0,0 +1,75 @@
+<script lang="ts">
+	import type { ComicFragment } from '$gql/graphql';
+	import AssociationPill from '$lib/pills/AssociationPill.svelte';
+	import FooterPill from '$lib/pills/FooterPill.svelte';
+	import TagPill from '$lib/pills/TagPill.svelte';
+	import { type Snippet } from 'svelte';
+	import Card from './Card.svelte';
+
+	interface Props {
+		comic: ComicFragment;
+		overlay?: Snippet;
+		compact?: boolean;
+		coverOnly?: boolean;
+		onclick?: (event: MouseEvent) => void;
+	}
+
+	let { comic, overlay, compact, coverOnly, onclick }: Props = $props();
+
+	let details = $derived({
+		title: comic.title,
+		subtitle: comic.originalTitle,
+		favourite: comic.favourite,
+		cover: comic.cover
+	});
+	let href = $derived(`/comics/${comic.id.toString()}`);
+</script>
+
+<Card {details} {href} {compact} {onclick} {overlay} {coverOnly}>
+	<div class="flex flex-col gap-1">
+		{#if comic.artists.length || comic.circles.length}
+			<div class="flex flex-wrap gap-1">
+				{#each comic.artists as { name } (name)}
+					<AssociationPill {name} type="artist" />
+				{/each}
+				{#each comic.circles as { name } (name)}
+					<AssociationPill {name} type="circle" />
+				{/each}
+			</div>
+		{/if}
+		{#if comic.characters.length || comic.worlds.length}
+			<div class="flex flex-wrap gap-1">
+				{#each comic.worlds as { name } (name)}
+					<AssociationPill {name} type="world" />
+				{/each}
+				{#each comic.characters as { name } (name)}
+					<AssociationPill {name} type="character" />
+				{/each}
+			</div>
+		{/if}
+		{#if comic.tags.length}
+			<div class="flex flex-wrap gap-1">
+				{#each comic.tags as { name, description } (name)}
+					<TagPill {name} {description} />
+				{/each}
+			</div>
+		{/if}
+	</div>
+	{#snippet footer()}
+		<div class="flex flex-wrap gap-1">
+			<FooterPill text={`${comic.pageCount} pages`}>
+				{#snippet icon()}
+					<span class="icon-[material-symbols--description] mr-0.5 text-sm"></span>
+				{/snippet}
+			</FooterPill>
+			<div class="flex grow"></div>
+			{#if comic.date}
+				<FooterPill text={comic.date}>
+					{#snippet icon()}
+						<span class="icon-[material-symbols--calendar-today] mr-0.5 text-sm"></span>
+					{/snippet}
+				</FooterPill>
+			{/if}
+		</div>
+	{/snippet}
+</Card>
-- 
cgit v1.2.3-2-gb3c3