commit cf53b734851a7035475589d636870057bf6b9dee Author: Rama Priyanto Date: Thu Apr 16 06:38:26 2026 +0700 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e6ce5a4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# env files +.env*.local + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..461b008 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +dist/ +node_modules/ +.next/ +.turbo/ +coverage/ +pnpm-lock.yaml +.pnpm-store/ \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..a8a2054 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "endOfLine": "lf", + "semi": false, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 80, + "plugins": ["prettier-plugin-tailwindcss"], + "tailwindStylesheet": "app/globals.css", + "tailwindFunctions": ["cn", "cva"] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..1e66186 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Next.js template + +This is a Next.js template with shadcn/ui. + +## Adding components + +To add components to your app, run the following command: + +```bash +npx shadcn@latest add button +``` + +This will place the ui components in the `components` directory. + +## Using components + +To use the components in your app, import them as follows: + +```tsx +import { Button } from "@/components/ui/button"; +``` diff --git a/app/dashboard/account-management/page.tsx b/app/dashboard/account-management/page.tsx new file mode 100644 index 0000000..ef0f4d4 --- /dev/null +++ b/app/dashboard/account-management/page.tsx @@ -0,0 +1,16 @@ +"use client" +import AccountManagementTable from "@/components/table/account-management/account-management-table" +import dynamic from "next/dynamic" + +const Navbar = dynamic(() => import("@/components/ui/navbar"), { + ssr: false, +}) + +export default function AccountManagement() { + return ( +
+ + +
+ ) +} diff --git a/app/dashboard/layout.tsx b/app/dashboard/layout.tsx new file mode 100644 index 0000000..c817385 --- /dev/null +++ b/app/dashboard/layout.tsx @@ -0,0 +1,20 @@ +import { AppSidebar } from "@/components/ui/app-sidebar" +import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar" + +export default function DashboardLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + +
+ + +
+ {children} +
+
+
+ ) +} diff --git a/app/dashboard/upload-account-data/page.tsx b/app/dashboard/upload-account-data/page.tsx new file mode 100644 index 0000000..6cfdb0b --- /dev/null +++ b/app/dashboard/upload-account-data/page.tsx @@ -0,0 +1,16 @@ +"use client" +import UploadCsvCard from "@/components/form/upload-accounts-data/upload-accounts-data-file" +import dynamic from "next/dynamic" + +const Navbar = dynamic(() => import("@/components/ui/navbar"), { + ssr: false, +}) + +export default function UploadAccountData() { + return ( +
+ + +
+ ) +} diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000..cd18e0c Binary files /dev/null and b/app/favicon.ico differ diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..b5c6873 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,129 @@ +@import "tailwindcss"; +@import "tw-animate-css"; +@import "shadcn/tailwind.css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --font-heading: var(--font-sans); + --font-sans: var(--font-sans); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --color-foreground: var(--foreground); + --color-background: var(--background); + --radius-sm: calc(var(--radius) * 0.6); + --radius-md: calc(var(--radius) * 0.8); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) * 1.4); + --radius-2xl: calc(var(--radius) * 1.8); + --radius-3xl: calc(var(--radius) * 2.2); + --radius-4xl: calc(var(--radius) * 2.6); +} + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } + html { + @apply font-sans; + } +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..562e01c --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,35 @@ +import { Geist, Geist_Mono, Inter } from "next/font/google" + +import "./globals.css" +import { ThemeProvider } from "@/components/theme-provider" +import { cn } from "@/lib/utils" + +const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }) + +const fontMono = Geist_Mono({ + subsets: ["latin"], + variable: "--font-mono", +}) + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + {children} + + + ) +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..75a8291 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,136 @@ +"use client" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { + InputGroup, + InputGroupAddon, + InputGroupInput, +} from "@/components/ui/input-group" +import { setCookiesEncrypt } from "@/utils/globals" +import Image from "next/image" +import { useRouter } from "next/navigation" +import { useState } from "react" +import { useForm, Controller } from "react-hook-form" +import "./globals.css" +import { EyeIcon, EyeOffIcon } from "@/components/icons" + +type FormValues = { + username: string + password: string +} + +export default function Page() { + const router = useRouter() + const [viewPassword, setViewPassword] = useState(false) + + const { + control, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm({ + defaultValues: { + username: "", + password: "", + }, + }) + + const onSubmit = async (data: FormValues) => { + await new Promise((res) => setTimeout(res, 1000)) + if (data.username == "multipool-admin" && data.password == "P@ssw0rd.1") { + setCookiesEncrypt("status", "Login", { expires: 1 }) + setCookiesEncrypt("username", "multipool-admin", { expires: 1 }) + router.push("/dashboard/account-management") + } + } + + return ( +
+
+
+ logo-multipool +

