mirror of
https://gh-proxy.org/https://github.com/klinsmaya/portal-template-default.git
synced 2026-11-08 00:57:57 +08:00
Initial commit from create-refine-app
This commit is contained in:
+30
@@ -0,0 +1,30 @@
|
||||
# Logs
|
||||
|
||||
logs
|
||||
_.log
|
||||
npm-debug.log_
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# Editor directories and files
|
||||
|
||||
.vscode/_
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
_.suo
|
||||
_.ntvs_
|
||||
_.njsproj
|
||||
_.sln
|
||||
\*.sw?
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# This Dockerfile uses `serve` npm package to serve the static files with node process.
|
||||
# You can find the Dockerfile for nginx in the following link:
|
||||
# https://github.com/refinedev/dockerfiles/blob/main/vite/Dockerfile.nginx
|
||||
FROM refinedev/node:18 AS base
|
||||
|
||||
FROM base as deps
|
||||
|
||||
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
|
||||
|
||||
RUN \
|
||||
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
|
||||
elif [ -f package-lock.json ]; then npm ci; \
|
||||
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
|
||||
else echo "Lockfile not found." && exit 1; \
|
||||
fi
|
||||
|
||||
FROM base as builder
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
COPY --from=deps /app/refine/node_modules ./node_modules
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM base as runner
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
RUN npm install -g serve
|
||||
|
||||
COPY --from=builder /app/refine/dist ./
|
||||
|
||||
USER refine
|
||||
|
||||
CMD ["serve"]
|
||||
@@ -0,0 +1,49 @@
|
||||
# my-admin3
|
||||
|
||||
<div align="center" style="margin: 30px;">
|
||||
<a href="https://refine.dev">
|
||||
<img alt="refine logo" src="https://refine.ams3.cdn.digitaloceanspaces.com/readme/refine-readme-banner.png">
|
||||
</a>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
This [Refine](https://github.com/refinedev/refine) project was generated with [create refine-app](https://github.com/refinedev/refine/tree/master/packages/create-refine-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
A React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility ✨
|
||||
|
||||
Refine's hooks and components simplifies the development process and eliminates the repetitive tasks by providing industry-standard solutions for crucial aspects of a project, including authentication, access control, routing, networking, state management, and i18n.
|
||||
|
||||
## Available Scripts
|
||||
|
||||
### Running the development server.
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Building for production.
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Running the production server.
|
||||
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about **Refine**, please check out the [Documentation](https://refine.dev/docs)
|
||||
|
||||
- **REST Data Provider** [Docs](https://refine.dev/docs/core/providers/data-provider/#overview)
|
||||
- **shadcn/ui** [Docs](https://refine.dev/docs/guides-concepts/general-concepts/#headless-concept)
|
||||
- **React Router** [Docs](https://refine.dev/docs/core/providers/router-provider/)
|
||||
- **Custom Auth Provider** [Docs](https://refine.dev/docs/core/providers/auth-provider/)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/App.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import js from "@eslint/js";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import reactRefresh from "eslint-plugin-react-refresh";
|
||||
import globals from "globals";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ["dist"] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
"react-hooks": reactHooks,
|
||||
"react-refresh": reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="refine | Build your React-based CRUD applications, without constraints."
|
||||
/>
|
||||
<meta
|
||||
data-rh="true"
|
||||
property="og:image"
|
||||
content="https://refine.dev/img/refine_social.png"
|
||||
/>
|
||||
<meta
|
||||
data-rh="true"
|
||||
name="twitter:image"
|
||||
content="https://refine.dev/img/refine_social.png"
|
||||
/>
|
||||
<title>
|
||||
Refine - Build your React-based CRUD applications, without constraints.
|
||||
</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm dev` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
Generated
+13124
File diff suppressed because it is too large
Load Diff
+108
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"name": "my-admin3",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@refinedev/cli": "^2.16.48",
|
||||
"@refinedev/core": "^5.0.8",
|
||||
"@refinedev/devtools": "^2.0.1",
|
||||
"@refinedev/kbar": "^2.0.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-router": "^7.0.2",
|
||||
"@refinedev/rest": "^2.1.0",
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@radix-ui/react-accordion": "^1.2.8",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.11",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.4",
|
||||
"@radix-ui/react-avatar": "^1.1.7",
|
||||
"@radix-ui/react-checkbox": "^1.2.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.8",
|
||||
"@radix-ui/react-context-menu": "^2.2.12",
|
||||
"@radix-ui/react-dialog": "^1.1.11",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.12",
|
||||
"@radix-ui/react-hover-card": "^1.1.11",
|
||||
"@radix-ui/react-label": "^2.1.4",
|
||||
"@radix-ui/react-menubar": "^1.1.12",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.10",
|
||||
"@radix-ui/react-popover": "^1.1.11",
|
||||
"@radix-ui/react-progress": "^1.1.4",
|
||||
"@radix-ui/react-radio-group": "^1.3.4",
|
||||
"@radix-ui/react-scroll-area": "^1.2.6",
|
||||
"@radix-ui/react-select": "^2.2.2",
|
||||
"@radix-ui/react-separator": "^1.1.4",
|
||||
"@radix-ui/react-slider": "^1.3.2",
|
||||
"@radix-ui/react-slot": "^1.2.0",
|
||||
"@radix-ui/react-switch": "^1.2.2",
|
||||
"@radix-ui/react-tabs": "^1.1.9",
|
||||
"@radix-ui/react-toggle": "^1.1.6",
|
||||
"@radix-ui/react-toggle-group": "^1.1.7",
|
||||
"@radix-ui/react-tooltip": "^1.2.4",
|
||||
"@refinedev/react-hook-form": "^5.0.0",
|
||||
"@refinedev/react-router": "^2.0.0",
|
||||
"@refinedev/react-table": "^6.0.0",
|
||||
"@refinedev/simple-rest": "^6.0.0",
|
||||
"@tailwindcss/vite": "^4.1.8",
|
||||
"@tanstack/react-table": "^8.2.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.10.7",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.487.0",
|
||||
"postcss": "^8.5.3",
|
||||
"react-day-picker": "8.10.1",
|
||||
"react-hook-form": "^7.57.0",
|
||||
"react-resizable-panels": "^2.1.8",
|
||||
"recharts": "^2.15.3",
|
||||
"shadcn": "^2.4.1",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tw-animate-css": "^1.2.5",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^3.24.3",
|
||||
"next-themes": "^0.4.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/react-dom": "^19.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.57.1",
|
||||
"@typescript-eslint/parser": "^5.57.1",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"vite": "^6.3.5",
|
||||
"typescript": "^5.8.3",
|
||||
"@tailwindcss/postcss": "^4.1.4",
|
||||
"tailwindcss": "^4.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "refine dev",
|
||||
"build": "tsc && refine build",
|
||||
"start": "refine start",
|
||||
"refine": "refine"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"refine": {
|
||||
"projectId": "72Ag5T-38hVrZ-1G44i8"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
+134
@@ -0,0 +1,134 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--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);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
|
||||
--font-sans: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.325rem;
|
||||
--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.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--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.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--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 antialiased;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
import {
|
||||
Refine,
|
||||
GitHubBanner,
|
||||
WelcomePage,
|
||||
Authenticated,
|
||||
} from "@refinedev/core";
|
||||
import { DevtoolsPanel, DevtoolsProvider } from "@refinedev/devtools";
|
||||
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";
|
||||
|
||||
import { BrowserRouter, Route, Routes, Outlet } from "react-router";
|
||||
import routerProvider, {
|
||||
NavigateToResource,
|
||||
CatchAllNavigate,
|
||||
UnsavedChangesNotifier,
|
||||
DocumentTitleHandler,
|
||||
} from "@refinedev/react-router";
|
||||
import {
|
||||
BlogPostList,
|
||||
BlogPostCreate,
|
||||
BlogPostEdit,
|
||||
BlogPostShow,
|
||||
} from "./pages/blog-posts";
|
||||
import {
|
||||
CategoryList,
|
||||
CategoryCreate,
|
||||
CategoryEdit,
|
||||
CategoryShow,
|
||||
} from "./pages/categories";
|
||||
import { dataProvider } from "./providers/data";
|
||||
import { Login } from "./pages/login";
|
||||
import { Register } from "./pages/register";
|
||||
import { ForgotPassword } from "./pages/forgot-password";
|
||||
import { ErrorComponent } from "./components/refine-ui/layout/error-component";
|
||||
import { Layout } from "./components/refine-ui/layout/layout";
|
||||
import { Header } from "./components/refine-ui/layout/header";
|
||||
import { useNotificationProvider } from "./components/refine-ui/notification/use-notification-provider";
|
||||
import { Toaster } from "./components/refine-ui/notification/toaster";
|
||||
import { ThemeProvider } from "./components/refine-ui/theme/theme-provider";
|
||||
import "./App.css";
|
||||
import { ForgotPassword } from "./pages/forgotPassword";
|
||||
import { authProvider } from "./providers/auth";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<GitHubBanner />
|
||||
<RefineKbarProvider>
|
||||
<ThemeProvider>
|
||||
<DevtoolsProvider>
|
||||
<Refine
|
||||
dataProvider={dataProvider}
|
||||
notificationProvider={useNotificationProvider()}
|
||||
routerProvider={routerProvider}
|
||||
authProvider={authProvider}
|
||||
resources={[
|
||||
{
|
||||
name: "blog_posts",
|
||||
list: "/blog-posts",
|
||||
create: "/blog-posts/create",
|
||||
edit: "/blog-posts/edit/:id",
|
||||
show: "/blog-posts/show/:id",
|
||||
meta: {
|
||||
canDelete: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "categories",
|
||||
list: "/categories",
|
||||
create: "/categories/create",
|
||||
edit: "/categories/edit/:id",
|
||||
show: "/categories/show/:id",
|
||||
meta: {
|
||||
canDelete: true,
|
||||
},
|
||||
},
|
||||
]}
|
||||
options={{
|
||||
syncWithLocation: true,
|
||||
warnWhenUnsavedChanges: true,
|
||||
projectId: "72Ag5T-38hVrZ-1G44i8",
|
||||
}}
|
||||
>
|
||||
<Routes>
|
||||
<Route
|
||||
element={
|
||||
<Authenticated
|
||||
key="authenticated-inner"
|
||||
fallback={<CatchAllNavigate to="/login" />}
|
||||
>
|
||||
<Layout>
|
||||
<Outlet />
|
||||
</Layout>
|
||||
</Authenticated>
|
||||
}
|
||||
>
|
||||
<Route
|
||||
index
|
||||
element={<NavigateToResource resource="blog_posts" />}
|
||||
/>
|
||||
<Route path="/blog-posts">
|
||||
<Route index element={<BlogPostList />} />
|
||||
<Route path="create" element={<BlogPostCreate />} />
|
||||
<Route path="edit/:id" element={<BlogPostEdit />} />
|
||||
<Route path="show/:id" element={<BlogPostShow />} />
|
||||
</Route>
|
||||
<Route path="/categories">
|
||||
<Route index element={<CategoryList />} />
|
||||
<Route path="create" element={<CategoryCreate />} />
|
||||
<Route path="edit/:id" element={<CategoryEdit />} />
|
||||
<Route path="show/:id" element={<CategoryShow />} />
|
||||
</Route>
|
||||
<Route path="*" element={<ErrorComponent />} />
|
||||
</Route>
|
||||
<Route
|
||||
element={
|
||||
<Authenticated
|
||||
key="authenticated-outer"
|
||||
fallback={<Outlet />}
|
||||
>
|
||||
<NavigateToResource />
|
||||
</Authenticated>
|
||||
}
|
||||
>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
<Route path="/forgot-password" element={<ForgotPassword />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
<Toaster />
|
||||
<RefineKbar />
|
||||
<UnsavedChangesNotifier />
|
||||
<DocumentTitleHandler />
|
||||
</Refine>
|
||||
<DevtoolsPanel />
|
||||
</DevtoolsProvider>
|
||||
</ThemeProvider>
|
||||
</RefineKbarProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { type BaseKey, useCloneButton } from "@refinedev/core";
|
||||
import { Copy } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type CloneButtonProps = {
|
||||
/**
|
||||
* Resource name for API data interactions. `identifier` of the resource can be used instead of the `name` of the resource.
|
||||
* @default Inferred resource name from the route
|
||||
*/
|
||||
resource?: string;
|
||||
/**
|
||||
* Data item identifier for the actions with the API
|
||||
* @default Reads `:id` from the URL
|
||||
*/
|
||||
recordItemId?: BaseKey;
|
||||
/**
|
||||
* Access Control configuration for the button
|
||||
* @default `{ enabled: true, hideIfUnauthorized: false }`
|
||||
*/
|
||||
accessControl?: {
|
||||
enabled?: boolean;
|
||||
hideIfUnauthorized?: boolean;
|
||||
};
|
||||
/**
|
||||
* `meta` property is used when creating the URL for the related action and path.
|
||||
*/
|
||||
meta?: Record<string, unknown>;
|
||||
} & React.ComponentProps<typeof Button>;
|
||||
|
||||
export const CloneButton = React.forwardRef<
|
||||
React.ComponentRef<typeof Button>,
|
||||
CloneButtonProps
|
||||
>(
|
||||
(
|
||||
{ resource, recordItemId, accessControl, meta, children, onClick, ...rest },
|
||||
ref
|
||||
) => {
|
||||
const { hidden, disabled, LinkComponent, to, label } = useCloneButton({
|
||||
accessControl,
|
||||
resource,
|
||||
id: recordItemId,
|
||||
meta,
|
||||
});
|
||||
|
||||
const isDisabled = disabled || rest.disabled;
|
||||
const isHidden = hidden || rest.hidden;
|
||||
|
||||
if (isHidden) return null;
|
||||
|
||||
return (
|
||||
<Button {...rest} ref={ref} disabled={isDisabled} asChild>
|
||||
<LinkComponent
|
||||
to={to}
|
||||
replace={false}
|
||||
onClick={(e: React.PointerEvent<HTMLButtonElement>) => {
|
||||
if (isDisabled) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (onClick) {
|
||||
e.preventDefault();
|
||||
onClick(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children ?? (
|
||||
<div className="flex items-center gap-2 font-semibold">
|
||||
<Copy className="h-4 w-4" />
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
)}
|
||||
</LinkComponent>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CloneButton.displayName = "CloneButton";
|
||||
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { type BaseKey, useCreateButton } from "@refinedev/core";
|
||||
import { Plus } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type CreateButtonProps = {
|
||||
/**
|
||||
* Resource name for API data interactions. `identifier` of the resource can be used instead of the `name` of the resource.
|
||||
* @default Inferred resource name from the route
|
||||
*/
|
||||
resource?: BaseKey;
|
||||
/**
|
||||
* Access Control configuration for the button
|
||||
* @default `{ enabled: true, hideIfUnauthorized: false }`
|
||||
*/
|
||||
accessControl?: {
|
||||
enabled?: boolean;
|
||||
hideIfUnauthorized?: boolean;
|
||||
};
|
||||
/**
|
||||
* `meta` property is used when creating the URL for the related action and path.
|
||||
*/
|
||||
meta?: Record<string, unknown>;
|
||||
} & React.ComponentProps<typeof Button>;
|
||||
|
||||
export const CreateButton = React.forwardRef<
|
||||
React.ComponentRef<typeof Button>,
|
||||
CreateButtonProps
|
||||
>(({ resource, accessControl, meta, children, onClick, ...rest }, ref) => {
|
||||
const { hidden, disabled, LinkComponent, to, label } = useCreateButton({
|
||||
resource,
|
||||
accessControl,
|
||||
meta,
|
||||
});
|
||||
|
||||
const isDisabled = disabled || rest.disabled;
|
||||
const isHidden = hidden || rest.hidden;
|
||||
|
||||
if (isHidden) return null;
|
||||
|
||||
return (
|
||||
<Button {...rest} ref={ref} disabled={isDisabled} asChild>
|
||||
<LinkComponent
|
||||
to={to}
|
||||
replace={false}
|
||||
onClick={(e: React.PointerEvent<HTMLButtonElement>) => {
|
||||
if (isDisabled) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (onClick) {
|
||||
e.preventDefault();
|
||||
onClick(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children ?? (
|
||||
<div className="flex items-center gap-2 font-semibold">
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>{label ?? "Create"}</span>
|
||||
</div>
|
||||
)}
|
||||
</LinkComponent>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
CreateButton.displayName = "CreateButton";
|
||||
@@ -0,0 +1,115 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { type BaseKey, useDeleteButton } from "@refinedev/core";
|
||||
import { Loader2, Trash } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
|
||||
type DeleteButtonProps = {
|
||||
/**
|
||||
* Resource name for API data interactions. `identifier` of the resource can be used instead of the `name` of the resource.
|
||||
* @default Inferred resource name from the route
|
||||
*/
|
||||
resource?: string;
|
||||
/**
|
||||
* Data item identifier for the actions with the API
|
||||
* @default Reads `:id` from the URL
|
||||
*/
|
||||
recordItemId?: BaseKey;
|
||||
/**
|
||||
* Access Control configuration for the button
|
||||
* @default `{ enabled: true, hideIfUnauthorized: false }`
|
||||
*/
|
||||
accessControl?: {
|
||||
enabled?: boolean;
|
||||
hideIfUnauthorized?: boolean;
|
||||
};
|
||||
/**
|
||||
* `meta` property is used when creating the URL for the related action and path.
|
||||
*/
|
||||
meta?: Record<string, unknown>;
|
||||
} & React.ComponentProps<typeof Button>;
|
||||
|
||||
export const DeleteButton = React.forwardRef<
|
||||
React.ComponentRef<typeof Button>,
|
||||
DeleteButtonProps
|
||||
>(({ resource, recordItemId, accessControl, meta, children, ...rest }, ref) => {
|
||||
const {
|
||||
hidden,
|
||||
disabled,
|
||||
loading,
|
||||
onConfirm,
|
||||
label,
|
||||
confirmTitle: defaultConfirmTitle,
|
||||
confirmOkLabel: defaultConfirmOkLabel,
|
||||
cancelLabel: defaultCancelLabel,
|
||||
} = useDeleteButton({
|
||||
resource,
|
||||
id: recordItemId,
|
||||
accessControl,
|
||||
meta,
|
||||
});
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const isDisabled = disabled || rest.disabled || loading;
|
||||
const isHidden = hidden || rest.hidden;
|
||||
|
||||
if (isHidden) return null;
|
||||
|
||||
const confirmCancelText = defaultCancelLabel;
|
||||
const confirmOkText = defaultConfirmOkLabel;
|
||||
const confirmTitle = defaultConfirmTitle;
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
variant="destructive"
|
||||
{...rest}
|
||||
ref={ref}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{children ?? (
|
||||
<div className="flex items-center gap-2 font-semibold">
|
||||
<Trash className="h-4 w-4" />
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</span>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto" align="start">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm">{confirmTitle}</p>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => setOpen(false)}>
|
||||
{confirmCancelText}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
disabled={loading}
|
||||
onClick={() => {
|
||||
if (typeof onConfirm === "function") {
|
||||
onConfirm();
|
||||
}
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{confirmOkText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
|
||||
DeleteButton.displayName = "DeleteButton";
|
||||
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { type BaseKey, useEditButton } from "@refinedev/core";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Pencil } from "lucide-react";
|
||||
|
||||
type EditButtonProps = {
|
||||
/**
|
||||
* Resource name for API data interactions. `identifier` of the resource can be used instead of the `name` of the resource.
|
||||
* @default Inferred resource name from the route
|
||||
*/
|
||||
resource?: string;
|
||||
/**
|
||||
* Data item identifier for the actions with the API
|
||||
* @default Reads `:id` from the URL
|
||||
*/
|
||||
recordItemId?: BaseKey;
|
||||
/**
|
||||
* Access Control configuration for the button
|
||||
* @default `{ enabled: true, hideIfUnauthorized: false }`
|
||||
*/
|
||||
accessControl?: {
|
||||
enabled?: boolean;
|
||||
hideIfUnauthorized?: boolean;
|
||||
};
|
||||
/**
|
||||
* `meta` property is used when creating the URL for the related action and path.
|
||||
*/
|
||||
meta?: Record<string, unknown>;
|
||||
} & React.ComponentProps<typeof Button>;
|
||||
|
||||
export const EditButton = React.forwardRef<
|
||||
React.ComponentRef<typeof Button>,
|
||||
EditButtonProps
|
||||
>(
|
||||
(
|
||||
{ resource, recordItemId, accessControl, meta, children, onClick, ...rest },
|
||||
ref
|
||||
) => {
|
||||
const { hidden, disabled, LinkComponent, to, label } = useEditButton({
|
||||
resource,
|
||||
id: recordItemId,
|
||||
accessControl,
|
||||
meta,
|
||||
});
|
||||
|
||||
const isDisabled = disabled || rest.disabled;
|
||||
const isHidden = hidden || rest.hidden;
|
||||
|
||||
if (isHidden) return null;
|
||||
|
||||
return (
|
||||
<Button {...rest} ref={ref} disabled={isDisabled} asChild>
|
||||
<LinkComponent
|
||||
to={to}
|
||||
replace={false}
|
||||
onClick={(e: React.PointerEvent<HTMLButtonElement>) => {
|
||||
if (isDisabled) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (onClick) {
|
||||
e.preventDefault();
|
||||
onClick(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children ?? (
|
||||
<div className="flex items-center gap-2 font-semibold">
|
||||
<Pencil className="h-4 w-4" />
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
)}
|
||||
</LinkComponent>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
EditButton.displayName = "EditButton";
|
||||
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { type BaseKey, useListButton } from "@refinedev/core";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { List } from "lucide-react";
|
||||
|
||||
type ListButtonProps = {
|
||||
/**
|
||||
* Resource name for API data interactions. `identifier` of the resource can be used instead of the `name` of the resource.
|
||||
* @default Inferred resource name from the route
|
||||
*/
|
||||
resource?: BaseKey;
|
||||
/**
|
||||
* Access Control configuration for the button
|
||||
* @default `{ enabled: true, hideIfUnauthorized: false }`
|
||||
*/
|
||||
accessControl?: {
|
||||
enabled?: boolean;
|
||||
hideIfUnauthorized?: boolean;
|
||||
};
|
||||
/**
|
||||
* `meta` property is used when creating the URL for the related action and path.
|
||||
*/
|
||||
meta?: Record<string, unknown>;
|
||||
} & React.ComponentProps<typeof Button>;
|
||||
|
||||
export const ListButton = React.forwardRef<
|
||||
React.ComponentRef<typeof Button>,
|
||||
ListButtonProps
|
||||
>(({ resource, accessControl, meta, children, onClick, ...rest }, ref) => {
|
||||
const { hidden, disabled, LinkComponent, to, label } = useListButton({
|
||||
resource,
|
||||
accessControl,
|
||||
meta,
|
||||
});
|
||||
|
||||
const isDisabled = disabled || rest.disabled;
|
||||
const isHidden = hidden || rest.hidden;
|
||||
|
||||
if (isHidden) return null;
|
||||
|
||||
return (
|
||||
<Button {...rest} ref={ref} disabled={isDisabled} asChild>
|
||||
<LinkComponent
|
||||
to={to}
|
||||
replace={false}
|
||||
onClick={(e: React.PointerEvent<HTMLButtonElement>) => {
|
||||
if (isDisabled) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (onClick) {
|
||||
e.preventDefault();
|
||||
onClick(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children ?? (
|
||||
<div className="flex items-center gap-2 font-semibold">
|
||||
<List className="w-4 h-4" />
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
)}
|
||||
</LinkComponent>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
ListButton.displayName = "ListButton";
|
||||
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { type BaseKey, useRefreshButton } from "@refinedev/core";
|
||||
import { RefreshCcw } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type RefreshButtonProps = {
|
||||
/**
|
||||
* Resource name for API data interactions. `identifier` of the resource can be used instead of the `name` of the resource.
|
||||
* @default Inferred resource name from the route
|
||||
*/
|
||||
resource?: string;
|
||||
/**
|
||||
* Data item identifier for the actions with the API
|
||||
* @default Reads `:id` from the URL
|
||||
*/
|
||||
recordItemId?: BaseKey;
|
||||
/**
|
||||
* Target data provider name for API call to be made
|
||||
* @default `"default"`
|
||||
*/
|
||||
dataProviderName?: string;
|
||||
/**
|
||||
* `meta` property is used when creating the URL for the related action and path.
|
||||
*/
|
||||
meta?: Record<string, unknown>;
|
||||
} & React.ComponentProps<typeof Button>;
|
||||
|
||||
export const RefreshButton = React.forwardRef<
|
||||
React.ComponentRef<typeof Button>,
|
||||
RefreshButtonProps
|
||||
>(
|
||||
(
|
||||
{ resource, recordItemId, dataProviderName, meta, children, ...rest },
|
||||
ref
|
||||
) => {
|
||||
const {
|
||||
onClick: refresh,
|
||||
loading,
|
||||
label,
|
||||
} = useRefreshButton({
|
||||
resource,
|
||||
id: recordItemId,
|
||||
dataProviderName,
|
||||
meta,
|
||||
});
|
||||
|
||||
const isDisabled = rest.disabled || loading;
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={(e: React.PointerEvent<HTMLButtonElement>) => {
|
||||
if (isDisabled) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
refresh();
|
||||
}}
|
||||
{...rest}
|
||||
ref={ref}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{children ?? (
|
||||
<div className="flex items-center gap-2">
|
||||
<RefreshCcw
|
||||
className={cn("h-4 w-4", {
|
||||
"animate-spin": loading,
|
||||
})}
|
||||
/>
|
||||
<span>{label ?? "Refresh"}</span>
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
RefreshButton.displayName = "RefreshButton";
|
||||
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { type BaseKey, useShowButton } from "@refinedev/core";
|
||||
import { Eye } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type ShowButtonProps = {
|
||||
/**
|
||||
* Resource name for API data interactions. `identifier` of the resource can be used instead of the `name` of the resource.
|
||||
* @default Inferred resource name from the route
|
||||
*/
|
||||
resource?: string;
|
||||
/**
|
||||
* Data item identifier for the actions with the API
|
||||
* @default Reads `:id` from the URL
|
||||
*/
|
||||
recordItemId?: BaseKey;
|
||||
/**
|
||||
* Access Control configuration for the button
|
||||
* @default `{ enabled: true, hideIfUnauthorized: false }`
|
||||
*/
|
||||
accessControl?: {
|
||||
enabled?: boolean;
|
||||
hideIfUnauthorized?: boolean;
|
||||
};
|
||||
/**
|
||||
* `meta` property is used when creating the URL for the related action and path.
|
||||
*/
|
||||
meta?: Record<string, unknown>;
|
||||
} & React.ComponentProps<typeof Button>;
|
||||
|
||||
export const ShowButton = React.forwardRef<
|
||||
React.ComponentRef<typeof Button>,
|
||||
ShowButtonProps
|
||||
>(
|
||||
(
|
||||
{ resource, recordItemId, accessControl, meta, children, onClick, ...rest },
|
||||
ref
|
||||
) => {
|
||||
const { hidden, disabled, LinkComponent, to, label } = useShowButton({
|
||||
resource,
|
||||
id: recordItemId,
|
||||
accessControl,
|
||||
meta,
|
||||
});
|
||||
|
||||
const isDisabled = disabled || rest.disabled;
|
||||
const isHidden = hidden || rest.hidden;
|
||||
|
||||
if (isHidden) return null;
|
||||
|
||||
return (
|
||||
<Button {...rest} ref={ref} disabled={isDisabled} asChild>
|
||||
<LinkComponent
|
||||
to={to}
|
||||
replace={false}
|
||||
onClick={(e: React.PointerEvent<HTMLButtonElement>) => {
|
||||
if (isDisabled) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (onClick) {
|
||||
e.preventDefault();
|
||||
onClick(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children ?? (
|
||||
<div className="flex items-center gap-2 font-semibold">
|
||||
<Eye className="h-4 w-4" />
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
)}
|
||||
</LinkComponent>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ShowButton.displayName = "ShowButton";
|
||||
@@ -0,0 +1,985 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useTranslate, type CrudOperators } from "@refinedev/core";
|
||||
import type { Column, Table as ReactTable } from "@tanstack/react-table";
|
||||
import type { DateRange } from "react-day-picker";
|
||||
import { Check, ChevronsUpDown, ListFilter, X } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type DataTableFilterDropdownProps<TData> = {
|
||||
column: Column<TData>;
|
||||
contentClassName?: string;
|
||||
triggerClassName?: string;
|
||||
children: (args: {
|
||||
isOpen: boolean;
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) => React.ReactNode;
|
||||
};
|
||||
|
||||
export function DataTableFilterDropdown<TData>({
|
||||
column,
|
||||
triggerClassName,
|
||||
contentClassName,
|
||||
children,
|
||||
}: DataTableFilterDropdownProps<TData>) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const isFiltered = column.getIsFiltered();
|
||||
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
onClick={() => setIsOpen(true)}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"data-[state=open]:bg-accent",
|
||||
"w-5 h-5",
|
||||
{
|
||||
"text-primary": isFiltered,
|
||||
"text-muted-foreground": !isFiltered,
|
||||
},
|
||||
triggerClassName
|
||||
)}
|
||||
>
|
||||
<ListFilter className={cn("!h-3", "!w-3")} />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className={cn("w-full", "shadow-sm", contentClassName)}
|
||||
>
|
||||
{children({ isOpen, setIsOpen })}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
type DataTableFilterDropdownActionsProps = {
|
||||
className?: string;
|
||||
isClearDisabled?: boolean;
|
||||
isApplyDisabled?: boolean;
|
||||
onClear: () => void;
|
||||
onApply: () => void;
|
||||
};
|
||||
|
||||
export function DataTableFilterDropdownActions({
|
||||
className,
|
||||
isClearDisabled,
|
||||
isApplyDisabled,
|
||||
onClear,
|
||||
onApply,
|
||||
}: DataTableFilterDropdownActionsProps) {
|
||||
const t = useTranslate();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
"items-center",
|
||||
"justify-between",
|
||||
"w-full",
|
||||
"gap-2",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
disabled={isClearDisabled}
|
||||
className={cn(
|
||||
"rounded-sm",
|
||||
"text-xs",
|
||||
"font-semibold",
|
||||
"text-muted-foreground"
|
||||
)}
|
||||
onClick={() => {
|
||||
onClear();
|
||||
}}
|
||||
>
|
||||
<X className={cn("w-3.5", "h-3.5", "text-muted-foreground")} />
|
||||
{t("buttons.clear", "Clear")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={isApplyDisabled}
|
||||
className={cn("rounded-sm", "text-xs", "font-semibold")}
|
||||
onClick={() => {
|
||||
onApply();
|
||||
}}
|
||||
>
|
||||
{t("buttons.apply", "Apply")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type DataTableFilterDropdownTextProps<TData> = {
|
||||
column: Column<TData>;
|
||||
table: ReactTable<TData>;
|
||||
defaultOperator?: CrudOperators;
|
||||
operators?: CrudOperators[];
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
export function DataTableFilterDropdownText<TData>({
|
||||
column,
|
||||
table,
|
||||
operators = [
|
||||
"eq",
|
||||
"ne",
|
||||
"contains",
|
||||
"ncontains",
|
||||
"containss",
|
||||
"ncontainss",
|
||||
"startswith",
|
||||
"nstartswith",
|
||||
"startswiths",
|
||||
"nstartswiths",
|
||||
"endswith",
|
||||
"nendswith",
|
||||
"endswiths",
|
||||
"nendswiths",
|
||||
"in",
|
||||
"nin",
|
||||
"ina",
|
||||
"nina",
|
||||
],
|
||||
defaultOperator = "eq",
|
||||
placeholder,
|
||||
}: DataTableFilterDropdownTextProps<TData>) {
|
||||
const t = useTranslate();
|
||||
|
||||
return (
|
||||
<DataTableFilterInput
|
||||
column={column}
|
||||
table={table}
|
||||
operators={operators}
|
||||
defaultOperator={defaultOperator}
|
||||
renderInput={({ value, onChange }) => (
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={
|
||||
placeholder ?? t("table.filter.text.placeholder", "Filter by...")
|
||||
}
|
||||
value={value}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export type DataTableFilterDropdownNumericProps<TData> = {
|
||||
column: Column<TData>;
|
||||
table: ReactTable<TData>;
|
||||
defaultOperator?: CrudOperators;
|
||||
operators?: CrudOperators[];
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
export function DataTableFilterDropdownNumeric<TData>({
|
||||
column,
|
||||
table,
|
||||
operators = ["eq", "ne", "gt", "lt", "gte", "lte"],
|
||||
defaultOperator = "eq",
|
||||
placeholder,
|
||||
}: DataTableFilterDropdownNumericProps<TData>) {
|
||||
const t = useTranslate();
|
||||
|
||||
return (
|
||||
<DataTableFilterInput
|
||||
column={column}
|
||||
table={table}
|
||||
operators={operators}
|
||||
defaultOperator={defaultOperator}
|
||||
renderInput={({ value, onChange }) => (
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={
|
||||
placeholder ?? t("table.filter.numeric.placeholder", "Filter by...")
|
||||
}
|
||||
value={value}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export type DataTableFilterComboboxProps<TData> = {
|
||||
column: Column<TData>;
|
||||
table?: ReactTable<TData>;
|
||||
options: { label: string; value: string }[];
|
||||
defaultOperator?: CrudOperators;
|
||||
operators?: CrudOperators[];
|
||||
placeholder?: string;
|
||||
noResultsText?: string;
|
||||
multiple?: boolean;
|
||||
};
|
||||
|
||||
export function DataTableFilterCombobox<TData>({
|
||||
column,
|
||||
table,
|
||||
options,
|
||||
defaultOperator = "eq",
|
||||
operators = ["eq", "ne", "in", "nin"],
|
||||
placeholder,
|
||||
noResultsText,
|
||||
multiple = false,
|
||||
}: DataTableFilterComboboxProps<TData>) {
|
||||
const t = useTranslate();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<DataTableFilterInput
|
||||
column={column}
|
||||
table={table}
|
||||
operators={operators}
|
||||
defaultOperator={defaultOperator}
|
||||
renderInput={({ value, onChange }) => {
|
||||
const currentValues = multiple
|
||||
? Array.isArray(value)
|
||||
? value
|
||||
: value && typeof value === "string"
|
||||
? [value]
|
||||
: []
|
||||
: value && typeof value === "string"
|
||||
? [value]
|
||||
: [];
|
||||
|
||||
const handleSelect = (optionValue: string) => {
|
||||
if (multiple) {
|
||||
const newValues = currentValues.includes(optionValue)
|
||||
? currentValues.filter((v) => v !== optionValue)
|
||||
: [...currentValues, optionValue];
|
||||
onChange(newValues);
|
||||
} else {
|
||||
onChange(optionValue);
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (optionValue: string) => {
|
||||
if (multiple) {
|
||||
const newValues = currentValues.filter((v) => v !== optionValue);
|
||||
onChange(newValues);
|
||||
}
|
||||
};
|
||||
|
||||
const getDisplayText = () => {
|
||||
if (currentValues.length === 0) {
|
||||
return (
|
||||
placeholder ?? t("table.filter.combobox.placeholder", "Select...")
|
||||
);
|
||||
}
|
||||
|
||||
if (multiple) {
|
||||
return `${currentValues.length} selected`;
|
||||
}
|
||||
|
||||
const selectedOption = options.find(
|
||||
(option) => option.value === currentValues[0]
|
||||
);
|
||||
return selectedOption ? selectedOption.label : currentValues[0];
|
||||
};
|
||||
|
||||
const getSelectedLabels = () => {
|
||||
return currentValues.map((val) => {
|
||||
const option = options.find((opt) => opt.value === val);
|
||||
return { label: option ? option.label : val, value: val };
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={isOpen}
|
||||
className={cn(
|
||||
"w-full",
|
||||
"min-w-48",
|
||||
"max-w-80",
|
||||
"justify-start",
|
||||
"h-auto",
|
||||
"min-h-9"
|
||||
)}
|
||||
>
|
||||
<div className={cn("flex", "gap-2", "w-full")}>
|
||||
{multiple && currentValues.length > 0 ? (
|
||||
<div className={cn("flex", "flex-wrap", "gap-1", "flex-1")}>
|
||||
{getSelectedLabels()
|
||||
.slice(0, 3)
|
||||
.map(({ label, value: val }) => (
|
||||
<Badge
|
||||
key={val}
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"inline-flex",
|
||||
"items-center",
|
||||
"gap-0",
|
||||
"h-4",
|
||||
"pr-0.5",
|
||||
"rounded-sm"
|
||||
)}
|
||||
>
|
||||
<span className={cn("text-[10px]", "leading-4")}>
|
||||
{label}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex",
|
||||
"items-center",
|
||||
"justify-center",
|
||||
"p-0",
|
||||
"w-4",
|
||||
"h-full",
|
||||
"text-muted-foreground",
|
||||
"hover:text-destructive",
|
||||
"rounded-sm",
|
||||
"cursor-pointer",
|
||||
"transition-colors"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleRemove(val);
|
||||
}}
|
||||
>
|
||||
<X className={cn("!h-2", "!w-2")} />
|
||||
</span>
|
||||
</Badge>
|
||||
))}
|
||||
{currentValues.length > 3 && (
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs",
|
||||
"text-muted-foreground",
|
||||
"px-1"
|
||||
)}
|
||||
>
|
||||
+{currentValues.length - 3} more
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span
|
||||
className={cn(
|
||||
"truncate",
|
||||
"flex-1",
|
||||
"text-start",
|
||||
"text-xs",
|
||||
currentValues.length === 0 && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{getDisplayText()}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<ChevronsUpDown
|
||||
className={cn("h-4", "w-4", "shrink-0", "opacity-50")}
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className={cn("w-[200px]", "p-0")} align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder={t("table.filter.combobox.search", "Search...")}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{noResultsText ??
|
||||
t(
|
||||
"table.filter.combobox.noResults",
|
||||
"Results not found."
|
||||
)}
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{options.map((option) => (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
onSelect={() => handleSelect(option.value)}
|
||||
keywords={option.label?.split(" ") ?? []}
|
||||
>
|
||||
{option.label}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto",
|
||||
"h-4",
|
||||
"w-4",
|
||||
currentValues.includes(option.value)
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export type DataTableFilterDropdownDateSinglePickerProps<TData> = {
|
||||
column: Column<TData>;
|
||||
defaultOperator?: CrudOperators;
|
||||
formatDate?: (date: Date | undefined) => string | undefined;
|
||||
};
|
||||
|
||||
export function DataTableFilterDropdownDateSinglePicker<TData>({
|
||||
column,
|
||||
defaultOperator = "eq",
|
||||
formatDate,
|
||||
}: DataTableFilterDropdownDateSinglePickerProps<TData>) {
|
||||
const columnFilterValue = column.getFilterValue() as string;
|
||||
|
||||
const parseDate = (value: string | undefined): Date | undefined => {
|
||||
if (!value) return undefined;
|
||||
|
||||
const date = new Date(value);
|
||||
|
||||
if (Number.isNaN(date.getTime())) return undefined;
|
||||
return date;
|
||||
};
|
||||
|
||||
const [filterValue, setFilterValue] = useState<Date | undefined>(() =>
|
||||
parseDate(columnFilterValue)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
column.columnDef.meta = {
|
||||
...column.columnDef.meta,
|
||||
filterOperator: defaultOperator,
|
||||
};
|
||||
}, [defaultOperator, column]);
|
||||
|
||||
useEffect(() => {
|
||||
setFilterValue(parseDate(columnFilterValue));
|
||||
}, [columnFilterValue]);
|
||||
|
||||
const hasDate = !!filterValue;
|
||||
|
||||
const handleApply = () => {
|
||||
if (!filterValue) return;
|
||||
|
||||
const value = formatDate?.(filterValue) ?? filterValue.toISOString();
|
||||
column.setFilterValue(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<DataTableFilterDropdown
|
||||
column={column}
|
||||
contentClassName={cn("w-fit", "p-0")}
|
||||
>
|
||||
{({ setIsOpen }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn("flex", "flex-col", "items-center")}
|
||||
onKeyDown={(event) => {
|
||||
if (!hasDate) return;
|
||||
if (event.key === "Enter") {
|
||||
handleApply();
|
||||
setIsOpen(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={filterValue}
|
||||
onSelect={(date) => {
|
||||
setFilterValue(date);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className={cn("w-full")}>
|
||||
<Separator />
|
||||
</div>
|
||||
|
||||
<DataTableFilterDropdownActions
|
||||
className={cn("p-4")}
|
||||
isApplyDisabled={!hasDate}
|
||||
onClear={() => {
|
||||
column.setFilterValue(undefined);
|
||||
setFilterValue(undefined);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
onApply={() => {
|
||||
handleApply();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</DataTableFilterDropdown>
|
||||
);
|
||||
}
|
||||
|
||||
export type DataTableFilterDropdownDateRangePickerProps<TData> = {
|
||||
column: Column<TData>;
|
||||
defaultOperator?: CrudOperators;
|
||||
formatDateRange?: (dateRange: DateRange | undefined) => string[] | undefined;
|
||||
};
|
||||
|
||||
export function DataTableFilterDropdownDateRangePicker<TData>({
|
||||
column,
|
||||
defaultOperator = "between",
|
||||
formatDateRange,
|
||||
}: DataTableFilterDropdownDateRangePickerProps<TData>) {
|
||||
const columnFilterValue = column.getFilterValue() as string[];
|
||||
|
||||
const parseDateRange = (
|
||||
value: string[] | undefined
|
||||
): DateRange | undefined => {
|
||||
if (!value || !Array.isArray(value) || value.length !== 2) return undefined;
|
||||
|
||||
const from = value[0] ? new Date(value[0]) : undefined;
|
||||
const to = value[1] ? new Date(value[1]) : undefined;
|
||||
|
||||
if (
|
||||
!from ||
|
||||
!to ||
|
||||
Number.isNaN(from.getTime()) ||
|
||||
Number.isNaN(to.getTime())
|
||||
)
|
||||
return undefined;
|
||||
return { from, to };
|
||||
};
|
||||
|
||||
const [filterValue, setFilterValue] = useState<DateRange | undefined>(() =>
|
||||
parseDateRange(columnFilterValue)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
column.columnDef.meta = {
|
||||
...column.columnDef.meta,
|
||||
filterOperator: defaultOperator,
|
||||
};
|
||||
}, [defaultOperator, column]);
|
||||
|
||||
useEffect(() => {
|
||||
setFilterValue(parseDateRange(columnFilterValue));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- objects are always different
|
||||
}, [JSON.stringify(columnFilterValue)]);
|
||||
|
||||
const hasDateRange = filterValue?.from && filterValue?.to;
|
||||
|
||||
const handleApply = () => {
|
||||
if (!filterValue?.from || !filterValue?.to) return;
|
||||
|
||||
const values = formatDateRange?.(filterValue) ?? [
|
||||
filterValue.from.toISOString(),
|
||||
filterValue.to.toISOString(),
|
||||
];
|
||||
column.setFilterValue(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<DataTableFilterDropdown
|
||||
column={column}
|
||||
contentClassName={cn("w-fit", "p-0")}
|
||||
>
|
||||
{({ setIsOpen }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn("flex", "flex-col", "items-center")}
|
||||
onKeyDown={(event) => {
|
||||
if (!hasDateRange) return;
|
||||
if (event.key === "Enter") {
|
||||
handleApply();
|
||||
setIsOpen(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Calendar
|
||||
mode="range"
|
||||
numberOfMonths={2}
|
||||
selected={filterValue}
|
||||
onSelect={(date) => {
|
||||
setFilterValue({
|
||||
from: date?.from,
|
||||
to: date?.to,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className={cn("w-full")}>
|
||||
<Separator />
|
||||
</div>
|
||||
|
||||
<DataTableFilterDropdownActions
|
||||
className={cn("p-4")}
|
||||
isApplyDisabled={!hasDateRange}
|
||||
onClear={() => {
|
||||
column.setFilterValue(undefined);
|
||||
setFilterValue(undefined);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
onApply={() => {
|
||||
handleApply();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</DataTableFilterDropdown>
|
||||
);
|
||||
}
|
||||
|
||||
export type DataTableFilterInputProps<TData> = {
|
||||
column: Column<TData>;
|
||||
table?: ReactTable<TData>;
|
||||
defaultOperator?: CrudOperators;
|
||||
operators?: CrudOperators[];
|
||||
renderInput: (props: {
|
||||
value: string | string[];
|
||||
onChange: (value: string | string[]) => void;
|
||||
}) => React.ReactNode;
|
||||
};
|
||||
|
||||
export function DataTableFilterInput<TData>({
|
||||
column: columnFromProps,
|
||||
table: tableFromProps,
|
||||
operators: operatorsFromProps,
|
||||
defaultOperator: defaultOperatorFromProps,
|
||||
renderInput,
|
||||
}: DataTableFilterInputProps<TData>) {
|
||||
const [filterValue, setFilterValue] = useState(
|
||||
(columnFromProps.getFilterValue() as string | string[]) || ""
|
||||
);
|
||||
|
||||
const [operator, setOperator] = useState<CrudOperators>(() => {
|
||||
if (!tableFromProps) {
|
||||
return defaultOperatorFromProps || "eq";
|
||||
}
|
||||
|
||||
const columnFilter = tableFromProps
|
||||
.getState()
|
||||
.columnFilters.find((filter) => {
|
||||
return filter.id === columnFromProps.id;
|
||||
});
|
||||
|
||||
if (columnFilter && "operator" in columnFilter) {
|
||||
return columnFilter.operator as CrudOperators;
|
||||
}
|
||||
|
||||
return defaultOperatorFromProps || "eq";
|
||||
});
|
||||
|
||||
const handleApply = () => {
|
||||
columnFromProps.setFilterValue(filterValue);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
columnFromProps.setFilterValue(undefined);
|
||||
setFilterValue("");
|
||||
};
|
||||
|
||||
const handleOperatorChange = (value: CrudOperators) => {
|
||||
setOperator(value);
|
||||
columnFromProps.columnDef.meta = {
|
||||
...columnFromProps.columnDef.meta,
|
||||
filterOperator: value,
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<DataTableFilterDropdown column={columnFromProps}>
|
||||
{({ setIsOpen }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
"flex-col",
|
||||
"items-center",
|
||||
"gap-4",
|
||||
"w-full"
|
||||
)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
handleApply();
|
||||
setIsOpen(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn("grid", "grid-cols-1", "md:grid-cols-2", "gap-2")}
|
||||
>
|
||||
{operatorsFromProps && operatorsFromProps.length > 1 && (
|
||||
<DataTableFilterOperatorSelect
|
||||
value={operator}
|
||||
operators={operatorsFromProps}
|
||||
onValueChange={handleOperatorChange}
|
||||
/>
|
||||
)}
|
||||
{renderInput({
|
||||
value: filterValue,
|
||||
onChange: setFilterValue,
|
||||
})}
|
||||
</div>
|
||||
<div className={cn("w-full")}>
|
||||
<Separator />
|
||||
</div>
|
||||
<DataTableFilterDropdownActions
|
||||
onClear={() => {
|
||||
handleClear();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
onApply={() => {
|
||||
handleApply();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</DataTableFilterDropdown>
|
||||
);
|
||||
}
|
||||
|
||||
const CRUD_OPERATOR_LABELS: Record<
|
||||
Exclude<CrudOperators, "or" | "and">,
|
||||
{ i18nKey: string; defaultLabel: string }
|
||||
> = {
|
||||
eq: { i18nKey: "table.filter.operator.eq", defaultLabel: "Equals" },
|
||||
ne: { i18nKey: "table.filter.operator.ne", defaultLabel: "Not equals" },
|
||||
lt: { i18nKey: "table.filter.operator.lt", defaultLabel: "Less than" },
|
||||
gt: { i18nKey: "table.filter.operator.gt", defaultLabel: "Greater than" },
|
||||
lte: {
|
||||
i18nKey: "table.filter.operator.lte",
|
||||
defaultLabel: "Less than or equal",
|
||||
},
|
||||
gte: {
|
||||
i18nKey: "table.filter.operator.gte",
|
||||
defaultLabel: "Greater than or equal",
|
||||
},
|
||||
in: {
|
||||
i18nKey: "table.filter.operator.in",
|
||||
defaultLabel: "Includes in an array",
|
||||
},
|
||||
nin: {
|
||||
i18nKey: "table.filter.operator.nin",
|
||||
defaultLabel: "Not includes in an array",
|
||||
},
|
||||
ina: {
|
||||
i18nKey: "table.filter.operator.ina",
|
||||
defaultLabel: "Includes in an array (case sensitive)",
|
||||
},
|
||||
nina: {
|
||||
i18nKey: "table.filter.operator.nina",
|
||||
defaultLabel: "Not includes in an array (case sensitive)",
|
||||
},
|
||||
contains: {
|
||||
i18nKey: "table.filter.operator.contains",
|
||||
defaultLabel: "Contains",
|
||||
},
|
||||
ncontains: {
|
||||
i18nKey: "table.filter.operator.ncontains",
|
||||
defaultLabel: "Not contains",
|
||||
},
|
||||
containss: {
|
||||
i18nKey: "table.filter.operator.containss",
|
||||
defaultLabel: "Contains (case sensitive)",
|
||||
},
|
||||
ncontainss: {
|
||||
i18nKey: "table.filter.operator.ncontainss",
|
||||
defaultLabel: "Not contains (case sensitive)",
|
||||
},
|
||||
between: {
|
||||
i18nKey: "table.filter.operator.between",
|
||||
defaultLabel: "Between",
|
||||
},
|
||||
nbetween: {
|
||||
i18nKey: "table.filter.operator.nbetween",
|
||||
defaultLabel: "Not between",
|
||||
},
|
||||
null: { i18nKey: "table.filter.operator.null", defaultLabel: "Is null" },
|
||||
nnull: {
|
||||
i18nKey: "table.filter.operator.nnull",
|
||||
defaultLabel: "Is not null",
|
||||
},
|
||||
startswith: {
|
||||
i18nKey: "table.filter.operator.startswith",
|
||||
defaultLabel: "Starts with",
|
||||
},
|
||||
nstartswith: {
|
||||
i18nKey: "table.filter.operator.nstartswith",
|
||||
defaultLabel: "Not starts with",
|
||||
},
|
||||
startswiths: {
|
||||
i18nKey: "table.filter.operator.startswiths",
|
||||
defaultLabel: "Starts with (case sensitive)",
|
||||
},
|
||||
nstartswiths: {
|
||||
i18nKey: "table.filter.operator.nstartswiths",
|
||||
defaultLabel: "Not starts with (case sensitive)",
|
||||
},
|
||||
endswith: {
|
||||
i18nKey: "table.filter.operator.endswith",
|
||||
defaultLabel: "Ends with",
|
||||
},
|
||||
nendswith: {
|
||||
i18nKey: "table.filter.operator.nendswith",
|
||||
defaultLabel: "Not ends with",
|
||||
},
|
||||
endswiths: {
|
||||
i18nKey: "table.filter.operator.endswiths",
|
||||
defaultLabel: "Ends with (case sensitive)",
|
||||
},
|
||||
nendswiths: {
|
||||
i18nKey: "table.filter.operator.nendswiths",
|
||||
defaultLabel: "Not ends with (case sensitive)",
|
||||
},
|
||||
eqs: {
|
||||
i18nKey: "table.filter.operator.eqs",
|
||||
defaultLabel: "Equals (case sensitive)",
|
||||
},
|
||||
nes: {
|
||||
i18nKey: "table.filter.operator.nes",
|
||||
defaultLabel: "Not equals (case sensitive)",
|
||||
},
|
||||
};
|
||||
|
||||
export type DataTableFilterOperatorSelectProps = {
|
||||
value: CrudOperators;
|
||||
onValueChange: (value: CrudOperators) => void;
|
||||
operators?: CrudOperators[];
|
||||
placeholder?: string;
|
||||
triggerClassName?: string;
|
||||
contentClassName?: string;
|
||||
};
|
||||
|
||||
export function DataTableFilterOperatorSelect({
|
||||
value,
|
||||
onValueChange,
|
||||
operators: operatorsFromProps,
|
||||
placeholder,
|
||||
triggerClassName,
|
||||
contentClassName,
|
||||
}: DataTableFilterOperatorSelectProps) {
|
||||
const t = useTranslate();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const operators = useMemo(() => {
|
||||
return Object.entries(CRUD_OPERATOR_LABELS).filter(([operator]) =>
|
||||
operatorsFromProps?.includes(operator as CrudOperators)
|
||||
);
|
||||
}, [operatorsFromProps]);
|
||||
|
||||
const selectedLabel = t(
|
||||
CRUD_OPERATOR_LABELS[value as Exclude<CrudOperators, "or" | "and">].i18nKey,
|
||||
CRUD_OPERATOR_LABELS[value as Exclude<CrudOperators, "or" | "and">]
|
||||
.defaultLabel
|
||||
);
|
||||
const placeholderText =
|
||||
placeholder ?? t("table.filter.operator.placeholder", "Search operator...");
|
||||
const noResultsText = t(
|
||||
"table.filter.operator.noResults",
|
||||
"No operator found."
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn(
|
||||
"w-full",
|
||||
"justify-between",
|
||||
"truncate",
|
||||
triggerClassName
|
||||
)}
|
||||
>
|
||||
<div className={cn("truncate")}>
|
||||
{selectedLabel ?? placeholderText}
|
||||
</div>
|
||||
<ChevronsUpDown
|
||||
className={cn("ml-2", "h-4", "w-4", "shrink-0", "opacity-50")}
|
||||
/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className={cn("p-0", contentClassName)} forceMount>
|
||||
<Command>
|
||||
<CommandInput placeholder={placeholderText} />
|
||||
<CommandList>
|
||||
<CommandEmpty>{noResultsText}</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{operators.map(([op, { i18nKey, defaultLabel }]) => (
|
||||
<CommandItem
|
||||
key={op}
|
||||
value={op}
|
||||
onSelect={() => {
|
||||
onValueChange(op as CrudOperators);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2",
|
||||
"h-4",
|
||||
"w-4",
|
||||
value === op ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{t(i18nKey, defaultLabel)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
DataTableFilterDropdown.displayName = "DataTableFilterDropdown";
|
||||
DataTableFilterDropdownText.displayName = "DataTableFilterDropdownText";
|
||||
DataTableFilterCombobox.displayName = "DataTableFilterCombobox";
|
||||
DataTableFilterDropdownDateRangePicker.displayName =
|
||||
"DataTableFilterDropdownDateRangePicker";
|
||||
DataTableFilterOperatorSelect.displayName = "DataTableFilterOperatorSelect";
|
||||
DataTableFilterDropdownActions.displayName = "DataTableFilterDropdownActions";
|
||||
DataTableFilterDropdownNumeric.displayName = "DataTableFilterDropdownNumeric";
|
||||
DataTableFilterInput.displayName = "DataTableFilterInput";
|
||||
DataTableFilterOperatorSelect.displayName = "DataTableFilterOperatorSelect";
|
||||
DataTableFilterDropdownDateSinglePicker.displayName =
|
||||
"DataTableFilterDropdownDateSinglePicker";
|
||||
@@ -0,0 +1,146 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronsLeft,
|
||||
ChevronsRight,
|
||||
} from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type DataTablePaginationProps = {
|
||||
currentPage: number;
|
||||
pageCount: number;
|
||||
setCurrentPage: (page: number) => void;
|
||||
pageSize: number;
|
||||
setPageSize: (size: number) => void;
|
||||
total?: number;
|
||||
};
|
||||
|
||||
export function DataTablePagination({
|
||||
currentPage,
|
||||
pageCount,
|
||||
setCurrentPage,
|
||||
pageSize,
|
||||
setPageSize,
|
||||
total,
|
||||
}: DataTablePaginationProps) {
|
||||
const pageSizeOptions = useMemo(() => {
|
||||
const baseOptions = [10, 20, 30, 40, 50];
|
||||
const optionsSet = new Set(baseOptions);
|
||||
|
||||
if (!optionsSet.has(pageSize)) {
|
||||
optionsSet.add(pageSize);
|
||||
}
|
||||
|
||||
return Array.from(optionsSet).sort((a, b) => a - b);
|
||||
}, [pageSize]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
"items-center",
|
||||
"justify-between",
|
||||
"flex-wrap",
|
||||
"px-2",
|
||||
"w-full",
|
||||
"gap-2"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex-1",
|
||||
"text-sm",
|
||||
"text-muted-foreground",
|
||||
"whitespace-nowrap"
|
||||
)}
|
||||
>
|
||||
{typeof total === "number" ? `${total} row(s)` : null}
|
||||
</div>
|
||||
<div className={cn("flex", "items-center", "flex-wrap", "gap-2")}>
|
||||
<div className={cn("flex", "items-center", "gap-2")}>
|
||||
<span className={cn("text-sm", "font-medium")}>Rows per page</span>
|
||||
<Select
|
||||
value={`${pageSize}`}
|
||||
onValueChange={(v) => setPageSize(Number(v))}
|
||||
>
|
||||
<SelectTrigger className={cn("h-8", "w-[70px]")}>
|
||||
<SelectValue placeholder={pageSize} />
|
||||
</SelectTrigger>
|
||||
<SelectContent side="top">
|
||||
{pageSizeOptions.map((size) => (
|
||||
<SelectItem key={size} value={`${size}`}>
|
||||
{size}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className={cn("flex", "items-center", "flex-wrap", "gap-2")}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
"items-center",
|
||||
"justify-center",
|
||||
"text-sm",
|
||||
"font-medium"
|
||||
)}
|
||||
>
|
||||
Page {currentPage} of {pageCount}
|
||||
</div>
|
||||
<div className={cn("flex", "items-center", "gap-2")}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn("hidden", "h-8", "w-8", "p-0", "lg:flex")}
|
||||
onClick={() => setCurrentPage(1)}
|
||||
disabled={currentPage === 1}
|
||||
aria-label="Go to first page"
|
||||
>
|
||||
<ChevronsLeft />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn("h-8", "w-8", "p-0")}
|
||||
onClick={() => setCurrentPage(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
aria-label="Go to previous page"
|
||||
>
|
||||
<ChevronLeft />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn("h-8", "w-8", "p-0")}
|
||||
onClick={() => setCurrentPage(currentPage + 1)}
|
||||
disabled={currentPage === pageCount}
|
||||
aria-label="Go to next page"
|
||||
>
|
||||
<ChevronRight />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn("hidden", "h-8", "w-8", "p-0", "lg:flex")}
|
||||
onClick={() => setCurrentPage(pageCount)}
|
||||
disabled={currentPage === pageCount}
|
||||
aria-label="Go to last page"
|
||||
>
|
||||
<ChevronsRight />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
DataTablePagination.displayName = "DataTablePagination";
|
||||
@@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import type { Column } from "@tanstack/react-table";
|
||||
import { ArrowDown, ArrowUp, ChevronsUpDown } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type DataTableSorterProps<TData> = {
|
||||
column: Column<TData>;
|
||||
} & React.ComponentProps<typeof Button>;
|
||||
|
||||
export function DataTableSorter<TData>({
|
||||
column,
|
||||
className,
|
||||
...props
|
||||
}: DataTableSorterProps<TData>) {
|
||||
const title =
|
||||
column.getIsSorted() === "desc"
|
||||
? `Sort by ${column.id} as descending`
|
||||
: column.getIsSorted() === "asc"
|
||||
? `Sort by ${column.id} as ascending`
|
||||
: `Sort by ${column.id}`;
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => column.toggleSorting(undefined, true)}
|
||||
title={title}
|
||||
aria-label={title}
|
||||
{...props}
|
||||
className={cn("data-[state=open]:bg-accent", "w-5 h-5", className)}
|
||||
>
|
||||
{column.getIsSorted() === "desc" ? (
|
||||
<ArrowDown className={cn("text-primary", "!w-3", "!h-3")} />
|
||||
) : column.getIsSorted() === "asc" ? (
|
||||
<ArrowUp className={cn("text-primary", "!w-3", "!h-3")} />
|
||||
) : (
|
||||
<ChevronsUpDown
|
||||
className={cn("text-muted-foreground", "!w-3", "!h-3")}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
DataTableSorter.displayName = "DataTableSorter";
|
||||
@@ -0,0 +1,316 @@
|
||||
"use client";
|
||||
|
||||
import type { HttpError, BaseRecord } from "@refinedev/core";
|
||||
import type { UseTableReturnType } from "@refinedev/react-table";
|
||||
import type { Column } from "@tanstack/react-table";
|
||||
import { flexRender } from "@tanstack/react-table";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { DataTablePagination } from "@/components/refine-ui/data-table/data-table-pagination";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type DataTableProps<TData extends BaseRecord> = {
|
||||
table: UseTableReturnType<TData, HttpError>;
|
||||
};
|
||||
|
||||
export function DataTable<TData extends BaseRecord>({
|
||||
table,
|
||||
}: DataTableProps<TData>) {
|
||||
const {
|
||||
reactTable: { getHeaderGroups, getRowModel, getAllColumns },
|
||||
refineCore: {
|
||||
tableQuery,
|
||||
currentPage,
|
||||
setCurrentPage,
|
||||
pageCount,
|
||||
pageSize,
|
||||
setPageSize,
|
||||
},
|
||||
} = table;
|
||||
|
||||
const columns = getAllColumns();
|
||||
const leafColumns = table.reactTable.getAllLeafColumns();
|
||||
const isLoading = tableQuery.isLoading;
|
||||
|
||||
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||
const tableRef = useRef<HTMLTableElement>(null);
|
||||
const [isOverflowing, setIsOverflowing] = useState({
|
||||
horizontal: false,
|
||||
vertical: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const checkOverflow = () => {
|
||||
if (tableRef.current && tableContainerRef.current) {
|
||||
const table = tableRef.current;
|
||||
const container = tableContainerRef.current;
|
||||
|
||||
const horizontalOverflow = table.offsetWidth > container.clientWidth;
|
||||
const verticalOverflow = table.offsetHeight > container.clientHeight;
|
||||
|
||||
setIsOverflowing({
|
||||
horizontal: horizontalOverflow,
|
||||
vertical: verticalOverflow,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
checkOverflow();
|
||||
|
||||
// Check on window resize
|
||||
window.addEventListener("resize", checkOverflow);
|
||||
|
||||
// Check when table data changes
|
||||
const timeoutId = setTimeout(checkOverflow, 100);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", checkOverflow);
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [tableQuery.data?.data, pageSize]);
|
||||
|
||||
return (
|
||||
<div className={cn("flex", "flex-col", "flex-1", "gap-4")}>
|
||||
<div ref={tableContainerRef} className={cn("rounded-md", "border")}>
|
||||
<Table ref={tableRef} style={{ tableLayout: "fixed", width: "100%" }}>
|
||||
<TableHeader>
|
||||
{getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
const isPlaceholder = header.isPlaceholder;
|
||||
|
||||
return (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
style={{
|
||||
...getCommonStyles({
|
||||
column: header.column,
|
||||
isOverflowing: isOverflowing,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{isPlaceholder ? null : (
|
||||
<div className={cn("flex", "items-center", "gap-1")}>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody className="relative">
|
||||
{isLoading ? (
|
||||
<>
|
||||
{Array.from({ length: pageSize < 1 ? 1 : pageSize }).map(
|
||||
(_, rowIndex) => (
|
||||
<TableRow
|
||||
key={`skeleton-row-${rowIndex}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{leafColumns.map((column) => (
|
||||
<TableCell
|
||||
key={`skeleton-cell-${rowIndex}-${column.id}`}
|
||||
style={{
|
||||
...getCommonStyles({
|
||||
column,
|
||||
isOverflowing: isOverflowing,
|
||||
}),
|
||||
}}
|
||||
className={cn("truncate")}
|
||||
>
|
||||
<div className="h-8" />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
)
|
||||
)}
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className={cn("absolute", "inset-0", "pointer-events-none")}
|
||||
>
|
||||
<Loader2
|
||||
className={cn(
|
||||
"absolute",
|
||||
"top-1/2",
|
||||
"left-1/2",
|
||||
"animate-spin",
|
||||
"text-primary",
|
||||
"h-8",
|
||||
"w-8",
|
||||
"-translate-x-1/2",
|
||||
"-translate-y-1/2"
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
) : getRowModel().rows?.length ? (
|
||||
getRowModel().rows.map((row) => {
|
||||
return (
|
||||
<TableRow
|
||||
key={row.original?.id ?? row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
return (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
style={{
|
||||
...getCommonStyles({
|
||||
column: cell.column,
|
||||
isOverflowing: isOverflowing,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<div className="truncate">
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<DataTableNoData
|
||||
isOverflowing={isOverflowing}
|
||||
columnsLength={columns.length}
|
||||
/>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
{!isLoading && getRowModel().rows?.length > 0 && (
|
||||
<DataTablePagination
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
setCurrentPage={setCurrentPage}
|
||||
pageSize={pageSize}
|
||||
setPageSize={setPageSize}
|
||||
total={tableQuery.data?.total}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DataTableNoData({
|
||||
isOverflowing,
|
||||
columnsLength,
|
||||
}: {
|
||||
isOverflowing: { horizontal: boolean; vertical: boolean };
|
||||
columnsLength: number;
|
||||
}) {
|
||||
return (
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableCell
|
||||
colSpan={columnsLength}
|
||||
className={cn("relative", "text-center")}
|
||||
style={{ height: "490px" }}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute",
|
||||
"inset-0",
|
||||
"flex",
|
||||
"flex-col",
|
||||
"items-center",
|
||||
"justify-center",
|
||||
"gap-2",
|
||||
"bg-background"
|
||||
)}
|
||||
style={{
|
||||
position: isOverflowing.horizontal ? "sticky" : "absolute",
|
||||
left: isOverflowing.horizontal ? "50%" : "50%",
|
||||
transform: "translateX(-50%)",
|
||||
zIndex: isOverflowing.horizontal ? 2 : 1,
|
||||
width: isOverflowing.horizontal ? "fit-content" : "100%",
|
||||
minWidth: "300px",
|
||||
}}
|
||||
>
|
||||
<div className={cn("text-lg", "font-semibold", "text-foreground")}>
|
||||
No data to display
|
||||
</div>
|
||||
<div className={cn("text-sm", "text-muted-foreground")}>
|
||||
This table is empty for the time being.
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export function getCommonStyles<TData>({
|
||||
column,
|
||||
isOverflowing,
|
||||
}: {
|
||||
column: Column<TData>;
|
||||
isOverflowing: {
|
||||
horizontal: boolean;
|
||||
vertical: boolean;
|
||||
};
|
||||
}): React.CSSProperties {
|
||||
const isPinned = column.getIsPinned();
|
||||
const isLastLeftPinnedColumn =
|
||||
isPinned === "left" && column.getIsLastColumn("left");
|
||||
const isFirstRightPinnedColumn =
|
||||
isPinned === "right" && column.getIsFirstColumn("right");
|
||||
|
||||
return {
|
||||
boxShadow:
|
||||
isOverflowing.horizontal && isLastLeftPinnedColumn
|
||||
? "-4px 0 4px -4px var(--border) inset"
|
||||
: isOverflowing.horizontal && isFirstRightPinnedColumn
|
||||
? "4px 0 4px -4px var(--border) inset"
|
||||
: undefined,
|
||||
left:
|
||||
isOverflowing.horizontal && isPinned === "left"
|
||||
? `${column.getStart("left")}px`
|
||||
: undefined,
|
||||
right:
|
||||
isOverflowing.horizontal && isPinned === "right"
|
||||
? `${column.getAfter("right")}px`
|
||||
: undefined,
|
||||
opacity: 1,
|
||||
position: isOverflowing.horizontal && isPinned ? "sticky" : "relative",
|
||||
background: isOverflowing.horizontal && isPinned ? "var(--background)" : "",
|
||||
borderTopRightRadius:
|
||||
isOverflowing.horizontal && isPinned === "right"
|
||||
? "var(--radius)"
|
||||
: undefined,
|
||||
borderBottomRightRadius:
|
||||
isOverflowing.horizontal && isPinned === "right"
|
||||
? "var(--radius)"
|
||||
: undefined,
|
||||
borderTopLeftRadius:
|
||||
isOverflowing.horizontal && isPinned === "left"
|
||||
? "var(--radius)"
|
||||
: undefined,
|
||||
borderBottomLeftRadius:
|
||||
isOverflowing.horizontal && isPinned === "left"
|
||||
? "var(--radius)"
|
||||
: undefined,
|
||||
width: column.getSize(),
|
||||
zIndex: isOverflowing.horizontal && isPinned ? 1 : 0,
|
||||
};
|
||||
}
|
||||
|
||||
DataTable.displayName = "DataTable";
|
||||
@@ -0,0 +1,129 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
|
||||
import { useForgotPassword, useRefineOptions, useLink } from "@refinedev/core";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const ForgotPasswordForm = () => {
|
||||
const [email, setEmail] = useState("");
|
||||
|
||||
const Link = useLink();
|
||||
|
||||
const { title } = useRefineOptions();
|
||||
|
||||
const { mutate: forgotPassword } = useForgotPassword();
|
||||
|
||||
const handleForgotPassword = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
forgotPassword({
|
||||
email,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
"flex-col",
|
||||
"items-center",
|
||||
"justify-center",
|
||||
"px-6",
|
||||
"py-8",
|
||||
"min-h-svh"
|
||||
)}
|
||||
>
|
||||
<div className={cn("flex", "items-center", "justify-center", "gap-2")}>
|
||||
{title.icon && (
|
||||
<div
|
||||
className={cn("text-foreground", "[&>svg]:w-12", "[&>svg]:h-12")}
|
||||
>
|
||||
{title.icon}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card className={cn("sm:w-[456px]", "p-12", "mt-6")}>
|
||||
<CardHeader className={cn("px-0")}>
|
||||
<CardTitle
|
||||
className={cn(
|
||||
"text-blue-600",
|
||||
"dark:text-blue-400",
|
||||
"text-3xl",
|
||||
"font-semibold"
|
||||
)}
|
||||
>
|
||||
Forgot password
|
||||
</CardTitle>
|
||||
<CardDescription
|
||||
className={cn("text-muted-foreground", "font-medium")}
|
||||
>
|
||||
Enter your email to change your password.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className={cn("px-0")}>
|
||||
<form onSubmit={handleForgotPassword}>
|
||||
<div className={cn("flex", "flex-col", "gap-2")}>
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<div className={cn("flex", "gap-2")}>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder=""
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className={cn("flex-1")}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className={cn(
|
||||
"bg-blue-600",
|
||||
"hover:bg-blue-700",
|
||||
"text-white",
|
||||
"px-6"
|
||||
)}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className={cn("mt-8")}>
|
||||
<Link
|
||||
to="/login"
|
||||
className={cn(
|
||||
"inline-flex",
|
||||
"items-center",
|
||||
"gap-2",
|
||||
"text-sm",
|
||||
"text-muted-foreground",
|
||||
"hover:text-foreground",
|
||||
"transition-colors"
|
||||
)}
|
||||
>
|
||||
<ArrowLeft className={cn("w-4", "h-4")} />
|
||||
<span>Back</span>
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ForgotPasswordForm.displayName = "ForgotPasswordForm";
|
||||
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type InputPasswordProps = React.ComponentProps<"input">;
|
||||
|
||||
export const InputPassword = ({ className, ...props }: InputPasswordProps) => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
return (
|
||||
<div className={cn("relative")}>
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"appearance-none",
|
||||
"absolute right-3 top-1/2 -translate-y-1/2"
|
||||
)}
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff size={18} className={cn("text-gray-500")} />
|
||||
) : (
|
||||
<Eye size={18} className={cn("text-gray-500")} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
InputPassword.displayName = "InputPassword";
|
||||
@@ -0,0 +1,244 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { CircleHelp } from "lucide-react";
|
||||
|
||||
import { useLogin, useRefineOptions, useLink } from "@refinedev/core";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
} from "@/components/ui/card";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { InputPassword } from "@/components/refine-ui/form/input-password";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const SignInForm = () => {
|
||||
const [rememberMe, setRememberMe] = useState(false);
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
const Link = useLink();
|
||||
|
||||
const { title } = useRefineOptions();
|
||||
|
||||
const { mutate: login } = useLogin();
|
||||
|
||||
const handleSignIn = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
login({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSignInWithGoogle = () => {
|
||||
login({
|
||||
providerName: "google",
|
||||
});
|
||||
};
|
||||
|
||||
const handleSignInWithGitHub = () => {
|
||||
login({
|
||||
providerName: "github",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
"flex-col",
|
||||
"items-center",
|
||||
"justify-center",
|
||||
"px-6",
|
||||
"py-8",
|
||||
"min-h-svh"
|
||||
)}
|
||||
>
|
||||
<div className={cn("flex", "items-center", "justify-center")}>
|
||||
{title.icon && (
|
||||
<div
|
||||
className={cn("text-foreground", "[&>svg]:w-12", "[&>svg]:h-12")}
|
||||
>
|
||||
{title.icon}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card className={cn("sm:w-[456px]", "p-12", "mt-6")}>
|
||||
<CardHeader className={cn("px-0")}>
|
||||
<CardTitle
|
||||
className={cn(
|
||||
"text-blue-600",
|
||||
"dark:text-blue-400",
|
||||
"text-3xl",
|
||||
"font-semibold"
|
||||
)}
|
||||
>
|
||||
Sign in
|
||||
</CardTitle>
|
||||
<CardDescription
|
||||
className={cn("text-muted-foreground", "font-medium")}
|
||||
>
|
||||
Welcome back
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<Separator />
|
||||
|
||||
<CardContent className={cn("px-0")}>
|
||||
<form onSubmit={handleSignIn}>
|
||||
<div className={cn("flex", "flex-col", "gap-2")}>
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder=""
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cn("relative", "flex", "flex-col", "gap-2", "mt-6")}
|
||||
>
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<InputPassword
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-between",
|
||||
"flex-wrap",
|
||||
"gap-2",
|
||||
"mt-4"
|
||||
)}
|
||||
>
|
||||
<div className={cn("flex items-center", "space-x-2")}>
|
||||
<Checkbox
|
||||
id="remember"
|
||||
checked={rememberMe}
|
||||
onCheckedChange={(checked) =>
|
||||
setRememberMe(checked === "indeterminate" ? false : checked)
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="remember">Remember me</Label>
|
||||
</div>
|
||||
<Link
|
||||
to="/forgot-password"
|
||||
className={cn(
|
||||
"text-sm",
|
||||
"flex",
|
||||
"items-center",
|
||||
"gap-2",
|
||||
"text-primary hover:underline",
|
||||
"text-blue-600",
|
||||
"dark:text-blue-400"
|
||||
)}
|
||||
>
|
||||
<span>Forgot password</span>
|
||||
<CircleHelp className={cn("w-4", "h-4")} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Button type="submit" size="lg" className={cn("w-full", "mt-6")}>
|
||||
Sign in
|
||||
</Button>
|
||||
|
||||
<div className={cn("flex", "items-center", "gap-4", "mt-6")}>
|
||||
<Separator className={cn("flex-1")} />
|
||||
<span className={cn("text-sm", "text-muted-foreground")}>or</span>
|
||||
<Separator className={cn("flex-1")} />
|
||||
</div>
|
||||
|
||||
<div className={cn("flex", "flex-col", "gap-4", "mt-6")}>
|
||||
<p className={cn("text-sm", "font-medium")}>Sign in using</p>
|
||||
<div className={cn("grid grid-cols-2", "gap-6")}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn("flex", "items-center", "gap-2")}
|
||||
onClick={handleSignInWithGoogle}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
width="21"
|
||||
height="20"
|
||||
viewBox="0 0 21 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18.8375 8.63637C16.1151 8.63503 13.3926 8.6357 10.6702 8.63601C10.6705 9.76521 10.6688 10.8944 10.6708 12.0233C12.2475 12.0229 13.8242 12.0226 15.4005 12.0233C15.2178 13.1053 14.5747 14.0949 13.6628 14.704C13.0895 15.0895 12.4309 15.3397 11.7519 15.4586C11.0685 15.5752 10.3623 15.5902 9.68064 15.4522C8.9874 15.3138 8.32566 15.025 7.74838 14.6179C6.82531 13.9694 6.12086 13.0205 5.75916 11.9527C5.38931 10.8666 5.38659 9.65804 5.76085 8.57294C6.02053 7.80816 6.45275 7.10169 7.02054 6.52677C7.7209 5.80979 8.63145 5.29725 9.61248 5.08707C10.4525 4.90775 11.3383 4.94197 12.1607 5.19078C12.8597 5.40301 13.5041 5.78605 14.032 6.29013C14.5655 5.75959 15.0964 5.22602 15.629 4.6945C15.9083 4.4084 16.2019 4.13482 16.4724 3.84092C15.6636 3.09241 14.7154 2.49071 13.6794 2.11035C11.8143 1.42392 9.7108 1.40935 7.83312 2.05923C5.71711 2.78366 3.91535 4.36606 2.91636 6.36616C2.56856 7.05534 2.31463 7.79094 2.16209 8.54757C1.77834 10.4327 2.04582 12.4426 2.91533 14.1596C3.48044 15.2803 4.29063 16.2766 5.27339 17.0577C6.20055 17.797 7.28124 18.3431 8.42705 18.6479C9.87286 19.0357 11.4119 19.0269 12.8672 18.6957C14.1825 18.393 15.4269 17.7645 16.4205 16.8472C17.4707 15.882 18.2199 14.6105 18.6165 13.244C19.0491 11.7534 19.1088 10.1622 18.8375 8.63637Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div>Google</div>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn("flex", "items-center", "gap-2")}
|
||||
onClick={handleSignInWithGitHub}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
width="21"
|
||||
height="20"
|
||||
viewBox="0 0 21 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.5 1.25C5.66797 1.25 1.75 5.26563 1.75 10.2227C1.75 14.1875 4.25781 17.5469 7.73438 18.7344C8.17188 18.8164 8.33203 18.5391 8.33203 18.3008C8.33203 18.0859 8.32422 17.5234 8.32031 16.7734C5.88672 17.3164 5.37109 15.5703 5.37109 15.5703C4.97266 14.5352 4.39844 14.2578 4.39844 14.2578C3.60547 13.6992 4.45703 13.7109 4.45703 13.7109C5.33594 13.7734 5.79688 14.6367 5.79688 14.6367C6.57812 16.0078 7.84375 15.6133 8.34375 15.3828C8.42188 14.8047 8.64844 14.4062 8.89844 14.1836C6.95703 13.957 4.91406 13.1875 4.91406 9.75C4.91406 8.76953 5.25391 7.96875 5.8125 7.34375C5.72266 7.11719 5.42188 6.20312 5.89844 4.96875C5.89844 4.96875 6.63281 4.72656 8.30469 5.88672C9.00391 5.6875 9.75 5.58984 10.4961 5.58594C11.2383 5.58984 11.9883 5.6875 12.6875 5.88672C14.3594 4.72656 15.0898 4.96875 15.0898 4.96875C15.5664 6.20312 15.2656 7.11719 15.1758 7.34375C15.7344 7.97266 16.0742 8.77344 16.0742 9.75C16.0742 13.1953 14.0273 13.9531 12.0781 14.1758C12.3906 14.4531 12.6719 15 12.6719 15.8359C12.6719 17.0352 12.6602 18.0039 12.6602 18.2969C12.6602 18.5352 12.8164 18.8164 13.2617 18.7266C16.7461 17.543 19.25 14.1836 19.25 10.2227C19.25 5.26563 15.332 1.25 10.5 1.25Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<div>GitHub</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
|
||||
<Separator />
|
||||
|
||||
<CardFooter>
|
||||
<div className={cn("w-full", "text-center text-sm")}>
|
||||
<span className={cn("text-sm", "text-muted-foreground")}>
|
||||
No account?{" "}
|
||||
</span>
|
||||
<Link
|
||||
to="/register"
|
||||
className={cn(
|
||||
"text-green-600",
|
||||
"dark:text-green-400",
|
||||
"font-semibold",
|
||||
"underline"
|
||||
)}
|
||||
>
|
||||
Sign up
|
||||
</Link>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SignInForm.displayName = "SignInForm";
|
||||
@@ -0,0 +1,246 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
useRegister,
|
||||
useRefineOptions,
|
||||
useLink,
|
||||
useNotification,
|
||||
} from "@refinedev/core";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
} from "@/components/ui/card";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { InputPassword } from "@/components/refine-ui/form/input-password";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const SignUpForm = () => {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
|
||||
const { open } = useNotification();
|
||||
|
||||
const Link = useLink();
|
||||
|
||||
const { title } = useRefineOptions();
|
||||
|
||||
const { mutate: register } = useRegister();
|
||||
|
||||
const handleSignUp = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
open?.({
|
||||
type: "error",
|
||||
message: "Passwords don't match",
|
||||
description:
|
||||
"Please make sure both password fields contain the same value.",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
register({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSignUpWithGoogle = () => {
|
||||
register({
|
||||
providerName: "google",
|
||||
});
|
||||
};
|
||||
|
||||
const handleSignUpWithGitHub = () => {
|
||||
register({
|
||||
providerName: "github",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
"flex-col",
|
||||
"items-center",
|
||||
"justify-center",
|
||||
"px-6",
|
||||
"py-8",
|
||||
"min-h-svh"
|
||||
)}
|
||||
>
|
||||
<div className={cn("flex", "items-center", "justify-center", "gap-2")}>
|
||||
{title.icon && (
|
||||
<div
|
||||
className={cn("text-foreground", "[&>svg]:w-12", "[&>svg]:h-12")}
|
||||
>
|
||||
{title.icon}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card className={cn("sm:w-[456px]", "p-12", "mt-6")}>
|
||||
<CardHeader className={cn("px-0")}>
|
||||
<CardTitle
|
||||
className={cn(
|
||||
"text-green-600",
|
||||
"dark:text-green-400",
|
||||
"text-3xl",
|
||||
"font-semibold"
|
||||
)}
|
||||
>
|
||||
Sign up
|
||||
</CardTitle>
|
||||
<CardDescription
|
||||
className={cn("text-muted-foreground", "font-medium")}
|
||||
>
|
||||
Welcome to lorem ipsum dolor.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<Separator />
|
||||
|
||||
<CardContent className={cn("px-0")}>
|
||||
<form onSubmit={handleSignUp}>
|
||||
<div className={cn("flex", "flex-col", "gap-2")}>
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder=""
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn("relative", "flex", "flex-col", "gap-2", "mt-6")}
|
||||
>
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<InputPassword
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn("relative", "flex", "flex-col", "gap-2", "mt-6")}
|
||||
>
|
||||
<Label htmlFor="confirmPassword">Confirm password</Label>
|
||||
<InputPassword
|
||||
id="confirmPassword"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
className={cn(
|
||||
"w-full",
|
||||
"mt-6",
|
||||
"bg-green-600",
|
||||
"hover:bg-green-700",
|
||||
"text-white"
|
||||
)}
|
||||
>
|
||||
Sign up
|
||||
</Button>
|
||||
|
||||
<div className={cn("flex", "items-center", "gap-4", "mt-6")}>
|
||||
<Separator className={cn("flex-1")} />
|
||||
<span className={cn("text-sm", "text-muted-foreground")}>or</span>
|
||||
<Separator className={cn("flex-1")} />
|
||||
</div>
|
||||
|
||||
<div className={cn("flex", "flex-col", "gap-4", "mt-6")}>
|
||||
<div className={cn("grid grid-cols-2", "gap-6")}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn("flex", "items-center", "gap-2")}
|
||||
onClick={handleSignUpWithGoogle}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
width="21"
|
||||
height="20"
|
||||
viewBox="0 0 21 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18.8375 8.63637C16.1151 8.63503 13.3926 8.6357 10.6702 8.63601C10.6705 9.76521 10.6688 10.8944 10.6708 12.0233C12.2475 12.0229 13.8242 12.0226 15.4005 12.0233C15.2178 13.1053 14.5747 14.0949 13.6628 14.704C13.0895 15.0895 12.4309 15.3397 11.7519 15.4586C11.0685 15.5752 10.3623 15.5902 9.68064 15.4522C8.9874 15.3138 8.32566 15.025 7.74838 14.6179C6.82531 13.9694 6.12086 13.0205 5.75916 11.9527C5.38931 10.8666 5.38659 9.65804 5.76085 8.57294C6.02053 7.80816 6.45275 7.10169 7.02054 6.52677C7.7209 5.80979 8.63145 5.29725 9.61248 5.08707C10.4525 4.90775 11.3383 4.94197 12.1607 5.19078C12.8597 5.40301 13.5041 5.78605 14.032 6.29013C14.5655 5.75959 15.0964 5.22602 15.629 4.6945C15.9083 4.4084 16.2019 4.13482 16.4724 3.84092C15.6636 3.09241 14.7154 2.49071 13.6794 2.11035C11.8143 1.42392 9.7108 1.40935 7.83312 2.05923C5.71711 2.78366 3.91535 4.36606 2.91636 6.36616C2.56856 7.05534 2.31463 7.79094 2.16209 8.54757C1.77834 10.4327 2.04582 12.4426 2.91533 14.1596C3.48044 15.2803 4.29063 16.2766 5.27339 17.0577C6.20055 17.797 7.28124 18.3431 8.42705 18.6479C9.87286 19.0357 11.4119 19.0269 12.8672 18.6957C14.1825 18.393 15.4269 17.7645 16.4205 16.8472C17.4707 15.882 18.2199 14.6105 18.6165 13.244C19.0491 11.7534 19.1088 10.1622 18.8375 8.63637Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<div>Google</div>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn("flex", "items-center", "gap-2")}
|
||||
onClick={handleSignUpWithGitHub}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
width="21"
|
||||
height="20"
|
||||
viewBox="0 0 21 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.5 1.25C5.66797 1.25 1.75 5.26563 1.75 10.2227C1.75 14.1875 4.25781 17.5469 7.73438 18.7344C8.17188 18.8164 8.33203 18.5391 8.33203 18.3008C8.33203 18.0859 8.32422 17.5234 8.32031 16.7734C5.88672 17.3164 5.37109 15.5703 5.37109 15.5703C4.97266 14.5352 4.39844 14.2578 4.39844 14.2578C3.60547 13.6992 4.45703 13.7109 4.45703 13.7109C5.33594 13.7734 5.79688 14.6367 5.79688 14.6367C6.57812 16.0078 7.84375 15.6133 8.34375 15.3828C8.42188 14.8047 8.64844 14.4062 8.89844 14.1836C6.95703 13.957 4.91406 13.1875 4.91406 9.75C4.91406 8.76953 5.25391 7.96875 5.8125 7.34375C5.72266 7.11719 5.42188 6.20312 5.89844 4.96875C5.89844 4.96875 6.63281 4.72656 8.30469 5.88672C9.00391 5.6875 9.75 5.58984 10.4961 5.58594C11.2383 5.58984 11.9883 5.6875 12.6875 5.88672C14.3594 4.72656 15.0898 4.96875 15.0898 4.96875C15.5664 6.20312 15.2656 7.11719 15.1758 7.34375C15.7344 7.97266 16.0742 8.77344 16.0742 9.75C16.0742 13.1953 14.0273 13.9531 12.0781 14.1758C12.3906 14.4531 12.6719 15 12.6719 15.8359C12.6719 17.0352 12.6602 18.0039 12.6602 18.2969C12.6602 18.5352 12.8164 18.8164 13.2617 18.7266C16.7461 17.543 19.25 14.1836 19.25 10.2227C19.25 5.26563 15.332 1.25 10.5 1.25Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<div>GitHub</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
|
||||
<Separator />
|
||||
|
||||
<CardFooter>
|
||||
<div className={cn("w-full", "text-center text-sm")}>
|
||||
<span className={cn("text-sm", "text-muted-foreground")}>
|
||||
Have an account?{" "}
|
||||
</span>
|
||||
<Link
|
||||
to="/login"
|
||||
className={cn(
|
||||
"text-blue-600",
|
||||
"dark:text-blue-400",
|
||||
"font-semibold",
|
||||
"underline"
|
||||
)}
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SignUpForm.displayName = "SignUpForm";
|
||||
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment, useMemo } from "react";
|
||||
import { Home } from "lucide-react";
|
||||
import {
|
||||
matchResourceFromRoute,
|
||||
useBreadcrumb,
|
||||
useLink,
|
||||
useResourceParams,
|
||||
} from "@refinedev/core";
|
||||
import {
|
||||
BreadcrumbSeparator as ShadcnBreadcrumbSeparator,
|
||||
BreadcrumbItem as ShadcnBreadcrumbItem,
|
||||
BreadcrumbList as ShadcnBreadcrumbList,
|
||||
BreadcrumbPage as ShadcnBreadcrumbPage,
|
||||
Breadcrumb as ShadcnBreadcrumb,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
|
||||
export function Breadcrumb() {
|
||||
const Link = useLink();
|
||||
const { breadcrumbs } = useBreadcrumb();
|
||||
const { resources } = useResourceParams();
|
||||
const rootRouteResource = matchResourceFromRoute("/", resources);
|
||||
|
||||
const breadCrumbItems = useMemo(() => {
|
||||
const list: {
|
||||
key: string;
|
||||
href: string;
|
||||
Component: React.ReactNode;
|
||||
}[] = [];
|
||||
|
||||
list.push({
|
||||
key: "breadcrumb-item-home",
|
||||
href: rootRouteResource.matchedRoute ?? "/",
|
||||
Component: (
|
||||
<Link to={rootRouteResource.matchedRoute ?? "/"}>
|
||||
{rootRouteResource?.resource?.meta?.icon ?? (
|
||||
<Home className="h-4 w-4" />
|
||||
)}
|
||||
</Link>
|
||||
),
|
||||
});
|
||||
|
||||
for (const { label, href } of breadcrumbs) {
|
||||
list.push({
|
||||
key: `breadcrumb-item-${label}`,
|
||||
href: href ?? "",
|
||||
Component: href ? <Link to={href}>{label}</Link> : <span>{label}</span>,
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
}, [breadcrumbs, Link, rootRouteResource]);
|
||||
|
||||
return (
|
||||
<ShadcnBreadcrumb>
|
||||
<ShadcnBreadcrumbList>
|
||||
{breadCrumbItems.map((item, index) => {
|
||||
if (index === breadCrumbItems.length - 1) {
|
||||
return (
|
||||
<ShadcnBreadcrumbPage key={item.key}>
|
||||
{item.Component}
|
||||
</ShadcnBreadcrumbPage>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment key={item.key}>
|
||||
<ShadcnBreadcrumbItem key={item.key}>
|
||||
{item.Component}
|
||||
</ShadcnBreadcrumbItem>
|
||||
<ShadcnBreadcrumbSeparator />
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</ShadcnBreadcrumbList>
|
||||
</ShadcnBreadcrumb>
|
||||
);
|
||||
}
|
||||
|
||||
Breadcrumb.displayName = "Breadcrumb";
|
||||
@@ -0,0 +1,134 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useGo, useResourceParams, useTranslate } from "@refinedev/core";
|
||||
import { ChevronLeft, InfoIcon } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
/**
|
||||
* When the app is navigated to a non-existent route, refine shows a default error page.
|
||||
* A custom error component can be used for this error page.
|
||||
*
|
||||
* @see {@link https://refine.dev/docs/packages/documentation/routers/} for more details.
|
||||
*/
|
||||
export function ErrorComponent() {
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
|
||||
const translate = useTranslate();
|
||||
const go = useGo();
|
||||
|
||||
const { resource, action } = useResourceParams();
|
||||
|
||||
useEffect(() => {
|
||||
if (resource && action) {
|
||||
setErrorMessage(
|
||||
translate(
|
||||
"pages.error.info",
|
||||
{
|
||||
action: action,
|
||||
resource: resource?.name,
|
||||
},
|
||||
`You may have forgotten to add the "${action}" component to "${resource?.name}" resource.`
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [resource, action, translate]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
"items-center",
|
||||
"justify-center",
|
||||
"bg-background",
|
||||
"my-auto"
|
||||
)}
|
||||
>
|
||||
<div className={cn("text-center", "space-y-8")}>
|
||||
<div className={cn("flex", "justify-center")}>
|
||||
<svg
|
||||
width="199"
|
||||
height="73"
|
||||
viewBox="0 0 199 73"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={cn("w-48", "h-auto")}
|
||||
>
|
||||
<title>404 text</title>
|
||||
<path
|
||||
d="M0.293794 60.0909V45.3636L25.112 1.18182H43.2483L18.9756 44.8182V45.3636H60.4302V60.0909H0.293794ZM34.2483 71V55.7273L34.9302 49.1818V27.3636H51.9756V71H34.2483ZM98.983 72.9091C92.642 72.8864 87.1648 71.4205 82.5511 68.5114C77.9375 65.6023 74.3807 61.4091 71.8807 55.9318C69.3807 50.4545 68.142 43.8864 68.1648 36.2273C68.1875 28.5455 69.4375 22.0227 71.9148 16.6591C74.4148 11.2955 77.9602 7.21591 82.5511 4.42045C87.1648 1.625 92.642 0.22727 98.983 0.22727C105.324 0.22727 110.801 1.63636 115.415 4.45455C120.028 7.25 123.585 11.3295 126.085 16.6932C128.585 22.0568 129.824 28.5682 129.801 36.2273C129.801 43.9318 128.551 50.5227 126.051 56C123.551 61.4773 119.994 65.6705 115.381 68.5795C110.79 71.4659 105.324 72.9091 98.983 72.9091ZM98.983 57.5C102.256 57.5 104.938 55.7955 107.028 52.3864C109.142 48.9545 110.188 43.5682 110.165 36.2273C110.165 31.4318 109.688 27.5114 108.733 24.4659C107.778 21.4205 106.46 19.1705 104.778 17.7159C103.097 16.2386 101.165 15.5 98.983 15.5C95.7102 15.5 93.0398 17.1591 90.9716 20.4773C88.9034 23.7955 87.8466 29.0455 87.8011 36.2273C87.7784 41.1136 88.2443 45.1364 89.1989 48.2955C90.1534 51.4318 91.4716 53.75 93.1534 55.25C94.858 56.75 96.8011 57.5 98.983 57.5ZM138.209 60.0909V45.3636L163.027 1.18182H181.164L156.891 44.8182V45.3636H198.345V60.0909H138.209ZM172.164 71V55.7273L172.845 49.1818V27.3636H189.891V71H172.164Z"
|
||||
fill="url(#paint0_linear_2672_10455)"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_2672_10455"
|
||||
x1="99"
|
||||
y1="-16"
|
||||
x2="99"
|
||||
y2="88"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#D4D4D8" />
|
||||
<stop offset="1" stopColor="#E4E4E7" stopOpacity="0.25" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className={cn("space-y-4")}>
|
||||
<h1 className={cn("text-2xl", "font-semibold", "text-foreground")}>
|
||||
{translate("pages.error.title", "Page not found.")}
|
||||
</h1>
|
||||
|
||||
<div
|
||||
className={cn("flex", "items-center", "justify-center", "gap-2")}
|
||||
>
|
||||
<p className={cn("text-muted-foreground")}>
|
||||
{translate(
|
||||
"pages.error.description",
|
||||
"The page you're looking for does not exist."
|
||||
)}
|
||||
</p>
|
||||
{errorMessage && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoIcon
|
||||
className={cn(
|
||||
"h-4",
|
||||
"w-4",
|
||||
"text-muted-foreground",
|
||||
"cursor-help"
|
||||
)}
|
||||
data-testid="error-component-tooltip"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{errorMessage}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
go({ to: "/" });
|
||||
}}
|
||||
className={cn("flex", "items-center", "gap-2", "mx-auto")}
|
||||
>
|
||||
<ChevronLeft className={cn("h-4", "w-4")} />
|
||||
{translate("pages.error.backHome", "Back to homepage")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ErrorComponent.displayName = "ErrorComponent";
|
||||
@@ -0,0 +1,154 @@
|
||||
import {
|
||||
useRefineOptions,
|
||||
useActiveAuthProvider,
|
||||
useLogout,
|
||||
} from "@refinedev/core";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuContent,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { ThemeToggle } from "@/components/refine-ui/theme/theme-toggle";
|
||||
import { UserAvatar } from "@/components/refine-ui/layout/user-avatar";
|
||||
import { useSidebar, SidebarTrigger } from "@/components/ui/sidebar";
|
||||
import { LogOutIcon } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const Header = () => {
|
||||
const { isMobile } = useSidebar();
|
||||
|
||||
return <>{isMobile ? <MobileHeader /> : <DesktopHeader />}</>;
|
||||
};
|
||||
|
||||
function DesktopHeader() {
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
"sticky",
|
||||
"top-0",
|
||||
"flex",
|
||||
"h-16",
|
||||
"shrink-0",
|
||||
"items-center",
|
||||
"gap-4",
|
||||
"border-b",
|
||||
"border-border",
|
||||
"bg-sidebar",
|
||||
"pr-3",
|
||||
"justify-end",
|
||||
"z-40"
|
||||
)}
|
||||
>
|
||||
<ThemeToggle />
|
||||
<UserDropdown />
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileHeader() {
|
||||
const { open, isMobile } = useSidebar();
|
||||
|
||||
const { title } = useRefineOptions();
|
||||
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
"sticky",
|
||||
"top-0",
|
||||
"flex",
|
||||
"h-12",
|
||||
"shrink-0",
|
||||
"items-center",
|
||||
"gap-2",
|
||||
"border-b",
|
||||
"border-border",
|
||||
"bg-sidebar",
|
||||
"pr-3",
|
||||
"justify-between",
|
||||
"z-40"
|
||||
)}
|
||||
>
|
||||
<SidebarTrigger
|
||||
className={cn("text-muted-foreground", "rotate-180", "ml-1", {
|
||||
"opacity-0": open,
|
||||
"opacity-100": !open || isMobile,
|
||||
"pointer-events-auto": !open || isMobile,
|
||||
"pointer-events-none": open && !isMobile,
|
||||
})}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"whitespace-nowrap",
|
||||
"flex",
|
||||
"flex-row",
|
||||
"h-full",
|
||||
"items-center",
|
||||
"justify-start",
|
||||
"gap-2",
|
||||
"transition-discrete",
|
||||
"duration-200",
|
||||
{
|
||||
"pl-3": !open,
|
||||
"pl-5": open,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div>{title.icon}</div>
|
||||
<h2
|
||||
className={cn(
|
||||
"text-sm",
|
||||
"font-bold",
|
||||
"transition-opacity",
|
||||
"duration-200",
|
||||
{
|
||||
"opacity-0": !open,
|
||||
"opacity-100": open,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{title.text}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<ThemeToggle className={cn("h-8", "w-8")} />
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
const UserDropdown = () => {
|
||||
const { mutate: logout, isPending: isLoggingOut } = useLogout();
|
||||
|
||||
const authProvider = useActiveAuthProvider();
|
||||
|
||||
if (!authProvider?.getIdentity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<UserAvatar />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
logout();
|
||||
}}
|
||||
>
|
||||
<LogOutIcon
|
||||
className={cn("text-destructive", "hover:text-destructive")}
|
||||
/>
|
||||
<span className={cn("text-destructive", "hover:text-destructive")}>
|
||||
{isLoggingOut ? "Logging out..." : "Logout"}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
Header.displayName = "Header";
|
||||
MobileHeader.displayName = "MobileHeader";
|
||||
DesktopHeader.displayName = "DesktopHeader";
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { Header } from "@/components/refine-ui/layout/header";
|
||||
import { ThemeProvider } from "@/components/refine-ui/theme/theme-provider";
|
||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { Sidebar } from "./sidebar";
|
||||
|
||||
export function Layout({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<SidebarProvider>
|
||||
<Sidebar />
|
||||
<SidebarInset>
|
||||
<Header />
|
||||
<main
|
||||
className={cn(
|
||||
"@container/main",
|
||||
"container",
|
||||
"mx-auto",
|
||||
"relative",
|
||||
"w-full",
|
||||
"flex",
|
||||
"flex-col",
|
||||
"flex-1",
|
||||
"px-2",
|
||||
"pt-4",
|
||||
"md:p-4",
|
||||
"lg:px-6",
|
||||
"lg:pt-6"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Layout.displayName = "Layout";
|
||||
@@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface LoadingOverlayProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
loading?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const LoadingOverlay = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
LoadingOverlayProps
|
||||
>(({ className, loading = false, children, ...props }, ref) => {
|
||||
if (!loading) return children;
|
||||
|
||||
return (
|
||||
<div className="relative" ref={ref} {...props}>
|
||||
{children}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 z-50 flex items-center justify-center",
|
||||
"bg-background/60",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
LoadingOverlay.displayName = "LoadingOverlay";
|
||||
@@ -0,0 +1,366 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
useMenu,
|
||||
useLink,
|
||||
useRefineOptions,
|
||||
type TreeMenuItem,
|
||||
} from "@refinedev/core";
|
||||
import {
|
||||
SidebarRail as ShadcnSidebarRail,
|
||||
Sidebar as ShadcnSidebar,
|
||||
SidebarContent as ShadcnSidebarContent,
|
||||
SidebarHeader as ShadcnSidebarHeader,
|
||||
useSidebar as useShadcnSidebar,
|
||||
SidebarTrigger as ShadcnSidebarTrigger,
|
||||
} from "@/components/ui/sidebar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ChevronRight, ListIcon } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Sidebar() {
|
||||
const { open } = useShadcnSidebar();
|
||||
const { menuItems, selectedKey } = useMenu();
|
||||
|
||||
return (
|
||||
<ShadcnSidebar collapsible="icon" className={cn("border-none")}>
|
||||
<ShadcnSidebarRail />
|
||||
<SidebarHeader />
|
||||
<ShadcnSidebarContent
|
||||
className={cn(
|
||||
"transition-discrete",
|
||||
"duration-200",
|
||||
"flex",
|
||||
"flex-col",
|
||||
"gap-2",
|
||||
"pt-2",
|
||||
"pb-2",
|
||||
"border-r",
|
||||
"border-border",
|
||||
{
|
||||
"px-3": open,
|
||||
"px-1": !open,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{menuItems.map((item: TreeMenuItem) => (
|
||||
<SidebarItem
|
||||
key={item.key || item.name}
|
||||
item={item}
|
||||
selectedKey={selectedKey}
|
||||
/>
|
||||
))}
|
||||
</ShadcnSidebarContent>
|
||||
</ShadcnSidebar>
|
||||
);
|
||||
}
|
||||
|
||||
type MenuItemProps = {
|
||||
item: TreeMenuItem;
|
||||
selectedKey?: string;
|
||||
};
|
||||
|
||||
function SidebarItem({ item, selectedKey }: MenuItemProps) {
|
||||
const { open } = useShadcnSidebar();
|
||||
|
||||
if (item.meta?.group) {
|
||||
return <SidebarItemGroup item={item} selectedKey={selectedKey} />;
|
||||
}
|
||||
|
||||
if (item.children && item.children.length > 0) {
|
||||
if (open) {
|
||||
return <SidebarItemCollapsible item={item} selectedKey={selectedKey} />;
|
||||
}
|
||||
return <SidebarItemDropdown item={item} selectedKey={selectedKey} />;
|
||||
}
|
||||
|
||||
return <SidebarItemLink item={item} selectedKey={selectedKey} />;
|
||||
}
|
||||
|
||||
function SidebarItemGroup({ item, selectedKey }: MenuItemProps) {
|
||||
const { children } = item;
|
||||
const { open } = useShadcnSidebar();
|
||||
|
||||
return (
|
||||
<div className={cn("border-t", "border-sidebar-border", "pt-4")}>
|
||||
<span
|
||||
className={cn(
|
||||
"ml-3",
|
||||
"block",
|
||||
"text-xs",
|
||||
"font-semibold",
|
||||
"uppercase",
|
||||
"text-muted-foreground",
|
||||
"transition-all",
|
||||
"duration-200",
|
||||
{
|
||||
"h-8": open,
|
||||
"h-0": !open,
|
||||
"opacity-0": !open,
|
||||
"opacity-100": open,
|
||||
"pointer-events-none": !open,
|
||||
"pointer-events-auto": open,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{getDisplayName(item)}
|
||||
</span>
|
||||
{children && children.length > 0 && (
|
||||
<div className={cn("flex", "flex-col")}>
|
||||
{children.map((child: TreeMenuItem) => (
|
||||
<SidebarItem
|
||||
key={child.key || child.name}
|
||||
item={child}
|
||||
selectedKey={selectedKey}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarItemCollapsible({ item, selectedKey }: MenuItemProps) {
|
||||
const { name, children } = item;
|
||||
|
||||
const chevronIcon = (
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
"h-4",
|
||||
"w-4",
|
||||
"shrink-0",
|
||||
"text-muted-foreground",
|
||||
"transition-transform",
|
||||
"duration-200",
|
||||
"group-data-[state=open]:rotate-90"
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Collapsible key={`collapsible-${name}`} className={cn("w-full", "group")}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarButton item={item} rightIcon={chevronIcon} />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className={cn("ml-6", "flex", "flex-col", "gap-2")}>
|
||||
{children?.map((child: TreeMenuItem) => (
|
||||
<SidebarItem
|
||||
key={child.key || child.name}
|
||||
item={child}
|
||||
selectedKey={selectedKey}
|
||||
/>
|
||||
))}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarItemDropdown({ item, selectedKey }: MenuItemProps) {
|
||||
const { children } = item;
|
||||
const Link = useLink();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarButton item={item} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="right" align="start">
|
||||
{children?.map((child: TreeMenuItem) => {
|
||||
const { key: childKey } = child;
|
||||
const isSelected = childKey === selectedKey;
|
||||
|
||||
return (
|
||||
<DropdownMenuItem key={childKey || child.name} asChild>
|
||||
<Link
|
||||
to={child.route || ""}
|
||||
className={cn("flex w-full items-center gap-2", {
|
||||
"bg-accent text-accent-foreground": isSelected,
|
||||
})}
|
||||
>
|
||||
<ItemIcon
|
||||
icon={child.meta?.icon ?? child.icon}
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
<span>{getDisplayName(child)}</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarItemLink({ item, selectedKey }: MenuItemProps) {
|
||||
const isSelected = item.key === selectedKey;
|
||||
|
||||
return <SidebarButton item={item} isSelected={isSelected} asLink={true} />;
|
||||
}
|
||||
|
||||
function SidebarHeader() {
|
||||
const { title } = useRefineOptions();
|
||||
const { open, isMobile } = useShadcnSidebar();
|
||||
|
||||
return (
|
||||
<ShadcnSidebarHeader
|
||||
className={cn(
|
||||
"p-0",
|
||||
"h-16",
|
||||
"border-b",
|
||||
"border-border",
|
||||
"flex-row",
|
||||
"items-center",
|
||||
"justify-between",
|
||||
"overflow-hidden"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"whitespace-nowrap",
|
||||
"flex",
|
||||
"flex-row",
|
||||
"h-full",
|
||||
"items-center",
|
||||
"justify-start",
|
||||
"gap-2",
|
||||
"transition-discrete",
|
||||
"duration-200",
|
||||
{
|
||||
"pl-3": !open,
|
||||
"pl-5": open,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div>{title.icon}</div>
|
||||
<h2
|
||||
className={cn(
|
||||
"text-sm",
|
||||
"font-bold",
|
||||
"transition-opacity",
|
||||
"duration-200",
|
||||
{
|
||||
"opacity-0": !open,
|
||||
"opacity-100": open,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{title.text}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<ShadcnSidebarTrigger
|
||||
className={cn("text-muted-foreground", "mr-1.5", {
|
||||
"opacity-0": !open,
|
||||
"opacity-100": open || isMobile,
|
||||
"pointer-events-auto": open || isMobile,
|
||||
"pointer-events-none": !open && !isMobile,
|
||||
})}
|
||||
/>
|
||||
</ShadcnSidebarHeader>
|
||||
);
|
||||
}
|
||||
|
||||
function getDisplayName(item: TreeMenuItem) {
|
||||
return item.meta?.label ?? item.label ?? item.name;
|
||||
}
|
||||
|
||||
type IconProps = {
|
||||
icon: React.ReactNode;
|
||||
isSelected?: boolean;
|
||||
};
|
||||
|
||||
function ItemIcon({ icon, isSelected }: IconProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn("w-4", {
|
||||
"text-muted-foreground": !isSelected,
|
||||
"text-sidebar-primary-foreground": isSelected,
|
||||
})}
|
||||
>
|
||||
{icon ?? <ListIcon />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type SidebarButtonProps = React.ComponentProps<typeof Button> & {
|
||||
item: TreeMenuItem;
|
||||
isSelected?: boolean;
|
||||
rightIcon?: React.ReactNode;
|
||||
asLink?: boolean;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
function SidebarButton({
|
||||
item,
|
||||
isSelected = false,
|
||||
rightIcon,
|
||||
asLink = false,
|
||||
className,
|
||||
onClick,
|
||||
...props
|
||||
}: SidebarButtonProps) {
|
||||
const Link = useLink();
|
||||
|
||||
const buttonContent = (
|
||||
<>
|
||||
<ItemIcon icon={item.meta?.icon ?? item.icon} isSelected={isSelected} />
|
||||
<span
|
||||
className={cn("tracking-[-0.00875rem]", {
|
||||
"flex-1": rightIcon,
|
||||
"text-left": rightIcon,
|
||||
"line-clamp-1": !rightIcon,
|
||||
truncate: !rightIcon,
|
||||
"font-normal": !isSelected,
|
||||
"font-semibold": isSelected,
|
||||
"text-sidebar-primary-foreground": isSelected,
|
||||
"text-foreground": !isSelected,
|
||||
})}
|
||||
>
|
||||
{getDisplayName(item)}
|
||||
</span>
|
||||
{rightIcon}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Button
|
||||
asChild={!!(asLink && item.route)}
|
||||
variant="ghost"
|
||||
size="lg"
|
||||
className={cn(
|
||||
"flex w-full items-center justify-start gap-2 py-2 !px-3 text-sm",
|
||||
{
|
||||
"bg-sidebar-primary": isSelected,
|
||||
"hover:!bg-sidebar-primary/90": isSelected,
|
||||
"text-sidebar-primary-foreground": isSelected,
|
||||
"hover:text-sidebar-primary-foreground": isSelected,
|
||||
},
|
||||
className
|
||||
)}
|
||||
onClick={onClick}
|
||||
{...props}
|
||||
>
|
||||
{asLink && item.route ? (
|
||||
<Link to={item.route} className={cn("flex w-full items-center gap-2")}>
|
||||
{buttonContent}
|
||||
</Link>
|
||||
) : (
|
||||
buttonContent
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
Sidebar.displayName = "Sidebar";
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useGetIdentity } from "@refinedev/core";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type User = {
|
||||
id: number;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
fullName: string;
|
||||
email: string;
|
||||
avatar?: string;
|
||||
};
|
||||
|
||||
export function UserAvatar() {
|
||||
const { data: user, isLoading: userIsLoading } = useGetIdentity<User>();
|
||||
|
||||
if (userIsLoading || !user) {
|
||||
return <Skeleton className={cn("h-10", "w-10", "rounded-full")} />;
|
||||
}
|
||||
|
||||
const { fullName, avatar } = user;
|
||||
|
||||
return (
|
||||
<Avatar className={cn("h-10", "w-10")}>
|
||||
{avatar && <AvatarImage src={avatar} alt={fullName} />}
|
||||
<AvatarFallback>{getInitials(fullName)}</AvatarFallback>
|
||||
</Avatar>
|
||||
);
|
||||
}
|
||||
|
||||
const getInitials = (name = "") => {
|
||||
const names = name.split(" ");
|
||||
let initials = names[0].substring(0, 1).toUpperCase();
|
||||
|
||||
if (names.length > 1) {
|
||||
initials += names[names.length - 1].substring(0, 1).toUpperCase();
|
||||
}
|
||||
return initials;
|
||||
};
|
||||
|
||||
UserAvatar.displayName = "UserAvatar";
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useGetIdentity } from "@refinedev/core";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { UserAvatar } from "@/components/refine-ui/layout/user-avatar";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type User = {
|
||||
id: number;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
fullName: string;
|
||||
email: string;
|
||||
avatar?: string;
|
||||
};
|
||||
|
||||
export function UserInfo() {
|
||||
const { data: user, isLoading: userIsLoading } = useGetIdentity<User>();
|
||||
|
||||
if (userIsLoading || !user) {
|
||||
return (
|
||||
<div className={cn("flex", "items-center", "gap-x-2")}>
|
||||
<Skeleton className={cn("h-10", "w-10", "rounded-full")} />
|
||||
<div className={cn("flex", "flex-col", "justify-between", "h-10")}>
|
||||
<Skeleton className={cn("h-4", "w-32")} />
|
||||
<Skeleton className={cn("h-4", "w-24")} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { firstName, lastName, email } = user;
|
||||
|
||||
return (
|
||||
<div className={cn("flex", "items-center", "gap-x-2")}>
|
||||
<UserAvatar />
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
"flex-col",
|
||||
"justify-between",
|
||||
"h-10",
|
||||
"text-left"
|
||||
)}
|
||||
>
|
||||
<span className={cn("text-sm", "font-medium", "text-muted-foreground")}>
|
||||
{firstName} {lastName}
|
||||
</span>
|
||||
<span className={cn("text-xs", "text-muted-foreground")}>{email}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
UserInfo.displayName = "UserInfo";
|
||||
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { Toaster as Sonner, type ToasterProps } from "sonner";
|
||||
import { useTheme } from "@/components/refine-ui/theme/theme-provider";
|
||||
|
||||
export function Toaster({ ...props }: ToasterProps) {
|
||||
const { theme = "system" } = useTheme();
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import React from "react";
|
||||
import { useTranslate } from "@refinedev/core";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type UndoableNotificationProps = {
|
||||
message: string;
|
||||
description?: string;
|
||||
undoableTimeout?: number;
|
||||
cancelMutation?: () => void;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
export function UndoableNotification({
|
||||
message,
|
||||
description,
|
||||
undoableTimeout = 5,
|
||||
cancelMutation,
|
||||
onClose,
|
||||
}: UndoableNotificationProps) {
|
||||
const t = useTranslate();
|
||||
|
||||
React.useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
onClose?.();
|
||||
}, undoableTimeout * 1000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [onClose, undoableTimeout]);
|
||||
|
||||
const handleUndo = () => {
|
||||
cancelMutation?.();
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-card",
|
||||
"text-card-foreground",
|
||||
"rounded-lg",
|
||||
"p-4",
|
||||
"shadow-xl",
|
||||
"border",
|
||||
"border-border",
|
||||
"min-w-[320px]",
|
||||
"max-w-md"
|
||||
)}
|
||||
>
|
||||
<div className={cn("flex", "items-center", "justify-between")}>
|
||||
<div className={cn("flex-1", "mr-4")}>
|
||||
<div className={cn("font-medium", "text-foreground", "text-sm")}>
|
||||
{message}
|
||||
</div>
|
||||
{description && (
|
||||
<div className={cn("text-muted-foreground", "text-sm", "mt-1")}>
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={handleUndo}
|
||||
className={cn(
|
||||
"bg-secondary",
|
||||
"hover:bg-secondary/80",
|
||||
"text-secondary-foreground",
|
||||
"border-0",
|
||||
"px-4",
|
||||
"py-2",
|
||||
"text-sm",
|
||||
"font-medium",
|
||||
"rounded-md"
|
||||
)}
|
||||
>
|
||||
{t("buttons.undo", "Undo")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
UndoableNotification.displayName = "UndoableNotification";
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { NotificationProvider } from "@refinedev/core";
|
||||
import { toast } from "sonner";
|
||||
import { UndoableNotification } from "@/components/refine-ui/notification/undoable-notification";
|
||||
|
||||
export function useNotificationProvider(): NotificationProvider {
|
||||
return {
|
||||
open: ({
|
||||
key,
|
||||
type,
|
||||
message,
|
||||
description,
|
||||
undoableTimeout,
|
||||
cancelMutation,
|
||||
}) => {
|
||||
switch (type) {
|
||||
case "success":
|
||||
toast.success(message, {
|
||||
id: key,
|
||||
description,
|
||||
richColors: true,
|
||||
});
|
||||
return;
|
||||
|
||||
case "error":
|
||||
toast.error(message, {
|
||||
id: key,
|
||||
description,
|
||||
richColors: true,
|
||||
});
|
||||
return;
|
||||
|
||||
case "progress": {
|
||||
const toastId = key || Date.now();
|
||||
|
||||
toast(
|
||||
() => (
|
||||
<UndoableNotification
|
||||
message={message}
|
||||
description={description}
|
||||
undoableTimeout={undoableTimeout}
|
||||
cancelMutation={cancelMutation}
|
||||
onClose={() => toast.dismiss(toastId)}
|
||||
/>
|
||||
),
|
||||
{
|
||||
id: toastId,
|
||||
duration: (undoableTimeout || 5) * 1000,
|
||||
unstyled: true,
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
},
|
||||
close: (id) => {
|
||||
toast.dismiss(id);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
type Theme = "dark" | "light" | "system";
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode;
|
||||
defaultTheme?: Theme;
|
||||
storageKey?: string;
|
||||
};
|
||||
|
||||
type ThemeProviderState = {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
};
|
||||
|
||||
const initialState: ThemeProviderState = {
|
||||
theme: "system",
|
||||
setTheme: () => null,
|
||||
};
|
||||
|
||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = "system",
|
||||
storageKey = "refine-ui-theme",
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
|
||||
root.classList.remove("light", "dark");
|
||||
|
||||
if (theme === "system") {
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
.matches
|
||||
? "dark"
|
||||
: "light";
|
||||
|
||||
root.classList.add(systemTheme);
|
||||
return;
|
||||
}
|
||||
|
||||
root.classList.add(theme);
|
||||
}, [theme]);
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
localStorage.setItem(storageKey, theme);
|
||||
setTheme(theme);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProviderContext.Provider {...props} value={value}>
|
||||
{children}
|
||||
</ThemeProviderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeProviderContext);
|
||||
|
||||
if (context === undefined) {
|
||||
console.error("useTheme must be used within a ThemeProvider");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
ThemeProvider.displayName = "ThemeProvider";
|
||||
@@ -0,0 +1,100 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useTheme } from "./theme-provider";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Moon, Sun, Monitor, ChevronDown, Check } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type ThemeOption = {
|
||||
value: "light" | "dark" | "system";
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
};
|
||||
|
||||
const themeOptions: ThemeOption[] = [
|
||||
{
|
||||
value: "light",
|
||||
label: "Light",
|
||||
icon: <Sun className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
value: "dark",
|
||||
label: "Dark",
|
||||
icon: <Moon className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
value: "system",
|
||||
label: "System",
|
||||
icon: <Monitor className="h-4 w-4" />,
|
||||
},
|
||||
];
|
||||
|
||||
export function ThemeSelect() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
const currentTheme = themeOptions.find((option) => option.value === theme);
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="lg"
|
||||
className={cn(
|
||||
"w-full",
|
||||
"justify-between",
|
||||
"px-3",
|
||||
"text-left",
|
||||
"text-sm",
|
||||
"font-normal",
|
||||
"text-foreground",
|
||||
"hover:bg-accent",
|
||||
"hover:text-accent-foreground",
|
||||
"focus-visible:outline-none",
|
||||
"focus-visible:ring-2",
|
||||
"focus-visible:ring-ring"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{currentTheme?.icon}
|
||||
<span>{currentTheme?.label}</span>
|
||||
</div>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="min-w-40 space-y-1">
|
||||
{themeOptions.map((option) => {
|
||||
const isSelected = theme === option.value;
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={option.value}
|
||||
onClick={() => setTheme(option.value)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 cursor-pointer relative pr-8",
|
||||
{
|
||||
"bg-accent text-accent-foreground": isSelected,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{option.icon}
|
||||
<span>{option.label}</span>
|
||||
{isSelected && (
|
||||
<Check className="h-4 w-4 absolute right-2 text-primary" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
ThemeSelect.displayName = "ThemeSelect";
|
||||
@@ -0,0 +1,93 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "@/components/refine-ui/theme/theme-provider";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Moon, Sun, Monitor } from "lucide-react";
|
||||
|
||||
type ThemeToggleProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function ThemeToggle({ className }: ThemeToggleProps) {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
const cycleTheme = () => {
|
||||
switch (theme) {
|
||||
case "light":
|
||||
setTheme("dark");
|
||||
break;
|
||||
case "dark":
|
||||
setTheme("system");
|
||||
break;
|
||||
case "system":
|
||||
setTheme("light");
|
||||
break;
|
||||
default:
|
||||
setTheme("light");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={cycleTheme}
|
||||
className={cn(
|
||||
"rounded-full",
|
||||
"border-sidebar-border",
|
||||
"bg-transparent",
|
||||
className,
|
||||
"h-10",
|
||||
"w-10"
|
||||
)}
|
||||
>
|
||||
<Sun
|
||||
className={cn(
|
||||
"h-[1.2rem]",
|
||||
"w-[1.2rem]",
|
||||
"rotate-0",
|
||||
"scale-100",
|
||||
"transition-all",
|
||||
"duration-200",
|
||||
{
|
||||
"-rotate-90 scale-0": theme === "dark" || theme === "system",
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<Moon
|
||||
className={cn(
|
||||
"absolute",
|
||||
"h-[1.2rem]",
|
||||
"w-[1.2rem]",
|
||||
"rotate-90",
|
||||
"scale-0",
|
||||
"transition-all",
|
||||
"duration-200",
|
||||
{
|
||||
"rotate-0 scale-100": theme === "dark",
|
||||
"rotate-90 scale-0": theme === "light" || theme === "system",
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<Monitor
|
||||
className={cn(
|
||||
"absolute",
|
||||
"h-[1.2rem]",
|
||||
"w-[1.2rem]",
|
||||
"rotate-0",
|
||||
"scale-0",
|
||||
"transition-all",
|
||||
"duration-200",
|
||||
{
|
||||
"scale-100": theme === "system",
|
||||
"scale-0": theme === "light" || theme === "dark",
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<span className="sr-only">Toggle theme (Light → Dark → System)</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
ThemeToggle.displayName = "ThemeToggle";
|
||||
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
useBack,
|
||||
useResourceParams,
|
||||
useUserFriendlyName,
|
||||
} from "@refinedev/core";
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { Breadcrumb } from "@/components/refine-ui/layout/breadcrumb";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeftIcon } from "lucide-react";
|
||||
|
||||
type CreateViewProps = PropsWithChildren<{
|
||||
className?: string;
|
||||
}>;
|
||||
|
||||
export function CreateView({ children, className }: CreateViewProps) {
|
||||
return (
|
||||
<div className={cn("flex flex-col", "gap-4", className)}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
type CreateHeaderProps = PropsWithChildren<{
|
||||
resource?: string;
|
||||
title?: string;
|
||||
wrapperClassName?: string;
|
||||
headerClassName?: string;
|
||||
}>;
|
||||
|
||||
export const CreateViewHeader = ({
|
||||
resource: resourceFromProps,
|
||||
title: titleFromProps,
|
||||
wrapperClassName,
|
||||
headerClassName,
|
||||
}: CreateHeaderProps) => {
|
||||
const back = useBack();
|
||||
|
||||
const getUserFriendlyName = useUserFriendlyName();
|
||||
|
||||
const { resource, identifier } = useResourceParams({
|
||||
resource: resourceFromProps,
|
||||
});
|
||||
|
||||
const title =
|
||||
titleFromProps ??
|
||||
getUserFriendlyName(
|
||||
resource?.meta?.label ?? identifier ?? resource?.name,
|
||||
"plural"
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col", "gap-4", wrapperClassName)}>
|
||||
<div className="flex items-center relative gap-2">
|
||||
<div className="bg-background z-[2] pr-4">
|
||||
<Breadcrumb />
|
||||
</div>
|
||||
<Separator className={cn("absolute", "left-0", "right-0", "z-[1]")} />
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
"gap-1",
|
||||
"items-center",
|
||||
"-ml-2.5",
|
||||
headerClassName
|
||||
)}
|
||||
>
|
||||
<Button variant="ghost" size="icon" onClick={back}>
|
||||
<ArrowLeftIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<h2 className="text-2xl font-bold">{title}</h2>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CreateView.displayName = "CreateView";
|
||||
@@ -0,0 +1,97 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
useBack,
|
||||
useResourceParams,
|
||||
useUserFriendlyName,
|
||||
} from "@refinedev/core";
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { Breadcrumb } from "@/components/refine-ui/layout/breadcrumb";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { RefreshButton } from "@/components/refine-ui/buttons/refresh";
|
||||
import { ArrowLeftIcon } from "lucide-react";
|
||||
|
||||
type EditViewProps = PropsWithChildren<{
|
||||
className?: string;
|
||||
}>;
|
||||
|
||||
export function EditView({ children, className }: EditViewProps) {
|
||||
return (
|
||||
<div className={cn("flex flex-col", "gap-4", className)}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
type EditViewHeaderProps = PropsWithChildren<{
|
||||
resource?: string;
|
||||
title?: string;
|
||||
wrapperClassName?: string;
|
||||
headerClassName?: string;
|
||||
actionsSlot?: React.ReactNode;
|
||||
}>;
|
||||
|
||||
export const EditViewHeader = ({
|
||||
resource: resourceFromProps,
|
||||
title: titleFromProps,
|
||||
actionsSlot,
|
||||
wrapperClassName,
|
||||
headerClassName,
|
||||
}: EditViewHeaderProps) => {
|
||||
const back = useBack();
|
||||
|
||||
const getUserFriendlyName = useUserFriendlyName();
|
||||
|
||||
const { resource, identifier } = useResourceParams({
|
||||
resource: resourceFromProps,
|
||||
});
|
||||
const { id: recordItemId } = useResourceParams();
|
||||
|
||||
const resourceName = resource?.name ?? identifier;
|
||||
|
||||
const title =
|
||||
titleFromProps ??
|
||||
getUserFriendlyName(
|
||||
resource?.meta?.label ?? identifier ?? resource?.name,
|
||||
"plural"
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col", "gap-4", wrapperClassName)}>
|
||||
<div className="flex items-center relative gap-2">
|
||||
<div className="bg-background z-[2] pr-4">
|
||||
<Breadcrumb />
|
||||
</div>
|
||||
<Separator className={cn("absolute", "left-0", "right-0", "z-[1]")} />
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
"gap-1",
|
||||
"items-center",
|
||||
"justify-between",
|
||||
"-ml-2.5",
|
||||
headerClassName
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button variant="ghost" size="icon" onClick={back}>
|
||||
<ArrowLeftIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<h2 className="text-2xl font-bold">{title}</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{actionsSlot}
|
||||
<RefreshButton
|
||||
variant="outline"
|
||||
recordItemId={recordItemId}
|
||||
resource={resourceName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
EditView.displayName = "EditView";
|
||||
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
import { useResourceParams, useUserFriendlyName } from "@refinedev/core";
|
||||
import { Breadcrumb } from "@/components/refine-ui/layout/breadcrumb";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { CreateButton } from "@/components/refine-ui/buttons/create";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type ListViewProps = PropsWithChildren<{
|
||||
className?: string;
|
||||
}>;
|
||||
|
||||
export function ListView({ children, className }: ListViewProps) {
|
||||
return (
|
||||
<div className={cn("flex flex-col", "gap-4", className)}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
type ListHeaderProps = PropsWithChildren<{
|
||||
resource?: string;
|
||||
title?: string;
|
||||
canCreate?: boolean;
|
||||
headerClassName?: string;
|
||||
wrapperClassName?: string;
|
||||
}>;
|
||||
|
||||
export const ListViewHeader = ({
|
||||
canCreate,
|
||||
resource: resourceFromProps,
|
||||
title: titleFromProps,
|
||||
wrapperClassName,
|
||||
headerClassName,
|
||||
}: ListHeaderProps) => {
|
||||
const getUserFriendlyName = useUserFriendlyName();
|
||||
|
||||
const { resource, identifier } = useResourceParams({
|
||||
resource: resourceFromProps,
|
||||
});
|
||||
const resourceName = identifier ?? resource?.name;
|
||||
|
||||
const isCreateButtonVisible = canCreate ?? !!resource?.create;
|
||||
|
||||
const title =
|
||||
titleFromProps ??
|
||||
getUserFriendlyName(
|
||||
resource?.meta?.label ?? identifier ?? resource?.name,
|
||||
"plural"
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col", "gap-4", wrapperClassName)}>
|
||||
<div className="flex items-center relative gap-2">
|
||||
<div className="bg-background z-[2] pr-4">
|
||||
<Breadcrumb />
|
||||
</div>
|
||||
<Separator className={cn("absolute", "left-0", "right-0", "z-[1]")} />
|
||||
</div>
|
||||
<div className={cn("flex", "justify-between", "gap-4", headerClassName)}>
|
||||
<h2 className="text-2xl font-bold">{title}</h2>
|
||||
{isCreateButtonVisible && (
|
||||
<div className="flex items-center gap-2">
|
||||
<CreateButton resource={resourceName} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ListView.displayName = "ListView";
|
||||
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
import { ArrowLeftIcon } from "lucide-react";
|
||||
import {
|
||||
useBack,
|
||||
useResourceParams,
|
||||
useUserFriendlyName,
|
||||
} from "@refinedev/core";
|
||||
import { Breadcrumb } from "@/components/refine-ui/layout/breadcrumb";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { RefreshButton } from "@/components/refine-ui/buttons/refresh";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { EditButton } from "../buttons/edit";
|
||||
|
||||
type ShowViewProps = PropsWithChildren<{
|
||||
className?: string;
|
||||
}>;
|
||||
|
||||
export function ShowView({ children, className }: ShowViewProps) {
|
||||
return (
|
||||
<div className={cn("flex flex-col", "gap-4", className)}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
type ShowViewHeaderProps = PropsWithChildren<{
|
||||
resource?: string;
|
||||
title?: string;
|
||||
wrapperClassName?: string;
|
||||
headerClassName?: string;
|
||||
}>;
|
||||
|
||||
export const ShowViewHeader = ({
|
||||
resource: resourceFromProps,
|
||||
title: titleFromProps,
|
||||
wrapperClassName,
|
||||
headerClassName,
|
||||
}: ShowViewHeaderProps) => {
|
||||
const back = useBack();
|
||||
|
||||
const getUserFriendlyName = useUserFriendlyName();
|
||||
|
||||
const { resource, identifier } = useResourceParams({
|
||||
resource: resourceFromProps,
|
||||
});
|
||||
const { id: recordItemId } = useResourceParams();
|
||||
|
||||
const resourceName = resource?.name ?? identifier;
|
||||
|
||||
const title =
|
||||
titleFromProps ??
|
||||
getUserFriendlyName(
|
||||
resource?.meta?.label ?? identifier ?? resource?.name,
|
||||
"singular"
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col", "gap-4", wrapperClassName)}>
|
||||
<div className="flex items-center relative gap-2">
|
||||
<div className="bg-background z-[2] pr-4">
|
||||
<Breadcrumb />
|
||||
</div>
|
||||
<Separator className={cn("absolute", "left-0", "right-0", "z-[1]")} />
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
"gap-1",
|
||||
"items-center",
|
||||
"justify-between",
|
||||
"-ml-2.5",
|
||||
headerClassName
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button variant="ghost" size="icon" onClick={back}>
|
||||
<ArrowLeftIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<h2 className="text-2xl font-bold">{title}</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<RefreshButton
|
||||
variant="outline"
|
||||
recordItemId={recordItemId}
|
||||
resource={resourceName}
|
||||
/>
|
||||
<EditButton
|
||||
variant="outline"
|
||||
recordItemId={recordItemId}
|
||||
resource={resourceName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ShowView.displayName = "ShowView";
|
||||
@@ -0,0 +1,64 @@
|
||||
import * as React from "react";
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Accordion({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
|
||||
}
|
||||
|
||||
function AccordionItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn("border-b last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||
return (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
data-slot="accordion-content"
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
);
|
||||
}
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||
@@ -0,0 +1,157 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
|
||||
function AlertDialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
||||
}
|
||||
|
||||
function AlertDialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
data-slot="alert-dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
data-slot="alert-dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogFooter({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
data-slot="alert-dialog-title"
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
data-slot="alert-dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogAction({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Action
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogCancel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-card text-card-foreground",
|
||||
destructive:
|
||||
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
@@ -0,0 +1,9 @@
|
||||
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
|
||||
|
||||
function AspectRatio({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
|
||||
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />;
|
||||
}
|
||||
|
||||
export { AspectRatio };
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn("aspect-square size-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback };
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
@@ -0,0 +1,109 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
|
||||
}
|
||||
|
||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||
return (
|
||||
<ol
|
||||
data-slot="breadcrumb-list"
|
||||
className={cn(
|
||||
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-item"
|
||||
className={cn("inline-flex items-center gap-1.5", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbLink({
|
||||
asChild,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="breadcrumb-link"
|
||||
className={cn("hover:text-foreground transition-colors", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-page"
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("text-foreground font-normal", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbSeparator({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-separator"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:size-3.5", className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-ellipsis"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Button, buttonVariants };
|
||||
@@ -0,0 +1,74 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { DayPicker } from "react-day-picker";
|
||||
import { buttonVariants } from "./button";
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayPicker>) {
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn("p-3", className)}
|
||||
classNames={{
|
||||
months: "flex flex-col sm:flex-row gap-2",
|
||||
month: "flex flex-col gap-4",
|
||||
caption: "flex justify-center pt-1 relative items-center w-full",
|
||||
caption_label: "text-sm font-medium",
|
||||
nav: "flex items-center gap-1",
|
||||
nav_button: cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
||||
),
|
||||
nav_button_previous: "absolute left-1",
|
||||
nav_button_next: "absolute right-1",
|
||||
table: "w-full border-collapse space-x-1",
|
||||
head_row: "flex",
|
||||
head_cell:
|
||||
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
||||
row: "flex w-full mt-2",
|
||||
cell: cn(
|
||||
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
|
||||
props.mode === "range"
|
||||
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
|
||||
: "[&:has([aria-selected])]:rounded-md"
|
||||
),
|
||||
day: cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"size-8 p-0 font-normal aria-selected:opacity-100"
|
||||
),
|
||||
day_range_start:
|
||||
"day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
|
||||
day_range_end:
|
||||
"day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
|
||||
day_selected:
|
||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||
day_today: "bg-accent text-accent-foreground",
|
||||
day_outside:
|
||||
"day-outside text-muted-foreground aria-selected:text-muted-foreground",
|
||||
day_disabled: "text-muted-foreground opacity-50",
|
||||
day_range_middle:
|
||||
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||
day_hidden: "invisible",
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
IconLeft: ({ className, ...props }) => (
|
||||
<ChevronLeft className={cn("size-4", className)} {...props} />
|
||||
),
|
||||
IconRight: ({ className, ...props }) => (
|
||||
<ChevronRight className={cn("size-4", className)} {...props} />
|
||||
),
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Calendar };
|
||||
@@ -0,0 +1,92 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
};
|
||||
@@ -0,0 +1,237 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import useEmblaCarousel, { UseEmblaCarouselType } from "embla-carousel-react";
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
export type CarouselApi = UseEmblaCarouselType[1];
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||
type CarouselOptions = UseCarouselParameters[0];
|
||||
type CarouselPlugin = UseCarouselParameters[1];
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions;
|
||||
plugins?: CarouselPlugin;
|
||||
orientation?: "horizontal" | "vertical";
|
||||
setApi?: (api: CarouselApi) => void;
|
||||
};
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||
scrollPrev: () => void;
|
||||
scrollNext: () => void;
|
||||
canScrollPrev: boolean;
|
||||
canScrollNext: boolean;
|
||||
} & CarouselProps;
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useCarousel must be used within a <Carousel />");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
function Carousel({
|
||||
orientation = "horizontal",
|
||||
opts,
|
||||
setApi,
|
||||
plugins,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & CarouselProps) {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === "horizontal" ? "x" : "y",
|
||||
},
|
||||
plugins
|
||||
);
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) return;
|
||||
setCanScrollPrev(api.canScrollPrev());
|
||||
setCanScrollNext(api.canScrollNext());
|
||||
}, []);
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev();
|
||||
}, [api]);
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext();
|
||||
}, [api]);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault();
|
||||
scrollPrev();
|
||||
} else if (event.key === "ArrowRight") {
|
||||
event.preventDefault();
|
||||
scrollNext();
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) return;
|
||||
setApi(api);
|
||||
}, [api, setApi]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) return;
|
||||
onSelect(api);
|
||||
api.on("reInit", onSelect);
|
||||
api.on("select", onSelect);
|
||||
|
||||
return () => {
|
||||
api?.off("select", onSelect);
|
||||
};
|
||||
}, [api, onSelect]);
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api: api,
|
||||
opts,
|
||||
orientation:
|
||||
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn("relative", className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
data-slot="carousel"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { carouselRef, orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={carouselRef}
|
||||
className="overflow-hidden"
|
||||
data-slot="carousel-content"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
data-slot="carousel-item"
|
||||
className={cn(
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselPrevious({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "icon",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-slot="carousel-previous"
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute size-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "top-1/2 -left-12 -translate-y-1/2"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
>
|
||||
<ArrowLeft />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselNext({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "icon",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-slot="carousel-next"
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute size-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "top-1/2 -right-12 -translate-y-1/2"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}
|
||||
>
|
||||
<ArrowRight />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselPrevious,
|
||||
CarouselNext,
|
||||
};
|
||||
@@ -0,0 +1,351 @@
|
||||
import * as React from "react";
|
||||
import * as RechartsPrimitive from "recharts";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: "", dark: ".dark" } as const;
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: React.ReactNode;
|
||||
icon?: React.ComponentType;
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
);
|
||||
};
|
||||
|
||||
type ChartContextProps = {
|
||||
config: ChartConfig;
|
||||
};
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useChart must be used within a <ChartContainer />");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
function ChartContainer({
|
||||
id,
|
||||
className,
|
||||
children,
|
||||
config,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
config: ChartConfig;
|
||||
children: React.ComponentProps<
|
||||
typeof RechartsPrimitive.ResponsiveContainer
|
||||
>["children"];
|
||||
}) {
|
||||
const uniqueId = React.useId();
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
data-slot="chart"
|
||||
data-chart={chartId}
|
||||
className={cn(
|
||||
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>
|
||||
{children}
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
const colorConfig = Object.entries(config).filter(
|
||||
([, config]) => config.theme || config.color
|
||||
);
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Object.entries(THEMES)
|
||||
.map(
|
||||
([theme, prefix]) => `
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||
itemConfig.color;
|
||||
return color ? ` --color-${key}: ${color};` : null;
|
||||
})
|
||||
.join("\n")}
|
||||
}
|
||||
`
|
||||
)
|
||||
.join("\n"),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||
|
||||
function ChartTooltipContent({
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = "dot",
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean;
|
||||
hideIndicator?: boolean;
|
||||
indicator?: "line" | "dot" | "dashed";
|
||||
nameKey?: string;
|
||||
labelKey?: string;
|
||||
}) {
|
||||
const { config } = useChart();
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [item] = payload;
|
||||
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
? config[label as keyof typeof config]?.label || label
|
||||
: itemConfig?.label;
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn("font-medium", labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
payload,
|
||||
hideLabel,
|
||||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
]);
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const indicatorColor = color || item.payload.fill || item.color;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
||||
indicator === "dot" && "items-center"
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn(
|
||||
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
||||
{
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
}
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--color-bg": indicatorColor,
|
||||
"--color-border": indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 justify-between leading-none",
|
||||
nestLabel ? "items-end" : "items-center"
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">
|
||||
{itemConfig?.label || item.name}
|
||||
</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
<span className="text-foreground font-mono font-medium tabular-nums">
|
||||
{item.value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend;
|
||||
|
||||
function ChartLegendContent({
|
||||
className,
|
||||
hideIcon = false,
|
||||
payload,
|
||||
verticalAlign = "bottom",
|
||||
nameKey,
|
||||
}: React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean;
|
||||
nameKey?: string;
|
||||
}) {
|
||||
const { config } = useChart();
|
||||
|
||||
if (!payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-4",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{payload.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(
|
||||
config: ChartConfig,
|
||||
payload: unknown,
|
||||
key: string
|
||||
) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
"payload" in payload &&
|
||||
typeof payload.payload === "object" &&
|
||||
payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined;
|
||||
|
||||
let configLabelKey: string = key;
|
||||
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key as keyof typeof payload] === "string"
|
||||
) {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string;
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||
) {
|
||||
configLabelKey = payloadPayload[
|
||||
key as keyof typeof payloadPayload
|
||||
] as string;
|
||||
}
|
||||
|
||||
return configLabelKey in config
|
||||
? config[configLabelKey]
|
||||
: config[key as keyof typeof config];
|
||||
}
|
||||
|
||||
export {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="flex items-center justify-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Checkbox };
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
||||
|
||||
function Collapsible({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
|
||||
}
|
||||
|
||||
function CollapsibleTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleTrigger
|
||||
data-slot="collapsible-trigger"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CollapsibleContent({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleContent
|
||||
data-slot="collapsible-content"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||
@@ -0,0 +1,182 @@
|
||||
import * as React from "react";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { SearchIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
function Command({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return (
|
||||
<CommandPrimitive
|
||||
data-slot="command"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandDialog({
|
||||
title = "Command Palette",
|
||||
description = "Search for a command to run...",
|
||||
children,
|
||||
className,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Dialog> & {
|
||||
title?: string;
|
||||
description?: string;
|
||||
className?: string;
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogContent
|
||||
className={cn("overflow-hidden p-0", className)}
|
||||
showCloseButton={showCloseButton}
|
||||
>
|
||||
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command-input-wrapper"
|
||||
className="flex h-9 items-center gap-2 border-b px-3"
|
||||
>
|
||||
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
className={cn(
|
||||
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||
return (
|
||||
<CommandPrimitive.List
|
||||
data-slot="command-list"
|
||||
className={cn(
|
||||
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandEmpty({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||
return (
|
||||
<CommandPrimitive.Empty
|
||||
data-slot="command-empty"
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||
return (
|
||||
<CommandPrimitive.Group
|
||||
data-slot="command-group"
|
||||
className={cn(
|
||||
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||
return (
|
||||
<CommandPrimitive.Separator
|
||||
data-slot="command-separator"
|
||||
className={cn("bg-border -mx-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||
return (
|
||||
<CommandPrimitive.Item
|
||||
data-slot="command-item"
|
||||
className={cn(
|
||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
};
|
||||
@@ -0,0 +1,252 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function ContextMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
||||
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.RadioGroup
|
||||
data-slot="context-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
data-slot="context-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto" />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubContent
|
||||
data-slot="context-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal>
|
||||
<ContextMenuPrimitive.Content
|
||||
data-slot="context-menu-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Item
|
||||
data-slot="context-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.CheckboxItem
|
||||
data-slot="context-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.RadioItem
|
||||
data-slot="context-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Label
|
||||
data-slot="context-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Separator
|
||||
data-slot="context-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="context-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
ContextMenu,
|
||||
ContextMenuTrigger,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuCheckboxItem,
|
||||
ContextMenuRadioItem,
|
||||
ContextMenuLabel,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuShortcut,
|
||||
ContextMenuGroup,
|
||||
ContextMenuPortal,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuRadioGroup,
|
||||
};
|
||||
@@ -0,0 +1,141 @@
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
};
|
||||
@@ -0,0 +1,130 @@
|
||||
import * as React from "react";
|
||||
import { Drawer as DrawerPrimitive } from "vaul";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Drawer({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||
return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
|
||||
}
|
||||
|
||||
function DrawerTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DrawerPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DrawerClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
|
||||
}
|
||||
|
||||
function DrawerOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||
return (
|
||||
<DrawerPrimitive.Overlay
|
||||
data-slot="drawer-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||
return (
|
||||
<DrawerPortal data-slot="drawer-portal">
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
data-slot="drawer-content"
|
||||
className={cn(
|
||||
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
||||
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
|
||||
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
||||
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-header"
|
||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||
return (
|
||||
<DrawerPrimitive.Title
|
||||
data-slot="drawer-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||
return (
|
||||
<DrawerPrimitive.Description
|
||||
data-slot="drawer-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerOverlay,
|
||||
DrawerTrigger,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
};
|
||||
@@ -0,0 +1,255 @@
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
};
|
||||
@@ -0,0 +1,166 @@
|
||||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import {
|
||||
Controller,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
useFormState,
|
||||
type ControllerProps,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
} from "react-hook-form";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
const Form = FormProvider;
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
> = {
|
||||
name: TName;
|
||||
};
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue
|
||||
);
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext);
|
||||
const itemContext = React.useContext(FormItemContext);
|
||||
const { getFieldState } = useFormContext();
|
||||
const formState = useFormState({ name: fieldContext.name });
|
||||
const fieldState = getFieldState(fieldContext.name, formState);
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>");
|
||||
}
|
||||
|
||||
const { id } = itemContext;
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
};
|
||||
};
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue
|
||||
);
|
||||
|
||||
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div
|
||||
data-slot="form-item"
|
||||
className={cn("grid gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
</FormItemContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function FormLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
const { error, formItemId } = useFormField();
|
||||
|
||||
return (
|
||||
<Label
|
||||
data-slot="form-label"
|
||||
data-error={!!error}
|
||||
className={cn("data-[error=true]:text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||
useFormField();
|
||||
|
||||
return (
|
||||
<Slot
|
||||
data-slot="form-control"
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
const { formDescriptionId } = useFormField();
|
||||
|
||||
return (
|
||||
<p
|
||||
data-slot="form-description"
|
||||
id={formDescriptionId}
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||
const { error, formMessageId } = useFormField();
|
||||
const body = error ? String(error?.message ?? "") : props.children;
|
||||
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
data-slot="form-message"
|
||||
id={formMessageId}
|
||||
className={cn("text-destructive text-sm", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as React from "react";
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function HoverCard({
|
||||
...props
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
||||
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
|
||||
}
|
||||
|
||||
function HoverCardTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
||||
return (
|
||||
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function HoverCardContent({
|
||||
className,
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
|
||||
return (
|
||||
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
|
||||
<HoverCardPrimitive.Content
|
||||
data-slot="hover-card-content"
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</HoverCardPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
||||
@@ -0,0 +1,77 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { OTPInput, OTPInputContext } from "input-otp";
|
||||
import { MinusIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function InputOTP({
|
||||
className,
|
||||
containerClassName,
|
||||
...props
|
||||
}: React.ComponentProps<typeof OTPInput> & {
|
||||
containerClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<OTPInput
|
||||
data-slot="input-otp"
|
||||
containerClassName={cn(
|
||||
"flex items-center gap-2 has-disabled:opacity-50",
|
||||
containerClassName
|
||||
)}
|
||||
className={cn("disabled:cursor-not-allowed", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="input-otp-group"
|
||||
className={cn("flex items-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputOTPSlot({
|
||||
index,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
index: number;
|
||||
}) {
|
||||
const inputOTPContext = React.useContext(OTPInputContext);
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="input-otp-slot"
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{char}
|
||||
{hasFakeCaret && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div data-slot="input-otp-separator" role="separator" {...props}>
|
||||
<MinusIcon />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Input };
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Label };
|
||||
@@ -0,0 +1,274 @@
|
||||
import * as React from "react";
|
||||
import * as MenubarPrimitive from "@radix-ui/react-menubar";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Menubar({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
|
||||
return (
|
||||
<MenubarPrimitive.Root
|
||||
data-slot="menubar"
|
||||
className={cn(
|
||||
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
|
||||
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />;
|
||||
}
|
||||
|
||||
function MenubarGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
|
||||
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />;
|
||||
}
|
||||
|
||||
function MenubarPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
|
||||
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />;
|
||||
}
|
||||
|
||||
function MenubarRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
|
||||
return (
|
||||
<MenubarPrimitive.Trigger
|
||||
data-slot="menubar-trigger"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarContent({
|
||||
className,
|
||||
align = "start",
|
||||
alignOffset = -4,
|
||||
sideOffset = 8,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
|
||||
return (
|
||||
<MenubarPortal>
|
||||
<MenubarPrimitive.Content
|
||||
data-slot="menubar-content"
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</MenubarPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<MenubarPrimitive.Item
|
||||
data-slot="menubar-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<MenubarPrimitive.CheckboxItem
|
||||
data-slot="menubar-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
|
||||
return (
|
||||
<MenubarPrimitive.RadioItem
|
||||
data-slot="menubar-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.RadioItem>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<MenubarPrimitive.Label
|
||||
data-slot="menubar-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
|
||||
return (
|
||||
<MenubarPrimitive.Separator
|
||||
data-slot="menubar-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="menubar-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
|
||||
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />;
|
||||
}
|
||||
|
||||
function MenubarSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<MenubarPrimitive.SubTrigger
|
||||
data-slot="menubar-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto h-4 w-4" />
|
||||
</MenubarPrimitive.SubTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
|
||||
return (
|
||||
<MenubarPrimitive.SubContent
|
||||
data-slot="menubar-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Menubar,
|
||||
MenubarPortal,
|
||||
MenubarMenu,
|
||||
MenubarTrigger,
|
||||
MenubarContent,
|
||||
MenubarGroup,
|
||||
MenubarSeparator,
|
||||
MenubarLabel,
|
||||
MenubarItem,
|
||||
MenubarShortcut,
|
||||
MenubarCheckboxItem,
|
||||
MenubarRadioGroup,
|
||||
MenubarRadioItem,
|
||||
MenubarSub,
|
||||
MenubarSubTrigger,
|
||||
MenubarSubContent,
|
||||
};
|
||||
@@ -0,0 +1,168 @@
|
||||
import * as React from "react";
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
|
||||
import { cva } from "class-variance-authority";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function NavigationMenu({
|
||||
className,
|
||||
children,
|
||||
viewport = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
|
||||
viewport?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Root
|
||||
data-slot="navigation-menu"
|
||||
data-viewport={viewport}
|
||||
className={cn(
|
||||
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{viewport && <NavigationMenuViewport />}
|
||||
</NavigationMenuPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.List
|
||||
data-slot="navigation-menu-list"
|
||||
className={cn(
|
||||
"group flex flex-1 list-none items-center justify-center gap-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Item
|
||||
data-slot="navigation-menu-item"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
);
|
||||
|
||||
function NavigationMenuTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
data-slot="navigation-menu-trigger"
|
||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}{" "}
|
||||
<ChevronDownIcon
|
||||
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Content
|
||||
data-slot="navigation-menu-content"
|
||||
className={cn(
|
||||
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
|
||||
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuViewport({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-full left-0 isolate z-50 flex justify-center"
|
||||
)}
|
||||
>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
data-slot="navigation-menu-viewport"
|
||||
className={cn(
|
||||
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuLink({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Link
|
||||
data-slot="navigation-menu-link"
|
||||
className={cn(
|
||||
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuIndicator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
data-slot="navigation-menu-indicator"
|
||||
className={cn(
|
||||
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
NavigationMenu,
|
||||
NavigationMenuList,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuTrigger,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuViewport,
|
||||
navigationMenuTriggerStyle,
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
MoreHorizontalIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
|
||||
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||
return (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
data-slot="pagination"
|
||||
className={cn("mx-auto flex w-full justify-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function PaginationContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="pagination-content"
|
||||
className={cn("flex flex-row items-center gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||
return <li data-slot="pagination-item" {...props} />;
|
||||
}
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean;
|
||||
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
||||
React.ComponentProps<"a">;
|
||||
|
||||
function PaginationLink({
|
||||
className,
|
||||
isActive,
|
||||
size = "icon",
|
||||
...props
|
||||
}: PaginationLinkProps) {
|
||||
return (
|
||||
<a
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
data-slot="pagination-link"
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? "outline" : "ghost",
|
||||
size,
|
||||
}),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function PaginationPrevious({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
<span className="hidden sm:block">Previous</span>
|
||||
</PaginationLink>
|
||||
);
|
||||
}
|
||||
|
||||
function PaginationNext({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="hidden sm:block">Next</span>
|
||||
<ChevronRightIcon />
|
||||
</PaginationLink>
|
||||
);
|
||||
}
|
||||
|
||||
function PaginationEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
data-slot="pagination-ellipsis"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationLink,
|
||||
PaginationItem,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationEllipsis,
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Popover({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
|
||||
}
|
||||
|
||||
function PopoverTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function PopoverContent({
|
||||
className,
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
data-slot="popover-content"
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function PopoverAnchor({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
|
||||
}
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|
||||
@@ -0,0 +1,29 @@
|
||||
import * as React from "react";
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Progress({
|
||||
className,
|
||||
value,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||
return (
|
||||
<ProgressPrimitive.Root
|
||||
data-slot="progress"
|
||||
className={cn(
|
||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
data-slot="progress-indicator"
|
||||
className="bg-primary h-full w-full flex-1 transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Progress };
|
||||
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||
import { CircleIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function RadioGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
data-slot="radio-group"
|
||||
className={cn("grid gap-3", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function RadioGroupItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
data-slot="radio-group-item"
|
||||
className={cn(
|
||||
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator
|
||||
data-slot="radio-group-indicator"
|
||||
className="relative flex items-center justify-center"
|
||||
>
|
||||
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
);
|
||||
}
|
||||
|
||||
export { RadioGroup, RadioGroupItem };
|
||||
@@ -0,0 +1,54 @@
|
||||
import * as React from "react";
|
||||
import { GripVerticalIcon } from "lucide-react";
|
||||
import * as ResizablePrimitive from "react-resizable-panels";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function ResizablePanelGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
|
||||
return (
|
||||
<ResizablePrimitive.PanelGroup
|
||||
data-slot="resizable-panel-group"
|
||||
className={cn(
|
||||
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ResizablePanel({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
|
||||
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
|
||||
}
|
||||
|
||||
function ResizableHandle({
|
||||
withHandle,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
||||
withHandle?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ResizablePrimitive.PanelResizeHandle
|
||||
data-slot="resizable-handle"
|
||||
className={cn(
|
||||
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{withHandle && (
|
||||
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
|
||||
<GripVerticalIcon className="size-2.5" />
|
||||
</div>
|
||||
)}
|
||||
</ResizablePrimitive.PanelResizeHandle>
|
||||
);
|
||||
}
|
||||
|
||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none p-px transition-colors select-none",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
);
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar };
|
||||
@@ -0,0 +1,183 @@
|
||||
import * as React from "react";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default";
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as React from "react";
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Separator };
|
||||
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
|
||||
}
|
||||
|
||||
function SheetTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function SheetClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
|
||||
}
|
||||
|
||||
function SheetPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
|
||||
}
|
||||
|
||||
function SheetOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||
return (
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetContent({
|
||||
className,
|
||||
children,
|
||||
side = "right",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
side === "right" &&
|
||||
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
||||
side === "left" &&
|
||||
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
||||
side === "top" &&
|
||||
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||
side === "bottom" &&
|
||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-header"
|
||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
data-slot="sheet-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
};
|
||||
@@ -0,0 +1,724 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
import { PanelLeftIcon } from "lucide-react";
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
const SIDEBAR_WIDTH = "16rem";
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem";
|
||||
const SIDEBAR_WIDTH_ICON = "3rem";
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
||||
|
||||
type SidebarContextProps = {
|
||||
state: "expanded" | "collapsed";
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
openMobile: boolean;
|
||||
setOpenMobile: (open: boolean) => void;
|
||||
isMobile: boolean;
|
||||
toggleSidebar: () => void;
|
||||
};
|
||||
|
||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
|
||||
|
||||
function useSidebar() {
|
||||
const context = React.useContext(SidebarContext);
|
||||
if (!context) {
|
||||
throw new Error("useSidebar must be used within a SidebarProvider.");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
function SidebarProvider({
|
||||
defaultOpen = true,
|
||||
open: openProp,
|
||||
onOpenChange: setOpenProp,
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
defaultOpen?: boolean;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}) {
|
||||
const isMobile = useIsMobile();
|
||||
const [openMobile, setOpenMobile] = React.useState(false);
|
||||
|
||||
// This is the internal state of the sidebar.
|
||||
// We use openProp and setOpenProp for control from outside the component.
|
||||
const [_open, _setOpen] = React.useState(defaultOpen);
|
||||
const open = openProp ?? _open;
|
||||
const setOpen = React.useCallback(
|
||||
(value: boolean | ((value: boolean) => boolean)) => {
|
||||
const openState = typeof value === "function" ? value(open) : value;
|
||||
if (setOpenProp) {
|
||||
setOpenProp(openState);
|
||||
} else {
|
||||
_setOpen(openState);
|
||||
}
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||
},
|
||||
[setOpenProp, open]
|
||||
);
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
const toggleSidebar = React.useCallback(() => {
|
||||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
|
||||
}, [isMobile, setOpen, setOpenMobile]);
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
) {
|
||||
event.preventDefault();
|
||||
toggleSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [toggleSidebar]);
|
||||
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = open ? "expanded" : "collapsed";
|
||||
|
||||
const contextValue = React.useMemo<SidebarContextProps>(
|
||||
() => ({
|
||||
state,
|
||||
open,
|
||||
setOpen,
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
}),
|
||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div
|
||||
data-slot="sidebar-wrapper"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH,
|
||||
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
||||
...style,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</SidebarContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Sidebar({
|
||||
side = "left",
|
||||
variant = "sidebar",
|
||||
collapsible = "offcanvas",
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
side?: "left" | "right";
|
||||
variant?: "sidebar" | "floating" | "inset";
|
||||
collapsible?: "offcanvas" | "icon" | "none";
|
||||
}) {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
||||
|
||||
if (collapsible === "none") {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar"
|
||||
className={cn(
|
||||
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||
<SheetContent
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar"
|
||||
data-mobile="true"
|
||||
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
side={side}
|
||||
>
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>Sidebar</SheetTitle>
|
||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group peer text-sidebar-foreground hidden md:block"
|
||||
data-state={state}
|
||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||
data-variant={variant}
|
||||
data-side={side}
|
||||
data-slot="sidebar"
|
||||
>
|
||||
{/* This is what handles the sidebar gap on desktop */}
|
||||
<div
|
||||
data-slot="sidebar-gap"
|
||||
className={cn(
|
||||
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
|
||||
"group-data-[collapsible=offcanvas]:w-0",
|
||||
"group-data-[side=right]:rotate-180",
|
||||
variant === "floating" || variant === "inset"
|
||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
data-slot="sidebar-container"
|
||||
className={cn(
|
||||
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||
side === "left"
|
||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === "floating" || variant === "inset"
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar-inner"
|
||||
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarTrigger({
|
||||
className,
|
||||
onClick,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-sidebar="trigger"
|
||||
data-slot="sidebar-trigger"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn("size-7", className)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event);
|
||||
toggleSidebar();
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<PanelLeftIcon />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<button
|
||||
data-sidebar="rail"
|
||||
data-slot="sidebar-rail"
|
||||
aria-label="Toggle Sidebar"
|
||||
tabIndex={-1}
|
||||
onClick={toggleSidebar}
|
||||
title="Toggle Sidebar"
|
||||
className={cn(
|
||||
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
|
||||
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
|
||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
||||
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
|
||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
||||
return (
|
||||
<main
|
||||
data-slot="sidebar-inset"
|
||||
className={cn(
|
||||
"bg-background relative flex w-full flex-1 flex-col",
|
||||
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Input>) {
|
||||
return (
|
||||
<Input
|
||||
data-slot="sidebar-input"
|
||||
data-sidebar="input"
|
||||
className={cn("bg-background h-8 w-full shadow-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-header"
|
||||
data-sidebar="header"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-footer"
|
||||
data-sidebar="footer"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Separator>) {
|
||||
return (
|
||||
<Separator
|
||||
data-slot="sidebar-separator"
|
||||
data-sidebar="separator"
|
||||
className={cn("bg-sidebar-border mx-2 w-auto", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-content"
|
||||
data-sidebar="content"
|
||||
className={cn(
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-group"
|
||||
data-sidebar="group"
|
||||
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroupLabel({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "div";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-group-label"
|
||||
data-sidebar="group-label"
|
||||
className={cn(
|
||||
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroupAction({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-group-action"
|
||||
data-sidebar="group-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroupContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-group-content"
|
||||
data-sidebar="group-content"
|
||||
className={cn("w-full text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="sidebar-menu"
|
||||
data-sidebar="menu"
|
||||
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="sidebar-menu-item"
|
||||
data-sidebar="menu-item"
|
||||
className={cn("group/menu-item relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
outline:
|
||||
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||
},
|
||||
size: {
|
||||
default: "h-8 text-sm",
|
||||
sm: "h-7 text-xs",
|
||||
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function SidebarMenuButton({
|
||||
asChild = false,
|
||||
isActive = false,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
tooltip,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & {
|
||||
asChild?: boolean;
|
||||
isActive?: boolean;
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const { isMobile, state } = useSidebar();
|
||||
|
||||
const button = (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-button"
|
||||
data-sidebar="menu-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!tooltip) {
|
||||
return button;
|
||||
}
|
||||
|
||||
if (typeof tooltip === "string") {
|
||||
tooltip = {
|
||||
children: tooltip,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
align="center"
|
||||
hidden={state !== "collapsed" || isMobile}
|
||||
{...tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuAction({
|
||||
className,
|
||||
asChild = false,
|
||||
showOnHover = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & {
|
||||
asChild?: boolean;
|
||||
showOnHover?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-action"
|
||||
data-sidebar="menu-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
showOnHover &&
|
||||
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuBadge({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-menu-badge"
|
||||
data-sidebar="menu-badge"
|
||||
className={cn(
|
||||
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
|
||||
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSkeleton({
|
||||
className,
|
||||
showIcon = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showIcon?: boolean;
|
||||
}) {
|
||||
// Random width between 50 to 90%.
|
||||
const width = React.useMemo(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-menu-skeleton"
|
||||
data-sidebar="menu-skeleton"
|
||||
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
||||
{...props}
|
||||
>
|
||||
{showIcon && (
|
||||
<Skeleton
|
||||
className="size-4 rounded-md"
|
||||
data-sidebar="menu-skeleton-icon"
|
||||
/>
|
||||
)}
|
||||
<Skeleton
|
||||
className="h-4 max-w-(--skeleton-width) flex-1"
|
||||
data-sidebar="menu-skeleton-text"
|
||||
style={
|
||||
{
|
||||
"--skeleton-width": width,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="sidebar-menu-sub"
|
||||
data-sidebar="menu-sub"
|
||||
className={cn(
|
||||
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSubItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="sidebar-menu-sub-item"
|
||||
data-sidebar="menu-sub-item"
|
||||
className={cn("group/menu-sub-item relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSubButton({
|
||||
asChild = false,
|
||||
size = "md",
|
||||
isActive = false,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean;
|
||||
size?: "sm" | "md";
|
||||
isActive?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-sub-button"
|
||||
data-sidebar="menu-sub-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
||||
size === "sm" && "text-xs",
|
||||
size === "md" && "text-sm",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupAction,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarInput,
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuBadge,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSkeleton,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarProvider,
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton };
|
||||
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Slider({
|
||||
className,
|
||||
defaultValue,
|
||||
value,
|
||||
min = 0,
|
||||
max = 100,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
||||
const _values = React.useMemo(
|
||||
() =>
|
||||
Array.isArray(value)
|
||||
? value
|
||||
: Array.isArray(defaultValue)
|
||||
? defaultValue
|
||||
: [min, max],
|
||||
[value, defaultValue, min, max]
|
||||
);
|
||||
|
||||
return (
|
||||
<SliderPrimitive.Root
|
||||
data-slot="slider"
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
className={cn(
|
||||
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track
|
||||
data-slot="slider-track"
|
||||
className={cn(
|
||||
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
|
||||
)}
|
||||
>
|
||||
<SliderPrimitive.Range
|
||||
data-slot="slider-range"
|
||||
className={cn(
|
||||
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
|
||||
)}
|
||||
/>
|
||||
</SliderPrimitive.Track>
|
||||
{Array.from({ length: _values.length }, (_, index) => (
|
||||
<SliderPrimitive.Thumb
|
||||
data-slot="slider-thumb"
|
||||
key={index}
|
||||
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
||||
/>
|
||||
))}
|
||||
</SliderPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Slider };
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useTheme } from "next-themes";
|
||||
import { Toaster as Sonner, ToasterProps } from "sonner";
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme();
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Toaster };
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Switch({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||
return (
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
className={cn(
|
||||
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
className={cn(
|
||||
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Switch };
|
||||
@@ -0,0 +1,114 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
className="relative w-full overflow-x-auto"
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("[&_tr]:border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn(
|
||||
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableCaption({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"caption">) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn("flex-1 outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Textarea };
|
||||
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
||||
import { type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toggleVariants } from "@/components/ui/toggle";
|
||||
|
||||
const ToggleGroupContext = React.createContext<
|
||||
VariantProps<typeof toggleVariants>
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<ToggleGroupPrimitive.Root
|
||||
data-slot="toggle-group"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ToggleGroupContext.Provider value={{ variant, size }}>
|
||||
{children}
|
||||
</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleGroupItem({
|
||||
className,
|
||||
children,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
const context = React.useContext(ToggleGroupContext);
|
||||
|
||||
return (
|
||||
<ToggleGroupPrimitive.Item
|
||||
data-slot="toggle-group-item"
|
||||
data-variant={context.variant || variant}
|
||||
data-size={context.size || size}
|
||||
className={cn(
|
||||
toggleVariants({
|
||||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
}),
|
||||
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ToggleGroupPrimitive.Item>
|
||||
);
|
||||
}
|
||||
|
||||
export { ToggleGroup, ToggleGroupItem };
|
||||
@@ -0,0 +1,45 @@
|
||||
import * as React from "react";
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-2 min-w-9",
|
||||
sm: "h-8 px-1.5 min-w-8",
|
||||
lg: "h-10 px-2.5 min-w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function Toggle({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<TogglePrimitive.Root
|
||||
data-slot="toggle"
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Toggle, toggleVariants };
|
||||
@@ -0,0 +1,59 @@
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as React from "react";
|
||||
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
};
|
||||
mql.addEventListener("change", onChange);
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
return () => mql.removeEventListener("change", onChange);
|
||||
}, []);
|
||||
|
||||
return !!isMobile;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
import App from "./App";
|
||||
|
||||
const container = document.getElementById("root") as HTMLElement;
|
||||
const root = createRoot(container);
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import { useForm } from "@refinedev/react-hook-form";
|
||||
import { useSelect } from "@refinedev/core";
|
||||
import { useNavigate } from "react-router";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
import { CreateView } from "@/components/refine-ui/views/create-view";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
|
||||
export const BlogPostCreate = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
refineCore: { onFinish },
|
||||
...form
|
||||
} = useForm({
|
||||
refineCoreProps: {},
|
||||
});
|
||||
|
||||
const { options: categoryOptions } = useSelect({
|
||||
resource: "categories",
|
||||
});
|
||||
|
||||
function onSubmit(values: Record<string, string>) {
|
||||
onFinish(values);
|
||||
}
|
||||
|
||||
return (
|
||||
<CreateView>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
rules={{ required: "Title is required" }}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Title</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
placeholder="Enter title"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="content"
|
||||
rules={{ required: "Content is required" }}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Content</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
placeholder="Enter content"
|
||||
rows={10}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"category.id"}
|
||||
rules={{ required: "Category is required" }}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Category</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value || ""}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a category" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{categoryOptions?.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="status"
|
||||
rules={{ required: "Status is required" }}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Status</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={"draft"}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select status" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="draft">Draft</SelectItem>
|
||||
<SelectItem value="published">Published</SelectItem>
|
||||
<SelectItem value="rejected">Rejected</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="submit"
|
||||
{...form.saveButtonProps}
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
{form.formState.isSubmitting ? "Creating..." : "Create"}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CreateView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,168 @@
|
||||
import { useForm } from "@refinedev/react-hook-form";
|
||||
import { useSelect } from "@refinedev/core";
|
||||
import { useNavigate } from "react-router";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
import { EditView } from "@/components/refine-ui/views/edit-view";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
|
||||
export const BlogPostEdit = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
refineCore: { onFinish, query },
|
||||
...form
|
||||
} = useForm({
|
||||
refineCoreProps: {},
|
||||
});
|
||||
|
||||
const blogPostsData = query?.data?.data;
|
||||
|
||||
const { options: categoryOptions } = useSelect({
|
||||
resource: "categories",
|
||||
defaultValue: blogPostsData?.category,
|
||||
queryOptions: {
|
||||
enabled: !!blogPostsData?.category,
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(values: Record<string, string>) {
|
||||
onFinish(values);
|
||||
}
|
||||
|
||||
return (
|
||||
<EditView>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
rules={{ required: "Title is required" }}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Title</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
placeholder="Enter title"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="content"
|
||||
rules={{ required: "Content is required" }}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Content</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
placeholder="Enter content"
|
||||
rows={10}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"category.id"}
|
||||
rules={{ required: "Category is required" }}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Category</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value || ""}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a category" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{categoryOptions?.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="status"
|
||||
rules={{ required: "Status is required" }}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Status</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value || ""}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select status" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="draft">Draft</SelectItem>
|
||||
<SelectItem value="published">Published</SelectItem>
|
||||
<SelectItem value="rejected">Rejected</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="submit"
|
||||
{...form.saveButtonProps}
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
{form.formState.isSubmitting ? "Updating..." : "Update"}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</EditView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./create";
|
||||
export * from "./edit";
|
||||
export * from "./list";
|
||||
export * from "./show";
|
||||
@@ -0,0 +1,121 @@
|
||||
import { useList } from "@refinedev/core";
|
||||
import { useTable } from "@refinedev/react-table";
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import React from "react";
|
||||
|
||||
import { DataTable } from "@/components/refine-ui/data-table/data-table";
|
||||
import { ListView } from "@/components/refine-ui/views/list-view";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { EditButton } from "@/components/refine-ui/buttons/edit";
|
||||
import { ShowButton } from "@/components/refine-ui/buttons/show";
|
||||
import { DeleteButton } from "@/components/refine-ui/buttons/delete";
|
||||
|
||||
type BlogPost = {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
category: { id: string; title: string };
|
||||
};
|
||||
|
||||
export const BlogPostList = () => {
|
||||
// fetch all categories to use in the combobox filter
|
||||
const {
|
||||
result: { data: categories },
|
||||
query: { isLoading: categoryIsLoading },
|
||||
} = useList({
|
||||
resource: "categories",
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
pageSize: 999,
|
||||
},
|
||||
});
|
||||
|
||||
const columns = React.useMemo(() => {
|
||||
const columnHelper = createColumnHelper<BlogPost>();
|
||||
|
||||
return [
|
||||
columnHelper.accessor("id", {
|
||||
id: "id",
|
||||
header: "ID",
|
||||
enableSorting: false,
|
||||
}),
|
||||
columnHelper.accessor("title", {
|
||||
id: "title",
|
||||
header: "Title",
|
||||
enableSorting: true,
|
||||
}),
|
||||
columnHelper.accessor("content", {
|
||||
id: "content",
|
||||
header: "Content",
|
||||
enableSorting: false,
|
||||
cell: ({ getValue }) => {
|
||||
const content = getValue();
|
||||
if (!content) return "-";
|
||||
return (
|
||||
<div className="max-w-xs truncate">{content.slice(0, 80)}...</div>
|
||||
);
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("category.title", {
|
||||
id: "category",
|
||||
header: "Category",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const categoryId = row.original.category?.id;
|
||||
const category = categories?.find((item) => item.id === categoryId);
|
||||
return categoryIsLoading ? "Loading..." : category?.title || "-";
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("status", {
|
||||
id: "status",
|
||||
header: "Status",
|
||||
enableSorting: true,
|
||||
cell: ({ getValue }) => {
|
||||
const status = getValue();
|
||||
return (
|
||||
<Badge variant={status === "published" ? "default" : "secondary"}>
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("createdAt", {
|
||||
id: "createdAt",
|
||||
header: "Created At",
|
||||
enableSorting: true,
|
||||
cell: ({ getValue }) => {
|
||||
const date = getValue();
|
||||
return date ? new Date(date).toLocaleDateString() : "-";
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
cell: ({ row }) => (
|
||||
<div className="flex gap-2">
|
||||
<EditButton recordItemId={row.original.id} size="sm" />
|
||||
<ShowButton recordItemId={row.original.id} size="sm" />
|
||||
<DeleteButton recordItemId={row.original.id} size="sm" />
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 290,
|
||||
}),
|
||||
];
|
||||
}, [categories, categoryIsLoading]);
|
||||
|
||||
const table = useTable({
|
||||
columns,
|
||||
refineCoreProps: {
|
||||
syncWithLocation: true,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ListView>
|
||||
<DataTable table={table} />
|
||||
</ListView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useOne, useShow } from "@refinedev/core";
|
||||
import React from "react";
|
||||
|
||||
import { ShowView } from "@/components/refine-ui/views/show-view";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
export const BlogPostShow = () => {
|
||||
const { result: record, query } = useShow({});
|
||||
|
||||
const {
|
||||
result: category,
|
||||
query: { isLoading: categoryIsLoading },
|
||||
} = useOne({
|
||||
resource: "categories",
|
||||
id: record?.category?.id || "",
|
||||
queryOptions: {
|
||||
enabled: !!record,
|
||||
},
|
||||
});
|
||||
const { isLoading } = query;
|
||||
|
||||
return (
|
||||
<ShowView>
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{record?.title}</CardTitle>
|
||||
<CardDescription>
|
||||
<div className="flex items-center gap-4">
|
||||
<Badge
|
||||
variant={
|
||||
record?.status === "published" ? "default" : "secondary"
|
||||
}
|
||||
>
|
||||
{record?.status}
|
||||
</Badge>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
ID: {record?.id}
|
||||
</span>
|
||||
</div>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2">Category</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{categoryIsLoading ? "Loading..." : category?.title || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2">Created At</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{record?.createdAt
|
||||
? new Date(record.createdAt).toLocaleDateString()
|
||||
: "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-4">Content</h4>
|
||||
<div className="prose prose-sm max-w-none">
|
||||
{record?.content ? (
|
||||
<div dangerouslySetInnerHTML={{ __html: record.content }} />
|
||||
) : (
|
||||
<p className="text-muted-foreground">No content available</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</ShowView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useForm } from "@refinedev/react-hook-form";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
import { CreateView } from "@/components/refine-ui/views/create-view";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
|
||||
export const CategoryCreate = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
refineCore: { onFinish },
|
||||
...form
|
||||
} = useForm({
|
||||
refineCoreProps: {},
|
||||
});
|
||||
|
||||
function onSubmit(values: Record<string, string>) {
|
||||
onFinish(values);
|
||||
}
|
||||
|
||||
return (
|
||||
<CreateView>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
rules={{ required: "Title is required" }}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Title</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
placeholder="Enter category title"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="submit"
|
||||
{...form.saveButtonProps}
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
{form.formState.isSubmitting ? "Creating..." : "Create"}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CreateView>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user