From a9f736ca8905d4ddf0cfbf88786e36d3b880e4c6 Mon Sep 17 00:00:00 2001 From: Nehan Ahmed Date: Wed, 15 Jul 2026 13:27:31 +0500 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20phase=203=20&=204=20=E2=80=94=20URL?= =?UTF-8?q?=20management=20list/detail/create/bulk=20and=20analytics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/dashboard/src/components/copy-button.tsx | 45 ++ .../dashboard/src/components/status-badge.tsx | 44 ++ apps/dashboard/src/components/ui/command.tsx | 114 ++++ apps/dashboard/src/components/ui/dialog.tsx | 60 ++ apps/dashboard/src/components/ui/label.tsx | 18 + .../src/components/ui/pagination.tsx | 88 +++ apps/dashboard/src/components/ui/popover.tsx | 60 ++ apps/dashboard/src/components/ui/select.tsx | 29 + apps/dashboard/src/components/ui/switch.tsx | 47 ++ apps/dashboard/src/components/ui/table.tsx | 77 +++ apps/dashboard/src/components/ui/tabs.tsx | 96 +++ apps/dashboard/src/components/ui/textarea.tsx | 22 + apps/dashboard/src/components/ui/tooltip.tsx | 42 ++ apps/dashboard/src/pages/BulkCreatePage.tsx | 204 +++++- apps/dashboard/src/pages/CreateUrlPage.tsx | 390 ++++++++++- apps/dashboard/src/pages/UrlDetailPage.tsx | 641 +++++++++++++++++- apps/dashboard/src/pages/UrlsListPage.tsx | 502 +++++++++++++- 17 files changed, 2465 insertions(+), 14 deletions(-) create mode 100644 apps/dashboard/src/components/copy-button.tsx create mode 100644 apps/dashboard/src/components/status-badge.tsx create mode 100644 apps/dashboard/src/components/ui/command.tsx create mode 100644 apps/dashboard/src/components/ui/dialog.tsx create mode 100644 apps/dashboard/src/components/ui/label.tsx create mode 100644 apps/dashboard/src/components/ui/pagination.tsx create mode 100644 apps/dashboard/src/components/ui/popover.tsx create mode 100644 apps/dashboard/src/components/ui/select.tsx create mode 100644 apps/dashboard/src/components/ui/switch.tsx create mode 100644 apps/dashboard/src/components/ui/table.tsx create mode 100644 apps/dashboard/src/components/ui/tabs.tsx create mode 100644 apps/dashboard/src/components/ui/textarea.tsx create mode 100644 apps/dashboard/src/components/ui/tooltip.tsx diff --git a/apps/dashboard/src/components/copy-button.tsx b/apps/dashboard/src/components/copy-button.tsx new file mode 100644 index 0000000..880218e --- /dev/null +++ b/apps/dashboard/src/components/copy-button.tsx @@ -0,0 +1,45 @@ +import { useState, useCallback } from "react" +import { Button } from "@/components/ui/button" +import { Check, Copy } from "lucide-react" +import { cn } from "@/lib/utils" + +interface CopyButtonProps { + text: string + className?: string + variant?: "primary" | "outline" | "ghost" + size?: "default" | "sm" | "lg" | "icon" +} + +export default function CopyButton({ text, className, variant = "outline", size = "icon" }: CopyButtonProps) { + const [copied, setCopied] = useState(false) + + const handleCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(text) + setCopied(true) + setTimeout(() => setCopied(false), 1500) + } catch { + const input = document.createElement("input") + input.value = text + document.body.appendChild(input) + input.select() + document.execCommand("copy") + document.body.removeChild(input) + setCopied(true) + setTimeout(() => setCopied(false), 1500) + } + }, [text]) + + return ( + + ) +} diff --git a/apps/dashboard/src/components/status-badge.tsx b/apps/dashboard/src/components/status-badge.tsx new file mode 100644 index 0000000..f33bdb8 --- /dev/null +++ b/apps/dashboard/src/components/status-badge.tsx @@ -0,0 +1,44 @@ +import { Badge } from "@/components/ui/badge" +import { cn } from "@/lib/utils" + +interface StatusBadgeProps { + status: string + className?: string +} + +const statusConfig: Record = { + active: { label: "Active", variant: "success" }, + expired: { label: "Expired", variant: "destructive" }, + scheduled: { label: "Scheduled", variant: "outline" }, + "password-protected": { label: "Password", variant: "default" }, + "blocked-bots": { label: "Bots Blocked", variant: "secondary" }, + inactive: { label: "Inactive", variant: "secondary" }, +} + +export default function StatusBadge({ status, className }: StatusBadgeProps) { + const config = statusConfig[status] ?? { label: status, variant: "outline" as const } + + return ( + + {config.label} + + ) +} + +export function getLinkStatus(link: { expiresAt: string | null; activeAt: string | null; hasPassword: boolean; blockBots: boolean }): string { + const now = new Date() + + if (link.activeAt && new Date(link.activeAt) > now) { + return "scheduled" + } + if (link.expiresAt && new Date(link.expiresAt) < now) { + return "expired" + } + if (link.hasPassword) { + return "password-protected" + } + if (link.blockBots) { + return "blocked-bots" + } + return "active" +} diff --git a/apps/dashboard/src/components/ui/command.tsx b/apps/dashboard/src/components/ui/command.tsx new file mode 100644 index 0000000..fa19473 --- /dev/null +++ b/apps/dashboard/src/components/ui/command.tsx @@ -0,0 +1,114 @@ +import { cn } from "@/lib/utils" +import { useState, useRef, useEffect, type ReactNode } from "react" + +interface CommandProps { + children: ReactNode + className?: string + filter?: (value: string, search: string) => boolean +} + +export function Command({ children, className, filter }: CommandProps) { + const [search, setSearch] = useState("") + const inputRef = useRef(null) + + useEffect(() => { + setTimeout(() => inputRef.current?.focus(), 50) + }, []) + + return ( +
+ {children} +
+ ) +} + +interface CommandInputProps { + placeholder?: string + value?: string + onValueChange?: (value: string) => void + className?: string +} + +export function CommandInput({ placeholder = "Search...", value, onValueChange, className }: CommandInputProps) { + return ( +
+ el?.focus()} + value={value} + onChange={(e) => onValueChange?.(e.target.value)} + placeholder={placeholder} + className={cn( + "flex h-9 w-full rounded-md bg-transparent py-2 text-sm text-foreground outline-none placeholder:text-muted-foreground", + className + )} + /> +
+ ) +} + +interface CommandListProps { + children: ReactNode + className?: string +} + +export function CommandList({ children, className }: CommandListProps) { + return ( +
+ {children} +
+ ) +} + +interface CommandEmptyProps { + children: ReactNode + className?: string +} + +export function CommandEmpty({ children, className }: CommandEmptyProps) { + return ( +
+ {children} +
+ ) +} + +interface CommandItemProps { + children: ReactNode + value?: string + onSelect?: () => void + disabled?: boolean + className?: string +} + +export function CommandItem({ children, value, onSelect, disabled, className }: CommandItemProps) { + return ( + + ) +} + +interface CommandGroupProps { + children: ReactNode + heading?: string + className?: string +} + +export function CommandGroup({ children, heading, className }: CommandGroupProps) { + return ( +
+ {heading && ( +
{heading}
+ )} + {children} +
+ ) +} diff --git a/apps/dashboard/src/components/ui/dialog.tsx b/apps/dashboard/src/components/ui/dialog.tsx new file mode 100644 index 0000000..470934f --- /dev/null +++ b/apps/dashboard/src/components/ui/dialog.tsx @@ -0,0 +1,60 @@ +import { cn } from "@/lib/utils" +import { useEffect, useRef, type ReactNode } from "react" + +interface DialogProps { + open: boolean + onClose: () => void + children: ReactNode +} + +export function Dialog({ open, onClose, children }: DialogProps) { + const overlayRef = useRef(null) + + useEffect(() => { + if (!open) return + const handler = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose() + } + document.addEventListener("keydown", handler) + document.body.style.overflow = "hidden" + return () => { + document.removeEventListener("keydown", handler) + document.body.style.overflow = "" + } + }, [open, onClose]) + + if (!open) return null + + return ( +
{ + if (e.target === overlayRef.current) onClose() + }} + > +
+ {children} +
+
+ ) +} + +export function DialogHeader({ children, className }: { children: ReactNode; className?: string }) { + return
{children}
+} + +export function DialogTitle({ children, className }: { children: ReactNode; className?: string }) { + return

{children}

+} + +export function DialogDescription({ children, className }: { children: ReactNode; className?: string }) { + return

{children}

+} + +export function DialogFooter({ children, className }: { children: ReactNode; className?: string }) { + return
{children}
+} diff --git a/apps/dashboard/src/components/ui/label.tsx b/apps/dashboard/src/components/ui/label.tsx new file mode 100644 index 0000000..16292c3 --- /dev/null +++ b/apps/dashboard/src/components/ui/label.tsx @@ -0,0 +1,18 @@ +import { cn } from "@/lib/utils" +import { forwardRef, type LabelHTMLAttributes } from "react" + +const Label = forwardRef>( + ({ className, ...props }, ref) => ( +