26 KiB
NoGood Radio Integration - Background Music Player
Concept
Add a persistent music player that plays NoGood Radio while users browse the app on desktop. Think of it like Spotify's mini-player, but for skate culture radio.
How It Would Look (Desktop)
Option 1: Bottom Bar Player (Spotify-style) ✅ RECOMMENDED
┌──────────────────────────────────────────────────────┐
│ Regular App Content │
│ (Map, Spots, Events, etc.) │
│ │
│ │
│ │
└──────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────┐
│ 🎵 NoGood Radio 🔊──── ▶️ ⏸️│
│ Now Playing: Lo-Fi Skate Beats │
└──────────────────────────────────────────────────────┘
↑
Persistent player bar (always visible)
Layout Details:
┌───────────────────────────────────────────────────────────┐
│ 🎧 NoGood Radio | Now Playing: Chill Skate Mix | 🔊──────── | ▶️ ⏸️ 🔇 ✕ │
└───────────────────────────────────────────────────────────┘
↑ ↑ ↑ ↑
Logo/Icon Track info Volume Controls
Features:
- Sticks to bottom of screen
- Plays while navigating between pages
- Volume slider
- Play/Pause button
- Minimize/Close button
- Shows current track/show name
- Doesn't interrupt browsing
Option 2: Floating Mini Player (Draggable)
Regular App Content
┌──────────────────────┐
│ 🎵 NoGood Radio │
│ ▶️ Lo-Fi Beats │
│ 🔊────── [–] [✕] │
└──────────────────────┘
↑
Draggable, can be moved anywhere
Features:
- Floats above content
- User can drag to preferred position
- Minimizes to small icon
- Expands on hover
- Less obtrusive than bottom bar
Option 3: Sidebar Docked Player
┌────────┬─────────────────────────────────┐
│ Nav │ Main Content │
│ Links │ (Spots, Events, etc.) │
│ │ │
│ ────── │ │
│ │ │
│ 🎵 ▶️ │ │
│ NoGood │ │
│ Radio │ │
│ │ │
│ 🔊──── │ │
└────────┴─────────────────────────────────┘
↑
Docked in sidebar (desktop only)
Features:
- Always visible in navigation
- Compact vertical design
- Doesn't cover content
- Desktop-only (sidebar space)
Detailed UI/UX Design (Option 1 - Bottom Bar)
Collapsed State (Default)
┌─────────────────────────────────────────────────────────────────┐
│ 🎧 NoGood Radio • Now Playing: Sunset Session Mix 🔊──── ▶️ [–]│
└─────────────────────────────────────────────────────────────────┘
Elements:
- Logo: 🎧 or NoGood brand icon
- Station name: "NoGood Radio"
- Track info: Scrolling text if too long
- Volume slider: 🔊──────
- Play/Pause: ▶️ / ⏸️
- Minimize: [–] (collapses to icon only)
Colors:
- Background: Dark gradient (matches app theme)
- Accent:
#ccff00(brand yellow) - Text: White/gray
Minimized State (When User Clicks [–])
┌──────────────────┐
│ 🎧 ▶️ 🔊──── [+]│
└──────────────────┘
↑
Small bar, bottom-right
Elements:
- Just icon, play button, volume
- Expand button [+] to restore
- Stays out of the way
Expanded State (When User Clicks for More Info)
┌─────────────────────────────────────────────────────┐
│ 🎧 NoGood Radio [–] [✕]│
│ │
│ Now Playing: │
│ 🎵 Sunset Session Mix - Track 04 │
│ │
│ ▶️ ⏸️ | 🔊──────────── | 12:34 / 45:00 │
│ │
│ 📻 About NoGood Radio: │
│ Curated skate culture music 24/7 │
│ │
│ ⚡ Support NoGood │
│ [Zap 100] [Zap 500] [Zap 1000] │
│ │
│ 🔗 [Visit zap.stream] [Share Radio] │
└─────────────────────────────────────────────────────┘
Height: ~250px (slides up from bottom) Features:
- Show more info about current track
- Zap button to support NoGood
- Links to their platform
- About section
Mobile Experience
Recommendation: Don't show on mobile
Why:
- Mobile users often have their own music playing
- Takes up valuable screen space
- Audio conflicts with phone calls
- Data usage concerns
- Better UX to keep mobile clean
How to detect:
const isDesktop = window.innerWidth > 768;
// Only render player if isDesktop === true
Technical Implementation
Component Structure
App.tsx
└─ Layout.tsx
└─ AppRouter (pages)
└─ RadioPlayer.tsx (NEW - conditional, desktop only)
RadioPlayer Component
import { useState, useRef, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Slider } from '@/components/ui/slider';
import { Play, Pause, Volume2, VolumeX, Minimize2, Maximize2, X } from 'lucide-react';
export function RadioPlayer() {
const [isPlaying, setIsPlaying] = useState(false);
const [volume, setVolume] = useState(0.7);
const [isMinimized, setIsMinimized] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const [isMuted, setIsMuted] = useState(false);
const audioRef = useRef<HTMLAudioElement>(null);
// Stream URL (NoGood Radio on zap.stream)
const streamUrl = "https://stream.zap.stream/nogoodradio";
// Only show on desktop
const isDesktop = window.innerWidth > 768;
if (!isDesktop) return null;
// Audio controls
const togglePlay = () => {
if (audioRef.current) {
if (isPlaying) {
audioRef.current.pause();
} else {
audioRef.current.play();
}
setIsPlaying(!isPlaying);
}
};
const handleVolumeChange = (value: number[]) => {
const newVolume = value[0];
setVolume(newVolume);
if (audioRef.current) {
audioRef.current.volume = newVolume;
}
setIsMuted(newVolume === 0);
};
const toggleMute = () => {
if (audioRef.current) {
audioRef.current.muted = !isMuted;
setIsMuted(!isMuted);
}
};
return (
<>
{/* Audio element (hidden) */}
<audio ref={audioRef} src={streamUrl} preload="none" />
{/* Player UI */}
{!isMinimized ? (
<div className="fixed bottom-0 left-0 right-0 z-50 bg-gradient-to-r from-black/95 to-gray-900/95 backdrop-blur border-t-2 border-[#ccff00]/30">
<div className="container px-4 py-3 flex items-center gap-4">
{/* Logo & Info */}
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="text-2xl">🎧</div>
<div className="min-w-0">
<div className="font-bold text-sm">NoGood Radio</div>
<div className="text-xs text-gray-400 truncate">
Now Playing: Lo-Fi Skate Beats
</div>
</div>
</div>
{/* Controls */}
<div className="flex items-center gap-3">
{/* Volume */}
<div className="hidden sm:flex items-center gap-2">
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={toggleMute}
>
{isMuted ? <VolumeX className="h-4 w-4" /> : <Volume2 className="h-4 w-4" />}
</Button>
<Slider
value={[volume]}
min={0}
max={1}
step={0.01}
onValueChange={handleVolumeChange}
className="w-24"
/>
</div>
{/* Play/Pause */}
<Button
variant="outline"
size="icon"
className="h-8 w-8 bg-[#ccff00] hover:bg-[#ccff00]/90 text-black border-[#ccff00]"
onClick={togglePlay}
>
{isPlaying ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
</Button>
{/* Minimize */}
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => setIsMinimized(true)}
>
<Minimize2 className="h-4 w-4" />
</Button>
{/* Close */}
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => {/* close player */}}
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
</div>
) : (
// Minimized version (bottom-right)
<div className="fixed bottom-4 right-4 z-50">
<Button
className="flex items-center gap-2 bg-gradient-to-r from-black/95 to-gray-900/95 backdrop-blur border-2 border-[#ccff00]/30 hover:border-[#ccff00]"
onClick={() => setIsMinimized(false)}
>
<span className="text-xl">🎧</span>
{isPlaying ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
<span className="text-xs">Radio</span>
</Button>
</div>
)}
</>
);
}
Visual Mockups
Default State (Bottom Bar)
┌──────────────────────────────────────────────────────────────────┐
│ │
│ Regular App Content │
│ (Map, Spots, Events, etc.) │
│ │
│ │
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ 🎧 NoGood Radio │
│ Now Playing: Lo-Fi Skate Beats Mix 🔊──── ▶️ [–] [✕] │
└──────────────────────────────────────────────────────────────────┘
↑
Always visible at bottom
Plays while you navigate
Key Features:
- Height: ~60px (minimal intrusion)
- Dark background with yellow accent
- Sticky positioning (scrolls with page)
- Persistent across page navigation
- Desktop only (hidden on mobile)
Minimized State (Small Floating Button)
┌──────────┐
│ 🎧 ▶️ │
│ Radio │
└──────────┘
↑
Bottom-right corner
Click to expand
When to use:
- User clicks minimize button
- Saves screen space
- Still shows play/pause
- Click to restore full bar
Expanded State (More Info)
┌──────────────────────────────────────────────────────────────────┐
│ Regular App Content │
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ 🎧 NoGood Radio [–] [✕] │
│ │
│ Now Playing: Lo-Fi Skate Beats Mix - Track 04 │
│ │
│ ▶️ ⏸️ 🔊────────────── │
│ │
│ 📻 About NoGood Radio │
│ 24/7 curated music for skate culture. Beats, hip-hop, punk, │
│ and everything that makes you want to push. │
│ │
│ ⚡ Support NoGood Radio │
│ [Zap 100 sats] [Zap 500 sats] [Zap 1000 sats] │
│ │
│ 🔗 [Visit zap.stream] [Share Radio] │
└──────────────────────────────────────────────────────────────────┘
↑
Slides up to ~250px when clicked
Shows more details and zap buttons
When to show:
- User clicks on player bar
- Shows description
- Zap buttons to support NoGood
- Links to their platform
User Experience Flow
First Visit (Not Playing)
User opens app
↓
Sees bottom bar: "🎧 NoGood Radio [▶️ Play]"
↓
Clicks Play button
↓
Music starts streaming
↓
Continues while browsing
↓
User browses spots, map, events
↓
Music keeps playing (uninterrupted)
Returning Visit (Remembers Preference)
User opens app
↓
If they were playing last time:
→ Auto-starts playing
If they closed it last time:
→ Shows collapsed bar
→ User can opt-in to play
Technical Details
Stream Source
NoGood Radio streams on zap.stream:
- Platform: https://zap.stream
- Profile: NoGood (@nogood.store)
- Stream format: HLS (HTTP Live Streaming) or Icecast
- Accessible via: Direct stream URL or iframe embed
How to find stream URL:
- Query Nostr for NoGood's kind:30311 events (Live Streaming Event)
- Extract
streamingtag with URL - Use that URL in HTML5 audio player
Fallback: If live stream not available, use:
- Pre-recorded NoGood mixes from SoundCloud
- NoGood's Spotify playlists
- Or disable player gracefully
Audio Player Implementation
// Simple HTML5 audio approach
<audio
ref={audioRef}
src="https://stream.zap.stream/nogoodradio" // Example URL
preload="none"
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
onError={(e) => handleError(e)}
/>
Browser Support:
- ✅ Chrome, Firefox, Safari, Edge
- ✅ HLS support (most modern browsers)
- ✅ Fallback for older browsers
State Management
What to Store in localStorage
{
"radio-enabled": true, // User wants radio available
"radio-autoplay": false, // Auto-start on load
"radio-volume": 0.7, // Volume level (0-1)
"radio-minimized": false, // UI state
"radio-last-played": 1234567890 // Timestamp
}
Behavior:
- Remember if user closed radio permanently (don't show again)
- Remember volume preference
- Remember minimized state
- Optional: Remember last position (if draggable)
Design Variants
Variant A: Minimal (Clean & Simple)
┌──────────────────────────────────────────────────┐
│ 🎧 NoGood Radio • Lo-Fi Mix 🔊─── ▶️ ✕ │
└──────────────────────────────────────────────────┘
Pros:
- Takes minimal space
- Clean and unobtrusive
- Fast to implement
Cons:
- Less engaging
- No zap integration
Variant B: Branded (Full Featured) ✅ RECOMMENDED
┌─────────────────────────────────────────────────────────────┐
│ ┌───┐ NoGood Radio │
│ │🎧 │ Now Playing: Skate Mix Vol. 3 🔊──── ▶️ ⚡ [–] [✕]│
│ └───┘ Curated skate culture music ^^^ │
└─────────────────────────────────────────────────────────────┘
Zap button!
Pros:
- Shows branding (NoGood logo/icon)
- Integrated zap support
- Subtitle text for context
- More engaging
Cons:
- Slightly taller (~80px vs 60px)
Variant C: Card Style (Modern)
┌─────────────────────────────────────────────────────────────┐
│ ╔═══════════════════════════════════════════════════════╗ │
│ ║ 🎧 NoGood Radio [–] [✕] ║ │
│ ║ ║ │
│ ║ 🎵 Sunset Session Mix ║ │
│ ║ ▶️ ⏸️ 🔊────────── ⚡ Zap ║ │
│ ╚═══════════════════════════════════════════════════════╝ │
└─────────────────────────────────────────────────────────────┘
Pros:
- Looks like a component (card design)
- Polished appearance
- Clear boundaries
Cons:
- Takes more space (~100px)
Page-Specific Behavior
On Different Pages
Home Page (/):
- Show player
- Auto-collapse to minimize screen clutter
Map Page (/map):
- Show player
- Slightly transparent to not block map
Event Pages (/events, /event/:id):
- Show player
- If event has its own music, allow switching
Spot Pages (/spot/:id):
- Show player normally
- Unobtrusive
Settings (/settings):
- Hide player (less distraction)
Advanced Features (Future)
Track Metadata (If Available)
🎵 Now Playing:
Artist: Unknown Brain
Track: Skate or Die
Album: Street Sessions Vol. 2
Playlist Integration
📻 Currently playing from:
"NoGood's Weekend Mix" playlist
[View Full Playlist] →
Listener Count
🎧 NoGood Radio • 1,234 listeners worldwide
Share What You're Hearing
🔗 Share: "I'm listening to NoGood Radio on GoSkateSpots!"
[Share on Twitter] [Share on Nostr]
Implementation Steps
Step 1: Basic Player (3-4 hours)
- Create RadioPlayer component
- HTML5 audio element with stream URL
- Play/pause button
- Volume slider
- Close button
Step 2: UI Polish (2 hours)
- Add branding (NoGood logo/colors)
- Smooth animations
- Responsive sizing
- Loading states
Step 3: State Persistence (1 hour)
- localStorage integration
- Remember user preferences
- Auto-restore on page load
Step 4: Integration (1 hour)
- Add to Layout component
- Desktop-only logic
- Test across all pages
Total: 6-8 hours
Questions to Answer
1. Stream URL
Q: What's the actual NoGood Radio stream URL?
A: Need to query Nostr for their kind:30311 event, extract streaming tag
2. Permissions
Q: Does NoGood Radio allow embedding? A: Should check with NoGood or zap.stream ToS
3. Attribution
Q: How to credit NoGood? A: Show their name, link to their profile, enable zaps
4. Fallback
Q: What if stream is offline? A: Show "Radio offline" message or hide player
Cost Analysis
Development Cost
- Time: 6-8 hours
- Complexity: Low-Medium
- Maintenance: Minimal
Ongoing Costs
- Bandwidth: $0 (stream hosted by zap.stream)
- Storage: $0 (no files)
- API: $0 (just audio streaming)
User Bandwidth
- Audio quality: ~128kbps
- Per hour: ~60MB
- Consideration: Some users may have data caps
Recommended Design (Final)
Bottom Bar - Collapsed (Default)
┌────────────────────────────────────────────────────────────────────┐
│ Main App Content │
└────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────┐
│ 🎧 NoGood Radio • Lo-Fi Skate Mix 🔊────── ▶️ ⚡ [–] [✕]│
└────────────────────────────────────────────────────────────────────┘
Height: 60px
Background: bg-gradient-to-r from-black/95 to-gray-900/95 backdrop-blur
Border: border-t-2 border-[#ccff00]/30
Position: fixed bottom-0 left-0 right-0 z-50
Elements:
- 🎧 Icon - Visual identifier
- Station Name - "NoGood Radio"
- Track Info - "Lo-Fi Skate Mix" (scrolls if long)
- Volume - Slider (🔊────── )
- Play/Pause - Yellow button (brand color)
- Zap - Quick support button
- Minimize - [–] collapses to corner
- Close - [✕] hides completely
Interactions:
- Click bar → Expands to show more info
- Click Play → Starts streaming
- Drag volume → Adjusts sound
- Click Zap → Opens ZapDialog for NoGood
- Click Minimize → Collapses to floating button
- Click Close → Hides player (remember preference)
Bottom Line
What You Get:
- 🎵 Background music while browsing
- 🎧 NoGood Radio streaming 24/7
- 🖥️ Desktop only (respects mobile UX)
- ⚡ Zap integration (support NoGood)
- 🎨 Beautiful UI (matches app design)
- 💾 Remembers preferences (volume, state)
- 🔄 Persistent (plays across pages)
Implementation:
- Time: 6-8 hours
- Cost: $0
- Complexity: Low-Medium
- Maintenance: Minimal
The Vibe:
Imagine browsing skate spots with curated beats in the background. That's the feeling. It makes the app feel alive and creates a skate culture atmosphere.
Like walking into a skate shop with music playing - it just feels right. 🎵🛹
Would you like me to implement this?