diff --git a/package-lock.json b/package-lock.json index 2117111..9c16f97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9709,4 +9709,4 @@ } } } -} \ No newline at end of file +} diff --git a/vynl/__tests__/party-mode-test.ts b/vynl/__tests__/party-mode-test.ts index 1379f83..d49d121 100644 --- a/vynl/__tests__/party-mode-test.ts +++ b/vynl/__tests__/party-mode-test.ts @@ -195,6 +195,7 @@ describe('Party Playlist Test', () => { console.log(await res.text()); } + console.log(await res.text()); expect(res.ok).toBeTruthy(); const { data: e_data, error: e_err } = await adminClient diff --git a/vynl/__tests__/playlist-endpoints-test.ts b/vynl/__tests__/playlist-endpoints-test.ts index 899fad4..87a91ba 100644 --- a/vynl/__tests__/playlist-endpoints-test.ts +++ b/vynl/__tests__/playlist-endpoints-test.ts @@ -197,8 +197,6 @@ describe('Playlist Test', () => { .from('songs') .select(); - console.log(songs_data); - expect(songs_data).toBeDefined(); expect(songs_error).toBeNull(); for (let i = 0; i < filled_playlist.songs.length; i++) { diff --git a/vynl/package-lock.json b/vynl/package-lock.json index 891575d..44dedea 100644 --- a/vynl/package-lock.json +++ b/vynl/package-lock.json @@ -62,7 +62,7 @@ "jest": "^29.7.0", "jest-expo": "^54.0.13", "react-test-renderer": "19.1.0", - "supabase": "^2.54.11", + "supabase": "^2.65.6", "ts-node": "^10.9.2", "typescript": "~5.9.2" } @@ -15421,9 +15421,9 @@ } }, "node_modules/supabase": { - "version": "2.62.5", - "resolved": "https://registry.npmjs.org/supabase/-/supabase-2.62.5.tgz", - "integrity": "sha512-KjR57sEwNpTLOMHo+Nt9bHtq9RGWV0GGp6MWALp7RQtOcZdUopUOH+hoOojAuVyk4ChOipB7POu0y3vssW272A==", + "version": "2.65.6", + "resolved": "https://registry.npmjs.org/supabase/-/supabase-2.65.6.tgz", + "integrity": "sha512-PKeKFwIpx/H65WZ8BVqvQU1cve2n2Er++Yo0EGjSfV/vNwnLkpdnlpD9o8ZJJLDF/hxlduOCwxHiTnBimnbXdA==", "dev": true, "hasInstallScript": true, "license": "MIT", diff --git a/vynl/package.json b/vynl/package.json index 2cbe088..acb645e 100644 --- a/vynl/package.json +++ b/vynl/package.json @@ -69,7 +69,7 @@ "jest": "^29.7.0", "jest-expo": "^54.0.13", "react-test-renderer": "19.1.0", - "supabase": "^2.54.11", + "supabase": "^2.65.6", "ts-node": "^10.9.2", "typescript": "~5.9.2" }, diff --git a/vynl/src/app/(tabs)/HostParty.tsx b/vynl/src/app/(tabs)/HostParty.tsx index ae2c692..5a58ab4 100644 --- a/vynl/src/app/(tabs)/HostParty.tsx +++ b/vynl/src/app/(tabs)/HostParty.tsx @@ -18,22 +18,13 @@ const PARTY_CODE_STORAGE_KEY = '@vynl:partyCode'; // Image assets const imgBackground = require('@/assets/images/background.png'); -// Generate a random 6-digit alphanumeric code -const generatePartyCode = (): string => { - const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - let code = ''; - for (let i = 0; i < 6; i++) { - code += chars.charAt(Math.floor(Math.random() * chars.length)); - } - return code; -}; - export default function HostPartyScreen() { const router = useRouter(); const params = useLocalSearchParams(); const playlistId = params.playlistId as string | undefined; const [partyCode, setPartyCode] = useState(''); const [isCreating, setIsCreating] = useState(false); + const [isFetchingCode, setIsFetchingCode] = useState(false); const { createPlaylist } = useCreatePlaylist(); const { authToken, loading: authLoading } = useAuth(); const { playlist, loading: playlistLoading } = usePlaylistWithID(playlistId || null); @@ -41,16 +32,121 @@ export default function HostPartyScreen() { Poppins: Poppins_400Regular, }); + // Load party code from AsyncStorage when component mounts (for quick display) + // The fresh code will be fetched when user clicks "START PARTY" useEffect(() => { - // Generate code when component mounts - setPartyCode(generatePartyCode()); - }, []); + const loadStoredPartyCode = async () => { + if (!playlistId || !playlist) return; + + try { + const storedParties = await AsyncStorage.getItem(PARTY_CODE_STORAGE_KEY); + if (storedParties) { + const parties = JSON.parse(storedParties); + // Handle migration from old format (single object) to new format (array) + const partiesArray = Array.isArray(parties) + ? parties + : (parties.playlistId ? [parties] : []); + + const activeParty = partiesArray.find((p: any) => p.playlistId === playlist.id.toString()); + if (activeParty && activeParty.partyCode) { + // Show stored code initially, but it will be updated when "START PARTY" is clicked + setPartyCode(activeParty.partyCode); + } + } + } catch (error) { + console.error('Error loading party code from storage:', error); + } + }; + + loadStoredPartyCode(); + }, [playlistId, playlist]); + + const fetchPartyCode = async (playlistId: number): Promise => { + if (!authToken) { + console.error('No auth token available'); + return null; + } + + try { + const { data: { user }, error: userError } = await supabase.auth.getUser(); + if (userError || !user) { + console.error('Error getting user:', userError); + return null; + } + + const url = `/api/playlist/party/toggle/${playlistId}`; + console.log('Fetching party code from:', url); + console.log('User ID:', user.id); + console.log('Playlist ID:', playlistId); + + const response = await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${authToken}`, + }, + body: JSON.stringify({ + uid: user.id, + enable: true, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Failed to enable party mode:', response.status, errorText); + return null; + } + + const responseText = await response.text(); + console.log('Response text:', responseText); + + // The API returns JSON.stringify(partyCode), so we need to parse it + let code: string; + try { + code = JSON.parse(responseText); + } catch (parseError) { + // If parsing fails, maybe it's already a string? + code = responseText; + } + + if (!code || typeof code !== 'string') { + console.error('Invalid party code format:', code); + return null; + } + + console.log('Party code received:', code); + return code; + } catch (error) { + console.error('Error fetching party code:', error); + if (error instanceof Error) { + console.error('Error message:', error.message); + console.error('Error stack:', error.stack); + } + return null; + } + }; const handleCreateParty = async () => { - if (isCreating || authLoading || !authToken) return; + if (isCreating || authLoading || !authToken || isFetchingCode) return; // If we have a playlist ID, use the existing playlist if (playlistId && playlist) { + setIsFetchingCode(true); + // Clear the old code immediately so user sees we're fetching a fresh one + setPartyCode(''); + + // Fetch party code from API + const fetchedCode = await fetchPartyCode(playlist.id); + + if (!fetchedCode) { + console.error('Failed to get party code from API'); + setIsFetchingCode(false); + return; + } + + // Set the new code + setPartyCode(fetchedCode); + // Save party code to storage (support multiple active parties) try { const existingParties = await AsyncStorage.getItem(PARTY_CODE_STORAGE_KEY); @@ -61,12 +157,12 @@ export default function HostPartyScreen() { if (existingIndex >= 0) { // Update existing party code - parties[existingIndex].partyCode = partyCode; + parties[existingIndex].partyCode = fetchedCode; } else { // Add new party parties.push({ playlistId: playlist.id.toString(), - partyCode: partyCode + partyCode: fetchedCode }); } @@ -75,14 +171,16 @@ export default function HostPartyScreen() { console.error('Error saving party code:', error); } - // Navigate back to playlist detail with party code + setIsFetchingCode(false); + + /* // Navigate back to playlist detail with party code router.push({ pathname: '/(tabs)/playlist-detail', params: { id: playlist.id.toString(), - partyCode: partyCode, + partyCode: fetchedCode, }, - }); + }); */ return; } @@ -109,12 +207,23 @@ export default function HostPartyScreen() { return; } + // Fetch party code from API + setIsFetchingCode(true); + const fetchedCode = await fetchPartyCode(playlist.id); + setIsFetchingCode(false); + + if (!fetchedCode) { + console.error('Failed to get party code from API'); + setIsCreating(false); + return; + } + // Navigate to UploadSongs with party mode params router.push({ pathname: '/(tabs)/UploadSongs', params: { partyMode: 'true', - partyCode: partyCode, + partyCode: fetchedCode, playlistId: playlist.id.toString(), playlistName: playlist.name, playlist: JSON.stringify(playlist), @@ -123,6 +232,7 @@ export default function HostPartyScreen() { } catch (error) { console.error('Error creating party:', error); setIsCreating(false); + setIsFetchingCode(false); } }; @@ -198,57 +308,90 @@ export default function HostPartyScreen() { {/* Party Code Display */} - Your Party Code - - {partyCode.split('').map((char, index) => ( - - {char} + {partyCode ? ( + <> + Your Party Code + + {partyCode.split('').map((char, index) => ( + + {char} + + ))} - ))} - - - Share this code with friends to join your party - + + Share this code with friends to join your party + + + ) : ( + + Click "START PARTY" to generate your party code + + )} - {/* Buttons Section */} - - {/* Create Party Button */} + {!partyCode ? ( + + {/* Create Party Button */} + + + + {isCreating ? 'CREATING...' : isFetchingCode ? 'GETTING CODE...' : 'START PARTY'} + + + + + {/* Cancel Button */} + { + if (playlistId) { + router.push({ + pathname: '/(tabs)/playlist-detail', + params: { id: playlistId } + }); + } else { + router.push('/(tabs)/PartyMode'); + } + }} + style={styles.cancelButton} + > + CANCEL + + + ) : ( { + router.push({ + pathname: '/(tabs)/playlist-detail', + params: { + id: playlistId, + partyCode: partyCode, + }, + }); + }} > - - {isCreating ? 'CREATING...' : 'START PARTY'} + Back to Playlist - - {/* Cancel Button */} - { - if (playlistId) { - router.push({ - pathname: '/(tabs)/playlist-detail', - params: { id: playlistId } - }); - } else { - router.push('/(tabs)/PartyMode'); - } - }} - style={styles.cancelButton} - > - CANCEL - - + ) + } diff --git a/vynl/src/app/(tabs)/JoinParty.tsx b/vynl/src/app/(tabs)/JoinParty.tsx index 0958d3e..ab4a2de 100644 --- a/vynl/src/app/(tabs)/JoinParty.tsx +++ b/vynl/src/app/(tabs)/JoinParty.tsx @@ -58,10 +58,15 @@ export default function JoinPartyScreen() { Alert.alert('Error', 'Failed to find playlist with your code'); setCode(['', '', '', '', '', '']); } else { - const playlist: ITunesPlaylist = await res.json(); + const playlist_id = await res.json(); + const int_id = parseInt(playlist_id); + if (Number.isNaN(int_id)) { + console.log("Expected a number, instead got id : ", playlist_id); + } + router.push({ pathname: '/(tabs)/playlist-detail', - params: { id: playlist.id } + params: { id: int_id } }); } diff --git a/vynl/src/app/(tabs)/PartyMode.tsx b/vynl/src/app/(tabs)/PartyMode.tsx index d5d68aa..c67a500 100644 --- a/vynl/src/app/(tabs)/PartyMode.tsx +++ b/vynl/src/app/(tabs)/PartyMode.tsx @@ -43,7 +43,7 @@ export default function PartyModeScreen() { Party Mode - Create or Join a Live Playlist and Team Up with Friends to build the Perfect Party Soundtrack. + Join a Live Playlist and Team Up with Friends to build the Perfect Party Soundtrack. diff --git a/vynl/src/app/(tabs)/playlist-detail.tsx b/vynl/src/app/(tabs)/playlist-detail.tsx index 7608908..fdac278 100644 --- a/vynl/src/app/(tabs)/playlist-detail.tsx +++ b/vynl/src/app/(tabs)/playlist-detail.tsx @@ -11,6 +11,9 @@ import SpotifyExportModal from '@/src/components/SpotifyExportModal'; import YouTubeExportModal from '@/src/components/YouTubeExportModal'; import { ITunesPlaylist } from '@/src/types'; import { usePlaylistWithID } from '@/src/hooks/use-playlist-with-id'; +import { supabase } from '@/src/utils/supabase'; +import { useAuth } from '@/src/context/auth-context'; +import { useUser } from '@/src/hooks/use-user'; const PARTY_CODE_STORAGE_KEY = '@vynl:partyCode'; @@ -19,11 +22,13 @@ export default function PlaylistDetailScreen() { const router = useRouter(); const playlistId = params.id as string; const partyCodeFromParams = params.partyCode as string | undefined; + const { authToken, loading: authLoading } = useAuth(); const [showExportModal, setShowExportModal] = useState(false); const [partyCode, setPartyCode] = useState(partyCodeFromParams); const [showYouTubeModal, setShowYouTubeModal] = useState(false); const { playlist, loading, error, refetch } = usePlaylistWithID(playlistId); + const { user, loading: userLoading } = useUser(); // Load party code from storage when component mounts or playlist changes useEffect(() => { @@ -176,6 +181,33 @@ export default function PlaylistDetailScreen() { ); }; + const disableParty = async () => { + if (!partyCode) { + return; + } + + if (!user) { + console.log("Unable to get user"); + return + } + + const url = `/api/playlist/party/toggle/${playlistId}`; + const response = await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${authToken}`, + }, + body: JSON.stringify({ + uid: user.id, + enable: false, + }), + }); + + setPartyCode(undefined); + return; + } + if (loading) { return ( @@ -225,7 +257,7 @@ export default function PlaylistDetailScreen() { - {!partyCode && ( + {(playlist.user_id === user?.id) && (!partyCode ? ( router.push({ pathname: '../HostParty', @@ -235,7 +267,14 @@ export default function PlaylistDetailScreen() { > HOST PARTY - )} + ) : ( + + STOP PARTY + + ))} @@ -243,6 +282,14 @@ export default function PlaylistDetailScreen() { {partyCode && ( + + {partyCode.split('').map((char, index) => ( + + {char} + + ))} + +/* Party Code @@ -329,7 +376,7 @@ export default function PlaylistDetailScreen() { > End Party - + */ )} ('myPlaylists'); const [activePartyPlaylistIds, setActivePartyPlaylistIds] = useState>(new Set()); // Calculate bottom padding: tab bar height (90) + safe area bottom + extra padding @@ -53,6 +56,16 @@ export default function PlaylistsScreen() { checkActiveParties(); }, [playlists]); + useEffect(() => { + if (!uid) return; + + if (activeView === 'myPlaylists' && refetch) { + refetch(); + } else if (activeView === 'partyPlaylists' && refetchParty) { + refetchParty(); + } + }, [activeView, uid, refetch, refetchParty]); + useFocusEffect( useCallback(() => { if (refetch && uid) { @@ -99,6 +112,10 @@ export default function PlaylistsScreen() { return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); }; + const currentPlaylists = activeView === 'myPlaylists' ? playlists : partyPlaylists; + const currentLoading = activeView === 'myPlaylists' ? playlistLoading : partyLoading; + const currentError = activeView === 'myPlaylists' ? error : partyError; + return ( @@ -106,12 +123,26 @@ export default function PlaylistsScreen() { My Playlists + + setActiveView('myPlaylists')} + > + My Playlists + + setActiveView('partyPlaylists')} + > + Party Playlists + + - {authLoading || playlistLoading ? ( + {authLoading || currentLoading ? ( Loading... - ) : playlists.length === 0 ? ( + ) : currentPlaylists.length === 0 ? ( No playlists yet @@ -124,7 +155,7 @@ export default function PlaylistsScreen() { showsVerticalScrollIndicator={false} contentInsetAdjustmentBehavior="automatic" > - {playlists.map((p) => ( + {currentPlaylists.map((p) => ( {p.name} - {activePartyPlaylistIds.has(p.id) && ( + {/* {activePartyPlaylistIds.has(p.id) && ( Party Mode - )} + )} */} {(p.songs ?? []).length} song{(p.songs ?? []).length !== 1 ? 's' : ''} ยท {new Date(p.created_at).toLocaleDateString()} - { - e.stopPropagation(); - handleDelete(p.id); - }} - style={styles.deleteButton} - > - - + {activeView === 'myPlaylists' && ( + { + e.stopPropagation(); + handleDelete(p.id); + }} + style={styles.deleteButton} + > + + + )} {(p.songs ?? []).length > 0 && ( @@ -325,5 +358,36 @@ const styles = StyleSheet.create({ marginTop: 4, paddingLeft: 52, }, + segmentControl: { + flexDirection: 'row', + marginHorizontal: 24, + marginBottom: 20, + backgroundColor: '#EBEBEB', + borderRadius: 12, + padding: 4, + }, + segmentButton: { + flex: 1, + paddingVertical: 10, + borderRadius: 8, + alignItems: 'center', + justifyContent: 'center', + }, + segmentButtonActive: { + backgroundColor: 'white', + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.1, + shadowRadius: 3, + elevation: 2, + }, + segmentText: { + fontSize: 14, + fontWeight: '600', + color: '#6F7A88', + }, + segmentTextActive: { + color: '#001133', + }, }); diff --git a/vynl/src/app/(tabs)/swipe.tsx b/vynl/src/app/(tabs)/swipe.tsx index fb2eee0..c045072 100644 --- a/vynl/src/app/(tabs)/swipe.tsx +++ b/vynl/src/app/(tabs)/swipe.tsx @@ -492,7 +492,6 @@ export default function Swiping() { ); const allSongs = [...seedSongs, ...filteredAddedSongs]; - console.log("Updating playlist ", newPlaylist.id, ", new name : '", playlistName,"' , added songs : ", allSongs); if (isAddingMode) await updatePlaylist(newPlaylist.id, allSongs, newPlaylist.name); else await updatePlaylist(newPlaylist.id, allSongs, playlistName); diff --git a/vynl/src/app/api/playlist/+api.ts b/vynl/src/app/api/playlist/+api.ts index b2a4f8a..3a91705 100644 --- a/vynl/src/app/api/playlist/+api.ts +++ b/vynl/src/app/api/playlist/+api.ts @@ -33,8 +33,8 @@ export async function GET(req: Request) { .from('party_users') .select(` playlist_id, - playlists (*)` - ) + playlists (*) + `) .eq('user_id', uid)); data = data?.map(d => d.playlists diff --git a/vynl/src/app/api/playlist/[id]+api.ts b/vynl/src/app/api/playlist/[id]+api.ts index 60faf11..8a1c9f7 100644 --- a/vynl/src/app/api/playlist/[id]+api.ts +++ b/vynl/src/app/api/playlist/[id]+api.ts @@ -80,7 +80,7 @@ export async function PUT(req: Request, { id }: Record) { }); } - if (newName) { + if (old_playlist.name != newName) { // Updating playlist object in database const { data: p_data, error: p_err } = await supabase .from('playlists') @@ -91,6 +91,7 @@ export async function PUT(req: Request, { id }: Record) { if (p_err || !isPlaylistData(p_data)) { console.log("p_data : ", p_err); + console.log("New name : ", newName, old_playlist.name); return new Response('Failed to insert into database', { status: 400 }); diff --git a/vynl/src/app/api/playlist/party/link/[code]+api.ts b/vynl/src/app/api/playlist/party/link/[code]+api.ts index 0482942..0130e45 100644 --- a/vynl/src/app/api/playlist/party/link/[code]+api.ts +++ b/vynl/src/app/api/playlist/party/link/[code]+api.ts @@ -20,6 +20,7 @@ export async function PUT(req: Request, { code }: Record) { }); } + console.log("Code : ", code); const { data: playlist_id, error: p_err } = await supabase .rpc('get_party_id', { code: code @@ -40,12 +41,14 @@ export async function PUT(req: Request, { code }: Record) { // }); // } + console.log("Playlist id : ", playlist_id) + const { data: link_data, error: link_err } = await supabase .from('party_users') .upsert({ 'playlist_id': playlist_id }); if (link_err) { - console.log(link_err); + console.log("link error", link_err); return new Response("Error linking playlist", { status: 400, headers: { 'Content-Type': 'text/html' } diff --git a/vynl/src/app/api/playlist/party/toggle/[id]+api.ts b/vynl/src/app/api/playlist/party/toggle/[id]+api.ts index 1ff8096..8b9a4cf 100644 --- a/vynl/src/app/api/playlist/party/toggle/[id]+api.ts +++ b/vynl/src/app/api/playlist/party/toggle/[id]+api.ts @@ -27,10 +27,8 @@ export async function PUT(req: Request, { id }: Record) { } const body = await req.json(); - // const uid = body.uid; const enable = body.enable; - - // if (uid === undefined || uid === null) return new Response("Missing user ID", { status: 400 }); + if (enable === undefined || enable === null) return new Response("Missing enable", { status: 400 }); const supabase = await createSupabaseClient(req); @@ -40,13 +38,21 @@ export async function PUT(req: Request, { id }: Record) { } if (enable) { + // Get the authenticated user from Supabase + const { data: { user: authUser }, error: authError } = await supabase.auth.getUser(); + if (authError || !authUser) { + return new Response('Unauthorized', { + status: 401 + }); + } + // 1. Create secret party code if it doesn't exist const { data: c_data, error: c_err } = await supabase .from('playlists') .select('party_code, uid') .eq('playlist_id', playlist_id) .single(); - + if (c_err || !c_data) { console.log(c_err); console.log(c_data); @@ -71,16 +77,18 @@ export async function PUT(req: Request, { id }: Record) { .eq("playlist_id", playlist_id); if (p_err) { + console.error('Error updating playlist:', p_err); return new Response('Failed to update playlist', { status: 400 }); } + // 3. Add owner to party_user - const party_user_to_add: party_user = {playlist_id: playlist_id, user_id: uid} - + // RLS will automatically set user_id based on the authenticated user + // Only insert playlist_id, just like the link endpoint does const { data: pu_data, error: pu_err } = await supabase .from('party_users') - .upsert(party_user_to_add) + .upsert({ playlist_id: playlist_id }); if (pu_err) { console.log(pu_err) diff --git a/vynl/src/hooks/use-party-playlists.ts b/vynl/src/hooks/use-party-playlists.ts new file mode 100644 index 0000000..dab2519 --- /dev/null +++ b/vynl/src/hooks/use-party-playlists.ts @@ -0,0 +1,59 @@ +import { useState, useEffect, useCallback } from "react"; +import { ITunesPlaylist, ITunesSong } from "../types"; +import { useAuth } from "../context/auth-context"; + +export function usePartyPlaylists(uid: string | null) { + const [playlists, setPlaylists] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const { authToken } = useAuth(); + + const fetchPlaylists = useCallback(async () => { + if (!uid) { + setPlaylists([]); + setLoading(false); + return; + } + + setLoading(true); + setError(null); + + try { + console.log("UID : ", uid); + const res = await fetch(`/api/playlist?uid=${uid}&party`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + authToken, + }, + }); + if (!res.ok) throw new Error("Failed to fetch playlists"); + + const data = await res.json(); + + const mapped: ITunesPlaylist[] = data.map((p: any) => ({ + id: p.id, + name: p.name, + created_at: p.created_at, + user_id: p.user_id, + songs: p.songs + })); + + //TODO : change order + mapped.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); + console.log("Party playlists : ", mapped); + + setPlaylists(mapped); + } catch (err: any) { + setError(err.message); + } finally { + setLoading(false); + } + }, [uid, authToken]); + + useEffect(() => { + fetchPlaylists(); + }, [fetchPlaylists]); + + return { playlists, loading, error, refetch: fetchPlaylists }; +} diff --git a/vynl/src/types/database.types.ts b/vynl/src/types/database.types.ts index 7cc6b97..732cad4 100644 Binary files a/vynl/src/types/database.types.ts and b/vynl/src/types/database.types.ts differ