Skip to content

Commit

Permalink
feat: make days keyboard calendar like (#19)
Browse files Browse the repository at this point in the history
- `<NoopButton/>` implemented.
- `dates` utility methods refactored, `Day` class created, related tests updated.
- Limit of the days in days list keyboard increased from 7 to 8.
- Keyboard of days list now renders as calendar.
  • Loading branch information
evermake authored Apr 2, 2024
1 parent 9766209 commit 52e3e0d
Show file tree
Hide file tree
Showing 10 changed files with 310 additions and 178 deletions.
23 changes: 7 additions & 16 deletions backend/src/bot/handlers/views/trainings_day-trainings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { View } from '.'
import views from '.'
import type { Ctx } from '~/bot/context'
import { TIMEZONE } from '~/constants'
import { clockTime, getDateDayInTimezone, getDayBoundaries } from '~/utils/dates'
import { Day, clockTime } from '~/utils/dates'

const VIEW_ID = 'trainings/day-trainings'

Expand Down Expand Up @@ -32,19 +32,10 @@ const TrainingButton = makeButton<{
},
})

export type Props = {
date: Date
}

const render: View<Ctx, Props>['render'] = async (ctx, { date }) => {
const { year, month, day } = getDateDayInTimezone(date, TIMEZONE)
const [from, to] = getDayBoundaries({
year: year,
month: month,
day: day,
timezone: TIMEZONE,
})
export type Props = { day: Day }

const render: View<Ctx, Props>['render'] = async (ctx, { day }) => {
const [from, to] = day.boundaries()
const trainings = await ctx.domain.getTrainingsForUser({
telegramId: ctx.from!.id,
from: from,
Expand Down Expand Up @@ -121,7 +112,7 @@ export default {
ctx.answerCallbackQuery({ text: alertMessage, show_alert: true })
await ctx
.edit(ctx.chat!.id, ctx.callbackQuery.message!.message_id)
.to(await render(ctx, { date: training.startsAt }))
.to(await render(ctx, { day: Day.fromDate(training.startsAt, TIMEZONE) }))
break
}
case 'cancel-check-in': {
Expand All @@ -146,7 +137,7 @@ export default {
ctx.answerCallbackQuery({ text: alertMessage, show_alert: true })
await ctx
.edit(ctx.chat!.id, ctx.callbackQuery.message!.message_id)
.to(await render(ctx, { date: training.startsAt }))
.to(await render(ctx, { day: Day.fromDate(training.startsAt, TIMEZONE) }))
break
}
case 'details': {
Expand All @@ -158,7 +149,7 @@ export default {
break
}
default:
throw new Error(`Invalid action: ${ctx.payload.action}`)
throw new Error(`Invalid action: ${ctx.payload.action}.`)
}
})

Expand Down
58 changes: 33 additions & 25 deletions backend/src/bot/handlers/views/trainings_days-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,52 @@ import type { View } from '.'
import views from '.'
import type { Ctx } from '~/bot/context'
import { TIMEZONE, TRAININGS_DAYS_LIST_COUNT } from '~/constants'
import { getDateDayInTimezone, getDayBoundaries } from '~/utils/dates'
import { Day, getSpanningWeeks } from '~/utils/dates'
import { NoopButton } from '~/bot/plugins/noop-button'

const VIEW_ID = 'trainings/days-list'

const BackButton = makeButton({ id: `${VIEW_ID}:back` })
const DayButton = makeButton<{ date: Date }>({
const DayButton = makeButton<{ day: Day }>({
id: `${VIEW_ID}:day`,
encode: ({ date }) => date.toISOString(),
decode: data => ({ date: new Date(data) }),
encode: ({ day }) => day.toString(),
decode: data => ({ day: Day.fromString(data) }),
})

export default {
render: async (ctx) => {
const now = new Date()
// Make keyboard like this:
//
// Mo Tu We Th Fr Sa Su
// -- -- 01 02 03 04 05
// 06 07 08 -- -- -- --

const today = Day.fromDate(new Date(), TIMEZONE)
const lastDay = today.shift(TRAININGS_DAYS_LIST_COUNT - 1)
const weeksDays = getSpanningWeeks(today, lastDay)

const weekdaysRow = (['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] as const)
.map(weekday => (
<NoopButton>{ctx.t['Weekday.TwoLetters'](weekday)}</NoopButton>
))
const daysRows = weeksDays.map(weekDays => (
<>
{weekDays.map(day => (
day === null
? <NoopButton />
: <DayButton day={day}>{day.date.toString()}</DayButton>
))}
<br />
</>
))

return (
<>
{ctx.t['Views.TrainingsDaysList.Message']}
<keyboard>
{Array(TRAININGS_DAYS_LIST_COUNT).fill(null).map((_, i) => {
const actualDate = new Date(now.getTime())
actualDate.setDate(now.getDate() + i)

const day = getDateDayInTimezone(actualDate, TIMEZONE)
const [timezoneDate, __] = getDayBoundaries({
...day,
timezone: TIMEZONE,
})

return (
<>
<DayButton date={actualDate}>
{ctx.t['Views.TrainingsDaysList.Buttons.Day'](timezoneDate)}
</DayButton>
<br />
</>
)
})}
{weekdaysRow}
<br />
{daysRows}
<BackButton>{ctx.t['Buttons.Back']}</BackButton>
</keyboard>
</>
Expand All @@ -55,7 +63,7 @@ export default {
.use(async (ctx) => {
await ctx
.edit(ctx.chat!.id, ctx.callbackQuery.message!.message_id)
.to(await views.trainingsDayTrainings.render(ctx, { date: ctx.payload.date }))
.to(await views.trainingsDayTrainings.render(ctx, { day: ctx.payload.day }))
ctx.answerCallbackQuery()
})

Expand Down
60 changes: 24 additions & 36 deletions backend/src/bot/handlers/views/trainings_training.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,36 @@ import type { View } from '.'
import views from '.'
import type { Ctx } from '~/bot/context'
import type { TrainingDetailed } from '~/services/sport/types'
import { getDayBoundaries, getTimezoneOffset } from '~/utils/dates'
import { Day } from '~/utils/dates'
import { TIMEZONE } from '~/constants'

const VIEW_ID = 'trainings/training'

const BackButton = makeButton<{ date: Date }>({
const BackButton = makeButton<{ day: Day }>({
id: `${VIEW_ID}:back`,
encode: ({ date }) => date.toISOString(),
decode: data => ({ date: new Date(data) }),
encode: ({ day }) => day.toString(),
decode: data => ({ day: Day.fromString(data) }),
})
const CheckInButton = makeButton<{ trainingId: number, date: { year: number, month: number, day: number } }>({
const CheckInButton = makeButton<{ trainingId: number, day: Day }>({
id: `${VIEW_ID}:check-in`,
encode: ({ trainingId, date }) => `${trainingId}:${date.day}-${date.month}-${date.year}`,
encode: ({ trainingId, day }) => `${trainingId}:${day.toString()}`,
decode: (data) => {
const [trainingId, date] = data.split(':')
const [day, month, year] = date.split('-').map(Number)
return { trainingId: Number(trainingId), date: { day, month, year } }
const [trainingId, dayStr] = data.split(':')
return {
trainingId: Number(trainingId),
day: Day.fromString(dayStr),
}
},
})
const CancelCheckInButton = makeButton<{ trainingId: number, date: { year: number, month: number, day: number } }>({
const CancelCheckInButton = makeButton<{ trainingId: number, day: Day }>({
id: `${VIEW_ID}:cancel-check-in`,
encode: ({ trainingId, date }) => `${trainingId}:${date.day}-${date.month}-${date.year}`,
encode: ({ trainingId, day }) => `${trainingId}:${day.toString()}`,
decode: (data) => {
const [trainingId, date] = data.split(':')
const [day, month, year] = date.split('-').map(Number)
return { trainingId: Number(trainingId), date: { day, month, year } }
const [trainingId, dayStr] = data.split(':')
return {
trainingId: Number(trainingId),
day: Day.fromString(dayStr),
}
},
})

Expand All @@ -39,29 +43,23 @@ export type Props = {

export default {
render: async (ctx, { training }) => {
const trainingDateInUtc = new Date(training.startsAt.getTime() - getTimezoneOffset(TIMEZONE))
const trainingDate = {
year: trainingDateInUtc.getUTCFullYear(),
month: trainingDateInUtc.getUTCMonth() + 1,
day: trainingDateInUtc.getUTCDate(),
}

const trainingDay = Day.fromDate(training.startsAt, TIMEZONE)
return (
<>
{ctx.t['Views.Training.Message'](training)}
<keyboard>
{training.checkedIn && (
<CancelCheckInButton trainingId={training.id} date={trainingDate}>
<CancelCheckInButton trainingId={training.id} day={trainingDay}>
{ctx.t['Views.Training.Buttons.CancelCheckIn']}
</CancelCheckInButton>
)}
{training.checkInAvailable && (
<CheckInButton trainingId={training.id} date={trainingDate}>
<CheckInButton trainingId={training.id} day={trainingDay}>
{ctx.t['Views.Training.Buttons.CheckIn']}
</CheckInButton>
)}
<br />
<BackButton date={training.startsAt}>
<BackButton day={trainingDay}>
{ctx.t['Buttons.Back']}
</BackButton>
</keyboard>
Expand All @@ -83,14 +81,9 @@ export default {
show_alert: true,
})

const [startDate, _] = getDayBoundaries({
...ctx.payload.date,
timezone: TIMEZONE,
})

await ctx
.edit(ctx.chat!.id, ctx.callbackQuery.message!.message_id)
.to(await views.trainingsDayTrainings.render(ctx, { date: startDate }))
.to(await views.trainingsDayTrainings.render(ctx, { day: ctx.payload.day }))
})

composer
Expand All @@ -105,14 +98,9 @@ export default {
show_alert: true,
})

const [startDate, _] = getDayBoundaries({
...ctx.payload.date,
timezone: TIMEZONE,
})

await ctx
.edit(ctx.chat!.id, ctx.callbackQuery.message!.message_id)
.to(await views.trainingsDayTrainings.render(ctx, { date: startDate }))
.to(await views.trainingsDayTrainings.render(ctx, { day: ctx.payload.day }))
})

composer
Expand Down
1 change: 1 addition & 0 deletions backend/src/bot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export function createBot({
plugins.messageSending.install(bot)
plugins.domain.install(bot, { domain, config })
plugins.translations.install(bot)
plugins.noopButton.install(bot)

bot.use(handlers)

Expand Down
2 changes: 2 additions & 0 deletions backend/src/bot/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as floodControl from './flood-control'
import * as messageSending from './message-sending'
import * as domain from './domain'
import * as translations from './translations'
import * as noopButton from './noop-button'

// eslint-disable-next-line ts/ban-types
export type InstallFn<F = {}, O = undefined> =
Expand All @@ -17,4 +18,5 @@ export default {
messageSending,
domain,
translations,
noopButton,
}
15 changes: 15 additions & 0 deletions backend/src/bot/plugins/noop-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { TgxElement } from '@telegum/tgx'
import type { InstallFn } from '.'

const CALLBACK_QUERY_DATA = 'noop'

export function NoopButton({ children }: { children?: string }): TgxElement {
return <button data={CALLBACK_QUERY_DATA}>{children || ' '}</button>
}

export const install: InstallFn = (bot) => {
bot.callbackQuery(CALLBACK_QUERY_DATA, async (ctx, next) => {
await ctx.answerCallbackQuery({ cache_time: 300 })
return next()
})
}
2 changes: 1 addition & 1 deletion backend/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export const TIMEZONE = 'Europe/Moscow'
export const TRAININGS_DAYS_LIST_COUNT = 7
export const TRAININGS_DAYS_LIST_COUNT = 8
20 changes: 13 additions & 7 deletions backend/src/translations/_en.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TIMEZONE } from '~/constants'
import type { TrainingDetailed } from '~/services/sport/types'
import type { Weekday } from '~/utils/dates'
import { clockTime } from '~/utils/dates'
import { tgxFromHtml } from '~/utils/tgx-from-html'

Expand All @@ -13,6 +14,18 @@ function dateLong(date: Date): string {
}

export default {
'Weekday.TwoLetters': (weekday: Weekday) => {
switch (weekday) {
case 'mon': return 'Mo'
case 'tue': return 'Tu'
case 'wed': return 'We'
case 'thu': return 'Th'
case 'fri': return 'Fr'
case 'sat': return 'Sa'
case 'sun': return 'Su'
}
},

'Buttons.Back': '← Back',

'HowGoodAmI.Thinking': 'Hmm... Let me think 🤔',
Expand Down Expand Up @@ -46,13 +59,6 @@ export default {
'Views.LanguageSettings.Message': 'Which flag do you like more?',

'Views.TrainingsDaysList.Message': 'Choose the date:',
'Views.TrainingsDaysList.Buttons.Day': (date: Date) => {
const day = date.toLocaleDateString('en-US', { weekday: 'long', timeZone: TIMEZONE })
const month = date.toLocaleDateString('en-US', { month: 'long', timeZone: TIMEZONE })
const dayOfMonth = date.getDate()

return `${day}, ${month} ${dayOfMonth}`
},

'Views.DayTrainings.Message': 'Choose the class:',

Expand Down
Loading

0 comments on commit 52e3e0d

Please sign in to comment.