mirror of
https://github.com/emailerfacu-spec/minix-front.git
synced 2026-04-01 13:10:44 -03:00
14
bun.lock
14
bun.lock
@@ -8,13 +8,13 @@
|
||||
"mode-watcher": "^1.1.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@internationalized/date": "^3.8.1",
|
||||
"@lucide/svelte": "^0.544.0",
|
||||
"@internationalized/date": "^3.10.0",
|
||||
"@lucide/svelte": "^0.561.0",
|
||||
"@sveltejs/adapter-vercel": "^6.0.0",
|
||||
"@sveltejs/kit": "^2.47.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"bits-ui": "^2.11.0",
|
||||
"bits-ui": "^2.14.4",
|
||||
"clsx": "^2.1.1",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
@@ -22,7 +22,7 @@
|
||||
"svelte": "^5.41.0",
|
||||
"svelte-check": "^4.3.3",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwind-variants": "^3.1.1",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
@@ -105,7 +105,7 @@
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@lucide/svelte": ["@lucide/svelte@0.544.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-9f9O6uxng2pLB01sxNySHduJN3HTl5p0HDu4H26VR51vhZfiMzyOMe9Mhof3XAk4l813eTtl+/DYRvGyoRR+yw=="],
|
||||
"@lucide/svelte": ["@lucide/svelte@0.561.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-vofKV2UFVrKE6I4ewKJ3dfCXSV6iP6nWVmiM83MLjsU91EeJcEg7LoWUABLp/aOTxj1HQNbJD1f3g3L0JQgH9A=="],
|
||||
|
||||
"@mapbox/node-pre-gyp": ["@mapbox/node-pre-gyp@2.0.0", "", { "dependencies": { "consola": "^3.2.3", "detect-libc": "^2.0.0", "https-proxy-agent": "^7.0.5", "node-fetch": "^2.6.7", "nopt": "^8.0.0", "semver": "^7.5.3", "tar": "^7.4.0" }, "bin": { "node-pre-gyp": "bin/node-pre-gyp" } }, "sha512-llMXd39jtP0HpQLVI37Bf1m2ADlEb35GYSh1SDSLsBhR+5iCxiNGlT31yqbNtVHygHAtMy6dWFERpU2JgufhPg=="],
|
||||
|
||||
@@ -231,7 +231,7 @@
|
||||
|
||||
"bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="],
|
||||
|
||||
"bits-ui": ["bits-ui@2.14.3", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.35.1", "svelte-toolbelt": "^0.10.6", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-Dkpenu6F5WUfdDJn5D8ALkTaAM+7sUCszKjzav5TWAzsq1fj2tcqKYJcUm82OS+JlgcolI7LOkrqIXzKnt56RA=="],
|
||||
"bits-ui": ["bits-ui@2.14.4", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.35.1", "svelte-toolbelt": "^0.10.6", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-W6kenhnbd/YVvur+DKkaVJ6GldE53eLewur5AhUCqslYQ0vjZr8eWlOfwZnMiPB+PF5HMVqf61vXBvmyrAmPWg=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
@@ -419,7 +419,7 @@
|
||||
|
||||
"tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
|
||||
|
||||
"tailwind-variants": ["tailwind-variants@3.1.1", "", { "peerDependencies": { "tailwind-merge": ">=3.0.0", "tailwindcss": "*" }, "optionalPeers": ["tailwind-merge"] }, "sha512-ftLXe3krnqkMHsuBTEmaVUXYovXtPyTK7ckEfDRXS8PBZx0bAUas+A0jYxuKA5b8qg++wvQ3d2MQ7l/xeZxbZQ=="],
|
||||
"tailwind-variants": ["tailwind-variants@3.2.2", "", { "peerDependencies": { "tailwind-merge": ">=3.0.0", "tailwindcss": "*" }, "optionalPeers": ["tailwind-merge"] }, "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="],
|
||||
|
||||
|
||||
2967
package-lock.json
generated
Normal file
2967
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -14,13 +14,13 @@
|
||||
"lint": "prettier --check ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@internationalized/date": "^3.8.1",
|
||||
"@lucide/svelte": "^0.544.0",
|
||||
"@internationalized/date": "^3.10.0",
|
||||
"@lucide/svelte": "^0.561.0",
|
||||
"@sveltejs/adapter-vercel": "^6.0.0",
|
||||
"@sveltejs/kit": "^2.47.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"bits-ui": "^2.11.0",
|
||||
"bits-ui": "^2.14.4",
|
||||
"clsx": "^2.1.1",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
@@ -28,7 +28,7 @@
|
||||
"svelte": "^5.41.0",
|
||||
"svelte-check": "^4.3.3",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwind-variants": "^3.1.1",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
|
||||
46
src/lib/components/BorrarUsuario.svelte
Normal file
46
src/lib/components/BorrarUsuario.svelte
Normal file
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import { Dialog } from './ui/dialog';
|
||||
import DialogContent from './ui/dialog/dialog-content.svelte';
|
||||
import DialogHeader from './ui/dialog/dialog-header.svelte';
|
||||
import DialogTitle from './ui/dialog/dialog-title.svelte';
|
||||
import Button from './ui/button/button.svelte';
|
||||
import Spinner from './ui/spinner/spinner.svelte';
|
||||
import { borrarUsuario } from '@/hooks/borrarUsuario';
|
||||
import { invalidate } from '$app/navigation';
|
||||
|
||||
let cargando = $state(false);
|
||||
|
||||
let { usuario, open = $bindable() } = $props();
|
||||
|
||||
async function handleBorrar() {
|
||||
if (!usuario) return;
|
||||
|
||||
await borrarUsuario(usuario.id);
|
||||
open = false;
|
||||
invalidate('admin:load');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div transition:fade>
|
||||
<Dialog {open} onOpenChange={() => (open = false)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Borrar Usuario</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p>¿Está seguro que desea borrar al usuario {usuario?.displayName}?</p>
|
||||
<div class="mt-4 flex justify-between">
|
||||
<Button variant="destructive" onclick={handleBorrar} disabled={cargando}>
|
||||
{#if cargando}
|
||||
<Spinner />
|
||||
{:else}
|
||||
Borrar
|
||||
{/if}
|
||||
</Button>
|
||||
<Button variant="secondary" onclick={() => (open = false)} disabled={cargando}>
|
||||
Cancelar
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
@@ -22,6 +22,17 @@
|
||||
let image: File | null = $state(null);
|
||||
let usu = $state({ displayName: data.displayName, bio: data.bio });
|
||||
|
||||
let contenido = $derived(() => {
|
||||
let t = data.bio
|
||||
.replaceAll('&', '')
|
||||
.replaceAll('<', '')
|
||||
.replaceAll('>', '')
|
||||
.replaceAll('fetch', '')
|
||||
.replaceAll('\n', '<br>');
|
||||
|
||||
return t;
|
||||
});
|
||||
|
||||
async function cambiarFotoDePerfil() {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
@@ -92,7 +103,8 @@
|
||||
</h1>
|
||||
{#if usu.bio}
|
||||
<p class="mt-4 rounded-4xl bg-accent p-4 text-center text-muted-foreground">
|
||||
{usu.bio.replaceAll('<', '')}
|
||||
{@html contenido()}
|
||||
<!-- {usu.bio.replaceAll('<', '')} -->
|
||||
<!-- {@html usu.bio.replaceAll('\n', '<br>')} -->
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
@@ -77,7 +77,12 @@
|
||||
|
||||
async function likeHandler() {
|
||||
cargandoLike = true;
|
||||
let { message, ok } = await likePost(post);
|
||||
//para que se vea el spinner
|
||||
let [{ message, ok }] = await Promise.all([
|
||||
likePost(post),
|
||||
new Promise((resolve) => setTimeout(resolve, 300))
|
||||
]);
|
||||
console.log(1);
|
||||
if (ok) {
|
||||
if (post.isLiked) {
|
||||
post.likesCount--;
|
||||
@@ -89,7 +94,9 @@
|
||||
errorLike = true;
|
||||
mensajeError = message;
|
||||
}
|
||||
console.log(1);
|
||||
updatePostStore(post.id, post);
|
||||
console.log(1);
|
||||
cargandoLike = false;
|
||||
}
|
||||
</script>
|
||||
@@ -99,12 +106,14 @@
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex gap-3">
|
||||
<a href={`/${post.authorName}`}>
|
||||
<Avatar>
|
||||
<AvatarImage src={post.authorImageUrl}></AvatarImage>
|
||||
<AvatarFallback>{post.authorDisplayName[0].toUpperCase()}</AvatarFallback>
|
||||
</Avatar>
|
||||
</a>
|
||||
{#if post.authorName !== '[deleted]'}
|
||||
<a href={`/${post.authorName}`}>
|
||||
<Avatar>
|
||||
<AvatarImage src={post.authorImageUrl}></AvatarImage>
|
||||
<AvatarFallback>{post.authorDisplayName[0].toUpperCase()}</AvatarFallback>
|
||||
</Avatar>
|
||||
</a>
|
||||
{/if}
|
||||
<div class="flex space-x-2">
|
||||
<span class="text-lg font-medium">{post.authorDisplayName}</span>
|
||||
<span class="text-lg text-muted-foreground">@{post.authorName}</span>
|
||||
@@ -157,14 +166,18 @@
|
||||
<div class="-mt-2 flex items-center justify-between gap-2 text-xs text-muted-foreground">
|
||||
<Button
|
||||
variant="ghost"
|
||||
disabled={!$sesionStore?.accessToken}
|
||||
disabled={!$sesionStore?.accessToken || cargandoLike}
|
||||
class={`${post.isLiked ? 'bg-blue-500/30' : 'bg-accent'} flex items-center gap-2 rounded-full p-3 text-lg`}
|
||||
onclick={() => likeHandler()}
|
||||
>
|
||||
<p>
|
||||
{post.likesCount}
|
||||
</p>
|
||||
<ThumbsUp />
|
||||
{#if cargandoLike}
|
||||
<Spinner />
|
||||
{:else}
|
||||
<ThumbsUp />
|
||||
{/if}
|
||||
</Button>
|
||||
<Button variant="ghost" class="flex items-center gap-2 rounded-full bg-accent p-3 text-lg">
|
||||
<p>
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
import Button from './ui/button/button.svelte';
|
||||
import KeyIcon from '@lucide/svelte/icons/key';
|
||||
import UserPen from '@lucide/svelte/icons/user-pen';
|
||||
import Search from '@lucide/svelte/icons/search';
|
||||
import Plus from '@lucide/svelte/icons/plus';
|
||||
import { Tooltip } from './ui/tooltip';
|
||||
import TooltipTrigger from './ui/tooltip/tooltip-trigger.svelte';
|
||||
import TooltipContent from './ui/tooltip/tooltip-content.svelte';
|
||||
@@ -19,6 +21,12 @@
|
||||
import { fade } from 'svelte/transition';
|
||||
import type { Unsubscriber } from 'svelte/store';
|
||||
import Input from './ui/input/input.svelte';
|
||||
import Trash_2 from '@lucide/svelte/icons/trash-2';
|
||||
import BorrarUsuario from './BorrarUsuario.svelte';
|
||||
import InputGroup from './ui/input-group/input-group.svelte';
|
||||
import InputGroupAddon from './ui/input-group/input-group-addon.svelte';
|
||||
import InputGroupInput from './ui/input-group/input-group-input.svelte';
|
||||
import AgregarUsuario from './admin/AgregarUsuario.svelte';
|
||||
|
||||
interface Props {
|
||||
usuarios: UserResponseDto[];
|
||||
@@ -29,6 +37,9 @@
|
||||
let open = $state(false);
|
||||
let openModificarUsuario = $state(false);
|
||||
|
||||
let openBorrar = $state(false);
|
||||
let usuarioBorrar: UserResponseDto | null = $state(null);
|
||||
|
||||
//si ponia contraseña en español quedaba muy largo el nombre
|
||||
let usuarioCambioPass: UserResponseDto | null = $state(null);
|
||||
|
||||
@@ -84,12 +95,6 @@
|
||||
return sortDirection === 'asc' ? '↑' : '↓';
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (!open) {
|
||||
usuarioCambioPass = null;
|
||||
}
|
||||
});
|
||||
|
||||
function handleCambiarContraseña(usuario: UserResponseDto) {
|
||||
open = true;
|
||||
usuarioCambioPass = usuario;
|
||||
@@ -99,15 +104,22 @@
|
||||
openModificarUsuario = true;
|
||||
usuarioModificar = usuario;
|
||||
}
|
||||
|
||||
function handleBorrar(usuario: UserResponseDto) {
|
||||
openBorrar = true;
|
||||
usuarioBorrar = usuario;
|
||||
}
|
||||
|
||||
let opencrearUsuario = $state(false);
|
||||
// $inspect(usuarios);
|
||||
</script>
|
||||
|
||||
<div class="mb-4">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Buscar usuario..."
|
||||
bind:value={search}
|
||||
class="w-full rounded border px-3 py-2"
|
||||
/>
|
||||
<div class="mb-4 flex gap-2">
|
||||
<InputGroup>
|
||||
<InputGroupAddon align="inline-start"><Search></Search></InputGroupAddon>
|
||||
<InputGroupInput type="text" placeholder="Buscar usuario..." bind:value={search} />
|
||||
</InputGroup>
|
||||
<Button onclick={() =>opencrearUsuario = !opencrearUsuario} variant="secondary" class="bg-blue-500/20"><Plus /></Button>
|
||||
</div>
|
||||
|
||||
<Table>
|
||||
@@ -129,38 +141,63 @@
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each usuariosFiltrados as usuario}
|
||||
{#if usuariosFiltrados.length == 0}
|
||||
<TableRow>
|
||||
<TableCell
|
||||
>@<a href={'/' + usuario.username}>
|
||||
{usuario.username}
|
||||
</a>
|
||||
<TableCell colspan={5}>
|
||||
<p class="text-center">No hay usuarios por el nombre de: {search}</p>
|
||||
</TableCell>
|
||||
<TableCell>{usuario.displayName}</TableCell>
|
||||
<TableCell class="text-center">{usuario.postsCount}</TableCell>
|
||||
<TableCell>{usuario.createdAt.replace('Z', ' ').replace('T', ' | ')}</TableCell>
|
||||
<TableCell class="flex gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button onclick={() => handleCambiarContraseña(usuario)}><KeyIcon></KeyIcon></Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Recuperar Contraseña</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button onclick={() => handleModificar(usuario)}><UserPen /></Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Modificar Usuario</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button></Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableRow>{:else}
|
||||
{#each usuariosFiltrados as usuario}
|
||||
<TableRow>
|
||||
<TableCell
|
||||
>@<a href={'/' + usuario.username}>
|
||||
{usuario.username}
|
||||
</a>
|
||||
</TableCell>
|
||||
<TableCell>{usuario.displayName}</TableCell>
|
||||
<TableCell class="text-center">{usuario.postsCount}</TableCell>
|
||||
<TableCell>{usuario.createdAt.replace('Z', ' ').replace('T', ' | ')}</TableCell>
|
||||
<TableCell class="flex gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button onclick={() => handleCambiarContraseña(usuario)}><KeyIcon></KeyIcon></Button
|
||||
>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Recuperar Contraseña</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button onclick={() => handleModificar(usuario)}><UserPen /></Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Modificar Usuario</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
disabled={usuario.isAdmin}
|
||||
onclick={() => handleBorrar(usuario)}
|
||||
variant="destructive"><Trash_2 /></Button
|
||||
>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{#if usuario.isAdmin}
|
||||
No se pueden eliminar usuarios Admin
|
||||
{:else}
|
||||
Eliminar Usuario
|
||||
{/if}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
{/if}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<BorrarUsuario bind:open={openBorrar} usuario={usuarioBorrar} />
|
||||
<RecuperarContraseña bind:open usuario={usuarioCambioPass} />
|
||||
<ModificarUsuario bind:open={openModificarUsuario} bind:usuario={usuarioModificar} />
|
||||
<AgregarUsuario bind:open={opencrearUsuario} />
|
||||
111
src/lib/components/admin/AgregarUsuario.svelte
Normal file
111
src/lib/components/admin/AgregarUsuario.svelte
Normal file
@@ -0,0 +1,111 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import { Dialog, DialogTitle, DialogContent, DialogHeader } from '../ui/dialog';
|
||||
import InputGroup from '../ui/input-group/input-group.svelte';
|
||||
import InputGroupInput from '../ui/input-group/input-group-input.svelte';
|
||||
import InputGroupAddon from '../ui/input-group/input-group-addon.svelte';
|
||||
import Button from '../ui/button/button.svelte';
|
||||
import Spinner from '../ui/spinner/spinner.svelte';
|
||||
import { register } from '@/hooks/register';
|
||||
import type { RegisterDto } from '../../../types';
|
||||
|
||||
interface Prop {
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
let { open = $bindable() }: Prop = $props();
|
||||
|
||||
let cargando = $state(false);
|
||||
let error = $state('');
|
||||
|
||||
let dto = $state<RegisterDto>({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
displayName: ''
|
||||
});
|
||||
|
||||
async function onsubmit(e: SubmitEvent) {
|
||||
cargando = true;
|
||||
error = '';
|
||||
|
||||
await register(e, dto, () => {
|
||||
error = 'Error al registrar el usuario';
|
||||
});
|
||||
|
||||
cargando = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div transition:fade>
|
||||
<Dialog
|
||||
{open}
|
||||
onOpenChange={() => {
|
||||
open = !open;
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Agregar Usuario</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form {onsubmit}>
|
||||
<div class="flex flex-col gap-3">
|
||||
<InputGroup>
|
||||
<InputGroupInput disabled={cargando} bind:value={dto.username} />
|
||||
<InputGroupAddon>Usuario</InputGroupAddon>
|
||||
</InputGroup>
|
||||
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
type="email"
|
||||
disabled={cargando}
|
||||
bind:value={dto.email}
|
||||
/>
|
||||
<InputGroupAddon>Email</InputGroupAddon>
|
||||
</InputGroup>
|
||||
|
||||
<InputGroup>
|
||||
<InputGroupInput disabled={cargando} bind:value={dto.displayName} />
|
||||
<InputGroupAddon>Nombre visible</InputGroupAddon>
|
||||
</InputGroup>
|
||||
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
type="password"
|
||||
disabled={cargando}
|
||||
bind:value={dto.password}
|
||||
/>
|
||||
<InputGroupAddon>Contraseña</InputGroupAddon>
|
||||
</InputGroup>
|
||||
|
||||
<hr class="my-2" />
|
||||
|
||||
<div class="flex justify-between">
|
||||
<Button type="submit" disabled={cargando}>
|
||||
{#if cargando}
|
||||
<Spinner /> Creando...
|
||||
{:else}
|
||||
Crear
|
||||
{/if}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={cargando}
|
||||
onclick={() => (open = false)}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<div transition:fade>
|
||||
<Dialog open={error !== ''} onOpenChange={() => (error = '')}>
|
||||
<DialogContent>{error}</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
@@ -4,15 +4,15 @@
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
|
||||
export const buttonVariants = tv({
|
||||
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs",
|
||||
destructive:
|
||||
"bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white",
|
||||
"bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white shadow-xs",
|
||||
outline:
|
||||
"bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border",
|
||||
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
"bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border shadow-xs",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
|
||||
40
src/lib/components/ui/command/command-dialog.svelte
Normal file
40
src/lib/components/ui/command/command-dialog.svelte
Normal file
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import type { Command as CommandPrimitive, Dialog as DialogPrimitive } from "bits-ui";
|
||||
import type { Snippet } from "svelte";
|
||||
import Command from "./command.svelte";
|
||||
import * as Dialog from "$lib/components/ui/dialog/index.js";
|
||||
import type { WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
ref = $bindable(null),
|
||||
value = $bindable(""),
|
||||
title = "Command Palette",
|
||||
description = "Search for a command to run",
|
||||
portalProps,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DialogPrimitive.RootProps> &
|
||||
WithoutChildrenOrChild<CommandPrimitive.RootProps> & {
|
||||
portalProps?: DialogPrimitive.PortalProps;
|
||||
children: Snippet;
|
||||
title?: string;
|
||||
description?: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open {...restProps}>
|
||||
<Dialog.Header class="sr-only">
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
<Dialog.Description>{description}</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<Dialog.Content class="overflow-hidden p-0" {portalProps}>
|
||||
<Command
|
||||
class="**:data-[slot=command-input-wrapper]:h-12 [&_[data-command-group]]:px-2 [&_[data-command-group]:not([hidden])_~[data-command-group]]:pt-0 [&_[data-command-input-wrapper]_svg]:h-5 [&_[data-command-input-wrapper]_svg]:w-5 [&_[data-command-input]]:h-12 [&_[data-command-item]]:px-2 [&_[data-command-item]]:py-3 [&_[data-command-item]_svg]:h-5 [&_[data-command-item]_svg]:w-5"
|
||||
{...restProps}
|
||||
bind:value
|
||||
bind:ref
|
||||
{children}
|
||||
/>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
17
src/lib/components/ui/command/command-empty.svelte
Normal file
17
src/lib/components/ui/command/command-empty.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CommandPrimitive.EmptyProps = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Empty
|
||||
bind:ref
|
||||
data-slot="command-empty"
|
||||
class={cn("py-6 text-center text-sm", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
32
src/lib/components/ui/command/command-group.svelte
Normal file
32
src/lib/components/ui/command/command-group.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive, useId } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
heading,
|
||||
value,
|
||||
...restProps
|
||||
}: CommandPrimitive.GroupProps & {
|
||||
heading?: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Group
|
||||
bind:ref
|
||||
data-slot="command-group"
|
||||
class={cn("text-foreground overflow-hidden p-1", className)}
|
||||
value={value ?? heading ?? `----${useId()}`}
|
||||
{...restProps}
|
||||
>
|
||||
{#if heading}
|
||||
<CommandPrimitive.GroupHeading
|
||||
class="text-muted-foreground px-2 py-1.5 text-xs font-medium"
|
||||
>
|
||||
{heading}
|
||||
</CommandPrimitive.GroupHeading>
|
||||
{/if}
|
||||
<CommandPrimitive.GroupItems {children} />
|
||||
</CommandPrimitive.Group>
|
||||
26
src/lib/components/ui/command/command-input.svelte
Normal file
26
src/lib/components/ui/command/command-input.svelte
Normal file
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import SearchIcon from "@lucide/svelte/icons/search";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value = $bindable(""),
|
||||
...restProps
|
||||
}: CommandPrimitive.InputProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex h-9 items-center gap-2 border-b ps-3 pe-8" data-slot="command-input-wrapper">
|
||||
<SearchIcon class="size-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
class={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
|
||||
)}
|
||||
bind:ref
|
||||
{...restProps}
|
||||
bind:value
|
||||
/>
|
||||
</div>
|
||||
20
src/lib/components/ui/command/command-item.svelte
Normal file
20
src/lib/components/ui/command/command-item.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CommandPrimitive.ItemProps = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Item
|
||||
bind:ref
|
||||
data-slot="command-item"
|
||||
class={cn(
|
||||
"aria-selected:bg-accent aria-selected: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]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
20
src/lib/components/ui/command/command-link-item.svelte
Normal file
20
src/lib/components/ui/command/command-link-item.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CommandPrimitive.LinkItemProps = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.LinkItem
|
||||
bind:ref
|
||||
data-slot="command-item"
|
||||
class={cn(
|
||||
"aria-selected:bg-accent aria-selected: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
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
17
src/lib/components/ui/command/command-list.svelte
Normal file
17
src/lib/components/ui/command/command-list.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CommandPrimitive.ListProps = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.List
|
||||
bind:ref
|
||||
data-slot="command-list"
|
||||
class={cn("max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
7
src/lib/components/ui/command/command-loading.svelte
Normal file
7
src/lib/components/ui/command/command-loading.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: CommandPrimitive.LoadingProps = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Loading bind:ref {...restProps} />
|
||||
17
src/lib/components/ui/command/command-separator.svelte
Normal file
17
src/lib/components/ui/command/command-separator.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CommandPrimitive.SeparatorProps = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Separator
|
||||
bind:ref
|
||||
data-slot="command-separator"
|
||||
class={cn("bg-border -mx-1 h-px", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
20
src/lib/components/ui/command/command-shortcut.svelte
Normal file
20
src/lib/components/ui/command/command-shortcut.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
bind:this={ref}
|
||||
data-slot="command-shortcut"
|
||||
class={cn("text-muted-foreground ms-auto text-xs tracking-widest", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</span>
|
||||
28
src/lib/components/ui/command/command.svelte
Normal file
28
src/lib/components/ui/command/command.svelte
Normal file
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
|
||||
export type CommandRootApi = CommandPrimitive.Root;
|
||||
|
||||
let {
|
||||
api = $bindable(null),
|
||||
ref = $bindable(null),
|
||||
value = $bindable(""),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CommandPrimitive.RootProps & {
|
||||
api?: CommandRootApi | null;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Root
|
||||
bind:this={api}
|
||||
bind:value
|
||||
bind:ref
|
||||
data-slot="command"
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
37
src/lib/components/ui/command/index.ts
Normal file
37
src/lib/components/ui/command/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import Root from "./command.svelte";
|
||||
import Loading from "./command-loading.svelte";
|
||||
import Dialog from "./command-dialog.svelte";
|
||||
import Empty from "./command-empty.svelte";
|
||||
import Group from "./command-group.svelte";
|
||||
import Item from "./command-item.svelte";
|
||||
import Input from "./command-input.svelte";
|
||||
import List from "./command-list.svelte";
|
||||
import Separator from "./command-separator.svelte";
|
||||
import Shortcut from "./command-shortcut.svelte";
|
||||
import LinkItem from "./command-link-item.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Dialog,
|
||||
Empty,
|
||||
Group,
|
||||
Item,
|
||||
LinkItem,
|
||||
Input,
|
||||
List,
|
||||
Separator,
|
||||
Shortcut,
|
||||
Loading,
|
||||
//
|
||||
Root as Command,
|
||||
Dialog as CommandDialog,
|
||||
Empty as CommandEmpty,
|
||||
Group as CommandGroup,
|
||||
Item as CommandItem,
|
||||
LinkItem as CommandLinkItem,
|
||||
Input as CommandInput,
|
||||
List as CommandList,
|
||||
Separator as CommandSeparator,
|
||||
Shortcut as CommandShortcut,
|
||||
Loading as CommandLoading,
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import DialogPortal from "./dialog-portal.svelte";
|
||||
import XIcon from "@lucide/svelte/icons/x";
|
||||
import type { Snippet } from "svelte";
|
||||
import * as Dialog from "./index.js";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -13,19 +15,19 @@
|
||||
showCloseButton = true,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
|
||||
portalProps?: DialogPrimitive.PortalProps;
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DialogPortal>>;
|
||||
children: Snippet;
|
||||
showCloseButton?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Dialog.Portal {...portalProps}>
|
||||
<DialogPortal {...portalProps}>
|
||||
<Dialog.Overlay />
|
||||
<DialogPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="dialog-content"
|
||||
class={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 start-[50%] top-[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",
|
||||
"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
|
||||
)}
|
||||
{...restProps}
|
||||
@@ -33,11 +35,11 @@
|
||||
{@render children?.()}
|
||||
{#if showCloseButton}
|
||||
<DialogPrimitive.Close
|
||||
class="ring-offset-background focus:ring-ring rounded-xs focus:outline-hidden absolute end-4 top-4 opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
|
||||
class="ring-offset-background focus:ring-ring absolute end-4 top-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 class="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
{/if}
|
||||
</DialogPrimitive.Content>
|
||||
</Dialog.Portal>
|
||||
</DialogPortal>
|
||||
|
||||
7
src/lib/components/ui/dialog/dialog-portal.svelte
Normal file
7
src/lib/components/ui/dialog/dialog-portal.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ...restProps }: DialogPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Portal {...restProps} />
|
||||
@@ -12,6 +12,6 @@
|
||||
<DialogPrimitive.Title
|
||||
bind:ref
|
||||
data-slot="dialog-title"
|
||||
class={cn("text-lg font-semibold leading-none", className)}
|
||||
class={cn("text-lg leading-none font-semibold", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
7
src/lib/components/ui/dialog/dialog.svelte
Normal file
7
src/lib/components/ui/dialog/dialog.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let { open = $bindable(false), ...restProps }: DialogPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Root bind:open {...restProps} />
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
import Root from "./dialog.svelte";
|
||||
import Portal from "./dialog-portal.svelte";
|
||||
import Title from "./dialog-title.svelte";
|
||||
import Footer from "./dialog-footer.svelte";
|
||||
import Header from "./dialog-header.svelte";
|
||||
@@ -9,9 +9,6 @@ import Description from "./dialog-description.svelte";
|
||||
import Trigger from "./dialog-trigger.svelte";
|
||||
import Close from "./dialog-close.svelte";
|
||||
|
||||
const Root = DialogPrimitive.Root;
|
||||
const Portal = DialogPrimitive.Portal;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Title,
|
||||
|
||||
126
src/lib/head/Busqueda.svelte
Normal file
126
src/lib/head/Busqueda.svelte
Normal file
@@ -0,0 +1,126 @@
|
||||
<script lang="ts">
|
||||
import CommandDialog from '@/components/ui/command/command-dialog.svelte';
|
||||
import CommandGroup from '@/components/ui/command/command-group.svelte';
|
||||
import CommandInput from '@/components/ui/command/command-input.svelte';
|
||||
import CommandItem from '@/components/ui/command/command-item.svelte';
|
||||
import CommandList from '@/components/ui/command/command-list.svelte';
|
||||
import InputGroupAddon from '@/components/ui/input-group/input-group-addon.svelte';
|
||||
import InputGroupInput from '@/components/ui/input-group/input-group-input.svelte';
|
||||
import InputGroup from '@/components/ui/input-group/input-group.svelte';
|
||||
import Kbd from '@/components/ui/kbd/kbd.svelte';
|
||||
import Search from '@lucide/svelte/icons/search';
|
||||
import User from '@lucide/svelte/icons/user';
|
||||
import Hash from '@lucide/svelte/icons/hash';
|
||||
import Spinner from '@/components/ui/spinner/spinner.svelte';
|
||||
import { busquedaUsuarios } from '@/hooks/busquedaUsuarios';
|
||||
import type { UserResponseDto } from '../../types';
|
||||
import Avatar from '@/components/ui/avatar/avatar.svelte';
|
||||
import AvatarImage from '@/components/ui/avatar/avatar-image.svelte';
|
||||
import Label from '@/components/ui/label/label.svelte';
|
||||
import { resolve } from '$app/paths';
|
||||
import { busquedaHashtags } from '@/hooks/busquedaHashtags';
|
||||
|
||||
let search: string = $state('');
|
||||
let open = $state(false);
|
||||
|
||||
let usuarios: Promise<UserResponseDto[]> | undefined = $state();
|
||||
|
||||
let hashtags: Promise<any[]> | undefined = $state();
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
open = !open;
|
||||
}
|
||||
}
|
||||
// $inspect(usuarios, loading);
|
||||
|
||||
let timeoutId: number | undefined;
|
||||
function buscar() {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
timeoutId = setTimeout(async () => {
|
||||
usuarios = busquedaUsuarios(search);
|
||||
hashtags = busquedaHashtags(search);
|
||||
}, 200);
|
||||
|
||||
return () => {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:document onkeydown={handleKeydown} />
|
||||
|
||||
<InputGroup class="group">
|
||||
<InputGroupAddon align="inline-start"><Search /></InputGroupAddon>
|
||||
<InputGroupInput
|
||||
type="text"
|
||||
placeholder="Buscar Usuario o Hashtag"
|
||||
bind:value={search}
|
||||
oninput={() => (open = true)}
|
||||
class="max-w-0 transition-[max-width] duration-1000 ease-out group-hover:max-w-xs focus:max-w-xs"
|
||||
/>
|
||||
<InputGroupAddon align="inline-end" class="flex gap-0">
|
||||
<Kbd>Ctrl</Kbd>+<Kbd>K</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<CommandDialog bind:open>
|
||||
<CommandInput
|
||||
type="text"
|
||||
placeholder="Buscar Usuario o Hashtag"
|
||||
bind:value={search}
|
||||
oninput={buscar}
|
||||
/>
|
||||
{#if search}
|
||||
<ul class="m-2 space-y-2">
|
||||
<Label class="ms-3 text-sm font-medium text-muted-foreground">Usuarios</Label>
|
||||
{#await usuarios}
|
||||
<li>
|
||||
<Spinner class="ms-2" />
|
||||
</li>
|
||||
{:then usuariosr}
|
||||
{#if usuariosr?.length == 0}
|
||||
<li><p class="ms-2 text-sm">No se encontraron Usuarios</p></li>
|
||||
{/if}
|
||||
{#each usuariosr as usuario}
|
||||
<li class=" w-full cursor-pointer rounded-md hover:bg-accent">
|
||||
<a
|
||||
href={resolve('/[perfil]', { perfil: usuario.username })}
|
||||
class="flex items-center gap-2 px-3 py-2"
|
||||
>
|
||||
{#if usuario.imageUrl}
|
||||
<img src={usuario.imageUrl} alt={usuario.username} class="h-5 w-5 rounded-full" />
|
||||
{:else}
|
||||
<User class="h-5 w-5" />
|
||||
{/if}
|
||||
<span>{usuario.username}</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
{/await}
|
||||
<Label class="ms-3 text-sm font-medium text-muted-foreground">Hashtag</Label>
|
||||
{#await hashtags}
|
||||
<li>
|
||||
<Spinner class="ms-2" />
|
||||
</li>
|
||||
{:then hashtagsResult}
|
||||
{#if hashtagsResult?.length == 0}
|
||||
<li>
|
||||
<p class="ms-2 text-sm">No se encontraron Hashtags</p>
|
||||
</li>
|
||||
{/if}
|
||||
{#each hashtagsResult as hashtag}
|
||||
<a href={resolve('/htag/[htag]', { htag: hashtag })}>
|
||||
<li class="flex cursor-pointer items-center gap-2 rounded-md px-3 py-2 hover:bg-accent">
|
||||
<Hash class="h-5 w-5" />
|
||||
<span>{hashtag}</span>
|
||||
</li>
|
||||
</a>
|
||||
{/each}
|
||||
{/await}
|
||||
</ul>
|
||||
{/if}
|
||||
</CommandDialog>
|
||||
@@ -9,6 +9,9 @@
|
||||
import { apiBase } from '@/stores/url';
|
||||
import { goto } from '$app/navigation';
|
||||
import AvatarButton from './AvatarButton.svelte';
|
||||
import Busqueda from './Busqueda.svelte';
|
||||
import Avatar from '@/components/ui/avatar/avatar.svelte';
|
||||
import AvatarImage from '@/components/ui/avatar/avatar-image.svelte';
|
||||
|
||||
let menuOpen = $state(false);
|
||||
const toggleMenu = () => (menuOpen = !menuOpen);
|
||||
@@ -48,7 +51,11 @@
|
||||
<div class="mx-4 ms-2 flex h-12 items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<a href="/" class="mr-6 flex items-center space-x-2">
|
||||
<p class="leading-7 not-first:mt-6">Mini-X</p>
|
||||
<Avatar
|
||||
class="transform rounded-sm! transition-transform duration-300 ease-in-out hover:scale-130 hover:rotate-12"
|
||||
>
|
||||
<AvatarImage src="/x.png" alt="minix" />
|
||||
</Avatar>
|
||||
</a>
|
||||
<nav class="me-2 items-center space-x-6 text-sm font-medium md:flex">
|
||||
<ButtonTheme />
|
||||
@@ -56,7 +63,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Desktop menu -->
|
||||
<div class="flex items-center justify-end md:flex">
|
||||
<div class="flex items-center justify-end gap-2 md:flex">
|
||||
<Busqueda></Busqueda>
|
||||
{#if showCerrarSesion}
|
||||
{#if $sesionStore !== null}
|
||||
<AvatarButton></AvatarButton>
|
||||
|
||||
21
src/lib/hooks/borrarUsuario.ts
Normal file
21
src/lib/hooks/borrarUsuario.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { apiBase } from '@/stores/url';
|
||||
import { sesionStore } from '@/stores/usuario';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export async function borrarUsuario(id: string) {
|
||||
try {
|
||||
const req = await fetch(`${get(apiBase)}/api/users/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
if (req.ok) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
18
src/lib/hooks/busquedaHashtags.ts
Normal file
18
src/lib/hooks/busquedaHashtags.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { apiBase } from '@/stores/url';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export async function busquedaHashtags(htag: string) {
|
||||
if (!htag) return null;
|
||||
try {
|
||||
const req = await fetch(`${get(apiBase)}/api/htag?q=${htag}`, {
|
||||
method: 'GET'
|
||||
});
|
||||
if (req.ok) {
|
||||
let data = await req.json();
|
||||
return data;
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,11 @@ import { apiBase } from '@/stores/url';
|
||||
import { sesionStore } from '@/stores/usuario';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export async function obtenerCantidadDeUsosdeHtag(htag: string) {
|
||||
export async function obtenerCantidadDeUsosdeHtag(htag: string, fetch2?: Function) {
|
||||
if (!htag) return null;
|
||||
const fetchFn = fetch2 || fetch;
|
||||
try {
|
||||
const req = await fetch(`${get(apiBase)}/api/posts/hashtag/${htag}`, {
|
||||
const req = await fetchFn(`${get(apiBase)}/api/posts/hashtag/${htag}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
|
||||
|
||||
@@ -23,9 +23,11 @@
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{#if page.data.usuarios.length === 0}
|
||||
<CardDescription>No hay posts que mostar</CardDescription>
|
||||
<CardDescription>No hay usuarios que mostar</CardDescription>
|
||||
{:else}
|
||||
<TablaUsuarios bind:usuarios></TablaUsuarios>
|
||||
{#key page.data.usuarios}
|
||||
<TablaUsuarios bind:usuarios></TablaUsuarios>
|
||||
{/key}
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -6,7 +6,8 @@ import type { UserResponseDto } from '../../../types.js';
|
||||
|
||||
export const ssr = false;
|
||||
|
||||
export async function load({}) {
|
||||
export async function load({ depends, fetch }) {
|
||||
depends('admin:load');
|
||||
const response = await fetch(get(apiBase) + '/api/admin/users', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
import Card from '@/components/ui/card/card.svelte';
|
||||
import type { Post } from '../../../types.js';
|
||||
import ModalEditar from '../../[perfil]/modalEditar.svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { updatePostStore } from '@/stores/posts';
|
||||
import { fade, slide } from 'svelte/transition';
|
||||
import { posts, setPosts, updatePostStore } from '@/stores/posts';
|
||||
import { updatePost } from '@/hooks/updatePost';
|
||||
import Separator from '@/components/ui/separator/separator.svelte';
|
||||
import { page } from '$app/state';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface props {
|
||||
data: {
|
||||
@@ -23,11 +24,13 @@
|
||||
|
||||
let { data }: props = $props();
|
||||
|
||||
let posts = $state(data.posts.response);
|
||||
//seteo los posts en el store
|
||||
$effect(() => setPosts(data.posts.response));
|
||||
|
||||
let postAModificar: Post | null = $state(null);
|
||||
|
||||
let postsfiltro = $derived(
|
||||
posts.filter((x) => {
|
||||
$posts?.filter((x) => {
|
||||
const regex = new RegExp(`#${data.htag}\\b`, 'gm');
|
||||
return regex.test(x.content);
|
||||
})
|
||||
@@ -68,8 +71,10 @@
|
||||
<hr class="my-2" />
|
||||
|
||||
<div class="mt-1 flex flex-col gap-3">
|
||||
{#each postsfiltro as post}
|
||||
<PostCard {post} bind:postAModificar />
|
||||
{#each postsfiltro as post (post.id)}
|
||||
<div transition:slide>
|
||||
<PostCard {post} bind:postAModificar />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { obtenerCantidadDeUsosdeHtag } from '@/hooks/obtenerCantidadDeUsosdeHtag.js';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export async function load({ params }) {
|
||||
export async function load({ params, fetch }) {
|
||||
let { htag } = params;
|
||||
|
||||
const posts = await obtenerCantidadDeUsosdeHtag(htag);
|
||||
const posts = await obtenerCantidadDeUsosdeHtag(htag, fetch);
|
||||
// if (cantidad == null || posts.lenght == 0) return error(404, 'no existe el #(hashtag)');
|
||||
return { htag, posts };
|
||||
}
|
||||
|
||||
1
src/types.d.ts
vendored
1
src/types.d.ts
vendored
@@ -90,6 +90,7 @@ export interface UserResponseDto {
|
||||
followingCount: number;
|
||||
createdAt: string;
|
||||
postsCount: number;
|
||||
isAdmin?: bool;
|
||||
}
|
||||
export interface UsersResponseDto {
|
||||
response: {
|
||||
|
||||
Reference in New Issue
Block a user