From 76bd04727f55a1246faf1066197f52787bf500da Mon Sep 17 00:00:00 2001
From: Marco Kruppa
Date: Fri, 25 Sep 2026 00:35:03 +0200
Subject: [PATCH] feat: show equalizer bars while loading
---
src/App.tsx | 7 ++++++-
src/components/DedupeScreen.tsx | 8 +++++++-
src/components/Loading.tsx | 14 ++++++++++++++
src/components/PlaylistPicker.tsx | 3 ++-
src/components/SwipeScreen.tsx | 8 +++++++-
src/styles.css | 16 ++++++++++++++++
6 files changed, 52 insertions(+), 4 deletions(-)
create mode 100644 src/components/Loading.tsx
diff --git a/src/App.tsx b/src/App.tsx
index 34b98f7..483b1df 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { ConnectScreen } from './components/ConnectScreen';
import { DedupeScreen } from './components/DedupeScreen';
+import { Loading } from './components/Loading';
import { PlaylistPicker, type Mode } from './components/PlaylistPicker';
import { SwipeScreen } from './components/SwipeScreen';
import { createHistoryStore } from './core/historyStore';
@@ -101,7 +102,11 @@ export function App() {
switch (screen.name) {
case 'loading':
- return Connecting to Spotify…;
+ return (
+
+ Connecting to Spotify…
+
+ );
case 'connect':
return void auth.login()} />;
case 'picker':
diff --git a/src/components/DedupeScreen.tsx b/src/components/DedupeScreen.tsx
index ca53b9d..a7c2c81 100644
--- a/src/components/DedupeScreen.tsx
+++ b/src/components/DedupeScreen.tsx
@@ -8,6 +8,7 @@ import { useSnapshot } from '../hooks/useSnapshot';
import type { SpotifyApi } from '../spotify/api';
import { AuthError, describeError } from '../spotify/errors';
import { HistoryPanel } from './HistoryPanel';
+import { Loading } from './Loading';
import { Toast } from './Toast';
interface Props {
@@ -76,7 +77,12 @@ export function DedupeScreen(props: Props) {
);
}
- if (!loaded) return Loading songs…;
+ if (!loaded)
+ return (
+
+ Loading songs…
+
+ );
return (
+
+
+
+
+
+
+ {children}
+
+ );
+}
diff --git a/src/components/PlaylistPicker.tsx b/src/components/PlaylistPicker.tsx
index 120268f..ef8ace7 100644
--- a/src/components/PlaylistPicker.tsx
+++ b/src/components/PlaylistPicker.tsx
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { editablePlaylists, LIKED_SONGS_ID, type PlaylistSummary } from '../core/playlists';
import type { SpotifyApi } from '../spotify/api';
import { AuthError, describeError } from '../spotify/errors';
+import { Loading } from './Loading';
export type Mode = 'swipe' | 'dedupe';
@@ -84,7 +85,7 @@ export function PlaylistPicker({ api, mode, onModeChange, onPick, onAuthLost, on
{error}
)}
- {!error && !playlists && Loading your playlists…
}
+ {!error && !playlists && Loading your playlists…}
{playlists?.length === 0 && You don't own or collaborate on any playlists yet.
}
{playlists && (
diff --git a/src/components/SwipeScreen.tsx b/src/components/SwipeScreen.tsx
index 6c0509e..0e6463c 100644
--- a/src/components/SwipeScreen.tsx
+++ b/src/components/SwipeScreen.tsx
@@ -12,6 +12,7 @@ import { AuthError, describeError } from '../spotify/errors';
import type { WebPlayer } from '../spotify/player';
import { DoneScreen } from './DoneScreen';
import { HistoryPanel } from './HistoryPanel';
+import { Loading } from './Loading';
import { PlayerControls } from './PlayerControls';
import { SwipeCard, type SwipeDirection } from './SwipeCard';
import { Toast } from './Toast';
@@ -63,7 +64,12 @@ export function SwipeScreen(props: Props) {
);
}
- if (!loaded) return Loading songs…;
+ if (!loaded)
+ return (
+
+ Loading songs…
+
+ );
return ;
}
diff --git a/src/styles.css b/src/styles.css
index 1f209f9..f0ce277 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -24,6 +24,22 @@ button.link:hover { color: var(--text); }
.error { color: var(--red); }
.center { min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1rem; padding: 1rem; text-align: center; }
.row { display: flex; gap: 0.75rem; justify-content: center; }
+.loading { display: flex; flex-direction: column; align-items: center; gap: 0.9rem; padding: 1.5rem 0; color: var(--muted); }
+.eq { display: flex; align-items: flex-end; gap: 4px; height: 28px; }
+.eq span { width: 6px; height: 100%; border-radius: 3px; background: var(--green); transform-origin: bottom; animation: eq 1s ease-in-out infinite; }
+.eq span:nth-child(2) { animation-duration: 0.8s; animation-delay: -0.4s; }
+.eq span:nth-child(3) { animation-duration: 1.1s; animation-delay: -0.2s; }
+.eq span:nth-child(4) { animation-duration: 0.9s; animation-delay: -0.6s; }
+@keyframes eq {
+ 0%, 100% { transform: scaleY(0.3); }
+ 50% { transform: scaleY(1); }
+}
+@media (prefers-reduced-motion: reduce) {
+ .eq span { animation: none; transform: scaleY(0.5); }
+ .eq span:nth-child(2) { transform: scaleY(0.9); }
+ .eq span:nth-child(3) { transform: scaleY(0.4); }
+ .eq span:nth-child(4) { transform: scaleY(0.7); }
+}
/* Playlist picker */
.picker { max-width: 1100px; margin: 0 auto; padding: 1.5rem; }