+ AI Platform Kolaboratif +

+

+ Produksi Narasi Media +

+
+
+
+ ( + + )} + /> + + {errors.username && ( +

+ {errors.username.message} +

+ )} +
+ +
+ ( + + + + setViewPassword(!viewPassword)} + > + {viewPassword ? : } + + + + )} + /> + + {errors.password && ( +

+ {errors.password.message} +

+ )} +
+ + +
+
+
+
+
+ ) +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..02e61e0 --- /dev/null +++ b/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "radix-nova", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} +} diff --git a/components/.gitkeep b/components/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/components/form/upload-accounts-data/upload-accounts-data-file.tsx b/components/form/upload-accounts-data/upload-accounts-data-file.tsx new file mode 100644 index 0000000..d24c0f6 --- /dev/null +++ b/components/form/upload-accounts-data/upload-accounts-data-file.tsx @@ -0,0 +1,124 @@ +"use client" + +import { FeedbackDialog } from "@/components/ui/popup" +import { useRef, useState } from "react" + +export default function UploadCsvCard() { + const inputRef = useRef(null) + const [file, setFile] = useState(null) + const [dragActive, setDragActive] = useState(false) + const [open, setOpen] = useState(false) + const [dialogType, setDialogType] = useState<"success" | "error" | "warning">( + "success" + ) + const [message, setMessage] = useState("") + + const handleFile = (selected: File | null) => { + if (!selected) return + if (selected.type !== "text/csv") { + alert("Only CSV files are allowed") + return + } + setFile(selected) + } + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault() + setDragActive(false) + const droppedFile = e.dataTransfer.files?.[0] + handleFile(droppedFile) + } + + const onSave = () => { + setDialogType("warning") + setMessage("Upload this File?") + setOpen(true) + } + const save = () => { + setOpen(false) + + setTimeout(() => { + setDialogType("success") + setMessage("Success") + setOpen(true) + }, 0) + } + + return ( +
+

Upload a CSV file

+ + {/* DROP AREA */} +
{ + e.preventDefault() + setDragActive(true) + }} + onDragLeave={() => setDragActive(false)} + onDrop={handleDrop} + className={`flex flex-col items-center justify-center gap-3 rounded-xl border-2 border-dashed p-10 text-center transition ${dragActive ? "border-blue-500 bg-blue-50" : "border-blue-400"} `} + > +
📁
+ +

Drag your file(s) to start uploading

