Pull Lino

This commit is contained in:
jls846
2025-11-18 16:14:54 -06:00
13 changed files with 1324 additions and 504 deletions
-1
View File
@@ -1 +0,0 @@
NEXT_PUBLIC_API_URL=
+972 -370
View File
File diff suppressed because it is too large Load Diff
+8 -7
View File
@@ -10,23 +10,24 @@
},
"dependencies": {
"@ericblade/quagga2": "^1.8.4",
"@zxing/browser": "^0.1.5",
"@zxing/library": "^0.21.3",
"axios": "^1.13.1",
"axios": "^1.13.2",
"js-cookie": "^3.0.5",
"next": "15.5.6",
"react": "19.1.0",
"react-dom": "19.1.0",
"next": "16.0.2",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-hot-toast": "^2.6.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20",
"@types/node": "^24",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.5.6",
"sass": "^1.93.2",
"eslint-config-next": "16.0.2",
"sass": "^1.94.0",
"typescript": "^5"
}
}
@@ -0,0 +1,93 @@
"use client";
import { useState } from "react";
import Pregunta1 from "@/components/Equipo_Computo/Pregunta1";
import Pregunta1_1 from "@/components/Equipo_Computo/Pregunta1_1";
import Pregunta2_1 from "@/components/Equipo_Computo/Pregunta2_1";
import Pregunta3 from "@/components/Equipo_Computo/Pregunta3";
import Pregunta3_1 from "@/components/Equipo_Computo/Pregunta3_1";
import Pregunta3_2 from "@/components/Equipo_Computo/Pregunta3_2";
import Pregunta3_3 from "@/components/Equipo_Computo/Pregunta3_3";
import Pregunta3_4 from "@/components/Equipo_Computo/Pregunta3_4";
import Pregunta3_5 from "@/components/Equipo_Computo/Pregunta3_5";
import Pregunta7 from "@/components/Equipo_Computo/Pregunta7";
import Pregunta1EP from "@/components/Perifericos/Pregunta1EP";
import Pregunta2EP from "@/components/Perifericos/Pregunta2EP";
import Pregunta4EP from "@/components/Perifericos/Pregunta4EP";
import Pregunta7EP from "@/components/Perifericos/Pregunta7EP";
import Pregunta9 from "@/components/Equipo_Computo/Pregunta9";
import "../../styles/layout/reporte.scss";
type PreguntaKey =
| "pregunta1EP"
| "pregunta2EP"
| "pregunta4EP"
| "pregunta7"
const LABELS: Record<PreguntaKey, string> = {
pregunta1EP: "Pregunta 1 EP",
pregunta2EP: "Pregunta 2 EP",
pregunta4EP: "Pregunta 4 EP",
pregunta7: "Pregunta 7",
};
// Mapeo de fondo por pregunta (puedes personalizarlo)
const TAB_BACKGROUNDS: Record<PreguntaKey, string> = {
pregunta1EP: "bg-gray",
pregunta2EP: "bg-gray",
pregunta4EP: "bg-gray",
pregunta7: "bg-gray",
};
export default function Page() {
const [activeTab, setActiveTab] = useState<PreguntaKey>("pregunta1EP");
const renderPregunta = () => {
switch (activeTab) {
case "pregunta1EP":
return <Pregunta1EP />;
case "pregunta2EP":
return <Pregunta2EP />;
case "pregunta4EP":
return <Pregunta4EP />;
case "pregunta7":
return <Pregunta7 />;
default:
return (
<div className="p-6 text-center text-gray-500">
Contenido no disponible aún.
</div>
);
}
};
const currentBgClass = TAB_BACKGROUNDS[activeTab] || "bg-gray";
return (
<div className="scanView_reporte">
<div className="container_reporte">
<div className="main-content_reporte">
<div className="tabs_reporte">
{Object.entries(LABELS).map(([key, label]) => (
<button
key={key}
className={`tab_reporte ${activeTab === key ? "active" : ""}`}
onClick={() => setActiveTab(key as PreguntaKey)}
aria-selected={activeTab === key}
role="tab"
>
{label}
</button>
))}
</div>
<div className={`data-table_reporte ${currentBgClass}`}>
{renderPregunta()}
</div>
</div>
</div>
</div>
);
}
+5 -9
View File
@@ -3,7 +3,6 @@
import { useState } from "react";
import Pregunta1 from "@/components/Equipo_Computo/Pregunta1";
import Pregunta1_1 from "@/components/Equipo_Computo/Pregunta1_1";
import Pregunta2 from "@/components/Equipo_Computo/Pregunta2";
import Pregunta2_1 from "@/components/Equipo_Computo/Pregunta2_1";
import Pregunta3 from "@/components/Equipo_Computo/Pregunta3";
import Pregunta3_1 from "@/components/Equipo_Computo/Pregunta3_1";
@@ -16,7 +15,6 @@ import Pregunta9 from "@/components/Equipo_Computo/Pregunta9";
import "../../styles/layout/reporte.scss";
type PreguntaKey =
| "pregunta1_1"
| "pregunta1"
| "pregunta2_1"
| "pregunta3"
@@ -29,8 +27,7 @@ type PreguntaKey =
| "pregunta9";
const LABELS: Record<PreguntaKey, string> = {
pregunta1_1: "Pregunta 1 ",
pregunta1: "Pregunta 2",
pregunta1: "Pregunta 1 ",
pregunta2_1: "Pregunta 2.1",
pregunta3: "Pregunta 3",
pregunta3_1: "Pregunta 3.1",
@@ -44,8 +41,7 @@ const LABELS: Record<PreguntaKey, string> = {
// Mapeo de fondo por pregunta (puedes personalizarlo)
const TAB_BACKGROUNDS: Record<PreguntaKey, string> = {
pregunta1: "bg-blue",
pregunta1_1: "bg-gray",
pregunta1: "bg-gray",
pregunta2_1: "bg-gray",
pregunta3: "bg-gray",
pregunta3_1: "bg-gray",
@@ -57,13 +53,13 @@ const TAB_BACKGROUNDS: Record<PreguntaKey, string> = {
pregunta9: "bg-blue",
};
export default function Reporte() {
export default function Page() {
const [activeTab, setActiveTab] = useState<PreguntaKey>("pregunta1");
const renderPregunta = () => {
switch (activeTab) {
case "pregunta1_1":
return <Pregunta1_1 />;
case "pregunta1":
return <Pregunta1 />;
case "pregunta1":
return <Pregunta1 />;
case "pregunta2_1":
+4 -1
View File
@@ -67,7 +67,10 @@ export default function Page() {
<h3>Escanear Inventario</h3>
{isScanning ? (
<BarcodeScanner onScan={handleScan} />
<BarcodeScanner
key="scanner"
onScan={handleScan}
/>
) : (
<div className="scannerFrame">
<Image
+9 -2
View File
@@ -11,12 +11,15 @@ import toast from "react-hot-toast";
export default function Login() {
const [nombre, setNombre] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter();
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
try {
const response = await axios.post(
`${process.env.NEXT_PUBLIC_API_URL}/auth/login`,
@@ -43,13 +46,15 @@ export default function Login() {
} else {
toast.error("Ocurrió un error inesperado");
}
} finally {
setLoading(false);
}
};
return (
<section className="containerGrid">
<div className="login-container">
<h2>Inicio de sesión</h2>
<h2>Sistema de censo</h2>
<form onSubmit={handleSubmit}>
<div>
<label>Usuario</label>
@@ -71,7 +76,9 @@ export default function Login() {
required
/>
</div>
<button type="submit">Iniciar sesión</button>
<button type="submit" disabled={loading}>
{loading ? "Buscando..." : "Iniciar sesión"}
</button>
{/* <Link href={"/forgotPassword"}>Olvidaste Contraseña?</Link> */}
{/* <Link href={"/crearCuenta"}>Crear Cuenta</Link> */}
+1 -1
View File
@@ -1,4 +1,4 @@
@import "variables";
@use "./variables" as *;
html {
font-size: 62.5%;
+31 -14
View File
@@ -6,7 +6,6 @@ import axios from "axios";
import Cookies from "js-cookie";
import toast from "react-hot-toast";
import { AREAS } from "@/data/areas";
import { SO_POR_EQUIPO } from "@/data/so_por_equipo";
import { PROCESADORES_POR_EQUIPO } from "@/data/procesadores";
import { useRouter } from "next/navigation";
@@ -133,7 +132,6 @@ export default function Page() {
setAdscripciones(adscripcionesRes.data);
setTiposEquipo(tiposEquipoRes.data);
setSistemasOperativos(sistemasOperativosRes.data);
console.log(adscripciones);
} catch (err) {
if (axios.isAxiosError(err)) {
if (err.response) {
@@ -200,9 +198,15 @@ export default function Page() {
const textValue = value as string;
setAdscripcionLabel(textValue);
const matches = AREAS.filter((a) =>
a.label.toLowerCase().includes(textValue.toLowerCase())
).map((a) => a.label);
const normalize = (str: string) =>
str
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase();
const matches = adscripciones
.filter((a) => normalize(a.adscripcion).includes(normalize(textValue)))
.map((a) => a.adscripcion);
setSuggestions((prev) => ({
...prev,
@@ -234,7 +238,6 @@ export default function Page() {
};
const handleGuardar = async () => {
if (!formData.inventario) {
toast.error("Inventario no encontrado");
return;
@@ -272,7 +275,11 @@ export default function Page() {
}
}
if(formData.id_tipo_equipo != 8 && formData.id_tipo_equipo != 7 && formData.id_tipo_equipo != 9){
if (
formData.id_tipo_equipo != 8 &&
formData.id_tipo_equipo != 7 &&
formData.id_tipo_equipo != 9
) {
if (!formData.id_sistema_operativo) {
toast.error("Sistema operativo no encontrado ");
return;
@@ -422,13 +429,17 @@ export default function Page() {
<div className="formGroup">
<label>Procesador</label>
<select
disabled={formData.id_tipo_equipo ==0}
disabled={formData.id_tipo_equipo == 0}
value={formData.id_procesador}
onChange={(e) =>
handleInputChange("id_procesador", e.target.value)
}
>
<option value="">{formData.id_tipo_equipo ? "Selecciona procesador":"Selecciona primero el tipo de equipo"}</option>
<option value="">
{formData.id_tipo_equipo
? "Selecciona procesador"
: "Selecciona primero el tipo de equipo"}
</option>
{PROCESADORES_POR_EQUIPO[formData.id_tipo_equipo]?.map(
(p) => (
<option key={p.id_procesador} value={p.id_procesador}>
@@ -442,7 +453,7 @@ export default function Page() {
<div className="formGroup">
<label>Sistema operativo</label>
<select
disabled={formData.id_tipo_equipo ==0}
disabled={formData.id_tipo_equipo == 0}
value={formData.id_sistema_operativo}
onChange={(e) =>
handleInputChange(
@@ -451,7 +462,11 @@ export default function Page() {
)
}
>
<option value="">{formData.id_tipo_equipo ? "Selecciona sistema operativo":"Selecciona primero el tipo de equipo"}</option>
<option value="">
{formData.id_tipo_equipo
? "Selecciona sistema operativo"
: "Selecciona primero el tipo de equipo"}
</option>
{SO_POR_EQUIPO[formData.id_tipo_equipo]?.map((so) => (
<option
key={so.id_sistema_operativo}
@@ -502,13 +517,15 @@ export default function Page() {
<li
key={s}
onClick={() => {
const selected = AREAS.find((a) => a.label === s);
const selected = adscripciones.find(
(a) => a.adscripcion === s
);
if (selected) {
setFormData((prev) => ({
...prev,
id_adscripcion: selected.id,
id_adscripcion: selected.id_adscripcion,
}));
setAdscripcionLabel(selected.label);
setAdscripcionLabel(selected.adscripcion);
setSuggestions((prev) => ({
...prev,
adscripcion: [],
+138 -66
View File
@@ -1,99 +1,171 @@
"use client";
import React, { useEffect, useRef } from "react";
import Quagga from "@ericblade/quagga2";
// Tipos para la configuración (opcional, pero mejora legibilidad)
interface QuaggaConfig {
inputStream: {
name: string;
type: string;
target: HTMLElement;
constraints: MediaTrackConstraints;
};
decoder: {
readers: string[];
};
locate: boolean;
}
import { useEffect, useRef } from "react";
import toast from "react-hot-toast";
import { BrowserMultiFormatReader } from "@zxing/browser";
import { NotFoundException } from "@zxing/library";
interface BarcodeScannerProps {
onScan: (code: string) => void;
}
export default function BarcodeScanner({ onScan }: BarcodeScannerProps) {
const scannerRef = useRef<HTMLDivElement>(null);
const videoRef = useRef<HTMLVideoElement>(null);
const lastScan = useRef<string | null>(null);
useEffect(() => {
if (!scannerRef.current) return;
let stream: MediaStream | null = null;
let animationFrame: number;
let zxingReader: BrowserMultiFormatReader | null = null;
let detector: any = null;
const config: QuaggaConfig = {
inputStream: {
name: "Live",
type: "LiveStream",
target: scannerRef.current,
constraints: {
width: 300,
height: 500,
facingMode: "environment",
},
},
decoder: {
readers: ["code_128_reader"],
},
locate: true,
};
// El callback de error: tipamos `err` como `unknown`
const initCallback = (err: unknown) => {
if (err) {
console.error("Error al iniciar el escáner:", err);
return;
/** 🚀 Intentar usar BarcodeDetector API (moderno) */
const startBarcodeDetector = async () => {
const supported = "BarcodeDetector" in window;
if (!supported) {
console.warn("BarcodeDetector no soportado, usando ZXing...");
return startZXing();
}
try {
stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: { ideal: "environment" } }, // 👈 fuerza cámara trasera
});
if (!videoRef.current) return;
videoRef.current.srcObject = stream;
await new Promise<void>((resolve) => {
videoRef.current!.onloadedmetadata = async () => {
try {
await videoRef.current!.play();
resolve();
} catch {
resolve();
}
};
});
detector = new (window as any).BarcodeDetector({
formats: ["code_128", "ean_13", "qr_code"],
});
const detect = async () => {
if (!videoRef.current) return;
if (videoRef.current.readyState < 2) {
animationFrame = requestAnimationFrame(detect);
return;
}
try {
const barcodes = await detector.detect(videoRef.current);
if (barcodes.length > 0) {
const raw = barcodes[0].rawValue.trim();
if (raw && raw !== lastScan.current) {
lastScan.current = raw;
const processed = raw.startsWith("0") ? raw.slice(1) : raw;
onScan(processed);
}
}
} catch (err: any) {
if (
err.message?.includes("Invalid element") ||
err.name === "InvalidStateError"
) {
// ignorar mientras no haya frame válido
} else {
console.error("Error detectando código:", err);
}
}
animationFrame = requestAnimationFrame(detect);
};
detect();
} catch (err) {
console.error("Error con BarcodeDetector:", err);
toast.error("No se pudo acceder a la cámara");
startZXing();
}
Quagga.start();
};
Quagga.init(config as unknown as Record<string, unknown>, initCallback);
const startZXing = async () => {
try {
zxingReader = new BrowserMultiFormatReader();
const devices = await BrowserMultiFormatReader.listVideoInputDevices();
// Handler de detección: `data` es `unknown`, lo validamos
const handleDetected = (data: unknown) => {
// Validación segura
if (
data != null &&
typeof data === "object" &&
"codeResult" in data &&
data.codeResult != null &&
typeof data.codeResult === "object" &&
"code" in data.codeResult &&
typeof data.codeResult.code === "string"
) {
const code = data.codeResult.code.trim();
if (code !== "") {
onScan(code);
Quagga.stop(); // Opcional
if (devices.length === 0) {
toast.error("No se detectó cámara disponible");
return;
}
// 🔍 Intentar encontrar la cámara trasera
const backCamera =
devices.find((d) =>
d.label.toLowerCase().includes("back")
) || devices.find((d) =>
d.label.toLowerCase().includes("rear")
) || devices[devices.length - 1]; // fallback
const selectedDeviceId = backCamera.deviceId;
console.log("Usando cámara:", backCamera.label || selectedDeviceId);
await zxingReader.decodeFromVideoDevice(
selectedDeviceId,
videoRef.current!,
(result, err) => {
if (result) {
const code = result.getText().trim();
if (code && code !== lastScan.current) {
lastScan.current = code;
const processed = code.startsWith("0") ? code.slice(1) : code;
onScan(processed);
}
} else if (err && !(err instanceof NotFoundException)) {
console.error("Error ZXing:", err);
}
}
);
} catch (error) {
console.error("Error al iniciar ZXing:", error);
toast.error("Error al iniciar escáner");
}
};
Quagga.onDetected(handleDetected);
startBarcodeDetector();
return () => {
Quagga.offDetected(handleDetected);
Quagga.stop();
if (animationFrame) cancelAnimationFrame(animationFrame);
if (stream) stream.getTracks().forEach((t) => t.stop());
if (zxingReader) {
if (typeof (zxingReader as any).stopContinuousDecode === "function") {
(zxingReader as any).stopContinuousDecode();
} else if (typeof (zxingReader as any).reset === "function") {
(zxingReader as any).reset();
}
}
};
}, [onScan]);
return (
<div
ref={scannerRef}
<video
ref={videoRef}
style={{
width: "100%",
maxWidth: "500px",
maxWidth: "480px",
height: "300px",
borderRadius: "12px",
overflow: "hidden",
border: "2px solid #0056b3",
borderRadius: "12px",
objectFit: "cover",
}}
muted
playsInline
/>
);
}
//Chatgpt
//Gemini
+43 -27
View File
@@ -7,7 +7,6 @@ import axios from "axios";
import Cookies from "js-cookie";
import toast from "react-hot-toast";
import { SO_POR_EQUIPO } from "@/data/so_por_equipo";
import { AREAS } from "@/data/areas";
import { PROCESADORES_POR_EQUIPO } from "@/data/procesadores";
import "../app/styles/layout/agregarEquipo.scss";
@@ -34,7 +33,7 @@ export default function Editar() {
observaciones: "",
responsable: "",
id_periferico: 0,
fechaFactura: "",
fechaMovimiento: "",
});
interface TipoUso {
@@ -159,7 +158,7 @@ export default function Editar() {
inventario: string;
serie: string;
lugar: string;
fechaFactura: string;
fechaMovimiento: string;
antiguedad: string;
modelo: string;
estado: { id_estado: number; estado: string };
@@ -213,20 +212,20 @@ export default function Editar() {
setFormData({
inventario: equipo.inventario || "",
serie: equipo.serie || "",
serie: (equipo.serie || "").toUpperCase(),
id_marca: equipo.marca?.id_marca || 0,
modelo: equipo.modelo || "",
id_tipo_equipo: equipo.tipoEquipo?.id_tipo_de_equipo || 0,
id_estado: equipo.estado?.id_estado || 0,
id_sistema_operativo:
equipo.sistemaOperativo?.id_sistema_operativo || 0,
id_procesador:equipo.procesador?.id_procesador || 0,
id_procesador: equipo.procesador?.id_procesador || 0,
id_uso: equipo.tipoUso?.id_uso || 0,
observaciones: equipo.observaciones || "",
id_adscripcion: equipo.adscripcion?.id_adscripcion || 0,
lugar: equipo.lugar || "",
id_periferico: equipo.periferico?.id_periferico || 0,
fechaFactura: equipo.fechaFactura || "",
fechaMovimiento: equipo.fechaMovimiento || "",
responsable,
});
@@ -280,8 +279,6 @@ export default function Editar() {
}, [formData.id_tipo_equipo]);
const handleGuardar = async () => {
if (!formData.inventario) {
toast.error("Inventario no encontrado");
return;
@@ -319,7 +316,11 @@ export default function Editar() {
}
}
if (formData.id_tipo_equipo != 8 && formData.id_tipo_equipo != 7 && formData.id_tipo_equipo != 9) {
if (
formData.id_tipo_equipo != 8 &&
formData.id_tipo_equipo != 7 &&
formData.id_tipo_equipo != 9
) {
if (!formData.id_sistema_operativo) {
toast.error("Sistema operativo no encontrado ");
return;
@@ -350,14 +351,11 @@ export default function Editar() {
};
try {
await axios.patch(
`${api_url}/equipos/update/${id}`,
data,
{
headers,
}
);
await axios.patch(`${api_url}/equipos/update/${id}`, data, {
headers,
});
toast.success("Equipo actualizado correctamente");
router.push("/escaner");
} catch (error) {
console.error("Error al guardar:", error);
toast.error("Hubo un error al guardar el equipo.");
@@ -372,9 +370,15 @@ export default function Editar() {
const textValue = value as string;
setAdscripcionLabel(textValue);
const matches = AREAS.filter((a) =>
a.label.toLowerCase().includes(textValue.toLowerCase())
).map((a) => a.label);
const normalize = (str: string) =>
str
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase();
const matches = adscripciones
.filter((a) => normalize(a.adscripcion).includes(normalize(textValue)))
.map((a) => a.adscripcion);
setSuggestions((prev) => ({
...prev,
@@ -425,8 +429,9 @@ export default function Editar() {
const first = data[0];
setFormData((prev) => ({
...prev,
responsable: `${first.nombre ?? ""} ${first.apellidos ?? ""
}`.trim(),
responsable: `${first.nombre ?? ""} ${
first.apellidos ?? ""
}`.trim(),
}));
} else {
toast.error(
@@ -460,7 +465,9 @@ export default function Editar() {
<span>Inventario: {formData.inventario}</span>
<span>
Fecha de censo:{" "}
{new Date(formData.fechaFactura).toLocaleDateString("es-MX")}
{formData.fechaMovimiento
? new Date(formData.fechaMovimiento).toLocaleDateString("es-MX")
: "Sin fecha"}
</span>
</h2>
@@ -474,7 +481,10 @@ export default function Editar() {
value={formData.serie}
placeholder="Ingresa serie"
onChange={(e) =>
setFormData({ ...formData, serie: e.target.value })
setFormData({
...formData,
serie: e.target.value.toUpperCase(),
})
}
/>
</div>
@@ -578,7 +588,11 @@ export default function Editar() {
handleInputChange("id_procesador", e.target.value)
}
>
<option value="">{formData.id_tipo_equipo ? "Selecciona procesador" : "Selecciona primero el tipo de equipo"}</option>
<option value="">
{formData.id_tipo_equipo
? "Selecciona procesador"
: "Selecciona primero el tipo de equipo"}
</option>
{PROCESADORES_POR_EQUIPO[formData.id_tipo_equipo]?.map(
(p) => (
<option key={p.id_procesador} value={p.id_procesador}>
@@ -648,13 +662,15 @@ export default function Editar() {
<li
key={s}
onClick={() => {
const selected = AREAS.find((a) => a.label === s);
const selected = adscripciones.find(
(a) => a.adscripcion === s
);
if (selected) {
setFormData((prev) => ({
...prev,
id_adscripcion: selected.id,
id_adscripcion: selected.id_adscripcion,
}));
setAdscripcionLabel(selected.label);
setAdscripcionLabel(selected.adscripcion);
setSuggestions((prev) => ({
...prev,
adscripcion: [],
+1 -1
View File
@@ -13,6 +13,6 @@ export function middleware(request: NextRequest) {
}
export const config = {
matcher: [],
matcher: ["/escaner","/agregarEquipo","/editar"],
};
//IO
+19 -5
View File
@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -11,7 +15,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
@@ -19,9 +23,19 @@
}
],
"paths": {
"@/*": ["./src/*"]
"@/*": [
"./src/*"
]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}