Compare commits

47 Commits

Author SHA1 Message Date
emailerfacu-spec
398592ea1d Merge pull request #85 from emailerfacu-spec/dev
Dev
2026-01-29 23:29:29 -03:00
dc9c3b4f3c otro caso donde fran saco el reset posts 2026-01-29 20:58:20 -03:00
emailerfacu-spec
93c4b15d5b Creado un readme para el proyecto 2026-01-29 20:48:19 -03:00
7901a677d5 no se actualizaba la ui cuando se modifica el usuario 2026-01-29 20:26:36 -03:00
0ea873e039 fix: no se inicializaba el firebaseapp 2026-01-29 20:18:37 -03:00
c0d637a34c Update .prettierrc 2026-01-27 19:45:14 -03:00
e1864660a7 bun run format 2026-01-27 19:43:32 -03:00
37f1b46306 fix: error cuando no hay usuarios pero si htags 2026-01-27 19:38:13 -03:00
1f4166a651 fix: seguia mostrando el boton cuando se llega a una pagina vacia 2026-01-27 19:03:21 -03:00
b5ba2437b4 paginacion en /posts lista 2026-01-27 18:57:04 -03:00
ceec19fb91 añadido soporte para mandar una query al back 2026-01-26 16:33:26 -03:00
9c59c3d036 fix: eliminados algunos log 2026-01-24 14:12:25 -03:00
921836c115 fix: se cortaba el boton con el footer 2026-01-24 14:09:26 -03:00
Fran
54593dd2eb add pagination in following and followers 2026-01-24 09:02:49 -03:00
Fran
9eb92b0c06 fix pagination in profile 2026-01-24 01:43:34 -03:00
1c79c5fcda esta fixeado el que salia / vacio o con posts de otras paginas 2026-01-23 18:28:13 -03:00
emailerfacu-spec
000c65712f Merge pull request #82 from emailerfacu-spec/firebase-Oauth
Firebase oauth
2026-01-22 23:53:18 -03:00
Fran
d35de05a7b some dude removed the + button (that dude was me) 2026-01-19 21:49:40 -03:00
Fran
0ba88d14ed add pagination in profile 2026-01-19 20:28:22 -03:00
65d65baaee hecha pagina de about 2026-01-16 20:15:58 -03:00
e4508a0487 fix: llamada innecesaria a cargarSeguido
emmm skill issue?
2026-01-16 01:29:24 -03:00
6cf920799c arreglado reset de posts 2026-01-16 01:22:17 -03:00
ebc2ebc322 cambiado a runes 2026-01-16 01:22:07 -03:00
8fbe62d391 fix se borraban los posts en / 2026-01-16 01:18:37 -03:00
emailerfacu-spec
4d99695a59 Merge pull request #72 from emailerfacu-spec/dev
Dev
2026-01-09 22:39:01 -03:00
emailerfacu-spec
1f2f26f780 Merge pull request #67 from emailerfacu-spec/dev
otra try
2026-01-06 13:55:55 -03:00
emailerfacu-spec
3dc0cfc8a4 Merge pull request #66 from emailerfacu-spec/dev
cambios en el path
2026-01-06 13:47:28 -03:00
emailerfacu-spec
22ecdb6e9d Merge pull request #65 from emailerfacu-spec/dev
renderiza una imagen del post en og:image
2026-01-06 13:40:37 -03:00
emailerfacu-spec
e785b60935 Merge pull request #63 from emailerfacu-spec/dev
Dev
2026-01-05 21:22:13 -03:00
emailerfacu-spec
c47b9956b9 Merge pull request #62 from emailerfacu-spec/dev
Dev
2026-01-02 19:04:14 -03:00
emailerfacu-spec
61232ada05 Merge pull request #58 from emailerfacu-spec/dev
Dev
2026-01-01 22:07:06 -03:00
emailerfacu-spec
fae9a676e2 Merge pull request #54 from emailerfacu-spec/dev
otras correcciones
2025-12-26 19:24:55 -03:00
emailerfacu-spec
9b0937b731 Merge pull request #53 from emailerfacu-spec/dev
otro fix
2025-12-26 19:16:58 -03:00
emailerfacu-spec
77f3901cb3 Merge pull request #52 from emailerfacu-spec/dev
el regex no era global
2025-12-26 19:13:00 -03:00
emailerfacu-spec
bd09ae005b Merge pull request #51 from emailerfacu-spec/dev
modificadas las reglas de como renderizo los posts
2025-12-26 19:08:53 -03:00
emailerfacu-spec
d1a26cc132 Merge pull request #50 from emailerfacu-spec/dev
Dev
2025-12-26 16:55:09 -03:00
emailerfacu-spec
670f8ae3e2 Merge pull request #48 from emailerfacu-spec/dev
Hotfix
2025-12-26 16:41:08 -03:00
emailerfacu-spec
b4382b361a Merge pull request #46 from emailerfacu-spec/dev
Dev
2025-12-26 11:48:23 -03:00
emailerfacu-spec
17f7bed1d9 Merge pull request #45 from emailerfacu-spec/dev
Hotfix-2: hotfix siguie llamando al length
2025-12-26 11:37:28 -03:00
emailerfacu-spec
29b7effd57 Merge pull request #44 from emailerfacu-spec/dev
Hotfix-1
2025-12-26 11:24:24 -03:00
emailerfacu-spec
f425dd13a7 Merge pull request #38 from emailerfacu-spec/dev
algunos refactors y caracteristicas como:

    añadida pagina para ver htags
    añadida pagina para ver resultados de busquedas
    Ahora se puede editar el perfil propio en la pagina de perfil
2025-12-26 01:54:11 -03:00
emailerfacu-spec
09ddb0800c Merge pull request #33 from emailerfacu-spec/dev
Dev
2025-12-20 19:02:56 -03:00
emailerfacu-spec
d60daa624c Merge pull request #24 from emailerfacu-spec/dev
Dev
2025-12-05 13:30:11 -03:00
emailerfacu-spec
ee5535dbc6 Merge pull request #12 from emailerfacu-spec/dev
Dev
2025-11-26 20:14:27 -03:00
emailerfacu-spec
8c0da761e6 Merge pull request #6 from emailerfacu-spec/dev
snapshot 25/11/2025 - B
2025-11-25 19:21:35 -03:00
emailerfacu-spec
ef16f649dd Merge pull request #5 from emailerfacu-spec/dev
snapshot 25/11/2025
2025-11-25 19:18:22 -03:00
emailerfacu-spec
8decf85d3b Merge pull request #1 from emailerfacu-spec/dev
Login, Register y Logout Terminado
2025-11-14 22:24:55 -03:00
39 changed files with 584 additions and 292 deletions

View File

@@ -1,4 +1,5 @@
# Package Managers # Package Managers
src/lib/components/ui/
package-lock.json package-lock.json
pnpm-lock.yaml pnpm-lock.yaml
yarn.lock yarn.lock

View File

