From cf53b734851a7035475589d636870057bf6b9dee Mon Sep 17 00:00:00 2001 From: Rama Priyanto Date: Thu, 16 Apr 2026 06:38:26 +0700 Subject: [PATCH] first commit --- .gitignore | 36 + .prettierignore | 7 + .prettierrc | 11 + README.md | 21 + app/dashboard/account-management/page.tsx | 16 + app/dashboard/layout.tsx | 20 + app/dashboard/upload-account-data/page.tsx | 16 + app/favicon.ico | Bin 0 -> 3362 bytes app/globals.css | 129 + app/layout.tsx | 35 + app/page.tsx | 136 + components.json | 25 + components/.gitkeep | 0 .../upload-accounts-data-file.tsx | 124 + components/icons.tsx | 176 + .../account-management-table.tsx | 308 + components/theme-provider.tsx | 71 + components/ui/app-sidebar.tsx | 68 + components/ui/button.tsx | 67 + components/ui/checkbox.tsx | 33 + components/ui/dialog.tsx | 168 + components/ui/input-group.tsx | 156 + components/ui/input.tsx | 19 + components/ui/navbar.tsx | 25 + components/ui/popup.tsx | 84 + components/ui/select.tsx | 192 + components/ui/separator.tsx | 28 + components/ui/sheet.tsx | 147 + components/ui/sidebar.tsx | 702 ++ components/ui/skeleton.tsx | 13 + components/ui/table.tsx | 116 + components/ui/textarea.tsx | 18 + components/ui/tooltip.tsx | 57 + eslint.config.mjs | 18 + hooks/.gitkeep | 0 hooks/use-mobile.ts | 19 + lib/.gitkeep | 0 lib/utils.ts | 6 + next.config.mjs | 4 + package-lock.json | 10519 ++++++++++++++++ package.json | 44 + postcss.config.mjs | 8 + public/.gitkeep | 0 public/favicon.ico | Bin 0 -> 3362 bytes public/multipool-logo.png | Bin 0 -> 7524 bytes tsconfig.json | 34 + utils/globals.tsx | 37 + 47 files changed, 13713 insertions(+) create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 README.md create mode 100644 app/dashboard/account-management/page.tsx create mode 100644 app/dashboard/layout.tsx create mode 100644 app/dashboard/upload-account-data/page.tsx create mode 100644 app/favicon.ico create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/page.tsx create mode 100644 components.json create mode 100644 components/.gitkeep create mode 100644 components/form/upload-accounts-data/upload-accounts-data-file.tsx create mode 100644 components/icons.tsx create mode 100644 components/table/account-management/account-management-table.tsx create mode 100644 components/theme-provider.tsx create mode 100644 components/ui/app-sidebar.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/input-group.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/navbar.tsx create mode 100644 components/ui/popup.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/sidebar.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/table.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 eslint.config.mjs create mode 100644 hooks/.gitkeep create mode 100644 hooks/use-mobile.ts create mode 100644 lib/.gitkeep create mode 100644 lib/utils.ts create mode 100644 next.config.mjs create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 public/.gitkeep create mode 100644 public/favicon.ico create mode 100644 public/multipool-logo.png create mode 100644 tsconfig.json create mode 100644 utils/globals.tsx 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 0000000000000000000000000000000000000000..cd18e0c7b757cbe6689a381ad755559682f4fd59 GIT binary patch literal 3362 zcmbW430PBC7Jx5e)p4!WTB}y9Go2C9j%}5;Y6t5`tyZmF)J{LKwJueu7N`)!4Um8m z2(s^MNJv6RfFuwg`@TZhgd`?}01*gMkg7mS?z|VOAGJ=$uQ%VhdGEZN^PhW`_g(;S zLq4ls2Ivgt&I4cx0ALLws}Ok;5mb+k@VNjinlmXb#WPNDyazxy;5KA!n$@gPEYK=t z3r%{}{85K_CWO!p!e5@z(z7p{816=x5C|0N1&I}+O_xjfenExY;M2L>E5|cyBaWpo zubfUQ3k*)Bex6=Z>18oA&qmin=zMm_Q&yrjqDrcOOsk)pz>@i1qDe1?P-UUPRQZ7m zrOLN4rRskKm8kyapD*(ICav1%3mom}!RX>ECvk+c8I;P`NAGscsHG8HzCXqMAF9R& zdI8uT1IpwLD`IQ30TC?q`FM`@pMqxd%#Y(}zq4tu54v+>(ky!iM zoAvXKocSI==l_qz*w~oMhPk6-)=-~)CS7-Hb&{a|N;+3{f-6(4hU2}{*%H~ zWB`VSh5*?6J)pN5_R=M?-K2WM;S7N$(0gCSUg{O{()5b@V)EF&h5Rhrz)? z0Agr>*lG$$;Yo?Pd|5zzwLCVZf_pToQgQ5D0jE5)usSl9A=`B-yC(m;OjZhoRk!lR z!#BXO6xREm!Njhmr*h!p<7v<{q{z7^GEbD8N7%t=Fzk^wxBV5Zr;(A7C%66h+{F(q z0Hjvaxh$R-pCr^}$8%({Sf(`Q5<`xSWXS!3h=Qm<0uLKX5}iAnQxjLhtX+xNb>9|M zp1dlM1X2ErC?7+F+Y{K%uaYS>`E=$UsnNQ%ulLR@gV6{kCMF&;ryktDe;=%^)_}6x zwoh`D4T%|wrZ9%4`88rg=kj8WY1!E5h&ztoz8KGKUP)3Imi<5!2P5%+2}>1uymgh) z^jm&0{C5%u9wsX%0*ja|GMn>>eQ;=jqNVMB=AgN+Z?@EQ+so3`HDA%)zr42b)>{l^ zqi1rpbbo3E&!>>D^QuxduE3Scyi=Ld?O8RN9UW%VykL^__5FA@ZeI*7+AF9)^wROP zU)EqI`F=DV{6WzEa|DB1Mj(+q(Hso*A-(mKq3n08s%UGAp~;kch!WN3nL_n-JXaGP z!7GZ1(BR zkyPDwgnzhH3q#}`@TT=Am~OlGFqXy7C@Cs>1%T%O93T6Q2daLD2d%Sh5be$tahVCEX-*v)_ehBgHckf}TRahpuc(L#9j~@Hb z(&qrpeUACM&hAaU_MRE`o*q|x#yZsi4!5`|_%#bNN*HUehe?$T0!?3LKFR$fB->O$V&k;(1v88o7rlu(rBkvebc*O5>!W27%u`L5XMpLRU zaVUg^CyyO<`Cu9t1Z{VRW~e&5w;HUS3ndyYpfEY0QLFw-1WowqF@l(OAXn19CrjG* zL5lcCp9I11J2&cv--uw1cwQ(QT@_diw_V81xo1$#sjcIJ6iExnmA5VnS9NL=(D)A^ z`*-o3Fu?|4l=hC}8i5fcWzyy)W)4g0`rLz{!r3cSOLtUD5`e{uC6TK|vo|3v(;#t9g$?&H&|YF9tvqu=%) z?;&#_z^gm9V?}LI83Mg;t+r(`S`)ac`qdw2D^wq*O0|Ajg4obp_K71na=169Q0Q?w z>)sON`Sa-hJK6u%N&a2v>c7eOPdMYuKmUYl9g$}#3_D@B+I=b?J%;0V(7tfv-LfpF z$eTYX73(&t8yXj9^BUI%7jYv(vdV)FBrtQ{jba+tgwe-6rab>p{-gUJM%Lj>v<%l^RP>3Jy5w*HizZ5j&5ZyVT2*PB;7)K^V2Yk5!NUFSNR!_}K5YQv%wuHNTnwdOy0 zJl*aCnIiVTZi?=2#EIa#U+W*`FFTzBmz_+1@J1N1^W#*G*q^S>j*%Gx^IN-iSKRLL z6t)^)uxq)~VF|K7;l8IbpQdc?9t6E3qizLqDnl3YK6%gWa(TlF|6=9V zpkm2}>?-+^?k?+$Nf!K5cA@Yp;;9Gn4`L=9^|=Ox?ru;x4G9w}09-`{F^q~2A|8k= oK#FE29z5?J2bQ`C!8) { + 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 ( +