Formatting
This commit is contained in:
@@ -10,11 +10,11 @@ export function SidebarClient({ children }: { children: ReactNode }) {
|
|||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full">
|
<div className="flex flex-col w-full">
|
||||||
<div className="p-2 border-b flex items-center gap-1">
|
<div className="flex gap-1 items-center p-2 border-b">
|
||||||
<SidebarTrigger />
|
<SidebarTrigger />
|
||||||
<span className="text-xl">Linker</span>
|
<span className="text-xl">Linker</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex">{children}</div>
|
<div className="flex flex-1">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function SidebarThemeToggle() {
|
|||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton disabled tooltip="Theme">
|
<SidebarMenuButton disabled tooltip="Theme">
|
||||||
<Sun className="h-4 w-4" />
|
<Sun className="w-4 h-4" />
|
||||||
<span>Theme</span>
|
<span>Theme</span>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
@@ -31,11 +31,11 @@ export function SidebarThemeToggle() {
|
|||||||
const getIcon = () => {
|
const getIcon = () => {
|
||||||
switch (theme) {
|
switch (theme) {
|
||||||
case "light":
|
case "light":
|
||||||
return <Sun className="h-4 w-4" />
|
return <Sun className="w-4 h-4" />
|
||||||
case "dark":
|
case "dark":
|
||||||
return <Moon className="h-4 w-4" />
|
return <Moon className="w-4 h-4" />
|
||||||
default:
|
default:
|
||||||
return <Monitor className="h-4 w-4" />
|
return <Monitor className="w-4 h-4" />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,20 +61,20 @@ export function SidebarThemeToggle() {
|
|||||||
>
|
>
|
||||||
{getIcon()}
|
{getIcon()}
|
||||||
<span>Theme ({getThemeLabel()})</span>
|
<span>Theme ({getThemeLabel()})</span>
|
||||||
<ChevronDown className="ml-auto h-4 w-4" />
|
<ChevronDown className="ml-auto w-4 h-4" />
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" side="top">
|
<DropdownMenuContent align="end" side="top">
|
||||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||||
<Sun className="h-4 w-4 mr-2" />
|
<Sun className="mr-2 w-4 h-4" />
|
||||||
Light
|
Light
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||||
<Moon className="h-4 w-4 mr-2" />
|
<Moon className="mr-2 w-4 h-4" />
|
||||||
Dark
|
Dark
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||||
<Monitor className="h-4 w-4 mr-2" />
|
<Monitor className="mr-2 w-4 h-4" />
|
||||||
System
|
System
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -76,37 +76,37 @@ export function SidebarUserDropdown() {
|
|||||||
size="lg"
|
size="lg"
|
||||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||||
>
|
>
|
||||||
<Avatar className="h-8 w-8 rounded-lg">
|
<Avatar className="w-8 h-8 rounded-lg">
|
||||||
<AvatarImage src={user.image || undefined} alt={user.name || "User"} />
|
<AvatarImage src={user.image || undefined} alt={user.name || "User"} />
|
||||||
<AvatarFallback className="rounded-lg">{userInitials}</AvatarFallback>
|
<AvatarFallback className="rounded-lg">{userInitials}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
<div className="grid flex-1 text-sm leading-tight text-left">
|
||||||
<span className="truncate font-semibold">
|
<span className="font-semibold truncate">
|
||||||
{user.name || "User"}
|
{user.name || "User"}
|
||||||
</span>
|
</span>
|
||||||
<span className="truncate text-xs text-muted-foreground">
|
<span className="text-xs truncate text-muted-foreground">
|
||||||
{user.email}
|
{user.email}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
className="rounded-lg w-[--radix-dropdown-menu-trigger-width] min-w-56"
|
||||||
side={state === "collapsed" ? "right" : "bottom"}
|
side={state === "collapsed" ? "right" : "bottom"}
|
||||||
align="end"
|
align="end"
|
||||||
sideOffset={4}
|
sideOffset={4}
|
||||||
>
|
>
|
||||||
<DropdownMenuLabel className="p-0 font-normal">
|
<DropdownMenuLabel className="p-0 font-normal">
|
||||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
<div className="flex gap-2 items-center py-1.5 px-1 text-sm text-left">
|
||||||
<Avatar className="h-8 w-8 rounded-lg">
|
<Avatar className="w-8 h-8 rounded-lg">
|
||||||
<AvatarImage src={user.image || undefined} alt={user.name || "User"} />
|
<AvatarImage src={user.image || undefined} alt={user.name || "User"} />
|
||||||
<AvatarFallback className="rounded-lg">{userInitials}</AvatarFallback>
|
<AvatarFallback className="rounded-lg">{userInitials}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
<div className="grid flex-1 text-sm leading-tight text-left">
|
||||||
<span className="truncate font-semibold">
|
<span className="font-semibold truncate">
|
||||||
{user.name || "User"}
|
{user.name || "User"}
|
||||||
</span>
|
</span>
|
||||||
<span className="truncate text-xs text-muted-foreground">
|
<span className="text-xs truncate text-muted-foreground">
|
||||||
{user.email}
|
{user.email}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,7 +114,7 @@ export function SidebarUserDropdown() {
|
|||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={handleSignOut}>
|
<DropdownMenuItem onClick={handleSignOut}>
|
||||||
<LogOut className="mr-2 h-4 w-4" />
|
<LogOut className="mr-2 w-4 h-4" />
|
||||||
<span>Log out</span>
|
<span>Log out</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function StatsCard({ title, value, icon, description }: StatsCardProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row justify-between items-center pb-2 space-y-0">
|
||||||
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
||||||
{icon}
|
{icon}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ function AdvancedUrlForm() {
|
|||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -84,7 +84,7 @@ function AdvancedUrlForm() {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="forwardQueryParams"
|
name="forwardQueryParams"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
<FormItem className="flex justify-between items-center p-4 rounded-lg border">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel className="text-base">
|
<FormLabel className="text-base">
|
||||||
Forward Query Parameters
|
Forward Query Parameters
|
||||||
@@ -106,7 +106,7 @@ function AdvancedUrlForm() {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="crawlable"
|
name="crawlable"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
<FormItem className="flex justify-between items-center p-4 rounded-lg border">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel className="text-base">
|
<FormLabel className="text-base">
|
||||||
Crawlable
|
Crawlable
|
||||||
@@ -237,7 +237,7 @@ function EditUrlForm({ data }: { data: typeof urls.$inferSelect }) {
|
|||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -275,7 +275,7 @@ function EditUrlForm({ data }: { data: typeof urls.$inferSelect }) {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="forwardQueryParams"
|
name="forwardQueryParams"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
<FormItem className="flex justify-between items-center p-4 rounded-lg border">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel className="text-base">
|
<FormLabel className="text-base">
|
||||||
Forward Query Parameters
|
Forward Query Parameters
|
||||||
@@ -297,7 +297,7 @@ function EditUrlForm({ data }: { data: typeof urls.$inferSelect }) {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="crawlable"
|
name="crawlable"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
<FormItem className="flex justify-between items-center p-4 rounded-lg border">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel className="text-base">
|
<FormLabel className="text-base">
|
||||||
Crawlable
|
Crawlable
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const columns: ColumnDef<UrlRecord>[] = [
|
|||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
>
|
>
|
||||||
Slug
|
Slug
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -62,7 +62,7 @@ const columns: ColumnDef<UrlRecord>[] = [
|
|||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
>
|
>
|
||||||
URL
|
URL
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -94,7 +94,7 @@ const columns: ColumnDef<UrlRecord>[] = [
|
|||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
>
|
>
|
||||||
Visits
|
Visits
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -118,7 +118,7 @@ const columns: ColumnDef<UrlRecord>[] = [
|
|||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
>
|
>
|
||||||
Max Visits
|
Max Visits
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -136,7 +136,7 @@ const columns: ColumnDef<UrlRecord>[] = [
|
|||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
>
|
>
|
||||||
Expires
|
Expires
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -155,7 +155,7 @@ const columns: ColumnDef<UrlRecord>[] = [
|
|||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
>
|
>
|
||||||
Created
|
Created
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -173,7 +173,7 @@ const columns: ColumnDef<UrlRecord>[] = [
|
|||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
>
|
>
|
||||||
Updated
|
Updated
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -191,7 +191,7 @@ const columns: ColumnDef<UrlRecord>[] = [
|
|||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
>
|
>
|
||||||
Crawlable
|
Crawlable
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -199,7 +199,7 @@ const columns: ColumnDef<UrlRecord>[] = [
|
|||||||
const crawlable = row.getValue("crawlable") as boolean
|
const crawlable = row.getValue("crawlable") as boolean
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
{crawlable ? <Check className="h-4 w-4 text-green-600" /> : <X className="h-4 w-4 text-red-600" />}
|
{crawlable ? <Check className="w-4 h-4 text-green-600" /> : <X className="w-4 h-4 text-red-600" />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -213,7 +213,7 @@ const columns: ColumnDef<UrlRecord>[] = [
|
|||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
>
|
>
|
||||||
Forward Params
|
Forward Params
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -221,7 +221,7 @@ const columns: ColumnDef<UrlRecord>[] = [
|
|||||||
const forwardQueryParams = row.getValue("forwardQueryParams") as boolean
|
const forwardQueryParams = row.getValue("forwardQueryParams") as boolean
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
{forwardQueryParams ? <Check className="h-4 w-4 text-green-600" /> : <X className="h-4 w-4 text-red-600" />}
|
{forwardQueryParams ? <Check className="w-4 h-4 text-green-600" /> : <X className="w-4 h-4 text-red-600" />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -267,9 +267,9 @@ export function UrlsDataTable({ data }: UrlsDataTableProps) {
|
|||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
<Button variant="ghost" className="p-0 w-8 h-8">
|
||||||
<span className="sr-only">Open menu</span>
|
<span className="sr-only">Open menu</span>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
@@ -279,12 +279,12 @@ export function UrlsDataTable({ data }: UrlsDataTableProps) {
|
|||||||
handleCopy(`${window.location.origin}/r/${urlRecord.slug}`)
|
handleCopy(`${window.location.origin}/r/${urlRecord.slug}`)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Copy className="mr-2 h-4 w-4" />
|
<Copy className="mr-2 w-4 h-4" />
|
||||||
Copy URL
|
Copy URL
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link href={`/dashboard/edit/${urlRecord.id}`}>
|
<Link href={`/dashboard/edit/${urlRecord.id}`}>
|
||||||
<Copy className="mr-2 h-4 w-4" />
|
<Copy className="mr-2 w-4 h-4" />
|
||||||
Edit URL
|
Edit URL
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -295,7 +295,7 @@ export function UrlsDataTable({ data }: UrlsDataTableProps) {
|
|||||||
handleDelete(urlRecord.id)
|
handleDelete(urlRecord.id)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
<Trash2 className="mr-2 w-4 h-4" />
|
||||||
Delete
|
Delete
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
@@ -410,7 +410,7 @@ export function UrlsDataTable({ data }: UrlsDataTableProps) {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-end space-x-2 py-4">
|
<div className="flex justify-end items-center py-4 space-x-2">
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
{table.getFilteredRowModel().rows.length} row(s) total.
|
{table.getFilteredRowModel().rows.length} row(s) total.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default async function DashboardCreatePage() {
|
|||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground mb-2">Create Short Link</h1>
|
<h1 className="mb-2 text-2xl font-bold text-foreground">Create Short Link</h1>
|
||||||
</div>
|
</div>
|
||||||
<UrlFormCard />
|
<UrlFormCard />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import Link from "next/link"
|
|||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-8">
|
<div className="container py-8 mx-auto">
|
||||||
<div className="max-w-2xl mx-auto">
|
<div className="mx-auto max-w-2xl">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>URL Not Found</CardTitle>
|
<CardTitle>URL Not Found</CardTitle>
|
||||||
@@ -18,7 +18,7 @@ export default function NotFound() {
|
|||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
This could happen if:
|
This could happen if:
|
||||||
</p>
|
</p>
|
||||||
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-1">
|
<ul className="space-y-1 text-sm list-disc list-inside text-muted-foreground">
|
||||||
<li>The link has been deleted</li>
|
<li>The link has been deleted</li>
|
||||||
<li>You don't have permission to edit this link</li>
|
<li>You don't have permission to edit this link</li>
|
||||||
<li>The link ID is invalid</li>
|
<li>The link ID is invalid</li>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default async function EditPage({
|
|||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground mb-2">Edit Short Link</h1>
|
<h1 className="mb-2 text-2xl font-bold text-foreground">Edit Short Link</h1>
|
||||||
</div>
|
</div>
|
||||||
<UrlFormCard editMode data={url} />
|
<UrlFormCard editMode data={url} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ export default function DashboardLayout({
|
|||||||
return (
|
return (
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<SidebarClient>
|
<SidebarClient>
|
||||||
<div className="min-h-screen flex w-full">
|
<div className="flex w-full min-h-screen">
|
||||||
<DashboardSidebar />
|
<DashboardSidebar />
|
||||||
<main className="flex-1 overflow-auto">
|
<main className="overflow-auto flex-1">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ export default async function DashboardListPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-2xl font-bold text-foreground mb-2 block">URLs</h1>
|
<h1 className="block mb-2 text-2xl font-bold text-foreground">URLs</h1>
|
||||||
<h1 className="text-muted-foreground block">Manage all your shortened URLs.</h1>
|
<h1 className="block text-muted-foreground">Manage all your shortened URLs.</h1>
|
||||||
</div>
|
</div>
|
||||||
<UrlsDataTable data={urls} />
|
<UrlsDataTable data={urls} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export default async function Dashboard() {
|
|||||||
|
|
||||||
const stats = await getDashboardStats()
|
const stats = await getDashboardStats()
|
||||||
|
|
||||||
// Determine the most visited URL display value
|
|
||||||
const mostVisitedDisplay = stats.mostVisitedUrl
|
const mostVisitedDisplay = stats.mostVisitedUrl
|
||||||
? stats.mostVisitedUrl.visitCount > 0
|
? stats.mostVisitedUrl.visitCount > 0
|
||||||
? `${stats.mostVisitedUrl.title || stats.mostVisitedUrl.slug || "Untitled"} (${stats.mostVisitedUrl.visitCount})`
|
? `${stats.mostVisitedUrl.title || stats.mostVisitedUrl.slug || "Untitled"} (${stats.mostVisitedUrl.visitCount})`
|
||||||
@@ -22,24 +21,24 @@ export default async function Dashboard() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<h1 className="text-2xl font-bold text-foreground mb-4 block">Dashboard</h1>
|
<h1 className="block mb-4 text-2xl font-bold text-foreground">Dashboard</h1>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
<div className="grid grid-cols-1 gap-4 mb-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Shortened URLs"
|
title="Shortened URLs"
|
||||||
value={stats.totalUrls}
|
value={stats.totalUrls}
|
||||||
icon={<LinkIcon className="h-4 w-4 text-muted-foreground" />}
|
icon={<LinkIcon className="w-4 h-4 text-muted-foreground" />}
|
||||||
description="Total number of shortened links"
|
description="Total number of shortened links"
|
||||||
/>
|
/>
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Total Visits"
|
title="Total Visits"
|
||||||
value={stats.totalVisits}
|
value={stats.totalVisits}
|
||||||
icon={<MousePointerClick className="h-4 w-4 text-muted-foreground" />}
|
icon={<MousePointerClick className="w-4 h-4 text-muted-foreground" />}
|
||||||
description="Combined visits across all links"
|
description="Combined visits across all links"
|
||||||
/>
|
/>
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Most Visited URL"
|
title="Most Visited URL"
|
||||||
value={mostVisitedDisplay}
|
value={mostVisitedDisplay}
|
||||||
icon={<TrendingUp className="h-4 w-4 text-muted-foreground" />}
|
icon={<TrendingUp className="w-4 h-4 text-muted-foreground" />}
|
||||||
description="Most popular shortened link"
|
description="Most popular shortened link"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,18 +10,18 @@ export default function PublicLayout({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<nav className="bg-background shadow-sm border-b border-border">
|
<nav className="border-b shadow-sm bg-background border-border">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="px-4 mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||||
<div className="flex justify-between items-center h-16">
|
<div className="flex justify-between items-center h-16">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<h1 className="text-xl font-bold text-foreground hover:text-muted-foreground transition-colors cursor-pointer">
|
<h1 className="text-xl font-bold transition-colors cursor-pointer text-foreground hover:text-muted-foreground">
|
||||||
Linker
|
Linker
|
||||||
</h1>
|
</h1>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex gap-2 items-center">
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
<UserDropdown />
|
<UserDropdown />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Link from "next/link"
|
|||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center">
|
<div className="flex justify-center items-center min-h-screen">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="text-6xl font-bold text-gray-900 dark:text-gray-100">404</h1>
|
<h1 className="text-6xl font-bold text-gray-900 dark:text-gray-100">404</h1>
|
||||||
<h2 className="mt-4 text-2xl font-semibold text-gray-700 dark:text-gray-300">
|
<h2 className="mt-4 text-2xl font-semibold text-gray-700 dark:text-gray-300">
|
||||||
@@ -13,7 +13,7 @@ export default function NotFound() {
|
|||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className="mt-6 inline-block rounded-lg bg-blue-600 px-6 py-3 text-white hover:bg-blue-700 transition-colors"
|
className="inline-block py-3 px-6 mt-6 text-white bg-blue-600 rounded-lg transition-colors hover:bg-blue-700"
|
||||||
>
|
>
|
||||||
Go Home
|
Go Home
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -38,13 +38,13 @@ export function OAuthSignInButton({
|
|||||||
{isLoading ?
|
{isLoading ?
|
||||||
(
|
(
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 w-4 h-4 animate-spin" />
|
||||||
Signing in...
|
Signing in...
|
||||||
</>
|
</>
|
||||||
) :
|
) :
|
||||||
(
|
(
|
||||||
<>
|
<>
|
||||||
<AuthentikIcon className="mr-2 h-5 w-5" />
|
<AuthentikIcon className="mr-2 w-5 h-5" />
|
||||||
Sign in with Authentik
|
Sign in with Authentik
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ export default async function SignInPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-background p-4">
|
<div className="flex justify-center items-center p-4 min-h-screen bg-background">
|
||||||
<Card className="w-full max-w-md shadow-xl">
|
<Card className="w-full max-w-md shadow-xl">
|
||||||
<CardHeader className="space-y-1 text-center">
|
<CardHeader className="space-y-1 text-center">
|
||||||
<div className="flex justify-center mb-4">
|
<div className="flex justify-center mb-4">
|
||||||
<div className="p-3 bg-primary/10 rounded-full">
|
<div className="p-3 rounded-full bg-primary/10">
|
||||||
<LogIn className="h-8 w-8 text-primary" />
|
<LogIn className="w-8 h-8 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-2xl font-bold">Welcome back</CardTitle>
|
<CardTitle className="text-2xl font-bold">Welcome back</CardTitle>
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ export function DatePicker({
|
|||||||
!value && "text-muted-foreground"
|
!value && "text-muted-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
<CalendarIcon className="mr-2 w-4 h-4" />
|
||||||
{value ? format(value, "PPP") : <span>Pick a date</span>}
|
{value ? format(value, "PPP") : <span>Pick a date</span>}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-auto p-0" align="start">
|
<PopoverContent className="p-0 w-auto" align="start">
|
||||||
<Calendar
|
<Calendar
|
||||||
mode="single"
|
mode="single"
|
||||||
selected={value ?? undefined}
|
selected={value ?? undefined}
|
||||||
|
|||||||
@@ -44,15 +44,15 @@ export function ThemeToggle() {
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||||
<Sun className="mr-2 h-4 w-4" />
|
<Sun className="mr-2 w-4 h-4" />
|
||||||
<span>Light</span>
|
<span>Light</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||||
<Moon className="mr-2 h-4 w-4" />
|
<Moon className="mr-2 w-4 h-4" />
|
||||||
<span>Dark</span>
|
<span>Dark</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||||
<Monitor className="mr-2 h-4 w-4" />
|
<Monitor className="mr-2 w-4 h-4" />
|
||||||
<span>System</span>
|
<span>System</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -63,8 +63,8 @@ export function UserDropdown({ className }: UserDropdownProps) {
|
|||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
|
<Button variant="ghost" className="relative w-8 h-8 rounded-full">
|
||||||
<Avatar className="h-8 w-8">
|
<Avatar className="w-8 h-8">
|
||||||
<AvatarImage src={user.image || undefined} alt={user.name || "User"} />
|
<AvatarImage src={user.image || undefined} alt={user.name || "User"} />
|
||||||
<AvatarFallback>{userInitials}</AvatarFallback>
|
<AvatarFallback>{userInitials}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
@@ -84,13 +84,13 @@ export function UserDropdown({ className }: UserDropdownProps) {
|
|||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link href="/dashboard">
|
<Link href="/dashboard">
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
<Settings className="mr-2 w-4 h-4" />
|
||||||
<span>Dashboard</span>
|
<span>Dashboard</span>
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={handleSignOut}>
|
<DropdownMenuItem onClick={handleSignOut}>
|
||||||
<LogOut className="mr-2 h-4 w-4" />
|
<LogOut className="mr-2 w-4 h-4" />
|
||||||
<span>Log out</span>
|
<span>Log out</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export async function createAdvanceUrl(unsafeData: unknown): Promise<Response> {
|
|||||||
...data,
|
...data,
|
||||||
slug: data.slug ? data.slug.trim() : undefined,
|
slug: data.slug ? data.slug.trim() : undefined,
|
||||||
title: data.title?.length === 0 ? await getWebsiteTitle(data.url) : data.title,
|
title: data.title?.length === 0 ? await getWebsiteTitle(data.url) : data.title,
|
||||||
maxVisits: data.maxVisits ? data.maxVisits : null,
|
maxVisits: data.maxVisits ? data.maxVisits : null
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -4,13 +4,8 @@ import { urls, visits } from "../drizzle/schema"
|
|||||||
|
|
||||||
export async function getDashboardStats() {
|
export async function getDashboardStats() {
|
||||||
try {
|
try {
|
||||||
// Get count of shortened URLs
|
|
||||||
const [urlsCount] = await db.select({ count: count() }).from(urls)
|
const [urlsCount] = await db.select({ count: count() }).from(urls)
|
||||||
|
|
||||||
// Get count of total visits
|
|
||||||
const [visitsCount] = await db.select({ count: count() }).from(visits)
|
const [visitsCount] = await db.select({ count: count() }).from(visits)
|
||||||
|
|
||||||
// Get most visited URL
|
|
||||||
const mostVisitedUrl = await db
|
const mostVisitedUrl = await db
|
||||||
.select({
|
.select({
|
||||||
id: urls.id,
|
id: urls.id,
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import crypto from "node:crypto"
|
import crypto from "node:crypto"
|
||||||
import { env } from "./env/server"
|
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
import { env } from "./env/server"
|
||||||
|
|
||||||
const gravatarSchema = z.object({
|
const gravatarSchema = z.object({
|
||||||
avatar_url: z.string().url(),
|
avatar_url: z.string().url()
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function getGravatar(email: string) {
|
export async function getGravatar(email: string) {
|
||||||
const baseUrl = "https://api.gravatar.com/v3/profiles"
|
const baseUrl = "https://api.gravatar.com/v3/profiles"
|
||||||
const formattedEmail = email.trim().toLowerCase()
|
const formattedEmail = email.trim().toLowerCase()
|
||||||
const hash = crypto.createHash('sha256').update(formattedEmail).digest('hex')
|
const hash = crypto.createHash("sha256").update(formattedEmail).digest("hex")
|
||||||
|
|
||||||
const res = await fetch(`${baseUrl}/${hash}`, {
|
const res = await fetch(`${baseUrl}/${hash}`, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -25,4 +25,4 @@ export async function getGravatar(email: string) {
|
|||||||
if (!success) return null
|
if (!success) return null
|
||||||
|
|
||||||
return data.avatar_url
|
return data.avatar_url
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user