refactor(ui): complete ui template migration and fix playback

This commit is contained in:
2026-05-01 17:28:09 +02:00
committed by Mikkel Elvers
parent 33a939ca81
commit 4f3a61e143
23 changed files with 1298 additions and 68 deletions

View File

@@ -1,9 +1,87 @@
{{define "anime_card"}}
<div class="bg-gray-800 rounded-lg overflow-hidden shadow-lg">
<img src="{{.ImageURL}}" alt="{{.DisplayTitle}}" class="w-full h-48 object-cover">
<div class="p-4">
<h3 class="text-lg font-semibold mb-2">{{.DisplayTitle}}</h3>
<p class="text-sm text-gray-400">{{.Type}}</p>
</div>
</div>
{{$anime := .Anime}}
{{$withActions := .WithActions}}
{{$compact := .Compact}}
{{$hideTitle := .HideTitle}}
{{$isWatchlist := .IsWatchlist}}
{{$hasTopBadge := .HasTopBadge}}
{{$imageUrl := "https://placehold.co/400x600?text=No+Image"}}
{{if $anime.Images.Webp.LargeImageURL}}
{{$imageUrl = $anime.Images.Webp.LargeImageURL}}
{{else if $anime.Images.Jpg.LargeImageURL}}
{{$imageUrl = $anime.Images.Jpg.LargeImageURL}}
{{end}}
{{$displayTitle := $anime.Title}}
{{if $anime.TitleEnglish}}
{{$displayTitle = $anime.TitleEnglish}}
{{end}}
<div class="flex w-full flex-col gap-2">
<a href="/anime/{{$anime.MalID}}" class="group relative flex aspect-[2/3] w-full flex-col overflow-hidden bg-white/5 after:absolute after:inset-0 {{if $withActions}}after:bg-black/80 after:opacity-0 hover:after:opacity-100{{else}}after:bg-linear-to-t after:from-black/80 after:via-black/20 after:to-transparent after:opacity-80 hover:after:opacity-100{{end}} after:transition-opacity">
<img src="{{$imageUrl}}" alt="{{$displayTitle}}" class="h-full w-full object-cover" loading="lazy" />
{{if $withActions}}
<div class="absolute inset-0 z-10 flex flex-col p-3 {{if $hasTopBadge}}pt-10{{end}} opacity-0 transition-opacity duration-300 group-hover:opacity-100">
{{if $isWatchlist}}
<div class="flex justify-end">
<button class="text-white/70 transition-colors hover:text-white focus:outline-none disabled:opacity-50" aria-label="Remove from Watchlist">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-5 shadow-black drop-shadow-md"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
</div>
{{end}}
{{if not $isWatchlist}}
<h3 class="mb-1.5 line-clamp-2 text-sm font-medium text-white shadow-black drop-shadow-md">
{{$displayTitle}}
</h3>
{{end}}
{{if $anime.Synopsis}}
<p class="line-clamp-4 text-xs leading-relaxed text-white/80 shadow-black drop-shadow-md">
{{$anime.Synopsis}}
</p>
{{end}}
{{if not $isWatchlist}}
<div class="mt-auto flex items-center justify-start pb-2 pl-2">
<button class="text-accent hover:text-accent/80 transition-colors focus:outline-none disabled:opacity-50" aria-label="Add to Watchlist">
<svg class="size-6 shadow-black drop-shadow-md" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" /></svg>
</button>
</div>
{{end}}
</div>
{{else}}
<div class="absolute bottom-0 left-0 z-10 w-full p-4">
<h3 class="line-clamp-2 text-sm font-medium text-white shadow-black drop-shadow-md {{if $compact}}mb-0{{end}}">
{{$displayTitle}}
</h3>
{{if not $compact}}
<div class="mt-1 flex items-center gap-2 text-xs text-white/70">
{{if $anime.Score}}
<span class="flex items-center gap-1">
<svg class="text-accent h-3 w-3" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
{{$anime.Score}}
</span>
{{end}}
{{if $anime.Year}}
<span>•</span>
<span>{{$anime.Year}}</span>
{{end}}
{{if $anime.Episodes}}
<span>•</span>
<span>{{$anime.Episodes}} ep</span>
{{end}}
</div>
{{end}}
</div>
{{end}}
</a>
{{if and $withActions (not $hideTitle)}}
<h3 class="line-clamp-2 text-sm font-medium text-white">
{{$displayTitle}}
</h3>
{{end}}
</div>
{{end}}

View File

