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 VOD API, add mobile support, convert iSongs to audio-only

- Fix PDS endpoint error by using vod-beta.stream.place API directly
- Update VideoPlayer and all components to use correct API endpoint
- Convert iSongs to play stream.place videos as audio-only
- Add HLS.js support for Safari compatibility
- Add comprehensive mobile responsive CSS with touch support
- Optimize for iOS Safari with specific fixes
- Add reduced motion support for accessibility

jack 809003f8 e5937df1

+507 -133
+268
src/app/icarly.css
··· 812 812 .floating-star { 813 813 animation: float 4s ease-in-out infinite; 814 814 } 815 + 816 + /* Mobile Responsive Styles */ 817 + @media (max-width: 768px) { 818 + html { 819 + font-size: 12px; 820 + } 821 + 822 + .icarly-container { 823 + padding: 0 10px; 824 + } 825 + 826 + /* Header - Mobile */ 827 + .icarly-header { 828 + padding: 15px; 829 + min-height: auto; 830 + } 831 + 832 + .homepage-btn { 833 + position: relative; 834 + top: auto; 835 + left: auto; 836 + transform: none; 837 + display: inline-block; 838 + margin-bottom: 10px; 839 + padding: 6px 15px; 840 + font-size: 14px; 841 + } 842 + 843 + .icarly-logo { 844 + position: relative !important; 845 + left: auto !important; 846 + top: auto !important; 847 + display: block; 848 + text-align: center; 849 + margin: 10px 0; 850 + } 851 + 852 + .date-display { 853 + position: relative !important; 854 + right: auto !important; 855 + top: auto !important; 856 + text-align: center; 857 + margin: 10px 0; 858 + font-size: 14px; 859 + } 860 + 861 + .search-section { 862 + position: relative !important; 863 + right: auto !important; 864 + top: auto !important; 865 + width: 100%; 866 + max-width: none; 867 + margin: 10px 0; 868 + } 869 + 870 + .search-box { 871 + flex-direction: row; 872 + } 873 + 874 + .search-input { 875 + font-size: 16px; /* Prevent zoom on iOS */ 876 + } 877 + 878 + .login-btn { 879 + position: relative !important; 880 + right: auto !important; 881 + top: auto !important; 882 + display: inline-block; 883 + margin: 5px; 884 + padding: 8px 20px; 885 + font-size: 14px; 886 + } 887 + 888 + .info-btn, 889 + .feedback-btn { 890 + position: relative !important; 891 + right: auto !important; 892 + bottom: auto !important; 893 + display: inline-block; 894 + margin: 5px; 895 + padding: 10px 15px; 896 + font-size: 12px; 897 + } 898 + 899 + .character-image { 900 + position: relative !important; 901 + right: auto !important; 902 + bottom: auto !important; 903 + width: 120px; 904 + height: 120px; 905 + margin: 10px auto; 906 + } 907 + 908 + /* Navigation - Mobile */ 909 + .nav-tabs { 910 + flex-wrap: wrap; 911 + padding: 5px; 912 + gap: 5px; 913 + } 914 + 915 + .nav-tab { 916 + padding: 10px 15px; 917 + font-size: 14px; 918 + flex: 1 1 calc(25% - 10px); 919 + min-width: 80px; 920 + text-align: center; 921 + } 922 + 923 + .nav-tab-icon { 924 + display: block; 925 + margin: 0 auto 3px; 926 + font-size: 18px; 927 + } 928 + 929 + .you-are-here { 930 + display: none; 931 + } 932 + 933 + /* Content - Mobile */ 934 + .content-area { 935 + padding: 20px 15px; 936 + min-height: auto; 937 + } 938 + 939 + .video-grid { 940 + grid-template-columns: 1fr; 941 + gap: 15px; 942 + } 943 + 944 + /* Video Card - Mobile */ 945 + .video-card { 946 + border-width: 3px; 947 + } 948 + 949 + .video-info { 950 + padding: 15px; 951 + } 952 + 953 + .video-title { 954 + font-size: 16px; 955 + } 956 + 957 + /* Footer - Mobile */ 958 + .site-footer { 959 + padding: 20px 15px; 960 + } 961 + 962 + .footer-content { 963 + grid-template-columns: 1fr; 964 + gap: 20px; 965 + text-align: center; 966 + } 967 + 968 + /* Touch-friendly buttons */ 969 + button, 970 + .nav-tab, 971 + .homepage-btn, 972 + .login-btn, 973 + .video-card, 974 + .random-play-btn { 975 + min-height: 44px; /* iOS recommended touch target */ 976 + touch-action: manipulation; 977 + -webkit-tap-highlight-color: transparent; 978 + } 979 + 980 + /* Hide some decorative elements on mobile */ 981 + .decorative-circles { 982 + display: none; 983 + } 984 + 985 + /* Adjust floating elements */ 986 + .fun-label { 987 + font-size: 14px !important; 988 + padding: 8px 15px !important; 989 + } 990 + 991 + /* iSongs mobile adjustments */ 992 + .fun-label[style*="top: 30px"] { 993 + position: relative !important; 994 + top: auto !important; 995 + right: auto !important; 996 + margin-bottom: 10px; 997 + display: inline-block; 998 + } 999 + 1000 + /* Watch page mobile */ 1001 + .content-area > div[style*="maxWidth: 1000px"] { 1002 + padding: 0 10px; 1003 + } 1004 + } 1005 + 1006 + /* Small mobile devices */ 1007 + @media (max-width: 480px) { 1008 + .nav-tab { 1009 + flex: 1 1 calc(33.333% - 10px); 1010 + padding: 8px 10px; 1011 + font-size: 12px; 1012 + } 1013 + 1014 + .nav-tab-icon { 1015 + font-size: 16px; 1016 + } 1017 + 1018 + .random-play-btn { 1019 + padding: 30px 50px; 1020 + font-size: 24px; 1021 + } 1022 + 1023 + .search-btn { 1024 + padding: 10px 20px; 1025 + } 1026 + } 1027 + 1028 + /* Touch device optimizations */ 1029 + @media (hover: none) and (pointer: coarse) { 1030 + .video-card:hover { 1031 + transform: none; 1032 + } 1033 + 1034 + .video-card:active { 1035 + transform: scale(0.98); 1036 + } 1037 + 1038 + .nav-tab:hover { 1039 + transform: none; 1040 + } 1041 + 1042 + .nav-tab:active { 1043 + transform: translateY(2px); 1044 + } 1045 + 1046 + .random-play-btn:hover { 1047 + animation: float 4s ease-in-out infinite; 1048 + transform: none; 1049 + } 1050 + 1051 + .random-play-btn:active { 1052 + transform: scale(0.95); 1053 + } 1054 + } 1055 + 1056 + /* Safari-specific fixes */ 1057 + @supports (-webkit-touch-callout: none) { 1058 + /* iOS Safari specific styles */ 1059 + .video-thumbnail, 1060 + .snap-item, 1061 + .video-card { 1062 + -webkit-transform: translateZ(0); 1063 + transform: translateZ(0); 1064 + } 1065 + 1066 + input, 1067 + button { 1068 + -webkit-appearance: none; 1069 + border-radius: 0; 1070 + } 1071 + } 1072 + 1073 + /* Reduced motion preference */ 1074 + @media (prefers-reduced-motion: reduce) { 1075 + *, 1076 + *::before, 1077 + *::after { 1078 + animation-duration: 0.01ms !important; 1079 + animation-iteration-count: 1 !important; 1080 + transition-duration: 0.01ms !important; 1081 + } 1082 + }
+5 -15
src/app/watch/[id]/WatchPageClient.tsx
··· 49 49 try { 50 50 setLoading(true); 51 51 52 - // Get PDS URL 53 - const pdsRes = await fetch('https://stream.place/.well-known/atproto-did'); 54 - const pdsDid = await pdsRes.text(); 55 - 56 - // Resolve DID to get PDS endpoint 57 - const didRes = await fetch(`https://plc.directory/${pdsDid.trim()}`); 58 - const didDoc = await didRes.json(); 59 - const pdsUrl = didDoc.service?.find((s: { type: string }) => s.type === 'AtprotoPersonalDataServer')?.serviceEndpoint; 60 - 61 - if (!pdsUrl) { 62 - throw new Error('Could not find PDS endpoint'); 52 + // Use the stream.place VOD API directly 53 + const videosRes = await fetch('https://vod-beta.stream.place/xrpc/com.stream.place.vod.getVideos'); 54 + if (!videosRes.ok) { 55 + throw new Error(`Failed to fetch videos: ${videosRes.status}`); 63 56 } 64 - 65 - // Fetch videos 66 - const xrpcUrl = `${pdsUrl}/xrpc/com.stream.place.vod.getVideos`; 67 - const videosRes = await fetch(xrpcUrl); 57 + 68 58 const data = await videosRes.json(); 69 59 const allVideos = data.videos as { uri: string; cid: string; value: VideoRecord }[]; 70 60
+7 -15
src/components/HomeClient.tsx
··· 30 30 const [loading, setLoading] = useState(true); 31 31 const [error, setError] = useState<string | null>(null); 32 32 33 - // Fetch videos on client side 33 + // Fetch videos on client side - using stream.place API 34 34 useEffect(() => { 35 35 const fetchVideos = async () => { 36 36 try { 37 37 setLoading(true); 38 - // Get PDS URL 39 - const pdsRes = await fetch('https://stream.place/.well-known/atproto-did'); 40 - const pdsDid = await pdsRes.text(); 41 38 42 - // Resolve DID to get PDS endpoint 43 - const didRes = await fetch(`https://plc.directory/${pdsDid.trim()}`); 44 - const didDoc = await didRes.json(); 45 - const pdsUrl = didDoc.service?.find((s: { type: string }) => s.type === 'AtprotoPersonalDataServer')?.serviceEndpoint; 46 - 47 - if (!pdsUrl) { 48 - throw new Error('Could not find PDS endpoint'); 39 + // Use the stream.place VOD API directly 40 + const videosRes = await fetch('https://vod-beta.stream.place/xrpc/com.stream.place.vod.getVideos'); 41 + if (!videosRes.ok) { 42 + throw new Error(`Failed to fetch videos: ${videosRes.status}`); 49 43 } 50 - 51 - // Fetch videos 52 - const xrpcUrl = `${pdsUrl}/xrpc/com.stream.place.vod.getVideos`; 53 - const videosRes = await fetch(xrpcUrl); 44 + 54 45 const data = await videosRes.json(); 55 46 const allVideos = data.videos as { uri: string; cid: string; value: VideoRecord }[]; 56 47 ··· 59 50 allVideos.map(async (video) => { 60 51 try { 61 52 const handleRes = await fetch(`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveDid?did=${video.value.creator}`); 53 + if (!handleRes.ok) throw new Error('Failed to resolve handle'); 62 54 const handleData = await handleRes.json(); 63 55 return { 64 56 ...video,
+227 -103
src/components/ISongs.tsx
··· 1 1 'use client'; 2 2 3 - import { useState, useEffect } from 'react'; 3 + import { useState, useEffect, useRef } from 'react'; 4 + import Hls from 'hls.js'; 5 + import { VideoRecord } from '@/lib/types'; 4 6 5 - interface Track { 6 - id: number; 7 - title: string; 8 - user: { 9 - username: string; 10 - permalink_url: string; 11 - }; 12 - artwork_url?: string; 13 - duration: number; 14 - permalink_url: string; 15 - stream_url?: string; 16 - playback_count?: number; 7 + interface VideoWithHandle { 8 + uri: string; 9 + cid: string; 10 + value: VideoRecord; 11 + handle: string; 17 12 } 18 13 14 + const VOD_BETA_BASE = 'https://vod-beta.stream.place/xrpc'; 15 + 19 16 export default function ISongs() { 20 - const [tracks, setTracks] = useState<Track[]>([]); 21 - const [currentTrack, setCurrentTrack] = useState<Track | null>(null); 17 + const [videos, setVideos] = useState<VideoWithHandle[]>([]); 18 + const [currentVideo, setCurrentVideo] = useState<VideoWithHandle | null>(null); 22 19 const [loading, setLoading] = useState(true); 23 20 const [searchQuery, setSearchQuery] = useState(''); 24 21 const [isPlaying, setIsPlaying] = useState(false); 22 + const [currentTime, setCurrentTime] = useState(0); 23 + const [duration, setDuration] = useState(0); 24 + const audioRef = useRef<HTMLAudioElement>(null); 25 + const hlsRef = useRef<Hls | null>(null); 25 26 27 + // Fetch videos on mount 26 28 useEffect(() => { 27 - // Load SoundCloud widget API 28 - const script = document.createElement('script'); 29 - script.src = 'https://w.soundcloud.com/player/api.js'; 30 - script.async = true; 31 - document.body.appendChild(script); 29 + const fetchVideos = async () => { 30 + try { 31 + setLoading(true); 32 + const videosRes = await fetch('https://vod-beta.stream.place/xrpc/com.stream.place.vod.getVideos'); 33 + if (!videosRes.ok) { 34 + throw new Error(`Failed to fetch videos: ${videosRes.status}`); 35 + } 36 + 37 + const data = await videosRes.json(); 38 + const allVideos = data.videos as { uri: string; cid: string; value: VideoRecord }[]; 32 39 33 - // Use hardcoded popular tracks for demo since SoundCloud API requires auth 34 - const demoTracks: Track[] = [ 35 - { 36 - id: 1, 37 - title: 'Adventure Club - Do I See Color', 38 - user: { username: 'Adventure Club', permalink_url: '#' }, 39 - duration: 245000, 40 - permalink_url: 'https://soundcloud.com/adventureclub/do-i-see-color', 41 - playback_count: 8500000, 42 - }, 43 - { 44 - id: 2, 45 - title: 'Porter Robinson - Language', 46 - user: { username: 'Porter Robinson', permalink_url: '#' }, 47 - duration: 215000, 48 - permalink_url: 'https://soundcloud.com/porter-robinson/language', 49 - playback_count: 12000000, 50 - }, 51 - { 52 - id: 3, 53 - title: 'Nero - Promises', 54 - user: { username: 'Nero', permalink_url: '#' }, 55 - duration: 254000, 56 - permalink_url: 'https://soundcloud.com/nero/promises-skream-remix', 57 - playback_count: 6500000, 58 - }, 59 - { 60 - id: 4, 61 - title: 'Swedish House Mafia - Greyhound', 62 - user: { username: 'Swedish House Mafia', permalink_url: '#' }, 63 - duration: 195000, 64 - permalink_url: 'https://soundcloud.com/swedish-house-mafia/greyhound', 65 - playback_count: 9200000, 66 - }, 67 - { 68 - id: 5, 69 - title: 'Zedd - Clarity', 70 - user: { username: 'Zedd', permalink_url: '#' }, 71 - duration: 248000, 72 - permalink_url: 'https://soundcloud.com/zedd/clarity-ft-foxes', 73 - playback_count: 15000000, 74 - }, 75 - ]; 40 + // Resolve handles 41 + const videosWithHandles = await Promise.all( 42 + allVideos.map(async (video) => { 43 + try { 44 + const handleRes = await fetch(`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveDid?did=${video.value.creator}`); 45 + if (!handleRes.ok) throw new Error('Failed to resolve handle'); 46 + const handleData = await handleRes.json(); 47 + return { 48 + ...video, 49 + handle: handleData.handle || video.value.creator, 50 + }; 51 + } catch { 52 + return { 53 + ...video, 54 + handle: video.value.creator, 55 + }; 56 + } 57 + }) 58 + ); 76 59 77 - setTracks(demoTracks); 78 - setLoading(false); 79 - 80 - return () => { 81 - document.body.removeChild(script); 60 + setVideos(videosWithHandles); 61 + } catch (e) { 62 + console.error('Failed to load videos:', e); 63 + } finally { 64 + setLoading(false); 65 + } 82 66 }; 67 + 68 + fetchVideos(); 83 69 }, []); 84 70 85 - const playTrack = (track: Track) => { 86 - setCurrentTrack(track); 87 - setIsPlaying(true); 71 + // Setup audio playback with HLS 72 + useEffect(() => { 73 + if (!currentVideo || !audioRef.current) return; 74 + 75 + const audio = audioRef.current; 76 + const playlistUrl = `${VOD_BETA_BASE}/place.stream.playback.getVideoPlaylist?uri=${encodeURIComponent(currentVideo.uri)}`; 77 + 78 + // Cleanup previous HLS instance 79 + if (hlsRef.current) { 80 + hlsRef.current.destroy(); 81 + hlsRef.current = null; 82 + } 83 + 84 + if (Hls.isSupported()) { 85 + const hls = new Hls({ 86 + // Enable audio-only mode 87 + startLevel: -1, 88 + // Safari compatibility settings 89 + enableWorker: true, 90 + lowLatencyMode: false, 91 + }); 92 + 93 + hlsRef.current = hls; 94 + hls.loadSource(playlistUrl); 95 + hls.attachMedia(audio); 96 + 97 + hls.on(Hls.Events.MANIFEST_PARSED, () => { 98 + if (isPlaying) { 99 + audio.play().catch(console.error); 100 + } 101 + }); 102 + 103 + hls.on(Hls.Events.ERROR, (event, data) => { 104 + console.error('HLS error:', data); 105 + }); 106 + 107 + return () => { 108 + hls.destroy(); 109 + hlsRef.current = null; 110 + }; 111 + } else if (audio.canPlayType('application/vnd.apple.mpegurl')) { 112 + // Safari native HLS support 113 + audio.src = playlistUrl; 114 + if (isPlaying) { 115 + audio.play().catch(console.error); 116 + } 117 + } 118 + }, [currentVideo]); 119 + 120 + // Handle play/pause 121 + useEffect(() => { 122 + if (!audioRef.current) return; 123 + 124 + if (isPlaying) { 125 + audioRef.current.play().catch(console.error); 126 + } else { 127 + audioRef.current.pause(); 128 + } 129 + }, [isPlaying]); 130 + 131 + const playVideo = (video: VideoWithHandle) => { 132 + if (currentVideo?.uri === video.uri) { 133 + togglePlay(); 134 + } else { 135 + setCurrentVideo(video); 136 + setIsPlaying(true); 137 + setCurrentTime(0); 138 + } 88 139 }; 89 140 90 141 const togglePlay = () => { 91 142 setIsPlaying(!isPlaying); 92 143 }; 93 144 94 - const formatDuration = (ms: number) => { 95 - const seconds = Math.floor(ms / 1000); 145 + const handleTimeUpdate = () => { 146 + if (audioRef.current) { 147 + setCurrentTime(audioRef.current.currentTime); 148 + } 149 + }; 150 + 151 + const handleLoadedMetadata = () => { 152 + if (audioRef.current) { 153 + setDuration(audioRef.current.duration); 154 + } 155 + }; 156 + 157 + const formatDuration = (ns: number) => { 158 + const totalSeconds = Math.floor(ns / 1_000_000_000); 159 + const hours = Math.floor(totalSeconds / 3600); 160 + const minutes = Math.floor((totalSeconds % 3600) / 60); 161 + const secs = totalSeconds % 60; 162 + 163 + if (hours > 0) { 164 + return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; 165 + } 166 + return `${minutes}:${secs.toString().padStart(2, '0')}`; 167 + }; 168 + 169 + const formatTime = (seconds: number) => { 96 170 const mins = Math.floor(seconds / 60); 97 - const secs = seconds % 60; 171 + const secs = Math.floor(seconds % 60); 98 172 return `${mins}:${secs.toString().padStart(2, '0')}`; 99 173 }; 100 174 101 - const filteredTracks = tracks.filter(track => 102 - track.title.toLowerCase().includes(searchQuery.toLowerCase()) || 103 - track.user.username.toLowerCase().includes(searchQuery.toLowerCase()) 175 + const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => { 176 + const time = parseFloat(e.target.value); 177 + setCurrentTime(time); 178 + if (audioRef.current) { 179 + audioRef.current.currentTime = time; 180 + } 181 + }; 182 + 183 + const filteredVideos = videos.filter(video => 184 + video.value.title.toLowerCase().includes(searchQuery.toLowerCase()) || 185 + video.handle.toLowerCase().includes(searchQuery.toLowerCase()) 104 186 ); 105 187 106 188 if (loading) { ··· 113 195 114 196 return ( 115 197 <div style={{ position: 'relative', zIndex: 1 }}> 198 + {/* Hidden audio element */} 199 + <audio 200 + ref={audioRef} 201 + onTimeUpdate={handleTimeUpdate} 202 + onLoadedMetadata={handleLoadedMetadata} 203 + onEnded={() => setIsPlaying(false)} 204 + style={{ display: 'none' }} 205 + /> 206 + 116 207 {/* Now Playing */} 117 - {currentTrack && ( 208 + {currentVideo && ( 118 209 <div style={{ 119 210 background: 'linear-gradient(135deg, #62166F, #8B2F9B)', 120 211 border: '4px solid #fff', ··· 122 213 padding: '25px', 123 214 marginBottom: '30px', 124 215 }}> 125 - <div style={{ display: 'flex', alignItems: 'center', gap: '25px' }}> 216 + <div style={{ display: 'flex', alignItems: 'center', gap: '25px', flexWrap: 'wrap' }}> 126 217 <div style={{ 127 218 width: '100px', 128 219 height: '100px', ··· 132 223 alignItems: 'center', 133 224 justifyContent: 'center', 134 225 fontSize: '50px', 226 + flexShrink: 0, 135 227 }}> 136 228 🎵 137 229 </div> 138 230 139 - <div style={{ flex: 1 }}> 231 + <div style={{ flex: 1, minWidth: '200px' }}> 140 232 <h3 style={{ color: '#fff', fontSize: '24px', marginBottom: '5px' }}> 141 - {currentTrack.title} 233 + {currentVideo.value.title} 142 234 </h3> 143 235 <p style={{ color: '#A6CC3A', fontSize: '18px' }}> 144 - {currentTrack.user.username} 236 + {currentVideo.handle} 237 + </p> 238 + <p style={{ color: '#fff', fontSize: '14px', marginTop: '5px', opacity: 0.8 }}> 239 + 🔊 Audio Only Mode 145 240 </p> 146 241 </div> 147 242 ··· 155 250 fontSize: '30px', 156 251 cursor: 'pointer', 157 252 boxShadow: '4px 4px 0 rgba(0,0,0,0.3)', 253 + flexShrink: 0, 158 254 }} 159 255 > 160 256 {isPlaying ? '⏸️' : '▶️'} 161 257 </button> 162 258 </div> 163 259 164 - {/* SoundCloud Embed */} 165 - <iframe 166 - width="100%" 167 - height="120" 168 - scrolling="no" 169 - frameBorder="no" 170 - allow="autoplay" 171 - src={`https://w.soundcloud.com/player/?url=${encodeURIComponent(currentTrack.permalink_url)}&auto_play=${isPlaying ? 'true' : 'false'}&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true`} 172 - style={{ marginTop: '20px', border: '3px solid #fff' }} 173 - /> 260 + {/* Progress bar */} 261 + <div style={{ marginTop: '20px' }}> 262 + <input 263 + type="range" 264 + min={0} 265 + max={duration || 100} 266 + value={currentTime} 267 + onChange={handleSeek} 268 + style={{ 269 + width: '100%', 270 + height: '10px', 271 + cursor: 'pointer', 272 + }} 273 + /> 274 + <div style={{ 275 + display: 'flex', 276 + justifyContent: 'space-between', 277 + color: '#fff', 278 + fontSize: '14px', 279 + marginTop: '5px' 280 + }}> 281 + <span>{formatTime(currentTime)}</span> 282 + <span>{formatTime(duration)}</span> 283 + </div> 284 + </div> 174 285 </div> 175 286 )} 176 287 ··· 185 296 color: '#fff', 186 297 fontWeight: 'bold', 187 298 }}> 188 - 🎵 Powered by <a href="https://soundcloud.com" target="_blank" rel="noopener noreferrer" style={{ color: '#A6CC3A' }}>SoundCloud</a> 299 + 🎵 Audio from <a href="https://stream.place" target="_blank" rel="noopener noreferrer" style={{ color: '#A6CC3A' }}>stream.place</a> VODs 189 300 </div> 190 301 191 302 {/* Search */} 192 303 <div style={{ display: 'flex', gap: '10px', marginBottom: '25px' }}> 193 304 <input 194 305 type="text" 195 - placeholder="Search tracks..." 306 + placeholder="Search audio tracks..." 196 307 value={searchQuery} 197 308 onChange={(e) => setSearchQuery(e.target.value)} 198 309 style={{ ··· 222 333 223 334 {/* Track List */} 224 335 <div style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}> 225 - {filteredTracks.map((track) => ( 336 + {filteredVideos.map((video) => ( 226 337 <div 227 - key={track.id} 228 - onClick={() => playTrack(track)} 338 + key={video.uri} 339 + onClick={() => playVideo(video)} 229 340 style={{ 230 341 background: '#fff', 231 342 border: '3px solid #62166F', ··· 235 346 alignItems: 'center', 236 347 gap: '20px', 237 348 cursor: 'pointer', 349 + flexWrap: 'wrap', 238 350 }} 239 351 > 240 352 <div style={{ ··· 246 358 alignItems: 'center', 247 359 justifyContent: 'center', 248 360 fontSize: '28px', 361 + flexShrink: 0, 249 362 }}> 250 - {currentTrack?.id === track.id && isPlaying ? '⏸️' : '▶️'} 363 + {currentVideo?.uri === video.uri && isPlaying ? '⏸️' : '▶️'} 251 364 </div> 252 - <div style={{ flex: 1 }}> 365 + <div style={{ flex: 1, minWidth: '200px' }}> 253 366 <h4 style={{ fontSize: '20px', color: '#333', marginBottom: '5px' }}> 254 - {track.title} 367 + {video.value.title} 255 368 </h4> 256 369 <p style={{ color: '#62166F', fontSize: '16px' }}> 257 - {track.user.username} 370 + {video.handle} 258 371 </p> 259 372 </div> 260 373 <div style={{ textAlign: 'right' }}> 261 374 <p style={{ color: '#666', fontSize: '14px' }}> 262 - ▶️ {(track.playback_count || 0).toLocaleString()} plays 375 + {formatDuration(video.value.duration)} 263 376 </p> 264 377 <p style={{ color: '#999', fontSize: '14px' }}> 265 - {formatDuration(track.duration)} 378 + {new Date(video.value.createdAt).toLocaleDateString()} 266 379 </p> 267 380 </div> 268 381 </div> 269 382 ))} 270 383 </div> 384 + 385 + {filteredVideos.length === 0 && !loading && ( 386 + <div style={{ 387 + textAlign: 'center', 388 + padding: '40px', 389 + color: '#fff', 390 + fontSize: '18px', 391 + }}> 392 + 🎵 No audio tracks found. Try a different search! 393 + </div> 394 + )} 271 395 </div> 272 396 ); 273 397 }