Skip to content

Commit b883792

Browse files
nullcoderClaude
andauthored
feat: implement VersionSelector component (#71) (#99)
- Add VersionSelector dropdown with version list and timestamps - Implement relative time formatting (e.g., "2 hours ago", "Yesterday") - Show current version highlighting and original version badge - Support PIN-edited version indicators with lock icon - Add CompactVersionSelector variant for mobile layouts - Handle loading and disabled states - Implement keyboard navigation and accessibility - Add comprehensive tests (with Radix UI mocks) - Create interactive demo page 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <claude@ghostpaste.dev>
1 parent 65b8554 commit b883792

File tree

5 files changed

+993
-11
lines changed

5 files changed

+993
-11
lines changed

app/demo/version-selector/page.tsx

Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import {
5+
VersionSelector,
6+
CompactVersionSelector,
7+
formatVersionTime,
8+
type Version,
9+
} from "@/components/ui/version-selector";
10+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
11+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
12+
import { Badge } from "@/components/ui/badge";
13+
import { toast } from "sonner";
14+
15+
// Generate mock versions with realistic data
16+
function generateMockVersions(count: number): Version[] {
17+
const versions: Version[] = [];
18+
const now = Date.now();
19+
20+
for (let i = count; i > 0; i--) {
21+
// Create versions with varying time intervals
22+
let createdAt: number;
23+
if (i === count) {
24+
// Current version - 2 hours ago
25+
createdAt = now - 2 * 60 * 60 * 1000;
26+
} else if (i === count - 1) {
27+
// Previous version - 1 day ago
28+
createdAt = now - 24 * 60 * 60 * 1000;
29+
} else if (i === count - 2) {
30+
// 3 days ago
31+
createdAt = now - 3 * 24 * 60 * 60 * 1000;
32+
} else if (i === count - 3) {
33+
// 1 week ago
34+
createdAt = now - 7 * 24 * 60 * 60 * 1000;
35+
} else {
36+
// Older versions - weeks/months ago
37+
createdAt = now - (count - i + 7) * 24 * 60 * 60 * 1000;
38+
}
39+
40+
versions.push({
41+
version: i,
42+
created_at: new Date(createdAt).toISOString(),
43+
size: Math.floor(Math.random() * 5000) + 500,
44+
file_count: Math.floor(Math.random() * 5) + 1,
45+
edited_with_pin: i > 1 && Math.random() > 0.7, // 30% chance for non-original versions
46+
});
47+
}
48+
49+
return versions;
50+
}
51+
52+
export default function VersionSelectorDemo() {
53+
const [currentVersion, setCurrentVersion] = React.useState(5);
54+
const [compactVersion, setCompactVersion] = React.useState(3);
55+
const [loadingDemo, setLoadingDemo] = React.useState(false);
56+
57+
const fewVersions = generateMockVersions(3);
58+
const manyVersions = generateMockVersions(12);
59+
const singleVersion = generateMockVersions(1);
60+
61+
const handleVersionChange = (version: number) => {
62+
setLoadingDemo(true);
63+
toast.info(`Loading version ${version}...`);
64+
65+
setTimeout(() => {
66+
setCurrentVersion(version);
67+
setLoadingDemo(false);
68+
toast.success(`Switched to version ${version}`);
69+
}, 1000);
70+
};
71+
72+
return (
73+
<div className="container mx-auto py-8">
74+
<h1 className="mb-8 text-3xl font-bold">Version Selector Demo</h1>
75+
76+
<Tabs defaultValue="standard" className="w-full">
77+
<TabsList className="grid w-full grid-cols-4">
78+
<TabsTrigger value="standard">Standard</TabsTrigger>
79+
<TabsTrigger value="compact">Compact</TabsTrigger>
80+
<TabsTrigger value="states">States</TabsTrigger>
81+
<TabsTrigger value="formatting">Time Format</TabsTrigger>
82+
</TabsList>
83+
84+
<TabsContent value="standard" className="space-y-4">
85+
<Card>
86+
<CardHeader>
87+
<CardTitle>Standard Version Selector</CardTitle>
88+
</CardHeader>
89+
<CardContent className="space-y-6">
90+
<div>
91+
<h3 className="mb-3 text-lg font-semibold">
92+
Multiple Versions (12 versions)
93+
</h3>
94+
<VersionSelector
95+
currentVersion={currentVersion}
96+
versions={manyVersions}
97+
onVersionChangeAction={handleVersionChange}
98+
loading={loadingDemo}
99+
/>
100+
<p className="text-muted-foreground mt-2 text-sm">
101+
Currently viewing version {currentVersion}
102+
</p>
103+
</div>
104+
105+
<div>
106+
<h3 className="mb-3 text-lg font-semibold">
107+
Few Versions (3 versions)
108+
</h3>
109+
<VersionSelector
110+
currentVersion={2}
111+
versions={fewVersions}
112+
onVersionChangeAction={(v) =>
113+
toast.info(`Selected version ${v}`)
114+
}
115+
/>
116+
</div>
117+
118+
<div>
119+
<h3 className="mb-3 text-lg font-semibold">
120+
Single Version (no dropdown)
121+
</h3>
122+
<VersionSelector
123+
currentVersion={1}
124+
versions={singleVersion}
125+
onVersionChangeAction={(v) =>
126+
toast.info(`Selected version ${v}`)
127+
}
128+
/>
129+
</div>
130+
131+
<div>
132+
<h3 className="mb-3 text-lg font-semibold">No Versions</h3>
133+
<VersionSelector
134+
currentVersion={1}
135+
versions={[]}
136+
onVersionChangeAction={(v) =>
137+
toast.info(`Selected version ${v}`)
138+
}
139+
/>
140+
<p className="text-muted-foreground text-sm">
141+
Returns null when no versions
142+
</p>
143+
</div>
144+
</CardContent>
145+
</Card>
146+
</TabsContent>
147+
148+
<TabsContent value="compact" className="space-y-4">
149+
<Card>
150+
<CardHeader>
151+
<CardTitle>Compact Version Selector</CardTitle>
152+
</CardHeader>
153+
<CardContent className="space-y-6">
154+
<div>
155+
<h3 className="mb-3 text-lg font-semibold">
156+
Compact Mode (Mobile-friendly)
157+
</h3>
158+
<CompactVersionSelector
159+
currentVersion={compactVersion}
160+
versions={manyVersions.slice(0, 5)}
161+
onVersionChangeAction={setCompactVersion}
162+
/>
163+
<p className="text-muted-foreground mt-2 text-sm">
164+
Currently viewing version {compactVersion}
165+
</p>
166+
</div>
167+
168+
<div>
169+
<h3 className="mb-3 text-lg font-semibold">
170+
Compact with Custom Styling
171+
</h3>
172+
<div className="flex gap-2">
173+
<CompactVersionSelector
174+
currentVersion={2}
175+
versions={fewVersions}
176+
onVersionChangeAction={(v) =>
177+
toast.info(`Selected version ${v}`)
178+
}
179+
className="w-[100px]"
180+
/>
181+
<span className="text-muted-foreground text-sm">
182+
Custom width
183+
</span>
184+
</div>
185+
</div>
186+
</CardContent>
187+
</Card>
188+
</TabsContent>
189+
190+
<TabsContent value="states" className="space-y-4">
191+
<Card>
192+
<CardHeader>
193+
<CardTitle>Different States</CardTitle>
194+
</CardHeader>
195+
<CardContent className="space-y-6">
196+
<div>
197+
<h3 className="mb-3 text-lg font-semibold">Loading State</h3>
198+
<VersionSelector
199+
currentVersion={3}
200+
versions={fewVersions}
201+
onVersionChangeAction={() => {}}
202+
loading={true}
203+
/>
204+
<p className="text-muted-foreground mt-2 text-sm">
205+
Selector is disabled during loading
206+
</p>
207+
</div>
208+
209+
<div>
210+
<h3 className="mb-3 text-lg font-semibold">Disabled State</h3>
211+
<VersionSelector
212+
currentVersion={3}
213+
versions={fewVersions}
214+
onVersionChangeAction={() => {}}
215+
disabled={true}
216+
/>
217+
</div>
218+
219+
<div>
220+
<h3 className="mb-3 text-lg font-semibold">
221+
With PIN-edited Versions
222+
</h3>
223+
<VersionSelector
224+
currentVersion={3}
225+
versions={[
226+
...fewVersions,
227+
{
228+
version: 4,
229+
created_at: new Date(
230+
Date.now() - 60 * 60 * 1000
231+
).toISOString(),
232+
size: 2048,
233+
file_count: 3,
234+
edited_with_pin: true,
235+
},
236+
]}
237+
onVersionChangeAction={(v) =>
238+
toast.info(`Selected version ${v}`)
239+
}
240+
/>
241+
<p className="text-muted-foreground mt-2 text-sm">
242+
Lock icon indicates PIN-protected edits
243+
</p>
244+
</div>
245+
</CardContent>
246+
</Card>
247+
</TabsContent>
248+
249+
<TabsContent value="formatting" className="space-y-4">
250+
<Card>
251+
<CardHeader>
252+
<CardTitle>Time Formatting Examples</CardTitle>
253+
</CardHeader>
254+
<CardContent className="space-y-4">
255+
{[
256+
{ time: 30, label: "30 seconds ago" },
257+
{ time: 60, label: "1 minute ago" },
258+
{ time: 300, label: "5 minutes ago" },
259+
{ time: 3600, label: "1 hour ago" },
260+
{ time: 7200, label: "2 hours ago" },
261+
{ time: 86400, label: "Yesterday" },
262+
{ time: 259200, label: "3 days ago" },
263+
{ time: 604800, label: "1 week ago" },
264+
{ time: 2592000, label: "1 month ago" },
265+
{ time: 31536000, label: "1 year ago" },
266+
].map(({ time, label }) => {
267+
const timestamp = new Date(
268+
Date.now() - time * 1000
269+
).toISOString();
270+
const formatted = formatVersionTime(timestamp);
271+
return (
272+
<div key={time} className="flex items-center gap-4">
273+
<Badge variant="outline" className="w-32">
274+
{label}
275+
</Badge>
276+
<span className="font-mono text-sm">{formatted}</span>
277+
</div>
278+
);
279+
})}
280+
</CardContent>
281+
</Card>
282+
</TabsContent>
283+
</Tabs>
284+
285+
<Card className="mt-8">
286+
<CardHeader>
287+
<CardTitle>Usage Example</CardTitle>
288+
</CardHeader>
289+
<CardContent>
290+
<pre className="bg-muted overflow-x-auto rounded-lg p-4 text-sm">
291+
{`// Standard version selector
292+
<VersionSelector
293+
currentVersion={3}
294+
versions={versions}
295+
onVersionChangeAction={handleVersionChange}
296+
loading={isLoading}
297+
/>
298+
299+
// Compact version selector
300+
<CompactVersionSelector
301+
currentVersion={3}
302+
versions={versions}
303+
onVersionChangeAction={handleVersionChange}
304+
/>
305+
306+
// Version data structure
307+
const version: Version = {
308+
version: 3,
309+
created_at: "2025-01-15T10:30:00Z",
310+
size: 2048,
311+
file_count: 2,
312+
edited_with_pin: true
313+
};
314+
315+
// Format timestamp
316+
const formatted = formatVersionTime(timestamp);`}
317+
</pre>
318+
</CardContent>
319+
</Card>
320+
321+
<Card className="mt-8">
322+
<CardHeader>
323+
<CardTitle>Features</CardTitle>
324+
</CardHeader>
325+
<CardContent className="space-y-2">
326+
<p>✓ Dropdown showing version list with timestamps</p>
327+
<p>✓ Current version clearly highlighted</p>
328+
<p>✓ Human-readable relative timestamps</p>
329+
<p>✓ Shows version metadata (file count, PIN edits)</p>
330+
<p>✓ Maximum 50 versions support</p>
331+
<p>✓ Reverse chronological order (newest first)</p>
332+
<p>✓ Loading and disabled states</p>
333+
<p>✓ Mobile-friendly compact variant</p>
334+
<p>✓ Keyboard navigable</p>
335+
<p>✓ Accessible with ARIA labels</p>
336+
</CardContent>
337+
</Card>
338+
</div>
339+
);
340+
}

0 commit comments

Comments
 (0)