@@ -0,0 +1,42 @@
{{define "continue_watching"}}
<section class="w-full">
<h2 class="mb-3 text-lg font-normal text-neutral-300">Continue Watching</h2>
<div class="scrollbar-hide flex snap-x snap-mandatory gap-2 overflow-x-auto pb-4">
{{range .}}
{{$title := .TitleOriginal}}
{{if .TitleEnglish.Valid}}{{$title = .TitleEnglish.String}}{{end}}
<a href="/anime/{{.AnimeID}}/watch" class="group relative w-70 shrink-0 snap-start cursor-pointer space-y-2 2xl:w-lg">
<div class="bg-background/80 relative aspect-video w-full overflow-hidden">
<img src="{{if .ImageUrl}}{{.ImageUrl}}{{else}}https://placehold.co/500x500{{end}}" alt="{{$title}}" class="h-full w-full object-cover" />
<div class="absolute inset-0 z-10 flex flex-col p-3 opacity-0 transition-opacity duration-300 group-hover:opacity-100 bg-black/40">
<div class="flex justify-end">
<button class="bg-black/60 hover:bg-black/80 rounded-full p-1.5 text-white transition-colors focus:outline-none disabled:opacity-50 backdrop-blur-sm" aria-label="Remove from Continue Watching">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-5"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
</div>
</div>
{{if .CurrentTimeSeconds}}
<!-- Progress calculation would go here if total duration was available -->
<div class="bg-foreground-muted/60 absolute bottom-0 left-0 h-1.5 w-full z-20">
<div class="shadow-background/20 bg-accent h-full shadow-[0_-2px_5px_0]" style="width: 50%"></div>
</div>
{{end}}
</div>
<div>
<h3 class="text-foreground truncate text-lg font-normal">
{{$title}}
</h3>
<p class="text-foreground-muted mt-0.5 text-base">
{{if .CurrentEpisode.Valid}}Episode {{.CurrentEpisode.Int64}}{{end}}
</p>
</div>
</a>
{{end}}
</div>
</section>
{{end}}

View File

@@ -0,0 +1,13 @@
{{define "dropdown"}}
<ui-dropdown class="relative block" data-align="{{if .Align}}{{.Align}}{{else}}right{{end}}" data-position="{{if .Position}}{{.Position}}{{else}}bottom{{end}}" data-width="{{if .Width}}{{.Width}}{{else}}min-w-[320px]{{end}}">
<div data-trigger>
{{template "dropdown_trigger" .}}
</div>
<div data-content class="hidden absolute z-50 {{if .Width}}{{.Width}}{{else}}min-w-[320px]{{end}} bg-background-button rounded-none shadow-2xl {{if eq .Align "left"}}left-0{{else}}right-0{{end}} {{if eq .Position "top"}}bottom-full mb-2{{else}}top-full mt-2{{end}}">
<div class="flex flex-col py-1">
{{template "dropdown_children" .}}
</div>
</div>
</ui-dropdown>
{{end}}

View File

