this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: refactor te full code

+525 -1573
+1 -1
src/app/[id]/page.tsx
··· 1 1 import { redirect } from "next/navigation"; 2 2 3 - import Group from "@/components/group"; 3 + import { Group } from "@/components/group"; 4 4 import { getGroup } from "@/lib/group"; 5 5 import { redeemInvite } from "@/lib/invite"; 6 6 import { getVouchers } from "@/lib/voucher";
+30 -37
src/app/auth/page.tsx
··· 2 2 3 3 import { TicketStarIcon } from "@hugeicons/core-free-icons"; 4 4 import { HugeiconsIcon } from "@hugeicons/react"; 5 + import { use } from "react"; 5 6 import { Button } from "@/components/ui/button"; 6 7 import { 7 - Field, 8 - FieldDescription, 9 - FieldGroup, 10 - FieldLabel, 11 - } from "@/components/ui/field"; 8 + Empty, 9 + EmptyContent, 10 + EmptyDescription, 11 + EmptyHeader, 12 + EmptyMedia, 13 + EmptyTitle, 14 + } from "@/components/ui/empty"; 12 15 import { authClient } from "@/lib/auth-client"; 13 16 14 17 export default function Auth({ 15 18 searchParams, 16 19 }: { 17 - searchParams: { next?: string }; 20 + searchParams: Promise<{ next?: string }>; 18 21 }) { 19 - const next = searchParams.next || "/"; 22 + const { next } = use(searchParams); 20 23 21 24 return ( 22 - <div className="flex flex-col h-screen justify-center gap-6 w-md mx-auto"> 23 - <FieldGroup> 24 - <div className="flex flex-col items-center gap-2"> 25 + <Empty className="h-screen"> 26 + <EmptyHeader> 27 + <EmptyMedia variant="icon"> 25 28 <HugeiconsIcon icon={TicketStarIcon} /> 26 - <FieldLabel className="text-xl font-bold"> 27 - Welcome to Vouch 28 - </FieldLabel> 29 - <FieldDescription className="text-center"> 30 - Sign in using a third-party provider. 31 - </FieldDescription> 32 - </div> 33 - <Field> 34 - <Button 35 - variant="outline" 36 - type="button" 37 - onClick={() => { 38 - authClient.signIn.social({ 39 - provider: "google", 40 - callbackURL: next, 41 - }); 42 - }} 43 - > 44 - Continue with Google 45 - </Button> 46 - </Field> 47 - </FieldGroup> 48 - <FieldDescription className="px-6 text-center"> 49 - By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "} 50 - and <a href="#">Privacy Policy</a>. 51 - </FieldDescription> 52 - </div> 29 + </EmptyMedia> 30 + <EmptyTitle>Welcome to Vouch</EmptyTitle> 31 + <EmptyDescription>You need to sign in to continue</EmptyDescription> 32 + </EmptyHeader> 33 + <EmptyContent> 34 + <Button 35 + onClick={() => 36 + authClient.signIn.social({ 37 + provider: "google", 38 + callbackURL: next || "/", 39 + }) 40 + } 41 + > 42 + Continue with Google 43 + </Button> 44 + </EmptyContent> 45 + </Empty> 53 46 ); 54 47 }
+15 -156
src/app/page.tsx
··· 1 - "use client"; 1 + import { headers } from "next/headers"; 2 + import { Groups } from "@/components/groups"; 3 + import { auth } from "@/lib/auth"; 4 + import { getGroups } from "@/lib/group"; 2 5 3 - import { Delete01Icon } from "@hugeicons/core-free-icons"; 4 - import { HugeiconsIcon } from "@hugeicons/react"; 5 - import Link from "next/link"; 6 - import { useEffect, useState } from "react"; 7 - import CustomAvatar from "@/components/avatar"; 8 - import { Button } from "@/components/ui/button"; 9 - import { 10 - Card, 11 - CardAction, 12 - CardContent, 13 - CardDescription, 14 - CardHeader, 15 - CardTitle, 16 - } from "@/components/ui/card"; 17 - import { 18 - Dialog, 19 - DialogClose, 20 - DialogContent, 21 - DialogTrigger, 22 - } from "@/components/ui/dialog"; 23 - import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; 24 - import { Input } from "@/components/ui/input"; 25 - import type { group } from "@/db/schema"; 26 - import { authClient } from "@/lib/auth-client"; 27 - import { createGroup, deleteGroup, getGroups, updateGroup } from "@/lib/group"; 6 + export default async function Page() { 7 + const session = await auth.api.getSession({ 8 + headers: await headers(), 9 + }); 28 10 29 - export type Group = typeof group.$inferSelect; 30 - 31 - export default function Home() { 32 - const { data: session } = authClient.useSession(); 33 - const user = session?.user; 34 - 35 - const [groups, setGroups] = useState<Group[]>([]); 36 - 37 - useEffect(() => { 38 - getGroups().then(setGroups); 39 - }, []); 11 + if (!session) return null; 12 + const groups = await getGroups(); 40 13 41 - if (!user) return null; 14 + const user = { 15 + ...session.user, 16 + image: session.user.image || "", 17 + }; 42 18 43 - return ( 44 - <Card> 45 - <CardHeader> 46 - <CardTitle>Vouch</CardTitle> 47 - <CardDescription>Your shared promises</CardDescription> 48 - <CardAction> 49 - <Link href="/settings"> 50 - <CustomAvatar name={user.name} image={user.image as string} /> 51 - </Link> 52 - </CardAction> 53 - </CardHeader> 54 - <CardContent className="grid gap-4"> 55 - {groups.map((group) => ( 56 - <Card key={group.id}> 57 - <CardHeader> 58 - <Link href={`/${group.id}`}> 59 - <CardTitle>{group.name}</CardTitle> 60 - {/* <CardDescription> 61 - {group.stats.members} members, {group.stats.total} vouches 62 - </CardDescription> */} 63 - </Link> 64 - <CardAction> 65 - {group.userId === user.id && ( 66 - <Dialog> 67 - <DialogTrigger render={<Button>Edit</Button>} /> 68 - <DialogContent> 69 - <form 70 - onSubmit={(e) => { 71 - e.preventDefault(); 72 - const formData = new FormData(e.currentTarget); 73 - const name = formData.get("name"); 74 - updateGroup(group.id, name as string).then((res) => { 75 - if (!res) return; 76 - setGroups((prevGroups) => 77 - prevGroups.map((g) => 78 - g.id === group.id ? res : g, 79 - ), 80 - ); 81 - }); 82 - }} 83 - > 84 - <FieldGroup> 85 - <Field> 86 - <FieldLabel htmlFor="name">Name</FieldLabel> 87 - <Input 88 - id="name" 89 - name="name" 90 - defaultValue={group.name} 91 - required 92 - /> 93 - </Field> 94 - <Field orientation="horizontal"> 95 - <DialogClose 96 - render={ 97 - <Button 98 - variant="destructive" 99 - onClick={(e) => { 100 - e.preventDefault(); 101 - deleteGroup(group.id).then((res) => { 102 - if (!res) return; 103 - setGroups((prevGroups) => 104 - prevGroups.filter( 105 - (g) => g.id !== group.id, 106 - ), 107 - ); 108 - }); 109 - }} 110 - > 111 - <HugeiconsIcon icon={Delete01Icon} /> 112 - </Button> 113 - } 114 - /> 115 - <DialogClose 116 - render={ 117 - <Button type="submit" className="flex-1"> 118 - Update 119 - </Button> 120 - } 121 - /> 122 - </Field> 123 - </FieldGroup> 124 - </form> 125 - </DialogContent> 126 - </Dialog> 127 - )} 128 - </CardAction> 129 - </CardHeader> 130 - </Card> 131 - ))} 132 - <Dialog> 133 - <DialogTrigger render={<Button>Create group</Button>} /> 134 - <DialogContent> 135 - <form 136 - onSubmit={(e) => { 137 - e.preventDefault(); 138 - const formData = new FormData(e.currentTarget); 139 - const name = formData.get("name"); 140 - createGroup(name as string).then((res) => { 141 - if (!res) return; 142 - setGroups((prevGroups) => [...prevGroups, res]); 143 - }); 144 - }} 145 - > 146 - <FieldGroup> 147 - <Field> 148 - <FieldLabel htmlFor="name">Name</FieldLabel> 149 - <Input id="name" name="name" required /> 150 - </Field> 151 - <Field> 152 - <DialogClose render={<Button type="submit">Create</Button>} /> 153 - </Field> 154 - </FieldGroup> 155 - </form> 156 - </DialogContent> 157 - </Dialog> 158 - </CardContent> 159 - </Card> 160 - ); 19 + return <Groups user={user} groups={groups} />; 161 20 }
+134 -106
src/components/group.tsx
··· 4 4 ArrowLeft01Icon, 5 5 Copy01Icon, 6 6 Delete01Icon, 7 - Settings01Icon, 8 - Share08Icon, 9 - UserGroupIcon, 10 7 UserMultipleIcon, 11 8 } from "@hugeicons/core-free-icons"; 12 9 import { HugeiconsIcon } from "@hugeicons/react"; ··· 14 11 import { useState } from "react"; 15 12 import CustomAvatar from "@/components/avatar"; 16 13 import { Button } from "@/components/ui/button"; 14 + import { ButtonGroup } from "@/components/ui/button-group"; 17 15 import { 18 16 Card, 19 17 CardAction, ··· 26 24 Dialog, 27 25 DialogClose, 28 26 DialogContent, 29 - DialogDescription, 30 27 DialogHeader, 31 28 DialogTitle, 32 29 DialogTrigger, 33 30 } from "@/components/ui/dialog"; 34 - import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; 31 + import { 32 + Field, 33 + FieldDescription, 34 + FieldGroup, 35 + FieldLabel, 36 + FieldLegend, 37 + FieldSet, 38 + } from "@/components/ui/field"; 35 39 import { Input } from "@/components/ui/input"; 36 40 import { 37 41 InputGroup, ··· 48 52 ItemMedia, 49 53 ItemTitle, 50 54 } from "@/components/ui/item"; 51 - import { Progress } from "@/components/ui/progress"; 52 - import { Slider } from "@/components/ui/slider"; 53 55 import type { voucher } from "@/db/schema"; 54 56 import type { GroupWithMembers } from "@/lib/group"; 55 57 import { deleteMember } from "@/lib/member"; ··· 57 59 58 60 type Voucher = typeof voucher.$inferSelect; 59 61 60 - export default function Group({ 62 + export function Group({ 61 63 group, 62 64 vouchers: initialVouchers, 63 65 }: { ··· 65 67 vouchers: Voucher[]; 66 68 }) { 67 69 const [vouchers, setVouchers] = useState(initialVouchers); 70 + const users = [group.owner, ...group.members.map((m) => m.user)]; 68 71 69 72 return ( 70 - <> 71 - <header className="flex justify-between"> 72 - <Button variant="secondary" size="icon" render={<Link href="/" />}> 73 - <HugeiconsIcon icon={ArrowLeft01Icon} /> 74 - </Button> 75 - </header> 76 - <Card> 77 - <CardHeader> 78 - <CardTitle>{group.name}</CardTitle> 79 - <CardAction> 73 + <Card> 74 + <CardHeader> 75 + <CardTitle>{group.name}</CardTitle> 76 + <CardAction> 77 + <ButtonGroup> 78 + <Button 79 + variant="secondary" 80 + size="icon" 81 + render={<Link href="/" />} 82 + nativeButton={false} 83 + > 84 + <HugeiconsIcon icon={ArrowLeft01Icon} /> 85 + </Button> 80 86 <Dialog> 81 87 <DialogTrigger 82 88 render={ ··· 90 96 <DialogTitle>Members</DialogTitle> 91 97 </DialogHeader> 92 98 <ItemGroup> 93 - {[group.owner, ...group.members].map((user) => ( 99 + {users.map((user) => ( 94 100 <Item key={user.id}> 95 101 <ItemMedia> 96 102 <CustomAvatar ··· 137 143 </InputGroup> 138 144 </DialogContent> 139 145 </Dialog> 140 - </CardAction> 141 - </CardHeader> 142 - <CardContent className="grid gap-6"> 146 + </ButtonGroup> 147 + </CardAction> 148 + </CardHeader> 149 + <CardContent> 150 + <ItemGroup> 143 151 {vouchers.map((voucher) => ( 144 - <Card key={voucher.id}> 145 - <CardHeader> 146 - <CardTitle>{voucher.name}</CardTitle> 147 - <CardAction> 148 - <Button>{voucher.limit}</Button> 149 - </CardAction> 150 - </CardHeader> 151 - <CardContent> 152 - <Progress value={(1 / voucher.limit) * 100} /> 153 - </CardContent> 154 - <CardFooter> 152 + <Item key={voucher.id}> 153 + <ItemContent> 154 + <ItemTitle>{voucher.name}</ItemTitle> 155 + <ItemDescription>{voucher.description}</ItemDescription> 156 + </ItemContent> 157 + <ItemActions> 155 158 <Dialog> 156 159 <DialogTrigger 157 160 render={<Button className="w-full">Edit</Button>} ··· 162 165 e.preventDefault(); 163 166 const formData = new FormData(e.currentTarget); 164 167 const name = formData.get("name") as string; 165 - const limit = formData.get("limit") as string; 166 - updateVoucher(voucher.id, name, Number(limit)).then( 168 + const description = formData.get( 169 + "description", 170 + ) as string; 171 + updateVoucher(voucher.id, name, description).then( 167 172 (res) => { 168 173 if (!res) return; 169 174 setVouchers((prevVouchers) => ··· 175 180 ); 176 181 }} 177 182 > 178 - <FieldGroup> 179 - <Field> 180 - <FieldLabel htmlFor="name">Name</FieldLabel> 181 - <Input 182 - id="name" 183 - name="name" 184 - defaultValue={voucher.name} 185 - required 186 - /> 187 - </Field> 188 - <Field> 189 - <FieldLabel htmlFor="limit">Limit</FieldLabel> 190 - <Slider 191 - id="limit" 192 - name="limit" 193 - defaultValue={[voucher.limit]} 194 - max={20} 195 - /> 196 - </Field> 197 - <Field orientation="horizontal"> 198 - <DialogClose 199 - render={ 200 - <Button 201 - variant="destructive" 202 - onClick={(e) => { 203 - e.preventDefault(); 204 - deleteVoucher(voucher.id).then((res) => { 205 - if (!res) return; 206 - setVouchers((prevVouchers) => 207 - prevVouchers.filter( 208 - (v) => v.id !== voucher.id, 209 - ), 210 - ); 211 - }); 212 - }} 213 - > 214 - <HugeiconsIcon icon={Delete01Icon} /> 215 - </Button> 216 - } 217 - /> 218 - <DialogClose 219 - render={ 220 - <Button type="submit" className="flex-1"> 221 - Update 222 - </Button> 223 - } 224 - /> 225 - </Field> 226 - </FieldGroup> 183 + <FieldSet> 184 + <FieldLegend>Edit the voucher</FieldLegend> 185 + <FieldDescription>Are you happy now ?</FieldDescription> 186 + <FieldGroup> 187 + <Field> 188 + <FieldLabel htmlFor="name">Name</FieldLabel> 189 + <Input 190 + id="name" 191 + name="name" 192 + defaultValue={voucher.name} 193 + required 194 + /> 195 + </Field> 196 + <Field> 197 + <FieldLabel htmlFor="description"> 198 + Description 199 + </FieldLabel> 200 + <Input 201 + id="description" 202 + name="description" 203 + defaultValue={voucher.description || ""} 204 + required 205 + /> 206 + </Field> 207 + <Field orientation="horizontal"> 208 + <DialogClose 209 + render={ 210 + <Button 211 + variant="destructive" 212 + onClick={(e) => { 213 + e.preventDefault(); 214 + deleteVoucher(voucher.id).then((res) => { 215 + if (!res) return; 216 + setVouchers((prevVouchers) => 217 + prevVouchers.filter( 218 + (v) => v.id !== voucher.id, 219 + ), 220 + ); 221 + }); 222 + }} 223 + > 224 + <HugeiconsIcon icon={Delete01Icon} /> 225 + </Button> 226 + } 227 + /> 228 + <DialogClose 229 + render={ 230 + <Button type="submit" className="flex-1"> 231 + Update 232 + </Button> 233 + } 234 + /> 235 + </Field> 236 + </FieldGroup> 237 + </FieldSet> 227 238 </form> 228 239 </DialogContent> 229 240 </Dialog> 230 - </CardFooter> 231 - </Card> 241 + </ItemActions> 242 + </Item> 232 243 ))} 233 244 <Dialog> 234 245 <DialogTrigger render={<Button>Create Voucher</Button>} /> ··· 238 249 e.preventDefault(); 239 250 const formData = new FormData(e.currentTarget); 240 251 const name = formData.get("name") as string; 241 - const limit = formData.get("limit") as string; 242 - createVoucher(name, Number(limit), group.id).then((res) => { 252 + const description = formData.get("description") as string; 253 + createVoucher(group.id, name, description).then((res) => { 243 254 if (!res) return; 244 255 setVouchers((prevVouchers) => [...prevVouchers, res]); 245 256 }); 246 257 }} 247 258 > 248 - <FieldGroup> 249 - <Field> 250 - <FieldLabel htmlFor="name">Name</FieldLabel> 251 - <Input id="name" name="name" required /> 252 - </Field> 253 - <Field> 254 - <FieldLabel htmlFor="limit">Limit</FieldLabel> 255 - <Slider id="limit" name="limit" max={20} /> 256 - </Field> 257 - <Field> 258 - <DialogClose 259 - render={<Button type="submit">Create</Button>} 260 - /> 261 - </Field> 262 - </FieldGroup> 259 + <FieldSet> 260 + <FieldLegend>New voucher</FieldLegend> 261 + <FieldDescription> 262 + What's the name and description of your new voucher? You can 263 + always change this later. 264 + </FieldDescription> 265 + <FieldGroup> 266 + <Field> 267 + <FieldLabel htmlFor="name">Name</FieldLabel> 268 + <Input 269 + id="name" 270 + name="name" 271 + placeholder="My voucher" 272 + required 273 + /> 274 + </Field> 275 + <Field> 276 + <FieldLabel htmlFor="description">Description</FieldLabel> 277 + <Input 278 + id="description" 279 + name="description" 280 + placeholder="This voucher can be used to redeem 1 free coffee." 281 + required 282 + /> 283 + </Field> 284 + <Field> 285 + <DialogClose 286 + render={<Button type="submit">Create</Button>} 287 + /> 288 + </Field> 289 + </FieldGroup> 290 + </FieldSet> 263 291 </form> 264 292 </DialogContent> 265 293 </Dialog> 266 - </CardContent> 267 - </Card> 268 - </> 294 + </ItemGroup> 295 + </CardContent> 296 + </Card> 269 297 ); 270 298 }
+233
src/components/groups.tsx
··· 1 + "use client"; 2 + 3 + import { Delete01Icon } from "@hugeicons/core-free-icons"; 4 + import { HugeiconsIcon } from "@hugeicons/react"; 5 + import Link from "next/link"; 6 + import { useState } from "react"; 7 + import CustomAvatar from "@/components/avatar"; 8 + import { Button } from "@/components/ui/button"; 9 + import { ButtonGroup } from "@/components/ui/button-group"; 10 + import { 11 + Card, 12 + CardAction, 13 + CardContent, 14 + CardDescription, 15 + CardHeader, 16 + CardTitle, 17 + } from "@/components/ui/card"; 18 + import { 19 + Dialog, 20 + DialogClose, 21 + DialogContent, 22 + DialogTrigger, 23 + } from "@/components/ui/dialog"; 24 + import { 25 + Field, 26 + FieldDescription, 27 + FieldGroup, 28 + FieldLabel, 29 + FieldLegend, 30 + FieldSet, 31 + } from "@/components/ui/field"; 32 + import { Input } from "@/components/ui/input"; 33 + import { 34 + Item, 35 + ItemActions, 36 + ItemContent, 37 + ItemDescription, 38 + ItemGroup, 39 + ItemTitle, 40 + } from "@/components/ui/item"; 41 + import type { group } from "@/db/schema"; 42 + import { createGroup, deleteGroup, updateGroup } from "@/lib/group"; 43 + 44 + type Group = typeof group.$inferSelect; 45 + type User = { 46 + id: string; 47 + name: string; 48 + image: string; 49 + }; 50 + 51 + export function Groups({ 52 + user, 53 + groups: initialGroups, 54 + }: { 55 + user: User; 56 + groups: Group[]; 57 + }) { 58 + const [groups, setGroups] = useState(initialGroups); 59 + 60 + return ( 61 + <Card> 62 + <CardHeader> 63 + <CardTitle>Vouch</CardTitle> 64 + <CardDescription>Your shared promises</CardDescription> 65 + <CardAction> 66 + <Link href="/settings"> 67 + <CustomAvatar name={user.name} image={user.image as string} /> 68 + </Link> 69 + </CardAction> 70 + </CardHeader> 71 + <CardContent> 72 + <ItemGroup> 73 + {groups.map((group) => ( 74 + <Item key={group.id}> 75 + <ItemContent> 76 + <ItemTitle>{group.name}</ItemTitle> 77 + <ItemDescription>{group.description}</ItemDescription> 78 + </ItemContent> 79 + <ItemActions> 80 + <ButtonGroup> 81 + <Button 82 + render={<Link href={`/${group.id}`} />} 83 + nativeButton={false} 84 + > 85 + View 86 + </Button> 87 + {group.userId === user.id && ( 88 + <Dialog> 89 + <DialogTrigger 90 + render={<Button>Edit</Button>} 91 + id={`edit-${group.id}`} 92 + /> 93 + <DialogContent> 94 + <form 95 + onSubmit={(e) => { 96 + e.preventDefault(); 97 + const formData = new FormData(e.currentTarget); 98 + const name = formData.get("name") as string; 99 + const description = formData.get( 100 + "description", 101 + ) as string; 102 + updateGroup(group.id, name, description).then( 103 + (res) => { 104 + if (!res) return; 105 + setGroups((prevGroups) => 106 + prevGroups.map((g) => 107 + g.id === group.id ? res : g, 108 + ), 109 + ); 110 + }, 111 + ); 112 + }} 113 + > 114 + <FieldSet> 115 + <FieldLegend>Edit the group</FieldLegend> 116 + <FieldDescription> 117 + Are you happy now ? 118 + </FieldDescription> 119 + <FieldGroup> 120 + <Field> 121 + <FieldLabel htmlFor="name">Name</FieldLabel> 122 + <Input 123 + id="name" 124 + name="name" 125 + defaultValue={group.name} 126 + required 127 + /> 128 + </Field> 129 + <Field> 130 + <FieldLabel htmlFor="description"> 131 + Description 132 + </FieldLabel> 133 + <Input 134 + id="description" 135 + name="description" 136 + defaultValue={group.description || ""} 137 + required 138 + /> 139 + </Field> 140 + <Field orientation="horizontal"> 141 + <DialogClose 142 + render={ 143 + <Button 144 + variant="destructive" 145 + onClick={(e) => { 146 + e.preventDefault(); 147 + deleteGroup(group.id).then((res) => { 148 + if (!res) return; 149 + setGroups((prevGroups) => 150 + prevGroups.filter( 151 + (g) => g.id !== group.id, 152 + ), 153 + ); 154 + }); 155 + }} 156 + > 157 + <HugeiconsIcon icon={Delete01Icon} /> 158 + </Button> 159 + } 160 + /> 161 + <DialogClose 162 + render={ 163 + <Button type="submit" className="flex-1"> 164 + Update 165 + </Button> 166 + } 167 + /> 168 + </Field> 169 + </FieldGroup> 170 + </FieldSet> 171 + </form> 172 + </DialogContent> 173 + </Dialog> 174 + )} 175 + </ButtonGroup> 176 + </ItemActions> 177 + </Item> 178 + ))} 179 + <Dialog> 180 + <DialogTrigger render={<Button>Create group</Button>} id="create" /> 181 + <DialogContent> 182 + <form 183 + onSubmit={(e) => { 184 + e.preventDefault(); 185 + const formData = new FormData(e.currentTarget); 186 + const name = formData.get("name") as string; 187 + const description = formData.get("description") as string; 188 + createGroup(name, description).then((res) => { 189 + if (!res) return; 190 + setGroups((prevGroups) => [...prevGroups, res]); 191 + }); 192 + }} 193 + > 194 + <FieldSet> 195 + <FieldLegend>New group</FieldLegend> 196 + <FieldDescription> 197 + What's the name and description of your new group? You can 198 + always change this later. 199 + </FieldDescription> 200 + <FieldGroup> 201 + <Field> 202 + <FieldLabel htmlFor="name">Name</FieldLabel> 203 + <Input 204 + id="name" 205 + name="name" 206 + placeholder="My group" 207 + required 208 + /> 209 + </Field> 210 + <Field> 211 + <FieldLabel htmlFor="description">Description</FieldLabel> 212 + <Input 213 + id="description" 214 + name="description" 215 + placeholder="A group for my promises" 216 + required 217 + /> 218 + </Field> 219 + <Field> 220 + <DialogClose 221 + render={<Button type="submit">Create</Button>} 222 + /> 223 + </Field> 224 + </FieldGroup> 225 + </FieldSet> 226 + </form> 227 + </DialogContent> 228 + </Dialog> 229 + </ItemGroup> 230 + </CardContent> 231 + </Card> 232 + ); 233 + }
-187
src/components/ui/alert-dialog.tsx
··· 1 - "use client"; 2 - 3 - import * as React from "react"; 4 - import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"; 5 - 6 - import { cn } from "@/lib/utils"; 7 - import { Button } from "@/components/ui/button"; 8 - 9 - function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) { 10 - return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />; 11 - } 12 - 13 - function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) { 14 - return ( 15 - <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} /> 16 - ); 17 - } 18 - 19 - function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) { 20 - return ( 21 - <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} /> 22 - ); 23 - } 24 - 25 - function AlertDialogOverlay({ 26 - className, 27 - ...props 28 - }: AlertDialogPrimitive.Backdrop.Props) { 29 - return ( 30 - <AlertDialogPrimitive.Backdrop 31 - data-slot="alert-dialog-overlay" 32 - className={cn( 33 - "data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/80 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50", 34 - className, 35 - )} 36 - {...props} 37 - /> 38 - ); 39 - } 40 - 41 - function AlertDialogContent({ 42 - className, 43 - size = "default", 44 - ...props 45 - }: AlertDialogPrimitive.Popup.Props & { 46 - size?: "default" | "sm"; 47 - }) { 48 - return ( 49 - <AlertDialogPortal> 50 - <AlertDialogOverlay /> 51 - <AlertDialogPrimitive.Popup 52 - data-slot="alert-dialog-content" 53 - data-size={size} 54 - className={cn( 55 - "data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-background ring-foreground/5 gap-6 rounded-4xl p-6 ring-1 duration-100 data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-md group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 outline-none", 56 - className, 57 - )} 58 - {...props} 59 - /> 60 - </AlertDialogPortal> 61 - ); 62 - } 63 - 64 - function AlertDialogHeader({ 65 - className, 66 - ...props 67 - }: React.ComponentProps<"div">) { 68 - return ( 69 - <div 70 - data-slot="alert-dialog-header" 71 - className={cn( 72 - "grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-6 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]", 73 - className, 74 - )} 75 - {...props} 76 - /> 77 - ); 78 - } 79 - 80 - function AlertDialogFooter({ 81 - className, 82 - ...props 83 - }: React.ComponentProps<"div">) { 84 - return ( 85 - <div 86 - data-slot="alert-dialog-footer" 87 - className={cn( 88 - "flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end", 89 - className, 90 - )} 91 - {...props} 92 - /> 93 - ); 94 - } 95 - 96 - function AlertDialogMedia({ 97 - className, 98 - ...props 99 - }: React.ComponentProps<"div">) { 100 - return ( 101 - <div 102 - data-slot="alert-dialog-media" 103 - className={cn( 104 - "bg-muted mb-2 inline-flex size-16 items-center justify-center rounded-full sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-8", 105 - className, 106 - )} 107 - {...props} 108 - /> 109 - ); 110 - } 111 - 112 - function AlertDialogTitle({ 113 - className, 114 - ...props 115 - }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) { 116 - return ( 117 - <AlertDialogPrimitive.Title 118 - data-slot="alert-dialog-title" 119 - className={cn( 120 - "text-lg font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2", 121 - className, 122 - )} 123 - {...props} 124 - /> 125 - ); 126 - } 127 - 128 - function AlertDialogDescription({ 129 - className, 130 - ...props 131 - }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) { 132 - return ( 133 - <AlertDialogPrimitive.Description 134 - data-slot="alert-dialog-description" 135 - className={cn( 136 - "text-muted-foreground *:[a]:hover:text-foreground text-sm text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3", 137 - className, 138 - )} 139 - {...props} 140 - /> 141 - ); 142 - } 143 - 144 - function AlertDialogAction({ 145 - className, 146 - ...props 147 - }: React.ComponentProps<typeof Button>) { 148 - return ( 149 - <Button 150 - data-slot="alert-dialog-action" 151 - className={cn(className)} 152 - {...props} 153 - /> 154 - ); 155 - } 156 - 157 - function AlertDialogCancel({ 158 - className, 159 - variant = "outline", 160 - size = "default", 161 - ...props 162 - }: AlertDialogPrimitive.Close.Props & 163 - Pick<React.ComponentProps<typeof Button>, "variant" | "size">) { 164 - return ( 165 - <AlertDialogPrimitive.Close 166 - data-slot="alert-dialog-cancel" 167 - className={cn(className)} 168 - render={<Button variant={variant} size={size} />} 169 - {...props} 170 - /> 171 - ); 172 - } 173 - 174 - export { 175 - AlertDialog, 176 - AlertDialogAction, 177 - AlertDialogCancel, 178 - AlertDialogContent, 179 - AlertDialogDescription, 180 - AlertDialogFooter, 181 - AlertDialogHeader, 182 - AlertDialogMedia, 183 - AlertDialogOverlay, 184 - AlertDialogPortal, 185 - AlertDialogTitle, 186 - AlertDialogTrigger, 187 - };
-52
src/components/ui/badge.tsx
··· 1 - import { mergeProps } from "@base-ui/react/merge-props"; 2 - import { useRender } from "@base-ui/react/use-render"; 3 - import { cva, type VariantProps } from "class-variance-authority"; 4 - 5 - import { cn } from "@/lib/utils"; 6 - 7 - const badgeVariants = cva( 8 - "h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive overflow-hidden group/badge", 9 - { 10 - variants: { 11 - variant: { 12 - default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", 13 - secondary: 14 - "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", 15 - destructive: 16 - "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20", 17 - outline: 18 - "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground bg-input/30", 19 - ghost: 20 - "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", 21 - link: "text-primary underline-offset-4 hover:underline", 22 - }, 23 - }, 24 - defaultVariants: { 25 - variant: "default", 26 - }, 27 - }, 28 - ); 29 - 30 - function Badge({ 31 - className, 32 - variant = "default", 33 - render, 34 - ...props 35 - }: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) { 36 - return useRender({ 37 - defaultTagName: "span", 38 - props: mergeProps<"span">( 39 - { 40 - className: cn(badgeVariants({ className, variant })), 41 - }, 42 - props, 43 - ), 44 - render, 45 - state: { 46 - slot: "badge", 47 - variant, 48 - }, 49 - }); 50 - } 51 - 52 - export { Badge, badgeVariants };
+87
src/components/ui/button-group.tsx
··· 1 + import { mergeProps } from "@base-ui/react/merge-props"; 2 + import { useRender } from "@base-ui/react/use-render"; 3 + import { cva, type VariantProps } from "class-variance-authority"; 4 + 5 + import { cn } from "@/lib/utils"; 6 + import { Separator } from "@/components/ui/separator"; 7 + 8 + const buttonGroupVariants = cva( 9 + "has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-4xl flex w-fit items-stretch *:focus-visible:z-10 *:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1", 10 + { 11 + variants: { 12 + orientation: { 13 + horizontal: 14 + "[&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-4xl! [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0 *:data-slot:rounded-r-none", 15 + vertical: 16 + "[&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-4xl! flex-col [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0 *:data-slot:rounded-b-none", 17 + }, 18 + }, 19 + defaultVariants: { 20 + orientation: "horizontal", 21 + }, 22 + }, 23 + ); 24 + 25 + function ButtonGroup({ 26 + className, 27 + orientation, 28 + ...props 29 + }: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) { 30 + return ( 31 + <div 32 + role="group" 33 + data-slot="button-group" 34 + data-orientation={orientation} 35 + className={cn(buttonGroupVariants({ orientation }), className)} 36 + {...props} 37 + /> 38 + ); 39 + } 40 + 41 + function ButtonGroupText({ 42 + className, 43 + render, 44 + ...props 45 + }: useRender.ComponentProps<"div">) { 46 + return useRender({ 47 + defaultTagName: "div", 48 + props: mergeProps<"div">( 49 + { 50 + className: cn( 51 + "bg-muted gap-2 rounded-4xl border px-2.5 text-sm font-medium [&_svg:not([class*='size-'])]:size-4 flex items-center [&_svg]:pointer-events-none", 52 + className, 53 + ), 54 + }, 55 + props, 56 + ), 57 + render, 58 + state: { 59 + slot: "button-group-text", 60 + }, 61 + }); 62 + } 63 + 64 + function ButtonGroupSeparator({ 65 + className, 66 + orientation = "vertical", 67 + ...props 68 + }: React.ComponentProps<typeof Separator>) { 69 + return ( 70 + <Separator 71 + data-slot="button-group-separator" 72 + orientation={orientation} 73 + className={cn( 74 + "bg-input relative self-stretch data-horizontal:mx-px data-horizontal:w-auto data-vertical:my-px data-vertical:h-auto", 75 + className, 76 + )} 77 + {...props} 78 + /> 79 + ); 80 + } 81 + 82 + export { 83 + ButtonGroup, 84 + ButtonGroupSeparator, 85 + ButtonGroupText, 86 + buttonGroupVariants, 87 + };
-321
src/components/ui/combobox.tsx
··· 1 - "use client"; 2 - 3 - import * as React from "react"; 4 - import { Combobox as ComboboxPrimitive } from "@base-ui/react"; 5 - 6 - import { cn } from "@/lib/utils"; 7 - import { Button } from "@/components/ui/button"; 8 - import { 9 - InputGroup, 10 - InputGroupAddon, 11 - InputGroupButton, 12 - InputGroupInput, 13 - } from "@/components/ui/input-group"; 14 - import { HugeiconsIcon } from "@hugeicons/react"; 15 - import { 16 - ArrowDown01Icon, 17 - Cancel01Icon, 18 - Tick02Icon, 19 - } from "@hugeicons/core-free-icons"; 20 - 21 - const Combobox = ComboboxPrimitive.Root; 22 - 23 - function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) { 24 - return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />; 25 - } 26 - 27 - function ComboboxTrigger({ 28 - className, 29 - children, 30 - ...props 31 - }: ComboboxPrimitive.Trigger.Props) { 32 - return ( 33 - <ComboboxPrimitive.Trigger 34 - data-slot="combobox-trigger" 35 - className={cn("[&_svg:not([class*='size-'])]:size-4", className)} 36 - {...props} 37 - > 38 - {children} 39 - <HugeiconsIcon 40 - icon={ArrowDown01Icon} 41 - strokeWidth={2} 42 - className="text-muted-foreground size-4 pointer-events-none" 43 - /> 44 - </ComboboxPrimitive.Trigger> 45 - ); 46 - } 47 - 48 - function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) { 49 - return ( 50 - <ComboboxPrimitive.Clear 51 - data-slot="combobox-clear" 52 - render={<InputGroupButton variant="ghost" size="icon-xs" />} 53 - className={cn(className)} 54 - {...props} 55 - > 56 - <HugeiconsIcon 57 - icon={Cancel01Icon} 58 - strokeWidth={2} 59 - className="pointer-events-none" 60 - /> 61 - </ComboboxPrimitive.Clear> 62 - ); 63 - } 64 - 65 - function ComboboxInput({ 66 - className, 67 - children, 68 - disabled = false, 69 - showTrigger = true, 70 - showClear = false, 71 - ...props 72 - }: ComboboxPrimitive.Input.Props & { 73 - showTrigger?: boolean; 74 - showClear?: boolean; 75 - }) { 76 - return ( 77 - <InputGroup className={cn("w-auto", className)}> 78 - <ComboboxPrimitive.Input 79 - render={<InputGroupInput disabled={disabled} />} 80 - {...props} 81 - /> 82 - <InputGroupAddon align="inline-end"> 83 - {showTrigger && ( 84 - <InputGroupButton 85 - size="icon-xs" 86 - variant="ghost" 87 - render={<ComboboxTrigger />} 88 - data-slot="input-group-button" 89 - className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent" 90 - disabled={disabled} 91 - /> 92 - )} 93 - {showClear && <ComboboxClear disabled={disabled} />} 94 - </InputGroupAddon> 95 - {children} 96 - </InputGroup> 97 - ); 98 - } 99 - 100 - function ComboboxContent({ 101 - className, 102 - side = "bottom", 103 - sideOffset = 6, 104 - align = "start", 105 - alignOffset = 0, 106 - anchor, 107 - ...props 108 - }: ComboboxPrimitive.Popup.Props & 109 - Pick< 110 - ComboboxPrimitive.Positioner.Props, 111 - "side" | "align" | "sideOffset" | "alignOffset" | "anchor" 112 - >) { 113 - return ( 114 - <ComboboxPrimitive.Portal> 115 - <ComboboxPrimitive.Positioner 116 - side={side} 117 - sideOffset={sideOffset} 118 - align={align} 119 - alignOffset={alignOffset} 120 - anchor={anchor} 121 - className="isolate z-50" 122 - > 123 - <ComboboxPrimitive.Popup 124 - data-slot="combobox-content" 125 - data-chips={!!anchor} 126 - className={cn( 127 - "bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/5 *:data-[slot=input-group]:bg-input/30 overflow-hidden rounded-2xl shadow-2xl ring-1 duration-100 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-9 *:data-[slot=input-group]:border-none *:data-[slot=input-group]:shadow-none data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) data-[chips=true]:min-w-(--anchor-width)", 128 - className, 129 - )} 130 - {...props} 131 - /> 132 - </ComboboxPrimitive.Positioner> 133 - </ComboboxPrimitive.Portal> 134 - ); 135 - } 136 - 137 - function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) { 138 - return ( 139 - <ComboboxPrimitive.List 140 - data-slot="combobox-list" 141 - className={cn( 142 - "no-scrollbar max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 p-1 data-empty:p-0 overflow-y-auto overscroll-contain", 143 - className, 144 - )} 145 - {...props} 146 - /> 147 - ); 148 - } 149 - 150 - function ComboboxItem({ 151 - className, 152 - children, 153 - ...props 154 - }: ComboboxPrimitive.Item.Props) { 155 - return ( 156 - <ComboboxPrimitive.Item 157 - data-slot="combobox-item" 158 - className={cn( 159 - "data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground gap-2.5 rounded-xl py-2 pr-8 pl-3 text-sm [&_svg:not([class*='size-'])]:size-4 relative flex w-full cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", 160 - className, 161 - )} 162 - {...props} 163 - > 164 - {children} 165 - <ComboboxPrimitive.ItemIndicator 166 - render={ 167 - <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" /> 168 - } 169 - > 170 - <HugeiconsIcon 171 - icon={Tick02Icon} 172 - strokeWidth={2} 173 - className="pointer-events-none" 174 - /> 175 - </ComboboxPrimitive.ItemIndicator> 176 - </ComboboxPrimitive.Item> 177 - ); 178 - } 179 - 180 - function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) { 181 - return ( 182 - <ComboboxPrimitive.Group 183 - data-slot="combobox-group" 184 - className={cn(className)} 185 - {...props} 186 - /> 187 - ); 188 - } 189 - 190 - function ComboboxLabel({ 191 - className, 192 - ...props 193 - }: ComboboxPrimitive.GroupLabel.Props) { 194 - return ( 195 - <ComboboxPrimitive.GroupLabel 196 - data-slot="combobox-label" 197 - className={cn("text-muted-foreground px-3.5 py-2.5 text-xs", className)} 198 - {...props} 199 - /> 200 - ); 201 - } 202 - 203 - function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) { 204 - return ( 205 - <ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} /> 206 - ); 207 - } 208 - 209 - function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) { 210 - return ( 211 - <ComboboxPrimitive.Empty 212 - data-slot="combobox-empty" 213 - className={cn( 214 - "text-muted-foreground hidden w-full justify-center py-2 text-center text-sm group-data-empty/combobox-content:flex", 215 - className, 216 - )} 217 - {...props} 218 - /> 219 - ); 220 - } 221 - 222 - function ComboboxSeparator({ 223 - className, 224 - ...props 225 - }: ComboboxPrimitive.Separator.Props) { 226 - return ( 227 - <ComboboxPrimitive.Separator 228 - data-slot="combobox-separator" 229 - className={cn("bg-border/50 -mx-1 my-1 h-px", className)} 230 - {...props} 231 - /> 232 - ); 233 - } 234 - 235 - function ComboboxChips({ 236 - className, 237 - ...props 238 - }: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> & 239 - ComboboxPrimitive.Chips.Props) { 240 - return ( 241 - <ComboboxPrimitive.Chips 242 - data-slot="combobox-chips" 243 - className={cn( 244 - "bg-input/30 border-input focus-within:border-ring focus-within:ring-ring/50 has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive dark:has-aria-invalid:border-destructive/50 flex min-h-9 flex-wrap items-center gap-1.5 rounded-4xl border bg-clip-padding px-2.5 py-1.5 text-sm transition-colors focus-within:ring-[3px] has-aria-invalid:ring-[3px] has-data-[slot=combobox-chip]:px-1.5", 245 - className, 246 - )} 247 - {...props} 248 - /> 249 - ); 250 - } 251 - 252 - function ComboboxChip({ 253 - className, 254 - children, 255 - showRemove = true, 256 - ...props 257 - }: ComboboxPrimitive.Chip.Props & { 258 - showRemove?: boolean; 259 - }) { 260 - return ( 261 - <ComboboxPrimitive.Chip 262 - data-slot="combobox-chip" 263 - className={cn( 264 - "bg-muted-foreground/10 text-foreground flex h-[calc(--spacing(5.5))] w-fit items-center justify-center gap-1 rounded-4xl px-2 text-xs font-medium whitespace-nowrap has-data-[slot=combobox-chip-remove]:pr-0 has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50", 265 - className, 266 - )} 267 - {...props} 268 - > 269 - {children} 270 - {showRemove && ( 271 - <ComboboxPrimitive.ChipRemove 272 - render={<Button variant="ghost" size="icon-xs" />} 273 - className="-ml-1 opacity-50 hover:opacity-100" 274 - data-slot="combobox-chip-remove" 275 - > 276 - <HugeiconsIcon 277 - icon={Cancel01Icon} 278 - strokeWidth={2} 279 - className="pointer-events-none" 280 - /> 281 - </ComboboxPrimitive.ChipRemove> 282 - )} 283 - </ComboboxPrimitive.Chip> 284 - ); 285 - } 286 - 287 - function ComboboxChipsInput({ 288 - className, 289 - ...props 290 - }: ComboboxPrimitive.Input.Props) { 291 - return ( 292 - <ComboboxPrimitive.Input 293 - data-slot="combobox-chip-input" 294 - className={cn("min-w-16 flex-1 outline-none", className)} 295 - {...props} 296 - /> 297 - ); 298 - } 299 - 300 - function useComboboxAnchor() { 301 - return React.useRef<HTMLDivElement | null>(null); 302 - } 303 - 304 - export { 305 - Combobox, 306 - ComboboxInput, 307 - ComboboxContent, 308 - ComboboxList, 309 - ComboboxItem, 310 - ComboboxGroup, 311 - ComboboxLabel, 312 - ComboboxCollection, 313 - ComboboxEmpty, 314 - ComboboxSeparator, 315 - ComboboxChips, 316 - ComboboxChip, 317 - ComboboxChipsInput, 318 - ComboboxTrigger, 319 - ComboboxValue, 320 - useComboboxAnchor, 321 - };
-277
src/components/ui/dropdown-menu.tsx
··· 1 - "use client"; 2 - 3 - import * as React from "react"; 4 - import { Menu as MenuPrimitive } from "@base-ui/react/menu"; 5 - 6 - import { cn } from "@/lib/utils"; 7 - import { HugeiconsIcon } from "@hugeicons/react"; 8 - import { ArrowRight01Icon, Tick02Icon } from "@hugeicons/core-free-icons"; 9 - 10 - function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) { 11 - return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />; 12 - } 13 - 14 - function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) { 15 - return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />; 16 - } 17 - 18 - function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) { 19 - return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />; 20 - } 21 - 22 - function DropdownMenuContent({ 23 - align = "start", 24 - alignOffset = 0, 25 - side = "bottom", 26 - sideOffset = 4, 27 - className, 28 - ...props 29 - }: MenuPrimitive.Popup.Props & 30 - Pick< 31 - MenuPrimitive.Positioner.Props, 32 - "align" | "alignOffset" | "side" | "sideOffset" 33 - >) { 34 - return ( 35 - <MenuPrimitive.Portal> 36 - <MenuPrimitive.Positioner 37 - className="isolate z-50 outline-none" 38 - align={align} 39 - alignOffset={alignOffset} 40 - side={side} 41 - sideOffset={sideOffset} 42 - > 43 - <MenuPrimitive.Popup 44 - data-slot="dropdown-menu-content" 45 - className={cn( 46 - "data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/5 bg-popover text-popover-foreground min-w-48 rounded-2xl p-1 shadow-2xl ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 max-h-(--available-height) w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden", 47 - className, 48 - )} 49 - {...props} 50 - /> 51 - </MenuPrimitive.Positioner> 52 - </MenuPrimitive.Portal> 53 - ); 54 - } 55 - 56 - function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) { 57 - return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />; 58 - } 59 - 60 - function DropdownMenuLabel({ 61 - className, 62 - inset, 63 - ...props 64 - }: MenuPrimitive.GroupLabel.Props & { 65 - inset?: boolean; 66 - }) { 67 - return ( 68 - <MenuPrimitive.GroupLabel 69 - data-slot="dropdown-menu-label" 70 - data-inset={inset} 71 - className={cn( 72 - "text-muted-foreground px-3 py-2.5 text-xs data-inset:pl-9.5", 73 - className, 74 - )} 75 - {...props} 76 - /> 77 - ); 78 - } 79 - 80 - function DropdownMenuItem({ 81 - className, 82 - inset, 83 - variant = "default", 84 - ...props 85 - }: MenuPrimitive.Item.Props & { 86 - inset?: boolean; 87 - variant?: "default" | "destructive"; 88 - }) { 89 - return ( 90 - <MenuPrimitive.Item 91 - data-slot="dropdown-menu-item" 92 - data-inset={inset} 93 - data-variant={variant} 94 - className={cn( 95 - "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2.5 rounded-xl px-3 py-2 text-sm data-inset:pl-9.5 [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", 96 - className, 97 - )} 98 - {...props} 99 - /> 100 - ); 101 - } 102 - 103 - function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) { 104 - return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />; 105 - } 106 - 107 - function DropdownMenuSubTrigger({ 108 - className, 109 - inset, 110 - children, 111 - ...props 112 - }: MenuPrimitive.SubmenuTrigger.Props & { 113 - inset?: boolean; 114 - }) { 115 - return ( 116 - <MenuPrimitive.SubmenuTrigger 117 - data-slot="dropdown-menu-sub-trigger" 118 - data-inset={inset} 119 - className={cn( 120 - "focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-xl px-3 py-2 text-sm data-inset:pl-9.5 [&_svg:not([class*='size-'])]:size-4 data-popup-open:bg-accent data-popup-open:text-accent-foreground flex cursor-default items-center outline-hidden select-none [&_svg]:pointer-events-none [&_svg]:shrink-0", 121 - className, 122 - )} 123 - {...props} 124 - > 125 - {children} 126 - <HugeiconsIcon 127 - icon={ArrowRight01Icon} 128 - strokeWidth={2} 129 - className="ml-auto" 130 - /> 131 - </MenuPrimitive.SubmenuTrigger> 132 - ); 133 - } 134 - 135 - function DropdownMenuSubContent({ 136 - align = "start", 137 - alignOffset = -3, 138 - side = "right", 139 - sideOffset = 0, 140 - className, 141 - ...props 142 - }: React.ComponentProps<typeof DropdownMenuContent>) { 143 - return ( 144 - <DropdownMenuContent 145 - data-slot="dropdown-menu-sub-content" 146 - className={cn( 147 - "data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/5 bg-popover text-popover-foreground min-w-36 rounded-2xl p-1 shadow-2xl ring-1 duration-100 w-auto", 148 - className, 149 - )} 150 - align={align} 151 - alignOffset={alignOffset} 152 - side={side} 153 - sideOffset={sideOffset} 154 - {...props} 155 - /> 156 - ); 157 - } 158 - 159 - function DropdownMenuCheckboxItem({ 160 - className, 161 - children, 162 - checked, 163 - inset, 164 - ...props 165 - }: MenuPrimitive.CheckboxItem.Props & { 166 - inset?: boolean; 167 - }) { 168 - return ( 169 - <MenuPrimitive.CheckboxItem 170 - data-slot="dropdown-menu-checkbox-item" 171 - data-inset={inset} 172 - className={cn( 173 - "focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2.5 rounded-xl py-2 pr-8 pl-3 text-sm data-inset:pl-9.5 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", 174 - className, 175 - )} 176 - checked={checked} 177 - {...props} 178 - > 179 - <span 180 - className="absolute right-2 flex items-center justify-center pointer-events-none" 181 - data-slot="dropdown-menu-checkbox-item-indicator" 182 - > 183 - <MenuPrimitive.CheckboxItemIndicator> 184 - <HugeiconsIcon icon={Tick02Icon} strokeWidth={2} /> 185 - </MenuPrimitive.CheckboxItemIndicator> 186 - </span> 187 - {children} 188 - </MenuPrimitive.CheckboxItem> 189 - ); 190 - } 191 - 192 - function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) { 193 - return ( 194 - <MenuPrimitive.RadioGroup 195 - data-slot="dropdown-menu-radio-group" 196 - {...props} 197 - /> 198 - ); 199 - } 200 - 201 - function DropdownMenuRadioItem({ 202 - className, 203 - children, 204 - inset, 205 - ...props 206 - }: MenuPrimitive.RadioItem.Props & { 207 - inset?: boolean; 208 - }) { 209 - return ( 210 - <MenuPrimitive.RadioItem 211 - data-slot="dropdown-menu-radio-item" 212 - data-inset={inset} 213 - className={cn( 214 - "focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2.5 rounded-xl py-2 pr-8 pl-3 text-sm data-inset:pl-9.5 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", 215 - className, 216 - )} 217 - {...props} 218 - > 219 - <span 220 - className="absolute right-2 flex items-center justify-center pointer-events-none" 221 - data-slot="dropdown-menu-radio-item-indicator" 222 - > 223 - <MenuPrimitive.RadioItemIndicator> 224 - <HugeiconsIcon icon={Tick02Icon} strokeWidth={2} /> 225 - </MenuPrimitive.RadioItemIndicator> 226 - </span> 227 - {children} 228 - </MenuPrimitive.RadioItem> 229 - ); 230 - } 231 - 232 - function DropdownMenuSeparator({ 233 - className, 234 - ...props 235 - }: MenuPrimitive.Separator.Props) { 236 - return ( 237 - <MenuPrimitive.Separator 238 - data-slot="dropdown-menu-separator" 239 - className={cn("bg-border/50 -mx-1 my-1 h-px", className)} 240 - {...props} 241 - /> 242 - ); 243 - } 244 - 245 - function DropdownMenuShortcut({ 246 - className, 247 - ...props 248 - }: React.ComponentProps<"span">) { 249 - return ( 250 - <span 251 - data-slot="dropdown-menu-shortcut" 252 - className={cn( 253 - "text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest", 254 - className, 255 - )} 256 - {...props} 257 - /> 258 - ); 259 - } 260 - 261 - export { 262 - DropdownMenu, 263 - DropdownMenuPortal, 264 - DropdownMenuTrigger, 265 - DropdownMenuContent, 266 - DropdownMenuGroup, 267 - DropdownMenuLabel, 268 - DropdownMenuItem, 269 - DropdownMenuCheckboxItem, 270 - DropdownMenuRadioGroup, 271 - DropdownMenuRadioItem, 272 - DropdownMenuSeparator, 273 - DropdownMenuShortcut, 274 - DropdownMenuSub, 275 - DropdownMenuSubTrigger, 276 - DropdownMenuSubContent, 277 - };
-83
src/components/ui/progress.tsx
··· 1 - "use client"; 2 - 3 - import { Progress as ProgressPrimitive } from "@base-ui/react/progress"; 4 - 5 - import { cn } from "@/lib/utils"; 6 - 7 - function Progress({ 8 - className, 9 - children, 10 - value, 11 - ...props 12 - }: ProgressPrimitive.Root.Props) { 13 - return ( 14 - <ProgressPrimitive.Root 15 - value={value} 16 - data-slot="progress" 17 - className={cn("flex flex-wrap gap-3", className)} 18 - {...props} 19 - > 20 - {children} 21 - <ProgressTrack> 22 - <ProgressIndicator /> 23 - </ProgressTrack> 24 - </ProgressPrimitive.Root> 25 - ); 26 - } 27 - 28 - function ProgressTrack({ className, ...props }: ProgressPrimitive.Track.Props) { 29 - return ( 30 - <ProgressPrimitive.Track 31 - className={cn( 32 - "bg-muted h-3 rounded-4xl relative flex w-full items-center overflow-x-hidden", 33 - className, 34 - )} 35 - data-slot="progress-track" 36 - {...props} 37 - /> 38 - ); 39 - } 40 - 41 - function ProgressIndicator({ 42 - className, 43 - ...props 44 - }: ProgressPrimitive.Indicator.Props) { 45 - return ( 46 - <ProgressPrimitive.Indicator 47 - data-slot="progress-indicator" 48 - className={cn("bg-primary h-full transition-all", className)} 49 - {...props} 50 - /> 51 - ); 52 - } 53 - 54 - function ProgressLabel({ className, ...props }: ProgressPrimitive.Label.Props) { 55 - return ( 56 - <ProgressPrimitive.Label 57 - className={cn("text-sm font-medium", className)} 58 - data-slot="progress-label" 59 - {...props} 60 - /> 61 - ); 62 - } 63 - 64 - function ProgressValue({ className, ...props }: ProgressPrimitive.Value.Props) { 65 - return ( 66 - <ProgressPrimitive.Value 67 - className={cn( 68 - "text-muted-foreground ml-auto text-sm tabular-nums", 69 - className, 70 - )} 71 - data-slot="progress-value" 72 - {...props} 73 - /> 74 - ); 75 - } 76 - 77 - export { 78 - Progress, 79 - ProgressTrack, 80 - ProgressIndicator, 81 - ProgressLabel, 82 - ProgressValue, 83 - };
-219
src/components/ui/select.tsx
··· 1 - "use client"; 2 - 3 - import * as React from "react"; 4 - import { Select as SelectPrimitive } from "@base-ui/react/select"; 5 - 6 - import { cn } from "@/lib/utils"; 7 - import { HugeiconsIcon } from "@hugeicons/react"; 8 - import { 9 - UnfoldMoreIcon, 10 - Tick02Icon, 11 - ArrowUp01Icon, 12 - ArrowDown01Icon, 13 - } from "@hugeicons/core-free-icons"; 14 - 15 - const Select = SelectPrimitive.Root; 16 - 17 - function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) { 18 - return ( 19 - <SelectPrimitive.Group 20 - data-slot="select-group" 21 - className={cn("scroll-my-1 p-1", className)} 22 - {...props} 23 - /> 24 - ); 25 - } 26 - 27 - function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) { 28 - return ( 29 - <SelectPrimitive.Value 30 - data-slot="select-value" 31 - className={cn("flex flex-1 text-left", className)} 32 - {...props} 33 - /> 34 - ); 35 - } 36 - 37 - function SelectTrigger({ 38 - className, 39 - size = "default", 40 - children, 41 - ...props 42 - }: SelectPrimitive.Trigger.Props & { 43 - size?: "sm" | "default"; 44 - }) { 45 - return ( 46 - <SelectPrimitive.Trigger 47 - data-slot="select-trigger" 48 - data-size={size} 49 - className={cn( 50 - "border-input data-placeholder:text-muted-foreground bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 gap-1.5 rounded-4xl border px-3 py-2 text-sm transition-colors focus-visible:ring-[3px] aria-invalid:ring-[3px] data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*='size-'])]:size-4 flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0", 51 - className, 52 - )} 53 - {...props} 54 - > 55 - {children} 56 - <SelectPrimitive.Icon 57 - render={ 58 - <HugeiconsIcon 59 - icon={UnfoldMoreIcon} 60 - strokeWidth={2} 61 - className="text-muted-foreground size-4 pointer-events-none" 62 - /> 63 - } 64 - /> 65 - </SelectPrimitive.Trigger> 66 - ); 67 - } 68 - 69 - function SelectContent({ 70 - className, 71 - children, 72 - side = "bottom", 73 - sideOffset = 4, 74 - align = "center", 75 - alignOffset = 0, 76 - alignItemWithTrigger = true, 77 - ...props 78 - }: SelectPrimitive.Popup.Props & 79 - Pick< 80 - SelectPrimitive.Positioner.Props, 81 - "align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger" 82 - >) { 83 - return ( 84 - <SelectPrimitive.Portal> 85 - <SelectPrimitive.Positioner 86 - side={side} 87 - sideOffset={sideOffset} 88 - align={align} 89 - alignOffset={alignOffset} 90 - alignItemWithTrigger={alignItemWithTrigger} 91 - className="isolate z-50" 92 - > 93 - <SelectPrimitive.Popup 94 - data-slot="select-content" 95 - data-align-trigger={alignItemWithTrigger} 96 - className={cn( 97 - "bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/5 min-w-36 rounded-2xl shadow-2xl ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 relative isolate z-50 max-h-(--available-height) w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto data-[align-trigger=true]:animate-none", 98 - className, 99 - )} 100 - {...props} 101 - > 102 - <SelectScrollUpButton /> 103 - <SelectPrimitive.List>{children}</SelectPrimitive.List> 104 - <SelectScrollDownButton /> 105 - </SelectPrimitive.Popup> 106 - </SelectPrimitive.Positioner> 107 - </SelectPrimitive.Portal> 108 - ); 109 - } 110 - 111 - function SelectLabel({ 112 - className, 113 - ...props 114 - }: SelectPrimitive.GroupLabel.Props) { 115 - return ( 116 - <SelectPrimitive.GroupLabel 117 - data-slot="select-label" 118 - className={cn("text-muted-foreground px-3 py-2.5 text-xs", className)} 119 - {...props} 120 - /> 121 - ); 122 - } 123 - 124 - function SelectItem({ 125 - className, 126 - children, 127 - ...props 128 - }: SelectPrimitive.Item.Props) { 129 - return ( 130 - <SelectPrimitive.Item 131 - data-slot="select-item" 132 - className={cn( 133 - "focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2.5 rounded-xl py-2 pr-8 pl-3 text-sm [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", 134 - className, 135 - )} 136 - {...props} 137 - > 138 - <SelectPrimitive.ItemText className="flex flex-1 gap-2 shrink-0 whitespace-nowrap"> 139 - {children} 140 - </SelectPrimitive.ItemText> 141 - <SelectPrimitive.ItemIndicator 142 - render={ 143 - <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" /> 144 - } 145 - > 146 - <HugeiconsIcon 147 - icon={Tick02Icon} 148 - strokeWidth={2} 149 - className="pointer-events-none" 150 - /> 151 - </SelectPrimitive.ItemIndicator> 152 - </SelectPrimitive.Item> 153 - ); 154 - } 155 - 156 - function SelectSeparator({ 157 - className, 158 - ...props 159 - }: SelectPrimitive.Separator.Props) { 160 - return ( 161 - <SelectPrimitive.Separator 162 - data-slot="select-separator" 163 - className={cn( 164 - "bg-border/50 -mx-1 my-1 h-px pointer-events-none", 165 - className, 166 - )} 167 - {...props} 168 - /> 169 - ); 170 - } 171 - 172 - function SelectScrollUpButton({ 173 - className, 174 - ...props 175 - }: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) { 176 - return ( 177 - <SelectPrimitive.ScrollUpArrow 178 - data-slot="select-scroll-up-button" 179 - className={cn( 180 - "bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 top-0 w-full", 181 - className, 182 - )} 183 - {...props} 184 - > 185 - <HugeiconsIcon icon={ArrowUp01Icon} strokeWidth={2} /> 186 - </SelectPrimitive.ScrollUpArrow> 187 - ); 188 - } 189 - 190 - function SelectScrollDownButton({ 191 - className, 192 - ...props 193 - }: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) { 194 - return ( 195 - <SelectPrimitive.ScrollDownArrow 196 - data-slot="select-scroll-down-button" 197 - className={cn( 198 - "bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 bottom-0 w-full", 199 - className, 200 - )} 201 - {...props} 202 - > 203 - <HugeiconsIcon icon={ArrowDown01Icon} strokeWidth={2} /> 204 - </SelectPrimitive.ScrollDownArrow> 205 - ); 206 - } 207 - 208 - export { 209 - Select, 210 - SelectContent, 211 - SelectGroup, 212 - SelectItem, 213 - SelectLabel, 214 - SelectScrollDownButton, 215 - SelectScrollUpButton, 216 - SelectSeparator, 217 - SelectTrigger, 218 - SelectValue, 219 - };
-59
src/components/ui/slider.tsx
··· 1 - "use client"; 2 - 3 - import * as React from "react"; 4 - import { Slider as SliderPrimitive } from "@base-ui/react/slider"; 5 - 6 - import { cn } from "@/lib/utils"; 7 - 8 - function Slider({ 9 - className, 10 - defaultValue, 11 - value, 12 - min = 0, 13 - max = 100, 14 - ...props 15 - }: SliderPrimitive.Root.Props) { 16 - const _values = React.useMemo( 17 - () => 18 - Array.isArray(value) 19 - ? value 20 - : Array.isArray(defaultValue) 21 - ? defaultValue 22 - : [min, max], 23 - [value, defaultValue, min, max], 24 - ); 25 - 26 - return ( 27 - <SliderPrimitive.Root 28 - className={cn("data-horizontal:w-full data-vertical:h-full", className)} 29 - data-slot="slider" 30 - defaultValue={defaultValue} 31 - value={value} 32 - min={min} 33 - max={max} 34 - thumbAlignment="edge" 35 - {...props} 36 - > 37 - <SliderPrimitive.Control className="data-vertical:min-h-40 relative flex w-full touch-none items-center select-none data-disabled:opacity-50 data-vertical:h-full data-vertical:w-auto data-vertical:flex-col"> 38 - <SliderPrimitive.Track 39 - data-slot="slider-track" 40 - className="bg-muted rounded-4xl data-horizontal:h-3 data-horizontal:w-full data-vertical:h-full data-vertical:w-3 relative grow overflow-hidden select-none" 41 - > 42 - <SliderPrimitive.Indicator 43 - data-slot="slider-range" 44 - className="bg-primary select-none data-horizontal:h-full data-vertical:w-full" 45 - /> 46 - </SliderPrimitive.Track> 47 - {Array.from({ length: _values.length }, (_, index) => ( 48 - <SliderPrimitive.Thumb 49 - data-slot="slider-thumb" 50 - key={index} 51 - className="border-primary ring-ring/50 size-4 rounded-4xl border bg-white shadow-sm transition-colors hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden block shrink-0 select-none disabled:pointer-events-none disabled:opacity-50" 52 - /> 53 - ))} 54 - </SliderPrimitive.Control> 55 - </SliderPrimitive.Root> 56 - ); 57 - } 58 - 59 - export { Slider };
-20
src/components/ui/spinner.tsx
··· 1 - import { Loading03Icon } from "@hugeicons/core-free-icons"; 2 - import { HugeiconsIcon, type HugeiconsIconProps } from "@hugeicons/react"; 3 - import { cn } from "@/lib/utils"; 4 - 5 - type SpinnerProps = Omit<HugeiconsIconProps, "icon">; 6 - 7 - function Spinner({ className, ...props }: SpinnerProps) { 8 - return ( 9 - <HugeiconsIcon 10 - icon={Loading03Icon} 11 - strokeWidth={2} 12 - role="status" 13 - aria-label="Loading" 14 - className={cn("size-4 animate-spin", className)} 15 - {...props} 16 - /> 17 - ); 18 - } 19 - 20 - export { Spinner };
-32
src/components/ui/switch.tsx
··· 1 - "use client"; 2 - 3 - import { Switch as SwitchPrimitive } from "@base-ui/react/switch"; 4 - 5 - import { cn } from "@/lib/utils"; 6 - 7 - function Switch({ 8 - className, 9 - size = "default", 10 - ...props 11 - }: SwitchPrimitive.Root.Props & { 12 - size?: "sm" | "default"; 13 - }) { 14 - return ( 15 - <SwitchPrimitive.Root 16 - data-slot="switch" 17 - data-size={size} 18 - className={cn( 19 - "data-checked:bg-primary data-unchecked:bg-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 dark:data-unchecked:bg-input/80 shrink-0 rounded-full border border-transparent focus-visible:ring-[3px] aria-invalid:ring-[3px] data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] peer group/switch relative inline-flex items-center transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 data-disabled:cursor-not-allowed data-disabled:opacity-50", 20 - className, 21 - )} 22 - {...props} 23 - > 24 - <SwitchPrimitive.Thumb 25 - data-slot="switch-thumb" 26 - className="bg-background dark:data-unchecked:bg-foreground dark:data-checked:bg-primary-foreground rounded-full group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 pointer-events-none block ring-0 transition-transform" 27 - /> 28 - </SwitchPrimitive.Root> 29 - ); 30 - } 31 - 32 - export { Switch };
+2 -1
src/db/schema.ts
··· 18 18 .notNull() 19 19 .references(() => user.id, { onDelete: "cascade" }), 20 20 name: varchar({ length: 255 }).notNull(), 21 + description: text(), 21 22 invite: varchar({ length: 12 }).notNull().unique(), 22 23 }); 23 24 ··· 29 30 .notNull() 30 31 .references(() => group.id, { onDelete: "cascade" }), 31 32 name: varchar({ length: 255 }).notNull(), 32 - limit: integer().notNull(), 33 + description: text(), 33 34 }, 34 35 (t) => [index("voucher_group_idx").on(t.groupId)], 35 36 );
+12 -15
src/lib/group.ts
··· 1 1 "use server"; 2 2 3 - import { and, eq, or } from "drizzle-orm"; 3 + import { and, eq } from "drizzle-orm"; 4 4 5 - import { group, member, user } from "@/db/schema"; 5 + import { group, member } from "@/db/schema"; 6 6 import { getUserId } from "@/lib/auth"; 7 7 import { db } from "@/lib/db"; 8 8 import { generateInvite } from "@/lib/invite"; 9 9 10 10 export type GroupWithMembers = Awaited<ReturnType<typeof getGroups>>[number]; 11 11 12 - export async function createGroup(name: string) { 12 + export async function createGroup(name: string, description?: string) { 13 13 const userId = await getUserId(); 14 14 if (!userId) return null; 15 15 16 16 const invite = await generateInvite(); 17 17 const [res] = await db 18 18 .insert(group) 19 - .values({ userId, name, invite }) 19 + .values({ userId, invite, name, description }) 20 20 .returning(); 21 21 22 22 return res; ··· 26 26 const userId = await getUserId(); 27 27 if (!userId) return []; 28 28 29 - const groups = await db.query.group.findMany({ 29 + return await db.query.group.findMany({ 30 30 where: (group, { exists, eq, or, and }) => 31 31 or( 32 32 eq(group.userId, userId), ··· 48 48 }, 49 49 }, 50 50 }); 51 - 52 - return groups.map((g) => ({ 53 - ...g, 54 - members: g.members.map((m) => m.user), 55 - })); 56 51 } 57 52 58 53 export async function getGroup(id: number) { ··· 62 57 63 58 export async function isGroupOwner(id: number) { 64 59 const userId = await getUserId(); 65 - if (!userId) return false; 66 - 67 60 const groupData = await getGroup(id); 68 - if (!groupData) return false; 69 61 62 + if (!userId || !groupData) return false; 70 63 return groupData.userId === userId; 71 64 } 72 65 ··· 76 69 return res.rowCount > 0; 77 70 } 78 71 79 - export async function updateGroup(id: number, name: string) { 72 + export async function updateGroup( 73 + id: number, 74 + name: string, 75 + description?: string, 76 + ) { 80 77 if (!(await isGroupOwner(id))) return null; 81 78 82 79 const [res] = await db 83 80 .update(group) 84 - .set({ name }) 81 + .set({ name, description }) 85 82 .where(and(eq(group.id, id))) 86 83 .returning(); 87 84
+9 -6
src/lib/voucher.ts
··· 6 6 import { getGroups, isGroupOwner } from "@/lib/group"; 7 7 8 8 export async function createVoucher( 9 - name: string, 10 - limit: number, 11 9 groupId: number, 10 + name: string, 11 + description?: string, 12 12 ) { 13 13 if (!(await isGroupOwner(groupId))) return null; 14 14 15 15 const [res] = await db 16 16 .insert(voucher) 17 - .values({ name, limit, groupId }) 17 + .values({ groupId, name, description }) 18 18 .returning(); 19 19 20 20 return res; ··· 45 45 async function isVoucherOwner(id: number) { 46 46 const voucherData = await getVoucher(id); 47 47 if (!voucherData) return false; 48 - 49 48 return await isGroupOwner(voucherData.groupId); 50 49 } 51 50 ··· 55 54 return res.rowCount > 0; 56 55 } 57 56 58 - export async function updateVoucher(id: number, name: string, limit: number) { 57 + export async function updateVoucher( 58 + id: number, 59 + name: string, 60 + description?: string, 61 + ) { 59 62 if (!(await isVoucherOwner(id))) return null; 60 63 61 64 const [res] = await db 62 65 .update(voucher) 63 - .set({ name, limit }) 66 + .set({ name, description }) 64 67 .where(and(eq(voucher.id, id))) 65 68 .returning(); 66 69
+2 -1
src/proxy.ts
··· 10 10 if (session) return NextResponse.next(); 11 11 12 12 const authUrl = new URL("/auth", request.url); 13 - authUrl.searchParams.set("next", request.nextUrl.pathname); 13 + const next = request.nextUrl.href.replace(request.nextUrl.origin, ""); 14 + authUrl.searchParams.set("next", next); 14 15 return NextResponse.redirect(authUrl); 15 16 } 16 17