Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Websockets + Google Authorization #55

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions client/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
/* config options here */
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'upload.wikimedia.org',
pathname: '/**/*.{png,svg}'
// TODO: Change below (will be unneeded)
hostname: '*'
// hostname: 'upload.wikimedia.org',
// pathname: '/**/*.{png,svg}'
},
]
}
Expand Down
1 change: 1 addition & 0 deletions client/public/images/award-solid.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions client/public/images/circle-plus-solid.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions client/public/images/circle-solid.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions client/public/images/clock-solid.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions client/public/images/heart-solid.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions client/public/images/magnifying-glass-solid.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions client/public/images/message-solid.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions client/public/images/thumbs-down-solid.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions client/public/images/thumbs-up-solid.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions client/src/app/communication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import axios, { AxiosRequestConfig, InternalAxiosRequestConfig,AxiosHeaders } from "axios";
import { ACCESS_TOKEN } from "../constants/constants";

const backendConnection = axios.create();

backendConnection.interceptors.request.use(
(config: AxiosRequestConfig) => {
const internalConfig = config as InternalAxiosRequestConfig<any>;
const token = localStorage.getItem(ACCESS_TOKEN);
if (token) {
internalConfig.headers = new AxiosHeaders({
...internalConfig.headers,
Authorization: `Bearer ${token}`,
});
}
return internalConfig;
},
(error) => {
return Promise.reject(error);
},
);

export default backendConnection;

150 changes: 150 additions & 0 deletions client/src/app/components/Leaderboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import React from "react";
import clsx from "clsx";

// Type definitions
type LeaderboardEntry = {
id: number;
name: string;
profilePicture: string;
likes: number;
};

type LeaderboardProps = {
users: LeaderboardEntry[];
};

export const Leaderboard: React.FC<LeaderboardProps> = ({ users }) => {
const crownIcon =
"https://cdn.builder.io/api/v1/image/assets/TEMP/e57ce0fb4d7e6f9d8ae6cf2dda741add41521ccec68798186f13f301773163b5?placeholderIfAbsent=true";

return (
<div className="flex items-center justify-center min-h-screen bg-white">
<div className="w-full max-w-4xl p-8">
{/* Top 3 Section */}
<div className="flex items-end justify-center gap-16 mb-12 relative">
{/* Crown */}
<img
src={crownIcon}
alt="Crown"
className="absolute -top-16 left-1/2 transform -translate-x-1/2 w-16"
/>

{/* Second Place */}
{users[1] && (
<div className="flex flex-col items-center w-1/3">
<h2 className="text-2xl font-extrabold text-gray-800 mb-4">2nd</h2>
<div
className={clsx(
"w-28 h-28 rounded-full flex items-center justify-center bg-blue-400 shadow-md"
)}
>
<img
src={users[1].profilePicture}
alt="Second place"
className="w-24 h-24 rounded-full"
/>
</div>
<div className="flex items-center gap-2 mt-2">
<span className="text-gray-700 font-bold text-lg">{users[1].likes}</span>
<HeartIcon size="lg" />
</div>
</div>
)}

{/* First Place */}
{users[0] && (
<div className="flex flex-col items-center w-1/3">
<div className="relative">
<div
className={clsx(
"w-36 h-36 rounded-full flex items-center justify-center bg-blue-500 shadow-lg"
)}
>
<img
src={users[0].profilePicture}
alt="First place"
className="w-32 h-32 rounded-full"
/>
</div>
</div>
<div className="flex items-center gap-2 mt-2">
<span className="text-gray-700 font-bold text-lg">{users[0].likes}</span>
<HeartIcon size="lg" />
</div>
</div>
)}

{/* Third Place */}
{users[2] && (
<div className="flex flex-col items-center w-1/3">
<h2 className="text-2xl font-extrabold text-gray-800 mb-4">3rd</h2>
<div
className={clsx(
"w-28 h-28 rounded-full flex items-center justify-center bg-blue-400 shadow-md"
)}
>
<img
src={users[2].profilePicture}
alt="Third place"
className="w-24 h-24 rounded-full"
/>
</div>
<div className="flex items-center gap-2 mt-2">
<span className="text-gray-700 font-bold text-lg">{users[2].likes}</span>
<HeartIcon size="lg" />
</div>
</div>
)}
</div>

{/* Full Leaderboard */}
<ul className="space-y-4">
{users.map((user, index) => (
<li
key={user.id}
className={clsx(
"flex items-center justify-between p-4 border rounded-full shadow-md bg-white",
"border-black"
)}
>
<div className="flex items-center gap-4">
<div
className="flex items-center justify-center w-10 h-10 rounded-full border border-black"
>
<img
src={user.profilePicture}
alt={`${user.name}'s avatar`}
className="w-6 h-6"
/>
</div>
<p className="font-medium text-gray-800">{`${index + 1}. ${user.name}`}</p>
</div>
<div className="flex items-center gap-2">
<span className="text-gray-700 font-semibold">{user.likes}</span>
<HeartIcon />
</div>
</li>
))}
</ul>
</div>
</div>
);
};

const HeartIcon: React.FC<{ size?: "lg" }> = ({ size }) => {
const className = size === "lg" ? "w-8 h-8" : "w-6 h-6";
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="black"
strokeWidth="2"
className={className}
>
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
</svg>
);
};

