trackers: media hub with webtoons, podcasts and videos; shows and anime filters
Deploy production / deploy (push) Successful in 1m22s
Deploy production / deploy (push) Successful in 1m22s
- /trackers/media: one card per child (reading, movies, shows, webtoons, music, podcasts, videos) with quick stats, a combined currently-consuming feed and a mixed watchlist - webtoons, podcasts, videos: new owner-editable trackers on one generic media.items table, JSON import (owner form or scripts/media-run.sh) - shows: /trackers/shows -> /trackers/movies?type=show; anime flag on movies.titles (backfilled, set from TMDB, owner toggle) and ?type=anime filter - nav: media replaces the top-level reading, movies and music entries; their URLs are kept
This commit is contained in:
1 parent
4ef75f9d25
commit
079399635d
33 files changed
+48352
-44
No files matched your search
@@ -42,8 +42,12 @@ const staticPaths = [
|
||||
'/trackers/trains/countries',
|
||||
'/trackers/wildlife',
|
||||
'/trackers/wildlife/groups',
|
||||
'/trackers/media',
|
||||
'/trackers/music',
|
||||
'/trackers/movies',
|
||||
'/trackers/webtoons',
|
||||
'/trackers/podcasts',
|
||||
'/trackers/videos',
|
||||
'/trackers/pc',
|
||||
'/trackers/games',
|
||||
...gameTrackers.map((g) => g.href),
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { isOwner } from '#lib/server/owner.js';
|
||||
import { loadHub } from '#lib/server/media/hub.js';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
// The Media hub: one card per child tracker, what we are reading / watching / hearing now,
|
||||
// and one watchlist across all of them. Public (movies and shows follow their own switch).
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const owner = isOwner(event);
|
||||
if (owner) event.setHeaders({ 'cache-control': 'private, no-store' });
|
||||
return {
|
||||
owner,
|
||||
...(await loadHub(owner)),
|
||||
seo: {
|
||||
title: 'Media',
|
||||
description:
|
||||
'Everything we read, watch and listen to: books, movies, shows and anime, webtoons, music, podcasts and videos.'
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,463 @@
|
||||
<script lang="ts">
|
||||
// The Media hub: a card per child tracker with its quick stats, what is on right now
|
||||
// across all of them, and one mixed watchlist. Data comes from #lib/server/media/hub.
|
||||
import ArrowLeft from 'lucide-svelte/icons/arrow-left';
|
||||
import TrackerIcon from '#lib/components/TrackerIcon.svelte';
|
||||
import PrivateLock from '#lib/components/PrivateLock.svelte';
|
||||
import { mediaTrackers } from '#lib/projects.js';
|
||||
import type { HubEntry, HubType } from '#lib/server/media/hub.js';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
const NAMES: Record<HubType, string> = {
|
||||
reading: 'reading',
|
||||
movies: 'movies',
|
||||
shows: 'shows',
|
||||
webtoons: 'webtoons',
|
||||
music: 'music',
|
||||
podcasts: 'podcasts',
|
||||
videos: 'videos'
|
||||
};
|
||||
const MOVIE_KIDS = new Set(['movies', 'shows']);
|
||||
|
||||
const cards = $derived(
|
||||
mediaTrackers.filter((t) => data.moviesVisible || !MOVIE_KIDS.has(t.name))
|
||||
);
|
||||
|
||||
let typeFilter = $state<HubType | 'all'>('all');
|
||||
let showAll = $state(false);
|
||||
const CAP = 24;
|
||||
const counts = $derived.by(() => {
|
||||
const c: Partial<Record<HubType, number>> = {};
|
||||
for (const e of data.watchlist) c[e.type] = (c[e.type] ?? 0) + 1;
|
||||
return c;
|
||||
});
|
||||
const filtered = $derived(
|
||||
typeFilter === 'all' ? data.watchlist : data.watchlist.filter((e) => e.type === typeFilter)
|
||||
);
|
||||
const shown = $derived(showAll ? filtered : filtered.slice(0, CAP));
|
||||
|
||||
function initials(s: string) {
|
||||
return (
|
||||
s
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((w) => w[0]?.toUpperCase())
|
||||
.join('') || '?'
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet thumb(e: HubEntry)}
|
||||
<span class="thumb">
|
||||
{#if e.image}
|
||||
<img src={e.image} alt="" loading="lazy" referrerpolicy="no-referrer" />
|
||||
{:else}
|
||||
<span class="init">{initials(e.title)}</span>
|
||||
{/if}
|
||||
<span class="badge"><TrackerIcon name={e.type} class="size-5" /></span>
|
||||
</span>
|
||||
{/snippet}
|
||||
|
||||
<div class="page">
|
||||
<a class="back" href="/trackers"><ArrowLeft size={14} /> trackers</a>
|
||||
<header class="head">
|
||||
<TrackerIcon name="media" class="size-11" />
|
||||
<div>
|
||||
<h1>media</h1>
|
||||
<p class="blurb">everything we read, watch and listen to, in one place.</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="cards" aria-label="media trackers">
|
||||
{#each cards as t (t.name)}
|
||||
{@const st = data.stats[t.name as HubType]}
|
||||
<a class="card" href={t.href}>
|
||||
<span class="card-top">
|
||||
<TrackerIcon name={t.name} class="size-9" />
|
||||
<span class="card-name"
|
||||
>{t.name}{#if data.owner && data.moviesPrivate && MOVIE_KIDS.has(t.name)}<PrivateLock
|
||||
/>{/if}</span
|
||||
>
|
||||
</span>
|
||||
{#if st?.length}
|
||||
<span class="stats">
|
||||
{#each st as s (s.label)}
|
||||
<span class="stat"><b>{s.value}</b> {s.label}</span>
|
||||
{/each}
|
||||
</span>
|
||||
{/if}
|
||||
{#if t.name === 'music' && data.music}
|
||||
<span class="music-line" class:live={data.music.nowPlaying}>
|
||||
{data.music.nowPlaying ? '▶ ' : 'last: '}{data.music.track}
|
||||
<span class="dim">· {data.music.artists}</span>
|
||||
</span>
|
||||
{:else}
|
||||
<span class="desc">{t.subtitle}</span>
|
||||
{/if}
|
||||
</a>
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="now-h">
|
||||
<h2 id="now-h">currently consuming <span class="count">{data.current.length}</span></h2>
|
||||
{#if data.current.length}
|
||||
<ul class="feed">
|
||||
{#each data.current as e (e.key)}
|
||||
<li>
|
||||
<a class="entry" class:live={e.live} href={e.href}>
|
||||
{@render thumb(e)}
|
||||
<span class="body">
|
||||
<span class="title">{e.title}</span>
|
||||
{#if e.sub}<span class="sub">{e.sub}</span>{/if}
|
||||
{#if e.progress != null || e.progressText}
|
||||
<span class="prog">
|
||||
{#if e.progress != null}
|
||||
<span class="bar"
|
||||
><span style:width="{Math.round(e.progress * 100)}%"></span></span
|
||||
>
|
||||
{/if}
|
||||
{#if e.progressText}<span class="ptext">{e.progressText}</span>{/if}
|
||||
</span>
|
||||
{/if}
|
||||
<span class="kind">{NAMES[e.type]}</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<p class="empty">nothing on right now.</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="wl-h">
|
||||
<h2 id="wl-h">up next <span class="count">{data.watchlist.length}</span></h2>
|
||||
{#if data.watchlist.length}
|
||||
<div class="chips" role="group" aria-label="filter by type">
|
||||
<button
|
||||
type="button"
|
||||
class="chip"
|
||||
aria-pressed={typeFilter === 'all'}
|
||||
onclick={() => (typeFilter = 'all')}>all <span>{data.watchlist.length}</span></button
|
||||
>
|
||||
{#each cards as t (t.name)}
|
||||
{@const n = counts[t.name as HubType] ?? 0}
|
||||
{#if n}
|
||||
<button
|
||||
type="button"
|
||||
class="chip"
|
||||
aria-pressed={typeFilter === t.name}
|
||||
onclick={() => (typeFilter = t.name as HubType)}
|
||||
><TrackerIcon name={t.name} class="size-4" /> {t.name} <span>{n}</span></button
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<ul class="wl">
|
||||
{#each shown as e (e.key)}
|
||||
<li>
|
||||
<a class="entry small" href={e.href}>
|
||||
{@render thumb(e)}
|
||||
<span class="body">
|
||||
<span class="title">{e.title}</span>
|
||||
{#if e.sub}<span class="sub">{e.sub}</span>{/if}
|
||||
<span class="kind"
|
||||
>{NAMES[e.type]}{#if e.progressText} · {e.progressText}{/if}</span
|
||||
>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if filtered.length > CAP}
|
||||
<button type="button" class="more" onclick={() => (showAll = !showAll)}>
|
||||
{showAll ? 'show less' : `show all ${filtered.length}`}
|
||||
</button>
|
||||
{/if}
|
||||
{:else}
|
||||
<p class="empty">the watchlist is empty.</p>
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
max-width: 64rem;
|
||||
margin: 0 auto;
|
||||
padding: 1.25rem 1rem 4rem;
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
}
|
||||
.back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--site-muted);
|
||||
text-decoration: none;
|
||||
width: fit-content;
|
||||
}
|
||||
.back:hover {
|
||||
color: inherit;
|
||||
}
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.9rem;
|
||||
margin-top: -1rem;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 1.9rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.blurb {
|
||||
margin: 0.15rem 0 0;
|
||||
color: var(--site-muted);
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.05rem;
|
||||
margin: 0 0 0.75rem;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.count {
|
||||
font-size: 0.8rem;
|
||||
color: var(--site-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(13.5rem, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.card {
|
||||
display: grid;
|
||||
gap: 0.55rem;
|
||||
align-content: start;
|
||||
padding: 0.9rem;
|
||||
border: 1px solid var(--site-border);
|
||||
border-radius: 0.9rem;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
background: var(--site-surface, transparent);
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
transform 0.15s;
|
||||
}
|
||||
.card:hover {
|
||||
border-color: currentColor;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.card-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.card-name {
|
||||
font-weight: 650;
|
||||
font-size: 1.05rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
.stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.2rem 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--site-muted);
|
||||
}
|
||||
.stat b {
|
||||
color: var(--site-text);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.desc,
|
||||
.music-line {
|
||||
font-size: 0.8rem;
|
||||
color: var(--site-muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
.music-line.live {
|
||||
color: #16a34a;
|
||||
}
|
||||
.dim {
|
||||
opacity: 0.75;
|
||||
}
|
||||
.feed,
|
||||
.wl {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.wl {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
}
|
||||
.entry {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
padding: 0.6rem;
|
||||
border: 1px solid var(--site-border);
|
||||
border-radius: 0.75rem;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.entry:hover {
|
||||
border-color: currentColor;
|
||||
}
|
||||
.entry.live {
|
||||
border-color: #16a34a;
|
||||
}
|
||||
.thumb {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 3.4rem;
|
||||
height: 5rem;
|
||||
border-radius: 0.45rem;
|
||||
background: rgb(127 127 127 / 0.15);
|
||||
}
|
||||
.small .thumb {
|
||||
width: 2.6rem;
|
||||
height: 3.8rem;
|
||||
}
|
||||
.thumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: inherit;
|
||||
display: block;
|
||||
}
|
||||
.init {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
height: 100%;
|
||||
font-weight: 700;
|
||||
color: var(--site-muted);
|
||||
}
|
||||
.badge {
|
||||
position: absolute;
|
||||
right: -0.35rem;
|
||||
bottom: -0.35rem;
|
||||
display: flex;
|
||||
border-radius: 0.4rem;
|
||||
box-shadow: 0 0 0 2px var(--site-bg);
|
||||
}
|
||||
.body {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 0.15rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.title {
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.sub,
|
||||
.kind {
|
||||
font-size: 0.78rem;
|
||||
color: var(--site-muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.prog {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
font-size: 0.75rem;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
.bar {
|
||||
flex: 1;
|
||||
min-width: 3rem;
|
||||
height: 0.3rem;
|
||||
border-radius: 1rem;
|
||||
background: rgb(127 127 127 / 0.2);
|
||||
overflow: hidden;
|
||||
}
|
||||
.bar span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background: currentColor;
|
||||
border-radius: inherit;
|
||||
}
|
||||
.ptext {
|
||||
color: var(--site-muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.3rem 0.65rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--site-border);
|
||||
background: none;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-size: 0.82rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.chip span {
|
||||
color: var(--site-muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.chip[aria-pressed='true'] {
|
||||
background: var(--site-text);
|
||||
color: var(--site-bg);
|
||||
border-color: transparent;
|
||||
}
|
||||
.chip[aria-pressed='true'] span {
|
||||
color: inherit;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.more {
|
||||
margin-top: 0.75rem;
|
||||
background: none;
|
||||
border: 1px solid var(--site-border);
|
||||
border-radius: 999px;
|
||||
padding: 0.4rem 0.9rem;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.empty {
|
||||
color: var(--site-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@media (max-width: 30rem) {
|
||||
.cards {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.card {
|
||||
padding: 0.7rem;
|
||||
}
|
||||
.desc {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -50,7 +50,23 @@
|
||||
let status = $state<'all' | (typeof STATUSES)[number]>(
|
||||
STATUSES.find((s) => s === initialStatus) ?? 'all'
|
||||
);
|
||||
let kind = $state<'all' | 'movie' | 'show'>('all');
|
||||
// ?type=movie|show|anime (the Media hub's Movies, Shows and Anime links) preselects the type.
|
||||
const KINDS = ['all', 'movie', 'show', 'anime'] as const;
|
||||
type Kind = (typeof KINDS)[number];
|
||||
const initialKind = page.url.searchParams.get('type');
|
||||
let kind = $state<Kind>(KINDS.find((k) => k === initialKind) ?? 'all');
|
||||
function pickKind(next: Kind) {
|
||||
kind = next;
|
||||
limit = 120;
|
||||
const params = new SvelteURLSearchParams(page.url.searchParams);
|
||||
if (next === 'all') params.delete('type');
|
||||
else params.set('type', next);
|
||||
const qs = params.toString();
|
||||
history.replaceState(history.state, '', qs ? `?${qs}` : page.url.pathname);
|
||||
}
|
||||
const heading = $derived(
|
||||
kind === 'movie' ? 'movies' : kind === 'show' ? 'shows' : kind === 'anime' ? 'anime' : 'movies'
|
||||
);
|
||||
let who = $state<'all' | (typeof WHO)[number]>('all');
|
||||
let sort = $state<'recent' | 'rating' | 'title' | 'year'>('recent');
|
||||
let query = $state('');
|
||||
@@ -67,7 +83,7 @@
|
||||
const list = data.titles.filter(
|
||||
(t) =>
|
||||
(status === 'all' || t.status === status) &&
|
||||
(kind === 'all' || t.type === kind) &&
|
||||
(kind === 'all' || (kind === 'anime' ? t.anime : t.type === kind)) &&
|
||||
(who === 'all' || t.watchedBy === who || (who !== 'both' && t.watchedBy === 'both')) &&
|
||||
(!q ||
|
||||
t.title.toLowerCase().includes(q) ||
|
||||
@@ -159,15 +175,16 @@
|
||||
<TrackerIcon name="movies" class="size-10" />
|
||||
<div>
|
||||
<h1>
|
||||
movies {#if data.visibility === 'private'}<PrivateLock size={13} />{/if}
|
||||
{heading}
|
||||
{#if data.visibility === 'private'}<PrivateLock size={13} />{/if}
|
||||
</h1>
|
||||
<p>Movies and TV shows Juhee and I watch, from Netflix, Plex and the cinema.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mv-head-links">
|
||||
<a class="tile" href={resolve('/trackers/music')}>
|
||||
<TrackerIcon name="music" class="size-7" />
|
||||
<span><strong>music</strong><small>what I listen to</small></span>
|
||||
<a class="tile" href={resolve('/trackers/media')}>
|
||||
<TrackerIcon name="media" class="size-7" />
|
||||
<span><strong>media</strong><small>all we read, watch and hear</small></span>
|
||||
</a>
|
||||
{#if data.owner}
|
||||
<button type="button" class="tile" onclick={() => adder?.showModal()}>
|
||||
@@ -258,10 +275,15 @@
|
||||
aria-label="Search"
|
||||
/>
|
||||
</label>
|
||||
<select bind:value={kind} aria-label="Type">
|
||||
<select
|
||||
value={kind}
|
||||
onchange={(e) => pickKind(e.currentTarget.value as Kind)}
|
||||
aria-label="Type"
|
||||
>
|
||||
<option value="all">movies + shows</option>
|
||||
<option value="movie">movies</option>
|
||||
<option value="show">shows</option>
|
||||
<option value="anime">anime</option>
|
||||
</select>
|
||||
<select bind:value={who} aria-label="Who watched">
|
||||
<option value="all">everyone</option>
|
||||
@@ -583,6 +605,13 @@
|
||||
value={selected.rewatchCount}
|
||||
/>
|
||||
</label>
|
||||
{#if selected.type === 'show'}
|
||||
<label class="check"
|
||||
><input type="checkbox" name="anime" checked={selected.anime} /> anime</label
|
||||
>
|
||||
{:else if selected.anime}
|
||||
<input type="hidden" name="anime" value="true" />
|
||||
{/if}
|
||||
<label class="full"
|
||||
>notes
|
||||
<textarea name="notes" rows="2" maxlength="2000" value={selected.notes ?? ''}></textarea>
|
||||
@@ -1461,6 +1490,14 @@
|
||||
.d-form .full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.d-form label.check {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-self: end;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--site-text);
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { mediaActions, mediaLoad } from '#lib/server/media/route.js';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
// Podcasts: a Media child (see #lib/media/kinds). Public to read, owner-only to edit.
|
||||
export const load: PageServerLoad = mediaLoad('podcast');
|
||||
export const actions: Actions = mediaActions('podcast');
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import MediaTracker from '#lib/media/MediaTracker.svelte';
|
||||
|
||||
let { data, form } = $props();
|
||||
</script>
|
||||
|
||||
<MediaTracker {data} {form} />
|
||||
@@ -0,0 +1,10 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
// Shows live in the movies tracker as a type filter (?type=show; anime is ?type=anime).
|
||||
// Temporary redirect, so a real /trackers/shows page can replace it later.
|
||||
export const GET: RequestHandler = ({ url }) => {
|
||||
const params = new URLSearchParams(url.search);
|
||||
if (!params.has('type')) params.set('type', 'show');
|
||||
redirect(307, `/trackers/movies?${params}`);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { mediaActions, mediaLoad } from '#lib/server/media/route.js';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
// Videos: a Media child (see #lib/media/kinds). Public to read, owner-only to edit.
|
||||
export const load: PageServerLoad = mediaLoad('video');
|
||||
export const actions: Actions = mediaActions('video');
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import MediaTracker from '#lib/media/MediaTracker.svelte';
|
||||
|
||||
let { data, form } = $props();
|
||||
</script>
|
||||
|
||||
<MediaTracker {data} {form} />
|
||||
@@ -0,0 +1,6 @@
|
||||
import { mediaActions, mediaLoad } from '#lib/server/media/route.js';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
// Webtoons: a Media child (see #lib/media/kinds). Public to read, owner-only to edit.
|
||||
export const load: PageServerLoad = mediaLoad('webtoon');
|
||||
export const actions: Actions = mediaActions('webtoon');
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import MediaTracker from '#lib/media/MediaTracker.svelte';
|
||||
|
||||
let { data, form } = $props();
|
||||
</script>
|
||||
|
||||
<MediaTracker {data} {form} />
|
||||
Reference in new issue
Block a user