From 0b4e61b3ab6dbfd52051b2f98b03f4b5e5c7e429 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Tue, 14 Jul 2026 04:55:04 +0200 Subject: [PATCH] feat(frontend): add (app) route group and rework auth routing Move authenticated pages under an (app) group with a Sidebar shell, add /auth/login and page-based /login, drop the old explore page and login +server endpoint, and add the server upload helper. --- frontend/src/lib/components/Sidebar.svelte | 150 ++++++++ frontend/src/lib/server/upload.ts | 51 +++ .../src/routes/(app)/downloads/+page.svelte | 25 ++ .../{ => (app)}/explore/+page.server.ts | 0 .../routes/{ => (app)}/explore/+page.svelte | 55 ++- frontend/src/routes/(app)/import/+page.svelte | 25 ++ .../src/routes/(app)/library/+page.server.ts | 18 + .../src/routes/(app)/library/+page.svelte | 96 +++++ .../routes/(app)/playlists/+page.server.ts | 50 +++ .../src/routes/(app)/playlists/+page.svelte | 181 ++++++++++ .../(app)/playlists/[id]/+page.server.ts | 86 +++++ .../routes/(app)/playlists/[id]/+page.svelte | 312 +++++++++++++++++ .../src/routes/(app)/upload/+page.server.ts | 105 ++++++ frontend/src/routes/(app)/upload/+page.svelte | 328 ++++++++++++++++++ frontend/src/routes/+layout.server.ts | 12 +- frontend/src/routes/+layout.svelte | 73 +++- .../src/routes/{ => auth}/login/+server.ts | 0 frontend/src/routes/login/+page.server.ts | 9 + frontend/src/routes/login/+page.svelte | 39 +++ 19 files changed, 1584 insertions(+), 31 deletions(-) create mode 100644 frontend/src/lib/components/Sidebar.svelte create mode 100644 frontend/src/lib/server/upload.ts create mode 100644 frontend/src/routes/(app)/downloads/+page.svelte rename frontend/src/routes/{ => (app)}/explore/+page.server.ts (100%) rename frontend/src/routes/{ => (app)}/explore/+page.svelte (57%) create mode 100644 frontend/src/routes/(app)/import/+page.svelte create mode 100644 frontend/src/routes/(app)/library/+page.server.ts create mode 100644 frontend/src/routes/(app)/library/+page.svelte create mode 100644 frontend/src/routes/(app)/playlists/+page.server.ts create mode 100644 frontend/src/routes/(app)/playlists/+page.svelte create mode 100644 frontend/src/routes/(app)/playlists/[id]/+page.server.ts create mode 100644 frontend/src/routes/(app)/playlists/[id]/+page.svelte create mode 100644 frontend/src/routes/(app)/upload/+page.server.ts create mode 100644 frontend/src/routes/(app)/upload/+page.svelte rename frontend/src/routes/{ => auth}/login/+server.ts (100%) create mode 100644 frontend/src/routes/login/+page.server.ts create mode 100644 frontend/src/routes/login/+page.svelte diff --git a/frontend/src/lib/components/Sidebar.svelte b/frontend/src/lib/components/Sidebar.svelte new file mode 100644 index 0000000..320ae89 --- /dev/null +++ b/frontend/src/lib/components/Sidebar.svelte @@ -0,0 +1,150 @@ + + +{#snippet navLink(link: { href: string; label: string; icon: typeof Home })} + {@const active = isActive(link.href)} + + + {link.label} + +{/snippet} + + + + diff --git a/frontend/src/lib/server/upload.ts b/frontend/src/lib/server/upload.ts new file mode 100644 index 0000000..ea15106 --- /dev/null +++ b/frontend/src/lib/server/upload.ts @@ -0,0 +1,51 @@ +import http from 'node:http'; +import https from 'node:https'; +import { env } from '$env/dynamic/private'; + +const CONNECT_HOST = env.S3_UPLOAD_CONNECT_HOST || '127.0.0.1'; + +export async function putPresigned( + presignedUrl: string, + body: Buffer, + contentType: string +): Promise { + const url = new URL(presignedUrl); + const signed = new Set( + (url.searchParams.get('X-Amz-SignedHeaders') ?? 'host').toLowerCase().split(';') + ); + + const headers: Record = { + Host: url.host, + 'Content-Length': body.byteLength + }; + if (signed.has('content-type')) headers['Content-Type'] = contentType; + if (signed.has('if-none-match')) headers['If-None-Match'] = '*'; + + const secure = url.protocol === 'https:'; + const transport = secure ? https : http; + const port = url.port || (secure ? 443 : 80); + + await new Promise((resolve, reject) => { + const request = transport.request( + { + host: CONNECT_HOST, + port, + method: 'PUT', + path: url.pathname + url.search, + headers + }, + (response) => { + let raw = ''; + response.on('data', (chunk) => (raw += chunk)); + response.on('end', () => { + const status = response.statusCode ?? 0; + if (status >= 200 && status < 300) resolve(); + else reject(new Error(`S3 PUT ${status}: ${raw.slice(0, 200)}`)); + }); + } + ); + request.on('error', reject); + request.write(body); + request.end(); + }); +} diff --git a/frontend/src/routes/(app)/downloads/+page.svelte b/frontend/src/routes/(app)/downloads/+page.svelte new file mode 100644 index 0000000..eb12010 --- /dev/null +++ b/frontend/src/routes/(app)/downloads/+page.svelte @@ -0,0 +1,25 @@ + + + + Descargas · Musify + + + +
+

Descargas

+

+ Escucha sin conexión lo que descargues. +

+ +
+ +

Todavía no tienes descargas

+

+ Las canciones que descargues aparecerán aquí. +

+
+
diff --git a/frontend/src/routes/explore/+page.server.ts b/frontend/src/routes/(app)/explore/+page.server.ts similarity index 100% rename from frontend/src/routes/explore/+page.server.ts rename to frontend/src/routes/(app)/explore/+page.server.ts diff --git a/frontend/src/routes/explore/+page.svelte b/frontend/src/routes/(app)/explore/+page.svelte similarity index 57% rename from frontend/src/routes/explore/+page.svelte rename to frontend/src/routes/(app)/explore/+page.svelte index 0e0b7a2..bceeeda 100644 --- a/frontend/src/routes/explore/+page.svelte +++ b/frontend/src/routes/(app)/explore/+page.svelte @@ -1,5 +1,9 @@ + + + Importar · Musify + + + +
+

Importar

+

+ Trae tu música desde otros servicios. +

+ +
+ +

Próximamente

+

+ Podrás importar tu música desde otras plataformas. +

+
+
diff --git a/frontend/src/routes/(app)/library/+page.server.ts b/frontend/src/routes/(app)/library/+page.server.ts new file mode 100644 index 0000000..5f4bb59 --- /dev/null +++ b/frontend/src/routes/(app)/library/+page.server.ts @@ -0,0 +1,18 @@ +import type { PageServerLoad } from './$types'; +import { error } from '@sveltejs/kit'; +import { createApiClient } from '$lib/server/api'; + +const PAGE_SIZE = 50; + +export const load: PageServerLoad = async ({ locals, fetch }) => { + if (!locals.user) error(401, 'Inicia sesión.'); + + const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined }); + const { data, error: err } = await api.GET('/tracks/users/{userId}', { + params: { path: { userId: locals.user.sub }, query: { pageNumber: 1, pageSize: PAGE_SIZE } } + }); + + if (err || !data) error(502, 'No se pudo cargar tu biblioteca.'); + + return { tracks: data }; +}; diff --git a/frontend/src/routes/(app)/library/+page.svelte b/frontend/src/routes/(app)/library/+page.svelte new file mode 100644 index 0000000..7540c1b --- /dev/null +++ b/frontend/src/routes/(app)/library/+page.svelte @@ -0,0 +1,96 @@ + + + + Tu biblioteca · Musify + + + +
+
+
+

