From fb267b7ebd9fa7ab1cd625f0a379369de2860d33 Mon Sep 17 00:00:00 2001 From: Zeeshan Tariq Date: Tue, 21 Apr 2026 23:46:52 +0500 Subject: [PATCH 1/2] move sonora to lbry apps --- src/alex_frontend/core/config/apps.ts | 2 +- .../core/layouts/SonoraLayout.tsx | 78 ++++ .../lbry/public/logos/Sonora.svg | 37 ++ src/alex_frontend/lbry/routeTree.gen.ts | 241 +++++++++++++ .../lbry/src/pages/sonora/ArchivePage.tsx | 207 +++++++++++ .../lbry/src/pages/sonora/BrowsePage.tsx | 213 +++++++++++ .../lbry/src/pages/sonora/MarketPage.tsx | 208 +++++++++++ .../lbry/src/pages/sonora/RecordPage.tsx | 334 ++++++++++++++++++ .../lbry/src/pages/sonora/StudioPage.tsx | 229 ++++++++++++ .../lbry/src/pages/sonora/UploadPage.tsx | 258 ++++++++++++++ .../app/sonora/archive.$principal.lazy.tsx | 6 + .../_auth/app/sonora/archive.$principal.tsx | 5 + .../routes/_auth/app/sonora/archive.lazy.tsx | 6 + .../src/routes/_auth/app/sonora/archive.tsx | 5 + .../routes/_auth/app/sonora/index.lazy.tsx | 6 + .../src/routes/_auth/app/sonora/index.tsx | 5 + .../routes/_auth/app/sonora/market.lazy.tsx | 6 + .../src/routes/_auth/app/sonora/market.tsx | 5 + .../routes/_auth/app/sonora/record.lazy.tsx | 6 + .../src/routes/_auth/app/sonora/record.tsx | 5 + .../routes/_auth/app/sonora/route.lazy.tsx | 6 + .../app/sonora/studio.$principal.lazy.tsx | 6 + .../_auth/app/sonora/studio.$principal.tsx | 5 + .../routes/_auth/app/sonora/studio.lazy.tsx | 6 + .../src/routes/_auth/app/sonora/studio.tsx | 5 + .../routes/_auth/app/sonora/upload.lazy.tsx | 6 + .../src/routes/_auth/app/sonora/upload.tsx | 5 + 27 files changed, 1900 insertions(+), 1 deletion(-) create mode 100644 src/alex_frontend/core/layouts/SonoraLayout.tsx create mode 100644 src/alex_frontend/lbry/public/logos/Sonora.svg create mode 100644 src/alex_frontend/lbry/src/pages/sonora/ArchivePage.tsx create mode 100644 src/alex_frontend/lbry/src/pages/sonora/BrowsePage.tsx create mode 100644 src/alex_frontend/lbry/src/pages/sonora/MarketPage.tsx create mode 100644 src/alex_frontend/lbry/src/pages/sonora/RecordPage.tsx create mode 100644 src/alex_frontend/lbry/src/pages/sonora/StudioPage.tsx create mode 100644 src/alex_frontend/lbry/src/pages/sonora/UploadPage.tsx create mode 100644 src/alex_frontend/lbry/src/routes/_auth/app/sonora/archive.$principal.lazy.tsx create mode 100644 src/alex_frontend/lbry/src/routes/_auth/app/sonora/archive.$principal.tsx create mode 100644 src/alex_frontend/lbry/src/routes/_auth/app/sonora/archive.lazy.tsx create mode 100644 src/alex_frontend/lbry/src/routes/_auth/app/sonora/archive.tsx create mode 100644 src/alex_frontend/lbry/src/routes/_auth/app/sonora/index.lazy.tsx create mode 100644 src/alex_frontend/lbry/src/routes/_auth/app/sonora/index.tsx create mode 100644 src/alex_frontend/lbry/src/routes/_auth/app/sonora/market.lazy.tsx create mode 100644 src/alex_frontend/lbry/src/routes/_auth/app/sonora/market.tsx create mode 100644 src/alex_frontend/lbry/src/routes/_auth/app/sonora/record.lazy.tsx create mode 100644 src/alex_frontend/lbry/src/routes/_auth/app/sonora/record.tsx create mode 100644 src/alex_frontend/lbry/src/routes/_auth/app/sonora/route.lazy.tsx create mode 100644 src/alex_frontend/lbry/src/routes/_auth/app/sonora/studio.$principal.lazy.tsx create mode 100644 src/alex_frontend/lbry/src/routes/_auth/app/sonora/studio.$principal.tsx create mode 100644 src/alex_frontend/lbry/src/routes/_auth/app/sonora/studio.lazy.tsx create mode 100644 src/alex_frontend/lbry/src/routes/_auth/app/sonora/studio.tsx create mode 100644 src/alex_frontend/lbry/src/routes/_auth/app/sonora/upload.lazy.tsx create mode 100644 src/alex_frontend/lbry/src/routes/_auth/app/sonora/upload.tsx diff --git a/src/alex_frontend/core/config/apps.ts b/src/alex_frontend/core/config/apps.ts index 0fbcd12ab..003625415 100644 --- a/src/alex_frontend/core/config/apps.ts +++ b/src/alex_frontend/core/config/apps.ts @@ -13,7 +13,7 @@ export const appsData: App[] = [ { name: 'Permasearch', description: 'Explore', path: '/app/permasearch', logo: '/logos/Permasearch.svg' }, { name: 'Emporium', description: 'Trade', path: '/app/emporium', logo: '/logos/Emporium.svg' }, { name: 'Pinax', description: 'Upload', path: '/app/pinax', logo: '/logos/Pinax.svg', comingSoon: false }, - { name: 'Sonora', description: 'Audio', path: '/app/sonora', logo: `https://picsum.photos/seed/${Date.now()}/300/300`, comingSoon: true }, + { name: 'Sonora', description: 'Audio', path: '/app/sonora', logo: '/logos/Sonora.svg' }, { name: 'Syllogos', description: 'Aggregate', path: '/app/syllogos', logo: '/logos/Syllogos.svg', comingSoon: true }, { name: 'Bibliotheca', description: 'Library', path: '/app/bibliotheca', comingSoon: true }, { name: 'Dialectica', description: 'Debate', path: '/app/dialectica', comingSoon: true }, diff --git a/src/alex_frontend/core/layouts/SonoraLayout.tsx b/src/alex_frontend/core/layouts/SonoraLayout.tsx new file mode 100644 index 000000000..61e0f95c4 --- /dev/null +++ b/src/alex_frontend/core/layouts/SonoraLayout.tsx @@ -0,0 +1,78 @@ +import React from "react"; +import { Link, Outlet } from "@tanstack/react-router"; + +function SonoraLayout() { + return ( + <> +
+
+

Sonora

+

Discover, Record, and Trade Audio NFTs

+
+
+ +
+
+
+ +
+ + ); +} + +export default SonoraLayout; diff --git a/src/alex_frontend/lbry/public/logos/Sonora.svg b/src/alex_frontend/lbry/public/logos/Sonora.svg new file mode 100644 index 000000000..894e194a5 --- /dev/null +++ b/src/alex_frontend/lbry/public/logos/Sonora.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/alex_frontend/lbry/routeTree.gen.ts b/src/alex_frontend/lbry/routeTree.gen.ts index c07071a6a..489519024 100644 --- a/src/alex_frontend/lbry/routeTree.gen.ts +++ b/src/alex_frontend/lbry/routeTree.gen.ts @@ -19,9 +19,17 @@ import { Route as SwapInsightsRouteImport } from './src/routes/swap/insights' import { Route as NftTokenIdRouteImport } from './src/routes/nft.$tokenId' import { Route as AppPermasearchRouteImport } from './src/routes/app/permasearch' import { Route as AppAlexandrianRouteImport } from './src/routes/app/alexandrian' +import { Route as AuthAppSonoraIndexRouteImport } from './src/routes/_auth/app/sonora/index' import { Route as AuthAppEmporiumIndexRouteImport } from './src/routes/_auth/app/emporium/index' +import { Route as AuthAppSonoraUploadRouteImport } from './src/routes/_auth/app/sonora/upload' +import { Route as AuthAppSonoraStudioRouteImport } from './src/routes/_auth/app/sonora/studio' +import { Route as AuthAppSonoraRecordRouteImport } from './src/routes/_auth/app/sonora/record' +import { Route as AuthAppSonoraMarketRouteImport } from './src/routes/_auth/app/sonora/market' +import { Route as AuthAppSonoraArchiveRouteImport } from './src/routes/_auth/app/sonora/archive' import { Route as AuthAppEmporiumMyLogsRouteImport } from './src/routes/_auth/app/emporium/my-logs' import { Route as AuthAppEmporiumMarketLogsRouteImport } from './src/routes/_auth/app/emporium/market-logs' +import { Route as AuthAppSonoraStudioPrincipalRouteImport } from './src/routes/_auth/app/sonora/studio.$principal' +import { Route as AuthAppSonoraArchivePrincipalRouteImport } from './src/routes/_auth/app/sonora/archive.$principal' const ManagerLazyRouteImport = createFileRoute('/manager')() const SwapRouteLazyRouteImport = createFileRoute('/swap')() @@ -54,6 +62,7 @@ const AuthAppPinaxLazyRouteImport = createFileRoute('/_auth/app/pinax')() const AuthDashboardLibrarianRouteLazyRouteImport = createFileRoute( '/_auth/dashboard/_librarian', )() +const AuthAppSonoraRouteLazyRouteImport = createFileRoute('/_auth/app/sonora')() const AuthAppEmporiumRouteLazyRouteImport = createFileRoute( '/_auth/app/emporium', )() @@ -281,6 +290,13 @@ const AuthDashboardLibrarianRouteLazyRoute = (d) => d.Route, ), ) +const AuthAppSonoraRouteLazyRoute = AuthAppSonoraRouteLazyRouteImport.update({ + id: '/app/sonora', + path: '/app/sonora', + getParentRoute: () => AuthRouteLazyRoute, +} as any).lazy(() => + import('./src/routes/_auth/app/sonora/route.lazy').then((d) => d.Route), +) const AuthAppEmporiumRouteLazyRoute = AuthAppEmporiumRouteLazyRouteImport.update({ id: '/app/emporium', @@ -307,6 +323,13 @@ const AuthAppPerpetuaIndexLazyRoute = } as any).lazy(() => import('./src/routes/_auth/app/perpetua/index.lazy').then((d) => d.Route), ) +const AuthAppSonoraIndexRoute = AuthAppSonoraIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => AuthAppSonoraRouteLazyRoute, +} as any).lazy(() => + import('./src/routes/_auth/app/sonora/index.lazy').then((d) => d.Route), +) const AuthAppEmporiumIndexRoute = AuthAppEmporiumIndexRouteImport.update({ id: '/', path: '/', @@ -334,6 +357,41 @@ const AuthDashboardLibrarianWalletsLazyRoute = (d) => d.Route, ), ) +const AuthAppSonoraUploadRoute = AuthAppSonoraUploadRouteImport.update({ + id: '/upload', + path: '/upload', + getParentRoute: () => AuthAppSonoraRouteLazyRoute, +} as any).lazy(() => + import('./src/routes/_auth/app/sonora/upload.lazy').then((d) => d.Route), +) +const AuthAppSonoraStudioRoute = AuthAppSonoraStudioRouteImport.update({ + id: '/studio', + path: '/studio', + getParentRoute: () => AuthAppSonoraRouteLazyRoute, +} as any).lazy(() => + import('./src/routes/_auth/app/sonora/studio.lazy').then((d) => d.Route), +) +const AuthAppSonoraRecordRoute = AuthAppSonoraRecordRouteImport.update({ + id: '/record', + path: '/record', + getParentRoute: () => AuthAppSonoraRouteLazyRoute, +} as any).lazy(() => + import('./src/routes/_auth/app/sonora/record.lazy').then((d) => d.Route), +) +const AuthAppSonoraMarketRoute = AuthAppSonoraMarketRouteImport.update({ + id: '/market', + path: '/market', + getParentRoute: () => AuthAppSonoraRouteLazyRoute, +} as any).lazy(() => + import('./src/routes/_auth/app/sonora/market.lazy').then((d) => d.Route), +) +const AuthAppSonoraArchiveRoute = AuthAppSonoraArchiveRouteImport.update({ + id: '/archive', + path: '/archive', + getParentRoute: () => AuthAppSonoraRouteLazyRoute, +} as any).lazy(() => + import('./src/routes/_auth/app/sonora/archive.lazy').then((d) => d.Route), +) const AuthAppEmporiumMyLogsRoute = AuthAppEmporiumMyLogsRouteImport.update({ id: '/my-logs', path: '/my-logs', @@ -361,6 +419,26 @@ const AuthAppPerpetuaShelfShelfIdLazyRoute = (d) => d.Route, ), ) +const AuthAppSonoraStudioPrincipalRoute = + AuthAppSonoraStudioPrincipalRouteImport.update({ + id: '/$principal', + path: '/$principal', + getParentRoute: () => AuthAppSonoraStudioRoute, + } as any).lazy(() => + import('./src/routes/_auth/app/sonora/studio.$principal.lazy').then( + (d) => d.Route, + ), + ) +const AuthAppSonoraArchivePrincipalRoute = + AuthAppSonoraArchivePrincipalRouteImport.update({ + id: '/$principal', + path: '/$principal', + getParentRoute: () => AuthAppSonoraArchiveRoute, + } as any).lazy(() => + import('./src/routes/_auth/app/sonora/archive.$principal.lazy').then( + (d) => d.Route, + ), + ) const AuthAppPerpetuaUserUserIdIndexLazyRoute = AuthAppPerpetuaUserUserIdIndexLazyRouteImport.update({ id: '/app/perpetua/user/$userId/', @@ -403,6 +481,7 @@ export interface FileRoutesByFullPath { '/swap/stake': typeof SwapStakeLazyRoute '/info/': typeof InfoIndexLazyRoute '/app/emporium': typeof AuthAppEmporiumRouteLazyRouteWithChildren + '/app/sonora': typeof AuthAppSonoraRouteLazyRouteWithChildren '/app/pinax': typeof AuthAppPinaxLazyRoute '/dashboard/analytics': typeof AuthDashboardAnalyticsLazyRoute '/dashboard/arweave-assets': typeof AuthDashboardArweaveAssetsLazyRoute @@ -412,11 +491,19 @@ export interface FileRoutesByFullPath { '/swap/': typeof AuthSwapIndexLazyRoute '/app/emporium/market-logs': typeof AuthAppEmporiumMarketLogsRoute '/app/emporium/my-logs': typeof AuthAppEmporiumMyLogsRoute + '/app/sonora/archive': typeof AuthAppSonoraArchiveRouteWithChildren + '/app/sonora/market': typeof AuthAppSonoraMarketRoute + '/app/sonora/record': typeof AuthAppSonoraRecordRoute + '/app/sonora/studio': typeof AuthAppSonoraStudioRouteWithChildren + '/app/sonora/upload': typeof AuthAppSonoraUploadRoute '/dashboard/wallets': typeof AuthDashboardLibrarianWalletsLazyRoute '/dashboard/profile/upgrade': typeof AuthDashboardProfileUpgradeLazyRoute '/app/emporium/': typeof AuthAppEmporiumIndexRoute + '/app/sonora/': typeof AuthAppSonoraIndexRoute '/app/perpetua': typeof AuthAppPerpetuaIndexLazyRoute '/dashboard/': typeof AuthDashboardLibrarianIndexLazyRoute + '/app/sonora/archive/$principal': typeof AuthAppSonoraArchivePrincipalRoute + '/app/sonora/studio/$principal': typeof AuthAppSonoraStudioPrincipalRoute '/app/perpetua/shelf/$shelfId': typeof AuthAppPerpetuaShelfShelfIdLazyRoute '/app/perpetua/user/$userId': typeof AuthAppPerpetuaUserUserIdIndexLazyRoute '/app/perpetua/user/$userId/shelf/$shelfId': typeof AuthAppPerpetuaUserUserIdShelfShelfIdLazyRoute @@ -448,10 +535,18 @@ export interface FileRoutesByTo { '/swap/burn': typeof AuthSwapBurnLazyRoute '/app/emporium/market-logs': typeof AuthAppEmporiumMarketLogsRoute '/app/emporium/my-logs': typeof AuthAppEmporiumMyLogsRoute + '/app/sonora/archive': typeof AuthAppSonoraArchiveRouteWithChildren + '/app/sonora/market': typeof AuthAppSonoraMarketRoute + '/app/sonora/record': typeof AuthAppSonoraRecordRoute + '/app/sonora/studio': typeof AuthAppSonoraStudioRouteWithChildren + '/app/sonora/upload': typeof AuthAppSonoraUploadRoute '/dashboard/wallets': typeof AuthDashboardLibrarianWalletsLazyRoute '/dashboard/profile/upgrade': typeof AuthDashboardProfileUpgradeLazyRoute '/app/emporium': typeof AuthAppEmporiumIndexRoute + '/app/sonora': typeof AuthAppSonoraIndexRoute '/app/perpetua': typeof AuthAppPerpetuaIndexLazyRoute + '/app/sonora/archive/$principal': typeof AuthAppSonoraArchivePrincipalRoute + '/app/sonora/studio/$principal': typeof AuthAppSonoraStudioPrincipalRoute '/app/perpetua/shelf/$shelfId': typeof AuthAppPerpetuaShelfShelfIdLazyRoute '/app/perpetua/user/$userId': typeof AuthAppPerpetuaUserUserIdIndexLazyRoute '/app/perpetua/user/$userId/shelf/$shelfId': typeof AuthAppPerpetuaUserUserIdShelfShelfIdLazyRoute @@ -480,6 +575,7 @@ export interface FileRoutesById { '/swap/stake': typeof SwapStakeLazyRoute '/info/': typeof InfoIndexLazyRoute '/_auth/app/emporium': typeof AuthAppEmporiumRouteLazyRouteWithChildren + '/_auth/app/sonora': typeof AuthAppSonoraRouteLazyRouteWithChildren '/_auth/dashboard/_librarian': typeof AuthDashboardLibrarianRouteLazyRouteWithChildren '/_auth/app/pinax': typeof AuthAppPinaxLazyRoute '/_auth/dashboard/analytics': typeof AuthDashboardAnalyticsLazyRoute @@ -490,11 +586,19 @@ export interface FileRoutesById { '/_auth/swap/': typeof AuthSwapIndexLazyRoute '/_auth/app/emporium/market-logs': typeof AuthAppEmporiumMarketLogsRoute '/_auth/app/emporium/my-logs': typeof AuthAppEmporiumMyLogsRoute + '/_auth/app/sonora/archive': typeof AuthAppSonoraArchiveRouteWithChildren + '/_auth/app/sonora/market': typeof AuthAppSonoraMarketRoute + '/_auth/app/sonora/record': typeof AuthAppSonoraRecordRoute + '/_auth/app/sonora/studio': typeof AuthAppSonoraStudioRouteWithChildren + '/_auth/app/sonora/upload': typeof AuthAppSonoraUploadRoute '/_auth/dashboard/_librarian/wallets': typeof AuthDashboardLibrarianWalletsLazyRoute '/_auth/dashboard/profile/upgrade': typeof AuthDashboardProfileUpgradeLazyRoute '/_auth/app/emporium/': typeof AuthAppEmporiumIndexRoute + '/_auth/app/sonora/': typeof AuthAppSonoraIndexRoute '/_auth/app/perpetua/': typeof AuthAppPerpetuaIndexLazyRoute '/_auth/dashboard/_librarian/': typeof AuthDashboardLibrarianIndexLazyRoute + '/_auth/app/sonora/archive/$principal': typeof AuthAppSonoraArchivePrincipalRoute + '/_auth/app/sonora/studio/$principal': typeof AuthAppSonoraStudioPrincipalRoute '/_auth/app/perpetua/shelf/$shelfId': typeof AuthAppPerpetuaShelfShelfIdLazyRoute '/_auth/app/perpetua/user/$userId/': typeof AuthAppPerpetuaUserUserIdIndexLazyRoute '/_auth/app/perpetua/user/$userId/shelf/$shelfId': typeof AuthAppPerpetuaUserUserIdShelfShelfIdLazyRoute @@ -522,6 +626,7 @@ export interface FileRouteTypes { | '/swap/stake' | '/info/' | '/app/emporium' + | '/app/sonora' | '/app/pinax' | '/dashboard/analytics' | '/dashboard/arweave-assets' @@ -531,11 +636,19 @@ export interface FileRouteTypes { | '/swap/' | '/app/emporium/market-logs' | '/app/emporium/my-logs' + | '/app/sonora/archive' + | '/app/sonora/market' + | '/app/sonora/record' + | '/app/sonora/studio' + | '/app/sonora/upload' | '/dashboard/wallets' | '/dashboard/profile/upgrade' | '/app/emporium/' + | '/app/sonora/' | '/app/perpetua' | '/dashboard/' + | '/app/sonora/archive/$principal' + | '/app/sonora/studio/$principal' | '/app/perpetua/shelf/$shelfId' | '/app/perpetua/user/$userId' | '/app/perpetua/user/$userId/shelf/$shelfId' @@ -567,10 +680,18 @@ export interface FileRouteTypes { | '/swap/burn' | '/app/emporium/market-logs' | '/app/emporium/my-logs' + | '/app/sonora/archive' + | '/app/sonora/market' + | '/app/sonora/record' + | '/app/sonora/studio' + | '/app/sonora/upload' | '/dashboard/wallets' | '/dashboard/profile/upgrade' | '/app/emporium' + | '/app/sonora' | '/app/perpetua' + | '/app/sonora/archive/$principal' + | '/app/sonora/studio/$principal' | '/app/perpetua/shelf/$shelfId' | '/app/perpetua/user/$userId' | '/app/perpetua/user/$userId/shelf/$shelfId' @@ -598,6 +719,7 @@ export interface FileRouteTypes { | '/swap/stake' | '/info/' | '/_auth/app/emporium' + | '/_auth/app/sonora' | '/_auth/dashboard/_librarian' | '/_auth/app/pinax' | '/_auth/dashboard/analytics' @@ -608,11 +730,19 @@ export interface FileRouteTypes { | '/_auth/swap/' | '/_auth/app/emporium/market-logs' | '/_auth/app/emporium/my-logs' + | '/_auth/app/sonora/archive' + | '/_auth/app/sonora/market' + | '/_auth/app/sonora/record' + | '/_auth/app/sonora/studio' + | '/_auth/app/sonora/upload' | '/_auth/dashboard/_librarian/wallets' | '/_auth/dashboard/profile/upgrade' | '/_auth/app/emporium/' + | '/_auth/app/sonora/' | '/_auth/app/perpetua/' | '/_auth/dashboard/_librarian/' + | '/_auth/app/sonora/archive/$principal' + | '/_auth/app/sonora/studio/$principal' | '/_auth/app/perpetua/shelf/$shelfId' | '/_auth/app/perpetua/user/$userId/' | '/_auth/app/perpetua/user/$userId/shelf/$shelfId' @@ -837,6 +967,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthDashboardLibrarianRouteLazyRouteImport parentRoute: typeof AuthDashboardRouteLazyRoute } + '/_auth/app/sonora': { + id: '/_auth/app/sonora' + path: '/app/sonora' + fullPath: '/app/sonora' + preLoaderRoute: typeof AuthAppSonoraRouteLazyRouteImport + parentRoute: typeof AuthRouteLazyRoute + } '/_auth/app/emporium': { id: '/_auth/app/emporium' path: '/app/emporium' @@ -858,6 +995,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthAppPerpetuaIndexLazyRouteImport parentRoute: typeof AuthRouteLazyRoute } + '/_auth/app/sonora/': { + id: '/_auth/app/sonora/' + path: '/' + fullPath: '/app/sonora/' + preLoaderRoute: typeof AuthAppSonoraIndexRouteImport + parentRoute: typeof AuthAppSonoraRouteLazyRoute + } '/_auth/app/emporium/': { id: '/_auth/app/emporium/' path: '/' @@ -879,6 +1023,41 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthDashboardLibrarianWalletsLazyRouteImport parentRoute: typeof AuthDashboardLibrarianRouteLazyRoute } + '/_auth/app/sonora/upload': { + id: '/_auth/app/sonora/upload' + path: '/upload' + fullPath: '/app/sonora/upload' + preLoaderRoute: typeof AuthAppSonoraUploadRouteImport + parentRoute: typeof AuthAppSonoraRouteLazyRoute + } + '/_auth/app/sonora/studio': { + id: '/_auth/app/sonora/studio' + path: '/studio' + fullPath: '/app/sonora/studio' + preLoaderRoute: typeof AuthAppSonoraStudioRouteImport + parentRoute: typeof AuthAppSonoraRouteLazyRoute + } + '/_auth/app/sonora/record': { + id: '/_auth/app/sonora/record' + path: '/record' + fullPath: '/app/sonora/record' + preLoaderRoute: typeof AuthAppSonoraRecordRouteImport + parentRoute: typeof AuthAppSonoraRouteLazyRoute + } + '/_auth/app/sonora/market': { + id: '/_auth/app/sonora/market' + path: '/market' + fullPath: '/app/sonora/market' + preLoaderRoute: typeof AuthAppSonoraMarketRouteImport + parentRoute: typeof AuthAppSonoraRouteLazyRoute + } + '/_auth/app/sonora/archive': { + id: '/_auth/app/sonora/archive' + path: '/archive' + fullPath: '/app/sonora/archive' + preLoaderRoute: typeof AuthAppSonoraArchiveRouteImport + parentRoute: typeof AuthAppSonoraRouteLazyRoute + } '/_auth/app/emporium/my-logs': { id: '/_auth/app/emporium/my-logs' path: '/my-logs' @@ -900,6 +1079,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthAppPerpetuaShelfShelfIdLazyRouteImport parentRoute: typeof AuthRouteLazyRoute } + '/_auth/app/sonora/studio/$principal': { + id: '/_auth/app/sonora/studio/$principal' + path: '/$principal' + fullPath: '/app/sonora/studio/$principal' + preLoaderRoute: typeof AuthAppSonoraStudioPrincipalRouteImport + parentRoute: typeof AuthAppSonoraStudioRoute + } + '/_auth/app/sonora/archive/$principal': { + id: '/_auth/app/sonora/archive/$principal' + path: '/$principal' + fullPath: '/app/sonora/archive/$principal' + preLoaderRoute: typeof AuthAppSonoraArchivePrincipalRouteImport + parentRoute: typeof AuthAppSonoraArchiveRoute + } '/_auth/app/perpetua/user/$userId/': { id: '/_auth/app/perpetua/user/$userId/' path: '/app/perpetua/user/$userId' @@ -1002,10 +1195,57 @@ const AuthAppEmporiumRouteLazyRouteWithChildren = AuthAppEmporiumRouteLazyRouteChildren, ) +interface AuthAppSonoraArchiveRouteChildren { + AuthAppSonoraArchivePrincipalRoute: typeof AuthAppSonoraArchivePrincipalRoute +} + +const AuthAppSonoraArchiveRouteChildren: AuthAppSonoraArchiveRouteChildren = { + AuthAppSonoraArchivePrincipalRoute: AuthAppSonoraArchivePrincipalRoute, +} + +const AuthAppSonoraArchiveRouteWithChildren = + AuthAppSonoraArchiveRoute._addFileChildren(AuthAppSonoraArchiveRouteChildren) + +interface AuthAppSonoraStudioRouteChildren { + AuthAppSonoraStudioPrincipalRoute: typeof AuthAppSonoraStudioPrincipalRoute +} + +const AuthAppSonoraStudioRouteChildren: AuthAppSonoraStudioRouteChildren = { + AuthAppSonoraStudioPrincipalRoute: AuthAppSonoraStudioPrincipalRoute, +} + +const AuthAppSonoraStudioRouteWithChildren = + AuthAppSonoraStudioRoute._addFileChildren(AuthAppSonoraStudioRouteChildren) + +interface AuthAppSonoraRouteLazyRouteChildren { + AuthAppSonoraArchiveRoute: typeof AuthAppSonoraArchiveRouteWithChildren + AuthAppSonoraMarketRoute: typeof AuthAppSonoraMarketRoute + AuthAppSonoraRecordRoute: typeof AuthAppSonoraRecordRoute + AuthAppSonoraStudioRoute: typeof AuthAppSonoraStudioRouteWithChildren + AuthAppSonoraUploadRoute: typeof AuthAppSonoraUploadRoute + AuthAppSonoraIndexRoute: typeof AuthAppSonoraIndexRoute +} + +const AuthAppSonoraRouteLazyRouteChildren: AuthAppSonoraRouteLazyRouteChildren = + { + AuthAppSonoraArchiveRoute: AuthAppSonoraArchiveRouteWithChildren, + AuthAppSonoraMarketRoute: AuthAppSonoraMarketRoute, + AuthAppSonoraRecordRoute: AuthAppSonoraRecordRoute, + AuthAppSonoraStudioRoute: AuthAppSonoraStudioRouteWithChildren, + AuthAppSonoraUploadRoute: AuthAppSonoraUploadRoute, + AuthAppSonoraIndexRoute: AuthAppSonoraIndexRoute, + } + +const AuthAppSonoraRouteLazyRouteWithChildren = + AuthAppSonoraRouteLazyRoute._addFileChildren( + AuthAppSonoraRouteLazyRouteChildren, + ) + interface AuthRouteLazyRouteChildren { AuthDashboardRouteLazyRoute: typeof AuthDashboardRouteLazyRouteWithChildren AuthSwapRouteLazyRoute: typeof AuthSwapRouteLazyRouteWithChildren AuthAppEmporiumRouteLazyRoute: typeof AuthAppEmporiumRouteLazyRouteWithChildren + AuthAppSonoraRouteLazyRoute: typeof AuthAppSonoraRouteLazyRouteWithChildren AuthAppPinaxLazyRoute: typeof AuthAppPinaxLazyRoute AuthAppPerpetuaIndexLazyRoute: typeof AuthAppPerpetuaIndexLazyRoute AuthAppPerpetuaShelfShelfIdLazyRoute: typeof AuthAppPerpetuaShelfShelfIdLazyRoute @@ -1017,6 +1257,7 @@ const AuthRouteLazyRouteChildren: AuthRouteLazyRouteChildren = { AuthDashboardRouteLazyRoute: AuthDashboardRouteLazyRouteWithChildren, AuthSwapRouteLazyRoute: AuthSwapRouteLazyRouteWithChildren, AuthAppEmporiumRouteLazyRoute: AuthAppEmporiumRouteLazyRouteWithChildren, + AuthAppSonoraRouteLazyRoute: AuthAppSonoraRouteLazyRouteWithChildren, AuthAppPinaxLazyRoute: AuthAppPinaxLazyRoute, AuthAppPerpetuaIndexLazyRoute: AuthAppPerpetuaIndexLazyRoute, AuthAppPerpetuaShelfShelfIdLazyRoute: AuthAppPerpetuaShelfShelfIdLazyRoute, diff --git a/src/alex_frontend/lbry/src/pages/sonora/ArchivePage.tsx b/src/alex_frontend/lbry/src/pages/sonora/ArchivePage.tsx new file mode 100644 index 000000000..3b9c3b58d --- /dev/null +++ b/src/alex_frontend/lbry/src/pages/sonora/ArchivePage.tsx @@ -0,0 +1,207 @@ +import React, { useEffect } from "react"; +import { Helmet } from "react-helmet-async"; +import { Button } from "@/lib/components/button"; +import { Link } from "@tanstack/react-router"; +import { + Archive, + User, + FileAudio, + Download, + ArrowDown, + LoaderPinwheel, +} from "lucide-react"; +import { AudioCard } from "@/features/sonora/components/AudioCard"; +import { SellButton } from "@/features/sonora/components/SellButton"; +import { useUserAudioNFTs } from "@/features/sonora/hooks/useUserAudioNFTs"; +import { useAppSelector } from "@/store/hooks/useAppSelector"; +import { ArchiveAudio } from "@/features/sonora/types"; + +const SonoraArchivePage: React.FC = () => { + const { user } = useAppSelector((state) => state.auth); + const { + audios, + loading, + loadingMore, + error, + pagination, + refreshAudioNFTs, + } = useUserAudioNFTs(); + + // Fetch user's audio NFTs when component mounts or user changes + useEffect(() => { + if (user?.principal) { + refreshAudioNFTs(user.principal, 1, false); // First page, not append mode + } + }, [user?.principal, refreshAudioNFTs]); + + // Load More handler + const handleLoadMore = () => { + if (user?.principal && !loadingMore && pagination.hasMore) { + refreshAudioNFTs(user.principal, pagination.page + 1, true); // Next page, append mode + } + }; + + return ( + <> + + My Collection | Sonora | Alexandria + + +
+ {/* Left Sidebar */} +
+
+
+
+ +

Your Collection

+
+

+ Your personal audio NFT collection and uploaded + content. +

+
+
+ + Owned by you +
+
+ + Ready to trade +
+
+ + Download anytime +
+
+
+
+ +
+
+
+ +

Manage Content

+
+

+ Download, share, or list your audio NFTs for sale. +

+
+
+

+ Available actions: +

+
    +
  • • Download original files
  • +
  • • Share with others
  • +
  • • List on marketplace
  • +
+
+
+
+

+ + {audios.length} + {" "} + of{" "} + + {pagination.totalCount} + {" "} + items +

+ + → View your listings + +
+
+
+ + {/* Load More */} + {pagination.hasMore && ( + + )} +
+ + {/* Audio Grid */} +
+ {loading ? ( +
+
+ +

+ Loading your audio NFTs... +

+
+
+ ) : error ? ( +
+
+

+ Error loading NFTs +

+

+ {error} +

+ +
+
+ ) : audios.length === 0 ? ( +
+
+ +

+ No Audio NFTs Found +

+

+ You haven't minted any audio NFTs yet. +

+
+
+ ) : ( + audios.map((item: ArchiveAudio) => ( + + } + /> + )) + )} +
+
+ + ); +}; + +export default SonoraArchivePage; diff --git a/src/alex_frontend/lbry/src/pages/sonora/BrowsePage.tsx b/src/alex_frontend/lbry/src/pages/sonora/BrowsePage.tsx new file mode 100644 index 000000000..47937a173 --- /dev/null +++ b/src/alex_frontend/lbry/src/pages/sonora/BrowsePage.tsx @@ -0,0 +1,213 @@ +import React, { useEffect } from "react"; +import { Helmet } from "react-helmet-async"; +import { Button } from "@/lib/components/button"; +import { + Database, + Globe, + ArrowDown, + FileAudio, + Coins, + AlertCircle, + Loader2, +} from "lucide-react"; +import { AudioCard } from "@/features/sonora/components/AudioCard"; +import { MintButton } from "@/features/sonora/components/MintButton"; +import { useArweaveAudios } from "@/features/sonora/hooks/useArweaveAudios"; +import { fetchAudios } from "@/features/sonora/browse/thunks/fetchAudios"; +import { useAppDispatch } from "@/store/hooks/useAppDispatch"; +import { ArweaveAudio, Audio } from "@/features/sonora/types"; + +const BrowsePage: React.FC = () => { + const dispatch = useAppDispatch(); + const { audios, loading, error, hasNext, loadMore, isEmpty } = + useArweaveAudios(); + + // Convert ArweaveAudio to Audio format for AudioCard + const convertToAudio = (arweaveAudio: ArweaveAudio): Audio => { + // Try to get content type from data.type or Content-Type tag + let contentType = arweaveAudio.data?.type; + if (!contentType) { + const contentTypeTag = arweaveAudio.tags?.find( + (tag) => tag.name === "Content-Type" + ); + contentType = contentTypeTag?.value || ""; + } + + return { + id: arweaveAudio.id, + type: contentType, + size: arweaveAudio.data?.size || null, + timestamp: new Date( + arweaveAudio.block.timestamp * 1000 + ).toISOString(), + }; + }; + + // Fetch initial data on mount + useEffect(() => { + if (audios.length === 0 && !loading && !error) { + dispatch(fetchAudios({ reset: true })); + } + }, [dispatch, audios.length, loading, error]); + + const formatFileSize = (sizeString: string | null) => { + if (!sizeString) return "Unknown size"; + const size = parseInt(sizeString); + if (isNaN(size)) return "Unknown size"; + if (size < 1024 * 1024) { + return `${(size / 1024).toFixed(1)} KB`; + } + return `${(size / (1024 * 1024)).toFixed(1)} MB`; + }; + + return ( + <> + + Browse Audio | Sonora | Alexandria + + +
+ {/* Left Sidebar */} +
+
+
+
+ +

From Arweave

+
+

+ Audio files from the permanent web, ready to + discover and mint. +

+
+
+ + Permanent storage +
+
+ + Always accessible +
+
+ + Ready to mint +
+
+
+
+ +
+
+
+ +

Create NFTs

+
+

+ Transform any audio into a tradeable NFT on + Alexandria. +

+
+
+

+ Quick steps: +

+
    +
  • • Click mint icon on any audio
  • +
  • • Confirm the transaction
  • +
  • • Own the NFT permanently
  • +
+
+
+
+

+ {loading && audios.length === 0 ? ( + "Loading files..." + ) : ( + <> + + {audios.length} + {" "} + files available + + )} +

+
+
+
+ + {/* Load More */} + {hasNext && ( + + )} +
+ + {/* Audio Grid */} +
+ {error && ( +
+ + + {error} + +
+ )} + + {loading && audios.length === 0 && ( +
+ + + Loading audio files from Arweave... + +
+ )} + + {isEmpty && !loading && ( +
+ +

+ No audio files found +

+

+ Try refreshing the page +

+
+ )} + + {audios.map((arweaveAudio) => { + const audioItem = convertToAudio(arweaveAudio); + // Format size better + audioItem.size = formatFileSize(audioItem.size); + + return ( + } + /> + ); + })} +
+
+ + ); +}; + +export default BrowsePage; diff --git a/src/alex_frontend/lbry/src/pages/sonora/MarketPage.tsx b/src/alex_frontend/lbry/src/pages/sonora/MarketPage.tsx new file mode 100644 index 000000000..0d4a5068e --- /dev/null +++ b/src/alex_frontend/lbry/src/pages/sonora/MarketPage.tsx @@ -0,0 +1,208 @@ +import React, { useEffect } from "react"; +import { Helmet } from "react-helmet-async"; +import { Button } from "@/lib/components/button"; +import { Link } from "@tanstack/react-router"; +import { + Store, + TrendingUp, + Search, + Filter, + ArrowDown, + FileAudio, + LoaderPinwheel, +} from "lucide-react"; +import { AudioCard } from "@/features/sonora/components/AudioCard"; +import { BuyButton } from "@/features/sonora/components/BuyButton"; +import { useMarketAudioNFTs } from "@/features/sonora/hooks/useMarketAudioNFTs"; +import { MarketAudio } from "@/features/sonora/types"; + +const SonoraMarketPage: React.FC = () => { + const { + audios, + loading, + loadingMore, + error, + pagination, + refreshMarketAudioNFTs, + } = useMarketAudioNFTs(); + + // Fetch market audio NFTs on component mount + useEffect(() => { + refreshMarketAudioNFTs(1, 8, false); // First page, not append mode + }, []); + + // Load More handler + const handleLoadMore = () => { + if (!loadingMore && pagination.page < pagination.totalPages) { + refreshMarketAudioNFTs(pagination.page + 1, 8, true); // Next page, append mode + } + }; + + return ( + <> + + Audio Marketplace | Sonora | Alexandria + + +
+ {/* Left Sidebar */} +
+
+
+
+ +

Marketplace

+
+

+ Discover and purchase audio NFTs from creators + worldwide. +

+
+
+ + Trending content +
+
+ + Search by genre +
+
+ + Filter by price +
+
+
+
+ +
+
+
+ +

Buy & Own

+
+

+ Purchase audio NFTs with ALEX or LBRY tokens and own + them forever. +

+
+
+

+ How it works: +

+
    +
  • • Preview before buying
  • +
  • • Secure blockchain purchase
  • +
  • • Instant ownership transfer
  • +
+
+
+
+

+ + {audios.length} + {" "} + of{" "} + + {pagination.totalCount} + {" "} + items shown +

+ + → Manage your listings + +
+
+
+ + {/* Load More */} + {pagination.page < pagination.totalPages && ( + + )} +
+ + {/* Audio Grid */} +
+ {loading ? ( +
+
+ +

+ Loading marketplace audio NFTs... +

+
+
+ ) : error ? ( +
+
+

+ Error loading marketplace +

+

+ {error} +

+ +
+
+ ) : audios.length === 0 ? ( +
+
+ +

+ No Items for Sale +

+

+ There are currently no audio NFTs listed in the + marketplace. +

+
+
+ ) : ( + audios.map((audio: MarketAudio) => ( + + } + /> + )) + )} +
+
+ + ); +}; + +export default SonoraMarketPage; diff --git a/src/alex_frontend/lbry/src/pages/sonora/RecordPage.tsx b/src/alex_frontend/lbry/src/pages/sonora/RecordPage.tsx new file mode 100644 index 000000000..0d04f8025 --- /dev/null +++ b/src/alex_frontend/lbry/src/pages/sonora/RecordPage.tsx @@ -0,0 +1,334 @@ +import React, { useState, useRef } from "react"; +import { Helmet } from "react-helmet-async"; +import { Button } from "@/lib/components/button"; +import { Mic, Square, Upload, Trash2, LoaderPinwheel } from "lucide-react"; +import { Link } from "@tanstack/react-router"; +import { useAppDispatch } from "@/store/hooks/useAppDispatch"; +import { useAppSelector } from "@/store/hooks/useAppSelector"; +import { clearSelected, setSelected } from "@/features/sonora/sonoraSlice"; +import { Audio } from "@/features/sonora/types"; +import { AudioCard } from "@/features/sonora/components/AudioCard"; +import { useUploadAndMint } from "@/features/pinax/hooks/useUploadAndMint"; + +const SonoraRecordPage: React.FC = () => { + const [isRecording, setIsRecording] = useState(false); + const [recordedBlob, setRecordedBlob] = useState(null); + const [recordedUrl, setRecordedUrl] = useState(""); + const [recordingTime, setRecordingTime] = useState(0); + const [recordingError, setRecordingError] = useState(""); + const { + uploadAndMint, + isProcessing, + error: uploadError, + success, + progress, + estimating, + uploading, + minting, + resetUpload, + } = useUploadAndMint(); + + const mediaRecorderRef = useRef(null); + const streamRef = useRef(null); + const timerRef = useRef(null); + const dispatch = useAppDispatch(); + const { selected } = useAppSelector((state) => state.sonora); + + const startRecording = async () => { + try { + setRecordingError(""); + const stream = await navigator.mediaDevices.getUserMedia({ + audio: true, + }); + streamRef.current = stream; + + const mediaRecorder = new MediaRecorder(stream); + mediaRecorderRef.current = mediaRecorder; + + const chunks: BlobPart[] = []; + + mediaRecorder.ondataavailable = (event) => { + if (event.data.size > 0) { + chunks.push(event.data); + } + }; + + mediaRecorder.onstop = () => { + const blob = new Blob(chunks, { type: "audio/webm" }); + setRecordedBlob(blob); + const url = URL.createObjectURL(blob); + setRecordedUrl(url); + + // Automatically create audio data for preview + const audioData: Audio = { + id: url, + type: "audio/webm", + size: `${(blob.size / (1024 * 1024)).toFixed(2)} MB`, + timestamp: new Date().toISOString(), + }; + dispatch(setSelected(audioData)); + }; + + mediaRecorder.start(); + setIsRecording(true); + setRecordingTime(0); + + // Start timer + timerRef.current = setInterval(() => { + setRecordingTime((prev) => prev + 1); + }, 1000); + } catch (err) { + setRecordingError( + "Failed to access microphone. Please check permissions." + ); + } + }; + + const stopRecording = () => { + if (mediaRecorderRef.current && isRecording) { + mediaRecorderRef.current.stop(); + setIsRecording(false); + + if (timerRef.current) { + clearInterval(timerRef.current); + } + + if (streamRef.current) { + streamRef.current.getTracks().forEach((track) => track.stop()); + } + } + }; + + const deleteRecording = () => { + if (recordedUrl) { + URL.revokeObjectURL(recordedUrl); + } + setRecordedBlob(null); + setRecordedUrl(""); + setRecordingTime(0); + dispatch(clearSelected()); + // Reset upload state + resetUpload(); + }; + + const handleUpload = async () => { + if (recordedBlob) { + try { + // Convert blob to File object + const timestamp = new Date() + .toISOString() + .replace(/[:.]/g, "-"); + const fileName = `sonora-recording-${timestamp}.webm`; + const file = new File([recordedBlob], fileName, { + type: "audio/webm", + }); + + const transactionId = await uploadAndMint(file); + // Replace blob URL with Arweave transaction URL + if (recordedUrl) { + URL.revokeObjectURL(recordedUrl); + } + const arweaveUrl = `https://arweave.net/${transactionId}`; + setRecordedUrl(arweaveUrl); + + // Update the audio data with Arweave URL + const audioData: Audio = { + id: transactionId, // Use transaction ID as the ID + type: "audio/webm", + size: `${(recordedBlob.size / (1024 * 1024)).toFixed(2)} MB`, + timestamp: new Date().toISOString(), + }; + dispatch(setSelected(audioData)); + + // Clear the blob reference but keep the UI showing the uploaded file + setRecordedBlob(null); + } catch (error) { + // Error handling is done in the hook, keep recording for retry + } + } + }; + + const formatTime = (seconds: number) => { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`; + }; + + return ( + <> + + Record Audio | Sonora | Alexandria + + +
+
+
+

+ Record audio content directly in your browser +

+
+ + {/* Recording Interface */} +
+
+ {/* Recording Status */} +
+
+ +
+ +
+ {formatTime(recordingTime)} +
+ + {isRecording && ( +

+ Recording in progress... +

+ )} + + {recordedBlob && !isRecording && ( +

+ Recording completed +

+ )} +
+ + {/* Error Message */} + {recordingError && ( +
+

+ {recordingError} +

+
+ )} + + {/* Control Buttons */} +
+ {!isRecording && !recordedBlob && ( + + )} + + {isRecording && ( + + )} +
+
+ + {/* Upload option below recording box */} +
+

+ + + +

+
+
+ + {/* Audio Preview */} + {(recordedBlob || recordedUrl) && selected && !isRecording && ( +
+
+

+ {recordedBlob + ? "Preview" + : "Uploaded to Arweave"} +

+ +
+ +
+ )} + + {/* Upload Error and Success Messages */} + {uploadError && ( +
+

+ {uploadError} +

+
+ )} + + {success && ( +
+

{success}

+
+ )} + + {/* Upload Button */} + {recordedBlob && ( +
+ +
+ )} +
+
+ + ); +}; + +export default SonoraRecordPage; diff --git a/src/alex_frontend/lbry/src/pages/sonora/StudioPage.tsx b/src/alex_frontend/lbry/src/pages/sonora/StudioPage.tsx new file mode 100644 index 000000000..7ba4b9b9c --- /dev/null +++ b/src/alex_frontend/lbry/src/pages/sonora/StudioPage.tsx @@ -0,0 +1,229 @@ +import React, { useEffect } from "react"; +import { Helmet } from "react-helmet-async"; +import { Button } from "@/lib/components/button"; +import { + Palette, + Settings, + TrendingUp, + BarChart3, + ArrowDown, + FileAudio, + LoaderPinwheel, +} from "lucide-react"; +import { AudioCard } from "@/features/sonora/components/AudioCard"; +import { EditButton } from "@/features/sonora/components/EditButton"; +import { UnlistButton } from "@/features/sonora/components/UnlistButton"; +import { useStudioAudioNFTs } from "@/features/sonora/hooks/useStudioAudioNFTs"; +import { useAppSelector } from "@/store/hooks/useAppSelector"; +import { StudioAudio } from "@/features/sonora/types"; + +const SonoraStudioPage: React.FC = () => { + const { user } = useAppSelector((state) => state.auth); + const { + audios, + loading, + loadingMore, + error, + pagination, + refreshStudioAudioNFTs, + } = useStudioAudioNFTs(); + + // Fetch user's listed audio NFTs on component mount + useEffect(() => { + if (user?.principal) { + refreshStudioAudioNFTs(user.principal, 1, 8, false); // First page, not append mode + } + }, [user?.principal]); + + // Load More handler + const handleLoadMore = () => { + if ( + user?.principal && + !loadingMore && + pagination.page < pagination.totalPages + ) { + refreshStudioAudioNFTs( + user.principal, + pagination.page + 1, + 8, + true + ); // Next page, append mode + } + }; + + return ( + <> + + Creator Studio | Sonora | Alexandria + + +
+ {/* Left Sidebar */} +
+
+
+
+ +

Creator Studio

+
+

+ Manage your listed audio NFTs and marketplace + presence. +

+
+
+ + Track sales +
+
+ + Edit listings +
+
+ + View analytics +
+
+
+
+ +
+
+
+ +

Manage Listings

+
+

+ Edit metadata, pricing, and availability of your + audio NFTs. +

+
+
+

+ Available actions: +

+
    +
  • • Edit titles and descriptions
  • +
  • • Update pricing
  • +
  • • Remove from marketplace
  • +
+
+
+
+

+ + {audios.length} + {" "} + of{" "} + + {pagination.totalCount} + {" "} + listings shown +

+
+
+
+ + {/* Load More */} + {pagination.page < pagination.totalPages && ( + + )} +
+ + {/* Audio Grid */} +
+ {loading ? ( +
+
+ +

+ Loading your listed audio NFTs... +

+
+
+ ) : error ? ( +
+
+

+ Error loading listed audio NFTs +

+

+ {error} +

+ +
+
+ ) : audios.length === 0 ? ( +
+
+ +

+ No Active Listings +

+

+ You don't have any audio NFTs listed for sale + yet. +

+
+
+ ) : ( + audios.map((item: StudioAudio) => ( + + + + + } + /> + )) + )} +
+
+ + ); +}; + +export default SonoraStudioPage; diff --git a/src/alex_frontend/lbry/src/pages/sonora/UploadPage.tsx b/src/alex_frontend/lbry/src/pages/sonora/UploadPage.tsx new file mode 100644 index 000000000..69328b2eb --- /dev/null +++ b/src/alex_frontend/lbry/src/pages/sonora/UploadPage.tsx @@ -0,0 +1,258 @@ +import React, { useState, useRef } from "react"; +import { Helmet } from "react-helmet-async"; +import { Button } from "@/lib/components/button"; +import { Upload, Mic, X, LoaderPinwheel } from "lucide-react"; +import { Link } from "@tanstack/react-router"; +import { useAppDispatch } from "@/store/hooks/useAppDispatch"; +import { useAppSelector } from "@/store/hooks/useAppSelector"; +import { setSelected, clearSelected } from "@/features/sonora/sonoraSlice"; +import { Audio } from "@/features/sonora/types"; +import { AudioCard } from "@/features/sonora/components/AudioCard"; +import { useUploadAndMint } from "@/features/pinax/hooks/useUploadAndMint"; + +const SonoraUploadPage: React.FC = () => { + const [selectedFile, setSelectedFile] = useState(null); + const [audioUrl, setAudioUrl] = useState(""); + const [isDragging, setIsDragging] = useState(false); + const fileInputRef = useRef(null); + const dispatch = useAppDispatch(); + const { selected } = useAppSelector((state) => state.sonora); + const { + uploadAndMint, + isProcessing, + error, + success, + progress, + estimating, + uploading, + minting, + resetUpload, + } = useUploadAndMint(); + + const handleFileSelect = (file: File) => { + // Validate audio file + if (!file.type.startsWith("audio/")) { + alert("Please select an audio file"); + return; + } + + setSelectedFile(file); + // Create object URL for preview + const url = URL.createObjectURL(file); + setAudioUrl(url); + + // Create Audio object for Redux + const audioData: Audio = { + id: url, // Use object URL as ID for local files + type: file.type, + size: `${(file.size / (1024 * 1024)).toFixed(2)} MB`, + timestamp: new Date().toISOString(), + }; + + // Set audio in global state for player + dispatch(setSelected(audioData)); + }; + + const handleFileInput = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) handleFileSelect(file); + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(true); + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + const file = e.dataTransfer.files[0]; + if (file) handleFileSelect(file); + }; + + const handleRemoveFile = () => { + if (audioUrl) { + URL.revokeObjectURL(audioUrl); + } + setSelectedFile(null); + setAudioUrl(""); + if (fileInputRef.current) fileInputRef.current.value = ""; + // Clear audio from global state + dispatch(clearSelected()); + // Reset upload state + resetUpload(); + }; + + const handleUpload = async () => { + if (selectedFile) { + try { + const transactionId = await uploadAndMint(selectedFile); + // Replace blob URL with Arweave transaction URL + if (audioUrl) { + URL.revokeObjectURL(audioUrl); + } + const arweaveUrl = `https://arweave.net/${transactionId}`; + setAudioUrl(arweaveUrl); + + // Update the audio data with Arweave URL + const audioData: Audio = { + id: transactionId, // Use transaction ID as the ID + type: selectedFile.type, + size: `${(selectedFile.size / (1024 * 1024)).toFixed(2)} MB`, + timestamp: new Date().toISOString(), + }; + dispatch(setSelected(audioData)); + } catch (error) { + // Error handling is done in the hook, keep file for retry + } + } + }; + + return ( + <> + + Upload Audio | Sonora | Alexandria + + +
+
+
+

+ Share your audio content with the world +

+
+ + {/* File Upload Area */} +
+
+ + +
+ +
+

+ {selectedFile + ? selectedFile.name + : "Drop your audio file here"} +

+

+ or click to browse • Supports MP3, WAV, OGG +

+
+
+
+ + {/* Record option below upload box */} +
+

+ + + +

+
+
+ + {/* Audio Preview */} + {selectedFile && selected && ( +
+
+

+ {audioUrl.startsWith("blob:") + ? "Preview" + : "Uploaded to Arweave"} +

+ +
+ +
+ )} + + {/* Error and Success Messages */} + {error && ( +
+

{error}

+
+ )} + + {success && ( +
+

{success}

+
+ )} + + {/* Upload Button */} + {selectedFile && audioUrl.startsWith("blob:") && ( +
+ +
+ )} +
+
+ + ); +}; + +export default SonoraUploadPage; diff --git a/src/alex_frontend/lbry/src/routes/_auth/app/sonora/archive.$principal.lazy.tsx b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/archive.$principal.lazy.tsx new file mode 100644 index 000000000..525ec4c98 --- /dev/null +++ b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/archive.$principal.lazy.tsx @@ -0,0 +1,6 @@ +import { createLazyFileRoute } from '@tanstack/react-router' +import ArchivePage from './../../../../pages/sonora/ArchivePage' + +export const Route = createLazyFileRoute('/_auth/app/sonora/archive/$principal')({ + component: ArchivePage, +}) diff --git a/src/alex_frontend/lbry/src/routes/_auth/app/sonora/archive.$principal.tsx b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/archive.$principal.tsx new file mode 100644 index 000000000..f247017b3 --- /dev/null +++ b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/archive.$principal.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_auth/app/sonora/archive/$principal')({ + loader: () => void 0, +}) diff --git a/src/alex_frontend/lbry/src/routes/_auth/app/sonora/archive.lazy.tsx b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/archive.lazy.tsx new file mode 100644 index 000000000..74e3a2d8a --- /dev/null +++ b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/archive.lazy.tsx @@ -0,0 +1,6 @@ +import { createLazyFileRoute } from '@tanstack/react-router' +import ArchivePage from './../../../../pages/sonora/ArchivePage' + +export const Route = createLazyFileRoute('/_auth/app/sonora/archive')({ + component: ArchivePage, +}) diff --git a/src/alex_frontend/lbry/src/routes/_auth/app/sonora/archive.tsx b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/archive.tsx new file mode 100644 index 000000000..c5e6f018c --- /dev/null +++ b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/archive.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_auth/app/sonora/archive')({ + loader: () => void 0, +}) diff --git a/src/alex_frontend/lbry/src/routes/_auth/app/sonora/index.lazy.tsx b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/index.lazy.tsx new file mode 100644 index 000000000..dd6e029fb --- /dev/null +++ b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/index.lazy.tsx @@ -0,0 +1,6 @@ +import { createLazyFileRoute } from '@tanstack/react-router' +import BrowsePage from './../../../../pages/sonora/BrowsePage' + +export const Route = createLazyFileRoute('/_auth/app/sonora/')({ + component: BrowsePage, +}) diff --git a/src/alex_frontend/lbry/src/routes/_auth/app/sonora/index.tsx b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/index.tsx new file mode 100644 index 000000000..b26e9a1e7 --- /dev/null +++ b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/index.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_auth/app/sonora/')({ + loader: () => void 0, +}) diff --git a/src/alex_frontend/lbry/src/routes/_auth/app/sonora/market.lazy.tsx b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/market.lazy.tsx new file mode 100644 index 000000000..02309f9d3 --- /dev/null +++ b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/market.lazy.tsx @@ -0,0 +1,6 @@ +import { createLazyFileRoute } from '@tanstack/react-router' +import MarketPage from './../../../../pages/sonora/MarketPage' + +export const Route = createLazyFileRoute('/_auth/app/sonora/market')({ + component: MarketPage, +}) diff --git a/src/alex_frontend/lbry/src/routes/_auth/app/sonora/market.tsx b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/market.tsx new file mode 100644 index 000000000..8f32560c4 --- /dev/null +++ b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/market.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_auth/app/sonora/market')({ + loader: () => void 0, +}) diff --git a/src/alex_frontend/lbry/src/routes/_auth/app/sonora/record.lazy.tsx b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/record.lazy.tsx new file mode 100644 index 000000000..0ff9d0ce9 --- /dev/null +++ b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/record.lazy.tsx @@ -0,0 +1,6 @@ +import { createLazyFileRoute } from '@tanstack/react-router' +import RecordPage from './../../../../pages/sonora/RecordPage' + +export const Route = createLazyFileRoute('/_auth/app/sonora/record')({ + component: RecordPage, +}) diff --git a/src/alex_frontend/lbry/src/routes/_auth/app/sonora/record.tsx b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/record.tsx new file mode 100644 index 000000000..88c4a3d56 --- /dev/null +++ b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/record.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_auth/app/sonora/record')({ + loader: () => void 0, +}) diff --git a/src/alex_frontend/lbry/src/routes/_auth/app/sonora/route.lazy.tsx b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/route.lazy.tsx new file mode 100644 index 000000000..cea51cbe4 --- /dev/null +++ b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/route.lazy.tsx @@ -0,0 +1,6 @@ +import SonoraLayout from '@/layouts/SonoraLayout' +import { createLazyFileRoute } from '@tanstack/react-router' + +export const Route = createLazyFileRoute('/_auth/app/sonora')({ + component: SonoraLayout, +}) diff --git a/src/alex_frontend/lbry/src/routes/_auth/app/sonora/studio.$principal.lazy.tsx b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/studio.$principal.lazy.tsx new file mode 100644 index 000000000..9b697a1a0 --- /dev/null +++ b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/studio.$principal.lazy.tsx @@ -0,0 +1,6 @@ +import { createLazyFileRoute } from '@tanstack/react-router' +import StudioPage from './../../../../pages/sonora/StudioPage' + +export const Route = createLazyFileRoute('/_auth/app/sonora/studio/$principal')({ + component: StudioPage, +}) diff --git a/src/alex_frontend/lbry/src/routes/_auth/app/sonora/studio.$principal.tsx b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/studio.$principal.tsx new file mode 100644 index 000000000..3d509df98 --- /dev/null +++ b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/studio.$principal.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_auth/app/sonora/studio/$principal')({ + loader: () => void 0, +}) diff --git a/src/alex_frontend/lbry/src/routes/_auth/app/sonora/studio.lazy.tsx b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/studio.lazy.tsx new file mode 100644 index 000000000..a3d7583a0 --- /dev/null +++ b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/studio.lazy.tsx @@ -0,0 +1,6 @@ +import { createLazyFileRoute } from '@tanstack/react-router' +import StudioPage from './../../../../pages/sonora/StudioPage' + +export const Route = createLazyFileRoute('/_auth/app/sonora/studio')({ + component: StudioPage, +}) diff --git a/src/alex_frontend/lbry/src/routes/_auth/app/sonora/studio.tsx b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/studio.tsx new file mode 100644 index 000000000..511bc356b --- /dev/null +++ b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/studio.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_auth/app/sonora/studio')({ + loader: () => void 0, +}) diff --git a/src/alex_frontend/lbry/src/routes/_auth/app/sonora/upload.lazy.tsx b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/upload.lazy.tsx new file mode 100644 index 000000000..83b8aa2a4 --- /dev/null +++ b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/upload.lazy.tsx @@ -0,0 +1,6 @@ +import { createLazyFileRoute } from '@tanstack/react-router' +import UploadPage from './../../../../pages/sonora/UploadPage' + +export const Route = createLazyFileRoute('/_auth/app/sonora/upload')({ + component: UploadPage, +}) diff --git a/src/alex_frontend/lbry/src/routes/_auth/app/sonora/upload.tsx b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/upload.tsx new file mode 100644 index 000000000..15a7ac587 --- /dev/null +++ b/src/alex_frontend/lbry/src/routes/_auth/app/sonora/upload.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_auth/app/sonora/upload')({ + loader: () => void 0, +}) From 8ae0b6c5d2405cf0adec349d4c0ee002a0ff2de1 Mon Sep 17 00:00:00 2001 From: Zeeshan Tariq Date: Wed, 22 Apr 2026 01:40:27 +0500 Subject: [PATCH 2/2] fix/simplify sonora code issues --- .../features/sonora/components/AudioCard.tsx | 31 +- .../sonora/hooks/useMarketAudioNFTs.ts | 16 +- .../sonora/hooks/useSonoraProfileTarget.ts | 10 + .../sonora/hooks/useStudioAudioNFTs.ts | 6 +- .../core/layouts/SonoraLayout.tsx | 78 ++-- .../lbry/src/pages/sonora/ArchivePage.tsx | 330 +++++++++-------- .../lbry/src/pages/sonora/BrowsePage.tsx | 39 +- .../lbry/src/pages/sonora/MarketPage.tsx | 295 ++++++++------- .../lbry/src/pages/sonora/RecordPage.tsx | 324 ++++++++-------- .../lbry/src/pages/sonora/StudioPage.tsx | 350 +++++++++--------- .../lbry/src/pages/sonora/UploadPage.tsx | 253 +++++++------ 11 files changed, 899 insertions(+), 833 deletions(-) create mode 100644 src/alex_frontend/core/features/sonora/hooks/useSonoraProfileTarget.ts diff --git a/src/alex_frontend/core/features/sonora/components/AudioCard.tsx b/src/alex_frontend/core/features/sonora/components/AudioCard.tsx index 814301e35..368319d9a 100644 --- a/src/alex_frontend/core/features/sonora/components/AudioCard.tsx +++ b/src/alex_frontend/core/features/sonora/components/AudioCard.tsx @@ -1,9 +1,16 @@ import React, { useRef, useState, useEffect } from "react"; import { FileAudio, HardDrive, Calendar, Play, Pause, Loader2, DollarSign, User } from "lucide-react"; +import { Link } from "@tanstack/react-router"; import { useAppDispatch } from "@/store/hooks/useAppDispatch"; import { useAppSelector } from "@/store/hooks/useAppSelector"; import { setSelected, clearSelected } from "../sonoraSlice"; import { Audio } from "../types"; +import { shortenPrincipal } from "@/features/perpetua"; + +const ownerLinks = [ + { to: "/app/sonora/studio/$principal", label: "listings" }, + { to: "/app/sonora/archive/$principal", label: "archive" }, +] as const; interface AudioCardProps { item: Audio; @@ -158,15 +165,6 @@ export const AudioCard: React.FC = ({ item, actions, price, owne return type || 'No Type'; }; - const formatOwner = (ownerPrincipal: string) => { - if (!ownerPrincipal) return 'Unknown'; - // Show first 6 and last 4 characters with ellipsis - if (ownerPrincipal.length > 12) { - return `${ownerPrincipal.slice(0, 6)}...${ownerPrincipal.slice(-4)}`; - } - return ownerPrincipal; - }; - return (
= ({ item, actions, price, owne {owner && (
- {formatOwner(owner)} + {shortenPrincipal(owner)} + {ownerLinks.map((link) => ( + + · + e.stopPropagation()} + className="text-primary hover:underline" + > + {link.label} + + + ))}
)} {isSelected && duration > 0 && ( diff --git a/src/alex_frontend/core/features/sonora/hooks/useMarketAudioNFTs.ts b/src/alex_frontend/core/features/sonora/hooks/useMarketAudioNFTs.ts index 3f380dee8..3419093e8 100644 --- a/src/alex_frontend/core/features/sonora/hooks/useMarketAudioNFTs.ts +++ b/src/alex_frontend/core/features/sonora/hooks/useMarketAudioNFTs.ts @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useCallback } from "react"; import { useAppSelector } from "@/store/hooks/useAppSelector"; import { useAppDispatch } from "@/store/hooks/useAppDispatch"; import { fetchMarketAudioNFTs } from "../marketSlice"; @@ -9,14 +9,14 @@ export const useMarketAudioNFTs = () => { const user = useAppSelector((state) => state.auth.user); const { audios, loading, loadingMore, error, pagination } = useAppSelector((state) => state.sonora.market); - const refreshMarketAudioNFTs = (page = 1, pageSize = 8, appendMode = false) => { - dispatch(fetchMarketAudioNFTs({ - page, - pageSize, - appendMode, - currentUserPrincipal: user?.principal + const refreshMarketAudioNFTs = useCallback((page = 1, pageSize = 8, appendMode = false) => { + dispatch(fetchMarketAudioNFTs({ + page, + pageSize, + appendMode, + currentUserPrincipal: user?.principal })); - }; + }, [dispatch, user?.principal]); return { audios, diff --git a/src/alex_frontend/core/features/sonora/hooks/useSonoraProfileTarget.ts b/src/alex_frontend/core/features/sonora/hooks/useSonoraProfileTarget.ts new file mode 100644 index 000000000..c299e049b --- /dev/null +++ b/src/alex_frontend/core/features/sonora/hooks/useSonoraProfileTarget.ts @@ -0,0 +1,10 @@ +import { useParams } from "@tanstack/react-router"; +import { useAppSelector } from "@/store/hooks/useAppSelector"; + +export const useSonoraProfileTarget = () => { + const { user } = useAppSelector((state) => state.auth); + const { principal } = useParams({ strict: false }); + const targetPrincipal = principal ?? user?.principal; + const isOwner = !principal || principal === user?.principal; + return { targetPrincipal, isOwner }; +}; diff --git a/src/alex_frontend/core/features/sonora/hooks/useStudioAudioNFTs.ts b/src/alex_frontend/core/features/sonora/hooks/useStudioAudioNFTs.ts index 77d65acdb..08d2228dd 100644 --- a/src/alex_frontend/core/features/sonora/hooks/useStudioAudioNFTs.ts +++ b/src/alex_frontend/core/features/sonora/hooks/useStudioAudioNFTs.ts @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useCallback } from "react"; import { useAppSelector } from "@/store/hooks/useAppSelector"; import { useAppDispatch } from "@/store/hooks/useAppDispatch"; import { fetchStudioAudioNFTs } from "../studioSlice"; @@ -8,9 +8,9 @@ export const useStudioAudioNFTs = () => { const dispatch = useAppDispatch(); const { audios, loading, loadingMore, error, pagination } = useAppSelector((state) => state.sonora.studio); - const refreshStudioAudioNFTs = (userPrincipal: string, page = 1, pageSize = 8, appendMode = false) => { + const refreshStudioAudioNFTs = useCallback((userPrincipal: string, page = 1, pageSize = 8, appendMode = false) => { dispatch(fetchStudioAudioNFTs({ userPrincipal, page, pageSize, appendMode })); - }; + }, [dispatch]); return { audios, diff --git a/src/alex_frontend/core/layouts/SonoraLayout.tsx b/src/alex_frontend/core/layouts/SonoraLayout.tsx index 61e0f95c4..edac00743 100644 --- a/src/alex_frontend/core/layouts/SonoraLayout.tsx +++ b/src/alex_frontend/core/layouts/SonoraLayout.tsx @@ -1,6 +1,18 @@ import React from "react"; import { Link, Outlet } from "@tanstack/react-router"; +const tabs = [ + { to: "/app/sonora", label: "Browse", exact: true }, + { to: "/app/sonora/record", label: "Record" }, + { to: "/app/sonora/archive", label: "Archive" }, + { to: "/app/sonora/studio", label: "Studio" }, + { to: "/app/sonora/market", label: "Market" }, +] as const; + +const tabClass = "px-6 py-2 rounded-bordertb font-medium text-tabsheading transition-colors duration-200"; +const activeProps = { className: "bg-primary text-primary-foreground shadow-md" }; +const inactiveProps = { className: "bg-transparent text-muted-foreground hover:bg-muted hover:text-primary" }; + function SonoraLayout() { return ( <> @@ -11,60 +23,18 @@ function SonoraLayout() {
diff --git a/src/alex_frontend/lbry/src/pages/sonora/ArchivePage.tsx b/src/alex_frontend/lbry/src/pages/sonora/ArchivePage.tsx index 3b9c3b58d..c042e1eeb 100644 --- a/src/alex_frontend/lbry/src/pages/sonora/ArchivePage.tsx +++ b/src/alex_frontend/lbry/src/pages/sonora/ArchivePage.tsx @@ -8,16 +8,18 @@ import { FileAudio, Download, ArrowDown, + ArrowLeft, LoaderPinwheel, } from "lucide-react"; import { AudioCard } from "@/features/sonora/components/AudioCard"; import { SellButton } from "@/features/sonora/components/SellButton"; import { useUserAudioNFTs } from "@/features/sonora/hooks/useUserAudioNFTs"; -import { useAppSelector } from "@/store/hooks/useAppSelector"; +import { useSonoraProfileTarget } from "@/features/sonora/hooks/useSonoraProfileTarget"; import { ArchiveAudio } from "@/features/sonora/types"; +import { shortenPrincipal } from "@/features/perpetua"; const SonoraArchivePage: React.FC = () => { - const { user } = useAppSelector((state) => state.auth); + const { targetPrincipal, isOwner } = useSonoraProfileTarget(); const { audios, loading, @@ -27,179 +29,207 @@ const SonoraArchivePage: React.FC = () => { refreshAudioNFTs, } = useUserAudioNFTs(); - // Fetch user's audio NFTs when component mounts or user changes useEffect(() => { - if (user?.principal) { - refreshAudioNFTs(user.principal, 1, false); // First page, not append mode + if (targetPrincipal) { + refreshAudioNFTs(targetPrincipal, 1, false); } - }, [user?.principal, refreshAudioNFTs]); + }, [targetPrincipal, refreshAudioNFTs]); - // Load More handler const handleLoadMore = () => { - if (user?.principal && !loadingMore && pagination.hasMore) { - refreshAudioNFTs(user.principal, pagination.page + 1, true); // Next page, append mode + if (targetPrincipal && !loadingMore && pagination.hasMore) { + refreshAudioNFTs(targetPrincipal, pagination.page + 1, true); } }; return ( <> - - My Collection | Sonora | Alexandria - - -
- {/* Left Sidebar */} -
-
-
-
- -

Your Collection

-
-

- Your personal audio NFT collection and uploaded - content. -

-
-
- - Owned by you -
+ + {isOwner ? "My Archive" : `${shortenPrincipal(targetPrincipal ?? "")}'s Archive`} | Sonora | Alexandria + + + {!isOwner && ( +
+ + + Back to my archive + +
+ )} +
+ {/* Left Sidebar */} +
+
+
- - Ready to trade + +

{isOwner ? "Your Archive" : "Archive"}

-
- - Download anytime +

+ {isOwner + ? "Your personal audio NFT collection and uploaded content." + : `Viewing ${shortenPrincipal(targetPrincipal ?? "")}'s audio NFT collection.`} +

+
+
+ + {isOwner ? "Owned by you" : "Owned by this user"} +
+
+ + Ready to trade +
+
+ + Download anytime +
-
-
-
-
- -

Manage Content

-
-

- Download, share, or list your audio NFTs for sale. -

-
-
-

- Available actions: -

-
    -
  • • Download original files
  • -
  • • Share with others
  • -
  • • List on marketplace
  • -
+
+
+
+ +

{isOwner ? "Manage Content" : "Collection Info"}

-
-
-

- - {audios.length} - {" "} - of{" "} - - {pagination.totalCount} - {" "} - items +

+ {isOwner + ? "Download, share, or list your audio NFTs for sale." + : "Audio NFTs minted by this user on Alexandria."}

- - → View your listings - + {isOwner && ( +
+
+

+ Available actions: +

+
    +
  • • Download original files
  • +
  • • Share with others
  • +
  • • List on marketplace
  • +
+
+
+ )} +
+

+ + {audios.length} + {" "} + of{" "} + + {pagination.totalCount} + {" "} + items +

+ {isOwner ? ( + + → View your listings + + ) : ( + + → View their listings + + )} +
-
- {/* Load More */} - {pagination.hasMore && ( - - )} -
+ {/* Load More */} + {pagination.hasMore && ( + + )} +
- {/* Audio Grid */} -
- {loading ? ( -
-
- -

- Loading your audio NFTs... -

+ {/* Audio Grid */} +
+ {loading ? ( +
+
+ +

+ Loading your audio NFTs... +

+
-
- ) : error ? ( -
-
-

- Error loading NFTs -

-

- {error} -

- + ) : error ? ( +
+
+

+ Error loading NFTs +

+

+ {error} +

+ +
-
- ) : audios.length === 0 ? ( -
-
- -

- No Audio NFTs Found -

-

- You haven't minted any audio NFTs yet. -

+ ) : audios.length === 0 ? ( +
+
+ +

+ No Audio NFTs Found +

+

+ {isOwner + ? "You haven't minted any audio NFTs yet." + : "This user hasn't minted any audio NFTs yet."} +

+
-
- ) : ( - audios.map((item: ArchiveAudio) => ( - - } - /> - )) - )} + ) : ( + audios.map((item: ArchiveAudio) => ( + + ) : undefined + } + /> + )) + )} +
-
); }; diff --git a/src/alex_frontend/lbry/src/pages/sonora/BrowsePage.tsx b/src/alex_frontend/lbry/src/pages/sonora/BrowsePage.tsx index 47937a173..c7b56a845 100644 --- a/src/alex_frontend/lbry/src/pages/sonora/BrowsePage.tsx +++ b/src/alex_frontend/lbry/src/pages/sonora/BrowsePage.tsx @@ -17,32 +17,29 @@ import { fetchAudios } from "@/features/sonora/browse/thunks/fetchAudios"; import { useAppDispatch } from "@/store/hooks/useAppDispatch"; import { ArweaveAudio, Audio } from "@/features/sonora/types"; +const convertToAudio = (arweaveAudio: ArweaveAudio): Audio => { + let contentType = arweaveAudio.data?.type; + if (!contentType) { + const contentTypeTag = arweaveAudio.tags?.find( + (tag) => tag.name === "Content-Type" + ); + contentType = contentTypeTag?.value || ""; + } + return { + id: arweaveAudio.id, + type: contentType, + size: arweaveAudio.data?.size || null, + timestamp: new Date( + arweaveAudio.block.timestamp * 1000 + ).toISOString(), + }; +}; + const BrowsePage: React.FC = () => { const dispatch = useAppDispatch(); const { audios, loading, error, hasNext, loadMore, isEmpty } = useArweaveAudios(); - // Convert ArweaveAudio to Audio format for AudioCard - const convertToAudio = (arweaveAudio: ArweaveAudio): Audio => { - // Try to get content type from data.type or Content-Type tag - let contentType = arweaveAudio.data?.type; - if (!contentType) { - const contentTypeTag = arweaveAudio.tags?.find( - (tag) => tag.name === "Content-Type" - ); - contentType = contentTypeTag?.value || ""; - } - - return { - id: arweaveAudio.id, - type: contentType, - size: arweaveAudio.data?.size || null, - timestamp: new Date( - arweaveAudio.block.timestamp * 1000 - ).toISOString(), - }; - }; - // Fetch initial data on mount useEffect(() => { if (audios.length === 0 && !loading && !error) { diff --git a/src/alex_frontend/lbry/src/pages/sonora/MarketPage.tsx b/src/alex_frontend/lbry/src/pages/sonora/MarketPage.tsx index 0d4a5068e..556435218 100644 --- a/src/alex_frontend/lbry/src/pages/sonora/MarketPage.tsx +++ b/src/alex_frontend/lbry/src/pages/sonora/MarketPage.tsx @@ -26,10 +26,9 @@ const SonoraMarketPage: React.FC = () => { refreshMarketAudioNFTs, } = useMarketAudioNFTs(); - // Fetch market audio NFTs on component mount useEffect(() => { - refreshMarketAudioNFTs(1, 8, false); // First page, not append mode - }, []); + refreshMarketAudioNFTs(1, 8, false); + }, [refreshMarketAudioNFTs]); // Load More handler const handleLoadMore = () => { @@ -40,167 +39,167 @@ const SonoraMarketPage: React.FC = () => { return ( <> - - Audio Marketplace | Sonora | Alexandria - - -
- {/* Left Sidebar */} -
-
-
-
- -

Marketplace

-
-

- Discover and purchase audio NFTs from creators - worldwide. -

-
-
- - Trending content -
+ + Audio Marketplace | Sonora | Alexandria + + +
+ {/* Left Sidebar */} +
+
+
- - Search by genre + +

Marketplace

-
- - Filter by price +

+ Discover and purchase audio NFTs from creators + worldwide. +

+
+
+ + Trending content +
+
+ + Search by genre +
+
+ + Filter by price +
-
-
-
-
- -

Buy & Own

-
-

- Purchase audio NFTs with ALEX or LBRY tokens and own - them forever. -

-
-
-

- How it works: -

-
    -
  • • Preview before buying
  • -
  • • Secure blockchain purchase
  • -
  • • Instant ownership transfer
  • -
+
+
+
+ +

Buy & Own

-
-
-

- - {audios.length} - {" "} - of{" "} - - {pagination.totalCount} - {" "} - items shown +

+ Purchase audio NFTs with ALEX or LBRY tokens and own + them forever.

- - → Manage your listings - +
+
+

+ How it works: +

+
    +
  • • Preview before buying
  • +
  • • Secure blockchain purchase
  • +
  • • Instant ownership transfer
  • +
+
+
+
+

+ + {audios.length} + {" "} + of{" "} + + {pagination.totalCount} + {" "} + items shown +

+ + → Manage your listings + +
-
- {/* Load More */} - {pagination.page < pagination.totalPages && ( - - )} -
+ {/* Load More */} + {pagination.page < pagination.totalPages && ( + + )} +
- {/* Audio Grid */} -
- {loading ? ( -
-
- -

- Loading marketplace audio NFTs... -

+ {/* Audio Grid */} +
+ {loading ? ( +
+
+ +

+ Loading marketplace audio NFTs... +

+
-
- ) : error ? ( -
-
-

- Error loading marketplace -

-

- {error} -

- + ) : error ? ( +
+
+

+ Error loading marketplace +

+

+ {error} +

+ +
-
- ) : audios.length === 0 ? ( -
-
- -

- No Items for Sale -

-

- There are currently no audio NFTs listed in the - marketplace. -

+ ) : audios.length === 0 ? ( +
+
+ +

+ No Items for Sale +

+

+ There are currently no audio NFTs listed in the + marketplace. +

+
-
- ) : ( - audios.map((audio: MarketAudio) => ( - - } - /> - )) - )} + ) : ( + audios.map((audio: MarketAudio) => ( + + } + /> + )) + )} +
-
); }; diff --git a/src/alex_frontend/lbry/src/pages/sonora/RecordPage.tsx b/src/alex_frontend/lbry/src/pages/sonora/RecordPage.tsx index 0d04f8025..ed4034fd4 100644 --- a/src/alex_frontend/lbry/src/pages/sonora/RecordPage.tsx +++ b/src/alex_frontend/lbry/src/pages/sonora/RecordPage.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef } from "react"; +import React, { useState, useRef, useEffect } from "react"; import { Helmet } from "react-helmet-async"; import { Button } from "@/lib/components/button"; import { Mic, Square, Upload, Trash2, LoaderPinwheel } from "lucide-react"; @@ -34,6 +34,26 @@ const SonoraRecordPage: React.FC = () => { const dispatch = useAppDispatch(); const { selected } = useAppSelector((state) => state.sonora); + useEffect(() => { + return () => { + streamRef.current?.getTracks().forEach((t) => t.stop()); + if (timerRef.current) clearInterval(timerRef.current); + const recorder = mediaRecorderRef.current; + if (recorder?.state === "recording") { + recorder.onstop = null; + recorder.stop(); + } + }; + }, []); + + useEffect(() => { + return () => { + if (recordedUrl.startsWith("blob:")) { + URL.revokeObjectURL(recordedUrl); + } + }; + }, [recordedUrl]); + const startRecording = async () => { try { setRecordingError(""); @@ -156,177 +176,177 @@ const SonoraRecordPage: React.FC = () => { return ( <> - - Record Audio | Sonora | Alexandria - - -
-
-
-

- Record audio content directly in your browser -

-
- - {/* Recording Interface */} -
-
- {/* Recording Status */} -
-
- -
+ + Record Audio | Sonora | Alexandria + + +
+
+
+

+ Record audio content directly in your browser +

+
-
- {formatTime(recordingTime)} + {/* Recording Interface */} +
+
+ {/* Recording Status */} +
+
+ +
+ +
+ {formatTime(recordingTime)} +
+ + {isRecording && ( +

+ Recording in progress... +

+ )} + + {recordedBlob && !isRecording && ( +

+ Recording completed +

+ )}
- {isRecording && ( -

- Recording in progress... -

+ {/* Error Message */} + {recordingError && ( +
+

+ {recordingError} +

+
)} - {recordedBlob && !isRecording && ( -

- Recording completed -

- )} -
- - {/* Error Message */} - {recordingError && ( -
-

- {recordingError} -

+ {/* Control Buttons */} +
+ {!isRecording && !recordedBlob && ( + + )} + + {isRecording && ( + + )}
- )} - - {/* Control Buttons */} -
- {!isRecording && !recordedBlob && ( - - )} +
- {isRecording && ( - - )} + {/* Upload option below recording box */} +
+

+ + + +

- {/* Upload option below recording box */} -
-

- + {/* Audio Preview */} + {(recordedBlob || recordedUrl) && selected && !isRecording && ( +

+
+

+ {recordedBlob + ? "Preview" + : "Uploaded to Arweave"} +

- -

-
-
+
+ +
+ )} + + {/* Upload Error and Success Messages */} + {uploadError && ( +
+

+ {uploadError} +

+
+ )} + + {success && ( +
+

{success}

+
+ )} - {/* Audio Preview */} - {(recordedBlob || recordedUrl) && selected && !isRecording && ( -
-
-

- {recordedBlob - ? "Preview" - : "Uploaded to Arweave"} -

+ {/* Upload Button */} + {recordedBlob && ( +
- -
- )} - - {/* Upload Error and Success Messages */} - {uploadError && ( -
-

- {uploadError} -

-
- )} - - {success && ( -
-

{success}

-
- )} - - {/* Upload Button */} - {recordedBlob && ( -
- -
- )} + )} +
-
); }; diff --git a/src/alex_frontend/lbry/src/pages/sonora/StudioPage.tsx b/src/alex_frontend/lbry/src/pages/sonora/StudioPage.tsx index 7ba4b9b9c..fcedc5db3 100644 --- a/src/alex_frontend/lbry/src/pages/sonora/StudioPage.tsx +++ b/src/alex_frontend/lbry/src/pages/sonora/StudioPage.tsx @@ -1,12 +1,14 @@ import React, { useEffect } from "react"; import { Helmet } from "react-helmet-async"; import { Button } from "@/lib/components/button"; +import { Link } from "@tanstack/react-router"; import { Palette, Settings, TrendingUp, BarChart3, ArrowDown, + ArrowLeft, FileAudio, LoaderPinwheel, } from "lucide-react"; @@ -14,11 +16,12 @@ import { AudioCard } from "@/features/sonora/components/AudioCard"; import { EditButton } from "@/features/sonora/components/EditButton"; import { UnlistButton } from "@/features/sonora/components/UnlistButton"; import { useStudioAudioNFTs } from "@/features/sonora/hooks/useStudioAudioNFTs"; -import { useAppSelector } from "@/store/hooks/useAppSelector"; +import { useSonoraProfileTarget } from "@/features/sonora/hooks/useSonoraProfileTarget"; import { StudioAudio } from "@/features/sonora/types"; +import { shortenPrincipal } from "@/features/perpetua"; const SonoraStudioPage: React.FC = () => { - const { user } = useAppSelector((state) => state.auth); + const { targetPrincipal, isOwner } = useSonoraProfileTarget(); const { audios, loading, @@ -28,200 +31,217 @@ const SonoraStudioPage: React.FC = () => { refreshStudioAudioNFTs, } = useStudioAudioNFTs(); - // Fetch user's listed audio NFTs on component mount useEffect(() => { - if (user?.principal) { - refreshStudioAudioNFTs(user.principal, 1, 8, false); // First page, not append mode + if (targetPrincipal) { + refreshStudioAudioNFTs(targetPrincipal, 1, 8, false); } - }, [user?.principal]); + }, [targetPrincipal, refreshStudioAudioNFTs]); - // Load More handler const handleLoadMore = () => { if ( - user?.principal && + targetPrincipal && !loadingMore && pagination.page < pagination.totalPages ) { refreshStudioAudioNFTs( - user.principal, + targetPrincipal, pagination.page + 1, 8, true - ); // Next page, append mode + ); } }; return ( <> - - Creator Studio | Sonora | Alexandria - - -
- {/* Left Sidebar */} -
-
-
-
- -

Creator Studio

-
-

- Manage your listed audio NFTs and marketplace - presence. -

-
-
- - Track sales -
-
- - Edit listings -
+ + {isOwner ? "My Studio" : `${shortenPrincipal(targetPrincipal ?? "")}'s Studio`} | Sonora | Alexandria + + + {!isOwner && ( +
+ + + Back to my studio + +
+ )} +
+ {/* Left Sidebar */} +
+
+
- - View analytics + +

{isOwner ? "Creator Studio" : "Studio"}

+

+ {isOwner + ? "Manage your listed audio NFTs and marketplace presence." + : `Viewing ${shortenPrincipal(targetPrincipal ?? "")}'s listed audio NFTs.`} +

+ {isOwner && ( +
+
+ + Track sales +
+
+ + Edit listings +
+
+ + View analytics +
+
+ )}
-
-
-
-
- -

Manage Listings

-
-

- Edit metadata, pricing, and availability of your - audio NFTs. -

-
-
-

- Available actions: + {isOwner && ( +

+
+
+ +

Manage Listings

+
+

+ Edit metadata, pricing, and availability of your + audio NFTs.

-
    -
  • • Edit titles and descriptions
  • -
  • • Update pricing
  • -
  • • Remove from marketplace
  • -
+
+
+

+ Available actions: +

+
    +
  • • Edit titles and descriptions
  • +
  • • Update pricing
  • +
  • • Remove from marketplace
  • +
+
+
+
+

+ + {audios.length} + {" "} + of{" "} + + {pagination.totalCount} + {" "} + listings shown +

+
-
-

- - {audios.length} - {" "} - of{" "} - - {pagination.totalCount} - {" "} - listings shown -

-
-
-
+ )} - {/* Load More */} - {pagination.page < pagination.totalPages && ( - - )} -
+ {/* Load More */} + {pagination.page < pagination.totalPages && ( + + )} +
- {/* Audio Grid */} -
- {loading ? ( -
-
- -

- Loading your listed audio NFTs... -

+ {/* Audio Grid */} +
+ {loading ? ( +
+
+ +

+ Loading your listed audio NFTs... +

+
-
- ) : error ? ( -
-
-

- Error loading listed audio NFTs -

-

- {error} -

- + ) : error ? ( +
+
+

+ Error loading listed audio NFTs +

+

+ {error} +

+ +
-
- ) : audios.length === 0 ? ( -
-
- -

- No Active Listings -

-

- You don't have any audio NFTs listed for sale - yet. -

+ ) : audios.length === 0 ? ( +
+
+ +

+ No Active Listings +

+

+ {isOwner + ? "You don't have any audio NFTs listed for sale yet." + : "This user doesn't have any audio NFTs listed for sale."} +

+
-
- ) : ( - audios.map((item: StudioAudio) => ( - - - - - } - /> - )) - )} + ) : ( + audios.map((item: StudioAudio) => ( + + + + + ) : undefined + } + /> + )) + )} +
-
); }; diff --git a/src/alex_frontend/lbry/src/pages/sonora/UploadPage.tsx b/src/alex_frontend/lbry/src/pages/sonora/UploadPage.tsx index 69328b2eb..afbb83b93 100644 --- a/src/alex_frontend/lbry/src/pages/sonora/UploadPage.tsx +++ b/src/alex_frontend/lbry/src/pages/sonora/UploadPage.tsx @@ -1,5 +1,6 @@ -import React, { useState, useRef } from "react"; +import React, { useState, useRef, useEffect } from "react"; import { Helmet } from "react-helmet-async"; +import { toast } from "sonner"; import { Button } from "@/lib/components/button"; import { Upload, Mic, X, LoaderPinwheel } from "lucide-react"; import { Link } from "@tanstack/react-router"; @@ -29,10 +30,18 @@ const SonoraUploadPage: React.FC = () => { resetUpload, } = useUploadAndMint(); + useEffect(() => { + return () => { + if (audioUrl.startsWith("blob:")) { + URL.revokeObjectURL(audioUrl); + } + }; + }, [audioUrl]); + const handleFileSelect = (file: File) => { // Validate audio file if (!file.type.startsWith("audio/")) { - alert("Please select an audio file"); + toast.error("Please select an audio file"); return; } @@ -115,142 +124,142 @@ const SonoraUploadPage: React.FC = () => { return ( <> - - Upload Audio | Sonora | Alexandria - - -
-
-
-

- Share your audio content with the world -

-
+ + Upload Audio | Sonora | Alexandria + + +
+
+
+

+ Share your audio content with the world +

+
- {/* File Upload Area */} -
-
- + {/* File Upload Area */} +
+
+ -
- -
-

- {selectedFile - ? selectedFile.name - : "Drop your audio file here"} -

-

- or click to browse • Supports MP3, WAV, OGG -

+
+ +
+

+ {selectedFile + ? selectedFile.name + : "Drop your audio file here"} +

+

+ or click to browse • Supports MP3, WAV, OGG +

+
+ + {/* Record option below upload box */} +
+

+ + + +

+
- {/* Record option below upload box */} -
-

- + {/* Audio Preview */} + {selectedFile && selected && ( +

+
+

+ {audioUrl.startsWith("blob:") + ? "Preview" + : "Uploaded to Arweave"} +

- -

-
-
+
+ +
+ )} + + {/* Error and Success Messages */} + {error && ( +
+

{error}

+
+ )} - {/* Audio Preview */} - {selectedFile && selected && ( -
-
-

- {audioUrl.startsWith("blob:") - ? "Preview" - : "Uploaded to Arweave"} -

+ {success && ( +
+

{success}

+
+ )} + + {/* Upload Button */} + {selectedFile && audioUrl.startsWith("blob:") && ( +
- -
- )} - - {/* Error and Success Messages */} - {error && ( -
-

{error}

-
- )} - - {success && ( -
-

{success}

-
- )} - - {/* Upload Button */} - {selectedFile && audioUrl.startsWith("blob:") && ( -
- -
- )} + )} +
-
); };