chore: format

This commit is contained in:
2025-10-10 16:43:21 +02:00
parent f0aabd63b6
commit 75c29e0ba4
551 changed files with 433948 additions and 94145 deletions

View File

@@ -1,57 +1,57 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@repo/ui"
import { ReactNode } from "react"
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@repo/ui";
import { ReactNode } from "react";
interface AlertDialogConfirmationProps {
trigger: ReactNode
title: string
description: string
cancelText?: string
confirmText?: string
onConfirm: () => void
variant?: "default" | "destructive"
trigger: ReactNode;
title: string;
description: string;
cancelText?: string;
confirmText?: string;
onConfirm: () => void;
variant?: "default" | "destructive";
}
export function AlertDialogConfirmation({
trigger,
title,
description,
cancelText = "Cancel",
confirmText = "Confirm",
onConfirm,
variant = "default",
trigger,
title,
description,
cancelText = "Cancel",
confirmText = "Confirm",
onConfirm,
variant = "default",
}: AlertDialogConfirmationProps) {
return (
<AlertDialog>
<AlertDialogTrigger asChild>{trigger}</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{cancelText}</AlertDialogCancel>
<AlertDialogAction
className={
variant === "destructive"
? "bg-destructive hover:bg-destructive/90 text-white"
: undefined
}
onClick={onConfirm}
>
{confirmText}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
return (
<AlertDialog>
<AlertDialogTrigger asChild>{trigger}</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{cancelText}</AlertDialogCancel>
<AlertDialogAction
className={
variant === "destructive"
? "bg-destructive hover:bg-destructive/90 text-white"
: undefined
}
onClick={onConfirm}
>
{confirmText}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

View File

@@ -1,8 +1,8 @@
import { Skeleton } from "@repo/ui"
import { Skeleton } from "@repo/ui";
export const CardSkeleton = () => (
<div className="grid gap-2">
<Skeleton className="w-[150px] h-6" />
<Skeleton className="w-[80px] h-4" />
</div>
)
<div className="grid gap-2">
<Skeleton className="w-[150px] h-6" />
<Skeleton className="w-[80px] h-4" />
</div>
);

View File

@@ -1,9 +1,9 @@
import { Loader2 } from "lucide-react"
import { Loader2 } from "lucide-react";
export function CenteredLoader() {
return (
<div className="flex items-center justify-center h-screen w-full">
<Loader2 className="animate-spin h-8 w-8 text-primary" />
</div>
)
return (
<div className="flex items-center justify-center h-screen w-full">
<Loader2 className="animate-spin h-8 w-8 text-primary" />
</div>
);
}

View File

@@ -1,33 +1,33 @@
import { Check, Copy } from "lucide-react"
import { Button } from "@repo/ui"
import { useState } from "react"
import { Check, Copy } from "lucide-react";
import { Button } from "@repo/ui";
import { useState } from "react";
interface CopyButtonProps {
onCopy: () => void | Promise<void>
className?: string
onCopy: () => void | Promise<void>;
className?: string;
}
export function CopyButton({ onCopy, className }: CopyButtonProps) {
const [isCopied, setIsCopied] = useState(false)
const [isCopied, setIsCopied] = useState(false);
const handleCopy = async () => {
await onCopy()
setIsCopied(true)
setTimeout(() => setIsCopied(false), 2000)
}
const handleCopy = async () => {
await onCopy();
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
};
return (
<Button
variant="ghost"
size="icon"
onClick={handleCopy}
className={className}
>
{isCopied ? (
<Check className="h-4 w-4 text-emerald-500" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
)
return (
<Button
variant="ghost"
size="icon"
onClick={handleCopy}
className={className}
>
{isCopied ? (
<Check className="h-4 w-4 text-emerald-500" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
);
}

View File

@@ -1,47 +1,47 @@
import { cn } from "@repo/ui"
import { useEffect, useRef } from "react"
import { cn } from "@repo/ui";
import { useEffect, useRef } from "react";
interface EmailPreviewProps {
content: string
className?: string
content: string;
className?: string;
}
export function EmailPreview({ content, className = "" }: EmailPreviewProps) {
const iframeRef = useRef<HTMLIFrameElement>(null)
const iframeRef = useRef<HTMLIFrameElement>(null);
useEffect(() => {
if (!iframeRef.current) return
useEffect(() => {
if (!iframeRef.current) return;
const doc = iframeRef.current.contentDocument
if (!doc) return
const doc = iframeRef.current.contentDocument;
if (!doc) return;
// Modify the tracking pixel URL for preview
// Regex to match /img/any-id/img.png
const trackingPixelRegex = /\/img\/[^/]+\/img\.png/g
const modifiedContent = content.replace(
trackingPixelRegex,
"#invalid-tracking-pixel-for-preview"
)
// Modify the tracking pixel URL for preview
// Regex to match /img/any-id/img.png
const trackingPixelRegex = /\/img\/[^/]+\/img\.png/g;
const modifiedContent = content.replace(
trackingPixelRegex,
"#invalid-tracking-pixel-for-preview",
);
// Write the content to the iframe
doc.open()
doc.write(modifiedContent)
doc.close()
// Write the content to the iframe
doc.open();
doc.write(modifiedContent);
doc.close();
// Make links open in new tab
const links = doc.getElementsByTagName("a")
for (const link of links) {
link.target = "_blank"
link.rel = "noopener noreferrer"
}
}, [content])
// Make links open in new tab
const links = doc.getElementsByTagName("a");
for (const link of links) {
link.target = "_blank";
link.rel = "noopener noreferrer";
}
}, [content]);
return (
<iframe
ref={iframeRef}
className={cn("w-full scroll-hidden rounded-md bg-white", className)}
sandbox="allow-same-origin"
title="Email Preview"
/>
)
return (
<iframe
ref={iframeRef}
className={cn("w-full scroll-hidden rounded-md bg-white", className)}
sandbox="allow-same-origin"
title="Email Preview"
/>
);
}

View File

@@ -1,54 +1,54 @@
import { Component, ErrorInfo, ReactNode } from "react"
import { Button } from "@repo/ui"
import { AlertCircle } from "lucide-react"
import { Component, ErrorInfo, ReactNode } from "react";
import { Button } from "@repo/ui";
import { AlertCircle } from "lucide-react";
interface Props {
children: ReactNode
children: ReactNode;
}
interface State {
hasError: boolean
error?: Error
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
}
public state: State = {
hasError: false,
};
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error }
}
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error("Uncaught error:", error, errorInfo)
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error("Uncaught error:", error, errorInfo);
}
public render() {
if (this.state.hasError) {
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="text-center p-8 max-w-2xl mx-auto">
<div className="flex justify-center mb-6">
<div className="h-12 w-12 rounded-full bg-destructive/10 flex items-center justify-center">
<AlertCircle className="h-6 w-6 text-destructive" />
</div>
</div>
<h1 className="text-3xl font-bold mb-2">Something went wrong</h1>
<p className="text-muted-foreground mb-6">
An unexpected error has occurred.
</p>
<div className="bg-muted/50 rounded-lg p-4 mb-6">
<pre className="text-sm text-muted-foreground break-words whitespace-pre-wrap">
{this.state.error?.message}
</pre>
</div>
<Button onClick={() => window.location.reload()}>Try Again</Button>
</div>
</div>
)
}
public render() {
if (this.state.hasError) {
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="text-center p-8 max-w-2xl mx-auto">
<div className="flex justify-center mb-6">
<div className="h-12 w-12 rounded-full bg-destructive/10 flex items-center justify-center">
<AlertCircle className="h-6 w-6 text-destructive" />
</div>
</div>
<h1 className="text-3xl font-bold mb-2">Something went wrong</h1>
<p className="text-muted-foreground mb-6">
An unexpected error has occurred.
</p>
<div className="bg-muted/50 rounded-lg p-4 mb-6">
<pre className="text-sm text-muted-foreground break-words whitespace-pre-wrap">
{this.state.error?.message}
</pre>
</div>
<Button onClick={() => window.location.reload()}>Try Again</Button>
</div>
</div>
);
}
return this.props.children
}
return this.props.children;
}
}

