mirror of
https://github.com/emailerfacu-spec/minix-front.git
synced 2026-04-01 13:10:44 -03:00
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import Ellipsis from '@lucide/svelte/icons/ellipsis';
|
||||
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 type { Post } from '../../types';
|
||||
import Button from './ui/button/button.svelte';
|
||||
@@ -27,6 +29,7 @@
|
||||
import DialogTitle from './ui/dialog/dialog-title.svelte';
|
||||
import DialogDescription from './ui/dialog/dialog-description.svelte';
|
||||
import { sesionStore } from '@/stores/usuario';
|
||||
import { likePost } from '@/hooks/likePost';
|
||||
|
||||
interface postProp {
|
||||
post: Post;
|
||||
@@ -38,6 +41,8 @@
|
||||
let cargandoBorrar = $state(false);
|
||||
let mensajeError = $state('');
|
||||
let cargandoEditar = $state(false);
|
||||
let cargandoLike = $state(false);
|
||||
let errorLike = $state(false);
|
||||
|
||||
async function handleBorrar() {
|
||||
await deletePost(
|
||||
@@ -50,9 +55,27 @@
|
||||
);
|
||||
}
|
||||
|
||||
async function handleEditar() {
|
||||
function handleEditar() {
|
||||
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>
|
||||
|
||||
<Card>
|
||||
@@ -60,10 +83,12 @@
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex gap-3">
|
||||
<Avatar>
|
||||
<AvatarImage></AvatarImage>
|
||||
<AvatarFallback>{post.authorDisplayName[0].toUpperCase()}</AvatarFallback>
|
||||
</Avatar>
|
||||
<a href={`/${post.authorName}`}>
|
||||
<Avatar>
|
||||
<AvatarImage></AvatarImage>
|
||||
<AvatarFallback>{post.authorDisplayName[0].toUpperCase()}</AvatarFallback>
|
||||
</Avatar>
|
||||
</a>
|
||||
<div class="flex space-x-2">
|
||||
<span class="text-lg font-medium">{post.authorDisplayName}</span>
|
||||
<span class="text-lg text-muted-foreground">@{post.authorName}</span>
|
||||
@@ -72,7 +97,7 @@
|
||||
{#if post.authorName === $sesionStore?.username}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button variant="ghost" class="rounded-full"><Ellipsis /></Button>
|
||||
<Button variant="ghost" class=" rounded-full bg-accent"><Ellipsis /></Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuGroup>
|
||||
@@ -101,16 +126,32 @@
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<Content>
|
||||
<p class="text-sm">{post.content}</p>
|
||||
<Content class="mx-5 -mt-4 rounded-full bg-accent p-6">
|
||||
<p class=" text-sm">{post.content}</p>
|
||||
{#if post.imageUrl}
|
||||
<img src={post.imageUrl} alt="Post" class="mt-2 rounded-md" />
|
||||
{/if}
|
||||
</Content>
|
||||
<CardFooter>
|
||||
<div class="flex items-center justify-between gap-2 pt-2 text-xs text-muted-foreground">
|
||||
<span>{post.likesCount} likes</span>
|
||||
<span>{post.repliesCount} replies</span>
|
||||
<div class="-mt-2 flex items-center justify-between gap-2 text-xs text-muted-foreground">
|
||||
<Button
|
||||
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"
|
||||
>{post.createdAt.replace('T', ' ').split('.')[0]}</span
|
||||
>
|
||||
@@ -120,7 +161,7 @@
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
{#if mensajeError}
|
||||
{#if mensajeError || errorLike}
|
||||
<Dialog>
|
||||
<DialogContent>
|
||||
<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 InputGroup from './ui/input-group/input-group.svelte';
|
||||
import ArrowUpIcon from '@lucide/svelte/icons/arrow-up';
|
||||
import Loader2Icon from '@lucide/svelte/icons/loader-2';
|
||||
import Kbd from './ui/kbd/kbd.svelte';
|
||||
|
||||
import { apiBase } from '@/stores/url';
|
||||
import { sesionStore } from '@/stores/usuario';
|
||||
import type { CreatePostDto } from '../../types';
|
||||
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 TooltipTrigger from './ui/tooltip/tooltip-trigger.svelte';
|
||||
|
||||
@@ -22,21 +23,18 @@
|
||||
async function handlePost(e: Event) {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const data: CreatePostDto = {
|
||||
content: mensaje,
|
||||
imageUrl: null,
|
||||
parentPostId: null
|
||||
};
|
||||
const formData = new FormData();
|
||||
formData.append('content', mensaje);
|
||||
// formData.append('imageUrl', '');
|
||||
// formData.append('parentPostId', '');
|
||||
|
||||
const req = fetch($apiBase + '/api/posts', {
|
||||
method: 'POST',
|
||||
//credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${$sesionStore?.accessToken}`
|
||||
},
|
||||
|
||||
body: JSON.stringify(data)
|
||||
body: formData
|
||||
});
|
||||
cargando = true;
|
||||
|
||||
@@ -79,24 +77,28 @@
|
||||
</p>
|
||||
/ 280
|
||||
</Kbd>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger class="*: flex">
|
||||
<InputGroupButton
|
||||
variant="default"
|
||||
type="submit"
|
||||
class="transform rounded-full transition-transform ease-in hover:scale-120"
|
||||
size="xs"
|
||||
>
|
||||
<p>Publicar</p>
|
||||
<Tooltip>
|
||||
<TooltipTrigger class="*: flex">
|
||||
<InputGroupButton
|
||||
variant="default"
|
||||
disabled={cargando}
|
||||
type="submit"
|
||||
class="transform rounded-full transition-transform ease-in hover:scale-120"
|
||||
size="xs"
|
||||
>
|
||||
{#if cargando}
|
||||
<Loader2Icon class="animate-spin" />
|
||||
Publicando...
|
||||
{:else}
|
||||
Publicar
|
||||
<ArrowUpIcon class="mt-0.5 h-3.5! w-3.5!" />
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<Kbd>Ctrl</Kbd>+<Kbd>Enter</Kbd>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
{/if}
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<Kbd>Ctrl</Kbd>+<Kbd>Enter</Kbd>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
|
||||
@@ -5,13 +5,55 @@
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
import type { RegisterDto } from '../../types';
|
||||
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>
|
||||
|
||||
<Card.Root>
|
||||
@@ -20,11 +62,29 @@
|
||||
<hr />
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form onsubmit={(e)=>register(e, dto, setAlert)}>
|
||||
<form onsubmit={handleSubmit}>
|
||||
<Field.Group>
|
||||
<Field.Field>
|
||||
<Field.Label for="name">Nombre de Usuario</Field.Label>
|
||||
<Input id="name" bind:value={dto.username} type="text" placeholder="JPepe" required />
|
||||
<div class="flex justify-between">
|
||||
<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>
|
||||
@@ -33,22 +93,62 @@
|
||||
</Field.Field>
|
||||
|
||||
<Field.Field>
|
||||
<Field.Label for="email">Email</Field.Label>
|
||||
<Input id="email" type="email" bind:value={dto.email} placeholder="m@ejemplo.com" required />
|
||||
<div class="flex justify-between">
|
||||
<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.Label for="password">Contraseña</Field.Label>
|
||||
<Input id="password" type="password" bind:value={dto.password} required />
|
||||
<Field.Description>Debe de tener por lo menos 8 caracteres.</Field.Description>
|
||||
<Input
|
||||
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.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.Field>
|
||||
<Field.Group>
|
||||
<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">
|
||||
Tenes una cuenta? <a href="/login">Iniciar Sesion</a>
|
||||
</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 type { LoginDto } from '../../../../types';
|
||||
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>
|
||||
|
||||
<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.Header>
|
||||
<Card.Content>
|
||||
<form onsubmit="{(e)=>login(e,dto, setAlert)}">
|
||||
<form onsubmit={handleSubmit}>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel for="email-{id}">Usuario</FieldLabel>
|
||||
@@ -34,7 +43,14 @@
|
||||
<Input bind:value={dto.password} type="password" required />
|
||||
</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">
|
||||
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)}
|
||||
>Mi Perfil</DropdownMenuItem
|
||||
>
|
||||
{#if $sesionStore?.isAdmin}
|
||||
<DropdownMenuItem onclick={() => goto('/admin')}>Menu Admin</DropdownMenuItem>
|
||||
{/if}
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<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 { 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();
|
||||
if (dto.password == "" || dto.username == "") return;
|
||||
try {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { apiBase } from '@/stores/url';
|
||||
import { sesionStore } from '@/stores/usuario';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export async function logout(menuOpen: boolean) {
|
||||
try {
|
||||
const req = await fetch($apiBase + '/api/auth/logout', {
|
||||
const req = await fetch(get(apiBase) + '/api/auth/logout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${$sesionStore.accessToken}`
|
||||
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
|
||||
},
|
||||
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 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();
|
||||
if (dto.password == "" || dto.username == "" ||
|
||||
!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) {
|
||||
try {
|
||||
const data = {
|
||||
content: post.content,
|
||||
imageUrl: post.imageUrl
|
||||
};
|
||||
const formData = new FormData();
|
||||
formData.append("content", post.content);
|
||||
formData.append("imageUrl", post.imageUrl||"");
|
||||
|
||||
const req = await fetch(get(apiBase) + `/api/posts/${post.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
body: formData
|
||||
});
|
||||
if (req.ok) {
|
||||
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) {
|
||||
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 () => {
|
||||
try {
|
||||
const sesion = get(currentSesion);
|
||||
if (!shouldRefreshToken(sesion)) return;
|
||||
|
||||
console.log('refrescando token');
|
||||
const response = await fetch(get(apiBase) + '/api/auth/refresh', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -46,14 +81,15 @@ if (browser) {
|
||||
return sesion;
|
||||
});
|
||||
} else {
|
||||
console.error('Error refreshing token:', response.statusText);
|
||||
console.error('Error refrescando token:', response.statusText);
|
||||
currentSesion.set(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error refreshing token:', error);
|
||||
console.error('Error refrescando token:', error);
|
||||
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 { ModeWatcher } from 'mode-watcher';
|
||||
import Header from '@/head/Header.svelte';
|
||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
@@ -13,4 +14,6 @@
|
||||
</svelte:head>
|
||||
<ModeWatcher />
|
||||
<Header />
|
||||
{@render children()}
|
||||
<TooltipProvider>
|
||||
{@render children()}
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import ModalEditar from './[perfil]/modalEditar.svelte';
|
||||
import { updatePost } from '@/hooks/updatePost';
|
||||
import { fade, slide } from 'svelte/transition';
|
||||
import { getPosts } from '@/hooks/getPosts';
|
||||
|
||||
$effect(() => {
|
||||
(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 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">
|
||||
import { apiBase } from '@/stores/url';
|
||||
import Ban from '@lucide/svelte/icons/ban';
|
||||
import PenLine from '@lucide/svelte/icons/pen-line';
|
||||
import Card from '@/components/ui/card/card.svelte';
|
||||
import Avatar from '@/components/ui/avatar/avatar.svelte';
|
||||
import AvatarImage from '@/components/ui/avatar/avatar-image.svelte';
|
||||
@@ -11,11 +12,19 @@
|
||||
import { fade, slide } from 'svelte/transition';
|
||||
import PostCard from '@/components/PostCard.svelte';
|
||||
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 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();
|
||||
|
||||
@@ -23,6 +32,9 @@
|
||||
let mensajeError = $state('');
|
||||
let postAModificar: Post | null = $state(null);
|
||||
|
||||
let showCrearPost = $state(false);
|
||||
const toggleCrearPost = () => (showCrearPost = !showCrearPost);
|
||||
|
||||
const { subscribe } = apiBase;
|
||||
let baseUrl: string = '';
|
||||
|
||||
@@ -53,7 +65,6 @@
|
||||
|
||||
async function handleEditar(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
// post.content = 'test';
|
||||
if (postAModificar == null) return;
|
||||
await updatePost(
|
||||
postAModificar,
|
||||
@@ -67,25 +78,87 @@
|
||||
|
||||
<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="mb-2 overflow-hidden">
|
||||
<CardContent>
|
||||
<div class="flex justify-center">
|
||||
<Avatar class="mt-2 scale-250 border-2 border-slate-950">
|
||||
<AvatarImage></AvatarImage>
|
||||
<AvatarFallback>{params.perfil[0].toUpperCase()}</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
<h1
|
||||
class="mt-10 scroll-m-20 text-center text-2xl font-extrabold tracking-tight lg:text-5xl"
|
||||
<div class="flex gap-2">
|
||||
<Card class="mb-2 flex w-3/4 overflow-hidden">
|
||||
<CardContent>
|
||||
<div class="flex justify-center">
|
||||
<Avatar class="mt-2 scale-250 border-2 border-slate-950">
|
||||
<AvatarImage></AvatarImage>
|
||||
<AvatarFallback>{page.data.displayName?.[0]?.toUpperCase() || ''}</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
<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'}
|
||||
</h1>
|
||||
<h3 class="scroll-m-20 text-center text-2xl tracking-tight text-muted-foreground">
|
||||
@{params.perfil}
|
||||
</h3>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<h1 class="mt-10 scroll-m-20 text-3xl font-extrabold tracking-tight lg:text-3xl">Posts:</h1>
|
||||
<PenLine />
|
||||
</Button>
|
||||
{/if}
|
||||
</h1>
|
||||
|
||||
<hr class="mb-8" />
|
||||
{#if cargando}
|
||||
<div out:slide>
|
||||
@@ -123,3 +196,18 @@
|
||||
<ModalEditar callbackfn={handleEditar} bind:post={postAModificar} />
|
||||
</div>
|
||||
{/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 TooltipTrigger from '@/components/ui/tooltip/tooltip-trigger.svelte';
|
||||
import TooltipContent from '@/components/ui/tooltip/tooltip-content.svelte';
|
||||
import Spinner from '@/components/ui/spinner/spinner.svelte';
|
||||
|
||||
interface Props {
|
||||
post: Post | null;
|
||||
@@ -21,11 +22,21 @@
|
||||
}
|
||||
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') {
|
||||
callbackfn(e);
|
||||
cargando = true;
|
||||
await callbackfn(e);
|
||||
cargando = false;
|
||||
}
|
||||
}
|
||||
async function onsubmit(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
cargando = true;
|
||||
await callbackfn(e);
|
||||
cargando = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog open={true} onOpenChange={() => (post = null)}>
|
||||
@@ -34,11 +45,7 @@
|
||||
<DialogTitle>Editar Publicacion</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription>
|
||||
<form
|
||||
onsubmit={(e: SubmitEvent) => {
|
||||
callbackfn(e);
|
||||
}}
|
||||
>
|
||||
<form {onsubmit}>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
bind:value={post!.content}
|
||||
@@ -61,13 +68,19 @@
|
||||
<TooltipTrigger>
|
||||
<InputGroupButton
|
||||
variant="default"
|
||||
disabled={cargando}
|
||||
type="submit"
|
||||
class="transform rounded-full transition-transform ease-in hover:scale-120"
|
||||
size="xs"
|
||||
>
|
||||
<p class="flex items-center gap-1">
|
||||
Modificar
|
||||
<ArrowUpIcon class="mt-0.5 h-3.5! w-3.5!" />
|
||||
{#if cargando}
|
||||
<Spinner />
|
||||
Cargando...
|
||||
{:else}
|
||||
Modificar
|
||||
<ArrowUpIcon class="mt-0.5 h-3.5! w-3.5!" />
|
||||
{/if}
|
||||
</p>
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
|
||||
21
src/types.d.ts
vendored
21
src/types.d.ts
vendored
@@ -15,6 +15,7 @@ export interface Post {
|
||||
isEdited: boolean;
|
||||
visibility: string;
|
||||
hashtags?: string[];
|
||||
isLiked: boolean;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
@@ -37,6 +38,7 @@ export interface Sesion {
|
||||
url: string;
|
||||
displayName: string;
|
||||
username: string;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
export interface LoginDto {
|
||||
@@ -45,10 +47,10 @@ export interface LoginDto {
|
||||
}
|
||||
|
||||
export interface RegisterDto {
|
||||
username: string?;
|
||||
email: string?;
|
||||
username: string;
|
||||
email: string;
|
||||
password: string?;
|
||||
displayName: string?;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export interface CreatePostDto {
|
||||
@@ -74,3 +76,16 @@ export interface PostResponseDto {
|
||||
visibility: 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