Tu biblioteca

+

+ {total} + {total === 1 ? 'canción' : 'canciones'} en tu biblioteca. +

+
+ + + Subir música + +
+ + {#if items.length > 0} +
+ {#each items as track, i (track.id)} + + {/each} +
+ {:else} +
+

Todavía no tienes canciones

+ + + Sube la primera + +
+ {/if} +
diff --git a/frontend/src/routes/(app)/playlists/+page.server.ts b/frontend/src/routes/(app)/playlists/+page.server.ts new file mode 100644 index 0000000..f573652 --- /dev/null +++ b/frontend/src/routes/(app)/playlists/+page.server.ts @@ -0,0 +1,50 @@ +import type { PageServerLoad, Actions } from './$types'; +import { error, fail, redirect } from '@sveltejs/kit'; +import { createApiClient } from '$lib/server/api'; + +const MAX_NAME = 100; + +export const load: PageServerLoad = async ({ locals, fetch }) => { + if (!locals.user) error(401, 'Inicia sesión.'); + + const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined }); + const { data, error: err } = await api.GET('/playlists/users/{userId}', { + params: { path: { userId: locals.user.sub }, query: { pageNumber: 1, pageSize: 50 } } + }); + + if (err || !data) error(502, 'No se pudieron cargar tus playlists.'); + + const covers = await Promise.all( + data.items.map(async (playlist) => { + const { data: tracks } = await api.GET('/playlists/{playlistId}/tracks', { + params: { path: { playlistId: playlist.id }, query: { pageNumber: 1, pageSize: 4 } } + }); + return [playlist.id, tracks?.items.map((t) => t.id) ?? []] as const; + }) + ); + + return { playlists: data, trackIds: Object.fromEntries(covers) as Record }; +}; + +export const actions: Actions = { + create: async ({ request, locals, fetch }) => { + if (!locals.accessToken) return fail(401, { message: 'Inicia sesión.' }); + + const form = await request.formData(); + const name = String(form.get('name') ?? '').trim(); + const description = String(form.get('description') ?? '').trim(); + + if (name === '' || name.length > MAX_NAME) { + return fail(400, { message: 'El nombre es obligatorio (máx. 100 caracteres).', name, description }); + } + + const api = createApiClient({ fetch, accessToken: locals.accessToken }); + const { data, error: err } = await api.POST('/playlists', { + body: { name, description, pictureIntentId: null } + }); + + if (err || !data) return fail(502, { message: 'No se pudo crear la playlist.', name, description }); + + redirect(303, `/playlists/${data.id}`); + } +}; diff --git a/frontend/src/routes/(app)/playlists/+page.svelte b/frontend/src/routes/(app)/playlists/+page.svelte new file mode 100644 index 0000000..2f1b8de --- /dev/null +++ b/frontend/src/routes/(app)/playlists/+page.svelte @@ -0,0 +1,181 @@ + + + + Tus listas · Musify + + + +
+
+
+

