Skip to content

Commit

Permalink
rework Hours display component
Browse files Browse the repository at this point in the history
  • Loading branch information
JoeKarow committed Dec 4, 2023
1 parent 6a7fe59 commit 3a08d77
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 158 deletions.
4 changes: 4 additions & 0 deletions apps/app/public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@
"form-error-password-blank": "Password cannot be blank.",
"form-error-password-req": "Your password does not meet requirements, please try again.",
"go-to-x": "Go to {{value}}",
"hours": {
"closed": "Closed",
"open24": "Open 24 hours"
},
"in-reach-user": "InReach User",
"in-reach-verified-reviewer": "InReach Verified Reviewer",
"include": "Include",
Expand Down
1 change: 1 addition & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"geolib": "3.3.4",
"just-compact": "3.2.0",
"just-flush": "2.3.0",
"just-group-by": "2.2.0",
"just-map-values": "3.2.0",
"just-omit": "2.2.0",
"just-pick": "4.2.0",
Expand Down
24 changes: 20 additions & 4 deletions packages/api/router/orgHours/query.forHoursDisplay.handler.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import groupBy from 'just-group-by'
import { DateTime, Interval } from 'luxon'

import { isIdFor, prisma, type Prisma } from '@weareinreach/db'
Expand Down Expand Up @@ -31,16 +32,31 @@ export const forHoursDisplay = async ({ input }: TRPCHandlerParams<TForHoursDisp
select: { id: true, dayIndex: true, start: true, end: true, closed: true, tz: true },
orderBy: [{ dayIndex: 'asc' }, { start: 'asc' }],
})

