From 51defe764752f075c07497ab2c8cde43e28373ca Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Mon, 3 Nov 2025 16:15:34 +0000 Subject: [PATCH 1/4] Add remote functions to dashboard --- api/package-lock.json | 27 +-- api/src/routes/artists/[slug]/+server.ts | 10 +- api/src/routes/releases/[slug]/+server.ts | 12 + api/src/routes/tracks/+server.ts | 11 +- api/src/routes/tracks/[slug]/+server.ts | 14 ++ app/package-lock.json | 42 ++-- dashboard/package-lock.json | 119 ++++++---- dashboard/package.json | 13 +- dashboard/src/lib/components/Card.svelte | 2 +- .../src/lib/components/ProfileSummary.svelte | 76 +----- .../src/lib/components/ReleaseInfo.svelte | 8 +- .../components/forms/AddReleaseForm.svelte | 41 ++++ .../forms/AddTrackToReleaseForm.svelte | 38 +++ .../components/forms/ArtistProfileForm.svelte | 64 ++++++ .../components/forms/UploadTrackForm.svelte | 26 +++ .../src/lib/components/icons/BinIcon.svelte | 16 ++ .../components/layout/ButtonWrapper.svelte | 28 +++ dashboard/src/lib/config.ts | 16 +- .../src/lib/remote-functions/artist.remote.ts | 57 +++++ dashboard/src/lib/remote-functions/auth.ts | 12 + .../src/lib/remote-functions/music.remote.ts | 84 +++++++ dashboard/src/lib/utils/index.ts | 23 ++ dashboard/src/routes/+layout.svelte | 23 +- dashboard/src/routes/+page.server.ts | 158 ------------- dashboard/src/routes/+page.svelte | 216 ++++-------------- dashboard/svelte.config.js | 11 +- shared/types/forms.ts | 9 - 27 files changed, 632 insertions(+), 524 deletions(-) create mode 100644 api/src/routes/tracks/[slug]/+server.ts create mode 100644 dashboard/src/lib/components/forms/AddReleaseForm.svelte create mode 100644 dashboard/src/lib/components/forms/AddTrackToReleaseForm.svelte create mode 100644 dashboard/src/lib/components/forms/ArtistProfileForm.svelte create mode 100644 dashboard/src/lib/components/forms/UploadTrackForm.svelte create mode 100644 dashboard/src/lib/components/icons/BinIcon.svelte create mode 100644 dashboard/src/lib/components/layout/ButtonWrapper.svelte create mode 100644 dashboard/src/lib/remote-functions/artist.remote.ts create mode 100644 dashboard/src/lib/remote-functions/auth.ts create mode 100644 dashboard/src/lib/remote-functions/music.remote.ts delete mode 100644 dashboard/src/routes/+page.server.ts delete mode 100644 shared/types/forms.ts diff --git a/api/package-lock.json b/api/package-lock.json index 28bafe8..002d3f7 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -1216,6 +1216,7 @@ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.76.1.tgz", "integrity": "sha512-dYMh9EsTVXZ6WbQ0QmMGIhbXct5+x636tXXaaxUmwjj3kY1jyBTQU8QehxAIfjyRu1mWGV07hoYmTYakkxdSGQ==", "license": "MIT", + "peer": true, "dependencies": { "@supabase/auth-js": "2.76.1", "@supabase/functions-js": "2.76.1", @@ -1251,6 +1252,7 @@ "integrity": "sha512-zN2yzBc2dIES2BSzLhNP2weYhwB77kgM/oAktICZVmmljyEmPZrlUwr14jjdK9/eDu7WdAuf6gTdYIJLTcN3Fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", @@ -1290,6 +1292,7 @@ "integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", @@ -1438,6 +1441,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -1656,6 +1660,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2066,6 +2071,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3078,6 +3084,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3126,6 +3133,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3259,6 +3267,7 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -3647,6 +3656,7 @@ "integrity": "sha512-iSry5jsBHispVczyt9UrBX/1qu3HQ/UyKPAIjqlvlu3o/eUvc+kpyMyRS2O4HLLx4MvLurLGIUOyyP11pyD59g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -3833,6 +3843,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3906,6 +3917,7 @@ "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -4058,21 +4070,6 @@ } } }, - "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/api/src/routes/artists/[slug]/+server.ts b/api/src/routes/artists/[slug]/+server.ts index e8aaf25..96953c5 100644 --- a/api/src/routes/artists/[slug]/+server.ts +++ b/api/src/routes/artists/[slug]/+server.ts @@ -2,6 +2,7 @@ import { TABLES } from '../../../../../shared/config'; import { handlePostgrestQuery, supabase } from '$lib/server/supabase'; import type { Artist } from '../../../../../shared/types/core'; import type { ArtistHydrated } from '../../../../../shared/types/hydrated'; +import { json } from '@sveltejs/kit'; export async function GET({ params }) { return handlePostgrestQuery( @@ -12,8 +13,9 @@ export async function GET({ params }) { 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), - { errorMessage: 'Failed to update artist details' } - ); + const { error } = await supabase.from(TABLES.artists).update(body).eq('id', params.slug); + if (error) { + return json({ error: 'Failed to update artist details' }, { status: 500 }); + } + return json({ success: true }); } diff --git a/api/src/routes/releases/[slug]/+server.ts b/api/src/routes/releases/[slug]/+server.ts index 3283249..bc71596 100644 --- a/api/src/routes/releases/[slug]/+server.ts +++ b/api/src/routes/releases/[slug]/+server.ts @@ -1,4 +1,5 @@ import { handlePostgrestQuery, supabase } from '$lib/server/supabase'; +import { json } from '@sveltejs/kit'; import { TABLES } from '../../../../../shared/config'; import type { ReleaseHydrated } from '../../../../../shared/types/hydrated'; @@ -8,3 +9,14 @@ export async function GET({ params }) { { errorMessage: 'Failed to fetch release' } ); } + +export async function DELETE({ params }) { + const { error } = await supabase.from(TABLES.releases).delete().eq('id', params.slug); + + if (error) { + console.error('Error deleting release:', error); + return json({ error: 'Failed to delete release' }, { status: 500 }); + } + + return json({ success: true }); +} diff --git a/api/src/routes/tracks/+server.ts b/api/src/routes/tracks/+server.ts index e46b656..c07f55c 100644 --- a/api/src/routes/tracks/+server.ts +++ b/api/src/routes/tracks/+server.ts @@ -2,7 +2,6 @@ import { json, type RequestHandler } from '@sveltejs/kit'; import { supabase } from '$lib/server/supabase'; import { pinata } from '$lib/server/pinata'; import { TABLES } from '../../../../shared/config'; -import { formFieldNames } from '../../../../shared/types/forms'; import { parseFile } from 'music-metadata'; import fs from 'fs/promises'; @@ -26,11 +25,11 @@ export const POST: RequestHandler = async ({ request }) => { try { const formData = await request.formData(); - const file = formData.get(formFieldNames.track.file) as File; - const artistId = formData.get(formFieldNames.track.artistID) as string; - const title = formData.get(formFieldNames.track.title) as string; - const artistName = formData.get(formFieldNames.track.artistName) as string; - const artistGroup = formData.get(formFieldNames.track.artistGroup) as string; + const file = formData.get('file') as File; + const artistId = formData.get('artistId') as string; + const title = formData.get('title') as string; + const artistName = formData.get('artistName') as string; + const artistGroup = formData.get('artistGroup') as string; if (!file || !artistId || !title) { return json({ error: 'Missing required fields' }, { status: 400 }); diff --git a/api/src/routes/tracks/[slug]/+server.ts b/api/src/routes/tracks/[slug]/+server.ts new file mode 100644 index 0000000..d77e004 --- /dev/null +++ b/api/src/routes/tracks/[slug]/+server.ts @@ -0,0 +1,14 @@ +import { supabase } from '$lib/server/supabase'; +import { json, type RequestHandler } from '@sveltejs/kit'; +import { TABLES } from '../../../../../shared/config'; + +export const DELETE: RequestHandler = async ({ params }) => { + const { error } = await supabase.from(TABLES.tracks).delete().eq('id', params.slug); + + if (error) { + console.error('Error deleting track:', error); + return json({ error: 'Failed to delete track' }, { status: 500 }); + } + + return json({ success: true }); +}; diff --git a/app/package-lock.json b/app/package-lock.json index cb64a51..842be1e 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -204,6 +204,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -250,6 +251,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -1434,6 +1436,7 @@ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.76.1.tgz", "integrity": "sha512-dYMh9EsTVXZ6WbQ0QmMGIhbXct5+x636tXXaaxUmwjj3kY1jyBTQU8QehxAIfjyRu1mWGV07hoYmTYakkxdSGQ==", "license": "MIT", + "peer": true, "dependencies": { "@supabase/auth-js": "2.76.1", "@supabase/functions-js": "2.76.1", @@ -1474,6 +1477,7 @@ "integrity": "sha512-zN2yzBc2dIES2BSzLhNP2weYhwB77kgM/oAktICZVmmljyEmPZrlUwr14jjdK9/eDu7WdAuf6gTdYIJLTcN3Fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", @@ -1513,6 +1517,7 @@ "integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", @@ -1779,6 +1784,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -2108,6 +2114,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2591,6 +2598,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3202,18 +3210,6 @@ "dev": true, "license": "ISC" }, - "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "bin": { - "jiti": "bin/jiti.js" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3687,6 +3683,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3714,6 +3711,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3847,6 +3845,7 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -4256,6 +4255,7 @@ "integrity": "sha512-iSry5jsBHispVczyt9UrBX/1qu3HQ/UyKPAIjqlvlu3o/eUvc+kpyMyRS2O4HLLx4MvLurLGIUOyyP11pyD59g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -4478,6 +4478,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4610,6 +4611,7 @@ "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -4705,6 +4707,7 @@ "integrity": "sha512-IUSop8jgaT7w0g1yOM/35qVtKjr/8Va4PrjzH1OUb0YH4c3OXB2lCZDkMAB6glA8T5w8S164oJGsbcmAecr4sA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.3", "@vitest/mocker": "4.0.3", @@ -4920,21 +4923,6 @@ "dev": true, "license": "MIT" }, - "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index ea5225f..1e35e30 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -9,17 +9,18 @@ "version": "0.0.1", "dependencies": { "@supabase/ssr": "^0.7.0", - "@supabase/supabase-js": "^2.74.0", - "pinata": "^2.5.1" + "@supabase/supabase-js": "^2.78.0", + "pinata": "^2.5.1", + "zod": "^4.1.12" }, "devDependencies": { "@sveltejs/adapter-netlify": "^5.2.4", - "@sveltejs/kit": "^2.44.0", + "@sveltejs/kit": "^2.48.4", "@sveltejs/vite-plugin-svelte": "^6.2.1", - "svelte": "^5.39.9", - "svelte-check": "^4.3.2", + "svelte": "^5.43.2", + "svelte-check": "^4.3.3", "typescript": "^5.9.3", - "vite": "^7.1.9" + "vite": "^7.1.12" } }, "node_modules/@esbuild/aix-ppc64": { @@ -813,21 +814,23 @@ "license": "MIT" }, "node_modules/@supabase/auth-js": { - "version": "2.74.0", - "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.74.0.tgz", - "integrity": "sha512-EJYDxYhBCOS40VJvfQ5zSjo8Ku7JbTICLTcmXt4xHMQZt4IumpRfHg11exXI9uZ6G7fhsQlNgbzDhFN4Ni9NnA==", + "version": "2.78.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.78.0.tgz", + "integrity": "sha512-cXDtu1U0LeZj/xfnFoV7yCze37TcbNo8FCxy1FpqhMbB9u9QxxDSW6pA5gm/07Ei7m260Lof4CZx67Cu6DPeig==", "license": "MIT", "dependencies": { - "@supabase/node-fetch": "2.6.15" + "@supabase/node-fetch": "2.6.15", + "tslib": "2.8.1" } }, "node_modules/@supabase/functions-js": { - "version": "2.74.0", - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.74.0.tgz", - "integrity": "sha512-VqWYa981t7xtIFVf7LRb9meklHckbH/tqwaML5P3LgvlaZHpoSPjMCNLcquuLYiJLxnh2rio7IxLh+VlvRvSWw==", + "version": "2.78.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.78.0.tgz", + "integrity": "sha512-t1jOvArBsOINyqaRee1xJ3gryXLvkBzqnKfi6q3YRzzhJbGS6eXz0pXR5fqmJeB01fLC+1njpf3YhMszdPEF7g==", "license": "MIT", "dependencies": { - "@supabase/node-fetch": "2.6.15" + "@supabase/node-fetch": "2.6.15", + "tslib": "2.8.1" } }, "node_modules/@supabase/node-fetch": { @@ -843,23 +846,25 @@ } }, "node_modules/@supabase/postgrest-js": { - "version": "2.74.0", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.74.0.tgz", - "integrity": "sha512-9Ypa2eS0Ib/YQClE+BhDSjx7OKjYEF6LAGjTB8X4HucdboGEwR0LZKctNfw6V0PPIAVjjzZxIlNBXGv0ypIkHw==", + "version": "2.78.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.78.0.tgz", + "integrity": "sha512-AwhpYlSvJ+PSnPmIK8sHj7NGDyDENYfQGKrMtpVIEzQA2ApUjgpUGxzXWN4Z0wEtLQsvv7g4y9HVad9Hzo1TNA==", "license": "MIT", "dependencies": { - "@supabase/node-fetch": "2.6.15" + "@supabase/node-fetch": "2.6.15", + "tslib": "2.8.1" } }, "node_modules/@supabase/realtime-js": { - "version": "2.74.0", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.74.0.tgz", - "integrity": "sha512-K5VqpA4/7RO1u1nyD5ICFKzWKu58bIDcPxHY0aFA7MyWkFd0pzi/XYXeoSsAifnD9p72gPIpgxVXCQZKJg1ktQ==", + "version": "2.78.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.78.0.tgz", + "integrity": "sha512-rCs1zmLe7of7hj4s7G9z8rTqzWuNVtmwDr3FiCRCJFawEoa+RQO1xpZGbdeuVvVmKDyVN6b542Okci+117y/LQ==", "license": "MIT", "dependencies": { "@supabase/node-fetch": "2.6.15", "@types/phoenix": "^1.6.6", "@types/ws": "^8.18.1", + "tslib": "2.8.1", "ws": "^8.18.2" } }, @@ -885,26 +890,28 @@ } }, "node_modules/@supabase/storage-js": { - "version": "2.74.0", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.74.0.tgz", - "integrity": "sha512-o0cTQdMqHh4ERDLtjUp1/KGPbQoNwKRxUh6f8+KQyjC5DSmiw/r+jgFe/WHh067aW+WU8nA9Ytw9ag7OhzxEkQ==", + "version": "2.78.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.78.0.tgz", + "integrity": "sha512-n17P0JbjHOlxqJpkaGFOn97i3EusEKPEbWOpuk1r4t00Wg06B8Z4GUiq0O0n1vUpjiMgJUkLIMuBVp+bEgunzQ==", "license": "MIT", "dependencies": { - "@supabase/node-fetch": "2.6.15" + "@supabase/node-fetch": "2.6.15", + "tslib": "2.8.1" } }, "node_modules/@supabase/supabase-js": { - "version": "2.74.0", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.74.0.tgz", - "integrity": "sha512-IEMM/V6gKdP+N/X31KDIczVzghDpiPWFGLNjS8Rus71KvV6y6ueLrrE/JGCHDrU+9pq5copF3iCa0YQh+9Lq9Q==", + "version": "2.78.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.78.0.tgz", + "integrity": "sha512-xYMRNBFmKp2m1gMuwcp/gr/HlfZKqjye1Ib8kJe29XJNsgwsfO/f8skxnWiscFKTlkOKLuBexNgl5L8dzGt6vA==", "license": "MIT", + "peer": true, "dependencies": { - "@supabase/auth-js": "2.74.0", - "@supabase/functions-js": "2.74.0", + "@supabase/auth-js": "2.78.0", + "@supabase/functions-js": "2.78.0", "@supabase/node-fetch": "2.6.15", - "@supabase/postgrest-js": "2.74.0", - "@supabase/realtime-js": "2.74.0", - "@supabase/storage-js": "2.74.0" + "@supabase/postgrest-js": "2.78.0", + "@supabase/realtime-js": "2.78.0", + "@supabase/storage-js": "2.78.0" } }, "node_modules/@sveltejs/acorn-typescript": { @@ -933,11 +940,12 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.44.0", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.44.0.tgz", - "integrity": "sha512-xU5qP7PiYmrSH70Whm/I+nf0j4xBnHyRQNkC1SEfaBOwCCkkeuL6WNxSb8q4Ib7+Z+sZ4JUTDYHfoyVm02EXVQ==", + "version": "2.48.4", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.48.4.tgz", + "integrity": "sha512-TGFX1pZUt9qqY20Cv5NyYvy0iLWHf2jXi8s+eCGsig7jQMdwZWKUFMR6TbvFNhfDSUpc1sH/Y5EHv20g3HHA3g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", @@ -977,6 +985,7 @@ "integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", @@ -1054,6 +1063,7 @@ "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1339,6 +1349,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -1497,11 +1508,12 @@ } }, "node_modules/svelte": { - "version": "5.39.9", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.39.9.tgz", - "integrity": "sha512-sVOie0sbU9F/Lh0IoUfaq9hLzujRKxiL7xTMbG0y8ROx/qErtbfmm6sLSlJUbUMW4NcIgqHQPFiHX4LakA8fzA==", + "version": "5.43.2", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.2.tgz", + "integrity": "sha512-ro1umEzX8rT5JpCmlf0PPv7ncD8MdVob9e18bhwqTKNoLjS8kDvhVpaoYVPc+qMwDAOfcwJtyY7ZFSDbOaNPgA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1523,9 +1535,9 @@ } }, "node_modules/svelte-check": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.2.tgz", - "integrity": "sha512-71udP5w2kaSTcX8iV0hn3o2FWlabQHhJTJLIQrCqMsrcOeDUO2VhCQKKCA8AMVHSPwdxLEWkUWh9OKxns5PD9w==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.3.tgz", + "integrity": "sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg==", "dev": true, "license": "MIT", "dependencies": { @@ -1579,12 +1591,19 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -1600,11 +1619,12 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.1.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz", - "integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==", + "version": "7.1.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", + "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -1737,6 +1757,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/dashboard/package.json b/dashboard/package.json index 3653dc9..67339e1 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -13,16 +13,17 @@ }, "devDependencies": { "@sveltejs/adapter-netlify": "^5.2.4", - "@sveltejs/kit": "^2.44.0", + "@sveltejs/kit": "^2.48.4", "@sveltejs/vite-plugin-svelte": "^6.2.1", - "svelte": "^5.39.9", - "svelte-check": "^4.3.2", + "svelte": "^5.43.2", + "svelte-check": "^4.3.3", "typescript": "^5.9.3", - "vite": "^7.1.9" + "vite": "^7.1.12" }, "dependencies": { "@supabase/ssr": "^0.7.0", - "@supabase/supabase-js": "^2.74.0", - "pinata": "^2.5.1" + "@supabase/supabase-js": "^2.78.0", + "pinata": "^2.5.1", + "zod": "^4.1.12" } } \ No newline at end of file diff --git a/dashboard/src/lib/components/Card.svelte b/dashboard/src/lib/components/Card.svelte index cc117d0..dff2fd6 100644 --- a/dashboard/src/lib/components/Card.svelte +++ b/dashboard/src/lib/components/Card.svelte @@ -22,7 +22,7 @@ background-color: var(--card-background); line-height: 1.1; padding: 0.5rem; - margin: 0.5rem 0; + margin: 0 0 1rem 0; border-radius: 5px; box-shadow: var(--box-shadow); } diff --git a/dashboard/src/lib/components/ProfileSummary.svelte b/dashboard/src/lib/components/ProfileSummary.svelte index 8accaf4..e89e01a 100644 --- a/dashboard/src/lib/components/ProfileSummary.svelte +++ b/dashboard/src/lib/components/ProfileSummary.svelte @@ -2,68 +2,21 @@ import { enhance } from "$app/forms"; import { makeImageLink } from "$lib/utils"; import type { Artist } from "../../../../shared/types/core"; + import ArtistProfileForm from "./forms/ArtistProfileForm.svelte"; let { activeArtist }: { activeArtist: Artist } = $props(); - let uploading = $state(false); - - function handleUpload() { - uploading = true; - return async ({ update }: { update: () => Promise }) => { - await update(); - uploading = false; - }; - }

