diff --git a/src/lib/components/PostCard.svelte b/src/lib/components/PostCard.svelte index 10d49b4..1decfe9 100644 --- a/src/lib/components/PostCard.svelte +++ b/src/lib/components/PostCard.svelte @@ -1,6 +1,8 @@ @@ -60,10 +83,12 @@
- - - {post.authorDisplayName[0].toUpperCase()} - + + + + {post.authorDisplayName[0].toUpperCase()} + +
{post.authorDisplayName} @{post.authorName} @@ -72,7 +97,7 @@ {#if post.authorName === $sesionStore?.username} - + @@ -101,16 +126,32 @@
- -

{post.content}

+ +

{post.content}

{#if post.imageUrl} Post {/if}
-
- {post.likesCount} likes - {post.repliesCount} replies +
+ + + {post.createdAt.replace('T', ' ').split('.')[0]} @@ -120,7 +161,7 @@
-{#if mensajeError} +{#if mensajeError || errorLike} diff --git a/src/lib/components/TablaUsuarios.svelte b/src/lib/components/TablaUsuarios.svelte new file mode 100644 index 0000000..db21f4a --- /dev/null +++ b/src/lib/components/TablaUsuarios.svelte @@ -0,0 +1,177 @@ + + +
+ +
+ + + + + ordenarPor("username")} class="cursor-pointer select-none"> + Usuario {getSortIcon("username")} + + ordenarPor("displayName")} class="cursor-pointer select-none"> + Nombre {getSortIcon("displayName")} + + ordenarPor("postsCount")} class="cursor-pointer select-none"> + Cantidad de posts {getSortIcon("postsCount")} + + ordenarPor("createdAt")} class="cursor-pointer select-none"> + Fecha de Creacion {getSortIcon("createdAt")} + + Acciones + + + + {#each usuariosFiltrados as usuario} + + @ + {usuario.username} + + + {usuario.displayName} + {usuario.postsCount} + {usuario.createdAt.replace('Z', ' ').replace('T', ' | ')} + + + + + + +

Recuperar Contraseña

+
+
+ + + + + +

Modificar Usuario

+
+
+ +
+
+ {/each} +
+
+ + diff --git a/src/lib/components/admin/ModificarUsuario.svelte b/src/lib/components/admin/ModificarUsuario.svelte new file mode 100644 index 0000000..f2b77f7 --- /dev/null +++ b/src/lib/components/admin/ModificarUsuario.svelte @@ -0,0 +1,103 @@ + + +
+ { + open = !open; + }} + > + + + Modificar Usuario + +
+
+ + + Nombre + + + + Bio + +
+ + +
+
+
+ + +
+
+
+
+
+
+
+ (error = '')}> + + {error} + + +
diff --git a/src/lib/components/admin/RecuperarContraseña.svelte b/src/lib/components/admin/RecuperarContraseña.svelte new file mode 100644 index 0000000..829d8af --- /dev/null +++ b/src/lib/components/admin/RecuperarContraseña.svelte @@ -0,0 +1,88 @@ + + +
+ (open = !open)}> + + +

Cambiar Contraseña

+
+
+ + + + +

= 8 + }} + > + {nuevapass.length} +

+ / 8 +
+
+
+ + +
+
+
+
+
+
+ (openMensaje = false)}> + + {error === '' ? 'Se modificó el usuario' : error} + +
diff --git a/src/lib/components/crear-post.svelte b/src/lib/components/crear-post.svelte index 0e42575..62c0748 100644 --- a/src/lib/components/crear-post.svelte +++ b/src/lib/components/crear-post.svelte @@ -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 @@

/ 280 - - - - -

Publicar

