Skip to content

Commit 0d641ba

Browse files
committed
feat: move user flags to table
1 parent 98f3554 commit 0d641ba

6 files changed

Lines changed: 397 additions & 162 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { Attendee, User } from "@dotkomonline/types"
2+
import { cn, Text } from "@dotkomonline/ui"
3+
import { getAttendeePlate } from "./AttendeePlate"
4+
5+
interface AttendeeListProps {
6+
attendees: Attendee[]
7+
user: User
8+
maxNumberOfAttendees: number
9+
}
10+
11+
export const AttendeeList = ({ attendees, user, maxNumberOfAttendees }: AttendeeListProps) => {
12+
if (!attendees.length) {
13+
return <Text className="text-gray-900 text-sm mx-2">Ingen påmeldte</Text>
14+
}
15+
16+
return attendees.map((attendee, index) => {
17+
const Plate = getAttendeePlate(attendee)
18+
const minWidth = getMinWidth(maxNumberOfAttendees)
19+
20+
return (
21+
<div key={attendee.id} className="flex flex-row gap-1 items-center group">
22+
<Text
23+
style={{ minWidth }}
24+
className={cn(
25+
"text-gray-400 group-hover:text-black dark:text-stone-500 dark:group-hover:text-stone-300",
26+
"text-right text-sm font-mono transition-colors"
27+
)}
28+
>
29+
{index + 1}.
30+
</Text>
31+
32+
<Plate attendee={attendee} user={user} />
33+
</div>
34+
)
35+
})
36+
}
37+
38+
function getMinWidth(maxNumberOfAttendees: number) {
39+
const value = maxNumberOfAttendees.toString().length
40+
41+
return `${value}ch`
42+
}
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import { type Attendee, FlagName, type User } from "@dotkomonline/types"
2+
import {
3+
Avatar,
4+
AvatarFallback,
5+
AvatarImage,
6+
cn,
7+
Text,
8+
Title,
9+
Tooltip,
10+
TooltipContent,
11+
TooltipTrigger,
12+
} from "@dotkomonline/ui"
13+
import { IconRosetteDiscountCheckFilled, IconUser } from "@tabler/icons-react"
14+
import Image from "next/image"
15+
import Link from "next/link.js"
16+
import type { FC, JSX, PropsWithChildren, ReactNode } from "react"
17+
18+
const getAttendeeIcons = (attendee: Attendee) => {
19+
const smallIcons: JSX.Element[] = []
20+
let largeIcon: JSX.Element | null = null
21+
22+
const exceptionallyDistinguishedFlag = attendee.user.flags.find(
23+
({ name }) => name === FlagName.EXCEPTIONALLY_DISTINGUISHED
24+
)
25+
26+
if (exceptionallyDistinguishedFlag !== undefined) {
27+
const isLarge = largeIcon === null
28+
//const isLarge = false
29+
30+
const icon = (
31+
<Tooltip key={FlagName.EXCEPTIONALLY_DISTINGUISHED} delayDuration={100}>
32+
<TooltipTrigger
33+
className={cn(
34+
"relative h-fit rounded-full p-0.5 bg-[#fff8e7] shadow-[0_0_12px_rgba(245,183,66,.35)] dark:bg-[#0f4c61]",
35+
!isLarge && "-m-1"
36+
)}
37+
>
38+
<Avatar
39+
className={cn(
40+
"shimmer shadow-[0_0_10px_rgba(245,183,66,.25)]",
41+
isLarge ? "size-9" : "size-5 ring-1 ring-[#efbd5b] ring-offset-1"
42+
)}
43+
>
44+
<AvatarImage src={exceptionallyDistinguishedFlag.imageUrl ?? undefined} />
45+
<AvatarFallback>
46+
<Text
47+
element="div"
48+
className="h-full w-full bg-[#efbd5b] text-[9px] font-semibold text-[#0f5872] flex items-center justify-center"
49+
>
50+
SU
51+
</Text>
52+
</AvatarFallback>
53+
</Avatar>
54+
</TooltipTrigger>
55+
56+
<TooltipContent className="flex flex-col gap-2">
57+
<Title element="p" className="">
58+
Særskilt utmerket
59+
</Title>
60+
{exceptionallyDistinguishedFlag.imageUrl && (
61+
<div className="w-full mt-4">
62+
<div className="relative mx-auto rounded-full w-fit">
63+
<div className="absolute inset-0 rounded-full size-52 bg-accent/40 blur-lg animate-pulse" />
64+
<div className="relative p-2 bg-white rounded-full">
65+
<Image
66+
src={exceptionallyDistinguishedFlag.imageUrl}
67+
alt={exceptionallyDistinguishedFlag.name}
68+
width={200}
69+
height={200}
70+
className="rounded-full"
71+
/>
72+
</div>
73+
</div>
74+
</div>
75+
)}
76+
{exceptionallyDistinguishedFlag.description && (
77+
<Text className="text-xs text-gray-500 dark:text-stone-500">
78+
{exceptionallyDistinguishedFlag.description}
79+
</Text>
80+
)}
81+
</TooltipContent>
82+
</Tooltip>
83+
)
84+
85+
if (isLarge) {
86+
largeIcon = icon
87+
} else {
88+
smallIcons.push(icon)
89+
}
90+
}
91+
92+
if (attendee.user.flags.some(({ name }) => name === FlagName.VANITY_VERIFIED)) {
93+
smallIcons.push(
94+
<Tooltip key={FlagName.VANITY_VERIFIED} delayDuration={100}>
95+
<TooltipTrigger>
96+
<IconRosetteDiscountCheckFilled className="size-[1.25em] text-blue-600 dark:text-sky-700" />
97+
</TooltipTrigger>
98+
<TooltipContent>
99+
<Text>OW Verified</Text>
100+
</TooltipContent>
101+
</Tooltip>
102+
)
103+
}
104+
105+
return { largeIcon, smallIcons }
106+
}
107+
108+
export const getAttendeePlate = (attendee: Attendee): FC<PlateProps> => {
109+
if (attendee.user.flags.some(({ name }) => name === FlagName.EXCEPTIONALLY_DISTINGUISHED)) {
110+
return ExceptionallyDistinguishedPlate
111+
}
112+
113+
if (attendee.user.flags.some(({ name }) => name === FlagName.VANITY_VERIFIED)) {
114+
return VanityVerifiedPlate
115+
}
116+
117+
return GenericPlate
118+
}
119+
120+
interface PlateProps {
121+
attendee: Attendee
122+
user: User
123+
userSection?: ReactNode
124+
rightSection?: ReactNode
125+
}
126+
127+
type GenericUserSectionProps = PropsWithChildren<{
128+
attendee: Attendee
129+
nameClassName?: string
130+
gradeClassName?: string
131+
}>
132+
133+
const GenericUserSection = ({ attendee, nameClassName, gradeClassName, children }: GenericUserSectionProps) => (
134+
<div className="flex flex-col gap-0.5 grow min-w-0">
135+
<div className="flex items-center gap-2">
136+
<Text className={cn("text-sm truncate", nameClassName)} title={attendee.user.name ?? undefined}>
137+
{attendee.user.name}
138+
</Text>
139+
140+
{children}
141+
</div>
142+
<Text className={cn("text-xs truncate", gradeClassName)}>
143+
{attendee.userGrade ? `${attendee.userGrade}. klasse` : "Ingen klasse"}
144+
</Text>
145+
</div>
146+
)
147+
148+
const GenericPlate = ({ attendee, user, userSection: customUserSection, rightSection }: PlateProps) => {
149+
const { largeIcon, smallIcons } = getAttendeeIcons(attendee)
150+
const isUser = attendee.userId === user.id
151+
152+
return (
153+
<Link
154+
href={`/profil/${attendee.user.username}`}
155+
className={cn(
156+
"flex flex-1 min-w-0 items-center gap-4 p-1.5 rounded-lg w-full overflow-x-hidden transition-colors",
157+
!isUser && "hover:bg-gray-100 dark:hover:bg-stone-700",
158+
isUser && "bg-blue-100 hover:bg-blue-200 dark:bg-sky-950 dark:hover:bg-sky-900"
159+
)}
160+
>
161+
<Avatar className={cn("size-10", isUser && "outline-2 outline-blue-500 dark:outline-sky-800")}>
162+
<AvatarImage src={attendee.user.imageUrl ?? undefined} />
163+
<AvatarFallback className="bg-gray-500 dark:bg-stone-500">
164+
<IconUser className="size-[1.25em]" />
165+
</AvatarFallback>
166+
</Avatar>
167+
168+
{customUserSection || (
169+
<GenericUserSection
170+
attendee={attendee}
171+
gradeClassName={cn(!isUser && "text-gray-900 dark:text-stone-300", isUser && "text-black dark:text-white")}
172+
>
173+
{smallIcons}
174+
</GenericUserSection>
175+
)}
176+
177+
{rightSection ||
178+
(largeIcon && <div className="ml-auto flex items-center justify-center">{rightSection || largeIcon}</div>)}
179+
</Link>
180+
)
181+
}
182+
183+
export { GenericPlate as AttendeePlate }
184+
185+
const VanityVerifiedPlate = ({ attendee, rightSection }: PlateProps) => {
186+
const { largeIcon, smallIcons } = getAttendeeIcons(attendee)
187+
188+
return (
189+
<Link
190+
href={`/profil/${attendee.user.username}`}
191+
className={cn(
192+
"flex flex-1 min-w-0 items-center gap-4 p-1.5 rounded-lg w-full overflow-x-hidden transition-colors",
193+
"bg-linear-to-r",
194+
"from-yellow-200 via-yellow-100 hover:from-yellow-300 hover:via-yellow-200 hover:to-yellow-200",
195+
"dark:from-yellow-500 dark:via-yellow-600 dark:hover:from-yellow-400 dark:hover:via-yellow-500 dark:hover:to-yellow-800"
196+
)}
197+
>
198+
<Avatar className="size-10 outline-2 outline-yellow-500 dark:outline-yellow-600">
199+
<AvatarImage src={attendee.user.imageUrl ?? undefined} />
200+
<AvatarFallback className="bg-yellow-500 dark:bg-yellow-700">
201+
<IconUser className="size-[1.25em]" />
202+
</AvatarFallback>
203+
</Avatar>
204+
205+
<GenericUserSection attendee={attendee} nameClassName="dark:text-black" gradeClassName="dark:text-black">
206+
{smallIcons}
207+
</GenericUserSection>
208+
209+
{rightSection ||
210+
(largeIcon && <div className="ml-auto flex items-center justify-center">{rightSection || largeIcon}</div>)}
211+
</Link>
212+
)
213+
}
214+
215+
const ExceptionallyDistinguishedPlate = ({ attendee, user, rightSection }: PlateProps) => {
216+
const { largeIcon, smallIcons } = getAttendeeIcons(attendee)
217+
const isUser = attendee.userId === user.id
218+
219+
return (
220+
<Link
221+
href={`/profil/${attendee.user.username}`}
222+
className={cn(
223+
"group relative flex flex-1 min-w-0 items-center gap-4 p-1.5 rounded-lg w-full overflow-hidden",
224+
"transition-all duration-300",
225+
226+
// Base card
227+
"bg-[linear-gradient(115deg,#0f4f67_0%,#135a73_28%,#1f6a78_46%,#d6b15c_68%,#f2d28a_82%,#d7e5e6_100%)]",
228+
"dark:bg-[linear-gradient(115deg,#083245_0%,#0d4457_30%,#145568_46%,#8b6420_70%,#c69a41_84%,#b8d0d2_100%)]",
229+
230+
// Soft internal lighting
231+
"before:pointer-events-none before:absolute before:inset-0 before:content-['']",
232+
"before:bg-[radial-gradient(circle_at_3.2rem_50%,rgba(255,213,128,0.30),transparent_18%),radial-gradient(circle_at_58%_50%,rgba(255,205,110,0.18),transparent_24%)]",
233+
"dark:before:bg-[radial-gradient(circle_at_3.2rem_50%,rgba(255,213,128,0.18),transparent_18%),radial-gradient(circle_at_58%_50%,rgba(255,205,110,0.12),transparent_24%)]"
234+
)}
235+
>
236+
<Avatar
237+
className={cn(
238+
"relative size-10 shrink-0",
239+
"outline-2 outline-[#f0bb55]",
240+
"shadow-[0_0_0_3px_rgba(255,215,130,.12),0_0_18px_rgba(255,191,73,.35)]"
241+
)}
242+
>
243+
<AvatarImage src={attendee.user.imageUrl ?? undefined} />
244+
<AvatarFallback className="bg-[#0f5872] text-white">
245+
<IconUser className="size-[1.25em]" />
246+
</AvatarFallback>
247+
</Avatar>
248+
249+
<GenericUserSection
250+
attendee={attendee}
251+
nameClassName="text-[#fff1cc] font-medium"
252+
gradeClassName="text-[#f4d79a]"
253+
>
254+
{smallIcons}
255+
</GenericUserSection>
256+
257+
{rightSection || (largeIcon && <div className="ml-auto flex items-center justify-center">{largeIcon}</div>)}
258+
</Link>
259+
)
260+
}

0 commit comments

Comments
 (0)