Music analytics dashboard providing lyrics analysis, song data visualization, and video discovery using Spotify design system.
UX/UI Designer, Frontend Engineer
ReactJS, TypeScript, Python, NextJS, TailwindCSS, Figma, Motion, p5JS, AWS Lambda
Jan 2024 - May 2025
Total streams is derived by scraping mystreamcount.com.
Most streamed country is derived by scraping kworb.net.
Chart data is derived by scraping mystreamcount.com.
Longevity is derived by an algorithm based on recent streams ratio via peak performance.
Lyrics score is derived Perplexity API based on criteria of depth, meaning, and complexity.
Popularity is derived from Spotify Web API results.
Lyrics is derived by passing Spotify ISRC through MusixMatch API.
Lyrics analysis is derived by passing lyrics, artist and album name into Perplexity API.
Scrolling to desired lyric was done by calculating the pixel distance from top of container and storing each element inside a ref array.
Related content is derived by calling Invidious API.
Filtering is done on the frontend via string matching.
export const debounce = (fn, wait) => {let timeout;let pendingPromise = null;return function (...args) {// eslint-disable-next-line @typescript-eslint/no-this-aliasconst context = this;// If there's already a pending promise for this debounce, return itif (pendingPromise) return pendingPromise;// Create a new promise that will resolve with the result of fnpendingPromise = new Promise((resolve) => {if (timeout) clearTimeout(timeout);timeout = setTimeout(() => {const result = fn.apply(context, args);pendingPromise = null;resolve(result);}, wait);});return pendingPromise;};};
By debouncing API calls, I can reduce the number of API calls to only the most recent changes to state. This is a common technique to improve performance and reduce server load by preventing excessive API requests during rapid user interactions like typing.
// Example: Using TanStack Query for API callsconst { data, isLoading, error } = useQuery({queryKey: ['songData', songId],queryFn: () => fetchSongData(songId),staleTime: 5 * 60 * 1000, // 5 minutescacheTime: 10 * 60 * 1000, // 10 minutes});
TanStack Query provides intelligent caching, background refetching, and optimistic updates. By setting appropriate stale and cache times, I reduce redundant API calls and improve user experience with instant data loading from cache.
import { cache } from 'react';// Cache expensive operations across component treeconst getCachedSongAnalysis = cache(async (songId) => {const response = await fetch(`/api/analysis/${songId}`);return response.json();});// Multiple components can call this without duplicate requestsconst analysis = await getCachedSongAnalysis(songId);
React Cache eliminates duplicate requests during server-side rendering, ensuring expensive operations like AI analysis are only performed once per request cycle.
// Server Component (RSC) - runs on serverexport default async function SongPage({ params }) {// Pre-fetch data on serverconst songData = await fetchSongData(params.id);return (<div><SongDetails data={songData} /><InteractiveLyrics songId={params.id} /></div>);}// Client Component (CSR) - runs in browser'use client';export default function InteractiveLyrics({ songId }) {const [selectedLine, setSelectedLine] = useState(null);// Interactive features here}
By strategically combining Server Components for initial data fetching and Client Components for interactive features, we achieve optimal performance with fast initial loads and rich interactivity.
// Dynamic routing with Next.js// app/song/[id]/page.jsexport default function SongPage({ params }) {const songId = params.id;return (<div><SongDetails id={songId} />{/* URLs like /song/4uLU6hMCjMI75M1A2tKUQC */}</div>);}// Generate URLs from Spotify track IDsconst generateSongUrl = (trackId) => `/song/${trackId}`;
Following Spotify's URL pattern, each song gets a unique URL based on its Spotify track ID, enabling direct sharing and bookmarking of specific songs.
// Extract colors from album artworkconst getColorPalette = async (imageUrl) => {const img = new Image();img.crossOrigin = 'anonymous';img.src = imageUrl;return new Promise((resolve) => {img.onload = () => {const canvas = document.createElement('canvas');const ctx = canvas.getContext('2d');canvas.width = img.width;canvas.height = img.height;ctx.drawImage(img, 0, 0);// Extract dominant colors using color quantizationconst colors = extractDominantColors(ctx.getImageData(0, 0, canvas.width, canvas.height));resolve(colors);};});};
Like Spotify, I extract dominant colors from album artwork to create dynamic color schemes that change based on the currently viewed song, creating a cohesive visual experience.
There was an issue where the search input would lose focus every time the search element would render results. This was caused by the entire page re-rendering when new search results were fetched, causing the DOM to rebuild and lose focus state.
Vercel's serverless functions don't support mixed runtimes in a single deployment. Since the scraping logic was written in Python but the web app used Node.js, I had to separate the Python scrapers into AWS Lambda functions with API Gateway endpoints.
Multiple APIs (MusixMatch, Perplexity, Invidious) had different rate limits. I implemented a request queue system with exponential backoff to handle rate limiting gracefully and ensure data consistency.