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
2 changes: 1 addition & 1 deletion knip.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"ignoreDependencies": ["tw-animate-css"]
"ignoreDependencies": ["tw-animate-css", "shadcn"]
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"axios": "^1.13.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.562.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router": "^7.11.0",
Expand All @@ -37,6 +38,7 @@
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react-swc": "^3.9.0",
"knip": "^5.59.1",
"shadcn": "^3.6.3",
"tw-animate-css": "^1.4.0",
"typescript": "~5.8.3",
"vite": "^6.3.5"
Expand Down
101 changes: 101 additions & 0 deletions src/components/EventCardView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemGroup,
ItemMedia,
ItemTitle,
} from '@/components/ui/item';
import type { Events } from '@/types/schema';
import { getShortenedDate } from '@/utils/date';
import { ChevronRightIcon, Plus } from 'lucide-react';
import { Link } from 'react-router';
import { Button } from './ui/button';

type Event = Events & { guests: number; registration_start: string | null };

function getDateLabel(
startDateString: string | null,
endDateString: string | null
) {
if (!endDateString) {
return 'μƒμ‹œ λͺ¨μ§‘ 쀑';
}

const now = new Date();
const endDate = new Date(endDateString!);

if (startDateString && now < new Date(startDateString)) {
return `${getShortenedDate(startDateString)}λΆ€ν„° λͺ¨μ§‘`;
}

if (now <= endDate) {
return `${getShortenedDate(endDateString)}κΉŒμ§€ λͺ¨μ§‘`;
}

return 'λͺ¨μ§‘ 마감';
}

export default function EventCardView({ events }: { events: Event[] }) {
if (events.length === 0) {
return (
<div className="flex-1 flex items-center justify-center">
<div className="flex w-full max-w-md flex-col gap-8 items-center">
<div className="flex flex-col gap-4 text-center">
<h1 className="text-2xl font-bold">κ°œμ„€ν•œ 일정이 μ—†λ„€μš”!</h1>
<h2 className="text-base font-base">
μ‚¬λžŒλ“€κ³Ό μƒˆλ‘œμš΄ 일정을 μž‘μ•„ λ³ΌκΉŒμš”?
</h2>
</div>
<Link to="/new-event">
<Button className="text-lg font-bold px-20 py-5">
μƒˆλ‘œμš΄ 일정 λ§Œλ“€κΈ°
</Button>
</Link>
</div>
</div>
);
}

return (
<ItemGroup className="gap-4">
<Item variant="default" asChild role="listitem">
<Link to="/new-event">
<ItemMedia variant="icon">
<Plus />
</ItemMedia>
<ItemContent>
<ItemTitle>
<h2 className="text-lg font-semibold line-clamp-1">
μƒˆλ‘œμš΄ 일정 λ§Œλ“€κΈ°
</h2>
</ItemTitle>
</ItemContent>
</Link>
</Item>
{events.map((event) => (
<Item key={event.id} variant="outline" asChild role="listitem">
<Link to={`/event/${event.id}`}>
<ItemContent>
<ItemTitle>
<h2 className="text-lg font-semibold line-clamp-1">
{event.title}
</h2>
</ItemTitle>
<ItemDescription>
{getDateLabel(
event.registration_start,
event.registration_deadline
)}
</ItemDescription>
</ItemContent>
<ItemActions>
<ChevronRightIcon className="size-4" />
</ItemActions>
</Link>
</Item>
))}
</ItemGroup>
);
}
192 changes: 192 additions & 0 deletions src/components/ui/item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { Slot } from '@radix-ui/react-slot';
import { type VariantProps, cva } from 'class-variance-authority';
import * as React from 'react';

import { cn } from '@/lib/utils';

function ItemGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
role="list"
data-slot="item-group"
className={cn('group/item-group flex flex-col', className)}
{...props}
/>
);
}

// function ItemSeparator({
// className,
// ...props
// }: React.ComponentProps<typeof Separator>) {
// return (
// <Separator
// data-slot="item-separator"
// orientation="horizontal"
// className={cn('my-0', className)}
// {...props}
// />
// );
// }

const itemVariants = cva(
'group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
{
variants: {
variant: {
default: 'bg-transparent',
outline: 'border-border',
muted: 'bg-muted/50',
},
size: {
default: 'p-4 gap-4 ',
sm: 'py-3 px-4 gap-2.5',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);

function Item({
className,
variant = 'default',
size = 'default',
asChild = false,
...props
}: React.ComponentProps<'div'> &
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : 'div';
return (
<Comp
data-slot="item"
data-variant={variant}
data-size={size}
className={cn(itemVariants({ variant, size, className }))}
{...props}
/>
);
}

const itemMediaVariants = cva(
'flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5',
{
variants: {
variant: {
default: 'bg-transparent',
icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
image:
'size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover',
},
},
defaultVariants: {
variant: 'default',
},
}
);

function ItemMedia({
className,
variant = 'default',
...props
}: React.ComponentProps<'div'> & VariantProps<typeof itemMediaVariants>) {
return (
<div
data-slot="item-media"
data-variant={variant}
className={cn(itemMediaVariants({ variant, className }))}
{...props}
/>
);
}

function ItemContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-content"
className={cn(
'flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none',
className
)}
{...props}
/>
);
}

function ItemTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-title"
className={cn(
'flex w-fit items-center gap-2 text-sm leading-snug font-medium',
className
)}
{...props}
/>
);
}

function ItemDescription({ className, ...props }: React.ComponentProps<'p'>) {
return (
<p
data-slot="item-description"
className={cn(
'text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance',
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
className
)}
{...props}
/>
);
}

function ItemActions({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-actions"
className={cn('flex items-center gap-2', className)}
{...props}
/>
);
}

// function ItemHeader({ className, ...props }: React.ComponentProps<'div'>) {
// return (
// <div
// data-slot="item-header"
// className={cn(
// 'flex basis-full items-center justify-between gap-2',
// className
// )}
// {...props}
// />
// );
// }

// function ItemFooter({ className, ...props }: React.ComponentProps<'div'>) {
// return (
// <div
// data-slot="item-footer"
// className={cn(
// 'flex basis-full items-center justify-between gap-2',
// className
// )}
// {...props}
// />
// );
// }

export {
Item,
ItemMedia,
ItemContent,
ItemActions,
ItemGroup,
// ItemSeparator,
ItemTitle,
ItemDescription,
// ItemHeader,
// ItemFooter,
};
2 changes: 1 addition & 1 deletion src/routes/Guests.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router';
import { useNavigate } from 'react-router';
import { toast } from 'sonner';

// shadcn UI μ»΄ν¬λ„ŒνŠΈ
Expand Down
Loading