Skip to content

Commit

Permalink
Merge pull request #65 from casesandberg/feature/user_timezone
Browse files Browse the repository at this point in the history
Save User Timezone
  • Loading branch information
casesandberg authored Aug 1, 2024
2 parents 836f00f + 0c71d49 commit e6dbedd
Show file tree
Hide file tree
Showing 10 changed files with 110 additions and 8 deletions.
8 changes: 7 additions & 1 deletion apps/api/app/api/users/me/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ export default createSchema({
},
},
PATCH: {
requestBody: UserSchema.pick({ username: true, bio: true, displayName: true, avatarUrl: true }).partial(),
requestBody: UserSchema.pick({
username: true,
bio: true,
displayName: true,
avatarUrl: true,
timezone: true,
}).partial(),
responses: {
200: UserSchema,
404: ServerErrorSchema,
Expand Down
13 changes: 13 additions & 0 deletions packages/auth/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,17 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
from: '[email protected]',
}),
],

callbacks: {
async signIn({ user }) {
if (user) {
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
await db.user.update({
where: { id: user.id },
data: { timezone },
})
}
return true
},
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "timezone" TEXT NOT NULL DEFAULT 'America/Los_Angeles';
17 changes: 17 additions & 0 deletions packages/database/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@ import {
Notification,
} from './zod'

// Monkey patch for es2022
declare namespace Intl {
type Key = 'calendar' | 'collation' | 'currency' | 'numberingSystem' | 'timeZone' | 'unit'

function supportedValuesOf(input: Key): string[]

type DateTimeFormatOptions = {
timeZone: string
timeZoneName: string
}
class DateTimeFormat {
constructor(locale: string, options: DateTimeFormatOptions)
format(date: Date): string
}
}

export function mockUser(overrides?: Partial<User>): User {
const firstName = faker.person.firstName()
const lastName = faker.person.lastName()
Expand All @@ -30,6 +46,7 @@ export function mockUser(overrides?: Partial<User>): User {
website: faker.helpers.maybe(faker.internet.domainName, { probability: 0.3 }) ?? null,
createdAt: faker.date.past(),
updatedAt: faker.date.recent(),
timezone: faker.helpers.arrayElement(Intl.supportedValuesOf('timeZone')),
...overrides,
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/database/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ model User {
discordHandle String?
website String?
bio String?
timezone String @default("America/Los_Angeles")
authAccounts AuthAccount[]
sessions Session[]
comments Comment[]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from 'zod';

export const UserScalarFieldEnumSchema = z.enum(['id','username','displayName','avatarUrl','twitterHandle','discordHandle','website','bio','createdAt','updatedAt','email','emailVerified']);
export const UserScalarFieldEnumSchema = z.enum(['id','username','displayName','avatarUrl','twitterHandle','discordHandle','website','bio','timezone','createdAt','updatedAt','email','emailVerified']);

export default UserScalarFieldEnumSchema;
1 change: 1 addition & 0 deletions packages/database/zod/modelSchema/UserSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const UserSchema = z.object({
discordHandle: z.string().nullable(),
website: z.string().nullable(),
bio: z.string().nullable(),
timezone: z.string(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
// omitted: email: z.string(),
Expand Down
12 changes: 11 additions & 1 deletion packages/ui/src/components/ui/combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,17 @@ export function Combobox({
</Button>
</PopoverTrigger>
<PopoverContent className="p-0">
<Command>
<Command
filter={(rowValue, search, keywords) => {
const normalize = (str: string) => str.toLowerCase().replace(/[^a-z0-9]/g, '')
const normalizedValue = normalize(rowValue)
const normalizedSearch = normalize(search)
const normalizedKeywords = keywords?.map(normalize) || []

const extendedValue = `${normalizedValue} ${normalizedKeywords.join(' ')}`
return extendedValue.includes(normalizedSearch) ? 1 : 0
}}
>
<CommandInput placeholder={searchLabel} />
<CommandList>
<CommandEmpty>{emptyLabel}</CommandEmpty>
Expand Down
56 changes: 52 additions & 4 deletions packages/users/components/SettingsProfileForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,33 @@
import { debounce } from 'lodash'
import React from 'react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { UserSchema } from '@play-money/database'
import { User } from '@play-money/database'
import { Button } from '@play-money/ui/button'
import { Combobox } from '@play-money/ui/combobox'
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@play-money/ui/form'
import { Input } from '@play-money/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@play-money/ui/select'
import { Textarea } from '@play-money/ui/textarea'
import { toast } from '@play-money/ui/use-toast'
import { useUser } from '../context/UserContext'

const profileFormSchema = UserSchema.pick({ username: true, bio: true, avatarUrl: true, displayName: true })
type ProfileFormValues = z.infer<typeof profileFormSchema>
// Monkey patch for es2022
declare namespace Intl {
type Key = 'calendar' | 'collation' | 'currency' | 'numberingSystem' | 'timeZone' | 'unit'

function supportedValuesOf(input: Key): string[]

type DateTimeFormatOptions = {
timeZone: string
timeZoneName: string
}
class DateTimeFormat {
constructor(locale: string, options: DateTimeFormatOptions)
format(date: Date): string
}
}

type ProfileFormValues = Pick<User, 'username' | 'bio' | 'avatarUrl' | 'displayName' | 'timezone'>

// TODO: @casesandberg Generate this from OpenAPI schema
async function checkUsernameAvailability(username: string): Promise<{ available: boolean; message?: string }> {
Expand All @@ -31,6 +47,7 @@ export function SettingsProfileForm() {
username: user?.username ?? '',
bio: user?.bio ?? '',
displayName: user?.displayName ?? '',
timezone: user?.timezone ?? '',
},
})

Expand Down Expand Up @@ -127,6 +144,37 @@ export function SettingsProfileForm() {
)}
/>

<FormField
control={form.control}
name="timezone"
render={({ field }) => (
<FormItem>
<FormLabel>Timezone</FormLabel>
<FormControl>
<Combobox
buttonClassName="w-full"
{...field}
items={Intl.supportedValuesOf('timeZone').map((tz) => {
const offset = new Intl.DateTimeFormat('en-US', {
timeZone: tz,
timeZoneName: 'shortOffset',
})
.format(new Date())
.split(' ')
.pop()
return { value: tz, label: `${tz} (${offset})`, keywords: [...tz.split(' ')] }
})}
/>
</FormControl>
<FormDescription>
This will be used for sending notifications, daily quest resets and displaying times in your local
timezone.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>

<Button type="submit" disabled={!isDirty || !isValid} loading={isSubmitting}>
Update profile
</Button>
Expand Down
6 changes: 5 additions & 1 deletion packages/users/lib/updateUserById.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ export async function updateUserById({
displayName,
bio,
avatarUrl,
timezone,
}: {
id: string
username?: string
displayName?: string
bio?: string
avatarUrl?: string
timezone?: string
}) {
const user = await getUserById({ id })

Expand All @@ -33,7 +35,9 @@ export async function updateUserById({
if (avatarUrl) {
updatedData.avatarUrl = avatarUrl
}

if (timezone) {
updatedData.timezone = timezone
}
if (displayName) {
updatedData.displayName = displayName
}
Expand Down

0 comments on commit e6dbedd

Please sign in to comment.