diff --git a/src/lib/components/BotonSeguir.svelte b/src/lib/components/BotonSeguir.svelte new file mode 100644 index 0000000..cd28885 --- /dev/null +++ b/src/lib/components/BotonSeguir.svelte @@ -0,0 +1,99 @@ + + +{#if mensajeError} + +{/if} + +{#if post.authorName !== $sesionStore?.username && post.authorName !== '[deleted]'} + + + + + + {#if seguido == true} + Dejar de seguir + {:else} + Seguir + {/if} + + +{/if} diff --git a/src/lib/components/CardPerfil.svelte b/src/lib/components/CardPerfil.svelte index 3fefccb..a766b77 100644 --- a/src/lib/components/CardPerfil.svelte +++ b/src/lib/components/CardPerfil.svelte @@ -1,5 +1,6 @@ - + {#if usu.bio} diff --git a/src/lib/hooks/esSeguido.ts b/src/lib/hooks/esSeguido.ts new file mode 100644 index 0000000..ea2b51d --- /dev/null +++ b/src/lib/hooks/esSeguido.ts @@ -0,0 +1,26 @@ +import { sesionStore } from '@/stores/usuario'; +import { get } from 'svelte/store'; +import type { Post } from '../../types'; +import { apiBase } from '@/stores/url'; + +export async function esSeguido(post: Post) { + if (!get(sesionStore)?.accessToken || post.authorName === '[deleted]') return; + + const id = post.authorId; + try { + const response = await fetch(`${get(apiBase)}/api/users/${id}/is-following`, { + headers: { + Authorization: `Bearer ${get(sesionStore)?.accessToken}` + } + }); + + if (response.ok) { + const data = await response.json(); + return data; + } else { + return false; + } + } catch { + return null; + } +} diff --git a/src/lib/hooks/logout.ts b/src/lib/hooks/logout.ts index c22dfaf..c21c50c 100644 --- a/src/lib/hooks/logout.ts +++ b/src/lib/hooks/logout.ts @@ -1,4 +1,5 @@ import { goto } from '$app/navigation'; +import { cacheSeguidos } from '@/stores/cacheSeguidos.svelte'; import { apiBase } from '@/stores/url'; import { sesionStore } from '@/stores/usuario'; import { get } from 'svelte/store'; @@ -15,6 +16,7 @@ export async function logout(menuOpen: boolean) { }); if (req.ok) { sesionStore.reset(); + cacheSeguidos.clear(); menuOpen = false; } } catch { diff --git a/src/lib/hooks/obtenerCantidadDeSeguidores.ts b/src/lib/hooks/obtenerCantidadDeSeguidores.ts new file mode 100644 index 0000000..2da003d --- /dev/null +++ b/src/lib/hooks/obtenerCantidadDeSeguidores.ts @@ -0,0 +1,22 @@ +import { apiBase } from '@/stores/url'; +import { sesionStore } from '@/stores/usuario'; +import { get } from 'svelte/store'; + +export async function obtenerCantidadDeSeguidores(id: string, fetch2?: Function) { + const fetchFn = fetch2 || fetch; + try { + const response = await fetchFn(`${get(apiBase)}/api/users/${id}/followers/count`, { + method: 'GET', + headers: { + Authorization: `Bearer ${get(sesionStore)?.accessToken}` + } + }); + + if (!response.ok) { + return 0; + } + return await response.json(); + } catch { + return 0; + } +} diff --git a/src/lib/hooks/obtenerCantidadDeSeguidos.ts b/src/lib/hooks/obtenerCantidadDeSeguidos.ts new file mode 100644 index 0000000..5fab6c8 --- /dev/null +++ b/src/lib/hooks/obtenerCantidadDeSeguidos.ts @@ -0,0 +1,22 @@ +import { apiBase } from '@/stores/url'; +import { sesionStore } from '@/stores/usuario'; +import { get } from 'svelte/store'; + +export async function obtenerCantidadDeSeguidos(id: string, fetch2?: Function) { + const fetchFn = fetch2 || fetch; + try { + const response = await fetchFn(`${get(apiBase)}/api/users/${id}/following/count`, { + method: 'GET', + headers: { + Authorization: `Bearer ${get(sesionStore)?.accessToken}` + } + }); + + if (!response.ok) { + return 0; + } + return await response.json(); + } catch { + return 0; + } +} diff --git a/src/lib/hooks/seguirUsuario.ts b/src/lib/hooks/seguirUsuario.ts new file mode 100644 index 0000000..378bfd8 --- /dev/null +++ b/src/lib/hooks/seguirUsuario.ts @@ -0,0 +1,21 @@ +import { apiBase } from '@/stores/url'; +import { sesionStore } from '@/stores/usuario'; +import { get } from 'svelte/store'; + +export async function seguirUsuario(idusuario: string, toggle: Boolean = false) { + try { + const req = await fetch(`${get(apiBase)}/api/users/${idusuario}/follow`, { + method: !toggle ? 'POST' : 'DELETE', + headers: { + Authorization: `Bearer ${get(sesionStore)?.accessToken}` + } + }); + if (req.ok) { + return true; + } else { + return false; + } + } catch { + return null; + } +} diff --git a/src/lib/stores/cacheSeguidos.svelte.js b/src/lib/stores/cacheSeguidos.svelte.js new file mode 100644 index 0000000..279429a --- /dev/null +++ b/src/lib/stores/cacheSeguidos.svelte.js @@ -0,0 +1,105 @@ +import { browser } from '$app/environment'; +import { writable } from 'svelte/store'; + +class FollowCache { + constructor() { + if (browser) { + this.loadFromStorage(); + } + } + + /** @type {Map} */ + #cache = new Map(); + + /** @type {import('svelte/store').Writable>} */ + store = writable(this.#cache); + + /** + * @param {string} userId + * @returns {boolean | undefined} + */ + get(userId) { + return this.#cache.get(userId); + } + + /** + * @param {string} userId + * @param {boolean} isFollowed + */ + set(userId, isFollowed) { + this.#cache.set(userId, isFollowed); + this.store.set(this.#cache); + this.saveToStorage(); + + if (browser) { + window.dispatchEvent( + new CustomEvent('followCacheUpdated', { + detail: { userId, isFollowed } + }) + ); + } + } + + /** + * @param {string} userId + * @returns {boolean} + */ + has(userId) { + return this.#cache.has(userId); + } + + /** + * @param {string} userId + */ + delete(userId) { + this.#cache.delete(userId); + this.store.set(this.#cache); + this.saveToStorage(); + + if (browser) { + window.dispatchEvent( + new CustomEvent('followCacheUpdated', { + detail: { userId, isFollowed: false } + }) + ); + } + } + + clear() { + this.#cache.clear(); + this.store.set(this.#cache); + this.saveToStorage(); + + if (browser) { + window.dispatchEvent( + new CustomEvent('followCacheUpdated', { + detail: { clearAll: true } + }) + ); + } + } + + saveToStorage() { + if (browser) { + const data = Object.fromEntries(this.#cache); + sessionStorage.setItem('follow-cache', JSON.stringify(data)); + } + } + + loadFromStorage() { + if (browser) { + try { + const stored = sessionStorage.getItem('follow-cache'); + if (stored) { + const data = JSON.parse(stored); + this.#cache = new Map(Object.entries(data)); + this.store.set(this.#cache); + } + } catch (error) { + console.error('Error cargando desde sesion:', error); + } + } + } +} + +export const cacheSeguidos = new FollowCache(); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 1e1533d..eec2ce0 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -38,7 +38,7 @@ - + diff --git a/src/routes/[perfil]/+page.svelte b/src/routes/[perfil]/+page.svelte index ae807da..50d6836 100644 --- a/src/routes/[perfil]/+page.svelte +++ b/src/routes/[perfil]/+page.svelte @@ -18,6 +18,7 @@ import CardError from '@/components/CardError.svelte'; import CardPerfil from '@/components/CardPerfil.svelte'; import DialogModificarUsuario from '@/components/DialogModificarUsuario.svelte'; + import BotonSeguir from '@/components/BotonSeguir.svelte'; let { params } = $props(); @@ -79,6 +80,7 @@ {#if params.perfil == $sesionStore?.username} + {:else if $posts?.length == 0} + {/if} diff --git a/src/routes/[perfil]/+page.ts b/src/routes/[perfil]/+page.ts index f678027..df6bef5 100644 --- a/src/routes/[perfil]/+page.ts +++ b/src/routes/[perfil]/+page.ts @@ -3,16 +3,26 @@ import type { User, UserResponseDto } from '../../types.js'; import { error } from '@sveltejs/kit'; import { obtenerSeguidosPorUsuario } from '@/hooks/obtenerSeguidosPorUsuario.js'; import { obtenerSeguidoresPorUsuario } from '@/hooks/obtenerSeguidoresPorUsuario.js'; +import { obtenerCantidadDeSeguidores } from '@/hooks/obtenerCantidadDeSeguidores.js'; +import { obtenerCantidadDeSeguidos } from '@/hooks/obtenerCantidadDeSeguidos.js'; export async function load({ params, depends, fetch }) { depends('perfil:general'); const usuario: UserResponseDto | null = await obtenerUsuarioPorUsername(params.perfil, fetch); if (!usuario) error(404, 'No se encontro el usuario, ' + params.perfil); - const [seguidos, seguidores] = await Promise.all([ - obtenerSeguidosPorUsuario(usuario.id, 3, fetch), - obtenerSeguidoresPorUsuario(usuario.id, 3, fetch) + const [seguidos, seguidores, countSeguidores, countSeguidos] = await Promise.all([ + obtenerSeguidosPorUsuario(usuario.id, 5, fetch), + obtenerSeguidoresPorUsuario(usuario.id, 5, fetch), + obtenerCantidadDeSeguidores(usuario.id, fetch), + obtenerCantidadDeSeguidos(usuario.id, fetch) ]); - return { ...usuario, seguidos, seguidores }; + return { + ...usuario, + seguidos, + seguidores, + countSeguidores: countSeguidores.count, + countSeguidos: countSeguidos.count + }; } diff --git a/src/routes/[perfil]/seguidores/+page.svelte b/src/routes/[perfil]/seguidores/+page.svelte new file mode 100644 index 0000000..5f6bee8 --- /dev/null +++ b/src/routes/[perfil]/seguidores/+page.svelte @@ -0,0 +1,42 @@ + + +
+
+
+

+ Seguidores de @{data.usuario.username} +

+ +
+ {#if data.seguidores.length === 0} +
+

No hay seguidores para mostrar.

+
+ {:else} +
+ {#each data.seguidores as follower (follower.id)} +
+ +
+ {/each} +
+ {/if} +
+
diff --git a/src/routes/[perfil]/seguidores/+page.ts b/src/routes/[perfil]/seguidores/+page.ts new file mode 100644 index 0000000..e9f0f8d --- /dev/null +++ b/src/routes/[perfil]/seguidores/+page.ts @@ -0,0 +1,20 @@ +import { obtenerSeguidoresPorUsuario } from '@/hooks/obtenerSeguidoresPorUsuario'; +import { obtenerUsuarioPorUsername } from '@/hooks/obtenerUsuario'; +import { error } from '@sveltejs/kit'; +import type { UserResponseDto, UsersResponseDto } from '../../../types'; + +export async function load({ params, fetch }) { + const usuario: UserResponseDto | null = await obtenerUsuarioPorUsername(params.perfil, fetch); + if (!usuario) error(404, 'No se encontro el usuario, ' + params.perfil); + + const seguidoresResponse: UsersResponseDto | null = await obtenerSeguidoresPorUsuario( + usuario.id, + 100, + fetch + ); + + return { + usuario, + seguidores: seguidoresResponse?.response || [] + }; +} diff --git a/src/routes/[perfil]/seguidos/+page.svelte b/src/routes/[perfil]/seguidos/+page.svelte new file mode 100644 index 0000000..f8501c1 --- /dev/null +++ b/src/routes/[perfil]/seguidos/+page.svelte @@ -0,0 +1,42 @@ + + +
+
+
+

+ Seguidos de @{data.usuario.username} +

+ +
+ {#if data.seguidos.length === 0} +
+

No hay seguidos para mostrar.

+
+ {:else} +
+ {#each data.seguidos as follower (follower.id)} +
+ +
+ {/each} +
+ {/if} +
+
diff --git a/src/routes/[perfil]/seguidos/+page.ts b/src/routes/[perfil]/seguidos/+page.ts new file mode 100644 index 0000000..4e0e506 --- /dev/null +++ b/src/routes/[perfil]/seguidos/+page.ts @@ -0,0 +1,20 @@ +import { obtenerUsuarioPorUsername } from '@/hooks/obtenerUsuario'; +import { error } from '@sveltejs/kit'; +import type { UserResponseDto, UsersResponseDto } from '../../../types'; +import { obtenerSeguidosPorUsuario } from '@/hooks/obtenerSeguidosPorUsuario'; + +export async function load({ params, fetch }) { + const usuario: UserResponseDto | null = await obtenerUsuarioPorUsername(params.perfil, fetch); + if (!usuario) error(404, 'No se encontro el usuario, ' + params.perfil); + + const seguidosResponse: UsersResponseDto | null = await obtenerSeguidosPorUsuario( + usuario.id, + 100, + fetch + ); + + return { + usuario, + seguidos: seguidosResponse?.response || [] + }; +} diff --git a/src/routes/post/[idpost]/+page.svelte b/src/routes/post/[idpost]/+page.svelte index 7fbd4dd..a6dbdff 100644 --- a/src/routes/post/[idpost]/+page.svelte +++ b/src/routes/post/[idpost]/+page.svelte @@ -17,6 +17,7 @@ import { likePost } from '@/hooks/likePost'; import ThumbsUp from '@lucide/svelte/icons/thumbs-up'; import { TamañoPantalla } from './TamañoPantalla.svelte'; + import BotonSeguir from '@/components/BotonSeguir.svelte'; interface Prop { data: { @@ -65,7 +66,11 @@ : ''} - + {#if data.post?.imageUrl} + + {:else} + + {/if}
@@ -111,22 +116,29 @@ {#snippet Respuesta(post: Post)}
-
- {#if post.authorImageUrl} - {post.authorDisplayName} - {:else} -
- {post.authorName?.charAt(0).toUpperCase()} +
+
+ {#if post.authorImageUrl} + {post.authorDisplayName} + {:else} +
+ {post.authorName?.charAt(0).toUpperCase()} +
+ {/if} + @{post.authorName} + {new Date(post.createdAt).toLocaleDateString()} +
+ {#key $sesionStore?.accessToken} +
+
- {/if} - @{post.authorName} - {new Date(post.createdAt).toLocaleDateString()} + {/key}

{post.content} diff --git a/src/routes/post/img/[idpost]/+server.ts b/src/routes/post/img/[idpost]/+server.ts index 6f88bab..debec56 100644 --- a/src/routes/post/img/[idpost]/+server.ts +++ b/src/routes/post/img/[idpost]/+server.ts @@ -212,7 +212,8 @@ export const GET: RequestHandler = async ({ params, fetch, request }) => { return new Response(new Uint8Array(pngBuffer), { headers: { 'Content-Type': 'image/png', - 'Cache-Control': 'public, max-age=31536000, immutable' + 'Cache-Control': 'public, max-age=31536000, immutable', + 'Content-Length': pngBuffer.length.toString() } }); };