An open API service indexing awesome lists of open source software.

https://github.com/codingshot/afrobeatsdao


https://github.com/codingshot/afrobeatsdao

afrobeats afrobeats-dance amapiano events-platform highlife music music-platform music-player

Last synced: 5 months ago
JSON representation

Awesome Lists containing this project

README

          

# AfrobeatsDAO

**URL**: https://lovable.dev/projects/d16edf1f-4126-499a-89e7-db99f81ad1c2

A comprehensive Afrobeats music platform featuring a global audio player, artist discovery, dance tutorials, event listings, and community features.

---

## Technology Stack

- **Framework**: React 18 with TypeScript
- **Build Tool**: Vite
- **Styling**: Tailwind CSS with shadcn/ui components
- **Routing**: React Router DOM v6
- **State Management**: React Context API
- **Audio**: YouTube IFrame API
- **Drag & Drop**: react-beautiful-dnd
- **Icons**: Lucide React
- **Animations**: Framer Motion

---

## Global Audio Player - Comprehensive Documentation

The 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.

### File Structure

```
src/components/
├── GlobalAudioPlayer.tsx # Main player component & context provider
├── QueueDrawer.tsx # Queue and history management drawer
├── MarkdownPreviewDialog.tsx # Export preview dialog
└── VibeOfTheDay.tsx # Contains VIBE_VIDEOS array for random playback
```

---

### Core Architecture

#### Context Provider Pattern

The player uses React Context to provide global state and controls accessible from any component in the application.

```typescript
// Context type definition
interface GlobalAudioPlayerContextType {
currentSong: Song | null;
queue: Song[];
isPlaying: boolean;
playNow: (song: Song) => void;
addToQueue: (song: Song) => void;
removeFromQueue: (songId: string) => void;
togglePlay: () => void;
nextSong: () => void;
previousSong: () => void;
setVolume: (value: number) => void;
toggleRepeat: () => void;
reorderQueue: (from: number, to: number) => void;
duration: number;
currentTime: number;
isDragging: boolean;
}
```

#### Song Interface

```typescript
interface Song {
id: string; // Unique identifier for the song
youtube: string; // YouTube video URL or video ID
title?: string; // Optional display title
artist?: string; // Optional artist name
}
```

---

### State Management

#### Local Storage Persistence

The player persists the following state to `localStorage`:

| Key | Purpose |
|-----|---------|
| `afrobeats_current_song` | Currently playing song object |
| `afrobeats_queue` | Array of songs in the queue |
| `afrobeats_volume` | Volume level (0-100) |
| `afrobeats_repeat` | Repeat mode boolean |
| `afrobeats_played_songs` | Array of recently played song IDs |
| `afrobeats_video_visible` | Video visibility state boolean |

#### State Variables

| State | Type | Default | Description |
|-------|------|---------|-------------|
| `player` | any | null | YouTube player instance |
| `currentSong` | Song \| null | null | Currently playing song |
| `queue` | Song[] | [] | Queue of upcoming songs |
| `isPlaying` | boolean | false | Playback state |
| `volume` | number | 100 | Volume level (0-100) |
| `repeat` | boolean | false | Repeat mode |
| `youtubeApiLoaded` | boolean | false | YouTube API load state |
| `expandedView` | boolean | true | Player view mode |
| `videoTitle` | string | "Loading..." | Current video title from YouTube |
| `channelTitle` | string | "Loading..." | Channel name from YouTube |
| `previousVideoData` | Song \| null | null | Previous song for error recovery |
| `isMobile` | boolean | (detected) | Mobile device detection |
| `videoVisible` | boolean | false | Video player visibility |
| `duration` | number | 0 | Song duration in seconds |
| `currentTime` | number | 0 | Current playback position |
| `isDragging` | boolean | false | Seek slider drag state |
| `isLoading` | boolean | false | Loading state |
| `loadingTitle` | string | "Loading..." | Title shown during load |
| `queueVisible` | boolean | false | Queue drawer visibility |
| `showPlayedSongs` | boolean | false | Filter played songs in queue |
| `playedSongs` | Set\ | new Set() | Set of played song IDs |
| `isInitialLoad` | boolean | true | Initial load flag |
| `thumbnailUrl` | string | "" | YouTube thumbnail URL |
| `showVolumeSlider` | boolean | false | Volume popup visibility (mobile) |