@@ -0,0 +1,87 @@
{{define "filter_bar"}}
<div class="flex flex-wrap items-center gap-3 bg-white/5 p-3">
<div class="min-w-50 flex-1">
<form action="/browse" method="GET" id="browse-search-form">
<input
id="search"
name="q"
type="text"
value="{{.Query}}"
placeholder="Search anime..."
class="focus:ring-accent w-full bg-black/20 px-3 py-2 text-sm text-white placeholder-neutral-500 outline-none focus:ring-1"
onkeydown="if(event.key === 'Enter'){this.form.submit()}"
/>
<!-- Preserve other params -->
{{if .Type}}<input type="hidden" name="type" value="{{.Type}}">{{end}}
{{if .Status}}<input type="hidden" name="status" value="{{.Status}}">{{end}}
{{if .OrderBy}}<input type="hidden" name="order_by" value="{{.OrderBy}}">{{end}}
{{if .Sort}}<input type="hidden" name="sort" value="{{.Sort}}">{{end}}
</form>
</div>
<ui-dropdown class="relative block" data-align="left" data-width="w-40">
<div data-trigger class="cursor-pointer">
<button class="flex items-center gap-2 bg-black/20 px-3 py-2 text-sm text-white hover:bg-black/30">
{{if .Status}}{{.Status}}{{else}}Any Status{{end}}
<svg class="h-4 w-4 opacity-50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6" /></svg>
</button>
</div>
<div data-content class="hidden absolute z-50 w-40 bg-background-button rounded-none shadow-2xl left-0 top-full mt-2">
<div class="flex flex-col py-1">
<a href="?status=&q={{.Query}}&type={{.Type}}&order_by={{.OrderBy}}&sort={{.Sort}}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Any Status</a>
<a href="?status=airing&q={{.Query}}&type={{.Type}}&order_by={{.OrderBy}}&sort={{.Sort}}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Airing</a>
<a href="?status=complete&q={{.Query}}&type={{.Type}}&order_by={{.OrderBy}}&sort={{.Sort}}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Complete</a>
<a href="?status=upcoming&q={{.Query}}&type={{.Type}}&order_by={{.OrderBy}}&sort={{.Sort}}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Upcoming</a>
</div>
</div>
</ui-dropdown>
<ui-dropdown class="relative block" data-align="left" data-width="w-40">
<div data-trigger class="cursor-pointer">
<button class="flex items-center gap-2 bg-black/20 px-3 py-2 text-sm text-white hover:bg-black/30">
{{if .Type}}{{.Type}}{{else}}Any Type{{end}}
<svg class="h-4 w-4 opacity-50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6" /></svg>
</button>
</div>
<div data-content class="hidden absolute z-50 w-40 bg-background-button rounded-none shadow-2xl left-0 top-full mt-2">
<div class="flex flex-col py-1">
<a href="?type=&q={{.Query}}&status={{.Status}}&order_by={{.OrderBy}}&sort={{.Sort}}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Any Type</a>
<a href="?type=tv&q={{.Query}}&status={{.Status}}&order_by={{.OrderBy}}&sort={{.Sort}}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">TV</a>
<a href="?type=movie&q={{.Query}}&status={{.Status}}&order_by={{.OrderBy}}&sort={{.Sort}}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Movie</a>
<a href="?type=ova&q={{.Query}}&status={{.Status}}&order_by={{.OrderBy}}&sort={{.Sort}}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">OVA</a>
<a href="?type=special&q={{.Query}}&status={{.Status}}&order_by={{.OrderBy}}&sort={{.Sort}}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Special</a>
<a href="?type=ona&q={{.Query}}&status={{.Status}}&order_by={{.OrderBy}}&sort={{.Sort}}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">ONA</a>
</div>
</div>
</ui-dropdown>
<div class="flex items-center gap-1">
<ui-dropdown class="relative block" data-align="left" data-width="w-48">
<div data-trigger class="cursor-pointer">
<button class="flex items-center gap-2 bg-black/20 px-3 py-2 text-sm text-white hover:bg-black/30">
Sort: {{if .OrderBy}}{{.OrderBy}}{{else}}Default{{end}}
<svg class="h-4 w-4 opacity-50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6" /></svg>
</button>
</div>
<div data-content class="hidden absolute z-50 w-48 bg-background-button rounded-none shadow-2xl left-0 top-full mt-2">
<div class="flex flex-col py-1">
<a href="?order_by=&q={{.Query}}&status={{.Status}}&type={{.Type}}&sort={{.Sort}}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Default</a>
<a href="?order_by=popularity&q={{.Query}}&status={{.Status}}&type={{.Type}}&sort={{.Sort}}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Popularity</a>
<a href="?order_by=score&q={{.Query}}&status={{.Status}}&type={{.Type}}&sort={{.Sort}}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Score</a>
<a href="?order_by=title&q={{.Query}}&status={{.Status}}&type={{.Type}}&sort={{.Sort}}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Title</a>
<a href="?order_by=start_date&q={{.Query}}&status={{.Status}}&type={{.Type}}&sort={{.Sort}}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Start Date</a>
<a href="?order_by=episodes&q={{.Query}}&status={{.Status}}&type={{.Type}}&sort={{.Sort}}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Episodes</a>
</div>
</div>
</ui-dropdown>
<a href="?sort={{if eq .Sort "asc"}}desc{{else}}asc{{end}}&q={{.Query}}&status={{.Status}}&type={{.Type}}&order_by={{.OrderBy}}" class="flex h-9 w-9 items-center justify-center bg-black/20 text-neutral-300 hover:text-white">
{{if eq .Sort "asc"}}
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 19V5M5 12l7-7 7 7" /></svg>
{{else}}
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M19 12l-7 7-7-7" /></svg>
{{end}}
</a>
</div>
</div>
{{end}}

View File