// TODO: alter db schema
const intervalResults = result.map(({ start, end, ...rest }) => {

const { weekYear, weekNumber } = DateTime.now()
const intervalResults = result.map(({ start, end, tz, dayIndex, ...rest }) => {
const interval = Interval.fromDateTimes(
DateTime.fromJSDate(start, { zone: rest.tz ?? 'America/New_York' }),
DateTime.fromJSDate(end, { zone: rest.tz ?? 'America/New_York' })
DateTime.fromJSDate(start, { zone: tz ?? 'America/New_York' }).set({
weekday: dayIndex,
weekYear,
weekNumber,
}),
DateTime.fromJSDate(end, { zone: tz ?? 'America/New_York' }).set({
weekday: dayIndex,
weekYear,
weekNumber,
})
)
return {
tz,
dayIndex,
...rest,
interval,
}
})
return intervalResults
const grouped = groupBy(intervalResults, ({ dayIndex }) => dayIndex)
// console.log(grouped)
return grouped
}
15 changes: 9 additions & 6 deletions packages/api/router/orgHours/query.forHoursDisplay.schema.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { z } from 'zod'

import { prefixedId } from '~api/schemas/idPrefix'
import { isIdFor } from '@weareinreach/db'

export const ZForHoursDisplaySchema = z.union([
prefixedId('organization'),
prefixedId('orgService'),
prefixedId('orgLocation'),
])
export const ZForHoursDisplaySchema = z.string()
// .refine((val) => isIdFor('organization', val) || isIdFor('orgLocation', val) || isIdFor('orgService', val))

// z.union([
// prefixedId('organization'),
// prefixedId('orgService'),
// prefixedId('orgLocation'),
// ])
export type TForHoursDisplaySchema = z.infer<typeof ZForHoursDisplaySchema>
108 changes: 38 additions & 70 deletions packages/ui/components/data-display/Hours.tsx
Original file line number Diff line number Diff line change
@@ -1,97 +1,65 @@
import { List, Stack, Text, Title } from '@mantine/core'
import { DateTime, Interval } from 'luxon'
import { createStyles, List, rem, Stack, Table, Text, Title } from '@mantine/core'
import { useTranslation } from 'next-i18next'
import { type JSX } from 'react'

import { useCustomVariant } from '~ui/hooks'
import { useCustomVariant } from '~ui/hooks/useCustomVariant'
import { useLocalizedDays } from '~ui/hooks/useLocalizedDays'
import { trpc as api } from '~ui/lib/trpcClient'

const labelKeys = {
regular: 'words.hours',
service: 'words.service-hours',
} as const

const OPEN_24_MILLISECONDS = 86340000

const useStyles = createStyles(() => ({
dow: {
verticalAlign: 'baseline',
paddingRight: rem(4),
},
}))

export const Hours = ({ parentId, label = 'regular' }: HoursProps) => {
const { t, i18n } = useTranslation('common')
const variants = useCustomVariant()
const hourDisplay: JSX.Element[] = []
const { classes } = useStyles()
const { data } = api.orgHours.forHoursDisplay.useQuery(parentId)
const dayMap = useLocalizedDays(i18n.resolvedLanguage)
if (!data) return null

const labelKey = labelKeys[label]

const hourMap = new Map<number, Set<NonNullable<typeof data>[number]>>()
let timezone: string | null = null

for (const entry of data) {
const daySet = hourMap.get(entry.dayIndex)
if (!daySet) {
hourMap.set(entry.dayIndex, new Set([entry]))
} else {
hourMap.set(entry.dayIndex, new Set([...daySet, entry]))
}
}

function formatClosed(dayIndex: number) {
const day = DateTime.fromObject({ weekday: dayIndex === 0 ? 7 : dayIndex }).toLocaleString({
weekday: 'short',
})
return `${day}: Closed`
}

function formatInterval(interval: { s: string; e: string }, dayIndex: number, displayDay: boolean) {
const startDate = DateTime.fromISO(interval.s)
const endDate = DateTime.fromISO(interval.e)

//if start and end hh:mm are the same, it's open 24 hours
const isSameTime = startDate.hour === endDate.hour && startDate.minute === endDate.minute
if (isSameTime) {
return `${startDate.set({ weekday: dayIndex === 0 ? 7 : dayIndex }).toFormat('EEE: ')} Open 24 Hours`
}

const formatPattern = displayDay ? 'EEE: h:mm a' : 'h:mm a'
const formattedStart = DateTime.fromISO(interval.s)
.set({ weekday: dayIndex === 0 ? 7 : dayIndex })
.toFormat(formatPattern)
const formattedEnd = DateTime.fromISO(interval.e).toFormat('h:mm a')

return `${formattedStart} - ${formattedEnd}`
}

hourMap.forEach((value, key) => {
const entry = [...value].map(({ dayIndex, tz, interval, closed }, daySegment) => {
const zone = tz ?? undefined

if (!timezone && zone) {
timezone = DateTime.now().setZone(zone).toFormat('ZZZZZ (ZZZZ)', { locale: i18n.language })
}

if (daySegment === 0 && !closed) {
const range = formatInterval(interval, dayIndex, true)
return range
} else if (daySegment !== 0 && !closed) {
const range = formatInterval(interval, dayIndex, false)
return range
} else if (closed) {
const range = formatClosed(dayIndex)
return range
}
})

if (entry[0] === null) return

hourDisplay.push(<List.Item key={key}>{entry.filter(Boolean).join(' & ')}</List.Item>)
const timezone: string | null = null

const hourTable = Object.entries(data).map(([dayIdx, data]) => {
return (
<>
<tr>
<td className={classes.dow}>{dayMap.get(parseInt(dayIdx))}</td>
<td>
<List listStyleType='none'>
{data.map(({ id, interval, closed }) => (
<List.Item key={id}>
{closed
? t('hours.closed')
: interval.toDuration('hours').valueOf() === OPEN_24_MILLISECONDS
? t('hours.open24')
: interval.toFormat('hh:mm a')}
</List.Item>
))}
</List>
</td>
</tr>
</>
)
})

if (!hourDisplay.length) return null

return (
<Stack spacing={12}>
<div>
<Title order={3}>{t(labelKey)}</Title>
<Text variant={variants.Text.utility4darkGray}>{timezone}</Text>
</div>
<List listStyleType='none'>{hourDisplay}</List>
<Table>{hourTable}</Table>
</Stack>
)
}
Expand Down
13 changes: 13 additions & 0 deletions packages/ui/hooks/useLocalizedDays.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useRouter } from 'next/router'

const proper = (s: string) => s.charAt(0).toLocaleUpperCase() + s.slice(1)

export const useLocalizedDays = (locale?: string) => {
const routerLocale = useRouter().locale
locale ??= routerLocale
const { format: dayFormat } = new Intl.DateTimeFormat(locale, { weekday: 'short' })
const dayMap = new Map(
[...Array(7).keys()].map((day, i) => [i, proper(dayFormat(new Date(Date.UTC(2021, 5, day))))])
)
return dayMap
}
Loading

0 comments on commit 3a08d77

Please sign in to comment.