Tus listas

+

Crea y organiza tus colecciones.

+
+ +
+ + {#if items.length > 0} + + {:else} +
+ + + +

Todavía no tienes listas

+

Crea la primera y añádele canciones de tu biblioteca.

+ +
+ {/if} +
+ +{#if creating} +
{ + if (e.target === e.currentTarget) creating = false; + }} + onkeydown={(e) => { + if (e.key === 'Escape') creating = false; + }} + > +
+
+

Nueva lista

+ +
+ +
{ + submitting = true; + return async ({ update }) => { + await update(); + submitting = false; + }; + }} + class="mt-5 space-y-4" + > +
+ + +
+
+ + +
+ + {#if form?.message} +

+ {form.message} +

+ {/if} + +
+ + +
+
+
+
+{/if} diff --git a/frontend/src/routes/(app)/playlists/[id]/+page.server.ts b/frontend/src/routes/(app)/playlists/[id]/+page.server.ts new file mode 100644 index 0000000..ba854f3 --- /dev/null +++ b/frontend/src/routes/(app)/playlists/[id]/+page.server.ts @@ -0,0 +1,86 @@ +import type { PageServerLoad, Actions } from './$types'; +import { error, fail, redirect } from '@sveltejs/kit'; +import { createApiClient } from '$lib/server/api'; + +const MAX_NAME = 100; + +export const load: PageServerLoad = async ({ params, locals, fetch }) => { + if (!locals.user) error(401, 'Inicia sesión.'); + + const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined }); + + const [playlistRes, tracksRes, libraryRes] = await Promise.all([ + api.GET('/playlists/{id}', { params: { path: { id: params.id } } }), + api.GET('/playlists/{playlistId}/tracks', { + params: { path: { playlistId: params.id }, query: { pageNumber: 1, pageSize: 200 } } + }), + api.GET('/tracks/users/{userId}', { + params: { path: { userId: locals.user.sub }, query: { pageNumber: 1, pageSize: 200 } } + }) + ]); + + if (playlistRes.error || !playlistRes.data) error(404, 'Playlist no encontrada.'); + + const tracks = tracksRes.data?.items ?? []; + const inPlaylist = new Set(tracks.map((t) => t.id)); + const library = (libraryRes.data?.items ?? []).filter((t) => !inPlaylist.has(t.id)); + + return { playlist: playlistRes.data, tracks, library }; +}; + +export const actions: Actions = { + addTrack: async ({ request, params, locals, fetch }) => { + if (!locals.accessToken) return fail(401, { message: 'Inicia sesión.' }); + const trackId = String((await request.formData()).get('trackId') ?? ''); + if (!trackId) return fail(400, { message: 'Falta la canción.' }); + + const api = createApiClient({ fetch, accessToken: locals.accessToken }); + const { error: err } = await api.POST('/playlists/{playlistId}/tracks/{trackId}', { + params: { path: { playlistId: params.id, trackId } } + }); + if (err) return fail(502, { message: 'No se pudo añadir la canción.' }); + return { added: true }; + }, + + removeTrack: async ({ request, params, locals, fetch }) => { + if (!locals.accessToken) return fail(401, { message: 'Inicia sesión.' }); + const trackId = String((await request.formData()).get('trackId') ?? ''); + if (!trackId) return fail(400, { message: 'Falta la canción.' }); + + const api = createApiClient({ fetch, accessToken: locals.accessToken }); + const { error: err } = await api.DELETE('/playlists/{playlistId}/tracks/{trackId}', { + params: { path: { playlistId: params.id, trackId } } + }); + if (err) return fail(502, { message: 'No se pudo quitar la canción.' }); + return { removed: true }; + }, + + rename: async ({ request, params, locals, fetch }) => { + if (!locals.accessToken) return fail(401, { message: 'Inicia sesión.' }); + const form = await request.formData(); + const name = String(form.get('name') ?? '').trim(); + const description = String(form.get('description') ?? '').trim(); + if (name === '' || name.length > MAX_NAME) { + return fail(400, { message: 'El nombre es obligatorio (máx. 100 caracteres).' }); + } + + const api = createApiClient({ fetch, accessToken: locals.accessToken }); + const { error: err } = await api.PUT('/playlists/{playlistId}', { + params: { path: { playlistId: params.id } }, + body: { newName: name, newDescription: description, newPictureIntentId: null } + }); + if (err) return fail(502, { message: 'No se pudo actualizar la playlist.' }); + return { renamed: true }; + }, + + delete: async ({ params, locals, fetch }) => { + if (!locals.accessToken) return fail(401, { message: 'Inicia sesión.' }); + + const api = createApiClient({ fetch, accessToken: locals.accessToken }); + const { error: err } = await api.DELETE('/playlists/{playlistId}', { + params: { path: { playlistId: params.id } } + }); + if (err) return fail(502, { message: 'No se pudo eliminar la playlist.' }); + redirect(303, '/playlists'); + } +}; diff --git a/frontend/src/routes/(app)/playlists/[id]/+page.svelte b/frontend/src/routes/(app)/playlists/[id]/+page.svelte new file mode 100644 index 0000000..4c69ed4 --- /dev/null +++ b/frontend/src/routes/(app)/playlists/[id]/+page.svelte @@ -0,0 +1,312 @@ + + + + {playlist.name} · Musify + + + +
+ + + + + Tus listas + + +
+ t.id)} + hue={hueFor(playlist.id)} + class="h-44 w-44 flex-shrink-0 rounded-2xl shadow-[0_20px_45px_-15px_rgba(0,0,0,0.7)]" + /> +
+

