Skip to content

Commit

Permalink
Merge pull request #16 from yamitzky/feat/upcoming-reminder
Browse files Browse the repository at this point in the history
Feat/upcoming reminder
  • Loading branch information
yamitzky authored Feb 14, 2025
2 parents 492cb10 + b2e3bcd commit 8b7882e
Show file tree
Hide file tree
Showing 11 changed files with 599 additions and 386 deletions.
57 changes: 57 additions & 0 deletions apps/web/app/components/UpcomingReminders.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Card, CardBody, CardHeader } from '@nextui-org/react'
import { ReminderTarget } from '@synk-cal/usecase'
import { parseISO } from 'date-fns'
import useLocale from '~/hooks/useLocale'

type SerializedReminderTarget = Omit<ReminderTarget, 'sendAt'> & {
sendAt: string
}

type Props = {
reminders: SerializedReminderTarget[]
className?: string
}

export function UpcomingReminders({ reminders, className }: Props) {
const locale = useLocale()
const sortedReminders = [...reminders].sort((a, b) => parseISO(a.sendAt).getTime() - parseISO(b.sendAt).getTime())

const dateTimeFormatter = new Intl.DateTimeFormat(locale, {
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false,
})

return (
<Card className={className}>
<CardHeader>
<h2 className="text-lg font-semibold">{locale === 'ja' ? '今後のリマインダー' : 'Upcoming Reminders'}</h2>
</CardHeader>
<CardBody>
<div className="flex flex-col gap-4">
{sortedReminders.length === 0 ? (
<p className="text-default-500">{locale === 'ja' ? 'リマインダーはありません' : 'No upcoming reminders'}</p>
) : (
sortedReminders.map((reminder, index) => (
<div key={index} className="flex flex-col gap-1 p-4 border rounded">
<div className="text-sm text-default-500">
{reminder.sendAt}
{locale === 'ja'
? `${dateTimeFormatter.format(parseISO(reminder.sendAt))}に通知`
: `Notify at ${dateTimeFormatter.format(parseISO(reminder.sendAt))}`}
</div>
<div className="text-default-700">{reminder.message}</div>
<div className="text-xs text-default-400">
{locale === 'ja' ? '通知方法:' : 'Notification type: '}
{reminder.notificationType}
</div>
</div>
))
)}
</div>
</CardBody>
</Card>
)
}
33 changes: 29 additions & 4 deletions apps/web/app/routes/settings.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from '@remix-run/node'
import { useFetcher, useLoaderData } from '@remix-run/react'
import { ReminderSetting } from '@synk-cal/core'
import { ReminderSetting, config } from '@synk-cal/core'
import { ReminderTarget, getRemindTargets } from '@synk-cal/usecase'
import { addDays } from 'date-fns'
import { ReminderSettings } from '~/components/Settings'
import { UpcomingReminders } from '~/components/UpcomingReminders'
import { getAuthRepository } from '~/services/getAuthRepository'
import { getCalendarRepository } from '~/services/getCalendarRepository'
import { getGroupRepository } from '~/services/getGroupRepository'
import { getReminderSettingsRepository } from '~/services/getReminderSettingsRepository'

export const loader = async ({ request }: LoaderFunctionArgs) => {
Expand All @@ -11,14 +16,32 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
const reminderSettingsRepo = getReminderSettingsRepository()

let reminders: ReminderSetting[] = []
let upcomingReminders: ReminderTarget[] = []
if (user) {
reminders = (await reminderSettingsRepo.getReminderSettings(user.email)) || []

// Get upcoming reminders for the next 48 hours
const now = new Date()
const calendarRepositories = [
...config.CALENDAR_IDS.map((id) => getCalendarRepository(id)),
...config.PRIVATE_CALENDAR_IDS.map((id) => getCalendarRepository(id)),
]

upcomingReminders = await getRemindTargets({
startDate: now,
endDate: addDays(now, 7),
calendarRepositories,
groupRepository: getGroupRepository(),
reminderSettingsRepository: reminderSettingsRepo,
userEmail: user.email, // Only get reminders for the current user
})
}

return json({
user,
isReminderSettingsEnabled: !!reminderSettingsRepo,
reminders,
upcomingReminders,
})
}

