{"id":38332267,"url":"https://github.com/codingshot/afrobeatsdao","last_synced_at":"2026-01-17T02:52:26.019Z","repository":{"id":321781961,"uuid":"969994754","full_name":"codingshot/afrobeatsdao","owner":"codingshot","description":null,"archived":false,"fork":false,"pushed_at":"2026-01-14T05:21:13.000Z","size":30388,"stargazers_count":0,"open_issues_count":1,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-01-14T09:19:09.282Z","etag":null,"topics":["afrobeats","afrobeats-dance","amapiano","events-platform","highlife","music","music-platform","music-player"],"latest_commit_sha":null,"homepage":"https://afrobeats.party","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/codingshot.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-04-21T09:28:57.000Z","updated_at":"2026-01-14T05:21:17.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/codingshot/afrobeatsdao","commit_stats":null,"previous_names":["codingshot/afrobeatsdao"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/codingshot/afrobeatsdao","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codingshot%2Fafrobeatsdao","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codingshot%2Fafrobeatsdao/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codingshot%2Fafrobeatsdao/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codingshot%2Fafrobeatsdao/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/codingshot","download_url":"https://codeload.github.com/codingshot/afrobeatsdao/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codingshot%2Fafrobeatsdao/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28492590,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-17T02:39:23.645Z","status":"ssl_error","status_checked_at":"2026-01-17T02:34:19.649Z","response_time":85,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["afrobeats","afrobeats-dance","amapiano","events-platform","highlife","music","music-platform","music-player"],"created_at":"2026-01-17T02:52:25.342Z","updated_at":"2026-01-17T02:52:25.991Z","avatar_url":"https://github.com/codingshot.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# AfrobeatsDAO\n\n**URL**: https://lovable.dev/projects/d16edf1f-4126-499a-89e7-db99f81ad1c2\n\nA comprehensive Afrobeats music platform featuring a global audio player, artist discovery, dance tutorials, event listings, and community features.\n\n---\n\n## Technology Stack\n\n- **Framework**: React 18 with TypeScript\n- **Build Tool**: Vite\n- **Styling**: Tailwind CSS with shadcn/ui components\n- **Routing**: React Router DOM v6\n- **State Management**: React Context API\n- **Audio**: YouTube IFrame API\n- **Drag \u0026 Drop**: react-beautiful-dnd\n- **Icons**: Lucide React\n- **Animations**: Framer Motion\n\n---\n\n## Global Audio Player - Comprehensive Documentation\n\nThe Global Audio Player is a persistent, YouTube-powered music player that provides seamless audio playback across all pages of the application. It is implemented as a React Context provider that wraps the entire application.\n\n### File Structure\n\n```\nsrc/components/\n├── GlobalAudioPlayer.tsx    # Main player component \u0026 context provider\n├── QueueDrawer.tsx          # Queue and history management drawer\n├── MarkdownPreviewDialog.tsx # Export preview dialog\n└── VibeOfTheDay.tsx         # Contains VIBE_VIDEOS array for random playback\n```\n\n---\n\n### Core Architecture\n\n#### Context Provider Pattern\n\nThe player uses React Context to provide global state and controls accessible from any component in the application.\n\n```typescript\n// Context type definition\ninterface GlobalAudioPlayerContextType {\n  currentSong: Song | null;\n  queue: Song[];\n  isPlaying: boolean;\n  playNow: (song: Song) =\u003e void;\n  addToQueue: (song: Song) =\u003e void;\n  removeFromQueue: (songId: string) =\u003e void;\n  togglePlay: () =\u003e void;\n  nextSong: () =\u003e void;\n  previousSong: () =\u003e void;\n  setVolume: (value: number) =\u003e void;\n  toggleRepeat: () =\u003e void;\n  reorderQueue: (from: number, to: number) =\u003e void;\n  duration: number;\n  currentTime: number;\n  isDragging: boolean;\n}\n```\n\n#### Song Interface\n\n```typescript\ninterface Song {\n  id: string;        // Unique identifier for the song\n  youtube: string;   // YouTube video URL or video ID\n  title?: string;    // Optional display title\n  artist?: string;   // Optional artist name\n}\n```\n\n---\n\n### State Management\n\n#### Local Storage Persistence\n\nThe player persists the following state to `localStorage`:\n\n| Key | Purpose |\n|-----|---------|\n| `afrobeats_current_song` | Currently playing song object |\n| `afrobeats_queue` | Array of songs in the queue |\n| `afrobeats_volume` | Volume level (0-100) |\n| `afrobeats_repeat` | Repeat mode boolean |\n| `afrobeats_played_songs` | Array of recently played song IDs |\n| `afrobeats_video_visible` | Video visibility state boolean |\n\n#### State Variables\n\n| State | Type | Default | Description |\n|-------|------|---------|-------------|\n| `player` | any | null | YouTube player instance |\n| `currentSong` | Song \\| null | null | Currently playing song |\n| `queue` | Song[] | [] | Queue of upcoming songs |\n| `isPlaying` | boolean | false | Playback state |\n| `volume` | number | 100 | Volume level (0-100) |\n| `repeat` | boolean | false | Repeat mode |\n| `youtubeApiLoaded` | boolean | false | YouTube API load state |\n| `expandedView` | boolean | true | Player view mode |\n| `videoTitle` | string | \"Loading...\" | Current video title from YouTube |\n| `channelTitle` | string | \"Loading...\" | Channel name from YouTube |\n| `previousVideoData` | Song \\| null | null | Previous song for error recovery |\n| `isMobile` | boolean | (detected) | Mobile device detection |\n| `videoVisible` | boolean | false | Video player visibility |\n| `duration` | number | 0 | Song duration in seconds |\n| `currentTime` | number | 0 | Current playback position |\n| `isDragging` | boolean | false | Seek slider drag state |\n| `isLoading` | boolean | false | Loading state |\n| `loadingTitle` | string | \"Loading...\" | Title shown during load |\n| `queueVisible` | boolean | false | Queue drawer visibility |\n| `showPlayedSongs` | boolean | false | Filter played songs in queue |\n| `playedSongs` | Set\\\u003cstring\\\u003e | new Set() | Set of played song IDs |\n| `isInitialLoad` | boolean | true | Initial load flag |\n| `thumbnailUrl` | string | \"\" | YouTube thumbnail URL |\n| `showVolumeSlider` | boolean | false | Volume popup visibility (mobile) |\n\n---\n\n### Core Functions\n\n#### `playNow(song: Song)`\n\nImmediately plays a song, replacing the current track.\n\n**Behavior:**\n1. Sets loading state to true\n2. Stores previous song for error recovery\n3. Updates `currentSong` state\n4. Sets `isPlaying` to true\n5. Extracts video ID from YouTube URL\n6. Calls `player.loadVideoById(videoId)`\n7. Updates thumbnail URL\n\n#### `addToQueue(song: Song)`\n\nAdds a song to the end of the playback queue.\n\n```typescript\nconst addToQueue = useCallback((song: Song) =\u003e {\n  setQueue(prev =\u003e [...prev, song]);\n}, []);\n```\n\n#### `removeFromQueue(songId: string)`\n\nRemoves a specific song from the queue by its ID.\n\n```typescript\nconst removeFromQueue = useCallback((songId: string) =\u003e {\n  setQueue(prev =\u003e prev.filter(song =\u003e song.id !== songId));\n}, []);\n```\n\n#### `togglePlay()`\n\nToggles between play and pause states.\n\n**Behavior:**\n- If playing → calls `player.pauseVideo()`\n- If paused → calls `player.playVideo()`\n- Updates `isPlaying` state\n\n#### `nextSong()`\n\nAdvances to the next song.\n\n**Behavior:**\n1. If queue has songs → plays first song in queue, removes from queue\n2. If queue is empty → calls `findUnplayedSong()` to get a random vibe video\n\n#### `previousSong()`\n\nSeeks to the beginning of the current song.\n\n```typescript\nconst previousSong = useCallback(() =\u003e {\n  if (player) {\n    player.seekTo(0);\n  }\n}, [player]);\n```\n\n#### `updateVolume(value: number)`\n\nSets the playback volume.\n\n```typescript\nconst updateVolume = useCallback((value: number) =\u003e {\n  if (player) {\n    player.setVolume(value);\n    setVolume(value);\n  }\n}, [player]);\n```\n\n#### `toggleRepeat()`\n\nToggles repeat mode on/off.\n\n#### `reorderQueue(from: number, to: number)`\n\nReorders the queue via drag and drop.\n\n```typescript\nconst reorderQueue = useCallback((from: number, to: number) =\u003e {\n  setQueue(prev =\u003e {\n    const newQueue = [...prev];\n    const [removed] = newQueue.splice(from, 1);\n    newQueue.splice(to, 0, removed);\n    return newQueue;\n  });\n}, []);\n```\n\n#### `toggleVideo()`\n\nToggles YouTube video player visibility.\n\n#### `toggleExpandedView()`\n\nToggles between expanded and collapsed player views.\n\n#### `toggleQueueVisibility()`\n\nShows/hides the queue drawer.\n\n---\n\n### Helper Functions\n\n#### `getVideoId(youtube: string)`\n\nExtracts video ID from various YouTube URL formats.\n\n**Supported formats:**\n- `https://www.youtube.com/watch?v=VIDEO_ID`\n- `https://youtu.be/VIDEO_ID`\n- Direct video ID string\n\n```typescript\nconst getVideoId = useCallback((youtube: string): string =\u003e {\n  if (youtube.includes('v=')) {\n    return youtube.split('v=')[1].split('\u0026')[0];\n  } else if (youtube.includes('youtu.be/')) {\n    return youtube.split('youtu.be/')[1].split('?')[0];\n  }\n  return youtube;\n}, []);\n```\n\n#### `formatTime(seconds: number)`\n\nFormats seconds into `MM:SS` display format.\n\n```typescript\nconst formatTime = (seconds: number) =\u003e {\n  const mins = Math.floor(seconds / 60);\n  const secs = Math.floor(seconds % 60);\n  return `${mins}:${secs.toString().padStart(2, '0')}`;\n};\n```\n\n#### `getRandomVibeVideo(excludeId?: string)`\n\nReturns a random video from `VIBE_VIDEOS` array, optionally excluding a specific ID.\n\n```typescript\nconst getRandomVibeVideo = useCallback((excludeId?: string) =\u003e {\n  const availableVideos = VIBE_VIDEOS.filter(id =\u003e id !== excludeId);\n  const randomIndex = Math.floor(Math.random() * availableVideos.length);\n  return availableVideos[randomIndex];\n}, []);\n```\n\n#### `findUnplayedSong(currentId: string | undefined)`\n\nIntelligently finds the next song to play.\n\n**Algorithm:**\n1. First, searches queue for unplayed songs\n2. If all queue songs played, gets random vibe video\n3. Tries up to 5 times to avoid recently played videos\n4. Returns a Song object\n\n---\n\n### YouTube Integration\n\n#### API Loading\n\nThe player dynamically loads the YouTube IFrame API:\n\n```typescript\nuseEffect(() =\u003e {\n  if (!window.YT \u0026\u0026 !document.getElementById('youtube-iframe-api')) {\n    const tag = document.createElement('script');\n    tag.id = 'youtube-iframe-api';\n    tag.src = 'https://www.youtube.com/iframe_api';\n    const firstScriptTag = document.getElementsByTagName('script')[0];\n    firstScriptTag.parentNode?.insertBefore(tag, firstScriptTag);\n    window.onYouTubeIframeAPIReady = () =\u003e {\n      setYoutubeApiLoaded(true);\n    };\n  } else if (window.YT) {\n    setYoutubeApiLoaded(true);\n  }\n}, []);\n```\n\n#### Player Initialization\n\n```typescript\nconst newPlayer = new window.YT.Player('youtube-player', {\n  height: '240',\n  width: '426',\n  playerVars: {\n    playsinline: 1,\n    controls: 1\n  },\n  events: {\n    onStateChange: handleStateChange,\n    onError: handleError,\n    onReady: handleReady\n  }\n});\n```\n\n#### Event Handlers\n\n**onStateChange:**\n- `ENDED` → Plays next song or repeats current\n- `PLAYING` → Updates metadata, sets duration\n- `PAUSED` → Updates isPlaying state\n- `BUFFERING` → Sets loading state\n\n**onError:**\n- Logs error to console\n- Shows toast notification\n- Adds failed song to end of queue\n- Plays next song\n- Falls back to previous video if available\n\n**onReady:**\n- Sets initial volume\n- Loads saved current song if available\n\n---\n\n### Media Session API Integration\n\nThe player integrates with the browser's Media Session API for background playback control:\n\n```typescript\nuseEffect(() =\u003e {\n  if ('mediaSession' in navigator \u0026\u0026 currentSong) {\n    navigator.mediaSession.metadata = new MediaMetadata({\n      title: videoTitle || 'Unknown Title',\n      artist: channelTitle || 'Unknown Artist',\n      album: 'Afrobeats Player',\n      artwork: [{ \n        src: thumbnailUrl || '/AfrobeatsDAOMeta.png', \n        sizes: '128x128', \n        type: 'image/png' \n      }]\n    });\n    \n    navigator.mediaSession.setActionHandler('play', togglePlay);\n    navigator.mediaSession.setActionHandler('pause', togglePlay);\n    navigator.mediaSession.setActionHandler('previoustrack', previousSong);\n    navigator.mediaSession.setActionHandler('nexttrack', nextSong);\n  }\n}, [currentSong, videoTitle, channelTitle, thumbnailUrl]);\n```\n\n---\n\n### UI Layout\n\n#### Positioning\n\nThe player is fixed to the bottom of the viewport:\n\n```css\nposition: fixed;\nbottom: 0;\nleft: 0;\nright: 0;\nz-index: 150;\n```\n\n#### Color Scheme\n\n| Element | Color |\n|---------|-------|\n| Background | `bg-black/95` (95% opacity black) |\n| Border | `border-white/10` (10% opacity white) |\n| Text | `text-white` |\n| Accent | `#FFD600` (Golden yellow) |\n| Secondary text | `text-gray-400` |\n\n#### Desktop Layout (3-column)\n\n```\n┌─────────────────────────────────────────────────────────────────────┐\n│ [Thumbnail] Title          [\u003c\u003c][▶][\u003e\u003e][🔁]    [Queue][Video][🔊]━━━ │\n│             Channel                                                  │\n├─────────────────────────────────────────────────────────────────────┤\n│ 0:00 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3:45 │\n└─────────────────────────────────────────────────────────────────────┘\n```\n\n**Column 1 (Left):** Song info with thumbnail, title, channel\n**Column 2 (Center):** Playback controls (Previous, Play/Pause, Next, Repeat)\n**Column 3 (Right):** Queue toggle, Video toggle, Volume controls\n\n#### Mobile Layout (Vertical Stack)\n\n```\n┌─────────────────────────────────────────────┐\n│ [Thumbnail] Title                    [Queue]│\n│             Channel                         │\n├─────────────────────────────────────────────┤\n│ 0:00 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3:45 │\n├─────────────────────────────────────────────┤\n│     [\u003c\u003c] [▶] [\u003e\u003e] [🔁] [🔊] [📹]          │\n└─────────────────────────────────────────────┘\n```\n\n**Row 1:** Song info + Queue button\n**Row 2:** Time slider with current/total time\n**Row 3:** All controls centered\n\n---\n\n### Control Buttons\n\n| Icon | Component | Action |\n|------|-----------|--------|\n| `\u003cSkipBack /\u003e` | Previous | Seeks to start of song |\n| `\u003cPlay /\u003e` / `\u003cPause /\u003e` | Play/Pause | Toggle playback |\n| `\u003cSkipForward /\u003e` | Next | Play next song |\n| `\u003cRepeat /\u003e` / `\u003cRepeat1 /\u003e` | Repeat | Toggle repeat mode |\n| `\u003cList /\u003e` / `\u003cListCollapse /\u003e` | Queue | Toggle queue drawer |\n| `\u003cVideo /\u003e` / `\u003cVideoOff /\u003e` | Video | Toggle video visibility |\n| `\u003cVolume2 /\u003e` / `\u003cVolumeX /\u003e` | Volume | Mute/unmute or show slider |\n\n#### Active State Styling\n\nActive controls use accent color: `text-[#FFD600]`\n\n---\n\n### Volume Control\n\n#### Desktop\n\nHorizontal slider inline with controls:\n\n```typescript\n\u003cdiv className=\"w-24\"\u003e\n  \u003cSlider \n    value={[volume]} \n    min={0} \n    max={100} \n    step={1} \n    onValueChange={([value]) =\u003e updateVolume(value)} \n  /\u003e\n\u003c/div\u003e\n```\n\n#### Mobile\n\nVertical popup slider on hover/tap:\n\n```typescript\n\u003cdiv className=\"relative\"\u003e\n  \u003cButton \n    onMouseEnter={() =\u003e setShowVolumeSlider(true)}\n    onMouseLeave={() =\u003e setShowVolumeSlider(false)}\n    onClick={() =\u003e setVolume(volume === 0 ? 100 : 0)}\n  \u003e\n    {volume === 0 ? \u003cVolumeX /\u003e : \u003cVolume2 /\u003e}\n  \u003c/Button\u003e\n  {showVolumeSlider \u0026\u0026 (\n    \u003cdiv className=\"absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2\"\u003e\n      \u003cSlider orientation=\"vertical\" className=\"h-16\" ... /\u003e\n    \u003c/div\u003e\n  )}\n\u003c/div\u003e\n```\n\n---\n\n### Time/Seek Slider\n\n#### Functionality\n\n- Displays current position and total duration\n- Drag to seek (sets `isDragging` state)\n- Updates on release via `onValueCommit`\n\n```typescript\n\u003cSlider \n  value={[currentTime]} \n  min={0} \n  max={duration} \n  step={1} \n  onValueChange={([value]) =\u003e {\n    setCurrentTime(value);\n    setIsDragging(true);\n  }} \n  onValueCommit={([value]) =\u003e {\n    handleTimeChange(value);\n    setIsDragging(false);\n  }} \n/\u003e\n```\n\n#### Time Update Loop\n\n```typescript\nuseEffect(() =\u003e {\n  if (!player || !isPlaying || isDragging) return;\n  \n  const interval = setInterval(() =\u003e {\n    if (player.getCurrentTime) {\n      setCurrentTime(player.getCurrentTime());\n    }\n  }, 1000);\n  \n  return () =\u003e clearInterval(interval);\n}, [player, isPlaying, isDragging]);\n```\n\n---\n\n### Video Player Container\n\nThe YouTube video is rendered in a positioned container:\n\n```typescript\n\u003cdiv \n  ref={playerContainerRef} \n  className={`fixed z-[200] bg-black/95 border border-white/10 rounded-lg overflow-hidden shadow-xl ${\n    isMobile \n      ? 'bottom-[100px] right-4 left-4' \n      : 'bottom-[80px] right-4'\n  }`}\n  style={{\n    display: expandedView ? 'block' : 'none',\n    visibility: videoVisible ? 'visible' : 'hidden',\n    ...(expandedView \u0026\u0026 !videoVisible ? { left: '-9999px' } : {})\n  }}\n\u003e\n  \u003cdiv id=\"youtube-player\"\u003e\u003c/div\u003e\n\u003c/div\u003e\n```\n\n**Desktop positioning:** Bottom right, above player bar\n**Mobile positioning:** Full width with margins, above player bar\n\n---\n\n### Empty State\n\nWhen no song is playing:\n\n```typescript\n\u003cdiv className=\"max-w-7xl mx-auto flex items-center justify-between\"\u003e\n  \u003cdiv className=\"flex items-center gap-4\"\u003e\n    \u003cMusic2 className=\"h-8 w-8 text-[#FFD600]\" /\u003e\n    \u003cspan className=\"text-sm\"\u003eAfrobeats Player\u003c/span\u003e\n  \u003c/div\u003e\n  \u003cButton \n    onClick={() =\u003e {\n      const defaultVideo = getRandomVibeVideo();\n      playNow({\n        id: `default-vibe-${defaultVideo}`,\n        youtube: defaultVideo,\n        title: \"Random Vibe\"\n      });\n    }} \n    className=\"bg-[#FFD600] text-black hover:bg-[#FFD600]/90\"\n  \u003e\n    \u003cPlay className=\"mr-2 h-4 w-4\" /\u003e\n    Play Something\n  \u003c/Button\u003e\n\u003c/div\u003e\n```\n\n---\n\n### Queue Drawer Component\n\nLocation: `src/components/QueueDrawer.tsx`\n\n#### Props\n\n```typescript\ninterface QueueDrawerProps {\n  queue: Song[];\n  isVisible: boolean;\n  playNow: (song: Song) =\u003e void;\n  reorderQueue: (from: number, to: number) =\u003e void;\n  playedSongs: Set\u003cstring\u003e;\n  showPlayedSongs: boolean;\n  setShowPlayedSongs: (show: boolean) =\u003e void;\n}\n```\n\n#### Features\n\n1. **Tabs:** Queue / History switching\n2. **Drag \u0026 Drop:** Reorder queue items\n3. **Minimizable:** Collapse to small header\n4. **Export:** Download queue/history as markdown\n\n#### Layout\n\n```\n┌─────────────────────────────────────┐\n│ [Queue] [History]           [—]     │\n├─────────────────────────────────────┤\n│                                     │\n│ [≡] [Thumb] Title           [Played]│\n│             Artist                  │\n│                                     │\n│ [≡] [Thumb] Title                   │\n│             Artist                  │\n│                                     │\n│ [≡] [Thumb] Title                   │\n│             Artist                  │\n│                                     │\n├─────────────────────────────────────┤\n│     [📥 Export Queue]              │\n└─────────────────────────────────────┘\n```\n\n#### Positioning\n\n```css\nposition: fixed;\nright: 4 (1rem);\nbottom: 80px;\nwidth: 350px;\nz-index: 40;\n```\n\n#### Drag \u0026 Drop Implementation\n\nUses `react-beautiful-dnd`:\n\n```typescript\n\u003cDragDropContext onDragEnd={handleDragEnd}\u003e\n  \u003cDroppable droppableId=\"queue-drawer-droppable\"\u003e\n    {(provided) =\u003e (\n      \u003cdiv {...provided.droppableProps} ref={provided.innerRef}\u003e\n        {filteredQueue.map((song, index) =\u003e (\n          \u003cDraggable \n            key={`queue-drawer-item-${song.id}`} \n            draggableId={`queue-drawer-item-${song.id}`} \n            index={index}\n          \u003e\n            {(provided) =\u003e (\n              \u003cdiv ref={provided.innerRef} {...provided.draggableProps}\u003e\n                \u003cdiv {...provided.dragHandleProps}\u003e\n                  \u003cMoveVertical /\u003e\n                \u003c/div\u003e\n                {/* Song content */}\n              \u003c/div\u003e\n            )}\n          \u003c/Draggable\u003e\n        ))}\n        {provided.placeholder}\n      \u003c/div\u003e\n    )}\n  \u003c/Droppable\u003e\n\u003c/DragDropContext\u003e\n```\n\n#### Queue Item\n\n```typescript\n\u003cdiv className=\"flex items-center gap-3 p-2 rounded-md hover:bg-accent/10 group\"\u003e\n  {/* Drag handle */}\n  \u003cdiv {...provided.dragHandleProps}\u003e\n    \u003cMoveVertical className=\"h-4 w-4\" /\u003e\n  \u003c/div\u003e\n  \n  {/* Thumbnail with play overlay */}\n  \u003cdiv className=\"relative w-16 h-12 rounded-md overflow-hidden\"\u003e\n    \u003cimg src={getVideoThumbnail(videoId)} /\u003e\n    \u003cButton \n      className=\"absolute inset-0 opacity-0 group-hover:opacity-100 bg-black/50\"\n      onClick={() =\u003e playNow(song)}\n    \u003e\n      \u003cPlay /\u003e\n    \u003c/Button\u003e\n  \u003c/div\u003e\n  \n  {/* Song info */}\n  \u003cdiv className=\"flex-1 min-w-0\"\u003e\n    \u003ch4 className=\"font-medium text-sm truncate text-black\"\u003e\n      {song.title || \"Title of video\"}\n    \u003c/h4\u003e\n    \u003cp className=\"text-xs text-muted-foreground truncate\"\u003e\n      {song.artist || \"Unknown Artist\"}\n    \u003c/p\u003e\n  \u003c/div\u003e\n  \n  {/* Played badge */}\n  {playedSongs.has(song.id) \u0026\u0026 (\n    \u003cBadge variant=\"outline\"\u003ePlayed\u003c/Badge\u003e\n  )}\n\u003c/div\u003e\n```\n\n#### History Tab\n\nDisplays recently played songs (without drag \u0026 drop):\n\n```typescript\n{playedSongsList.map((song, index) =\u003e (\n  \u003cdiv key={`history-item-${song.id}`} className=\"flex items-center gap-3 p-2\"\u003e\n    {/* Same structure as queue item, without drag handle */}\n  \u003c/div\u003e\n))}\n```\n\n#### Export Functionality\n\nGenerates markdown content for queue or history:\n\n```typescript\nconst generateMarkdownContent = (tab: TabType) =\u003e {\n  let content = \"# Afrobeats Music History\\n\\n\";\n  \n  if (tab === \"queue\") {\n    content += \"## Current Queue\\n\\n\";\n    filteredQueue.forEach((song, index) =\u003e {\n      content += `${index + 1}. **${song.title}** - ${song.artist}\\n`;\n      content += `   - [Watch Video](https://www.youtube.com/watch?v=${videoId})\\n\\n`;\n    });\n  }\n  // ... similar for history\n  \n  content += `Exported on ${new Date().toLocaleString()}\\n`;\n  return content;\n};\n```\n\nDownload function:\n\n```typescript\nconst handleDownload = () =\u003e {\n  const blob = new Blob([markdownContent], { type: 'text/markdown' });\n  const url = URL.createObjectURL(blob);\n  const a = document.createElement('a');\n  a.href = url;\n  a.download = \"afrobeats-queue.md\";\n  a.click();\n  URL.revokeObjectURL(url);\n};\n```\n\n#### Empty States\n\n**Queue empty:**\n```\n[ListMusic icon]\nYour queue is empty\nAdd songs from playlists\n```\n\n**History empty:**\n```\n[ListMusic icon]\nNo play history yet\nSongs will appear here after playing\n```\n\n---\n\n### Recently Played Tracking\n\nThe player tracks recently played songs to avoid repetition:\n\n```typescript\nconst RECENTLY_PLAYED_LIMIT = 10;\n\nuseEffect(() =\u003e {\n  if (currentSong?.id) {\n    setPlayedSongs(prev =\u003e {\n      const newSet = new Set([...prev, currentSong.id]);\n      \n      // Limit size to prevent unbounded growth\n      if (newSet.size \u003e RECENTLY_PLAYED_LIMIT * 2) {\n        const array = Array.from(newSet);\n        const newArray = array.slice(-RECENTLY_PLAYED_LIMIT);\n        return new Set(newArray);\n      }\n      \n      return newSet;\n    });\n  }\n}, [currentSong]);\n```\n\n---\n\n### Error Handling\n\n```typescript\nonError: (event: any) =\u003e {\n  console.error(\"YouTube player error:\", event);\n  setIsLoading(false);\n  \n  if (currentSong) {\n    // Show error toast\n    toast({\n      title: \"Error playing song\",\n      description: \"This song couldn't be played. Adding to end of queue and moving to next.\"\n    });\n    \n    // Add failed song to end of queue for retry\n    setQueue(prevQueue =\u003e [...prevQueue, currentSong]);\n    \n    // Play next song\n    nextSong();\n  } else if (previousVideoData) {\n    // Revert to previous video\n    setCurrentSong(previousVideoData);\n    event.target.loadVideoById(previousVideoData.youtube);\n  } else {\n    setVideoTitle(\"Error loading video\");\n    setChannelTitle(\"Unknown\");\n  }\n}\n```\n\n---\n\n### Usage in Components\n\n#### Accessing the Player Context\n\n```typescript\nimport { useGlobalAudioPlayer } from '@/components/GlobalAudioPlayer';\n\nconst MyComponent = () =\u003e {\n  const { playNow, addToQueue, isPlaying, togglePlay } = useGlobalAudioPlayer();\n  \n  const handlePlay = () =\u003e {\n    playNow({\n      id: 'song-123',\n      youtube: 'dQw4w9WgXcQ',\n      title: 'Song Title',\n      artist: 'Artist Name'\n    });\n  };\n  \n  return \u003cbutton onClick={handlePlay}\u003ePlay\u003c/button\u003e;\n};\n```\n\n#### Provider Setup (App.tsx)\n\n```typescript\nimport { GlobalAudioPlayerProvider } from '@/components/GlobalAudioPlayer';\n\nfunction App() {\n  return (\n    \u003cGlobalAudioPlayerProvider\u003e\n      \u003cRoutes\u003e\n        {/* ... routes */}\n      \u003c/Routes\u003e\n    \u003c/GlobalAudioPlayerProvider\u003e\n  );\n}\n```\n\n---\n\n### Thumbnail Handling\n\nYouTube thumbnails are fetched using:\n\n```typescript\nconst getVideoThumbnail = (videoId: string) =\u003e {\n  return `https://img.youtube.com/vi/${videoId}/default.jpg`;\n};\n```\n\nFallback on error:\n\n```typescript\n\u003cimg \n  src={getVideoThumbnail(videoId)}\n  onError={(e) =\u003e {\n    e.currentTarget.src = \"/AfrobeatsDAOMeta.png\";\n  }}\n/\u003e\n```\n\n---\n\n### Dependencies\n\n```json\n{\n  \"react-beautiful-dnd\": \"^13.1.1\",  // Drag \u0026 drop\n  \"lucide-react\": \"^0.462.0\",         // Icons\n  \"@radix-ui/react-slider\": \"...\",    // Slider component (via shadcn)\n  \"@radix-ui/react-tabs\": \"...\",      // Tabs component (via shadcn)\n  \"@radix-ui/react-scroll-area\": \"...\", // Scroll area (via shadcn)\n  \"@radix-ui/react-avatar\": \"...\"     // Avatar component (via shadcn)\n}\n```\n\n---\n\n### TypeScript Global Declaration\n\n```typescript\ndeclare global {\n  interface Window {\n    onYouTubeIframeAPIReady: () =\u003e void;\n    YT: any;\n  }\n}\n```\n\n---\n\n## How to Edit This Code\n\n### Use Lovable\n\nSimply visit the [Lovable Project](https://lovable.dev/projects/d16edf1f-4126-499a-89e7-db99f81ad1c2) and start prompting.\n\n### Use Your Preferred IDE\n\n```sh\n# Clone the repository\ngit clone \u003cYOUR_GIT_URL\u003e\n\n# Navigate to the project directory\ncd \u003cYOUR_PROJECT_NAME\u003e\n\n# Install dependencies\nnpm i\n\n# Start the development server\nnpm run dev\n```\n\n---\n\n## Deployment\n\nOpen [Lovable](https://lovable.dev/projects/d16edf1f-4126-499a-89e7-db99f81ad1c2) and click on Share → Publish.\n\n### Custom Domain\n\nNavigate to Project \u003e Settings \u003e Domains and click Connect Domain.\n\nRead more: [Setting up a custom domain](https://docs.lovable.dev/tips-tricks/custom-domain#step-by-step-guide)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodingshot%2Fafrobeatsdao","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcodingshot%2Fafrobeatsdao","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodingshot%2Fafrobeatsdao/lists"}