diff --git a/.gitignore b/.gitignore index e7d733d..6f9b8e5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ data fetch-and-convert.sh deploy.sh hugo.toml +content/playlists diff --git a/fetch-playlist.sh b/fetch-playlist.sh new file mode 100755 index 0000000..da8e21b --- /dev/null +++ b/fetch-playlist.sh @@ -0,0 +1,129 @@ +#!/bin/bash + +# ============================================ +# Nostr Playlist Fetcher for Hugo +# ============================================ + +# Configuration +PUBKEY="3f11abb2e235da2d4dda5d6deb2f123173476a745a3ca56895a1d0f632a42f40" +RELAY="wss://drops.basspistol.org" +DATA_DIR="data/playlists" +RAW_DIR="data/playlists/raw" + +mkdir -p "$DATA_DIR" "$RAW_DIR" + +log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"; } + +# ============================================ +# Step 1: Fetch playlists and tracks +# ============================================ + +log "Fetching playlist events (kind 34139) from $RELAY..." +nak req -k 34139 -a "$PUBKEY" "$RELAY" > "$RAW_DIR/playlists-raw.jsonl" + +log "Fetching ALL tracks (kind 36787) from $PUBKEY..." +nak req -k 36787 -a "$PUBKEY" "$RELAY" > "$RAW_DIR/tracks-raw.jsonl" + +# ============================================ +# Step 2: Convert JSONL to JSON arrays +# ============================================ + +log "Converting raw data to JSON arrays..." + +# Convert jsonl to json arrays +cat "$RAW_DIR/playlists-raw.jsonl" | jq -s '.' > "$RAW_DIR/playlists-raw.json" +cat "$RAW_DIR/tracks-raw.jsonl" | jq -s '.' > "$RAW_DIR/tracks-raw.json" + +# ============================================ +# Step 3: Build playlist data with jq +# ============================================ + +log "Building playlist data structure..." + +# Build the playlists.json using the json files +jq -n \ + --slurpfile playlists "$RAW_DIR/playlists-raw.json" \ + --slurpfile tracks "$RAW_DIR/tracks-raw.json" \ + '{ + playlists: [ + $playlists[] | .[] | + select(.kind == 34139) | + { + id: .id, + created_at: .created_at, + name: (.tags[] | select(.[0] == "title") | .[1] // ""), + description: (.tags[] | select(.[0] == "description") | .[1] // ""), + image: (.tags[] | select(.[0] == "image") | .[1] // ""), + tracks: [ + .tags[] | + select(.[0] == "a") | .[1] as $ref | + $tracks[] | .[] | + select( + ($ref | split(":")[2]) == (.tags[] | select(.[0] == "d") | .[1]) + ) | + { + id: .id, + created_at: .created_at, + title: (.tags[] | select(.[0] == "title") | .[1] // "Untitled"), + artist: (.tags[] | select(.[0] == "artist") | .[1] // "Unknown Artist"), + album: (.tags[] | select(.[0] == "album") | .[1] // ""), + url: (.tags[] | select(.[0] == "url") | .[1] // ""), + image: (.tags[] | select(.[0] == "image") | .[1] // ""), + duration: (.tags[] | select(.[0] == "duration") | .[1] // ""), + tags: [.tags[] | select(.[0] == "t") | .[1]] + } + ] + } + ] + }' > "$DATA_DIR/playlists.json" + +# ============================================ +# Step 4: Generate flattened tracks file +# ============================================ + +log "Generating flattened tracks file..." + +cat "$RAW_DIR/tracks-raw.json" | jq ' + .[] | { + id: .id, + created_at: .created_at, + title: (.tags[] | select(.[0] == "title") | .[1] // "Untitled"), + artist: (.tags[] | select(.[0] == "artist") | .[1] // "Unknown Artist"), + album: (.tags[] | select(.[0] == "album") | .[1] // ""), + url: (.tags[] | select(.[0] == "url") | .[1] // ""), + image: (.tags[] | select(.[0] == "image") | .[1] // ""), + duration: (.tags[] | select(.[0] == "duration") | .[1] // ""), + tags: [.tags[] | select(.[0] == "t") | .[1]], + d_tag: (.tags[] | select(.[0] == "d") | .[1] // "") + } +' | jq -s '.' > "$DATA_DIR/tracks.json" + +# ============================================ +# Step 5: Cleanup raw jsonl files +# ============================================ + +rm -f "$RAW_DIR"/*.jsonl + +# ============================================ +# Step 6: Summary +# ============================================ + +PLAYLIST_COUNT=$(cat "$DATA_DIR/playlists.json" | jq '.playlists | length' 2>/dev/null || echo "0") +TRACK_COUNT=$(cat "$DATA_DIR/tracks.json" | jq 'length' 2>/dev/null || echo "0") + +echo "" +echo "========================================" +echo "📊 Playlist Summary" +echo "========================================" +echo " 📀 Playlists: $PLAYLIST_COUNT" +echo " 🎵 Total tracks found: $TRACK_COUNT" +echo " 📁 Data saved to: $DATA_DIR/" +echo "========================================" + +echo "" +echo "📝 Playlist Names:" +cat "$DATA_DIR/playlists.json" | jq -r '.playlists[].name' 2>/dev/null | while read -r name; do + if [ -n "$name" ]; then + echo " 🎵 $name" + fi +done diff --git a/generate-playlist-pages.sh b/generate-playlist-pages.sh new file mode 100755 index 0000000..3ac88f9 --- /dev/null +++ b/generate-playlist-pages.sh @@ -0,0 +1,101 @@ +#!/bin/bash + +# ============================================ +# Generate Hugo Playlist Pages from JSON +# ============================================ + +DATA_FILE="data/playlists/playlists.json" +CONTENT_DIR="content/playlists" + +# Check if data file exists +if [ ! -f "$DATA_FILE" ]; then + echo "❌ Error: $DATA_FILE not found. Run fetch-playlist.sh first." + exit 1 +fi + +# Check if data file has content +if [ ! -s "$DATA_FILE" ]; then + echo "❌ Error: $DATA_FILE is empty. Run fetch-playlist.sh first." + exit 1 +fi + +# Create content directory +mkdir -p "$CONTENT_DIR" + +# Create _index.md if it doesn't exist +if [ ! -f "$CONTENT_DIR/_index.md" ]; then + cat > "$CONTENT_DIR/_index.md" << 'EOF' +--- +title: "Playlists" +--- +EOF +fi + +# Clear old playlist pages (keep _index.md) +echo "Clearing old playlist pages..." +find "$CONTENT_DIR" -maxdepth 1 -type f -name "*.md" ! -name "_index.md" -delete + +echo "Generating playlist pages..." + +# Function to escape YAML strings +escape_yaml() { + echo "$1" | sed 's/"/\\"/g' | sed 's/:/\\:/g' | tr -d '\n\r' +} + +# Process each playlist +cat "$DATA_FILE" | jq -c '.playlists[]' 2>/dev/null | while read -r playlist; do + # Skip if playlist is empty + if [ -z "$playlist" ] || [ "$playlist" == "null" ]; then + continue + fi + + # Extract fields + NAME=$(echo "$playlist" | jq -r '.name // "Untitled Playlist"') + PLAYLIST_ID=$(echo "$playlist" | jq -r '.id // ""') + DESCRIPTION=$(echo "$playlist" | jq -r '.description // ""') + IMAGE=$(echo "$playlist" | jq -r '.image // ""') + CREATED_AT=$(echo "$playlist" | jq -r '.created_at // 0') + TRACKS=$(echo "$playlist" | jq -c '.tracks // []') + + # Escape YAML strings + NAME_ESC=$(echo "$NAME" | sed 's/"/\\"/g') + DESCRIPTION_ESC=$(echo "$DESCRIPTION" | sed 's/"/\\"/g' | tr -d '\n\r') + IMAGE_ESC=$(echo "$IMAGE" | sed 's/"/\\"/g') + + # Skip if no name + if [ -z "$NAME" ] || [ "$NAME" == "null" ]; then + continue + fi + + # Create URL-friendly slug + SLUG=$(echo "$NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//') + if [ -z "$SLUG" ]; then + SLUG="${PLAYLIST_ID:0:12}" + fi + + # Convert created_at to date + if [ "$CREATED_AT" != "null" ] && [ "$CREATED_AT" -gt 0 ] 2>/dev/null; then + CREATED_DATE=$(date -d "@$CREATED_AT" "+%Y-%m-%d" 2>/dev/null || echo $(date "+%Y-%m-%d")) + else + CREATED_DATE=$(date "+%Y-%m-%d") + fi + + # Write the markdown file with escaped values + cat > "$CONTENT_DIR/$SLUG.md" <' character */ .main-header::after { @@ -156,6 +137,59 @@ body { pointer-events: none; } +/* */ +/* */ +/* */ + +.playlist-header::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 0; + pointer-events: none; + background: + /* Dark overlay for readability */ + linear-gradient( + to bottom, + rgba(0, 0, 0, 0.5) 0%, + rgba(0, 0, 0, 0.25) 40%, + rgba(0, 0, 0, 0.25) 60%, + rgba(0, 0, 0, 0.5) 100% + ), + /* Cyberpunk pattern */ + repeating-linear-gradient( + 33deg, + transparent, + transparent 50px, + var(--accent-yellow) 50px, + var(--accent-yellow) 51px + ); + opacity: 0.6; +} + +/* Decorative '>' character */ +.playlist-header::after { + content: '>'; + position: absolute; + bottom: 40px; + right: 40px; + z-index: 1; + font-family: 'Poppins', sans-serif; + font-weight: 900; + font-size: 4rem; + color: var(--accent-y); + opacity: 0.15; + pointer-events: none; +} + + +/* */ + + + #bannerino { filter: grayscale(50%); position:absolute; @@ -217,13 +251,7 @@ body { } .profile-picture:hover { filter: grayscale(0%); -} - -.profile-picture:hover { - transform: scale(1.02); - box-shadow: 16px 16px 0 var(--accent-yellow); - border: 4px solid var(--accent-yellow); - + transform: scale(1.02); } /* Header text container */ @@ -2093,3 +2121,710 @@ button { } /* ============ END CYBERPUNK FOOTER ============ */ + +/* ============================================ + * PLAYLISTS + * ============================================ */ + +.playlists-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: 2.5rem; +} + +.playlist-card { + background: var(--card-bg); + border: 2px solid var(--accent-grey); + padding: 1.8rem; + transition: all 0.3s ease; + transform: skewX(-2deg); + box-shadow: 8px 8px 0 var(--accent-grey); +} + +.playlist-card:hover { + transform: skewX(0deg) translate(-4px, -4px); + box-shadow: 12px 12px 0 var(--accent-grey); +} + +.playlist-image { + width: 100%; + height: 180px; + object-fit: cover; + border: 2px solid var(--accent-grey); + margin-bottom: 1rem; + filter: grayscale(30%); + transition: all 0.3s ease; +} + +.playlist-card:hover .playlist-image { + filter: grayscale(0%); +} + +.playlist-info h3 { + font-family: 'Poppins', sans-serif; + font-weight: 700; + font-size: 1.3rem; + letter-spacing: -1px; + margin-bottom: 0.3rem; + color: var(--text-primary); +} + +.playlist-description { + color: var(--text-secondary); + font-size: 0.9rem; + margin-bottom: 0.5rem; +} + +.playlist-track-count { + display: inline-block; + background: var(--accent-grey); + color: var(--accent-black); + padding: 0.2rem 0.8rem; + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 1px; + border-radius: 4px; + margin-top: 0.5rem; +} + +.playlist-tracks { + margin-top: 1.5rem; + border-top: 2px solid var(--accent-grey); + padding-top: 1rem; +} + +.playlist-tracks h4 { + font-family: 'Poppins', sans-serif; + font-weight: 600; + font-size: 1rem; + color: var(--accent-yellow); + margin-bottom: 1rem; + letter-spacing: 1px; +} + +.tracks-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.track-item { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.8rem; + padding: 0.8rem; + background: var(--bg-secondary); + border-radius: 8px; + border-left: 3px solid var(--accent-yellow); + transition: all 0.3s ease; +} + +.track-item:hover { + background: var(--bg-primary); + border-left-color: var(--accent-yellow); +} + +.track-thumbnail { + width: 50px; + height: 50px; + object-fit: cover; + border-radius: 4px; + border: 1px solid var(--accent-grey); +} + +.track-info { + flex: 1; + min-width: 150px; +} + +.track-title { + display: block; + font-family: 'Poppins', sans-serif; + font-weight: 600; + font-size: 0.9rem; + color: var(--text-primary); +} + +.track-artist { + display: block; + font-size: 0.75rem; + color: var(--accent-yellow); +} + +.track-album { + display: block; + font-size: 0.65rem; + color: var(--accent-grey); +} + +.track-duration { + font-size: 0.65rem; + color: var(--accent-grey); + background: var(--tag-bg); + padding: 0.1rem 0.5rem; + border-radius: 3px; +} + +.track-player { + width: 100%; + margin-top: 0.5rem; + border: 1px solid var(--accent-grey); + border-radius: 4px; +} + +.track-item .track-tags { + display: flex; + flex-wrap: wrap; + gap: 0.3rem; + margin-top: 0.3rem; +} + +/* Responsive */ +@media (max-width: 768px) { + .playlists-grid { + grid-template-columns: 1fr; + } + + .playlist-card { + transform: skewX(0deg); + } + + .playlist-card:hover { + transform: translate(-2px, -2px); + } + + .track-item { + flex-direction: column; + align-items: flex-start; + } + + .track-thumbnail { + width: 100%; + height: 100px; + } +} +/* //////// END PLAYLISTS ////////// */ + +/* ============================================ + * PLAYLIST LIST PAGE + * ============================================ */ + +.playlists-list .section-description { + color: var(--text-secondary); + font-size: 1.1rem; + margin-bottom: 3rem; + text-align: center; + opacity: 0.8; +} + +.playlists-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 2.5rem; +} + +.playlist-card { + background: var(--card-bg); + border: 2px solid var(--accent-grey); + overflow: hidden; + transition: all 0.3s ease; + transform: skewX(-2deg); + box-shadow: 8px 8px 0 var(--accent-grey); + position: relative; +} + +.playlist-card:hover { + transform: skewX(0deg) translate(-4px, -4px); + box-shadow: 12px 12px 0 var(--accent-grey); +} + +.playlist-link { + display: block; + text-decoration: none; + color: inherit; +} + +.playlist-image { + width: 100%; + height: 200px; + object-fit: cover; + border-bottom: 3px solid var(--accent-yellow); + filter: grayscale(30%); + transition: all 0.3s ease; +} + +.playlist-card:hover .playlist-image { + filter: grayscale(0%); +} + +.playlist-info { + padding: 1.5rem; +} + +.playlist-info h2 { + font-family: 'Poppins', sans-serif; + font-weight: 700; + font-size: 1.3rem; + letter-spacing: -1px; + color: var(--text-primary); + margin-bottom: 0.3rem; +} + +.playlist-description { + color: var(--text-secondary); + font-size: 0.9rem; + margin-bottom: 0.5rem; +} + +.playlist-track-count { + display: inline-block; + background: var(--accent-grey); + color: var(--bg-primary); + padding: 0.2rem 0.8rem; + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 1px; + border-radius: 4px; +} + +.playlist-hover-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.6); + opacity: 0; + transition: opacity 0.3s ease; + backdrop-filter: blur(20px); +} + +.playlist-card:hover .playlist-hover-overlay { + opacity: 1; +} + +.playlist-view-btn { + padding: 0.8rem 2rem; + background: var(--accent-yellow); + color: var(--bg-primary); + font-family: 'Poppins', sans-serif; + font-weight: 700; + font-size: 0.8rem; + letter-spacing: 2px; + text-transform: uppercase; + border-radius: 7px; + transition: all 0.3s ease; + filter: drop-shadow(0 0 20px var(--accent-yellow)); +} + +.playlist-view-btn:hover { + background: var(--accent-yellow); + color: var(--bg-primary); + transition: all .3s ease; + filter: drop-shadow(0 0 20px var(--accent-yellow)); +} + +/* ============================================ + * PLAYLIST SINGLE PAGE + * ============================================ */ + +.playlist-detail { + padding-top: 2rem; +} + +.playlist-header { + margin-bottom: 3rem; + padding: 2rem; + background: var(--bg-secondary); + border: 2px solid var(--accent-grey); + box-shadow: 8px 8px 0 var(--accent-grey); + transform: skewX(-2deg); +} + +.playlist-header-content { + display: flex; + align-items: center; + gap: 2.5rem; + flex-wrap: wrap; + transform: skewX(2deg); +} + +.playlist-header-image { + width: 200px; + height: 200px; + object-fit: cover; + border: 3px solid var(--accent-yellow); + box-shadow: 4px 4px 0 var(--accent-grey); + flex-shrink: 0; +} + +.playlist-header-info h1 { + font-family: 'Poppins', sans-serif; + font-weight: 900; + font-size: 2.8rem; + letter-spacing: -2px; + color: var(--text-primary); + text-transform: uppercase; + text-shadow: 2px 2px 0 var(--accent-yellow); +} + +.playlist-header-info .playlist-description { + font-size: 1.1rem; + color: var(--text-secondary); + margin: 0.5rem 0 1rem 0; +} + +.playlist-meta { + display: flex; + gap: 1.5rem; + flex-wrap: wrap; +} + +.playlist-meta span { + font-size: 0.85rem; + color: var(--bg-primary); + letter-spacing: 1px; +} + +/* Track list */ +.playlist-tracks-list { + display: flex; + flex-direction: column; + gap: 1rem; + margin: 2rem 0; +} + +.playlist-track-item { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem; + background: var(--card-bg); + border: 2px solid var(--accent-grey); + transition: all 0.3s ease; + transform: skewX(-2deg); + box-shadow: 4px 4px 0 var(--accent-grey); +} + +.playlist-track-item:hover { + transform: skewX(0deg) translate(-2px, -2px); + box-shadow: 6px 6px 0 var(--accent-grey); +} + +.track-number { + font-family: 'Poppins', sans-serif; + font-weight: 700; + font-size: 1.2rem; + color: var(--accent-grey); + min-width: 30px; + text-align: center; +} + +.track-thumbnail { + width: 60px; + height: 60px; + object-fit: cover; + border: 2px solid var(--accent-grey); + border-radius: 4px; + flex-shrink: 0; +} + +.track-info { + flex: 1; + min-width: 200px; +} + +.track-title { + display: block; + font-family: 'Poppins', sans-serif; + font-weight: 600; + font-size: 1rem; + color: var(--text-primary); +} + +.track-artist { + display: block; + font-size: 0.8rem; + color: var(--accent-yellow); +} + +.track-album { + display: block; + font-size: 0.7rem; + color: var(--accent-grey); +} + +.track-duration { + font-size: 0.7rem; + color: var(--accent-grey); + background: var(--tag-bg); + padding: 0.1rem 0.5rem; + border-radius: 3px; +} + +.track-tags { + display: flex; + flex-wrap: wrap; + gap: 0.3rem; + margin-top: 0.2rem; +} + +.track-player-wrapper { + display: flex; + align-items: center; + gap: 0.5rem; + flex-shrink: 0; +} + +.track-player { + width: 100%; + border: 1px solid var(--accent-grey); + border-radius: 4px; +} + +.track-share-btn { + background: none; + border: 1px solid var(--accent-grey); + color: var(--text-primary); + padding: 0.3rem 0.6rem; + border-radius: 4px; + cursor: pointer; + transition: all 0.3s ease; + font-size: 0.8rem; +} + +.track-share-btn:hover { + border-color: var(--accent-yellow); + color: var(--accent-yellow); + transform: scale(1.05); +} + +/* Play all button */ +.playlist-actions { + text-align: center; + margin: 2rem 0; +} + +.playlist-play-all { + background: var(--bg-primary); + color: var(--accent-yellow) !important; + border: 2px solid var(--accent-yellow); + padding: .5rem 1.5rem; + font-family: poppins,sans-serif; + font-weight: 700; + font-size: .7rem; + letter-spacing: 3px; + text-transform: uppercase; + cursor: pointer; + transition: all .3s ease; + filter: drop-shadow(0 0 20px var(--accent-yellow)); + animation: glow 3s infinite alternate; +} + +.playlist-play-all:hover { + transform: scale(1.05); + background: var(--accent-yellow); + color: var(--bg-primary) !important; + border-color: var(--accent-yellow); +} + +/* Back link */ +.playlist-back { + margin-top: 2rem; + text-align: center; +} + +.back-link { + color: var(--accent-grey); + text-decoration: none; + font-family: 'Poppins', sans-serif; + font-weight: 500; + font-size: 0.8rem; + letter-spacing: 2px; + text-transform: uppercase; + transition: all 0.3s ease; + padding: 0.5rem 1.5rem; + border: 1px solid transparent; +} + +.back-link:hover { + color: var(--accent-yellow); + border-color: var(--accent-yellow); +} + +.no-tracks { + text-align: center; + padding: 3rem; + color: var(--text-secondary); + font-size: 1.1rem; +} + +/* Responsive */ +@media (max-width: 768px) { + .playlist-header-content { + flex-direction: column; + text-align: center; + } + + .playlist-header-info h1 { + font-size: 2rem; + } + + .playlist-header-image { + width: 150px; + height: 150px; + } + + .playlist-meta { + justify-content: center; + } + + .playlist-track-item { + flex-wrap: wrap; + gap: 0.5rem; + } + + .track-player-wrapper { + width: 100%; + flex-wrap: wrap; + justify-content: center; + } + + .track-player { + width: 100%; + max-width: 300px; + } + + .playlist-play-all { + padding: 0.8rem 2rem; + font-size: 0.8rem; + } +} + +@media (max-width: 480px) { + .playlist-header-info h1 { + font-size: 1.5rem; + } + + .playlist-header-image { + width: 120px; + height: 120px; + } + + .track-thumbnail { + width: 40px; + height: 40px; + } + + .track-title { + font-size: 0.85rem; + } +} +/* ============================================ + * NAV STRIP (sub-page header) + * ============================================ */ + +.nav-strip { + position: sticky; + top: 0; + z-index: 998; + background: rgba(0, 0, 0, 0.55); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-bottom: 3px solid var(--accent-yellow); + padding: 0.6rem 2rem; +} + +.nav-strip-inner { + max-width: 1200px; + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1.5rem; + flex-wrap: wrap; +} + +.nav-strip-brand { + display: flex; + align-items: center; + gap: 0.8rem; + text-decoration: none; + color: var(--text-primary); + transition: all 0.3s ease; +} + +.nav-strip-brand:hover { + color: var(--accent-yellow); +} + +.nav-strip-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + object-fit: cover; + border: 2px solid var(--accent-yellow); + filter: grayscale(100%); + transition: filter 0.3s ease; +} + +.nav-strip-brand:hover .nav-strip-avatar { + filter: grayscale(0%); +} + +.nav-strip-name { + font-family: 'Poppins', sans-serif; + font-weight: 700; + font-size: 0.9rem; + letter-spacing: -0.5px; + text-transform: uppercase; +} + +.nav-strip-links { + display: flex; + gap: 0.6rem; + flex-wrap: wrap; +} + +.nav-strip-links a { + color: var(--text-primary); + text-decoration: none; + font-family: 'Poppins', sans-serif; + font-weight: 600; + font-size: 0.7rem; + letter-spacing: 2px; + text-transform: uppercase; + padding: 0.4rem 0.9rem; + border: 2px solid transparent; + border-radius: 6px; + transition: all 0.3s ease; + background: rgba(0, 0, 0, 0.3); +} + +.nav-strip-links a:hover { + color: var(--accent-yellow); + border-color: var(--accent-yellow); + transform: scale(1.05); +} + +/* Responsive */ +@media (max-width: 640px) { + .nav-strip { + padding: 0.5rem 1rem; + } + .nav-strip-inner { + gap: 0.6rem; + } + .nav-strip-name { + display: none; + } + .nav-strip-links a { + font-size: 0.6rem; + padding: 0.3rem 0.6rem; + letter-spacing: 1px; + } +} diff --git a/themes/one-pager/assets/js/scripts.js b/themes/one-pager/assets/js/scripts.js index 3e6ecd1..9fafaac 100644 --- a/themes/one-pager/assets/js/scripts.js +++ b/themes/one-pager/assets/js/scripts.js @@ -2,7 +2,7 @@ // 1. UTILITY FUNCTIONS // ============================================ -// Function to shuffle array (Fisher-Yates) +// Fisher-Yates shuffle function shuffleArray(array) { const shuffled = [...array]; for (let i = shuffled.length - 1; i > 0; i--) { @@ -12,24 +12,49 @@ function shuffleArray(array) { return shuffled; } -// Artist filter list +// Artist whitelist — only tracks by these artists are shown const allowedArtists = [ '徒 Setto セット', -'Settoshi Tonami', -'Sethy Bowoy' + 'Settoshi Tonami', + 'Sethy Bowoy', + 'reSet Sakrecoer', + 'Sakrecoer', + 'Vigesimal', + 'Settoshi Tonami' ]; -// Function to check if a track is by an allowed artist function isAllowedArtist(artist) { if (!artist) return false; const normalizedArtist = artist.trim().toLowerCase(); return allowedArtists.some(allowed => { const normalizedAllowed = allowed.trim().toLowerCase(); return normalizedArtist.includes(normalizedAllowed) || - normalizedAllowed.includes(normalizedArtist); + normalizedAllowed.includes(normalizedArtist); }); } +// Find a track's raw Nostr event by its audio URL — used to look up `content` (lyrics/info) +// Checks homepage data first, then falls back to playlist-page data. +function findRawTrack(url) { + if (!url) return null; + + const fromHome = (window.allMusicData || []).find(t => { + if (!t.tags) return false; + return t.tags.some(tag => tag[0] === 'url' && tag[1] === url); + }); + if (fromHome) return fromHome; + + const fromPlaylist = (window.playlistOriginalTracks || []).find(t => t.url === url); + return fromPlaylist || null; +} + +function formatTime(seconds) { + if (!seconds || isNaN(seconds)) return '0:00'; + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${secs.toString().padStart(2, '0')}`; +} + // ============================================ // 2. PHOTO LIGHTBOX // ============================================ @@ -62,7 +87,6 @@ function openPhotoLightbox(index) { }); if (dateStr) metaText += `📅 ${dateStr}`; - lightboxMeta.textContent = metaText || '📅 No date available'; lightboxCounter.textContent = `${currentPhotoIndex + 1} / ${allPicturesArray.length}`; @@ -89,13 +113,12 @@ function nextPhoto() { function initPhotoLightbox() { const lightbox = document.getElementById('customLightbox'); + if (!lightbox) return; + const lightboxClose = document.getElementById('lightboxClose'); const lightboxPrev = document.getElementById('lightboxPrev'); const lightboxNext = document.getElementById('lightboxNext'); - if (!lightbox) return; - - // Remove old listeners by cloning const newClose = lightboxClose.cloneNode(true); const newPrev = lightboxPrev.cloneNode(true); const newNext = lightboxNext.cloneNode(true); @@ -104,29 +127,23 @@ function initPhotoLightbox() { lightboxPrev.parentNode.replaceChild(newPrev, lightboxPrev); lightboxNext.parentNode.replaceChild(newNext, lightboxNext); - // Add new listeners newClose.addEventListener('click', closePhotoLightbox); newPrev.addEventListener('click', prevPhoto); newNext.addEventListener('click', nextPhoto); - // Click outside to close lightbox.removeEventListener('click', handlePhotoClick); lightbox.addEventListener('click', handlePhotoClick); - // Keyboard controls document.removeEventListener('keydown', handlePhotoKeydown); document.addEventListener('keydown', handlePhotoKeydown); - // Setup photo click listeners document.querySelectorAll('.photo-item img').forEach((img) => { img.removeEventListener('click', img._photoClick); img._photoClick = function(e) { e.preventDefault(); const url = this.src; const fullIndex = allPicturesArray.findIndex(p => p.url === url); - if (fullIndex !== -1) { - openPhotoLightbox(fullIndex); - } + if (fullIndex !== -1) openPhotoLightbox(fullIndex); }; img.addEventListener('click', img._photoClick); }); @@ -136,16 +153,9 @@ function handlePhotoKeydown(e) { const lightbox = document.getElementById('customLightbox'); if (!lightbox || !lightbox.classList.contains('active')) return; - if (e.key === 'Escape') { - e.preventDefault(); - closePhotoLightbox(); - } else if (e.key === 'ArrowLeft') { - e.preventDefault(); - prevPhoto(); - } else if (e.key === 'ArrowRight') { - e.preventDefault(); - nextPhoto(); - } + if (e.key === 'Escape') { e.preventDefault(); closePhotoLightbox(); } + else if (e.key === 'ArrowLeft') { e.preventDefault(); prevPhoto(); } + else if (e.key === 'ArrowRight') { e.preventDefault(); nextPhoto(); } } function handlePhotoClick(e) { @@ -154,7 +164,7 @@ function handlePhotoClick(e) { } // ============================================ -// 3. MUSIC RENDERER +// 3. MUSIC RENDERER (homepage cards) // ============================================ function renderMusicTracks(tracks, containerId, randomize = false) { @@ -164,9 +174,7 @@ function renderMusicTracks(tracks, containerId, randomize = false) { const filteredTracks = tracks.filter(track => { let artist = ''; if (track.tags) { - track.tags.forEach(tag => { - if (tag[0] === 'artist') artist = tag[1]; - }); + track.tags.forEach(tag => { if (tag[0] === 'artist') artist = tag[1]; }); } return isAllowedArtist(artist); }); @@ -176,11 +184,10 @@ function renderMusicTracks(tracks, containerId, randomize = false) { return; } - // Build playlist from ALL filtered tracks + // Build global playlist from all filtered tracks window.musicPlaylist = []; filteredTracks.forEach(track => { let title = '', url = '', image = '', artist = '', album = '', duration = ''; - // ADD THIS: Store the track ID const trackId = track.id || ''; if (track.tags) { @@ -197,9 +204,9 @@ function renderMusicTracks(tracks, containerId, randomize = false) { } if (url) { window.musicPlaylist.push({ - id: trackId, // ADD THIS LINE + id: trackId, title: title || 'Untitled', - url: url, + url, image: image || '', artist: artist || 'Unknown Artist', album: album || '', @@ -209,16 +216,14 @@ function renderMusicTracks(tracks, containerId, randomize = false) { } }); - // Randomize or sort the playlist if (randomize) { window.musicPlaylist = shuffleArray(window.musicPlaylist); } else { window.musicPlaylist.sort((a, b) => (b.created_at || 0) - (a.created_at || 0)); } - // Render only 6 cards + // Only render the first 6 cards const displayTracks = window.musicPlaylist.slice(0, 6); - container.innerHTML = ''; displayTracks.forEach((track, index) => { @@ -226,72 +231,50 @@ function renderMusicTracks(tracks, containerId, randomize = false) { card.className = 'music-card clickable'; card.dataset.playlistIndex = index; - // Click on card opens lightbox (except when clicking on info button) card.addEventListener('click', function(e) { - // Don't trigger if clicking on lyrics-toggle or inside lyrics - if (e.target.closest('.lyrics-toggle') || e.target.closest('.lyrics')) { - return; - } + if (e.target.closest('.lyrics-toggle') || e.target.closest('.lyrics')) return; const idx = parseInt(this.dataset.playlistIndex); - if (!isNaN(idx) && idx >= 0) { - openMusicLightbox(idx); - } + if (!isNaN(idx) && idx >= 0) openMusicLightbox(idx); }); - // Find genre tags from the original track data + // Collect genre tags + info from the raw event let genreTags = []; let content = ''; - let alt = ''; - - // Find the original track data to get tags and content const originalTrack = filteredTracks.find(t => { - let url = ''; - if (t.tags) { - t.tags.forEach(tag => { - if (tag[0] === 'url') url = tag[1]; - }); - } - return url === track.url; + if (!t.tags) return false; + return t.tags.some(tag => tag[0] === 'url' && tag[1] === track.url); }); if (originalTrack && originalTrack.tags) { originalTrack.tags.forEach(tag => { - const key = tag[0]; - const value = tag[1]; - if (key === 't') genreTags.push(value); - if (key === 'alt') alt = value; + if (tag[0] === 't') genreTags.push(tag[1]); }); - content = originalTrack.content || ''; + content = originalTrack.content || ''; } - // Build the card HTML - let cardHTML = ` -
-
- ${track.image ? `${track.title}` : ''} -
- ▶ -
-
-
-

