Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: pagination for sessions page #200

Merged
merged 3 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,17 @@ func (ch *CommandHandler) GetSessions(userId, date string, limit uint8, offset u
return sessions, err
}

func (ch *CommandHandler) GetSessionsStatistics(userId string) (*model.SessionsStatistics, error) {
sessionStatistics, err := ch.sqlDb.GetSessionsStatistics(context.Background(), userId)
if err != nil {
log.Println(err)
if !errorsx.ContainsFormattedError(err) {
err = errorsx.NewFormattedError(http.StatusNotFound, fmt.Errorf(`failed to get monthly session counts %w`, err))
}
}
return sessionStatistics, err
}

func (ch *CommandHandler) GetMatches(sessionId uint16, userId string, limit uint8, offset uint16) ([]*model.Match, error) {
matches, err := ch.sqlDb.GetMatches(context.Background(), sessionId, userId, limit, offset)
if err != nil {
Expand Down
9 changes: 1 addition & 8 deletions gui/src/main/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,7 @@ const router = createHashRouter([
},
{
element: <SessionsListPage />,
path: '/sessions/:userId?/:date?/:page?/:limit?',
loader: ({ params }) =>
GetSessions(
params.userId ?? '',
'',
Number(params.page ?? 0),
Number(params.limit ?? 0)
)
path: '/sessions'
},
{
element: <MatchesListPage />,
Expand Down
227 changes: 128 additions & 99 deletions gui/src/pages/sessions.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,56 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useLoaderData, useNavigate } from 'react-router-dom'

import * as Page from '@/ui/page'
import * as Table from '@/ui/table'
import { useNavigate } from 'react-router-dom'
import { motion } from 'framer-motion'
import { Icon } from '@iconify/react'

import { GetSessions, GetSessionsStatistics } from '@cmd/CommandHandler'
import type { model } from '@model'
import { Button } from '@/ui/button'
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/ui/hover-card'

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

export function SessionsListPage() {
const sessions = (useLoaderData() ?? []) as model.Session[]
const { i18n, t } = useTranslation()
const navigate = useNavigate()
const setError = useErrorPopup()

const [sessions, setSessions] = React.useState<model.Session[]>([])
const [sessionStatistics, setSessionStatistics] = React.useState<model.SessionsStatistics>()
const [year, setYear] = React.useState('')
const [month, setMonth] = React.useState('01')
const [monthIndex, setMonthIndex] = React.useState(0)

const months = sessionStatistics?.Months ?? []

const groupedSessions: YearGroup = sessions.reduce((group, sesh) => {
const date = new Date(sesh.createdAt)
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
React.useEffect(() => {
GetSessionsStatistics('').then(setSessionStatistics).catch(setError)
}, [])

group[year] = group[year] ?? {}
group[year][month] = group[year][month] ?? []
group[year][month][day] = group[year][month][day] ?? []
group[year][month][day].push(sesh)
React.useEffect(() => {
if (months.length > 0 && months[monthIndex]) {
const [month, year] = months[monthIndex].Date.split('-')
setMonth(month)
setYear(year)
}
}, [sessionStatistics, monthIndex])

return group
}, {})
React.useEffect(() => {
GetSessions('', month, 0, 0).then(setSessions).catch(setError)
}, [month])

const sessionsByDay = (sessions ?? []).reduce(
(group, session) => {
const date = new Date(session.createdAt)
const day = date.getDate()
group[day] = group[day] ?? []
group[day].push(session)
return group
},
{} as Record<string, model.Session[]>
)

return (
<Page.Root>
Expand All @@ -44,86 +63,96 @@ export function SessionsListPage() {
transition={{ delay: 0.125 }}
className='overflow-y-scroll'
>
{Object.keys(groupedSessions)
.reverse()
.map(year => (
<section key={year}>
<h2 className='px-8 py-6 text-4xl font-bold'>{year}</h2>
{Object.keys(groupedSessions[year])
.reverse()
.map(month => (
<div key={month}>
<h3 className='px-8 py-4 text-xl font-bold'>
{Intl.DateTimeFormat(i18n.resolvedLanguage, {
month: 'long'
}).format(new Date(`2024-${Number(month) < 10 ? '0' + month : month}-01`))}
</h3>
<div
style={{
background: `repeating-linear-gradient(
90deg,
transparent 31.5px,
transparent 224px,
rgba(255, 255, 255, 0.125) 225px
)`
}}
className='relative flex flex-wrap items-stretch border-y-[0.5px] border-solid border-divider px-8'
<header className='flex items-center gap-2 px-8 py-4 text-xl'>
<Button
className='!text-md !px-0 !py-0 !font-normal'
disabled={months[monthIndex + 1] === undefined}
onClick={() => setMonthIndex(monthIndex + 1)}
>
<Icon width={26} height={26} icon='material-symbols:chevron-left' />
</Button>
<Button
className='!text-md !px-0 !py-0 !font-normal'
disabled={monthIndex === 0}
onClick={() => setMonthIndex(monthIndex - 1)}
>
<Icon
width={26}
height={26}
icon='material-symbols:chevron-left'
className='rotate-180'
/>
</Button>
<h2 className='ml-2 font-bold'>
{year} /{' '}
{Intl.DateTimeFormat(i18n.resolvedLanguage, {
month: 'long'
}).format(new Date(`2024-${month}-01`))}
</h2>
</header>

<div
style={{
background: `repeating-linear-gradient(
90deg,
transparent 31.5px,
transparent 224px,
rgba(255, 255, 255, 0.125) 225px
)`
}}
className='relative flex flex-wrap items-stretch border-y-[0.5px] border-solid border-divider px-8'
>
{Object.keys(sessionsByDay).map(day => (
<div
key={day}
className='flex w-[193.5px] flex-col border-b-[0.5px] border-solid border-divider px-2'
>
<span className='text-center text-xl font-bold'>{day}</span>
{sessionsByDay[day].reverse().map(s => (
<HoverCard key={s.id} openDelay={250}>
<HoverCardTrigger>
<Button
className='mb-1 w-full !justify-between gap-2 rounded-xl !px-[6px] !py-0 !pt-[2px] text-xl'
onClick={() => navigate(`/sessions/${s.id}/matches`)}
>
{Object.keys(groupedSessions[year][month]).map(day => (
<div
key={day}
className='flex w-[193.5px] flex-col border-b-[0.5px] border-solid border-divider px-2'
>
<span className='text-center text-xl font-bold'>{day}</span>
{groupedSessions[year][month][day].reverse().map(s => (
<HoverCard>
<HoverCardTrigger>
<Button
className='mb-1 w-full !justify-between gap-2 rounded-xl !px-[6px] !py-0 !pt-[2px] text-xl'
onClick={() => navigate(`/sessions/${s.id}/matches`)}
>
<span className='text-base font-bold'>
{Intl.DateTimeFormat(i18n.resolvedLanguage, {
hour: '2-digit',
minute: '2-digit'
}).format(new Date(s.createdAt))}
</span>
<span className='text-base font-light'>{s.userName}</span>
</Button>
</HoverCardTrigger>
<HoverCardContent side='bottom'>
<dl>
<div className='flex justify-between gap-2'>
<dt>{t('wins')}</dt>
<dd>{s.matchesWon}</dd>
</div>
<div className='flex justify-between gap-2'>
<dt>{t('losses')}</dt>
<dd>{s.matchesLost}</dd>
</div>
{s.lpGain != 0 && s.mrGain != 0 && (
<>
<div className='flex justify-between gap-2'>
<dt>{t('mrGain')}</dt>
<dd>{s.mrGain}</dd>
</div>
<div className='flex justify-between gap-2'>
<dt>{t('lpGain')}</dt>
<dd>{s.lpGain}</dd>
</div>
</>
)}
</dl>
</HoverCardContent>
</HoverCard>
))}
</div>
))}
</div>
</div>
))}
</section>
<span className='text-base font-bold'>
{Intl.DateTimeFormat(i18n.resolvedLanguage, {
hour: '2-digit',
minute: '2-digit'
}).format(new Date(s.createdAt))}
</span>
<span className='text-base font-light'>{s.userName}</span>
</Button>
</HoverCardTrigger>
<HoverCardContent side='bottom'>
<dl>
<div className='flex justify-between gap-2'>
<dt>{t('wins')}</dt>
<dd>{s.matchesWon}</dd>
</div>
<div className='flex justify-between gap-2'>
<dt>{t('losses')}</dt>
<dd>{s.matchesLost}</dd>
</div>
{s.lpGain != 0 && s.mrGain != 0 && (
<>
<div className='flex justify-between gap-2'>
<dt>{t('mrGain')}</dt>
<dd>{s.mrGain}</dd>
</div>
<div className='flex justify-between gap-2'>
<dt>{t('lpGain')}</dt>
<dd>{s.lpGain}</dd>
</div>
</>
)}
</dl>
</HoverCardContent>
</HoverCard>
))}
</div>
))}
</div>
</motion.div>
</Page.Root>
)
Expand Down
12 changes: 9 additions & 3 deletions gui/src/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,23 @@ export const Button = React.forwardRef<
HTMLButtonElement,
React.PropsWithChildren<React.ButtonHTMLAttributes<HTMLButtonElement>>
>((props, ref) => {
const { disabled, className, children, ...restProps } = props
const { disabled, className, children, onClick, ...restProps } = props
return (
<button
ref={ref}
onClick={disabled ? undefined : onClick}
{...(disabled && {
style: { filter: 'saturate(0)' }
style: {
backgroundColor: 'rgba(0,0,0,.25)',
border: '1px solid rgba(255,255,255,.25)',
cursor: 'not-allowed'
}
})}
className={cn(
'flex items-center justify-between',
'text-md whitespace-nowrap font-semibold',
'bg-[rgba(255,10,10,.1)] transition-colors hover:bg-[#FF3D51] active:bg-[#ff6474]',
'bg-[rgba(255,10,10,.1)] transition-colors',
'hover:bg-[#FF3D51] active:bg-[#ff6474]',
'rounded-[18px] border-[1px] border-[#FF3D51]',
'px-5 py-3',
className
Expand Down
2 changes: 2 additions & 0 deletions gui/wailsjs/go/cmd/CommandHandler.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export function GetMatches(arg1:number,arg2:string,arg3:number,arg4:number):Prom

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

export function GetSessionsStatistics(arg1:string):Promise<model.SessionsStatistics>;

export function GetSupportedLanguages():Promise<Array<string>>;

export function GetThemes():Promise<Array<model.Theme>>;
Expand Down
44 changes: 44 additions & 0 deletions gui/wailsjs/go/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,50 @@ export namespace model {
return a;
}
}
export class SessionMonth {
Date: string;
Count: number;

static createFrom(source: any = {}) {
return new SessionMonth(source);
}

constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.Date = source["Date"];
this.Count = source["Count"];
}
}
export class SessionsStatistics {
Months: SessionMonth[];

static createFrom(source: any = {}) {
return new SessionsStatistics(source);
}

constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.Months = this.convertValues(source["Months"], SessionMonth);
}

convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class Theme {
name: string;
css: string;
Expand Down
10 changes: 10 additions & 0 deletions pkg/model/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,13 @@ type Session struct {
LPGain int `db:"lp_gain" json:"lpGain"`
MRGain int `db:"mr_gain" json:"mrGain"`
}

type SessionMonth struct {
Date string
Count uint16
}

// future: extend with legendary stats
type SessionsStatistics struct {
Months []SessionMonth
}
Loading