View File

@@ -1,48 +1,48 @@
import { Control, FieldPath, FieldValues } from "react-hook-form"
import type { ComponentProps } from "react"
import { Control, FieldPath, FieldValues } from "react-hook-form";
import type { ComponentProps } from "react";
import {
FormField,
FormItem,
FormLabel,
FormControl,
Input,
FormDescription,
FormMessage,
} from "@repo/ui"
FormField,
FormItem,
FormLabel,
FormControl,
Input,
FormDescription,
FormMessage,
} from "@repo/ui";
interface FormControlledInputProps<
TFieldValues extends FieldValues = FieldValues,
TFieldValues extends FieldValues = FieldValues,
> {
control: Control<TFieldValues>
name: FieldPath<TFieldValues>
label: string
description?: string
inputProps?: ComponentProps<typeof Input>
control: Control<TFieldValues>;
name: FieldPath<TFieldValues>;
label: string;
description?: string;
inputProps?: ComponentProps<typeof Input>;
}
export function FormControlledInput<
TFieldValues extends FieldValues = FieldValues,
TFieldValues extends FieldValues = FieldValues,
>({
control,
name,
label,
description,
inputProps,
control,
name,
label,
description,
inputProps,
}: FormControlledInputProps<TFieldValues>) {
return (
<FormField
control={control}
name={name}
render={({ field }) => (
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<Input {...inputProps} {...field} />
</FormControl>
{description && <FormDescription>{description}</FormDescription>}
<FormMessage />
</FormItem>
)}
/>
)
return (
<FormField
control={control}
name={name}
render={({ field }) => (
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<Input {...inputProps} {...field} />
</FormControl>
{description && <FormDescription>{description}</FormDescription>}
<FormMessage />
</FormItem>
)}
/>
);
}

