Skip to content

Commit 68dbafa

Browse files
committed
feat(history): enhance version history display with session grouping and time formatting
1 parent 5efde9f commit 68dbafa

3 files changed

Lines changed: 484 additions & 121 deletions

File tree

Lines changed: 272 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,297 @@
11
import SidebarLoader from '@components/skeleton/SidebarLoader'
22
import { useStore } from '@stores'
3-
import React, { useEffect } from 'react'
4-
import { MdCalendarToday, MdAccessTime } from 'react-icons/md'
3+
import { useState } from 'react'
4+
import { MdExpandMore, MdExpandLess } from 'react-icons/md'
55
import {
6-
dayContainsCurrentVersion,
7-
groupContainsCurrentVersion,
8-
groupHistoryByDay
6+
groupSessionsByDay,
7+
dayContainsVersion,
8+
sessionContainsVersion,
9+
formatTime,
10+
formatRelativeTime,
11+
type VersionSession
912
} from '../helpers'
1013
import { useVersionContent } from '../hooks/useVersionContent'
1114
import { twMerge } from 'tailwind-merge'
1215
import { useModal } from '@components/ui/ModalDrawer'
16+
1317
const Sidebar = ({ className }: { className?: string }) => {
1418
const { loadingHistory, activeHistory, historyList } = useStore((state) => state)
1519
const { watchVersionContent } = useVersionContent()
1620
const { close: closeModal } = useModal() || {}
1721

1822
if (loadingHistory || !activeHistory) return <SidebarLoader />
1923

24+
const groupedByDay = groupSessionsByDay(historyList)
25+
const latestVersion = historyList[0]?.version
26+
2027
return (
2128
<div
22-
className={twMerge('sidebar h-full w-[25%] border-l border-gray-200 bg-base-100', className)}>
29+
className={twMerge('sidebar bg-base-100 h-full w-[25%] border-l border-gray-200', className)}>
2330
<div className="flex h-full flex-col">
24-
<div className="h-[94px] border-gray-200 p-4">
25-
<h2 className="mb-2 text-2xl font-bold text-base-content">Version History</h2>
26-
<p className="text-sm font-medium text-base-content/60">Auto versioning enabled</p>
31+
{/* Header */}
32+
<div className="border-b border-gray-200 px-4 py-4">
33+
<h2 className="text-base-content text-lg font-bold">Version History</h2>
34+
<p className="text-base-content/60 mt-0.5 text-xs">
35+
{historyList.length} version{historyList.length !== 1 ? 's' : ''}
36+
</p>
2737
</div>
28-
<div className="flex-1 overflow-y-auto px-4">
29-
<ul className="menu w-full rounded-box">
30-
{Object.entries(groupHistoryByDay(historyList)).map(([date, hourGroups], dateIndex) => (
31-
<li key={date}>
32-
<details open={dayContainsCurrentVersion(hourGroups, activeHistory.version)}>
33-
<summary className="font-medium">
34-
<MdCalendarToday className="h-4 w-4" />
35-
{date}
36-
</summary>
37-
<ul>
38-
{Object.entries(hourGroups).map(([hour, items], hourIndex) => (
39-
<li key={`${date}-${hour}`}>
40-
<details open={groupContainsCurrentVersion(items, activeHistory.version)}>
41-
<summary className="text-sm font-medium">
42-
<MdAccessTime className="h-4 w-4" />
43-
{hour}
44-
</summary>
45-
<ul>
46-
{items.map((item) => (
47-
<li key={item.version}>
48-
<button
49-
onClick={() => {
50-
watchVersionContent(item.version)
51-
closeModal && closeModal()
52-
}}
53-
className={`w-full p-3 ${activeHistory.version === item.version ? 'active text-primary' : ''}`}>
54-
<div className="flex flex-col">
55-
<span className="font-medium">Version {item.version}</span>
56-
<span
57-
className={`text-xs ${
58-
activeHistory.version === item.version
59-
? 'text-primary'
60-
: 'text-base-content/60'
61-
}`}>
62-
{new Date(item.createdAt).toLocaleTimeString()}
63-
</span>
64-
{item.commitMessage && (
65-
<span className="mt-1 text-sm">{item.commitMessage}</span>
66-
)}
67-
</div>
68-
</button>
69-
</li>
70-
))}
71-
</ul>
72-
</details>
73-
</li>
74-
))}
75-
</ul>
76-
</details>
77-
</li>
78-
))}
79-
</ul>
38+
39+
{/* Version List */}
40+
<div className="flex-1 overflow-y-auto">
41+
{Object.entries(groupedByDay).map(([dayLabel, sessions]) => (
42+
<DayGroup
43+
key={dayLabel}
44+
label={dayLabel}
45+
sessions={sessions}
46+
activeVersion={activeHistory.version}
47+
latestVersion={latestVersion}
48+
onSelectVersion={(version) => {
49+
watchVersionContent(version)
50+
closeModal?.()
51+
}}
52+
defaultOpen={dayContainsVersion(sessions, activeHistory.version)}
53+
/>
54+
))}
8055
</div>
8156
</div>
8257
</div>
8358
)
8459
}
8560