@@ -0,0 +1,77 @@
{{define "header"}}
<header class="bg-background-header flex flex-col border-b border-white/5 relative z-50">
<div class="flex h-16 items-center justify-between px-4 md:px-6">
<div class="flex items-center gap-4 lg:w-72">
<button onclick="toggleMobileMenu()" class="block text-neutral-400 transition-colors hover:text-white focus:outline-none md:hidden">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>
</button>
<a href="/" class="group flex items-center gap-2 focus:outline-none">
<svg class="h-7 w-7 transition-transform group-hover:scale-110" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 80 L25 30 L50 60 L75 30 L85 80" stroke="currentColor" class="text-accent" stroke-width="12" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="50" cy="25" r="8" fill="white" />
</svg>
<span class="text-xl font-bold tracking-tight text-white hidden sm:block">MyAnime<span class="text-accent">List</span></span>
</a>
</div>
<div class="hidden max-w-3xl flex-1 items-center justify-center px-4 md:flex">
<form action="/search" method="GET" class="w-full max-w-lg">
<div class="focus-within:border-accent bg-background-surface flex h-10 w-full items-center overflow-hidden border border-transparent transition-colors">
<div class="pr-2 pl-4 text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
</div>
<input type="text" name="q" placeholder="Search..." class="h-full w-full bg-transparent px-1 text-sm text-white focus:outline-none" />
</div>
</form>
</div>
<div class="flex items-center justify-end lg:w-72 relative">
<ui-dropdown class="relative block">
<div data-trigger class="cursor-pointer">
<button class="flex items-center gap-1 rounded-full p-1 transition-colors hover:bg-white/5 focus:outline-none">
{{if .User}}
<div class="bg-accent flex h-8 w-8 items-center justify-center overflow-hidden rounded-full text-sm font-semibold text-white">
{{slice .User.Username 0 1}}
</div>
{{else}}
<div class="bg-accent flex h-8 w-8 items-center justify-center rounded-full text-sm font-semibold text-white">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
</div>
{{end}}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-neutral-400"><path d="m6 9 6 6 6-6"/></svg>
</button>
</div>
<div data-content class="hidden absolute z-50 min-w-[320px] bg-background-button rounded-none shadow-2xl right-0 top-full mt-2">
<div class="flex flex-col py-1">
{{if .User}}
<a href="/logout" class="flex w-full items-center px-5 py-3.5 transition-colors focus:outline-none hover:bg-red-500/10 focus:bg-red-500/10">
<div class="flex w-full items-center justify-between text-left">
<span class="font-medium text-[15px] text-red-500">Log out</span>
</div>
</a>
{{else}}
<a href="/login" class="flex w-full items-center px-5 py-3.5 transition-colors focus:outline-none hover:bg-white/10 focus:bg-white/10">
<div class="flex w-full items-center justify-between text-left">
<span class="font-medium text-[15px] text-white">Log in</span>
</div>
</a>
{{end}}
</div>
</div>
</ui-dropdown>
</div>
</div>
<div class="flex border-t border-white/5 p-3 md:hidden">
<form action="/search" method="GET" class="w-full">
<div class="focus-within:border-accent bg-background-surface flex h-10 w-full items-center overflow-hidden border border-transparent transition-colors">
<div class="pr-2 pl-4 text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
</div>
<input type="text" name="q" placeholder="Search..." class="h-full w-full bg-transparent px-1 text-sm text-white focus:outline-none" />
</div>
</form>
</div>
</header>
{{end}}

View File

