;
-}
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 (