Profile

{activeArtist.name}

-
- - - {#if activeArtist.image_ipfs_cid} - {activeArtist.name} - {:else} -
No profile image
- {/if} - - - - - - - - -
+
diff --git a/dashboard/src/lib/components/ReleaseInfo.svelte b/dashboard/src/lib/components/ReleaseInfo.svelte index 9001243..9cc978b 100644 --- a/dashboard/src/lib/components/ReleaseInfo.svelte +++ b/dashboard/src/lib/components/ReleaseInfo.svelte @@ -1,7 +1,10 @@ + +

Add Release

+ +
+ + + + + + + +
diff --git a/dashboard/src/lib/components/forms/AddTrackToReleaseForm.svelte b/dashboard/src/lib/components/forms/AddTrackToReleaseForm.svelte new file mode 100644 index 0000000..7175f50 --- /dev/null +++ b/dashboard/src/lib/components/forms/AddTrackToReleaseForm.svelte @@ -0,0 +1,38 @@ + + +

Add Track to Release

+ +
+ + + + +
diff --git a/dashboard/src/lib/components/forms/ArtistProfileForm.svelte b/dashboard/src/lib/components/forms/ArtistProfileForm.svelte new file mode 100644 index 0000000..9bb8929 --- /dev/null +++ b/dashboard/src/lib/components/forms/ArtistProfileForm.svelte @@ -0,0 +1,64 @@ + + +
+ + + {#if currentArtistImageCID} +
+ Current Artist Profile +
+ {/if} + + + {#each updateArtistDetails.fields.artistBio.issues() as issue} +
{issue.message}
+ {/each} + + {#each updateArtistDetails.fields.artistWebsite.issues() as issue} +
{issue.message}
+ {/each} + +
diff --git a/dashboard/src/lib/components/forms/UploadTrackForm.svelte b/dashboard/src/lib/components/forms/UploadTrackForm.svelte new file mode 100644 index 0000000..6a085a7 --- /dev/null +++ b/dashboard/src/lib/components/forms/UploadTrackForm.svelte @@ -0,0 +1,26 @@ + + +

Upload Track

+ +
+ + + + + + +
diff --git a/dashboard/src/lib/components/icons/BinIcon.svelte b/dashboard/src/lib/components/icons/BinIcon.svelte new file mode 100644 index 0000000..7229ec0 --- /dev/null +++ b/dashboard/src/lib/components/icons/BinIcon.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/dashboard/src/lib/components/layout/ButtonWrapper.svelte b/dashboard/src/lib/components/layout/ButtonWrapper.svelte new file mode 100644 index 0000000..e55791a --- /dev/null +++ b/dashboard/src/lib/components/layout/ButtonWrapper.svelte @@ -0,0 +1,28 @@ + + + + + diff --git a/dashboard/src/lib/config.ts b/dashboard/src/lib/config.ts index 46df17e..a3de984 100644 --- a/dashboard/src/lib/config.ts +++ b/dashboard/src/lib/config.ts @@ -1,5 +1,10 @@ import { dev } from "$app/environment"; -import { API_DOMAIN, API_LOCAL_PORT } from "../../../shared/config"; +import { + API_DOMAIN, + API_LOCAL_PORT, + DASHBOARD_DOMAIN, + DASHBOARD_LOCAL_PORT, +} from "../../../shared/config"; export const PUBLIC_SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL; export const PUBLIC_SUPABASE_ANON_KEY = import.meta.env.VITE_SUPABASE_ANON_KEY; @@ -9,3 +14,12 @@ export const PINATA_ARTIST_IMAGES_GROUP = import.meta.env export const POWER_USER_ID = import.meta.env.VITE_POWER_USER_ID; export const API_BASE = dev ? `http://localhost:${API_LOCAL_PORT}` : API_DOMAIN; + +export const DOMAIN_BASE = dev + ? `http://localhost:${DASHBOARD_LOCAL_PORT}` + : DASHBOARD_DOMAIN; + +export const REQUEST_HEADER_BOILERPLATE = { + "Content-Type": "application/json", + origin: DOMAIN_BASE, +}; diff --git a/dashboard/src/lib/remote-functions/artist.remote.ts b/dashboard/src/lib/remote-functions/artist.remote.ts new file mode 100644 index 0000000..d6fbfb0 --- /dev/null +++ b/dashboard/src/lib/remote-functions/artist.remote.ts @@ -0,0 +1,57 @@ +import { form, getRequestEvent, query } from "$app/server"; +import { + API_BASE, + PINATA_ARTIST_IMAGES_GROUP, + REQUEST_HEADER_BOILERPLATE, +} from "$lib/config"; +import * as z from "zod"; +import { pinata } from "$lib/server/pinata"; +import { fail, redirect } from "@sveltejs/kit"; + +const ArtistDetailsForm = z.object({ + artistId: z.string(), + artistName: z.string().min(1), + artistImageNew: z.instanceof(File).optional(), + artistBio: z.string().max(1000).optional(), + artistWebsite: z.string().optional(), +}); + +export const updateArtistDetails = form(ArtistDetailsForm, async (data) => { + let imageCid: string | undefined; + if (data.artistImageNew && data.artistImageNew.size > 0) { + const pinataFileName = `${data.artistName} profile image`; + const upload = await pinata.upload.public + .file(data.artistImageNew) + .name(pinataFileName) + .group(PINATA_ARTIST_IMAGES_GROUP); + + if (!upload || !upload.cid) { + console.error("Error uploading artist image to Pinata:", upload); + return fail(500, { + error: true, + message: "Failed to upload artist image to Pinata", + }); + } + // TODO: Delete old artist image if it exists + imageCid = upload.cid; + } + await fetch(`${API_BASE}/artists/${data.artistId}`, { + method: "PATCH", + headers: REQUEST_HEADER_BOILERPLATE, + body: JSON.stringify({ + id: data.artistId, + image_ipfs_cid: imageCid, + bio: data.artistBio, + website_url: data.artistWebsite, + }), + }); +}); + +export const logOut = query(async () => { + const { locals } = getRequestEvent(); + const { session } = await locals.safeGetSession(); + if (session) { + await locals.supabase.auth.signOut(); + redirect(303, "/login"); + } +}); diff --git a/dashboard/src/lib/remote-functions/auth.ts b/dashboard/src/lib/remote-functions/auth.ts new file mode 100644 index 0000000..7120c06 --- /dev/null +++ b/dashboard/src/lib/remote-functions/auth.ts @@ -0,0 +1,12 @@ +import { getRequestEvent } from "$app/server"; +import type { User } from "@supabase/supabase-js"; + +export const requireAuth = () => { + const { locals } = getRequestEvent(); + + if (!locals.user) { + throw new Error("User is not authenticated"); + } + + return locals.user as User; +}; diff --git a/dashboard/src/lib/remote-functions/music.remote.ts b/dashboard/src/lib/remote-functions/music.remote.ts new file mode 100644 index 0000000..2f9bd09 --- /dev/null +++ b/dashboard/src/lib/remote-functions/music.remote.ts @@ -0,0 +1,84 @@ +import { form, query } from "$app/server"; +import { API_BASE, DOMAIN_BASE, REQUEST_HEADER_BOILERPLATE } from "$lib/config"; +import * as z from "zod"; + +const AddReleaseForm = z.object({ + artistId: z.string(), + releaseArtwork: z.instanceof(File), + releaseName: z.string().min(1), + releaseType: z.enum(["album", "ep", "single"]), + releaseDate: z.string(), + releaseGenres: z.array(z.string()).optional(), +}); + +const UploadTrackForm = z.object({ + file: z.instanceof(File), + artistId: z.string(), + title: z.string().min(1), + artistName: z.string().min(1), + artistGroup: z.string().optional(), +}); + +const AddTrackToReleaseForm = z.object({ + releaseId: z.string(), + trackId: z.string(), + trackNumber: z.number(), +}); + +export const addRelease = form(AddReleaseForm, async (data) => { + const formData = new FormData(); + formData.append("artistId", data.artistId); + formData.append("releaseArtwork", data.releaseArtwork); + formData.append("releaseName", data.releaseName); + formData.append("releaseType", data.releaseType); + if (data.releaseDate) { + formData.append("releaseDate", data.releaseDate); + } + await fetch(`${API_BASE}/releases`, { + method: "POST", + headers: { + origin: DOMAIN_BASE, + }, + body: formData, + }); +}); + +export const uploadTrack = form(UploadTrackForm, async (data) => { + const formData = new FormData(); + formData.append("file", data.file); + formData.append("artistId", data.artistId); + formData.append("title", data.title); + formData.append("artistName", data.artistName); + if (data.artistGroup) { + formData.append("artistGroup", data.artistGroup); + } + await fetch(`${API_BASE}/tracks`, { + method: "POST", + headers: { + origin: DOMAIN_BASE, + }, + body: formData, + }); +}); + +export const addTrackToRelease = form(AddTrackToReleaseForm, async (data) => { + await fetch(`${API_BASE}/tracks`, { + method: "PATCH", + headers: REQUEST_HEADER_BOILERPLATE, + body: JSON.stringify(data), + }); +}); + +export const deleteTrack = query(z.string(), async (trackId) => { + await fetch(`${API_BASE}/tracks/${trackId}`, { + method: "DELETE", + headers: REQUEST_HEADER_BOILERPLATE, + }); +}); + +export const deleteRelease = query(z.string(), async (releaseId) => { + await fetch(`${API_BASE}/releases/${releaseId}`, { + method: "DELETE", + headers: REQUEST_HEADER_BOILERPLATE, + }); +}); diff --git a/dashboard/src/lib/utils/index.ts b/dashboard/src/lib/utils/index.ts index 5d3153b..4f8465f 100644 --- a/dashboard/src/lib/utils/index.ts +++ b/dashboard/src/lib/utils/index.ts @@ -3,3 +3,26 @@ import { PUBLIC_GATEWAY_URL } from "$env/static/public"; export const makeImageLink = (cid: string, width: number) => { return `https://${PUBLIC_GATEWAY_URL}/ipfs/${cid}?img-width=${width}`; }; + +export const genres = [ + "Pop", + "Rock", + "Hip Hop", + "Jazz", + "Classical", + "Electronic", + "Country", + "Reggae", + "Blues", + "Folk", + "Metal", + "R&B", + "Soul", + "Punk", + "Disco", + "Funk", + "Gospel", + "Ska", + "Ambient", + "Indie", +]; diff --git a/dashboard/src/routes/+layout.svelte b/dashboard/src/routes/+layout.svelte index 3b6d1bd..cf119e7 100644 --- a/dashboard/src/routes/+layout.svelte +++ b/dashboard/src/routes/+layout.svelte @@ -6,6 +6,7 @@ import { onMount } from "svelte"; import { type SubmitFunction } from "@sveltejs/kit"; import { enhance } from "$app/forms"; + import { logOut } from "$lib/remote-functions/artist.remote"; let { children, data } = $props(); @@ -91,11 +92,7 @@ {/each} {/if}
-
-
- -
-
+ {/if} @@ -105,6 +102,22 @@ diff --git a/dashboard/src/lib/components/ReleaseInfo.svelte b/dashboard/src/lib/components/views/music/ReleaseInfo.svelte similarity index 80% rename from dashboard/src/lib/components/ReleaseInfo.svelte rename to dashboard/src/lib/components/views/music/ReleaseInfo.svelte index 9cc978b..c14faf0 100644 --- a/dashboard/src/lib/components/ReleaseInfo.svelte +++ b/dashboard/src/lib/components/views/music/ReleaseInfo.svelte @@ -1,10 +1,10 @@ + +
+
+
{song.title}
+
+ {prettifyDuration(song.duration_seconds)} +
+
+ deleteTrack(song.id)}> + + +
+ + diff --git a/dashboard/src/lib/components/ProfileSummary.svelte b/dashboard/src/lib/components/views/profile/ProfileView.svelte similarity index 71% rename from dashboard/src/lib/components/ProfileSummary.svelte rename to dashboard/src/lib/components/views/profile/ProfileView.svelte index e89e01a..78e4696 100644 --- a/dashboard/src/lib/components/ProfileSummary.svelte +++ b/dashboard/src/lib/components/views/profile/ProfileView.svelte @@ -1,8 +1,6 @@ diff --git a/dashboard/src/lib/components/views/stats/StatsView.svelte b/dashboard/src/lib/components/views/stats/StatsView.svelte new file mode 100644 index 0000000..0ca0789 --- /dev/null +++ b/dashboard/src/lib/components/views/stats/StatsView.svelte @@ -0,0 +1,33 @@ + + +
+

Stats

+
Total streams: {streams.length}
+
+ Average payout per stream: + {calculateAveragePayout(streams).toFixed(2)}p +
+
+ Total earnings: + {calculateTotalEarnings(streams, "readable-string")} +
+
diff --git a/dashboard/src/routes/+layout.server.ts b/dashboard/src/routes/+layout.server.ts index 316e82f..73ca378 100644 --- a/dashboard/src/routes/+layout.server.ts +++ b/dashboard/src/routes/+layout.server.ts @@ -1,13 +1,12 @@ import { fail } from "@sveltejs/kit"; import { supabase } from "$lib/server/supabase"; -import type { - StreamLog, -} from "../../../shared/types/core"; +import type { StreamLog } from "../../../shared/types/core"; import { sortReleasesByDate } from "../../../shared/utils"; import type { LayoutServerLoad } from "./$types"; import { POWER_USER_ID } from "$lib/config"; -import type { Artist, Release, Track } from "../../../shared/types/core"; +import type { Artist, Track } from "../../../shared/types/core"; import type { ReleaseHydrated } from "../../../shared/types/hydrated"; +import { TABLES } from "../../../shared/config"; export const load: LayoutServerLoad = async ({ locals: { safeGetSession }, @@ -15,33 +14,19 @@ export const load: LayoutServerLoad = async ({ }) => { const { session, user } = await safeGetSession(); - if (!session) { + if (!session || !user) { return { session, user, cookies: cookies.getAll(), artists: [], - releasesRaw: [], - releasesHydrated: [], + releases: [], songs: [], streams: [], }; } - const userID = session?.user.id; - - if (!userID) { - return { - session, - user, - cookies: cookies.getAll(), - artists: [], - releasesRaw: [], - releasesHydrated: [], - songs: [], - streams: [], - }; - } + const userID = session.user.id; const { data: userData, @@ -50,7 +35,7 @@ export const load: LayoutServerLoad = async ({ data: { artist_id: string }[] | null; error: Error | null; } = await supabase - .from("artist_members") + .from(TABLES.artistMembers) .select("artist_id") .eq("user_id", userID); @@ -69,9 +54,9 @@ export const load: LayoutServerLoad = async ({ error: Error | null; } = userID === POWER_USER_ID - ? await supabase.from("artists").select("*") + ? await supabase.from(TABLES.artists).select("*") : await supabase - .from("artists") + .from(TABLES.artists) .select("*") .in( "id", @@ -89,27 +74,15 @@ export const load: LayoutServerLoad = async ({ }: { data: Track[] | null; error: Error | null; - } = await supabase.from("tracks").select("*"); + } = await supabase.from(TABLES.tracks).select("*"); const { - data: releasesRaw, + data: releases, error: releasesError, - }: { - data: Release[] | null; - error: Error | null; - } = await supabase.from("releases").select("*"); - if (releasesError || !releasesRaw) { - console.error("Error fetching releases:", releasesError); - return fail(500, { error: "Failed to fetch releases" }); - } - - const { - data: releasesHydrated, - error: releasesHydratedError, }: { data: ReleaseHydrated[] | null; // Adjust type as needed error: Error | null; - } = await supabase.from("hydrated_releases").select("*"); + } = await supabase.from(TABLES.releasesRich).select("*"); if (artistsError || !connectedArtists) { console.error("Error fetching artists:", artistsError); @@ -119,8 +92,8 @@ export const load: LayoutServerLoad = async ({ console.error("Error fetching songs:", songsError); return fail(500, { error: "Failed to fetch songs" }); } - if (releasesHydratedError || !releasesHydrated) { - console.error("Error fetching releases:", releasesHydratedError); + if (releasesError || !releases) { + console.error("Error fetching releases:", releasesError); return fail(500, { error: "Failed to fetch releases" }); } @@ -130,12 +103,6 @@ export const load: LayoutServerLoad = async ({ return 0; }); - releasesRaw.sort((a, b) => { - if (a.title < b.title) return -1; - if (a.title > b.title) return 1; - return 0; - }); - connectedArtists.sort((a, b) => { if (a.name < b.name) return -1; if (a.name > b.name) return 1; @@ -147,9 +114,9 @@ export const load: LayoutServerLoad = async ({ data: streams, error: streamsError, }: { - data: StreamLog[] | null; // Adjust type as needed + data: StreamLog[] | null; error: Error | null; - } = await supabase.from("streams").select("*"); + } = await supabase.from(TABLES.streams).select("*"); if (streamsError || !streams) { console.error("Error fetching streams:", streamsError); return fail(500, { error: "Failed to fetch streams" }); @@ -160,8 +127,7 @@ export const load: LayoutServerLoad = async ({ user, cookies: cookies.getAll(), artists: connectedArtists, - releasesRaw, - releasesHydrated: sortReleasesByDate(releasesHydrated), + releases: sortReleasesByDate(releases), songs, streams, }; diff --git a/dashboard/src/routes/+layout.svelte b/dashboard/src/routes/+layout.svelte index cf119e7..8a64e66 100644 --- a/dashboard/src/routes/+layout.svelte +++ b/dashboard/src/routes/+layout.svelte @@ -4,26 +4,16 @@ import { dashboardState, type DashboardSectionId } from "$lib/state.svelte"; import { invalidate } from "$app/navigation"; import { onMount } from "svelte"; - import { type SubmitFunction } from "@sveltejs/kit"; - import { enhance } from "$app/forms"; import { logOut } from "$lib/remote-functions/artist.remote"; let { children, data } = $props(); let { supabase, session } = $state(data); - let loading = $state(false); - const artists = $derived(data.artists); - const handleSignOut: SubmitFunction = () => { - loading = true; - return async ({ update }) => { - loading = false; - update(); - }; - }; + const artists = $derived(data.artists); onMount(() => { - const { data } = supabase.auth.onAuthStateChange((event, newSession) => { + const { data } = supabase.auth.onAuthStateChange((_, newSession) => { if (newSession?.expires_at !== session?.expires_at) { invalidate("supabase:auth"); } diff --git a/dashboard/src/routes/+layout.ts b/dashboard/src/routes/+layout.ts index 429bc4d..2e934fb 100644 --- a/dashboard/src/routes/+layout.ts +++ b/dashboard/src/routes/+layout.ts @@ -34,8 +34,7 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => { session: data.session ?? null, user: data.user, artists: data.artists, - releasesRaw: data.releasesRaw, - releasesHydrated: data.releasesHydrated, + releases: data.releases, songs: data.songs, streams: data.streams, }; diff --git a/dashboard/src/routes/+page.svelte b/dashboard/src/routes/+page.svelte index 68042d7..9157191 100644 --- a/dashboard/src/routes/+page.svelte +++ b/dashboard/src/routes/+page.svelte @@ -2,18 +2,11 @@ import type { GroupListResponse } from "pinata"; import type { StreamLog } from "../../../shared/types/core"; import { dashboardState } from "$lib/state.svelte"; - import Card from "$lib/components/Card.svelte"; - import ReleaseInfo from "$lib/components/ReleaseInfo.svelte"; - import ProfileSummary from "$lib/components/ProfileSummary.svelte"; - import { prettifyDuration } from "../../../shared/utils"; + import ProfileView from "$lib/components/views/profile/ProfileView.svelte"; import type { Artist, Release, Track } from "../../../shared/types/core"; import type { ReleaseHydrated } from "../../../shared/types/hydrated"; - import UploadTrackForm from "$lib/components/forms/UploadTrackForm.svelte"; - import AddReleaseForm from "$lib/components/forms/AddReleaseForm.svelte"; - import AddTrackToReleaseForm from "$lib/components/forms/AddTrackToReleaseForm.svelte"; - import BinIcon from "$lib/components/icons/BinIcon.svelte"; - import { deleteTrack } from "$lib/remote-functions/music.remote"; - import ButtonWrapper from "$lib/components/layout/ButtonWrapper.svelte"; + import StatsView from "$lib/components/views/stats/StatsView.svelte"; + import MusicView from "$lib/components/views/music/MusicView.svelte"; let { data, @@ -22,42 +15,25 @@ groups: GroupListResponse; artists: Artist[]; songs: Track[]; - releasesRaw: Release[]; - releasesHydrated: ReleaseHydrated[]; + releases: ReleaseHydrated[]; streams: StreamLog[]; }; } = $props(); - let activeArtist: Artist | null = $derived(dashboardState.activeArtist); + let activeArtist: Artist | null = $derived( + data.artists.find( + (artist) => artist.id === dashboardState.activeArtist?.id + ) || null + ); let activeArtistSongs = $derived( data.songs.filter((song) => song.artist_id === activeArtist?.id) ); - let activeArtistReleasesRaw = $derived( - data.releasesRaw.filter((release) => release.artist_id === activeArtist?.id) - ); - let activeArtistReleasesHydrated = $derived( - data.releasesHydrated.filter( - (release) => release.artist_id === activeArtist?.id - ) + let activeArtistReleases = $derived( + data.releases.filter((release) => release.artist_id === activeArtist?.id) ); let activeArtistStreams = $derived( data.streams.filter((stream) => stream.artist_id === activeArtist?.id) ); - - const calculateAveragePayout = (streams: StreamLog[]) => { - const total = streams.reduce((acc, stream) => acc + stream.tokens_used, 0); - return total / streams.length || 0; - }; - - const calculateTotalEarnings = ( - streams: StreamLog[], - outputFormat: "readable-string" | "raw" - ) => { - const total = streams.reduce((acc, stream) => acc + stream.tokens_used, 0); - return outputFormat === "readable-string" - ? `£${(total / 100).toFixed(2)}` - : total / 100; - }; @@ -68,128 +44,13 @@
{#if activeArtist} {#if dashboardState.activeSection === "music"} -
-
-

- Releases ({activeArtistReleasesHydrated.length}) -

- {#each activeArtistReleasesHydrated as release} - - - - {/each} -
-
-

- Songs ({activeArtistSongs.length}) -

- {#each activeArtistSongs as song} - -
-
-
{song.title}
-
- {prettifyDuration(song.duration_seconds)} -
-
- deleteTrack(song.id)}> - - -
-
- {/each} -
-
-

Manage

-
- - - - - - - - - -
-
-
+ {:else if dashboardState.activeSection === "profile"} - + {:else if dashboardState.activeSection === "stats"} -
-

Stats

-
Total streams: {activeArtistStreams.length}
-
- Average payout per stream: - {calculateAveragePayout(activeArtistStreams).toFixed(2)}p -
-
- Total earnings: - {calculateTotalEarnings(activeArtistStreams, "readable-string")} -
-
- + {/if} {:else}
Select an artist to manage their releases and songs.
{/if}
- - From 7891982a57f962b7c74c7e1923d5aa000e1c4b41 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 4 Nov 2025 02:06:47 +0000 Subject: [PATCH 3/4] Popup menus for tracks and releases Making things a bit less easy to delete feels like a good idea, as does not having bins everywhere --- .../lib/components/icons/ThreeDotsIcon.svelte | 12 +++++++ .../lib/components/layout/PopupWrapper.svelte | 32 +++++++++++++++++++ .../components/views/music/ReleaseInfo.svelte | 22 +++++++++++-- .../components/views/music/TrackInfo.svelte | 22 +++++++++++-- dashboard/src/routes/+layout.svelte | 2 +- 5 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 dashboard/src/lib/components/icons/ThreeDotsIcon.svelte create mode 100644 dashboard/src/lib/components/layout/PopupWrapper.svelte diff --git a/dashboard/src/lib/components/icons/ThreeDotsIcon.svelte b/dashboard/src/lib/components/icons/ThreeDotsIcon.svelte new file mode 100644 index 0000000..9790da1 --- /dev/null +++ b/dashboard/src/lib/components/icons/ThreeDotsIcon.svelte @@ -0,0 +1,12 @@ + + + diff --git a/dashboard/src/lib/components/layout/PopupWrapper.svelte b/dashboard/src/lib/components/layout/PopupWrapper.svelte new file mode 100644 index 0000000..cd56136 --- /dev/null +++ b/dashboard/src/lib/components/layout/PopupWrapper.svelte @@ -0,0 +1,32 @@ + + + + + diff --git a/dashboard/src/lib/components/views/music/ReleaseInfo.svelte b/dashboard/src/lib/components/views/music/ReleaseInfo.svelte index c14faf0..0e09c6c 100644 --- a/dashboard/src/lib/components/views/music/ReleaseInfo.svelte +++ b/dashboard/src/lib/components/views/music/ReleaseInfo.svelte @@ -1,4 +1,6 @@
@@ -35,11 +39,20 @@ {/each}
- deleteRelease(release.id)}> - + (popupMenuOpen = !popupMenuOpen)}> + +{#if popupMenuOpen} + +
{release.title}
+ deleteRelease(release.id)}> +
Delete Release
+
+
+{/if} + diff --git a/dashboard/src/lib/components/views/music/TrackInfo.svelte b/dashboard/src/lib/components/views/music/TrackInfo.svelte index 6e7ab16..0d76467 100644 --- a/dashboard/src/lib/components/views/music/TrackInfo.svelte +++ b/dashboard/src/lib/components/views/music/TrackInfo.svelte @@ -1,4 +1,6 @@
@@ -15,11 +19,20 @@ {prettifyDuration(song.duration_seconds)}
- deleteTrack(song.id)}> - + (popupMenuOpen = !popupMenuOpen)}> + +{#if popupMenuOpen} + +
{song.title}
+ deleteTrack(song.id)}> +
Delete Track
+
+
+{/if} + diff --git a/dashboard/src/routes/+layout.svelte b/dashboard/src/routes/+layout.svelte index 8a64e66..0c9a167 100644 --- a/dashboard/src/routes/+layout.svelte +++ b/dashboard/src/routes/+layout.svelte @@ -8,7 +8,7 @@ let { children, data } = $props(); - let { supabase, session } = $state(data); + let { supabase, session } = $derived(data); const artists = $derived(data.artists); From dcabd0f8e2e7e9169918845cd267db18e5e7833c Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 4 Nov 2025 12:32:16 +0000 Subject: [PATCH 4/4] Force refresh when logging out No doubt there's a nicer way to update things but I haven't been able to work it out --- .../src/lib/remote-functions/artist.remote.ts | 6 ++---- dashboard/src/routes/+layout.svelte | 20 +++++++++++++------ dashboard/src/routes/login/+page.svelte | 2 +- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/dashboard/src/lib/remote-functions/artist.remote.ts b/dashboard/src/lib/remote-functions/artist.remote.ts index d6fbfb0..a6815fb 100644 --- a/dashboard/src/lib/remote-functions/artist.remote.ts +++ b/dashboard/src/lib/remote-functions/artist.remote.ts @@ -47,11 +47,9 @@ export const updateArtistDetails = form(ArtistDetailsForm, async (data) => { }); }); -export const logOut = query(async () => { +export const signOut = query(async () => { const { locals } = getRequestEvent(); - const { session } = await locals.safeGetSession(); - if (session) { + if (locals.session) { await locals.supabase.auth.signOut(); - redirect(303, "/login"); } }); diff --git a/dashboard/src/routes/+layout.svelte b/dashboard/src/routes/+layout.svelte index 0c9a167..ffe300f 100644 --- a/dashboard/src/routes/+layout.svelte +++ b/dashboard/src/routes/+layout.svelte @@ -4,13 +4,11 @@ import { dashboardState, type DashboardSectionId } from "$lib/state.svelte"; import { invalidate } from "$app/navigation"; import { onMount } from "svelte"; - import { logOut } from "$lib/remote-functions/artist.remote"; + import { signOut } from "$lib/remote-functions/artist.remote"; let { children, data } = $props(); - let { supabase, session } = $derived(data); - - const artists = $derived(data.artists); + let { supabase, session, artists } = $derived(data); onMount(() => { const { data } = supabase.auth.onAuthStateChange((_, newSession) => { @@ -29,8 +27,12 @@ ]; + + + +
- {#if data.session} + {#if session}

Soli • Dashboard


@@ -82,7 +84,13 @@ {/each} {/if}
- +
{/if} diff --git a/dashboard/src/routes/login/+page.svelte b/dashboard/src/routes/login/+page.svelte index f038f63..ebeaf33 100644 --- a/dashboard/src/routes/login/+page.svelte +++ b/dashboard/src/routes/login/+page.svelte @@ -21,7 +21,7 @@ - Login • Soli + Login • Soli Dashboard