@@ -0,0 +1,87 @@
{{define "navigation"}}
{{$currentPath := .CurrentPath}}
{{$isCollapsed := .IsCollapsed}}
{{$navItems := dict
"home" (dict "href" "/" "label" "Home")
"browse" (dict "href" "/browse" "label" "Browse")
"discover" (dict "href" "/discover" "label" "Discover")
"watchlist" (dict "href" "/watchlist" "label" "Watchlist")
}}
<nav class="bg-background-sidebar h-full py-6">
<div class="flex flex-col">
{{/* Home */}}
{{$isActive := eq $currentPath "/"}}
<a href="/" class="group relative flex items-center px-7 py-3 transition-colors hover:bg-white/5" {{if $isCollapsed}}title="Home"{{end}}>
{{if $isActive}}
<div class="bg-accent absolute top-1/2 left-0 h-8 w-0.5 -translate-y-1/2 rounded-r-sm shadow-[0_0_8px_rgba(163,230,53,0.6)]"></div>
{{end}}
<svg class="size-6 shrink-0 transition-colors duration-200 {{if $isActive}}text-accent{{else}}text-foreground-muted{{end}}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
<polyline points="9 22 9 12 15 12 15 22" />
</svg>
<div class="grid transition-all duration-300 ease-in-out {{if $isCollapsed}}grid-cols-[0fr] opacity-0 ml-0{{else}}grid-cols-[1fr] opacity-100 ml-4{{end}}">
<div class="overflow-hidden min-w-0">
<span class="whitespace-nowrap text-sm font-medium transition-colors duration-200 {{if $isActive}}text-accent{{else}}text-foreground-muted{{end}}">Home</span>
</div>
</div>
</a>
{{/* Browse */}}
{{$isActive := eq $currentPath "/browse"}}
<a href="/browse" class="group relative flex items-center px-7 py-3 transition-colors hover:bg-white/5" {{if $isCollapsed}}title="Browse"{{end}}>
{{if $isActive}}
<div class="bg-accent absolute top-1/2 left-0 h-8 w-0.5 -translate-y-1/2 rounded-r-sm shadow-[0_0_8px_rgba(163,230,53,0.6)]"></div>
{{end}}
<svg class="size-6 shrink-0 transition-colors duration-200 {{if $isActive}}text-accent{{else}}text-foreground-muted{{end}}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="2" width="20" height="20" rx="2" ry="2" />
<line x1="7" y1="2" x2="7" y2="22" />
<line x1="17" y1="2" x2="17" y2="22" />
<line x1="2" y1="12" x2="22" y2="12" />
<line x1="2" y1="7" x2="7" y2="7" />
<line x1="2" y1="17" x2="7" y2="17" />
<line x1="17" y1="17" x2="22" y2="17" />
<line x1="17" y1="7" x2="22" y2="7" />
</svg>
<div class="grid transition-all duration-300 ease-in-out {{if $isCollapsed}}grid-cols-[0fr] opacity-0 ml-0{{else}}grid-cols-[1fr] opacity-100 ml-4{{end}}">
<div class="overflow-hidden min-w-0">
<span class="whitespace-nowrap text-sm font-medium transition-colors duration-200 {{if $isActive}}text-accent{{else}}text-foreground-muted{{end}}">Browse</span>
</div>
</div>
</a>
{{/* Discover */}}
{{$isActive := eq $currentPath "/discover"}}
<a href="/discover" class="group relative flex items-center px-7 py-3 transition-colors hover:bg-white/5" {{if $isCollapsed}}title="Discover"{{end}}>
{{if $isActive}}
<div class="bg-accent absolute top-1/2 left-0 h-8 w-0.5 -translate-y-1/2 rounded-r-sm shadow-[0_0_8px_rgba(163,230,53,0.6)]"></div>
{{end}}
<svg class="size-6 shrink-0 transition-colors duration-200 {{if $isActive}}text-accent{{else}}text-foreground-muted{{end}}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
<polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76" />
</svg>
<div class="grid transition-all duration-300 ease-in-out {{if $isCollapsed}}grid-cols-[0fr] opacity-0 ml-0{{else}}grid-cols-[1fr] opacity-100 ml-4{{end}}">
<div class="overflow-hidden min-w-0">
<span class="whitespace-nowrap text-sm font-medium transition-colors duration-200 {{if $isActive}}text-accent{{else}}text-foreground-muted{{end}}">Discover</span>
</div>
</div>
</a>
{{/* Watchlist */}}
{{$isActive := eq $currentPath "/watchlist"}}
<a href="/watchlist" class="group relative flex items-center px-7 py-3 transition-colors hover:bg-white/5" {{if $isCollapsed}}title="Watchlist"{{end}}>
{{if $isActive}}
<div class="bg-accent absolute top-1/2 left-0 h-8 w-0.5 -translate-y-1/2 rounded-r-sm shadow-[0_0_8px_rgba(163,230,53,0.6)]"></div>
{{end}}
<svg class="size-6 shrink-0 transition-colors duration-200 {{if $isActive}}text-accent{{else}}text-foreground-muted{{end}}" viewBox="0 0 24 24" fill="currentColor">
<path d="M17 18.113l-3.256-2.326A2.989 2.989 0 0 0 12 15.228c-.629 0-1.232.194-1.744.559L7 18.113V4h10v14.113zM18 2H6a1 1 0 0 0-1 1v17.056c0 .209.065.412.187.581a.994.994 0 0 0 1.394.233l4.838-3.455a1 1 0 0 1 1.162 0l4.838 3.455A1 1 0 0 0 19 20.056V3a1 1 0 0 0-1-1z" />
</svg>
<div class="grid transition-all duration-300 ease-in-out {{if $isCollapsed}}grid-cols-[0fr] opacity-0 ml-0{{else}}grid-cols-[1fr] opacity-100 ml-4{{end}}">
<div class="overflow-hidden min-w-0">
<span class="whitespace-nowrap text-sm font-medium transition-colors duration-200 {{if $isActive}}text-accent{{else}}text-foreground-muted{{end}}">Watchlist</span>
</div>
</div>
</a>
</div>
</nav>
{{end}}

View File

