Skip to content

Commit 2f55e14

Browse files
feat: add history page
1 parent d7482ee commit 2f55e14

14 files changed

+143
-73
lines changed

package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
"@eslint/config-inspector": "1.0.2",
6868
"@iconify/json": "2.2.316",
6969
"@iconify/tailwind4": "1.0.6",
70-
"@tailwindcss/vite": "4.0.12",
70+
"@tailwindcss/vite": "4.0.14",
7171
"@tsconfig/vite-react": "3.4.0",
7272
"@types/canvas-confetti": "1.9.0",
7373
"@types/js-cookie": "3.0.6",
@@ -80,16 +80,16 @@
8080
"eslint": "9.22.0",
8181
"libretrodb": "1.0.0",
8282
"lint-staged": "15.5.0",
83-
"miniflare": "3.20250310.0",
83+
"miniflare": "4.20250310.0",
8484
"nanoid": "5.1.3",
8585
"react-server-dom-webpack": "19.0.0",
8686
"sax": "1.4.1",
8787
"simple-git-hooks": "2.11.1",
88-
"tailwindcss": "4.0.12",
88+
"tailwindcss": "4.0.14",
8989
"typescript": "5.8.2",
9090
"vite-plugin-cjs-interop": "2.1.6",
9191
"vite-tsconfig-paths": "5.1.4",
92-
"wrangler": "3.114.1",
92+
"wrangler": "4.0.0",
9393
"zx": "8.4.1"
9494
},
9595
"packageManager": "[email protected]",

src/api/index.ts

-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,6 @@ app.post(
119119
'form',
120120
z.object({
121121
core: z.string(),
122-
platform: z.string(),
123122
rom: z.string(),
124123
}),
125124
),

src/controllers/create-launch-record.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
1+
import { eq } from 'drizzle-orm'
12
import { getContextData } from 'waku/middleware/context'
2-
import { launchRecordTable } from '../databases/library/schema.ts'
3+
import { launchRecordTable, romTable } from '../databases/library/schema.ts'
34

45
interface CreateRomParams {
56
core: string
6-
platform: string
77
rom: string
88
}
99

