Formatting

This commit is contained in:
2025-06-28 00:15:51 +02:00
parent 0088b8f02a
commit 4ac986745d
22 changed files with 1180 additions and 1245 deletions

View File

@@ -1,16 +1,17 @@
import type { MetadataRoute } from 'next' import type { MetadataRoute } from "next"
export default function manifest(): MetadataRoute.Manifest { export default function manifest(): MetadataRoute.Manifest {
return { return {
name: "Linker - Smart URL Shortener & Link Management", name: "Linker - Smart URL Shortener & Link Management",
short_name: "Linker", short_name: "Linker",
description: "Transform long URLs into short, shareable links. Perfect for social media, emails, and anywhere you need clean, manageable links. Get started for free!", description:
"Transform long URLs into short, shareable links. Perfect for social media, emails, and anywhere you need clean, manageable links. Get started for free!",
start_url: "/", start_url: "/",
display: "standalone", display: "standalone",
icons: [{ icons: [{
src: '/favicon.ico', src: "/favicon.ico",
sizes: 'any', sizes: "any",
type: 'image/x-icon', type: "image/x-icon"
}], }]
} }
} }

View File

@@ -1,5 +1,5 @@
import { getAllUrls } from '@/lib/db/urls' import { getAllUrls } from "@/lib/db/urls"
import type { MetadataRoute } from 'next' import type { MetadataRoute } from "next"
export default async function robots(): Promise<MetadataRoute.Robots> { export default async function robots(): Promise<MetadataRoute.Robots> {
const urls = await getAllUrls() const urls = await getAllUrls()
@@ -8,7 +8,7 @@ export default async function robots(): Promise<MetadataRoute.Robots> {
if (u.crawlable) { if (u.crawlable) {
return `/r/${u.slug}` return `/r/${u.slug}`
} }
}).filter(v => typeof v === 'string') }).filter(v => typeof v === "string")
return { return {
rules: [ rules: [
@@ -17,7 +17,8 @@ export default async function robots(): Promise<MetadataRoute.Robots> {
allow: "/", allow: "/",
disallow: ["/api", "/dasboard", "/sign-in"], disallow: ["/api", "/dasboard", "/sign-in"],
crawlDelay: 1 crawlDelay: 1
}, { },
{
userAgent: "*", userAgent: "*",
disallow: "/r/", disallow: "/r/",
allow: crawlableUrls, allow: crawlableUrls,

View File

@@ -1,210 +1,199 @@
"use client" "use client"
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react"
import * as React from "react" import * as React from "react"
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react"
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker" import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button" import { Button, buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils"
function Calendar({ function Calendar({
className, className,
classNames, classNames,
showOutsideDays = true, showOutsideDays = true,
captionLayout = "label", captionLayout = "label",
buttonVariant = "ghost", buttonVariant = "ghost",
formatters, formatters,
components, components,
...props ...props
}: React.ComponentProps<typeof DayPicker> & { }: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"] buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) { }) {
const defaultClassNames = getDefaultClassNames() const defaultClassNames = getDefaultClassNames()
return ( return (
<DayPicker <DayPicker
showOutsideDays={showOutsideDays} showOutsideDays={showOutsideDays}
className={cn( className={cn(
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent", "bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`, String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className className
)} )}
captionLayout={captionLayout} captionLayout={captionLayout}
formatters={{ formatters={{
formatMonthDropdown: (date) => formatMonthDropdown: (date) => date.toLocaleString("default", { month: "short" }),
date.toLocaleString("default", { month: "short" }), ...formatters
...formatters, }}
}} classNames={{
classNames={{ root: cn("w-fit", defaultClassNames.root),
root: cn("w-fit", defaultClassNames.root), months: cn(
months: cn( "flex gap-4 flex-col md:flex-row relative",
"flex gap-4 flex-col md:flex-row relative", defaultClassNames.months
defaultClassNames.months ),
), month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
month: cn("flex flex-col w-full gap-4", defaultClassNames.month), nav: cn(
nav: cn( "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between", defaultClassNames.nav
defaultClassNames.nav ),
), button_previous: cn(
button_previous: cn( buttonVariants({ variant: buttonVariant }),
buttonVariants({ variant: buttonVariant }), "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", defaultClassNames.button_previous
defaultClassNames.button_previous ),
), button_next: cn(
button_next: cn( buttonVariants({ variant: buttonVariant }),
buttonVariants({ variant: buttonVariant }), "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", defaultClassNames.button_next
defaultClassNames.button_next ),
), month_caption: cn(
month_caption: cn( "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)", defaultClassNames.month_caption
defaultClassNames.month_caption ),
), dropdowns: cn(
dropdowns: cn( "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5", defaultClassNames.dropdowns
defaultClassNames.dropdowns ),
), dropdown_root: cn(
dropdown_root: cn( "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md", defaultClassNames.dropdown_root
defaultClassNames.dropdown_root ),
), dropdown: cn("absolute inset-0 opacity-0", defaultClassNames.dropdown),
dropdown: cn("absolute inset-0 opacity-0", defaultClassNames.dropdown), caption_label: cn(
caption_label: cn( "select-none font-medium",
"select-none font-medium", captionLayout === "label"
captionLayout === "label" ? "text-sm"
? "text-sm" : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5", defaultClassNames.caption_label
defaultClassNames.caption_label ),
), table: "w-full border-collapse",
table: "w-full border-collapse", weekdays: cn("flex", defaultClassNames.weekdays),
weekdays: cn("flex", defaultClassNames.weekdays), weekday: cn(
weekday: cn( "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none", defaultClassNames.weekday
defaultClassNames.weekday ),
), week: cn("flex w-full mt-2", defaultClassNames.week),
week: cn("flex w-full mt-2", defaultClassNames.week), week_number_header: cn(
week_number_header: cn( "select-none w-(--cell-size)",
"select-none w-(--cell-size)", defaultClassNames.week_number_header
defaultClassNames.week_number_header ),
), week_number: cn(
week_number: cn( "text-[0.8rem] select-none text-muted-foreground",
"text-[0.8rem] select-none text-muted-foreground", defaultClassNames.week_number
defaultClassNames.week_number ),
), day: cn(
day: cn( "relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none", defaultClassNames.day
defaultClassNames.day ),
), range_start: cn(
range_start: cn( "rounded-l-md bg-accent",
"rounded-l-md bg-accent", defaultClassNames.range_start
defaultClassNames.range_start ),
), range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_middle: cn("rounded-none", defaultClassNames.range_middle), range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end), today: cn(
today: cn( "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none", defaultClassNames.today
defaultClassNames.today ),
), outside: cn(
outside: cn( "text-muted-foreground aria-selected:text-muted-foreground",
"text-muted-foreground aria-selected:text-muted-foreground", defaultClassNames.outside
defaultClassNames.outside ),
), disabled: cn(
disabled: cn( "text-muted-foreground opacity-50",
"text-muted-foreground opacity-50", defaultClassNames.disabled
defaultClassNames.disabled ),
), hidden: cn("invisible", defaultClassNames.hidden),
hidden: cn("invisible", defaultClassNames.hidden), ...classNames
...classNames, }}
}} components={{
components={{ Root: ({ className, rootRef, ...props }) => {
Root: ({ className, rootRef, ...props }) => { return (
return ( <div
<div data-slot="calendar"
data-slot="calendar" ref={rootRef}
ref={rootRef} className={cn(className)}
className={cn(className)} {...props}
{...props} />
/> )
) },
}, Chevron: ({ className, orientation, ...props }) => {
Chevron: ({ className, orientation, ...props }) => { if (orientation === "left") {
if (orientation === "left") { return <ChevronLeftIcon className={cn("size-4", className)} {...props} />
return ( }
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
)
}
if (orientation === "right") { if (orientation === "right") {
return ( return (
<ChevronRightIcon <ChevronRightIcon
className={cn("size-4", className)} className={cn("size-4", className)}
{...props} {...props}
/> />
) )
} }
return ( return <ChevronDownIcon className={cn("size-4", className)} {...props} />
<ChevronDownIcon className={cn("size-4", className)} {...props} /> },
) DayButton: CalendarDayButton,
}, WeekNumber: ({ children, ...props }) => {
DayButton: CalendarDayButton, return (
WeekNumber: ({ children, ...props }) => { <td {...props}>
return ( <div className="flex size-(--cell-size) items-center justify-center text-center">
<td {...props}> {children}
<div className="flex size-(--cell-size) items-center justify-center text-center"> </div>
{children} </td>
</div> )
</td> },
) ...components
}, }}
...components, {...props}
}} />
{...props} )
/>
)
} }
function CalendarDayButton({ function CalendarDayButton({
className, className,
day, day,
modifiers, modifiers,
...props ...props
}: React.ComponentProps<typeof DayButton>) { }: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames() const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null) const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => { React.useEffect(() => {
if (modifiers.focused) ref.current?.focus() if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused]) }, [modifiers.focused])
return ( return (
<Button <Button
ref={ref} ref={ref}
variant="ghost" variant="ghost"
size="icon" size="icon"
data-day={day.date.toLocaleDateString()} data-day={day.date.toLocaleDateString()}
data-selected-single={ data-selected-single={modifiers.selected &&
modifiers.selected && !modifiers.range_start &&
!modifiers.range_start && !modifiers.range_end &&
!modifiers.range_end && !modifiers.range_middle}
!modifiers.range_middle data-range-start={modifiers.range_start}
} data-range-end={modifiers.range_end}
data-range-start={modifiers.range_start} data-range-middle={modifiers.range_middle}
data-range-end={modifiers.range_end} className={cn(
data-range-middle={modifiers.range_middle} "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
className={cn( defaultClassNames.day,
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70", className
defaultClassNames.day, )}
className {...props}
)} />
{...props} )
/>
)
} }
export { Calendar, CalendarDayButton } export { Calendar, CalendarDayButton }

View File

@@ -1,167 +1,148 @@
"use client" "use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label" import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot"
import { import * as React from "react"
Controller, import { Controller, type ControllerProps, type FieldPath, type FieldValues, FormProvider, useFormContext, useFormState } from "react-hook-form"
FormProvider,
useFormContext,
useFormState,
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { cn } from "@/lib/utils"
const Form = FormProvider const Form = FormProvider
type FormFieldContextValue< type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = { > = {
name: TName name: TName
} }
const FormFieldContext = React.createContext<FormFieldContextValue>( const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue {} as FormFieldContextValue
) )
const FormField = < const FormField = <
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({ >({
...props ...props
}: ControllerProps<TFieldValues, TName>) => { }: ControllerProps<TFieldValues, TName>) => {
return ( return (
<FormFieldContext.Provider value={{ name: props.name }}> <FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} /> <Controller {...props} />
</FormFieldContext.Provider> </FormFieldContext.Provider>
) )
} }
const useFormField = () => { const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext) const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext) const itemContext = React.useContext(FormItemContext)
const { getFieldState } = useFormContext() const { getFieldState } = useFormContext()
const formState = useFormState({ name: fieldContext.name }) const formState = useFormState({ name: fieldContext.name })
const fieldState = getFieldState(fieldContext.name, formState) const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) { if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>") throw new Error("useFormField should be used within <FormField>")
} }
const { id } = itemContext const { id } = itemContext
return { return {
id, id,
name: fieldContext.name, name: fieldContext.name,
formItemId: `${id}-form-item`, formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`, formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`, formMessageId: `${id}-form-item-message`,
...fieldState, ...fieldState
} }
} }
type FormItemContextValue = { type FormItemContextValue = {
id: string id: string
} }
const FormItemContext = React.createContext<FormItemContextValue>( const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue {} as FormItemContextValue
) )
function FormItem({ className, ...props }: React.ComponentProps<"div">) { function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId() const id = React.useId()
return ( return (
<FormItemContext.Provider value={{ id }}> <FormItemContext.Provider value={{ id }}>
<div <div
data-slot="form-item" data-slot="form-item"
className={cn("grid gap-2", className)} className={cn("grid gap-2", className)}
{...props} {...props}
/> />
</FormItemContext.Provider> </FormItemContext.Provider>
) )
} }
function FormLabel({ function FormLabel({
className, className,
...props ...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) { }: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField() const { error, formItemId } = useFormField()
return ( return (
<Label <Label
data-slot="form-label" data-slot="form-label"
data-error={!!error} data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)} className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId} htmlFor={formItemId}
{...props} {...props}
/> />
) )
} }
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) { function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField() const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return ( return (
<Slot <Slot
data-slot="form-control" data-slot="form-control"
id={formItemId} id={formItemId}
aria-describedby={ aria-describedby={!error
!error ? `${formDescriptionId}`
? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
: `${formDescriptionId} ${formMessageId}` aria-invalid={!!error}
} {...props}
aria-invalid={!!error} />
{...props} )
/>
)
} }
function FormDescription({ className, ...props }: React.ComponentProps<"p">) { function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField() const { formDescriptionId } = useFormField()
return ( return (
<p <p
data-slot="form-description" data-slot="form-description"
id={formDescriptionId} id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm", className)}
{...props} {...props}
/> />
) )
} }
function FormMessage({ className, ...props }: React.ComponentProps<"p">) { function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField() const { error, formMessageId } = useFormField()
const body = error ? String(error?.message ?? "") : props.children const body = error ? String(error?.message ?? "") : props.children
if (!body) { if (!body) {
return null return null
} }
return ( return (
<p <p
data-slot="form-message" data-slot="form-message"
id={formMessageId} id={formMessageId}
className={cn("text-destructive text-sm", className)} className={cn("text-destructive text-sm", className)}
{...props} {...props}
> >
{body} {body}
</p> </p>
) )
} }
export { export { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useFormField }
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

@@ -1,48 +1,48 @@
"use client" "use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover" import * as PopoverPrimitive from "@radix-ui/react-popover"
import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
function Popover({ function Popover({
...props ...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) { }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} /> return <PopoverPrimitive.Root data-slot="popover" {...props} />
} }
function PopoverTrigger({ function PopoverTrigger({
...props ...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) { }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} /> return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
} }
function PopoverContent({ function PopoverContent({
className, className,
align = "center", align = "center",
sideOffset = 4, sideOffset = 4,
...props ...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) { }: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return ( return (
<PopoverPrimitive.Portal> <PopoverPrimitive.Portal>
<PopoverPrimitive.Content <PopoverPrimitive.Content
data-slot="popover-content" data-slot="popover-content"
align={align} align={align}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=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 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden", "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=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 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className className
)} )}
{...props} {...props}
/> />
</PopoverPrimitive.Portal> </PopoverPrimitive.Portal>
) )
} }
function PopoverAnchor({ function PopoverAnchor({
...props ...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) { }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} /> return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
} }
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger }

View File

@@ -1,28 +1,28 @@
"use client" "use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator" import * as SeparatorPrimitive from "@radix-ui/react-separator"
import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
function Separator({ function Separator({
className, className,
orientation = "horizontal", orientation = "horizontal",
decorative = true, decorative = true,
...props ...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) { }: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return ( return (
<SeparatorPrimitive.Root <SeparatorPrimitive.Root
data-slot="separator" data-slot="separator"
decorative={decorative} decorative={decorative}
orientation={orientation} orientation={orientation}
className={cn( className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className className
)} )}
{...props} {...props}
/> />
) )
} }
export { Separator } export { Separator }

View File

@@ -1,139 +1,130 @@
"use client" "use client"
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog" import * as SheetPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react" import { XIcon } from "lucide-react"
import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) { function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} /> return <SheetPrimitive.Root data-slot="sheet" {...props} />
} }
function SheetTrigger({ function SheetTrigger({
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) { }: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} /> return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
} }
function SheetClose({ function SheetClose({
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) { }: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} /> return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
} }
function SheetPortal({ function SheetPortal({
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) { }: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} /> return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
} }
function SheetOverlay({ function SheetOverlay({
className, className,
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) { }: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return ( return (
<SheetPrimitive.Overlay <SheetPrimitive.Overlay
data-slot="sheet-overlay" data-slot="sheet-overlay"
className={cn( className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50", "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className className
)} )}
{...props} {...props}
/> />
) )
} }
function SheetContent({ function SheetContent({
className, className,
children, children,
side = "right", side = "right",
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & { }: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left" side?: "top" | "right" | "bottom" | "left"
}) { }) {
return ( return (
<SheetPortal> <SheetPortal>
<SheetOverlay /> <SheetOverlay />
<SheetPrimitive.Content <SheetPrimitive.Content
data-slot="sheet-content" data-slot="sheet-content"
className={cn( className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500", "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" && side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm", "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" && side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm", "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" && side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b", "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" && side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t", "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className className
)} )}
{...props} {...props}
> >
{children} {children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"> <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" /> <XIcon className="size-4" />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</SheetPrimitive.Close> </SheetPrimitive.Close>
</SheetPrimitive.Content> </SheetPrimitive.Content>
</SheetPortal> </SheetPortal>
) )
} }
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="sheet-header" data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)} className={cn("flex flex-col gap-1.5 p-4", className)}
{...props} {...props}
/> />
) )
} }
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="sheet-footer" data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)} className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props} {...props}
/> />
) )
} }
function SheetTitle({ function SheetTitle({
className, className,
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) { }: React.ComponentProps<typeof SheetPrimitive.Title>) {
return ( return (
<SheetPrimitive.Title <SheetPrimitive.Title
data-slot="sheet-title" data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)} className={cn("text-foreground font-semibold", className)}
{...props} {...props}
/> />
) )
} }
function SheetDescription({ function SheetDescription({
className, className,
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) { }: React.ComponentProps<typeof SheetPrimitive.Description>) {
return ( return (
<SheetPrimitive.Description <SheetPrimitive.Description
data-slot="sheet-description" data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm", className)}
{...props} {...props}
/> />
) )
} }
export { export { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger }
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
function Skeleton({ className, ...props }: React.ComponentProps<"div">) { function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="skeleton" data-slot="skeleton"
className={cn("bg-accent animate-pulse rounded-md", className)} className={cn("bg-accent animate-pulse rounded-md", className)}
{...props} {...props}
/> />
) )
} }
export { Skeleton } export { Skeleton }

View File

@@ -4,22 +4,20 @@ import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner" import { Toaster as Sonner, ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => { const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme() const { theme = "system" } = useTheme()
return ( return (
<Sonner <Sonner
theme={theme as ToasterProps["theme"]} theme={theme as ToasterProps["theme"]}
className="toaster group" className="toaster group"
style={ style={{
{ "--normal-bg": "var(--popover)",
"--normal-bg": "var(--popover)", "--normal-text": "var(--popover-foreground)",
"--normal-text": "var(--popover-foreground)", "--normal-border": "var(--border)"
"--normal-border": "var(--border)", } as React.CSSProperties}
} as React.CSSProperties {...props}
} />
{...props} )
/>
)
} }
export { Toaster } export { Toaster }

View File

@@ -1,31 +1,31 @@
"use client" "use client"
import * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch" import * as SwitchPrimitive from "@radix-ui/react-switch"
import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
function Switch({ function Switch({
className, className,
...props ...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) { }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return ( return (
<SwitchPrimitive.Root <SwitchPrimitive.Root
data-slot="switch" data-slot="switch"
className={cn( className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", "peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className className
)} )}
{...props} {...props}
> >
<SwitchPrimitive.Thumb <SwitchPrimitive.Thumb
data-slot="switch-thumb" data-slot="switch-thumb"
className={cn( className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0" "bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)} )}
/> />
</SwitchPrimitive.Root> </SwitchPrimitive.Root>
) )
} }
export { Switch } export { Switch }

View File

@@ -5,112 +5,103 @@ import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) { function Table({ className, ...props }: React.ComponentProps<"table">) {
return ( return (
<div <div
data-slot="table-container" data-slot="table-container"
className="relative w-full overflow-x-auto" className="relative w-full overflow-x-auto"
> >
<table <table
data-slot="table" data-slot="table"
className={cn("w-full caption-bottom text-sm", className)} className={cn("w-full caption-bottom text-sm", className)}
{...props} {...props}
/> />
</div> </div>
) )
} }
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return ( return (
<thead <thead
data-slot="table-header" data-slot="table-header"
className={cn("[&_tr]:border-b", className)} className={cn("[&_tr]:border-b", className)}
{...props} {...props}
/> />
) )
} }
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return ( return (
<tbody <tbody
data-slot="table-body" data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)} className={cn("[&_tr:last-child]:border-0", className)}
{...props} {...props}
/> />
) )
} }
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return ( return (
<tfoot <tfoot
data-slot="table-footer" data-slot="table-footer"
className={cn( className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", "bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className className
)} )}
{...props} {...props}
/> />
) )
} }
function TableRow({ className, ...props }: React.ComponentProps<"tr">) { function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return ( return (
<tr <tr
data-slot="table-row" data-slot="table-row"
className={cn( className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", "hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className className
)} )}
{...props} {...props}
/> />
) )
} }
function TableHead({ className, ...props }: React.ComponentProps<"th">) { function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return ( return (
<th <th
data-slot="table-head" data-slot="table-head"
className={cn( className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", "text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className className
)} )}
{...props} {...props}
/> />
) )
} }
function TableCell({ className, ...props }: React.ComponentProps<"td">) { function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return ( return (
<td <td
data-slot="table-cell" data-slot="table-cell"
className={cn( className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className className
)} )}
{...props} {...props}
/> />
) )
} }
function TableCaption({ function TableCaption({
className, className,
...props ...props
}: React.ComponentProps<"caption">) { }: React.ComponentProps<"caption">) {
return ( return (
<caption <caption
data-slot="table-caption" data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)} className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props} {...props}
/> />
) )
} }
export { export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow }
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@@ -1,61 +1,61 @@
"use client" "use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip" import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
function TooltipProvider({ function TooltipProvider({
delayDuration = 0, delayDuration = 0,
...props ...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) { }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return ( return (
<TooltipPrimitive.Provider <TooltipPrimitive.Provider
data-slot="tooltip-provider" data-slot="tooltip-provider"
delayDuration={delayDuration} delayDuration={delayDuration}
{...props} {...props}
/> />
) )
} }
function Tooltip({ function Tooltip({
...props ...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) { }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return ( return (
<TooltipProvider> <TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} /> <TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider> </TooltipProvider>
) )
} }
function TooltipTrigger({ function TooltipTrigger({
...props ...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) { }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} /> return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
} }
function TooltipContent({ function TooltipContent({
className, className,
sideOffset = 0, sideOffset = 0,
children, children,
...props ...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) { }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return ( return (
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipPrimitive.Content <TooltipPrimitive.Content
data-slot="tooltip-content" data-slot="tooltip-content"
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance", "bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className className
)} )}
{...props} {...props}
> >
{children} {children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" /> <TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content> </TooltipPrimitive.Content>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
) )
} }
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }

View File

@@ -3,17 +3,17 @@ import * as React from "react"
const MOBILE_BREAKPOINT = 768 const MOBILE_BREAKPOINT = 768
export function useIsMobile() { export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined) const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => { React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => { const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
} }
mql.addEventListener("change", onChange) mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange) return () => mql.removeEventListener("change", onChange)
}, []) }, [])
return !!isMobile return !!isMobile
} }

View File

@@ -1,10 +1,10 @@
"use server" "use server"
import { revalidatePath } from "next/cache"
import { getSession } from "../auth/session" import { getSession } from "../auth/session"
import { insertUrl } from "../db/urls" import { insertUrl } from "../db/urls"
import { advancedUrlSchema, editUrlSchema, urlFormSchema } from "../schema/url"
import { deleteUrl as deleteUrlDb, updateUrl as updateUrlDb } from "../db/urls" import { deleteUrl as deleteUrlDb, updateUrl as updateUrlDb } from "../db/urls"
import { revalidatePath } from "next/cache" import { advancedUrlSchema, editUrlSchema, urlFormSchema } from "../schema/url"
import { getWebsiteTitle } from "../websiteTitle" import { getWebsiteTitle } from "../websiteTitle"
type Response = { type Response = {
@@ -68,7 +68,7 @@ export async function createAdvanceUrl(unsafeData: unknown): Promise<Response> {
...data, ...data,
slug: data.slug?.length === 0 ? undefined : data.slug, slug: data.slug?.length === 0 ? undefined : data.slug,
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 > 0 ? data.maxVisits : undefined, maxVisits: data.maxVisits > 0 ? data.maxVisits : undefined
}) })
return { return {

View File

@@ -1,9 +1,9 @@
import { betterAuth } from "better-auth" import { betterAuth } from "better-auth"
import { drizzleAdapter } from "better-auth/adapters/drizzle" import { drizzleAdapter } from "better-auth/adapters/drizzle"
import { nextCookies } from "better-auth/next-js"
import { genericOAuth } from "better-auth/plugins" import { genericOAuth } from "better-auth/plugins"
import { db } from "../drizzle/db" import { db } from "../drizzle/db"
import { env } from "../env/server" import { env } from "../env/server"
import { nextCookies } from "better-auth/next-js"
export const auth = betterAuth({ export const auth = betterAuth({
database: drizzleAdapter(db, { database: drizzleAdapter(db, {
@@ -18,6 +18,6 @@ export const auth = betterAuth({
clientSecret: env.AUTHENTIK_CLIENT_SECRET, clientSecret: env.AUTHENTIK_CLIENT_SECRET,
discoveryUrl: env.AUTHENTIK_DISCOVERY_URL discoveryUrl: env.AUTHENTIK_DISCOVERY_URL
}] }]
}), })
] ]
}) })

View File

@@ -1,6 +1,6 @@
import { headers } from "next/headers" import { headers } from "next/headers"
import { auth } from "./auth"
import { redirect } from "next/navigation" import { redirect } from "next/navigation"
import { auth } from "./auth"
export async function getSession() { export async function getSession() {
const session = await auth.api.getSession({ const session = await auth.api.getSession({

View File

@@ -1,8 +1,8 @@
import { eq, desc, count } from "drizzle-orm"; import { count, desc, eq } from "drizzle-orm"
import { db } from "../drizzle/db"; import { revalidateTag } from "next/cache"
import { urls, visits } from "../drizzle/schema"; import { cacheTag } from "next/dist/server/use-cache/cache-tag"
import { cacheTag } from "next/dist/server/use-cache/cache-tag"; import { db } from "../drizzle/db"
import { revalidateTag } from "next/cache"; import { urls, visits } from "../drizzle/schema"
export async function getAllUrls() { export async function getAllUrls() {
"use cache" "use cache"

View File

@@ -1,10 +1,10 @@
export function generateRandomSlug(): string { export function generateRandomSlug(): string {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
let result = ''; let result = ""
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length)); result += characters.charAt(Math.floor(Math.random() * characters.length))
} }
return result; return result
} }

View File

@@ -1,4 +1,4 @@
import { z } from "zod"; import { z } from "zod"
export const urlFormSchema = z.object({ export const urlFormSchema = z.object({
url: z.string().url("Please enter a valid URL"), url: z.string().url("Please enter a valid URL"),
@@ -12,7 +12,7 @@ export const advancedUrlSchema = z.object({
maxVisits: z.number().int().positive().nullable(), maxVisits: z.number().int().positive().nullable(),
expDate: z.date().optional(), expDate: z.date().optional(),
forwardQueryParams: z.boolean(), forwardQueryParams: z.boolean(),
crawlable: z.boolean(), crawlable: z.boolean()
}) })
export const editUrlSchema = z.object({ export const editUrlSchema = z.object({
@@ -22,5 +22,5 @@ export const editUrlSchema = z.object({
maxVisits: z.number().int().positive().nullable(), maxVisits: z.number().int().positive().nullable(),
expDate: z.date().nullable(), expDate: z.date().nullable(),
forwardQueryParams: z.boolean(), forwardQueryParams: z.boolean(),
crawlable: z.boolean(), crawlable: z.boolean()
}) })

View File

@@ -1,12 +1,12 @@
export async function getWebsiteTitle(url: string): Promise<string | null> { export async function getWebsiteTitle(url: string): Promise<string | null> {
const res = await fetch(url, { redirect: 'follow' }) const res = await fetch(url, { redirect: "follow" })
if (!res.ok) { if (!res.ok) {
return null return null
} }
const contentType = res.headers.get('content-type') const contentType = res.headers.get("content-type")
if (!contentType || !contentType.includes('text/html')) { if (!contentType || !contentType.includes("text/html")) {
return null return null
} }
@@ -16,12 +16,12 @@ export async function getWebsiteTitle(url: string): Promise<string | null> {
if (titleMatch && titleMatch[1]) { if (titleMatch && titleMatch[1]) {
const title = titleMatch[1] const title = titleMatch[1]
.replace(/&lt;/g, '<') .replace(/&lt;/g, "<")
.replace(/&gt;/g, '>') .replace(/&gt;/g, ">")
.replace(/&amp;/g, '&') .replace(/&amp;/g, "&")
.replace(/&quot;/g, '"') .replace(/&quot;/g, "\"")
.replace(/&#39;/g, "'") .replace(/&#39;/g, "'")
.replace(/&nbsp;/g, ' ') .replace(/&nbsp;/g, " ")
.trim() .trim()
return title || null return title || null

View File

@@ -1,13 +1,13 @@
import { NextRequest, NextResponse } from "next/server"; import { getSessionCookie } from "better-auth/cookies"
import { getSessionCookie } from "better-auth/cookies"; import { NextRequest, NextResponse } from "next/server"
export async function middleware(request: NextRequest) { export async function middleware(request: NextRequest) {
const sessionCookie = getSessionCookie(request); const sessionCookie = getSessionCookie(request)
if (!sessionCookie) { if (!sessionCookie) {
return NextResponse.redirect(new URL("/", request.url)); return NextResponse.redirect(new URL("/", request.url))
} }
return NextResponse.next(); return NextResponse.next()
} }
export const config = { matcher: ["/dashboard"] }; export const config = { matcher: ["/dashboard"] }