---

### Core Functions

#### `playNow(song: Song)`

Immediately plays a song, replacing the current track.

**Behavior:**
1. Sets loading state to true
2. Stores previous song for error recovery
3. Updates `currentSong` state
4. Sets `isPlaying` to true
5. Extracts video ID from YouTube URL
6. Calls `player.loadVideoById(videoId)`
7. Updates thumbnail URL

#### `addToQueue(song: Song)`

Adds a song to the end of the playback queue.

```typescript
const addToQueue = useCallback((song: Song) => {
setQueue(prev => [...prev, song]);
}, []);
```

#### `removeFromQueue(songId: string)`

Removes a specific song from the queue by its ID.

```typescript
const removeFromQueue = useCallback((songId: string) => {
setQueue(prev => prev.filter(song => song.id !== songId));
}, []);
```

#### `togglePlay()`

Toggles between play and pause states.

**Behavior:**
- If playing → calls `player.pauseVideo()`
- If paused → calls `player.playVideo()`
- Updates `isPlaying` state

#### `nextSong()`

Advances to the next song.

**Behavior:**
1. If queue has songs → plays first song in queue, removes from queue
2. If queue is empty → calls `findUnplayedSong()` to get a random vibe video

#### `previousSong()`

Seeks to the beginning of the current song.

```typescript
const previousSong = useCallback(() => {
if (player) {
player.seekTo(0);
}
}, [player]);
```

#### `updateVolume(value: number)`

Sets the playback volume.

```typescript
const updateVolume = useCallback((value: number) => {
if (player) {
player.setVolume(value);
setVolume(value);
}
}, [player]);
```

#### `toggleRepeat()`

Toggles repeat mode on/off.

#### `reorderQueue(from: number, to: number)`

Reorders the queue via drag and drop.

```typescript
const reorderQueue = useCallback((from: number, to: number) => {
setQueue(prev => {
const newQueue = [...prev];
const [removed] = newQueue.splice(from, 1);
newQueue.splice(to, 0, removed);
return newQueue;
});
}, []);
```

#### `toggleVideo()`

Toggles YouTube video player visibility.

#### `toggleExpandedView()`

Toggles between expanded and collapsed player views.

#### `toggleQueueVisibility()`

Shows/hides the queue drawer.

---

### Helper Functions

#### `getVideoId(youtube: string)`

Extracts video ID from various YouTube URL formats.

**Supported formats:**
- `https://www.youtube.com/watch?v=VIDEO_ID`
- `https://youtu.be/VIDEO_ID`
- Direct video ID string

```typescript
const getVideoId = useCallback((youtube: string): string => {
if (youtube.includes('v=')) {
return youtube.split('v=')[1].split('&')[0];
} else if (youtube.includes('youtu.be/')) {
return youtube.split('youtu.be/')[1].split('?')[0];
}
return youtube;
}, []);
```

#### `formatTime(seconds: number)`

Formats seconds into `MM:SS` display format.

```typescript
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
```

#### `getRandomVibeVideo(excludeId?: string)`

Returns a random video from `VIBE_VIDEOS` array, optionally excluding a specific ID.

```typescript
const getRandomVibeVideo = useCallback((excludeId?: string) => {
const availableVideos = VIBE_VIDEOS.filter(id => id !== excludeId);
const randomIndex = Math.floor(Math.random() * availableVideos.length);
return availableVideos[randomIndex];
}, []);
```

#### `findUnplayedSong(currentId: string | undefined)`

Intelligently finds the next song to play.

**Algorithm:**
1. First, searches queue for unplayed songs
2. If all queue songs played, gets random vibe video
3. Tries up to 5 times to avoid recently played videos
4. Returns a Song object

---

### YouTube Integration

#### API Loading

The player dynamically loads the YouTube IFrame API:

```typescript
useEffect(() => {
if (!window.YT && !document.getElementById('youtube-iframe-api')) {
const tag = document.createElement('script');
tag.id = 'youtube-iframe-api';
tag.src = 'https://www.youtube.com/iframe_api';
const firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode?.insertBefore(tag, firstScriptTag);
window.onYouTubeIframeAPIReady = () => {
setYoutubeApiLoaded(true);
};
} else if (window.YT) {
setYoutubeApiLoaded(true);
}
}, []);
```

#### Player Initialization

```typescript
const newPlayer = new window.YT.Player('youtube-player', {
height: '240',
width: '426',
playerVars: {
playsinline: 1,
controls: 1
},
events: {
onStateChange: handleStateChange,
onError: handleError,
onReady: handleReady
}
});
```

#### Event Handlers

**onStateChange:**
- `ENDED` → Plays next song or repeats current
- `PLAYING` → Updates metadata, sets duration
- `PAUSED` → Updates isPlaying state
- `BUFFERING` → Sets loading state

**onError:**
- Logs error to console
- Shows toast notification
- Adds failed song to end of queue
- Plays next song
- Falls back to previous video if available

**onReady:**
- Sets initial volume
- Loads saved current song if available

---

### Media Session API Integration

The player integrates with the browser's Media Session API for background playback control:

```typescript
useEffect(() => {
if ('mediaSession' in navigator && currentSong) {
navigator.mediaSession.metadata = new MediaMetadata({
title: videoTitle || 'Unknown Title',
artist: channelTitle || 'Unknown Artist',
album: 'Afrobeats Player',
artwork: [{
src: thumbnailUrl || '/AfrobeatsDAOMeta.png',
sizes: '128x128',
type: 'image/png'
}]
});

navigator.mediaSession.setActionHandler('play', togglePlay);
navigator.mediaSession.setActionHandler('pause', togglePlay);
navigator.mediaSession.setActionHandler('previoustrack', previousSong);
navigator.mediaSession.setActionHandler('nexttrack', nextSong);
}
}, [currentSong, videoTitle, channelTitle, thumbnailUrl]);
```

---

### UI Layout

#### Positioning

The player is fixed to the bottom of the viewport:

```css
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 150;
```

#### Color Scheme

| Element | Color |
|---------|-------|
| Background | `bg-black/95` (95% opacity black) |
| Border | `border-white/10` (10% opacity white) |
| Text | `text-white` |
| Accent | `#FFD600` (Golden yellow) |
| Secondary text | `text-gray-400` |

#### Desktop Layout (3-column)

```
┌─────────────────────────────────────────────────────────────────────┐
│ [Thumbnail] Title [<<][▶][>>][🔁] [Queue][Video][🔊]━━━ │
│ Channel │
├─────────────────────────────────────────────────────────────────────┤
│ 0:00 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3:45 │
└─────────────────────────────────────────────────────────────────────┘
```

**Column 1 (Left):** Song info with thumbnail, title, channel
**Column 2 (Center):** Playback controls (Previous, Play/Pause, Next, Repeat)
**Column 3 (Right):** Queue toggle, Video toggle, Volume controls

#### Mobile Layout (Vertical Stack)

```
┌─────────────────────────────────────────────┐
│ [Thumbnail] Title [Queue]│
│ Channel │
├─────────────────────────────────────────────┤
│ 0:00 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3:45 │
├─────────────────────────────────────────────┤
│ [<<] [▶] [>>] [🔁] [🔊] [📹] │
└─────────────────────────────────────────────┘
```

**Row 1:** Song info + Queue button
**Row 2:** Time slider with current/total time
**Row 3:** All controls centered

---

### Control Buttons

| Icon | Component | Action |
|------|-----------|--------|
| `` | Previous | Seeks to start of song |
| `` / `` | Play/Pause | Toggle playback |
| `` | Next | Play next song |
| `` / `` | Repeat | Toggle repeat mode |
| `` / `` | Queue | Toggle queue drawer |
| `` / `` | Video | Toggle video visibility |
| `` / `` | Volume | Mute/unmute or show slider |

#### Active State Styling

Active controls use accent color: `text-[#FFD600]`

---

### Volume Control

#### Desktop

Horizontal slider inline with controls:

