mirror of
https://github.com/emailerfacu-spec/minix-front.git
synced 2026-04-19 16:07:32 -03:00
@@ -1,6 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Ellipsis from '@lucide/svelte/icons/ellipsis';
|
import Ellipsis from '@lucide/svelte/icons/ellipsis';
|
||||||
import Trash2 from '@lucide/svelte/icons/trash-2';
|
import Trash2 from '@lucide/svelte/icons/trash-2';
|
||||||
|
import ThumbsUp from '@lucide/svelte/icons/thumbs-up';
|
||||||
|
import MessageCircle from '@lucide/svelte/icons/message-circle-more';
|
||||||
import Pen from '@lucide/svelte/icons/pen';
|
import Pen from '@lucide/svelte/icons/pen';
|
||||||
import type { Post } from '../../types';
|
import type { Post } from '../../types';
|
||||||
import Button from './ui/button/button.svelte';
|
import Button from './ui/button/button.svelte';
|
||||||
@@ -27,6 +29,7 @@
|
|||||||
import DialogTitle from './ui/dialog/dialog-title.svelte';
|
import DialogTitle from './ui/dialog/dialog-title.svelte';
|
||||||
import DialogDescription from './ui/dialog/dialog-description.svelte';
|
import DialogDescription from './ui/dialog/dialog-description.svelte';
|
||||||
import { sesionStore } from '@/stores/usuario';
|
import { sesionStore } from '@/stores/usuario';
|
||||||
|
import { likePost } from '@/hooks/likePost';
|
||||||
|
|
||||||
interface postProp {
|
interface postProp {
|
||||||
post: Post;
|
post: Post;
|
||||||
@@ -38,6 +41,8 @@
|
|||||||
let cargandoBorrar = $state(false);
|
let cargandoBorrar = $state(false);
|
||||||
let mensajeError = $state('');
|
let mensajeError = $state('');
|
||||||
let cargandoEditar = $state(false);
|
let cargandoEditar = $state(false);
|
||||||
|
let cargandoLike = $state(false);
|
||||||
|
let errorLike = $state(false);
|
||||||
|
|
||||||
async function handleBorrar() {
|
async function handleBorrar() {
|
||||||
await deletePost(
|
await deletePost(
|
||||||
@@ -50,9 +55,27 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleEditar() {
|
function handleEditar() {
|
||||||
postAModificar = post;
|
postAModificar = post;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function likeHandler() {
|
||||||
|
cargandoLike = true;
|
||||||
|
let { message, ok } = await likePost(post);
|
||||||
|
if (ok) {
|
||||||
|
if (post.isLiked) {
|
||||||
|
post.likesCount--;
|
||||||
|
} else {
|
||||||
|
post.likesCount++;
|
||||||
|
}
|
||||||
|
post.isLiked = !post.isLiked;
|
||||||
|
} else {
|
||||||
|
errorLike = true;
|
||||||
|
mensajeError = message;
|
||||||
|
}
|
||||||
|
updatePostStore(post.id, post);
|
||||||
|
cargandoLike = false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
@@ -60,10 +83,12 @@
|
|||||||
<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">
|
||||||
<Avatar>
|
<a href={`/${post.authorName}`}>
|
||||||
<AvatarImage></AvatarImage>
|
<Avatar>
|
||||||
<AvatarFallback>{post.authorDisplayName[0].toUpperCase()}</AvatarFallback>
|
<AvatarImage></AvatarImage>
|
||||||
</Avatar>
|
<AvatarFallback>{post.authorDisplayName[0].toUpperCase()}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
</a>
|
||||||
<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>
|
||||||
@@ -72,7 +97,7 @@
|
|||||||
{#if post.authorName === $sesionStore?.username}
|
{#if post.authorName === $sesionStore?.username}
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger>
|
||||||
<Button variant="ghost" class="rounded-full"><Ellipsis /></Button>
|
<Button variant="ghost" class=" rounded-full bg-accent"><Ellipsis /></Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
@@ -101,16 +126,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<Content>
|
<Content class="mx-5 -mt-4 rounded-full bg-accent p-6">
|
||||||
<p class="text-sm">{post.content}</p>
|
<p class=" text-sm">{post.content}</p>
|
||||||
{#if post.imageUrl}
|
{#if post.imageUrl}
|
||||||
<img src={post.imageUrl} alt="Post" class="mt-2 rounded-md" />
|
<img src={post.imageUrl} alt="Post" class="mt-2 rounded-md" />
|
||||||
{/if}
|
{/if}
|
||||||
</Content>
|
</Content>
|
||||||
<CardFooter>
|
<CardFooter>
|
||||||
<div class="flex items-center justify-between gap-2 pt-2 text-xs text-muted-foreground">
|
<div class="-mt-2 flex items-center justify-between gap-2 text-xs text-muted-foreground">
|
||||||
<span>{post.likesCount} likes</span>
|
<Button
|
||||||
<span>{post.repliesCount} replies</span>
|
variant="ghost"
|
||||||
|
disabled={!$sesionStore?.accessToken}
|
||||||
|
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 />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" class="flex items-center gap-2 rounded-full bg-accent p-3 text-lg">
|
||||||
|
<p>
|
||||||
|
{post.repliesCount}
|
||||||
|
</p>
|
||||||
|
<MessageCircle />
|
||||||
|
</Button>
|
||||||
|
|
||||||
<span class="text-xs text-muted-foreground"
|
<span class="text-xs text-muted-foreground"
|
||||||
>{post.createdAt.replace('T', ' ').split('.')[0]}</span
|
>{post.createdAt.replace('T', ' ').split('.')[0]}</span
|
||||||
>
|
>
|
||||||
@@ -120,7 +161,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
{#if mensajeError}
|
{#if mensajeError || errorLike}
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|||||||
177
src/lib/components/TablaUsuarios.svelte
Normal file
177
src/lib/components/TablaUsuarios.svelte
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import TableBody from '@/components/ui/table/table-body.svelte';
|
||||||
|
import TableCell from '@/components/ui/table/table-cell.svelte';
|
||||||
|
import TableHead from '@/components/ui/table/table-head.svelte';
|
||||||
|
import TableHeader from '@/components/ui/table/table-header.svelte';
|
||||||
|
import TableRow from '@/components/ui/table/table-row.svelte';
|
||||||
|
import Table from '@/components/ui/table/table.svelte';
|
||||||
|
import type { UserResponseDto } from '../../types';
|
||||||
|
import Button from './ui/button/button.svelte';
|
||||||
|
import KeyIcon from '@lucide/svelte/icons/key';
|
||||||
|
import UserPen from '@lucide/svelte/icons/user-pen';
|
||||||
|
import { Tooltip } from './ui/tooltip';
|
||||||
|
import TooltipTrigger from './ui/tooltip/tooltip-trigger.svelte';
|
||||||
|
import TooltipContent from './ui/tooltip/tooltip-content.svelte';
|
||||||
|
import RecuperarContraseña from './admin/RecuperarContraseña.svelte';
|
||||||
|
import { Dialog } from './ui/dialog';
|
||||||
|
import DialogContent from './ui/dialog/dialog-content.svelte';
|
||||||
|
import ModificarUsuario from './admin/ModificarUsuario.svelte';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import type { Unsubscriber } from 'svelte/store';
|
||||||
|
import Input from './ui/input/input.svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
usuarios: UserResponseDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
let { usuarios = $bindable() }: Props = $props();
|
||||||
|
|
||||||
|
let open = $state(false);
|
||||||
|
let openModificarUsuario = $state(false);
|
||||||
|
|
||||||
|
//si ponia contraseña en español quedaba muy largo el nombre
|
||||||
|
let usuarioCambioPass: UserResponseDto | null = $state(null);
|
||||||
|
|
||||||
|
let usuarioModificar: UserResponseDto | null = $state(null);
|
||||||
|
|
||||||
|
let search = $state("");
|
||||||
|
|
||||||
|
type SortKey = 'username' | 'displayName' | 'postsCount' | 'createdAt';
|
||||||
|
let sortBy = $state<SortKey | null>(null);
|
||||||
|
let sortDirection = $state<'asc' | 'desc'>('asc');
|
||||||
|
|
||||||
|
function ordenarPor(campo: SortKey) {
|
||||||
|
if (sortBy === campo) {
|
||||||
|
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
||||||
|
} else {
|
||||||
|
sortBy = campo;
|
||||||
|
sortDirection = 'asc';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let usuariosFiltrados = $derived(
|
||||||
|
usuarios
|
||||||
|
.filter((u) =>
|
||||||
|
u.username.toLowerCase().startsWith(search.toLowerCase()) ||
|
||||||
|
u.displayName.toLowerCase().startsWith(search.toLowerCase())
|
||||||
|
)
|
||||||
|
.toSorted((a, b) => {
|
||||||
|
if (!sortBy) return 0;
|
||||||
|
|
||||||
|
const key: SortKey = sortBy;
|
||||||
|
|
||||||
|
if (key === 'createdAt') {
|
||||||
|
const ta = new Date(a.createdAt).getTime();
|
||||||
|
const tb = new Date(b.createdAt).getTime();
|
||||||
|
return sortDirection === 'asc' ? ta - tb : tb - ta;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'postsCount') {
|
||||||
|
return sortDirection === 'asc'
|
||||||
|
? a.postsCount - b.postsCount
|
||||||
|
: b.postsCount - a.postsCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sa = a[key].toString().toLowerCase();
|
||||||
|
const sb = b[key].toString().toLowerCase();
|
||||||
|
return sortDirection === 'asc'
|
||||||
|
? sa.localeCompare(sb)
|
||||||
|
: sb.localeCompare(sa);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
function getSortIcon(campo: SortKey) {
|
||||||
|
if (sortBy !== campo) return ""; // no ícono si no está ordenando por esa columna
|
||||||
|
return sortDirection === "asc" ? "↑" : "↓"; // ascendente / descendente
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//let usuariosFiltrados = $derived(
|
||||||
|
//usuarios.filter((u) =>
|
||||||
|
// u.username.toLowerCase().startsWith(search.toLowerCase()) ||
|
||||||
|
// u.displayName.toLowerCase().startsWith(search.toLowerCase())
|
||||||
|
// )
|
||||||
|
//);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!open) {
|
||||||
|
usuarioCambioPass = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleCambiarContraseña(usuario: UserResponseDto) {
|
||||||
|
open = true;
|
||||||
|
usuarioCambioPass = usuario;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleModificar(usuario: UserResponseDto) {
|
||||||
|
openModificarUsuario = true;
|
||||||
|
usuarioModificar = usuario;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<Input type= "text"
|
||||||
|
placeholder="Buscar usuario..."
|
||||||
|
bind:value={search}
|
||||||
|
class="border px-3 py-2 rounded w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead onclick={() => ordenarPor("username")} class="cursor-pointer select-none">
|
||||||
|
Usuario {getSortIcon("username")}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead onclick={() => ordenarPor("displayName")} class="cursor-pointer select-none">
|
||||||
|
Nombre {getSortIcon("displayName")}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead onclick={() => ordenarPor("postsCount")} class="cursor-pointer select-none">
|
||||||
|
Cantidad de posts {getSortIcon("postsCount")}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead onclick={() => ordenarPor("createdAt")} class="cursor-pointer select-none">
|
||||||
|
Fecha de Creacion {getSortIcon("createdAt")}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>Acciones</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{#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>
|
||||||
|
<Button></Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
{/each}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<RecuperarContraseña bind:open usuario={usuarioCambioPass} />
|
||||||
|
<ModificarUsuario bind:open={openModificarUsuario} bind:usuario={usuarioModificar} />
|
||||||
103
src/lib/components/admin/ModificarUsuario.svelte
Normal file
103
src/lib/components/admin/ModificarUsuario.svelte
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<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 type { UserResponseDto } from '../../../types';
|
||||||
|
import Checkbox from '../ui/checkbox/checkbox.svelte';
|
||||||
|
import Label from '../ui/label/label.svelte';
|
||||||
|
import Spinner from '../ui/spinner/spinner.svelte';
|
||||||
|
import { updateUsuario } from '@/hooks/updateUsuario';
|
||||||
|
|
||||||
|
interface Prop {
|
||||||
|
open: boolean;
|
||||||
|
usuario: UserResponseDto | null;
|
||||||
|
}
|
||||||
|
let { open = $bindable(), usuario = $bindable() }: Prop = $props();
|
||||||
|
|
||||||
|
let imagen = $state(!!usuario?.profileImageUrl);
|
||||||
|
let fallback = usuario?.displayName;
|
||||||
|
|
||||||
|
let cargando = $state(false);
|
||||||
|
let error = $state('');
|
||||||
|
async function onsubmit(e: SubmitEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
cargando = true;
|
||||||
|
let ret: { displayName: string } | string = await updateUsuario({
|
||||||
|
id: usuario?.id || '',
|
||||||
|
bio: usuario?.bio || '',
|
||||||
|
displayName: usuario?.displayName || '',
|
||||||
|
oldImageUrl: usuario?.profileImageUrl || '',
|
||||||
|
profileImage: imagen
|
||||||
|
});
|
||||||
|
if (typeof ret === 'string') {
|
||||||
|
error = ret;
|
||||||
|
} else {
|
||||||
|
usuario!.displayName = ret.displayName;
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
cargando = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div transition:fade>
|
||||||
|
<Dialog
|
||||||
|
{open}
|
||||||
|
onOpenChange={() => {
|
||||||
|
open = !open;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Modificar Usuario</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<form {onsubmit}>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupInput disabled={cargando} bind:value={usuario!.displayName} />
|
||||||
|
<InputGroupAddon>Nombre</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupInput disabled={cargando} bind:value={usuario!.bio} />
|
||||||
|
<InputGroupAddon>Bio</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
<div class="ms-1 flex gap-2">
|
||||||
|
<Checkbox disabled={usuario?.profileImageUrl == null || cargando} bind:checked={imagen}
|
||||||
|
></Checkbox>
|
||||||
|
<Label>
|
||||||
|
{#if usuario?.profileImageUrl == null}
|
||||||
|
No tiene imagen de perfil
|
||||||
|
{:else if imagen}
|
||||||
|
Borrar imagen
|
||||||
|
{:else}
|
||||||
|
Mantener imagen
|
||||||
|
{/if}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<hr class="my-2" />
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<Button type="submit" disabled={cargando}>
|
||||||
|
{#if cargando}
|
||||||
|
<Spinner /> Cargando...
|
||||||
|
{:else}
|
||||||
|
Aceptar
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" disabled={cargando} onclick={() => (open = !open)}
|
||||||
|
>Cerrar</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
<div transition:fade>
|
||||||
|
<Dialog open={error != ''} onOpenChange={() => (error = '')}>
|
||||||
|
<DialogContent>
|
||||||
|
{error}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
88
src/lib/components/admin/RecuperarContraseña.svelte
Normal file
88
src/lib/components/admin/RecuperarContraseña.svelte
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Key from '@lucide/svelte/icons/key';
|
||||||
|
import { Dialog } from '../ui/dialog';
|
||||||
|
import DialogContent from '../ui/dialog/dialog-content.svelte';
|
||||||
|
import InputGroupAddon from '../ui/input-group/input-group-addon.svelte';
|
||||||
|
import InputGroupInput from '../ui/input-group/input-group-input.svelte';
|
||||||
|
import InputGroup from '../ui/input-group/input-group.svelte';
|
||||||
|
import DialogTitle from '../ui/dialog/dialog-title.svelte';
|
||||||
|
import type { UserResponseDto } from '../../../types';
|
||||||
|
import DialogHeader from '../ui/dialog/dialog-header.svelte';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import { cambiarContraseña } from '@/hooks/cambiarContraseña';
|
||||||
|
import Button from '../ui/button/button.svelte';
|
||||||
|
import Spinner from '../ui/spinner/spinner.svelte';
|
||||||
|
|
||||||
|
interface Prop {
|
||||||
|
open: boolean;
|
||||||
|
usuario: UserResponseDto | null;
|
||||||
|
}
|
||||||
|
let { open = $bindable(), usuario }: Prop = $props();
|
||||||
|
|
||||||
|
let cargando = $state(false);
|
||||||
|
let error = $state('');
|
||||||
|
let nuevapass = $state('');
|
||||||
|
let openMensaje = $state(false);
|
||||||
|
|
||||||
|
async function onsubmit(e: SubmitEvent) {
|
||||||
|
cargando = true;
|
||||||
|
error = await cambiarContraseña(usuario!, nuevapass);
|
||||||
|
if (error === '') {
|
||||||
|
open = false;
|
||||||
|
nuevapass = '';
|
||||||
|
}
|
||||||
|
openMensaje = true;
|
||||||
|
cargando = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div transition:fade>
|
||||||
|
<Dialog open={open && !!usuario} onOpenChange={() => (open = !open)}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle><p>Cambiar Contraseña</p></DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<form {onsubmit}>
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupInput
|
||||||
|
disabled={cargando}
|
||||||
|
type="password"
|
||||||
|
name=""
|
||||||
|
minlength={8}
|
||||||
|
bind:value={nuevapass}
|
||||||
|
/>
|
||||||
|
<InputGroupAddon><Key /></InputGroupAddon>
|
||||||
|
<InputGroupAddon align="inline-end">
|
||||||
|
<p
|
||||||
|
class={{
|
||||||
|
'text-red-500': nuevapass.length < 8,
|
||||||
|
'text-blue-500': nuevapass.length >= 8
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{nuevapass.length}
|
||||||
|
</p>
|
||||||
|
/ 8
|
||||||
|
</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
<div class="mt-2 flex justify-between">
|
||||||
|
<Button type="submit" disabled={cargando}>
|
||||||
|
{#if cargando}
|
||||||
|
<Spinner />
|
||||||
|
{:else}
|
||||||
|
Aceptar
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" onclick={() => (open = false)} disabled={cargando}
|
||||||
|
>Cerrar</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
<div transition:fade>
|
||||||
|
<Dialog open={openMensaje} onOpenChange={() => (openMensaje = false)}>
|
||||||
|
<!-- {$inspect(error)} -->
|
||||||
|
<DialogContent>{error === '' ? 'Se modificó el usuario' : error}</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
@@ -4,13 +4,14 @@
|
|||||||
import InputGroupTextarea from './ui/input-group/input-group-textarea.svelte';
|
import InputGroupTextarea from './ui/input-group/input-group-textarea.svelte';
|
||||||
import InputGroup from './ui/input-group/input-group.svelte';
|
import InputGroup from './ui/input-group/input-group.svelte';
|
||||||
import ArrowUpIcon from '@lucide/svelte/icons/arrow-up';
|
import ArrowUpIcon from '@lucide/svelte/icons/arrow-up';
|
||||||
|
import Loader2Icon from '@lucide/svelte/icons/loader-2';
|
||||||
import Kbd from './ui/kbd/kbd.svelte';
|
import Kbd from './ui/kbd/kbd.svelte';
|
||||||
|
|
||||||
import { apiBase } from '@/stores/url';
|
import { apiBase } from '@/stores/url';
|
||||||
import { sesionStore } from '@/stores/usuario';
|
import { sesionStore } from '@/stores/usuario';
|
||||||
import type { CreatePostDto } from '../../types';
|
import type { CreatePostDto } from '../../types';
|
||||||
import { addPost } from '@/stores/posts';
|
import { addPost } from '@/stores/posts';
|
||||||
import { Tooltip, TooltipProvider } from './ui/tooltip';
|
import { Tooltip } from './ui/tooltip';
|
||||||
import TooltipContent from './ui/tooltip/tooltip-content.svelte';
|
import TooltipContent from './ui/tooltip/tooltip-content.svelte';
|
||||||
import TooltipTrigger from './ui/tooltip/tooltip-trigger.svelte';
|
import TooltipTrigger from './ui/tooltip/tooltip-trigger.svelte';
|
||||||
|
|
||||||
@@ -22,21 +23,18 @@
|
|||||||
async function handlePost(e: Event) {
|
async function handlePost(e: Event) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
const data: CreatePostDto = {
|
const formData = new FormData();
|
||||||
content: mensaje,
|
formData.append('content', mensaje);
|
||||||
imageUrl: null,
|
// formData.append('imageUrl', '');
|
||||||
parentPostId: null
|
// formData.append('parentPostId', '');
|
||||||
};
|
|
||||||
|
|
||||||
const req = fetch($apiBase + '/api/posts', {
|
const req = fetch($apiBase + '/api/posts', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
//credentials: 'include',
|
//credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${$sesionStore?.accessToken}`
|
Authorization: `Bearer ${$sesionStore?.accessToken}`
|
||||||
},
|
},
|
||||||
|
body: formData
|
||||||
body: JSON.stringify(data)
|
|
||||||
});
|
});
|
||||||
cargando = true;
|
cargando = true;
|
||||||
|
|
||||||
@@ -79,24 +77,28 @@
|
|||||||
</p>
|
</p>
|
||||||
/ 280
|
/ 280
|
||||||
</Kbd>
|
</Kbd>
|
||||||
<TooltipProvider>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger class="*: flex">
|
||||||
<TooltipTrigger class="*: flex">
|
<InputGroupButton
|
||||||
<InputGroupButton
|
variant="default"
|
||||||
variant="default"
|
disabled={cargando}
|
||||||
type="submit"
|
type="submit"
|
||||||
class="transform rounded-full transition-transform ease-in hover:scale-120"
|
class="transform rounded-full transition-transform ease-in hover:scale-120"
|
||||||
size="xs"
|
size="xs"
|
||||||
>
|
>
|
||||||
<p>Publicar</p>
|
{#if cargando}
|
||||||
|
<Loader2Icon class="animate-spin" />
|
||||||
|
Publicando...
|
||||||
|
{:else}
|
||||||
|
Publicar
|
||||||
<ArrowUpIcon class="mt-0.5 h-3.5! w-3.5!" />
|
<ArrowUpIcon class="mt-0.5 h-3.5! w-3.5!" />
|
||||||
</InputGroupButton>
|
{/if}
|
||||||
</TooltipTrigger>
|
</InputGroupButton>
|
||||||
<TooltipContent>
|
</TooltipTrigger>
|
||||||
<Kbd>Ctrl</Kbd>+<Kbd>Enter</Kbd>
|
<TooltipContent>
|
||||||
</TooltipContent>
|
<Kbd>Ctrl</Kbd>+<Kbd>Enter</Kbd>
|
||||||
</Tooltip>
|
</TooltipContent>
|
||||||
</TooltipProvider>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</InputGroupAddon>
|
</InputGroupAddon>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|||||||
@@ -5,13 +5,55 @@
|
|||||||
import { Input } from '$lib/components/ui/input/index.js';
|
import { Input } from '$lib/components/ui/input/index.js';
|
||||||
import type { RegisterDto } from '../../types';
|
import type { RegisterDto } from '../../types';
|
||||||
import { register } from '@/hooks/register';
|
import { register } from '@/hooks/register';
|
||||||
|
import Loader2Icon from '@lucide/svelte/icons/loader-2';
|
||||||
|
import Check from '@lucide/svelte/icons/check';
|
||||||
|
import Cross from '@lucide/svelte/icons/x';
|
||||||
|
|
||||||
let {showAlert = $bindable() } = $props();
|
import Spinner from './ui/spinner/spinner.svelte';
|
||||||
|
import { checkUsername } from '@/hooks/checkUsername';
|
||||||
|
import { checkEmail } from '@/hooks/checkEmail';
|
||||||
|
|
||||||
const setAlert = () => showAlert = true;
|
let { showAlert = $bindable() } = $props();
|
||||||
|
|
||||||
let dto: RegisterDto = $state({password: "", username: "", email:"", displayName: ""});
|
let cargando = $state(false);
|
||||||
|
|
||||||
|
let repetirContraseña = $state('');
|
||||||
|
|
||||||
|
let checkeandoUsuario: boolean | null = $state(null);
|
||||||
|
let esUsuarioValido = $state(false);
|
||||||
|
|
||||||
|
let checkeandoEmail: boolean | null = $state(null);
|
||||||
|
let esEmailValido = $state(false);
|
||||||
|
|
||||||
|
let dto: RegisterDto = $state({ password: '', username: '', email: '', displayName: '' });
|
||||||
|
|
||||||
|
let coinsidenLasPass = $derived(repetirContraseña == dto.password);
|
||||||
|
|
||||||
|
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9])[A-Za-z\d\W_]*$/;
|
||||||
|
let esContraseñaValida = $derived(passwordRegex.test(dto.password));
|
||||||
|
|
||||||
|
async function checkUsuario() {
|
||||||
|
checkeandoUsuario = true;
|
||||||
|
esUsuarioValido = await checkUsername(dto.username);
|
||||||
|
checkeandoUsuario = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkEmaill() {
|
||||||
|
checkeandoEmail = true;
|
||||||
|
esEmailValido = await checkEmail(dto.email);
|
||||||
|
checkeandoEmail = false;
|
||||||
|
}
|
||||||
|
const setAlert = () => (showAlert = true);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: SubmitEvent) => {
|
||||||
|
if (esUsuarioValido == false) return;
|
||||||
|
if (!coinsidenLasPass) return;
|
||||||
|
if (!esContraseñaValida) return;
|
||||||
|
|
||||||
|
cargando = true;
|
||||||
|
await register(e, dto, setAlert);
|
||||||
|
cargando = false;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card.Root>
|
<Card.Root>
|
||||||
@@ -20,11 +62,29 @@
|
|||||||
<hr />
|
<hr />
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<form onsubmit={(e)=>register(e, dto, setAlert)}>
|
<form onsubmit={handleSubmit}>
|
||||||
<Field.Group>
|
<Field.Group>
|
||||||
<Field.Field>
|
<Field.Field>
|
||||||
<Field.Label for="name">Nombre de Usuario</Field.Label>
|
<div class="flex justify-between">
|
||||||
<Input id="name" bind:value={dto.username} type="text" placeholder="JPepe" required />
|
<Field.Label for="name">Nombre de Usuario</Field.Label>
|
||||||
|
{#if checkeandoUsuario == null}
|
||||||
|
<div hidden></div>
|
||||||
|
{:else if checkeandoUsuario == true}
|
||||||
|
<Spinner></Spinner>
|
||||||
|
{:else if esUsuarioValido}
|
||||||
|
<Check class="text-green-500" />
|
||||||
|
{:else}
|
||||||
|
<Cross class="text-red-500" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
bind:value={dto.username}
|
||||||
|
type="text"
|
||||||
|
placeholder="JPepe"
|
||||||
|
required
|
||||||
|
onchange={checkUsuario}
|
||||||
|
/>
|
||||||
</Field.Field>
|
</Field.Field>
|
||||||
|
|
||||||
<Field.Field>
|
<Field.Field>
|
||||||
@@ -33,22 +93,62 @@
|
|||||||
</Field.Field>
|
</Field.Field>
|
||||||
|
|
||||||
<Field.Field>
|
<Field.Field>
|
||||||
<Field.Label for="email">Email</Field.Label>
|
<div class="flex justify-between">
|
||||||
<Input id="email" type="email" bind:value={dto.email} placeholder="m@ejemplo.com" required />
|
<Field.Label for="email">Email</Field.Label>
|
||||||
|
{#if checkeandoEmail == null}
|
||||||
|
<div hidden></div>
|
||||||
|
{:else if checkeandoEmail == true}
|
||||||
|
<Spinner></Spinner>
|
||||||
|
{:else if esEmailValido}
|
||||||
|
<Check class="text-green-500" />
|
||||||
|
{:else}
|
||||||
|
<Cross class="text-red-500" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
bind:value={dto.email}
|
||||||
|
placeholder="m@ejemplo.com"
|
||||||
|
required
|
||||||
|
onchange={checkEmaill}
|
||||||
|
/>
|
||||||
</Field.Field>
|
</Field.Field>
|
||||||
<Field.Field>
|
<Field.Field>
|
||||||
<Field.Label for="password">Contraseña</Field.Label>
|
<Field.Label for="password">Contraseña</Field.Label>
|
||||||
<Input id="password" type="password" bind:value={dto.password} required />
|
<Input
|
||||||
<Field.Description>Debe de tener por lo menos 8 caracteres.</Field.Description>
|
id="password"
|
||||||
|
type="password"
|
||||||
|
bind:value={dto.password}
|
||||||
|
required
|
||||||
|
class={{ 'border-red-500': dto.password && !esContraseñaValida }}
|
||||||
|
/>
|
||||||
|
<Field.Description
|
||||||
|
>Debe de tener por lo menos 8 caracteres, una minúscula, una mayúscula, un número y un
|
||||||
|
carácter especial.</Field.Description
|
||||||
|
>
|
||||||
</Field.Field>
|
</Field.Field>
|
||||||
<Field.Field>
|
<Field.Field>
|
||||||
<Field.Label for="confirm-password">Confirmar Contraseña</Field.Label>
|
<Field.Label for="confirm-password">Confirmar Contraseña</Field.Label>
|
||||||
<Input id="confirm-password" type="password" required />
|
<Input
|
||||||
|
id="confirm-password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
bind:value={repetirContraseña}
|
||||||
|
class={{ 'border-red-500': !coinsidenLasPass }}
|
||||||
|
/>
|
||||||
<Field.Description>Confirma la contraseña</Field.Description>
|
<Field.Description>Confirma la contraseña</Field.Description>
|
||||||
</Field.Field>
|
</Field.Field>
|
||||||
<Field.Group>
|
<Field.Group>
|
||||||
<Field.Field>
|
<Field.Field>
|
||||||
<Button type="submit">Crear Cuenta</Button>
|
<Button type="submit" disabled={cargando || !coinsidenLasPass || !esContraseñaValida}>
|
||||||
|
{#if cargando}
|
||||||
|
Creando Cuenta...
|
||||||
|
<Loader2Icon class="animate-spin" />
|
||||||
|
{:else}
|
||||||
|
Crear Cuenta
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
<Field.Description class="px-6 text-center">
|
<Field.Description class="px-6 text-center">
|
||||||
Tenes una cuenta? <a href="/login">Iniciar Sesion</a>
|
Tenes una cuenta? <a href="/login">Iniciar Sesion</a>
|
||||||
</Field.Description>
|
</Field.Description>
|
||||||
|
|||||||
50
src/lib/components/ui/badge/badge.svelte
Normal file
50
src/lib/components/ui/badge/badge.svelte
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import { type VariantProps, tv } from "tailwind-variants";
|
||||||
|
|
||||||
|
export const badgeVariants = 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 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-full border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white",
|
||||||
|
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAnchorAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
href,
|
||||||
|
class: className,
|
||||||
|
variant = "default",
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||||
|
variant?: BadgeVariant;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:element
|
||||||
|
this={href ? "a" : "span"}
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="badge"
|
||||||
|
{href}
|
||||||
|
class={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</svelte:element>
|
||||||
2
src/lib/components/ui/badge/index.ts
Normal file
2
src/lib/components/ui/badge/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Badge } from "./badge.svelte";
|
||||||
|
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
|
||||||
36
src/lib/components/ui/checkbox/checkbox.svelte
Normal file
36
src/lib/components/ui/checkbox/checkbox.svelte
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Checkbox as CheckboxPrimitive } from "bits-ui";
|
||||||
|
import CheckIcon from "@lucide/svelte/icons/check";
|
||||||
|
import MinusIcon from "@lucide/svelte/icons/minus";
|
||||||
|
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
checked = $bindable(false),
|
||||||
|
indeterminate = $bindable(false),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<CheckboxPrimitive.RootProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
bind:ref
|
||||||
|
data-slot="checkbox"
|
||||||
|
class={cn(
|
||||||
|
"border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive shadow-xs peer flex size-4 shrink-0 items-center justify-center rounded-[4px] border outline-none transition-shadow focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
bind:checked
|
||||||
|
bind:indeterminate
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#snippet children({ checked, indeterminate })}
|
||||||
|
<div data-slot="checkbox-indicator" class="text-current transition-none">
|
||||||
|
{#if checked}
|
||||||
|
<CheckIcon class="size-3.5" />
|
||||||
|
{:else if indeterminate}
|
||||||
|
<MinusIcon class="size-3.5" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
6
src/lib/components/ui/checkbox/index.ts
Normal file
6
src/lib/components/ui/checkbox/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import Root from "./checkbox.svelte";
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Checkbox,
|
||||||
|
};
|
||||||
@@ -5,11 +5,20 @@
|
|||||||
import { FieldGroup, Field, FieldLabel, FieldDescription } from '@/components/ui/field';
|
import { FieldGroup, Field, FieldLabel, FieldDescription } from '@/components/ui/field';
|
||||||
import type { LoginDto } from '../../../../types';
|
import type { LoginDto } from '../../../../types';
|
||||||
import { login } from '@/hooks/login';
|
import { login } from '@/hooks/login';
|
||||||
let {id, showAlert = $bindable() } = $props();
|
import Loader2Icon from '@lucide/svelte/icons/loader-2';
|
||||||
|
let { id, showAlert = $bindable() } = $props();
|
||||||
|
|
||||||
let dto: LoginDto = $state({password: "", username: ""});
|
let dto: LoginDto = $state({ password: '', username: '' });
|
||||||
|
|
||||||
const setAlert = () => showAlert = true;
|
const setAlert = () => (showAlert = true);
|
||||||
|
|
||||||
|
let cargando = $state(false);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: SubmitEvent) => {
|
||||||
|
cargando = true;
|
||||||
|
await login(e, dto, setAlert);
|
||||||
|
cargando = false;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card.Root class="mx-auto w-full max-w-sm">
|
<Card.Root class="mx-auto w-full max-w-sm">
|
||||||
@@ -18,7 +27,7 @@
|
|||||||
<Card.Description>ingrese su usuario para logearse en la cuenta</Card.Description>
|
<Card.Description>ingrese su usuario para logearse en la cuenta</Card.Description>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<form onsubmit="{(e)=>login(e,dto, setAlert)}">
|
<form onsubmit={handleSubmit}>
|
||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel for="email-{id}">Usuario</FieldLabel>
|
<FieldLabel for="email-{id}">Usuario</FieldLabel>
|
||||||
@@ -34,7 +43,14 @@
|
|||||||
<Input bind:value={dto.password} type="password" required />
|
<Input bind:value={dto.password} type="password" required />
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
<Button type="submit" class="w-full">Login</Button>
|
<Button type="submit" class="w-full" disabled={cargando}>
|
||||||
|
{#if cargando}
|
||||||
|
Cargando...
|
||||||
|
<Loader2Icon class="animate-spin" />
|
||||||
|
{:else}
|
||||||
|
Login
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
|
||||||
<FieldDescription class="text-center">
|
<FieldDescription class="text-center">
|
||||||
No tenes una cuenta? <a href="/register">Registrate</a>
|
No tenes una cuenta? <a href="/register">Registrate</a>
|
||||||
|
|||||||
28
src/lib/components/ui/table/index.ts
Normal file
28
src/lib/components/ui/table/index.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import Root from "./table.svelte";
|
||||||
|
import Body from "./table-body.svelte";
|
||||||
|
import Caption from "./table-caption.svelte";
|
||||||
|
import Cell from "./table-cell.svelte";
|
||||||
|
import Footer from "./table-footer.svelte";
|
||||||
|
import Head from "./table-head.svelte";
|
||||||
|
import Header from "./table-header.svelte";
|
||||||
|
import Row from "./table-row.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Body,
|
||||||
|
Caption,
|
||||||
|
Cell,
|
||||||
|
Footer,
|
||||||
|
Head,
|
||||||
|
Header,
|
||||||
|
Row,
|
||||||
|
//
|
||||||
|
Root as Table,
|
||||||
|
Body as TableBody,
|
||||||
|
Caption as TableCaption,
|
||||||
|
Cell as TableCell,
|
||||||
|
Footer as TableFooter,
|
||||||
|
Head as TableHead,
|
||||||
|
Header as TableHeader,
|
||||||
|
Row as TableRow,
|
||||||
|
};
|
||||||
20
src/lib/components/ui/table/table-body.svelte
Normal file
20
src/lib/components/ui/table/table-body.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<HTMLTableSectionElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<tbody
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="table-body"
|
||||||
|
class={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</tbody>
|
||||||
20
src/lib/components/ui/table/table-caption.svelte
Normal file
20
src/lib/components/ui/table/table-caption.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<HTMLElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<caption
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="table-caption"
|
||||||
|
class={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</caption>
|
||||||
23
src/lib/components/ui/table/table-cell.svelte
Normal file
23
src/lib/components/ui/table/table-cell.svelte
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLTdAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLTdAttributes> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<td
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="table-cell"
|
||||||
|
class={cn(
|
||||||
|
"whitespace-nowrap bg-clip-padding p-2 align-middle [&:has([role=checkbox])]:pe-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</td>
|
||||||
20
src/lib/components/ui/table/table-footer.svelte
Normal file
20
src/lib/components/ui/table/table-footer.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<HTMLTableSectionElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<tfoot
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="table-footer"
|
||||||
|
class={cn("bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</tfoot>
|
||||||
23
src/lib/components/ui/table/table-head.svelte
Normal file
23
src/lib/components/ui/table/table-head.svelte
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLThAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLThAttributes> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<th
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="table-head"
|
||||||
|
class={cn(
|
||||||
|
"text-foreground h-10 whitespace-nowrap bg-clip-padding px-2 text-start align-middle font-medium [&:has([role=checkbox])]:pe-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</th>
|
||||||
20
src/lib/components/ui/table/table-header.svelte
Normal file
20
src/lib/components/ui/table/table-header.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<HTMLTableSectionElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<thead
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="table-header"
|
||||||
|
class={cn("[&_tr]:border-b", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</thead>
|
||||||
23
src/lib/components/ui/table/table-row.svelte
Normal file
23
src/lib/components/ui/table/table-row.svelte
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<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<HTMLTableRowElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<tr
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="table-row"
|
||||||
|
class={cn(
|
||||||
|
"hover:[&,&>svelte-css-wrapper]:[&>th,td]:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</tr>
|
||||||
22
src/lib/components/ui/table/table.svelte
Normal file
22
src/lib/components/ui/table/table.svelte
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLTableAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLTableAttributes> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div data-slot="table-container" class="relative w-full overflow-x-auto">
|
||||||
|
<table
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="table"
|
||||||
|
class={cn("w-full caption-bottom text-sm", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
@@ -27,6 +27,10 @@
|
|||||||
<DropdownMenuItem onclick={() => goto('/' + $sesionStore?.username)}
|
<DropdownMenuItem onclick={() => goto('/' + $sesionStore?.username)}
|
||||||
>Mi Perfil</DropdownMenuItem
|
>Mi Perfil</DropdownMenuItem
|
||||||
>
|
>
|
||||||
|
{#if $sesionStore?.isAdmin}
|
||||||
|
<DropdownMenuItem onclick={() => goto('/admin')}>Menu Admin</DropdownMenuItem>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onclick={async () => await logout(menuOpen)}>Cerrar Sesion</DropdownMenuItem
|
<DropdownMenuItem onclick={async () => await logout(menuOpen)}>Cerrar Sesion</DropdownMenuItem
|
||||||
>
|
>
|
||||||
|
|||||||
24
src/lib/hooks/cambiarContraseña.ts
Normal file
24
src/lib/hooks/cambiarContraseña.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { apiBase } from '@/stores/url';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
import type { UserResponseDto } from '../../types';
|
||||||
|
import { sesionStore } from '@/stores/usuario';
|
||||||
|
|
||||||
|
export async function cambiarContraseña(usuario: UserResponseDto, newpass: string) {
|
||||||
|
try {
|
||||||
|
const req = await fetch(get(apiBase) + '/api/admin/password', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ id: usuario.id, newpass })
|
||||||
|
});
|
||||||
|
if (req.ok) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const data = await req.json();
|
||||||
|
return data.message;
|
||||||
|
} catch {
|
||||||
|
return 'No se pudo alcanzar el servidor';
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/lib/hooks/checkEmail.ts
Normal file
16
src/lib/hooks/checkEmail.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { apiBase } from "@/stores/url";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
|
export async function checkEmail(email: string) {
|
||||||
|
try {
|
||||||
|
const req = await fetch(`${get(apiBase)}/api/users/check-email/${email}`, {
|
||||||
|
method: "GET"
|
||||||
|
});
|
||||||
|
if (req.ok){
|
||||||
|
return (await req.json()).available;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/lib/hooks/checkUsername.ts
Normal file
16
src/lib/hooks/checkUsername.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { apiBase } from "@/stores/url";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
|
export async function checkUsername(username: string) {
|
||||||
|
try {
|
||||||
|
const req = await fetch(`${get(apiBase)}/api/users/check-username/${username}`, {
|
||||||
|
method: "GET"
|
||||||
|
});
|
||||||
|
if (req.ok){
|
||||||
|
return (await req.json()).available;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/lib/hooks/getPosts.ts
Normal file
17
src/lib/hooks/getPosts.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { apiBase } from "@/stores/url";
|
||||||
|
import { sesionStore } from "@/stores/usuario";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
|
export async function getPosts() {
|
||||||
|
|
||||||
|
|
||||||
|
const req = await fetch(`${get(apiBase)}/timeline?pageSize=20`,{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (req.ok) {
|
||||||
|
return await req.json();
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/lib/hooks/likePost.ts
Normal file
22
src/lib/hooks/likePost.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { apiBase } from '@/stores/url';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
import { sesionStore } from '@/stores/usuario';
|
||||||
|
import type { Post } from '../../types';
|
||||||
|
|
||||||
|
export async function likePost(post: Post) {
|
||||||
|
let method = post.isLiked ? "DELETE" : "POST";
|
||||||
|
try {
|
||||||
|
const req = await fetch(get(apiBase) + `/api/posts/${post.id}/like`, {
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const data: { message: string } = await req.json();
|
||||||
|
return { message: data.message, ok: req.ok };
|
||||||
|
} catch {
|
||||||
|
return { message: 'No se pudo alcanzar el servidor', ok: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import type { LoginDto } from "../../types";
|
|||||||
import { sesionStore } from "@/stores/usuario";
|
import { sesionStore } from "@/stores/usuario";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
|
|
||||||
export async function login(e:FormDataEvent,dto: LoginDto, callbackfn:()=>void){
|
export async function login(e:SubmitEvent, dto: LoginDto, callbackfn:()=>void){
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (dto.password == "" || dto.username == "") return;
|
if (dto.password == "" || dto.username == "") return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { apiBase } from '@/stores/url';
|
import { apiBase } from '@/stores/url';
|
||||||
import { sesionStore } from '@/stores/usuario';
|
import { sesionStore } from '@/stores/usuario';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
export async function logout(menuOpen: boolean) {
|
export async function logout(menuOpen: boolean) {
|
||||||
try {
|
try {
|
||||||
const req = await fetch($apiBase + '/api/auth/logout', {
|
const req = await fetch(get(apiBase) + '/api/auth/logout', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${$sesionStore.accessToken}`
|
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
|
||||||
},
|
},
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
|||||||
25
src/lib/hooks/obtenerSeguidoresPorUsuario.ts
Normal file
25
src/lib/hooks/obtenerSeguidoresPorUsuario.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { sesionStore } from "@/stores/usuario";
|
||||||
|
import type { UserResponseDto } from "../../types";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
import { apiBase } from "@/stores/url";
|
||||||
|
|
||||||
|
export async function obtenerSeguidoresPorUsuario(id: string, limit:number = 20): Promise<UserResponseDto[] | null> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${get(apiBase)}/api/users/${id}/followers?limit=${limit}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const followers: UserResponseDto[] = await response.json();
|
||||||
|
return followers;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/lib/hooks/obtenerSeguidosPorUsuario.ts
Normal file
25
src/lib/hooks/obtenerSeguidosPorUsuario.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { sesionStore } from "@/stores/usuario";
|
||||||
|
import type { UserResponseDto } from "../../types";
|
||||||
|
import { apiBase } from "@/stores/url";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
|
export async function obtenerSeguidosPorUsuario(id: string, limit:number = 20): Promise<UserResponseDto[] | null> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${get(apiBase)}/api/users/${id}/following?limit=${limit}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const users: UserResponseDto[] = await response.json();
|
||||||
|
return users;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/lib/hooks/obtenerUsuario.ts
Normal file
27
src/lib/hooks/obtenerUsuario.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { apiBase } from '@/stores/url';
|
||||||
|
import type { UserResponseDto } from '../../types';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
import { sesionStore } from '@/stores/usuario';
|
||||||
|
|
||||||
|
export async function obtenerUsuarioPorUsername(username: string): Promise<UserResponseDto | null> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${get(apiBase)}/api/users/username/${username}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
//console.error('Error fetching user data:', response.status);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user: UserResponseDto = await response.json();
|
||||||
|
return user;
|
||||||
|
} catch (error) {
|
||||||
|
//console.error('Failed to reach the server while fetching user:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { apiBase } from "@/stores/url";
|
|||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import type { RegisterDto } from "../../types";
|
import type { RegisterDto } from "../../types";
|
||||||
|
|
||||||
export async function register(e:FormDataEvent,dto: RegisterDto, callbackfn:()=>void){
|
export async function register(e: SubmitEvent, dto: RegisterDto, callbackfn:()=>void){
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (dto.password == "" || dto.username == "" ||
|
if (dto.password == "" || dto.username == "" ||
|
||||||
!dto.email?.includes("@") || dto.displayName=="") return;
|
!dto.email?.includes("@") || dto.displayName=="") return;
|
||||||
|
|||||||
@@ -5,17 +5,16 @@ import { sesionStore } from '@/stores/usuario';
|
|||||||
|
|
||||||
export async function updatePost(post: Post, callbackfn: Function, message: string) {
|
export async function updatePost(post: Post, callbackfn: Function, message: string) {
|
||||||
try {
|
try {
|
||||||
const data = {
|
const formData = new FormData();
|
||||||
content: post.content,
|
formData.append("content", post.content);
|
||||||
imageUrl: post.imageUrl
|
formData.append("imageUrl", post.imageUrl||"");
|
||||||
};
|
|
||||||
const req = await fetch(get(apiBase) + `/api/posts/${post.id}`, {
|
const req = await fetch(get(apiBase) + `/api/posts/${post.id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
|
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify(data)
|
body: formData
|
||||||
});
|
});
|
||||||
if (req.ok) {
|
if (req.ok) {
|
||||||
const newpost: PostResponseDto = await req.json();
|
const newpost: PostResponseDto = await req.json();
|
||||||
|
|||||||
44
src/lib/hooks/updateUsuario.ts
Normal file
44
src/lib/hooks/updateUsuario.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { apiBase } from "@/stores/url"
|
||||||
|
import { sesionStore } from "@/stores/usuario"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
|
export interface AdminUpdateUsuario {
|
||||||
|
id:string,
|
||||||
|
displayName: string,
|
||||||
|
bio: string,
|
||||||
|
profileImage:boolean,
|
||||||
|
oldImageUrl:string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateUsuario(usuario: AdminUpdateUsuario) {
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('displayName', usuario.displayName);
|
||||||
|
formData.append('bio', usuario.bio);
|
||||||
|
if (usuario.profileImage) {
|
||||||
|
formData.append('profileImageUrl', usuario.oldImageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const req = await fetch(get(apiBase) + "/api/users/"+usuario.id, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
if (req.status === 204) {
|
||||||
|
let ret = {
|
||||||
|
// bio: usuario.bio,
|
||||||
|
displayName: usuario.displayName,
|
||||||
|
// oldImageUrl: usuario.oldImageUrl,
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
const dataa = await req.json();
|
||||||
|
|
||||||
|
return dataa.message;
|
||||||
|
} catch {
|
||||||
|
return "No se pudo alcanzar el servidor"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,8 +27,43 @@ if (browser) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (browser) {
|
if (browser) {
|
||||||
|
const decodeJWT = (token: string) => {
|
||||||
|
try {
|
||||||
|
const base64Url = token.split('.')[1];
|
||||||
|
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
const jsonPayload = decodeURIComponent(
|
||||||
|
atob(base64)
|
||||||
|
.split('')
|
||||||
|
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
||||||
|
.join('')
|
||||||
|
);
|
||||||
|
|
||||||
|
return JSON.parse(jsonPayload);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error decodificando JWT:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldRefreshToken = (sesion: Sesion | null): boolean => {
|
||||||
|
if (!sesion || !sesion.accessToken) return false;
|
||||||
|
|
||||||
|
const decoded = decodeJWT(sesion.accessToken);
|
||||||
|
if (!decoded || !decoded.exp) return false;
|
||||||
|
|
||||||
|
const expirationTime = decoded.exp * 1000;
|
||||||
|
const currentTime = Date.now();
|
||||||
|
const timeUntilExpiration = expirationTime - currentTime;
|
||||||
|
|
||||||
|
return timeUntilExpiration <= 60 * 1000; // 1 minuto
|
||||||
|
};
|
||||||
|
|
||||||
const refreshAccessToken = async () => {
|
const refreshAccessToken = async () => {
|
||||||
try {
|
try {
|
||||||
|
const sesion = get(currentSesion);
|
||||||
|
if (!shouldRefreshToken(sesion)) return;
|
||||||
|
|
||||||
|
console.log('refrescando token');
|
||||||
const response = await fetch(get(apiBase) + '/api/auth/refresh', {
|
const response = await fetch(get(apiBase) + '/api/auth/refresh', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -46,14 +81,15 @@ if (browser) {
|
|||||||
return sesion;
|
return sesion;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.error('Error refreshing token:', response.statusText);
|
console.error('Error refrescando token:', response.statusText);
|
||||||
currentSesion.set(null);
|
currentSesion.set(null);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error refreshing token:', error);
|
console.error('Error refrescando token:', error);
|
||||||
currentSesion.set(null);
|
currentSesion.set(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setInterval(refreshAccessToken, 10 * 60 * 1000);
|
setInterval(refreshAccessToken, 30 * 1000); // Check every 30 seconds
|
||||||
|
refreshAccessToken();
|
||||||
}
|
}
|
||||||
|
|||||||
33
src/routes/(privado)/admin/+page.svelte
Normal file
33
src/routes/(privado)/admin/+page.svelte
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import CardContent from '@/components/ui/card/card-content.svelte';
|
||||||
|
import Card from '@/components/ui/card/card.svelte';
|
||||||
|
import CardDescription from '@/components/ui/card/card-description.svelte';
|
||||||
|
import { page } from '$app/state';
|
||||||
|
import TablaUsuarios from '@/components/TablaUsuarios.svelte';
|
||||||
|
import CardTitle from '@/components/ui/card/card-title.svelte';
|
||||||
|
import CardHeader from '@/components/ui/card/card-header.svelte';
|
||||||
|
|
||||||
|
let cargando = $state(true);
|
||||||
|
let usuarios = $state(page.data.usuarios);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex min-h-fit w-full items-center justify-center p-6 md:p-10">
|
||||||
|
<div class="w-full max-w-6xl">
|
||||||
|
<Card class={page.data.error ? 'border-red-400' : ''}>
|
||||||
|
<CardHeader class="w-full">
|
||||||
|
<CardTitle class="rounded-full bg-accent-foreground/10">
|
||||||
|
<h1 class="mt-3 mb-4 scroll-m-20 text-center text-2xl font-extrabold tracking-tight">
|
||||||
|
Gestion Usuarios
|
||||||
|
</h1>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{#if page.data.usuarios.length === 0}
|
||||||
|
<CardDescription>No hay posts que mostar</CardDescription>
|
||||||
|
{:else}
|
||||||
|
<TablaUsuarios bind:usuarios></TablaUsuarios>
|
||||||
|
{/if}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
28
src/routes/(privado)/admin/+page.ts
Normal file
28
src/routes/(privado)/admin/+page.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { apiBase } from '@/stores/url.js';
|
||||||
|
import { sesionStore } from '@/stores/usuario';
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
import type { UserResponseDto } from '../../../types.js';
|
||||||
|
|
||||||
|
export const ssr = false;
|
||||||
|
|
||||||
|
export async function load({}) {
|
||||||
|
const response = await fetch(get(apiBase) + '/api/admin/users', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
throw redirect(302, '/');
|
||||||
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
return { error: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const usuarios: UserResponseDto[] = await response.json();
|
||||||
|
|
||||||
|
return { usuarios, error: false };
|
||||||
|
}
|
||||||
20
src/routes/+error.svelte
Normal file
20
src/routes/+error.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/state';
|
||||||
|
import CardContent from '@/components/ui/card/card-content.svelte';
|
||||||
|
import Card from '@/components/ui/card/card.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex min-h-fit w-full items-center justify-center p-6 md:p-10">
|
||||||
|
<div class="w-full max-w-6xl">
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<h1 class="mb-4 text-center text-3xl font-bold text-gray-800">
|
||||||
|
{page.status}
|
||||||
|
</h1>
|
||||||
|
<p class="text-center">
|
||||||
|
{page.error!.message}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import favicon from '$lib/assets/favicon.ico';
|
import favicon from '$lib/assets/favicon.ico';
|
||||||
import { ModeWatcher } from 'mode-watcher';
|
import { ModeWatcher } from 'mode-watcher';
|
||||||
import Header from '@/head/Header.svelte';
|
import Header from '@/head/Header.svelte';
|
||||||
|
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
@@ -13,4 +14,6 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
<ModeWatcher />
|
<ModeWatcher />
|
||||||
<Header />
|
<Header />
|
||||||
{@render children()}
|
<TooltipProvider>
|
||||||
|
{@render children()}
|
||||||
|
</TooltipProvider>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
import ModalEditar from './[perfil]/modalEditar.svelte';
|
import ModalEditar from './[perfil]/modalEditar.svelte';
|
||||||
import { updatePost } from '@/hooks/updatePost';
|
import { updatePost } from '@/hooks/updatePost';
|
||||||
import { fade, slide } from 'svelte/transition';
|
import { fade, slide } from 'svelte/transition';
|
||||||
|
import { getPosts } from '@/hooks/getPosts';
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -17,20 +18,6 @@
|
|||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function getPosts() {
|
|
||||||
const { subscribe } = apiBase;
|
|
||||||
let baseUrl: string = '';
|
|
||||||
|
|
||||||
subscribe((value) => {
|
|
||||||
baseUrl = value;
|
|
||||||
})();
|
|
||||||
|
|
||||||
const req = await fetch(`${baseUrl}/timeline?pageSize=20`);
|
|
||||||
if (req.ok) {
|
|
||||||
return await req.json();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let postAModificar: Post | null = $state(null);
|
let postAModificar: Post | null = $state(null);
|
||||||
let mensajeError = $state('');
|
let mensajeError = $state('');
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const ssr = true;
|
//export const ssr = true;
|
||||||
|
|||||||
15
src/routes/[perfil]/+page.server.ts
Normal file
15
src/routes/[perfil]/+page.server.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { obtenerUsuarioPorUsername } from '@/hooks/obtenerUsuario.js';
|
||||||
|
import type { User, UserResponseDto } from '../../types.js';
|
||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
import { obtenerSeguidosPorUsuario } from '@/hooks/obtenerSeguidosPorUsuario.js';
|
||||||
|
import { obtenerSeguidoresPorUsuario } from '@/hooks/obtenerSeguidoresPorUsuario.js';
|
||||||
|
|
||||||
|
export async function load({ params }) {
|
||||||
|
const usuario: UserResponseDto | null = await obtenerUsuarioPorUsername(params.perfil);
|
||||||
|
if(!usuario) error(404, 'No se encontro el usuario, ' + params.perfil);
|
||||||
|
|
||||||
|
const seguidos = await obtenerSeguidosPorUsuario(usuario.id, 3);
|
||||||
|
const seguidores = await obtenerSeguidoresPorUsuario(usuario.id, 3);
|
||||||
|
|
||||||
|
return { ...usuario, seguidos, seguidores };
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { apiBase } from '@/stores/url';
|
import { apiBase } from '@/stores/url';
|
||||||
import Ban from '@lucide/svelte/icons/ban';
|
import Ban from '@lucide/svelte/icons/ban';
|
||||||
|
import PenLine from '@lucide/svelte/icons/pen-line';
|
||||||
import Card from '@/components/ui/card/card.svelte';
|
import Card from '@/components/ui/card/card.svelte';
|
||||||
import Avatar from '@/components/ui/avatar/avatar.svelte';
|
import Avatar from '@/components/ui/avatar/avatar.svelte';
|
||||||
import AvatarImage from '@/components/ui/avatar/avatar-image.svelte';
|
import AvatarImage from '@/components/ui/avatar/avatar-image.svelte';
|
||||||
@@ -11,11 +12,19 @@
|
|||||||
import { fade, slide } from 'svelte/transition';
|
import { fade, slide } from 'svelte/transition';
|
||||||
import PostCard from '@/components/PostCard.svelte';
|
import PostCard from '@/components/PostCard.svelte';
|
||||||
import { posts, setPosts, updatePostStore } from '@/stores/posts.js';
|
import { posts, setPosts, updatePostStore } from '@/stores/posts.js';
|
||||||
import InputGroup from '@/components/ui/input-group/input-group.svelte';
|
|
||||||
import InputGroupTextarea from '@/components/ui/input-group/input-group-textarea.svelte';
|
|
||||||
import InputGroupAddon from '@/components/ui/input-group/input-group-addon.svelte';
|
|
||||||
import { updatePost } from '@/hooks/updatePost.js';
|
import { updatePost } from '@/hooks/updatePost.js';
|
||||||
import ModalEditar from './modalEditar.svelte';
|
import ModalEditar from './modalEditar.svelte';
|
||||||
|
import { page } from '$app/state';
|
||||||
|
import Button from '@/components/ui/button/button.svelte';
|
||||||
|
import { Dialog } from '@/components/ui/dialog/index.js';
|
||||||
|
import CrearPost from '@/components/crear-post.svelte';
|
||||||
|
import DialogContent from '@/components/ui/dialog/dialog-content.svelte';
|
||||||
|
import DialogHeader from '@/components/ui/dialog/dialog-header.svelte';
|
||||||
|
import DialogTitle from '@/components/ui/dialog/dialog-title.svelte';
|
||||||
|
import { sesionStore } from '@/stores/usuario.js';
|
||||||
|
import CardHeader from '@/components/ui/card/card-header.svelte';
|
||||||
|
import CardTitle from '@/components/ui/card/card-title.svelte';
|
||||||
|
import Badge from '@/components/ui/badge/badge.svelte';
|
||||||
|
|
||||||
let { params } = $props();
|
let { params } = $props();
|
||||||
|
|
||||||
@@ -23,6 +32,9 @@
|
|||||||
let mensajeError = $state('');
|
let mensajeError = $state('');
|
||||||
let postAModificar: Post | null = $state(null);
|
let postAModificar: Post | null = $state(null);
|
||||||
|
|
||||||
|
let showCrearPost = $state(false);
|
||||||
|
const toggleCrearPost = () => (showCrearPost = !showCrearPost);
|
||||||
|
|
||||||
const { subscribe } = apiBase;
|
const { subscribe } = apiBase;
|
||||||
let baseUrl: string = '';
|
let baseUrl: string = '';
|
||||||
|
|
||||||
@@ -53,7 +65,6 @@
|
|||||||
|
|
||||||
async function handleEditar(e: SubmitEvent) {
|
async function handleEditar(e: SubmitEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// post.content = 'test';
|
|
||||||
if (postAModificar == null) return;
|
if (postAModificar == null) return;
|
||||||
await updatePost(
|
await updatePost(
|
||||||
postAModificar,
|
postAModificar,
|
||||||
@@ -67,25 +78,87 @@
|
|||||||
|
|
||||||
<div class="flex min-h-fit w-full items-center justify-center p-6 md:p-10">
|
<div class="flex min-h-fit w-full items-center justify-center p-6 md:p-10">
|
||||||
<div class="w-full max-w-6xl">
|
<div class="w-full max-w-6xl">
|
||||||
<Card class="mb-2 overflow-hidden">
|
<div class="flex gap-2">
|
||||||
<CardContent>
|
<Card class="mb-2 flex w-3/4 overflow-hidden">
|
||||||
<div class="flex justify-center">
|
<CardContent>
|
||||||
<Avatar class="mt-2 scale-250 border-2 border-slate-950">
|
<div class="flex justify-center">
|
||||||
<AvatarImage></AvatarImage>
|
<Avatar class="mt-2 scale-250 border-2 border-slate-950">
|
||||||
<AvatarFallback>{params.perfil[0].toUpperCase()}</AvatarFallback>
|
<AvatarImage></AvatarImage>
|
||||||
</Avatar>
|
<AvatarFallback>{page.data.displayName?.[0]?.toUpperCase() || ''}</AvatarFallback>
|
||||||
</div>
|
</Avatar>
|
||||||
<h1
|
</div>
|
||||||
class="mt-10 scroll-m-20 text-center text-2xl font-extrabold tracking-tight lg:text-5xl"
|
<h1
|
||||||
|
class="mt-10 scroll-m-20 text-center text-2xl font-extrabold tracking-tight lg:text-5xl"
|
||||||
|
>
|
||||||
|
{page.data.displayName}
|
||||||
|
</h1>
|
||||||
|
<h3 class="scroll-m-20 text-center text-2xl tracking-tight text-muted-foreground">
|
||||||
|
@{params.perfil}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-4 rounded-full bg-accent p-4 text-center text-muted-foreground">
|
||||||
|
{page.data.bio}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<aside class="flex w-1/4 flex-col gap-2">
|
||||||
|
<Card class="w-full">
|
||||||
|
<CardContent>
|
||||||
|
<CardHeader class="flex justify-between">
|
||||||
|
<CardTitle>Seguidos:</CardTitle>
|
||||||
|
<Badge variant="secondary">{page.data.seguidos.length}</Badge>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{#if page.data.seguidos.length === 0}
|
||||||
|
<h3>No hay Seguidos</h3>
|
||||||
|
{:else}
|
||||||
|
{#each page.data.seguidos as seguidos (seguidos.id)}
|
||||||
|
<p class="text-muted-foreground">
|
||||||
|
{seguidos.username}
|
||||||
|
</p>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</CardContent>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card class="w-full">
|
||||||
|
<CardContent>
|
||||||
|
<CardHeader class="flex justify-between">
|
||||||
|
<CardTitle>Seguidores:</CardTitle>
|
||||||
|
<Badge variant="secondary">{page.data.seguidores.length}</Badge>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{#if page.data.seguidores.length === 0}
|
||||||
|
<h3>No hay Seguidores</h3>
|
||||||
|
{:else}
|
||||||
|
{#each page.data.seguidores as seguidores (seguidores.id)}
|
||||||
|
<p class="text-muted-foreground">
|
||||||
|
{seguidores.username}
|
||||||
|
</p>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</CardContent>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1
|
||||||
|
class="mt-10 flex scroll-m-20 justify-between text-3xl font-extrabold tracking-tight lg:text-3xl"
|
||||||
|
>
|
||||||
|
Posts:
|
||||||
|
{#if params.perfil == $sesionStore?.username}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="m-1 rounded-full bg-blue-600"
|
||||||
|
onclick={() => {
|
||||||
|
showCrearPost = true;
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{'test'}
|
<PenLine />
|
||||||
</h1>
|
</Button>
|
||||||
<h3 class="scroll-m-20 text-center text-2xl tracking-tight text-muted-foreground">
|
{/if}
|
||||||
@{params.perfil}
|
</h1>
|
||||||
</h3>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<h1 class="mt-10 scroll-m-20 text-3xl font-extrabold tracking-tight lg:text-3xl">Posts:</h1>
|
|
||||||
<hr class="mb-8" />
|
<hr class="mb-8" />
|
||||||
{#if cargando}
|
{#if cargando}
|
||||||
<div out:slide>
|
<div out:slide>
|
||||||
@@ -123,3 +196,18 @@
|
|||||||
<ModalEditar callbackfn={handleEditar} bind:post={postAModificar} />
|
<ModalEditar callbackfn={handleEditar} bind:post={postAModificar} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<div transition:fade>
|
||||||
|
<Dialog open={showCrearPost} onOpenChange={() => (showCrearPost = false)}>
|
||||||
|
<DialogContent
|
||||||
|
onkeydown={(e: KeyboardEvent) => {
|
||||||
|
if (e.ctrlKey && e.key === 'Enter') {
|
||||||
|
showCrearPost = false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle>Crear Publicacion</DialogTitle>
|
||||||
|
<CrearPost />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
import { Tooltip, TooltipProvider } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipProvider } from '@/components/ui/tooltip';
|
||||||
import TooltipTrigger from '@/components/ui/tooltip/tooltip-trigger.svelte';
|
import TooltipTrigger from '@/components/ui/tooltip/tooltip-trigger.svelte';
|
||||||
import TooltipContent from '@/components/ui/tooltip/tooltip-content.svelte';
|
import TooltipContent from '@/components/ui/tooltip/tooltip-content.svelte';
|
||||||
|
import Spinner from '@/components/ui/spinner/spinner.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
post: Post | null;
|
post: Post | null;
|
||||||
@@ -21,11 +22,21 @@
|
|||||||
}
|
}
|
||||||
let { post = $bindable(), callbackfn }: Props = $props();
|
let { post = $bindable(), callbackfn }: Props = $props();
|
||||||
|
|
||||||
function handleKeydown(e: KeyboardEvent) {
|
let cargando = $state(false);
|
||||||
|
|
||||||
|
async function handleKeydown(e: KeyboardEvent) {
|
||||||
if (e.ctrlKey && e.key === 'Enter') {
|
if (e.ctrlKey && e.key === 'Enter') {
|
||||||
callbackfn(e);
|
cargando = true;
|
||||||
|
await callbackfn(e);
|
||||||
|
cargando = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function onsubmit(e: SubmitEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
cargando = true;
|
||||||
|
await callbackfn(e);
|
||||||
|
cargando = false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Dialog open={true} onOpenChange={() => (post = null)}>
|
<Dialog open={true} onOpenChange={() => (post = null)}>
|
||||||
@@ -34,11 +45,7 @@
|
|||||||
<DialogTitle>Editar Publicacion</DialogTitle>
|
<DialogTitle>Editar Publicacion</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
<form
|
<form {onsubmit}>
|
||||||
onsubmit={(e: SubmitEvent) => {
|
|
||||||
callbackfn(e);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<InputGroupTextarea
|
<InputGroupTextarea
|
||||||
bind:value={post!.content}
|
bind:value={post!.content}
|
||||||
@@ -61,13 +68,19 @@
|
|||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<InputGroupButton
|
<InputGroupButton
|
||||||
variant="default"
|
variant="default"
|
||||||
|
disabled={cargando}
|
||||||
type="submit"
|
type="submit"
|
||||||
class="transform rounded-full transition-transform ease-in hover:scale-120"
|
class="transform rounded-full transition-transform ease-in hover:scale-120"
|
||||||
size="xs"
|
size="xs"
|
||||||
>
|
>
|
||||||
<p class="flex items-center gap-1">
|
<p class="flex items-center gap-1">
|
||||||
Modificar
|
{#if cargando}
|
||||||
<ArrowUpIcon class="mt-0.5 h-3.5! w-3.5!" />
|
<Spinner />
|
||||||
|
Cargando...
|
||||||
|
{:else}
|
||||||
|
Modificar
|
||||||
|
<ArrowUpIcon class="mt-0.5 h-3.5! w-3.5!" />
|
||||||
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
</InputGroupButton>
|
</InputGroupButton>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|||||||
21
src/types.d.ts
vendored
21
src/types.d.ts
vendored
@@ -15,6 +15,7 @@ export interface Post {
|
|||||||
isEdited: boolean;
|
isEdited: boolean;
|
||||||
visibility: string;
|
visibility: string;
|
||||||
hashtags?: string[];
|
hashtags?: string[];
|
||||||
|
isLiked: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
@@ -37,6 +38,7 @@ export interface Sesion {
|
|||||||
url: string;
|
url: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
isAdmin: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginDto {
|
export interface LoginDto {
|
||||||
@@ -45,10 +47,10 @@ export interface LoginDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface RegisterDto {
|
export interface RegisterDto {
|
||||||
username: string?;
|
username: string;
|
||||||
email: string?;
|
email: string;
|
||||||
password: string?;
|
password: string?;
|
||||||
displayName: string?;
|
displayName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreatePostDto {
|
export interface CreatePostDto {
|
||||||
@@ -74,3 +76,16 @@ export interface PostResponseDto {
|
|||||||
visibility: string;
|
visibility: string;
|
||||||
hashtags: string[]?;
|
hashtags: string[]?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserResponseDto {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
displayName: string;
|
||||||
|
email: string;
|
||||||
|
bio: string;
|
||||||
|
profileImageUrl: string;
|
||||||
|
followersCount: number;
|
||||||
|
followingCount: number;
|
||||||
|
createdAt: string;
|
||||||
|
postsCount: number;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user