Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions app/tipping/group-admin/_components/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,8 @@ import UserAvatar from '@/components/user-avatar'
import { getLabel } from '@/lib/utils/prediction-fields'
import RowAction from './row-action'
import { Button } from '@/components/ui/button'
import {
LucideArrowDown,
LucideArrowUp,
LucideArrowUpDown,
LucideMinus,
} from 'lucide-react'
import { LucideArrowDown, LucideArrowUp, LucideArrowUpDown } from 'lucide-react'
import { RACE_PREDICTION_FIELDS } from '@/constants'

export const columns: ColumnDef<PredictionRow>[] = [
{
Expand Down Expand Up @@ -62,7 +58,16 @@ export const columns: ColumnDef<PredictionRow>[] = [
},
{
accessorKey: 'position',
header: 'Position',
id: 'position',
// header: 'Position',
header: ({ column }) => <SortHeader column={column} label='Position' />,
sortingFn: (rowA, roB, columnId) => {
const fields = RACE_PREDICTION_FIELDS
return (
fields.indexOf(roB.getValue(columnId)) -
fields.indexOf(rowA.getValue(columnId))
)
},
cell({
row: {
original: { position },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,34 +34,54 @@ import {
ControllerRenderProps,
useForm,
} from 'react-hook-form'
import { createTip } from '../_utils/create-tip-action'
import { createTip, updateTip } from '../_utils/create-tip-action'
import { toast } from 'sonner'
import Button from '@/components/button'
import { formSchema, Schema } from '../_utils/schema'
import { useRouter } from 'next/navigation'
import { SelectUser } from './select-user'

type RaceOption = Pick<
Database.Race,
'id' | 'locality' | 'grandPrixDate' | 'sprintQualifyingDate'
>

export default function CreateTipDialog({
users,
races,
drivers,
constructors,
}: {
export type TipFormData = {
users: Pick<Database.User, 'id' | 'name'>[]
drivers: DriverOptionProps[]
constructors: ConstructorProps[]
races: RaceOption[]
}) {
}

type TipFormProps = TipFormData & {
defaultValues?: Partial<Schema>
button?: React.ReactNode
predictionEntryId?: Database.PredictionEntryId
}

export default function CreateOrEditTipDialog({
users,
races,
drivers,
constructors,
defaultValues,
predictionEntryId,
button = (
<Button variant='outline' size='sm' icon={LucidePlus} label='Create tip' />
),
}: TipFormProps) {
const form = useForm<Schema>({
resolver: zodResolver(formSchema),
defaultValues,
})

const router = useRouter()

const mode = !Object.values(defaultValues ?? {})?.length ? 'create' : 'edit'
const isEditing = mode === 'edit'

const copy = getCopy()

const [isPending, startTransition] = React.useTransition()
const [open, setOpen] = React.useState(false)

Expand Down Expand Up @@ -104,20 +124,11 @@ export default function CreateTipDialog({

return (
<Dialog onOpenChange={setOpen} open={open}>
<DialogTrigger asChild>
<Button
variant='outline'
size='sm'
icon={LucidePlus}
label='Create tip'
/>
</DialogTrigger>
<DialogTrigger asChild>{button}</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create new tip</DialogTitle>
<DialogDescription>
Create a tip on behalf of a member of the group.
</DialogDescription>
<DialogTitle>{copy.title[mode]}</DialogTitle>
<DialogDescription>{copy.description[mode]}</DialogDescription>
</DialogHeader>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
Expand All @@ -137,15 +148,15 @@ export default function CreateTipDialog({
)}

<Field className='mt-4'>
<div className='flex justify-end gap-2'>
<div className='flex justify-between gap-2'>
<Button
label='Cancel'
type='button'
onClick={() => setOpen(false)}
variant='outline'
/>
<Button
label='Save'
label={copy.button[mode]}
type='submit'
isPending={isPending}
variant='default'
Expand Down Expand Up @@ -179,7 +190,7 @@ export default function CreateTipDialog({
if (isFuture) {
setMessage({
title: 'This race is in the future',
description: 'Are you sure you want to set a tip for it?',
description: `Are you sure you want to ${mode === 'create' ? 'set a tip for it' : 'edit the tip for it'}?`,
})
}

Expand All @@ -202,6 +213,7 @@ export default function CreateTipDialog({
return (
<Combobox
items={availablePositions.map((field) => ({ id: field }))}
disabled={isEditing}
value={field.value}
onSelect={(value) => {
field.onChange(value)
Expand Down Expand Up @@ -241,7 +253,9 @@ export default function CreateTipDialog({
getSearchValue={(driver) =>
[driver.givenName, driver.familyName].join(' ')
}
renderItem={(driver) => driver.familyName}
renderItem={(driver) =>
[driver.givenName, driver.familyName].join(' ')
}
/>
)
}}
Expand All @@ -256,23 +270,11 @@ export default function CreateTipDialog({
label='Tipper'
renderItem={({ field }) => {
return (
<Combobox
items={users}
<SelectUser
users={users}
value={field.value}
onSelect={field.onChange}
getSearchValue={(user) => user.name}
placeholder='Search users…'
emptyText='Select a user'
renderItem={(user) => (
<div className='flex items-center gap-2'>
<UserAvatar
name={user.name}
id={user.id}
className='size-6'
/>
<p>{user.name}</p>
</div>
)}
disabled={isEditing}
/>
)
}}
Expand Down Expand Up @@ -322,6 +324,7 @@ export default function CreateTipDialog({
renderItem={({ field }) => (
<Combobox
items={sortedRaces}
disabled={isEditing}
value={field.value}
onSelect={(raceId) => {
field.onChange(raceId)
Expand All @@ -341,8 +344,16 @@ export default function CreateTipDialog({

async function onSubmit(data: Schema) {
setMessage(undefined)
if (!predictionEntryId) {
toast.error('Something went wrong', {
description: 'Tip does not seem to exist',
})
return
}
startTransition(async () => {
const response = await createTip(data)
const response = isEditing
? await updateTip(predictionEntryId, data)
: await createTip(data)
if (!response.ok) {
setMessage({
title: 'Did not save',
Expand All @@ -351,10 +362,31 @@ export default function CreateTipDialog({
})
return
}
toast.success('Tip created')
toast.success(copy.toast[mode])
setOpen(false)
router.refresh()
return
})
}

function getCopy() {
return {
title: {
create: 'Create new tip',
edit: 'Update tip',
},
description: {
create: 'Create a tip on behalf of a member of the group.',
edit: 'Change this existing tip',
},
button: {
create: 'Save',
edit: 'Save',
},
toast: {
create: 'Tip created',
edit: 'Tip updated',
},
} as const
}
}
16 changes: 16 additions & 0 deletions app/tipping/group-admin/_components/data-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
getPaginationRowModel,
SortingState,
getSortedRowModel,
Row,
SortingFn,
} from '@tanstack/react-table'
import { Button } from '@/components/ui/button'

Expand All @@ -20,6 +22,7 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/table'
import { RACE_PREDICTION_FIELDS } from '@/constants'

interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
Expand All @@ -39,6 +42,10 @@ export function DataTable<TData, TValue>({
id: 'user',
desc: true,
},
{
id: 'position',
desc: true,
},
])

const table = useReactTable({
Expand Down Expand Up @@ -130,4 +137,13 @@ export function DataTable<TData, TValue>({
</div>
</div>
)
function sortByPosition<TData>(): SortingFn<TData> {
const fields = RACE_PREDICTION_FIELDS
return (rowA: Row<TData>, rowB: Row<TData>, columnId: string) => {
return (
fields.indexOf(rowA.getValue(columnId)) -
fields.indexOf(rowB.getValue(columnId))
)
}
}
}
26 changes: 26 additions & 0 deletions app/tipping/group-admin/_components/edit-tip-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use client'
import * as React from 'react'
import { TipFormData } from './create-edit-tip-dialog'

const GroupAdminContext = React.createContext<TipFormData | null>(null)
export function useTipFormContext() {
const context = React.useContext(GroupAdminContext)
if (!context) {
throw new Error('useTipFormContext must be used within a TipFormProvider')
}
return context
}

export default function TipFormProvider({
context,
children,
}: {
context: TipFormData
children: React.ReactNode
}) {
return (
<GroupAdminContext.Provider value={context}>
{children}
</GroupAdminContext.Provider>
)
}
35 changes: 29 additions & 6 deletions app/tipping/group-admin/_components/row-action.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
'use client'

import { PredictionRow } from '../_utils/rows'
import { LucideMoreHorizontal } from 'lucide-react'
import { LucideMoreHorizontal, LucidePen } from 'lucide-react'

import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
Expand All @@ -12,21 +11,45 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { useTipFormContext } from './edit-tip-context'
import CreateOrEditTipDialog from './create-edit-tip-dialog'
import Button from '@/components/button'
import { Button as ShadButton } from '@/components/ui/button'

export default function RowAction({ row }: { row: PredictionRow }) {
const context = useTipFormContext()

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='ghost' className='h-8 w-8 p-0'>
<ShadButton variant='ghost' className='h-8 w-8 p-0'>
<span className='sr-only'>Open menu</span>
<LucideMoreHorizontal className='h-4 w-4' />
</Button>
</ShadButton>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>View customer</DropdownMenuItem>
<DropdownMenuItem>View payment details</DropdownMenuItem>
<DropdownMenuItem asChild>
<CreateOrEditTipDialog
{...context}
predictionEntryId={row.id}
defaultValues={{
userId: row.user.id,
raceId: row.race.id,
position: row.position,
valueId: row.value.id,
}}
button={
<Button
label='Edit tip'
icon={LucidePen}
variant='ghost'
className='w-full text-start justify-start'
/>
}
/>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
Expand Down
Loading