Refactor components by removing unused imports and optimizing state management; add configuration for Azure Static Web Apps
This commit is contained in:
@@ -2,7 +2,6 @@ import { useQuery } from "@tanstack/react-query"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { supabase } from "@/auth/supabase"
|
||||
import ReactMarkdown from "react-markdown"
|
||||
import { useSupabaseAuth } from "@/auth/supabase"
|
||||
|
||||
export function HistorialCambiosModal({
|
||||
|
||||
@@ -6,15 +6,12 @@ import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { AuroraButton } from "@/components/effect/aurora-button"
|
||||
import confetti from "canvas-confetti"
|
||||
import { useQueryClient } from "@tanstack/react-query"
|
||||
import { supabase, useSupabaseAuth } from "@/auth/supabase"
|
||||
import { Field } from "./Field"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"
|
||||
import { asignaturaKeys } from "./planQueries"
|
||||
import { useRouter } from "@tanstack/react-router"
|
||||
|
||||
export function AddAsignaturaButton({ planId, onAdded }: { planId: string; onAdded?: () => void }) {
|
||||
const qc = useQueryClient()
|
||||
const router = useRouter()
|
||||
const supabaseAuth = useSupabaseAuth()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
@@ -33,7 +33,6 @@ export function DownloadPlanPDF({ plan }: { plan: PlanLike }) {
|
||||
const sectionGap = 10 // Espacio entre recuadros de sección
|
||||
const bodyFontSize = 10.5
|
||||
const headingFontSize = 12
|
||||
const subHeadingFontSize = 10
|
||||
const bulletGlifo = "\u21D2" // Flecha doble (⇒) para el glifo
|
||||
const bulletIndent = 6 // Sangría para el texto de la lista
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ const rgba = (rgb: [number, number, number], a: number) => `rgba(${rgb[0]},${rgb
|
||||
/* =====================================================
|
||||
Expandable text
|
||||
===================================================== */
|
||||
function ExpandableText({ text, mono = false }: { text?: string | string[] | null; mono?: boolean }) {
|
||||
function ExpandableText({ text }: { text?: string | string[] | null; mono?: boolean }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
if (!text || (Array.isArray(text) && text.length === 0)) {
|
||||
return <span className="text-neutral-400">—</span>
|
||||
@@ -127,16 +127,6 @@ export function AcademicSections({ planId, color }: { planId: string; color?: st
|
||||
const [editing, setEditing] = useState<null | { key: keyof PlanTextFields; title: string }>(null)
|
||||
const [draft, setDraft] = useState("")
|
||||
|
||||
const plan_format={
|
||||
"objetivo_general": "...",
|
||||
"sistema_evaluacion": "...",
|
||||
"perfil_ingreso": "...",
|
||||
"perfil_egreso": "...",
|
||||
"competencias_genericas": "...",
|
||||
"competencias_especificas": "...",
|
||||
"indicadores_desempeno": "...",
|
||||
"pertinencia": "..."
|
||||
}
|
||||
|
||||
// --- mutation con actualización optimista ---
|
||||
const updateField = useMutation({
|
||||
@@ -313,12 +303,12 @@ export function AcademicSections({ planId, color }: { planId: string; color?: st
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<HistorialCambiosModal
|
||||
<HistorialCambiosModal
|
||||
open={openHistorial}
|
||||
onClose={() => setOpenHistorial(false)}
|
||||
planId={planId}
|
||||
onRestore={async (key, value) => {
|
||||
updateField.mutate({ key, value })
|
||||
updateField.mutate({ key: key as keyof PlanTextFields, value })
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { supabase } from "@/auth/supabase";
|
||||
|
||||
/**
|
||||
@@ -11,8 +11,6 @@ export function useSupabaseUpdateWithHistory<T extends Record<string, any>>(
|
||||
tableName: string,
|
||||
idKey: keyof T = "id" as keyof T
|
||||
) {
|
||||
const qc = useQueryClient();
|
||||
|
||||
// Generar diferencias tipo JSON Patch
|
||||
function generateDiff(oldData: T, newData: Partial<T>) {
|
||||
const changes: any[] = [];
|
||||
|
||||
@@ -149,8 +149,6 @@ function Layout() {
|
||||
|
||||
function Sidebar({ onNavigate }: { onNavigate?: () => void }) {
|
||||
const { claims } = useSupabaseAuth()
|
||||
const isAdmin = claims?.role === 'lci' || claims?.role === 'vicerrectoria'
|
||||
|
||||
const canSeeCarreras = ["jefe_carrera", 'vicerrectoria', 'secretario_academico', 'lci'].includes(claims?.role ?? '')
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createFileRoute, Link, useRouter } from '@tanstack/react-router'
|
||||
import { useSuspenseQuery, queryOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { supabase } from '@/auth/supabase'
|
||||
import * as Icons from 'lucide-react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select'
|
||||
@@ -168,25 +168,25 @@ function RouteComponent() {
|
||||
const [q, setQ] = useState(search.q ?? '')
|
||||
const [sem, setSem] = useState<string>('todos')
|
||||
const [tipo, setTipo] = useState<string>('todos')
|
||||
const [groupBy, setGroupBy] = useState<'semestre' | 'ninguno'>('semestre')
|
||||
const [groupBy] = useState<'semestre' | 'ninguno'>('semestre')
|
||||
const [flag, setFlag] = useState<'' | 'sinBibliografia' | 'sinCriterios' | 'sinContenidos'>(search.f ?? '')
|
||||
|
||||
const [facultad, setFacultad] = useState("todas")
|
||||
const [carrera, setCarrera] = useState("todas")
|
||||
const [facultad, setFacultad] = useState("todas")
|
||||
const [carrera, setCarrera] = useState("todas")
|
||||
|
||||
/* useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
router.navigate({
|
||||
to: '/asignaturas',
|
||||
search: { ...search, q },
|
||||
replace: true,
|
||||
})
|
||||
}, 400)
|
||||
return () => clearTimeout(timeout)
|
||||
}, [q]) */
|
||||
/* useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
router.navigate({
|
||||
to: '/asignaturas',
|
||||
search: { ...search, q },
|
||||
replace: true,
|
||||
})
|
||||
}, 400)
|
||||
return () => clearTimeout(timeout)
|
||||
}, [q]) */
|
||||
|
||||
|
||||
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const value = e.target.value
|
||||
setQ(value)
|
||||
router.navigate({
|
||||
@@ -199,30 +199,30 @@ function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
})
|
||||
}
|
||||
|
||||
// 🟣 Lista única de facultades
|
||||
const facultadesList = useMemo(() => {
|
||||
const unique = new Map<string, string>()
|
||||
planes?.forEach((p) => {
|
||||
const fac = p.carrera?.facultad
|
||||
if (fac?.id && fac?.nombre) unique.set(fac.id, fac.nombre)
|
||||
})
|
||||
return Array.from(unique.entries())
|
||||
}, [planes])
|
||||
// 🟣 Lista única de facultades
|
||||
const facultadesList = useMemo(() => {
|
||||
const unique = new Map<string, string>()
|
||||
planes?.forEach((p) => {
|
||||
const fac = p.carrera?.facultad
|
||||
if (fac?.id && fac?.nombre) unique.set(fac.id, fac.nombre)
|
||||
})
|
||||
return Array.from(unique.entries())
|
||||
}, [planes])
|
||||
|
||||
// 🎓 Lista de carreras según la facultad seleccionada
|
||||
const carrerasList = useMemo(() => {
|
||||
const unique = new Map<string, string>()
|
||||
planes?.forEach((p) => {
|
||||
if (
|
||||
p.carrera?.id &&
|
||||
p.carrera?.nombre &&
|
||||
(!facultad || facultad === "todas" || p.carrera?.facultad?.id === facultad)
|
||||
) {
|
||||
unique.set(p.carrera.id, p.carrera.nombre)
|
||||
}
|
||||
})
|
||||
return Array.from(unique.entries())
|
||||
}, [planes, facultad])
|
||||
// 🎓 Lista de carreras según la facultad seleccionada
|
||||
const carrerasList = useMemo(() => {
|
||||
const unique = new Map<string, string>()
|
||||
planes?.forEach((p) => {
|
||||
if (
|
||||
p.carrera?.id &&
|
||||
p.carrera?.nombre &&
|
||||
(!facultad || facultad === "todas" || p.carrera?.facultad?.id === facultad)
|
||||
) {
|
||||
unique.set(p.carrera.id, p.carrera.nombre)
|
||||
}
|
||||
})
|
||||
return Array.from(unique.entries())
|
||||
}, [planes, facultad])
|
||||
|
||||
|
||||
// NEW: Clonado individual
|
||||
@@ -256,12 +256,6 @@ const carrerasList = useMemo(() => {
|
||||
return Array.from(s).sort((a, b) => (a === '—' ? 1 : 0) - (b === '—' ? 1 : 0) || Number(a) - Number(b))
|
||||
}, [asignaturas])
|
||||
|
||||
const tipos = useMemo(() => {
|
||||
const s = new Set<string>()
|
||||
asignaturas.forEach(a => s.add(a.tipo ?? '—'))
|
||||
return Array.from(s).sort()
|
||||
}, [asignaturas])
|
||||
|
||||
// Salud
|
||||
const salud = useMemo(() => {
|
||||
let sinBibliografia = 0, sinCriterios = 0, sinContenidos = 0
|
||||
@@ -274,29 +268,29 @@ const carrerasList = useMemo(() => {
|
||||
}, [asignaturas])
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const t = q.trim().toLowerCase()
|
||||
return asignaturas.filter(a => {
|
||||
const matchesQ =
|
||||
!t ||
|
||||
[a.nombre, a.clave, a.tipo, a.objetivos, a.plan?.nombre, a.plan?.carrera?.nombre, a.plan?.carrera?.facultad?.nombre]
|
||||
.filter(Boolean)
|
||||
.some(v => String(v).toLowerCase().includes(t))
|
||||
const t = q.trim().toLowerCase()
|
||||
return asignaturas.filter(a => {
|
||||
const matchesQ =
|
||||
!t ||
|
||||
[a.nombre, a.clave, a.tipo, a.objetivos, a.plan?.nombre, a.plan?.carrera?.nombre, a.plan?.carrera?.facultad?.nombre]
|
||||
.filter(Boolean)
|
||||
.some(v => String(v).toLowerCase().includes(t))
|
||||
|
||||
const semOK = sem === 'todos' || String(a.semestre ?? '—') === sem
|
||||
const tipoOK = tipo === 'todos' || (a.tipo ?? '—') === tipo
|
||||
const carreraOK = carrera === 'todas' || a.plan?.carrera?.id === carrera
|
||||
const facultadOK = facultad === 'todas' || a.plan?.carrera?.facultad?.id === facultad
|
||||
const planOK = !search.planId || a.plan?.id === search.planId
|
||||
const semOK = sem === 'todos' || String(a.semestre ?? '—') === sem
|
||||
const tipoOK = tipo === 'todos' || (a.tipo ?? '—') === tipo
|
||||
const carreraOK = carrera === 'todas' || a.plan?.carrera?.id === carrera
|
||||
const facultadOK = facultad === 'todas' || a.plan?.carrera?.facultad?.id === facultad
|
||||
const planOK = !search.planId || a.plan?.id === search.planId
|
||||
|
||||
const flagOK =
|
||||
!flag ||
|
||||
(flag === 'sinBibliografia' && (!a.bibliografia || a.bibliografia.length === 0)) ||
|
||||
(flag === 'sinCriterios' && (!a.criterios_evaluacion || !a.criterios_evaluacion.trim())) ||
|
||||
(flag === 'sinContenidos' && (!a.contenidos || Object.keys(a.contenidos ?? {}).length === 0))
|
||||
const flagOK =
|
||||
!flag ||
|
||||
(flag === 'sinBibliografia' && (!a.bibliografia || a.bibliografia.length === 0)) ||
|
||||
(flag === 'sinCriterios' && (!a.criterios_evaluacion || !a.criterios_evaluacion.trim())) ||
|
||||
(flag === 'sinContenidos' && (!a.contenidos || Object.keys(a.contenidos ?? {}).length === 0))
|
||||
|
||||
return matchesQ && semOK && tipoOK && flagOK && carreraOK && facultadOK && planOK
|
||||
})
|
||||
}, [q, sem, tipo, flag, carrera, facultad, asignaturas])
|
||||
return matchesQ && semOK && tipoOK && flagOK && carreraOK && facultadOK && planOK
|
||||
})
|
||||
}, [q, sem, tipo, flag, carrera, facultad, asignaturas])
|
||||
|
||||
|
||||
// Agrupación
|
||||
@@ -316,18 +310,19 @@ const carrerasList = useMemo(() => {
|
||||
}, [filtered, groupBy])
|
||||
|
||||
// Helpers
|
||||
const clearFilters = () => { setQ(''); setSem('todos'); setTipo('todos'); setCarrera('todas'); setFlag('') ; setFacultad('todas')
|
||||
const clearFilters = () => {
|
||||
setQ(''); setSem('todos'); setTipo('todos'); setCarrera('todas'); setFlag(''); setFacultad('todas')
|
||||
// Actualiza la URL limpiando todos los query params
|
||||
router.navigate({
|
||||
to: '/asignaturas',
|
||||
search: {
|
||||
q: '',
|
||||
planId: '',
|
||||
carreraId: '',
|
||||
facultadId: '',
|
||||
f: ''
|
||||
},
|
||||
})
|
||||
router.navigate({
|
||||
to: '/asignaturas',
|
||||
search: {
|
||||
q: '',
|
||||
planId: '',
|
||||
carreraId: '',
|
||||
facultadId: '',
|
||||
f: ''
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// NEW: util para clonar 1 asignatura
|
||||
@@ -550,7 +545,12 @@ const carrerasList = useMemo(() => {
|
||||
value={search.planId ?? "todos"}
|
||||
onValueChange={(val) => {
|
||||
router.navigate({
|
||||
search: { ...search, planId: val === "todos" ? "" : val },
|
||||
to: '/asignaturas',
|
||||
search: {
|
||||
...search,
|
||||
planId: val === 'todos' ? '' : val,
|
||||
},
|
||||
replace: true,
|
||||
})
|
||||
}}
|
||||
>
|
||||
@@ -828,15 +828,14 @@ function AsignaturaCard({ a, onClone, onAddToCart }: { a: Asignatura; onClone: (
|
||||
const horasT = a.horas_teoricas ?? 0
|
||||
const horasP = a.horas_practicas ?? 0
|
||||
const meta = tipoMeta(a.tipo)
|
||||
const FacIcon = (Icons as any)[a.plan?.carrera?.facultad?.icon ?? 'Building2'] || Icons.Building2
|
||||
console.log(a);
|
||||
|
||||
return (
|
||||
<li className="group relative overflow-hidden rounded-2xl border bg-white/75 dark:bg-neutral-900/60 shadow-sm hover:shadow-md transition-all"
|
||||
style={{
|
||||
borderColor: a.plan?.carrera?.facultad?.color ?? '#ccc',
|
||||
backgroundColor: `${a.plan?.carrera?.facultad?.color}15`, // 15 = transparencia HEX
|
||||
}}
|
||||
style={{
|
||||
borderColor: a.plan?.carrera?.facultad?.color ?? '#ccc',
|
||||
backgroundColor: `${a.plan?.carrera?.facultad?.color}15`, // 15 = transparencia HEX
|
||||
}}
|
||||
>
|
||||
<div className="p-3">
|
||||
<div className="flex items-start gap-3">
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { AcademicSections, planKeys } from "@/components/planes/academic-sections"
|
||||
import { GradientMesh } from "../../../components/planes/GradientMesh"
|
||||
import { asignaturaExtraOptions, asignaturaKeys, asignaturasCountOptions, asignaturasPreviewOptions, planByIdOptions, type AsignaturaLite, type PlanFull } from "@/components/planes/planQueries"
|
||||
import { asignaturaExtraOptions, asignaturaKeys, asignaturasPreviewOptions, planByIdOptions, type AsignaturaLite, type PlanFull } from "@/components/planes/planQueries"
|
||||
import { softAccentStyle } from "@/components/planes/planHelpers"
|
||||
import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog"
|
||||
import { DialogFooter, DialogHeader } from "@/components/ui/dialog"
|
||||
|
||||
@@ -17,13 +17,6 @@ import { toast } from "sonner"
|
||||
|
||||
|
||||
|
||||
/* -------------------- Tipos -------------------- */
|
||||
|
||||
|
||||
const SCOPED_ROLES = ["director_facultad", "secretario_academico", "jefe_carrera"] as const
|
||||
|
||||
|
||||
|
||||
|
||||
/* -------------------- Query Keys & Fetcher -------------------- */
|
||||
const usersKeys = {
|
||||
@@ -149,35 +142,6 @@ function RouteComponent() {
|
||||
carrera_id?: string | null
|
||||
}>({ email: "", password: "" })
|
||||
|
||||
function genPassword() {
|
||||
/*
|
||||
Supabase requiere que las contraseñas tengan las siguientes características:
|
||||
- Mínimo de 6 caracteres
|
||||
- Debe contener al menos una letra minúscula
|
||||
- Debe contener al menos una letra mayúscula
|
||||
- Debe contener al menos un número
|
||||
- Debe contener al menos un carácter especial
|
||||
Para garantizar la seguridad, generaremos contraseñas de 12 caracteres en vez del mínimo de 6
|
||||
*/
|
||||
|
||||
// 1. Generar una permutación de los números de 1 al 12 con el método Fisher-Yates
|
||||
|
||||
const positions = Array.from({ length: 12 }, (_, i) => i);
|
||||
for (let i = positions.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[positions[i], positions[j]] = [positions[j], positions[i]];
|
||||
}
|
||||
|
||||
// 2. Las correspondencias son las siguientes:
|
||||
// - El primer número indica la posición de la letra minúscula
|
||||
// - El segundo número indica la posición de la letra mayúscula
|
||||
// - El tercer número indica la posición del número
|
||||
// - El cuarto número indica la posición del carácter especial
|
||||
// - En las demás posiciones puede haber cualquier caracter alfanumérico
|
||||
|
||||
const s = Array.from(crypto.getRandomValues(new Uint32Array(4))).map((n) => n.toString(36)).join("")
|
||||
return s.slice(0, 14)
|
||||
}
|
||||
|
||||
function RolePill({ role }: { role: Role }) {
|
||||
const meta = ROLE_META[role]
|
||||
@@ -197,61 +161,6 @@ function RouteComponent() {
|
||||
router.invalidate()
|
||||
}
|
||||
|
||||
const upsertNombramiento = useMutation({
|
||||
mutationFn: async (opts: {
|
||||
user_id: string
|
||||
puesto: "director_facultad" | "secretario_academico" | "jefe_carrera"
|
||||
facultad_id?: string | null
|
||||
carrera_id?: string | null
|
||||
}) => {
|
||||
// cierra vigentes
|
||||
if (opts.puesto === "jefe_carrera") {
|
||||
if (!opts.carrera_id) throw new Error("Selecciona carrera")
|
||||
await supabase
|
||||
.from("nombramientos")
|
||||
.update({ hasta: new Date().toISOString().slice(0, 10) })
|
||||
.eq("puesto", "jefe_carrera")
|
||||
.eq("carrera_id", opts.carrera_id)
|
||||
.is("hasta", null)
|
||||
} else {
|
||||
if (!opts.facultad_id) throw new Error("Selecciona facultad")
|
||||
await supabase
|
||||
.from("nombramientos")
|
||||
.update({ hasta: new Date().toISOString().slice(0, 10) })
|
||||
.eq("puesto", opts.puesto)
|
||||
.eq("facultad_id", opts.facultad_id)
|
||||
.is("hasta", null)
|
||||
}
|
||||
const { error } = await supabase.from("nombramientos").insert({
|
||||
user_id: opts.user_id,
|
||||
puesto: opts.puesto,
|
||||
facultad_id: opts.facultad_id ?? null,
|
||||
carrera_id: opts.carrera_id ?? null,
|
||||
desde: new Date().toISOString().slice(0, 10),
|
||||
hasta: null,
|
||||
})
|
||||
if (error) throw error
|
||||
},
|
||||
onError: (e: any) => toast.error(e?.message || "Error al registrar nombramiento"),
|
||||
})
|
||||
|
||||
const toggleBan = useMutation({
|
||||
mutationFn: async (u: UserClaims) => {
|
||||
throw new Error("Funcionalidad de baneo no implementada aún.")
|
||||
const banned = false // cuando se tenga acceso a ese campo
|
||||
// const banned = !!u.banned_until && new Date(u.banned_until) > new Date()
|
||||
const payload = banned ? { banned_until: null } : { banned_until: new Date(Date.now() + 100 * 365 * 24 * 60 * 60 * 1000).toISOString() }
|
||||
// const { error } = await supabase.auth.admin.updateUserById(u.id, payload as any)
|
||||
// if (error) throw new Error(error.message)
|
||||
return !banned
|
||||
},
|
||||
onSuccess: async (isBanned) => {
|
||||
toast.success(isBanned ? "Usuario baneado" : "Usuario desbaneado")
|
||||
await invalidateAll()
|
||||
},
|
||||
onError: (e: any) => toast.error(e?.message || "Error al cambiar estado de baneo"),
|
||||
})
|
||||
|
||||
const createUser = useMutation({
|
||||
mutationFn: async (payload: typeof createForm) => {
|
||||
// Validaciones previas
|
||||
@@ -409,7 +318,7 @@ function RouteComponent() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button variant="outline" size="sm" onClick={() => toggleBan.mutate(u)} title={banned ? "Restaurar acceso" : "Inhabilitar la cuenta"} className="hidden sm:inline-flex">
|
||||
<Button variant="outline" size="sm" onClick={() => {}} title={banned ? "Restaurar acceso" : "Inhabilitar la cuenta"} className="hidden sm:inline-flex">
|
||||
<Icons.BanIcon className="w-4 h-4 mr-1" /> {banned ? "Restaurar acceso" : "Inhabilitar la cuenta"}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="hidden sm:inline-flex shrink-0" onClick={() => openEdit(u)}>
|
||||
@@ -425,7 +334,7 @@ function RouteComponent() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="sm:hidden self-start shrink-0 flex gap-1">
|
||||
<Button variant="outline" size="icon" onClick={() => toggleBan.mutate(u)} aria-label="Ban/Unban"><Icons.BanIcon className="w-4 h-4" /></Button>
|
||||
<Button variant="outline" size="icon" onClick={() => {}} aria-label="Ban/Unban"><Icons.BanIcon className="w-4 h-4" /></Button>
|
||||
<Button variant="ghost" size="icon" onClick={() => openEdit(u)} aria-label="Editar"><Icons.Pencil className="w-4 h-4" /></Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
12
swa-cli.config.json
Normal file
12
swa-cli.config.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://aka.ms/azure/static-web-apps-cli/schema",
|
||||
"configurations": {
|
||||
"acad-ia": {
|
||||
"appLocation": ".",
|
||||
"outputLocation": "dist",
|
||||
"appBuildCommand": "npm run build",
|
||||
"run": "npm run dev",
|
||||
"appDevserverUrl": "http://localhost:5173"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user