View File

@@ -1,182 +1,182 @@
import { useState } from "react"
import { Upload, FileText, AlertCircle, Check } from "lucide-react"
import { useState } from "react";
import { Upload, FileText, AlertCircle, Check } from "lucide-react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
Button,
Alert,
AlertDescription,
AlertTitle,
} from "@repo/ui"
import { trpc } from "@/trpc"
import { useSession } from "@/hooks"
import { WithTooltip } from "./with-tooltip"
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
Button,
Alert,
AlertDescription,
AlertTitle,
} from "@repo/ui";
import { trpc } from "@/trpc";
import { useSession } from "@/hooks";
import { WithTooltip } from "./with-tooltip";
const VALID_HEADERS = [
"email",
"first_name",
"last_name",
"phone",
"company",
"job_title",
"city",
"country",
"subscribed_at",
"tags",
] as const
"email",
"first_name",
"last_name",
"phone",
"company",
"job_title",
"city",
"country",
"subscribed_at",
"tags",
] as const;
export type ValidHeader = (typeof VALID_HEADERS)[number]
export type ValidHeader = (typeof VALID_HEADERS)[number];
interface ImportSubscribersDialogProps {
onSuccess?: () => void
listId?: string
onSuccess?: () => void;
listId?: string;
}
export function ImportSubscribersDialog({
onSuccess,
listId,
onSuccess,
listId,
}: ImportSubscribersDialogProps) {
const [open, setOpen] = useState(false)
const [file, setFile] = useState<File | null>(null)
const [error, setError] = useState<string | null>(null)
const { organization } = useSession()
const [open, setOpen] = useState(false);
const [file, setFile] = useState<File | null>(null);
const [error, setError] = useState<string | null>(null);
const { organization } = useSession();
const importMutation = trpc.subscriber.import.useMutation({
onSuccess: () => {
setOpen(false)
setFile(null)
setError(null)
onSuccess?.()
},
onError: (error) => {
setError(error.message)
},
})
const importMutation = trpc.subscriber.import.useMutation({
onSuccess: () => {
setOpen(false);
setFile(null);
setError(null);
onSuccess?.();
},
onError: (error) => {
setError(error.message);
},
});
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!file.name.endsWith(".csv")) {
setError("Please upload a CSV file")
return
}
if (!file.name.endsWith(".csv")) {
setError("Please upload a CSV file");
return;
}
setFile(file)
setError(null)
}
setFile(file);
setError(null);
};
const handleImport = async () => {
if (!file || !organization?.id) return
const handleImport = async () => {
if (!file || !organization?.id) return;
const formData = new FormData()
formData.append("file", file)
formData.append("organizationId", organization.id)
if (listId) {
formData.append("listId", listId)
}
const formData = new FormData();
formData.append("file", file);
formData.append("organizationId", organization.id);
if (listId) {
formData.append("listId", listId);
}
importMutation.mutate({
file: formData,
organizationId: organization.id,
listId,
})
}
importMutation.mutate({
file: formData,
organizationId: organization.id,
listId,
});
};
const downloadTemplate = () => {
const headers = VALID_HEADERS.join(",")
const blob = new Blob([headers], { type: "text/csv" })
const url = window.URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = "subscribers_template.csv"
a.click()
window.URL.revokeObjectURL(url)
}
const downloadTemplate = () => {
const headers = VALID_HEADERS.join(",");
const blob = new Blob([headers], { type: "text/csv" });
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "subscribers_template.csv";
a.click();
window.URL.revokeObjectURL(url);
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<WithTooltip content="Import subscribers">
<DialogTrigger asChild>
<Button variant="outline" size="icon">
<Upload className="h-4 w-4" />
</Button>
</DialogTrigger>
</WithTooltip>
<DialogContent>
<DialogHeader>
<DialogTitle>Import Subscribers</DialogTitle>
<DialogDescription>
Upload a CSV file with subscriber data. Download the template to see
the correct format.
</DialogDescription>
</DialogHeader>
return (
<Dialog open={open} onOpenChange={setOpen}>
<WithTooltip content="Import subscribers">
<DialogTrigger asChild>
<Button variant="outline" size="icon">
<Upload className="h-4 w-4" />
</Button>
</DialogTrigger>
</WithTooltip>
<DialogContent>
<DialogHeader>
<DialogTitle>Import Subscribers</DialogTitle>
<DialogDescription>
Upload a CSV file with subscriber data. Download the template to see
the correct format.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<Button variant="outline" onClick={downloadTemplate}>
<FileText className="h-4 w-4 mr-2" />
Download Template
</Button>
<div className="space-y-4">
<Button variant="outline" onClick={downloadTemplate}>
<FileText className="h-4 w-4 mr-2" />
Download Template
</Button>
<div className="grid w-full items-center gap-1.5">
<label
htmlFor="file"
className="border-2 border-dashed rounded-lg p-8 text-center cursor-pointer hover:border-primary/50 transition-colors"
>
{file ? (
<div className="flex items-center justify-center gap-2 text-sm">
<Check className="h-4 w-4 text-emerald-500" />
{file.name}
</div>
) : (
<div className="text-sm text-muted-foreground">
Click to upload or drag and drop
<br />
CSV files only
</div>
)}
<input
id="file"
type="file"
accept=".csv"
onChange={handleFileChange}
className="hidden"
/>
</label>
</div>
<div className="grid w-full items-center gap-1.5">
<label
htmlFor="file"
className="border-2 border-dashed rounded-lg p-8 text-center cursor-pointer hover:border-primary/50 transition-colors"
>
{file ? (
<div className="flex items-center justify-center gap-2 text-sm">
<Check className="h-4 w-4 text-emerald-500" />
{file.name}
</div>
) : (
<div className="text-sm text-muted-foreground">
Click to upload or drag and drop
<br />
CSV files only
</div>
)}
<input
id="file"
type="file"
accept=".csv"
onChange={handleFileChange}
className="hidden"
/>
</label>
</div>
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
onClick={handleImport}
disabled={!file || importMutation.isPending}
>
{importMutation.isPending ? (
<>
<div className="h-4 w-4 mr-2 animate-spin rounded-full border-2 border-current border-t-transparent" />
Importing...
</>
) : (
"Import"
)}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
)
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
onClick={handleImport}
disabled={!file || importMutation.isPending}
>
{importMutation.isPending ? (
<>
<div className="h-4 w-4 mr-2 animate-spin rounded-full border-2 border-current border-t-transparent" />
Importing...
</>
) : (
"Import"
)}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,15 +1,15 @@
export * from "./copy-button"
export * from "./error-boundary"
export * from "./import-subscribers-dialog"
export * from "./with-tooltip"
export * from "./email-preview"
export * from "./theme-toggle"
export * from "./theme-provider"
export * from "./card-skeleton"
export * from "./stat-card"
export * from "./alert-dialog-confirmation"
export * from "./pagination"
export * from "./centered-loader"
export * from "./loader"
export * from "./letter-space-text"
export * from "./form-controlled-input"
export * from "./copy-button";
export * from "./error-boundary";
export * from "./import-subscribers-dialog";
export * from "./with-tooltip";
export * from "./email-preview";
export * from "./theme-toggle";
export * from "./theme-provider";
export * from "./card-skeleton";
export * from "./stat-card";
export * from "./alert-dialog-confirmation";
export * from "./pagination";
export * from "./centered-loader";
export * from "./loader";
export * from "./letter-space-text";
export * from "./form-controlled-input";

View File

@@ -1,20 +1,20 @@
import { cn } from "@repo/ui"
import { cn } from "@repo/ui";
type LetterSpaceTextProps = {
className?: string
as?: React.ElementType
}
className?: string;
as?: React.ElementType;
};
export const LetterSpaceText = ({
className,
as = "p",
className,
as = "p",
}: LetterSpaceTextProps) => {
const Comp = as
const Comp = as;
return (
<Comp className={cn("text-lg font-semibold", className)}>
<span>Letter</span>
<span className="text-brand-primary">Space</span>
</Comp>
)
}
return (
<Comp className={cn("text-lg font-semibold", className)}>
<span>Letter</span>
<span className="text-brand-primary">Space</span>
</Comp>
);
};