Lista

+

{playlist.name}

+ {#if playlist.description} +

{playlist.description}

+ {/if} +

+ {tracks.length} + {tracks.length === 1 ? 'canción' : 'canciones'} +

+
+
+ +
+ + + +
+ + {#if tracks.length > 0} +
+ {#each tracks as track, i (track.id)} +
+ +
async ({ update }) => update()}> + + +
+
+ {/each} +
+ {:else} +
+ +

Esta playlist está vacía. Añade canciones de tu biblioteca.

+
+ {/if} + + {#if library.length > 0} +
+

Añadir de tu biblioteca

+
+ {#each library as track (track.id)} +
+ +
{track.title}
+
async ({ update }) => update()}> + + +
+
+ {/each} +
+
+ {/if} +
+ +{#if editing} +
{ + if (e.target === e.currentTarget) editing = false; + }} + onkeydown={(e) => { + if (e.key === 'Escape') editing = false; + }} + > +
+
+

Editar playlist

+ +
+
async ({ update, result }) => { + await update({ reset: false }); + if (result.type === 'success') editing = false; + }} + class="mt-5 space-y-4" + > +
+ + +
+
+ + +
+ {#if form?.message} +

+ {form.message} +

+ {/if} +
+ + +
+
+
+
+{/if} + +{#if confirmingDelete} +
{ + if (e.target === e.currentTarget) confirmingDelete = false; + }} + onkeydown={(e) => { + if (e.key === 'Escape') confirmingDelete = false; + }} + > +
+

¿Eliminar «{playlist.name}»?

+

+ Esta acción no se puede deshacer. Las canciones seguirán en tu biblioteca. +

+
+ +
+ +
+
+
+
+{/if} diff --git a/frontend/src/routes/(app)/upload/+page.server.ts b/frontend/src/routes/(app)/upload/+page.server.ts new file mode 100644 index 0000000..385505b --- /dev/null +++ b/frontend/src/routes/(app)/upload/+page.server.ts @@ -0,0 +1,105 @@ +import type { Actions } from './$types'; +import { fail } from '@sveltejs/kit'; +import { createApiClient } from '$lib/server/api'; +import { putPresigned } from '$lib/server/upload'; + +const MAX_TITLE = 100; + +const AUDIO_TYPES: Record = { + mp3: 'audio/mpeg', + m4a: 'audio/mp4', + aac: 'audio/aac', + flac: 'audio/flac', + wav: 'audio/wav', + ogg: 'audio/ogg', + opus: 'audio/opus' +}; + +const IMAGE_TYPES: Record = { + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + png: 'image/png', + webp: 'image/webp' +}; + +function extOf(name: string): string { + const dot = name.lastIndexOf('.'); + return dot >= 0 ? name.slice(dot + 1).toLowerCase() : ''; +} + +function contentTypeOf(file: File, table: Record): string { + if (file.type) return file.type; + return table[extOf(file.name)] ?? 'application/octet-stream'; +} + +export const actions: Actions = { + default: async ({ request, locals, fetch }) => { + if (!locals.accessToken) { + return fail(401, { message: 'Inicia sesión para subir música.' }); + } + + const form = await request.formData(); + const title = String(form.get('title') ?? '').trim(); + const audio = form.get('audio'); + const cover = form.get('cover'); + + if (title === '' || title.length > MAX_TITLE) { + return fail(400, { message: 'El título es obligatorio (máx. 100 caracteres).' }); + } + if (!(audio instanceof File) || audio.size === 0) { + return fail(400, { message: 'Selecciona un archivo de audio.' }); + } + if (!(cover instanceof File) || cover.size === 0) { + return fail(400, { message: 'Selecciona una portada.' }); + } + + const audioContentType = contentTypeOf(audio, AUDIO_TYPES); + const pictureContentType = contentTypeOf(cover, IMAGE_TYPES); + const api = createApiClient({ fetch, accessToken: locals.accessToken }); + + const { data: urls, error: urlsError } = await api.POST('/tracks/upload-urls', { + body: { + pictureFileType: extOf(cover.name) || 'jpg', + pictureContentType, + audioFileType: extOf(audio.name) || 'mp3', + audioContentType, + expectedPictureSizeBytes: cover.size, + expectedAudioSizeBytes: audio.size + } + }); + + if (urlsError || !urls) { + return fail(502, { message: 'No se pudieron reservar las URLs de subida.' }); + } + + try { + const [audioBuffer, coverBuffer] = await Promise.all([ + audio.arrayBuffer(), + cover.arrayBuffer() + ]); + await Promise.all([ + putPresigned(urls.audioUploadUrl, Buffer.from(audioBuffer), urls.audioContentType), + putPresigned(urls.pictureUploadUrl, Buffer.from(coverBuffer), urls.pictureContentType) + ]); + } catch (err) { + return fail(502, { + message: 'Falló la subida del archivo al almacenamiento.', + detail: err instanceof Error ? err.message : undefined + }); + } + + const { data: track, error: createError } = await api.POST('/tracks', { + body: { + title, + pictureIntentId: urls.pictureIntentId, + audioIntentId: urls.audioIntentId + } + }); + + if (createError || !track) { + return fail(502, { message: 'No se pudo crear la pista tras la subida.' }); + } + + return { success: true, trackId: track.id, title: track.title }; + } +}; diff --git a/frontend/src/routes/(app)/upload/+page.svelte b/frontend/src/routes/(app)/upload/+page.svelte new file mode 100644 index 0000000..0e51502 --- /dev/null +++ b/frontend/src/routes/(app)/upload/+page.svelte @@ -0,0 +1,328 @@ + + + + Subir música · Musify + + + +
+ + + + + Volver a la biblioteca + +

Subir música

+

+ Añade una canción con su portada y título. Nosotros la procesamos para streaming. +

+ +
    + {#each steps as step, i (step.key)} +
  1. +
    + + {#if stepStates[i] === 'done'} + + {:else} + {i + 1} + {/if} + + {step.label} +
    +

    {step.hint}

    +
  2. + {/each} +
+ + {#if status === 'done'} +
+ + + +

¡Subida!

+

+ «{publishedTitle}» se está procesando. Aparecerá lista para reproducir en tu biblioteca en + unos momentos. +

+
+ + Ir a la biblioteca + + +
+
+ {:else} +
{ + status = 'uploading'; + errorMsg = ''; + return async ({ result }) => { + if (result.type === 'success') { + publishedTitle = title; + status = 'done'; + } else if (result.type === 'failure') { + status = 'error'; + errorMsg = (result.data?.message as string) ?? 'No se pudo subir la canción.'; + } else { + status = 'error'; + errorMsg = 'Error inesperado durante la subida.'; + } + }; + }} + class="mt-10 space-y-8" + > +
{ + e.preventDefault(); + dragging = true; + }} + ondragleave={() => (dragging = false)} + ondrop={onDrop} + class="relative flex min-h-72 flex-col items-center justify-center rounded-xl border border-dashed bg-neutral-950 p-10 text-center transition {dropClass}" + > + + + {#if audioName === ''} + + {:else} + + {/if} + + {#if audioName === ''} +

+ Arrastra tu audio aquí o haz clic para elegir +

+

MP3, FLAC, WAV…

+ {:else} +

{audioName}

+

{formatSize(audioSize)} · Listo para subir

+ {/if} +
+ +
+
+ Portada + +
+ +
+ + +
+

Se usa como nombre público de la pista en el catálogo.

+ {title.length}/{MAX_TITLE} +
+
+
+ + + + {#if status === 'uploading'} +
+
+ Subiendo y creando la pista… + un momento +
+
+
+
+
+ {/if} + + {#if status === 'error'} +
+ {errorMsg} +
+ {/if} + +
+ + Cancelar + + +
+
+ {/if} +
diff --git a/frontend/src/routes/+layout.server.ts b/frontend/src/routes/+layout.server.ts index d2511ab..9e6807d 100644 --- a/frontend/src/routes/+layout.server.ts +++ b/frontend/src/routes/+layout.server.ts @@ -1,5 +1,11 @@ +import { redirect } from '@sveltejs/kit'; import type { LayoutServerLoad } from './$types'; -export const load: LayoutServerLoad = ({ locals }) => ({ - user: locals.user -}); +const PUBLIC_PATHS = new Set(['/login']); + +export const load: LayoutServerLoad = ({ locals, url }) => { + if (!locals.user && !PUBLIC_PATHS.has(url.pathname)) { + redirect(302, `/login?returnTo=${encodeURIComponent(url.pathname + url.search)}`); + } + return { user: locals.user }; +}; diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index b8d7216..f1ea9b6 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -1,20 +1,75 @@ -
- -
{@render children()}
-
-
- -
+{#if isAuthPage} +
+ {@render children()} +
+{:else} +
+ +
+
+ {#key page.url.pathname} +
{@render children()}
+ {/key} +
+ {#if hasTrack} + + {/if} +
+
+{/if} diff --git a/frontend/src/routes/login/+server.ts b/frontend/src/routes/auth/login/+server.ts similarity index 100% rename from frontend/src/routes/login/+server.ts rename to frontend/src/routes/auth/login/+server.ts diff --git a/frontend/src/routes/login/+page.server.ts b/frontend/src/routes/login/+page.server.ts new file mode 100644 index 0000000..13f45cd --- /dev/null +++ b/frontend/src/routes/login/+page.server.ts @@ -0,0 +1,9 @@ +import { redirect } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = ({ locals, url }) => { + if (locals.user) { + redirect(302, url.searchParams.get('returnTo') ?? '/'); + } + return { returnTo: url.searchParams.get('returnTo') ?? '/' }; +}; diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte new file mode 100644 index 0000000..b1ab69d --- /dev/null +++ b/frontend/src/routes/login/+page.svelte @@ -0,0 +1,39 @@ + + + + Iniciar sesión · Musify + + + +
+
+
Musify
+ +
+

Tu música. Sin límites.

+

Inicia sesión para continuar.

+
+ + +
+