diff --git a/.env b/.env
index 05886e1..4d9441a 100644
--- a/.env
+++ b/.env
@@ -3,3 +3,4 @@ NEXT_PUBLIC_REDIRECT_URI="http://localhost:3000/auth/callback"
NEXT_PUBLIC_AUTH_ENDPOINT="https://accounts.spotify.com/authorize"
NEXT_PUBLIC_RESPONSE_TYPE="token"
NEXT_PUBLIC_BACKEND_URL="http://localhost:3001"
+NEXT_PUBLIC_FRONTENT_URL="localhost:3000"
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 81956b7..00c1867 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"name": "unify",
"version": "0.1.0",
"dependencies": {
+ "@nivo/pie": "^0.85.1",
"@nivo/radar": "^0.85.1",
"@supabase/ssr": "^0.1.0",
"@supabase/supabase-js": "^2.39.8",
@@ -1495,6 +1496,21 @@
"node": ">= 10"
}
},
+ "node_modules/@nivo/arcs": {
+ "version": "0.85.1",
+ "resolved": "https://registry.npmjs.org/@nivo/arcs/-/arcs-0.85.1.tgz",
+ "integrity": "sha512-UwwiSXHWY8cIgi3FADQJX8gyFCJfdx1N80MzxFGuHOYbTcBmsRMMbZYfqXJ5z/x61ulTkLcv/yVvlTEOCKMlcQ==",
+ "dependencies": {
+ "@nivo/colors": "0.85.1",
+ "@nivo/core": "0.85.1",
+ "@react-spring/web": "9.4.5 || ^9.7.2",
+ "@types/d3-shape": "^2.0.0",
+ "d3-shape": "^1.3.5"
+ },
+ "peerDependencies": {
+ "react": ">= 16.14.0 < 19.0.0"
+ }
+ },
"node_modules/@nivo/colors": {
"version": "0.85.1",
"resolved": "https://registry.npmjs.org/@nivo/colors/-/colors-0.85.1.tgz",
@@ -1558,6 +1574,23 @@
"react": ">= 16.14.0 < 19.0.0"
}
},
+ "node_modules/@nivo/pie": {
+ "version": "0.85.1",
+ "resolved": "https://registry.npmjs.org/@nivo/pie/-/pie-0.85.1.tgz",
+ "integrity": "sha512-2dSQ7YIc6BLkYFadg+r6uOR5FXOCRSCWAYEIlvMapAvYqQ6/ie3ZnMtEB9idiucy8F4I/zF5C08OSr2jE4DJ9g==",
+ "dependencies": {
+ "@nivo/arcs": "0.85.1",
+ "@nivo/colors": "0.85.1",
+ "@nivo/core": "0.85.1",
+ "@nivo/legends": "0.85.1",
+ "@nivo/tooltip": "0.85.1",
+ "@types/d3-shape": "^2.0.0",
+ "d3-shape": "^1.3.5"
+ },
+ "peerDependencies": {
+ "react": ">= 16.14.0 < 19.0.0"
+ }
+ },
"node_modules/@nivo/radar": {
"version": "0.85.1",
"resolved": "https://registry.npmjs.org/@nivo/radar/-/radar-0.85.1.tgz",
diff --git a/package.json b/package.json
index 963c3fc..8168531 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"prepare": "husky install"
},
"dependencies": {
+ "@nivo/pie": "^0.85.1",
"@nivo/radar": "^0.85.1",
"@supabase/ssr": "^0.1.0",
"@supabase/supabase-js": "^2.39.8",
diff --git a/src/app/Index.jsx b/src/app/Index.jsx
index f4b01da..9b3ae11 100644
--- a/src/app/Index.jsx
+++ b/src/app/Index.jsx
@@ -3,54 +3,35 @@
import { useEffect, useState } from "react";
import IndexContent from "@/components/svg-art/index_content";
-
-const CLIENT_ID = process.env.NEXT_PUBLIC_CLIENT_ID;
-const REDIRECT_URI = process.env.NEXT_PUBLIC_REDIRECT_URI;
-const AUTH_ENDPOINT = process.env.NEXT_PUBLIC_AUTH_ENDPOINT;
-const RESPONSE_TYPE = process.env.NEXT_PUBLIC_RESPONSE_TYPE;
+import createClient from "@/utils/supabase/client";
+import { loginWithSpotify } from "./login/actions";
export default function HomePage() {
- const [token, setToken] = useState(null);
-
- const handleLogin = () => {
- const params = new URLSearchParams();
- params.append("client_id", CLIENT_ID);
- params.append("response_type", RESPONSE_TYPE);
- params.append("redirect_uri", REDIRECT_URI);
- params.append(
- "scope",
- "user-read-private user-read-email user-library-read user-follow-read user-top-read user-modify-playback-state",
- );
-
- const url = `${AUTH_ENDPOINT}?${params.toString()}`;
-
- // Open Spotify login in same window, will redirect back
- window.open(url, "_self");
- };
-
- const enterCode = () => {
- // console.log("enter code");
- };
-
- const handleTokenFromCallback = () => {
- // Extract the token from the URL hash
- const urlParams = new URLSearchParams(window.location.hash.substr(1));
- const newToken = urlParams.get("access_token");
-
- if (newToken) {
- setToken(newToken);
- window.localStorage.setItem("token", newToken);
- }
- };
+ const supabase = createClient();
- // Check for token in the URL hash when component mounts
+ // check if user is already logged in
useEffect(() => {
- handleTokenFromCallback();
+ (async () => {
+ // console.log("use effect running");
+ const {
+ data: { user },
+ } = await supabase.auth.getUser();
+ // console.log("user: ", user);
+ if (user) {
+ // already logged in
+ // router.replace("/account");
+ // console.log("user is logged in");
+ } else {
+ // console.log("user is not logged in");
+ }
+ })().catch((err) => {
+ console.error(err); // TODO display error message to user
+ });
}, []);
return (
-
+
);
}
diff --git a/src/app/auth/callback/route.js b/src/app/auth/callback/route.js
index b226e28..fe72327 100644
--- a/src/app/auth/callback/route.js
+++ b/src/app/auth/callback/route.js
@@ -10,6 +10,8 @@ export async function GET(request) {
const redirectTo = request.nextUrl.clone();
redirectTo.searchParams.delete("code");
+ // console.log("code: ", code);
+
if (code) {
const supabase = createClient();
@@ -37,10 +39,17 @@ export async function GET(request) {
return NextResponse.redirect(redirectTo);
}
+ // console.log("spotify user data: ", spotifyUserData);
+
+ // console.log("username: ", spotifyUserData.userProfile.id);
+
// update DB with Spotify username + Spotify data
const { dbError } = await supabase
.from("profiles")
- .update({ username: spotifyUserData.id, spotify_data: spotifyUserData })
+ .update({
+ username: spotifyUserData.userProfile.id,
+ spotify_data: spotifyUserData,
+ })
.eq("id", data.user.id);
if (dbError) {
@@ -50,7 +59,10 @@ export async function GET(request) {
}
// once finished, redirect user to account page
- redirectTo.pathname = "/account";
+ redirectTo.pathname = `/user/${spotifyUserData.userProfile.id}`;
return NextResponse.redirect(redirectTo);
}
+
+ redirectTo.pathname = "/";
+ return NextResponse.redirect(redirectTo);
}
diff --git a/src/app/globals.css b/src/app/globals.css
index 867624e..e78ae48 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -9,3 +9,36 @@
font-weight: 400;
font-style: normal;
}
+
+.circle-row {
+ display: flex;
+ justify-content: center;
+}
+
+.circle {
+ width: 300px;
+ height: 300px;
+ border-radius: 50%;
+ background-color: #d1d5db;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin: 0 10px;
+ flex-direction: column;
+}
+
+.circle:nth-child(2) {
+ margin-top: -40px; /* Adjust as needed */
+}
+
+.circle-text-large {
+ font-size: 58px;
+ color: black;
+ font-family: "Koulen", sans-serif;
+}
+
+.circle-text-small {
+ font-size: 36px;
+ color: black;
+ font-family: "Koulen", sans-serif;
+}
diff --git a/src/app/login/page.jsx b/src/app/login/page.jsx
index 3886f01..5765047 100644
--- a/src/app/login/page.jsx
+++ b/src/app/login/page.jsx
@@ -8,7 +8,7 @@ import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import createClient from "@/utils/supabase/client";
-import { login, loginWithSpotify, signup } from "./actions";
+import { loginWithSpotify } from "./actions";
export default function LoginPage() {
const router = useRouter();
@@ -37,35 +37,13 @@ export default function LoginPage() {
return (
!loading && (
- <>
-
+
- NOTE: You will need to confirm your email after signing up! If not,
- you will not be able to login.
+
-
-
-
-
-
-
-
-
-
- >
+
)
);
}
diff --git a/src/app/unify/[users]/page.jsx b/src/app/unify/[users]/page.jsx
new file mode 100644
index 0000000..84fc418
--- /dev/null
+++ b/src/app/unify/[users]/page.jsx
@@ -0,0 +1,128 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import PropTypes from "prop-types";
+import createClient from "@/utils/supabase/client";
+import UnifyContent from "@/components/UnifyContent";
+
+export default function UnifyPage({ params: { users } }) {
+ // console.log(users);
+
+ const [user1, user2] = users.split("%26");
+
+ const supabase = createClient();
+
+ const [loading, setLoading] = useState(true);
+ const [user1Data, setUser1Data] = useState(null);
+ const [user2Data, setUser2Data] = useState(null);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ if (!users.includes("%26")) {
+ // console.log("user: ", users);
+
+ // Get current user's information from Supabase
+ const { data: currentUser, error } = await supabase.auth.getUser();
+
+ if (error) {
+ throw error;
+ }
+
+ // console.log(currentUser);
+
+ // console.log("id: ", currentUser.user.id);
+
+ supabase
+ .from("profiles")
+ .select("username")
+ .eq("id", currentUser.user.id)
+ .then(({ data, error2 }) => {
+ if (error2) {
+ // TODO
+ console.error(error2); // TODO display error message to user
+ }
+
+ // 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;
+ }
+ });
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error.message);
+ }
+ };
+
+ 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
+ supabase
+ .from("profiles")
+ .select("spotify_data")
+ .eq("username", user1)
+ .then(({ data, error }) => {
+ if (error) {
+ // TODO
+ console.error(error); // TODO display error message to user
+ }
+
+ if (data && data.length > 0) {
+ setUser1Data(data[0].spotify_data);
+ }
+
+ setLoading(false);
+ });
+ }, []);
+
+ useEffect(() => {
+ // find user by username (given as URL slug) in DB
+ supabase
+ .from("profiles")
+ .select("spotify_data")
+ .eq("username", user2)
+ .then(({ data, error }) => {
+ if (error) {
+ // TODO
+ console.error(error); // TODO display error message to user
+ }
+
+ if (data && data.length > 0) {
+ setUser2Data(data[0].spotify_data);
+ }
+
+ setLoading(false);
+ });
+ }, []);
+
+ return (
+
+ {!loading && user1Data && user2Data && (
+
+
+
+ )}
+ {!loading && (!user1Data || !user2Data) &&
User not found!
}
+
+ );
+}
+
+UnifyPage.propTypes = {
+ params: PropTypes.shape({
+ users: PropTypes.string.isRequired,
+ }).isRequired,
+};
diff --git a/src/app/user/[slug]/page.jsx b/src/app/user/[slug]/page.jsx
index bf7cc35..443d31e 100644
--- a/src/app/user/[slug]/page.jsx
+++ b/src/app/user/[slug]/page.jsx
@@ -2,7 +2,10 @@
import { useEffect, useState } from "react";
import PropTypes from "prop-types";
+import ReactDOMServer from "react-dom/server";
import createClient from "@/utils/supabase/client";
+import UserContent from "@/components/svg-art/user_content";
+import ShareCassette from "@/components/svg-art/share_cassette";
export default function UserPage({ params: { slug } }) {
const supabase = createClient();
@@ -10,6 +13,84 @@ export default function UserPage({ params: { slug } }) {
const [loading, setLoading] = useState(true);
const [userData, setUserData] = useState(null);
+ // Function to handle sharing
+ const shareCassette = async () => {
+ // console.log("sharing song");
+
+ // Use Web Share API to share the default image
+ const svgString = ReactDOMServer.renderToString();
+ // console.log(svgString);
+
+ const img = new Image();
+
+ // Set the source of the image
+ img.src = `data:image/svg+xml;utf8,${encodeURIComponent(svgString)}`;
+
+ // Wait for the image to load
+ img.onload = () => {
+ // Create a canvas element
+ const canvas = document.createElement("canvas");
+ const ctx = canvas.getContext("2d");
+
+ if (!ctx) {
+ console.error("Unable to obtain 2D context for canvas.");
+ return;
+ }
+
+ canvas.width = img.width;
+ canvas.height = img.height;
+
+ // Clear canvas
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+ // Add canvas to the document body for debugging
+ // document.body.appendChild(canvas);
+
+ canvas.width = img.width;
+ canvas.height = img.height;
+
+ // Draw the image on the canvas
+ canvas.getContext("2d")?.drawImage(img, 0, 0);
+
+ ctx.textAlign = "center";
+
+ // Render the text onto the canvas
+ ctx.font = "20px Koulen";
+ ctx.fillStyle = "black";
+ ctx.fillText(
+ `@${userData.userProfile.display_name}`,
+ canvas.width / 2,
+ 389,
+ );
+
+ // Convert canvas to blob
+ canvas.toBlob((blob) => {
+ // console.log(blob);
+
+ if (navigator.share) {
+ // console.log("Web share API supported");
+ navigator
+ .share({
+ title: "Unify with me!",
+ text: `Compare our stats on Uni.fy`,
+ url: `http://${process.env.NEXT_PUBLIC_FRONTENT_URL}/unify/${userData.userProfile.id}`,
+ files: [
+ new File([blob], "file.png", {
+ type: blob.type,
+ }),
+ ],
+ })
+ .then(() => {
+ // console.log("Shared successfully");
+ })
+ .catch((error) => console.error("Error sharing:", error));
+ } else {
+ // console.log("Web Share API not supported");
+ }
+ }, "image/png");
+ };
+ };
+
useEffect(() => {
// find user by username (given as URL slug) in DB
supabase
@@ -23,6 +104,7 @@ export default function UserPage({ params: { slug } }) {
}
if (data && data.length > 0) {
+ // console.log("topArtists: ", data[0].spotify_data.topArtists);
setUserData(data[0].spotify_data);
}
@@ -31,8 +113,15 @@ export default function UserPage({ params: { slug } }) {
}, []);
return (
-
User Page
- {!loading && userData &&
{JSON.stringify(userData)}
}
+ {!loading && userData && (
+
+
+
+ )}
{!loading && !userData &&
User not found!
}
);
diff --git a/src/components/UnifyContent.jsx b/src/components/UnifyContent.jsx
new file mode 100644
index 0000000..51ebbb4
--- /dev/null
+++ b/src/components/UnifyContent.jsx
@@ -0,0 +1,347 @@
+import { ResponsiveRadar } from "@nivo/radar";
+import { ResponsivePie } from "@nivo/pie";
+import "@/app/globals.css";
+import ReactDOMServer from "react-dom/server";
+import ShareUnify from "@/components/svg-art/share_unify";
+
+function calculateSimilarity(list1, list2) {
+ const intersection = Object.keys(list1).filter((key) =>
+ Object.prototype.hasOwnProperty.call(list2, key),
+ ).length;
+ const union = Object.keys({ ...list1, ...list2 }).length;
+ const similarity = intersection / union;
+ return similarity * 100; // Convert to percentage
+}
+
+function UnifyContent({ user1Data, user2Data }) {
+ // console.log(user1Data.featuresData);
+ // console.log(user2Data.featuresData);
+
+ // Function to handle sharing
+ const shareUnify = async () => {
+ // console.log("sharing song");
+
+ // Use Web Share API to share the default image
+ const svgString = ReactDOMServer.renderToString();
+ // console.log(svgString);
+
+ const img = new Image();
+
+ // Set the source of the image
+ img.src = `data:image/svg+xml;utf8,${encodeURIComponent(svgString)}`;
+
+ // Wait for the image to load
+ img.onload = () => {
+ // Create a canvas element
+ const canvas = document.createElement("canvas");
+ const ctx = canvas.getContext("2d");
+
+ if (!ctx) {
+ console.error("Unable to obtain 2D context for canvas.");
+ return;
+ }
+
+ canvas.width = img.width;
+ canvas.height = img.height;
+
+ // Clear canvas
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+ // Add canvas to the document body for debugging
+ // document.body.appendChild(canvas);
+
+ canvas.width = img.width;
+ canvas.height = img.height;
+
+ // Draw the image on the canvas
+ canvas.getContext("2d")?.drawImage(img, 0, 0);
+
+ ctx.textAlign = "center";
+
+ // Render the text onto the canvas
+ ctx.font = "20px Koulen";
+ ctx.fillStyle = "black";
+ ctx.fillText(
+ `@${user2Data.userProfile.display_name}`,
+ canvas.width / 2,
+ 302,
+ );
+ ctx.fillText(
+ `@${user1Data.userProfile.display_name}`,
+ canvas.width / 2,
+ 501,
+ );
+
+ // Convert canvas to blob
+ canvas.toBlob((blob) => {
+ // console.log(blob);
+
+ if (navigator.share) {
+ // console.log("Web share API supported");
+ navigator
+ .share({
+ title: "Unify with me!",
+ text: `Compare our stats on Uni.fy`,
+ url: "unify",
+ files: [
+ new File([blob], "file.png", {
+ type: blob.type,
+ }),
+ ],
+ })
+ .then(() => {
+ // console.log("Shared successfully");
+ })
+ .catch((error) => console.error("Error sharing:", error));
+ } else {
+ // console.log("Web Share API not supported");
+ }
+ }, "image/png");
+ };
+ };
+
+ const user1Name = user1Data.userProfile.display_name;
+ const user2Name = user2Data.userProfile.display_name;
+
+ const map = {};
+ user1Data.featuresData.forEach((item) => {
+ map[item.feature] = item.value;
+ });
+
+ const combinedFeaturesData = user2Data.featuresData.map((item) => {
+ const userData = {};
+ userData[user1Name] =
+ map[item.feature] !== undefined ? map[item.feature] : undefined;
+ userData[user2Name] = item.value;
+ userData.feature = item.feature;
+ return userData;
+ });
+
+ 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
+
+ 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
+
+ const genreSimilarity = calculateSimilarity(
+ user1Data.topGenres,
+ user2Data.topGenres,
+ );
+
+ // console.log("combinedFeaturesData: ", combinedFeaturesData);
+
+ return (
+ <>
+
+ {" "}
+ 0% Match!
+
+
+ {/* */}
+
+
+ @{user1Data.userProfile.display_name}
+
+
+
+
+ @{user2Data.userProfile.display_name}
+
+
+
+
+
+ +5%
+ Same Top Artist
+
+
+ {genreSimilarity}%
+ Genres Shared
+
+
+ +10%
+ Same Top Song
+
+
+
+
+
+
+
+
+ Top Artists:
+
+ {user1Data.topArtists.map((artist) => (
+
{artist.name}
+ ))}
+
+
+
+
+ Top Artists:
+
+ {user2Data.topArtists.map((artist) => (
+
{artist.name}
+ ))}
+
+
+ {user1topGenres ? (
+
+
+
+ ) : (
+
Loading...
+ )}
+ {user2topGenres ? (
+
+
+
+ ) : (
+
Loading...
+ )}
+
+
+ Top Songs:
+
+ {user1Data.topSongs.map((song) => (
+
{song.name}
+ ))}
+
+
+
+
+ Top Songs:
+
+ {user2Data.topSongs.map((song) => (
+
{song.name}
+ ))}
+
+
+
+
+ {combinedFeaturesData ? (
+
+
+
+ ) : (
+
Loading...
+ )}
+
+ >
+ );
+}
+export default UnifyContent;
diff --git a/src/components/svg-art/index_content.jsx b/src/components/svg-art/index_content.jsx
index 0565582..fbed7d2 100644
--- a/src/components/svg-art/index_content.jsx
+++ b/src/components/svg-art/index_content.jsx
@@ -1,4 +1,6 @@
-export default function IndexContent({ handleLogin, enterCode }) {
+import { loginWithSpotify } from "@/app/login/actions";
+
+export default function IndexContent() {
return (