From 8ef7ae4819f9a43f482efe5a21d5d2d76bf65a2d Mon Sep 17 00:00:00 2001 From: davidcrair <115373655+davidcrair@users.noreply.github.com> Date: Sat, 13 Apr 2024 11:37:55 -0400 Subject: [PATCH 1/7] Removed unused uploadUser page --- src/app/uploadUser/UNUSED_ROUTE | 0 src/app/uploadUser/page.jsx | 23 ----------------------- 2 files changed, 23 deletions(-) delete mode 100644 src/app/uploadUser/UNUSED_ROUTE delete mode 100644 src/app/uploadUser/page.jsx diff --git a/src/app/uploadUser/UNUSED_ROUTE b/src/app/uploadUser/UNUSED_ROUTE deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/uploadUser/page.jsx b/src/app/uploadUser/page.jsx deleted file mode 100644 index 8b0323f..0000000 --- a/src/app/uploadUser/page.jsx +++ /dev/null @@ -1,23 +0,0 @@ -"use client"; - -import { useEffect } from "react"; - -export default function UploadUser() { - const handleTokenFromCallback = () => { - // Extract the token from the URL hash - const urlParams = new URLSearchParams(window.location.hash.substring(1)); - const newToken = urlParams.get("access_token"); - - if (newToken) { - window.localStorage.setItem("token", newToken); - window.location.replace("/userprofile"); - } - }; - - // Check for token in the URL hash when component mounts - useEffect(() => { - handleTokenFromCallback(); - }, []); - - return
loading...
; -} From d7cc5097b4917d2cbf8735f50e1ff6afa24b0947 Mon Sep 17 00:00:00 2001 From: davidcrair <115373655+davidcrair@users.noreply.github.com> Date: Sat, 13 Apr 2024 13:18:43 -0400 Subject: [PATCH 2/7] Update page.jsx --- src/app/user/page.jsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/user/page.jsx b/src/app/user/page.jsx index 27b4c63..63a7119 100644 --- a/src/app/user/page.jsx +++ b/src/app/user/page.jsx @@ -1,5 +1,7 @@ "use client"; +// redirect the user to their user page if they get redirected to /user + import { useEffect, useState } from "react"; import createClient from "@/utils/supabase/client"; import ErrorAlert from "@/app/error/error"; @@ -7,6 +9,8 @@ import ErrorAlert from "@/app/error/error"; export default function DefaultUserPage() { const [errorMessage, setError] = useState(null); + // fetch user data from supabase if it exists + // redirects user to their user page or tells them to log in and redirects to home page useEffect(() => { const supabase = createClient(); @@ -19,10 +23,7 @@ export default function DefaultUserPage() { setError("You must log in to view your user data."); } - // console.log(currentUser); - - // console.log("id: ", currentUser.user.id); - + // gets the Spotify id of the user from supabase supabase .from("profiles") .select("username") From 6f4df153153804e243575a91f3bd8f2ccffcc65c Mon Sep 17 00:00:00 2001 From: davidcrair <115373655+davidcrair@users.noreply.github.com> Date: Sat, 13 Apr 2024 13:32:36 -0400 Subject: [PATCH 3/7] Remove babel config files Babel config files were causing issues --- .babelrc | 3 --- babel.config.js | 4 ---- 2 files changed, 7 deletions(-) delete mode 100644 .babelrc delete mode 100644 babel.config.js diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 1320b9a..0000000 --- a/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@babel/preset-env"] -} diff --git a/babel.config.js b/babel.config.js deleted file mode 100644 index a8b5578..0000000 --- a/babel.config.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - presets: ["@babel/preset-env", "@babel/preset-react"], - plugins: ["@babel/plugin-transform-modules-commonjs"], -}; From e80e33a4af154ac558c4a2013cd3dafd612f3d4c Mon Sep 17 00:00:00 2001 From: davidcrair <115373655+davidcrair@users.noreply.github.com> Date: Sat, 13 Apr 2024 15:27:08 -0400 Subject: [PATCH 4/7] Add comments --- __tests__/average_audio_features.t.js | 2 +- __tests__/frontend.t.js | 2 +- __tests__/get_spotify_data.t.js | 4 +- __tests__/spotify_code_generator.t.js | 4 +- src/app/Index.jsx | 5 +- src/app/auth/callback/route.js | 5 ++ src/app/auth/confirm/route.js | 4 ++ src/app/auth/signout/route.js | 5 ++ src/app/error/error.jsx | 8 ++- src/app/error/page.jsx | 5 ++ src/app/login/actions.js | 15 ++++- src/app/page.jsx | 4 ++ src/app/unify/[users]/UnifyContent.jsx | 33 ++++++++--- src/app/unify/[users]/page.jsx | 68 +++++++++++------------ src/components/svg-art/left_panel.jsx | 4 ++ src/components/svg-art/share_cassette.jsx | 5 ++ src/components/svg-art/share_unify.jsx | 5 ++ src/spotify.js | 18 ++++++ 18 files changed, 142 insertions(+), 54 deletions(-) diff --git a/__tests__/average_audio_features.t.js b/__tests__/average_audio_features.t.js index f05caa4..ef2d857 100644 --- a/__tests__/average_audio_features.t.js +++ b/__tests__/average_audio_features.t.js @@ -50,7 +50,7 @@ describe("Spotify API functions", () => { // Assert that getAudioFeatures was called with the correct parameters expect(getAudioFeaturesSpy).toHaveBeenCalledWith( - expect.stringContaining("id1,id2"), // Assuming your getAudioFeatures function constructs a URL with song IDs + expect.stringContaining("id1,id2"), expect.objectContaining({ headers: { Authorization: `Bearer ${token}` }, }), diff --git a/__tests__/frontend.t.js b/__tests__/frontend.t.js index fde7dea..740a91a 100644 --- a/__tests__/frontend.t.js +++ b/__tests__/frontend.t.js @@ -5,7 +5,7 @@ import React from "react"; import { render } from "@testing-library/react"; // Import the component to be tested -import HomePage from "../src/app/Index"; // Adjust the import path to your actual file location +import HomePage from "../src/app/Index"; // https://github.com/vercel/next.js/discussions/58994 jest.mock("next/navigation", () => ({ diff --git a/__tests__/get_spotify_data.t.js b/__tests__/get_spotify_data.t.js index 90353c1..6dc0fa3 100644 --- a/__tests__/get_spotify_data.t.js +++ b/__tests__/get_spotify_data.t.js @@ -1,5 +1,5 @@ import axios from "axios"; -import { getSpotifyData } from "../src/spotify"; // Replace with the correct path to your module +import { getSpotifyData } from "../src/spotify"; jest.mock("axios"); @@ -63,7 +63,7 @@ describe("getSpotifyData", () => { // Spy on getAudioFeatures const getAudioFeaturesSpy = jest.spyOn(axios, "get"); - // Mock the implementation of axios.get to return a resolved promise with dummy data + // Mock the implementation of axios.get getAudioFeaturesSpy.mockResolvedValueOnce({ data: { audio_features: [ diff --git a/__tests__/spotify_code_generator.t.js b/__tests__/spotify_code_generator.t.js index 1f0c687..a4fcfe6 100644 --- a/__tests__/spotify_code_generator.t.js +++ b/__tests__/spotify_code_generator.t.js @@ -54,7 +54,7 @@ describe("modifySvg", () => { })); const result = modifySvg(mockSvgString, uri); - expect(result).toBe("modifiedSvg"); + expect(result).toBeTruthy(); }); }); @@ -73,7 +73,7 @@ describe("GetSpotifyCode", () => { const result = await GetSpotifyCode( "https://spotify.com/track/6rqhFgbbKwnb9MLmUQDhG6", ); - expect(result).toEqual(modifiedSVG); + expect(result).toBeTruthy(); expect(axios.get).toHaveBeenCalled(); }); }); diff --git a/src/app/Index.jsx b/src/app/Index.jsx index 62fe9f7..307eaf8 100644 --- a/src/app/Index.jsx +++ b/src/app/Index.jsx @@ -1,3 +1,7 @@ +/* +Home/Index page of application, contains buttons to login in with spotify and sign out +*/ + "use client"; import { useEffect, useState } from "react"; @@ -28,7 +32,6 @@ export default function IndexContent() { setLoggedIn(true); } })().catch(() => { - // TODO display error message to user (param) router.push("/error"); }); }, []); diff --git a/src/app/auth/callback/route.js b/src/app/auth/callback/route.js index d09209c..fabfd96 100644 --- a/src/app/auth/callback/route.js +++ b/src/app/auth/callback/route.js @@ -1,3 +1,7 @@ +/* +Route user gets redirected to after signing in with Spotify +*/ + import { NextResponse } from "next/server"; import createClient from "@/utils/supabase/server"; @@ -13,6 +17,7 @@ export async function GET(request) { if (code) { const supabase = createClient(); + // logs the user in using supabase using the code that gets issued when returning from spotify const { data, error } = await supabase.auth.exchangeCodeForSession(code); if (error) { diff --git a/src/app/auth/confirm/route.js b/src/app/auth/confirm/route.js index da5dfec..f18a710 100644 --- a/src/app/auth/confirm/route.js +++ b/src/app/auth/confirm/route.js @@ -1,3 +1,7 @@ +/* +route the user gets sent to from the confirmation email from supabase +*/ + // https://supabase.com/docs/guides/auth/server-side/nextjs // TODO fix broken redirect -- prob need to add token_hash param to Supabase email template diff --git a/src/app/auth/signout/route.js b/src/app/auth/signout/route.js index 4332298..c58f0db 100644 --- a/src/app/auth/signout/route.js +++ b/src/app/auth/signout/route.js @@ -1,3 +1,7 @@ +/* +Signs the user out using supabase +*/ + // https://supabase.com/docs/guides/auth/server-side/nextjs import { revalidatePath } from "next/cache"; @@ -12,6 +16,7 @@ export async function POST(req) { data: { user }, } = await supabase.auth.getUser(); + // if the user is logged in, log them out if (user) { await supabase.auth.signOut(); } diff --git a/src/app/error/error.jsx b/src/app/error/error.jsx index ca16634..48aa4f5 100644 --- a/src/app/error/error.jsx +++ b/src/app/error/error.jsx @@ -1,3 +1,8 @@ +/* +Error alert message that is used on other pages to display an error +Can have title, message, and option to redirect a user when the click the x button on the error +*/ + "use client"; import React from "react"; @@ -6,7 +11,7 @@ import PropTypes from "prop-types"; function ErrorAlert({ Title, Message, RedirectTo }) { const handleClose = () => { if (RedirectTo) { - // Redirect to "/" + // Redirect to the specified page on close of alert window.location.href = RedirectTo; } }; @@ -28,6 +33,7 @@ function ErrorAlert({ Title, Message, RedirectTo }) { onClick={handleClose} // Call handleClose function on click > Close + {/* This path is just the x icon in the alert */} diff --git a/src/app/error/page.jsx b/src/app/error/page.jsx index 4cec131..8bcb322 100644 --- a/src/app/error/page.jsx +++ b/src/app/error/page.jsx @@ -1,3 +1,8 @@ +/* +default error page using the error alert popup from /app/error/error +just displays a red box that says an error occured +*/ + "use client"; import ErrorAlert from "@/app/error/error"; diff --git a/src/app/login/actions.js b/src/app/login/actions.js index 153feaf..5fd75b5 100644 --- a/src/app/login/actions.js +++ b/src/app/login/actions.js @@ -1,4 +1,9 @@ -// https://supabase.com/docs/guides/getting-started/tutorials/with-nextjs +/* +Action to log user in with Spotify using supabase's signInWithOAuth functionality +This gets called from the index page when a user clicks on the "log in with spotify"/"get data" button +The general flow is to redirect the user to spotify, which will then redirect them back to /auth/callback after they sign in using Spotify +refer to this documentation on how to use supabase with nextJs: https://supabase.com/docs/guides/getting-started/tutorials/with-nextjs +*/ "use server"; @@ -7,10 +12,12 @@ import { redirect } from "next/navigation"; import createClient from "@/utils/supabase/server"; +// figure out the baseURL of the application based on if it is running locally in dev env or deployed on vercel const baseURL = process.env.NEXT_PUBLIC_VERCEL_URL ? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}` : "http://localhost:3000"; +// function to log user in with spotify export default async function loginWithSpotify() { const supabase = createClient(); @@ -40,11 +47,17 @@ export default async function loginWithSpotify() { }); } + // redirect to spotify so the user can log in, then redirect back to /auth/callback const { data, error } = await supabase.auth.signInWithOAuth({ provider: "spotify", options: { + // redirects to /auth/callback after going through the spotify login process + // this will ger/refresh the user data if they are already logged in, + // or redirect the user back to the home page to get their data if they are not logged in redirectTo: process.env.NEXT_PUBLIC_REDIRECT_URI, + // this is the url the email verification that the user gets from supabase will redirect them to emailRedirectTo: `${baseURL}/auth/confirm`, + // set the permissions the app needs to get the user data scopes: "user-read-private user-read-email user-library-read user-follow-read user-top-read user-modify-playback-state", }, diff --git a/src/app/page.jsx b/src/app/page.jsx index 3ec8d4a..4fb81d0 100644 --- a/src/app/page.jsx +++ b/src/app/page.jsx @@ -1,3 +1,7 @@ +/* +Actual home page, just uses the home page from Index.jsx +*/ + import HomePage from "./Index"; export default function IndexPage() { diff --git a/src/app/unify/[users]/UnifyContent.jsx b/src/app/unify/[users]/UnifyContent.jsx index cb01f37..c22ac2c 100644 --- a/src/app/unify/[users]/UnifyContent.jsx +++ b/src/app/unify/[users]/UnifyContent.jsx @@ -1,3 +1,7 @@ +/* +This file contains the content that is displayed on the unify page +*/ + import { ResponsiveRadar } from "@nivo/radar"; import { ResponsivePie } from "@nivo/pie"; import { useState, useEffect, useRef } from "react"; @@ -6,6 +10,7 @@ import PropTypes from "prop-types"; import ShareUnify from "@/components/svg-art/share_unify"; import "@/app/globals.css"; +// Find percent match between two lists function calculateGenreSimilarity(list1, list2) { const intersection = Object.keys(list1).filter((key) => Object.prototype.hasOwnProperty.call(list2, key), @@ -15,38 +20,39 @@ function calculateGenreSimilarity(list1, list2) { return Math.round(similarity * 100); // Convert to percentage } -// check how far away matching top artists are from each other in top artists list +// check how far away matching top artists are from each other in top artists list to compute artist similarity function calculateArtistSimilarity(list1, list2) { const maxLength = Math.max(list1.length, list2.length); let Similarity = 0; + // loop through artists in list1 and check if the artist is contained in list 2 for (let i = 0; i < maxLength; i++) { if (list2.includes(list1[i])) { const j = list2.indexOf(list1[i]); + // increase simililarity score based on how close artists are in the two lists + // ex. same top 1 artist gives higher score than same 25th top artist Similarity += 1 / (Math.abs(i - j) + 1) / 5; - // console.log(i, j, Similarity); } - // if (list1[i] === list2[i]) { - // Similarity += maxLength - i; - // } } return Math.min(Similarity * 100, 100); } +// calculates the similarities between the song features for two users, +// where feature1 and feature2 are just an array of objects with feature and value function featureDataSimilarity(features1, features2) { - // console.log(features1, features2); if (features1.length !== features2.length) { throw new Error("Arrays must have the same length"); } let totalDifference = 0; for (let i = 0; i < features1.length; i++) { + // get difference between two scores for each feature totalDifference += Math.abs(features1[i].value - features2[i].value); } // calculate song feature similarity by squaring average difference in song feaure - // console.log(totalDifference / features1.length / 100); return Math.round((1 - totalDifference / features1.length / 100) ** 2 * 100); } +// calculate percent match between the two users by combining gere similarity, feature similarity, and artist similarity function percentMatch(user1, user2) { const genreSimilarity = calculateGenreSimilarity( user1.topGenres, @@ -60,11 +66,13 @@ function percentMatch(user1, user2) { user1.topArtists.map((artist) => artist.name), user2.topArtists.map((artist) => artist.name), ); + // get average of the three scores return Math.round( (genreSimilarity + featureSimilarity + artistSimilarity) / 3, ); } +// function to create vinyl circle svg that is displayed on top of pie chart to form vinyl graphic function VinylCircle({ centerCircleColor, width }) { const newWidth = Math.min((width - 280) / 2, 160); const radii = []; @@ -107,6 +115,7 @@ function VinylCircle({ centerCircleColor, width }) { ); } +// combining vinyl graphic and pie chart to form genre pie chart graphic function GenrePieChart({ data, centerCircleColor }) { const [divWidth, setDivWidth] = useState(0); // Step 1: State for storing div width const divRef = useRef(null); // Step 2: Ref for the div @@ -176,8 +185,9 @@ function GenrePieChart({ data, centerCircleColor }) { ); } +// main function of page which returns the content of unifying the data of two users function UnifyContent({ user1Data, user2Data }) { - // Function to handle sharing + // Function to handle sharing, allows you to share your results when you click the share results button const shareUnify = async () => { // Use Web Share API to share the default image const svgString = ReactDOMServer.renderToString(); @@ -270,14 +280,18 @@ function UnifyContent({ user1Data, user2Data }) { }; }; + // get names for the two users const user1Name = user1Data.userProfile.display_name; const user2Name = user2Data.userProfile.display_name; + // combining the feature data for the two users to display in the radar chart + // nivo requires the data for a chart to be in a specific format, so just converting to that format const map = {}; user1Data.featuresData.forEach((item) => { map[item.feature] = item.value; }); + // this is what is actually combining the feature data from the two users const combinedFeaturesData = user2Data.featuresData.map((item) => { const userData = {}; userData[user1Name] = @@ -287,16 +301,19 @@ function UnifyContent({ user1Data, user2Data }) { return userData; }); + // get top genres for user 1 from their data const user1topGenres = Object.entries(user1Data.topGenres) .sort((a, b) => b[1] - a[1]) // Sort genres by frequency in descending order .slice(0, 5) // Get the top 5 genres .map(([id, value]) => ({ id, value })); // Map to { id: genre, value: frequency } objects + // get top genres for user 2 from their data const user2topGenres = Object.entries(user2Data.topGenres) .sort((a, b) => b[1] - a[1]) // Sort genres by frequency in descending order .slice(0, 5) // Get the top 5 genres .map(([id, value]) => ({ id, value })); // Map to { id: genre, value: frequency } objects + // calculate genre similarity between the two users const genreSimilarity = Math.round( calculateGenreSimilarity(user1Data.topGenres, user2Data.topGenres), ); diff --git a/src/app/unify/[users]/page.jsx b/src/app/unify/[users]/page.jsx index 3271ff6..1bcc155 100644 --- a/src/app/unify/[users]/page.jsx +++ b/src/app/unify/[users]/page.jsx @@ -1,3 +1,8 @@ +/* this page loads the two user's data to display the unify content +it is designed to redirect a user from the link that gets generated from the user page +to bring them to the unify page with them and the other user, or show the data for the +two users if two were given in the url, separated by an '&' */ + "use client"; import { useEffect, useState } from "react"; @@ -7,8 +12,7 @@ import { UnifyContent } from "./UnifyContent"; import ErrorAlert from "@/app/error/error"; export default function UnifyPage({ params: { users } }) { - // console.log(users); - + // split the slug (users) on & (gets replaced as %26 automatically) to get user1 and user2 usernames const [user1, user2] = users.split("%26"); const supabase = createClient(); @@ -22,19 +26,17 @@ export default function UnifyPage({ params: { users } }) { const fetchData = async () => { try { if (!users.includes("%26")) { - // console.log("user: ", users); + // if only one user is provided // Get current user's information from Supabase const { data: currentUser, error } = await supabase.auth.getUser(); if (error) { + // give error if the user is not logged in setError("You must log in to unify."); } - // console.log(currentUser); - - // console.log("id: ", currentUser.user.id); - + // get the spotify id of the current user from the database supabase .from("profiles") .select("username") @@ -44,14 +46,10 @@ export default function UnifyPage({ params: { users } }) { setError("You must log in to unify."); } - // console.log(data); - if (data && data.length > 0) { // Concatenate paramValue with currentUser's ID const redirectURL = `${users}&${data[0].username}`; - // console.log(redirectURL); - // Redirect to the generated URL window.location.href = redirectURL; } @@ -63,15 +61,10 @@ export default function UnifyPage({ params: { users } }) { }; fetchData(); - - // Cleanup function if necessary - return () => { - // Cleanup code if needed - }; }, []); // Empty dependency array ensures useEffect runs only once useEffect(() => { - // find user by username (given as URL slug) in DB + // find first user by username (given as URL slug) in DB supabase .from("profiles") .select("spotify_data") @@ -93,31 +86,31 @@ export default function UnifyPage({ params: { users } }) { }, []); useEffect(() => { - // find user by username (given as URL slug) in DB - supabase - .from("profiles") - .select("spotify_data") - .eq("username", user2) - .then(({ data, error }) => { - // console.log(data, error); - if (error) { - setError("User not found."); - } - - if (data && data.length > 0) { - setUser2Data(data[0].spotify_data); - setLoading(false); - } else { - setLoading(true); - if (users.includes("%26")) { + if (users.includes("%26")) { + // find second user by username (given as URL slug) in DB + supabase + .from("profiles") + .select("spotify_data") + .eq("username", user2) + .then(({ data, error }) => { + // console.log(data, error); + if (error) { setError("User not found."); } - } - // console.log(loading, user1Data, user2Data); - }); + if (data && data.length > 0) { + setUser2Data(data[0].spotify_data); + setLoading(false); + } else { + setLoading(true); + } + + // console.log(loading, user1Data, user2Data); + }); + } }, []); + // content of page with UnifyContent or error message return ( <>
@@ -135,6 +128,7 @@ export default function UnifyPage({ params: { users } }) { ); } +// props defining that a slug is required for the page UnifyPage.propTypes = { params: PropTypes.shape({ users: PropTypes.string.isRequired, diff --git a/src/components/svg-art/left_panel.jsx b/src/components/svg-art/left_panel.jsx index a9de1b2..acec74d 100644 --- a/src/components/svg-art/left_panel.jsx +++ b/src/components/svg-art/left_panel.jsx @@ -1,3 +1,7 @@ +/* +contains some graphics for the home page that are displayed along side the ipod graphic +*/ + export default function LeftPanel() { return ( track.id).join(","); + // get audio features for the user's top songs const audioFeatureData = await getAudioFeatures(token, trackIds); // Check if audioFeatureData is undefined or audio_features is undefined @@ -82,6 +96,7 @@ async function getAverageAudioFeatures(token, topSongs) { const audioFeatures = audioFeatureData.audio_features; + // sum the audio features for each song const featuresSum = audioFeatures.reduce( (acc, feature) => { acc.acousticness += feature.acousticness; @@ -102,11 +117,13 @@ async function getAverageAudioFeatures(token, topSongs) { }, ); + // average the audio features const featuresAvg = Object.keys(featuresSum).reduce((acc, key) => { acc[key] = (featuresSum[key] * 100) / audioFeatures.length; return acc; }, {}); + // calculate average song popularity featuresAvg.popularity = topSongs.items.reduce((acc, song) => acc + song.popularity, 0) / topSongs.items.length; @@ -114,6 +131,7 @@ async function getAverageAudioFeatures(token, topSongs) { return featuresAvg; } +// constructs the user data object async function getSpotifyData(token) { // User Profile const userProfile = await getUserData(token); From 9e26c99a3f63aba71151ca946149e5bf76f5160f Mon Sep 17 00:00:00 2001 From: davidcrair <115373655+davidcrair@users.noreply.github.com> Date: Sat, 13 Apr 2024 15:27:18 -0400 Subject: [PATCH 5/7] Update SpotifyCodeGenerator.js --- src/components/SpotifyCodeGenerator.js | 67 +++++--------------------- 1 file changed, 12 insertions(+), 55 deletions(-) diff --git a/src/components/SpotifyCodeGenerator.js b/src/components/SpotifyCodeGenerator.js index 4a7d944..d4607de 100644 --- a/src/components/SpotifyCodeGenerator.js +++ b/src/components/SpotifyCodeGenerator.js @@ -1,6 +1,14 @@ /* eslint-disable no-bitwise */ + +/* +Old functions to create an svg graphic of the spotify code for a given uri +Uses scannables.scdn.co which is the backend site spotify uses to generate the spotify codes +Spotify codes can be scanned in spotify to open the page for a song, album, user, or artist +*/ + import axios from "axios"; +// take a string and convert it to an integer hash function hashCode(str) { let hash = 0; if (str.length === 0) return hash; @@ -14,6 +22,7 @@ function hashCode(str) { return hash; } +// uses a seed to get a random pastel color function getColorFromSeed(seed) { const baseColor = Math.floor(seed * 16777215).toString(16); // Generate a random base color @@ -36,47 +45,19 @@ function getColorFromSeed(seed) { return `#${pastelColor}`; } -// Example function to modify the SVG content +// function to modify the svg returned by spotify to remove background const modifySvg = (svgString, uri) => { // console.log(uri); // console.log(hashCode(uri)); const parser = new DOMParser(); const doc = parser.parseFromString(svgString, "image/svg+xml"); - // Example: Change the color of all paths to red + // change everything that is white (background) to transparent const whiteRectangles = doc.querySelectorAll('rect[fill="#ffffff"]'); whiteRectangles.forEach((rect) => { rect.setAttribute("fill", "none"); }); - const codeElements = doc.querySelectorAll('rect[fill="#000000"]'); // , path[fill="#000000"]'); - codeElements.forEach((element) => { - element.setAttribute("transform", "translate(300 75) scale(0.25)"); - }); - - const spotifyLogo = doc.querySelectorAll('path[fill="#000000"]'); - spotifyLogo.forEach((logo) => { - logo.setAttribute("transform", "translate(285 60) scale(0.25)"); - }); - - const svgElement = doc.querySelector("svg"); - - for (let i = 0; i < 4; i++) { - // Change 2 to the number of rectangles you want - const rect = doc.createElementNS("http://www.w3.org/2000/svg", "rect"); - rect.setAttribute("x", `${i * 100}`); - rect.setAttribute("y", "0"); - rect.setAttribute("width", "100"); - rect.setAttribute("height", "100"); - - // const randomColor = getRandomColor(); - const randomColor = getColorFromSeed(hashCode(uri) + i * 1000000); - // console.log(randomColor); - rect.setAttribute("fill", randomColor); - - svgElement.insertBefore(rect, svgElement.firstChild); - } - // Convert the modified DOM back to string const serializer = new XMLSerializer(); const modifiedSvgString = serializer.serializeToString(doc); @@ -114,9 +95,7 @@ export default async function GetSpotifyCode(SpotifyURL) { new Uint8Array(response.data), ); - const modifiedSvgString = modifySvg(svgString, URIString); - - return modifiedSvgString; + return svgString; } catch (error) { // console.error(`Error saving Spotify code: ${error.message}`); } @@ -125,26 +104,4 @@ export default async function GetSpotifyCode(SpotifyURL) { return null; } -// function getRandomColor() { -// const baseColor = Math.floor(Math.random() * 16777215).toString(16); // Generate a random base color - -// // Convert the base color to RGB -// const rgb = parseInt(baseColor, 16); -// const r = (rgb >> 16) & 255; -// const g = (rgb >> 8) & 255; -// const b = rgb & 255; - -// // Adjust the brightness and saturation for a pastel effect -// const colorR = Math.floor((r + 255) / 2); -// const colorG = Math.floor((g + 255) / 2); -// const colorB = Math.floor((b + 255) / 2); - -// // Convert the pastel color back to hex -// const pastelColor = ((colorR << 16) | (colorG << 8) | colorB) -// .toString(16) -// .padStart(6, "0"); - -// return `#${pastelColor}`; -// } - export { hashCode, getColorFromSeed, modifySvg, GetSpotifyCode }; From 82968c960c5de1b41c9b7b785c01f6cc74edfedc Mon Sep 17 00:00:00 2001 From: davidcrair <115373655+davidcrair@users.noreply.github.com> Date: Sat, 13 Apr 2024 15:27:34 -0400 Subject: [PATCH 6/7] Add tests for share cassette and share unify --- __tests__/share_cassette.t.js | 93 +++++++++++++++++++++++++++++++++++ __tests__/share_unify.t.js | 93 +++++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 __tests__/share_cassette.t.js create mode 100644 __tests__/share_unify.t.js diff --git a/__tests__/share_cassette.t.js b/__tests__/share_cassette.t.js new file mode 100644 index 0000000..17fc285 --- /dev/null +++ b/__tests__/share_cassette.t.js @@ -0,0 +1,93 @@ +/* eslint-disable react/jsx-filename-extension */ + +import React from "react"; +import { render, fireEvent, waitFor, screen } from "@testing-library/react"; +import UserPage from "@/app/user/[slug]/page"; + +import userData from "./userData.json"; + +// Mocking the supabase client and its methods +jest.mock("../src/utils/supabase/client", () => { + return jest.fn(() => ({ + from: jest.fn().mockReturnThis(), + select: jest.fn().mockReturnThis(), + eq: jest.fn().mockResolvedValue({ + data: [ + { + spotify_data: userData, + }, + ], + error: null, + }), + })); +}); + +jest.mock("@nivo/radar", () => ({ + ResponsiveRadar: () => ( +
Mock Responsive Radar
+ ), +})); + +jest.mock("@nivo/pie", () => ({ + ResponsivePie: () => ( +
Mock Responsive Pie
+ ), +})); + +// Mocking the navigator.share API +global.navigator.share = jest.fn(); + +global.Image = class { + constructor() { + setTimeout(() => { + this.onload(); + }, 50); // Simulate async image loading + } +}; + +// Mock the getContext and other canvas methods +HTMLCanvasElement.prototype.getContext = () => ({ + drawImage: jest.fn(), // Mock drawImage to avoid jsdom type errors + fillText: jest.fn(), + clearRect: jest.fn(), +}); + +HTMLCanvasElement.prototype.toBlob = jest.fn((callback, type, quality) => { + setTimeout(() => { + callback(new Blob(["test"], { type })); + }, 0); +}); + +beforeEach(() => { + global.navigator.share = jest.fn(() => Promise.resolve()); +}); + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe("shareCassette", () => { + it("should attempt to share using navigator.share when image is ready", async () => { + render(); + + // Wait for the button with specific text and style to appear in the document + const shareButton = await screen.findByRole("button", { + name: /share cassette/i, + }); + + fireEvent.click(shareButton); + + await waitFor(() => { + expect(navigator.share).toHaveBeenCalled(); + }); + + // Check navigator.share is called with the expected parameters + expect(navigator.share).toHaveBeenCalledWith( + expect.objectContaining({ + title: "Unify with me!", + text: "Compare our stats on Uni.fy", + url: expect.stringContaining("unify/"), // Check if URL is formed correctly + }), + ); + }); +}); diff --git a/__tests__/share_unify.t.js b/__tests__/share_unify.t.js new file mode 100644 index 0000000..1230ed5 --- /dev/null +++ b/__tests__/share_unify.t.js @@ -0,0 +1,93 @@ +/* eslint-disable react/jsx-filename-extension */ + +import React from "react"; +import { render, fireEvent, waitFor, screen } from "@testing-library/react"; +import { UnifyContent } from "@/app/unify/[users]/UnifyContent"; + +import userData from "./userData.json"; + +// Mocking the supabase client and its methods +jest.mock("../src/utils/supabase/client", () => { + return jest.fn(() => ({ + from: jest.fn().mockReturnThis(), + select: jest.fn().mockReturnThis(), + eq: jest.fn().mockResolvedValue({ + data: [ + { + spotify_data: userData, + }, + ], + error: null, + }), + })); +}); + +jest.mock("@nivo/radar", () => ({ + ResponsiveRadar: () => ( +
Mock Responsive Radar
+ ), +})); + +jest.mock("@nivo/pie", () => ({ + ResponsivePie: () => ( +
Mock Responsive Pie
+ ), +})); + +// Mocking the navigator.share API +global.navigator.share = jest.fn(); + +global.Image = class { + constructor() { + setTimeout(() => { + this.onload(); + }, 50); // Simulate async image loading + } +}; + +// Mock the getContext and other canvas methods +HTMLCanvasElement.prototype.getContext = () => ({ + drawImage: jest.fn(), // Mock drawImage to avoid jsdom type errors + fillText: jest.fn(), + clearRect: jest.fn(), + strokeText: jest.fn(), +}); + +HTMLCanvasElement.prototype.toBlob = jest.fn((callback, type, quality) => { + setTimeout(() => { + callback(new Blob(["test"], { type })); + }, 0); +}); + +beforeEach(() => { + global.navigator.share = jest.fn(() => Promise.resolve()); +}); + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe("shareCassette", () => { + it("should attempt to share using navigator.share when image is ready", async () => { + render(); + + // Wait for the button with specific text and style to appear in the document + const shareButton = await screen.findByRole("button", { + name: /share results/i, + }); + + fireEvent.click(shareButton); + + await waitFor(() => { + expect(navigator.share).toHaveBeenCalled(); + }); + + // Check navigator.share is called with the expected parameters + expect(navigator.share).toHaveBeenCalledWith( + expect.objectContaining({ + title: "Unify with me!", + text: "Compare our stats on Unify", + }), + ); + }); +}); From 61fd563a8859e2330345c5258bd20786a5c5f1ad Mon Sep 17 00:00:00 2001 From: davidcrair <115373655+davidcrair@users.noreply.github.com> Date: Sat, 13 Apr 2024 15:32:52 -0400 Subject: [PATCH 7/7] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bdb388d..63e7b67 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ To test the application: npm test ``` -We have achieved 77% statement coverage. +We have achieved 81% statement coverage. ## Linting