+ +
+
+ OR +
+
+ + + + handleFile(e.target.files?.[0] ?? null)} + /> + + {file && ( +

Selected: {file.name}

+ )} +
+ + {/* ACTION BUTTONS */} +
+ {/* */} + + +
+ { + if (result) { + if (dialogType == "warning") { + save() + } + } else { + } + }} + /> +
+ ) +} diff --git a/components/icons.tsx b/components/icons.tsx new file mode 100644 index 0000000..c5095fb --- /dev/null +++ b/components/icons.tsx @@ -0,0 +1,176 @@ +import { SVGProps } from "react" + +export type IconSvgProps = SVGProps & { + size?: number +} + +export const EyeOffIcon = ({ + size, + height = 24, + width = 24, + ...props +}: IconSvgProps) => ( + + + + +) +export const EyeIcon = ({ + size, + height = 24, + width = 24, + ...props +}: IconSvgProps) => ( + + + + +) +export const ManagementIcon = ({ + size, + height = 24, + width = 24, + ...props +}: IconSvgProps) => ( + + + +) +export const UploadAccount = ({ + size, + height = 24, + width = 24, + ...props +}: IconSvgProps) => ( + + + +) +export const NotificationIcon = ({ + size, + height = 24, + width = 24, + ...props +}: IconSvgProps) => ( + + + + + +) +export const UserFillIcon = ({ + size, + height = 24, + width = 24, + ...props +}: IconSvgProps) => ( + + + + + + +) +export const ReloadIcon = ({ + size, + height = 24, + width = 24, + ...props +}: IconSvgProps) => ( + + + + + + + + +) diff --git a/components/table/account-management/account-management-table.tsx b/components/table/account-management/account-management-table.tsx new file mode 100644 index 0000000..fccb58e --- /dev/null +++ b/components/table/account-management/account-management-table.tsx @@ -0,0 +1,308 @@ +"use client" + +import { ReloadIcon } from "@/components/icons" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { Input } from "@/components/ui/input" +import { + InputGroup, + InputGroupAddon, + InputGroupInput, +} from "@/components/ui/input-group" +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Table, + TableBody, + TableCell, + TableFooter, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { ChevronLeft, ChevronRight, SearchIcon } from "lucide-react" +import { useEffect, useState } from "react" + +const dummyProxy = [ + { id: 1, value: "Jakarta" }, + { id: 2, value: "Bandung" }, + { id: 3, value: "Surabaya" }, + { id: 4, value: "Medan" }, +] + +const dummyStatus = [ + { id: 1, value: "Ready to Process" }, + { id: 2, value: "Healthy" }, + { id: 3, value: "Temporary Suspend" }, + { id: 4, value: "Suspended" }, +] + +const dummyData = [ + { + id: "1", + identifier: "MyUsername123", + proxy: "Jakarta", + status: 1, + }, + { + id: "2", + identifier: "MyUsername12223", + proxy: "Bandung", + status: 2, + }, + { + id: "3", + identifier: "MyUsername1132123", + proxy: "Jakarta", + status: 3, + }, + { + id: "4", + identifier: "MyUsername422123", + proxy: "Medan", + status: 4, + }, + { + id: "5", + identifier: "12MyUsername422123", + proxy: "Surabaya", + status: 1, + }, +] + +export default function AccountManagementTable() { + const [selectedStatus, setSelectedStatus] = useState("") + const [selectedLocation, setSelectedLocation] = useState("") + const [search, setSearch] = useState("") + const [limit, setLimit] = useState("5") + const [page, setPage] = useState(1) + const [totalPage, setTotalPage] = useState(1) + const [selectedDataTable, setSelectedDataTable] = useState([]) + + const getData = async () => { + const req = { + search: search, + status: selectedStatus, + proxy: selectedLocation, + page: page, + limit: limit, + } + console.log("request", req) + } + useEffect(() => { + getData() + }, [page, limit]) + + const resetFilter = () => { + setSelectedLocation("") + setSelectedStatus("") + setSearch("") + setPage(1) + getData() + } + + const getStatus = (id: number) => { + const color = [ + "text-green-600 border-2 border-green-600 bg-green-300", + "text-violet-600 border-2 border-violet-600 bg-violet-300", + "text-yellow-600 border-2 border-yellow-600 bg-yellow-300", + "text-red-600 border-2 border-red-600 bg-red-300", + ] + + const findStatusName = dummyStatus.find((a) => a.id == id) + + return ( +

+ {findStatusName?.value} +

+ ) + } + + return ( +
+
+

Search and Filter

+
+
+

Identifier

+ + setSearch(e.target.value)} + id="inline-end-input" + type="text" + placeholder="Search e.g username, email or ID" + /> + + setViewPassword(!viewPassword)} + > + + + + +
+
+

Proxy Location

+ +
+
+

Status

+ +
+ + +
+
+
+

X Server