```typescript


updateVolume(value)}
/>

```

#### Mobile

Vertical popup slider on hover/tap:

```typescript


setShowVolumeSlider(true)}
onMouseLeave={() => setShowVolumeSlider(false)}
onClick={() => setVolume(volume === 0 ? 100 : 0)}
>
{volume === 0 ? : }

{showVolumeSlider && (



)}

```

---

### Time/Seek Slider

#### Functionality

- Displays current position and total duration
- Drag to seek (sets `isDragging` state)
- Updates on release via `onValueCommit`

```typescript
{
setCurrentTime(value);
setIsDragging(true);
}}
onValueCommit={([value]) => {
handleTimeChange(value);
setIsDragging(false);
}}
/>
```

#### Time Update Loop

```typescript
useEffect(() => {
if (!player || !isPlaying || isDragging) return;

const interval = setInterval(() => {
if (player.getCurrentTime) {
setCurrentTime(player.getCurrentTime());
}
}, 1000);

return () => clearInterval(interval);
}, [player, isPlaying, isDragging]);
```

---

### Video Player Container

The YouTube video is rendered in a positioned container:

```typescript




```

**Desktop positioning:** Bottom right, above player bar
**Mobile positioning:** Full width with margins, above player bar

---

### Empty State

When no song is playing:

```typescript




Afrobeats Player

{
const defaultVideo = getRandomVibeVideo();
playNow({
id: `default-vibe-${defaultVideo}`,
youtube: defaultVideo,
title: "Random Vibe"
});
}}
className="bg-[#FFD600] text-black hover:bg-[#FFD600]/90"
>

Play Something


```

---

### Queue Drawer Component

Location: `src/components/QueueDrawer.tsx`

#### Props

```typescript
interface QueueDrawerProps {
queue: Song[];
isVisible: boolean;
playNow: (song: Song) => void;
reorderQueue: (from: number, to: number) => void;
playedSongs: Set;
showPlayedSongs: boolean;
setShowPlayedSongs: (show: boolean) => void;
}
```

#### Features

1. **Tabs:** Queue / History switching
2. **Drag & Drop:** Reorder queue items
3. **Minimizable:** Collapse to small header
4. **Export:** Download queue/history as markdown

#### Layout

```
┌─────────────────────────────────────┐
│ [Queue] [History] [—] │
├─────────────────────────────────────┤
│ │
│ [≡] [Thumb] Title [Played]│
│ Artist │
│ │
│ [≡] [Thumb] Title │
│ Artist │
│ │
│ [≡] [Thumb] Title │
│ Artist │
│ │
├─────────────────────────────────────┤
│ [📥 Export Queue] │
└─────────────────────────────────────┘
```

#### Positioning

```css
position: fixed;
right: 4 (1rem);
bottom: 80px;
width: 350px;
z-index: 40;
```

#### Drag & Drop Implementation

Uses `react-beautiful-dnd`:

```typescript


{(provided) => (


{filteredQueue.map((song, index) => (

{(provided) => (




{/* Song content */}

)}

))}
{provided.placeholder}

)}

```

#### Queue Item

```typescript


{/* Drag handle */}




{/* Thumbnail with play overlay */}


playNow(song)}
>




{/* Song info */}


{song.title || "Title of video"}



{song.artist || "Unknown Artist"}




{/* Played badge */}
{playedSongs.has(song.id) && (
Played
)}

```

#### History Tab

Displays recently played songs (without drag & drop):

```typescript
{playedSongsList.map((song, index) => (


{/* Same structure as queue item, without drag handle */}

))}
```

#### Export Functionality

Generates markdown content for queue or history:

```typescript
const generateMarkdownContent = (tab: TabType) => {
let content = "# Afrobeats Music History\n\n";

if (tab === "queue") {
content += "## Current Queue\n\n";
filteredQueue.forEach((song, index) => {
content += `${index + 1}. **${song.title}** - ${song.artist}\n`;
content += ` - [Watch Video](https://www.youtube.com/watch?v=${videoId})\n\n`;
});
}
// ... similar for history

