A stream.place VOD client inspired by icarly.com
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fix video routing and username display

- Changed watch page from dynamic route [id] to query param /watch?id=
- Fixed username display to show shortened DID when handle unavailable
- Added proper Suspense boundary for useSearchParams
- Video cards now link to /watch?id= instead of /watch/[id]

jack beba0ca3 df74a964

+59 -27
+22 -12
src/app/watch/[id]/WatchPageClient.tsx src/app/watch/WatchPageContent.tsx
··· 1 1 'use client'; 2 2 3 - import { useState, useEffect } from 'react'; 3 + import { useEffect, useState } from 'react'; 4 4 import VideoPlayer from '@/components/VideoPlayer'; 5 5 import { VideoRecord } from '@/lib/types'; 6 6 import Image from 'next/image'; 7 7 import Link from 'next/link'; 8 - import '../../icarly.css'; 8 + import { useSearchParams } from 'next/navigation'; 9 + import '../icarly.css'; 9 10 10 11 function formatDuration(ns: number): string { 11 12 const totalSeconds = Math.floor(ns / 1_000_000_000); ··· 26 27 }; 27 28 28 29 function getDisplayName(handle: string): string { 29 - return STREAM_DISPLAY_NAMES[handle] || handle; 30 - } 31 - 32 - interface WatchPageClientProps { 33 - id: string; 30 + if (STREAM_DISPLAY_NAMES[handle]) { 31 + return STREAM_DISPLAY_NAMES[handle]; 32 + } 33 + if (handle.startsWith('did:')) { 34 + return handle.split(':').pop()?.slice(0, 8) + '...' || handle; 35 + } 36 + return handle.split('.')[0]; 34 37 } 35 38 36 - export default function WatchPageClient({ id }: WatchPageClientProps) { 39 + export default function WatchPageContent() { 40 + const searchParams = useSearchParams(); 41 + const id = searchParams.get('id'); 42 + 37 43 const [video, setVideo] = useState<{ uri: string; cid: string; value: VideoRecord } | null>(null); 38 44 const [handle, setHandle] = useState<string>(''); 39 45 const [loading, setLoading] = useState(true); ··· 46 52 47 53 useEffect(() => { 48 54 const fetchVideo = async () => { 55 + if (!id) { 56 + setError('No video ID provided'); 57 + setLoading(false); 58 + return; 59 + } 60 + 49 61 try { 50 62 setLoading(true); 51 63 ··· 83 95 } 84 96 }; 85 97 86 - if (id) { 87 - fetchVideo(); 88 - } 98 + fetchVideo(); 89 99 }, [id]); 90 100 91 101 if (loading) { ··· 365 375 </main> 366 376 </div> 367 377 ); 368 - } 378 + }
-10
src/app/watch/[id]/page.tsx
··· 1 - import WatchPageClient from './WatchPageClient'; 2 - 3 - // Generate static params for static export 4 - export function generateStaticParams() { 5 - return [{ id: 'placeholder' }]; 6 - } 7 - 8 - export default function WatchPage({ params }: { params: { id: string } }) { 9 - return <WatchPageClient id={params.id} />; 10 - }
+25
src/app/watch/page.tsx
··· 1 + 'use client'; 2 + 3 + import { Suspense } from 'react'; 4 + import WatchPageContent from './WatchPageContent'; 5 + 6 + export default function WatchPage() { 7 + return ( 8 + <Suspense fallback={ 9 + <div style={{ textAlign: 'center', padding: '60px' }}> 10 + <div style={{ 11 + width: '50px', 12 + height: '50px', 13 + border: '5px solid #ddd', 14 + borderTopColor: '#E91E8C', 15 + borderRadius: '50%', 16 + margin: '0 auto', 17 + animation: 'spin 1s linear infinite' 18 + }}></div> 19 + <p style={{ marginTop: '20px', color: '#62166F', fontSize: '18px' }}>Loading video...</p> 20 + </div> 21 + }> 22 + <WatchPageContent /> 23 + </Suspense> 24 + ); 25 + }
+12 -5
src/components/VideoCard.tsx
··· 1 1 'use client'; 2 2 3 3 import { useState } from 'react'; 4 - import Link from 'next/link'; 5 4 import { VideoRecord } from '@/lib/types'; 6 5 7 6 function formatDuration(ns: number): string { ··· 23 22 }; 24 23 25 24 function getDisplayName(handle: string): string { 26 - return STREAM_DISPLAY_NAMES[handle] || handle.split('.')[0]; 25 + if (STREAM_DISPLAY_NAMES[handle]) { 26 + return STREAM_DISPLAY_NAMES[handle]; 27 + } 28 + // If it's a DID, show shortened version 29 + if (handle.startsWith('did:')) { 30 + return handle.split(':').pop()?.slice(0, 8) + '...' || handle; 31 + } 32 + // Otherwise show the first part of the handle 33 + return handle.split('.')[0]; 27 34 } 28 35 29 36 interface VideoCardProps { ··· 54 61 const [color1, color2] = colors[Math.abs(hash) % colors.length]; 55 62 56 63 return ( 57 - <Link 58 - href={`/watch/${id}`} 64 + <a 65 + href={`/watch?id=${id}`} 59 66 style={{ textDecoration: 'none' }} 60 67 onMouseEnter={() => setShowHandle(true)} 61 68 onMouseLeave={() => setShowHandle(false)} ··· 77 84 </p> 78 85 </div> 79 86 </div> 80 - </Link> 87 + </a> 81 88 ); 82 89 }