@@ -0,0 +1,107 @@
{{define "video_player"}}
<div data-video-player
data-anime-id="{{.WatchData.MalID}}"
data-current-episode="{{.WatchData.CurrentEpisode}}"
data-total-episodes="{{.TotalEpisodes}}"
data-initial-mode="{{.WatchData.InitialMode}}"
data-anime-title="{{.WatchData.Title}}"
data-start-time-seconds="{{.WatchData.StartTimeSeconds}}"
data-mode-sources='{{json .WatchData.ModeSources}}'
data-available-modes='{{json .WatchData.AvailableModes}}'
data-segments='{{json .WatchData.Segments}}'
class="group relative aspect-video w-full overflow-hidden bg-black">
<video class="h-full w-full cursor-pointer" preload="metadata" playsinline autoplay></video>
<div data-loading class="absolute inset-0 flex items-center justify-center bg-black/60 hidden z-50">
<div class="border-accent size-10 animate-spin rounded-full border-4 border-t-transparent"></div>
</div>
<div data-video-overlay class="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent p-4 transition-opacity duration-300 opacity-0 group-hover:opacity-100 z-40">
<div class="flex flex-col gap-3">
<div class="flex items-center justify-between px-2">
<div class="flex items-center gap-4">
<button data-play-pause class="flex items-center justify-center text-white transition-opacity hover:opacity-80 focus:outline-none">
<svg data-icon-play class="size-6 transition-colors duration-200" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z" /></svg>
<svg data-icon-pause class="size-6 transition-colors duration-200 hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="9" y1="6" x2="9" y2="18" /><line x1="15" y1="6" x2="15" y2="18" /></svg>
</button>
<div data-volume-wrap class="group/volume relative flex items-center justify-center">
<div data-volume-panel class="pointer-events-none absolute bottom-full left-1/2 flex h-26 w-12 -translate-x-1/2 items-start justify-center opacity-0 transition-opacity group-focus-within/volume:pointer-events-auto group-focus-within/volume:opacity-100 group-hover/volume:pointer-events-auto group-hover/volume:opacity-100">
<div class="relative flex h-24 w-8 items-center justify-center">
<div class="relative z-10 flex h-20 w-1.5 flex-col justify-end rounded-full bg-white/50 shadow-[0_0_6px_rgba(0,0,0,0.6)]">
<input type="range" data-volume-range min="0" max="1" step="0.05" class="absolute -inset-x-4 inset-y-0 z-10 cursor-pointer opacity-0" style="writing-mode: vertical-lr; direction: rtl;">
<div data-volume-underline class="bg-accent pointer-events-none w-full rounded-full shadow-[0_0_4px_rgba(0,0,0,0.4)]" style="height: 100%"></div>
</div>
</div>
</div>
<button data-mute class="flex items-center justify-center text-white transition-opacity hover:opacity-80 focus:outline-none">
<svg data-icon-volume class="size-6 transition-colors duration-200" viewBox="0 0 24 24" aria-hidden="true"><polygon points="5 10 9 10 13 6 13 18 9 14 5 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></polygon><path d="M16 9c1.3 1.3 1.3 4.7 0 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"></path><path d="M18.8 6.5c3 2.9 3 8.1 0 11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"></path></svg>
<svg data-icon-muted class="size-6 transition-colors duration-200 hidden" viewBox="0 0 24 24" aria-hidden="true"><polygon points="5 10 9 10 13 6 13 18 9 14 5 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></polygon><line x1="16" y1="9" x2="21" y2="15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></line><line x1="21" y1="9" x2="16" y2="15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></line></svg>
</button>
</div>
</div>
<div class="flex items-center gap-4">
<ui-dropdown class="relative block" data-align="right" data-position="top">
<div data-trigger class="cursor-pointer">
<button class="flex items-center justify-center text-white transition-opacity hover:opacity-80 focus:outline-none" aria-label="Settings">
<svg class="size-6 transition-transform duration-300" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
</button>
</div>
<div data-content class="hidden absolute z-50 w-64 bg-background-button rounded-none shadow-2xl right-0 bottom-full mb-2">
<div class="flex flex-col py-1">
<div class="flex items-center justify-between px-5 py-2.5">
<span class="text-[15px] font-medium text-white">Autoplay</span>
<label class="relative inline-flex cursor-pointer items-center">
<input type="checkbox" data-autoplay class="peer sr-only" checked />
<div class="peer-checked:bg-accent peer h-5 w-9 rounded-full bg-white/20 transition-colors after:absolute after:top-[2px] after:left-[2px] after:h-4 after:w-4 after:rounded-full after:bg-white after:transition-all peer-checked:after:translate-x-full"></div>
</label>
</div>
<div class="my-1 h-px w-full bg-white/10"></div>
<div class="py-1">
<span class="mb-1 block px-5 text-xs font-medium tracking-wider text-neutral-400 uppercase">Audio / Subtitles</span>
<div class="flex flex-col">
<button data-mode-dub class="flex items-center justify-between px-5 py-2.5 text-left transition-colors hover:bg-white/10 text-white focus:outline-none">
<span class="text-sm font-medium">English (Dub)</span>
</button>
<button data-mode-sub class="flex items-center justify-between px-5 py-2.5 text-left transition-colors hover:bg-white/10 text-white focus:outline-none">
<span class="text-sm font-medium">Japanese (Sub)</span>
</button>
<select data-subtitle-select class="mt-2 mx-4 bg-black/40 text-white text-xs border border-white/10 px-2 py-1 outline-none hidden"></select>
</div>
</div>
</div>
</div>
</ui-dropdown>
<button data-fullscreen class="flex items-center justify-center text-white transition-opacity hover:opacity-80 focus:outline-none">
<svg class="size-6 transition-colors duration-200" viewBox="0 0 240 240" aria-hidden="true"><path d="M143.7,53.9c-1.9-1.9-1.3-4,1.4-4.4l50.6-8.4c1.8-0.5,3.7,0.6,4.2,2.4c0.2,0.6,0.2,1.2,0,1.7l-8.4,50.6c-0.4,2.7-2.4,3.4-4.4,1.4l-14.5-14.5l-28.2,28.2l-14.3-14.3l28.2-28.2L143.7,53.9z M44.2,200.9l50.6-8.4c2.7-0.4,3.4-2.4,1.4-4.4l-14.5-14.5l28.2-28.2l-14.3-14.3l-28.2,28.2l-14.5-14.5c-1.9-1.9-4-1.3-4.4,1.4l-8.4,50.6c-0.5,1.8,0.6,3.6,2.4,4.2C43,201,43.6,201,44.2,200.9L44.2,200.9z" fill="currentColor"></path></svg>
</button>
</div>
</div>
<div class="flex items-center gap-4 text-xs font-medium text-white px-2">
<span data-time class="w-12 text-center">0:00</span>
<div data-progress-wrap class="group/progress relative flex h-6 flex-1 cursor-pointer items-center">
<div data-preview-popover class="pointer-events-none absolute bottom-full mb-2 -translate-x-1/2 bg-white px-2 py-1 text-xs font-bold text-black shadow-md opacity-0 transition-opacity duration-200">
<span data-preview-time>0:00</span>
</div>
<div data-segments class="absolute inset-0 z-10 pointer-events-none"></div>
<div class="h-1.5 w-full rounded-full bg-white/30 transition-all group-hover/progress:h-2">
<div data-progress class="bg-accent h-full rounded-full" style="width: 0%"></div>
</div>
<div data-scrubber class="bg-accent pointer-events-none absolute top-1/2 h-3.5 w-3.5 -translate-x-1/2 -translate-y-1/2 rounded-full opacity-0 shadow-sm transition-[opacity,transform] group-hover/progress:scale-110 group-hover/progress:opacity-100" style="left: 0%"></div>
</div>
<span class="w-12 text-center" data-duration>0:00</span>
</div>
<button data-skip class="hidden absolute right-6 bottom-16 bg-white text-black font-bold px-4 py-2 text-sm rounded shadow-lg transition-transform hover:scale-105 active:scale-95">Skip</button>
<button data-backward class="hidden absolute left-1/4 top-1/2 -translate-y-1/2 -translate-x-1/2 p-4 bg-black/40 rounded-full text-white opacity-0 transition-opacity"><svg class="size-8" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12.066 11.2a1 1 0 000 1.6l5.334 4A1 1 0 0019 16V8a1 1 0 00-1.6-.8l-5.333 4zM4.066 11.2a1 1 0 000 1.6l5.334 4A1 1 0 0011 16V8a1 1 0 00-1.6-.8l-5.334 4z" /></svg></button>
<button data-forward class="hidden absolute right-1/4 top-1/2 -translate-y-1/2 translate-x-1/2 p-4 bg-black/40 rounded-full text-white opacity-0 transition-opacity"><svg class="size-8" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.933 12.8a1 1 0 000-1.6l-5.333-4A1 1 0 005 8v8a1 1 0 001.6.8l5.333-4zM19.933 12.8a1 1 0 000-1.6l-5.333-4A1 1 0 0013 8v8a1 1 0 001.6.8l5.333-4z" /></svg></button>
<div data-subtitle-text class="absolute bottom-20 left-0 right-0 text-center pointer-events-none drop-shadow-md z-30" style="text-shadow: 0px 0px 4px black, 0px 0px 8px black; font-size: clamp(1rem, 2.5vw, 2rem); font-weight: 600; color: white;"></div>
</div>
</div>
</div>
{{end}}