View File

@@ -1,24 +1,24 @@
import { Loader2 } from "lucide-react"
import { Loader2 } from "lucide-react";
type LoaderProps = {
size?: number
text?: string
className?: string
height?: string
}
size?: number;
text?: string;
className?: string;
height?: string;
};
export function Loader({
size = 8,
text,
className = "",
height = "h-[300px]",
size = 8,
text,
className = "",
height = "h-[300px]",
}: LoaderProps) {
return (
<div className={`flex ${height} items-center justify-center ${className}`}>
<div className="flex flex-col items-center gap-4">
<Loader2 className={`h-${size} w-${size} animate-spin text-primary`} />
{text && <p className="text-sm text-muted-foreground">{text}</p>}
</div>
</div>
)
return (
<div className={`flex ${height} items-center justify-center ${className}`}>
<div className="flex flex-col items-center gap-4">
<Loader2 className={`h-${size} w-${size} animate-spin text-primary`} />
{text && <p className="text-sm text-muted-foreground">{text}</p>}
</div>
</div>
);
}

View File

@@ -1,113 +1,117 @@
import { Pagination as UIPagination, PaginationContent, Button } from "@repo/ui"
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
import {
Pagination as UIPagination,
PaginationContent,
Button,
} from "@repo/ui";
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
type PaginationProps = {
page: number
totalPages: number
onPageChange: (page: number) => void
hasNextPage: boolean
}
page: number;
totalPages: number;
onPageChange: (page: number) => void;
hasNextPage: boolean;
};
export function Pagination({
page,
totalPages,
onPageChange,
hasNextPage,
page,
totalPages,
onPageChange,
hasNextPage,
}: PaginationProps) {
const totalButtons = 7
const totalButtons = 7;
const renderPageButton = (pageNumber: number) => (
<Button
key={pageNumber}
variant={page === pageNumber ? "default" : "outline"}
size="icon"
onClick={() => onPageChange(pageNumber)}
>
{pageNumber}
</Button>
)
const renderPageButton = (pageNumber: number) => (
<Button
key={pageNumber}
variant={page === pageNumber ? "default" : "outline"}
size="icon"
onClick={() => onPageChange(pageNumber)}
>
{pageNumber}
</Button>
);
const renderEllipsis = (key: string) => (
<Button key={key} variant="outline" size="icon" disabled>
<MoreHorizontal className="h-4 w-4" />
</Button>
)
const renderEllipsis = (key: string) => (
<Button key={key} variant="outline" size="icon" disabled>
<MoreHorizontal className="h-4 w-4" />
</Button>
);
const renderPaginationButtons = () => {
const buttons = []
const renderPaginationButtons = () => {
const buttons = [];
if (totalPages <= totalButtons) {
for (let i = 1; i <= totalPages; i++) {
buttons.push(renderPageButton(i))
}
} else {
buttons.push(renderPageButton(1))
if (totalPages <= totalButtons) {
for (let i = 1; i <= totalPages; i++) {
buttons.push(renderPageButton(i));
}
} else {
buttons.push(renderPageButton(1));
if (page > 3) {
buttons.push(renderEllipsis("start-ellipsis"))
}
if (page > 3) {
buttons.push(renderEllipsis("start-ellipsis"));
}
let start = Math.max(2, page - 1)
let end = Math.min(totalPages - 1, page + 1)
let start = Math.max(2, page - 1);
let end = Math.min(totalPages - 1, page + 1);
if (page <= 3) {
end = Math.min(totalPages - 1, totalButtons - 2)
}
if (page <= 3) {
end = Math.min(totalPages - 1, totalButtons - 2);
}
if (page >= totalPages - 2) {
start = Math.max(2, totalPages - (totalButtons - 2))
}
if (page >= totalPages - 2) {
start = Math.max(2, totalPages - (totalButtons - 2));
}
for (let i = start; i <= end; i++) {
buttons.push(renderPageButton(i))
}
for (let i = start; i <= end; i++) {
buttons.push(renderPageButton(i));
}
if (page < totalPages - 2) {
buttons.push(renderEllipsis("end-ellipsis"))
}
if (page < totalPages - 2) {
buttons.push(renderEllipsis("end-ellipsis"));
}
buttons.push(renderPageButton(totalPages))
}
buttons.push(renderPageButton(totalPages));
}
while (buttons.length < totalButtons) {
buttons.push(
<Button
key={`filler-${buttons.length}`}
variant="outline"
size="icon"
disabled
>
{" "}
</Button>
)
}
while (buttons.length < totalButtons) {
buttons.push(
<Button
key={`filler-${buttons.length}`}
variant="outline"
size="icon"
disabled
>
{" "}
</Button>,
);
}
return buttons
}
return buttons;
};
return (
<UIPagination className="select-none">
<PaginationContent className="flex items-center justify-center space-x-2">
<Button
variant="outline"
size="icon"
onClick={() => onPageChange(Math.max(1, page - 1))}
disabled={page === 1}
>
<ChevronLeft className="h-4 w-4" />
<span className="sr-only">Previous page</span>
</Button>
{renderPaginationButtons()}
<Button
variant="outline"
size="icon"
onClick={() => onPageChange(Math.min(totalPages, page + 1))}
disabled={!hasNextPage}
>
<ChevronRight className="h-4 w-4" />
<span className="sr-only">Next page</span>
</Button>
</PaginationContent>
</UIPagination>
)
return (
<UIPagination className="select-none">
<PaginationContent className="flex items-center justify-center space-x-2">
<Button
variant="outline"
size="icon"
onClick={() => onPageChange(Math.max(1, page - 1))}
disabled={page === 1}
>
<ChevronLeft className="h-4 w-4" />
<span className="sr-only">Previous page</span>
</Button>
{renderPaginationButtons()}
<Button
variant="outline"
size="icon"
onClick={() => onPageChange(Math.min(totalPages, page + 1))}
disabled={!hasNextPage}
>
<ChevronRight className="h-4 w-4" />
<span className="sr-only">Next page</span>
</Button>
</PaginationContent>
</UIPagination>
);
}

