Initial Commit
33
.eslintrc.json
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// .eslintrc.json
|
||||
{
|
||||
"extends": [
|
||||
// 1. Configurações base do Next/React
|
||||
"next/core-web-vitals",
|
||||
// 2. Regras para ordenar e gerenciar importações
|
||||
"plugin:import/recommended",
|
||||
// 3. DESATIVA as regras do ESLint que conflitam com o Prettier (DEVE SER O ÚLTIMO)
|
||||
"prettier"
|
||||
],
|
||||
"plugins": ["import"],
|
||||
"rules": {
|
||||
/* --- Qualidade do Código (Next.js/React) --- */
|
||||
// Essa regra (já incluída no Next.js, mas bom reforçar) é a que remove imports não usados
|
||||
"no-unused-vars": "error",
|
||||
"react/jsx-uses-vars": "error",
|
||||
/* --- Ordem e Remoção de Importações (eslint-plugin-import) --- */
|
||||
// Configura a regra para a ordem das importações (groups/grupos)
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"groups": ["builtin", "external", "internal", "parent", "sibling", "index"],
|
||||
"newlines-between": "always",
|
||||
"alphabetize": {
|
||||
"order": "asc",
|
||||
"caseInsensitive": true
|
||||
}
|
||||
}
|
||||
],
|
||||
// Garante que o Next.js reconheça imports (como 'next/image', 'next/link')
|
||||
"import/no-unresolved": "error"
|
||||
}
|
||||
}
|
||||
43
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
/src/config/app.json
|
||||
9
.prettierrc
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"semi": true,
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"arrowParens": "always",
|
||||
"plugins": ["prettier-plugin-tailwindcss"]
|
||||
}
|
||||
22
Dockerfile
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Use uma imagem Node.js completa para o desenvolvimento
|
||||
FROM node:20-alpine
|
||||
|
||||
# Define o diretório de trabalho no container
|
||||
WORKDIR /app
|
||||
|
||||
# Copia os arquivos de configuração do projeto
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
# Instala todas as dependências do projeto
|
||||
# Isso é necessário para o modo de desenvolvimento, pois o build não pré-compila os arquivos.
|
||||
RUN npm install
|
||||
|
||||
# Copia o restante do código da sua aplicação
|
||||
COPY . .
|
||||
|
||||
# Expõe a porta de desenvolvimento padrão do Next.js
|
||||
EXPOSE 3000
|
||||
|
||||
# Define o comando para iniciar a aplicação em modo de desenvolvimento
|
||||
# Isso ativará o servidor de desenvolvimento e a recarga automática
|
||||
CMD ["npm", "run", "dev"]
|
||||
41
Dockerfile-homologacao
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Etapa 1: Construir a aplicação
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
# Define o diretório de trabalho
|
||||
WORKDIR /app
|
||||
|
||||
# Copia os arquivos de configuração do pacote
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
# Instala as dependências do projeto
|
||||
RUN npm install
|
||||
|
||||
# Copia todo o código da aplicação para o container
|
||||
COPY . .
|
||||
|
||||
# Constrói a aplicação com o output 'standalone'
|
||||
RUN npm run build
|
||||
|
||||
# Etapa 2: Executar a aplicação
|
||||
# Usa uma imagem Node.js leve
|
||||
FROM node:20-alpine AS runner
|
||||
|
||||
# Define o diretório de trabalho
|
||||
WORKDIR /app
|
||||
|
||||
# Copia o diretório 'standalone' da etapa de build, que já contém o servidor e as dependências
|
||||
# O diretório 'standalone' é a pasta .next/standalone gerada pela configuração 'output: standalone'
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
|
||||
# Copia os arquivos públicos
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
# Copia os arquivos estáticos gerados pelo build. É aqui que os arquivos CSS e JS ficam.
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
|
||||
# Expõe a porta padrão do Next.js
|
||||
EXPOSE 3000
|
||||
|
||||
# Define o comando para iniciar a aplicação
|
||||
# O 'start' do package.json não é necessário, o próprio servidor standalone já está no container
|
||||
CMD ["node", "server.js"]
|
||||
9
LICENSE
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 OriusTecnologia
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
1
README.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# saas_app
|
||||
21
components.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
23
eslint.config.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import next from 'eslint-config-next';
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
|
||||
export default [
|
||||
next,
|
||||
{
|
||||
plugins: {
|
||||
import: importPlugin,
|
||||
},
|
||||
rules: {
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
groups: ['builtin', 'external', 'internal', ['parent', 'sibling', 'index']],
|
||||
'newlines-between': 'always',
|
||||
alphabetize: { order: 'asc', caseInsensitive: true },
|
||||
},
|
||||
],
|
||||
semi: ['error', 'always'],
|
||||
quotes: ['error', 'double'],
|
||||
},
|
||||
},
|
||||
];
|
||||
8
next.config.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import type { NextConfig } from 'next';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
// Isso gera um diretório otimizado que inclui tudo o que a aplicação precisa para rodar
|
||||
output: 'standalone',
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
8125
package-lock.json
generated
Normal file
75
package.json
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
{
|
||||
"type": "module",
|
||||
"name": "saas",
|
||||
"version": "25.9.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"format": "prettier --write . --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "^10.0.0",
|
||||
"@hookform/resolvers": "^5.2.1",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tinymce/tinymce-react": "^6.3.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"cookies-next": "^6.1.0",
|
||||
"faker-js": "^1.0.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lucide-react": "^0.540.0",
|
||||
"next": "^15.5.3",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-masked-text": "^1.0.5",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tinymce": "^8.1.2",
|
||||
"zod": "^4.0.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.12.0",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@typescript-eslint/eslint-plugin": "^8.45.0",
|
||||
"@typescript-eslint/parser": "^8.45.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-next": "^15.5.4",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-unused-imports": "^4.2.0",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.3.7",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
5
postcss.config.mjs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
const config = {
|
||||
plugins: ['@tailwindcss/postcss'],
|
||||
};
|
||||
|
||||
export default config;
|
||||
1
public/file.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
1
public/globe.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1 KiB |
BIN
public/images/favicon.ico
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
public/images/logo-abb.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
27
public/images/logo-login.svg
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Creator: CorelDRAW 2018 (64 Bit) -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="109.504mm" height="32.45mm" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
|
||||
viewBox="0 0 4674.85 1385.32"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
<![CDATA[
|
||||
.fil0 {fill:#FF6600}
|
||||
.fil1 {fill:white;fill-rule:nonzero}
|
||||
]]>
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Camada_x0020_1">
|
||||
<metadata id="CorelCorpID_0Corel-Layer"/>
|
||||
<g id="_2536796572048">
|
||||
<path class="fil0" d="M444.51 337.84c440.36,-70.7 476.63,243.79 383.3,374.93 -121.36,170.53 -504.61,132.14 -589.48,-37.21 -31.1,-62.05 -10.18,-188.76 23.33,-238.81 35.98,-53.73 97.41,-85.19 182.85,-98.91zm-444.51 192.86c0,156.21 12.26,277.5 171.92,375.2 214.59,131.32 589.63,134.32 795.72,-31.29 174.6,-140.3 173.88,-487.9 -30.87,-624.12 -52.36,-34.83 -93.57,-53.67 -162.11,-73.16 -236.65,-67.31 -582.01,-30.52 -720.03,178.21 -25.47,38.51 -54.62,111.65 -54.62,175.17z"/>
|
||||
<path class="fil0" d="M3818.84 393.91c0,148.34 145.5,201.77 263.73,234.14 93.01,25.46 365.44,57.71 365.44,126.96 0,66.65 -152.26,76.59 -224.32,76.59 -221.11,0 -348.08,-81.05 -399.39,-93.01l0 196.95 192.2 43.05c87.27,14.66 138.58,22.6 234.54,22.6 441.47,0 507.61,-275.63 337.69,-419.75 -140.93,-119.53 -545.59,-120.82 -545.59,-203.95 0,-95.45 359.81,-45.21 409.3,-31.8l154.22 53.68 0 -186.02 -176.42 -42.42c-214.47,-43.37 -611.42,-48.39 -611.42,222.96z"/>
|
||||
<path class="fil0" d="M2631.61 755.01c0,86.12 62.34,149.87 115.84,185.07 127.76,84.05 323.75,68.39 463.32,16.58 29.5,-10.95 61.1,-25 91.99,-39.32 39.53,-18.31 67.28,-41.35 94.8,-41.97l0 103.95 218.85 0 0 -809.72 -218.85 0 0 492.39c0,18.46 -54.62,47.33 -68.92,56.91 -26.89,18 -50.44,32.94 -78.73,47.11 -130.98,65.59 -399.45,116.59 -399.45,-114.96l0 -481.45 -218.85 0 0 585.41z"/>
|
||||
<path class="fil0" d="M1515.5 289.97c0,-46.34 -5.47,-72.07 -5.47,-120.37l-218.84 0 0 809.72 218.84 0 0 -475.99c0,-25.16 113.08,-106.37 136.96,-120.18 84.99,-49.19 220.78,-69.84 320.2,-46.99 23.36,5.37 43.45,15 62.61,19.46l0 -186.02c-126.58,-10.54 -178.34,-55.19 -359.3,29.15 -83.29,38.81 -108.59,66.67 -154.99,91.22z"/>
|
||||
<polygon class="fil0" points="2166.56,979.32 2385.41,979.32 2385.41,169.6 2166.56,169.6 "/>
|
||||
<path class="fil0" d="M2172.04 131.3l218.85 0 0 -131.3c-30.09,7.01 -146.3,65.43 -180.57,82.04 -33.64,16.3 -38.27,6.4 -38.27,49.26z"/>
|
||||
</g>
|
||||
<path class="fil1" d="M99.26 1380.26l0 -225.54 -81.33 0 0 -27.47 193.01 0 0 27.47 -81.33 0 0 225.54 -30.36 0zm433.68 0l0 -253.02 155.79 0 0 27.47 -125.43 0 0 84.58 115.3 0 0 27.47 -115.3 0 0 86.02 127.23 0 0 27.47 -157.59 0zm578.27 5.06c-30.12,0 -54.1,-8.73 -71.93,-26.2 -17.83,-17.47 -26.75,-42.95 -26.75,-76.45l0 -57.83c0,-33.5 8.91,-58.98 26.75,-76.45 17.83,-17.47 41.81,-26.2 71.93,-26.2 29.88,0 52.83,8.31 68.86,24.94 16.03,16.62 24.04,39.4 24.04,68.31l0 1.81 -30 0 0 -2.89c0,-19.04 -5.12,-34.64 -15.36,-46.81 -10.24,-12.17 -26.08,-18.25 -47.53,-18.25 -21.44,0 -38.19,6.57 -50.24,19.7 -12.05,13.13 -18.07,31.51 -18.07,55.12l0 59.28c0,23.62 6.02,41.99 18.07,55.12 12.05,13.13 28.8,19.7 50.24,19.7 21.45,0 37.29,-6.08 47.53,-18.25 10.24,-12.17 15.36,-27.77 15.36,-46.81l0 -5.78 30 0 0 4.7c0,28.92 -8.01,51.69 -24.04,68.31 -16.02,16.63 -38.98,24.94 -68.86,24.94zm423.92 -5.06l0 -253.02 58.92 0 88.2 233.86 4.7 0 0 -233.86 30 0 0 253.02 -58.92 0 -87.83 -234.22 -5.06 0 0 234.22 -30 0zm615.13 5.06c-30.12,0 -54.16,-8.73 -72.11,-26.2 -17.95,-17.47 -26.93,-42.95 -26.93,-76.45l0 -57.83c0,-33.5 8.98,-58.98 26.93,-76.45 17.95,-17.47 41.99,-26.2 72.11,-26.2 30.36,0 54.52,8.73 72.47,26.2 17.95,17.47 26.93,42.95 26.93,76.45l0 57.83c0,33.5 -8.97,58.98 -26.93,76.45 -17.96,17.47 -42.11,26.2 -72.47,26.2zm0 -27.11c21.93,0 38.92,-6.57 50.96,-19.7 12.05,-13.13 18.07,-31.39 18.07,-54.76l0 -60c0,-23.37 -6.02,-41.63 -18.07,-54.76 -12.05,-13.13 -29.04,-19.7 -50.96,-19.7 -21.69,0 -38.55,6.57 -50.61,19.7 -12.05,13.13 -18.07,31.39 -18.07,54.76l0 60c0,23.37 6.02,41.63 18.07,54.76 12.05,13.13 28.92,19.7 50.61,19.7zm433.32 22.05l0 -253.02 30.36 0 0 225.54 127.96 0 0 27.47 -158.32 0zm565.25 5.06c-30.12,0 -54.16,-8.73 -72.11,-26.2 -17.95,-17.47 -26.93,-42.95 -26.93,-76.45l0 -57.83c0,-33.5 8.98,-58.98 26.93,-76.45 17.95,-17.47 41.99,-26.2 72.11,-26.2 30.36,0 54.52,8.73 72.47,26.2 17.95,17.47 26.93,42.95 26.93,76.45l0 57.83c0,33.5 -8.97,58.98 -26.93,76.45 -17.96,17.47 -42.11,26.2 -72.47,26.2zm0 -27.11c21.93,0 38.92,-6.57 50.96,-19.7 12.05,-13.13 18.07,-31.39 18.07,-54.76l0 -60c0,-23.37 -6.02,-41.63 -18.07,-54.76 -12.05,-13.13 -29.04,-19.7 -50.96,-19.7 -21.69,0 -38.55,6.57 -50.61,19.7 -12.05,13.13 -18.07,31.39 -18.07,54.76l0 60c0,23.37 6.02,41.63 18.07,54.76 12.05,13.13 28.92,19.7 50.61,19.7zm521.51 27.11c-18.8,0 -35.36,-3.91 -49.7,-11.74 -14.34,-7.83 -25.54,-19.34 -33.61,-34.52 -8.07,-15.18 -12.11,-33.98 -12.11,-56.39l0 -57.83c0,-33.5 8.91,-58.98 26.75,-76.45 17.83,-17.47 41.81,-26.2 71.93,-26.2 29.88,0 52.65,8.19 68.31,24.58 15.66,16.38 23.5,38.32 23.5,65.79l0 1.81 -30 0 0 -2.53c0,-12.05 -2.17,-22.77 -6.51,-32.17 -4.34,-9.4 -11.08,-16.81 -20.24,-22.23 -9.16,-5.42 -20.85,-8.13 -35.06,-8.13 -21.44,0 -38.19,6.57 -50.24,19.7 -12.05,13.13 -18.07,31.51 -18.07,55.12l0 59.28c0,23.62 6.02,41.99 18.07,55.12 12.05,13.13 28.92,19.7 50.61,19.7 21.2,0 36.81,-6.02 46.81,-18.07 10,-12.05 15,-28.07 15,-48.07l0 -6.14 -76.63 0 0 -26.02 106.27 0 0 120.36 -27.47 0 0 -27.47 -5.06 0c-2.65,5.3 -6.38,10.48 -11.2,15.54 -4.82,5.06 -11.33,9.16 -19.52,12.29 -8.19,3.13 -18.8,4.7 -31.81,4.7zm433.68 -5.06l0 -253.02 30.36 0 0 253.02 -30.36 0zm351.64 0l78.07 -253.02 53.49 0 78.08 253.02 -31.45 0 -19.52 -63.98 -107.71 0 -19.52 63.98 -31.45 0zm58.55 -91.45l92.17 0 -43.37 -143.13 -5.06 0 -43.73 143.13z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.8 KiB |
27
public/images/logo.svg
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Creator: CorelDRAW 2018 (64 Bit) -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="109.504mm" height="32.45mm" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
|
||||
viewBox="0 0 4674.85 1385.32"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
<![CDATA[
|
||||
.fil0 {fill:#FF6600}
|
||||
.fil1 {fill:white;fill-rule:nonzero}
|
||||
]]>
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Camada_x0020_1">
|
||||
<metadata id="CorelCorpID_0Corel-Layer"/>
|
||||
<g id="_2536796572048">
|
||||
<path class="fil0" d="M444.51 337.84c440.36,-70.7 476.63,243.79 383.3,374.93 -121.36,170.53 -504.61,132.14 -589.48,-37.21 -31.1,-62.05 -10.18,-188.76 23.33,-238.81 35.98,-53.73 97.41,-85.19 182.85,-98.91zm-444.51 192.86c0,156.21 12.26,277.5 171.92,375.2 214.59,131.32 589.63,134.32 795.72,-31.29 174.6,-140.3 173.88,-487.9 -30.87,-624.12 -52.36,-34.83 -93.57,-53.67 -162.11,-73.16 -236.65,-67.31 -582.01,-30.52 -720.03,178.21 -25.47,38.51 -54.62,111.65 -54.62,175.17z"/>
|
||||
<path class="fil0" d="M3818.84 393.91c0,148.34 145.5,201.77 263.73,234.14 93.01,25.46 365.44,57.71 365.44,126.96 0,66.65 -152.26,76.59 -224.32,76.59 -221.11,0 -348.08,-81.05 -399.39,-93.01l0 196.95 192.2 43.05c87.27,14.66 138.58,22.6 234.54,22.6 441.47,0 507.61,-275.63 337.69,-419.75 -140.93,-119.53 -545.59,-120.82 -545.59,-203.95 0,-95.45 359.81,-45.21 409.3,-31.8l154.22 53.68 0 -186.02 -176.42 -42.42c-214.47,-43.37 -611.42,-48.39 -611.42,222.96z"/>
|
||||
<path class="fil0" d="M2631.61 755.01c0,86.12 62.34,149.87 115.84,185.07 127.76,84.05 323.75,68.39 463.32,16.58 29.5,-10.95 61.1,-25 91.99,-39.32 39.53,-18.31 67.28,-41.35 94.8,-41.97l0 103.95 218.85 0 0 -809.72 -218.85 0 0 492.39c0,18.46 -54.62,47.33 -68.92,56.91 -26.89,18 -50.44,32.94 -78.73,47.11 -130.98,65.59 -399.45,116.59 -399.45,-114.96l0 -481.45 -218.85 0 0 585.41z"/>
|
||||
<path class="fil0" d="M1515.5 289.97c0,-46.34 -5.47,-72.07 -5.47,-120.37l-218.84 0 0 809.72 218.84 0 0 -475.99c0,-25.16 113.08,-106.37 136.96,-120.18 84.99,-49.19 220.78,-69.84 320.2,-46.99 23.36,5.37 43.45,15 62.61,19.46l0 -186.02c-126.58,-10.54 -178.34,-55.19 -359.3,29.15 -83.29,38.81 -108.59,66.67 -154.99,91.22z"/>
|
||||
<polygon class="fil0" points="2166.56,979.32 2385.41,979.32 2385.41,169.6 2166.56,169.6 "/>
|
||||
<path class="fil0" d="M2172.04 131.3l218.85 0 0 -131.3c-30.09,7.01 -146.3,65.43 -180.57,82.04 -33.64,16.3 -38.27,6.4 -38.27,49.26z"/>
|
||||
</g>
|
||||
<path class="fil1" d="M99.26 1380.26l0 -225.54 -81.33 0 0 -27.47 193.01 0 0 27.47 -81.33 0 0 225.54 -30.36 0zm433.68 0l0 -253.02 155.79 0 0 27.47 -125.43 0 0 84.58 115.3 0 0 27.47 -115.3 0 0 86.02 127.23 0 0 27.47 -157.59 0zm578.27 5.06c-30.12,0 -54.1,-8.73 -71.93,-26.2 -17.83,-17.47 -26.75,-42.95 -26.75,-76.45l0 -57.83c0,-33.5 8.91,-58.98 26.75,-76.45 17.83,-17.47 41.81,-26.2 71.93,-26.2 29.88,0 52.83,8.31 68.86,24.94 16.03,16.62 24.04,39.4 24.04,68.31l0 1.81 -30 0 0 -2.89c0,-19.04 -5.12,-34.64 -15.36,-46.81 -10.24,-12.17 -26.08,-18.25 -47.53,-18.25 -21.44,0 -38.19,6.57 -50.24,19.7 -12.05,13.13 -18.07,31.51 -18.07,55.12l0 59.28c0,23.62 6.02,41.99 18.07,55.12 12.05,13.13 28.8,19.7 50.24,19.7 21.45,0 37.29,-6.08 47.53,-18.25 10.24,-12.17 15.36,-27.77 15.36,-46.81l0 -5.78 30 0 0 4.7c0,28.92 -8.01,51.69 -24.04,68.31 -16.02,16.63 -38.98,24.94 -68.86,24.94zm423.92 -5.06l0 -253.02 58.92 0 88.2 233.86 4.7 0 0 -233.86 30 0 0 253.02 -58.92 0 -87.83 -234.22 -5.06 0 0 234.22 -30 0zm615.13 5.06c-30.12,0 -54.16,-8.73 -72.11,-26.2 -17.95,-17.47 -26.93,-42.95 -26.93,-76.45l0 -57.83c0,-33.5 8.98,-58.98 26.93,-76.45 17.95,-17.47 41.99,-26.2 72.11,-26.2 30.36,0 54.52,8.73 72.47,26.2 17.95,17.47 26.93,42.95 26.93,76.45l0 57.83c0,33.5 -8.97,58.98 -26.93,76.45 -17.96,17.47 -42.11,26.2 -72.47,26.2zm0 -27.11c21.93,0 38.92,-6.57 50.96,-19.7 12.05,-13.13 18.07,-31.39 18.07,-54.76l0 -60c0,-23.37 -6.02,-41.63 -18.07,-54.76 -12.05,-13.13 -29.04,-19.7 -50.96,-19.7 -21.69,0 -38.55,6.57 -50.61,19.7 -12.05,13.13 -18.07,31.39 -18.07,54.76l0 60c0,23.37 6.02,41.63 18.07,54.76 12.05,13.13 28.92,19.7 50.61,19.7zm433.32 22.05l0 -253.02 30.36 0 0 225.54 127.96 0 0 27.47 -158.32 0zm565.25 5.06c-30.12,0 -54.16,-8.73 -72.11,-26.2 -17.95,-17.47 -26.93,-42.95 -26.93,-76.45l0 -57.83c0,-33.5 8.98,-58.98 26.93,-76.45 17.95,-17.47 41.99,-26.2 72.11,-26.2 30.36,0 54.52,8.73 72.47,26.2 17.95,17.47 26.93,42.95 26.93,76.45l0 57.83c0,33.5 -8.97,58.98 -26.93,76.45 -17.96,17.47 -42.11,26.2 -72.47,26.2zm0 -27.11c21.93,0 38.92,-6.57 50.96,-19.7 12.05,-13.13 18.07,-31.39 18.07,-54.76l0 -60c0,-23.37 -6.02,-41.63 -18.07,-54.76 -12.05,-13.13 -29.04,-19.7 -50.96,-19.7 -21.69,0 -38.55,6.57 -50.61,19.7 -12.05,13.13 -18.07,31.39 -18.07,54.76l0 60c0,23.37 6.02,41.63 18.07,54.76 12.05,13.13 28.92,19.7 50.61,19.7zm521.51 27.11c-18.8,0 -35.36,-3.91 -49.7,-11.74 -14.34,-7.83 -25.54,-19.34 -33.61,-34.52 -8.07,-15.18 -12.11,-33.98 -12.11,-56.39l0 -57.83c0,-33.5 8.91,-58.98 26.75,-76.45 17.83,-17.47 41.81,-26.2 71.93,-26.2 29.88,0 52.65,8.19 68.31,24.58 15.66,16.38 23.5,38.32 23.5,65.79l0 1.81 -30 0 0 -2.53c0,-12.05 -2.17,-22.77 -6.51,-32.17 -4.34,-9.4 -11.08,-16.81 -20.24,-22.23 -9.16,-5.42 -20.85,-8.13 -35.06,-8.13 -21.44,0 -38.19,6.57 -50.24,19.7 -12.05,13.13 -18.07,31.51 -18.07,55.12l0 59.28c0,23.62 6.02,41.99 18.07,55.12 12.05,13.13 28.92,19.7 50.61,19.7 21.2,0 36.81,-6.02 46.81,-18.07 10,-12.05 15,-28.07 15,-48.07l0 -6.14 -76.63 0 0 -26.02 106.27 0 0 120.36 -27.47 0 0 -27.47 -5.06 0c-2.65,5.3 -6.38,10.48 -11.2,15.54 -4.82,5.06 -11.33,9.16 -19.52,12.29 -8.19,3.13 -18.8,4.7 -31.81,4.7zm433.68 -5.06l0 -253.02 30.36 0 0 253.02 -30.36 0zm351.64 0l78.07 -253.02 53.49 0 78.08 253.02 -31.45 0 -19.52 -63.98 -107.71 0 -19.52 63.98 -31.45 0zm58.55 -91.45l92.17 0 -43.37 -143.13 -5.06 0 -43.73 143.13z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.8 KiB |
1
public/vercel.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
1
public/window.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
13
src/app/(protected)/administrativo/imoveis/rurais/page.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
'use client';
|
||||
|
||||
import TImovelIndex from "@/packages/administrativo/components/TImovel/TImovelIndex";
|
||||
|
||||
export default function TImovelRuralPage() {
|
||||
return (
|
||||
<TImovelIndex
|
||||
pageTitle="Imóveis Rurais"
|
||||
pageDescription="Gerenciamento de imóveis rurais"
|
||||
tipoClasse={3}
|
||||
/>
|
||||
);
|
||||
}
|
||||
85
src/app/(protected)/layout.tsx
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import type { Metadata } from 'next';
|
||||
import { Geist, Geist_Mono } from 'next/font/google';
|
||||
import '../globals.css';
|
||||
|
||||
import { ResponseProvider } from '../../shared/components/response/ResponseContext';
|
||||
import { AppSidebar } from '@/components/app-sidebar';
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from '@/components/ui/breadcrumb';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar';
|
||||
import { Toaster } from '@/components/ui/sonner';
|
||||
import Response from '../../shared/components/response/response';
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: '--font-geist-sans',
|
||||
subsets: ['latin'],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: '--font-geist-mono',
|
||||
subsets: ['latin'],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'SAAS - Orius Tecnologia',
|
||||
description: 'Evolução tecnológica com toque humano',
|
||||
icons: {
|
||||
icon: '/images/favicon.ico',
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<header className="mb-4 flex h-16 shrink-0 items-center gap-2 border-b-1 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
|
||||
<div className="flex items-center gap-2 px-4">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mr-2 data-[orientation=vertical]:h-4"
|
||||
/>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem className="hidden md:block">
|
||||
<BreadcrumbLink href="#">
|
||||
Building Your Application
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden md:block" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>
|
||||
Data Fetching
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</header>
|
||||
<ResponseProvider>
|
||||
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
|
||||
{children}
|
||||
<Toaster richColors position="top-center" />
|
||||
<Response />
|
||||
</div>
|
||||
</ResponseProvider>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
3
src/app/(protected)/page.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export default function Page() {
|
||||
return <div></div>;
|
||||
}
|
||||
31
src/app/(public)/login/layout.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import type { Metadata } from 'next';
|
||||
import { Geist, Geist_Mono } from 'next/font/google';
|
||||
import '../../globals.css';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: '--font-geist-sans',
|
||||
subsets: ['latin'],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: '--font-geist-mono',
|
||||
subsets: ['latin'],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Create Next App',
|
||||
description: 'Generated by create next app',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
11
src/app/(public)/login/page.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { LoginForm } from '@/packages/administrativo/components/GUsuario/GUsuarioLoginForm';
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<div className="bg-muted flex min-h-svh flex-col items-center justify-center p-6 md:p-10">
|
||||
<div className="w-full max-w-sm md:max-w-3xl">
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
BIN
src/app/favicon.ico
Normal file
|
After Width: | Height: | Size: 25 KiB |
193
src/app/globals.css
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: Inter, sans-serif;
|
||||
--font-mono: JetBrains Mono, monospace;
|
||||
--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-serif: Source Serif 4, serif;
|
||||
--radius: 0.375rem;
|
||||
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
|
||||
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
|
||||
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
|
||||
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
|
||||
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
|
||||
--tracking-normal: var(--tracking-normal);
|
||||
--shadow-2xl: var(--shadow-2xl);
|
||||
--shadow-xl: var(--shadow-xl);
|
||||
--shadow-lg: var(--shadow-lg);
|
||||
--shadow-md: var(--shadow-md);
|
||||
--shadow: var(--shadow);
|
||||
--shadow-sm: var(--shadow-sm);
|
||||
--shadow-xs: var(--shadow-xs);
|
||||
--shadow-2xs: var(--shadow-2xs);
|
||||
--spacing: var(--spacing);
|
||||
--letter-spacing: var(--letter-spacing);
|
||||
--shadow-offset-y: var(--shadow-offset-y);
|
||||
--shadow-offset-x: var(--shadow-offset-x);
|
||||
--shadow-spread: var(--shadow-spread);
|
||||
--shadow-blur: var(--shadow-blur);
|
||||
--shadow-opacity: var(--shadow-opacity);
|
||||
--color-shadow-color: var(--shadow-color);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.375rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.2686 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.2686 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.2686 0 0);
|
||||
--primary: oklch(0.7686 0.1647 70.0804);
|
||||
--primary-foreground: oklch(0 0 0);
|
||||
--secondary: oklch(0.967 0.0029 264.5419);
|
||||
--secondary-foreground: oklch(0.4461 0.0263 256.8018);
|
||||
--muted: oklch(0.9846 0.0017 247.8389);
|
||||
--muted-foreground: oklch(0.551 0.0234 264.3637);
|
||||
--accent: oklch(0.9869 0.0214 95.2774);
|
||||
--accent-foreground: oklch(0.4732 0.1247 46.2007);
|
||||
--destructive: oklch(0.6368 0.2078 25.3313);
|
||||
--border: oklch(0.9276 0.0058 264.5313);
|
||||
--input: oklch(0.9276 0.0058 264.5313);
|
||||
--ring: oklch(0.7686 0.1647 70.0804);
|
||||
--chart-1: oklch(0.7686 0.1647 70.0804);
|
||||
--chart-2: oklch(0.6658 0.1574 58.3183);
|
||||
--chart-3: oklch(0.5553 0.1455 48.9975);
|
||||
--chart-4: oklch(0.4732 0.1247 46.2007);
|
||||
--chart-5: oklch(0.4137 0.1054 45.9038);
|
||||
--sidebar: oklch(0.9846 0.0017 247.8389);
|
||||
--sidebar-foreground: oklch(0.2686 0 0);
|
||||
--sidebar-primary: oklch(0.7686 0.1647 70.0804);
|
||||
--sidebar-primary-foreground: oklch(1 0 0);
|
||||
--sidebar-accent: oklch(0.9869 0.0214 95.2774);
|
||||
--sidebar-accent-foreground: oklch(0.4732 0.1247 46.2007);
|
||||
--sidebar-border: oklch(0.9276 0.0058 264.5313);
|
||||
--sidebar-ring: oklch(0.7686 0.1647 70.0804);
|
||||
--destructive-foreground: oklch(1 0 0);
|
||||
--font-sans: Inter, sans-serif;
|
||||
--font-serif: Source Serif 4, serif;
|
||||
--font-mono: JetBrains Mono, monospace;
|
||||
--shadow-color: hsl(0 0% 0%);
|
||||
--shadow-opacity: 0.1;
|
||||
--shadow-blur: 8px;
|
||||
--shadow-spread: -1px;
|
||||
--shadow-offset-x: 0px;
|
||||
--shadow-offset-y: 4px;
|
||||
--letter-spacing: 0em;
|
||||
--spacing: 0.25rem;
|
||||
--shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
|
||||
--shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 1px 2px -2px hsl(0 0% 0% / 0.1);
|
||||
--shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 1px 2px -2px hsl(0 0% 0% / 0.1);
|
||||
--shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 2px 4px -2px hsl(0 0% 0% / 0.1);
|
||||
--shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 4px 6px -2px hsl(0 0% 0% / 0.1);
|
||||
--shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 8px 10px -2px hsl(0 0% 0% / 0.1);
|
||||
--shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25);
|
||||
--tracking-normal: 0em;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.2046 0 0);
|
||||
--foreground: oklch(0.9219 0 0);
|
||||
--card: oklch(0.2686 0 0);
|
||||
--card-foreground: oklch(0.9219 0 0);
|
||||
--popover: oklch(0.2686 0 0);
|
||||
--popover-foreground: oklch(0.9219 0 0);
|
||||
--primary: oklch(0.7686 0.1647 70.0804);
|
||||
--primary-foreground: oklch(0 0 0);
|
||||
--secondary: oklch(0.2686 0 0);
|
||||
--secondary-foreground: oklch(0.9219 0 0);
|
||||
--muted: oklch(0.2686 0 0);
|
||||
--muted-foreground: oklch(0.7155 0 0);
|
||||
--accent: oklch(0.4732 0.1247 46.2007);
|
||||
--accent-foreground: oklch(0.9243 0.1151 95.7459);
|
||||
--destructive: oklch(0.6368 0.2078 25.3313);
|
||||
--border: oklch(0.3715 0 0);
|
||||
--input: oklch(0.3715 0 0);
|
||||
--ring: oklch(0.7686 0.1647 70.0804);
|
||||
--chart-1: oklch(0.8369 0.1644 84.4286);
|
||||
--chart-2: oklch(0.6658 0.1574 58.3183);
|
||||
--chart-3: oklch(0.4732 0.1247 46.2007);
|
||||
--chart-4: oklch(0.5553 0.1455 48.9975);
|
||||
--chart-5: oklch(0.4732 0.1247 46.2007);
|
||||
--sidebar: oklch(0.1684 0 0);
|
||||
--sidebar-foreground: oklch(0.9219 0 0);
|
||||
--sidebar-primary: oklch(0.7686 0.1647 70.0804);
|
||||
--sidebar-primary-foreground: oklch(1 0 0);
|
||||
--sidebar-accent: oklch(0.4732 0.1247 46.2007);
|
||||
--sidebar-accent-foreground: oklch(0.9243 0.1151 95.7459);
|
||||
--sidebar-border: oklch(0.3715 0 0);
|
||||
--sidebar-ring: oklch(0.7686 0.1647 70.0804);
|
||||
--destructive-foreground: oklch(1 0 0);
|
||||
--radius: 0.375rem;
|
||||
--font-sans: Inter, sans-serif;
|
||||
--font-serif: Source Serif 4, serif;
|
||||
--font-mono: JetBrains Mono, monospace;
|
||||
--shadow-color: hsl(0 0% 0%);
|
||||
--shadow-opacity: 0.1;
|
||||
--shadow-blur: 8px;
|
||||
--shadow-spread: -1px;
|
||||
--shadow-offset-x: 0px;
|
||||
--shadow-offset-y: 4px;
|
||||
--letter-spacing: 0em;
|
||||
--spacing: 0.25rem;
|
||||
--shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
|
||||
--shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 1px 2px -2px hsl(0 0% 0% / 0.1);
|
||||
--shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 1px 2px -2px hsl(0 0% 0% / 0.1);
|
||||
--shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 2px 4px -2px hsl(0 0% 0% / 0.1);
|
||||
--shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 4px 6px -2px hsl(0 0% 0% / 0.1);
|
||||
--shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 8px 10px -2px hsl(0 0% 0% / 0.1);
|
||||
--shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
letter-spacing: var(--tracking-normal);
|
||||
}
|
||||
.bg-brand {
|
||||
background-color: #1a292f;
|
||||
}
|
||||
}
|
||||
4
src/app/loading.tsx
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// components/Loading.tsx
|
||||
export default function Loading() {
|
||||
return <span className="loading loading-spinner loading-xs"></span>;
|
||||
}
|
||||
160
src/components/MainEditor.tsx
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import React from 'react';
|
||||
import { Editor } from '@tinymce/tinymce-react';
|
||||
|
||||
// 1. Define as propriedades que nosso componente vai receber
|
||||
interface MainEditorProps {
|
||||
initialValue: string;
|
||||
onEditorChange: (content: string) => void;
|
||||
margins: {
|
||||
top: string;
|
||||
bottom: string;
|
||||
left: string;
|
||||
right: string;
|
||||
};
|
||||
size: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
const MainEditor: React.FC<MainEditorProps> = ({ initialValue, onEditorChange, margins, size }) => {
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<Editor
|
||||
apiKey="sny4ncto4hf42akdz2eqss2tqd0loo439vfttpuydjc2kqpi"
|
||||
value={initialValue}
|
||||
onEditorChange={onEditorChange}
|
||||
init={{
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
menubar: false,
|
||||
browser_spellcheck: true,
|
||||
contextmenu: false,
|
||||
plugins: [
|
||||
'advlist',
|
||||
'autolink',
|
||||
'lists',
|
||||
'link',
|
||||
'image',
|
||||
'charmap',
|
||||
'preview',
|
||||
'anchor',
|
||||
'searchreplace',
|
||||
'visualblocks',
|
||||
'fullscreen',
|
||||
'insertdatetime',
|
||||
'media',
|
||||
'table',
|
||||
'code',
|
||||
'codesample',
|
||||
'help',
|
||||
'wordcount',
|
||||
'autosave',
|
||||
'quickbars',
|
||||
'quickbars link',
|
||||
'quickbars image editimage',
|
||||
'pagebreak',
|
||||
'nonbreaking',
|
||||
'advlist',
|
||||
'autolink',
|
||||
],
|
||||
autosave_ask_before_unload: true,
|
||||
toolbar:
|
||||
'undo redo styles removeformat formatselect fontfamily fontsize forecolor link quickimage bold italic underline align bullist numlist outdent indent removeformat preview fullscreen searchreplace help code codesample quicktable pagebreak nonbreaking charmap customTemplates',
|
||||
formats: {
|
||||
linhatopo: {
|
||||
selector: 'div',
|
||||
classes: 'borda-superior',
|
||||
},
|
||||
linhainferior: {
|
||||
selector: 'div',
|
||||
classes: 'borda-inferior',
|
||||
},
|
||||
},
|
||||
|
||||
style_formats: [
|
||||
{
|
||||
title: 'Estilos de Borda',
|
||||
items: [
|
||||
{ title: 'Linha Superior', format: 'linhatopo' },
|
||||
{ title: 'Linha Inferior', format: 'linhainferior' },
|
||||
],
|
||||
},
|
||||
],
|
||||
setup: (editor: any) => {
|
||||
const customTemplates = [
|
||||
{
|
||||
title: 'Qualificação das Partes (Casamento)',
|
||||
description: 'Insere o bloco de qualificação para contraentes.',
|
||||
content: `<p><strong>QUALIFICAÇÃO DOS CONTRAENTES:</strong> Ele, brasileiro, solteiro, maior, [Profissão], portador da CI nº [RG], inscrito no CPF sob o nº [CPF], residente e domiciliado em [Endereço]. Ela, brasileira, solteira, maior, [Profissão], portadora da CI nº [RG], inscrita no CPF sob o nº [CPF], residente e domiciliada em [Endereço].</p><br>`,
|
||||
},
|
||||
{
|
||||
title: 'Cláusula de Regime de Bens',
|
||||
description: 'Cláusula padrão de Comunhão Parcial de Bens.',
|
||||
content:
|
||||
'<p>O regime de bens adotado é o da <strong>Comunhão Parcial de Bens</strong>, nos termos dos artigos 1.658 e seguintes do Código Civil brasileiro.</p><br>',
|
||||
},
|
||||
{
|
||||
title: 'Cláusula de Encerramento (Selo)',
|
||||
description: 'Texto final com espaço para o selo digital.',
|
||||
content:
|
||||
'<p>O referido é verdade e dou fé. Emitida nesta data. Selo Digital de Fiscalização: [Número do Selo]</p>',
|
||||
},
|
||||
];
|
||||
editor.ui.registry.addMenuButton('customTemplates', {
|
||||
text: 'Modelos', // O texto que aparecerá no botão
|
||||
fetch: (callback: any) => {
|
||||
const items = customTemplates.map((template) => ({
|
||||
type: 'menuitem',
|
||||
text: template.title,
|
||||
onAction: () => {
|
||||
// 3. Ação que acontece ao clicar no item do menu: insere o conteúdo
|
||||
editor.insertContent(template.content);
|
||||
},
|
||||
}));
|
||||
callback(items);
|
||||
},
|
||||
});
|
||||
},
|
||||
quickbars_selection_toolbar:
|
||||
'bold italic underline | fontfamily | fontsize | quicklink blockquote | quicklink',
|
||||
quickbars_insert_toolbar:
|
||||
'bold italic underline fontfamily fontsize quicklink blockquote quicklink quickimage quicktable hr',
|
||||
quickbars_image_toolbar:
|
||||
'alignleft aligncenter alignright | rotateleft rotateright | imageoptions',
|
||||
fontsize_formats:
|
||||
'4pt 5pt 6pt 7pt 8pt 9pt 10pt 12pt 14pt 16pt 18pt 20pt 22pt 24pt 26pt 28pt 30pt 32pt 34pt 36pt',
|
||||
font_family_formats: ` Times New Roman=Times New Roman, Times, serif; Arial=Arial, Helvetica, sans-serif; Calibri=Calibri, sans-serif; Courier New=Courier New, Courier, monospace; Georgia=Georgia, serif; Verdana=Verdana, Geneva, sans-serif;`,
|
||||
fullscreen_native: true,
|
||||
content_style: `
|
||||
body {
|
||||
font-family: 'Times New Roman', Times, serif;
|
||||
font-size: 12pt;
|
||||
background: #fff;
|
||||
margin: ${margins.top}cm ${margins.right}cm ${margins.bottom}cm ${margins.left}cm;
|
||||
}
|
||||
.mce-pagebreak { /* Estiliza a linha da quebra de página no editor */
|
||||
border-top: 1px dashed #bbb;
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.borda-superior {
|
||||
border-top: 1px solid #000;
|
||||
padding-top: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.borda-inferior {
|
||||
border-bottom: 1px solid #000;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainEditor;
|
||||
101
src/components/app-sidebar.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
GalleryVerticalEnd,
|
||||
House,
|
||||
SquareTerminal,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { NavMain } from '@/components/nav-main';
|
||||
import { NavProjects } from '@/components/nav-projects';
|
||||
import { NavUser } from '@/components/nav-user';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarRail,
|
||||
} from '@/components/ui/sidebar';
|
||||
|
||||
import useGUsuarioGetJWTHook from '@/shared/hooks/auth/useGUsuarioGetJWTHook';
|
||||
import Image from 'next/image';
|
||||
|
||||
// This is sample data.
|
||||
const data = {
|
||||
teams: [],
|
||||
navMain: [
|
||||
{
|
||||
title: 'Início',
|
||||
url: '#',
|
||||
icon: House,
|
||||
isActive: false,
|
||||
items: [
|
||||
{
|
||||
title: 'Dashboard',
|
||||
url: '/dashboard/',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Administrativo',
|
||||
url: '#',
|
||||
icon: SquareTerminal,
|
||||
isActive: true,
|
||||
items: [
|
||||
{
|
||||
title: 'Usuários',
|
||||
url: '/usuarios/',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
projects: [],
|
||||
};
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
const { userAuthenticated } = useGUsuarioGetJWTHook();
|
||||
return (
|
||||
<Sidebar collapsible="icon" {...props}>
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton size="lg" asChild>
|
||||
<a href="#">
|
||||
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
|
||||
<GalleryVerticalEnd className="size-4" />
|
||||
<Image
|
||||
src="/images/logo-abb.png"
|
||||
alt="Logo do site"
|
||||
width={100}
|
||||
height={100}
|
||||
className='rounded-lg'
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 leading-none">
|
||||
<span className="font-semibold">
|
||||
Orius Tecnologia
|
||||
</span>
|
||||
<span className="">
|
||||
25.9.1
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<NavMain items={data.navMain} />
|
||||
<NavProjects projects={data.projects} />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
{userAuthenticated?.data ? <NavUser user={userAuthenticated.data} /> : 'Carregando...'}
|
||||
</SidebarFooter>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
74
src/components/nav-main.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
'use client';
|
||||
|
||||
import { ChevronRight, type LucideIcon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
} from '@/components/ui/sidebar';
|
||||
|
||||
export function NavMain({
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
title: string;
|
||||
url: string;
|
||||
icon?: LucideIcon;
|
||||
isActive?: boolean;
|
||||
items?: {
|
||||
title: string;
|
||||
url: string;
|
||||
}[];
|
||||
}[];
|
||||
}) {
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Platform</SidebarGroupLabel>
|
||||
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<Collapsible
|
||||
key={item.title}
|
||||
asChild
|
||||
defaultOpen={item.isActive}
|
||||
className="group/collapsible"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton tooltip={item.title}>
|
||||
{item.icon && <item.icon />}
|
||||
|
||||
<span>{item.title}</span>
|
||||
|
||||
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{item.items?.map((subItem) => (
|
||||
<SidebarMenuSubItem key={subItem.title}>
|
||||
<SidebarMenuSubButton asChild>
|
||||
<Link href={subItem.url}>
|
||||
<span>{subItem.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
);
|
||||
}
|
||||
83
src/components/nav-projects.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
'use client';
|
||||
|
||||
import { Folder, Forward, MoreHorizontal, Trash2, type LucideIcon } from 'lucide-react';
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar';
|
||||
|
||||
export function NavProjects({
|
||||
projects,
|
||||
}: {
|
||||
projects: {
|
||||
name: string;
|
||||
url: string;
|
||||
icon: LucideIcon;
|
||||
}[];
|
||||
}) {
|
||||
const { isMobile } = useSidebar();
|
||||
|
||||
return (
|
||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||
<SidebarGroupLabel>Projects</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{projects.map((item) => (
|
||||
<SidebarMenuItem key={item.name}>
|
||||
<SidebarMenuButton asChild>
|
||||
<a href={item.url}>
|
||||
<item.icon />
|
||||
<span>{item.name}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuAction showOnHover>
|
||||
<MoreHorizontal />
|
||||
<span className="sr-only">More</span>
|
||||
</SidebarMenuAction>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-48 rounded-lg"
|
||||
side={isMobile ? 'bottom' : 'right'}
|
||||
align={isMobile ? 'end' : 'start'}
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
<Folder className="text-muted-foreground" />
|
||||
<span>View Project</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Forward className="text-muted-foreground" />
|
||||
<span>Share Project</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<Trash2 className="text-muted-foreground" />
|
||||
<span>Delete Project</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton className="text-sidebar-foreground/70">
|
||||
<MoreHorizontal className="text-sidebar-foreground/70" />
|
||||
<span>More</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
);
|
||||
}
|
||||
133
src/components/nav-user.tsx
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
'use client';
|
||||
|
||||
import { ChevronsUpDown, LogOut, Sparkles } from 'lucide-react';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar';
|
||||
|
||||
import GUsuarioAuthenticatedInterface from '@/shared/interfaces/GUsuarioAuthenticatedInterface';
|
||||
import ConfirmDialog from '@/shared/components/confirmDialog/ConfirmDialog';
|
||||
import { useGUsuarioLogoutHook } from '@/packages/administrativo/hooks/GUsuario/useGUsuarioLogoutHook';
|
||||
import { use, useCallback, useState } from 'react';
|
||||
|
||||
export function NavUser({ user }: { user: GUsuarioAuthenticatedInterface }) {
|
||||
// Hook para encerrar sessão
|
||||
const { logoutUsuario } = useGUsuarioLogoutHook();
|
||||
|
||||
// Controle de exibição do formulário de confirmação
|
||||
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
|
||||
|
||||
const { isMobile } = useSidebar();
|
||||
|
||||
// Manipulação de formulário de confirmação
|
||||
const handleConfirmOpen = useCallback(async () => {
|
||||
setIsConfirmOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleLogoutConfirm = useCallback(async () => {
|
||||
logoutUsuario();
|
||||
}, []);
|
||||
|
||||
const handleLogoutCancel = useCallback(async () => {
|
||||
setIsConfirmOpen(false);
|
||||
}, []);
|
||||
|
||||
if (!user) {
|
||||
return 'Carregando...';
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground cursor-pointer"
|
||||
>
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage src={user.sigla} alt={user.nome} />
|
||||
|
||||
<AvatarFallback className="rounded-lg">{user.sigla}</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.nome}</span>
|
||||
|
||||
<span className="truncate text-xs">{user.email}</span>
|
||||
</div>
|
||||
|
||||
<ChevronsUpDown className="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent
|
||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
side={isMobile ? 'bottom' : 'right'}
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage src={user.sigla} alt={user.nome} />
|
||||
|
||||
<AvatarFallback className="rounded-lg">{user.sigla}</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.nome}</span>
|
||||
|
||||
<span className="truncate text-xs">{user.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem className="cursor-pointer">
|
||||
<Sparkles />
|
||||
Configurações
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem className="cursor-pointer" onClick={() => handleConfirmOpen()}>
|
||||
<LogOut />
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
|
||||
{/* Modal de confirmação */}
|
||||
<ConfirmDialog
|
||||
isOpen={isConfirmOpen}
|
||||
title="Log-out"
|
||||
description="Atenção"
|
||||
message={`Deseja realmente encerrar a sessão ? Dados não salvos serão perdidos`}
|
||||
confirmText="Sim, sair"
|
||||
cancelText="Cancelar"
|
||||
onConfirm={() => handleLogoutConfirm()}
|
||||
onCancel={() => handleLogoutCancel()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
89
src/components/team-switcher.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { ChevronsUpDown, Plus } from 'lucide-react';
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar';
|
||||
|
||||
export function TeamSwitcher({
|
||||
teams,
|
||||
}: {
|
||||
teams: {
|
||||
name: string;
|
||||
logo: React.ElementType;
|
||||
plan: string;
|
||||
}[];
|
||||
}) {
|
||||
const { isMobile } = useSidebar();
|
||||
const [activeTeam, setActiveTeam] = React.useState(teams[0]);
|
||||
|
||||
if (!activeTeam) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
|
||||
<activeTeam.logo className="size-4" />
|
||||
</div>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{activeTeam.name}</span>
|
||||
<span className="truncate text-xs">{activeTeam.plan}</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
align="start"
|
||||
side={isMobile ? 'bottom' : 'right'}
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">Teams</DropdownMenuLabel>
|
||||
{teams.map((team, index) => (
|
||||
<DropdownMenuItem
|
||||
key={team.name}
|
||||
onClick={() => setActiveTeam(team)}
|
||||
className="gap-2 p-2"
|
||||
>
|
||||
<div className="flex size-6 items-center justify-center rounded-md border">
|
||||
<team.logo className="size-3.5 shrink-0" />
|
||||
</div>
|
||||
{team.name}
|
||||
<DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="gap-2 p-2">
|
||||
<div className="flex size-6 items-center justify-center rounded-md border bg-transparent">
|
||||
<Plus className="size-4" />
|
||||
</div>
|
||||
<div className="text-muted-foreground font-medium">Add team</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
);
|
||||
}
|
||||
135
src/components/ui/alert-dialog.tsx
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
'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,
|
||||
};
|
||||
41
src/components/ui/avatar.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
'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 };
|
||||
102
src/components/ui/breadcrumb.tsx
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
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,
|
||||
};
|
||||
56
src/components/ui/button.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
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 };
|
||||
75
src/components/ui/card.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
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 };
|
||||
29
src/components/ui/checkbox.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
'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 };
|
||||
21
src/components/ui/collapsible.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
'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 };
|
||||
161
src/components/ui/command.tsx
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
'use client';
|
||||
|
||||
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,
|
||||
};
|
||||
129
src/components/ui/dialog.tsx
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
'use client';
|
||||
|
||||
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,
|
||||
};
|
||||
228
src/components/ui/dropdown-menu.tsx
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
'use client';
|
||||
|
||||
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,
|
||||
};
|
||||
152
src/components/ui/form.tsx
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
'use client';
|
||||
|
||||
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,
|
||||
};
|
||||
77
src/components/ui/input-otp.tsx
Normal file
|
|
@ -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 }
|
||||
22
src/components/ui/input.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Input({ className, value, 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,
|
||||
)}
|
||||
value={value ?? ''}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Input };
|
||||
21
src/components/ui/label.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
'use client';
|
||||
|
||||
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 };
|
||||
42
src/components/ui/popover.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
'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 };
|
||||
170
src/components/ui/select.tsx
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
'use client';
|
||||
|
||||
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,
|
||||
};
|
||||
28
src/components/ui/separator.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
'use client';
|
||||
|
||||
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 };
|
||||
130
src/components/ui/sheet.tsx
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
'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,
|
||||
};
|
||||
694
src/components/ui/sidebar.tsx
Normal file
|
|
@ -0,0 +1,694 @@
|
|||
'use client';
|
||||
|
||||
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,
|
||||
};
|
||||
13
src/components/ui/skeleton.tsx
Normal file
|
|
@ -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 };
|
||||
25
src/components/ui/sonner.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
'use client';
|
||||
|
||||
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 };
|
||||
92
src/components/ui/table.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
'use client';
|
||||
|
||||
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 };
|
||||
54
src/components/ui/tabs.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
'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 };
|
||||
18
src/components/ui/textarea.tsx
Normal file
|
|
@ -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 };
|
||||
57
src/components/ui/tooltip.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
'use client';
|
||||
|
||||
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 };
|
||||
19
src/hooks/use-mobile.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
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;
|
||||
}
|
||||
6
src/lib/utils.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
59
src/middleware.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import jwt from 'jsonwebtoken';
|
||||
import { NextResponse, type MiddlewareConfig, type NextRequest } from 'next/server';
|
||||
|
||||
const publicRoutes = [
|
||||
{ path: '/login', whenAuthenticated: 'redirect' },
|
||||
{ path: '/register', whenAuthenticated: 'redirect' },
|
||||
{ path: '/', whenAuthenticated: 'next' },
|
||||
] as const;
|
||||
|
||||
const ROOT_PATH = '/login'; // url raiz
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const path = request.nextUrl.pathname;
|
||||
const publicRoute = publicRoutes.find((route) => route.path === path);
|
||||
const authToken = request.cookies.get('access_token');
|
||||
|
||||
// 1. Não autenticado e rota pública → segue normal
|
||||
if (!authToken && publicRoute) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// 2. Não autenticado e rota privada → redireciona para raiz
|
||||
if (!authToken && !publicRoute) {
|
||||
return NextResponse.redirect(new URL(ROOT_PATH, request.url));
|
||||
}
|
||||
|
||||
// 3. Autenticado em rota pública com flag "redirect" → vai para raiz
|
||||
if (authToken && publicRoute && publicRoute.whenAuthenticated === 'redirect') {
|
||||
return NextResponse.redirect(new URL(ROOT_PATH, request.url));
|
||||
}
|
||||
|
||||
// 4. Autenticado em rota privada → valida token
|
||||
if (authToken && !publicRoute) {
|
||||
const decoded: any = jwt.decode(authToken.value);
|
||||
|
||||
if (!decoded || !decoded.exp) {
|
||||
console.log('Token inválido');
|
||||
return NextResponse.redirect(new URL(ROOT_PATH, request.url));
|
||||
}
|
||||
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
|
||||
if (decoded.exp <= currentTime) {
|
||||
console.log('Token expirado');
|
||||
return NextResponse.redirect(new URL(ROOT_PATH, request.url));
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config: MiddlewareConfig = {
|
||||
matcher: [
|
||||
// ignora APIs, arquivos estáticos e metadados
|
||||
'/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|images).*)',
|
||||
],
|
||||
};
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import z from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import GUsuarioLoginService from '@/packages/administrativo/services/GUsuario/GUsuarioLoginService';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useState } from 'react';
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../../../../components/ui/form';
|
||||
import LoadingButton from '@/shared/components/loadingButton/LoadingButton';
|
||||
import { Button } from '../../../../components/ui/button';
|
||||
import { GUsuarioLoginSchema } from '@/packages/administrativo/schemas/GUsuario/GUsuarioLoginSchema';
|
||||
|
||||
type FormValues = z.infer<typeof GUsuarioLoginSchema>;
|
||||
|
||||
export function LoginForm({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(GUsuarioLoginSchema),
|
||||
defaultValues: {
|
||||
login: '',
|
||||
senha_api: '',
|
||||
},
|
||||
});
|
||||
|
||||
// onSubmit agora recebe o evento do form através do handleSubmit
|
||||
const onSubmit = async (values: FormValues) => {
|
||||
// Ativa o estado de loading do botão
|
||||
setLoading(true);
|
||||
|
||||
// Realiza o login
|
||||
await GUsuarioLoginService(values);
|
||||
|
||||
// Removo o estado de loading do botão
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-6', className)} {...props}>
|
||||
<Card className="overflow-hidden p-0">
|
||||
<CardContent className="grid p-0 md:grid-cols-2">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-3 p-6 md:p-8">
|
||||
<div className="mb-6 flex flex-col items-center text-center">
|
||||
<h1 className="text-2xl font-bold">Bem vindo de volta!</h1>
|
||||
<p className="text-muted-foreground text-balance">
|
||||
Entre na sua conta Orius Tecnologia.
|
||||
</p>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="login"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Login</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="senha_api"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Senha</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Botão de loading */}
|
||||
<LoadingButton
|
||||
text="Entrar"
|
||||
textLoading="Aguarde..."
|
||||
type="submit"
|
||||
loading={loading}
|
||||
/>
|
||||
<div className="after:border-border relative my-4 text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t">
|
||||
<span className="bg-card text-muted-foreground relative z-10 px-2">
|
||||
Ou entre em contato
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Button variant="outline" type="button" className="w-full">
|
||||
Chamar no Whatsapp
|
||||
</Button>
|
||||
<Button variant="outline" type="button" className="w-full">
|
||||
Chamar no Local
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
<div className="bg-brand relative hidden items-center justify-center md:flex">
|
||||
<Image
|
||||
src="/images/logo-login.svg"
|
||||
alt="Logo"
|
||||
width={300}
|
||||
height={300}
|
||||
className="object-contain dark:brightness-[0.2] dark:grayscale"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="text-muted-foreground text-center text-xs">
|
||||
Ao clicar você concorda com <a href="#">Nossos termos de serviços</a> e{' '}
|
||||
<a href="#">
|
||||
Políticas de Privacidade
|
||||
</a>.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import TImovelInterface from "../../interfaces/TImovel/TImovelInterface";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
EllipsisIcon,
|
||||
PencilIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react";
|
||||
import { FormatDateTime } from "@/shared/actions/dateTime/FormatDateTime";
|
||||
import { FormatCEP } from "@/shared/actions/CEP/FormatCEP";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { ImovelTipoRegistro } from "@/shared/enums/ImovelTipoRegistro";
|
||||
import { SortableHeader } from "@/shared/components/dataTable/SortableHeader";
|
||||
import GetCapitalize from "@/shared/actions/text/GetCapitalize";
|
||||
|
||||
export default function TImovelColumns(
|
||||
onEdit: (item: TImovelInterface, isEditingFormStatus: boolean) => void,
|
||||
onDelete: (item: TImovelInterface, isEditingFormStatus: boolean) => void
|
||||
): ColumnDef<TImovelInterface>[] {
|
||||
return [
|
||||
// ID
|
||||
{
|
||||
accessorKey: "imovel_id",
|
||||
header: ({ column }) => SortableHeader("#", column),
|
||||
cell: ({ row }) => Number(row.getValue("imovel_id")),
|
||||
enableSorting: false,
|
||||
},
|
||||
|
||||
// Tipo Registro
|
||||
{
|
||||
accessorKey: "tipo_registro",
|
||||
header: ({ column }) => SortableHeader("Tipo Registro", column),
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue("tipo_registro") as keyof typeof ImovelTipoRegistro;
|
||||
return ImovelTipoRegistro[value] ?? value;
|
||||
},
|
||||
},
|
||||
|
||||
// Número
|
||||
{
|
||||
accessorKey: "numero",
|
||||
header: ({ column }) => SortableHeader("Número", column),
|
||||
cell: ({ row }) => row.getValue("numero"),
|
||||
},
|
||||
|
||||
// UF / Cidade / Bairro
|
||||
{
|
||||
id: "uf_cidade_bairro",
|
||||
accessorFn: (row) => row,
|
||||
header: ({ column }) => SortableHeader("Cidade / UF / Bairro", column),
|
||||
cell: ({ row }) => {
|
||||
const imovel = row.original;
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<span className="font-semibold text-gray-900 capitalize">
|
||||
{GetCapitalize(imovel.cidade)}/{imovel.uf}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">{GetCapitalize(imovel.gtbb_descricao)}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
sortingFn: (a, b) =>
|
||||
(a.original.cartorio?.toLowerCase() || "").localeCompare(
|
||||
b.original.cartorio?.toLowerCase() || ""
|
||||
),
|
||||
},
|
||||
|
||||
// CEP
|
||||
{
|
||||
accessorKey: "cep",
|
||||
header: ({ column }) => SortableHeader("CEP", column),
|
||||
cell: ({ row }) => FormatCEP(row.getValue("cep")),
|
||||
},
|
||||
|
||||
// Data de Registro
|
||||
{
|
||||
accessorKey: "data_registro",
|
||||
header: ({ column }) => SortableHeader("Cadastro", column),
|
||||
cell: ({ row }) => FormatDateTime(row.getValue("data_registro")),
|
||||
sortingFn: "datetime",
|
||||
},
|
||||
|
||||
// Ações
|
||||
{
|
||||
id: "actions",
|
||||
header: "Ações",
|
||||
cell: ({ row }) => {
|
||||
const imovel = row.original;
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<EllipsisIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="left" align="start">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem onSelect={() => onEdit(imovel, true)}>
|
||||
<PencilIcon className="mr-2 h-4 w-4" />
|
||||
Editar
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-red-600"
|
||||
onSelect={() => onDelete(imovel, true)}
|
||||
>
|
||||
<Trash2Icon className="mr-2 h-4 w-4" />
|
||||
Remover
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
385
src/packages/administrativo/components/TImovel/TImovelForm.tsx
Normal file
|
|
@ -0,0 +1,385 @@
|
|||
'use client';
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
|
||||
import LoadingButton from '@/shared/components/loadingButton/LoadingButton';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { CheckIcon, ChevronsUpDownIcon, HouseIcon, IdCardIcon } from 'lucide-react';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select';
|
||||
import { ImovelTipoRegistro } from '@/shared/enums/ImovelTipoRegistro';
|
||||
import { ImovelTipoClasseEnum } from '@/shared/enums/ImovelTipoClasseEnum';
|
||||
import { ResetFormIfData } from '@/shared/actions/form/ResetFormIfData';
|
||||
import { useGTBBairroReadHook } from '../../../../app/(protected)/(cadastros)/cadastros/_hooks/g_tb_bairro/useGTBBairroReadHook';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command';
|
||||
import { cn } from '@/lib/utils';
|
||||
import GetCapitalize from '@/shared/actions/text/GetCapitalize';
|
||||
import { TImovelFormInterface } from '../../interfaces/TImovel/TImovelFormInterface';
|
||||
import { useTImovelFormHook } from '../../hooks/TImovel/useTImovelFormHook';
|
||||
import TImovelUnidadeUrbanoPage from '../TImovelUnidade/TImovelUnidadeUrbano/TImovelUnidadeUrbanoPage';
|
||||
import TImovelUnidadeRuralPage from '../TImovelUnidade/TImovelUnidadeRural/TImovelUnidadeRuralPage';
|
||||
import { parseNumberInput } from '@/shared/actions/form/parseNumberInput';
|
||||
|
||||
export default function TImovelForm({ isOpen, data, onClose, onSave, buttonIsLoading, tipoClasse }: TImovelFormInterface) {
|
||||
|
||||
const { gTBBairro, fetchGTBBairro } = useGTBBairroReadHook();
|
||||
|
||||
// Inicializa o react-hook-form com schema zod
|
||||
const form = useTImovelFormHook();
|
||||
|
||||
// Atualiza o formulário quando recebe dados para edição
|
||||
useEffect(() => {
|
||||
|
||||
// Se existir dados, reseta o formulário com os mesmos
|
||||
ResetFormIfData(form, data);
|
||||
|
||||
// Função sincrona para carregamento de dados
|
||||
async function loadData() {
|
||||
|
||||
// Busca os bairros
|
||||
await fetchGTBBairro();
|
||||
|
||||
}
|
||||
|
||||
// Executa a função
|
||||
loadData();
|
||||
|
||||
}, [data, form]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) onClose(null, false);
|
||||
}}
|
||||
>
|
||||
<DialogContent className="w-full max-w-full p-6 sm:max-w-3xl md:max-w-4xl lg:max-w-5xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Imóvel Urbano
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Cadastro de imóvel urbano
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSave)} className="space-y-6">
|
||||
{/* Tabs */}
|
||||
<Tabs defaultValue="dadosDoImovel" className="space-y-4">
|
||||
<TabsList className="flex w-full">
|
||||
<TabsTrigger className="flex-1 text-center cursor-pointer" value="dadosDoImovel">
|
||||
<HouseIcon className="me-1 inline" />
|
||||
Dados do Imóvel
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className="flex-1 text-center cursor-pointer" value="unidades">
|
||||
<IdCardIcon className="inline" />
|
||||
Unidades
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
{/* Dados do Imóvel */}
|
||||
<TabsContent value="dadosDoImovel" className="space-y-4">
|
||||
<div className="grid w-full grid-cols-12 gap-4">
|
||||
{/* UF */}
|
||||
<div className="col-span-12 sm:col-span-6 md:col-span-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="uf"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>UF</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type='text' placeholder="UF" maxLength={2} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/* CEP */}
|
||||
<div className="col-span-12 sm:col-span-6 md:col-span-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cep"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>CEP</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type='text' placeholder="Digite o CEP" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/* Cidade */}
|
||||
<div className="col-span-12 sm:col-span-6 md:col-span-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cidade"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Cidade</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type="text" placeholder="Digite a cidade" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/* Bairro */}
|
||||
<div className="col-span-12 sm:col-span-6 md:col-span-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tb_bairro_id"
|
||||
render={({ field }) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Bairro</FormLabel>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl className="w-full">
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="justify-between"
|
||||
>
|
||||
{field.value
|
||||
? gTBBairro.find(
|
||||
(item) =>
|
||||
String(item.tb_bairro_id) === String(field.value),
|
||||
)?.descricao
|
||||
: 'Selecione...'}
|
||||
<ChevronsUpDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Buscar tipo logradouro..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>Nenhum resultado encontrado.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{gTBBairro?.map((item) => (
|
||||
<CommandItem
|
||||
key={item.tb_bairro_id}
|
||||
value={(item.descricao ?? '').toLowerCase()}
|
||||
onSelect={() => {
|
||||
field.onChange(Number(item.tb_bairro_id));
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
'mr-2 h-4 w-4',
|
||||
String(field.value) === String(item.descricao)
|
||||
? 'opacity-100'
|
||||
: 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
{GetCapitalize(item.descricao)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* Cartório */}
|
||||
<div className="col-span-12 sm:col-span-6 md:col-span-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cartorio"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Cartório</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type='number' placeholder="Digite o cartório" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/* CNS */}
|
||||
<div className="col-span-12 sm:col-span-6 md:col-span-5">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cns"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>CNS</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type="number" placeholder="Digite o CNS" onChange={e => field.onChange(parseNumberInput(e))} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/* Livro */}
|
||||
<div className="col-span-12 sm:col-span-6 md:col-span-5">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="livro"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Livro</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type='text' placeholder="Digite o livro" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/* Tipo Registro */}
|
||||
<div className="col-span-12 sm:col-span-6 md:col-span-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tipo_registro"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormLabel>Tipo Registro</FormLabel>
|
||||
<FormControl>
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="w-full">
|
||||
{field.value
|
||||
? ImovelTipoRegistro[field.value as keyof typeof ImovelTipoRegistro]
|
||||
: "Selecione"}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(ImovelTipoRegistro).map(([key, label]) => (
|
||||
<SelectItem key={key} value={key} className="cursor-pointer">
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/* Número */}
|
||||
<div className="col-span-12 sm:col-span-6 md:col-span-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="numero"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Número</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type="number" placeholder="Digite o número" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/* Número Letra */}
|
||||
<div className="col-span-12 sm:col-span-6 md:col-span-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="numero_letra"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Número Letra</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type='text' placeholder="Digite a letra" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/* Tipo Registro */}
|
||||
<div className="col-span-12 sm:col-span-6 md:col-span-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tipo_classe"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormLabel>Tipo Classe</FormLabel>
|
||||
<FormControl>
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="w-full">
|
||||
{field.value
|
||||
? ImovelTipoClasseEnum[Number(field.value) as keyof typeof ImovelTipoClasseEnum]
|
||||
: "Selecione"}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(ImovelTipoClasseEnum).map(([key, label]) => (
|
||||
<SelectItem key={key} value={key} className="cursor-pointer">
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
{/* Unidades */}
|
||||
<TabsContent value="unidades" className="space-y-4">
|
||||
{/* Conteúdo das unidades */}
|
||||
{tipoClasse === 1 ? <TImovelUnidadeUrbanoPage imovel_id={data?.imovel_id} /> : null}
|
||||
{/* Conteúdo das unidades */}
|
||||
{tipoClasse === 3 ? <TImovelUnidadeRuralPage /> : null}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
{/* Rodapé do Dialog */}
|
||||
<DialogFooter className="mt-4 flex flex-col sm:flex-row gap-2 justify-end">
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline" type="button" onClick={() => onClose(null, false)}>
|
||||
Cancelar
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<LoadingButton
|
||||
text="Salvar"
|
||||
textLoading="Aguarde..."
|
||||
type="submit"
|
||||
loading={buttonIsLoading}
|
||||
/>
|
||||
</DialogFooter>
|
||||
{/* Campo oculto */}
|
||||
<input type="hidden" {...form.register("imovel_id")} />
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
170
src/packages/administrativo/components/TImovel/TImovelIndex.tsx
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
|
||||
import Loading from '@/shared/components/loading/loading';
|
||||
|
||||
import { useTImovelIndexHook } from '@/packages/administrativo/hooks/TImovel/useTImovelIndexHook';
|
||||
import { useTImovelSaveHook } from '@/packages/administrativo/hooks/TImovel/useTImovelSaveHook';
|
||||
import { useTImovelDeleteHook } from '@/packages/administrativo/hooks/TImovel/useTImovelDeleteHook';
|
||||
|
||||
import ConfirmDialog from '@/shared/components/confirmDialog/ConfirmDialog';
|
||||
import { useConfirmDialog } from '@/shared/components/confirmDialog/useConfirmDialog';
|
||||
|
||||
import TImovelInterface from '@/packages/administrativo/interfaces/TImovel/TImovelInterface';
|
||||
import Header from '@/shared/components/structure/Header';
|
||||
import { TImovelIndexInterface } from '../../interfaces/TImovel/TImovelIndexInterface';
|
||||
import TImovelTable from './TImovelTable';
|
||||
import TImovelForm from './TImovelForm';
|
||||
|
||||
export default function TImovelIndex({ pageTitle, pageDescription, tipoClasse }: TImovelIndexInterface) {
|
||||
|
||||
// Controle de estado do botão
|
||||
const [buttonIsLoading, setButtonIsLoading] = useState(false);
|
||||
|
||||
// Hooks para leitura e salvamento
|
||||
const { tImovel, indexTImovel } = useTImovelIndexHook();
|
||||
const { saveTImovel } = useTImovelSaveHook();
|
||||
const { deleteTImovel } = useTImovelDeleteHook();
|
||||
|
||||
// Estados
|
||||
const [selectedAndamento, setSelectedAndamento] = useState<TImovelInterface | null>(null);
|
||||
const [isFormOpen, setIsFormOpen] = useState(false);
|
||||
|
||||
// Estado para saber qual item será deletado
|
||||
const [itemToDelete, setItemToDelete] = useState<TImovelInterface | null>(null);
|
||||
|
||||
/**
|
||||
* Hook do modal de confirmação
|
||||
*/
|
||||
const {
|
||||
isOpen: isConfirmOpen,
|
||||
openDialog: openConfirmDialog,
|
||||
handleConfirm,
|
||||
handleCancel,
|
||||
} = useConfirmDialog();
|
||||
|
||||
/**
|
||||
* Abre o formulário no modo de edição ou criação
|
||||
*/
|
||||
const handleOpenForm = useCallback((data: TImovelInterface | null) => {
|
||||
setSelectedAndamento(data);
|
||||
setIsFormOpen(true);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Fecha o formulário e limpa o andamento selecionado
|
||||
*/
|
||||
const handleCloseForm = useCallback(() => {
|
||||
setSelectedAndamento(null);
|
||||
setIsFormOpen(false);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Salva os dados do formulário
|
||||
*/
|
||||
const handleSave = useCallback(
|
||||
async (formData: TImovelInterface) => {
|
||||
// Coloca o botão em estado de loading
|
||||
setButtonIsLoading(true);
|
||||
|
||||
// Aguarda salvar o registro
|
||||
await saveTImovel(formData);
|
||||
|
||||
// Remove o botão em estado de loading
|
||||
setButtonIsLoading(false);
|
||||
|
||||
// Atualiza a lista de dados
|
||||
indexTImovel();
|
||||
},
|
||||
[saveTImovel, indexTImovel, handleCloseForm],
|
||||
);
|
||||
|
||||
/**
|
||||
* Quando o usuário clica em "remover" na tabela
|
||||
*/
|
||||
const handleConfirmDelete = useCallback(
|
||||
(item: TImovelInterface) => {
|
||||
// Define o item atual para remoção
|
||||
setItemToDelete(item);
|
||||
|
||||
// Abre o modal de confirmação
|
||||
openConfirmDialog();
|
||||
},
|
||||
[openConfirmDialog],
|
||||
);
|
||||
|
||||
/**
|
||||
* Executa a exclusão de fato quando o usuário confirma
|
||||
*/
|
||||
const handleDelete = useCallback(async () => {
|
||||
// Protege contra null
|
||||
if (!itemToDelete) return;
|
||||
|
||||
// Executa o Hook de remoção
|
||||
await deleteTImovel(itemToDelete);
|
||||
|
||||
// Atualiza a lista
|
||||
await indexTImovel();
|
||||
|
||||
// Limpa o item selecionado
|
||||
setItemToDelete(null);
|
||||
|
||||
// Fecha o modal
|
||||
handleCancel();
|
||||
}, [itemToDelete, indexTImovel, handleCancel]);
|
||||
|
||||
/**
|
||||
* Busca inicial dos dados
|
||||
*/
|
||||
useEffect(() => {
|
||||
indexTImovel();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Tela de loading enquanto carrega os dados
|
||||
*/
|
||||
if (tImovel?.length == 0) {
|
||||
return <Loading type={2} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Cabeçalho */}
|
||||
<Header
|
||||
title={pageTitle}
|
||||
description={pageDescription}
|
||||
buttonText={'Novo imóvel'}
|
||||
buttonAction={() => {
|
||||
handleOpenForm(null);
|
||||
}}
|
||||
/>
|
||||
{/* Tabela de andamentos */}
|
||||
<TImovelTable
|
||||
data={tImovel}
|
||||
onEdit={handleOpenForm}
|
||||
onDelete={handleConfirmDelete}
|
||||
/>
|
||||
{/* Modal de confirmação */}
|
||||
<ConfirmDialog
|
||||
isOpen={isConfirmOpen}
|
||||
title="Confirmar exclusão"
|
||||
description="Atenção"
|
||||
message={`Deseja realmente excluir o imóvel "${itemToDelete?.cidade}"?`}
|
||||
confirmText="Sim, excluir"
|
||||
cancelText="Cancelar"
|
||||
onConfirm={handleDelete}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
{/* Formulário de criação/edição */}
|
||||
<TImovelForm
|
||||
isOpen={isFormOpen}
|
||||
data={selectedAndamento}
|
||||
onClose={handleCloseForm}
|
||||
onSave={handleSave}
|
||||
buttonIsLoading={buttonIsLoading}
|
||||
tipoClasse={tipoClasse}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
'use client';
|
||||
|
||||
import { DataTable } from '@/shared/components/dataTable/DataTable';
|
||||
import TImovelColumns from './TImovelColumns';
|
||||
import TImovelTableInterface from '../../interfaces/TImovel/TImovelTabelInterface';
|
||||
|
||||
/**
|
||||
* Componente principal da tabela
|
||||
*/
|
||||
export default function TImovelTable({ data, onEdit, onDelete }: TImovelTableInterface) {
|
||||
const columns = TImovelColumns(onEdit, onDelete);
|
||||
return (
|
||||
<div>
|
||||
<DataTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
filterColumn="numero"
|
||||
filterPlaceholder="Buscar pelo numero de transcrição, matricula etc..."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
'use server';
|
||||
|
||||
import { Methods } from '@/shared/services/api/enums/ApiMethodEnum';
|
||||
import API from '@/shared/services/api/Api';
|
||||
|
||||
export default async function GUsuarioLoginData(form: any) {
|
||||
const api = new API();
|
||||
// Realiza o envio dos dados
|
||||
const response = await api.send({
|
||||
method: Methods.POST,
|
||||
endpoint: `administrativo/g_usuario/authenticate`,
|
||||
body: form,
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { withClientErrorHandler } from "@/shared/actions/withClientErrorHandler/withClientErrorHandler";
|
||||
import ApiResponseInterface from "@/shared/services/api/interfaces/ApiResponseInterface";
|
||||
import API from "@/shared/services/api/Api";
|
||||
import { Methods } from "@/shared/services/api/enums/ApiMethodEnum";
|
||||
import TImovelInterface from "../../interfaces/TImovel/TImovelInterface";
|
||||
|
||||
async function executeTImovelDeleteData(data: TImovelInterface): Promise<ApiResponseInterface> {
|
||||
|
||||
const api = new API();
|
||||
|
||||
return await api.send({
|
||||
method: Methods.DELETE,
|
||||
endpoint: `administrativo/t_imovel/${data.imovel_id}`
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
export const TImovelDeleteData = withClientErrorHandler(executeTImovelDeleteData);
|
||||
17
src/packages/administrativo/data/TImovel/TImovelIndexData.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { withClientErrorHandler } from "@/shared/actions/withClientErrorHandler/withClientErrorHandler";
|
||||
import API from "@/shared/services/api/Api";
|
||||
import { Methods } from "@/shared/services/api/enums/ApiMethodEnum";
|
||||
import ApiResponseInterface from "@/shared/services/api/interfaces/ApiResponseInterface";
|
||||
|
||||
async function executeTImovelIndexData(): Promise<ApiResponseInterface> {
|
||||
|
||||
const api = new API();
|
||||
|
||||
return api.send({
|
||||
method: Methods.GET,
|
||||
endpoint: `administrativo/t_imovel/classe/1`
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
export const TImovelIndexData = withClientErrorHandler(executeTImovelIndexData);
|
||||
24
src/packages/administrativo/data/TImovel/TImovelSaveData.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { withClientErrorHandler } from "@/shared/actions/withClientErrorHandler/withClientErrorHandler";
|
||||
import TImovelInterface from "../../interfaces/TImovel/TImovelInterface";
|
||||
import ApiResponseInterface from "@/shared/services/api/interfaces/ApiResponseInterface";
|
||||
import API from "@/shared/services/api/Api";
|
||||
import { Methods } from "@/shared/services/api/enums/ApiMethodEnum";
|
||||
|
||||
async function executeTImovelSaveData(data: TImovelInterface): Promise<ApiResponseInterface> {
|
||||
|
||||
// Verifica se existe ID da cidade para decidir se é atualização (PUT) ou criação (POST)
|
||||
const isUpdate = Boolean(data.imovel_id);
|
||||
|
||||
// Instancia o cliente da API para enviar a requisição
|
||||
const api = new API();
|
||||
|
||||
// Executa a requisição para a API com o método apropriado e envia os dados no corpo
|
||||
return await api.send({
|
||||
method: isUpdate ? Methods.PUT : Methods.POST, // PUT se atualizar, POST se criar
|
||||
endpoint: `administrativo/t_imovel/${data.imovel_id || ''}`, // endpoint dinâmico
|
||||
body: data, // payload enviado para a API
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
export const TImovelSaveData = withClientErrorHandler(executeTImovelSaveData);
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
'use client';
|
||||
|
||||
import GUsuarioLogoutService from '../../services/GUsuario/GUsuarioLogoutService';
|
||||
|
||||
export const useGUsuarioLogoutHook = () => {
|
||||
const logoutUsuario = async () => {
|
||||
await GUsuarioLogoutService('access_token');
|
||||
};
|
||||
|
||||
return { logoutUsuario };
|
||||
};
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { useResponse } from '@/shared/components/response/ResponseContext';
|
||||
import { useState } from 'react';
|
||||
import TImovelInterface from '../../interfaces/TImovel/TImovelInterface';
|
||||
import { TImovelDeleteService } from '../../services/TImovel/TImovelDeleteService';
|
||||
|
||||
export const useTImovelDeleteHook = () => {
|
||||
const { setResponse } = useResponse();
|
||||
|
||||
const [tImovel, setTImovel] = useState<TImovelInterface>();
|
||||
|
||||
const deleteTImovel = async (data: TImovelInterface) => {
|
||||
const response = await TImovelDeleteService(data);
|
||||
|
||||
setTImovel(data);
|
||||
|
||||
setResponse(response);
|
||||
};
|
||||
|
||||
return { tImovel, deleteTImovel };
|
||||
};
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { TImovelFormValues, TImovelSchema } from "../../schemas/TImovel/TImovelSchema";
|
||||
|
||||
export function useTImovelFormHook(defaults?: Partial<TImovelFormValues>) {
|
||||
return useForm<TImovelFormValues>({
|
||||
resolver: zodResolver(TImovelSchema),
|
||||
defaultValues: {
|
||||
...defaults,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
'use client';
|
||||
|
||||
import { useResponse } from '@/shared/components/response/ResponseContext';
|
||||
import { useState } from 'react';
|
||||
import TImovelInterface from '../../interfaces/TImovel/TImovelInterface';
|
||||
import { TImovelIndexData } from '../../data/TImovel/TImovelIndexData';
|
||||
|
||||
export const useTImovelIndexHook = () => {
|
||||
const { setResponse } = useResponse();
|
||||
|
||||
const [tImovel, setTImovel] = useState<TImovelInterface[]>([]);
|
||||
|
||||
const indexTImovel = async () => {
|
||||
const response = await TImovelIndexData();
|
||||
|
||||
// Armazena os dados consultados
|
||||
setTImovel(response.data);
|
||||
|
||||
// Define os dados do componente de resposta (toast, modal, etc)
|
||||
setResponse(response);
|
||||
};
|
||||
|
||||
return {
|
||||
tImovel,
|
||||
indexTImovel
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
'use client';
|
||||
|
||||
import { useResponse } from '@/shared/components/response/ResponseContext';
|
||||
import { useState } from 'react';
|
||||
import TImovelInterface from '../../interfaces/TImovel/TImovelInterface';
|
||||
import { TImovelSaveService } from '../../services/TImovel/TImovelSaveService';
|
||||
|
||||
export const useTImovelSaveHook = () => {
|
||||
const { setResponse } = useResponse();
|
||||
|
||||
const [tImovel, setTImovel] = useState<TImovelInterface>();
|
||||
|
||||
// controla se o formulário está aberto ou fechado
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const saveTImovel = async (data: TImovelInterface) => {
|
||||
const response = await TImovelSaveService(data);
|
||||
|
||||
// Armazena os dados da repsota
|
||||
setTImovel(response.data);
|
||||
|
||||
// Define os dados da respota(toast, modal, etc)
|
||||
setResponse(response);
|
||||
|
||||
// Fecha o formulário automaticamente após salvar
|
||||
setIsOpen(false);
|
||||
|
||||
// Retorna os valores de forma imediata
|
||||
return response.data;
|
||||
};
|
||||
|
||||
return { tImovel, saveTImovel };
|
||||
};
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
export default interface GUsuario {
|
||||
usuario_id: number;
|
||||
trocarsenha: string;
|
||||
login: string;
|
||||
senha: string;
|
||||
situacao: string;
|
||||
nome_completo: string;
|
||||
funcao: string;
|
||||
assina: string;
|
||||
sigla: string;
|
||||
usuario_tab: string;
|
||||
ultimo_login: string;
|
||||
ultimo_login_regs: string;
|
||||
data_expiracao: string;
|
||||
senha_anterior: string;
|
||||
andamento_padrao: string;
|
||||
lembrete_pergunta: string;
|
||||
lembrete_resposta: string;
|
||||
andamento_padrao2: string;
|
||||
receber_mensagem_arrolamento: string;
|
||||
email: string;
|
||||
assina_certidao: string;
|
||||
receber_email_penhora: string;
|
||||
foto: string;
|
||||
nao_receber_chat_todos: string;
|
||||
pode_alterar_caixa: string;
|
||||
receber_chat_certidao_online: string;
|
||||
receber_chat_cancelamento: string;
|
||||
cpf: string;
|
||||
somente_leitura: string;
|
||||
receber_chat_envio_onr: string;
|
||||
tipo_usuario: string;
|
||||
senha_api: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { TImovelFormValues } from "../../schemas/TImovel/TImovelSchema";
|
||||
|
||||
export interface TImovelFormInterface {
|
||||
isOpen: boolean;
|
||||
data: TImovelFormValues | null;
|
||||
onClose: (item: null, isFormStatus: boolean) => void;
|
||||
onSave: (data: TImovelFormValues) => void;
|
||||
buttonIsLoading: boolean;
|
||||
tipoClasse: number;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
export interface TImovelIndexInterface {
|
||||
pageTitle: string,
|
||||
pageDescription: string,
|
||||
tipoClasse: number
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
export default interface TImovelInterface {
|
||||
imovel_id?: number,
|
||||
tipo_classe?: string,
|
||||
tipo_registro?: string,
|
||||
data_registro?: string,
|
||||
numero?: number,
|
||||
numero_letra?: string,
|
||||
cidade?: string,
|
||||
cep?: string,
|
||||
uf?: string,
|
||||
tb_bairro_id?: number,
|
||||
cartorio?: string,
|
||||
livro?: string,
|
||||
cns?: number,
|
||||
gtbb_descricao?: string,
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import TImovelInterface from "./TImovelInterface";
|
||||
|
||||
export default interface TImovelTableInterface {
|
||||
data?: TImovelInterface[];
|
||||
onEdit: (item: TImovelInterface, isEditingFormStatus: boolean) => void;
|
||||
onDelete: (item: TImovelInterface, isEditingFormStatus: boolean) => void;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const GUsuarioLoginSchema = z.object({
|
||||
login: z.string().min(1, 'O campo deve ser preenchido'),
|
||||
senha_api: z.string().min(1, 'O campo deve ser preenchido'),
|
||||
});
|
||||
19
src/packages/administrativo/schemas/TImovel/TImovelSchema.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import z from "zod";
|
||||
|
||||
export const TImovelSchema = z.object({
|
||||
imovel_id: z.number().optional(),
|
||||
tipo_classe: z.string().optional(),
|
||||
tipo_registro: z.string().optional(),
|
||||
data_registro: z.string().optional(),
|
||||
numero: z.number().optional(),
|
||||
numero_letra: z.string().optional(),
|
||||
cidade: z.string().optional(),
|
||||
cep: z.string().optional(),
|
||||
uf: z.string().optional(),
|
||||
tb_bairro_id: z.number().optional(),
|
||||
cartorio: z.string().optional(),
|
||||
livro: z.string().optional(),
|
||||
cns: z.number().optional(),
|
||||
});
|
||||
|
||||
export type TImovelFormValues = z.infer<typeof TImovelSchema>;
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
import GUsuarioLoginData from '../../data/GUsuario/GUsuarioLoginData';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default async function GUsuarioLoginService(form: any) {
|
||||
// Obtem a resposta da requisição
|
||||
const response = await GUsuarioLoginData(form);
|
||||
// Verifica se localizou o usuário
|
||||
if (response.data.usuario_id <= 0) {
|
||||
return {
|
||||
code: 404,
|
||||
message: 'Não foi localizado o usuário',
|
||||
};
|
||||
}
|
||||
|
||||
// Importação do manipulador de cookies
|
||||
const cookieStore = await cookies();
|
||||
|
||||
// Cria um novo cookie
|
||||
cookieStore.set('access_token', response.data.token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24,
|
||||
});
|
||||
|
||||
// Redireciona para a págian desejada
|
||||
redirect('/servicos');
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default async function GUsuarioLogoutService(token: string) {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set(token, '', {
|
||||
expires: new Date(0),
|
||||
path: '/',
|
||||
});
|
||||
|
||||
redirect('/login');
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { withClientErrorHandler } from '@/shared/actions/withClientErrorHandler/withClientErrorHandler';
|
||||
import { TImovelDeleteData } from '../../data/TImovel/TImovelDeleteData';
|
||||
import TImovelInterface from '../../interfaces/TImovel/TImovelInterface';
|
||||
|
||||
async function executeTImovelDeleteService(data: TImovelInterface) {
|
||||
const response = await TImovelDeleteData(data);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export const TImovelDeleteService = withClientErrorHandler(executeTImovelDeleteService);
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { withClientErrorHandler } from '@/shared/actions/withClientErrorHandler/withClientErrorHandler';
|
||||
import { TImovelIndexData } from '../../data/TImovel/TImovelIndexData';
|
||||
|
||||
export default async function executeTImovelIndexService() {
|
||||
const response = await TImovelIndexData();
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export const TImovelIndexService = withClientErrorHandler(executeTImovelIndexService);
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { withClientErrorHandler } from '@/shared/actions/withClientErrorHandler/withClientErrorHandler';
|
||||
import { TImovelSaveData } from '../../data/TImovel/TImovelSaveData';
|
||||
import TImovelInterface from '../../interfaces/TImovel/TImovelInterface';
|
||||
|
||||
async function executeTImovelSaveService(data: TImovelInterface) {
|
||||
const response = await TImovelSaveData(data);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export const TImovelSaveService = withClientErrorHandler(executeTImovelSaveService);
|
||||
11
src/shared/abstracts/Schema.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export default class Schema {
|
||||
private _errors: any[] = [];
|
||||
|
||||
get errors(): any {
|
||||
return this._errors;
|
||||
}
|
||||
|
||||
set errors(value: any) {
|
||||
this._errors.push(value);
|
||||
}
|
||||
}
|
||||
20
src/shared/actions/CEP/FormatCEP.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* Formata um número de CEP no padrão 99999-999
|
||||
*
|
||||
* @param value - CEP em string ou number
|
||||
* @returns CEP formatado ou string vazia se inválido
|
||||
*/
|
||||
export function FormatCEP(value: string | number): string {
|
||||
if (!value) return '';
|
||||
|
||||
// Converte para string e remove tudo que não seja número
|
||||
const digits = String(value).replace(/\D/g, '');
|
||||
|
||||
// Garante que tenha no máximo 8 dígitos
|
||||
const cleanValue = digits.slice(0, 8);
|
||||
|
||||
// Retorna formatado ou valor limpo se não tiver tamanho suficiente
|
||||
if (cleanValue.length !== 8) return cleanValue;
|
||||
|
||||
return cleanValue.replace(/(\d{5})(\d{3})/, '$1-$2');
|
||||
}
|
||||
20
src/shared/actions/CNPJ/FormatCNPJ.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* Formata um número de CNPJ no padrão 99.999.999/9999-99
|
||||
*
|
||||
* @param value - CNPJ em string ou number
|
||||
* @returns CNPJ formatado ou string parcial se incompleto
|
||||
*/
|
||||
export function FormatCNPJ(value: string | number): string {
|
||||
if (!value) return '';
|
||||
|
||||
// Converte para string e remove tudo que não seja número
|
||||
const digits = String(value).replace(/\D/g, '');
|
||||
|
||||
// Garante que tenha no máximo 14 dígitos
|
||||
const cleanValue = digits.slice(0, 14);
|
||||
|
||||
// Retorna parcialmente formatado se ainda não tiver 14 dígitos
|
||||
if (cleanValue.length < 14) return cleanValue;
|
||||
|
||||
return cleanValue.replace(/(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})/, '$1.$2.$3/$4-$5');
|
||||
}
|
||||
20
src/shared/actions/CPF/FormatCPF.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* Formata um número de CPF no padrão 999.999.999-99
|
||||
*
|
||||
* @param value - CPF em string ou number
|
||||
* @returns CPF formatado ou string vazia se inválido
|
||||
*/
|
||||
export function FormatCPF(value: string | number): string {
|
||||
if (!value) return '';
|
||||
|
||||
// Converte para string e remove tudo que não seja número
|
||||
const digits = String(value).replace(/\D/g, '');
|
||||
|
||||
// Garante que tenha no máximo 11 dígitos
|
||||
const cleanValue = digits.slice(0, 11);
|
||||
|
||||
// Retorna formatado ou vazio se não tiver tamanho suficiente
|
||||
if (cleanValue.length !== 11) return cleanValue;
|
||||
|
||||
return cleanValue.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4');
|
||||
}
|
||||
17
src/shared/actions/cookies/CookiesGet.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
/**
|
||||
* Função para obter tokens do lado servidorde acordo com o nome informado
|
||||
*/
|
||||
export default async function CookiesGet(token: string) {
|
||||
// Obtem a lista de Tokens
|
||||
const cookieStore = await cookies();
|
||||
|
||||
// Obtem um token em especifico
|
||||
const data = cookieStore.get(token);
|
||||
|
||||
// Retorna nulo ou o valor do token desejado
|
||||
return data?.value;
|
||||
}
|
||||
53
src/shared/actions/dateTime/FormatDateTime.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* Formata uma data e hora brasileira (DD/MM/YYYY HH:mm)
|
||||
*
|
||||
* Suporta:
|
||||
* - Entrada como string, Date ou number (timestamp)
|
||||
* - Dados incompletos (apenas dia/mês, sem hora, etc.)
|
||||
* - Retorna "-" se vazio ou inválido
|
||||
*
|
||||
* @param value - Data ou hora em string, Date ou timestamp
|
||||
* @returns Data formatada no padrão DD/MM/YYYY HH:mm ou parcial
|
||||
*/
|
||||
export function FormatDateTime(value: string | Date | number | null | undefined): string {
|
||||
if (!value) return '-';
|
||||
|
||||
let date: Date;
|
||||
|
||||
// Converte entrada para Date
|
||||
if (value instanceof Date) {
|
||||
date = value;
|
||||
} else if (typeof value === 'number') {
|
||||
date = new Date(value);
|
||||
} else if (typeof value === 'string') {
|
||||
// Remove caracteres extras e tenta criar Date
|
||||
const cleanValue = value.trim().replace(/[^0-9]/g, '');
|
||||
|
||||
if (cleanValue.length === 8) {
|
||||
// DDMMYYYY
|
||||
const day = parseInt(cleanValue.slice(0, 2), 10);
|
||||
const month = parseInt(cleanValue.slice(2, 4), 10) - 1;
|
||||
const year = parseInt(cleanValue.slice(4, 8), 10);
|
||||
date = new Date(year, month, day);
|
||||
} else {
|
||||
// Tenta parse padrão
|
||||
const parsed = new Date(value);
|
||||
if (isNaN(parsed.getTime())) return '-';
|
||||
date = parsed;
|
||||
}
|
||||
} else {
|
||||
return '-';
|
||||
}
|
||||
|
||||
// Extrai partes da data
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const year = date.getFullYear();
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
|
||||
// Monta string parcialmente, dependendo da hora estar disponível
|
||||
const hasTime = !(hours === '00' && minutes === '00');
|
||||
|
||||
return `${day}/${month}/${year}${hasTime ? ` ${hours}:${minutes}` : ''}`;
|
||||
}
|
||||
15
src/shared/actions/form/ResetFormIfData.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { UseFormReturn, FieldValues } from "react-hook-form";
|
||||
|
||||
/**
|
||||
* Reseta o formulário com os dados recebidos (se existirem)
|
||||
* @param form - Instância do react-hook-form
|
||||
* @param data - Dados para popular o formulário
|
||||
*/
|
||||
export function ResetFormIfData<T extends FieldValues>(
|
||||
form: UseFormReturn<T>,
|
||||
data: T | null
|
||||
) {
|
||||
if (data) {
|
||||
form.reset(data);
|
||||
}
|
||||
}
|
||||
6
src/shared/actions/form/parseNumberInput.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* Converte o valor do input para número, enviando undefined se estiver vazio
|
||||
*/
|
||||
export function parseNumberInput(e: React.ChangeEvent<HTMLInputElement>): number | undefined {
|
||||
return e.target.value ? Number(e.target.value) : undefined;
|
||||
}
|
||||
6
src/shared/actions/json/Json.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import appConfig from '../../../config/app.json';
|
||||
export default class Json {
|
||||
static execute() {
|
||||
return appConfig;
|
||||
}
|
||||
}
|
||||
47
src/shared/actions/phone/FormatPhone.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* Formata um número de telefone brasileiro.
|
||||
*
|
||||
* Suporta:
|
||||
* - Com ou sem DDD
|
||||
* - Números incompletos
|
||||
* - Telefones com 8 ou 9 dígitos
|
||||
*
|
||||
* @param value - Número de telefone em string ou number
|
||||
* @returns Telefone formatado ou "-" se vazio
|
||||
*/
|
||||
export function FormatPhone(value: string | number): string {
|
||||
if (!value) return '-';
|
||||
|
||||
// Converte para string e remove tudo que não for número
|
||||
const digits = String(value).replace(/\D/g, '');
|
||||
|
||||
// Se não tiver nada após limpar, retorna "-"
|
||||
if (digits.length === 0) return '-';
|
||||
|
||||
// Garante no máximo 11 dígitos
|
||||
const cleanValue = digits.slice(0, 11);
|
||||
|
||||
// -------------------------------
|
||||
// SEM DDD
|
||||
// -------------------------------
|
||||
if (cleanValue.length <= 8) {
|
||||
// Até 8 dígitos → formato parcial
|
||||
return cleanValue.replace(/(\d{4})(\d{0,4})/, '$1-$2').replace(/-$/, '');
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// COM DDD
|
||||
// -------------------------------
|
||||
if (cleanValue.length === 9 || cleanValue.length === 10) {
|
||||
// DDD + telefone de 8 dígitos
|
||||
return cleanValue.replace(/^(\d{2})(\d{4})(\d{0,4})$/, '($1) $2-$3').replace(/-$/, '');
|
||||
}
|
||||
|
||||
if (cleanValue.length === 11) {
|
||||
// DDD + telefone de 9 dígitos
|
||||
return cleanValue.replace(/^(\d{2})(\d{5})(\d{0,4})$/, '($1) $2-$3').replace(/-$/, '');
|
||||
}
|
||||
|
||||
// Caso genérico, se não cair em nenhuma regra
|
||||
return cleanValue;
|
||||
}
|
||||
11
src/shared/actions/text/GetCapitalize.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* Capitaliza a primeira letra de uma string.
|
||||
*
|
||||
* @param text - Texto que será capitalizado
|
||||
* @returns String com a primeira letra em maiúscula
|
||||
*/
|
||||
export default function GetCapitalize(text?: string): string {
|
||||
if (!text) return '';
|
||||
|
||||
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
|
||||
}
|
||||