diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index ea15abd..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: CI - -on: - pull_request: - branches: [dev] - push: - branches: [main] - -jobs: - ci: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 22.12.0 - cache: npm - - - name: Install dependencies - run: npm ci - - - name: Lint - run: npm run lint - - - name: Type check - run: npx tsc --noEmit - - - name: Build - run: npm run build - env: - NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} - NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY }} - SUPABASE_SECRET_KEY: ${{ secrets.SUPABASE_SECRET_KEY }} \ No newline at end of file diff --git a/app/admin/clients/[id]/layout.tsx b/app/admin/clients/[id]/layout.tsx new file mode 100644 index 0000000..771e83d --- /dev/null +++ b/app/admin/clients/[id]/layout.tsx @@ -0,0 +1,29 @@ +import { notFound, redirect } from "next/navigation"; +import { createAdminClient } from "@/lib/supabase/admin"; +import { verifySession } from "@/lib/supabase/dal"; + +type Params = Promise<{ id: string }>; + +export default async function ClientDetailLayout({ + children, + params, +}: { + children: React.ReactNode; + params: Params; +}) { + const session = await verifySession(); + if (!session.isAdmin) redirect("/client/dashboard"); + + const { id } = await params; + const supabase = createAdminClient(); + const { data: client } = await supabase + .from("profiles") + .select("id") + .eq("id", id) + .eq("role", "client") + .single(); + + if (!client) notFound(); + + return children; +} diff --git a/app/admin/clients/[id]/page.tsx b/app/admin/clients/[id]/page.tsx new file mode 100644 index 0000000..1de47bb --- /dev/null +++ b/app/admin/clients/[id]/page.tsx @@ -0,0 +1,26 @@ +import { notFound } from "next/navigation"; +import { createAdminClient } from "@/lib/supabase/admin"; + +type Params = Promise<{ id: string }>; + +const ClientDetailPage = async ({ params }: { params: Params }) => { + const { id } = await params; + const supabase = createAdminClient(); + const { data: client } = await supabase + .from("profiles") + .select("id, name, email") + .eq("id", id) + .eq("role", "client") + .single(); + + if (!client) notFound(); + + return ( +
+

{client.name}

+

{client.email}

+
+ ) +} + +export default ClientDetailPage diff --git a/app/admin/clients/page.tsx b/app/admin/clients/page.tsx new file mode 100644 index 0000000..2ecb934 --- /dev/null +++ b/app/admin/clients/page.tsx @@ -0,0 +1,12 @@ +import { verifySession } from "@/lib/supabase/dal" +import ClientListViewPage from "@/modules/admin/ui/views/client-list-view"; +import { redirect } from "next/navigation"; + +const Clients = async () => { + const session = await verifySession(); + if (!session.isAdmin) redirect("/client/dashboard"); + + return +} + +export default Clients \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 8a7c3be..f44301c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { Inter, JetBrains_Mono } from "next/font/google"; +import { ThemeProvider } from "@/components/theme-provider"; import "./globals.css"; const inter = Inter({ @@ -22,8 +23,11 @@ export default function RootLayout({ children }: LayoutProps<"/">) { - {children} + + {children} + ); } diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx new file mode 100644 index 0000000..f401999 --- /dev/null +++ b/components/theme-provider.tsx @@ -0,0 +1,15 @@ +"use client" + +import { ThemeProvider as NextThemesProvider } from "next-themes" + +export function ThemeProvider({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/components/ui/button.tsx b/components/ui/button.tsx index fdc1736..9eaf3f9 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -3,11 +3,11 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "cn" const buttonVariants = cva( - "group/button inline-flex shrink-0 items-center justify-center gap-2 rounded-md border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-[background-color,border-color,color] duration-[var(--duration-fast)] ease-out outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "group/button inline-flex shrink-0 items-center justify-center gap-2 cursor-pointer rounded-md border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-[background-color,border-color,color] duration-[var(--duration-fast)] ease-out outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", { variants: { variant: { - primary: "bg-primary text-primary-foreground hover:bg-accent-600", + primary: "bg-primary text-primary-foreground hover:bg-accent-600 dark:hover:bg-ink-100", secondary: "bg-secondary text-secondary-foreground border-border hover:bg-ink-100 dark:hover:bg-ink-800", outline: diff --git a/components/ui/collapsible.tsx b/components/ui/collapsible.tsx new file mode 100644 index 0000000..488fb33 --- /dev/null +++ b/components/ui/collapsible.tsx @@ -0,0 +1,21 @@ +"use client" + +import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible" + +function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) { + return +} + +function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.Props) { + return ( + + ) +} + +function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Props) { + return ( + + ) +} + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx new file mode 100644 index 0000000..5cadf3f --- /dev/null +++ b/components/ui/dialog.tsx @@ -0,0 +1,160 @@ +"use client" + +import * as React from "react" +import { Dialog as DialogPrimitive } from "@base-ui/react/dialog" +import { cn } from "cn" + +import { Button } from "@/components/ui/button" +import { XIcon } from "lucide-react" + +function Dialog({ ...props }: DialogPrimitive.Root.Props) { + return +} + +function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) { + return +} + +function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) { + return +} + +function DialogClose({ ...props }: DialogPrimitive.Close.Props) { + return +} + +function DialogOverlay({ + className, + ...props +}: DialogPrimitive.Backdrop.Props) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: DialogPrimitive.Popup.Props & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + } + > + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ + className, + showCloseButton = false, + children, + ...props +}: React.ComponentProps<"div"> & { + showCloseButton?: boolean +}) { + return ( +
+ {children} + {showCloseButton && ( + }> + Close + + )} +
+ ) +} + +function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: DialogPrimitive.Description.Props) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..cafe6ba --- /dev/null +++ b/components/ui/dropdown-menu.tsx @@ -0,0 +1,267 @@ +"use client" + +import * as React from "react" +import { Menu as MenuPrimitive } from "@base-ui/react/menu" +import { cn } from "cn" +import { ChevronRightIcon, CheckIcon } from "lucide-react" + +function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) { + return +} + +function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) { + return +} + +function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) { + return +} + +function DropdownMenuContent({ + align = "start", + alignOffset = 0, + side = "bottom", + sideOffset = 4, + className, + ...props +}: MenuPrimitive.Popup.Props & + Pick< + MenuPrimitive.Positioner.Props, + "align" | "alignOffset" | "side" | "sideOffset" + >) { + return ( + + + + + + ) +} + +function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) { + return +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: MenuPrimitive.GroupLabel.Props & { + inset?: boolean +}) { + return ( + + ) +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: MenuPrimitive.Item.Props & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) { + return +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: MenuPrimitive.SubmenuTrigger.Props & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function DropdownMenuSubContent({ + align = "start", + alignOffset = -3, + side = "right", + sideOffset = 0, + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + inset, + ...props +}: MenuPrimitive.CheckboxItem.Props & { + inset?: boolean +}) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) { + return ( + + ) +} + +function DropdownMenuRadioItem({ + className, + children, + inset, + ...props +}: MenuPrimitive.RadioItem.Props & { + inset?: boolean +}) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuSeparator({ + className, + ...props +}: MenuPrimitive.Separator.Props) { + return ( + + ) +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +} diff --git a/components/ui/input-group.tsx b/components/ui/input-group.tsx new file mode 100644 index 0000000..0adaf5f --- /dev/null +++ b/components/ui/input-group.tsx @@ -0,0 +1,158 @@ +"use client" + +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "cn" + +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" + +function InputGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5", + className + )} + {...props} + /> + ) +} + +const inputGroupAddonVariants = cva( + "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4", + { + variants: { + align: { + "inline-start": + "order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]", + "inline-end": + "order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]", + "block-start": + "order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2", + "block-end": + "order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2", + }, + }, + defaultVariants: { + align: "inline-start", + }, + } +) + +function InputGroupAddon({ + className, + align = "inline-start", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
{ + if ((e.target as HTMLElement).closest("button")) { + return + } + e.currentTarget.parentElement?.querySelector("input")?.focus() + }} + {...props} + /> + ) +} + +const inputGroupButtonVariants = cva( + "flex items-center gap-2 text-sm shadow-none", + { + variants: { + size: { + xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5", + sm: "", + "icon-xs": + "size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0", + "icon-sm": "size-8 p-0 has-[>svg]:p-0", + }, + }, + defaultVariants: { + size: "xs", + }, + } +) + +function InputGroupButton({ + className, + type = "button", + variant = "ghost", + size = "xs", + ...props +}: Omit, "size" | "type"> & + VariantProps & { + type?: "button" | "submit" | "reset" + }) { + return ( +