export default Leaderboard;
178 changes: 178 additions & 0 deletions client/src/app/components/NavBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"use client";

import React, { useState, useEffect, useRef } from "react";
import NavBarLink from "./NavBarLink.tsx";
import Image from "next/image";
import Link from "next/link";
import createIcon from "/public/images/circle-plus-solid.svg";
import messagesIcon from "/public/images/message-solid.svg";
import historyIcon from "/public/images/clock-solid.svg";
import leaderboardIcon from "/public/images/award-solid.svg";
import tempPFP from "/public/images/circle-solid.svg";
import searchIcon from "/public/images/magnifying-glass-solid.svg";

interface MyComponentProps {}

const NavBar: React.FC<MyComponentProps> = ({}) => {
const [PFP, setPFP] = useState<string>("");
const [username, setUsername] = useState<string>("");

// Dropdown state
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);

// TODO: Get data from BE
const fetchData = () => {
return {
PFP: "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_1280.png",
username: "ORIGAMI",
};
};

// Initial fetch
useEffect(() => {
const data = fetchData();
setPFP(data.PFP);
setUsername(data.username);
}, []);

// State to toggle the search bar in mobile view
const [isMobileShowNav, setIsMobileShowNav] = useState(false);

// Reference to the search bar div for detecting clicks outside
const searchBarRef = useRef<HTMLDivElement>(null);

// Function to handle screen resize and toggle off mobile view when screen becomes large
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= 1024) {
setIsMobileShowNav(false);
}
};

window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);

// Close the search bar if clicked outside
useEffect(() => {
const handleOutsideClick = (event: MouseEvent) => {
if (
searchBarRef.current &&
!searchBarRef.current.contains(event.target as Node)
) {
setIsMobileShowNav(false);
}
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsDropdownOpen(false);
}
};

document.addEventListener("mousedown", handleOutsideClick);
return () => {
document.removeEventListener("mousedown", handleOutsideClick);
};
}, []);

return (
<div className="mt-4 flex h-10 w-10/12 items-center justify-between gap-1 rounded bg-white outline outline-gray-300">
{/* Left Options */}
<div className="flex">
{/* Logo */}
<Link
href="/"
className="ml-2 flex flex-shrink-0 items-center justify-center p-1 text-[20px] font-bold text-blue-500 transition hover:scale-105 hover:opacity-90 active:opacity-80 lg:mr-4 lg:p-2 lg:text-[24px]"
>
Swipe Style
</Link>
{/* Component Pop Ups */}
<div
className={`flex ${isMobileShowNav && window.innerWidth < 1024 ? "hidden" : ""}`}
>
<NavBarLink image={createIcon} imageAlt={"Create"}></NavBarLink>
<NavBarLink image={historyIcon} imageAlt={"History"}></NavBarLink>
<NavBarLink image={messagesIcon} imageAlt={"Messages"}></NavBarLink>
<NavBarLink
image={leaderboardIcon}
imageAlt={"Leaderboard"}
></NavBarLink>
</div>
</div>

{/* Search Bar */}
<div
ref={searchBarRef}
className={`flex items-center transition-all duration-300 ${
isMobileShowNav && window.innerWidth < 1024
? "w-6/12 bg-gray-100 outline outline-gray-400"
: "w-2/12 md:w-3/12 lg:bg-gray-100"
} justify-center rounded focus-within:outline-gray-400 lg:outline lg:outline-gray-200`}
>
<Image
src={searchIcon}
width={20}
height={20}
alt="Search"
className={`ml-2 cursor-pointer opacity-50 transition hover:scale-110 hover:opacity-60 lg:mx-2`}
onClick={() => {
if (window.innerWidth < 1024) setIsMobileShowNav(!isMobileShowNav);
}}
/>
<input
type="text"
className={`w-full bg-gray-100 text-gray-600 outline-none ${
isMobileShowNav && window.innerWidth < 1024
? "block"
: "hidden lg:block"
}`}
placeholder="Search for styles..."
/>
</div>

{/* User Profile Button */}
<div
className="relative z-50 mx-1 flex items-center justify-center rounded p-0.5 px-2 transition hover:opacity-90 active:opacity-75 md:bg-blue-500"
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
>
<Image
src={PFP ? PFP : tempPFP}
width={30}
height={30}
alt="Profile Picture"
className="m-0.5 mr-2 flex-shrink-0 rounded-full opacity-30"
/>
<p className="mr-1 hidden font-bold text-white md:block">{username}</p>
{isDropdownOpen && (
<div
ref={dropdownRef}
className="absolute right-0 top-full z-50 mt-2 flex w-40 flex-col rounded bg-white p-2 shadow-lg outline outline-gray-300"
onMouseEnter={() => setIsDropdownOpen(true)}
onMouseLeave={() => setIsDropdownOpen(false)}
>
<Link
href="/profile"
className="rounded bg-white px-4 py-2 text-gray-800 hover:bg-gray-200"
>
Profile
</Link>
<span
className="rounded bg-white px-4 py-2 text-left text-gray-800 hover:bg-gray-200"
onClick={() => {
console.log("Log Out clicked"); // Replace with actual logout logic
}}
>
Log Out
</span>
</div>
)}
</div>
</div>
);
};

export default NavBar;
Loading