diff options
Diffstat (limited to 'src/routes/channel')
| -rw-r--r-- | src/routes/channel/[id]/+page.svelte | 175 |
1 files changed, 175 insertions, 0 deletions
diff --git a/src/routes/channel/[id]/+page.svelte b/src/routes/channel/[id]/+page.svelte new file mode 100644 index 0000000..05ba2d7 --- /dev/null +++ b/src/routes/channel/[id]/+page.svelte @@ -0,0 +1,175 @@ +<script lang="ts"> + import { page } from '$app/stores'; + import { getChannel, type ChannelInfo } from '$lib/api/youtube'; + import { subscriptions } from '$lib/stores/subscriptions'; + import VideoCard from '$lib/components/VideoCard.svelte'; + + let channel = $state<ChannelInfo | null>(null); + let loading = $state(true); + let error = $state(''); + + const channelId = $derived($page.params.id); + const isSubscribed = $derived( + channel ? subscriptions.isSubscribed($subscriptions, channel.authorId) : false + ); + + $effect(() => { + const id = $page.params.id; + if (id) loadChannel(id); + }); + + async function loadChannel(id: string) { + loading = true; + error = ''; + + try { + channel = await getChannel(id); + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to load channel'; + channel = null; + } finally { + loading = false; + } + } + + function handleSubscribe() { + if (!channel) return; + if (isSubscribed) { + subscriptions.remove(channel.authorId); + } else { + subscriptions.add(channel.authorId, channel.author, channel.authorThumbnails); + } + } + + function formatSubCount(count: number): string { + if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M subscribers`; + if (count >= 1000) return `${(count / 1000).toFixed(1)}K subscribers`; + return `${count} subscribers`; + } +</script> + +<svelte:head> + <title>{channel?.author || 'Channel'} - ActualYT</title> +</svelte:head> + +<div class="container"> + {#if loading} + <div class="loading">Loading channel</div> + {:else if error} + <div class="error">{error}</div> + {:else if channel} + <div class="channel-page"> + <div class="channel-header"> + <div class="channel-info"> + {#if channel.authorThumbnails && channel.authorThumbnails.length > 0} + <img + src={channel.authorThumbnails[0].url} + alt={channel.author} + class="avatar" + /> + {/if} + <div class="channel-text"> + <h1 class="channel-name">{channel.author}</h1> + {#if channel.subCount > 0} + <p class="sub-count">{formatSubCount(channel.subCount)}</p> + {/if} + </div> + </div> + <button + class="btn" + class:btn-primary={!isSubscribed} + class:btn-secondary={isSubscribed} + onclick={handleSubscribe} + > + {isSubscribed ? 'Subscribed' : 'Subscribe'} + </button> + </div> + + {#if channel.description} + <p class="description">{channel.description}</p> + {/if} + + <h2 class="section-title">Videos</h2> + + {#if channel.videos.length === 0} + <div class="empty">No videos found</div> + {:else} + <div class="video-grid"> + {#each channel.videos as video (video.videoId)} + <VideoCard + videoId={video.videoId} + title={video.title} + author={video.author} + authorId={video.authorId} + thumbnails={video.videoThumbnails} + viewCount={video.viewCount} + publishedText={video.publishedText} + lengthSeconds={video.lengthSeconds} + /> + {/each} + </div> + {/if} + </div> + {/if} +</div> + +<style> + .channel-page { + max-width: 1200px; + } + + .channel-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1.5rem; + } + + .channel-info { + display: flex; + align-items: center; + gap: 1rem; + } + + .avatar { + width: 80px; + height: 80px; + border-radius: 50%; + object-fit: cover; + } + + .channel-name { + font-size: 1.5rem; + font-weight: 600; + margin: 0 0 0.25rem; + } + + .sub-count { + color: var(--text-muted); + margin: 0; + } + + .description { + color: var(--text-secondary); + margin-bottom: 2rem; + white-space: pre-wrap; + line-height: 1.6; + } + + @media (max-width: 600px) { + .channel-header { + flex-direction: column; + align-items: flex-start; + } + + .avatar { + width: 60px; + height: 60px; + } + + .channel-name { + font-size: 1.25rem; + } + } +</style> |