View File

@@ -1,68 +1,68 @@
import { TrendingUp } from "lucide-react"
import { TrendingDown } from "lucide-react"
import { Card, CardContent, CardHeader, CardTitle } from "@repo/ui"
import { LucideIcon } from "lucide-react"
import { CardSkeleton } from "./card-skeleton"
import { TrendingUp } from "lucide-react";
import { TrendingDown } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@repo/ui";
import { LucideIcon } from "lucide-react";
import { CardSkeleton } from "./card-skeleton";
export function StatCard({
title,
value,
icon: Icon,
change,
subtitle,
isLoading,
smallTitle,
title,
value,
icon: Icon,
change,
subtitle,
isLoading,
smallTitle,
}: {
title: string
value: string | number
icon: LucideIcon
change?: number
subtitle?: string
isLoading: boolean
smallTitle?: string
title: string;
value: string | number;
icon: LucideIcon;
change?: number;
subtitle?: string;
isLoading: boolean;
smallTitle?: string;
}) {
const showChange = change !== undefined && !isNaN(change)
const showChange = change !== undefined && !isNaN(change);
return (
<Card hoverEffect>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
{title}{" "}
{smallTitle && (
<small className="text-xs text-muted-foreground">
({smallTitle})
</small>
)}
</CardTitle>
<Icon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{isLoading ? (
<CardSkeleton />
) : (
<>
<div className="text-2xl font-bold">{value}</div>
{showChange && (
<div
className={`flex items-center text-sm ${
change >= 0 ? "text-emerald-600" : "text-red-600"
}`}
>
{change >= 0 ? (
<TrendingUp className="mr-1 h-4 w-4" />
) : (
<TrendingDown className="mr-1 h-4 w-4" />
)}
{change >= 0 ? "+" : "-"}
{Math.abs(change).toFixed(1)}% from last month
</div>
)}
{subtitle && (
<p className="text-xs text-muted-foreground">{subtitle}</p>
)}
</>
)}
</CardContent>
</Card>
)
return (
<Card hoverEffect>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
{title}{" "}
{smallTitle && (
<small className="text-xs text-muted-foreground">
({smallTitle})
</small>
)}
</CardTitle>
<Icon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{isLoading ? (
<CardSkeleton />
) : (
<>
<div className="text-2xl font-bold">{value}</div>
{showChange && (
<div
className={`flex items-center text-sm ${
change >= 0 ? "text-emerald-600" : "text-red-600"
}`}
>
{change >= 0 ? (
<TrendingUp className="mr-1 h-4 w-4" />
) : (
<TrendingDown className="mr-1 h-4 w-4" />
)}
{change >= 0 ? "+" : "-"}
{Math.abs(change).toFixed(1)}% from last month
</div>
)}
{subtitle && (
<p className="text-xs text-muted-foreground">{subtitle}</p>
)}
</>
)}
</CardContent>
</Card>
);
}