+ + + + {#if cargando} + + Publicando... + {:else} + Publicar - - - - Ctrl+Enter - - -
+ {/if} + + + + Ctrl+Enter + +
diff --git a/src/lib/components/signup-form.svelte b/src/lib/components/signup-form.svelte index 4db6eaf..aaa9f61 100644 --- a/src/lib/components/signup-form.svelte +++ b/src/lib/components/signup-form.svelte @@ -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; + }; @@ -20,11 +62,29 @@
-
register(e, dto, setAlert)}> + - Nombre de Usuario - +
+ Nombre de Usuario + {#if checkeandoUsuario == null} + + {:else if checkeandoUsuario == true} + + {:else if esUsuarioValido} + + {:else} + + {/if} +
+
@@ -33,22 +93,62 @@ - Email - +
+ Email + {#if checkeandoEmail == null} + + {:else if checkeandoEmail == true} + + {:else if esEmailValido} + + {:else} + + {/if} +
+
Contraseña - - Debe de tener por lo menos 8 caracteres. + + Debe de tener por lo menos 8 caracteres, una minúscula, una mayúscula, un número y un + carácter especial. Confirmar Contraseña - + Confirma la contraseña - + Tenes una cuenta? Iniciar Sesion diff --git a/src/lib/components/ui/badge/badge.svelte b/src/lib/components/ui/badge/badge.svelte new file mode 100644 index 0000000..bfaa9c5 --- /dev/null +++ b/src/lib/components/ui/badge/badge.svelte @@ -0,0 +1,50 @@ + + + + + + {@render children?.()} + diff --git a/src/lib/components/ui/badge/index.ts b/src/lib/components/ui/badge/index.ts new file mode 100644 index 0000000..64e0aa9 --- /dev/null +++ b/src/lib/components/ui/badge/index.ts @@ -0,0 +1,2 @@ +export { default as Badge } from "./badge.svelte"; +export { badgeVariants, type BadgeVariant } from "./badge.svelte"; diff --git a/src/lib/components/ui/checkbox/checkbox.svelte b/src/lib/components/ui/checkbox/checkbox.svelte new file mode 100644 index 0000000..1622e05 --- /dev/null +++ b/src/lib/components/ui/checkbox/checkbox.svelte @@ -0,0 +1,36 @@ + + + + {#snippet children({ checked, indeterminate })} +
+ {#if checked} + + {:else if indeterminate} + + {/if} +
+ {/snippet} +
diff --git a/src/lib/components/ui/checkbox/index.ts b/src/lib/components/ui/checkbox/index.ts new file mode 100644 index 0000000..6d92d94 --- /dev/null +++ b/src/lib/components/ui/checkbox/index.ts @@ -0,0 +1,6 @@ +import Root from "./checkbox.svelte"; +export { + Root, + // + Root as Checkbox, +}; diff --git a/src/lib/components/ui/login-form/login-form.svelte b/src/lib/components/ui/login-form/login-form.svelte index b866e05..52f83e7 100644 --- a/src/lib/components/ui/login-form/login-form.svelte +++ b/src/lib/components/ui/login-form/login-form.svelte @@ -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; + }; @@ -18,7 +27,7 @@ ingrese su usuario para logearse en la cuenta - + Usuario @@ -34,7 +43,14 @@ - + No tenes una cuenta? Registrate diff --git a/src/lib/components/ui/table/index.ts b/src/lib/components/ui/table/index.ts new file mode 100644 index 0000000..14695c8 --- /dev/null +++ b/src/lib/components/ui/table/index.ts @@ -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, +}; diff --git a/src/lib/components/ui/table/table-body.svelte b/src/lib/components/ui/table/table-body.svelte new file mode 100644 index 0000000..29e9687 --- /dev/null +++ b/src/lib/components/ui/table/table-body.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/table/table-caption.svelte b/src/lib/components/ui/table/table-caption.svelte new file mode 100644 index 0000000..4696cff --- /dev/null +++ b/src/lib/components/ui/table/table-caption.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/table/table-cell.svelte b/src/lib/components/ui/table/table-cell.svelte new file mode 100644 index 0000000..2e1036f --- /dev/null +++ b/src/lib/components/ui/table/table-cell.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/table/table-footer.svelte b/src/lib/components/ui/table/table-footer.svelte new file mode 100644 index 0000000..b9b14eb --- /dev/null +++ b/src/lib/components/ui/table/table-footer.svelte @@ -0,0 +1,20 @@ + + +tr]:last:border-b-0", className)} + {...restProps} +> + {@render children?.()} + diff --git a/src/lib/components/ui/table/table-head.svelte b/src/lib/components/ui/table/table-head.svelte new file mode 100644 index 0000000..b0fb68f --- /dev/null +++ b/src/lib/components/ui/table/table-head.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/table/table-header.svelte b/src/lib/components/ui/table/table-header.svelte new file mode 100644 index 0000000..f47d259 --- /dev/null +++ b/src/lib/components/ui/table/table-header.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/table/table-row.svelte b/src/lib/components/ui/table/table-row.svelte new file mode 100644 index 0000000..0df769e --- /dev/null +++ b/src/lib/components/ui/table/table-row.svelte @@ -0,0 +1,23 @@ + + +svelte-css-wrapper]:[&>th,td]:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", + className + )} + {...restProps} +> + {@render children?.()} + diff --git a/src/lib/components/ui/table/table.svelte b/src/lib/components/ui/table/table.svelte new file mode 100644 index 0000000..a334956 --- /dev/null +++ b/src/lib/components/ui/table/table.svelte @@ -0,0 +1,22 @@ + + +
+ + {@render children?.()} +
+
diff --git a/src/lib/head/AvatarButton.svelte b/src/lib/head/AvatarButton.svelte index 1d25635..1ae2328 100644 --- a/src/lib/head/AvatarButton.svelte +++ b/src/lib/head/AvatarButton.svelte @@ -27,6 +27,10 @@ goto('/' + $sesionStore?.username)} >Mi Perfil + {#if $sesionStore?.isAdmin} + goto('/admin')}>Menu Admin + {/if} + await logout(menuOpen)}>Cerrar Sesion diff --git a/src/lib/hooks/cambiarContraseña.ts b/src/lib/hooks/cambiarContraseña.ts new file mode 100644 index 0000000..5721693 --- /dev/null +++ b/src/lib/hooks/cambiarContraseña.ts @@ -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'; + } +} diff --git a/src/lib/hooks/checkEmail.ts b/src/lib/hooks/checkEmail.ts new file mode 100644 index 0000000..2bbc8e8 --- /dev/null +++ b/src/lib/hooks/checkEmail.ts @@ -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; + } +} diff --git a/src/lib/hooks/checkUsername.ts b/src/lib/hooks/checkUsername.ts new file mode 100644 index 0000000..2d82818 --- /dev/null +++ b/src/lib/hooks/checkUsername.ts @@ -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; + } +} diff --git a/src/lib/hooks/getPosts.ts b/src/lib/hooks/getPosts.ts new file mode 100644 index 0000000..e95c52e --- /dev/null +++ b/src/lib/hooks/getPosts.ts @@ -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(); + } +} diff --git a/src/lib/hooks/likePost.ts b/src/lib/hooks/likePost.ts new file mode 100644 index 0000000..a16787c --- /dev/null +++ b/src/lib/hooks/likePost.ts @@ -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 }; + } +} diff --git a/src/lib/hooks/login.ts b/src/lib/hooks/login.ts index 9794a88..7d9dc2d 100644 --- a/src/lib/hooks/login.ts +++ b/src/lib/hooks/login.ts @@ -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 { diff --git a/src/lib/hooks/logout.ts b/src/lib/hooks/logout.ts index 9fe4b4e..c22dfaf 100644 --- a/src/lib/hooks/logout.ts +++ b/src/lib/hooks/logout.ts @@ -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' }); diff --git a/src/lib/hooks/obtenerSeguidoresPorUsuario.ts b/src/lib/hooks/obtenerSeguidoresPorUsuario.ts new file mode 100644 index 0000000..0a57e4c --- /dev/null +++ b/src/lib/hooks/obtenerSeguidoresPorUsuario.ts @@ -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 { + 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; + } +} diff --git a/src/lib/hooks/obtenerSeguidosPorUsuario.ts b/src/lib/hooks/obtenerSeguidosPorUsuario.ts new file mode 100644 index 0000000..a32aebe --- /dev/null +++ b/src/lib/hooks/obtenerSeguidosPorUsuario.ts @@ -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 { + 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; + } +} diff --git a/src/lib/hooks/obtenerUsuario.ts b/src/lib/hooks/obtenerUsuario.ts new file mode 100644 index 0000000..40481bf --- /dev/null +++ b/src/lib/hooks/obtenerUsuario.ts @@ -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 { + 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; + } +} diff --git a/src/lib/hooks/register.ts b/src/lib/hooks/register.ts index 681959e..531b7dc 100644 --- a/src/lib/hooks/register.ts +++ b/src/lib/hooks/register.ts @@ -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; diff --git a/src/lib/hooks/updatePost.ts b/src/lib/hooks/updatePost.ts index aaa0ca7..523f526 100644 --- a/src/lib/hooks/updatePost.ts +++ b/src/lib/hooks/updatePost.ts @@ -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(); diff --git a/src/lib/hooks/updateUsuario.ts b/src/lib/hooks/updateUsuario.ts new file mode 100644 index 0000000..2a2bfb3 --- /dev/null +++ b/src/lib/hooks/updateUsuario.ts @@ -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" + } +} diff --git a/src/lib/stores/usuario.ts b/src/lib/stores/usuario.ts index 4a7c5ac..aa1b2e5 100644 --- a/src/lib/stores/usuario.ts +++ b/src/lib/stores/usuario.ts @@ -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(); } diff --git a/src/routes/(privado)/admin/+page.svelte b/src/routes/(privado)/admin/+page.svelte new file mode 100644 index 0000000..956243c --- /dev/null +++ b/src/routes/(privado)/admin/+page.svelte @@ -0,0 +1,33 @@ + + +
+
+ + + +

+ Gestion Usuarios +

+
+
+ + {#if page.data.usuarios.length === 0} + No hay posts que mostar + {:else} + + {/if} + +
+
+
diff --git a/src/routes/(privado)/admin/+page.ts b/src/routes/(privado)/admin/+page.ts new file mode 100644 index 0000000..7d23379 --- /dev/null +++ b/src/routes/(privado)/admin/+page.ts @@ -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 }; +} diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte new file mode 100644 index 0000000..460a950 --- /dev/null +++ b/src/routes/+error.svelte @@ -0,0 +1,20 @@ + + +
+
+ + +

+ {page.status} +

+

+ {page.error!.message} +

+
+
+
+
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 718627a..a0ca932 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -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(); @@ -13,4 +14,6 @@
-{@render children()} + + {@render children()} + diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index b21b01c..cfb4bff 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -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(''); diff --git a/src/routes/+page.ts b/src/routes/+page.ts index 77ab0a0..fcb6fab 100644 --- a/src/routes/+page.ts +++ b/src/routes/+page.ts @@ -1 +1 @@ -export const ssr = true; +//export const ssr = true; diff --git a/src/routes/[perfil]/+page.server.ts b/src/routes/[perfil]/+page.server.ts new file mode 100644 index 0000000..f6695b5 --- /dev/null +++ b/src/routes/[perfil]/+page.server.ts @@ -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 }; +} diff --git a/src/routes/[perfil]/+page.svelte b/src/routes/[perfil]/+page.svelte index e1160cc..4ccc71e 100644 --- a/src/routes/[perfil]/+page.svelte +++ b/src/routes/[perfil]/+page.svelte @@ -1,6 +1,7 @@ (post = null)}> @@ -34,11 +45,7 @@ Editar Publicacion - { - callbackfn(e); - }} - > +

- Modificar - + {#if cargando} + + Cargando... + {:else} + Modificar + + {/if}

diff --git a/src/types.d.ts b/src/types.d.ts index 16d437a..c7749ce 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -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; +}