Skip to content

Commit 6f7f282

Browse files
Reapply "feat: add pagination"
This reverts commit c873625.
1 parent 4a0c3eb commit 6f7f282

File tree

8 files changed

+243
-111
lines changed

8 files changed

+243
-111
lines changed

cmd/cmd.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,17 @@ func (ch *CommandHandler) GetSessions(userId, date string, limit uint8, offset u
120120
return sessions, err
121121
}
122122

123+
func (ch *CommandHandler) GetSessionsStatistics(userId string) (*model.SessionsStatistics, error) {
124+
sessionStatistics, err := ch.sqlDb.GetSessionsStatistics(context.Background(), userId)
125+
if err != nil {
126+
log.Println(err)
127+
if !errorsx.ContainsFormattedError(err) {
128+
err = errorsx.NewFormattedError(http.StatusNotFound, fmt.Errorf(`failed to get monthly session counts %w`, err))
129+
}
130+
}
131+
return sessionStatistics, err
132+
}
133+
123134
func (ch *CommandHandler) GetMatches(sessionId uint16, userId string, limit uint8, offset uint16) ([]*model.Match, error) {
124135
matches, err := ch.sqlDb.GetMatches(context.Background(), sessionId, userId, limit, offset)
125136
if err != nil {

gui/src/main/router.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,7 @@ const router = createHashRouter([
3838
},
3939
{
4040
element: <SessionsListPage />,
41-
path: '/sessions/:userId?/:date?/:page?/:limit?',
42-
loader: ({ params }) =>
43-
GetSessions(
44-
params.userId ?? '',
45-
'',
46-
Number(params.page ?? 0),
47-
Number(params.limit ?? 0)
48-
)
41+
path: '/sessions',
4942
},
5043
{
5144
element: <MatchesListPage />,

gui/src/pages/sessions.tsx

Lines changed: 119 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,55 @@
11
import React from 'react'
22
import { useTranslation } from 'react-i18next'
3-
import { useLoaderData, useNavigate } from 'react-router-dom'
4-
5-
import * as Page from '@/ui/page'
6-
import * as Table from '@/ui/table'
3+
import { useNavigate } from 'react-router-dom'
74
import { motion } from 'framer-motion'
5+
import { Icon } from '@iconify/react'
86

7+
8+
import { GetSessions, GetSessionsStatistics } from '@cmd/CommandHandler'
99
import type { model } from '@model'
10-
import { Button } from '@/ui/button'
11-
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/ui/hover-card'
1210

13-
type DayGroup = Record<string, model.Session[]>
14-
type MonthGroup = Record<string, DayGroup>
15-
type YearGroup = Record<string, MonthGroup>
11+
12+
import { useErrorPopup } from '@/main/error-popup'
13+
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/ui/hover-card'
14+
import * as Page from '@/ui/page'
15+
import { Button } from '@/ui/button'
1616

1717
export function SessionsListPage() {
18-
const sessions = (useLoaderData() ?? []) as model.Session[]
1918
const { i18n, t } = useTranslation()
2019
const navigate = useNavigate()
20+
const setError = useErrorPopup()
2121

22-
const groupedSessions: YearGroup = sessions.reduce((group, sesh) => {
23-
const date = new Date(sesh.createdAt)
24-
const year = date.getFullYear()
25-
const month = date.getMonth() + 1
26-
const day = date.getDate()
22+
const [sessions, setSessions] = React.useState<model.Session[]>([])
23+
const [sessionStatistics, setSessionStatistics] = React.useState<model.SessionsStatistics>()
24+
const [year, setYear] = React.useState("")
25+
const [month, setMonth] = React.useState("01")
26+
const [monthIndex, setMonthIndex] = React.useState(0)
27+
28+
const months = sessionStatistics?.Months ?? []
29+
30+
React.useEffect(() => {
31+
GetSessionsStatistics('').then(setSessionStatistics).catch(setError)
32+
}, [])
2733

28-
group[year] = group[year] ?? {}
29-
group[year][month] = group[year][month] ?? []
30-
group[year][month][day] = group[year][month][day] ?? []
31-
group[year][month][day].push(sesh)
34+
React.useEffect(() => {
35+
if (months.length > 0 && months[monthIndex]) {
36+
const [month, year] = months[monthIndex].Date.split('-')
37+
setMonth(month)
38+
setYear(year)
39+
}
40+
}, [sessionStatistics, monthIndex])
3241

42+
React.useEffect(() => {
43+
GetSessions("", month, 0, 0).then(setSessions).catch(setError)
44+
}, [month])
45+
46+
const sessionsByDay = (sessions ?? []).reduce((group, session) => {
47+
const date = new Date(session.createdAt)
48+
const day = date.getDate()
49+
group[day] = group[day] ?? []
50+
group[day].push(session)
3351
return group
34-
}, {})
52+
}, {} as Record<string, model.Session[]>)
3553

3654
return (
3755
<Page.Root>
@@ -44,86 +62,89 @@ export function SessionsListPage() {
4462
transition={{ delay: 0.125 }}
4563
className='overflow-y-scroll'
4664
>
47-
{Object.keys(groupedSessions)
48-
.reverse()
49-
.map(year => (
50-
<section key={year}>
51-
<h2 className='px-8 py-6 text-4xl font-bold'>{year}</h2>
52-
{Object.keys(groupedSessions[year])
53-
.reverse()
54-
.map(month => (
55-
<div key={month}>
56-
<h3 className='px-8 py-4 text-xl font-bold'>
57-
{Intl.DateTimeFormat(i18n.resolvedLanguage, {
58-
month: 'long'
59-
}).format(new Date(`2024-${Number(month) < 10 ? '0' + month : month}-01`))}
60-
</h3>
61-
<div
62-
style={{
63-
background: `repeating-linear-gradient(
64-
90deg,
65-
transparent 31.5px,
66-
transparent 224px,
67-
rgba(255, 255, 255, 0.125) 225px
68-
)`
69-
}}
70-
className='relative flex flex-wrap items-stretch border-y-[0.5px] border-solid border-divider px-8'
65+
<header className='px-8 py-4 text-xl flex gap-2 items-center'>
66+
<Button
67+
className="!py-0 !px-0 !text-md !font-normal"
68+
disabled={months[monthIndex + 1] === undefined}
69+
onClick={() => setMonthIndex(monthIndex + 1)}>
70+
<Icon width={26} height={26} icon='material-symbols:chevron-left' />
71+
</Button>
72+
<Button
73+
className="!py-0 !px-0 !text-md !font-normal"
74+
disabled={monthIndex === 0}
75+
onClick={() => setMonthIndex(monthIndex - 1)}>
76+
<Icon width={26} height={26} icon='material-symbols:chevron-left' className='rotate-180' />
77+
</Button>
78+
<h2 className='ml-2 font-bold'>
79+
{year}{" "}/{" "}
80+
{Intl.DateTimeFormat(i18n.resolvedLanguage, {
81+
month: 'long'
82+
}).format(new Date(`2024-${month}-01`))}
83+
</h2>
84+
</header>
85+
86+
<div
87+
style={{
88+
background: `repeating-linear-gradient(
89+
90deg,
90+
transparent 31.5px,
91+
transparent 224px,
92+
rgba(255, 255, 255, 0.125) 225px
93+
)`
94+
}}
95+
className='relative flex flex-wrap items-stretch border-y-[0.5px] border-solid border-divider px-8'
96+
>
97+
{Object.keys(sessionsByDay).map(day => (
98+
<div
99+
key={day}
100+
className='flex w-[193.5px] flex-col border-b-[0.5px] border-solid border-divider px-2'
101+
>
102+
<span className='text-center text-xl font-bold'>{day}</span>
103+
{sessionsByDay[day].reverse().map(s => (
104+
<HoverCard key={s.id} openDelay={250}>
105+
<HoverCardTrigger>
106+
<Button
107+
className='mb-1 w-full !justify-between gap-2 rounded-xl !px-[6px] !py-0 !pt-[2px] text-xl'
108+
onClick={() => navigate(`/sessions/${s.id}/matches`)}
71109
>
72-
{Object.keys(groupedSessions[year][month]).map(day => (
73-
<div
74-
key={day}
75-
className='flex w-[193.5px] flex-col border-b-[0.5px] border-solid border-divider px-2'
76-
>
77-
<span className='text-center text-xl font-bold'>{day}</span>
78-
{groupedSessions[year][month][day].reverse().map(s => (
79-
<HoverCard>
80-
<HoverCardTrigger>
81-
<Button
82-
className='mb-1 w-full !justify-between gap-2 rounded-xl !px-[6px] !py-0 !pt-[2px] text-xl'
83-
onClick={() => navigate(`/sessions/${s.id}/matches`)}
84-
>
85-
<span className='text-base font-bold'>
86-
{Intl.DateTimeFormat(i18n.resolvedLanguage, {
87-
hour: '2-digit',
88-
minute: '2-digit'
89-
}).format(new Date(s.createdAt))}
90-
</span>
91-
<span className='text-base font-light'>{s.userName}</span>
92-
</Button>
93-
</HoverCardTrigger>
94-
<HoverCardContent side='bottom'>
95-
<dl>
96-
<div className='flex justify-between gap-2'>
97-
<dt>{t('wins')}</dt>
98-
<dd>{s.matchesWon}</dd>
99-
</div>
100-
<div className='flex justify-between gap-2'>
101-
<dt>{t('losses')}</dt>
102-
<dd>{s.matchesLost}</dd>
103-
</div>
104-
{s.lpGain != 0 && s.mrGain != 0 && (
105-
<>
106-
<div className='flex justify-between gap-2'>
107-
<dt>{t('mrGain')}</dt>
108-
<dd>{s.mrGain}</dd>
109-
</div>
110-
<div className='flex justify-between gap-2'>
111-
<dt>{t('lpGain')}</dt>
112-
<dd>{s.lpGain}</dd>
113-
</div>
114-
</>
115-
)}
116-
</dl>
117-
</HoverCardContent>
118-
</HoverCard>
119-
))}
120-
</div>
121-
))}
122-
</div>
123-
</div>
124-
))}
125-
</section>
110+
<span className='text-base font-bold'>
111+
{Intl.DateTimeFormat(i18n.resolvedLanguage, {
112+
hour: '2-digit',
113+
minute: '2-digit'
114+
}).format(new Date(s.createdAt))}
115+
</span>
116+
<span className='text-base font-light'>{s.userName}</span>
117+
</Button>
118+
</HoverCardTrigger>
119+
<HoverCardContent side='bottom'>
120+
<dl>
121+
<div className='flex justify-between gap-2'>
122+
<dt>{t('wins')}</dt>
123+
<dd>{s.matchesWon}</dd>
124+
</div>
125+
<div className='flex justify-between gap-2'>
126+
<dt>{t('losses')}</dt>
127+
<dd>{s.matchesLost}</dd>
128+
</div>
129+
{s.lpGain != 0 && s.mrGain != 0 && (
130+
<>
131+
<div className='flex justify-between gap-2'>
132+
<dt>{t('mrGain')}</dt>
133+
<dd>{s.mrGain}</dd>
134+
</div>
135+
<div className='flex justify-between gap-2'>
136+
<dt>{t('lpGain')}</dt>
137+
<dd>{s.lpGain}</dd>
138+
</div>
139+
</>
140+
)}
141+
</dl>
142+
</HoverCardContent>
143+
</HoverCard>
144+
))}
145+
</div>
126146
))}
147+
</div>
127148
</motion.div>
128149
</Page.Root>
129150
)

gui/src/ui/button.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,23 @@ export const Button = React.forwardRef<
66
HTMLButtonElement,
77
React.PropsWithChildren<React.ButtonHTMLAttributes<HTMLButtonElement>>
88
>((props, ref) => {
9-
const { disabled, className, children, ...restProps } = props
9+
const { disabled, className, children, onClick, ...restProps } = props
1010
return (
1111
<button
1212
ref={ref}
13+
onClick={disabled ? undefined : onClick}
1314
{...(disabled && {
14-
style: { filter: 'saturate(0)' }
15+
style: {
16+
backgroundColor: 'rgba(0,0,0,.25)',
17+
border: '1px solid rgba(255,255,255,.25)',
18+
cursor: 'not-allowed'
19+
},
1520
})}
1621
className={cn(
1722
'flex items-center justify-between',
1823
'text-md whitespace-nowrap font-semibold',
19-
'bg-[rgba(255,10,10,.1)] transition-colors hover:bg-[#FF3D51] active:bg-[#ff6474]',
24+
'bg-[rgba(255,10,10,.1)] transition-colors',
25+
'hover:bg-[#FF3D51] active:bg-[#ff6474]',
2026
'rounded-[18px] border-[1px] border-[#FF3D51]',
2127
'px-5 py-3',
2228
className

gui/wailsjs/go/cmd/CommandHandler.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export function GetMatches(arg1:number,arg2:string,arg3:number,arg4:number):Prom
1717

1818
export function GetSessions(arg1:string,arg2:string,arg3:number,arg4:number):Promise<Array<model.Session>>;
1919

20+
export function GetSessionsStatistics(arg1:string):Promise<model.SessionsStatistics>;
21+
2022
export function GetSupportedLanguages():Promise<Array<string>>;
2123

2224
export function GetThemes():Promise<Array<model.Theme>>;

gui/wailsjs/go/models.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,50 @@ export namespace model {
361361
return a;
362362
}
363363
}
364+
export class SessionMonth {
365+
Date: string;
366+
Count: number;
367+
368+
static createFrom(source: any = {}) {
369+
return new SessionMonth(source);
370+
}
371+
372+
constructor(source: any = {}) {
373+
if ('string' === typeof source) source = JSON.parse(source);
374+
this.Date = source["Date"];
375+
this.Count = source["Count"];
376+
}
377+
}
378+
export class SessionsStatistics {
379+
Months: SessionMonth[];
380+
381+
static createFrom(source: any = {}) {
382+
return new SessionsStatistics(source);
383+
}
384+
385+
constructor(source: any = {}) {
386+
if ('string' === typeof source) source = JSON.parse(source);
387+
this.Months = this.convertValues(source["Months"], SessionMonth);
388+
}
389+
390+
convertValues(a: any, classs: any, asMap: boolean = false): any {
391+
if (!a) {
392+
return a;
393+
}
394+
if (a.slice && a.map) {
395+
return (a as any[]).map(elem => this.convertValues(elem, classs));
396+
} else if ("object" === typeof a) {
397+
if (asMap) {
398+
for (const key of Object.keys(a)) {
399+
a[key] = new classs(a[key]);
400+
}
401+
return a;
402+
}
403+
return new classs(a);
404+
}
405+
return a;
406+
}
407+
}
364408
export class Theme {
365409
name: string;
366410
css: string;

pkg/model/session.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,13 @@ type Session struct {
1717
LPGain int `db:"lp_gain" json:"lpGain"`
1818
MRGain int `db:"mr_gain" json:"mrGain"`
1919
}
20+
21+
type SessionMonth struct {
22+
Date string
23+
Count uint16
24+
}
25+
26+
// future: extend with legendary stats
27+
type SessionsStatistics struct {
28+
Months []SessionMonth
29+
}

0 commit comments

Comments
 (0)