1010
export async function createLaunchRecord(params: CreateRomParams) {
1111
const { currentUser, db } = getContextData()
1212
const { library } = db
1313

14+
const results = await library.select().from(romTable).where(eq(romTable.id, params.rom))
15+
const [rom] = results
16+
1417
const [result] = await library
1518
.insert(launchRecordTable)
1619
.values({
1720
core: params.core,
18-
platform: params.platform,
19-
rom_id: params.rom,
21+
platform: rom.platform,
22+
rom_id: rom.id,
2023
user_id: currentUser.id,
2124
})
2225
.returning()

src/controllers/get-launch-records.ts

+23-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { eq, max } from 'drizzle-orm'
1+
import { count, countDistinct, desc, eq, max } from 'drizzle-orm'
22
import { getContextData } from 'waku/middleware/context'
3-
import { launchRecordTable } from '../databases/library/schema.ts'
3+
import { launchRecordTable, romTable } from '../databases/library/schema.ts'
4+
import { getRomsMetadata } from './utils.ts'
45

56
export async function getLaunchRecords({ page = 1, pageSize = 50 }: { page?: number; pageSize?: number }) {
67
const { currentUser, db } = getContextData()
@@ -10,15 +11,32 @@ export async function getLaunchRecords({ page = 1, pageSize = 50 }: { page?: num
1011
const results = await library
1112
.select({
1213
core: launchRecordTable.core,
14+
count: count(launchRecordTable.id),
15+
file_name: romTable.file_name,
1316
lastLaunched: max(launchRecordTable.created_at),
17+
launchbox_game_id: romTable.launchbox_game_id,
18+
libretro_game_id: romTable.libretro_game_id,
19+
platform: launchRecordTable.platform,
1420
romId: launchRecordTable.rom_id,
15-
userId: launchRecordTable.user_id,
1621
})
1722
.from(launchRecordTable)
1823
.where(eq(launchRecordTable.user_id, currentUser.id))
19-
.groupBy(launchRecordTable.rom_id)
24+
.leftJoin(romTable, eq(launchRecordTable.rom_id, romTable.id))
25+
.groupBy(launchRecordTable.rom_id, romTable.file_name, romTable.launchbox_game_id, romTable.libretro_game_id)
26+
.orderBy(desc(max(launchRecordTable.created_at)))
2027
.offset(offset)
2128
.limit(pageSize)
2229

23-
return results
30+
const [{ total }] = await library
31+
.select({
32+
total: countDistinct(launchRecordTable.rom_id),
33+
})
34+
.from(launchRecordTable)
35+
.where(eq(launchRecordTable.user_id, currentUser.id))
36+
const pagination = { current: page, pages: Math.ceil(total / pageSize), size: pageSize, total }
37+
const roms = await getRomsMetadata(results)
38+
return {
39+
pagination,
40+
roms,
41+
}
2442
}

src/controllers/get-roms.ts

+3-39
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,7 @@
1-
import { and, count, eq, inArray, type InferSelectModel } from 'drizzle-orm'
2-
import { compact, keyBy } from 'es-toolkit'
1+
import { and, count, eq } from 'drizzle-orm'
32
import { getContextData } from 'waku/middleware/context'
43
import { romTable } from '../databases/library/schema.ts'
5-
import { launchboxGameTable, libretroGameTable } from '../databases/metadata/schema.ts'
6-
7-
async function getMetadata(romResults: InferSelectModel<typeof romTable>[]) {
8-
const { db } = getContextData()
9-
const { metadata } = db
10-
const launchboxGameIds = compact(romResults.map((romResult) => romResult.launchbox_game_id))
11-
const launchboxResults = await metadata
12-
.select()
13-
.from(launchboxGameTable)
14-
.where(inArray(launchboxGameTable.database_id, launchboxGameIds))
15-
const launchboxResultMap = keyBy(launchboxResults, (launchboxResult) => launchboxResult.database_id)
16-
17-
const libretroGameIds = compact(romResults.map((romResult) => romResult.libretro_game_id))
18-
const libretroResults = await metadata
19-
.select()
20-
.from(libretroGameTable)
21-
.where(inArray(libretroGameTable.id, libretroGameIds))
22-
const libretroResultMap = keyBy(libretroResults, (libretroResult) => libretroResult.id)
23-
24-
const results = romResults.map((romResult) => ({
25-
...romResult,
26-
launchboxGame: null as InferSelectModel<typeof launchboxGameTable> | null,
27-
libretroGame: null as InferSelectModel<typeof libretroGameTable> | null,
28-
}))
29-
30-
for (const result of results) {
31-
if (result.libretro_game_id) {
32-
result.libretroGame = libretroResultMap[result.libretro_game_id]
33-
}
34-
if (result.launchbox_game_id) {
35-
result.launchboxGame = launchboxResultMap[result.launchbox_game_id]
36-
}
37-
}
38-
39-
return results
40-
}
4+
import { getRomsMetadata } from './utils.ts'
415

426
type GetRomsReturning = Awaited<ReturnType<typeof getRoms>>
437
export type Roms = GetRomsReturning['roms']
@@ -72,7 +36,7 @@ export async function getRoms({
7236

7337
const [{ total }] = await library.select({ total: count() }).from(romTable).orderBy(romTable.file_name).where(where)
7438

75-
const results = await getMetadata(romResults)
39+
const results = await getRomsMetadata(romResults)
7640

7741
return { pagination: { current: page, pages: Math.ceil(total / pageSize), size: pageSize, total }, roms: results }
7842
}

src/controllers/utils.ts

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { inArray, type InferSelectModel } from 'drizzle-orm'
2+
import { compact, keyBy } from 'es-toolkit'
3+
import { getContextData } from 'waku/middleware/context'
4+
import { launchboxGameTable, libretroGameTable } from '../databases/metadata/schema.ts'
5+
6+
interface RomModelLike {
7+
launchbox_game_id: null | number
8+
libretro_game_id: null | string
9+
}
10+
11+
export async function getRomsMetadata<T extends RomModelLike[]>(romResults: T) {
12+
const { db } = getContextData()
13+
const { metadata } = db
14+
const launchboxGameIds = compact(romResults.map((romResult) => romResult.launchbox_game_id))
15+
const launchboxResults = await metadata
16+
.select()
17+
.from(launchboxGameTable)
18+
.where(inArray(launchboxGameTable.database_id, launchboxGameIds))
19+
const launchboxResultMap = keyBy(launchboxResults, (launchboxResult) => launchboxResult.database_id)
20+
21+
const libretroGameIds = compact(romResults.map((romResult) => romResult.libretro_game_id))
22+
const libretroResults = await metadata
23+
.select()
24+
.from(libretroGameTable)
25+
.where(inArray(libretroGameTable.id, libretroGameIds))
26+
const libretroResultMap = keyBy(libretroResults, (libretroResult) => libretroResult.id)
27+
28+
const results = romResults.map((romResult) => ({
29+
...romResult,
30+
launchboxGame: null as InferSelectModel<typeof launchboxGameTable> | null,
31+
libretroGame: null as InferSelectModel<typeof libretroGameTable> | null,
32+
}))
33+
34+
for (const result of results) {
35+
if (result.libretro_game_id) {
36+
result.libretroGame = libretroResultMap[result.libretro_game_id]
37+
}
38+
if (result.launchbox_game_id) {
39+
result.launchboxGame = launchboxResultMap[result.launchbox_game_id]
40+
}
41+
}
42+
43+
return results
44+
}

src/entries.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createPages } from 'waku'
22
import { getContext } from 'waku/middleware/context'
33
import type { PathsForPages } from 'waku/router'
44
import { api } from '@/api/index.ts'
5+
import { HistoryPage } from '@/pages/library/history/page.tsx'
56
import { LibraryPage } from '@/pages/library/page.tsx'
67
import { PlatformPage } from '@/pages/library/platform/page.tsx'
78
import { RomPage } from '@/pages/library/platform/rom/page.tsx'
@@ -22,6 +23,7 @@ const pages: ReturnType<typeof createPages> = createPages(({ createApi, createPa
2223
createPage({ component: HomePage, path: '/', render: 'dynamic' }),
2324
createPage({ component: LoginPage, path: '/login', render: 'dynamic' }),
2425
createPage({ component: LibraryPage, path: '/library', render: 'dynamic' }),
26+
createPage({ component: HistoryPage, path: '/library/history', render: 'dynamic' }),
2527
createPage({ component: PlatformPage, path: '/library/platform/[platform]', render: 'dynamic' }),
2628
createPage({ component: RomPage, path: '/library/platform/[platform]/rom/[fileName]', render: 'dynamic' }),
2729
createPage({ component: RomPage, path: '/library/rom/[id]', render: 'dynamic' }),

src/pages/library/components/sidebar-link.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
'use client'
22
import { Button } from '@radix-ui/themes'
33
import { clsx } from 'clsx'
4-
import { Link } from 'waku/router/client'
4+
import { Link, useRouter_UNSTABLE } from 'waku/router/client'
55

6-
export function SidebarLink({ active = false, children, href }) {
6+
export function SidebarLink({ children, href }) {
7+
const router = useRouter_UNSTABLE()
8+
const active = router.path === href
79
return (
810
<Button asChild size='3' variant={active ? 'ghost' : 'solid'}>
911
<Link className={clsx('!m-0 !h-auto !px-4 !py-2.5', { '!bg-white': active })} scroll to={href}>
10-
<div className='flex h-auto w-full justify-start gap-2 font-semibold'>{children}</div>
12+
<div className='flex h-auto w-full items-center justify-start gap-2 font-semibold'>{children}</div>
1113
</Link>
1214
</Button>
1315
)

src/pages/library/components/sidebar-links.tsx

+11-10
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { platformMap } from '@/constants/platform.ts'
33
import { getPlatformIcon } from '@/utils/rom.ts'
44
import { SidebarLink } from './sidebar-link.tsx'
55

6-
export function SidebarLinks({ platform }: { platform?: string }) {
6+
export function SidebarLinks() {
77
const { preference } = getContextData()
88

99
const platformLinks = preference.ui.platforms
@@ -18,14 +18,15 @@ export function SidebarLinks({ platform }: { platform?: string }) {
1818
return (
1919
<>
2020
<div className='flex flex-col'>
21-
{[{ href: '/library', icon: <span className='icon-[mdi--bookshelf] size-5' />, text: 'Library' }].map(
22-
({ href, icon, text }) => (
23-
<SidebarLink active={!platform} href={href} key={text}>
24-
<div className='flex size-5 items-center justify-center'>{icon}</div>
25-
{text}
26-
</SidebarLink>
27-
),
28-
)}
21+
{[
22+
{ href: '/library', icon: <span className='icon-[mdi--bookshelf] size-5' />, text: 'Library' },
23+
{ href: '/library/history', icon: <span className='icon-[mdi--history] size-5' />, text: 'History' },
24+
].map(({ href, icon, text }) => (
25+
<SidebarLink href={href} key={text}>
26+
{icon}
27+
{text}
28+
</SidebarLink>
29+
))}
2930
</div>
3031

3132
<div className='mt-4'>
@@ -36,7 +37,7 @@ export function SidebarLinks({ platform }: { platform?: string }) {
3637

3738
<div className='mt-2 flex flex-col gap-y-2'>
3839
{platformLinks.map(({ href, icon, name, text }) => (
39-
<SidebarLink active={platform === name} href={href} key={text}>
40+
<SidebarLink href={href} key={text}>
4041
{icon ? <img alt='icon' className='size-5' src={icon} /> : null}
4142
{text}
4243
</SidebarLink>

src/pages/library/history/page.tsx

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { getLaunchRecords } from '@/controllers/get-launch-records.ts'
2+
import AppLayout from '../components/app-layout.tsx'
3+
import { GameList } from '../components/game-list.tsx'
4+
5+
export async function HistoryPage({ query }: { query: string }) {
6+
const page = Number.parseInt(new URLSearchParams(query).get('page') || '', 10) || 1
7+
const { pagination, roms } = await getLaunchRecords({ page })
8+
9+
if (page > 1 && roms.length === 0) {
10+
return '404'
11+
}
12+
13+
return (
14+
<AppLayout>
15+
<title>Library - RetroAssembly</title>
16+
<div className='flex flex-col gap-5'>
17+
<div className='relative flex justify-between px-4 pt-4'>
18+
<h1 className='text-5xl font-[Oswald_Variable] font-semibold'>History</h1>
19+
<div className='mt-4 flex items-center gap-2 text-zinc-400'>
20+
<span className='icon-[mdi--bar-chart] text-black' />
21+
Played
22+
<span className='font-[DSEG7_Modern] font-bold text-rose-700'>{pagination.total}</span>
23+
games.
24+
</div>
25+
</div>
26+
<hr className='border-t-1 border-t-black/20' />
27+
<GameList pagination={pagination} roms={roms} />
28+
</div>
29+
</AppLayout>
30+
)
31+
}

src/pages/library/platform/page.tsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { getRoms } from '@/controllers/get-roms.ts'
33
import AppLayout from '../components/app-layout.tsx'
44
import { DeviceInfo } from '../components/device-info.tsx'
55
import { GameList } from '../components/game-list.tsx'
6-
import { SidebarLinks } from '../components/sidebar-links.tsx'
76
import { PlatformBackground } from './components/platform-background.tsx'
87
import { UploadButton } from './components/upload-button.tsx'
98

@@ -25,7 +24,7 @@ export async function PlatformPage({ platform, query }: PlatformPageProps) {
2524
}
2625

2726
return (
28-
<AppLayout append={<PlatformBackground platform={platform} />} sidebar={<SidebarLinks platform={platform} />}>
27+
<AppLayout append={<PlatformBackground platform={platform} />}>
2928
<title>{`${platformMap[platform].displayName} - RetroAssembly`}</title>
3029

3130
<div className='flex flex-col gap-5'>

src/pages/library/platform/rom/components/game-overlay/game-overlay-button.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export function GameOverlayButton({
99
}: { children: ReactNode; isLoading?: boolean; onClick?: any }) {
1010
return (
1111
<Button
12-
className='[--accent-a11:white] [--accent-a3:rgba(0,0,0,.3)] [--accent-a4:rgba(0,0,0,.3)] [--accent-a5:rgba(0,0,0,.2)] [--accent-a8:white] [--gray-a3:rgba(0,0,0,.2)] [--gray-a8:white]'
12+
className='!border-1 border-solid border-white !bg-black/30 !text-white !shadow-none'
1313
disabled={isLoading}
1414
onClick={onClick}
1515
radius='full'

src/pages/library/platform/rom/components/launch-button.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ const directionKeys = new Set(['ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowUp'
99
export function LaunchButton() {
1010
const { emulator, isPreparing, launch } = useEmulator()
1111

12-
useKeyboardEvent(true, (event) => {
12+
useKeyboardEvent(true, async (event) => {
1313
if (emulator?.getStatus() === 'initial') {
1414
const isEscapeKey = event.key === 'Escape'
1515
const isSpecialKey = event.ctrlKey || event.metaKey || event.altKey || event.shiftKey
1616
const isDirectionKey = directionKeys.has(event.key)
1717
const shoudLaunch = !isSpecialKey && !isDirectionKey && !isEscapeKey
1818
if (shoudLaunch) {
19-
launch()
19+
await launch()
2020
}
2121
}
2222
})

src/pages/library/platform/rom/hooks/use-emulator.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useAtom } from 'jotai'
2+
import ky from 'ky'
23
import { Nostalgist } from 'nostalgist'
34
import useSWRImmutable from 'swr/immutable'
45
import { usePreference } from '../../../hooks/use-preference.ts'
@@ -32,9 +33,15 @@ export function useEmulator() {
3233
})
3334
})
3435

35-
function launch() {
36-
emulator?.start()
36+
async function launch() {
37+
await emulator?.start()
3738
setLaunched(true)
39+
const formData = new FormData()
40+
formData.append('core', core)
41+
formData.append('rom', rom.id)
42+
await ky.post('/api/v1/launch_record/new', {
43+
body: formData,
44+
})
3845
}
3946

4047
function exit() {

0 commit comments

Comments
 (0)