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
24 changes: 22 additions & 2 deletions app/tipping/group-admin/_components/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import RowAction from './row-action'
import { Button } from '@/components/ui/button'
import { LucideArrowDown, LucideArrowUp, LucideArrowUpDown } from 'lucide-react'
import { RACE_PREDICTION_FIELDS } from '@/constants'
import { Icon } from '@/components/icon'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'

export const columns: ColumnDef<PredictionRow>[] = [
{
Expand Down Expand Up @@ -86,9 +92,23 @@ export const columns: ColumnDef<PredictionRow>[] = [
}) {
switch (overwrite) {
case 'countAsCorrect':
return <p className='text-green-600 dark:text-green-200'>Correct</p>
return (
<Tooltip>
<TooltipTrigger className='flex items-center'>
<Icon.CorrectTip className='text-success' size={16} />
</TooltipTrigger>
<TooltipContent>Scored as correct</TooltipContent>
</Tooltip>
)
case 'countAsIncorrect':
return <p className='text-destructive'>Incorrect</p>
return (
<Tooltip>
<TooltipTrigger className='flex items-center'>
<Icon.IncorrectTip className='text-destructive' size={16} />
</TooltipTrigger>
<TooltipContent>Scored as incorrect</TooltipContent>
</Tooltip>
)

default:
return <p className='italic text-muted-foreground/50 text-xs'>None</p>
Expand Down
68 changes: 66 additions & 2 deletions app/tipping/group-admin/_components/create-edit-tip-dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
'use client'

import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue as ShadSelectValue,
} from '@/components/ui/select'
import Alert from '@/components/alert'
import { Combobox } from '@/components/combobox'
import { ConstructorProps } from '@/components/constructor'
Expand All @@ -20,7 +27,6 @@ import {
FieldGroup,
FieldLabel,
} from '@/components/ui/field'
import UserAvatar from '@/components/user-avatar'
import { RACE_PREDICTION_FIELDS, RacePredictionField } from '@/constants'
import { Database } from '@/db/types'
import {
Expand All @@ -31,7 +37,13 @@ import {
} from '@/lib/utils/prediction-fields'
import { zodResolver } from '@hookform/resolvers/zod'
import { isFuture } from 'date-fns'
import { LucideInfo, LucidePlus, LucideTriangleAlert } from 'lucide-react'
import {
LucideCheckCircle,
LucideInfo,
LucidePlus,
LucideTriangleAlert,
LucideXCircle,
} from 'lucide-react'
import React from 'react'
import {
Controller,
Expand All @@ -45,6 +57,8 @@ import Button from '@/components/button'
import { formSchema, Schema } from '../_utils/schema'
import { useRouter } from 'next/navigation'
import { SelectUser } from './select-user'
import { TIP_OVERWRITE_OPTIONS } from '@/db/schema/schema'
import { Icon } from '@/components/icon'

type RaceOption = Pick<
Database.Race,
Expand Down Expand Up @@ -145,6 +159,30 @@ export default function CreateOrEditTipDialog({
<SelectRace />
<SelectPosition />
<SelectValue />
<FormField
name='overwriteTo'
label='Score as'
renderItem={({ field }) => (
<Select
value={field.value ?? undefined}
onValueChange={field.onChange}
>
<SelectTrigger className='w-[180px]'>
<ShadSelectValue placeholder='Select' />
</SelectTrigger>
<SelectContent>
{getSelectOptions().map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.icon && (
<option.icon className={option.className} />
)}
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
</FieldGroup>
{message && (
<Alert
Expand Down Expand Up @@ -181,6 +219,32 @@ export default function CreateOrEditTipDialog({
</Dialog>
)

function getSelectOptions() {
const options = [
{
label: 'Normal',
value: 'normal',
},
{
label: 'Correct',
value: 'countAsCorrect',
className: 'text-success',
icon: Icon.CorrectTip,
},
{
label: 'Incorrect',
value: 'countAsIncorrect',
className: 'text-destructive',
icon: Icon.IncorrectTip,
},
] satisfies ({
value: (typeof TIP_OVERWRITE_OPTIONS)[number] | 'normal'
label: string
} & Record<string, any>)[]

return options
}

function onPositionChange(position: RacePredictionField | undefined) {
if (!position) {
setTipType('driver')
Expand Down
1 change: 1 addition & 0 deletions app/tipping/group-admin/_components/row-action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export default function RowAction({ row }: { row: PredictionRow }) {
raceId: row.race.id,
position: row.position,
valueId: row.value.id,
overwriteTo: row.overwrite,
}}
button={
<Button
Expand Down
12 changes: 12 additions & 0 deletions app/tipping/group-admin/_utils/create-tip-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export async function createTip(data: Schema): Promise<ServerResponse> {
await db.insert(predictionEntriesTable).values({
predictionId,
position,
overwriteTo: getOverwrite(data.overwriteTo),
...valueObject,
})
}
Expand Down Expand Up @@ -188,6 +189,7 @@ export async function updateTip(
.update(predictionEntriesTable)
.set({
...getValueObject(data),
overwriteTo: getOverwrite(data.overwriteTo),
})
.where(eq(predictionEntriesTable.id, predictionEntryId))
}
Expand Down Expand Up @@ -221,3 +223,13 @@ async function getPrediction({
},
})
}

function getOverwrite(overwrite: Schema['overwriteTo']) {
if (!overwrite) {
return null
}
if (overwrite === 'normal') {
return null
}
return overwrite
}
2 changes: 2 additions & 0 deletions app/tipping/group-admin/_utils/schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { RACE_PREDICTION_FIELDS } from '@/constants'
import { TIP_OVERWRITE_OPTIONS } from '@/db/schema/schema'
import z from 'zod/v3'

export type Schema = z.infer<typeof formSchema>
Expand All @@ -7,4 +8,5 @@ export const formSchema = z.object({
raceId: z.string(),
position: z.enum(RACE_PREDICTION_FIELDS),
valueId: z.string(),
overwriteTo: z.enum([...TIP_OVERWRITE_OPTIONS, 'normal']).nullish(),
})
11 changes: 3 additions & 8 deletions app/tipping/group-admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,9 @@ export default async function GroupSettings() {
</div>
<CreateOrEditTipDialog {...formProps} />
</div>
<div>
<div className='flex items-center flex-wrap gap-2'>
<p>Filters</p>
</div>
<TipFormProvider context={formProps}>
<DataTable columns={columns} data={rows} />
</TipFormProvider>
</div>
<TipFormProvider context={formProps}>
<DataTable columns={columns} data={rows} />
</TipFormProvider>
</section>
</div>
)
Expand Down
60 changes: 36 additions & 24 deletions app/tipping/leaderboard/_components/PastRacesClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Constructors,
RacePredictionMaps,
RacesWithResults,
UserMapEntry,
} from './PastRacesServer'
import { Button } from '@/components/ui/button'
import {
Expand Down Expand Up @@ -146,21 +147,21 @@ export default function PastRacesClient({
<div>
<UserResults
positionText='P1'
users={row.predictedP1By}
userInfo={row.predictedP1By}
isCorrect={row.isP1Correct}
/>
</div>
<div>
<UserResults
positionText='P10'
users={row.predictedP10By}
userInfo={row.predictedP10By}
isCorrect={row.isP10Correct}
/>
</div>
<div>
<UserResults
positionText='Last'
users={row.predictedLast}
userInfo={row.predictedLast}
isCorrect={row.isLastCorrect}
/>
</div>
Expand Down Expand Up @@ -188,7 +189,7 @@ export default function PastRacesClient({
<div>
<UserResults
positionText='Pole'
users={row.predictedBy}
userInfo={row.predictedBy}
isCorrect={row.isCorrect}
maxUsersOverwrite={6}
/>
Expand Down Expand Up @@ -223,7 +224,7 @@ export default function PastRacesClient({
<TableCell>
<div>
<UserResults
users={row.users}
userInfo={row.users}
isCorrect={row.isCorrect}
maxUsersOverwrite={8}
/>
Expand All @@ -250,8 +251,8 @@ export default function PastRacesClient({
<TableCell>
<div>
<UserResults
positionText='Pole'
users={row.predictedP1By}
positionText='P1'
userInfo={row.predictedP1By}
isCorrect={row.isP1Correct}
/>
</div>
Expand Down Expand Up @@ -306,32 +307,32 @@ export default function PastRacesClient({
}

function UserResults({
users,
userInfo: userInfo,
positionText,
isCorrect,
maxUsersOverwrite,
}: {
users?: Pick<Database.User, 'id' | 'name' | 'image'>[]
userInfo?: UserMapEntry[]
positionText?: string
isCorrect: boolean
maxUsersOverwrite?: number
}) {
if (!users?.length) {
if (!userInfo?.length) {
return
}
const localMaxUsers = maxUsersOverwrite ?? maxUsers
return (
<Collapsible>
<CollapsibleTrigger className='flex items-center'>
<TriggerRow users={users} />
<TriggerRow users={userInfo} />
</CollapsibleTrigger>
<CollapsibleContent>
<Content users={users} />
<Content users={userInfo} />
</CollapsibleContent>
</Collapsible>
)

function Content(props: { users: NonNullable<typeof users> }) {
function Content(props: { users: NonNullable<typeof userInfo> }) {
return (
<>
<Separator className='my-3' />
Expand All @@ -340,19 +341,19 @@ export default function PastRacesClient({
{positionText ? `Tipped ${positionText} by` : 'Tipped by'}
</p>
<ul className='py-2'>
{props.users.map((user) => {
{props.users.map((userInfo) => {
return (
<li
className='py-2 first:pt-0 last:pb-0 border-b last:border-b-0 flex items-center gap-1'
key={user.id}
key={userInfo.user.id}
>
<UserAvatar
key={user.id}
{...user}
key={userInfo.user.id}
{...userInfo.user}
className='hidden sm:block size-6 lg:size-8 rounded-lg'
/>
<span className='text-xs lg:text-sm text-muted-foreground'>
{user.name}
{userInfo.user.name}
</span>
</li>
)
Expand All @@ -363,7 +364,7 @@ export default function PastRacesClient({
)
}

function TriggerRow(props: { users: NonNullable<typeof users> }) {
function TriggerRow(props: { users: NonNullable<typeof userInfo> }) {
return (
<span className='flex items-center gap-1'>
{positionText && (
Expand All @@ -381,17 +382,28 @@ export default function PastRacesClient({
<div
className={cn(
'*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2',
!isCorrect && '*:data-[slot=avatar]:grayscale',
)}
>
{users?.slice(0, localMaxUsers).map((user) => {
{userInfo?.slice(0, localMaxUsers).map((userInfo) => {
return (
<UserAvatar
key={user.id}
{...user}
className='size-6 lg:size-8 rounded-full'
key={userInfo.user.id}
{...userInfo.user}
className={cn(
'size-6 lg:size-8 rounded-full ',
getIsGrayscale() && 'data-[slot=avatar]:grayscale',
)}
/>
)
function getIsGrayscale() {
if (userInfo.overwriteTo === 'countAsIncorrect') {
return true
}
if (userInfo.overwriteTo === 'countAsCorrect') {
return false
}
return !isCorrect
}
})}
{props.users.length > localMaxUsers && (
<div className='size-6 lg:size-8 rounded-full bg-muted relative z-10 grid place-items-center text-xs font-medium ring-border text-muted-foreground tracking-tight ring-2 dark:ring-background'>
Expand Down
Loading