From d1e0ee1b28b2032cabc45e094caf0a121e8d5dd5 Mon Sep 17 00:00:00 2001 From: vliu526 Date: Fri, 5 Dec 2025 19:33:29 -0800 Subject: [PATCH 01/10] Linked party code endpoint with the host party button --- package-lock.json | 2 +- vynl/package-lock.json | 29 +-- vynl/src/app/(tabs)/HostParty.tsx | 178 +++++++++++++++--- .../app/api/playlist/party/toggle/[id]+api.ts | 43 ++++- 4 files changed, 185 insertions(+), 67 deletions(-) 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/package-lock.json b/vynl/package-lock.json index f1bbfd8..0c24942 100644 --- a/vynl/package-lock.json +++ b/vynl/package-lock.json @@ -112,7 +112,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -3718,7 +3717,6 @@ "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.21.tgz", "integrity": "sha512-mhpAewdivBL01ibErr91FUW9bvKhfAF6Xv/yr6UOJtDhv0jU6iUASUcA3i3T8VJCOB/vxmoke7VDp8M+wBFs/Q==", "license": "MIT", - "peer": true, "dependencies": { "@react-navigation/core": "^7.13.2", "escape-string-regexp": "^4.0.0", @@ -3882,7 +3880,6 @@ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.84.0.tgz", "integrity": "sha512-byMqYBvb91sx2jcZsdp0qLpmd4Dioe80e4OU/UexXftCkpTcgrkoENXHf5dO8FCSai8SgNeq16BKg10QiDI6xg==", "license": "MIT", - "peer": true, "dependencies": { "@supabase/auth-js": "2.84.0", "@supabase/functions-js": "2.84.0", @@ -3999,7 +3996,6 @@ "integrity": "sha512-k6Mjsd9dbZgvY4Bl7P1NIpePQNi+dfYtlJ5voi9KQlynxSyQkfOgJmYGCYmw/aSgH/rUcFvG8u5gd4npzgRDyg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "jest-matcher-utils": "^30.0.5", "picocolors": "^1.1.1", @@ -4261,7 +4257,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4278,7 +4273,6 @@ "integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4375,7 +4369,6 @@ "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.47.0", "@typescript-eslint/types": "8.47.0", @@ -4946,7 +4939,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5718,7 +5710,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -7114,7 +7105,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7311,7 +7301,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7675,7 +7664,6 @@ "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.27.tgz", "integrity": "sha512-50BcJs8eqGwRiMUoWwphkRGYtKFS2bBnemxLzy0lrGVA1E6F4Q7L5h3WT6w1ehEZybtOVkfJu4Z6GWo2IJcpEA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "54.0.18", @@ -7810,7 +7798,6 @@ "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.11.tgz", "integrity": "sha512-xnfrfZ7lHjb+03skhmDSYeFF7OU2K3Xn/lAeP+7RhkV2xp2f5RCKtOUYajCnYeZesvMrsUxOsbGOP2JXSOH3NA==", "license": "MIT", - "peer": true, "dependencies": { "@expo/config": "~12.0.11", "@expo/env": "~2.0.8" @@ -7835,7 +7822,6 @@ "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.10.tgz", "integrity": "sha512-UqyNaaLKRpj4pKAP4HZSLnuDQqueaO5tB1c/NWu5vh1/LF9ulItyyg2kF/IpeOp0DeOLk0GY0HrIXaKUMrwB+Q==", "license": "MIT", - "peer": true, "dependencies": { "fontfaceobserver": "^2.1.0" }, @@ -7897,7 +7883,6 @@ "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.10.tgz", "integrity": "sha512-0EKtn4Sk6OYmb/5ZqK8riO0k1Ic+wyT3xExbmDvUYhT7p/cKqlVUExMuOIAt3Cx3KUUU1WCgGmdd493W/D5XjA==", "license": "MIT", - "peer": true, "dependencies": { "expo-constants": "~18.0.11", "invariant": "^2.2.4" @@ -10139,7 +10124,6 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -13712,7 +13696,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -13749,7 +13732,6 @@ "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz", "integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==", "license": "MIT", - "peer": true, "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.81.5", @@ -13807,7 +13789,6 @@ "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz", "integrity": "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==", "license": "MIT", - "peer": true, "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", @@ -13861,7 +13842,6 @@ "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz", "integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==", "license": "MIT", - "peer": true, "peerDependencies": { "react": "*", "react-native": "*" @@ -13872,7 +13852,6 @@ "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz", "integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==", "license": "MIT", - "peer": true, "dependencies": { "react-freeze": "^1.0.0", "react-native-is-edge-to-edge": "^1.2.1", @@ -13888,7 +13867,6 @@ "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz", "integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", @@ -14063,7 +14041,6 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -14143,7 +14120,6 @@ "integrity": "sha512-jXkSl3CpvPYEF+p/eGDLB4sPoDX8pKkYvRl9+rR8HxLY0X04vW7hCm1/0zHoUSjPZ3bDa+wXWNTDVIw/R8aDVw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "react-is": "^19.1.0", "scheduler": "^0.26.0" @@ -15735,7 +15711,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -15824,7 +15799,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -16030,7 +16004,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -17027,4 +17000,4 @@ } } } -} \ No newline at end of file +} diff --git a/vynl/src/app/(tabs)/HostParty.tsx b/vynl/src/app/(tabs)/HostParty.tsx index ae2c692..007a03b 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,12 +171,14 @@ export default function HostPartyScreen() { console.error('Error saving party code:', error); } + 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); } }; @@ -199,16 +309,24 @@ export default function HostPartyScreen() { {/* Party Code Display */} Your Party Code - - {partyCode.split('').map((char, index) => ( - - {char} + {partyCode ? ( + <> + + {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 */} @@ -217,16 +335,16 @@ export default function HostPartyScreen() { - {isCreating ? 'CREATING...' : 'START PARTY'} + {isCreating ? 'CREATING...' : isFetchingCode ? 'GETTING CODE...' : 'START PARTY'} 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 9517718..500bdab 100644 --- a/vynl/src/app/api/playlist/party/toggle/[id]+api.ts +++ b/vynl/src/app/api/playlist/party/toggle/[id]+api.ts @@ -40,6 +40,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 + }); + } + + // Verify that the user from request body matches the authenticated user + if (authUser.id !== uid) { + return new Response('User ID mismatch', { + status: 403 + }); + } + // 1. Create secret party code if it doesn't exist const { data: c_data, error: c_err } = await supabase .from('playlists') @@ -49,13 +64,15 @@ export async function PUT(req: Request, { id }: Record) { let partyCode; if (c_err) { + console.error('Error checking party code:', c_err); return new Response('Unable to check the party code', { status: 404 }); } - if (c_data) { - partyCode = c_data[0]?.party_code; + // Check if playlist exists and has a party code + if (c_data && c_data.length > 0 && c_data[0]?.party_code) { + partyCode = c_data[0].party_code; } else { partyCode = generatePartyCode(6); } @@ -67,21 +84,31 @@ 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) + .insert({ playlist_id: playlist_id }); if (pu_err) { - return new Response('Failed to update party_user', { - status: 400 - }); + // If it's a unique constraint violation, the user is already linked, which is fine + // Check if the error is about duplicate key + if (pu_err.code === '23505' || pu_err.message?.includes('duplicate') || pu_err.message?.includes('unique')) { + // User is already in party_users, which is fine + console.log('User already linked to playlist, continuing...'); + } else { + console.error('Error inserting party_user:', pu_err); + return new Response(`Failed to update party_user: ${pu_err.message}`, { + status: 400 + }); + } } // 4. Return party code From 09117b738b21a24402599c2df76d6af506d668f1 Mon Sep 17 00:00:00 2001 From: "louis.bernard18" Date: Fri, 5 Dec 2025 19:43:41 -0800 Subject: [PATCH 02/10] Linked join to backend --- vynl/package-lock.json | 10 +++++----- vynl/package.json | 2 +- vynl/src/app/(tabs)/JoinParty.tsx | 9 +++++++-- .../app/api/playlist/party/link/[code]+api.ts | 9 ++++++--- vynl/src/types/database.types.ts | Bin 17224 -> 17386 bytes 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/vynl/package-lock.json b/vynl/package-lock.json index f1bbfd8..0115426 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" } @@ -15445,9 +15445,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", @@ -17027,4 +17027,4 @@ } } } -} \ No newline at end of file +} 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)/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/api/playlist/party/link/[code]+api.ts b/vynl/src/app/api/playlist/party/link/[code]+api.ts index 58b83b8..235a50c 100644 --- a/vynl/src/app/api/playlist/party/link/[code]+api.ts +++ b/vynl/src/app/api/playlist/party/link/[code]+api.ts @@ -20,9 +20,10 @@ 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', { - party_code: code + .rpc('get_party_id_duplicate', { + input_code: code }); if (p_err || !playlist_id) { @@ -39,12 +40,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') .insert({ '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/types/database.types.ts b/vynl/src/types/database.types.ts index 594489a750b72eb1549fb0223a365a607bc0bec4..cc9954bffcedaf4552b248dc10cd09693c0262aa 100644 GIT binary patch delta 76 zcmX@n#`vn8af6@S Date: Sat, 6 Dec 2025 12:10:36 -0800 Subject: [PATCH 03/10] Party playlists available in playlists tab --- vynl/src/app/(tabs)/playlists.tsx | 88 +++++++++++++++++++++++---- vynl/src/app/api/playlist/+api.ts | 4 +- vynl/src/hooks/use-party-playlists.ts | 59 ++++++++++++++++++ 3 files changed, 137 insertions(+), 14 deletions(-) create mode 100644 vynl/src/hooks/use-party-playlists.ts diff --git a/vynl/src/app/(tabs)/playlists.tsx b/vynl/src/app/(tabs)/playlists.tsx index 38572e0..00bbd48 100644 --- a/vynl/src/app/(tabs)/playlists.tsx +++ b/vynl/src/app/(tabs)/playlists.tsx @@ -8,6 +8,7 @@ import { useFocusEffect, useRouter } from 'expo-router'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { useUser } from '@/src/hooks/use-user'; import { useUserPlaylists } from '@/src/hooks/use-playlist-for-user'; +import { usePartyPlaylists } from '@/src/hooks/use-party-playlists'; const PARTY_CODE_STORAGE_KEY = '@vynl:partyCode'; @@ -18,6 +19,8 @@ export default function PlaylistsScreen() { const uid = user?.id; const { playlists, loading: playlistLoading, error, refetch } = useUserPlaylists(uid ?? null); + const { playlists: partyPlaylists, loading: partyLoading, error: partyError, refetch: refetchParty } = usePartyPlaylists(uid ?? null); + const [activeView, setActiveView] = useState<'myPlaylists' | 'partyPlaylists'>('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) => ( - { - 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/api/playlist/+api.ts b/vynl/src/app/api/playlist/+api.ts index 7abb966..f99aeba 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 => 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 }; +} From 05e1c9a1699fa2be2c478e9a6526d7bd97fdb495 Mon Sep 17 00:00:00 2001 From: "louis.bernard18" Date: Sat, 6 Dec 2025 15:09:26 -0800 Subject: [PATCH 04/10] Hosting screen changed and no direct redirection to playlist-detail. --- vynl/src/app/(tabs)/HostParty.tsx | 83 ++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 29 deletions(-) diff --git a/vynl/src/app/(tabs)/HostParty.tsx b/vynl/src/app/(tabs)/HostParty.tsx index 007a03b..5a58ab4 100644 --- a/vynl/src/app/(tabs)/HostParty.tsx +++ b/vynl/src/app/(tabs)/HostParty.tsx @@ -173,14 +173,14 @@ export default function HostPartyScreen() { setIsFetchingCode(false); - // Navigate back to playlist detail with party code + /* // Navigate back to playlist detail with party code router.push({ pathname: '/(tabs)/playlist-detail', params: { id: playlist.id.toString(), partyCode: fetchedCode, }, - }); + }); */ return; } @@ -308,9 +308,9 @@ export default function HostPartyScreen() { {/* Party Code Display */} - Your Party Code {partyCode ? ( <> + Your Party Code {partyCode.split('').map((char, index) => ( @@ -329,13 +329,56 @@ export default function HostPartyScreen() { )} - {/* 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...' : isFetchingCode ? 'GETTING CODE...' : '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 - - + ) + } From db200e170f15de92f5698d527219a6c488ffa120 Mon Sep 17 00:00:00 2001 From: "louis.bernard18" Date: Sat, 6 Dec 2025 16:06:55 -0800 Subject: [PATCH 05/10] Disabling party mode --- vynl/src/app/(tabs)/playlist-detail.tsx | 58 ++++++++++++++++++- .../app/api/playlist/party/toggle/[id]+api.ts | 7 +++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/vynl/src/app/(tabs)/playlist-detail.tsx b/vynl/src/app/(tabs)/playlist-detail.tsx index 7608908..708c6d0 100644 --- a/vynl/src/app/(tabs)/playlist-detail.tsx +++ b/vynl/src/app/(tabs)/playlist-detail.tsx @@ -11,6 +11,8 @@ 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'; const PARTY_CODE_STORAGE_KEY = '@vynl:partyCode'; @@ -19,6 +21,7 @@ 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); @@ -176,6 +179,35 @@ export default function PlaylistDetailScreen() { ); }; + const disableParty = async () => { + if (!partyCode) { + return; + } + + const { data: { user }, error: userError } = await supabase.auth.getUser(); + + if (userError || !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 && ( + {!partyCode ? ( router.push({ pathname: '../HostParty', @@ -235,6 +267,13 @@ 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 - + */ )} ) { return new Response("Missing playlist ID", { status: 400 }); } try { + console.log("PUT"); const playlist_id = parseInt(id); if (Number.isNaN(playlist_id)) { @@ -124,6 +125,12 @@ export async function PUT(req: Request, { id }: Record) { .update({ in_party_mode: false }) .eq("playlist_id", playlist_id); + if (p_err) { + return new Response("Failed to disable party mode", { + status: 400 + }); + } + return new Response(null, { status: 200, headers: { 'Content-Type': 'application/json' } From 7d93b1851e02d7438d77d9befd00aaed42c30ac7 Mon Sep 17 00:00:00 2001 From: "louis.bernard18" Date: Sun, 7 Dec 2025 15:40:33 -0800 Subject: [PATCH 06/10] Fixed different users adding songs issue. --- vynl/src/app/(tabs)/swipe.tsx | 1 - vynl/src/app/api/playlist/[id]+api.ts | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vynl/src/app/(tabs)/swipe.tsx b/vynl/src/app/(tabs)/swipe.tsx index 4762dbc..ac59166 100644 --- a/vynl/src/app/(tabs)/swipe.tsx +++ b/vynl/src/app/(tabs)/swipe.tsx @@ -482,7 +482,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/[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 }); From 14e38cdd3aefdea7235a9e9a7c953013e1ac0f7e Mon Sep 17 00:00:00 2001 From: "louis.bernard18" Date: Sun, 7 Dec 2025 15:58:16 -0800 Subject: [PATCH 07/10] Fixed rendering of party managment, and removed Party Mode indication because not correct. --- vynl/src/app/(tabs)/playlist-detail.tsx | 10 +++++----- vynl/src/app/(tabs)/playlists.tsx | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/vynl/src/app/(tabs)/playlist-detail.tsx b/vynl/src/app/(tabs)/playlist-detail.tsx index 708c6d0..fdac278 100644 --- a/vynl/src/app/(tabs)/playlist-detail.tsx +++ b/vynl/src/app/(tabs)/playlist-detail.tsx @@ -13,6 +13,7 @@ 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'; @@ -27,6 +28,7 @@ export default function PlaylistDetailScreen() { 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(() => { @@ -184,9 +186,7 @@ export default function PlaylistDetailScreen() { return; } - const { data: { user }, error: userError } = await supabase.auth.getUser(); - - if (userError || !user) { + if (!user) { console.log("Unable to get user"); return } @@ -257,7 +257,7 @@ export default function PlaylistDetailScreen() { - {!partyCode ? ( + {(playlist.user_id === user?.id) && (!partyCode ? ( router.push({ pathname: '../HostParty', @@ -274,7 +274,7 @@ export default function PlaylistDetailScreen() { > STOP PARTY - )} + ))} diff --git a/vynl/src/app/(tabs)/playlists.tsx b/vynl/src/app/(tabs)/playlists.tsx index 00bbd48..04c3059 100644 --- a/vynl/src/app/(tabs)/playlists.tsx +++ b/vynl/src/app/(tabs)/playlists.tsx @@ -169,12 +169,12 @@ export default function PlaylistsScreen() { {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()} From 104f240bab8604e82ce63a959cc38c7586f84f06 Mon Sep 17 00:00:00 2001 From: vliu526 Date: Sun, 7 Dec 2025 20:20:49 -0800 Subject: [PATCH 08/10] Updated the description text for the join feature on party mode tab screen --- vynl/src/app/(tabs)/PartyMode.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From 053e8ef91259f50ca4b7dd3b9a1230aeaf911dc8 Mon Sep 17 00:00:00 2001 From: Zack Crouse Date: Sun, 7 Dec 2025 20:32:27 -0800 Subject: [PATCH 09/10] Updated to match database functions --- vynl/src/types/database.types.ts | Bin 17386 -> 17638 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/vynl/src/types/database.types.ts b/vynl/src/types/database.types.ts index cc9954bffcedaf4552b248dc10cd09693c0262aa..732cad4e64f76e842b695d9edc6a15eeda211fb6 100644 GIT binary patch delta 96 zcmaFW&iJg8af1oVNgBLDyZ From dfd080e3da7b5c20c11129b20c4316b43d77ce2f Mon Sep 17 00:00:00 2001 From: Zack Crouse Date: Sun, 7 Dec 2025 20:33:06 -0800 Subject: [PATCH 10/10] Fixed test and added back missing patches --- vynl/__tests__/party-mode-test.ts | 1 + vynl/__tests__/playlist-endpoints-test.ts | 2 -- vynl/src/app/api/playlist/party/link/[code]+api.ts | 4 ++-- vynl/src/app/api/playlist/party/toggle/[id]+api.ts | 7 ++----- 4 files changed, 5 insertions(+), 9 deletions(-) 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/src/app/api/playlist/party/link/[code]+api.ts b/vynl/src/app/api/playlist/party/link/[code]+api.ts index 0d9d35b..0130e45 100644 --- a/vynl/src/app/api/playlist/party/link/[code]+api.ts +++ b/vynl/src/app/api/playlist/party/link/[code]+api.ts @@ -22,8 +22,8 @@ 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_duplicate', { - input_code: code + .rpc('get_party_id', { + code: code }); if (p_err || !playlist_id) { 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 6c81d9e..8b9a4cf 100644 --- a/vynl/src/app/api/playlist/party/toggle/[id]+api.ts +++ b/vynl/src/app/api/playlist/party/toggle/[id]+api.ts @@ -17,7 +17,6 @@ export async function PUT(req: Request, { id }: Record) { return new Response("Missing playlist ID", { status: 400 }); } try { - console.log("PUT"); const playlist_id = parseInt(id); if (Number.isNaN(playlist_id)) { @@ -28,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); @@ -91,7 +88,7 @@ export async function PUT(req: Request, { id }: Record) { // Only insert playlist_id, just like the link endpoint does const { data: pu_data, error: pu_err } = await supabase .from('party_users') - .insert({ playlist_id: playlist_id }); + .upsert({ playlist_id: playlist_id }); if (pu_err) { console.log(pu_err)