+ +
+
+ + + + + Identifier + Proxy Location + Status + Action + + + + {dummyData.map((invoice) => ( + + + { + if (e) { + setSelectedDataTable([...selectedDataTable, invoice.id]) + } else { + const temp = [] + for (const element of selectedDataTable) { + if (element !== invoice.id) { + temp.push(element) + } + } + setSelectedDataTable(temp) + } + }} + className="border-gray-300" + /> + + {invoice.identifier} + {invoice.proxy} + {getStatus(invoice.status)} + + + + + ))} + + + + + + + +
+

ROWS PER PAGE:

+ + + + + + + +
+
+
+
+
+
+
+ ) +} diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx new file mode 100644 index 0000000..bf4a9fa --- /dev/null +++ b/components/theme-provider.tsx @@ -0,0 +1,71 @@ +"use client" + +import * as React from "react" +import { ThemeProvider as NextThemesProvider, useTheme } from "next-themes" + +function ThemeProvider({ + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + ) +} + +function isTypingTarget(target: EventTarget | null) { + if (!(target instanceof HTMLElement)) { + return false + } + + return ( + target.isContentEditable || + target.tagName === "INPUT" || + target.tagName === "TEXTAREA" || + target.tagName === "SELECT" + ) +} + +function ThemeHotkey() { + const { resolvedTheme, setTheme } = useTheme() + + React.useEffect(() => { + function onKeyDown(event: KeyboardEvent) { + if (event.defaultPrevented || event.repeat) { + return + } + + if (event.metaKey || event.ctrlKey || event.altKey) { + return + } + + if (event.key.toLowerCase() !== "d") { + return + } + + if (isTypingTarget(event.target)) { + return + } + + setTheme(resolvedTheme === "light" ? "dark" : "light") + } + + window.addEventListener("keydown", onKeyDown) + + return () => { + window.removeEventListener("keydown", onKeyDown) + } + }, [resolvedTheme, setTheme]) + + return null +} + +export { ThemeProvider } diff --git a/components/ui/app-sidebar.tsx b/components/ui/app-sidebar.tsx new file mode 100644 index 0000000..c6aa1e0 --- /dev/null +++ b/components/ui/app-sidebar.tsx @@ -0,0 +1,68 @@ +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarHeader, + SidebarTrigger, +} from "@/components/ui/sidebar" +import Link from "next/link" +import { ManagementIcon, UploadAccount } from "../icons" +import Image from "next/image" + +export function AppSidebar() { + return ( + +
+ {/* TRIGGER */} +
+ +
+ + {/* LOGO */} +
+ logo +
+ + + + {/* MENU */} + + + + + + Management Account + + + + + + + Upload Account Data + + + + + + +
+
+ ) +} diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..6138844 --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,67 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px 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: { + default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", + outline: + "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", + ghost: + "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", + destructive: + "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: + "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", + lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + icon: "size-8", + "icon-xs": + "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3", + "icon-sm": + "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg", + "icon-lg": "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot.Root : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/components/ui/checkbox.tsx b/components/ui/checkbox.tsx new file mode 100644 index 0000000..6d1d6be --- /dev/null +++ b/components/ui/checkbox.tsx @@ -0,0 +1,33 @@ +"use client" + +import * as React from "react" +import { Checkbox as CheckboxPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" +import { CheckIcon } from "lucide-react" + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx new file mode 100644 index 0000000..d9aecca --- /dev/null +++ b/components/ui/dialog.tsx @@ -0,0 +1,168 @@ +"use client" + +import * as React from "react" +import { Dialog as DialogPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { XIcon } from "lucide-react" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ + className, + showCloseButton = false, + children, + ...props +}: React.ComponentProps<"div"> & { + showCloseButton?: boolean +}) { + return ( +
+ {children} + {showCloseButton && ( + + + + )} +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/components/ui/input-group.tsx b/components/ui/input-group.tsx new file mode 100644 index 0000000..256ba4b --- /dev/null +++ b/components/ui/input-group.tsx @@ -0,0 +1,156 @@ +"use client" + +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" +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"> & + VariantProps) { + return ( +