content += `Exported on ${new Date().toLocaleString()}\n`;
return content;
};
```

Download function:

```typescript
const handleDownload = () => {
const blob = new Blob([markdownContent], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = "afrobeats-queue.md";
a.click();
URL.revokeObjectURL(url);
};
```

#### Empty States

**Queue empty:**
```
[ListMusic icon]
Your queue is empty
Add songs from playlists
```

**History empty:**
```
[ListMusic icon]
No play history yet
Songs will appear here after playing
```

---

### Recently Played Tracking

The player tracks recently played songs to avoid repetition:

```typescript
const RECENTLY_PLAYED_LIMIT = 10;

useEffect(() => {
if (currentSong?.id) {
setPlayedSongs(prev => {
const newSet = new Set([...prev, currentSong.id]);

// Limit size to prevent unbounded growth
if (newSet.size > RECENTLY_PLAYED_LIMIT * 2) {
const array = Array.from(newSet);
const newArray = array.slice(-RECENTLY_PLAYED_LIMIT);
return new Set(newArray);
}

return newSet;
});
}
}, [currentSong]);
```

---

### Error Handling

```typescript
onError: (event: any) => {
console.error("YouTube player error:", event);
setIsLoading(false);

if (currentSong) {
// Show error toast
toast({
title: "Error playing song",
description: "This song couldn't be played. Adding to end of queue and moving to next."
});

// Add failed song to end of queue for retry
setQueue(prevQueue => [...prevQueue, currentSong]);

// Play next song
nextSong();
} else if (previousVideoData) {
// Revert to previous video
setCurrentSong(previousVideoData);
event.target.loadVideoById(previousVideoData.youtube);
} else {
setVideoTitle("Error loading video");
setChannelTitle("Unknown");
}
}
```

---

### Usage in Components

#### Accessing the Player Context

```typescript
import { useGlobalAudioPlayer } from '@/components/GlobalAudioPlayer';

const MyComponent = () => {
const { playNow, addToQueue, isPlaying, togglePlay } = useGlobalAudioPlayer();

const handlePlay = () => {
playNow({
id: 'song-123',
youtube: 'dQw4w9WgXcQ',
title: 'Song Title',
artist: 'Artist Name'
});
};

return Play;
};
```

#### Provider Setup (App.tsx)

```typescript
import { GlobalAudioPlayerProvider } from '@/components/GlobalAudioPlayer';

function App() {
return (


{/* ... routes */}


);
}
```

---

### Thumbnail Handling

YouTube thumbnails are fetched using:

```typescript
const getVideoThumbnail = (videoId: string) => {
return `https://img.youtube.com/vi/${videoId}/default.jpg`;
};
```

Fallback on error:

```typescript
{
e.currentTarget.src = "/AfrobeatsDAOMeta.png";
}}
/>
```

---

### Dependencies

```json
{
"react-beautiful-dnd": "^13.1.1", // Drag & drop
"lucide-react": "^0.462.0", // Icons
"@radix-ui/react-slider": "...", // Slider component (via shadcn)
"@radix-ui/react-tabs": "...", // Tabs component (via shadcn)
"@radix-ui/react-scroll-area": "...", // Scroll area (via shadcn)
"@radix-ui/react-avatar": "..." // Avatar component (via shadcn)
}
```

---

### TypeScript Global Declaration

```typescript
declare global {
interface Window {
onYouTubeIframeAPIReady: () => void;
YT: any;
}
}
```

---

## How to Edit This Code

### Use Lovable

Simply visit the [Lovable Project](https://lovable.dev/projects/d16edf1f-4126-499a-89e7-db99f81ad1c2) and start prompting.

### Use Your Preferred IDE

```sh
# Clone the repository
git clone

# Navigate to the project directory
cd

# Install dependencies
npm i

# Start the development server
npm run dev
```

---

## Deployment

Open [Lovable](https://lovable.dev/projects/d16edf1f-4126-499a-89e7-db99f81ad1c2) and click on Share → Publish.

### Custom Domain

Navigate to Project > Settings > Domains and click Connect Domain.

Read more: [Setting up a custom domain](https://docs.lovable.dev/tips-tricks/custom-domain#step-by-step-guide)