PEACHFEST (spotify stats!)

login using spotify below and receive your stats in FUN FORM

/

document.addEventListener('DOMContentLoaded', function () {
  (function () {
    const CLIENT_ID = '0ed77f382f824e7895a26ba94de99fca';
    const REDIRECT_URI = window.location.origin + window.location.pathname;
    const SCOPES = 'user-top-read user-read-email user-read-private';

    const loginBtn = document.getElementById('ss-login-btn');
    const refreshBtn = document.getElementById('ss-refresh-btn');
    const logoutBtn = document.getElementById('ss-logout-btn');

    const userWrap = document.getElementById('ss-user');
    const toolbar = document.getElementById('ss-toolbar');
    const topCards = document.getElementById('ss-top-cards');
    const sections = document.getElementById('ss-sections');
    const actions = document.getElementById('ss-actions');

    const avatarEl = document.getElementById('ss-avatar');
    const userNameEl = document.getElementById('ss-user-name');
    const userSubEl = document.getElementById('ss-user-sub');

    const topTrackImg = document.getElementById('ss-top-track-img');
    const topTrackTitle = document.getElementById('ss-top-track-title');
    const topTrackSub = document.getElementById('ss-top-track-sub');

    const topArtistImg = document.getElementById('ss-top-artist-img');
    const topArtistTitle = document.getElementById('ss-top-artist-title');
    const topArtistSub = document.getElementById('ss-top-artist-sub');

    const tracksList = document.getElementById('ss-tracks-list');
    const artistsList = document.getElementById('ss-artists-list');

    const statusEl = document.getElementById('ss-status');
    const errorEl = document.getElementById('ss-error');

    if (
      !loginBtn ||
      !refreshBtn ||
      !logoutBtn ||
      !userWrap ||
      !toolbar ||
      !topCards ||
      !sections ||
      !actions ||
      !avatarEl ||
      !userNameEl ||
      !userSubEl ||
      !topTrackImg ||
      !topTrackTitle ||
      !topTrackSub ||
      !topArtistImg ||
      !topArtistTitle ||
      !topArtistSub ||
      !tracksList ||
      !artistsList ||
      !statusEl ||
      !errorEl
    ) {
      return;
    }

    const STORAGE = {
      verifier: 'spotify_pkce_code_verifier',
      accessToken: 'spotify_access_token',
      expiresAt: 'spotify_access_token_expires_at',
      currentRange: 'spotify_current_time_range'
    };

    let currentRange = localStorage.getItem(STORAGE.currentRange) || 'short_term';

    function setStatus(msg) {
      statusEl.textContent = msg || '';
    }

    function setError(msg) {
      errorEl.textContent = msg || '';
    }

    function showApp() {
      userWrap.style.display = 'flex';
      toolbar.style.display = 'flex';
      topCards.style.display = 'grid';
      sections.style.display = 'grid';
      actions.style.display = 'flex';
      loginBtn.style.display = 'none';
    }

    function hideApp() {
      userWrap.style.display = 'none';
      toolbar.style.display = 'none';
      topCards.style.display = 'none';
      sections.style.display = 'none';
      actions.style.display = 'none';
      loginBtn.style.display = 'inline-block';
    }

    function setActiveRange(range) {
      currentRange = range;
      localStorage.setItem(STORAGE.currentRange, range);
      document.querySelectorAll('#ss-toolbar .ss-pill').forEach(btn => {
        btn.classList.toggle('active', btn.getAttribute('data-range') === range);
      });
    }

    function randomString(length) {
      const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
      let text = '';
      for (let i = 0; i < length; i++) {
        text += possible.charAt(Math.floor(Math.random() * possible.length));
      }
      return text;
    }

    async function sha256(plain) {
      const encoder = new TextEncoder();
      const data = encoder.encode(plain);
      return window.crypto.subtle.digest('SHA-256', data);
    }

    function base64encode(buffer) {
      const bytes = new Uint8Array(buffer);
      let binary = '';
      for (let i = 0; i < bytes.byteLength; i++) {
        binary += String.fromCharCode(bytes[i]);
      }
      return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
    }

    async function generateCodeChallenge(verifier) {
      const hashed = await sha256(verifier);
      return base64encode(hashed);
    }

    async function loginWithSpotify() {
      setError('');
      setStatus('Redirecting to Spotify...');
      loginBtn.disabled = true;

      try {
        const verifier = randomString(64);
        const challenge = await generateCodeChallenge(verifier);

        localStorage.setItem(STORAGE.verifier, verifier);

        const params = new URLSearchParams({
          client_id: CLIENT_ID,
          response_type: 'code',
          redirect_uri: REDIRECT_URI,
          scope: SCOPES,
          code_challenge_method: 'S256',
          code_challenge: challenge
        });

        window.location.href = 'https://accounts.spotify.com/authorize?' + params.toString();
      } catch (err) {
        console.error(err);
        setError('Could not start Spotify login.');
        setStatus('');
        loginBtn.disabled = false;
      }
    }

    async function exchangeCodeForToken(code) {
      const verifier = localStorage.getItem(STORAGE.verifier);

      if (!verifier) {
        throw new Error('Missing PKCE code verifier.');
      }

      const body = new URLSearchParams({
        client_id: CLIENT_ID,
        grant_type: 'authorization_code',
        code: code,
        redirect_uri: REDIRECT_URI,
        code_verifier: verifier
      });

      const response = await fetch('https://accounts.spotify.com/api/token', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: body
      });

      const data = await response.json();

      if (!response.ok) {
        console.error(data);
        throw new Error(data.error_description || data.error || 'Token exchange failed.');
      }

      const expiresAt = Date.now() + (data.expires_in * 1000);

      localStorage.setItem(STORAGE.accessToken, data.access_token);
      localStorage.setItem(STORAGE.expiresAt, String(expiresAt));

      localStorage.removeItem(STORAGE.verifier);

      return data.access_token;
    }

    function getStoredToken() {
      const token = localStorage.getItem(STORAGE.accessToken);
      const expiresAt = parseInt(localStorage.getItem(STORAGE.expiresAt) || '0', 10);

      if (!token || !expiresAt) return null;

      if (Date.now() >= expiresAt) {
        localStorage.removeItem(STORAGE.accessToken);
        localStorage.removeItem(STORAGE.expiresAt);
        return null;
      }

      return token;
    }

    function clearSession() {
      localStorage.removeItem(STORAGE.accessToken);
      localStorage.removeItem(STORAGE.expiresAt);
      localStorage.removeItem(STORAGE.verifier);
    }

    async function spotifyFetch(endpoint, token) {
      const response = await fetch('https://api.spotify.com/v1' + endpoint, {
        headers: {
          Authorization: 'Bearer ' + token
        }
      });

      const data = await response.json();

      if (!response.ok) {
        console.error(data);
        throw new Error(data.error?.message || 'Spotify API request failed.');
      }

      return data;
    }

    function safeImage(item, type) {
      if (type === 'track') {
        return item?.album?.images?.[2]?.url || item?.album?.images?.[1]?.url || item?.album?.images?.[0]?.url || '';
      }
      if (type === 'artist') {
        return item?.images?.[2]?.url || item?.images?.[1]?.url || item?.images?.[0]?.url || '';
      }
      return '';
    }

    function renderUser(profile) {
      const avatar = profile?.images?.[0]?.url || '';
      avatarEl.src = avatar || 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(
        '<svg xmlns="http://www.w3.org/2000/svg" width="56" height="56"><rect width="100%" height="100%" fill="#2a2a2a"/></svg>'
      );
      userNameEl.textContent = profile.display_name || 'Spotify User';
      userSubEl.textContent = profile.email || (profile.product ? 'Plan: ' + profile.product : 'Connected');
    }

    function renderTopCards(tracks, artists) {
      const topTrack = tracks[0];
      const topArtist = artists[0];

      if (topTrack) {
        topTrackImg.src = safeImage(topTrack, 'track') || 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(
          '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><rect width="100%" height="100%" rx="12" fill="#2a2a2a"/></svg>'
        );
        topTrackTitle.textContent = topTrack.name || '';
        topTrackSub.textContent = (topTrack.artists || []).map(a => a.name).join(', ') || '';
      } else {
        topTrackImg.src = '';
        topTrackTitle.textContent = 'No top track found';
        topTrackSub.textContent = 'Try another time range';
      }

      if (topArtist) {
        topArtistImg.src = safeImage(topArtist, 'artist') || 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(
          '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><rect width="100%" height="100%" rx="12" fill="#2a2a2a"/></svg>'
        );
        topArtistTitle.textContent = topArtist.name || '';
        topArtistSub.textContent = (topArtist.genres && topArtist.genres.length)
          ? topArtist.genres.slice(0, 2).join(', ')
          : 'Top artist';
      } else {
        topArtistImg.src = '';
        topArtistTitle.textContent = 'No top artist found';
        topArtistSub.textContent = 'Try another time range';
      }
    }

    function renderTracks(tracks) {
      tracksList.innerHTML = '';

      if (!tracks.length) {
        tracksList.innerHTML = '<div class="ss-sub">No top tracks found for this time range.</div>';
        return;
      }

      tracks.slice(0, 10).forEach((track, index) => {
        const row = document.createElement('div');
        row.className = 'ss-row';

        const artists = (track.artists || []).map(a => a.name).join(', ');
        const imgSrc = safeImage(track, 'track') || 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(
          '<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48"><rect width="100%" height="100%" rx="10" fill="#2a2a2a"/></svg>'
        );

        row.innerHTML = `
          <div class="ss-rank">${index + 1}</div>
          <img class="ss-thumb" src="${imgSrc}" alt="">
          <div class="ss-meta">
            <div class="ss-name">${escapeHtml(track.name || 'Untitled')}</div>
            <div class="ss-sub">${escapeHtml(artists || 'Unknown artist')}</div>
          </div>
        `;

        tracksList.appendChild(row);
      });
    }

    function renderArtists(artists) {
      artistsList.innerHTML = '';

      if (!artists.length) {
        artistsList.innerHTML = '<div class="ss-sub">No top artists found for this time range.</div>';
        return;
      }

      artists.slice(0, 10).forEach((artist, index) => {
        const row = document.createElement('div');
        row.className = 'ss-row';

        const genreText = (artist.genres && artist.genres.length)
          ? artist.genres.slice(0, 2).join(', ')
          : 'Artist';

        const imgSrc = safeImage(artist, 'artist') || 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(
          '<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48"><rect width="100%" height="100%" rx="10" fill="#2a2a2a"/></svg>'
        );

        row.innerHTML = `
          <div class="ss-rank">${index + 1}</div>
          <img class="ss-thumb" src="${imgSrc}" alt="">
          <div class="ss-meta">
            <div class="ss-name">${escapeHtml(artist.name || 'Unnamed artist')}</div>
            <div class="ss-sub">${escapeHtml(genreText)}</div>
          </div>
        `;

        artistsList.appendChild(row);
      });
    }

    function escapeHtml(str) {
      return String(str)
        .replace(/&/g, '&')
        .replace(/</g, '<')
        .replace(/>/g, '>')
        .replace(/"/g, '"')
        .replace(/'/g, ''');
    }

    async function loadStats(token) {
      setError('');
      setStatus('Loading your Spotify stats...');

      try {
        const [profile, topTracks, topArtists] = await Promise.all([
          spotifyFetch('/me', token),
          spotifyFetch('/me/top/tracks?limit=10&time_range=' + currentRange, token),
          spotifyFetch('/me/top/artists?limit=10&time_range=' + currentRange, token)
        ]);

        renderUser(profile);
        renderTopCards(topTracks.items || [], topArtists.items || []);
        renderTracks(topTracks.items || []);
        renderArtists(topArtists.items || []);

        setActiveRange(currentRange);
        showApp();
        setStatus('Showing ' + labelForRange(currentRange) + ' stats.');
      } catch (err) {
        console.error(err);
        clearSession();
        hideApp();
        setError(
          'Could not load Spotify stats.\n\n' +
          (err.message || 'Unknown error') +
          '\n\nMake sure your Spotify app redirect URI exactly matches this page URL:\n' +
          REDIRECT_URI
        );
        setStatus('');
      }
    }

    function labelForRange(range) {
      if (range === 'short_term') return 'Last 4 Weeks';
      if (range === 'medium_term') return 'Last 6 Months';
      return 'All Time';
    }

    async function handleCallbackIfPresent() {
      const params = new URLSearchParams(window.location.search);
      const code = params.get('code');
      const error = params.get('error');

      if (error) {
        setError('Spotify login was cancelled or failed: ' + error);
        return null;
      }

      if (!code) {
        return null;
      }

      setStatus('Finishing Spotify login...');

      const token = await exchangeCodeForToken(code);

      const cleanUrl = window.location.origin + window.location.pathname;
      window.history.replaceState({}, document.title, cleanUrl);

      return token;
    }

    async function init() {
      if (!CLIENT_ID) {
        setError('Add your Spotify Client ID in the code first.');
        hideApp();
        return;
      }

      setActiveRange(currentRange);

      document.querySelectorAll('#ss-toolbar .ss-pill').forEach(btn => {
        btn.addEventListener('click', async function () {
          const token = getStoredToken();
          if (!token) {
            setError('Your session expired. Please connect again.');
            hideApp();
            return;
          }

          const range = this.getAttribute('data-range');
          setActiveRange(range);
          await loadStats(token);
        });
      });

      loginBtn.addEventListener('click', loginWithSpotify);

      refreshBtn.addEventListener('click', async function () {
        const token = getStoredToken();
        if (!token) {
          setError('Your session expired. Please connect again.');
          hideApp();
          return;
        }
        await loadStats(token);
      });

      logoutBtn.addEventListener('click', function () {
        clearSession();
        hideApp();
        setStatus('Disconnected.');
        setError('');
        tracksList.innerHTML = '';
        artistsList.innerHTML = '';
      });

      try {
        const callbackToken = await handleCallbackIfPresent();
        const token = callbackToken || getStoredToken();

        if (token) {
          await loadStats(token);
        } else {
          hideApp();
          setStatus('Click "Connect with Spotify" to begin.');
        }
      } catch (err) {
        console.error(err);
        clearSession();
        hideApp();
        setError(
          'Spotify login failed.\n\n' +
          (err.message || 'Unknown error') +
          '\n\nCheck that your Spotify redirect URI exactly matches:\n' +
          REDIRECT_URI
        );
        setStatus('');
      } finally {
        loginBtn.disabled = false;
      }
    }

    init();
  })();
});