@@ -3,10 +3,7 @@
"singleQuote": true, "singleQuote": true,
"trailingComma": "none", "trailingComma": "none",
"printWidth": 100, "printWidth": 100,
"plugins": [ "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"prettier-plugin-svelte",
"prettier-plugin-tailwindcss"
],
"overrides": [ "overrides": [
{ {
"files": "*.svelte", "files": "*.svelte",

View File

@@ -1,38 +1,23 @@
# sv # Minix - Front
Este repositorio consiste del repo que contiene el codigo para poder hacer deploy de una instancia de minix
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). # ¿Que es Minix?
Intentamos hacer algo parecido a x.com pero adaptado a nuestra vision.
## Creating a project # Galeria
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/2ebe2983-04dc-4cca-ab46-e361ec6f72ee" />
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/49529256-2c36-4a40-bbab-d02228028def" />
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/38045721-c350-4e06-a5e1-4aa271139894" />
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/5c228110-feac-4d52-8451-222f04034f94" />
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/9618162d-fcde-42ff-bd7e-fe334498e9c7" />
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/580f0ad3-89ed-4686-98c2-f8dea04f80de" />
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/ce40fd1a-bac0-443f-b01e-d861c335236c" />
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/ea24be47-f35a-4d68-a9d6-f6796e656911" />
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/adf1f792-31e0-4f10-a4f3-e8590b4f0582" />
If you're seeing this, you've probably already done this step. Congrats! # ¿Que tecnologias usamos?
- svelte(kit) (framework)
- shadcn-svelte (ui)
- firebase/auth
- vercel (host)
```sh
# create a new project in the current directory
npx sv create
# create a new project in my-app
npx sv create my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```sh
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```sh
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

View File

@@ -16,16 +16,18 @@
let { let {
post, post,
variant = 'icon-lg' variant = 'icon-lg'
}: { post: Omit<Partial<Post>, 'authorId'> & { authorId: string }; variant?: string } = $props(); }: {
post: Omit<Partial<Post>, 'authorId'> & { authorId: string; id: string };
variant?: 'icon-lg' | 'default' | 'sm' | 'lg' | 'icon' | 'icon-sm';
} = $props();
let seguido: Boolean | null = $state(null); let seguido: boolean | null = $state(null);
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window.addEventListener('followCacheUpdated', (( window.addEventListener('followCacheUpdated', ((
event: CustomEvent<{ userId: string; isFollowed: boolean } | { clearAll: true }> event: CustomEvent<{ userId: string; isFollowed: boolean } | { clearAll: true }>
) => { ) => {
if ('clearAll' in event.detail && event.detail.clearAll === true) { if ('clearAll' in event.detail && event.detail.clearAll === true) {
cargarSeguido();
} else if ('userId' in event.detail && event.detail.userId === post.authorId) { } else if ('userId' in event.detail && event.detail.userId === post.authorId) {
seguido = event.detail.isFollowed; seguido = event.detail.isFollowed;
} }
@@ -41,9 +43,11 @@
async function cargarSeguido() { async function cargarSeguido() {
let a = cacheSeguidos.get(post.authorId); let a = cacheSeguidos.get(post.authorId);
if (a === undefined) { if (a === undefined) {
const seguidoStatus = await esSeguido(post); const seguidoStatus = await esSeguido(post as Post);
cacheSeguidos.set(post.authorId, seguidoStatus.isFollowing || false); if (seguidoStatus) {
seguido = seguidoStatus.isFollowing || false; cacheSeguidos.set(post.authorId, seguidoStatus.isFollowing || false);
seguido = seguidoStatus.isFollowing || false;
}
return; return;
} }
seguido = a; seguido = a;

View File

@@ -104,10 +104,10 @@
</div> </div>
<h1 class="mt-10 scroll-m-20 text-center text-2xl font-extrabold tracking-tight lg:text-5xl"> <h1 class="mt-10 scroll-m-20 text-center text-2xl font-extrabold tracking-tight lg:text-5xl">
{usu.displayName} {data.displayName}
<p class="ml-2 text-2xl font-medium text-muted-foreground">@{data.username}</p> <p class="ml-2 text-2xl font-medium text-muted-foreground">@{data.username}</p>
</h1> </h1>
{#if usu.bio} {#if data.bio}
<p class="mt-4 rounded-4xl bg-accent p-4 text-center text-muted-foreground"> <p class="mt-4 rounded-4xl bg-accent p-4 text-center text-muted-foreground">
{@html contenido()} {@html contenido()}
<!-- {usu.bio.replaceAll('<', '')} --> <!-- {usu.bio.replaceAll('<', '')} -->
@@ -129,10 +129,10 @@
</Avatar> </Avatar>
</div> </div>
<h1 class="mt-10 scroll-m-20 text-center text-2xl font-extrabold tracking-tight lg:text-5xl"> <h1 class="mt-10 scroll-m-20 text-center text-2xl font-extrabold tracking-tight lg:text-5xl">
{usu.displayName} {data.displayName}
<p class="ml-2 text-2xl font-medium text-muted-foreground">@{data.username}</p> <p class="ml-2 text-2xl font-medium text-muted-foreground">@{data.username}</p>
</h1> </h1>
{#if usu.bio} {#if data.bio}
<p class="mt-4 rounded-4xl bg-accent p-4 text-center text-muted-foreground"> <p class="mt-4 rounded-4xl bg-accent p-4 text-center text-muted-foreground">
{@html usu.bio.replaceAll('\n', '<br>')} {@html usu.bio.replaceAll('\n', '<br>')}
</p> </p>

View File

@@ -14,7 +14,7 @@
import { updateUsuario } from '@/hooks/updateUsuario'; import { updateUsuario } from '@/hooks/updateUsuario';
import DialogFooter from './ui/dialog/dialog-footer.svelte'; import DialogFooter from './ui/dialog/dialog-footer.svelte';
import Spinner from './ui/spinner/spinner.svelte'; import Spinner from './ui/spinner/spinner.svelte';
import { invalidate } from '$app/navigation'; import { invalidate, invalidateAll } from '$app/navigation';
import { page } from '$app/state'; import { page } from '$app/state';
let { data = $bindable(), children } = $props(); let { data = $bindable(), children } = $props();
@@ -42,9 +42,9 @@
}); });
cargando = false; cargando = false;
open = false; open = false;
// invalidateAll(); await invalidateAll();
await invalidate(page.url); // await invalidate(page.url);
await invalidate('perfil:general'); // await invalidate('perfil:general');
} }
function onkeydown(e: KeyboardEvent) { function onkeydown(e: KeyboardEvent) {

View File

@@ -44,7 +44,7 @@
} }
cargando = true; cargando = true;
try { try {
await cambiarContraseñaUsuario(data.id, passwordData.oldPassword, passwordData.newPassword); await cambiarContraseñaUsuario(passwordData.oldPassword, passwordData.newPassword, data.id);
cargando = false; cargando = false;
open = false; open = false;
passwordData.oldPassword = ''; passwordData.oldPassword = '';
@@ -103,7 +103,7 @@
/> />
</Field> </Field>
</FieldGroup> </FieldGroup>
<Button type="submit" disabled={!coinsiden || cargando}> <Button type="submit" class="mt-6" disabled={!coinsiden || cargando}>
{#if cargando} {#if cargando}
<Spinner /> <Spinner />
{:else} {:else}

View File

@@ -29,6 +29,7 @@
import InputGroupInput from './ui/input-group/input-group-input.svelte'; import InputGroupInput from './ui/input-group/input-group-input.svelte';
import AgregarUsuario from './admin/AgregarUsuario.svelte'; import AgregarUsuario from './admin/AgregarUsuario.svelte';
import DarAdmin from './admin/DarAdmin.svelte'; import DarAdmin from './admin/DarAdmin.svelte';
import { busquedaAdminUsuarios } from '@/hooks/busquedaAdminUsuarios';
interface Props { interface Props {
usuarios: UserResponseDto[]; usuarios: UserResponseDto[];
@@ -43,12 +44,8 @@
let opencrearUsuario = $state(false); let opencrearUsuario = $state(false);
let usuarioBorrar: UserResponseDto | null = $state(null); let usuarioBorrar: UserResponseDto | null = $state(null);
//si ponia contraseña en español quedaba muy largo el nombre
let usuarioCambioPass: UserResponseDto | null = $state(null); let usuarioCambioPass: UserResponseDto | null = $state(null);
let usuarioModificar: UserResponseDto | null = $state(null); let usuarioModificar: UserResponseDto | null = $state(null);
let usuarioDarAdmin: UserResponseDto | null = $state(null); let usuarioDarAdmin: UserResponseDto | null = $state(null);
let search = $state(''); let search = $state('');
@@ -57,6 +54,8 @@
let sortBy = $state<SortKey | null>(null); let sortBy = $state<SortKey | null>(null);
let sortDirection = $state<'asc' | 'desc'>('asc'); let sortDirection = $state<'asc' | 'desc'>('asc');
let usuariosFiltrados = $state(usuarios);
function ordenarPor(campo: SortKey) { function ordenarPor(campo: SortKey) {
if (sortBy === campo) { if (sortBy === campo) {
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc'; sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
@@ -64,38 +63,27 @@
sortBy = campo; sortBy = campo;
sortDirection = 'asc'; sortDirection = 'asc';
} }
usuariosFiltrados = usuariosFiltrados.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);
});
} }
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) { function getSortIcon(campo: SortKey) {
if (sortBy !== campo) return ''; if (sortBy !== campo) return '';
return sortDirection === 'asc' ? '↑' : '↓'; return sortDirection === 'asc' ? '↑' : '↓';
@@ -122,12 +110,36 @@
} }
// $inspect(usuarios); // $inspect(usuarios);
let timeoutId: ReturnType<typeof setTimeout> | number | undefined;
function buscarUsuarios() {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(async () => {
if (search === '') {
usuariosFiltrados = usuarios;
return;
}
usuariosFiltrados = await busquedaAdminUsuarios(search);
}, 200);
return () => {
if (timeoutId) clearTimeout(timeoutId);
};
}
</script> </script>
<div class="mb-4 flex gap-2"> <div class="mb-4 flex gap-2">
<InputGroup> <InputGroup>
<InputGroupAddon align="inline-start"><Search></Search></InputGroupAddon> <InputGroupAddon align="inline-start"><Search></Search></InputGroupAddon>
<InputGroupInput type="text" placeholder="Buscar usuario..." bind:value={search} /> <InputGroupInput
type="text"
placeholder="Buscar usuario..."
bind:value={search}
oninput={() => buscarUsuarios()}
/>
</InputGroup> </InputGroup>
<Button <Button
onclick={() => (opencrearUsuario = !opencrearUsuario)} onclick={() => (opencrearUsuario = !opencrearUsuario)}

View File

@@ -35,8 +35,5 @@
<BotonSeguir post={{ authorId: usu.id }} /> <BotonSeguir post={{ authorId: usu.id }} />
</div> </div>
</div> </div>
{#if usu.bio}
<div class="mt-4 rounded-full bg-accent p-4 text-muted-foreground">{usu.bio}</div>
{/if}
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -50,7 +50,7 @@
<header class="border-b bg-background/95 backdrop-blur"> <header class="border-b bg-background/95 backdrop-blur">
<div class="mx-4 ms-2 flex h-12 items-center justify-between"> <div class="mx-4 ms-2 flex h-12 items-center justify-between">
<div class="flex items-center"> <div class="flex items-center">
<a href="/" class="mr-6 flex items-center space-x-2"> <a data-sveltekit-preload-data={false} href="/" class="mr-6 flex items-center space-x-2">
<Avatar <Avatar
class="h-8 w-8 transform rounded-sm! transition-transform duration-300 ease-in-out hover:scale-130 hover:rotate-12" class="h-8 w-8 transform rounded-sm! transition-transform duration-300 ease-in-out hover:scale-130 hover:rotate-12"
> >

View File

@@ -0,0 +1,21 @@
import { apiBase } from '@/stores/url';
import { sesionStore } from '@/stores/usuario';
import { get } from 'svelte/store';
export async function busquedaAdminUsuarios(q: string) {
try {
const response = await fetch(get(apiBase) + '/api/admin/users?q=' + q, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
}
});
if (response.ok) {
return await response.json();
}
return [];
} catch {
return [];
}
}

View File

@@ -1,16 +1,16 @@
import { apiBase } from "@/stores/url"; import { apiBase } from '@/stores/url';
import { get } from "svelte/store"; import { get } from 'svelte/store';
export async function checkEmail(email: string) { export async function checkEmail(email: string) {
try { try {
const req = await fetch(`${get(apiBase)}/api/users/check-email/${email}`, { const req = await fetch(`${get(apiBase)}/api/users/check-email/${email}`, {
method: "GET" method: 'GET'
}); });
if (req.ok){ if (req.ok) {
return (await req.json()).available; return (await req.json()).available;
} }
return false; return false;
} catch { } catch {
return false; return false;
} }
} }

View File

@@ -1,16 +1,16 @@
import { apiBase } from "@/stores/url"; import { apiBase } from '@/stores/url';
import { get } from "svelte/store"; import { get } from 'svelte/store';
export async function checkUsername(username: string) { export async function checkUsername(username: string) {
try { try {
const req = await fetch(`${get(apiBase)}/api/users/check-username/${username}`, { const req = await fetch(`${get(apiBase)}/api/users/check-username/${username}`, {
method: "GET" method: 'GET'
}); });
if (req.ok){ if (req.ok) {
return (await req.json()).available; return (await req.json()).available;
} }
return false; return false;
} catch { } catch {
return false; return false;
} }
} }

View File

@@ -1,6 +1,6 @@
import { apiBase } from "@/stores/url"; import { apiBase } from '@/stores/url';
import { sesionStore } from "@/stores/usuario"; import { sesionStore } from '@/stores/usuario';
import { get } from "svelte/store"; import { get } from 'svelte/store';
import type { Post } from '../../types'; import type { Post } from '../../types';
import { PAGE_SIZE } from '../stores/posts'; import { PAGE_SIZE } from '../stores/posts';
@@ -10,12 +10,11 @@ export async function getPosts(page: number = 1): Promise<Post[]> {
const headers: HeadersInit = {}; const headers: HeadersInit = {};
if (token) headers.Authorization = `Bearer ${token}`; if (token) headers.Authorization = `Bearer ${token}`;
const res = await fetch( const res = await fetch(`${get(apiBase)}/timeline?page=${page}&pageSize=${PAGE_SIZE}`, {
`${get(apiBase)}/timeline?page=${page}&pageSize=${PAGE_SIZE}`, headers
{ headers } });
);
if (!res.ok) throw new Error('Error cargando posts'); if (!res.ok) throw new Error('Error cargando posts');
return res.json(); return res.json();
} }

View File

@@ -4,7 +4,7 @@ import { sesionStore } from '@/stores/usuario';
import type { Post } from '../../types'; import type { Post } from '../../types';
export async function likePost(post: Post) { export async function likePost(post: Post) {
let method = post.isLiked ? "DELETE" : "POST"; let method = post.isLiked ? 'DELETE' : 'POST';
try { try {
const req = await fetch(get(apiBase) + `/api/posts/${post.id}/like`, { const req = await fetch(get(apiBase) + `/api/posts/${post.id}/like`, {
method: method, method: method,

View File

@@ -24,7 +24,7 @@ export async function loadMorePosts() {
if (newPosts.length < PAGE_SIZE) { if (newPosts.length < PAGE_SIZE) {
finished = true; finished = true;
} else { } else {
page.update(p => p + 1); page.update((p) => p + 1);
} }
} finally { } finally {
loadingPosts.set(false); loadingPosts.set(false);

View File

@@ -6,12 +6,13 @@ import type { Post } from '../../types';
export async function obtenerRespuestasPorId( export async function obtenerRespuestasPorId(
id: string, id: string,
fetch2?: Function, fetch2?: Function,
depends?: Function depends?: Function,
page: number = 1
): Promise<string | Post[] | null> { ): Promise<string | Post[] | null> {
if (depends) depends('post:respuestas'); if (depends) depends('post:respuestas');
const fetchFn = fetch2 ? fetch2 : fetch; const fetchFn = fetch2 ? fetch2 : fetch;
try { try {
const req = await fetchFn(`${get(apiBase)}/api/posts/${id}/replies`, { const req = await fetchFn(`${get(apiBase)}/api/posts/${id}/replies?page=${page}`, {
method: 'GET', method: 'GET',
headers: { headers: {
Authorization: `Bearer ${get(sesionStore)?.accessToken}` Authorization: `Bearer ${get(sesionStore)?.accessToken}`

View File

@@ -1,22 +1,28 @@
import { sesionStore } from '@/stores/usuario'; import { sesionStore } from '@/stores/usuario';
import type { UsersResponseDto } from '../../types'; import type { UsersResponseDto } from '../../types';
import { get } from 'svelte/store';
import { apiBase } from '@/stores/url'; import { apiBase } from '@/stores/url';
import { get } from 'svelte/store';
export async function obtenerSeguidoresPorUsuario( export async function obtenerSeguidoresPorUsuario(
id: string, id: string,
page: number = 1,
limit: number = 20, limit: number = 20,
fetch2: Function fetch2?: Function
): Promise<UsersResponseDto | null> { ): Promise<UsersResponseDto | null> {
try { try {
const fetchFunc = fetch2 || fetch; const fetchFunc = fetch2 || fetch;
const response = await fetchFunc(`${get(apiBase)}/api/users/${id}/followers?limit=${limit}`, { const skip = (page - 1) * limit;
method: 'GET',
headers: { const response = await fetchFunc(
'Content-Type': 'application/json', `${get(apiBase)}/api/users/${id}/followers?skip=${skip}&limit=${limit}`,
Authorization: `Bearer ${get(sesionStore)?.accessToken}` {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
}
} }
}); );
if (!response.ok) { if (!response.ok) {
return null; return null;

View File

@@ -5,26 +5,31 @@ import { get } from 'svelte/store';
export async function obtenerSeguidosPorUsuario( export async function obtenerSeguidosPorUsuario(
id: string, id: string,
page: number = 1,
limit: number = 20, limit: number = 20,
fetch2?: Function fetch2?: Function
): Promise<UsersResponseDto | null> { ): Promise<UsersResponseDto | null> {
try { try {
const fetchFunc = fetch2 || fetch; const fetchFunc = fetch2 || fetch;
const skip = (page - 1) * limit;
const response = await fetchFunc(`${get(apiBase)}/api/users/${id}/following?limit=${limit}`, { const response = await fetchFunc(
method: 'GET', `${get(apiBase)}/api/users/${id}/following?skip=${skip}&limit=${limit}`,
headers: { {
'Content-Type': 'application/json', method: 'GET',
Authorization: `Bearer ${get(sesionStore)?.accessToken}` headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
}
} }
}); );
if (!response.ok) { if (!response.ok) {
return null; return null;
} }
const users: UsersResponseDto = await response.json(); const data: UsersResponseDto = await response.json();
return users; return data;
} catch (error) { } catch (error) {
return null; return null;
} }

View File

@@ -1,27 +1,27 @@
import { addPost } from "@/stores/posts"; import { addPost } from '@/stores/posts';
import { apiBase } from "@/stores/url"; import { apiBase } from '@/stores/url';
import { sesionStore } from "@/stores/usuario"; import { sesionStore } from '@/stores/usuario';
import { get } from "svelte/store"; import { get } from 'svelte/store';
export async function publicarPost(formData: FormData){ export async function publicarPost(formData: FormData) {
try{ try {
const req = fetch(get(apiBase) + '/api/posts', { const req = fetch(get(apiBase) + '/api/posts', {
method: 'POST', method: 'POST',
//credentials: 'include', //credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${get(sesionStore)?.accessToken}` Authorization: `Bearer ${get(sesionStore)?.accessToken}`
}, },
body: formData body: formData
}); });
const res = await req; const res = await req;
if (res.ok) { if (res.ok) {
const post = await res.json(); const post = await res.json();
addPost(post); addPost(post);
return ''; return '';
}
return 'No se pudo crear el post.';
} catch {
return 'Fallo al alcanzar el servidor';
} }
return 'No se pudo crear el post.';
} catch {
return 'Fallo al alcanzar el servidor';
}
} }

