Skip to content

Commit c11da34

Browse files
committed
feat: enhance HomePage and LikeButton functionality
- Replaced LikeButton with LikeWrapper to manage initial like count from Redis - Improved HomePage layout with additional padding for better readability - Added floating heart animation to LikeButton for enhanced user interaction - Updated LikeButton to handle optimistic updates and manage floating hearts state - Adjusted footer layout for better responsiveness and alignment
1 parent 7d7ba11 commit c11da34

4 files changed

Lines changed: 159 additions & 87 deletions

File tree

demo/app/page.tsx

Lines changed: 47 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
import LikeButton from "@/components/like-button";
2-
import { getRedis } from "@/lib/redis";
3-
import { incrementLikesAction } from "./actions";
41
import {
52
codeExampleToJsx,
63
dockerCommand,
@@ -23,24 +20,22 @@ import { Terminal } from "lucide-react";
2320
import Accordion from "@/components/accordion";
2421
import { RiDiscordFill, RiGithubFill } from "@remixicon/react";
2522
import Link from "next/link";
23+
import LikeWrapper from "@/components/like-wrapper";
2624

2725
export default async function HomePage() {
28-
const redis = getRedis();
29-
const initialCount = Number(await redis.get("demo:likes")) || 0;
30-
3126
return (
3227
<main className="border-x border-gray-700 min-h-screen mx-auto w-[90vw] max-w-4xl pt-[10vh]">
3328
<hr className="absolute left-0 right-0 border-gray-700" />
3429
<header className="flex flex-col gap-4 items-center text-center justify-center h-[80vh]">
35-
<h1 className="text-5xl font-bold leading-tight">
30+
<h1 className="text-5xl font-bold leading-tight px-4">
3631
Type-safe live updates <br />
3732
in minutes
3833
</h1>
39-
<p className="text-lg text-gray-500">
34+
<p className="text-lg text-gray-500 px-4">
4035
Zero-config SSE. Shared types. Simple client & server helpers.
4136
</p>
4237

43-
<LikeButton initialCount={initialCount} onLike={incrementLikesAction} />
38+
<LikeWrapper />
4439
</header>
4540

4641
<hr className="absolute left-0 right-0 border-gray-700" />
@@ -97,7 +92,7 @@ export default async function HomePage() {
9792
</div>
9893
</section>
9994

100-
<h2 className="text-4xl font-bold mt-32 mb-8 w-full text-center">
95+
<h2 className="text-4xl font-bold mt-32 mb-8 w-full text-center px-4">
10196
Let&rsquo;s get started!
10297
</h2>
10398
<hr className="absolute left-0 right-0 border-gray-700" />
@@ -159,19 +154,21 @@ export default async function HomePage() {
159154
</div>
160155

161156
<div className="px-4 py-8 flex flex-col gap-4 border-b border-gray-700">
162-
<h3 className="text-xl font-mono text-gray-300">4. You&rsquo;re done</h3>
157+
<h3 className="text-xl font-mono text-gray-300">
158+
4. You&rsquo;re done
159+
</h3>
163160
<p className="text-gray-400">
164161
Perfect! You&rsquo;re now free to add more events to your app.
165162
</p>
166163
<p className="text-gray-400">
167164
You may want more customizations, like authentication or topics.
168165
<br />
169-
Follow the link below to learn more.
166+
Follow the <code>Deep dive!</code> section below to learn more.
170167
</p>
171168
</div>
172169
</section>
173170

174-
<h2 className="text-4xl font-bold mt-32 mb-8 w-full text-center">
171+
<h2 className="text-4xl font-bold mt-32 mb-8 w-full text-center px-4">
175172
Deep dive!
176173
</h2>
177174
<hr className="absolute left-0 right-0 border-gray-700" />
@@ -246,27 +243,27 @@ export default async function HomePage() {
246243
<hr className="w-full border-gray-700" />
247244
</section>
248245

249-
<h2 className="text-4xl font-bold mt-32 mb-8 w-full text-center">
246+
<h2 className="text-4xl font-bold mt-32 mb-8 w-full text-center px-4">
250247
Missing something?
251248
</h2>
252249
<hr className="absolute left-0 right-0 border-gray-700" />
253250

254-
<section className="flex flex-col items-center justify-center pt-24 pb-36 gap-4">
251+
<section className="flex flex-col items-center justify-center pt-24 pb-36 gap-4 px-4">
255252
<p className="text-gray-400 text-center max-w-2xl">
256253
If you&rsquo;re missing something,
257254
<br />
258255
feel free to open an issue on the GitHub repository or discuss it on
259256
the Discord server.
260257
</p>
261-
<div className="flex items-center gap-4">
258+
<div className="flex items-center gap-4 flex-wrap justify-center">
262259
<Link
263260
href="https://github.com/impulse-studio/realtime"
264261
target="_blank"
265262
className="bg-white text-black px-6 py-3 rounded-2xl flex items-center gap-2 font-medium hover:bg-gray-200 relative cursor-pointer"
266263
>
267264
<div className="absolute left-0.5 right-0.5 bottom-0.5 rounded-b-2xl bg-gradient-to-t pointer-events-none h-4 from-black to-transparent opacity-20" />
268265
<RiGithubFill />
269-
Open in GitHub
266+
<span className="text-nowrap">Open in GitHub</span>
270267
</Link>
271268
<Link
272269
href="https://discord.gg/bBWXedJwWN"
@@ -275,14 +272,14 @@ export default async function HomePage() {
275272
>
276273
<div className="absolute left-0.5 right-0.5 bottom-0.5 rounded-b-2xl bg-gradient-to-t pointer-events-none h-4 from-black to-transparent opacity-20" />
277274
<RiDiscordFill />
278-
Open in Discord
275+
<span className="text-nowrap">Open in Discord</span>
279276
</Link>
280277
</div>
281278
</section>
282279

283280
<hr className="absolute left-0 right-0 border-gray-700" />
284-
<footer className="flex items-center p-8 gap-4">
285-
<span className="text-gray-400 me-auto">
281+
<footer className="flex items-center p-8 gap-4 justify-between flex-col sm:flex-row">
282+
<span className="text-gray-400">
286283
Made with ❤️ by{" "}
287284
<Link
288285
href="https://impulselab.ai"
@@ -293,34 +290,36 @@ export default async function HomePage() {
293290
</Link>
294291
</span>
295292

296-
<Link
297-
href="https://github.com/impulse-studio/realtime"
298-
target="_blank"
299-
className="text-amber-400 hover:text-amber-600"
300-
>
301-
GitHub
302-
</Link>
303-
<Link
304-
href="https://discord.gg/bBWXedJwWN"
305-
target="_blank"
306-
className="text-amber-400 hover:text-amber-600"
307-
>
308-
Discord
309-
</Link>
310-
<Link
311-
href="https://x.com/impulselab_ai"
312-
target="_blank"
313-
className="text-amber-400 hover:text-amber-600"
314-
>
315-
X
316-
</Link>
317-
<Link
318-
href="https://linkedin.com/company/impulselab"
319-
target="_blank"
320-
className="text-amber-400 hover:text-amber-600"
321-
>
322-
LinkedIn
323-
</Link>
293+
<div className="flex items-center gap-4 flex-wrap justify-center">
294+
<Link
295+
href="https://github.com/impulse-studio/realtime"
296+
target="_blank"
297+
className="text-amber-400 hover:text-amber-600"
298+
>
299+
GitHub
300+
</Link>
301+
<Link
302+
href="https://discord.gg/bBWXedJwWN"
303+
target="_blank"
304+
className="text-amber-400 hover:text-amber-600"
305+
>
306+
Discord
307+
</Link>
308+
<Link
309+
href="https://x.com/impulselab_ai"
310+
target="_blank"
311+
className="text-amber-400 hover:text-amber-600"
312+
>
313+
X
314+
</Link>
315+
<Link
316+
href="https://linkedin.com/company/impulselab"
317+
target="_blank"
318+
className="text-amber-400 hover:text-amber-600"
319+
>
320+
LinkedIn
321+
</Link>
322+
</div>
324323
</footer>
325324
</main>
326325
);

demo/components/docker-icon.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export default function DockerIcon() {
22
return (
33
<svg
4+
className="size-6 min-w-6 min-h-6"
45
width="24"
56
height="24"
67
viewBox="0 0 64 64"

demo/components/like-button.tsx

Lines changed: 101 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,65 @@
11
"use client";
22

3-
import { useState, useEffect } from "react";
4-
import { motion, useAnimate } from "motion/react";
3+
import { useState, useEffect, useCallback, useRef } from "react";
4+
import { motion, useAnimate, AnimatePresence } from "motion/react";
55
import { getClientRealtime } from "../lib/realtime/client";
66
import { HeartIcon } from "lucide-react";
77

8+
interface FloatingHeart {
9+
id: string;
10+
left: number;
11+
}
12+
813
interface LikeButtonProps {
914
initialCount: number;
10-
onLike: (formData: FormData) => Promise<void>;
15+
onLike: () => Promise<void>;
1116
}
1217

1318
export default function LikeButton({ initialCount, onLike }: LikeButtonProps) {
1419
const [count, setCount] = useState(initialCount);
1520
const [scope, animate] = useAnimate();
21+
const [floatingHearts, setFloatingHearts] = useState<FloatingHeart[]>([]);
22+
const timeoutsRef = useRef<Set<NodeJS.Timeout>>(new Set());
23+
24+
const spawnFloatingHeart = useCallback(() => {
25+
const id = `heart-${Date.now()}-${Math.random()}`;
26+
const left = Math.random() * 100;
27+
28+
setFloatingHearts((prev) => [...prev, { id, left }]);
29+
30+
const timeout = setTimeout(() => {
31+
setFloatingHearts((prev) => prev.filter((heart) => heart.id !== id));
32+
timeoutsRef.current.delete(timeout);
33+
}, 2500);
34+
35+
timeoutsRef.current.add(timeout);
36+
}, []);
1637

1738
useEffect(() => {
1839
const client = getClientRealtime();
1940

2041
const unsubscribe = client.subscribe("demo:likes:updated", (event) => {
21-
try {
22-
setCount(event.payload.count);
23-
} catch (error) {
24-
console.error("Error handling likes update:", error);
25-
}
42+
spawnFloatingHeart();
43+
setCount((prev) => Math.max(prev, event.payload.count));
2644
});
2745

46+
return unsubscribe;
47+
}, [spawnFloatingHeart]);
48+
49+
useEffect(() => {
2850
return () => {
29-
unsubscribe();
51+
timeoutsRef.current.forEach((timeout) => clearTimeout(timeout));
52+
timeoutsRef.current.clear();
3053
};
3154
}, []);
3255

56+
const handleOptimisticLike = useCallback(async () => {
57+
setCount((prev) => prev + 1);
58+
await onLike();
59+
}, [count, onLike]);
60+
3361
return (
34-
<div className="relative mt-8 border rounded-2xl w-80 h-48 mx-4 flex items-center justify-center border-gray-800 bg-gray-950 bg-[linear-gradient(rgba(75,85,99,0.3)_1px,transparent_1px),linear-gradient(90deg,rgba(75,85,99,0.3)_1px,transparent_1px)] bg-[size:24px_24px] bg-center">
62+
<div className="relative mt-8 border rounded-2xl w-80 h-48 mx-4 flex items-center justify-center border-gray-800 bg-gray-950 bg-[linear-gradient(rgba(75,85,99,0.3)_1px,transparent_1px),linear-gradient(90deg,rgba(75,85,99,0.3)_1px,transparent_1px)] bg-[size:24px_24px] bg-center overflow-hidden">
3563
<div className="absolute right-4 bottom-2 flex">
3664
<svg
3765
width="34"
@@ -61,35 +89,69 @@ export default function LikeButton({ initialCount, onLike }: LikeButtonProps) {
6189
</p>
6290
</div>
6391

64-
<form action={onLike}>
65-
<motion.button
66-
ref={scope}
67-
type="submit"
68-
className="bg-gradient-to-t from-amber-700 to-amber-600 text-white rounded-2xl px-4 py-2 flex items-center gap-2 relative hover:from-amber-600 hover:to-amber-500 cursor-pointer"
69-
whileTap={{ scale: 0.95 }}
70-
onTapStart={() => {
71-
const dx = (Math.random() - 0.5) * 8;
72-
const dy = (Math.random() - 0.5) * 8;
73-
const dr = (Math.random() - 0.5) * 10;
74-
animate(
75-
scope.current,
76-
{
77-
x: [0, dx, -dx, 0],
78-
y: [0, dy, -dy, 0],
79-
rotate: [0, dr, -dr, 0],
80-
},
81-
{
82-
duration: 0.25,
83-
ease: [0.16, 1, 0.3, 1],
84-
}
85-
);
86-
}}
87-
>
88-
<div className="absolute left-0.5 right-0.5 bottom-0.5 rounded-b-2xl bg-gradient-to-t pointer-events-none h-4 from-white to-transparent opacity-30" />
89-
<HeartIcon fill="currentColor" className="w-4 h-4" />
90-
{count}
91-
</motion.button>
92-
</form>
92+
<AnimatePresence>
93+
{floatingHearts.map((heart) => (
94+
<motion.div
95+
key={heart.id}
96+
className="absolute pointer-events-none"
97+
style={{
98+
left: `${heart.left}%`,
99+
bottom: "-24px",
100+
}}
101+
initial={{
102+
opacity: 0.8,
103+
scale: 1.2,
104+
y: 0,
105+
x: 0,
106+
}}
107+
animate={{
108+
opacity: 0,
109+
scale: 0.8,
110+
y: -200 - Math.random() * 100,
111+
x: (Math.random() - 0.5) * 40,
112+
}}
113+
exit={{
114+
opacity: 0,
115+
scale: 0.3,
116+
}}
117+
transition={{
118+
duration: 2.5,
119+
ease: [0.16, 1, 0.3, 1],
120+
}}
121+
>
122+
<HeartIcon fill="currentColor" className="w-6 h-6 text-amber-500" />
123+
</motion.div>
124+
))}
125+
</AnimatePresence>
126+
127+
<motion.button
128+
ref={scope}
129+
type="button"
130+
onClick={handleOptimisticLike}
131+
className="bg-gradient-to-t from-amber-700 to-amber-600 text-white rounded-2xl px-4 py-2 flex items-center gap-2 relative hover:from-amber-600 hover:to-amber-500 cursor-pointer"
132+
whileTap={{ scale: 0.95 }}
133+
onTapStart={() => {
134+
const dx = (Math.random() - 0.5) * 8;
135+
const dy = (Math.random() - 0.5) * 8;
136+
const dr = (Math.random() - 0.5) * 10;
137+
animate(
138+
scope.current,
139+
{
140+
x: [0, dx, -dx, 0],
141+
y: [0, dy, -dy, 0],
142+
rotate: [0, dr, -dr, 0],
143+
},
144+
{
145+
duration: 0.25,
146+
ease: [0.16, 1, 0.3, 1],
147+
}
148+
);
149+
}}
150+
>
151+
<div className="absolute left-0.5 right-0.5 bottom-0.5 rounded-b-2xl bg-gradient-to-t pointer-events-none h-4 from-white to-transparent opacity-30" />
152+
<HeartIcon fill="currentColor" className="w-4 h-4" />
153+
{count}
154+
</motion.button>
93155
</div>
94156
);
95157
}

demo/components/like-wrapper.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { incrementLikesAction } from "@/app/actions";
2+
import LikeButton from "./like-button";
3+
import { getRedis } from "@/lib/redis";
4+
5+
export default async function LikeWrapper() {
6+
const initialCount = Number(await getRedis().get("demo:likes")) || 0;
7+
return (
8+
<LikeButton initialCount={initialCount} onLike={incrementLikesAction} />
9+
);
10+
}

0 commit comments

Comments
 (0)