Skip to content

Commit 790c848

Browse files
benthecoderclaude
andcommitted
Add unified Loader component and improve loading states
- Create minimal Loader component with pulsing squares matching heatmap design - Update SpotifyNowPlaying to use Loader and inherit blog font (removed monospace) - Add smooth fade-in transition to SpotifyNowPlaying - Replace search page loading bar with clean Loader component - Replace KnowledgeMap spinner with Loader component All loaders now use consistent, minimal design following Japanese aesthetic. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent afed545 commit 790c848

File tree

4 files changed

+82
-33
lines changed

4 files changed

+82
-33
lines changed

app/search/page.tsx

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import React, { useState, useEffect, Suspense } from "react";
44
import Link from "next/link";
55
import { useSearchParams, usePathname, useRouter } from "next/navigation";
6+
import Loader from "@/components/Loader";
67

78
interface SearchResult {
89
content: string;
@@ -24,13 +25,8 @@ interface SearchResult {
2425

2526
const LoadingComponent = () => {
2627
return (
27-
<div className="flex flex-col items-center space-y-4 py-8">
28-
<div className="w-48 h-1 bg-light-border/20 dark:bg-dark-tag rounded-full overflow-hidden">
29-
<div className="w-full h-full bg-light-accent dark:bg-dark-accent animate-loading-bar" />
30-
</div>
31-
<span className="text-sm text-light-accent/70 dark:text-dark-accent/70">
32-
Searching posts...
33-
</span>
28+
<div className="py-8">
29+
<Loader text="searching posts..." size="md" />
3430
</div>
3531
);
3632
};
@@ -323,14 +319,7 @@ export default function SearchPage() {
323319
<Suspense
324320
fallback={
325321
<div className="max-w-3xl mx-auto px-4 py-8">
326-
<div className="flex flex-col items-center space-y-4 py-8">
327-
<div className="w-48 h-1 bg-light-border/20 dark:bg-dark-tag rounded-full overflow-hidden">
328-
<div className="w-full h-full bg-light-accent dark:bg-dark-accent animate-loading-bar" />
329-
</div>
330-
<span className="text-sm text-light-accent/70 dark:text-dark-accent/70">
331-
Loading search...
332-
</span>
333-
</div>
322+
<Loader text="loading search..." size="md" />
334323
</div>
335324
}
336325
>

components/KnowledgeMap.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useTheme } from "next-themes";
55
import { scaleLinear } from "d3-scale";
66
import { zoom as d3Zoom } from "d3-zoom";
77
import { select } from "d3-selection";
8+
import Loader from "./Loader";
89

910
interface Article {
1011
id: string;
@@ -273,10 +274,7 @@ export default function KnowledgeMap({
273274
if (loading) {
274275
return (
275276
<div className={`flex items-center justify-center ${className}`}>
276-
<div className="text-center">
277-
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-400 mx-auto mb-2"></div>
278-
<p className="text-xs text-gray-500">loading...</p>
279-
</div>
277+
<Loader text="loading knowledge map..." size="lg" />
280278
</div>
281279
);
282280
}

components/Loader.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from "react";
2+
3+
interface LoaderProps {
4+
text?: string;
5+
size?: "sm" | "md" | "lg";
6+
}
7+
8+
export default function Loader({ text, size = "md" }: LoaderProps) {
9+
const sizeClasses = {
10+
sm: "w-[6px] h-[6px]",
11+
md: "w-[7px] h-[7px]",
12+
lg: "w-[8px] h-[8px]",
13+
};
14+
15+
const gapClasses = {
16+
sm: "gap-1",
17+
md: "gap-1.5",
18+
lg: "gap-2",
19+
};
20+
21+
return (
22+
<div className="flex flex-col items-center justify-center gap-3">
23+
<div className={`flex items-center ${gapClasses[size]}`}>
24+
<div
25+
className={`${sizeClasses[size]} rounded-[1px] bg-japanese-sumiiro dark:bg-japanese-shironezu animate-pulse`}
26+
style={{ animationDelay: "0ms", animationDuration: "1400ms" }}
27+
/>
28+
<div
29+
className={`${sizeClasses[size]} rounded-[1px] bg-japanese-sumiiro dark:bg-japanese-shironezu animate-pulse`}
30+
style={{ animationDelay: "200ms", animationDuration: "1400ms" }}
31+
/>
32+
<div
33+
className={`${sizeClasses[size]} rounded-[1px] bg-japanese-sumiiro dark:bg-japanese-shironezu animate-pulse`}
34+
style={{ animationDelay: "400ms", animationDuration: "1400ms" }}
35+
/>
36+
</div>
37+
{text && (
38+
<p className="text-xs text-japanese-sumiiro/50 dark:text-japanese-shironezu/50 font-light">
39+
{text}
40+
</p>
41+
)}
42+
</div>
43+
);
44+
}

components/SpotifyNowPlaying.tsx

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"use client";
22

3-
import React from "react";
3+
import React, { useState, useEffect } from "react";
44
import useSWR from "swr";
55
import Image from "next/image";
6+
import Loader from "./Loader";
67

78
interface NowPlayingData {
89
isPlaying: boolean;
@@ -34,33 +35,50 @@ interface RecentlyPlayedData {
3435
const fetcher = (url: string) => fetch(url).then((r) => r.json());
3536

3637
export default function SpotifyNowPlaying() {
37-
const { data: nowPlaying } = useSWR<NowPlayingData>(
38-
"/api/spotify/now-playing",
39-
fetcher,
40-
{
38+
const [isVisible, setIsVisible] = useState(false);
39+
40+
const { data: nowPlaying, isLoading: nowPlayingLoading } =
41+
useSWR<NowPlayingData>("/api/spotify/now-playing", fetcher, {
4142
refreshInterval: 60000,
42-
}
43-
);
43+
});
4444

45-
const { data: recentlyPlayed } = useSWR<RecentlyPlayedData>(
46-
!nowPlaying?.isPlaying ? "/api/spotify/recently-played" : null,
47-
fetcher,
48-
{
49-
refreshInterval: 300000,
45+
const { data: recentlyPlayed, isLoading: recentlyPlayedLoading } =
46+
useSWR<RecentlyPlayedData>(
47+
!nowPlaying?.isPlaying ? "/api/spotify/recently-played" : null,
48+
fetcher,
49+
{
50+
refreshInterval: 300000,
51+
}
52+
);
53+
54+
useEffect(() => {
55+
if (nowPlaying || recentlyPlayed) {
56+
const timer = setTimeout(() => setIsVisible(true), 100);
57+
return () => clearTimeout(timer);
5058
}
51-
);
59+
}, [nowPlaying, recentlyPlayed]);
60+
61+
const isLoading = nowPlayingLoading || recentlyPlayedLoading;
62+
63+
if (isLoading) {
64+
return (
65+
<div className="mt-8 pt-6 border-t border-japanese-shiraumenezu dark:border-dark-tag">
66+
<Loader text="loading music..." size="sm" />
67+
</div>
68+
);
69+
}
5270

5371
if (!nowPlaying && !recentlyPlayed) {
5472
return null;
5573
}
5674

5775
return (
5876
<div
77+
className={`transition-opacity duration-700 ${isVisible ? "opacity-100" : "opacity-0"}`}
5978
style={{
6079
marginTop: "32px",
6180
paddingLeft: "0px",
6281
paddingRight: "0px",
63-
fontFamily: "monospace",
6482
fontSize: "14px",
6583
color: "#888888",
6684
lineHeight: "1.6",

0 commit comments

Comments
 (0)