View File

@@ -1,7 +1,4 @@
export async function updateImagenDePerfil(){ export async function updateImagenDePerfil() {
try{ try {
} catch {}
}catch{
}
} }

View File

@@ -5,9 +5,9 @@ import { sesionStore } from '@/stores/usuario';
export async function updatePost(post: Post, callbackfn: Function, message: string) { export async function updatePost(post: Post, callbackfn: Function, message: string) {
try { try {
const formData = new FormData(); const formData = new FormData();
formData.append("content", post.content); formData.append('content', post.content);
formData.append("image", post.image||""); formData.append('image', post.image || '');
const req = await fetch(get(apiBase) + `/api/posts/${post.id}`, { const req = await fetch(get(apiBase) + `/api/posts/${post.id}`, {
method: 'PUT', method: 'PUT',

View File

@@ -19,21 +19,14 @@ export const addPost = (post: Post) => {
posts.update((current) => [post, ...current]); posts.update((current) => [post, ...current]);
}; };
export const updatePostStore = ( export const updatePostStore = (postId: string, updatedData: Partial<Post>) => {
postId: string,
updatedData: Partial<Post>
) => {
posts.update((current) => posts.update((current) =>
current.map((post) => current.map((post) => (post.id === postId ? { ...post, ...updatedData } : post))
post.id === postId ? { ...post, ...updatedData } : post
)
); );
}; };
export const removePost = (postId: string) => { export const removePost = (postId: string) => {
posts.update((current) => posts.update((current) => current.filter((post) => post.id !== postId));
current.filter((post) => post.id !== postId)
);
}; };
export const resetPosts = () => { export const resetPosts = () => {

View File

@@ -2,6 +2,7 @@ import { get, writable } from 'svelte/store';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import type { Sesion } from '../../types'; import type { Sesion } from '../../types';
import { apiBase } from '@/stores/url'; import { apiBase } from '@/stores/url';
import { getFirebaseApp, getFirebaseAuth } from './firebase';
const { subscribe } = apiBase; const { subscribe } = apiBase;
let baseUrl: string = ''; let baseUrl: string = '';
@@ -21,9 +22,9 @@ export const sesionStore = {
reset: () => currentSesion.set(null) reset: () => currentSesion.set(null)
}; };
sesionStore.subscribe((value) => { // sesionStore.subscribe((value) => {
console.log(value); // console.log(value);
}); // });
if (browser) { if (browser) {
currentSesion.subscribe((value) => { currentSesion.subscribe((value) => {
@@ -54,9 +55,9 @@ if (browser) {
if (sesion.isFirebase) { if (sesion.isFirebase) {
try { try {
// Simulamos la importación dinámica de Firebase getFirebaseApp();
const { getAuth } = await import('firebase/auth');
const auth = getAuth(); const auth = getFirebaseAuth();
const user = auth.currentUser; const user = auth.currentUser;
if (user) { if (user) {

View File

@@ -13,11 +13,11 @@ export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null }; export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
export function filtrarImagen(file: File) { export function filtrarImagen(file: File) {
if (file) { if (file) {
const allowed = ['image/png', 'image/jpg', 'image/jpeg', 'image/gif', 'image/webp']; const allowed = ['image/png', 'image/jpg', 'image/jpeg', 'image/gif', 'image/webp'];
if (allowed.includes(file.type)) { if (allowed.includes(file.type)) {
return file; return file;
} }
} }
return null; return null;
} }

View File

@@ -2,7 +2,6 @@
import CardContent from '@/components/ui/card/card-content.svelte'; import CardContent from '@/components/ui/card/card-content.svelte';
import Card from '@/components/ui/card/card.svelte'; import Card from '@/components/ui/card/card.svelte';
import CardDescription from '@/components/ui/card/card-description.svelte'; import CardDescription from '@/components/ui/card/card-description.svelte';
import { page } from '$app/state';
import TablaUsuarios from '@/components/TablaUsuarios.svelte'; import TablaUsuarios from '@/components/TablaUsuarios.svelte';
import CardTitle from '@/components/ui/card/card-title.svelte'; import CardTitle from '@/components/ui/card/card-title.svelte';
import CardHeader from '@/components/ui/card/card-header.svelte'; import CardHeader from '@/components/ui/card/card-header.svelte';

View File

@@ -4,6 +4,7 @@
import { ModeWatcher } from 'mode-watcher'; import { ModeWatcher } from 'mode-watcher';
import Header from '@/head/Header.svelte'; import Header from '@/head/Header.svelte';
import { TooltipProvider } from '@/components/ui/tooltip'; import { TooltipProvider } from '@/components/ui/tooltip';
import { resolve } from '$app/paths';
let { children } = $props(); let { children } = $props();
</script> </script>
@@ -17,3 +18,11 @@
<TooltipProvider> <TooltipProvider>
{@render children()} {@render children()}
</TooltipProvider> </TooltipProvider>
<footer
class="fixed right-0 bottom-0 left-0 border-t bg-background p-2 text-center text-sm text-muted-foreground"
>
<p>
&copy; {new Date().getFullYear()} Mini X. Todos los derechos reservados.
<a class="text-blue-500 underline" href={resolve('/about')}>Sobre Nosotros</a>
</p>
</footer>

View File

@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import { replaceState } from '$app/navigation'; import { replaceState } from '$app/navigation';
import { page } from '$app/state'; import { page } from '$app/state';
import { onMount } from 'svelte';
import Card from '@/components/ui/card/card.svelte'; import Card from '@/components/ui/card/card.svelte';
import { Content } from '@/components/ui/card'; import { Content } from '@/components/ui/card';
import Spinner from '@/components/ui/spinner/spinner.svelte'; import Spinner from '@/components/ui/spinner/spinner.svelte';
@@ -11,21 +10,18 @@
import PostCard from '@/components/PostCard.svelte'; import PostCard from '@/components/PostCard.svelte';
import ModalEditar from './[perfil]/modalEditar.svelte'; import ModalEditar from './[perfil]/modalEditar.svelte';
import { sesionStore } from '@/stores/usuario'; import { sesionStore } from '@/stores/usuario';
import { import { posts, updatePostStore, loadingPosts, resetPosts } from '@/stores/posts';
posts,
updatePostStore,
loadingPosts
} from '@/stores/posts';
import { updatePost } from '@/hooks/updatePost'; import { updatePost } from '@/hooks/updatePost';
import { loadMorePosts } from '@/hooks/loadMorePosts'; import { loadMorePosts } from '@/hooks/loadMorePosts';
import type { Post } from '../types'; import type { Post } from '../types';
import { fade, slide } from 'svelte/transition'; import { fade, slide } from 'svelte/transition';
let postAModificar: Post | null = null; let postAModificar: Post | null = $state(null);
let mensajeError = ''; let mensajeError = $state('');
let sentinel: HTMLDivElement; let sentinel: HTMLDivElement;
onMount(() => { resetPosts();
$effect(() => {
loadMorePosts(); loadMorePosts();
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
@@ -47,8 +43,7 @@
await updatePost( await updatePost(
postAModificar, postAModificar,
(postNuevo: Post) => (postNuevo: Post) => updatePostStore(postAModificar!.id, postNuevo),
updatePostStore(postAModificar!.id, postNuevo),
mensajeError mensajeError
); );
postAModificar = null; postAModificar = null;
@@ -63,9 +58,7 @@
{#if from === 'cambio_contraseña'} {#if from === 'cambio_contraseña'}
<Dialog open> <Dialog open>
<DialogContent> <DialogContent>Se cambió la contraseña del usuario exitosamente</DialogContent>
Se cambió la contraseña del usuario exitosamente
</DialogContent>
</Dialog> </Dialog>
{/if} {/if}
@@ -92,13 +85,10 @@
<p>Cargando</p> <p>Cargando</p>
</Content> </Content>
</Card> </Card>
{:else if $posts.length === 0} {:else if $posts.length === 0}
<Card> <Card>
<Content> <Content>
<p class="text-center leading-7"> <p class="text-center leading-7">No hay Posts que mostrar</p>
No hay Posts que mostrar
</p>
</Content> </Content>
</Card> </Card>
{:else} {:else}
@@ -120,9 +110,6 @@
</div> </div>
{#if postAModificar} {#if postAModificar}
<div in:fade> <div in:fade>
<ModalEditar <ModalEditar callbackfn={handleEditar} bind:post={postAModificar} />
callbackfn={handleEditar}
bind:post={postAModificar}
/>
</div> </div>
{/if} {/if}

View File

@@ -1 +1,4 @@
import { resetPosts } from '@/stores/posts';
//export const ssr = true; //export const ssr = true;
// export async function load({}) {}

View File

@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { apiBase } from '@/stores/url'; import { apiBase } from '@/stores/url';
import PenLine from '@lucide/svelte/icons/pen-line';
import type { Post } from '../../types.js'; import type { Post } from '../../types.js';
import { fade, slide } from 'svelte/transition'; import { fade, slide } from 'svelte/transition';
import PostCard from '@/components/PostCard.svelte'; import PostCard from '@/components/PostCard.svelte';
@@ -9,7 +8,7 @@
import ModalEditar from './modalEditar.svelte'; import ModalEditar from './modalEditar.svelte';
import { page } from '$app/state'; import { page } from '$app/state';
import Button from '@/components/ui/button/button.svelte'; import Button from '@/components/ui/button/button.svelte';
import { Dialog } from '@/components/ui/dialog/index.js'; import { Dialog } from '@/components/ui/dialog';
import CrearPost from '@/components/crear-post.svelte'; import CrearPost from '@/components/crear-post.svelte';
import DialogContent from '@/components/ui/dialog/dialog-content.svelte'; import DialogContent from '@/components/ui/dialog/dialog-content.svelte';
import DialogTitle from '@/components/ui/dialog/dialog-title.svelte'; import DialogTitle from '@/components/ui/dialog/dialog-title.svelte';
@@ -19,54 +18,108 @@
import CardPerfil from '@/components/CardPerfil.svelte'; import CardPerfil from '@/components/CardPerfil.svelte';
import DialogModificarUsuario from '@/components/DialogModificarUsuario.svelte'; import DialogModificarUsuario from '@/components/DialogModificarUsuario.svelte';
import BotonSeguir from '@/components/BotonSeguir.svelte'; import BotonSeguir from '@/components/BotonSeguir.svelte';
import UserPen from '@lucide/svelte/icons/user-pen';
import DialogResetPassword from '@/components/DialogResetPassword.svelte'; import DialogResetPassword from '@/components/DialogResetPassword.svelte';
import Key from '@lucide/svelte/icons/key'; import Key from '@lucide/svelte/icons/key';
import UserPen from '@lucide/svelte/icons/user-pen';
import PenLine from '@lucide/svelte/icons/pen-line';
let { params } = $props(); let { params } = $props();
let cargando = $state(true); setPosts([]);
let cargando = $state(false);
let finished = $state(false);
let pageNumber = $state(1);
let sentinel = $state<HTMLDivElement | null>(null);
let mensajeError = $state(''); let mensajeError = $state('');
let postAModificar: Post | null = $state(null); let postAModificar: Post | null = $state(null);
let showCrearPost = $state(false); let showCrearPost = $state(false);
let data = $derived(page.data); let data = $derived(page.data);
$inspect(data); // $inspect(data);
$effect(() => { let fetching = false;
obtenerPosts(); // svelte-ignore state_referenced_locally
}); let currentProfile = $state(params.perfil);
async function obtenerPosts() { async function obtenerPosts() {
if (fetching || finished) return;
fetching = true;
cargando = true;
try { try {
const req = await fetch($apiBase + '/api/posts/user/' + params.perfil, { const res = await fetch(
method: 'GET', `${$apiBase}/api/posts/user/${params.perfil}?page=${pageNumber}&pageSize=20`,
headers: { {
Authorization: `Bearer ${$sesionStore?.accessToken}` headers: {
Authorization: `Bearer ${$sesionStore?.accessToken}`
}
} }
}); );
if (req.ok) { const nuevosPosts: Post[] = await res.json();
setPosts(await req.json());
if (nuevosPosts.length === 0) {
finished = true;
return; return;
} }
mensajeError = 'Fallo al obtener los datos';
} catch { posts.update((actuales = []) => [...actuales, ...nuevosPosts]);
mensajeError = 'No se alcanzo el servidor';
pageNumber++;
if (nuevosPosts.length < 20) {
finished = true;
}
} catch (error) {
mensajeError = 'Error al cargar los posts';
} finally { } finally {
fetching = false;
cargando = false; cargando = false;
} }
} }
$effect(() => {
if (currentProfile !== params.perfil) {
currentProfile = params.perfil;
setPosts([]);
pageNumber = 1;
finished = false;
mensajeError = '';
fetching = false;
obtenerPosts();
}
});
$effect(() => {
if (!sentinel || finished) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !fetching && !finished) {
obtenerPosts();
}
},
{ rootMargin: '100px' }
);
observer.observe(sentinel);
return () => observer.disconnect();
});
async function handleEditar(e: SubmitEvent) { async function handleEditar(e: SubmitEvent) {
e.preventDefault(); e.preventDefault();
if (postAModificar == null) return; if (!postAModificar) return;
await updatePost( await updatePost(
postAModificar, postAModificar,
(postnuevo: Post) => updatePostStore(postAModificar!.id, postnuevo), (postNuevo: Post) => updatePostStore(postAModificar!.id, postNuevo),
mensajeError mensajeError
); );
postAModificar = null; postAModificar = null;
} }
</script> </script>
@@ -74,9 +127,9 @@
<!-- {$inspect(data)} --> <!-- {$inspect(data)} -->
<div class="flex min-h-fit w-full items-center justify-center p-6 md:p-10"> <div class="flex min-h-fit w-full items-center justify-center p-6 md:p-10">
<div class="w-full max-w-6xl"> <div class="w-full max-w-6xl">
{#key data} <!-- {#key data.id} -->
<CardPerfil bind:data /> <CardPerfil bind:data />
{/key} <!-- {/key} -->
<h1 <h1
class="mt-10 flex scroll-m-20 justify-between text-3xl font-extrabold tracking-tight lg:text-3xl" class="mt-10 flex scroll-m-20 justify-between text-3xl font-extrabold tracking-tight lg:text-3xl"
> >
@@ -93,14 +146,12 @@
<PenLine /> <PenLine />
</Button> </Button>
{:else if $posts?.length == 0} {:else if $posts?.length == 0}
<BotonSeguir post={{ authorId: data.id }} /> <BotonSeguir post={{ authorId: data.id, id: data.id }} />
{/if} {/if}
</h1> </h1>
<hr class="mb-8" /> <hr class="mb-8" />
{#if cargando} {#if mensajeError !== ''}
<CardCargando />
{:else if mensajeError !== ''}
<CardError {mensajeError} /> <CardError {mensajeError} />
{:else} {:else}
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@@ -109,6 +160,16 @@
<PostCard {post} bind:postAModificar /> <PostCard {post} bind:postAModificar />
</div> </div>
{/each} {/each}
<div bind:this={sentinel} class="h-1"></div>
{#if cargando && !finished}
<CardCargando />
{/if}
{#if finished && $posts.length === 0}
<p class="text-center text-muted-foreground">No hay posts para mostrar</p>
{/if}
</div> </div>
{/if} {/if}
</div> </div>
@@ -135,7 +196,7 @@
</div> </div>
{#if $sesionStore?.isAdmin || $sesionStore?.username == params.perfil} {#if $sesionStore?.isAdmin || $sesionStore?.username == params.perfil}
<div class="fixed right-8 bottom-8 flex flex-col gap-2"> <div class="fixed right-8 bottom-12 flex flex-col gap-2">
<DialogModificarUsuario bind:data> <DialogModificarUsuario bind:data>
<Button variant="default" size="icon-lg"> <Button variant="default" size="icon-lg">
<UserPen /> <UserPen />

View File

@@ -1,19 +1,20 @@
import { obtenerUsuarioPorUsername } from '@/hooks/obtenerUsuario.js'; import { obtenerUsuarioPorUsername } from '@/hooks/obtenerUsuario.js';
import type { User, UserResponseDto } from '../../types.js'; import type { UserResponseDto } from '../../types.js';
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import type { PageLoad } from './$types';
import { obtenerSeguidosPorUsuario } from '@/hooks/obtenerSeguidosPorUsuario.js'; import { obtenerSeguidosPorUsuario } from '@/hooks/obtenerSeguidosPorUsuario.js';
import { obtenerSeguidoresPorUsuario } from '@/hooks/obtenerSeguidoresPorUsuario.js'; import { obtenerSeguidoresPorUsuario } from '@/hooks/obtenerSeguidoresPorUsuario.js';
import { obtenerCantidadDeSeguidores } from '@/hooks/obtenerCantidadDeSeguidores.js'; import { obtenerCantidadDeSeguidores } from '@/hooks/obtenerCantidadDeSeguidores.js';
import { obtenerCantidadDeSeguidos } from '@/hooks/obtenerCantidadDeSeguidos.js'; import { obtenerCantidadDeSeguidos } from '@/hooks/obtenerCantidadDeSeguidos.js';
export async function load({ params, depends, fetch }) { export const load: PageLoad = async ({ params, depends, fetch }) => {
depends('perfil:general'); depends('perfil:general');
const usuario: UserResponseDto | null = await obtenerUsuarioPorUsername(params.perfil, fetch); const usuario: UserResponseDto | null = await obtenerUsuarioPorUsername(params.perfil, fetch);
if (!usuario) error(404, 'No se encontro el usuario, ' + params.perfil); if (!usuario) error(404, 'No se encontro el usuario, ' + params.perfil);
const [seguidos, seguidores, countSeguidores, countSeguidos] = await Promise.all([ const [seguidos, seguidores, countSeguidores, countSeguidos] = await Promise.all([
obtenerSeguidosPorUsuario(usuario.id, 5, fetch), obtenerSeguidosPorUsuario(usuario.id, 1, 5, fetch),
obtenerSeguidoresPorUsuario(usuario.id, 5, fetch), obtenerSeguidoresPorUsuario(usuario.id, 1, 5, fetch),
obtenerCantidadDeSeguidores(usuario.id, fetch), obtenerCantidadDeSeguidores(usuario.id, fetch),
obtenerCantidadDeSeguidos(usuario.id, fetch) obtenerCantidadDeSeguidos(usuario.id, fetch)
]); ]);
@@ -25,4 +26,4 @@ export async function load({ params, depends, fetch }) {
countSeguidores: countSeguidores.count, countSeguidores: countSeguidores.count,
countSeguidos: countSeguidos.count countSeguidos: countSeguidos.count
}; };
} };

View File

@@ -1,15 +1,40 @@
<script lang="ts"> <script lang="ts">
import ArrowLeft from '@lucide/svelte/icons/chevron-left'; import ArrowLeft from '@lucide/svelte/icons/chevron-left';
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
import ChevronRight from '@lucide/svelte/icons/chevron-right';
import type { UserResponseDto } from '../../../types'; import type { UserResponseDto } from '../../../types';
import UserCard from '@/components/UserCard.svelte'; import UserCard from '@/components/UserCard.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { obtenerSeguidoresPorUsuario } from '@/hooks/obtenerSeguidoresPorUsuario';
type Data = { type Data = {
usuario: UserResponseDto; usuario: UserResponseDto;
seguidores: UserResponseDto[]; seguidores: UserResponseDto[];
totalCount: number;
}; };
let { data }: { data: Data } = $props(); let { data }: { data: Data } = $props();
let currentPage = $state(1);
let isLoading = $state(false);
const limit = 100;
let totalPages = $derived(Math.ceil(data.totalCount / limit));
async function loadPage(page: number) {
if (isLoading) return;
isLoading = true;
const response = await obtenerSeguidoresPorUsuario(data.usuario.id, page, limit);
if (response) {
data.seguidores = response.response as UserResponseDto[];
data.totalCount = response.totalCount;
currentPage = page;
}
isLoading = false;
}
</script> </script>
<div class="flex min-h-fit w-full items-center justify-center p-6 md:p-10"> <div class="flex min-h-fit w-full items-center justify-center p-6 md:p-10">
@@ -25,9 +50,14 @@
<ArrowLeft /> <ArrowLeft />
</button> </button>
</div> </div>
{#if data.seguidores.length === 0}
{#if isLoading}
<div class="py-8 text-center text-muted-foreground"> <div class="py-8 text-center text-muted-foreground">
<p>No hay seguidores para mostrar.</p> <p>Cargando...</p>
</div>
{:else if data.seguidores.length === 0}
<div class="py-8 text-center text-muted-foreground">
<p>No hay seguidos para mostrar.</p>
</div> </div>
{:else} {:else}
<div class="flex flex-col sm:grid" style="grid-template-columns: repeat(2, 1fr); gap: 1rem;"> <div class="flex flex-col sm:grid" style="grid-template-columns: repeat(2, 1fr); gap: 1rem;">
@@ -38,5 +68,29 @@
{/each} {/each}
</div> </div>
{/if} {/if}
{#if totalPages > 1}
<div class="mt-6 flex items-center justify-center gap-2">
<button
class="rounded-md border bg-card p-2 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50"
onclick={() => loadPage(currentPage - 1)}
disabled={currentPage === 1 || isLoading}
>
<ChevronLeft class="h-5 w-5" />
</button>
<span class="px-4 text-sm text-muted-foreground">
Página {currentPage} de {totalPages}
</span>
<button
class="rounded-md border bg-card p-2 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50"
onclick={() => loadPage(currentPage + 1)}
disabled={currentPage === totalPages || isLoading}
>
<ChevronRight class="h-5 w-5" />
</button>
</div>
{/if}
</div> </div>
</div> </div>

View File

@@ -1,14 +1,16 @@
import { obtenerSeguidoresPorUsuario } from '@/hooks/obtenerSeguidoresPorUsuario'; import { obtenerSeguidoresPorUsuario } from '@/hooks/obtenerSeguidoresPorUsuario';
import { obtenerUsuarioPorUsername } from '@/hooks/obtenerUsuario'; import { obtenerUsuarioPorUsername } from '@/hooks/obtenerUsuario';
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import type { PageLoad } from './$types';
import type { UserResponseDto, UsersResponseDto } from '../../../types'; import type { UserResponseDto, UsersResponseDto } from '../../../types';
export async function load({ params, fetch }) { export const load: PageLoad = async ({ params, fetch }) => {
const usuario: UserResponseDto | null = await obtenerUsuarioPorUsername(params.perfil, fetch); const usuario: UserResponseDto | null = await obtenerUsuarioPorUsername(params.perfil, fetch);
if (!usuario) error(404, 'No se encontro el usuario, ' + params.perfil); if (!usuario) error(404, 'No se encontro el usuario, ' + params.perfil);
const seguidoresResponse: UsersResponseDto | null = await obtenerSeguidoresPorUsuario( const seguidoresResponse: UsersResponseDto | null = await obtenerSeguidoresPorUsuario(
usuario.id, usuario.id,
1,
100, 100,
fetch fetch
); );
@@ -17,4 +19,4 @@ export async function load({ params, fetch }) {
usuario, usuario,
seguidores: seguidoresResponse?.response || [] seguidores: seguidoresResponse?.response || []
}; };
} };

View File

@@ -1,15 +1,40 @@
<script lang="ts"> <script lang="ts">
import ArrowLeft from '@lucide/svelte/icons/chevron-left'; import ArrowLeft from '@lucide/svelte/icons/chevron-left';
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
import ChevronRight from '@lucide/svelte/icons/chevron-right';
import type { UserResponseDto } from '../../../types'; import type { UserResponseDto } from '../../../types';
import UserCard from '@/components/UserCard.svelte'; import UserCard from '@/components/UserCard.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { obtenerSeguidosPorUsuario } from '@/hooks/obtenerSeguidosPorUsuario';
type Data = { type Data = {
usuario: UserResponseDto; usuario: UserResponseDto;
seguidos: UserResponseDto[]; seguidos: UserResponseDto[];
totalCount: number;
}; };
let { data }: { data: Data } = $props(); let { data }: { data: Data } = $props();
let currentPage = $state(1);
let isLoading = $state(false);
const limit = 100;
let totalPages = $derived(Math.ceil(data.totalCount / limit));
async function loadPage(page: number) {
if (isLoading) return;
isLoading = true;
const response = await obtenerSeguidosPorUsuario(data.usuario.id, page, limit);
if (response) {
data.seguidos = response.response as UserResponseDto[];
data.totalCount = response.totalCount;
currentPage = page;
}
isLoading = false;
}
</script> </script>
<div class="flex min-h-fit w-full items-center justify-center p-6 md:p-10"> <div class="flex min-h-fit w-full items-center justify-center p-6 md:p-10">
@@ -25,7 +50,12 @@
<ArrowLeft /> <ArrowLeft />
</button> </button>
</div> </div>
{#if data.seguidos.length === 0}
{#if isLoading}
<div class="py-8 text-center text-muted-foreground">
<p>Cargando...</p>
</div>
{:else if data.seguidos.length === 0}
<div class="py-8 text-center text-muted-foreground"> <div class="py-8 text-center text-muted-foreground">
<p>No hay seguidos para mostrar.</p> <p>No hay seguidos para mostrar.</p>
</div> </div>
@@ -38,5 +68,29 @@
{/each} {/each}
</div> </div>
{/if} {/if}
{#if totalPages > 1}
<div class="mt-6 flex items-center justify-center gap-2">
<button
class="rounded-md border bg-card p-2 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50"
onclick={() => loadPage(currentPage - 1)}
disabled={currentPage === 1 || isLoading}
>
<ChevronLeft class="h-5 w-5" />
</button>
<span class="px-4 text-sm text-muted-foreground">
Página {currentPage} de {totalPages}
</span>
<button
class="rounded-md border bg-card p-2 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50"
onclick={() => loadPage(currentPage + 1)}
disabled={currentPage === totalPages || isLoading}
>
<ChevronRight class="h-5 w-5" />
</button>
</div>
{/if}
</div> </div>
</div> </div>

View File

@@ -1,20 +1,23 @@
import { obtenerUsuarioPorUsername } from '@/hooks/obtenerUsuario'; import { obtenerUsuarioPorUsername } from '@/hooks/obtenerUsuario';
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import type { PageLoad } from './$types';
import type { UserResponseDto, UsersResponseDto } from '../../../types'; import type { UserResponseDto, UsersResponseDto } from '../../../types';
import { obtenerSeguidosPorUsuario } from '@/hooks/obtenerSeguidosPorUsuario'; import { obtenerSeguidosPorUsuario } from '@/hooks/obtenerSeguidosPorUsuario';
export async function load({ params, fetch }) { export const load: PageLoad = async ({ params, fetch }) => {
const usuario: UserResponseDto | null = await obtenerUsuarioPorUsername(params.perfil, fetch); const usuario: UserResponseDto | null = await obtenerUsuarioPorUsername(params.perfil, fetch);
if (!usuario) error(404, 'No se encontro el usuario, ' + params.perfil); if (!usuario) error(404, 'No se encontro el usuario, ' + params.perfil);
const seguidosResponse: UsersResponseDto | null = await obtenerSeguidosPorUsuario( const seguidosResponse: UsersResponseDto | null = await obtenerSeguidosPorUsuario(
usuario.id, usuario.id,
1,
100, 100,
fetch fetch
); );
return { return {
usuario, usuario,
seguidos: seguidosResponse?.response || [] seguidos: seguidosResponse?.response || [],
totalCount: seguidosResponse?.totalCount ?? 0
}; };
} };

View File

@@ -0,0 +1,66 @@
<script>
import AvatarFallback from '@/components/ui/avatar/avatar-fallback.svelte';
import AvatarImage from '@/components/ui/avatar/avatar-image.svelte';
import Avatar from '@/components/ui/avatar/avatar.svelte';
import CardContent from '@/components/ui/card/card-content.svelte';
import CardDescription from '@/components/ui/card/card-description.svelte';
import CardHeader from '@/components/ui/card/card-header.svelte';
import CardTitle from '@/components/ui/card/card-title.svelte';
import Card from '@/components/ui/card/card.svelte';
import Separator from '@/components/ui/separator/separator.svelte';
import Github from '@lucide/svelte/icons/github';
let devs = [
{
name: 'Fede',
role: 'Desarrollador Full Stack',
pic: 'https://avatars.githubusercontent.com/u/61921832?s=64&v=4',
gh: 'https://github.com/fedpo2'
},
{
name: 'Luca',
role: 'Desarrollador Frontend',
pic: 'https://avatars.githubusercontent.com/u/141507149?s=64&v=4',
gh: 'https://github.com/TroianoLuca2am'
},
{
name: 'Fran',
role: 'Desarrollador Backend',
pic: 'https://avatars.githubusercontent.com/u/126514899?s=64&v=4',
gh: 'https://github.com/franciscorosecerna'
}
];
</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">
<h1 class="mb-2 scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
Sobre Nosotros
</h1>
<Separator class="my-2" />
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
{#each devs as dev}
<Card>
<CardHeader class="flex flex-col items-center justify-center gap-2">
<Avatar class="h-24 w-24">
<AvatarImage src={dev.pic} class="h-24 w-24" />
<AvatarFallback class="flex h-24 w-24 items-center justify-center text-3xl">
{dev.name.toLocaleUpperCase()[0]}
</AvatarFallback>
</Avatar>
</CardHeader>
<CardContent>
<div>
<CardTitle>{dev.name}</CardTitle>
<CardDescription>{dev.role}</CardDescription>
</div>
<div class="mt-2">
<a href={dev.gh}><Github /></a>
</div>
</CardContent>
</Card>
{/each}
</div>
</div>
</div>

View File

@@ -25,6 +25,7 @@
import TooltipContent from '@/components/ui/tooltip/tooltip-content.svelte'; import TooltipContent from '@/components/ui/tooltip/tooltip-content.svelte';
import { deletePost } from '@/hooks/deletePost'; import { deletePost } from '@/hooks/deletePost';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import { obtenerRespuestasPorId } from '@/hooks/obtenerRespuestasPorId';
interface Prop { interface Prop {
data: { data: {
@@ -36,7 +37,17 @@
let tamaño = new TamañoPantalla(); let tamaño = new TamañoPantalla();
let { data }: Prop = $props(); let { data }: Prop = $props();
// $inspect(data);
let respuestasPaginadas: Post[] = $state([]);
let pagerespuestas: number = $state(1);
let seguirMostrandoMostrarMás = $derived.by(() => {
if (data.post.repliesCount <= 20) return false;
if (respuestasPaginadas.length == 0) return true;
return data.respuestas.length + respuestasPaginadas.length < data.post.repliesCount;
});
// $inspect([respuestasPaginadas, seguirMostrandoMostrarMás]);
let postAModificar: Post | null = $state(null); let postAModificar: Post | null = $state(null);
async function handleEditar(e: SubmitEvent) { async function handleEditar(e: SubmitEvent) {
@@ -104,7 +115,7 @@
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
{#each data.respuestas as respuesta (respuesta.id)} {#each [...data.respuestas, ...respuestasPaginadas] as respuesta (respuesta.id)}
<div transition:fade animate:flip> <div transition:fade animate:flip>
<!-- {#if tamaño.isMobile} --> <!-- {#if tamaño.isMobile} -->
<!-- {#if true} --> <!-- {#if true} -->
@@ -114,6 +125,26 @@
<!-- {/if} --> <!-- {/if} -->
</div> </div>
{/each} {/each}
{#if seguirMostrandoMostrarMás}
<Button
variant="link"
onclick={async () => {
let ret = await obtenerRespuestasPorId(
data.post.id,
undefined,
undefined,
++pagerespuestas
);
if (ret == null) return;
if (typeof ret == 'string') return;
if (ret.length == 0) {
seguirMostrandoMostrarMás = false;
return;
}
respuestasPaginadas.push(...ret);
}}>Cargar Más Respuestas</Button
>
{/if}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -13,31 +13,34 @@
<div class="flex min-h-fit w-full flex-col items-center justify-center gap-2 p-6 md:p-10"> <div class="flex min-h-fit w-full flex-col items-center justify-center gap-2 p-6 md:p-10">
<div class="flex w-full max-w-6xl flex-col gap-2"> <div class="flex w-full max-w-6xl flex-col gap-2">
<h1 class="text-2xl font-bold">Usuarios</h1> {#if data.usuarios.length != 0}
<Separator></Separator> <h1 class="text-2xl font-bold">Usuarios</h1>
{#each data.usuarios as usu} <Separator></Separator>
<div class="w-full"> {#each data.usuarios as usu}
<UserCard {usu} /> <div class="w-full">
</div> <UserCard {usu} />
{/each} </div>
<div class="mt-4">
<h2 class="mb-2 text-xl font-semibold">Hastags</h2>
</div>
<Separator />
<div class="mt-4 flex flex-col gap-2">
{#each data.htags as htag}
<a
href={`/htag/${htag}`}
class="w-full rounded-lg bg-accent p-3 text-lg font-medium text-foreground hover:bg-muted"
>
<div class="flex justify-between">
#{htag}
<ChevronRight />
</div>
</a>
{/each} {/each}
</div> {/if}
{#if data.htags.length != 0}
<div class="mt-4">
<h2 class="mb-2 text-xl font-semibold">Hastags</h2>
</div>
<Separator />
<div class="mt-4 flex flex-col gap-2">
{#each data.htags as htag}
<a
href={`/htag/${htag}`}
class="w-full rounded-lg bg-accent p-3 text-lg font-medium text-foreground hover:bg-muted"
>
<div class="flex justify-between">
#{htag}
<ChevronRight />
</div>
</a>
{/each}
</div>
{/if}
</div> </div>
</div> </div>

View File

@@ -14,8 +14,8 @@ export async function load({ params }) {
return error(500, 'No se pudo alcanzar el servidor.'); return error(500, 'No se pudo alcanzar el servidor.');
} }
if (usuarios.length == 0) { if (usuarios.length == 0 && htags.length == 0) {
return error(404, 'No se encontraron usuarios que coinsidan con la busqueda.'); return error(404, 'No se encontraron usuarios ni hashtags que coinsidan con la busqueda.');
} }
return { usuarios, htags }; return { usuarios, htags };
} }