Moved things and added title extraction

This commit is contained in:
2025-06-27 14:30:39 +02:00
parent 50cc087bed
commit e4c382a4dc
18 changed files with 43 additions and 13 deletions

View File

@@ -1,53 +0,0 @@
"use client"
import { AuthentikIcon } from "@/components/svgs/authentik"
import { Button } from "@/components/ui/button"
import { signIn } from "@/lib/auth/auth-client"
import { Loader2 } from "lucide-react"
import { useState } from "react"
interface OAuthSignInButtonProps {
className?: string
}
export function OAuthSignInButton({
className
}: OAuthSignInButtonProps) {
const [isLoading, setIsLoading] = useState(false)
const handleSignIn = async () => {
try {
setIsLoading(true)
await signIn.oauth2({
providerId: "authentik",
callbackURL: "/dashboard"
})
} catch (error) {
console.error("Sign in failed:", error)
setIsLoading(false)
}
}
return (
<Button
onClick={handleSignIn}
disabled={isLoading}
className={className}
size="lg"
>
{isLoading ?
(
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Signing in...
</>
) :
(
<>
<AuthentikIcon className="mr-2 h-5 w-5" />
Sign in with Authentik
</>
)}
</Button>
)
}

View File

@@ -1,217 +0,0 @@
"use client"
import { DatePicker } from "@/components/date-picker"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Switch } from "@/components/ui/switch"
import { createAdvanceUrl } from "@/lib/actions/url"
import { advancedUrlSchema } from "@/lib/schema/url"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
import { z } from "zod"
type AdvancedUrlFormValues = z.infer<typeof advancedUrlSchema>
export function AdvancedUrlFormCard() {
const form = useForm<AdvancedUrlFormValues>({
resolver: zodResolver(advancedUrlSchema),
defaultValues: {
url: "",
slug: "",
title: "",
maxVisits: 0,
expDate: undefined,
forwardQueryParams: true,
crawlable: false
}
})
async function handleSubmit(data: AdvancedUrlFormValues) {
const res = await createAdvanceUrl(data)
if (res.error) {
toast.error(res.message)
} else {
toast.success(res.message)
form.reset()
}
}
return (
<Card>
<CardHeader>
<CardTitle>Create Advanced Short Link</CardTitle>
<CardDescription>
Create a short link with advanced configuration options.
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4"
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-4">
<FormField
control={form.control}
name="url"
render={({ field }) => (
<FormItem>
<FormLabel>Original URL</FormLabel>
<FormControl>
<Input
placeholder="https://example.com"
{...field}
/>
</FormControl>
<FormDescription>
The URL you want to shorten.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="slug"
render={({ field }) => (
<FormItem>
<FormLabel>Custom Slug (Optional)</FormLabel>
<FormControl>
<Input
placeholder="my-custom-link"
{...field}
/>
</FormControl>
<FormDescription>
A unique identifier for your short link (max 10 characters).
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="forwardQueryParams"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base">
Forward Query Parameters
</FormLabel>
<FormDescription>
Forward query parameters from the short link to the destination URL.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="crawlable"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base">
Crawlable (Optional)
</FormLabel>
<FormDescription>
Allow search engines to crawl and index this link.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value ?? false}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
<div className="space-y-4">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Title (Optional)</FormLabel>
<FormControl>
<Input
placeholder="My Link Title"
{...field}
/>
</FormControl>
<FormDescription>
A descriptive title for your link (max 100 characters).
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="maxVisits"
render={({ field }) => (
<FormItem>
<FormLabel>Max Visits (Optional)</FormLabel>
<FormControl>
<Input
type="number"
placeholder="100"
{...field}
onChange={e => field.onChange(e.target.value ? parseInt(e.target.value) : undefined)}
/>
</FormControl>
<FormDescription>
Maximum number of visits before the link expires.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="expDate"
render={({ field }) => (
<FormItem>
<FormLabel>Expiration Date (Optional)</FormLabel>
<FormControl>
<DatePicker
value={field.value}
onChange={field.onChange}
placeholder="Select expiration date"
/>
</FormControl>
<FormDescription>
When this link should expire and become inaccessible.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
<Button
type="submit"
className="w-full"
disabled={form.formState.isSubmitting}
>
{form.formState.isSubmitting ? "Creating..." : "Create Short Link"}
</Button>
</form>
</Form>
</CardContent>
</Card>
)
}

View File

@@ -1,85 +0,0 @@
"use client"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar"
import { ChevronDown, Monitor, Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { useEffect, useState } from "react"
export function SidebarThemeToggle() {
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)
// Prevent hydration mismatch
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return (
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton disabled tooltip="Theme">
<Sun className="h-4 w-4" />
<span>Theme</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
)
}
const getIcon = () => {
switch (theme) {
case "light":
return <Sun className="h-4 w-4" />
case "dark":
return <Moon className="h-4 w-4" />
default:
return <Monitor className="h-4 w-4" />
}
}
const getThemeLabel = () => {
switch (theme) {
case "light":
return "Light"
case "dark":
return "Dark"
default:
return "System"
}
}
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
tooltip={`Theme: ${getThemeLabel()}`}
className="pl-4"
>
{getIcon()}
<span>Theme ({getThemeLabel()})</span>
<ChevronDown className="ml-auto h-4 w-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="top">
<DropdownMenuItem onClick={() => setTheme("light")}>
<Sun className="h-4 w-4 mr-2" />
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
<Moon className="h-4 w-4 mr-2" />
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
<Monitor className="h-4 w-4 mr-2" />
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}

View File

@@ -1,132 +0,0 @@
"use client"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu"
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from "@/components/ui/sidebar"
import { signOut, useSession } from "@/lib/auth/auth-client"
import { LogOut, Settings, User } from "lucide-react"
import Link from "next/link"
import { useRouter } from "next/navigation"
export function SidebarUserDropdown() {
const { data: session, isPending } = useSession()
const { state } = useSidebar()
const router = useRouter()
const handleSignOut = async () => {
try {
await signOut({
fetchOptions: {
onSuccess: () => {
router.push("/")
}
}
})
} catch (err) {
console.error("Failed to sign out:", err)
}
}
if (isPending) {
return (
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton disabled>
<User />
<span>Loading...</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
)
}
if (!session?.user) {
return (
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Link href="/sign-in">
<User />
<span>Sign in</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
)
}
const user = session.user
const userInitials = user.name
? user.name.split(" ").map(n => n[0]).join("").toUpperCase()
: user.email?.[0]?.toUpperCase() || "U"
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.image || undefined} alt={user.name || "User"} />
<AvatarFallback className="rounded-lg">{userInitials}</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">
{user.name || "User"}
</span>
<span className="truncate text-xs text-muted-foreground">
{user.email}
</span>
</div>
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
side={state === "collapsed" ? "right" : "bottom"}
align="end"
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.image || undefined} alt={user.name || "User"} />
<AvatarFallback className="rounded-lg">{userInitials}</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">
{user.name || "User"}
</span>
<span className="truncate text-xs text-muted-foreground">
{user.email}
</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link href="/dashboard">
<Settings className="mr-2 h-4 w-4" />
<span>Settings</span>
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleSignOut}>
<LogOut className="mr-2 h-4 w-4" />
<span>Log out</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}

View File

@@ -1,91 +0,0 @@
"use client"
import { LayoutDashboard, List, PanelLeft, Plus } from "lucide-react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar
} from "@/components/ui/sidebar"
import { SidebarThemeToggle } from "./sidebar-theme-toggle"
import { SidebarUserDropdown } from "./sidebar-user-dropdown"
const items = [
{
title: "Dashboard",
url: "/dashboard",
icon: LayoutDashboard
},
{
title: "List",
url: "/dashboard/list",
icon: List
},
{
title: "Create",
url: "/dashboard/create",
icon: Plus
}
]
export function DashboardSidebar() {
const pathname = usePathname()
const { toggleSidebar } = useSidebar()
return (
<Sidebar collapsible="icon">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
onClick={toggleSidebar}
tooltip="Toggle Sidebar"
>
<PanelLeft />
<span>Toggle Sidebar</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel className="group-data-[collapsible=icon]:hidden">
Navigation
</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
asChild
isActive={pathname === item.url}
tooltip={item.title}
>
<Link href={item.url}>
<item.icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarThemeToggle />
<SidebarUserDropdown />
</SidebarFooter>
</Sidebar>
)
}

View File

@@ -1,25 +0,0 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
interface StatsCardProps {
title: string
value: number | string
icon?: React.ReactNode
description?: string
}
export function StatsCard({ title, value, icon, description }: StatsCardProps) {
const displayValue = typeof value === "number" ? value.toLocaleString() : value
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{title}</CardTitle>
{icon}
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{displayValue}</div>
{description && <p className="text-xs text-muted-foreground">{description}</p>}
</CardContent>
</Card>
)
}

View File

@@ -1,100 +0,0 @@
"use client"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { addUrl } from "@/lib/actions/url"
import { urlFormSchema } from "@/lib/schema/url"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
import { z } from "zod"
type UrlFormValues = z.infer<typeof urlFormSchema>
export function UrlFormCard() {
const form = useForm<UrlFormValues>({
resolver: zodResolver(urlFormSchema),
defaultValues: {
url: "",
slug: ""
}
})
async function handleSubmit(data: UrlFormValues) {
const res = await addUrl(data)
if (res.error) {
toast.error(res.message)
} else {
toast.success(res.message)
form.reset()
}
}
return (
<Card>
<CardHeader>
<CardTitle>Create Short Link</CardTitle>
<CardDescription>
Enter a URL and create a custom slug for your short link.
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4"
>
<FormField
control={form.control}
name="url"
render={({ field }) => (
<FormItem>
<FormLabel>Original URL</FormLabel>
<FormControl>
<Input
placeholder="https://example.com"
{...field}
/>
</FormControl>
<FormDescription>
The URL you want to shorten.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="slug"
render={({ field }) => (
<FormItem>
<FormLabel>Custom Slug</FormLabel>
<FormControl>
<Input
placeholder="my-custom-link"
{...field}
/>
</FormControl>
<FormDescription>
A unique identifier for your short link.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
className="w-full"
disabled={form.formState.isSubmitting}
>
{form.formState.isSubmitting ? "Creating..." : "Create Short Link"}
</Button>
</form>
</Form>
</CardContent>
</Card>
)
}

View File

@@ -1,432 +0,0 @@
"use client"
import {
ColumnDef,
ColumnFiltersState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
SortingState,
useReactTable,
VisibilityState
} from "@tanstack/react-table"
import { ArrowUpDown, Check, Copy, MoreHorizontal, Trash2, X } from "lucide-react"
import * as React from "react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { deleteUrl } from "@/lib/actions/url"
import { urls, visits } from "@/lib/drizzle/schema"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { toast } from "sonner"
type UrlRecord = typeof urls.$inferSelect & {
visits?: (typeof visits.$inferSelect)[]
}
export const columns: ColumnDef<UrlRecord>[] = [
{
accessorKey: "slug",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Slug
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
cell: ({ row }) => <div className="font-mono text-sm">{row.getValue("slug")}</div>
},
{
accessorKey: "url",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
URL
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
cell: ({ row }) => {
const url = row.getValue("url") as string
return (
<div className="max-w-[300px] truncate" title={url}>
<Link href={url}>
{url.replace(/^(https?:\/\/)/, "")}
</Link>
</div>
)
}
},
{
accessorKey: "title",
header: "Title",
cell: ({ row }) => {
const title = row.getValue("title") as string | null
return <div className="max-w-[200px] truncate">{title || "No title"}</div>
}
},
{
id: "visits",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Visits
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
cell: ({ row }) => {
const urlRecord = row.original
const visitCount = urlRecord.visits?.length || 0
return <div className="text-center">{visitCount}</div>
},
sortingFn: (rowA, rowB) => {
const visitsA = rowA.original.visits?.length || 0
const visitsB = rowB.original.visits?.length || 0
return visitsA - visitsB
}
},
{
accessorKey: "maxVisits",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Max Visits
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
cell: ({ row }) => {
const maxVisits = row.getValue("maxVisits") as number | null
return <div>{maxVisits || "Unlimited"}</div>
}
},
{
accessorKey: "expDate",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Expires
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
cell: ({ row }) => {
const expDate = row.getValue("expDate") as Date | null
if (!expDate) return <div>Never</div>
return <div>{expDate.toLocaleDateString()}</div>
}
},
{
accessorKey: "createdAt",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Created
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
cell: ({ row }) => {
const createdAt = row.getValue("createdAt") as Date
return <div>{createdAt.toLocaleDateString()}</div>
}
},
{
accessorKey: "updatedAt",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Updated
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
cell: ({ row }) => {
const updatedAt = row.getValue("updatedAt") as Date
return <div>{updatedAt.toLocaleDateString()}</div>
}
},
{
accessorKey: "crawlable",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Crawlable
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
cell: ({ row }) => {
const crawlable = row.getValue("crawlable") as boolean
return (
<div className="flex justify-center">
{crawlable ? <Check className="h-4 w-4 text-green-600" /> : <X className="h-4 w-4 text-red-600" />}
</div>
)
}
},
{
accessorKey: "forwardQueryParams",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Forward Params
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
cell: ({ row }) => {
const forwardQueryParams = row.getValue("forwardQueryParams") as boolean
return (
<div className="flex justify-center">
{forwardQueryParams ? <Check className="h-4 w-4 text-green-600" /> : <X className="h-4 w-4 text-red-600" />}
</div>
)
}
}
]
interface UrlsDataTableProps {
data: UrlRecord[]
}
export function UrlsDataTable({ data }: UrlsDataTableProps) {
const [sorting, setSorting] = React.useState<SortingState>([])
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})
const router = useRouter()
const handleCopy = React.useCallback((string: string) => {
navigator.clipboard.writeText(string).then(() => {
toast.success("URL copied to clipboard")
}).catch(() => {
toast.error("Failed to copy URL")
})
}, [])
const handleDelete = React.useCallback(async (id: string) => {
const res = await deleteUrl(id)
if (res.error) {
toast.error("Failed to delete URL")
} else {
toast.success("URL deleted successfully")
router.refresh()
}
}, [router])
const tableColumns = React.useMemo(() => {
const actionRow: ColumnDef<UrlRecord> = {
id: "actions",
enableHiding: false,
cell: ({ row }) => {
const urlRecord = row.original
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => {
handleCopy(`${window.location.origin}/r/${urlRecord.slug}`)
}}
>
<Copy className="mr-2 h-4 w-4" />
Copy URL
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-red-600"
onClick={() => {
handleDelete(urlRecord.id)
}}
>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
}
return [...columns, actionRow]
}, [handleCopy, handleDelete])
const table = useReactTable({
data,
columns: tableColumns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
state: {
sorting,
columnFilters,
columnVisibility
}
})
return (
<div className="w-full">
<div className="flex items-center py-4">
<Input
placeholder="Filter URLs by slug..."
value={(table.getColumn("slug")?.getFilterValue() as string) ?? ""}
onChange={(event) => table.getColumn("slug")?.setFilterValue(event.target.value)}
className="max-w-sm"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto">
Columns
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
{column.id}
</DropdownMenuCheckboxItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ?
(
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) :
(
<TableRow>
<TableCell
colSpan={tableColumns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<div className="text-sm text-muted-foreground">
{table.getFilteredRowModel().rows.length} row(s) total.
</div>
<div className="space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
</div>
)
}

View File

@@ -12,7 +12,6 @@ import { cn } from "@/lib/utils"
interface DatePickerProps {
value?: Date
onChange?: (date: Date | undefined) => void
placeholder?: string
disabled?: boolean
className?: string
}
@@ -20,7 +19,6 @@ interface DatePickerProps {
export function DatePicker({
value,
onChange,
placeholder = "Pick a date",
disabled = false,
className
}: DatePickerProps) {
@@ -37,7 +35,7 @@ export function DatePicker({
disabled={disabled}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{value ? format(value, "PPP") : <span>{placeholder}</span>}
{value ? format(value, "PPP") : <span>Pick a date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
@@ -45,7 +43,6 @@ export function DatePicker({
mode="single"
selected={value}
onSelect={onChange}
initialFocus
/>
</PopoverContent>
</Popover>