${track.title}

- ${track.artist ? `

${track.artist}

` : ''} - ${track.album ? `

${track.album}

` : ''} -
- ${genreTags.length > 0 ? ` -
- ${genreTags.map(t => `${t}`).join('')} -
- ` : ''} - ${content ? ` - - - ` : ''} + card.innerHTML = ` +
+
+ ${track.image ? `${track.title}` : ''} +
▶
- `; - - card.innerHTML = cardHTML; - container.appendChild(card); +
+

${track.title}

+ ${track.artist ? `

${track.artist}

` : ''} + ${track.album ? `

${track.album}

` : ''} +
+ ${genreTags.length > 0 ? ` +
+ ${genreTags.map(t => `${t}`).join('')} +
+ ` : ''} + ${content ? ` + + + ` : ''} +
+ `; + container.appendChild(card); }); initMusicEventListeners(); @@ -305,18 +288,12 @@ function initMusicEventListeners() { } function handleLyricsToggle(e) { - // Stop the click from bubbling up to the card e.stopPropagation(); - const lyrics = this.nextElementSibling; if (lyrics && lyrics.classList.contains('lyrics')) { - if (lyrics.style.display === 'none') { - lyrics.style.display = 'block'; - this.innerHTML = 'Hide Info'; - } else { - lyrics.style.display = 'none'; - this.innerHTML = 'Show Info'; - } + const hidden = lyrics.style.display === 'none'; + lyrics.style.display = hidden ? 'block' : 'none'; + this.innerHTML = `${hidden ? 'Hide Info' : 'Show Info'}`; } } @@ -328,21 +305,14 @@ function renderRandomPictures(picturesData, containerId, randomize = false) { const container = document.getElementById(containerId); if (!container) return; - // First, flatten all pictures from all events but keep track of which event they belong to let allPictures = []; let allEvents = []; picturesData.forEach(event => { - const imetaTags = []; - event.tags.forEach(tag => { - if (tag[0] === 'imeta') { - imetaTags.push(tag); - } - }); + const imetaTags = event.tags.filter(tag => tag[0] === 'imeta'); if (imetaTags.length > 0) { let firstImage = null; - let eventImages = []; imetaTags.forEach(imetaTag => { let imageUrl = ''; @@ -350,28 +320,20 @@ function renderRandomPictures(picturesData, containerId, randomize = false) { imetaTag.forEach(item => { if (typeof item === 'string') { - if (item.startsWith('url ')) { - imageUrl = item.replace('url ', '').trim(); - } - if (item.startsWith('alt ')) { - alt = item.replace('alt ', '').trim(); - } + if (item.startsWith('url ')) imageUrl = item.replace('url ', '').trim(); + if (item.startsWith('alt ')) alt = item.replace('alt ', '').trim(); } }); if (imageUrl) { const pictureData = { - url: imageUrl, - alt: alt, + url: imageUrl, alt, created_at: event.created_at, pubkey: event.pubkey, event_id: event.id }; - eventImages.push(pictureData); allPictures.push(pictureData); - if (!firstImage) { - firstImage = pictureData; - } + if (!firstImage) firstImage = pictureData; } }); @@ -379,13 +341,11 @@ function renderRandomPictures(picturesData, containerId, randomize = false) { allEvents.push({ event_id: event.id, created_at: event.created_at, - firstImage: firstImage + firstImage }); } } else { - let imageUrl = ''; - let alt = event.content || ''; - + let imageUrl = '', alt = event.content || ''; event.tags.forEach(tag => { if (tag[0] === 'imeta') { const imetaStr = tag[1]; @@ -395,11 +355,9 @@ function renderRandomPictures(picturesData, containerId, randomize = false) { if (altMatch) alt = altMatch[1]; } }); - if (imageUrl) { const pictureData = { - url: imageUrl, - alt: alt, + url: imageUrl, alt, created_at: event.created_at, pubkey: event.pubkey, event_id: event.id @@ -414,24 +372,16 @@ function renderRandomPictures(picturesData, containerId, randomize = false) { } }); - // Store ALL pictures in the global array for the lightbox allPicturesArray = allPictures; - // Sort events by created_at (newest first) const sortedEvents = [...allEvents].sort((a, b) => b.created_at - a.created_at); - - // Select which events to display (6 max) - let displayEvents; - if (randomize) { - const shuffled = shuffleArray([...sortedEvents]); - displayEvents = shuffled.slice(0, 6); - } else { - displayEvents = sortedEvents.slice(0, 6); - } + const displayEvents = randomize + ? shuffleArray([...sortedEvents]).slice(0, 6) + : sortedEvents.slice(0, 6); container.innerHTML = ''; - displayEvents.forEach((event, index) => { + displayEvents.forEach(event => { const picture = event.firstImage; if (!picture) return; @@ -439,8 +389,6 @@ function renderRandomPictures(picturesData, containerId, randomize = false) { item.className = 'photo-item'; item.dataset.image = picture.url; item.dataset.alt = picture.alt; - item.dataset.index = index; - item.dataset.eventId = event.event_id; const date = new Date(picture.created_at * 1000); const dateStr = date.toLocaleDateString('en-US', { @@ -450,14 +398,12 @@ function renderRandomPictures(picturesData, containerId, randomize = false) { item.dataset.pubkey = picture.pubkey || ''; item.innerHTML = ` - ${picture.url ? `${picture.alt}` : ''} -
📅 ${dateStr}
+ ${picture.url ? `${picture.alt}` : ''} +
📅 ${dateStr}
`; - container.appendChild(item); }); - // Initialize photo lightbox initPhotoLightbox(); } @@ -469,14 +415,9 @@ function renderRandomVideos(videosData, containerId, randomize = false) { const container = document.getElementById(containerId); if (!container) return; - let selected; - if (randomize) { - const shuffled = shuffleArray(videosData); - selected = shuffled.slice(0, 2); - } else { - const sorted = [...videosData].sort((a, b) => b.created_at - a.created_at); - selected = sorted.slice(0, 2); - } + const selected = randomize + ? shuffleArray(videosData).slice(0, 2) + : [...videosData].sort((a, b) => b.created_at - a.created_at).slice(0, 2); container.innerHTML = ''; const posterImage = '/images/videoposter.png'; @@ -511,37 +452,246 @@ function renderRandomVideos(videosData, containerId, randomize = false) { }); const wrapperStyle = isVertical - ? 'style="position:relative;padding-bottom:177.78%;height:0;overflow:hidden;background:var(--accent-black);"' - : ''; + ? 'style="position:relative;padding-bottom:177.78%;height:0;overflow:hidden;background:var(--accent-black);"' + : ''; const videoStyle = isVertical - ? 'style="position:absolute;top:0;left:0;width:100%;height:100%;object-fit:cover;"' - : 'style="width:100%;"'; + ? 'style="position:absolute;top:0;left:0;width:100%;height:100%;object-fit:cover;"' + : 'style="width:100%;"'; - const html = ` -
- ${videoUrl ? ` -
- + container.insertAdjacentHTML('beforeend', ` +
+ ${videoUrl ? ` +
+ +
+ ` : ''} +
+

${alt}

+
+ 📅 ${dateStr} + ${dim ? `📐 ${dim}` : ''} +
+
- ` : ''} -
-

${alt}

-
- 📅 ${dateStr} - ${dim ? `📐 ${dim}` : ''} -
-
-
- `; - container.insertAdjacentHTML('beforeend', html); + `); }); } // ============================================ -// 6. REFRESH FUNCTIONS +// 6. MUSIC LIGHTBOX +// ============================================ + +let currentMusicIndex = 0; +let musicPlayer = null; + +function openMusicLightbox(index) { + if (!window.musicPlaylist || window.musicPlaylist.length === 0) return; + + if (musicPlayer) { + musicPlayer.pause(); + musicPlayer.remove(); + musicPlayer = null; + } + + if (index < 0) index = window.musicPlaylist.length - 1; + if (index >= window.musicPlaylist.length) index = 0; + currentMusicIndex = index; + + const track = window.musicPlaylist[currentMusicIndex]; + if (!track) return; + + const lightbox = document.getElementById('musicLightbox'); + const bg = document.getElementById('musicLightboxBg'); + const img = document.getElementById('musicLightboxImage'); + const title = document.getElementById('musicLightboxTitle'); + const artist = document.getElementById('musicLightboxArtist'); + const album = document.getElementById('musicLightboxAlbum'); + const durationDisplay = document.getElementById('musicLightboxDuration'); + const currentTimeEl = document.getElementById('musicLightboxCurrentTime'); + const progressFill = document.getElementById('musicLightboxProgressFill'); + const playIcon2 = document.getElementById('musicPlayIcon2'); + const lyricsDisplay = document.getElementById('musicLightboxLyrics'); + const shareBtn = document.getElementById('musicShareBtn'); + + // --- Share button --- + let feedbackEl = document.getElementById('shareFeedbackPopout'); + if (!feedbackEl) { + feedbackEl = document.createElement('div'); + feedbackEl.id = 'shareFeedbackPopout'; + feedbackEl.className = 'share-feedback'; + feedbackEl.innerHTML = '✓ Copied!'; + document.body.appendChild(feedbackEl); + } + if (shareBtn) { + shareBtn.onclick = function() { + if (!track.id) { + feedbackEl.textContent = '⚠️ No ID'; + feedbackEl.classList.add('visible'); + setTimeout(() => feedbackEl.classList.remove('visible'), 2000); + return; + } + const shareUrl = `https://nostr.basspistol.org/notes/${track.id}`; + + if (navigator.share) { + navigator.share({ + title: track.title || 'Music Track', + text: `Check out "${track.title}" by ${track.artist || 'Unknown Artist'}`, + url: shareUrl + }).catch(err => console.log('Share cancelled or failed:', err)); + } else { + const showCopied = () => { + feedbackEl.innerHTML = '✓ Copied!'; + feedbackEl.classList.add('visible'); + setTimeout(() => feedbackEl.classList.remove('visible'), 2000); + }; + if (navigator.clipboard) { + navigator.clipboard.writeText(shareUrl).then(showCopied).catch(() => { + const ta = document.createElement('textarea'); + ta.value = shareUrl; + document.body.appendChild(ta); + ta.select(); + try { document.execCommand('copy'); showCopied(); } catch(e) {} + document.body.removeChild(ta); + }); + } + } + }; + } + + // --- Album art + info --- + const artUrl = track.image || '/images/default-album.jpg'; + img.src = artUrl; + bg.style.backgroundImage = `url(${artUrl})`; + title.textContent = track.title || 'Untitled'; + artist.textContent = track.artist || 'Unknown Artist'; + album.textContent = track.album || ''; + + // --- Lyrics lookup (works on homepage & playlist pages) --- + let lyricsContent = track.content || ''; + if (!lyricsContent) { + const raw = findRawTrack(track.url); + if (raw && raw.content) lyricsContent = raw.content; + } + if (lyricsContent) { + lyricsDisplay.innerHTML = lyricsContent.replace(/\n/g, '
'); + lyricsDisplay.style.display = 'block'; + } else { + lyricsDisplay.style.display = 'none'; + } + + // --- Progress --- + progressFill.style.width = '0%'; + currentTimeEl.textContent = '0:00'; + durationDisplay.textContent = track.duration ? formatTime(track.duration) : '0:00'; + playIcon2.textContent = '▶'; + + // --- Audio player --- + musicPlayer = new Audio(track.url); + musicPlayer.preload = 'metadata'; + + musicPlayer.addEventListener('loadedmetadata', function() { + durationDisplay.textContent = formatTime(this.duration); + }); + musicPlayer.addEventListener('ended', nextMusicTrack); + musicPlayer.addEventListener('timeupdate', function() { + const percent = (this.currentTime / this.duration) * 100; + progressFill.style.width = `${percent}%`; + currentTimeEl.textContent = formatTime(this.currentTime); + }); + + lightbox.classList.add('active'); + document.body.style.overflow = 'hidden'; + playMusic(); +} + +function playMusic() { + if (!musicPlayer) return; + musicPlayer.play() + .then(() => { document.getElementById('musicPlayIcon2').textContent = '⏸️'; }) + .catch(e => console.log('Playback prevented:', e)); +} + +function pauseMusic() { + if (!musicPlayer) return; + musicPlayer.pause(); + document.getElementById('musicPlayIcon2').textContent = '▶️'; +} + +function toggleMusic() { + if (!musicPlayer) return; + musicPlayer.paused ? playMusic() : pauseMusic(); +} + +function nextMusicTrack() { openMusicLightbox(currentMusicIndex + 1); } +function prevMusicTrack() { openMusicLightbox(currentMusicIndex - 1); } + +function closeMusicLightbox() { + const lightbox = document.getElementById('musicLightbox'); + if (!lightbox) return; + if (musicPlayer) { + musicPlayer.pause(); + musicPlayer.remove(); + musicPlayer = null; + } + lightbox.classList.remove('active'); + document.body.style.overflow = ''; +} + +function initMusicLightbox() { + const lightbox = document.getElementById('musicLightbox'); + if (!lightbox) return; + + const closeBtn = document.getElementById('musicLightboxClose'); + const playBtn2 = document.getElementById('musicLightboxPlayBtn2'); + const prevBtn = document.getElementById('musicLightboxPrev'); + const nextBtn = document.getElementById('musicLightboxNext'); + const progressBar = document.getElementById('musicLightboxProgressBar'); + + const newClose = closeBtn.cloneNode(true); + const newPlayBtn2 = playBtn2.cloneNode(true); + const newPrev = prevBtn.cloneNode(true); + const newNext = nextBtn.cloneNode(true); + const newProgress = progressBar.cloneNode(true); + + closeBtn.parentNode.replaceChild(newClose, closeBtn); + playBtn2.parentNode.replaceChild(newPlayBtn2, playBtn2); + prevBtn.parentNode.replaceChild(newPrev, prevBtn); + nextBtn.parentNode.replaceChild(newNext, nextBtn); + progressBar.parentNode.replaceChild(newProgress, progressBar); + + newClose.addEventListener('click', closeMusicLightbox); + + lightbox.addEventListener('click', function(e) { + if (e.target === lightbox) closeMusicLightbox(); + }); + + newPlayBtn2.addEventListener('click', function(e) { e.stopPropagation(); toggleMusic(); }); + newPrev.addEventListener('click', function(e) { e.stopPropagation(); prevMusicTrack(); }); + newNext.addEventListener('click', function(e) { e.stopPropagation(); nextMusicTrack(); }); + + newProgress.addEventListener('click', function(e) { + if (!musicPlayer) return; + const rect = this.getBoundingClientRect(); + const percent = (e.clientX - rect.left) / rect.width; + musicPlayer.currentTime = percent * musicPlayer.duration; + }); + + document.addEventListener('keydown', function(e) { + const lb = document.getElementById('musicLightbox'); + if (!lb || !lb.classList.contains('active')) return; + + if (e.key === 'Escape') { e.preventDefault(); closeMusicLightbox(); } + else if (e.key === ' ' || e.key === 'Space') { e.preventDefault(); toggleMusic(); } + else if (e.key === 'ArrowLeft') { e.preventDefault(); prevMusicTrack(); } + else if (e.key === 'ArrowRight') { e.preventDefault(); nextMusicTrack(); } + }); +} + +// ============================================ +// 7. REFRESH / RANDOMIZE HELPERS // ============================================ function refreshMusic() { @@ -560,366 +710,38 @@ function refreshVideos() { } // ============================================ -// 7. MUSIC LIGHTBOX -// ============================================ - -let currentMusicIndex = 0; -let musicPlayer = null; - -// Open music lightbox -function openMusicLightbox(index) { - - - if (!window.musicPlaylist || window.musicPlaylist.length === 0) return; - - // Stop any existing playback - if (musicPlayer) { - musicPlayer.pause(); - musicPlayer.remove(); - musicPlayer = null; - } - - if (index < 0) index = window.musicPlaylist.length - 1; - if (index >= window.musicPlaylist.length) index = 0; - currentMusicIndex = index; - - const track = window.musicPlaylist[currentMusicIndex]; - if (!track) return; - - // Get elements - const lightbox = document.getElementById('musicLightbox'); - const bg = document.getElementById('musicLightboxBg'); - const img = document.getElementById('musicLightboxImage'); - const title = document.getElementById('musicLightboxTitle'); - const artist = document.getElementById('musicLightboxArtist'); - const album = document.getElementById('musicLightboxAlbum'); - const durationDisplay = document.getElementById('musicLightboxDuration'); - const currentTimeDisplay = document.getElementById('musicLightboxCurrentTime'); - const progressFill = document.getElementById('musicLightboxProgressFill'); - const playBtn2 = document.getElementById('musicLightboxPlayBtn2'); - const playIcon2 = document.getElementById('musicPlayIcon2'); - // Get the lyrics element - const lyricsDisplay = document.getElementById('musicLightboxLyrics'); - - // Share button - in its own row - const shareBtn = document.getElementById('musicShareBtn'); - - // Create feedback element if it doesn't exist - let feedbackEl = document.getElementById('shareFeedbackPopout'); - if (!feedbackEl) { - feedbackEl = document.createElement('div'); - feedbackEl.id = 'shareFeedbackPopout'; - feedbackEl.className = 'share-feedback'; - feedbackEl.innerHTML = '✓ Copied!'; - document.body.appendChild(feedbackEl); - } - - if (shareBtn) { - shareBtn.onclick = function() { - if (!track.id) { - feedbackEl.textContent = '⚠️ No ID'; - feedbackEl.classList.add('visible'); - setTimeout(() => { - feedbackEl.classList.remove('visible'); - }, 2000); - return; - } - - const shareUrl = `https://nostr.basspistol.org/notes/${track.id}`; - - if (navigator.share) { - navigator.share({ - title: track.title || 'Music Track', - text: `Check out "${track.title}" by ${track.artist || 'Unknown Artist'}`, - url: shareUrl - }).catch(err => { - console.log('Share cancelled or failed:', err); - }); - } else { - navigator.clipboard.writeText(shareUrl).then(() => { - feedbackEl.innerHTML = '✓ Copied!'; - feedbackEl.classList.add('visible'); - setTimeout(() => { - feedbackEl.classList.remove('visible'); - }, 2000); - }).catch(err => { - console.error('Could not copy text: ', err); - const textArea = document.createElement('textarea'); - textArea.value = shareUrl; - document.body.appendChild(textArea); - textArea.select(); - try { - document.execCommand('copy'); - feedbackEl.innerHTML = '✓ Copied!'; - feedbackEl.classList.add('visible'); - setTimeout(() => { - feedbackEl.classList.remove('visible'); - }, 2000); - } catch (err) { - console.error('Fallback copy failed:', err); - feedbackEl.textContent = '⚠️ Copy failed'; - feedbackEl.classList.add('visible'); - setTimeout(() => { - feedbackEl.classList.remove('visible'); - }, 2000); - } - document.body.removeChild(textArea); - }); - } - }; - } - - // Set album art - const artUrl = track.image || '/images/default-album.jpg'; - img.src = artUrl; - bg.style.backgroundImage = `url(${artUrl})`; - - // Set info - title.textContent = track.title || 'Untitled'; - artist.textContent = track.artist || 'Unknown Artist'; - album.textContent = track.album || ''; - - // Set lyrics - find the original track data to get content - let lyricsContent = ''; - // Find the original track data - const originalTrack = window.allMusicData ? window.allMusicData.find(t => { - let url = ''; - if (t.tags) { - t.tags.forEach(tag => { - if (tag[0] === 'url') url = tag[1]; - }); - } - return url === track.url; - }) : null; - - if (originalTrack && originalTrack.content) { - lyricsContent = originalTrack.content; - } - - // Display lyrics if they exist - if (lyricsContent) { - lyricsDisplay.innerHTML = lyricsContent.replace(/\n/g, '
'); - lyricsDisplay.style.display = 'block'; - } else { - lyricsDisplay.style.display = 'none'; - } - - // Reset progress - progressFill.style.width = '0%'; - currentTimeDisplay.textContent = '0:00'; - durationDisplay.textContent = track.duration ? formatTime(track.duration) : '0:00'; - - // Set play buttons to play state - playIcon2.textContent = '▶'; - - // Create audio player - musicPlayer = new Audio(track.url); - musicPlayer.preload = 'metadata'; - - // When metadata loaded, update duration - musicPlayer.addEventListener('loadedmetadata', function() { - durationDisplay.textContent = formatTime(this.duration); - }); - - // Track ended - go to next - musicPlayer.addEventListener('ended', function() { - nextMusicTrack(); - }); - - // Update progress - musicPlayer.addEventListener('timeupdate', function() { - const percent = (this.currentTime / this.duration) * 100; - progressFill.style.width = `${percent}%`; - currentTimeDisplay.textContent = formatTime(this.currentTime); - }); - - // Show lightbox - lightbox.classList.add('active'); - document.body.style.overflow = 'hidden'; - - // Auto-play - playMusic(); -} - -// Play music -function playMusic() { - if (!musicPlayer) return; - - musicPlayer.play() - .then(() => { - document.getElementById('musicPlayIcon2').textContent = '⏸️'; - }) - .catch(e => { - console.log('Playback prevented:', e); - }); -} - -// Pause music -function pauseMusic() { - if (!musicPlayer) return; - musicPlayer.pause(); - document.getElementById('musicPlayIcon2').textContent = '▶️'; -} - -// Toggle play/pause -function toggleMusic() { - if (!musicPlayer) return; - if (musicPlayer.paused) { - playMusic(); - } else { - pauseMusic(); - } -} - -// Next track -function nextMusicTrack() { - openMusicLightbox(currentMusicIndex + 1); -} - -// Previous track -function prevMusicTrack() { - openMusicLightbox(currentMusicIndex - 1); -} - -// Close music lightbox -function closeMusicLightbox() { - const lightbox = document.getElementById('musicLightbox'); - if (!lightbox) return; - - if (musicPlayer) { - musicPlayer.pause(); - musicPlayer.remove(); - musicPlayer = null; - } - - lightbox.classList.remove('active'); - document.body.style.overflow = ''; -} - -// Format time (seconds to MM:SS) -function formatTime(seconds) { - if (!seconds || isNaN(seconds)) return '0:00'; - const mins = Math.floor(seconds / 60); - const secs = Math.floor(seconds % 60); - return `${mins}:${secs.toString().padStart(2, '0')}`; -} - -// Initialize music lightbox event listeners -function initMusicLightbox() { - const lightbox = document.getElementById('musicLightbox'); - const closeBtn = document.getElementById('musicLightboxClose'); - const playBtn2 = document.getElementById('musicLightboxPlayBtn2'); - const prevBtn = document.getElementById('musicLightboxPrev'); - const nextBtn = document.getElementById('musicLightboxNext'); - const progressBar = document.getElementById('musicLightboxProgressBar'); - - if (!lightbox) return; - - // Remove old listeners by cloning - const newClose = closeBtn.cloneNode(true); - const newPlayBtn2 = playBtn2.cloneNode(true); - const newPrev = prevBtn.cloneNode(true); - const newNext = nextBtn.cloneNode(true); - const newProgressBar = progressBar.cloneNode(true); - - closeBtn.parentNode.replaceChild(newClose, closeBtn); - playBtn2.parentNode.replaceChild(newPlayBtn2, playBtn2); - prevBtn.parentNode.replaceChild(newPrev, prevBtn); - nextBtn.parentNode.replaceChild(newNext, nextBtn); - progressBar.parentNode.replaceChild(newProgressBar, progressBar); - - // Close button - newClose.addEventListener('click', closeMusicLightbox); - - // Click outside to close (on the background) - lightbox.addEventListener('click', function(e) { - if (e.target === lightbox) { - closeMusicLightbox(); - } - }); - - // Play button - newPlayBtn2.addEventListener('click', function(e) { - e.stopPropagation(); - toggleMusic(); - }); - - // Previous/Next - newPrev.addEventListener('click', function(e) { - e.stopPropagation(); - prevMusicTrack(); - }); - - newNext.addEventListener('click', function(e) { - e.stopPropagation(); - nextMusicTrack(); - }); - - // Progress bar click to seek - newProgressBar.addEventListener('click', function(e) { - if (!musicPlayer) return; - const rect = this.getBoundingClientRect(); - const x = e.clientX - rect.left; - const percent = x / rect.width; - musicPlayer.currentTime = percent * musicPlayer.duration; - }); - - // Keyboard controls - document.addEventListener('keydown', function(e) { - const lightbox = document.getElementById('musicLightbox'); - if (!lightbox || !lightbox.classList.contains('active')) return; - - if (e.key === 'Escape') { - e.preventDefault(); - closeMusicLightbox(); - } else if (e.key === ' ' || e.key === 'Space') { - e.preventDefault(); - toggleMusic(); - } else if (e.key === 'ArrowLeft') { - e.preventDefault(); - prevMusicTrack(); - } else if (e.key === 'ArrowRight') { - e.preventDefault(); - nextMusicTrack(); - } - }); -} - -// ============================================ -// 8. DOM CONTENT LOADED +// 8. INIT // ============================================ document.addEventListener('DOMContentLoaded', function() { - // Sticky Navigation + // --- Sticky navigation --- const stickyNav = document.getElementById('stickyNav'); const mainHeader = document.querySelector('.main-header'); - let lastScrollY = window.scrollY; let ticking = false; function updateStickyNav() { const scrollY = window.scrollY; const headerHeight = mainHeader ? mainHeader.offsetHeight : 400; - if (scrollY > headerHeight - 100) { - stickyNav.classList.add('visible'); - } else { - stickyNav.classList.remove('visible'); + if (stickyNav) { + if (scrollY > headerHeight - 100) stickyNav.classList.add('visible'); + else stickyNav.classList.remove('visible'); } - lastScrollY = scrollY; } - window.addEventListener('scroll', function() { - if (!ticking) { - window.requestAnimationFrame(function() { - updateStickyNav(); - ticking = false; - }); - ticking = true; - } - }); + if (stickyNav) { + window.addEventListener('scroll', function() { + if (!ticking) { + window.requestAnimationFrame(function() { + updateStickyNav(); + ticking = false; + }); + ticking = true; + } + }); + setTimeout(updateStickyNav, 100); + } - setTimeout(updateStickyNav, 100); - - // Read More toggle for blog posts + // --- Read more toggles --- document.querySelectorAll('.read-more-toggle').forEach(button => { button.addEventListener('click', function() { const content = this.nextElementSibling; @@ -933,39 +755,34 @@ document.addEventListener('DOMContentLoaded', function() { }); }); - // Smooth scrolling for navigation links (both main nav and sticky nav) + // --- Smooth scroll for # anchors --- document.querySelectorAll('nav a').forEach(link => { link.addEventListener('click', function(e) { - e.preventDefault(); - const targetId = this.getAttribute('href'); - const targetElement = document.querySelector(targetId); - if (targetElement) { - const navHeight = stickyNav.offsetHeight; - const targetPosition = targetElement.getBoundingClientRect().top + window.pageYOffset - navHeight - 20; - window.scrollTo({ - top: targetPosition, - behavior: 'smooth' - }); + const href = this.getAttribute('href'); + if (href && href.startsWith('#')) { + e.preventDefault(); + const targetElement = document.querySelector(href); + if (targetElement) { + const navHeight = stickyNav ? stickyNav.offsetHeight : 0; + const targetPosition = targetElement.getBoundingClientRect().top + window.pageYOffset - navHeight - 20; + window.scrollTo({ top: targetPosition, behavior: 'smooth' }); + } } }); }); - // Initialize Music (chronological, not randomized) + // --- Initialize renderers --- if (window.allMusicData && window.allMusicData.length > 0) { renderMusicTracks(window.allMusicData, 'musicGrid', false); } - - // Initialize Pictures (chronological, not randomized) if (window.allPicturesData && window.allPicturesData.length > 0) { renderRandomPictures(window.allPicturesData, 'photoGrid', false); } - - // Initialize Videos (chronological, not randomized) if (window.allVideosData && window.allVideosData.length > 0) { renderRandomVideos(window.allVideosData, 'videoGrid', false); } - // Initialize music lightbox + // --- Initialize the music lightbox controls --- initMusicLightbox(); }); diff --git a/themes/one-pager/layouts/_default/baseof.html b/themes/one-pager/layouts/_default/baseof.html new file mode 100644 index 0000000..95b34af --- /dev/null +++ b/themes/one-pager/layouts/_default/baseof.html @@ -0,0 +1,27 @@ + + + + + + + {{ partial "seo.html" . }} + + {{ $style := resources.Get "css/style.css" | resources.Minify | fingerprint }} + + + + + + {{ block "header" . }}{{ end }} +
+ {{ block "main" . }}{{ end }} +
+ + {{ partial "footer.html" . }} + {{ partial "photo-lightbox.html" . }} + {{ partial "music-lightbox.html" . }} + + {{ $js := resources.Get "js/scripts.js" | resources.Minify | fingerprint }} + + + diff --git a/themes/one-pager/layouts/_default/index.html b/themes/one-pager/layouts/_default/index.html new file mode 100644 index 0000000..a5b7df2 --- /dev/null +++ b/themes/one-pager/layouts/_default/index.html @@ -0,0 +1,12 @@ +{{ define "header" }} +{{ partial "header.html" . }} +{{ end }} + +{{ define "main" }} + + {{ partial "blog.html" . }} + {{ partial "music.html" . }} + {{ partial "pictures.html" . }} + {{ partial "videos.html" . }} + +{{ end }} diff --git a/themes/one-pager/layouts/index.html b/themes/one-pager/layouts/index.html deleted file mode 100644 index e98fd97..0000000 --- a/themes/one-pager/layouts/index.html +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - {{ partial "seo.html" . }} - - - - - - - - - - {{ $style := resources.Get "css/style.css" | resources.Minify | fingerprint }} - - - - - - {{ partial "header.html" . }} - -
- - {{ partial "blog.html" . }} - - {{ partial "music.html" . }} - - {{ partial "pictures.html" . }} - - {{ partial "videos.html" . }} - -
- - {{ partial "footer.html" . }} - - -
- ✕ - - -
- -
- - -
-
-
1 / 1
-
- - -
-
- - -
-
- Album art -
- -
-

Title

-

Artist

-

Album

- -
- 0:00 -
-
-
- 0:00 -
- -
- - - -
- - -
- -
- - - -
-
-
- - - {{ $js := resources.Get "js/scripts.js" | resources.Minify | fingerprint }} - - - diff --git a/themes/one-pager/layouts/partials/header.html b/themes/one-pager/layouts/partials/header.html index ef94269..836c35e 100644 --- a/themes/one-pager/layouts/partials/header.html +++ b/themes/one-pager/layouts/partials/header.html @@ -39,12 +39,13 @@