61+
interface DayGroupProps {
62+
label: string
63+
sessions: VersionSession[]
64+
activeVersion: number
65+
latestVersion: number
66+
onSelectVersion: (version: number) => void
67+
defaultOpen: boolean
68+
}
69+
70+
const DayGroup = ({
71+
label,
72+
sessions,
73+
activeVersion,
74+
latestVersion,
75+
onSelectVersion,
76+
defaultOpen
77+
}: DayGroupProps) => {
78+
const [isOpen, setIsOpen] = useState(defaultOpen)
79+
80+
return (
81+
<div className="border-b border-gray-200">
82+
{/* Day Header */}
83+
<button
84+
onClick={() => setIsOpen(!isOpen)}
85+
className="bg-base-200 hover:bg-base-300 flex w-full cursor-pointer items-center justify-between px-4 py-2.5 transition-colors">
86+
<span className="text-base-content text-sm font-semibold">{label}</span>
87+
{isOpen ? (
88+
<MdExpandLess className="text-base-content/50" size={18} />
89+
) : (
90+
<MdExpandMore className="text-base-content/50" size={18} />
91+
)}
92+
</button>
93+
94+
{/* Sessions */}
95+
{isOpen && (
96+
<div className="py-1">
97+
{sessions.map((session) => (
98+
<SessionItem
99+
key={session.id}
100+
session={session}
101+
activeVersion={activeVersion}
102+
latestVersion={latestVersion}
103+
onSelectVersion={onSelectVersion}
104+
/>
105+
))}
106+
</div>
107+
)}
108+
</div>
109+
)
110+
}
111+
112+
interface SessionItemProps {
113+
session: VersionSession
114+
activeVersion: number
115+
latestVersion: number
116+
onSelectVersion: (version: number) => void
117+
}
118+
119+
const SessionItem = ({
120+
session,
121+
activeVersion,
122+
latestVersion,
123+
onSelectVersion
124+
}: SessionItemProps) => {
125+
const [isExpanded, setIsExpanded] = useState(
126+
sessionContainsVersion(session, activeVersion) && session.versions.length > 1
127+
)
128+
129+
const hasMultipleVersions = session.versions.length > 1
130+
const isActive = sessionContainsVersion(session, activeVersion)
131+
const isLatestSession = session.isLatest
132+
133+
// Single version - show directly
134+
if (!hasMultipleVersions) {
135+
const version = session.versions[0]
136+
const isCurrentActive = version.version === activeVersion
137+
const isLatest = version.version === latestVersion
138+
139+
return (
140+
<button
141+
onClick={() => onSelectVersion(version.version)}
142+
className={twMerge(
143+
'flex w-full cursor-pointer items-start gap-3 px-4 py-2 text-left transition-all',
144+
isCurrentActive
145+
? 'bg-primary/15 hover:bg-base-300'
146+
: 'hover:bg-base-200 active:bg-base-200'
147+
)}>
148+
{/* Timeline dot */}
149+
<div className="flex flex-col items-center pt-1">
150+
<div
151+
className={twMerge(
152+
'hover:bg-docsy h-2.5 w-2.5 rounded-full transition-colors',
153+
isCurrentActive ? 'bg-primary' : 'bg-base-300'
154+
)}
155+
/>
156+
</div>
157+
158+
{/* Content */}
159+
<div className="min-w-0 flex-1">
160+
<div className="flex items-center gap-2">
161+
<span
162+
className={twMerge(
163+
'text-sm font-medium',
164+
isCurrentActive ? 'text-primary' : 'text-base-content'
165+
)}>
166+
{formatTime(version.createdAt)}
167+
</span>
168+
{isLatest && (
169+
<span className="bg-primary text-primary-content rounded-sm px-1.5 py-0.5 text-[10px] font-semibold">
170+
Latest
171+
</span>
172+
)}
173+
</div>
174+
<p
175+
className={twMerge(
176+
'mt-0.5 text-xs',
177+
isCurrentActive ? 'text-primary/70' : 'text-base-content/50'
178+
)}>
179+
{formatRelativeTime(version.createdAt)}
180+
</p>
181+
{version.commitMessage && (
182+
<p className="text-base-content/70 mt-1 truncate text-xs">{version.commitMessage}</p>
183+
)}
184+
</div>
185+
</button>
186+
)
187+
}
188+
189+
// Multiple versions - collapsible session
190+
return (
191+
<div className={twMerge('transition-colors', isActive ? 'bg-primary/5' : '')}>
192+
{/* Session header */}
193+
<button
194+
onClick={() => setIsExpanded(!isExpanded)}
195+
className={twMerge(
196+
'flex w-full cursor-pointer items-start gap-3 px-4 py-2 text-left transition-all',
197+
'hover:bg-base-200/50 active:bg-base-200'
198+
)}>
199+
{/* Timeline dot with count badge */}
200+
<div className="flex flex-col items-center pt-1">
201+
<div className="relative">
202+
<div
203+
className={twMerge(
204+
'h-2.5 w-2.5 rounded-full transition-colors',
205+
isActive ? 'bg-primary' : 'bg-base-300'
206+
)}
207+
/>
208+
{/* Count badge */}
209+
<span className="bg-base-content text-base-100 absolute -top-1.5 -right-1.5 flex h-4 w-4 items-center justify-center rounded-full text-[9px] font-bold">
210+
{session.versions.length}
211+
</span>
212+
</div>
213+
</div>
214+
215+
{/* Content */}
216+
<div className="min-w-0 flex-1">
217+
<div className="flex items-center gap-2">
218+
<span
219+
className={twMerge(
220+
'text-sm font-medium',
221+
isActive ? 'text-primary' : 'text-base-content'
222+
)}>
223+
{formatTime(session.startTime)}{formatTime(session.endTime)}
224+
</span>
225+
{isLatestSession && (
226+
<span className="bg-primary text-primary-content rounded-sm px-1.5 py-0.5 text-[10px] font-semibold">
227+
Latest
228+
</span>
229+
)}
230+
</div>
231+
<p
232+
className={twMerge(
233+
'mt-0.5 text-xs',
234+
isActive ? 'text-primary/70' : 'text-base-content/50'
235+
)}>
236+
{session.versions.length} changes · {formatRelativeTime(session.endTime)}
237+
</p>
238+
</div>
239+
240+
{/* Expand icon */}
241+
<div className="pt-0.5">
242+
{isExpanded ? (
243+
<MdExpandLess className="text-base-content/50" size={16} />
244+
) : (
245+
<MdExpandMore className="text-base-content/50" size={16} />
246+
)}
247+
</div>
248+
</button>
249+
250+
{/* Expanded versions list */}
251+
{isExpanded && (
252+
<div className="border-base-200 ml-[26px] border-l-2 py-1 pl-4">
253+
{session.versions.map((version) => {
254+
const isCurrentActive = version.version === activeVersion
255+
const isLatest = version.version === latestVersion
256+
257+
return (
258+
<button
259+
key={version.version}
260+
onClick={() => onSelectVersion(version.version)}
261+
className={twMerge(
262+
'flex w-full cursor-pointer items-center gap-2.5 rounded px-2 py-1.5 text-left transition-all',
263+
isCurrentActive
264+
? 'bg-primary/15 hover:bg-primary/20'
265+
: 'hover:bg-base-200/60 active:bg-base-200'
266+
)}>
267+
{/* Mini dot */}
268+
<div
269+
className={twMerge(
270+
'h-1.5 w-1.5 rounded-full',
271+
isCurrentActive ? 'bg-primary' : 'bg-base-300'
272+
)}
273+
/>
274+
275+
<span
276+
className={twMerge(
277+
'text-xs font-medium',
278+
isCurrentActive ? 'text-primary' : 'text-base-content/80'
279+
)}>
280+
{formatTime(version.createdAt)}
281+
</span>
282+
283+
{isLatest && (
284+
<span className="bg-primary text-primary-content rounded-sm px-1 py-0.5 text-[9px] font-semibold">
285+
Latest
286+
</span>
287+
)}
288+
</button>
289+
)
290+
})}
291+
</div>
292+
)}
293+
</div>
294+
)
295+
}
296+
86297
export default Sidebar

0 commit comments

Comments
 (0)