From 25b78c025491a00379fd7f79aa84c1cdd81fedf0 Mon Sep 17 00:00:00 2001
From: Fuwn <contact@fuwn.me>
Date: Wed, 7 Feb 2024 02:07:54 -0800
Subject: refactor(hololive): move to client-side evaluation

---
 src/routes/hololive/+page.svelte | 137 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 137 insertions(+)
 create mode 100644 src/routes/hololive/+page.svelte

(limited to 'src/routes/hololive/+page.svelte')

diff --git a/src/routes/hololive/+page.svelte b/src/routes/hololive/+page.svelte
new file mode 100644
index 00000000..42e01075
--- /dev/null
+++ b/src/routes/hololive/+page.svelte
@@ -0,0 +1,137 @@
+<script lang="ts">
+	import { onMount } from 'svelte';
+	import Message from '$lib/Loading/Message.svelte';
+	import Skeleton from '$lib/Loading/Skeleton.svelte';
+	import HeadTitle from '$lib/Home/HeadTitle.svelte';
+	import { parseScheduleHtml } from '$lib/hololive';
+
+	interface ParseResult {
+		lives: {
+			time: Date;
+			link: string;
+			videoId: string;
+			streamer: string;
+			livePreviewImage: string;
+			guests: string[];
+			streaming: boolean;
+		}[];
+		dict: Record<string, string>;
+	}
+
+	let schedulePromise: Promise<Response>;
+
+	onMount(async () => {
+		schedulePromise = fetch('https://schedule.hololive.tv', {
+			headers: {
+				Cookie: 'timezone=Asia/Tokyo'
+			}
+		});
+	});
+
+	const typeSchedule = (schedule: any) => schedule as ParseResult;
+</script>
+
+<HeadTitle route="hololive Schedule" path="/hololive" />
+
+{#await schedulePromise}
+	<Message message="Loading schedule ..." />
+
+	<Skeleton grid={true} count={100} width="49%" height="16.25em" />
+{:then scheduleResponse}
+	{#if scheduleResponse}
+		{#await scheduleResponse.text()}
+			<Message message="Parsing schedule ..." />
+
+			<Skeleton grid={true} count={100} width="49%" height="16.25em" />
+		{:then untypedSchedule}
+			{@const schedule = typeSchedule(parseScheduleHtml(untypedSchedule))}
+
+			{#if schedule.lives.length === 0}
+				<Message message="No upcoming streams." loader="ripple" />
+			{/if}
+
+			<div class="container">
+				{#each schedule.lives
+					.filter((live) => {
+						const time = new Date(live.time);
+
+						return time.getTime() > Date.now() - 12 * 60 * 60 * 1000 || time.getTime() > Date.now() || live.streaming;
+					})
+					.sort((a, b) => {
+						if (a.streaming && !b.streaming) return -1;
+
+						if (!a.streaming && b.streaming) return 1;
+
+						return new Date(a.time).getTime() - new Date(b.time).getTime();
+					}) as live}
+					<div class="stream card">
+						<p>
+							[{#if live.streaming}
+								<b class="live">LIVE</b>{:else}
+								<span class="upcoming">Upcoming</span>{/if}]
+							<b>{live.streamer}</b> <span class="opaque">|</span>
+							{new Date(live.time).toLocaleString()}
+							{#if live.guests.length > 0}
+								<br />
+								<small>
+									With {live.guests.join(', ').replace(/, ([^,]+)$/, ', & $1')}
+								</small>
+							{/if}
+						</p>
+
+						<a href={live.link} class="preview" target="_blank">
+							<img src={live.livePreviewImage} alt="Stream Thumbnail" />
+						</a>
+					</div>
+				{/each}
+			</div>
+		{:catch}
+			<Message message="Could not parse schedule. Please try again later." loader="ripple" />
+		{/await}
+	{:else}
+		<Message message="Loading schedule ..." />
+
+		<Skeleton grid={true} count={100} width="49%" height="16.25em" />
+	{/if}
+{:catch}
+	<Message message="Could not load schedule. Please try again later." loader="ripple" />
+{/await}
+
+<style lang="scss">
+	.preview {
+		// margin: 0.15rem;
+
+		img {
+			border-radius: 8px;
+			height: 20vh;
+			transition: transform 0.3s ease;
+		}
+
+		&:hover {
+			img {
+				position: relative;
+				z-index: 2;
+				transition: transform 0.3s ease;
+				transform: scale(1.05);
+			}
+		}
+
+		display: flex;
+		justify-content: center;
+		align-items: center;
+	}
+
+	.live {
+		color: var(--base0F);
+	}
+
+	.upcoming {
+		color: var(--base0C);
+	}
+
+	.container {
+		display: grid;
+		grid-template-columns: repeat(auto-fill, minmax(22.5em, 1fr));
+		gap: 0.5em;
+	}
+</style>
-- 
cgit v1.2.3

