Merge pull request #58 from emailerfacu-spec/dev

Dev
This commit is contained in:
emailerfacu-spec
2026-01-01 22:07:06 -03:00
committed by GitHub
36 changed files with 3757 additions and 95 deletions
+7 -7
View File
@@ -8,13 +8,13 @@
"mode-watcher": "^1.1.0", "mode-watcher": "^1.1.0",
}, },
"devDependencies": { "devDependencies": {
"@internationalized/date": "^3.8.1", "@internationalized/date": "^3.10.0",
"@lucide/svelte": "^0.544.0", "@lucide/svelte": "^0.561.0",
"@sveltejs/adapter-vercel": "^6.0.0", "@sveltejs/adapter-vercel": "^6.0.0",
"@sveltejs/kit": "^2.47.1", "@sveltejs/kit": "^2.47.1",
"@sveltejs/vite-plugin-svelte": "^6.2.1", "@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tailwindcss/vite": "^4.1.14", "@tailwindcss/vite": "^4.1.14",
"bits-ui": "^2.11.0", "bits-ui": "^2.14.4",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"prettier": "^3.7.4", "prettier": "^3.7.4",
"prettier-plugin-svelte": "^3.4.0", "prettier-plugin-svelte": "^3.4.0",
@@ -22,7 +22,7 @@
"svelte": "^5.41.0", "svelte": "^5.41.0",
"svelte-check": "^4.3.3", "svelte-check": "^4.3.3",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tailwind-variants": "^3.1.1", "tailwind-variants": "^3.2.2",
"tailwindcss": "^4.1.14", "tailwindcss": "^4.1.14",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "^5.9.3", "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=="], "@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=="], "@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=="], "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=="], "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-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=="], "tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="],
+2967
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -14,13 +14,13 @@
"lint": "prettier --check ." "lint": "prettier --check ."
}, },
"devDependencies": { "devDependencies": {
"@internationalized/date": "^3.8.1", "@internationalized/date": "^3.10.0",
"@lucide/svelte": "^0.544.0", "@lucide/svelte": "^0.561.0",
"@sveltejs/adapter-vercel": "^6.0.0", "@sveltejs/adapter-vercel": "^6.0.0",
"@sveltejs/kit": "^2.47.1", "@sveltejs/kit": "^2.47.1",
"@sveltejs/vite-plugin-svelte": "^6.2.1", "@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tailwindcss/vite": "^4.1.14", "@tailwindcss/vite": "^4.1.14",
"bits-ui": "^2.11.0", "bits-ui": "^2.14.4",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"prettier": "^3.7.4", "prettier": "^3.7.4",
"prettier-plugin-svelte": "^3.4.0", "prettier-plugin-svelte": "^3.4.0",
@@ -28,7 +28,7 @@
"svelte": "^5.41.0", "svelte": "^5.41.0",
"svelte-check": "^4.3.3", "svelte-check": "^4.3.3",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tailwind-variants": "^3.1.1", "tailwind-variants": "^3.2.2",
"tailwindcss": "^4.1.14", "tailwindcss": "^4.1.14",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
+46
View 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>
+13 -1
View File
@@ -22,6 +22,17 @@
let image: File | null = $state(null); let image: File | null = $state(null);
let usu = $state({ displayName: data.displayName, bio: data.bio }); 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() { async function cambiarFotoDePerfil() {
const input = document.createElement('input'); const input = document.createElement('input');
input.type = 'file'; input.type = 'file';
@@ -92,7 +103,8 @@
</h1> </h1>
{#if usu.bio} {#if usu.bio}
<p class="mt-4 rounded-4xl bg-accent p-4 text-center text-muted-foreground"> <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>')} --> <!-- {@html usu.bio.replaceAll('\n', '<br>')} -->
</p> </p>
{/if} {/if}
+15 -2
View File
@@ -77,7 +77,12 @@
async function likeHandler() { async function likeHandler() {
cargandoLike = true; 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 (ok) {
if (post.isLiked) { if (post.isLiked) {
post.likesCount--; post.likesCount--;
@@ -89,7 +94,9 @@
errorLike = true; errorLike = true;
mensajeError = message; mensajeError = message;
} }
console.log(1);
updatePostStore(post.id, post); updatePostStore(post.id, post);
console.log(1);
cargandoLike = false; cargandoLike = false;
} }
</script> </script>
@@ -99,12 +106,14 @@
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex gap-3"> <div class="flex gap-3">
{#if post.authorName !== '[deleted]'}
<a href={`/${post.authorName}`}> <a href={`/${post.authorName}`}>
<Avatar> <Avatar>
<AvatarImage src={post.authorImageUrl}></AvatarImage> <AvatarImage src={post.authorImageUrl}></AvatarImage>
<AvatarFallback>{post.authorDisplayName[0].toUpperCase()}</AvatarFallback> <AvatarFallback>{post.authorDisplayName[0].toUpperCase()}</AvatarFallback>
</Avatar> </Avatar>
</a> </a>
{/if}
<div class="flex space-x-2"> <div class="flex space-x-2">
<span class="text-lg font-medium">{post.authorDisplayName}</span> <span class="text-lg font-medium">{post.authorDisplayName}</span>
<span class="text-lg text-muted-foreground">@{post.authorName}</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"> <div class="-mt-2 flex items-center justify-between gap-2 text-xs text-muted-foreground">
<Button <Button
variant="ghost" 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`} class={`${post.isLiked ? 'bg-blue-500/30' : 'bg-accent'} flex items-center gap-2 rounded-full p-3 text-lg`}
onclick={() => likeHandler()} onclick={() => likeHandler()}
> >
<p> <p>
{post.likesCount} {post.likesCount}
</p> </p>
{#if cargandoLike}
<Spinner />
{:else}
<ThumbsUp /> <ThumbsUp />
{/if}
</Button> </Button>
<Button variant="ghost" class="flex items-center gap-2 rounded-full bg-accent p-3 text-lg"> <Button variant="ghost" class="flex items-center gap-2 rounded-full bg-accent p-3 text-lg">
<p> <p>
+52 -15
View File
@@ -9,6 +9,8 @@
import Button from './ui/button/button.svelte'; import Button from './ui/button/button.svelte';
import KeyIcon from '@lucide/svelte/icons/key'; import KeyIcon from '@lucide/svelte/icons/key';
import UserPen from '@lucide/svelte/icons/user-pen'; 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 { Tooltip } from './ui/tooltip';
import TooltipTrigger from './ui/tooltip/tooltip-trigger.svelte'; import TooltipTrigger from './ui/tooltip/tooltip-trigger.svelte';
import TooltipContent from './ui/tooltip/tooltip-content.svelte'; import TooltipContent from './ui/tooltip/tooltip-content.svelte';
@@ -19,6 +21,12 @@
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import type { Unsubscriber } from 'svelte/store'; import type { Unsubscriber } from 'svelte/store';
import Input from './ui/input/input.svelte'; 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 { interface Props {
usuarios: UserResponseDto[]; usuarios: UserResponseDto[];
@@ -29,6 +37,9 @@
let open = $state(false); let open = $state(false);
let openModificarUsuario = $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 //si ponia contraseña en español quedaba muy largo el nombre
let usuarioCambioPass: UserResponseDto | null = $state(null); let usuarioCambioPass: UserResponseDto | null = $state(null);
@@ -84,12 +95,6 @@
return sortDirection === 'asc' ? '↑' : '↓'; return sortDirection === 'asc' ? '↑' : '↓';
} }
$effect(() => {
if (!open) {
usuarioCambioPass = null;
}
});
function handleCambiarContraseña(usuario: UserResponseDto) { function handleCambiarContraseña(usuario: UserResponseDto) {
open = true; open = true;
usuarioCambioPass = usuario; usuarioCambioPass = usuario;
@@ -99,15 +104,22 @@
openModificarUsuario = true; openModificarUsuario = true;
usuarioModificar = usuario; usuarioModificar = usuario;
} }
function handleBorrar(usuario: UserResponseDto) {
openBorrar = true;
usuarioBorrar = usuario;
}
let opencrearUsuario = $state(false);
// $inspect(usuarios);
</script> </script>
<div class="mb-4"> <div class="mb-4 flex gap-2">
<Input <InputGroup>
type="text" <InputGroupAddon align="inline-start"><Search></Search></InputGroupAddon>
placeholder="Buscar usuario..." <InputGroupInput type="text" placeholder="Buscar usuario..." bind:value={search} />
bind:value={search} </InputGroup>
class="w-full rounded border px-3 py-2" <Button onclick={() =>opencrearUsuario = !opencrearUsuario} variant="secondary" class="bg-blue-500/20"><Plus /></Button>
/>
</div> </div>
<Table> <Table>
@@ -129,6 +141,12 @@
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{#if usuariosFiltrados.length == 0}
<TableRow>
<TableCell colspan={5}>
<p class="text-center">No hay usuarios por el nombre de: {search}</p>
</TableCell>
</TableRow>{:else}
{#each usuariosFiltrados as usuario} {#each usuariosFiltrados as usuario}
<TableRow> <TableRow>
<TableCell <TableCell
@@ -142,7 +160,8 @@
<TableCell class="flex gap-2"> <TableCell class="flex gap-2">
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
<Button onclick={() => handleCambiarContraseña(usuario)}><KeyIcon></KeyIcon></Button> <Button onclick={() => handleCambiarContraseña(usuario)}><KeyIcon></KeyIcon></Button
>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>Recuperar Contraseña</p> <p>Recuperar Contraseña</p>
@@ -156,11 +175,29 @@
<p>Modificar Usuario</p> <p>Modificar Usuario</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
<Button></Button> <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> </TableCell>
</TableRow> </TableRow>
{/each} {/each}
{/if}
</TableBody> </TableBody>
</Table> </Table>
<BorrarUsuario bind:open={openBorrar} usuario={usuarioBorrar} />
<RecuperarContraseña bind:open usuario={usuarioCambioPass} /> <RecuperarContraseña bind:open usuario={usuarioCambioPass} />
<ModificarUsuario bind:open={openModificarUsuario} bind:usuario={usuarioModificar} /> <ModificarUsuario bind:open={openModificarUsuario} bind:usuario={usuarioModificar} />
<AgregarUsuario bind:open={opencrearUsuario} />
@@ -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>
+5 -5
View File
@@ -4,15 +4,15 @@
import { type VariantProps, tv } from "tailwind-variants"; import { type VariantProps, tv } from "tailwind-variants";
export const buttonVariants = tv({ 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: { variants: {
variant: { 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: 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: 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", "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 shadow-xs hover:bg-secondary/80", 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", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
}, },
@@ -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>
@@ -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}
/>
@@ -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>
@@ -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>
@@ -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}
/>
@@ -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}
/>
@@ -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}
/>
@@ -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} />
@@ -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}
/>
@@ -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>
@@ -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
View 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"> <script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui"; import { Dialog as DialogPrimitive } from "bits-ui";
import DialogPortal from "./dialog-portal.svelte";
import XIcon from "@lucide/svelte/icons/x"; import XIcon from "@lucide/svelte/icons/x";
import type { Snippet } from "svelte"; import type { Snippet } from "svelte";
import * as Dialog from "./index.js"; import * as Dialog from "./index.js";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js"; import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -13,19 +15,19 @@
showCloseButton = true, showCloseButton = true,
...restProps ...restProps
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & { }: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
portalProps?: DialogPrimitive.PortalProps; portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DialogPortal>>;
children: Snippet; children: Snippet;
showCloseButton?: boolean; showCloseButton?: boolean;
} = $props(); } = $props();
</script> </script>
<Dialog.Portal {...portalProps}> <DialogPortal {...portalProps}>
<Dialog.Overlay /> <Dialog.Overlay />
<DialogPrimitive.Content <DialogPrimitive.Content
bind:ref bind:ref
data-slot="dialog-content" data-slot="dialog-content"
class={cn( 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 className
)} )}
{...restProps} {...restProps}
@@ -33,11 +35,11 @@
{@render children?.()} {@render children?.()}
{#if showCloseButton} {#if showCloseButton}
<DialogPrimitive.Close <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 /> <XIcon />
<span class="sr-only">Close</span> <span class="sr-only">Close</span>
</DialogPrimitive.Close> </DialogPrimitive.Close>
{/if} {/if}
</DialogPrimitive.Content> </DialogPrimitive.Content>
</Dialog.Portal> </DialogPortal>
@@ -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 <DialogPrimitive.Title
bind:ref bind:ref
data-slot="dialog-title" data-slot="dialog-title"
class={cn("text-lg font-semibold leading-none", className)} class={cn("text-lg leading-none font-semibold", className)}
{...restProps} {...restProps}
/> />
@@ -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} />
+2 -5
View File
@@ -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 Title from "./dialog-title.svelte";
import Footer from "./dialog-footer.svelte"; import Footer from "./dialog-footer.svelte";
import Header from "./dialog-header.svelte"; import Header from "./dialog-header.svelte";
@@ -9,9 +9,6 @@ import Description from "./dialog-description.svelte";
import Trigger from "./dialog-trigger.svelte"; import Trigger from "./dialog-trigger.svelte";
import Close from "./dialog-close.svelte"; import Close from "./dialog-close.svelte";
const Root = DialogPrimitive.Root;
const Portal = DialogPrimitive.Portal;
export { export {
Root, Root,
Title, Title,
+126
View 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>
+10 -2
View File
@@ -9,6 +9,9 @@
import { apiBase } from '@/stores/url'; import { apiBase } from '@/stores/url';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import AvatarButton from './AvatarButton.svelte'; 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); let menuOpen = $state(false);
const toggleMenu = () => (menuOpen = !menuOpen); const toggleMenu = () => (menuOpen = !menuOpen);
@@ -48,7 +51,11 @@
<div class="mx-4 ms-2 flex h-12 items-center justify-between"> <div class="mx-4 ms-2 flex h-12 items-center justify-between">
<div class="flex items-center"> <div class="flex items-center">
<a href="/" class="mr-6 flex items-center space-x-2"> <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> </a>
<nav class="me-2 items-center space-x-6 text-sm font-medium md:flex"> <nav class="me-2 items-center space-x-6 text-sm font-medium md:flex">
<ButtonTheme /> <ButtonTheme />
@@ -56,7 +63,8 @@
</div> </div>
<!-- Desktop menu --> <!-- 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 showCerrarSesion}
{#if $sesionStore !== null} {#if $sesionStore !== null}
<AvatarButton></AvatarButton> <AvatarButton></AvatarButton>
+21
View 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
View 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;
}
}
+3 -2
View File
@@ -2,10 +2,11 @@ import { apiBase } from '@/stores/url';
import { sesionStore } from '@/stores/usuario'; import { sesionStore } from '@/stores/usuario';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
export async function obtenerCantidadDeUsosdeHtag(htag: string) { export async function obtenerCantidadDeUsosdeHtag(htag: string, fetch2?: Function) {
if (!htag) return null; if (!htag) return null;
const fetchFn = fetch2 || fetch;
try { try {
const req = await fetch(`${get(apiBase)}/api/posts/hashtag/${htag}`, { const req = await fetchFn(`${get(apiBase)}/api/posts/hashtag/${htag}`, {
method: 'GET', method: 'GET',
headers: { headers: {
Authorization: `Bearer ${get(sesionStore)?.accessToken}` Authorization: `Bearer ${get(sesionStore)?.accessToken}`
+3 -1
View File
@@ -23,9 +23,11 @@
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{#if page.data.usuarios.length === 0} {#if page.data.usuarios.length === 0}
<CardDescription>No hay posts que mostar</CardDescription> <CardDescription>No hay usuarios que mostar</CardDescription>
{:else} {:else}
{#key page.data.usuarios}
<TablaUsuarios bind:usuarios></TablaUsuarios> <TablaUsuarios bind:usuarios></TablaUsuarios>
{/key}
{/if} {/if}
</CardContent> </CardContent>
</Card> </Card>
+2 -1
View File
@@ -6,7 +6,8 @@ import type { UserResponseDto } from '../../../types.js';
export const ssr = false; 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', { const response = await fetch(get(apiBase) + '/api/admin/users', {
method: 'GET', method: 'GET',
headers: { headers: {
+10 -5
View File
@@ -5,11 +5,12 @@
import Card from '@/components/ui/card/card.svelte'; import Card from '@/components/ui/card/card.svelte';
import type { Post } from '../../../types.js'; import type { Post } from '../../../types.js';
import ModalEditar from '../../[perfil]/modalEditar.svelte'; import ModalEditar from '../../[perfil]/modalEditar.svelte';
import { fade } from 'svelte/transition'; import { fade, slide } from 'svelte/transition';
import { updatePostStore } from '@/stores/posts'; import { posts, setPosts, updatePostStore } from '@/stores/posts';
import { updatePost } from '@/hooks/updatePost'; import { updatePost } from '@/hooks/updatePost';
import Separator from '@/components/ui/separator/separator.svelte'; import Separator from '@/components/ui/separator/separator.svelte';
import { page } from '$app/state'; import { page } from '$app/state';
import { onMount } from 'svelte';
interface props { interface props {
data: { data: {
@@ -23,11 +24,13 @@
let { data }: props = $props(); 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 postAModificar: Post | null = $state(null);
let postsfiltro = $derived( let postsfiltro = $derived(
posts.filter((x) => { $posts?.filter((x) => {
const regex = new RegExp(`#${data.htag}\\b`, 'gm'); const regex = new RegExp(`#${data.htag}\\b`, 'gm');
return regex.test(x.content); return regex.test(x.content);
}) })
@@ -68,8 +71,10 @@
<hr class="my-2" /> <hr class="my-2" />
<div class="mt-1 flex flex-col gap-3"> <div class="mt-1 flex flex-col gap-3">
{#each postsfiltro as post} {#each postsfiltro as post (post.id)}
<div transition:slide>
<PostCard {post} bind:postAModificar /> <PostCard {post} bind:postAModificar />
</div>
{/each} {/each}
</div> </div>
</div> </div>
+2 -3
View File
@@ -1,10 +1,9 @@
import { obtenerCantidadDeUsosdeHtag } from '@/hooks/obtenerCantidadDeUsosdeHtag.js'; 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; 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)'); // if (cantidad == null || posts.lenght == 0) return error(404, 'no existe el #(hashtag)');
return { htag, posts }; return { htag, posts };
} }
+1
View File
@@ -90,6 +90,7 @@ export interface UserResponseDto {
followingCount: number; followingCount: number;
createdAt: string; createdAt: string;
postsCount: number; postsCount: number;
isAdmin?: bool;
} }
export interface UsersResponseDto { export interface UsersResponseDto {
response: { response: {