View File

@@ -0,0 +1,20 @@
{{define "watch_order"}}
<div class="space-y-4">
<h2 class="text-lg font-medium text-neutral-300">Watch Order</h2>
<div class="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-6">
{{range .Relations}}
<div class="group relative">
{{template "anime_card" dict "Anime" .Anime "WithActions" true "Compact" true "HasTopBadge" true}}
{{if eq .Anime.MalID $.AnimeID}}
<div class="bg-accent absolute -top-2 -right-2 z-20 px-2 py-0.5 text-[10px] font-bold text-white shadow-md">
CURRENT
</div>
{{end}}
<div class="absolute top-2 left-2 z-20 border border-white/10 bg-black/80 px-2 py-0.5 text-[10px] font-medium text-white backdrop-blur-md">
{{.Relation}}
</div>
</div>
{{end}}
</div>
</div>
{{end}}

View File

@@ -0,0 +1,77 @@
{{define "watchlist_actions"}}
{{$anime := .Anime}}
{{$user := .User}}
{{$status := .Status}}
<div class="mb-4 flex gap-2">
<ui-dropdown class="relative block" data-align="left" data-width="min-w-[160px]">
<div data-trigger class="cursor-pointer">
<button class="bg-background-button hover:bg-background-button-hover flex items-center justify-between gap-3 px-4 py-2.5 text-sm font-medium text-white transition-colors disabled:opacity-50">
<span id="watchlist-status-display-{{$anime.MalID}}">
{{if $status}}
{{if eq $status "watching"}}Watching{{end}}
{{if eq $status "completed"}}Completed{{end}}
{{if eq $status "plan to watch"}}Plan to Watch{{end}}
{{if eq $status "dropped"}}Dropped{{end}}
{{else}}
Add to Watchlist
{{end}}
</span>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-neutral-400"><path d="m6 9 6 6 6-6"/></svg>
</button>
</div>
<div data-content class="hidden absolute z-50 min-w-[160px] bg-background-button rounded-none shadow-2xl left-0 top-full mt-2">
<div class="flex flex-col py-1">
<button class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 focus:bg-white/10" onclick="updateWatchlist({{$anime.MalID}}, 'watching', 'Watching')">
<span class="font-medium text-sm text-white">Watching</span>
</button>
<button class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 focus:bg-white/10" onclick="updateWatchlist({{$anime.MalID}}, 'completed', 'Completed')">
<span class="font-medium text-sm text-white">Completed</span>
</button>
<button class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 focus:bg-white/10" onclick="updateWatchlist({{$anime.MalID}}, 'plan to watch', 'Plan to Watch')">
<span class="font-medium text-sm text-white">Plan to Watch</span>
</button>
<button class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 focus:bg-white/10" onclick="updateWatchlist({{$anime.MalID}}, 'dropped', 'Dropped')">
<span class="font-medium text-sm text-white">Dropped</span>
</button>
<div id="remove-watchlist-container-{{$anime.MalID}}" class="{{if not $status}}hidden{{end}}">
<div class="my-1 h-px bg-white/10"></div>
<button class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-red-500/10 focus:bg-red-500/10" onclick="removeWatchlist({{$anime.MalID}})">
<span class="font-medium text-sm text-red-500 text-left whitespace-nowrap">Remove from Watchlist</span>
</button>
</div>
</div>
</div>
</ui-dropdown>
<a href="/anime/{{$anime.MalID}}/watch" class="bg-background-button hover:bg-background-button-hover px-5 py-2.5 text-sm font-medium text-white transition-colors">
Watch
</a>
</div>
<script>
function updateWatchlist(id, status, display) {
fetch('/api/watchlist', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ animeId: id, status: status })
}).then(res => {
if (res.ok) {
document.getElementById('watchlist-status-display-' + id).textContent = display;
document.getElementById('remove-watchlist-container-' + id).classList.remove('hidden');
}
});
}
function removeWatchlist(id) {
fetch('/api/watchlist/' + id, { method: 'DELETE' }).then(res => {
if (res.ok) {
document.getElementById('watchlist-status-display-' + id).textContent = 'Add to Watchlist';
document.getElementById('remove-watchlist-container-' + id).classList.add('hidden');
}
});
}
</script>
{{end}}