diff options
| author | Fuwn <[email protected]> | 2024-02-07 01:07:34 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2024-02-07 01:07:34 -0800 |
| commit | b2cd85ceddb1d39924b858cb198a9784954fd4c5 (patch) | |
| tree | 62c7afc68bbdf06da7221d0763db0c03bc28fcfa /src/routes | |
| parent | fix(match): skip specials and ovas in schedule (diff) | |
| download | due.moe-b2cd85ceddb1d39924b858cb198a9784954fd4c5.tar.xz due.moe-b2cd85ceddb1d39924b858cb198a9784954fd4c5.zip | |
feat(hololive): hololive schedule
Diffstat (limited to 'src/routes')
| -rw-r--r-- | src/routes/+layout.svelte | 5 | ||||
| -rw-r--r-- | src/routes/api/hololive/+server.ts | 10 | ||||
| -rw-r--r-- | src/routes/hololive/+page.svelte | 127 |
3 files changed, 140 insertions, 2 deletions
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index e8fb9f9b..cc09bb94 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -113,8 +113,9 @@ <Dropdown items={[ { name: $locale().navigation.subtitleSchedule, url: root('/schedule') }, - { name: $locale().navigation.newReleases, url: root('/updates') }, - { name: $locale().tools.tool.characterBirthdays.short, url: root('/birthdays') } + { name: $locale().navigation.hololive, url: root('/hololive') }, + { name: $locale().tools.tool.characterBirthdays.short, url: root('/birthdays') }, + { name: $locale().navigation.newReleases, url: root('/updates') } ]} title={$locale().navigation.schedule} /> diff --git a/src/routes/api/hololive/+server.ts b/src/routes/api/hololive/+server.ts new file mode 100644 index 00000000..331d776a --- /dev/null +++ b/src/routes/api/hololive/+server.ts @@ -0,0 +1,10 @@ +import parseScheduleHtml from 'holo-schedule'; +import getScheduleHtml from 'holo-schedule/lib/getScheduleHtml'; + +export const GET = async () => + Response.json(parseScheduleHtml(await getScheduleHtml()), { + headers: { + 'Access-Control-Allow-Origin': 'https://due.moe', + 'Cache-Control': 'public, max-age=300, s-maxage=300' + } + }); diff --git a/src/routes/hololive/+page.svelte b/src/routes/hololive/+page.svelte new file mode 100644 index 00000000..bd49bbf8 --- /dev/null +++ b/src/routes/hololive/+page.svelte @@ -0,0 +1,127 @@ +<script lang="ts"> + import { onMount } from 'svelte'; + import Message from '$lib/Loading/Message.svelte'; + import Skeleton from '$lib/Loading/Skeleton.svelte'; + + 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(() => (schedulePromise = fetch('/api/hololive'))); + + const typeSchedule = (schedule: any) => schedule as ParseResult; +</script> + +{#await schedulePromise} + <Message message="Loading schedule ..." /> + + <Skeleton grid={true} count={100} width="49%" height="16.25em" /> +{:then scheduleResponse} + {#if scheduleResponse} + {#await scheduleResponse.json()} + <Message message="Parsing schedule ..." /> + + <Skeleton grid={true} count={100} width="49%" height="16.25em" /> + {:then untypedSchedule} + {@const schedule = typeSchedule(untypedSchedule)} + + {#if schedule.lives.length === 0} + <Message message="No upcoming streams." /> + {/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"> + <img src={live.livePreviewImage} alt="Stream Preview" /> + </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> |