diff --git a/api/package.json b/api/package.json index c1abc25..688c4e9 100644 --- a/api/package.json +++ b/api/package.json @@ -38,4 +38,4 @@ "stripe": "^19.1.0", "music-metadata": "^11.9.0" } -} \ No newline at end of file +} diff --git a/api/src/routes/artists/+server.ts b/api/src/routes/artists/+server.ts index da637a1..a77509c 100644 --- a/api/src/routes/artists/+server.ts +++ b/api/src/routes/artists/+server.ts @@ -1,12 +1,9 @@ import { TABLES } from '../../../../shared/config'; import { handlePostgrestQuery, supabase } from '$lib/server/supabase'; -import type { ArtistRaw } from '../../../../shared/types'; +import type { Artist } from '../../../../shared/types/core'; export async function GET() { - return handlePostgrestQuery( - async () => await supabase.from(TABLES.artists).select(), - { - transform: (data) => data.sort((a, b) => a.name.localeCompare(b.name)) - } - ); + return handlePostgrestQuery(async () => await supabase.from(TABLES.artists).select(), { + transform: (data) => data.sort((a, b) => a.name.localeCompare(b.name)) + }); } diff --git a/api/src/routes/artists/[slug]/+server.ts b/api/src/routes/artists/[slug]/+server.ts index c59f719..b9ae3dc 100644 --- a/api/src/routes/artists/[slug]/+server.ts +++ b/api/src/routes/artists/[slug]/+server.ts @@ -1,16 +1,18 @@ import { TABLES } from '../../../../../shared/config'; import { handlePostgrestQuery, supabase } from '$lib/server/supabase'; -import type { ArtistRaw } from '../../../../../shared/types'; +import type { Artist } from '../../../../../shared/types/core'; export async function GET({ params }) { - return handlePostgrestQuery( - async () => await supabase.from(TABLES.artists).select().eq('id', params.slug).single() + return handlePostgrestQuery( + async () => await supabase.from(TABLES.artists).select().eq('id', params.slug).single(), + { errorMessage: 'Failed to fetch artist' } ); } export async function PATCH({ request, params }) { - const body: Partial = await request.json(); - return handlePostgrestQuery( - async () => await supabase.from(TABLES.artists).update(body).eq('id', params.slug) + const body: Partial = await request.json(); + return handlePostgrestQuery( + async () => await supabase.from(TABLES.artists).update(body).eq('id', params.slug), + { errorMessage: 'Failed to update artist details' } ); } diff --git a/api/src/routes/artists/[slug]/releases/+server.ts b/api/src/routes/artists/[slug]/releases/+server.ts index a4776bb..a7b2b95 100644 --- a/api/src/routes/artists/[slug]/releases/+server.ts +++ b/api/src/routes/artists/[slug]/releases/+server.ts @@ -1,9 +1,10 @@ -import { TABLES } from '../../../../../../shared/config'; import { handlePostgrestQuery, supabase } from '$lib/server/supabase'; -import type { ReleaseHydrated } from '../../../../../../shared/types'; +import { TABLES } from '../../../../../../shared/config'; +import type { ReleaseHydrated } from '../../../../../../shared/types/hydrated'; export async function GET({ params }) { return handlePostgrestQuery( - async () => await supabase.from(TABLES.releasesHydrated).select().eq('artist_id', params.slug) + async () => await supabase.from(TABLES.releasesRich).select().eq('artist_id', params.slug), + { errorMessage: 'Failed to fetch releases for artist' } ); } diff --git a/api/src/routes/collections/+server.ts b/api/src/routes/collections/+server.ts new file mode 100644 index 0000000..3289ba8 --- /dev/null +++ b/api/src/routes/collections/+server.ts @@ -0,0 +1,21 @@ +import { supabase } from '$lib/server/supabase'; +import { json } from '@sveltejs/kit'; +import { TABLES } from '../../../../shared/config'; + +export async function POST({ request }) { + const { userId, name, description } = await request.json(); + + if (!name) { + return json({ error: 'Missing collection name' }, { status: 400 }); + } + + const { error } = await supabase + .from(TABLES.collections) + .insert({ user_id: userId, name, description }); + + if (error) { + return json({ error: 'Failed to create collection' }, { status: 500 }); + } + + return json({ success: true }); +} diff --git a/api/src/routes/collections/[slug]/+server.ts b/api/src/routes/collections/[slug]/+server.ts new file mode 100644 index 0000000..8bb435c --- /dev/null +++ b/api/src/routes/collections/[slug]/+server.ts @@ -0,0 +1,63 @@ +import { handlePostgrestQuery, supabase } from '$lib/server/supabase'; +import { json } from '@sveltejs/kit'; +import { TABLES } from '../../../../../shared/config'; + +export async function GET({ params }) { + return handlePostgrestQuery( + async () => supabase.from(TABLES.collectionsRich).select('*').eq('id', params.slug).single(), + { errorMessage: 'Failed to fetch collection' } + ); +} + +export async function PATCH({ request, params }) { + const collectionId = params.slug; + const { releaseId, addOrRemove } = await request.json(); + + if (!releaseId) { + return json({ error: 'Missing release ID' }, { status: 400 }); + } + + if (addOrRemove === 'remove') { + const { error } = await supabase + .from(TABLES.collectionReleases) + .delete() + .eq('collection_id', collectionId) + .eq('release_id', releaseId); + + if (error) { + console.error('Error removing release from collection:', error); + return json({ error: 'Failed to remove release from collection' }, { status: 500 }); + } + + return json({ success: true }); + } + + const { error } = await supabase + .from(TABLES.collectionReleases) + .insert({ collection_id: collectionId, release_id: releaseId }); + + if (error) { + console.error('Error adding release to collection:', error); + return json({ error: 'Failed to add release to collection' }, { status: 500 }); + } + + return json({ success: true }); +} + +export async function DELETE({ request, params }) { + const collectionId = params.slug; + const { userId } = await request.json(); + + const { error } = await supabase + .from(TABLES.collections) + .delete() + .eq('id', collectionId) + .eq('user_id', userId); + + if (error) { + console.error('Error deleting collection:', error); + return json({ error: 'Failed to delete collection' }, { status: 500 }); + } + + return json({ success: true }); +} diff --git a/api/src/routes/mixtapes/+server.ts b/api/src/routes/mixtapes/+server.ts new file mode 100644 index 0000000..12e2d3e --- /dev/null +++ b/api/src/routes/mixtapes/+server.ts @@ -0,0 +1,23 @@ +import { supabase } from '$lib/server/supabase'; +import { json } from '@sveltejs/kit'; +import { TABLES } from '../../../../shared/config'; + +export async function POST({ request }) { + const { userId, name, description } = await request.json(); + + if (!name) { + console.log('Mixtape name is missing'); + return json({ error: 'Missing mixtape name' }, { status: 400 }); + } + + const { error } = await supabase + .from(TABLES.mixtapes) + .insert({ user_id: userId, name, description }); + + if (error) { + console.log('Error creating mixtape:', error); + return json({ error: 'Failed to create mixtape' }, { status: 500 }); + } + + return json({ success: true }); +} diff --git a/api/src/routes/mixtapes/[slug]/+server.ts b/api/src/routes/mixtapes/[slug]/+server.ts new file mode 100644 index 0000000..5aa9b04 --- /dev/null +++ b/api/src/routes/mixtapes/[slug]/+server.ts @@ -0,0 +1,47 @@ +import { handlePostgrestQuery, supabase } from '$lib/server/supabase'; +import { json } from '@sveltejs/kit'; +import { TABLES } from '../../../../../shared/config'; + +export async function GET({ params }) { + return handlePostgrestQuery( + async () => supabase.from(TABLES.mixtapesRich).select('*').eq('id', params.slug).single(), + { errorMessage: 'Failed to fetch mixtape' } + ); +} + +export async function PATCH({ request, params }) { + const mixtapeId = params.slug; + const { trackId } = await request.json(); + + if (!trackId) { + return json({ error: 'Missing track ID' }, { status: 400 }); + } + + const { error } = await supabase + .from(TABLES.mixtapeTracks) + .insert({ mixtape_id: mixtapeId, track_id: trackId }); + + if (error) { + return json({ error: 'Failed to add track to mixtape' }, { status: 500 }); + } + + return json({ success: true }); +} + +export const DELETE = async ({ request, params }) => { + const mixtapeId = params.slug; + const { userId } = await request.json(); + + const { error } = await supabase + .from(TABLES.mixtapes) + .delete() + .eq('id', mixtapeId) + .eq('user_id', userId); + + if (error) { + console.error('Error deleting mixtape:', error); + return json({ error: 'Failed to delete mixtape' }, { status: 500 }); + } + + return json({ success: true }); +}; diff --git a/api/src/routes/releases/+server.ts b/api/src/routes/releases/+server.ts index 10e6fac..bbfcf76 100644 --- a/api/src/routes/releases/+server.ts +++ b/api/src/routes/releases/+server.ts @@ -1,13 +1,14 @@ import { handlePostgrestQuery, supabase } from '$lib/server/supabase'; import { TABLES } from '../../../../shared/config'; import { sortReleasesByDate } from '../../../../shared/utils'; -import type { ReleaseHydrated, ReleaseRaw } from '../../../../shared/types'; +import type { ReleaseHydrated } from '../../../../shared/types/hydrated'; import { pinata } from '$lib/server/pinata'; import { json } from '@sveltejs/kit'; +import type { Release } from '../../../../shared/types/core'; export async function GET() { return handlePostgrestQuery( - async () => await supabase.from(TABLES.releasesHydrated).select(), + async () => await supabase.from(TABLES.releasesRich).select(), { errorMessage: 'Failed to fetch artist data', transform: sortReleasesByDate @@ -31,7 +32,7 @@ export async function POST({ request }) { .name(`'${releaseName}' cover art`) .group(import.meta.env.PINATA_ARTWORK_GROUP); - return handlePostgrestQuery( + return handlePostgrestQuery( async () => await supabase .from(TABLES.releases) diff --git a/api/src/routes/releases/[slug]/+server.ts b/api/src/routes/releases/[slug]/+server.ts index ea8c87b..3283249 100644 --- a/api/src/routes/releases/[slug]/+server.ts +++ b/api/src/routes/releases/[slug]/+server.ts @@ -1,9 +1,10 @@ -import { TABLES } from '../../../../../shared/config'; import { handlePostgrestQuery, supabase } from '$lib/server/supabase'; -import type { ReleaseHydrated } from '../../../../../shared/types'; +import { TABLES } from '../../../../../shared/config'; +import type { ReleaseHydrated } from '../../../../../shared/types/hydrated'; export async function GET({ params }) { return handlePostgrestQuery( - async () => await supabase.from(TABLES.releasesHydrated).select().eq('id', params.slug).single() + async () => await supabase.from(TABLES.releasesRich).select().eq('id', params.slug).single(), + { errorMessage: 'Failed to fetch release' } ); } diff --git a/api/src/routes/search/+server.ts b/api/src/routes/search/+server.ts index 59df2e4..8d42975 100644 --- a/api/src/routes/search/+server.ts +++ b/api/src/routes/search/+server.ts @@ -1,5 +1,5 @@ import { supabase } from '$lib/server/supabase'; -import type { SearchResult } from '../../../../shared/types'; +import type { SearchResult } from '../../../../shared/types/core'; import type { RequestHandler } from './$types'; import { json } from '@sveltejs/kit'; diff --git a/api/src/routes/users/[slug]/+server.ts b/api/src/routes/users/[slug]/+server.ts index c2e0881..a9b1780 100644 --- a/api/src/routes/users/[slug]/+server.ts +++ b/api/src/routes/users/[slug]/+server.ts @@ -1,9 +1,9 @@ import { TABLES } from '../../../../../shared/config'; import { supabase } from '$lib/server/supabase'; import { json } from '@sveltejs/kit'; -import type { UserProfile } from '../../../../../shared/types'; +import type { User } from '../../../../../shared/types/core'; -const getUser = async (id: string): Promise => { +const getUser = async (id: string): Promise => { const { data } = await supabase .from(TABLES.users) .select(`first_name, tokens_balance, pay_per_stream`) diff --git a/api/src/routes/users/[slug]/collections/+server.ts b/api/src/routes/users/[slug]/collections/+server.ts new file mode 100644 index 0000000..4627f51 --- /dev/null +++ b/api/src/routes/users/[slug]/collections/+server.ts @@ -0,0 +1,9 @@ +import { handlePostgrestQuery, supabase } from '$lib/server/supabase'; +import { TABLES } from '../../../../../../shared/config/index'; + +export async function GET({ params }) { + return handlePostgrestQuery( + async () => supabase.from(TABLES.collectionsRich).select('*').eq('user_id', params.slug), + { errorMessage: 'Failed to fetch collections' } + ); +} diff --git a/api/src/routes/users/[slug]/likes/+server.ts b/api/src/routes/users/[slug]/likes/+server.ts index 4a77b45..3f292ac 100644 --- a/api/src/routes/users/[slug]/likes/+server.ts +++ b/api/src/routes/users/[slug]/likes/+server.ts @@ -1,7 +1,6 @@ import { TABLES } from '../../../../../../shared/config'; import { supabase } from '$lib/server/supabase'; import { json } from '@sveltejs/kit'; -import type { LikedTrackObject } from '../../../../../../shared/types'; export async function GET({ params }) { const maybeUserID = params.slug; @@ -15,9 +14,9 @@ export async function GET({ params }) { return json({ error: 'Failed to fetch liked tracks' }, { status: 500 }); } - const { data: likedTracksRaw, error: likedTracksError } = await supabase - .from(TABLES.tracks) - .select(`id, artist_id, title, ipfs_cid, duration_seconds, created_at`) + const { data: likedTracks, error: likedTracksError } = await supabase + .from('tracks_hydrated') + .select('*') .in( 'id', likedTrackIDs.map((item) => item.track_id) @@ -27,27 +26,6 @@ export async function GET({ params }) { return json({ error: 'Failed to fetch liked tracks details' }, { status: 500 }); } - const likedTracks: LikedTrackObject[] = []; - - for (const likedTrack of likedTracksRaw) { - const { data: releaseID, error: releaseIDError } = await supabase - .from(TABLES.releaseTracks) - .select('release_id') - .eq('track_id', likedTrack.id) - .limit(1) - .single(); - if (releaseIDError) { - console.error('Error fetching release ID:', releaseIDError); - continue; - } - const { data: release } = await supabase - .from(TABLES.releasesHydrated) - .select('*') - .eq('id', releaseID.release_id) - .single(); - likedTracks.push({ track: likedTrack, release }); - } - return json(likedTracks); } diff --git a/api/src/routes/users/[slug]/mixtapes/+server.ts b/api/src/routes/users/[slug]/mixtapes/+server.ts new file mode 100644 index 0000000..0c02540 --- /dev/null +++ b/api/src/routes/users/[slug]/mixtapes/+server.ts @@ -0,0 +1,9 @@ +import { handlePostgrestQuery, supabase } from '$lib/server/supabase'; +import { TABLES } from '../../../../../../shared/config'; + +export async function GET({ params }) { + return handlePostgrestQuery( + async () => supabase.from(TABLES.mixtapesRich).select('*').eq('user_id', params.slug), + { errorMessage: 'Failed to fetch mixtapes' } + ); +} diff --git a/app/package-lock.json b/app/package-lock.json index bc1624a..5678f0d 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.1", "dependencies": { "@supabase/ssr": "^0.7.0", - "@supabase/supabase-js": "^2.74.0" + "@supabase/supabase-js": "^2.74.0", + "zod": "^4.1.12" }, "devDependencies": { "@eslint/compat": "^1.4.0", @@ -5057,6 +5058,15 @@ "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", "dev": true, "license": "MIT" + }, + "node_modules/zod": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/app/package.json b/app/package.json index 980b31b..ee3e1f9 100644 --- a/app/package.json +++ b/app/package.json @@ -40,6 +40,7 @@ }, "dependencies": { "@supabase/ssr": "^0.7.0", - "@supabase/supabase-js": "^2.74.0" + "@supabase/supabase-js": "^2.74.0", + "zod": "^4.1.12" } -} \ No newline at end of file +} diff --git a/app/src/lib/components/ArtistCardGrid.svelte b/app/src/lib/components/ArtistCardGrid.svelte index e92aed2..15f88e0 100644 --- a/app/src/lib/components/ArtistCardGrid.svelte +++ b/app/src/lib/components/ArtistCardGrid.svelte @@ -1,12 +1,12 @@ diff --git a/app/src/lib/components/ReleaseCardGrid.svelte b/app/src/lib/components/ReleaseCardGrid.svelte index 9575667..964da46 100644 --- a/app/src/lib/components/ReleaseCardGrid.svelte +++ b/app/src/lib/components/ReleaseCardGrid.svelte @@ -1,5 +1,5 @@
-
- “{track.title}” by {release.artist_name} +
+
+ “{track.title}” by {release.artist.name} +
+
-
diff --git a/app/src/lib/components/icons/Albums.svelte b/app/src/lib/components/icons/Albums.svelte new file mode 100644 index 0000000..030d421 --- /dev/null +++ b/app/src/lib/components/icons/Albums.svelte @@ -0,0 +1,20 @@ + + + + + diff --git a/app/src/lib/components/icons/Cassette.svelte b/app/src/lib/components/icons/Cassette.svelte new file mode 100644 index 0000000..f8f7370 --- /dev/null +++ b/app/src/lib/components/icons/Cassette.svelte @@ -0,0 +1,22 @@ + + + + + + diff --git a/app/src/lib/components/icons/Heart.svelte b/app/src/lib/components/icons/Heart.svelte index 851d0c2..e77e09b 100644 --- a/app/src/lib/components/icons/Heart.svelte +++ b/app/src/lib/components/icons/Heart.svelte @@ -20,5 +20,7 @@ svg { width: 1em; height: 1em; + display: inline-block; + vertical-align: middle; } diff --git a/app/src/lib/components/icons/ThreeDots.svelte b/app/src/lib/components/icons/ThreeDots.svelte new file mode 100644 index 0000000..d393953 --- /dev/null +++ b/app/src/lib/components/icons/ThreeDots.svelte @@ -0,0 +1,12 @@ + + + diff --git a/app/src/lib/components/icons/Vinyl.svelte b/app/src/lib/components/icons/Vinyl.svelte new file mode 100644 index 0000000..92bdeb8 --- /dev/null +++ b/app/src/lib/components/icons/Vinyl.svelte @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/lib/components/layout/ButtonWrapper.svelte b/app/src/lib/components/layout/ButtonWrapper.svelte index 2932b0b..2822d38 100644 --- a/app/src/lib/components/layout/ButtonWrapper.svelte +++ b/app/src/lib/components/layout/ButtonWrapper.svelte @@ -5,7 +5,7 @@
onClickFunction()} onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { onClickFunction; diff --git a/app/src/lib/components/layout/Header.svelte b/app/src/lib/components/layout/Header.svelte index 9474fd3..acd4a37 100644 --- a/app/src/lib/components/layout/Header.svelte +++ b/app/src/lib/components/layout/Header.svelte @@ -5,7 +5,7 @@ import ButtonWrapper from './ButtonWrapper.svelte'; import { API_BASE } from '$lib/global/config'; import { page } from '$app/state'; - import type { SearchResult } from '../../../../../shared/types'; + import type { SearchResult } from '../../../../../shared/types/core'; import SearchResults from '../search/SearchResults.svelte'; let { diff --git a/app/src/lib/components/layout/PopupWrapper.svelte b/app/src/lib/components/layout/PopupWrapper.svelte new file mode 100644 index 0000000..c1af69b --- /dev/null +++ b/app/src/lib/components/layout/PopupWrapper.svelte @@ -0,0 +1,27 @@ + + + + + diff --git a/app/src/lib/components/layout/SectionLink.svelte b/app/src/lib/components/layout/SectionLink.svelte new file mode 100644 index 0000000..4d577f8 --- /dev/null +++ b/app/src/lib/components/layout/SectionLink.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/lib/components/releases/ReleaseCollectionsMenu.svelte b/app/src/lib/components/releases/ReleaseCollectionsMenu.svelte new file mode 100644 index 0000000..f9e3a9c --- /dev/null +++ b/app/src/lib/components/releases/ReleaseCollectionsMenu.svelte @@ -0,0 +1,51 @@ + + +
+

Manage which collections {release.title} belongs to

+ {#each collections as collection} +
+
{collection.name}
+
+ + + r.id === release.id) ? 'remove' : 'add'} + /> + +
+
+ {/each} +
+ + diff --git a/app/src/lib/components/releases/ReleaseTrackButton.svelte b/app/src/lib/components/releases/ReleaseTrackButton.svelte index 2c8ef13..43a3b22 100644 --- a/app/src/lib/components/releases/ReleaseTrackButton.svelte +++ b/app/src/lib/components/releases/ReleaseTrackButton.svelte @@ -1,21 +1,16 @@ @@ -25,13 +20,7 @@ if (track.ipfs_cid == userState.activeSong?.ipfs_cid) { userState.activeSongIsPaused = !userState.activeSongIsPaused; } else { - setActiveSong( - track, - release, - session.user.id, - userState.liveBalance ?? profileData.tokens_balance, - profileData.pay_per_stream - ); + setActiveSong(track, release, userState.id, userState.liveBalance, userState.payPerStream); userState.autoPlay = false; } }} diff --git a/app/src/lib/components/releases/TrackLikeButton.svelte b/app/src/lib/components/releases/TrackLikeButton.svelte index 7332562..6d44616 100644 --- a/app/src/lib/components/releases/TrackLikeButton.svelte +++ b/app/src/lib/components/releases/TrackLikeButton.svelte @@ -1,8 +1,7 @@ -
- - t.track.id === trackID) ? 'remove' : 'add'} - /> -
@@ -49,7 +32,6 @@ } form { display: flex; - justify-content: flex-end; } button { background: none; diff --git a/app/src/lib/components/releases/TracksTable.svelte b/app/src/lib/components/releases/TracksTable.svelte index 0bd85fc..aed08c4 100644 --- a/app/src/lib/components/releases/TracksTable.svelte +++ b/app/src/lib/components/releases/TracksTable.svelte @@ -1,19 +1,12 @@ @@ -24,25 +17,18 @@ # Track {#if showReleaseAndArtist} - Release + Release Artist {/if} Duration + - {#each release.tracks as track, i} - + {#each tracks as track, i} + {/each} @@ -56,4 +42,12 @@ th { font-weight: 500; } + .hide-on-mobile { + display: none; + } + @media (min-width: 640px) { + .hide-on-mobile { + display: table-cell; + } + } diff --git a/app/src/lib/components/releases/TracksTableRow.svelte b/app/src/lib/components/releases/TracksTableRow.svelte index 819ca72..f4128c6 100644 --- a/app/src/lib/components/releases/TracksTableRow.svelte +++ b/app/src/lib/components/releases/TracksTableRow.svelte @@ -1,50 +1,75 @@ {i || ''} {track.title} {#if showReleaseAndArtist} - {release.title} - {release.artist_name} + {track.release.title} + {track.artist.name} {/if} {prettifyDuration(track.duration_seconds)} - + + + + - + (popupMenuOpen = !popupMenuOpen)}> + + +{#if popupMenuOpen} + +
+

Track options for "{track.title}"

+
+ + + +
+
+
+{/if} + diff --git a/app/src/routes/+layout.ts b/app/src/routes/+layout.ts index 6a03a0d..9b3ed28 100644 --- a/app/src/routes/+layout.ts +++ b/app/src/routes/+layout.ts @@ -39,6 +39,8 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => { if (data.profileData) { userState.liveBalance = data.profileData.tokens_balance; userState.payPerStream = data.profileData.pay_per_stream; + userState.music.likedTracks = data.likedTracks; + userState.music.mixtapes = data.mixtapes; } return { @@ -46,7 +48,9 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => { session, user, profileData: data.profileData, + collections: data.collections, followedArtists: data.followedArtists, - likedTracks: data.likedTracks + likedTracks: data.likedTracks, + mixtapes: data.mixtapes }; }; diff --git a/app/src/routes/+page.server.ts b/app/src/routes/+page.server.ts new file mode 100644 index 0000000..f189b24 --- /dev/null +++ b/app/src/routes/+page.server.ts @@ -0,0 +1 @@ +// I don't like this but SvelteKit requires a +page.server.ts file for hooks to work diff --git a/app/src/routes/+page.ts b/app/src/routes/+page.ts index fe3b99d..709f363 100644 --- a/app/src/routes/+page.ts +++ b/app/src/routes/+page.ts @@ -1,10 +1,12 @@ import { API_BASE } from '$lib/global/config'; -import type { ArtistRaw, ReleaseHydrated } from '../../../shared/types'; +import type { ReleaseHydrated } from '../../../shared/types/hydrated'; +import type { Artist } from '../../../shared/types/core'; export const load = async ({ fetch }) => { - const artists: ArtistRaw[] = await fetch(`${API_BASE}/artists`).then((res) => res.json()); + const artists: Artist[] = await fetch(`${API_BASE}/artists`).then((res) => res.json()); const releases: ReleaseHydrated[] = await fetch(`${API_BASE}/releases`).then((res) => res.json()); + // Filter artists to only those who have releases const filteredArtists = artists.filter((artist) => releases.some((release) => release.artist_id === artist.id) ); diff --git a/app/src/routes/account/+page.svelte b/app/src/routes/account/+page.svelte index c0845f1..8236900 100644 --- a/app/src/routes/account/+page.svelte +++ b/app/src/routes/account/+page.svelte @@ -2,7 +2,7 @@ import { enhance } from '$app/forms'; import { userState } from '$lib/global/state.svelte.js'; import { REVENUE_SPLIT } from '../../../../shared/config'; - import { prettifyBalance, prettifyPennies } from '$lib/utils/index.js'; + import { prettifyBalance, prettifyPennies } from '$lib/utils/index'; import type { SubmitFunction } from '@sveltejs/kit'; import ContentBlock from '$lib/components/ContentBlock.svelte'; import { API_BASE } from '$lib/global/config'; diff --git a/app/src/routes/artists/+page.ts b/app/src/routes/artists/+page.ts index 8003260..e143341 100644 --- a/app/src/routes/artists/+page.ts +++ b/app/src/routes/artists/+page.ts @@ -1,8 +1,9 @@ import { API_BASE } from '$lib/global/config'; -import type { ArtistRaw, ReleaseHydrated } from '../../../../shared/types'; +import type { ReleaseHydrated } from '../../../../shared/types/hydrated'; +import type { Artist } from '../../../../shared/types/core'; export const load = async ({ fetch }) => { - const artists: ArtistRaw[] = await fetch(`${API_BASE}/artists`).then((res) => res.json()); + const artists: Artist[] = await fetch(`${API_BASE}/artists`).then((res) => res.json()); const releases: ReleaseHydrated[] = await fetch(`${API_BASE}/releases`).then((res) => res.json()); const filteredArtists = artists.filter((artist) => diff --git a/app/src/routes/artists/[slug]/+page.server.ts b/app/src/routes/artists/[slug]/+page.server.ts index fe18e8e..9ed9b2e 100644 --- a/app/src/routes/artists/[slug]/+page.server.ts +++ b/app/src/routes/artists/[slug]/+page.server.ts @@ -1,17 +1,14 @@ -import type { Actions } from '@sveltejs/kit'; -import type { ArtistRaw } from '../../../../../shared/types'; import { API_BASE } from '$lib/global/config'; +import type { Artist } from '../../../../../shared/types/core'; import { sortReleasesByDate } from '../../../../../shared/utils'; export const load = async ({ params, fetch }) => { - const matchingArtist: ArtistRaw = await fetch(`${API_BASE}/artists/${params.slug}`).then( - (res) => { - if (!res.ok) { - throw new Error(`Failed to fetch artist with slug: ${params.slug}`); - } - return res.json(); + const matchingArtist: Artist = await fetch(`${API_BASE}/artists/${params.slug}`).then((res) => { + if (!res.ok) { + throw new Error(`Failed to fetch artist with slug: ${params.slug}`); } - ); + return res.json(); + }); if (!matchingArtist) { return { @@ -32,23 +29,3 @@ export const load = async ({ params, fetch }) => { releases: sortReleasesByDate(artistReleases) }; }; - -export const actions: Actions = { - toggleFollowedArtist: async ({ request, fetch, locals: { safeGetSession } }) => { - const { session } = await safeGetSession(); - if (session) { - const formData = await request.formData(); - const artistID = formData.get('artistID'); - const addOrRemove = formData.get('addOrRemove'); - if (artistID && addOrRemove) { - await fetch(`${API_BASE}/users/${session.user.id}/following`, { - method: addOrRemove === 'remove' ? 'DELETE' : 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ artistId: artistID }) - }); - } - } - } -}; diff --git a/app/src/routes/artists/[slug]/+page.svelte b/app/src/routes/artists/[slug]/+page.svelte index b26d2ff..f07dad8 100644 --- a/app/src/routes/artists/[slug]/+page.svelte +++ b/app/src/routes/artists/[slug]/+page.svelte @@ -1,12 +1,14 @@ - - - Your likes · Soli - - - -

Your liked tracks

- -{#if data.session} - - - - - - - - - - - - - - {#each data.likedTracks as { track, release }} - - {/each} - -
TrackReleaseArtistDuration
-{/if} - - diff --git a/app/src/routes/me/collections/+page.svelte b/app/src/routes/me/collections/+page.svelte new file mode 100644 index 0000000..7bd9993 --- /dev/null +++ b/app/src/routes/me/collections/+page.svelte @@ -0,0 +1,78 @@ + + + + Your collections · Soli + + + +
+

Your collections

+ + {#each collections as collection} + +
+

{collection.name}

+ {#if collection.description} +
{collection.description}
+ {/if} +
{collection.releases.length} release{collection.releases.length !== 1 ? 's' : ''}
+
+
+ {/each} +
+ +
+ {#if data.user} +

Create a new collection

+ +
+ + + +
+ {/if} +
+ + diff --git a/app/src/routes/me/collections/[slug]/+page.svelte b/app/src/routes/me/collections/[slug]/+page.svelte new file mode 100644 index 0000000..5f43e54 --- /dev/null +++ b/app/src/routes/me/collections/[slug]/+page.svelte @@ -0,0 +1,44 @@ + + + + {name} · Your collections · Soli + + + + + +
+
+

{name}

+ {#if description} +
{description}
+ {/if} +
+ +
+ +
+ + +
+ + diff --git a/app/src/routes/me/liked-tracks/+page.svelte b/app/src/routes/me/liked-tracks/+page.svelte new file mode 100644 index 0000000..4894130 --- /dev/null +++ b/app/src/routes/me/liked-tracks/+page.svelte @@ -0,0 +1,26 @@ + + + + Your liked tracks · Soli + + + +

Your liked tracks

+ + diff --git a/app/src/routes/me/mixtapes/+page.svelte b/app/src/routes/me/mixtapes/+page.svelte new file mode 100644 index 0000000..675f282 --- /dev/null +++ b/app/src/routes/me/mixtapes/+page.svelte @@ -0,0 +1,74 @@ + + + + Your mixtapes · Soli + + + +
+

Your mixtapes

+ + {#each mixtapes as mixtape} + +
+

{mixtape.name}

+ {#if mixtape.description} +
{mixtape.description}
+ {/if} +
{mixtape.tracks.length} track{mixtape.tracks.length !== 1 ? 's' : ''}
+
+
+ {/each} +
+ +
+

Create a new mixtape

+ +
+ + + +
+
+ + diff --git a/app/src/routes/me/mixtapes/[slug]/+page.svelte b/app/src/routes/me/mixtapes/[slug]/+page.svelte new file mode 100644 index 0000000..fcbe61b --- /dev/null +++ b/app/src/routes/me/mixtapes/[slug]/+page.svelte @@ -0,0 +1,44 @@ + + + + {name} · Your mixtapes · Soli + + + + + +
+
+

{name}

+ {#if description} +
{description}
+ {/if} +
+ +
+ +
+ + +
+ + diff --git a/app/src/routes/releases/+page.svelte b/app/src/routes/releases/+page.svelte index 880b417..ee6e8a1 100644 --- a/app/src/routes/releases/+page.svelte +++ b/app/src/routes/releases/+page.svelte @@ -1,6 +1,6 @@ diff --git a/app/src/routes/releases/+page.ts b/app/src/routes/releases/+page.ts index 10caaea..fa34381 100644 --- a/app/src/routes/releases/+page.ts +++ b/app/src/routes/releases/+page.ts @@ -1,5 +1,5 @@ import { API_BASE } from '$lib/global/config'; -import type { ReleaseHydrated } from '../../../../shared/types'; +import type { ReleaseHydrated } from '../../../../shared/types/hydrated'; export const load = async ({ fetch }) => { const releases: ReleaseHydrated[] = await fetch(`${API_BASE}/releases`).then((res) => res.json()); diff --git a/app/src/routes/releases/[slug]/+page.svelte b/app/src/routes/releases/[slug]/+page.svelte index c5d5bb3..736a952 100644 --- a/app/src/routes/releases/[slug]/+page.svelte +++ b/app/src/routes/releases/[slug]/+page.svelte @@ -1,12 +1,22 @@ - {release.title} · {release.artist_name} · Soli + {release.title} · {release.artist.name} · Soli - +
-
-

{release.title}

- +
- {release.artist_name} +

{release.title}

+
+ (popupMenuOpen = !popupMenuOpen)}> + +
+ {#if popupMenuOpen} + + + + {/if} + {`Cover @@ -66,11 +87,12 @@
- ({ + ...track, + release, + artist: release.artist + }))} />
@@ -86,15 +108,18 @@
diff --git a/app/src/routes/releases/[slug]/+page.ts b/app/src/routes/releases/[slug]/+page.ts index 07d4c7c..01311f6 100644 --- a/app/src/routes/releases/[slug]/+page.ts +++ b/app/src/routes/releases/[slug]/+page.ts @@ -1,5 +1,5 @@ import { API_BASE } from '$lib/global/config'; -import type { ReleaseHydrated } from '../../../../../shared/types'; +import type { ReleaseHydrated } from '../../../../../shared/types/hydrated'; export const load = async ({ fetch, params }) => { const release: ReleaseHydrated = await fetch(`${API_BASE}/releases/${params.slug}`).then((res) => diff --git a/app/svelte.config.js b/app/svelte.config.js index fe96962..1338620 100644 --- a/app/svelte.config.js +++ b/app/svelte.config.js @@ -9,9 +9,17 @@ const config = { preprocess: [vitePreprocess, mdsvex({ extensions: ['.md'] })], kit: { - adapter: adapter() + adapter: adapter(), + experimental: { + remoteFunctions: true + } }, - extensions: ['.svelte', '.md'] + extensions: ['.svelte', '.md'], + compilerOptions: { + experimental: { + async: true + } + } }; export default config; diff --git a/dashboard/src/lib/components/ProfileSummary.svelte b/dashboard/src/lib/components/ProfileSummary.svelte index 65155c3..8accaf4 100644 --- a/dashboard/src/lib/components/ProfileSummary.svelte +++ b/dashboard/src/lib/components/ProfileSummary.svelte @@ -1,9 +1,9 @@