diff --git a/backend/src/bot/handlers/root.ts b/backend/src/bot/handlers/root.ts index 9baa6ed..b39b8d3 100644 --- a/backend/src/bot/handlers/root.ts +++ b/backend/src/bot/handlers/root.ts @@ -9,6 +9,7 @@ export default handler((composer) => { composer.use(views.trainingsDaysList) composer.use(views.trainingsDayTrainings) composer.use(views.trainingsTraining) + composer.use(views.semestersSummary) composer.use(commands.start) composer.use(commands.howgoodami) diff --git a/backend/src/bot/handlers/views/index.ts b/backend/src/bot/handlers/views/index.ts index b089227..509b857 100644 --- a/backend/src/bot/handlers/views/index.ts +++ b/backend/src/bot/handlers/views/index.ts @@ -6,6 +6,7 @@ import settingsLanguage from './settings_language' import trainingsDaysList from './trainings_days-list' import trainingsDayTrainings from './trainings_day-trainings' import trainingsTraining from './trainings_training' +import semestersSummary from './semesters_summary' // eslint-disable-next-line ts/ban-types export type View = { @@ -20,4 +21,5 @@ export default { trainingsDaysList, trainingsDayTrainings, trainingsTraining, + semestersSummary, } diff --git a/backend/src/bot/handlers/views/main.tsx b/backend/src/bot/handlers/views/main.tsx index d68916e..21772bf 100644 --- a/backend/src/bot/handlers/views/main.tsx +++ b/backend/src/bot/handlers/views/main.tsx @@ -8,6 +8,7 @@ const VIEW_ID = 'main' const SettingsButton = makeButton({ id: `${VIEW_ID}:settings` }) const TrainingsButton = makeButton({ id: `${VIEW_ID}:trainings` }) +const SemestersButton = makeButton({ id: `${VIEW_ID}:semesters` }) export default { render: async ctx => ( @@ -16,6 +17,8 @@ export default { {ctx.t['Views.Main.Buttons.Settings']} {ctx.t['Views.Main.Buttons.Trainings']} +
+ {ctx.t['Views.Main.Buttons.Semesters']}
), @@ -25,19 +28,28 @@ export default { composer .filter(SettingsButton.filter) .use(async (ctx) => { - ctx.answerCallbackQuery() await ctx .edit(ctx.chat!.id, ctx.callbackQuery.message!.message_id) .to(await views.settings.render(ctx, {})) + ctx.answerCallbackQuery() }) composer .filter(TrainingsButton.filter) .use(async (ctx) => { - ctx.answerCallbackQuery() await ctx .edit(ctx.chat!.id, ctx.callbackQuery.message!.message_id) .to(await views.trainingsDaysList.render(ctx, {})) + ctx.answerCallbackQuery() + }) + + composer + .filter(SemestersButton.filter) + .use(async (ctx) => { + await ctx + .edit(ctx.chat!.id, ctx.callbackQuery.message!.message_id) + .to(await views.semestersSummary.render(ctx, {})) + ctx.answerCallbackQuery() }) return composer.middleware() diff --git a/backend/src/bot/handlers/views/semesters_summary.tsx b/backend/src/bot/handlers/views/semesters_summary.tsx new file mode 100644 index 0000000..dc0d04c --- /dev/null +++ b/backend/src/bot/handlers/views/semesters_summary.tsx @@ -0,0 +1,50 @@ +import { Composer } from 'grammy' +import { makeButton } from '@telegum/grammy-buttons' +import type { View } from '.' +import views from '.' +import type { Ctx } from '~/bot/context' +import { Table } from '~/bot/utils/table' + +const VIEW_ID = 'semesters/summary' + +const BackButton = makeButton({ id: `${VIEW_ID}:back` }) + +export default { + render: async (ctx) => { + const summary = await ctx.domain.getSemestersSummary({ telegramId: ctx.from!.id }) + + const rows = [ + ['Semester', 'Hours', 'Test'], + ...summary.map(({ title, hoursTotal, fitnessTest }) => [ + title.length > 8 + ? `${title.slice(0, 6)}...` + : title, + hoursTotal.toString(), + `${fitnessTest.passed ? '✔' : '✘'} ${fitnessTest.pointsTotal}%`, + ]), + ] + + return ( + <> + + + {ctx.t['Buttons.Back']} + + + ) + }, + middleware: () => { + const composer = new Composer() + + composer + .filter(BackButton.filter) + .use(async (ctx) => { + ctx.answerCallbackQuery() + await ctx + .edit(ctx.chat!.id, ctx.callbackQuery.message!.message_id) + .to(await views.main.render(ctx, {})) + }) + + return composer.middleware() + }, +} satisfies View as View diff --git a/backend/src/bot/utils/table.tsx b/backend/src/bot/utils/table.tsx new file mode 100644 index 0000000..0b50e57 --- /dev/null +++ b/backend/src/bot/utils/table.tsx @@ -0,0 +1,45 @@ +import type { TgxElement } from '@telegum/tgx' + +export type TableProps = { + rows: string[][] + headingRow?: boolean +} + +export function Table({ + rows, + headingRow = false, +}: TableProps): TgxElement { + if (rows.length === 0) { + return <> + } + + const colCount = rows[0].length + const colLengths = Array.from({ length: colCount }, () => 0) + + for (const row of rows) { + for (let i = 0; i < colCount; i++) { + colLengths[i] = Math.max(colLengths[i], row[i].length) + } + } + + const table = [] + + for (const row of rows) { + const cells = row.map((cell, i) => { + const padding = ' '.repeat(colLengths[i] - cell.length) + return `${cell}${padding}` + }) + table.push(cells.join(' | ')) + } + + if (headingRow) { + const divider = colLengths.map(len => '-'.repeat(len)).join('-+-') + table.splice(1, 0, divider) + } + + return ( + <> + {table.map(row => ({row}
))} + + ) +} diff --git a/backend/src/translations/_en.tsx b/backend/src/translations/_en.tsx index 5c6d33f..8fedab6 100644 --- a/backend/src/translations/_en.tsx +++ b/backend/src/translations/_en.tsx @@ -13,6 +13,7 @@ export default { 'Views.Main.Message': 'Hi! Here is a list of actions:', 'Views.Main.Buttons.Settings': '⚙️ Settings', 'Views.Main.Buttons.Trainings': '⛹️ Classes', + 'Views.Main.Buttons.Semesters': '📆 Semesters', 'Views.Settings.Message': 'A list of configurable things:', 'Views.Settings.Buttons.Language': '🌐 Language',