Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 199 additions & 0 deletions deno.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"date-fns-tz": "^3.2.0",
"embla-carousel-react": "^8.3.0",
"franc": "^6.2.0",
"framer-motion": "^12.23.12",
"idb": "^8.0.3",
"input-otp": "^1.2.4",
"lucide-react": "^0.462.0",
Expand Down
38 changes: 38 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/components/router/EditionRoutes.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Navigate, Route } from "react-router-dom";
import EditionView from "@/pages/EditionView/EditionView";
import { SetDetails } from "@/pages/SetDetails";
import { ExploreSetPage } from "@/pages/ExploreSetPage/ExploreSetPage";

// Tab components
import { ArtistsTab } from "@/pages/EditionView/tabs/ArtistsTab/ArtistsTab";
Expand Down Expand Up @@ -28,6 +29,10 @@ export function createEditionRoutes({
? () => <WrapperComponent component={SetDetails} />
: SetDetails;

const ExploreComponent = WrapperComponent
? () => <WrapperComponent component={ExploreSetPage} />
: ExploreSetPage;

return [
<Route key="main" path={basePath} element={<EditionComponent />}>
{/* Nested tab routes */}
Expand All @@ -47,5 +52,10 @@ export function createEditionRoutes({
path={`${basePath}/sets/:setSlug`}
element={<SetDetailsComponent />}
/>,
<Route
key="explore"
path={`${basePath}/explore`}
element={<ExploreComponent />}
/>,
];
}
42 changes: 42 additions & 0 deletions src/hooks/mutations/useSyncSoundCloudDataMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useMutation } from "@tanstack/react-query";
import { supabase } from "@/integrations/supabase/client";
import { toast } from "sonner";

type SyncResponse = {
message: string;
artistsToProcess: number;
startedAt: string;
};

async function syncSoundCloudData(): Promise<SyncResponse> {
const { data, error } = await supabase.functions.invoke("sync-artist-data", {
body: {},
});

if (error) {
throw new Error(error.message || "Failed to start SoundCloud sync");
}

return data;
}

export function useSyncSoundCloudDataMutation() {
return useMutation({
mutationFn: syncSoundCloudData,
onSuccess: (data) => {
toast.success(
`SoundCloud sync started! Processing ${data.artistsToProcess} artists.`,
{
description:
"The sync is running in the background. Check back in a few minutes.",
duration: 5000,
},
);
},
onError: (error) => {
toast.error("Failed to start SoundCloud sync", {
description: error.message,
});
},
});
}
23 changes: 22 additions & 1 deletion src/hooks/queries/artists/useArtists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Database } from "@/integrations/supabase/types";

export type Artist = Database["public"]["Tables"]["artists"]["Row"] & {
artist_music_genres: { music_genre_id: string }[] | null;
soundcloud_followers?: number;
};

// Query key factory
Expand Down Expand Up @@ -34,7 +35,27 @@ async function fetchArtists(): Promise<Artist[]> {
throw new Error("Failed to fetch artists");
}

return data || [];
const { data: soundcloudData, error: soundcloudError } = await supabase
.from("soundcloud")
.select("artist_id, followers_count");

if (soundcloudError) {
console.error("Error fetching soundcloud data:", soundcloudError);
throw new Error("Failed to fetch soundcloud data");
}

const soundcloudMap = new Map(
soundcloudData?.map((sc) => [sc.artist_id, sc.followers_count]) || [],
);

return (
data.map((artist) => {
return {
...artist,
soundcloud_followers: soundcloudMap.get(artist.id) || 0,
};
}) || []
);
}

// Hook
Expand Down
130 changes: 130 additions & 0 deletions src/hooks/useArtistSoundcloudPlaylist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { useQuery } from "@tanstack/react-query";
import { supabase } from "@/integrations/supabase/client";
import { FunctionsHttpError } from "@supabase/supabase-js";

// Enhanced error class that includes error codes
export class SoundCloudError extends Error {
public readonly code: string;

constructor(message: string, code: string = "UNKNOWN_ERROR") {
super(message);
this.name = "SoundCloudError";
this.code = code;
}
}

// Type guard for SoundCloudError
export function isSoundCloudError(error: unknown): error is SoundCloudError {
return error instanceof SoundCloudError;
}

// SoundCloud API response types
interface SoundCloudUser {
id: number;
username: string;
permalink_url: string;
}

interface SoundCloudPlaylist {
id: number;
title: string;
description: string | null;
permalink_url: string;
artwork_url: string | null;
user: SoundCloudUser;
track_count: number;
tracks?: SoundCloudTrack[];
likes_count?: number;
reposts_count?: number;
created_at: string;
}

interface SoundCloudTrack {
id: number;
title: string;
permalink_url: string;
stream_url?: string;
duration: number;
artwork_url: string | null;
}

interface UseArtistSoundcloudPlaylistOptions {
soundcloudUrl: string;
enabled?: boolean;
}

async function fetchArtistPlaylist(
soundcloudUrl: string,
): Promise<SoundCloudPlaylist> {
console.log(
"[fetchArtistPlaylist] Calling edge function for:",
soundcloudUrl,
);

const { data, error } = await supabase.functions.invoke(
"get-artist-soundcloud-playlist",
{
body: { url: soundcloudUrl },
},
);

if (error) {
console.error("[fetchArtistPlaylist] Edge function error:", error);

// Handle FunctionsHttpError to get the actual error details
if (!(error instanceof FunctionsHttpError)) {
throw error;
}

let soundcloudError: SoundCloudError | null = null;
try {
const errorDetails = await error.context.json();
console.error(
"[fetchArtistPlaylist] Function returned error:",
errorDetails,
);

// Use the specific error message and code from our edge function
const message =
errorDetails.error || "Failed to fetch SoundCloud playlist";
const errorCode = errorDetails.code || "UNKNOWN_ERROR";

soundcloudError = new SoundCloudError(message, errorCode);
} catch (parseError) {
console.error(
"[fetchArtistPlaylist] Failed to parse error response:",
parseError,
);
throw new Error("Failed to fetch SoundCloud playlist");
}

if (soundcloudError) {
throw soundcloudError;
}
}

if (!data?.playlist) {
console.error("[fetchArtistPlaylist] No playlist in response:", data);
throw new Error("No playlist found in response");
}

console.log("[fetchArtistPlaylist] Received playlist:", data.playlist.title);
return data.playlist;
}

export function useArtistSoundcloudPlaylist({
soundcloudUrl,
enabled = true,
}: UseArtistSoundcloudPlaylistOptions) {
return useQuery({
queryKey: ["soundcloud-playlist", soundcloudUrl],
queryFn: () => fetchArtistPlaylist(soundcloudUrl),
enabled: enabled && Boolean(soundcloudUrl),
retry(failureCount, error) {
if (isSoundCloudError(error)) {
return false;
}
return failureCount < 2;
},
});
}
Loading
Loading