feat(explode): first commit
commit
e3fbdb94f4
|
|
@ -0,0 +1,12 @@
|
|||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /code
|
||||
|
||||
COPY package.json package.json
|
||||
COPY package-lock.json package-lock.json
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD [ "npm", "run", "dev" ]
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
@Library('jenkins-library') _
|
||||
pipeline {
|
||||
agent {label 'docker-slave'}
|
||||
parameters {
|
||||
string(name: 'PROJETO', defaultValue: 'agdiariooficialweb', description: 'Digite o nome do projeto:')
|
||||
choice(name: 'AMBIENTE', choices: ['HML','BETA','PRD'], description: 'Selecione o Ambiente para publicação:')
|
||||
string(name: 'TAG', defaultValue: '', description: 'Digite a tag da imagem caso deseje republicar uma versão já existente:')
|
||||
}
|
||||
environment {
|
||||
DOCKERIMAGE = " "
|
||||
MAIL_NOTIFICATION_RECIPIENTS = "rafael.deda@gmail.com"
|
||||
}
|
||||
stages{
|
||||
stage('Build e Construção da Imagem') {
|
||||
when {
|
||||
environment name: 'TAG', value: ''
|
||||
}
|
||||
steps{
|
||||
script{
|
||||
DOCKERIMAGE = buildImages(DOCKERIMAGE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Submeter Imagem ao Registry') {
|
||||
when {
|
||||
environment name: 'TAG', value: ''
|
||||
}
|
||||
steps{
|
||||
script{
|
||||
DOCKERIMAGE = publishImages(DOCKERIMAGE)
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Deploy no Ambiente Selecionado no Cluster') {
|
||||
when {
|
||||
anyOf {
|
||||
environment name: 'AMBIENTE', value: 'BETA'
|
||||
environment name: 'AMBIENTE', value: 'PRD'
|
||||
}
|
||||
}
|
||||
steps {
|
||||
deployCluster()
|
||||
}
|
||||
}
|
||||
|
||||
stage('Deploy no Ambiente on Promise') {
|
||||
when {
|
||||
allOf {
|
||||
environment name: 'AMBIENTE', value: 'HML'
|
||||
environment name: 'ON_PROMISE', value: '1'
|
||||
}
|
||||
}
|
||||
steps{
|
||||
deployCluster('k8s-inovesolutions-hml')
|
||||
}
|
||||
}
|
||||
|
||||
// stage('Limpando Imagens') {
|
||||
// agent {label 'conteiner'}
|
||||
// steps{
|
||||
// limpandoImagens()
|
||||
// }
|
||||
// }
|
||||
}
|
||||
post {
|
||||
always {
|
||||
//sendNotification()
|
||||
cleanWorkspace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is enabled on this template. See [this documentation](https://react.dev/learn/react-compiler) for more information.
|
||||
|
||||
Note: This will impact Vite dev & build performances.
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/styles/index.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/views/components",
|
||||
"utils": "@/app/utils/index",
|
||||
"ui": "@/views/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"registries": {}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>agdiariooficialweb</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: agapesistemas-PROJETOAMBIENTE-tls
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
dnsNames:
|
||||
- PROJETOAMBIENTE.agapesistemas.com.br
|
||||
issuerRef:
|
||||
group: cert-manager.io
|
||||
kind: ClusterIssuer
|
||||
name: letsencrypt-prod
|
||||
secretName: agapesistemas-PROJETOAMBIENTE-tls
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: PROJETO
|
||||
workload.user.cattle.io/workloadselector: apps.deployment-agapesistemas-ns-AMBIENTE-PROJETO
|
||||
name: PROJETO-dp-AMBIENTE
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
rollingUpdate:
|
||||
maxSurge: 25%
|
||||
maxUnavailable: 25%
|
||||
type: RollingUpdate
|
||||
selector:
|
||||
matchLabels:
|
||||
workload.user.cattle.io/workloadselector: apps.deployment-agapesistemas-ns-AMBIENTE-PROJETO
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: PROJETO
|
||||
workload.user.cattle.io/workloadselector: apps.deployment-agapesistemas-ns-AMBIENTE-PROJETO
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
- labelSelector:
|
||||
matchExpressions:
|
||||
- key: app
|
||||
operator: In
|
||||
values:
|
||||
- PROJETO
|
||||
topologyKey: kubernetes.io/hostname
|
||||
containers:
|
||||
- env:
|
||||
- name: TZ
|
||||
value: America/Maceio
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: agapesistemas-db-credential-sc
|
||||
- secretRef:
|
||||
name: agapesistemas-PROJETO-sc
|
||||
- configMapRef:
|
||||
name: agapesistemas-PROJETO-cm
|
||||
- secretRef:
|
||||
name: agapesistemas-db-role-credential-sc
|
||||
optional: false
|
||||
image: IMAGEM:TAG
|
||||
name: PROJETO
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
protocol: TCP
|
||||
resources:
|
||||
limits:
|
||||
memory: 1000Mi
|
||||
requests:
|
||||
memory: 780Mi
|
||||
nodeSelector:
|
||||
doks.digitalocean.com/node-pool: pool-k8s-agapesistemas-app-AMBIENTE
|
||||
imagePullSecrets:
|
||||
- name: registry-agapesistemas
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: PROJETO
|
||||
workload.user.cattle.io/workloadselector: apps.deployment-agapesistemas-ns-AMBIENTE-PROJETO
|
||||
name: PROJETO-dp-AMBIENTE
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
rollingUpdate:
|
||||
maxSurge: 25%
|
||||
maxUnavailable: 25%
|
||||
type: RollingUpdate
|
||||
selector:
|
||||
matchLabels:
|
||||
workload.user.cattle.io/workloadselector: apps.deployment-agapesistemas-ns-AMBIENTE-PROJETO
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: PROJETO
|
||||
workload.user.cattle.io/workloadselector: apps.deployment-agapesistemas-ns-AMBIENTE-PROJETO
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
containers:
|
||||
- env:
|
||||
- name: TZ
|
||||
value: America/Maceio
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: agapesistemas-db-credential-sc
|
||||
- secretRef:
|
||||
name: agapesistemas-PROJETO-sc
|
||||
- configMapRef:
|
||||
name: agapesistemas-PROJETO-cm
|
||||
image: IMAGEM:TAG
|
||||
name: PROJETO
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
protocol: TCP
|
||||
resources:
|
||||
limits:
|
||||
memory: 1200Mi
|
||||
requests:
|
||||
memory: 1000Mi
|
||||
nodeSelector:
|
||||
doks.digitalocean.com/node-pool: pool-k8s-agapesistemas-app-AMBIENTE
|
||||
imagePullSecrets:
|
||||
- name: registry-agapesistemas
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: agapesistemas-PROJETO-hpa
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
maxReplicas: 1
|
||||
metrics:
|
||||
- resource:
|
||||
name: memory
|
||||
target:
|
||||
averageValue: 780Mi
|
||||
type: AverageValue
|
||||
type: Resource
|
||||
minReplicas: 1
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: PROJETO-dp-AMBIENTE
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.middlewares: traefik-http-to-https-redirectscheme@kubernetescrd,traefik-enable-cors-header@kubernetescrd
|
||||
name: agapesistemas-PROJETO-ing
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
rules:
|
||||
- host: PROJETOAMBIENTE.agapesistemas.com.br
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: agapesistemas-PROJETO-svc
|
||||
port:
|
||||
number: 8080
|
||||
pathType: ImplementationSpecific
|
||||
tls:
|
||||
- hosts:
|
||||
- PROJETOAMBIENTE.agapesistemas.com.br
|
||||
secretName: agapesistemas-PROJETOAMBIENTE-tls
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/service.sticky.cookie: 'true'
|
||||
#traefik.ingress.kubernetes.io/service.sticky.cookie.name: JSESSIONID
|
||||
traefik.ingress.kubernetes.io/service.sticky.cookie.secure: 'true'
|
||||
name: agapesistemas-PROJETO-svc
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
protocol: TCP
|
||||
targetPort: 8080
|
||||
selector:
|
||||
app: PROJETO
|
||||
type: ClusterIP
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: agapesistemas-PROJETO-cm
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: agapesistemas-PROJETO-sc
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: agapesistemas-PROJETOAMBIENTE-tls
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
dnsNames:
|
||||
- PROJETOAMBIENTE.agapesistemas.com.br
|
||||
issuerRef:
|
||||
group: cert-manager.io
|
||||
kind: ClusterIssuer
|
||||
name: letsencrypt-prod
|
||||
secretName: agapesistemas-PROJETOAMBIENTE-tls
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: PROJETO
|
||||
workload.user.cattle.io/workloadselector: apps.deployment-agapesistemas-ns-AMBIENTE-PROJETO
|
||||
name: PROJETO-dp-AMBIENTE
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
workload.user.cattle.io/workloadselector: apps.deployment-agapesistemas-ns-AMBIENTE-PROJETO
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: PROJETO
|
||||
workload.user.cattle.io/workloadselector: apps.deployment-agapesistemas-ns-AMBIENTE-PROJETO
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
- labelSelector:
|
||||
matchExpressions:
|
||||
- key: app
|
||||
operator: In
|
||||
values:
|
||||
- PROJETO
|
||||
topologyKey: kubernetes.io/hostname
|
||||
containers:
|
||||
- env:
|
||||
- name: TZ
|
||||
value: America/Maceio
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: agapesistemas-db-credential-sc
|
||||
- secretRef:
|
||||
name: agapesistemas-PROJETO-sc
|
||||
- configMapRef:
|
||||
name: agapesistemas-PROJETO-cm
|
||||
- secretRef:
|
||||
name: agapesistemas-db-role-credential-sc
|
||||
optional: false
|
||||
image: IMAGEM:TAG
|
||||
name: PROJETO
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
protocol: TCP
|
||||
resources:
|
||||
limits:
|
||||
memory: 857Mi
|
||||
requests:
|
||||
memory: 720Mi
|
||||
nodeSelector:
|
||||
doks.digitalocean.com/node-pool: pool-k8s-agapesistemas-app-AMBIENTE
|
||||
imagePullSecrets:
|
||||
- name: registry-agapesistemas
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: PROJETO
|
||||
workload.user.cattle.io/workloadselector: apps.deployment-agapesistemas-ns-AMBIENTE-PROJETO
|
||||
name: PROJETO-dp-AMBIENTE
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
workload.user.cattle.io/workloadselector: apps.deployment-agapesistemas-ns-AMBIENTE-PROJETO
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: PROJETO
|
||||
workload.user.cattle.io/workloadselector: apps.deployment-agapesistemas-ns-AMBIENTE-PROJETO
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
containers:
|
||||
- env:
|
||||
- name: TZ
|
||||
value: America/Maceio
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: agapesistemas-db-credential-sc
|
||||
- secretRef:
|
||||
name: agapesistemas-PROJETO-sc
|
||||
- configMapRef:
|
||||
name: agapesistemas-PROJETO-cm
|
||||
image: IMAGEM:TAG
|
||||
name: PROJETO
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
protocol: TCP
|
||||
resources:
|
||||
limits:
|
||||
memory: 1800Mi
|
||||
requests:
|
||||
memory: 1008Mi
|
||||
nodeSelector:
|
||||
doks.digitalocean.com/node-pool: pool-k8s-agapesistemas-app-AMBIENTE
|
||||
imagePullSecrets:
|
||||
- name: registry-agapesistemas
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: agapesistemas-PROJETO-hpa
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
maxReplicas: 1
|
||||
metrics:
|
||||
- resource:
|
||||
name: memory
|
||||
target:
|
||||
averageValue: 720Mi
|
||||
type: AverageValue
|
||||
type: Resource
|
||||
minReplicas: 1
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: PROJETO-dp-AMBIENTE
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.middlewares: traefik-http-to-https-redirectscheme@kubernetescrd,traefik-enable-cors-header@kubernetescrd
|
||||
name: agapesistemas-PROJETO-ing
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
rules:
|
||||
- host: PROJETOAMBIENTE.agapesistemas.com.br
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: agapesistemas-PROJETO-svc
|
||||
port:
|
||||
number: 8080
|
||||
pathType: ImplementationSpecific
|
||||
tls:
|
||||
- hosts:
|
||||
- PROJETOAMBIENTE.agapesistemas.com.br
|
||||
secretName: agapesistemas-PROJETOAMBIENTE-tls
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/service.sticky.cookie: 'true'
|
||||
#traefik.ingress.kubernetes.io/service.sticky.cookie.name: JSESSIONID
|
||||
traefik.ingress.kubernetes.io/service.sticky.cookie.secure: 'true'
|
||||
name: agapesistemas-PROJETO-svc
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
protocol: TCP
|
||||
targetPort: 8080
|
||||
selector:
|
||||
app: PROJETO
|
||||
type: ClusterIP
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: agapesistemas-PROJETO-cm
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
|
||||
data:
|
||||
VITE_API_URL: 'https://agpainelapi.hml.agapesistemas.com.br'
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: agapesistemas-PROJETO-sc
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: agapesistemas-PROJETOAMBIENTE-tls
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
dnsNames:
|
||||
- PROJETOAMBIENTE.agapesistemas.com.br
|
||||
issuerRef:
|
||||
group: cert-manager.io
|
||||
kind: ClusterIssuer
|
||||
name: letsencrypt-prod
|
||||
secretName: agapesistemas-PROJETOAMBIENTE-tls
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: PROJETO
|
||||
workload.user.cattle.io/workloadselector: apps.deployment-agapesistemas-ns-AMBIENTE-PROJETO
|
||||
name: PROJETO-dp-AMBIENTE
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
replicas: 2
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
workload.user.cattle.io/workloadselector: apps.deployment-agapesistemas-ns-AMBIENTE-PROJETO
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: PROJETO
|
||||
workload.user.cattle.io/workloadselector: apps.deployment-agapesistemas-ns-AMBIENTE-PROJETO
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
- labelSelector:
|
||||
matchExpressions:
|
||||
- key: app
|
||||
operator: In
|
||||
values:
|
||||
- PROJETO
|
||||
topologyKey: kubernetes.io/hostname
|
||||
containers:
|
||||
- env:
|
||||
- name: TZ
|
||||
value: America/Maceio
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: agapesistemas-db-credential-sc
|
||||
- secretRef:
|
||||
name: agapesistemas-PROJETO-sc
|
||||
- configMapRef:
|
||||
name: agapesistemas-PROJETO-cm
|
||||
image: IMAGEM:TAG
|
||||
name: PROJETO
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
protocol: TCP
|
||||
resources:
|
||||
limits:
|
||||
memory: 7142Mi
|
||||
requests:
|
||||
memory: 6000Mi
|
||||
nodeSelector:
|
||||
doks.digitalocean.com/node-pool: pool-k8s-agapesistemas-app-AMBIENTE-high
|
||||
imagePullSecrets:
|
||||
- name: registry-agapesistemas
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: PROJETO
|
||||
workload.user.cattle.io/workloadselector: apps.deployment-agapesistemas-ns-AMBIENTE-PROJETO
|
||||
name: PROJETO-dp-AMBIENTE
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
replicas: 2
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
workload.user.cattle.io/workloadselector: apps.deployment-agapesistemas-ns-AMBIENTE-PROJETO
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: PROJETO
|
||||
workload.user.cattle.io/workloadselector: apps.deployment-agapesistemas-ns-AMBIENTE-PROJETO
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
- labelSelector:
|
||||
matchExpressions:
|
||||
- key: app
|
||||
operator: In
|
||||
values:
|
||||
- PROJETO
|
||||
topologyKey: kubernetes.io/hostname
|
||||
containers:
|
||||
- env:
|
||||
- name: TZ
|
||||
value: America/Maceio
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: agapesistemas-db-credential-sc
|
||||
- secretRef:
|
||||
name: agapesistemas-PROJETO-sc
|
||||
- configMapRef:
|
||||
name: agapesistemas-PROJETO-cm
|
||||
- secretRef:
|
||||
name: agapesistemas-db-role-credential-sc
|
||||
optional: false
|
||||
image: IMAGEM:TAG
|
||||
name: PROJETO
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
protocol: TCP
|
||||
resources:
|
||||
limits:
|
||||
memory: 1000Mi
|
||||
requests:
|
||||
memory: 780Mi
|
||||
nodeSelector:
|
||||
doks.digitalocean.com/node-pool: pool-k8s-agapesistemas-app-AMBIENTE
|
||||
imagePullSecrets:
|
||||
- name: registry-agapesistemas
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: PROJETO
|
||||
workload.user.cattle.io/workloadselector: apps.deployment-agapesistemas-ns-AMBIENTE-PROJETO
|
||||
name: PROJETO-dp-AMBIENTE
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
workload.user.cattle.io/workloadselector: apps.deployment-agapesistemas-ns-AMBIENTE-PROJETO
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: PROJETO
|
||||
workload.user.cattle.io/workloadselector: apps.deployment-agapesistemas-ns-AMBIENTE-PROJETO
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
containers:
|
||||
- env:
|
||||
- name: TZ
|
||||
value: America/Maceio
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: agapesistemas-db-credential-sc
|
||||
- secretRef:
|
||||
name: agapesistemas-PROJETO-sc
|
||||
- configMapRef:
|
||||
name: agapesistemas-PROJETO-cm
|
||||
image: IMAGEM:TAG
|
||||
name: PROJETO
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
protocol: TCP
|
||||
resources:
|
||||
limits:
|
||||
memory: 1200Mi
|
||||
requests:
|
||||
memory: 1000Mi
|
||||
nodeSelector:
|
||||
doks.digitalocean.com/node-pool: pool-k8s-agapesistemas-app-AMBIENTE
|
||||
imagePullSecrets:
|
||||
- name: registry-agapesistemas
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: agapesistemas-PROJETO-hpa
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
maxReplicas: 3
|
||||
metrics:
|
||||
- resource:
|
||||
name: memory
|
||||
target:
|
||||
averageValue: 6000Mi
|
||||
type: AverageValue
|
||||
type: Resource
|
||||
minReplicas: 2
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: PROJETO-dp-AMBIENTE
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: agapesistemas-PROJETO-hpa
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
maxReplicas: 1
|
||||
metrics:
|
||||
- resource:
|
||||
name: memory
|
||||
target:
|
||||
averageValue: 780Mi
|
||||
type: AverageValue
|
||||
type: Resource
|
||||
minReplicas: 1
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: PROJETO-dp-AMBIENTE
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.middlewares: traefik-http-to-https-redirectscheme@kubernetescrd,traefik-enable-cors-header@kubernetescrd
|
||||
name: agapesistemas-PROJETO-ing
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
rules:
|
||||
- host: PROJETOAMBIENTE.agapesistemas.com.br
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: agapesistemas-PROJETO-svc
|
||||
port:
|
||||
number: 8080
|
||||
pathType: ImplementationSpecific
|
||||
tls:
|
||||
- hosts:
|
||||
- PROJETOAMBIENTE.agapesistemas.com.br
|
||||
secretName: agapesistemas-PROJETOAMBIENTE-tls
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/service.sticky.cookie: 'true'
|
||||
#traefik.ingress.kubernetes.io/service.sticky.cookie.name: JSESSIONID
|
||||
traefik.ingress.kubernetes.io/service.sticky.cookie.secure: 'true'
|
||||
name: agapesistemas-PROJETO-svc
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
protocol: TCP
|
||||
targetPort: 8080
|
||||
selector:
|
||||
app: PROJETO
|
||||
type: ClusterIP
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: agapesistemas-PROJETO-cm
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: agapesistemas-PROJETO-cm
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
|
||||
data:
|
||||
VITE_API_URL: 'https://agpainelapiprd.agapesistemas.com.br'
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: agapesistemas-PROJETO-sc
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: agapesistemas-PROJETO-sc
|
||||
namespace: agapesistemas-ns-AMBIENTE
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"name": "agdiariooficialweb",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@phosphor-icons/react": "^2.1.10",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@tanstack/react-query": "^5.90.10",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.554.0",
|
||||
"react": "^19.2.0",
|
||||
"react-day-picker": "^9.11.1",
|
||||
"react-dom": "^19.2.0",
|
||||
"recharts": "^2.15.4",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"vaul": "^1.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@vitejs/plugin-react": "^5.1.0",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.46.3",
|
||||
"vite": "^7.2.2"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,132 @@
|
|||
import { DownloadIcon } from '@phosphor-icons/react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useState } from 'react';
|
||||
import type { SearchParams } from './app/models';
|
||||
import { diarioApi } from './app/services';
|
||||
import { DiariosList } from './views/components/diario-list';
|
||||
import { Footer } from './views/components/footer';
|
||||
import { Header } from './views/components/header';
|
||||
import { InfoSection } from './views/components/info-section';
|
||||
import { SearchForm } from './views/components/search-form';
|
||||
import { Button } from './views/components/ui/button';
|
||||
|
||||
export function App() {
|
||||
const [searchParams, setSearchParams] = useState<SearchParams>({
|
||||
entidade: '',
|
||||
grupo: '',
|
||||
subgrupo: '',
|
||||
ano: '',
|
||||
periodoDe: '',
|
||||
periodoAte: '',
|
||||
certificado: '',
|
||||
palavraChave: '',
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: ultimosDiarios, isLoading: isLoadingUltimos } = useQuery({
|
||||
queryKey: ['ultimos-diarios'],
|
||||
queryFn: () => diarioApi.buscarUltimosDiarios(),
|
||||
});
|
||||
|
||||
const {
|
||||
data: diariosFiltrados,
|
||||
isLoading: isLoadingBusca,
|
||||
refetch: buscarDiarios,
|
||||
} = useQuery({
|
||||
queryKey: ['diarios-filtrados', searchParams],
|
||||
queryFn: () => diarioApi.buscarDiarios(searchParams),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
function handleParamChange(field: keyof SearchParams, value: string) {
|
||||
setSearchParams((prev) => ({ ...prev, [field]: value }));
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
buscarDiarios();
|
||||
}
|
||||
|
||||
function handleClear() {
|
||||
setSearchParams({
|
||||
entidade: '',
|
||||
grupo: '',
|
||||
subgrupo: '',
|
||||
ano: '',
|
||||
periodoDe: '',
|
||||
periodoAte: '',
|
||||
certificado: '',
|
||||
palavraChave: '',
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ['ultimos-diarios'] });
|
||||
}
|
||||
|
||||
const handleDownload = async (id: number) => {
|
||||
try {
|
||||
const diario = await diarioApi.buscarDiarioPorId(id);
|
||||
|
||||
alert(`Download do diário: ${diario.titulo}`);
|
||||
} catch (error) {
|
||||
console.error('Erro ao baixar diário:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleView = (id: number) => {
|
||||
console.log('Visualizar diário:', id);
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
alert('Exportando resultados...');
|
||||
} catch (error) {
|
||||
console.error('Erro ao exportar:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-linear-to-b from-slate-50 to-gray-100 dark:from-gray-900 dark:to-slate-950">
|
||||
<Header />
|
||||
|
||||
<div className="container mx-auto px-4 py-8 space-y-8">
|
||||
<SearchForm
|
||||
searchParams={searchParams}
|
||||
onSearch={handleSearch}
|
||||
onClear={handleClear}
|
||||
onParamChange={handleParamChange}
|
||||
isLoading={isLoadingBusca}
|
||||
/>
|
||||
|
||||
{diariosFiltrados?.data && diariosFiltrados.data.length > 0 && (
|
||||
<div className="flex justify-end">
|
||||
<Button variant="outline" onClick={handleExport} className="gap-2">
|
||||
<DownloadIcon size={18} />
|
||||
Exportar Resultados
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{diariosFiltrados?.data ? (
|
||||
<DiariosList
|
||||
diarios={diariosFiltrados.data}
|
||||
title="Resultados da Busca"
|
||||
subtitle={`${diariosFiltrados.total} diários encontrados com os filtros aplicados`}
|
||||
loading={isLoadingBusca}
|
||||
onDownload={handleDownload}
|
||||
onView={handleView}
|
||||
/>
|
||||
) : (
|
||||
<DiariosList
|
||||
diarios={ultimosDiarios || []}
|
||||
loading={isLoadingUltimos}
|
||||
onDownload={handleDownload}
|
||||
onView={handleView}
|
||||
/>
|
||||
)}
|
||||
|
||||
<InfoSection />
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
export interface Diario {
|
||||
id: number;
|
||||
data: string;
|
||||
titulo: string;
|
||||
descricao: string;
|
||||
tamanho: string;
|
||||
entidade?: string;
|
||||
grupo?: string;
|
||||
subgrupo?: string;
|
||||
}
|
||||
|
||||
export interface SearchParams {
|
||||
entidade: string;
|
||||
grupo: string;
|
||||
subgrupo: string;
|
||||
ano: string;
|
||||
periodoDe: string;
|
||||
periodoAte: string;
|
||||
certificado: string;
|
||||
palavraChave: string;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
data: T;
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import type { ApiResponse, Diario, SearchParams } from '@/app/models';
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
const mockDiarios: Diario[] = [
|
||||
{
|
||||
id: 1,
|
||||
data: '02/12/2024',
|
||||
titulo: 'Diário Oficial nº 2456',
|
||||
descricao: 'Publicações oficiais do município',
|
||||
tamanho: '4.2 MB',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
data: '01/12/2024',
|
||||
titulo: 'Diário Oficial nº 2455',
|
||||
descricao: 'Atos administrativos e licitações',
|
||||
tamanho: '3.8 MB',
|
||||
},
|
||||
];
|
||||
|
||||
export const diarioApi = {
|
||||
async buscarDiarios(
|
||||
params: Partial<SearchParams> = {},
|
||||
): Promise<ApiResponse<Diario[]>> {
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
|
||||
let filtered = [...mockDiarios];
|
||||
|
||||
if (params.entidade) {
|
||||
filtered = filtered.filter((d) => d.entidade === params.entidade);
|
||||
}
|
||||
|
||||
if (params.palavraChave) {
|
||||
const keyword = params.palavraChave.toLowerCase();
|
||||
filtered = filtered.filter(
|
||||
(d) =>
|
||||
d.titulo.toLowerCase().includes(keyword) ||
|
||||
d.descricao.toLowerCase().includes(keyword),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
data: filtered,
|
||||
total: filtered.length,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
};
|
||||
},
|
||||
|
||||
async buscarDiarioPorId(id: number): Promise<Diario> {
|
||||
const diario = mockDiarios.find((d) => d.id === id);
|
||||
if (!diario) throw new Error('Diário não encontrado');
|
||||
return diario;
|
||||
},
|
||||
|
||||
async buscarUltimosDiarios(limit: number = 6): Promise<Diario[]> {
|
||||
return mockDiarios.slice(0, limit);
|
||||
},
|
||||
};
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: true,
|
||||
staleTime: 1000 * 30,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
|
|
@ -0,0 +1,16 @@
|
|||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import './styles/index.css';
|
||||
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { App } from './App.tsx';
|
||||
import { queryClient } from './app/services/index.ts';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
</QueryClientProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
|
@ -0,0 +1,288 @@
|
|||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@font-face {
|
||||
font-family: 'DIN Next';
|
||||
font-weight: 900;
|
||||
src: url('../assets/fonts/din-next/din-next-black.otf') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'DIN Next';
|
||||
font-weight: 700;
|
||||
src: url('../assets/fonts/din-next/din-next-bold.otf') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'DIN Next';
|
||||
font-weight: 600;
|
||||
src: url('../assets/fonts/din-next/din-next-medium.otf') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'DIN Next';
|
||||
font-weight: 400;
|
||||
src: url('../assets/fonts/din-next/din-next-regular.otf') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'DIN Next';
|
||||
font-weight: 300;
|
||||
src: url('../assets/fonts/din-next/din-next-light.otf') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'DIN Next';
|
||||
font-weight: 200;
|
||||
src: url('../assets/fonts/din-next/din-next-ultra-light.otf') format('woff');
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-sans: 'DIN Next', sans-serif;
|
||||
|
||||
--bigstone-50: #f2f7fd;
|
||||
--bigstone-100: #e3edfb;
|
||||
--bigstone-200: #c1dcf6;
|
||||
--bigstone-300: #8bbfee;
|
||||
--bigstone-400: #4d9ee3;
|
||||
--bigstone-500: #2682d1;
|
||||
--bigstone-600: #1866b1;
|
||||
--bigstone-700: #145190;
|
||||
--bigstone-800: #154677;
|
||||
--bigstone-900: #173b63;
|
||||
--bigstone-950: #0e233d;
|
||||
|
||||
/* Cores usadas pelos gráficos */
|
||||
--color-desktop: var(--bigstone-500);
|
||||
--color-mobile: var(--bigstone-300);
|
||||
|
||||
--color-chrome: var(--bigstone-500);
|
||||
--color-safari: var(--bigstone-600);
|
||||
--color-firefox: var(--bigstone-700);
|
||||
--color-edge: var(--bigstone-800);
|
||||
--color-other: var(--bigstone-300);
|
||||
|
||||
--chart-1: var(--bigstone-500);
|
||||
--chart-2: var(--bigstone-400);
|
||||
--chart-3: var(--bigstone-600);
|
||||
--chart-4: var(--bigstone-700);
|
||||
--chart-5: var(--bigstone-300);
|
||||
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: #805737;
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slidein {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-slidein {
|
||||
animation: slidein 1s ease forwards;
|
||||
}
|
||||
.animate-slidein300 {
|
||||
animation: slidein 1s ease 300ms forwards;
|
||||
}
|
||||
.animate-slidein500 {
|
||||
animation: slidein 1s ease 500ms forwards;
|
||||
}
|
||||
.animate-slidein700 {
|
||||
animation: slidein 1s ease 700ms forwards;
|
||||
}
|
||||
|
||||
/* Greeting pill (avatar header) */
|
||||
.greeting-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
transition: transform 160ms ease, box-shadow 160ms ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.greeting-pill:hover {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
.greeting-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.greeting-text {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: inherit;
|
||||
display: none; /* esconder em telas muito pequenas */
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.greeting-text {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes float-y {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slow-rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.greeting-pill .greeting-icon {
|
||||
animation: float-y 3.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.greeting-pill.day .greeting-icon svg {
|
||||
filter: drop-shadow(0 1px 6px rgba(255, 180, 60, 0.12));
|
||||
}
|
||||
|
||||
.greeting-pill.night .greeting-icon svg {
|
||||
filter: drop-shadow(0 2px 8px rgba(96, 165, 250, 0.08));
|
||||
}
|
||||
|
||||
.greeting-pill.day .greeting-icon.animate-rotate {
|
||||
animation: slow-rotate 8s linear infinite, float-y 3.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* small touch: slightly scale on active */
|
||||
.greeting-pill:active {
|
||||
transform: translateY(-1px) scale(0.995);
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import type { Diario } from '@/app/models';
|
||||
import { Button } from '@/views/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/views/components/ui/card';
|
||||
import { CalendarIcon, DownloadIcon } from '@phosphor-icons/react';
|
||||
|
||||
interface DiarioCardProps {
|
||||
diario: Diario;
|
||||
onDownload?: (id: number) => void;
|
||||
onView?: (id: number) => void;
|
||||
}
|
||||
|
||||
export function DiarioCard({ diario, onDownload, onView }: DiarioCardProps) {
|
||||
return (
|
||||
<Card className="group hover:shadow-xl transition-all duration-300 border hover:border-primary/30 dark:hover:border-primary/80 hover:-translate-y-1 h-full">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-md bg-primary/20 dark:bg-blue-900/30 text-primary group-hover:bg-primary/20 dark:group-hover:bg-blue-900/50 transition-colors">
|
||||
<CalendarIcon className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-lg">{diario.data}</CardTitle>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Data de publicação
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="px-3 py-1 bg-primary/20 dark:bg-blue-900 text-primary rounded-full text-xs font-medium">
|
||||
{diario.tamanho}
|
||||
</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-800 dark:text-gray-100 mb-1 line-clamp-2">
|
||||
{diario.titulo}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||
{diario.descricao}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3 pt-2">
|
||||
<Button
|
||||
className="flex-1 bg-gradient-to-r from-primary to-primary/90 text-white"
|
||||
onClick={() => onDownload?.(diario.id)}>
|
||||
<DownloadIcon className="mr-2 h-4 w-4" />
|
||||
Baixar PDF
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-primary/20 text-primary hover:bg-primary/20 dark:border-primary/80"
|
||||
onClick={() => onView?.(diario.id)}>
|
||||
Visualizar
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import type { Diario } from '@/app/models';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { DiarioCard } from './diario-card';
|
||||
|
||||
interface DiariosListProps {
|
||||
diarios: Diario[];
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
loading?: boolean;
|
||||
onDownload?: (id: number) => void;
|
||||
onView?: (id: number) => void;
|
||||
}
|
||||
|
||||
export function DiariosList({
|
||||
diarios,
|
||||
title = 'Últimas Publicações',
|
||||
subtitle = 'Acesse as edições mais recentes do Diário Oficial do Município de Ribeirópolis',
|
||||
loading = false,
|
||||
onDownload,
|
||||
onView,
|
||||
}: DiariosListProps) {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center py-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="space-y-6">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-2xl font-medium text-gray-800 dark:text-gray-100">
|
||||
{title}
|
||||
</p>
|
||||
<p className="text-muted-foreground mt-2 max-w-3xl">{subtitle}</p>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Total de {diarios.length} publicações
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{diarios.length === 0 ? (
|
||||
<div className="text-center py-12 border rounded-lg">
|
||||
<p className="text-muted-foreground">Nenhum diário encontrado</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{diarios.map((diario) => (
|
||||
<DiarioCard
|
||||
key={diario.id}
|
||||
diario={diario}
|
||||
onDownload={onDownload}
|
||||
onView={onView}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
import { Button } from '@/views/components/ui/button';
|
||||
import { Calendar } from '@/views/components/ui/calendar';
|
||||
import { Input } from '@/views/components/ui/input';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/views/components/ui/popover';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/views/components/ui/select';
|
||||
import { CalendarIcon } from '@phosphor-icons/react';
|
||||
import { format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
|
||||
interface FilterFieldProps {
|
||||
type: 'select' | 'input' | 'date';
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
options?: Array<{ value: string; label: string }>;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export function FilterField({
|
||||
type,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
options = [],
|
||||
placeholder,
|
||||
}: FilterFieldProps) {
|
||||
if (type === 'select') {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{label}
|
||||
</label>
|
||||
<Select value={value} onValueChange={onChange}>
|
||||
<SelectTrigger className="h-11">
|
||||
<SelectValue
|
||||
placeholder={placeholder || `Selecione ${label.toLowerCase()}`}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'date') {
|
||||
const dateValue = value ? new Date(value) : undefined;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{label}
|
||||
</label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full h-11 justify-start text-left font-normal">
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{dateValue ? (
|
||||
format(dateValue, 'PPP', { locale: ptBR })
|
||||
) : (
|
||||
<span>{placeholder || 'Selecione uma data'}</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={dateValue}
|
||||
onSelect={(date) =>
|
||||
onChange(date ? format(date, 'yyyy-MM-dd') : '')
|
||||
}
|
||||
initialFocus
|
||||
locale={ptBR}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{label}
|
||||
</label>
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="h-11"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
export function Footer() {
|
||||
return (
|
||||
<div className="w-full flex items-center justify-center bg-primary mt-12">
|
||||
<footer className="flex flex-col md:flex-row items-center justify-between container mx-auto px-4 py-8 text-white text-center md:text-left gap-4">
|
||||
<p className="text-sm md:text-base">Ágape Sistemas e Tecnologia</p>
|
||||
<p className="text-sm md:text-base">
|
||||
© {new Date().getFullYear()} Todos os direitos reservados.
|
||||
</p>
|
||||
<p className="text-sm md:text-base">
|
||||
Contato: suporte@agapesistemas.com.br
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
export function Header() {
|
||||
return (
|
||||
<>
|
||||
<div className="py-4 bg-primary"></div>
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<img
|
||||
src="https://agportal.agapesistemas.com.br/DiarioOficial/Logo?codigo=251"
|
||||
alt="Brasão da Prefeitura"
|
||||
className="w-16 h-16 md:w-20 md:h-20 object-contain"
|
||||
/>
|
||||
<div className="text-black dark:text-white">
|
||||
<h1 className="text-lg md:text-xl tracking-wider">
|
||||
Diário Oficial de
|
||||
</h1>
|
||||
<h2 className="text-xl md:text-2xl font-bold mt-1">
|
||||
PREFEITURA MUN. DE RIBEIROPOLIS
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import {
|
||||
CalendarBlankIcon,
|
||||
FileTextIcon,
|
||||
ScalesIcon,
|
||||
} from '@phosphor-icons/react';
|
||||
|
||||
export function InfoSection() {
|
||||
return (
|
||||
<div className="mt-12 pt-8 border-t border-gray-200 dark:border-gray-800">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div className="space-y-3">
|
||||
<h3 className="flex items-center gap-2 font-semibold text-lg text-gray-800 dark:text-gray-100">
|
||||
<FileTextIcon size={22} weight="duotone" />
|
||||
Sobre o Diário Oficial
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Publicação oficial da Prefeitura Municipal de Ribeirópolis, contendo
|
||||
atos, editais, licitações e comunicados oficiais.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="flex items-center gap-2 font-semibold text-lg text-gray-800 dark:text-gray-100">
|
||||
<ScalesIcon size={22} weight="duotone" />
|
||||
Legalidade
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
As publicações têm validade jurídica e são essenciais para
|
||||
transparência pública conforme a Lei de Acesso à Informação.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="flex items-center gap-2 font-semibold text-lg text-gray-800 dark:text-gray-100">
|
||||
<CalendarBlankIcon size={22} weight="duotone" />
|
||||
Frequência
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Publicado diariamente, de segunda a sexta-feira, exceto feriados
|
||||
municipais e nacionais.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
import type { SearchParams } from '@/app/models';
|
||||
import { FilterField } from './filter-field';
|
||||
|
||||
interface SearchFiltersProps {
|
||||
searchParams: SearchParams;
|
||||
onParamChange: (field: keyof SearchParams, value: string) => void;
|
||||
}
|
||||
|
||||
export function SearchFilters({
|
||||
searchParams,
|
||||
onParamChange,
|
||||
}: SearchFiltersProps) {
|
||||
const entidadeOptions = [
|
||||
{ value: 'prefeitura', label: 'Prefeitura Municipal' },
|
||||
{ value: 'camara', label: 'Câmara dos Vereadores' },
|
||||
{ value: 'autarquias', label: 'Autarquias Municipais' },
|
||||
];
|
||||
|
||||
const grupoOptions = [
|
||||
{ value: 'licitacoes', label: 'Licitações' },
|
||||
{ value: 'concursos', label: 'Concursos Públicos' },
|
||||
{ value: 'contratacoes', label: 'Contratações' },
|
||||
{ value: 'editais', label: 'Editais' },
|
||||
];
|
||||
|
||||
const subgrupoOptions = [
|
||||
{ value: 'pregão', label: 'Pregão' },
|
||||
{ value: 'tomada', label: 'Tomada de Preços' },
|
||||
{ value: 'concurso', label: 'Concurso Público' },
|
||||
];
|
||||
|
||||
const anoOptions = [
|
||||
{ value: '2024', label: '2024' },
|
||||
{ value: '2023', label: '2023' },
|
||||
{ value: '2022', label: '2022' },
|
||||
{ value: '2021', label: '2021' },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<FilterField
|
||||
type="select"
|
||||
label="Entidade"
|
||||
value={searchParams.entidade}
|
||||
onChange={(value) => onParamChange('entidade', value)}
|
||||
options={entidadeOptions}
|
||||
/>
|
||||
|
||||
<FilterField
|
||||
type="select"
|
||||
label="Grupo"
|
||||
value={searchParams.grupo}
|
||||
onChange={(value) => onParamChange('grupo', value)}
|
||||
options={grupoOptions}
|
||||
/>
|
||||
|
||||
<FilterField
|
||||
type="select"
|
||||
label="Sub-Grupo"
|
||||
value={searchParams.subgrupo}
|
||||
onChange={(value) => onParamChange('subgrupo', value)}
|
||||
options={subgrupoOptions}
|
||||
/>
|
||||
|
||||
<FilterField
|
||||
type="select"
|
||||
label="Ano"
|
||||
value={searchParams.ano}
|
||||
onChange={(value) => onParamChange('ano', value)}
|
||||
options={anoOptions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<FilterField
|
||||
type="date"
|
||||
label="Período de"
|
||||
value={searchParams.periodoDe}
|
||||
onChange={(value) => onParamChange('periodoDe', value)}
|
||||
placeholder="Data inicial"
|
||||
/>
|
||||
|
||||
<FilterField
|
||||
type="date"
|
||||
label="Até"
|
||||
value={searchParams.periodoAte}
|
||||
onChange={(value) => onParamChange('periodoAte', value)}
|
||||
placeholder="Data final"
|
||||
/>
|
||||
|
||||
<FilterField
|
||||
type="input"
|
||||
label="Nº Certificado"
|
||||
value={searchParams.certificado}
|
||||
onChange={(value) => onParamChange('certificado', value)}
|
||||
placeholder="Digite o número"
|
||||
/>
|
||||
|
||||
<FilterField
|
||||
type="input"
|
||||
label="Palavra-chave"
|
||||
value={searchParams.palavraChave}
|
||||
onChange={(value) => onParamChange('palavraChave', value)}
|
||||
placeholder="Buscar no conteúdo"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import type { SearchParams } from '@/app/models';
|
||||
import { Button } from '@/views/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/views/components/ui/card';
|
||||
import { SearchFilters } from './search-filters';
|
||||
|
||||
interface SearchFormProps {
|
||||
searchParams: SearchParams;
|
||||
onSearch: () => void;
|
||||
onClear: () => void;
|
||||
onParamChange: (field: keyof SearchParams, value: string) => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function SearchForm({
|
||||
searchParams,
|
||||
onSearch,
|
||||
onClear,
|
||||
onParamChange,
|
||||
isLoading = false,
|
||||
}: SearchFormProps) {
|
||||
return (
|
||||
<Card className="border border-slate-200 dark:border-gray-800 shadow-xl">
|
||||
<CardHeader className="bg-linear-to-r py-4 rounded-t-2xl from-slate-50 to-gray-50 dark:from-gray-800 dark:to-gray-900 border-b">
|
||||
<div className="flex items-center gap-3">
|
||||
<CardTitle className="text-2xl text-gray-800 dark:text-gray-100 font-medium">
|
||||
Busca
|
||||
</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="pt-6">
|
||||
<SearchFilters
|
||||
searchParams={searchParams}
|
||||
onParamChange={onParamChange}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-between items-center pt-4 border-t">
|
||||
<Button
|
||||
onClick={onSearch}
|
||||
disabled={isLoading}
|
||||
className="bg-linear-to-r from-primary to-primary/90 text-lg text-white px-8 py-6 font-medium shadow-lg hover:shadow-xl transition-all duration-300 min-w-50">
|
||||
{isLoading ? 'Buscando...' : 'Realizar Busca'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClear}
|
||||
className="border-primary/30 text-primary hover:bg-primary/20 dark:border-primary/80 dark:hover:bg-blue-900/30">
|
||||
Limpar Filtros
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/* eslint-disable react-refresh/only-export-components */
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/app/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-sm text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-gray-950 disabled:pointer-events-none disabled:opacity-50 dark:focus-visible:ring-gray-300',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'bg-primary text-gray-50 shadow hover:bg-primary/90 dark:bg-gray-50 dark:!text-primary dark:hover:bg-gray-50/90',
|
||||
destructive:
|
||||
'bg-red-500 text-gray-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-gray-50 dark:hover:bg-red-900/90',
|
||||
outline:
|
||||
'border border-gray-200 bg-gray-50 shadow-sm hover:bg-gray-100 hover:text-gray-900 dark:border-gray-800 dark:bg-gray-950 dark:hover:bg-gray-800 dark:hover:text-gray-50',
|
||||
secondary:
|
||||
'bg-gray-50 text-gray-900 shadow-sm hover:bg-gray-300/60 dark:bg-gray-800 dark:text-gray-50 dark:hover:bg-gray-800/60',
|
||||
ghost:
|
||||
'hover:bg-gray-100 hover:text-gray-900 dark:text-gray-50 dark:hover:bg-gray-800/50',
|
||||
// link: 'text-gray-900 underline-offset-4 hover:underline dark:text-gray-50',
|
||||
link: 'hover:bg-white/10 underline-offset-4 hover:underline dark:text-gray-50 dark:hover:bg-gray-600/50',
|
||||
disable:
|
||||
'bg-red-900 text-gray-50 shadow-sm hover:bg-red-900/90 dark:bg-red-500 dark:text-gray-50 dark:hover:bg-red-500/90',
|
||||
search:
|
||||
'bg-gray-50 hover:bg-gray-200 text-gray-400 hover:text-gray-600',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-sm px-3 text-xs',
|
||||
lg: 'h-10 rounded-sm p-6 text-base',
|
||||
icon: 'h-9 w-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Button.displayName = 'Button';
|
||||
|
||||
export { Button, buttonVariants };
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
} from "lucide-react"
|
||||
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
|
||||
|
||||
import { cn } from "@/app/utils"
|
||||
import { Button, buttonVariants } from "@/views/components/ui/button"
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
captionLayout = "label",
|
||||
buttonVariant = "ghost",
|
||||
formatters,
|
||||
components,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayPicker> & {
|
||||
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
||||
}) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn(
|
||||
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
||||
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
||||
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
||||
className
|
||||
)}
|
||||
captionLayout={captionLayout}
|
||||
formatters={{
|
||||
formatMonthDropdown: (date) =>
|
||||
date.toLocaleString("default", { month: "short" }),
|
||||
...formatters,
|
||||
}}
|
||||
classNames={{
|
||||
root: cn("w-fit", defaultClassNames.root),
|
||||
months: cn(
|
||||
"flex gap-4 flex-col md:flex-row relative",
|
||||
defaultClassNames.months
|
||||
),
|
||||
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
|
||||
nav: cn(
|
||||
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
|
||||
defaultClassNames.nav
|
||||
),
|
||||
button_previous: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||
defaultClassNames.button_previous
|
||||
),
|
||||
button_next: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||
defaultClassNames.button_next
|
||||
),
|
||||
month_caption: cn(
|
||||
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
|
||||
defaultClassNames.month_caption
|
||||
),
|
||||
dropdowns: cn(
|
||||
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
|
||||
defaultClassNames.dropdowns
|
||||
),
|
||||
dropdown_root: cn(
|
||||
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
|
||||
defaultClassNames.dropdown_root
|
||||
),
|
||||
dropdown: cn(
|
||||
"absolute bg-popover inset-0 opacity-0",
|
||||
defaultClassNames.dropdown
|
||||
),
|
||||
caption_label: cn(
|
||||
"select-none font-medium",
|
||||
captionLayout === "label"
|
||||
? "text-sm"
|
||||
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
|
||||
defaultClassNames.caption_label
|
||||
),
|
||||
table: "w-full border-collapse",
|
||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||
weekday: cn(
|
||||
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
|
||||
defaultClassNames.weekday
|
||||
),
|
||||
week: cn("flex w-full mt-2", defaultClassNames.week),
|
||||
week_number_header: cn(
|
||||
"select-none w-(--cell-size)",
|
||||
defaultClassNames.week_number_header
|
||||
),
|
||||
week_number: cn(
|
||||
"text-[0.8rem] select-none text-muted-foreground",
|
||||
defaultClassNames.week_number
|
||||
),
|
||||
day: cn(
|
||||
"relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
|
||||
props.showWeekNumber
|
||||
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
|
||||
: "[&:first-child[data-selected=true]_button]:rounded-l-md",
|
||||
defaultClassNames.day
|
||||
),
|
||||
range_start: cn(
|
||||
"rounded-l-md bg-accent",
|
||||
defaultClassNames.range_start
|
||||
),
|
||||
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
||||
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
|
||||
today: cn(
|
||||
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
|
||||
defaultClassNames.today
|
||||
),
|
||||
outside: cn(
|
||||
"text-muted-foreground aria-selected:text-muted-foreground",
|
||||
defaultClassNames.outside
|
||||
),
|
||||
disabled: cn(
|
||||
"text-muted-foreground opacity-50",
|
||||
defaultClassNames.disabled
|
||||
),
|
||||
hidden: cn("invisible", defaultClassNames.hidden),
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
Root: ({ className, rootRef, ...props }) => {
|
||||
return (
|
||||
<div
|
||||
data-slot="calendar"
|
||||
ref={rootRef}
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
Chevron: ({ className, orientation, ...props }) => {
|
||||
if (orientation === "left") {
|
||||
return (
|
||||
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
if (orientation === "right") {
|
||||
return (
|
||||
<ChevronRightIcon
|
||||
className={cn("size-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ChevronDownIcon className={cn("size-4", className)} {...props} />
|
||||
)
|
||||
},
|
||||
DayButton: CalendarDayButton,
|
||||
WeekNumber: ({ children, ...props }) => {
|
||||
return (
|
||||
<td {...props}>
|
||||
<div className="flex size-(--cell-size) items-center justify-center text-center">
|
||||
{children}
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
},
|
||||
...components,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CalendarDayButton({
|
||||
className,
|
||||
day,
|
||||
modifiers,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayButton>) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
|
||||
const ref = React.useRef<HTMLButtonElement>(null)
|
||||
React.useEffect(() => {
|
||||
if (modifiers.focused) ref.current?.focus()
|
||||
}, [modifiers.focused])
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-day={day.date.toLocaleDateString()}
|
||||
data-selected-single={
|
||||
modifiers.selected &&
|
||||
!modifiers.range_start &&
|
||||
!modifiers.range_end &&
|
||||
!modifiers.range_middle
|
||||
}
|
||||
data-range-start={modifiers.range_start}
|
||||
data-range-end={modifiers.range_end}
|
||||
data-range-middle={modifiers.range_middle}
|
||||
className={cn(
|
||||
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
|
||||
defaultClassNames.day,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Calendar, CalendarDayButton }
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/app/utils';
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'rounded-xl hover:shadow-md border bg-white text-gray-950 dark:border-gray-800 hover:dark:border-gray-400 dark:bg-gray-950 dark:text-gray-50',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Card.displayName = 'Card';
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardHeader.displayName = 'CardHeader';
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn('font-semibold leading-none tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardTitle.displayName = 'CardTitle';
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn('text-sm text-gray-500 dark:text-gray-400', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardDescription.displayName = 'CardDescription';
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
||||
));
|
||||
CardContent.displayName = 'CardContent';
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex items-center p-6 pt-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardFooter.displayName = 'CardFooter';
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
};
|
||||
|
|
@ -0,0 +1,355 @@
|
|||
import * as React from "react"
|
||||
import * as RechartsPrimitive from "recharts"
|
||||
|
||||
import { cn } from "@/app/utils"
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: "", dark: ".dark" } as const
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: React.ReactNode
|
||||
icon?: React.ComponentType
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
)
|
||||
}
|
||||
|
||||
type ChartContextProps = {
|
||||
config: ChartConfig
|
||||
}
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useChart must be used within a <ChartContainer />")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
function ChartContainer({
|
||||
id,
|
||||
className,
|
||||
children,
|
||||
config,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
config: ChartConfig
|
||||
children: React.ComponentProps<
|
||||
typeof RechartsPrimitive.ResponsiveContainer
|
||||
>["children"]
|
||||
}) {
|
||||
const uniqueId = React.useId()
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
data-slot="chart"
|
||||
data-chart={chartId}
|
||||
className={cn(
|
||||
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>
|
||||
{children}
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
const colorConfig = Object.entries(config).filter(
|
||||
([, config]) => config.theme || config.color
|
||||
)
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Object.entries(THEMES)
|
||||
.map(
|
||||
([theme, prefix]) => `
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||
itemConfig.color
|
||||
return color ? ` --color-${key}: ${color};` : null
|
||||
})
|
||||
.join("\n")}
|
||||
}
|
||||
`
|
||||
)
|
||||
.join("\n"),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
||||
|
||||
function ChartTooltipContent({
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = "dot",
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean
|
||||
hideIndicator?: boolean
|
||||
indicator?: "line" | "dot" | "dashed"
|
||||
nameKey?: string
|
||||
labelKey?: string
|
||||
}) {
|
||||
const { config } = useChart()
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [item] = payload
|
||||
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
? config[label as keyof typeof config]?.label || label
|
||||
: itemConfig?.label
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn("font-medium", labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
payload,
|
||||
hideLabel,
|
||||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
])
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot"
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload
|
||||
.filter((item) => item.type !== "none")
|
||||
.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const indicatorColor = color || item.payload.fill || item.color
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
||||
indicator === "dot" && "items-center"
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn(
|
||||
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
||||
{
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
}
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--color-bg": indicatorColor,
|
||||
"--color-border": indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 justify-between leading-none",
|
||||
nestLabel ? "items-end" : "items-center"
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">
|
||||
{itemConfig?.label || item.name}
|
||||
</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
<span className="text-foreground font-mono font-medium tabular-nums">
|
||||
{item.value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend
|
||||
|
||||
function ChartLegendContent({
|
||||
className,
|
||||
hideIcon = false,
|
||||
payload,
|
||||
verticalAlign = "bottom",
|
||||
nameKey,
|
||||
}: React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean
|
||||
nameKey?: string
|
||||
}) {
|
||||
const { config } = useChart()
|
||||
|
||||
if (!payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-4",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{payload
|
||||
.filter((item) => item.type !== "none")
|
||||
.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(
|
||||
config: ChartConfig,
|
||||
payload: unknown,
|
||||
key: string
|
||||
) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
"payload" in payload &&
|
||||
typeof payload.payload === "object" &&
|
||||
payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined
|
||||
|
||||
let configLabelKey: string = key
|
||||
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key as keyof typeof payload] === "string"
|
||||
) {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||
) {
|
||||
configLabelKey = payloadPayload[
|
||||
key as keyof typeof payloadPayload
|
||||
] as string
|
||||
}
|
||||
|
||||
return configLabelKey in config
|
||||
? config[configLabelKey]
|
||||
: config[key as keyof typeof config]
|
||||
}
|
||||
|
||||
export {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
}
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { SearchIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/app/utils"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/views/components/ui/dialog"
|
||||
|
||||
function Command({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return (
|
||||
<CommandPrimitive
|
||||
data-slot="command"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandDialog({
|
||||
title = "Command Palette",
|
||||
description = "Search for a command to run...",
|
||||
children,
|
||||
className,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Dialog> & {
|
||||
title?: string
|
||||
description?: string
|
||||
className?: string
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogContent
|
||||
className={cn("overflow-hidden p-0", className)}
|
||||
showCloseButton={showCloseButton}
|
||||
>
|
||||
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command-input-wrapper"
|
||||
className="flex h-9 items-center gap-2 border-b px-3"
|
||||
>
|
||||
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
className={cn(
|
||||
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||
return (
|
||||
<CommandPrimitive.List
|
||||
data-slot="command-list"
|
||||
className={cn(
|
||||
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandEmpty({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||
return (
|
||||
<CommandPrimitive.Empty
|
||||
data-slot="command-empty"
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||
return (
|
||||
<CommandPrimitive.Group
|
||||
data-slot="command-group"
|
||||
className={cn(
|
||||
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||
return (
|
||||
<CommandPrimitive.Separator
|
||||
data-slot="command-separator"
|
||||
className={cn("bg-border -mx-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||
return (
|
||||
<CommandPrimitive.Item
|
||||
data-slot="command-item"
|
||||
className={cn(
|
||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/app/utils"
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Drawer as DrawerPrimitive } from "vaul"
|
||||
|
||||
import { cn } from "@/app/utils"
|
||||
|
||||
function Drawer({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
|
||||
}
|
||||
|
||||
function DrawerTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DrawerPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
|
||||
}
|
||||
|
||||
function DrawerClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
|
||||
}
|
||||
|
||||
function DrawerOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||
return (
|
||||
<DrawerPrimitive.Overlay
|
||||
data-slot="drawer-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||
return (
|
||||
<DrawerPortal data-slot="drawer-portal">
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
data-slot="drawer-content"
|
||||
className={cn(
|
||||
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
||||
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
|
||||
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
||||
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-header"
|
||||
className={cn(
|
||||
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||
return (
|
||||
<DrawerPrimitive.Title
|
||||
data-slot="drawer-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||
return (
|
||||
<DrawerPrimitive.Description
|
||||
data-slot="drawer-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerOverlay,
|
||||
DrawerTrigger,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/app/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
|
||||
import { cn } from "@/app/utils"
|
||||
|
||||
function Popover({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||
}
|
||||
|
||||
function PopoverTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||
}
|
||||
|
||||
function PopoverContent({
|
||||
className,
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
data-slot="popover-content"
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function PopoverAnchor({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||
}
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import * as ProgressPrimitive from '@radix-ui/react-progress';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/app/utils/index.ts';
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||
>(({ className, value, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative h-4 w-full overflow-hidden rounded-full bg-gray-100 dark:bg-gray-800',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-agprimary transition-all duration-700 dark:bg-gray-50"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
));
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName;
|
||||
|
||||
export { Progress };
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
import { cn } from '@/app/utils';
|
||||
import { Button } from '@/views/components/ui/button';
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@/views/components/ui/command';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/views/components/ui/popover';
|
||||
import { Check, ChevronsUpDown } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
type SelectPopoverProps = {
|
||||
label: string;
|
||||
items: { id: number; descricao: string }[];
|
||||
value: string;
|
||||
setValue: (value: string) => void;
|
||||
placeholder: string;
|
||||
disableUnselect?: boolean;
|
||||
};
|
||||
|
||||
export function SelectPopover({
|
||||
label,
|
||||
items,
|
||||
value,
|
||||
setValue,
|
||||
placeholder,
|
||||
disableUnselect,
|
||||
}: SelectPopoverProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const normalizeForSort = (str: string) =>
|
||||
str.replace(/^[\d.\-\s]+/, '').toLowerCase();
|
||||
|
||||
const sortedItems = [...items]
|
||||
.sort((a, b) =>
|
||||
normalizeForSort(a.descricao).localeCompare(
|
||||
normalizeForSort(b.descricao),
|
||||
'pt-BR',
|
||||
{ sensitivity: 'base' }
|
||||
)
|
||||
)
|
||||
.sort((a, b) => {
|
||||
const aVal = a.id.toString() + '-' + a.descricao;
|
||||
const bVal = b.id.toString() + '-' + b.descricao;
|
||||
|
||||
if (aVal === value) return -1;
|
||||
if (bVal === value) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full md:w-[200px] justify-between truncate capitalize"
|
||||
title={
|
||||
value
|
||||
? items
|
||||
.find(
|
||||
(item) =>
|
||||
item.id.toString() + '-' + item.descricao === value
|
||||
)
|
||||
?.descricao.replace(/[.\d-]/g, '')
|
||||
: placeholder
|
||||
}
|
||||
>
|
||||
<span className="truncate capitalize">
|
||||
{value
|
||||
? items
|
||||
.find(
|
||||
(item) =>
|
||||
item.id.toString() + '-' + item.descricao === value
|
||||
)
|
||||
?.descricao.replace(/[.\d-]/g, '')
|
||||
: placeholder}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder={`Buscar ${label.toLowerCase()}...`} />
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
Nenhum {label.toLowerCase()} encontrado.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{sortedItems.map((item) => {
|
||||
const itemValue = item.id.toString() + '-' + item.descricao;
|
||||
const displayText = item.descricao.replace(/[.\d-]/g, '');
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
key={itemValue}
|
||||
value={itemValue}
|
||||
onSelect={(currentValue) => {
|
||||
if (disableUnselect && currentValue === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
setValue(currentValue === value ? '' : currentValue);
|
||||
setOpen(false);
|
||||
}}
|
||||
title={displayText}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
'mr-2 h-4 w-4',
|
||||
value === itemValue ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
{displayText.toUpperCase()}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
import { cn } from '@/app/utils/index.ts';
|
||||
import * as SelectPrimitive from '@radix-ui/react-select';
|
||||
import { Check, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-9 w-full items-center justify-between border border-gray-200 bg-gray-50 shadow-sm hover:bg-gray-100 rounded-sm px-3 py-2 text-sm ring-offset-white placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-950 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 dark:border-gray-800 dark:bg-gray-950 dark:ring-offset-gray-950 dark:placeholder:text-gray-400 dark:focus:ring-gray-300',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50 ms-2" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
));
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
));
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = 'popper', ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border border-gray-200 bg-white text-gray-950 shadow-md 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 dark:border-gray-800 dark:bg-gray-950 dark:text-gray-50',
|
||||
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-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn('py-1.5 pl-8 pr-2 text-sm font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-gray-100 focus:text-gray-900 data-disabled:pointer-events-none data-disabled:opacity-50 dark:focus:bg-gray-800 dark:focus:text-gray-50',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 my-1 h-px bg-gray-100 dark:bg-gray-800', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
};
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
import * as SheetPrimitive from '@radix-ui/react-dialog';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { X } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/app/utils/index.ts';
|
||||
|
||||
const Sheet = SheetPrimitive.Root;
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger;
|
||||
|
||||
const SheetClose = SheetPrimitive.Close;
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal;
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||
|
||||
const sheetVariants = cva(
|
||||
'fixed z-50 gap-4 bg-white p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 dark:bg-gray-950',
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
||||
bottom:
|
||||
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
||||
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
|
||||
right:
|
||||
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: 'right',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = 'right', className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
{...props}>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-gray-950 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-gray-100 dark:ring-offset-gray-950 dark:focus:ring-gray-300 dark:data-[state=open]:bg-gray-800">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
));
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col space-y-2 text-center sm:text-left',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetHeader.displayName = 'SheetHeader';
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetFooter.displayName = 'SheetFooter';
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-lg font-semibold text-gray-950 dark:text-gray-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm text-gray-500 dark:text-gray-400', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetOverlay,
|
||||
SheetPortal,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
};
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/app/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-foreground text-background 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-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import tailwindcss from '@tailwindcss/vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
});
|
||||
Loading…
Reference in New Issue