diff --git a/cmd/cmd.go b/cmd/cmd.go index c6dbd4a..11150e2 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -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 { diff --git a/gui/src/main/router.tsx b/gui/src/main/router.tsx index 31c4d04..0da14d1 100644 --- a/gui/src/main/router.tsx +++ b/gui/src/main/router.tsx @@ -38,14 +38,7 @@ const router = createHashRouter([ }, { element: , - path: '/sessions/:userId?/:date?/:page?/:limit?', - loader: ({ params }) => - GetSessions( - params.userId ?? '', - '', - Number(params.page ?? 0), - Number(params.limit ?? 0) - ) + path: '/sessions' }, { element: , diff --git a/gui/src/pages/sessions.tsx b/gui/src/pages/sessions.tsx index 1aa0180..3afeec4 100644 --- a/gui/src/pages/sessions.tsx +++ b/gui/src/pages/sessions.tsx @@ -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 -type MonthGroup = Record -type YearGroup = Record +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([]) + const [sessionStatistics, setSessionStatistics] = React.useState() + 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 + ) return ( @@ -44,86 +63,96 @@ export function SessionsListPage() { transition={{ delay: 0.125 }} className='overflow-y-scroll' > - {Object.keys(groupedSessions) - .reverse() - .map(year => ( - - {year} - {Object.keys(groupedSessions[year]) - .reverse() - .map(month => ( - - - {Intl.DateTimeFormat(i18n.resolvedLanguage, { - month: 'long' - }).format(new Date(`2024-${Number(month) < 10 ? '0' + month : month}-01`))} - - + setMonthIndex(monthIndex + 1)} + > + + + setMonthIndex(monthIndex - 1)} + > + + + + {year} /{' '} + {Intl.DateTimeFormat(i18n.resolvedLanguage, { + month: 'long' + }).format(new Date(`2024-${month}-01`))} + + + + + {Object.keys(sessionsByDay).map(day => ( + + {day} + {sessionsByDay[day].reverse().map(s => ( + + + navigate(`/sessions/${s.id}/matches`)} > - {Object.keys(groupedSessions[year][month]).map(day => ( - - {day} - {groupedSessions[year][month][day].reverse().map(s => ( - - - navigate(`/sessions/${s.id}/matches`)} - > - - {Intl.DateTimeFormat(i18n.resolvedLanguage, { - hour: '2-digit', - minute: '2-digit' - }).format(new Date(s.createdAt))} - - {s.userName} - - - - - - {t('wins')} - {s.matchesWon} - - - {t('losses')} - {s.matchesLost} - - {s.lpGain != 0 && s.mrGain != 0 && ( - <> - - {t('mrGain')} - {s.mrGain} - - - {t('lpGain')} - {s.lpGain} - - > - )} - - - - ))} - - ))} - - - ))} - + + {Intl.DateTimeFormat(i18n.resolvedLanguage, { + hour: '2-digit', + minute: '2-digit' + }).format(new Date(s.createdAt))} + + {s.userName} + + + + + + {t('wins')} + {s.matchesWon} + + + {t('losses')} + {s.matchesLost} + + {s.lpGain != 0 && s.mrGain != 0 && ( + <> + + {t('mrGain')} + {s.mrGain} + + + {t('lpGain')} + {s.lpGain} + + > + )} + + + + ))} + ))} + ) diff --git a/gui/src/ui/button.tsx b/gui/src/ui/button.tsx index 918f188..8931aa6 100644 --- a/gui/src/ui/button.tsx +++ b/gui/src/ui/button.tsx @@ -6,17 +6,23 @@ export const Button = React.forwardRef< HTMLButtonElement, React.PropsWithChildren> >((props, ref) => { - const { disabled, className, children, ...restProps } = props + const { disabled, className, children, onClick, ...restProps } = props return ( >; +export function GetSessionsStatistics(arg1:string):Promise; + export function GetSupportedLanguages():Promise>; export function GetThemes():Promise>; diff --git a/gui/wailsjs/go/models.ts b/gui/wailsjs/go/models.ts index 470f730..5e43906 100755 --- a/gui/wailsjs/go/models.ts +++ b/gui/wailsjs/go/models.ts @@ -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; diff --git a/pkg/model/session.go b/pkg/model/session.go index 7e2be89..1eea07a 100644 --- a/pkg/model/session.go +++ b/pkg/model/session.go @@ -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 +} diff --git a/pkg/storage/sql/session.go b/pkg/storage/sql/session.go index 74bdbca..ddf63c7 100644 --- a/pkg/storage/sql/session.go +++ b/pkg/storage/sql/session.go @@ -13,6 +13,7 @@ import ( type SessionStorage interface { CreateSession(ctx context.Context, userId string) error GetSessions(ctx context.Context, userId string, date string, limit uint8, offset uint16) ([]*model.Session, error) + GetSessionsStatistics(ctx context.Context, userId string) (*model.SessionsStatistics, error) UpdateSession(ctx context.Context, session *model.Session) error } @@ -50,6 +51,52 @@ func (s *Storage) CreateSession(ctx context.Context, userId string) (*model.Sess return &sesh, nil } +type monthlySessionCount struct { + Month string `db:"month"` + Count uint16 `db:"count"` +} + +func (s *Storage) GetSessionsStatistics(ctx context.Context, userId string) (*model.SessionsStatistics, error) { + where := `` + var whereArgs []interface{} + if userId != "" { + where = `WHERE s.user_id = (?)` + whereArgs = append(whereArgs, userId) + } + query, args, err := sqlx.In(fmt.Sprintf(` + SELECT + STRFTIME('%%m-%%Y', s.created_at) as month, + COUNT(s.id) as count + FROM sessions as s + %s + GROUP BY STRFTIME('%%m-%%Y', s.created_at) + ORDER BY s.id DESC +`, where), whereArgs...) + if err != nil { + return nil, fmt.Errorf("prepare get monthly session count query: %w", err) + } + rows, err := s.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("excute get monthly session count query: %w", err) + } + monthCounts := make([]model.SessionMonth, 0, 10) + + for rows.Next() { + var monthCount monthlySessionCount + if err := rows.Scan(&monthCount.Month, &monthCount.Count); err != nil { + return nil, fmt.Errorf("scan monthly session count row: %w", err) + } + monthCounts = append(monthCounts, model.SessionMonth{ + Date: monthCount.Month, + Count: monthCount.Count, + }) + } + + return &model.SessionsStatistics{ + Months: monthCounts, + }, nil +} + func (s *Storage) GetSessions(ctx context.Context, userId string, date string, limit uint8, offset uint16) ([]*model.Session, error) { pagination := `` if limit != 0 || offset != 0 { @@ -63,9 +110,9 @@ func (s *Storage) GetSessions(ctx context.Context, userId string, date string, l } if date != "" { if where == "" { - where = `WHERE s.created_at = %(?)%` + where = `WHERE STRFTIME('%m', s.created_at) = (?)` } else { - where = ` AND s.created_at like %(?)%` + where = ` AND STRFTIME('%m', s.created_at) = (?)` } whereArgs = append(whereArgs, date) }