Skip to content

Commit d3e9cac

Browse files
committed
refactor: Restructure routes to improve layout and enhance user experience
Refactored the RootLayout component to utilize a scroll prevention hook, ensuring better scroll behavior. Updated the DiscordThemesRoute, HomeRoute, and TampermonkeyRoute components to incorporate new library meta components for displaying file metadata, improving the overall presentation and usability of the file lists. Adjusted layout classes for consistent flex behavior across routes.
1 parent 12a3b44 commit d3e9cac

6 files changed

Lines changed: 258 additions & 106 deletions

File tree

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { Clock } from 'lucide-react'
2+
3+
import { cn } from '@/lib/utils'
4+
5+
const fileSizeUnits = ['B', 'KB', 'MB', 'GB', 'TB']
6+
7+
export function formatFileSize(bytes: number) {
8+
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B'
9+
let size = bytes
10+
let unitIndex = 0
11+
12+
while (size >= 1024 && unitIndex < fileSizeUnits.length - 1) {
13+
size /= 1024
14+
unitIndex += 1
15+
}
16+
17+
const precision = size >= 10 || unitIndex === 0 ? 0 : 1
18+
return `${size.toFixed(precision)} ${fileSizeUnits[unitIndex]}`
19+
}
20+
21+
export function formatLastUpdated(timestamp: number) {
22+
if (!Number.isFinite(timestamp) || timestamp <= 0) return '—'
23+
return new Intl.DateTimeFormat(undefined, {
24+
year: 'numeric',
25+
month: 'short',
26+
day: '2-digit',
27+
}).format(new Date(timestamp))
28+
}
29+
30+
const metaGrid = 'grid grid-cols-[8rem_3.25rem] items-center justify-items-end gap-2'
31+
32+
export function LibraryMetaHeader({ className }: { className?: string }) {
33+
return (
34+
<div className={cn('hidden sm:block', className)}>
35+
<div className={cn(metaGrid, 'text-xs text-muted-foreground')}>
36+
<span className="flex items-center justify-end gap-1.5 text-right">
37+
<Clock className="size-3.5 shrink-0" />
38+
<span>Last updated</span>
39+
</span>
40+
<span>Size</span>
41+
</div>
42+
</div>
43+
)
44+
}
45+
46+
const updatedGrid = 'grid grid-cols-[8rem] items-center justify-items-end'
47+
48+
export function LibraryUpdatedHeader({ className }: { className?: string }) {
49+
return (
50+
<div className={cn('hidden sm:block', className)}>
51+
<div className={cn(updatedGrid, 'text-xs text-muted-foreground')}>
52+
<span className="flex items-center justify-end gap-1.5 text-right">
53+
<Clock className="size-3.5 shrink-0" />
54+
<span>Last updated</span>
55+
</span>
56+
</div>
57+
</div>
58+
)
59+
}
60+
61+
export function LibraryMetaValues({
62+
mtime,
63+
size,
64+
className,
65+
}: {
66+
mtime: number
67+
size: number
68+
className?: string
69+
}) {
70+
return (
71+
<div className={cn('shrink-0', className)}>
72+
<div className={cn(metaGrid, 'text-right')}>
73+
<div
74+
className="hidden sm:block text-xs font-medium text-muted-foreground tabular-nums"
75+
title={mtime > 0 ? new Date(mtime).toLocaleString() : undefined}
76+
>
77+
{formatLastUpdated(mtime)}
78+
</div>
79+
<div className="text-xs font-medium text-muted-foreground tabular-nums">
80+
{formatFileSize(size)}
81+
</div>
82+
</div>
83+
</div>
84+
)
85+
}
86+
87+
export function LibraryUpdatedValue({
88+
mtime,
89+
className,
90+
}: {
91+
mtime: number
92+
className?: string
93+
}) {
94+
return (
95+
<div className={cn('shrink-0', className)}>
96+
<div className={cn(updatedGrid, 'text-right')}>
97+
<div
98+
className="hidden sm:block text-xs font-medium text-muted-foreground tabular-nums"
99+
title={mtime > 0 ? new Date(mtime).toLocaleString() : undefined}
100+
>
101+
{formatLastUpdated(mtime)}
102+
</div>
103+
</div>
104+
</div>
105+
)
106+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { useEffect } from 'react'
2+
import type { RefObject } from 'react'
3+
4+
function elementCanScrollY(element: HTMLElement) {
5+
const style = window.getComputedStyle(element)
6+
const overflowY = style.overflowY
7+
const allowsScroll =
8+
overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay'
9+
10+
if (!allowsScroll) return false
11+
return element.scrollHeight - element.clientHeight > 1
12+
}
13+
14+
function findScrollableAncestor(
15+
start: HTMLElement,
16+
stopAt: HTMLElement,
17+
) {
18+
let current: HTMLElement | null = start
19+
while (current && current !== stopAt) {
20+
if (elementCanScrollY(current)) return current
21+
current = current.parentElement
22+
}
23+
return null
24+
}
25+
26+
export function usePreventScrollWhenNotOverflowing(
27+
ref: RefObject<HTMLElement | null>,
28+
) {
29+
useEffect(() => {
30+
const container = ref.current
31+
if (!container) return
32+
33+
const onWheel = (event: WheelEvent) => {
34+
if (event.ctrlKey) return
35+
if (!(event.target instanceof HTMLElement)) return
36+
if (findScrollableAncestor(event.target, container)) return
37+
38+
const overflow = container.scrollHeight - container.clientHeight
39+
if (overflow <= 1) {
40+
event.preventDefault()
41+
}
42+
}
43+
44+
const onTouchMove = (event: TouchEvent) => {
45+
if (!(event.target instanceof HTMLElement)) return
46+
if (findScrollableAncestor(event.target, container)) return
47+
48+
const overflow = container.scrollHeight - container.clientHeight
49+
if (overflow <= 1) {
50+
event.preventDefault()
51+
}
52+
}
53+
54+
container.addEventListener('wheel', onWheel, { passive: false })
55+
container.addEventListener('touchmove', onTouchMove, { passive: false })
56+
57+
return () => {
58+
container.removeEventListener('wheel', onWheel)
59+
container.removeEventListener('touchmove', onTouchMove)
60+
}
61+
}, [ref])
62+
}
63+

src/routes/__root.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,33 @@
1+
import { useRef } from 'react'
12
import { Outlet, createRootRoute } from '@tanstack/react-router'
23

34
import Header from '@/components/Header'
45
import { Toaster } from '@/components/ui/sonner'
6+
import { usePreventScrollWhenNotOverflowing } from '@/hooks/usePreventScrollWhenNotOverflowing'
57

68
export const Route = createRootRoute({
7-
component: () => (
9+
component: RootLayout,
10+
})
11+
12+
function RootLayout() {
13+
const scrollContainerRef = useRef<HTMLDivElement>(null)
14+
usePreventScrollWhenNotOverflowing(scrollContainerRef)
15+
16+
return (
817
<div className="relative flex h-dvh flex-col overflow-hidden bg-background text-foreground antialiased">
918
{/* Subtle background pattern */}
1019
<div className="pointer-events-none fixed inset-0 -z-10 overflow-hidden">
1120
<div className="absolute -left-1/4 -top-1/4 h-1/2 w-1/2 rounded-full bg-linear-to-br from-primary/2 to-transparent blur-3xl" />
1221
<div className="absolute -bottom-1/4 -right-1/4 h-1/2 w-1/2 rounded-full bg-linear-to-tl from-primary/2 to-transparent blur-3xl" />
1322
</div>
1423
<Header />
15-
<div className="flex min-h-0 flex-1 flex-col overflow-auto">
24+
<div
25+
ref={scrollContainerRef}
26+
className="flex min-h-0 flex-1 flex-col overflow-auto"
27+
>
1628
<Outlet />
1729
</div>
1830
<Toaster />
1931
</div>
20-
),
21-
})
32+
)
33+
}

