feat(explode): first commit

main
luiz.felipe 2025-12-22 16:16:59 -03:00
commit e3fbdb94f4
85 changed files with 9137 additions and 0 deletions

12
Dockerfile Normal file
View File

@ -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" ]

72
Jenkinsfile vendored Normal file
View File

@ -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()
}
}
}

75
README.md Normal file
View File

@ -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...
},
},
])
```

22
components.json Normal file
View File

@ -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": {}
}

23
eslint.config.js Normal file
View File

@ -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,
},
},
])

13
index.html Normal file
View File

@ -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>

13
k8s/BETA/certificate.yaml Normal file
View File

@ -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

View File

@ -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

51
k8s/BETA/deployment.yaml Normal file
View File

@ -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

19
k8s/BETA/hpa.yaml Normal file
View File

@ -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

23
k8s/BETA/ingress.yaml Normal file
View File

@ -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

18
k8s/BETA/service.yaml Normal file
View File

@ -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

View File

@ -0,0 +1,5 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: agapesistemas-PROJETO-cm
namespace: agapesistemas-ns-AMBIENTE

View File

@ -0,0 +1,5 @@
apiVersion: v1
kind: Secret
metadata:
name: agapesistemas-PROJETO-sc
namespace: agapesistemas-ns-AMBIENTE

13
k8s/HML/certificate.yaml Normal file
View File

@ -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

View File

@ -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

48
k8s/HML/deployment.yaml Normal file
View File

@ -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

19
k8s/HML/hpa.yaml Normal file
View File

@ -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

23
k8s/HML/ingress.yaml Normal file
View File

@ -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

18
k8s/HML/service.yaml Normal file
View File

@ -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

View File

@ -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'

View File

@ -0,0 +1,7 @@
apiVersion: v1
kind: Secret
metadata:
name: agapesistemas-PROJETO-sc
namespace: agapesistemas-ns-AMBIENTE

13
k8s/PRD/certificate.yaml Normal file
View File

@ -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

View File

@ -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

View File

@ -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

48
k8s/PRD/deployment.yaml Normal file
View File

@ -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

19
k8s/PRD/hpa-high.yaml Normal file
View File

@ -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

19
k8s/PRD/hpa.yaml Normal file
View File

@ -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

23
k8s/PRD/ingress.yaml Normal file
View File

@ -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

18
k8s/PRD/service.yaml Normal file
View File

@ -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

View File

@ -0,0 +1,5 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: agapesistemas-PROJETO-cm
namespace: agapesistemas-ns-AMBIENTE

View File

@ -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'

View File

@ -0,0 +1,5 @@
apiVersion: v1
kind: Secret
metadata:
name: agapesistemas-PROJETO-sc
namespace: agapesistemas-ns-AMBIENTE

View File

@ -0,0 +1,5 @@
apiVersion: v1
kind: Secret
metadata:
name: agapesistemas-PROJETO-sc
namespace: agapesistemas-ns-AMBIENTE

5316
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

51
package.json Normal file
View File

@ -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"
}
}

1
public/vite.svg Normal file
View File

@ -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

132
src/App.tsx Normal file
View File

@ -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>
);
}

28
src/app/models/index.ts Normal file
View File

@ -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;
}

68
src/app/services/index.ts Normal file
View File

@ -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,
},
},
});

6
src/app/utils/index.ts Normal file
View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

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

16
src/main.tsx Normal file
View File

@ -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>,
);

288
src/styles/index.css Normal file
View File

@ -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);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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">
&copy; {new Date().getFullYear()} Todos os direitos reservados.
</p>
<p className="text-sm md:text-base">
Contato: suporte@agapesistemas.com.br
</p>
</footer>
</div>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
);
}

View File

@ -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 };

View File

@ -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 }

View File

@ -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,
};

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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 }

View File

@ -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 }

View File

@ -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 };

View File

@ -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>
);
}

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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 }

33
tsconfig.app.json Normal file
View File

@ -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"]
}

13
tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

26
tsconfig.node.json Normal file
View File

@ -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"]
}

14
vite.config.ts Normal file
View File

@ -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'),
},
},
});