View File

@@ -1 +1 @@
export * from "./theme-provider"
export * from "./theme-provider";

View File

@@ -1,6 +1,6 @@
import { ThemeProviderState } from "./theme-provider"
import { ThemeProviderState } from "./theme-provider";
export const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
}
theme: "system",
setTheme: () => null,
};

View File

@@ -1,6 +1,6 @@
import { createContext } from "react"
import { ThemeProviderState } from "./theme-provider"
import { initialState } from "./state"
import { createContext } from "react";
import { ThemeProviderState } from "./theme-provider";
import { initialState } from "./state";
export const ThemeProviderContext =
createContext<ThemeProviderState>(initialState)
createContext<ThemeProviderState>(initialState);

View File

@@ -1,58 +1,58 @@
import { useEffect, useState } from "react"
import { ThemeProviderContext } from "./theme-context"
import { useEffect, useState } from "react";
import { ThemeProviderContext } from "./theme-context";
export type Theme = "dark" | "light" | "system"
export type Theme = "dark" | "light" | "system";
export type ThemeProviderProps = {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
}
children: React.ReactNode;
defaultTheme?: Theme;
storageKey?: string;
};
export type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
theme: Theme;
setTheme: (theme: Theme) => void;
};
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "vite-ui-theme",
...props
children,
defaultTheme = "system",
storageKey = "vite-ui-theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
)
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
);
useEffect(() => {
const root = window.document.documentElement
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove("light", "dark")
root.classList.remove("light", "dark");
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light"
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
root.classList.add(systemTheme)
return
}
root.classList.add(systemTheme);
return;
}
root.classList.add(theme)
}, [theme])
root.classList.add(theme);
}, [theme]);
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme)
setTheme(theme)
},
}
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme);
setTheme(theme);
},
};
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
)
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
);
}

View File

@@ -1,39 +1,39 @@
import { Moon, Sun, Laptop } from "lucide-react"
import { Button } from "@repo/ui"
import { useTheme } from "@/hooks"
import { Moon, Sun, Laptop } from "lucide-react";
import { Button } from "@repo/ui";
import { useTheme } from "@/hooks";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@repo/ui"
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@repo/ui";
export function ThemeToggle() {
const { setTheme } = useTheme()
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
<Sun className="mr-2 h-4 w-4" />
<span>Light</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
<Moon className="mr-2 h-4 w-4" />
<span>Dark</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
<Laptop className="mr-2 h-4 w-4" />
<span>System</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
<Sun className="mr-2 h-4 w-4" />
<span>Light</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
<Moon className="mr-2 h-4 w-4" />
<span>Dark</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
<Laptop className="mr-2 h-4 w-4" />
<span>System</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -1,25 +1,25 @@
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@repo/ui"
import { ReactNode } from "react"
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@repo/ui";
import { ReactNode } from "react";
interface WithTooltipProps {
content: string
children: ReactNode
content: string;
children: ReactNode;
}
export function WithTooltip({ content, children }: WithTooltipProps) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent>
<p>{content}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent>
<p>{content}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}