src/routes/discord-themes.tsx

Lines changed: 31 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { useEffect, useMemo, useState } from 'react'
22
import { Link, createFileRoute } from '@tanstack/react-router'
33
import {
44
ChevronLeft,
5-
Clock,
65
Copy,
76
Download,
87
ExternalLink,
@@ -12,6 +11,11 @@ import {
1211
import { toast } from 'sonner'
1312

1413
import { CodeFileViewer } from '@/components/code-file-viewer'
14+
import {
15+
LibraryMetaHeader,
16+
LibraryMetaValues,
17+
formatFileSize,
18+
} from '@/components/library/meta-columns'
1519
import { Button } from '@/components/ui/button'
1620
import { folderGroups } from '@/lib/library'
1721

@@ -22,31 +26,6 @@ export const Route = createFileRoute('/discord-themes')({
2226
component: DiscordThemesRoute,
2327
})
2428

25-
const fileSizeUnits = ['B', 'KB', 'MB', 'GB', 'TB']
26-
27-
function formatFileSize(bytes: number) {
28-
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B'
29-
let size = bytes
30-
let unitIndex = 0
31-
32-
while (size >= 1024 && unitIndex < fileSizeUnits.length - 1) {
33-
size /= 1024
34-
unitIndex += 1
35-
}
36-
37-
const precision = size >= 10 || unitIndex === 0 ? 0 : 1
38-
return `${size.toFixed(precision)} ${fileSizeUnits[unitIndex]}`
39-
}
40-
41-
function formatLastUpdated(timestamp: number) {
42-
if (!Number.isFinite(timestamp) || timestamp <= 0) return '—'
43-
return new Intl.DateTimeFormat(undefined, {
44-
year: 'numeric',
45-
month: 'short',
46-
day: '2-digit',
47-
}).format(new Date(timestamp))
48-
}
49-
5029
const THEMES_FOLDER_ID = 'discord/themes'
5130
const PRODUCTION_ORIGIN = 'https://hapwi.github.io'
5231

@@ -261,7 +240,7 @@ function DiscordThemesRoute() {
261240
// Show file list view when no file is selected
262241
if (!selectedAssetPath) {
263242
return (
264-
<div className="flex min-h-screen flex-col">
243+
<div className="flex min-h-0 flex-1 flex-col">
265244
<main className="mx-auto w-full max-w-7xl flex-1 px-4 py-6 sm:px-6 sm:py-8 lg:px-8">
266245
<div className="space-y-6">
267246
{/* File browser */}
@@ -272,13 +251,7 @@ function DiscordThemesRoute() {
272251
<FileText className="size-4 text-muted-foreground" />
273252
<span className="text-sm font-medium">{themeAssets.length} theme files</span>
274253
</div>
275-
<div className="flex items-center gap-4 text-xs text-muted-foreground">
276-
<span className="hidden sm:flex items-center gap-1.5">
277-
<Clock className="size-3.5" />
278-
Last updated
279-
</span>
280-
<span className="hidden sm:block">Size</span>
281-
</div>
254+
<LibraryMetaHeader />
282255
</div>
283256

284257
{/* File list */}
@@ -309,17 +282,7 @@ function DiscordThemesRoute() {
309282
</p>
310283
)}
311284
</div>
312-
<div className="shrink-0 flex items-center gap-4 text-right">
313-
<div
314-
className="hidden sm:block text-xs font-medium text-muted-foreground tabular-nums"
315-
title={new Date(item.mtime).toLocaleString()}
316-
>
317-
{formatLastUpdated(item.mtime)}
318-
</div>
319-
<div className="text-xs font-medium text-muted-foreground tabular-nums">
320-
{formatFileSize(item.size)}
321-
</div>
322-
</div>
285+
<LibraryMetaValues mtime={item.mtime} size={item.size} />
323286
</Link>
324287
))}
325288
</div>
@@ -358,20 +321,29 @@ function DiscordThemesRoute() {
358321
<main className="mx-auto flex min-h-0 w-full max-w-7xl flex-1 flex-col overflow-hidden px-4 py-6 sm:px-6 sm:py-8 lg:px-8">
359322
<div className="flex min-h-0 flex-1 flex-col gap-4">
360323
{/* Breadcrumb navigation */}
361-
<div className="flex shrink-0 items-center gap-2">
362-
<Button
363-
variant="ghost"
364-
size="sm"
365-
className="gap-1.5 text-muted-foreground hover:text-foreground"
366-
asChild
367-
>
368-
<Link to="/discord-themes" search={{ file: undefined }}>
369-
<ChevronLeft className="size-4" />
370-
<span>Themes</span>
371-
</Link>
372-
</Button>
373-
<span className="text-muted-foreground/50">/</span>
374-
<span className="text-sm font-medium truncate">{activeAsset?.displayName}</span>
324+
<div className="flex shrink-0 flex-col gap-1">
325+
<div className="flex items-center gap-2">
326+
<Button
327+
variant="ghost"
328+
size="sm"
329+
className="gap-1.5 text-muted-foreground hover:text-foreground"
330+
asChild
331+
>
332+
<Link to="/discord-themes" search={{ file: undefined }}>
333+
<ChevronLeft className="size-4" />
334+
<span>Themes</span>
335+
</Link>
336+
</Button>
337+
<span className="text-muted-foreground/50">/</span>
338+
<span className="text-sm font-medium truncate">
339+
{activeAsset?.displayName}
340+
</span>
341+
</div>
342+
{activeAsset?.description ? (
343+
<p className="pl-1 text-sm text-muted-foreground line-clamp-2">
344+
{activeAsset.description}
345+
</p>
346+
) : null}
375347
</div>
376348

377349
{/* File viewer card */}

0 commit comments

Comments
 (0)