Expand Down Expand Up @@ -52,7 +75,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
}

export default function SettingsRoute() {
const { user, reminders, isReminderSettingsEnabled } = useLoaderData<typeof loader>()
const { user, reminders, upcomingReminders, isReminderSettingsEnabled } = useLoaderData<typeof loader>()
const fetcher = useFetcher()

const handleRemindersChange = (newReminders: ReminderSetting[]) => {
Expand All @@ -72,8 +95,10 @@ export default function SettingsRoute() {
) : !isReminderSettingsEnabled ? (
<div>Reminder settings are not enabled.</div>
) : (
// FIXME: reminder options config
<ReminderSettings user={user} reminders={currentReminders} onChange={handleRemindersChange} />
<div className="flex flex-col gap-4">
<ReminderSettings user={user} reminders={currentReminders} onChange={handleRemindersChange} />
<UpcomingReminders reminders={upcomingReminders} />
</div>
)}
</div>
)
Expand Down
4 changes: 4 additions & 0 deletions apps/web/app/services/getAuthRepository.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { AuthRepository, config } from '@synk-cal/core'
import { IAPAuthRepository } from '@synk-cal/google'
import { MockAuthRepository } from '@synk-cal/repository'

export function getAuthRepository(): AuthRepository | undefined {
if (config.AUTH_PROVIDER === 'google-iap') {
return new IAPAuthRepository()
} else if (config.AUTH_PROVIDER === 'mock') {
const mockEmail = config.AUTH_MOCK_USER || '[email protected]'
return new MockAuthRepository({ email: mockEmail })
}
}
4 changes: 3 additions & 1 deletion packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ export const ConfigSchema = v.object({
REMINDER_TEMPLATE: v.optional(v.string()),
REMINDER_SETTINGS_PROVIDER: v.optional(v.union([v.literal('global'), v.literal('firestore')]), 'global'),
REMINDER_SETTINGS_FIRESTORE_DATABASE_ID: v.optional(v.string()),
AUTH_PROVIDER: v.optional(v.union([v.literal('google-iap')])),
AUTH_PROVIDER: v.optional(v.union([v.literal('google-iap'), v.literal('mock')])),
AUTH_MOCK_USER: v.optional(v.string()),
WEBHOOK_URL: v.optional(v.pipe(v.string(), v.url())),
GROUP_PROVIDER: v.optional(v.union([v.literal('google')])),
GROUP_CUSTOMER_ID: v.optional(v.string()),
Expand All @@ -62,6 +63,7 @@ export function parseConfig(env: NodeJS.ProcessEnv): Config {
REMINDER_SETTINGS_FIRESTORE_DATABASE_ID: env.REMINDER_SETTINGS_FIRESTORE_DATABASE_ID,
REMINDER_TEMPLATE: env.REMINDER_TEMPLATE,
AUTH_PROVIDER: env.AUTH_PROVIDER,
AUTH_MOCK_USER: env.AUTH_MOCK_USER,
WEBHOOK_URL: env.WEBHOOK_URL,
TIMEZONE: env.TIMEZONE,
GROUP_PROVIDER: env.GROUP_PROVIDER,
Expand Down
1 change: 1 addition & 0 deletions packages/repository/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './console_notification'
export * from './global_reminder_settings'
export * from './mock_auth'
export * from './webhook_notification'
20 changes: 20 additions & 0 deletions packages/repository/src/mock_auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { User } from '@synk-cal/core'

/**
* Mock implementation of AuthRepository that returns a dummy user
* with a specified email address
*/
export class MockAuthRepository {
email: string
constructor({ email = '[email protected]' }: { email: string }) {
this.email = email
}

async getUserFromHeader(_headers: Headers): Promise<User | undefined> {
// Always return the configured user
return {
email: this.email,
name: `Test User (${this.email})`,
}
}
}
Loading

0 comments on commit 8b7882e

Please sign in to comment.