diff --git a/client/next.config.ts b/client/next.config.ts index fb46ba2..c40d2d9 100644 --- a/client/next.config.ts +++ b/client/next.config.ts @@ -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}' }, ] } diff --git a/client/public/images/award-solid.svg b/client/public/images/award-solid.svg new file mode 100644 index 0000000..9a7a63d --- /dev/null +++ b/client/public/images/award-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/images/circle-plus-solid.svg b/client/public/images/circle-plus-solid.svg new file mode 100644 index 0000000..d5f857a --- /dev/null +++ b/client/public/images/circle-plus-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/images/circle-solid.svg b/client/public/images/circle-solid.svg new file mode 100644 index 0000000..bbfee0c --- /dev/null +++ b/client/public/images/circle-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/images/clock-solid.svg b/client/public/images/clock-solid.svg new file mode 100644 index 0000000..5be8ac5 --- /dev/null +++ b/client/public/images/clock-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/images/heart-solid.svg b/client/public/images/heart-solid.svg new file mode 100644 index 0000000..1292663 --- /dev/null +++ b/client/public/images/heart-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/images/magnifying-glass-solid.svg b/client/public/images/magnifying-glass-solid.svg new file mode 100644 index 0000000..c858d13 --- /dev/null +++ b/client/public/images/magnifying-glass-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/images/message-solid.svg b/client/public/images/message-solid.svg new file mode 100644 index 0000000..d8e5448 --- /dev/null +++ b/client/public/images/message-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/images/thumbs-down-solid.svg b/client/public/images/thumbs-down-solid.svg new file mode 100644 index 0000000..5779bc9 --- /dev/null +++ b/client/public/images/thumbs-down-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/images/thumbs-up-solid.svg b/client/public/images/thumbs-up-solid.svg new file mode 100644 index 0000000..7f8fa2d --- /dev/null +++ b/client/public/images/thumbs-up-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/app/communication.ts b/client/src/app/communication.ts new file mode 100644 index 0000000..aefeb09 --- /dev/null +++ b/client/src/app/communication.ts @@ -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; + 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; + diff --git a/client/src/app/components/Leaderboard.tsx b/client/src/app/components/Leaderboard.tsx new file mode 100644 index 0000000..5791141 --- /dev/null +++ b/client/src/app/components/Leaderboard.tsx @@ -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 = ({ users }) => { + const crownIcon = + "https://cdn.builder.io/api/v1/image/assets/TEMP/e57ce0fb4d7e6f9d8ae6cf2dda741add41521ccec68798186f13f301773163b5?placeholderIfAbsent=true"; + + return ( +
+
+ {/* Top 3 Section */} +
+ {/* Crown */} + Crown + + {/* Second Place */} + {users[1] && ( +
+

2nd

+
+ Second place +
+
+ {users[1].likes} + +
+
+ )} + + {/* First Place */} + {users[0] && ( +
+
+
+ First place +
+
+
+ {users[0].likes} + +
+
+ )} + + {/* Third Place */} + {users[2] && ( +
+

3rd

+
+ Third place +
+
+ {users[2].likes} + +
+
+ )} +
+ + {/* Full Leaderboard */} +
    + {users.map((user, index) => ( +
  • +
    +
    + {`${user.name}'s +
    +

    {`${index + 1}. ${user.name}`}

    +
    +
    + {user.likes} + +
    +
  • + ))} +
+
+
+ ); +}; + +const HeartIcon: React.FC<{ size?: "lg" }> = ({ size }) => { + const className = size === "lg" ? "w-8 h-8" : "w-6 h-6"; + return ( + + + + ); +}; + +export default Leaderboard; diff --git a/client/src/app/components/NavBar.tsx b/client/src/app/components/NavBar.tsx new file mode 100644 index 0000000..b73a094 --- /dev/null +++ b/client/src/app/components/NavBar.tsx @@ -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 = ({}) => { + const [PFP, setPFP] = useState(""); + const [username, setUsername] = useState(""); + + // Dropdown state + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const dropdownRef = useRef(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(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 ( +
+ {/* Left Options */} +
+ {/* Logo */} + + Swipe Style + + {/* Component Pop Ups */} +
+ + + + +
+
+ + {/* Search Bar */} +
+ Search { + if (window.innerWidth < 1024) setIsMobileShowNav(!isMobileShowNav); + }} + /> + +
+ + {/* User Profile Button */} +
setIsDropdownOpen(!isDropdownOpen)} + > + Profile Picture +

{username}

+ {isDropdownOpen && ( +
setIsDropdownOpen(true)} + onMouseLeave={() => setIsDropdownOpen(false)} + > + + Profile + + { + console.log("Log Out clicked"); // Replace with actual logout logic + }} + > + Log Out + +
+ )} +
+
+ ); +}; + +export default NavBar; diff --git a/client/src/app/components/NavBarLink.tsx b/client/src/app/components/NavBarLink.tsx new file mode 100644 index 0000000..ce3270b --- /dev/null +++ b/client/src/app/components/NavBarLink.tsx @@ -0,0 +1,30 @@ +import Image from "next/image"; +import Link from "next/link"; + +interface MyComponentProps { + linkAddress?: string; + image: string; + imageAlt: string; +} + +const NavBarLink: React.FC = ({ + linkAddress, + image, + imageAlt, +}) => { + const imageElement = ( + {imageAlt} + ); + + return ( + + {linkAddress ? ( + {imageElement} + ) : ( + imageElement + )} + + ); +}; + +export default NavBarLink; diff --git a/client/src/app/components/Swipeable.tsx b/client/src/app/components/Swipeable.tsx new file mode 100644 index 0000000..cd3c4b6 --- /dev/null +++ b/client/src/app/components/Swipeable.tsx @@ -0,0 +1,149 @@ +// Modified code from tonyqiu123 (GitHub) +// https://github.com/tonyqiu123/50-days-of-components/tree/main/frontend/src/components/Swipeable + +import React, { + useRef, + useState, + useEffect, + useCallback, + HTMLAttributes, +} from "react"; + +type SwipeableProps = { + onSwipeComplete?: () => void; // Function to run once swiped + onTap?: () => void; // Function to run when swipe is not enough + closeDirection?: "up" | "down" | "left" | "right"; + closeTravel?: number; + children: React.ReactNode; + transition?: string; + [key: string]: any; // Allow additional props without warning +} & HTMLAttributes; + +const Swipeable: React.FC = ({ + onSwipeComplete, + onTap, + closeDirection = "right", + closeTravel = 150, + children, + transition = "transform 500ms cubic-bezier(0.32, 0.72, 0, 1)", + ...props +}) => { + const transformToHide = `translate${closeDirection === "up" || closeDirection === "down" ? "Y" : "X"}(${closeDirection === "up" || closeDirection === "left" ? "-" : ""}${closeTravel}px)`; + + const [isSwiped, setSwiped] = useState(false); + const [transitionStyle, setTransitionStyle] = useState(transition); + const [transform, setTransform] = useState(""); + const modalRef = useRef(); + + let dragging = false; + let mouseDownClientY = 0; + let mouseDownClientX = 0; + let dragTravel = 0; + + const handleMouseDown = useCallback((event: React.MouseEvent) => { + const target = event.target as HTMLElement; + const interactiveElements = ["INPUT", "TEXTAREA", "SELECT", "BUTTON", "A"]; + if ( + interactiveElements.includes(target.tagName) || + target.isContentEditable + ) { + return; + } + + event.preventDefault(); + dragging = true; + mouseDownClientY = event.clientY; + mouseDownClientX = event.clientX; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + setTransitionStyle(""); + }, []); + + const handleMouseMove = useCallback( + (event: MouseEvent) => { + if (dragging) { + switch (closeDirection) { + case "up": + dragTravel = mouseDownClientY - event.clientY; + break; + case "down": + dragTravel = event.clientY - mouseDownClientY; + break; + case "left": + dragTravel = mouseDownClientX - event.clientX; + break; + default: + dragTravel = event.clientX - mouseDownClientX; + } + if (dragTravel >= 0) { + setTransform( + `translate${closeDirection === "up" || closeDirection === "down" ? "Y" : "X"}(${closeDirection === "up" || closeDirection === "left" ? "-" : ""}${dragTravel}px)`, + ); + } + } + }, + [closeDirection], + ); + + const handleMouseUp = useCallback(() => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + setTransitionStyle(transition); + dragging = false; + if (dragTravel > closeTravel) { + setSwiped(true); + setTransform(transformToHide); + if (onSwipeComplete) { + onSwipeComplete(); // Trigger the provided function + } + // Return to the original position after swipe completion + setTimeout(() => { + setSwiped(false); + setTransform(""); + }, 500); // Adjust the delay to match the transition duration + } else { + setTransform(""); + if (onTap) { + onTap(); // Trigger the provided function for tap + } + } + dragTravel = 0; + }, [closeTravel, transformToHide, transition, onSwipeComplete, onTap]); + + useEffect(() => { + if (!isSwiped) { + setTransform(transformToHide); + // Simulate a click event to trigger initial animation back to original position + setTimeout(() => { + setTransform(""); + }, 0); + } + + return () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + }, [isSwiped, transformToHide, handleMouseMove, handleMouseUp]); + + return ( +
+ {children} +
+ ); +}; + +export default Swipeable; diff --git a/client/src/app/components/ViewHistory.tsx b/client/src/app/components/ViewHistory.tsx new file mode 100644 index 0000000..bf8f3ed --- /dev/null +++ b/client/src/app/components/ViewHistory.tsx @@ -0,0 +1,215 @@ +'use client'; +import React, { useState } from 'react'; + +// dummy +const dummyPosts = [ + { id: 1, name: "Yonsei Baseball Jacket", description: "A stylish jacket for sports lovers.", likes: 12, likedByUser: true, imageUrl: "https://alexgear.com/cdn/shop/files/Yonsei-University-Baseball-Jacket.jpg?v=1704227559", userId: 1 }, + { id: 2, name: "Blue Hoodie", description: "Comfy and cozy hoodie for all occasions.", likes: 7, likedByUser: false, imageUrl: "https://media.istockphoto.com/id/1319572197/photo/mens-hooded-jacket-for-your-design-mockup-for-print.jpg?s=612x612&w=0&k=20&c=c3n5O6D_gKpiX7zrN-K2wvDBYNuf9VMwUUysBg3TjkU=", userId: 2 }, + { id: 3, name: "Black Sneakers", description: "Perfect sneakers for casual outings.", likes: 24, likedByUser: true, imageUrl: "https://www.tukshoes.com/cdn/shop/files/A3226_LEFT_OUTSIDE.jpg?v=1698766343", userId: 3 }, + { id: 4, name: "Classic T-Shirt", description: "A simple t-shirt for everyday wear.", likes: 15, likedByUser: false, imageUrl: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTjazf4sKrskubw0510UfUT5B0K8eNzw0_q9w&s", userId: 4 }, + { id: 5, name: "Denim Jeans", description: "Trendy jeans for a modern look.", likes: 10, likedByUser: true, imageUrl: "https://lsco.scene7.com/is/image/lsco/A60810002-front-gstk?fmt=jpeg&qlt=70&resMode=sharp2&fit=crop,1&op_usm=0.6,0.6,8&wid=2000&hei=1840", userId: 5 }, + { id: 6, name: "Winter Coat", description: "Warm and stylish coat for cold weather.", likes: 20, likedByUser: false, imageUrl: "https://cdni.llbean.net/is/image/wim/520163_699_82?hei=1095&wid=950&resMode=sharp2&defaultImage=llbprod/520163_699_41", userId: 6 }, + { id: 7, name: "Plaid Shirt", description: "A classic plaid shirt for any occasion.", likes: 30, likedByUser: true, imageUrl: "https://media.istockphoto.com/id/1070699648/vector/shirt.jpg?s=612x612&w=0&k=20&c=rolcUAsd6L6cYpRkVtHTjZvsR9YHQEam0kuNXjBLQAc=", userId: 7 }, + { id: 8, name: "Leather Boots", description: "Durable and stylish leather boots.", likes: 18, likedByUser: false, imageUrl: "https://gt.atitlanleather.com/cdn/shop/products/atitlan-leather-boots-women-s-tall-leather-boots-34324884488361_600x.png?v=1678100313", userId: 8 }, + { id: 9, name: "Graphic Sweatshirt", description: "A fun and bold graphic sweatshirt.", likes: 25, likedByUser: true, imageUrl: "https://gvartwork.com/cdn/shop/files/ClevelandArtworkTshirtBack.jpg?v=1684517332", userId: 9 }, + { id: 10, name: "Chinos", description: "Perfect pair of chinos for casual and formal wear.", likes: 15, likedByUser: false, imageUrl: "https://img.abercrombie.com/is/image/anf/KIC_123-4016-0072-100_prod2.jpg?policy=product-large", userId: 10 }, +]; + +const dummyUsers = [ + { id: 1, name: "John Doe", description: "Fashion enthusiast.", likes: 8, imageUrl: "https://via.placeholder.com/200x200/0000FF/FFFFFF?text=John+Doe" }, + { id: 2, name: "Jane Smith", description: "Loves collecting vintage clothes.", likes: 14, imageUrl: "https://via.placeholder.com/200x200/008000/FFFFFF?text=Jane+Smith" }, + { id: 3, name: "Alex Lee", description: "Streetwear and sneakerhead.", likes: 18, imageUrl: "https://via.placeholder.com/200x200/000000/FFFFFF?text=Alex+Lee" }, + { id: 4, name: "Emily Davis", description: "Fashion blogger and trendsetter.", likes: 22, imageUrl: "https://via.placeholder.com/200x200/FF0000/FFFFFF?text=Emily+Davis" }, + { id: 5, name: "Michael Brown", description: "Passionate about clothing and design.", likes: 9, imageUrl: "https://via.placeholder.com/200x200/FFFFFF/000000?text=Michael+Brown" }, + { id: 6, name: "Sara White", description: "Avid traveler and lover of unique fashion.", likes: 20, imageUrl: "https://via.placeholder.com/200x200/FFFF00/000000?text=Sara+White" }, + { id: 7, name: "Tom Black", description: "Street fashion photographer.", likes: 12, imageUrl: "https://via.placeholder.com/200x200/8B4513/FFFFFF?text=Tom+Black" }, + { id: 8, name: "Olivia Green", description: "Sustainable fashion advocate.", likes: 16, imageUrl: "https://via.placeholder.com/200x200/00FFFF/000000?text=Olivia+Green" }, + { id: 9, name: "Lucas Grey", description: "Designer and artist, blending fashion with art.", likes: 14, imageUrl: "https://via.placeholder.com/200x200/A52A2A/FFFFFF?text=Lucas+Grey" }, + { id: 10, name: "Sophia Blue", description: "Loves minimalist and functional clothing.", likes: 18, imageUrl: "https://via.placeholder.com/200x200/0000FF/FFFFFF?text=Sophia+Blue" }, +]; + +const ITEMS_PER_PAGE = 5; + +// card component +const Card: React.FC<{ data: any; type: 'post' | 'user' }> = ({ data, type, onClick }: any) => ( + +
+ {data.name} +
+
+
+

{data.name}

+

{data.description}

+
+
+ + {type === 'post' && ( +
+ {data.likedByUser ? '❤️' : '🤍'} + {data.likes} +
+ )} +
+); + + +// takes data and displays it +const ViewHistory: React.FC = () => { + const [currentPage, setCurrentPage] = useState(1); + const [showPosts, setShowPosts] = useState(true); + const [selectedUserId, setSelectedUserId] = useState(null); + const [selectedPostId, setSelectedPostId] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + + const currentData = showPosts ? dummyPosts : dummyUsers; + const totalPages = Math.ceil(currentData.length / ITEMS_PER_PAGE); + + const handleUserClick = (userId: number) => { + setSelectedUserId(userId); + setIsModalOpen(true); + }; + + const handlePostClick = (postId: number) => { + setSelectedPostId(postId); + setIsModalOpen(true); + }; + + const selectedUser = dummyUsers.find((user) => user.id === selectedUserId); + const selectedPost = dummyPosts.find((post) => post.id === selectedPostId); + const userPosts = selectedUser ? dummyPosts.filter((post) => post.userId === selectedUserId) : []; + const postUser = selectedPost ? dummyUsers.find((user) => user.id === selectedPost.userId) : null; + + return ( + <> + {/* Main Content */} +
+
+
+

View History

+
+ + + {/* Buttons */} +
+ + +
+ +
+ {currentData.slice( + (currentPage - 1) * ITEMS_PER_PAGE, + currentPage * ITEMS_PER_PAGE + ).map((item) => ( + handlePostClick(item.id) : () => handleUserClick(item.id)} + /> + ))} +
+ + {/* pages */} +
+ + Page {currentPage} of {totalPages} + +
+
+
+ + {isModalOpen && ( +
+
+ {/* Displaying user details */} + {selectedUser && ( +
+ {selectedUser.name} +

{selectedUser.name}

+

{selectedUser.description}

+
+ )} + + {/* Displaying post details */} + {selectedPost && postUser && ( + <> +
+ {selectedPost.name} +

{selectedPost.name}

+

{selectedPost.description}

+
+ {selectedPost.likedByUser ? '❤️' : '🤍'} + {selectedPost.likes} +
+
+ + {/* Display the user who posted */} +
+

Posted by: {postUser.name}

+ {postUser.name} +

{postUser.description}

+
+ + )} + + {/* Display user's posts if viewing the user */} + {selectedUser && userPosts.length > 0 && ( +
+

Posts

+
+ {userPosts.map((post) => ( + + ))} +
+
+ )} +
+
+ )} + + ); +}; + +export default ViewHistory; diff --git a/client/src/app/messages/Chatlog.tsx b/client/src/app/messages/Chatlog.tsx index deb3ea5..2b516a3 100644 --- a/client/src/app/messages/Chatlog.tsx +++ b/client/src/app/messages/Chatlog.tsx @@ -4,7 +4,8 @@ import React, { useRef, useState, useEffect } from "react"; import backendConnection from "../../communication"; import { useUserContext } from "./UserProvider"; import { Message } from "./MessageProps"; -import socket from "./socket"; +import createSocket from './socket'; +import { Socket } from 'socket.io-client'; const Chatlog = () => { const [input, setInput] = useState(""); @@ -13,6 +14,32 @@ const Chatlog = () => { const { username } = useUserContext(); const [currentUserId, setCurrentUserId] = useState(""); const chatContainerRef = useRef(null); // Ref to the chat container + const [socket, setSocket] = useState(null); + + // Initialize socket connection + useEffect(() => { + let mounted = true; + + const initSocket = async () => { + try { + const newSocket = await createSocket(); + if (mounted && newSocket) { + setSocket(newSocket); + } + } catch (error) { + console.error("Error initializing socket:", error); + } + }; + + initSocket(); + + return () => { + mounted = false; + if (socket) { + socket.disconnect(); + } + }; + }, []); const fetchUserData = async () => { try { @@ -29,10 +56,10 @@ const Chatlog = () => { useEffect(() => { if (selectedUserId) { // Emit 'join_chat' event to the server - socket.emit("join_chat", currentUserId, selectedUserId); + socket?.emit("join_chat", currentUserId, selectedUserId); // Listen for new messages - socket.on("receive_message", (data) => { + socket?.on("receive_message", (data) => { if (data.user_id !== currentUserId) { const newMessage: Message = { sender: data.user_id === currentUserId ? "user" : "other", @@ -45,7 +72,7 @@ const Chatlog = () => { }); return () => { - socket.off("receive_message"); + socket?.off("receive_message"); }; } fetchUserData(); @@ -69,7 +96,7 @@ const Chatlog = () => { user_id: currentUserId, }; - socket.emit("send_message", messageData); + socket?.emit("send_message", messageData); const newMessage: Message = { sender: "user", diff --git a/client/src/app/messages/socket.ts b/client/src/app/messages/socket.ts index 6b94da1..2240e4c 100644 --- a/client/src/app/messages/socket.ts +++ b/client/src/app/messages/socket.ts @@ -1,6 +1,39 @@ import io from "socket.io-client"; -const socket = io("http://localhost:3001"); +// Get token from backend route +const getToken = async() => { + const response = await fetch('http://localhost:3001/auth/get-token', { + method: 'GET', + credentials: 'include' + }); -export default socket; + if (response.ok){ + const data = await response.json(); + console.log('token:', data.token); + return data.token; + } else { + // If no token, that means user has not logged in through Google, so redirect to login + window.location.href = "http://localhost:3000/login" + throw new Error('Failed to retrieve token'); + } +}; + +// Create socket that accepts token + +const createSocket = async() =>{ + try { + const token = await getToken(); + console.log('token:', token); + const socket = io("http://localhost:3001", { + auth: { + token: token + } + }); + return socket; + } catch(error){ + console.error("Error creating socket:", error); + } +}; + +export default createSocket; diff --git a/client/src/app/page.tsx b/client/src/app/page.tsx index 1a1d95a..5ff9721 100644 --- a/client/src/app/page.tsx +++ b/client/src/app/page.tsx @@ -1,9 +1,309 @@ -import styles from "./homepage.module.css"; +"use client"; + +import { useState, useEffect } from "react"; +import Image from "next/image"; +import Link from "next/link"; +import Swipeable from "./components/Swipeable.tsx"; +import NavBar from "./components/NavBar.tsx"; +import likedIcon from "/public/images/heart-solid.svg"; +import interestIcon from "/public/images/thumbs-up-solid.svg"; +import disinterestIcon from "/public/images/thumbs-down-solid.svg"; + +const getRandomRotation = () => { + const positiveOrNegative = Math.random() < 0.5 ? 1 : -1; + return positiveOrNegative * (Math.random() * 10 + 5); // Range: 5 to 15 +}; + +const getRandomOpacity = () => Math.random() * 0.6 + 0.2; // Range: 0.2 to 0.8 const Home: React.FC = () => { + // Set up card states + const [cards, setCards] = useState([]); + const [authorPFP, setAuthorPFP] = useState(""); + const [postId, setPostId] = useState(""); + const [cardDetails, setCardDetails] = useState<{ + title: string; + description: string; + stores: string; + likeCount: Number; + material?: string; + brand?: string; + cost?: string; + } | null>(null); + const [availableStores, setAvailableStores] = useState< + { + location: string; + address: string; + status: string; + image: string; + link: string; + }[] + >([]); + const [rotations, setRotations] = useState([]); + const [opacities, setOpacities] = useState([]); + + // TODO: Get data from BE + const fetchData = () => { + return { + _id: "au83a90wr", + authorPFP: + "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_1280.png", + cards: [ + "https://files.idyllic.app/files/static/294916?width=750&optimizer=image", + "https://i.pinimg.com/originals/4a/bc/b7/4abcb7a620546e9fd26d3cbed76bbeaa.png", + "https://i.etsystatic.com/31914527/r/il/4063ee/4539566858/il_fullxfull.4539566858_ksxq.jpg", + ], + cardDetails: { + title: "Blue Dress", + description: "Best dress of all time!", + likeCount: 123, + stores: "Found in 2 stores", + material: "Silk", + cost: "$1000", + brand: "DanceLuxe", + }, + availableStores: [ + { + image: + "https://files.idyllic.app/files/static/294916?width=750&optimizer=image", + location: "Twirl Dance Boutique", + address: "6431 Independence Ave Woodland Hills, CA 91367", + status: "Available", + link: "/", + }, + { + image: + "https://files.idyllic.app/files/static/294916?width=750&optimizer=image", + location: "D's Dance Boutique", + address: "427 Imperial Hwy Fullerton, CA 92835", + status: "Unavailable", + link: "/", + }, + { + image: + "https://files.idyllic.app/files/static/294916?width=750&optimizer=image", + location: "D's Dance Boutique", + address: "427 Imperial Hwy Fullerton, CA 92835", + status: "Unavailable", + link: "/", + }, + { + image: + "https://files.idyllic.app/files/static/294916?width=750&optimizer=image", + location: "D's Dance Boutique", + address: "427 Imperial Hwy Fullerton, CA 92835", + status: "Available", + link: "/", + }, + ], + }; + }; + + // Fetch data after interest/disinterest buttons are clicked + const nextPost = (liked: boolean) => { + // TODO: send sendData to BE + const sendData = { + _id: postId, + liked: liked, + }; + const data = fetchData(); + setAuthorPFP(data.authorPFP); + setPostId(data._id); + setCards(data.cards); + setCardDetails(data.cardDetails); + setAvailableStores( + data.availableStores.sort((a, b) => (a.status === "Available" ? -1 : 1)), + ); + const newRotations = data.cards.slice(1).map(() => getRandomRotation()); + const newOpacities = data.cards.slice(1).map(() => getRandomOpacity()); + setRotations(newRotations); + setOpacities(newOpacities); + }; + + // Initial fetch + useEffect(() => { + nextPost(false); + }, []); + return ( -
-

Home Page

+
+ {/* Navbar */} +
+ +
+ {/* Content */} +
+ {/* Left Side */} +
+ {/* Cards */} + nextPost(false)} + className="relative flex h-[28rem] w-[20rem] items-center justify-center lg:h-[34rem] lg:w-[24rem]" + > + nextPost(true)} + className="relative flex h-[28rem] w-[20rem] items-center justify-center lg:h-[34rem] lg:w-[24rem]" + > +
+ {/* Other cards */} + {cards.slice(1).map((card, index) => ( + Card + ))} + {/* Main card */} + {cards.length > 0 && ( +
setCards([...cards.slice(1), cards[0]])} + > + Main Card + + + Author + + +
+

+ {cardDetails?.title} +

+

+ {cardDetails?.stores} +

+ + Like Button +

+ {cardDetails?.likeCount.toString()} +

+
+
+
+ )} +
+
+
+ {/* Swipe Buttons */} +
+
+ Uninterested nextPost(false)} + /> +
+
+ Interested nextPost(true)} + /> +
+
+
+ {/* Right Side */} +
+ {/* Description */} +
+

+ {cardDetails?.title} +

+

+ {cardDetails?.description} +

+ {cardDetails?.material && ( + <> +

+ Material +

+

{cardDetails.material}

+ + )} + {cardDetails?.brand && ( + <> +

+ Brand +

+

{cardDetails.brand}

+ + )} + {cardDetails?.cost && ( + <> +

+ Cost +

+

{cardDetails.cost}

+ + )} +
+ {/* Available Stores */} +
+

Available Stores

+ {availableStores.map((store, index) => ( + + + Location + +

{store.location}

+

{store.address}

+ +
+
+ + ))} +
+
+
); }; diff --git a/server/app.js b/server/app.js index f6ecf2d..ba1ae6f 100644 --- a/server/app.js +++ b/server/app.js @@ -10,13 +10,17 @@ const cookie_parser_1 = __importDefault(require("cookie-parser")); const morgan_1 = __importDefault(require("morgan")); const passport_1 = __importDefault(require("./utils/passport")); const express_session_1 = __importDefault(require("express-session")); +const db_1 = __importDefault(require("./db")); const cors_1 = __importDefault(require("cors")); const index_1 = __importDefault(require("./routes/index")); const users_1 = __importDefault(require("./routes/users")); const auth_1 = __importDefault(require("./routes/auth")); -const messages_1 = __importDefault(require("./routes/messages")); +const user_1 = __importDefault(require("./routes/user")); +const post_1 = __importDefault(require("./routes/post")); +const message_1 = __importDefault(require("./routes/message")); const dotenv_1 = __importDefault(require("dotenv")); dotenv_1.default.config(); +(0, db_1.default)(); const app = (0, express_1.default)(); const corsOptions = { origin: 'http://localhost:3000', // Allow requests from frontend (React app) @@ -44,7 +48,9 @@ app.use(passport_1.default.session()); app.use('/', index_1.default); app.use('/users', users_1.default); app.use('/auth', auth_1.default); -app.use('/messages', messages_1.default); +app.use('/api', user_1.default); +app.use('/api', post_1.default); +app.use('/api', message_1.default); // catch 404 and forward to error handler app.use((req, res, next) => { next((0, http_errors_1.default)(404)); diff --git a/server/app.ts b/server/app.ts index 5664707..e1beb06 100644 --- a/server/app.ts +++ b/server/app.ts @@ -5,17 +5,22 @@ import cookieParser from 'cookie-parser'; import logger from 'morgan'; import passport from './utils/passport'; import session from 'express-session'; +import connectDB from './db'; +import mongoose from 'mongoose'; import cors from 'cors'; import indexRouter from './routes/index'; import usersRouter from './routes/users'; import authRouter from './routes/auth'; -import messageRouter from './routes/messages'; +import userRoutes from './routes/user'; +import postRoutes from './routes/post'; +import messageRoutes from './routes/message'; import dotenv from 'dotenv'; dotenv.config(); +connectDB(); const app = express(); const corsOptions = { @@ -31,6 +36,7 @@ app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'pug'); // middleware + app.use(logger('dev')); app.use(express.json()); app.use(express.urlencoded({ extended: false })); @@ -51,7 +57,11 @@ app.use(passport.session()); app.use('/', indexRouter); app.use('/users', usersRouter); app.use('/auth', authRouter); -app.use('/messages', messageRouter); +app.use('/api', userRoutes); +app.use('/api', postRoutes); +app.use('/api', messageRoutes); + + // catch 404 and forward to error handler app.use((req: Request, res: Response, next: NextFunction) => { diff --git a/server/bin/www.js b/server/bin/www.js index d186d04..83b08d5 100644 --- a/server/bin/www.js +++ b/server/bin/www.js @@ -21,7 +21,8 @@ const app_1 = __importDefault(require("../app")); const debug_1 = __importDefault(require("debug")); const http_1 = __importDefault(require("http")); const socket_io_1 = require("socket.io"); -const mongoose_1 = __importDefault(require("mongoose")); +const google_auth_library_1 = require("google-auth-library"); +const User_1 = require("../models/User"); /** * Get port from environment and store in Express. */ @@ -32,14 +33,6 @@ app_1.default.set('port', port); */ const server = http_1.default.createServer(app_1.default); exports.server = server; -/** - * Connect to MongoDB - */ -mongoose_1.default.connect('mongodb://127.0.0.1:27017/Users').then(() => { - console.log('connected to mongodb'); -}).catch((err) => { - console.error('error connecting to mongodb', err); -}); /** * Create SocketIO server */ @@ -49,6 +42,40 @@ const io = new socket_io_1.Server(server, { credentials: true, // Allow cookies to be sent with requests } }); +/** + * Check if user has been verified through Google + */ +const CLIENT_ID = process.env.GOOGLE_CLIENT_ID; +const client = new google_auth_library_1.OAuth2Client(CLIENT_ID); +io.use((socket, next) => __awaiter(void 0, void 0, void 0, function* () { + // Get ID token from authorization handshake + const token = socket.handshake.auth.token; + if (!token) { + return new Error("Auth token required"); + } + try { + // Use the OAuth client to verify the token. + const ticket = yield client.verifyIdToken({ + idToken: token, + audience: CLIENT_ID, + }); + const payload = ticket.getPayload(); + if (!payload) { + return next(new Error("Invalid token payload")); + } + // Use the payload to find the user in the database. + let user = yield User_1.User.findOne({ user_id: payload.sub }); + // Store the user's ID inside the socket object. + socket.user = { + user_id: user === null || user === void 0 ? void 0 : user.user_id + }; + next(); + } + catch (error) { + console.error("Authentication error:", error); + return next(new Error("Authentication failed")); + } +})); /** * Listen to connection on socket server */ diff --git a/server/bin/www.ts b/server/bin/www.ts index c379a7d..76b1b5c 100644 --- a/server/bin/www.ts +++ b/server/bin/www.ts @@ -8,9 +8,8 @@ import app from '../app'; import debug from 'debug'; import http from 'http'; import { Socket, Server} from 'socket.io'; -import mongoose from 'mongoose'; -import { Message } from '../models/messageSchema'; -import { v4 as uuidv4} from 'uuid'; +import { OAuth2Client } from 'google-auth-library'; +import { User, AuthenticatedSocket } from '../models/User'; /** * Get port from environment and store in Express. @@ -24,15 +23,6 @@ app.set('port', port); */ const server = http.createServer(app); -/** - * Connect to MongoDB - */ - -mongoose.connect('mongodb://127.0.0.1:27017/Users').then(() =>{ - console.log('connected to mongodb'); -}).catch((err) => { - console.error('error connecting to mongodb', err); -}); /** * Create SocketIO server @@ -45,6 +35,48 @@ const io = new Server(server, { } }); +/** + * Check if user has been verified through Google + */ + +const CLIENT_ID = process.env.GOOGLE_CLIENT_ID; +const client = new OAuth2Client(CLIENT_ID); + +io.use(async (socket, next) => { + // Get ID token from authorization handshake + const token = socket.handshake.auth.token; + + if (!token){ + return new Error("Auth token required"); + } + + try { + // Use the OAuth client to verify the token. + const ticket = await client.verifyIdToken({ + idToken: token, + audience: CLIENT_ID, + }); + const payload = ticket.getPayload(); + + if (!payload){ + return next(new Error("Invalid token payload")); + } + + // Use the payload to find the user in the database. + let user = await User.findOne({ user_id: payload.sub }); + + // Store the user's ID inside the socket object. + (socket as AuthenticatedSocket).user = { + user_id: user?.user_id + }; + + next(); + } catch (error) { + console.error("Authentication error:", error); + return next(new Error("Authentication failed")); + } +}); + /** * Listen to connection on socket server */ diff --git a/server/db.js b/server/db.js new file mode 100644 index 0000000..6505c99 --- /dev/null +++ b/server/db.js @@ -0,0 +1,31 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const mongoose_1 = __importDefault(require("mongoose")); +const connectDB = () => __awaiter(void 0, void 0, void 0, function* () { + try { + const conn = yield mongoose_1.default.connect(process.env.MONGO_URI); + console.log(`MongoDB Connected: ${conn.connection.host}`); + } + catch (err) { + if (err instanceof Error) { + console.error(`Error: ${err.message}`); + } + else { + console.error('An unknown error occurred during database connection.'); + } + process.exit(1); // Exit the process with a failure + } +}); +exports.default = connectDB; diff --git a/server/db.ts b/server/db.ts new file mode 100644 index 0000000..5ed3e67 --- /dev/null +++ b/server/db.ts @@ -0,0 +1,17 @@ +import mongoose from 'mongoose'; + +const connectDB = async () => { + try { + const conn = await mongoose.connect(process.env.MONGO_URI as string); + console.log(`MongoDB Connected: ${conn.connection.host}`); + } catch (err) { + if (err instanceof Error) { + console.error(`Error: ${err.message}`); + } else { + console.error('An unknown error occurred during database connection.'); + } + process.exit(1); // Exit the process with a failure + } +}; + +export default connectDB; diff --git a/server/models/Message.js b/server/models/Message.js new file mode 100644 index 0000000..7827b77 --- /dev/null +++ b/server/models/Message.js @@ -0,0 +1,34 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const mongoose_1 = __importStar(require("mongoose")); +const messageSchema = new mongoose_1.Schema({ + conversation_id: { type: String, required: true }, + user_id: { type: String, required: true }, + timestamp: { type: String, required: true }, + message: { type: String, required: true }, +}); +const Message = mongoose_1.default.model('Message', messageSchema); +exports.default = Message; diff --git a/server/models/Message.ts b/server/models/Message.ts new file mode 100644 index 0000000..488649b --- /dev/null +++ b/server/models/Message.ts @@ -0,0 +1,20 @@ +import mongoose, { Schema, Model } from 'mongoose'; + +interface IMessage { + _id?: string; // Optional since MongoDB generates it + conversation_id: string; // A unique ID for the conversation (e.g., "user1ID_user2ID") + user_id: string; // The sender's user ID + timestamp: string; // When the message was sent + message: string; // The message content +} + +const messageSchema = new Schema({ + conversation_id: { type: String, required: true }, + user_id: { type: String, required: true }, + timestamp: { type: String, required: true }, + message: { type: String, required: true }, +}); + +const Message: Model = mongoose.model('Message', messageSchema); + +export default Message; diff --git a/server/models/Post.js b/server/models/Post.js new file mode 100644 index 0000000..e1e6061 --- /dev/null +++ b/server/models/Post.js @@ -0,0 +1,40 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const mongoose_1 = __importStar(require("mongoose")); +const postSchema = new mongoose_1.Schema({ + title: { type: String, required: true }, + product_details: { type: String }, + material: { type: String }, + brand: { type: String }, + cost: { type: Number }, + likes: { type: Number, default: 0 }, + numStores: { type: Number }, + author: { type: String, required: true }, + available_stores: { type: [String], default: [] }, + image: { type: String, required: true }, +}); +const Post = mongoose_1.default.model('Post', postSchema); +exports.default = Post; diff --git a/server/models/Post.ts b/server/models/Post.ts new file mode 100644 index 0000000..341a82c --- /dev/null +++ b/server/models/Post.ts @@ -0,0 +1,32 @@ +import mongoose, { Schema, Model } from 'mongoose'; + +interface IPost { + _id?: string; // Optional as MongoDB will generate it + title: string; + product_details?: string; + material?: string; + brand?: string; + cost?: number; + likes: number; + numStores?: number; + author: string; // user_id of the post author + available_stores: string[]; + image: string; +} + +const postSchema = new Schema({ + title: { type: String, required: true }, + product_details: { type: String }, + material: { type: String }, + brand: { type: String }, + cost: { type: Number }, + likes: { type: Number, default: 0 }, + numStores: { type: Number }, + author: { type: String, required: true }, + available_stores: { type: [String], default: [] }, + image: { type: String, required: true }, +}); + +const Post: Model = mongoose.model('Post', postSchema); + +export default Post; diff --git a/server/models/User.js b/server/models/User.js index 4056960..7a30ac9 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -15,28 +15,36 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( }) : function(o, v) { o["default"] = v; }); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.User = void 0; const mongoose_1 = __importStar(require("mongoose")); -const userSchema = new mongoose_1.default.Schema({ +; +const userSchema = new mongoose_1.Schema({ user_id: { type: String, unique: true, required: true }, - username: String, + username: { type: String, required: true }, + token: { type: String }, + bio: { type: String }, + pronouns: { type: String }, + tags: { type: [String], default: [] }, + followers: { type: Number, default: 0 }, + following: { type: Number, default: 0 }, + wishlist: { type: [String], default: [] }, + posts: { type: [String], default: [] }, + liked: { type: [String], default: [] }, + disliked: { type: [String], default: [] }, + picture: { type: String }, + settings: { + privateAccount: { type: Boolean, default: false }, + }, }); -exports.User = (0, mongoose_1.model)('User', userSchema); +// type UserDocument = InferSchemaType; +const User = mongoose_1.default.model('User', userSchema); +exports.User = User; +; diff --git a/server/models/User.ts b/server/models/User.ts index 6fc8140..265a1a1 100644 --- a/server/models/User.ts +++ b/server/models/User.ts @@ -1,13 +1,54 @@ -import mongoose,{ Document, model, Schema} from 'mongoose'; +import mongoose, { Schema, Model, InferSchemaType } from 'mongoose'; +import { Socket } from 'socket.io' -export interface User extends Document { - user_id: string; - username: string; - } +interface IUser { + user_id: string; + username: string; + token: string; + bio?: string; + pronouns?: string; + tags: string[]; + followers: number; + following: number; + wishlist: string[]; + posts: string[]; + liked: string[]; + disliked: string[]; + picture?: string; // Assuming a profile picture URL + settings: { + privateAccount: boolean; + }; +} -const userSchema = new mongoose.Schema({ - user_id: {type: String, unique: true, required: true}, - username: String, +interface MinUser { + user_id?: string; +}; + +const userSchema = new Schema({ + user_id: { type: String, unique: true, required: true }, + username: { type: String, required: true }, + token: { type: String }, + bio: { type: String }, + pronouns: { type: String }, + tags: { type: [String], default: [] }, + followers: { type: Number, default: 0 }, + following: { type: Number, default: 0 }, + wishlist: { type: [String], default: [] }, + posts: { type: [String], default: [] }, + liked: { type: [String], default: [] }, + disliked: { type: [String], default: [] }, + picture: { type: String }, + settings: { + privateAccount: { type: Boolean, default: false }, + }, }); -export const User = model('User', userSchema); \ No newline at end of file +// type UserDocument = InferSchemaType; + +const User: Model = mongoose.model('User', userSchema); + +interface AuthenticatedSocket extends Socket{ + user: MinUser; +}; + +export { User, IUser, AuthenticatedSocket }; \ No newline at end of file diff --git a/server/package.json b/server/package.json index 13ee04f..65bae54 100644 --- a/server/package.json +++ b/server/package.json @@ -32,7 +32,8 @@ "express-session": "^1.18.1", "google-auth-library": "^9.15.0", "googleapis": "^144.0.0", - "mongoose": "^8.9.1", + "mongodb": "^6.12.0", + "mongoose": "^8.9.2", "morgan": "^1.10.0", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", diff --git a/server/routes/auth.js b/server/routes/auth.js index 2b8e2e5..c6d5737 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -1,10 +1,20 @@ "use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const express_1 = __importDefault(require("express")); const passport_1 = __importDefault(require("passport")); +const User_1 = require("../models/User"); const router = express_1.default.Router(); function isLoggedIn(req, res, next) { req.user ? next() : res.sendStatus(401); @@ -28,4 +38,35 @@ router.get('/google/callback', passport_1.default.authenticate('google', { failu router.get('/protected', isLoggedIn, (req, res) => { res.send('protected route'); }); +/** + * @route /get-token + * @desc Gets the user's ID token + * @access Private + * + * This endpoint retrieves the authenticated user's ID token from the database. + * + * Request User: + * - req.user.user_id: The user's user ID. + * + * Response: + * - 200: Token was sent successfully. + * - 404: User was not found in the database. + * - 500: Internal server error. + */ +router.get('/get-token', isLoggedIn, (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const user_id = req.user.user_id; + const user = yield User_1.User.findOne({ user_id: user_id }); + if (!user) { + res.sendStatus(404); + return; + } + res.status(200).json({ token: user.token }); + } + catch (err) { + console.error(err); + res.sendStatus(500); + return; + } +})); exports.default = router; diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 347fa42..ee61cc3 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -1,5 +1,7 @@ import express, { Request, Response, NextFunction } from 'express'; import passport from 'passport'; +import { User } from '../models/User'; +import type { IUser } from '../models/User'; const router = express.Router(); @@ -37,5 +39,36 @@ router.get('/protected', isLoggedIn, (req: Request, res: Response) => { res.send('protected route'); }); +/** + * @route /get-token + * @desc Gets the user's ID token + * @access Private + * + * This endpoint retrieves the authenticated user's ID token from the database. + * + * Request User: + * - req.user.user_id: The user's user ID. + * + * Response: + * - 200: Token was sent successfully. + * - 404: User was not found in the database. + * - 500: Internal server error. + */ +router.get('/get-token', isLoggedIn, async (req, res) => { + try { + const user_id = (req.user as IUser).user_id; + const user = await User.findOne( {user_id: user_id}); + + if (!user){ + res.sendStatus(404); + return; + } + res.status(200).json({token: user.token }); + } catch (err){ + console.error(err); + res.sendStatus(500); + return; + } +}) export default router; diff --git a/server/routes/message.js b/server/routes/message.js new file mode 100644 index 0000000..377d99f --- /dev/null +++ b/server/routes/message.js @@ -0,0 +1,52 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = __importDefault(require("express")); +const Message_1 = __importDefault(require("../models/Message")); +const router = express_1.default.Router(); +// Test route +router.get('/messages/test', (req, res) => { + res.send('Messages route is working'); +}); +// GET: Fetch all messages in a conversation +router.get('/messages/:conversation_id', (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const { conversation_id } = req.params; + const messages = yield Message_1.default.find({ conversation_id }).sort({ timestamp: 1 }); + res.status(200).json(messages); + } + catch (error) { + console.error('Error fetching messages:', error); + res.status(500).json({ error: 'Error fetching messages', details: error }); + } +})); +// POST: Send a new message +router.post('/messages', (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const { conversation_id, user_id, timestamp, message } = req.body; + const newMessage = new Message_1.default({ + conversation_id, + user_id, + timestamp, + message, + }); + const savedMessage = yield newMessage.save(); + res.status(201).json(savedMessage); + } + catch (error) { + console.error('Error sending message:', error); + res.status(400).json({ error: 'Error sending message', details: error }); + } +})); +exports.default = router; diff --git a/server/routes/message.ts b/server/routes/message.ts new file mode 100644 index 0000000..9a2a6c7 --- /dev/null +++ b/server/routes/message.ts @@ -0,0 +1,43 @@ +import express, { Request, Response } from 'express'; +import Message from '../models/Message'; + +const router = express.Router(); + +// Test route +router.get('/messages/test', (req: Request, res: Response) => { + res.send('Messages route is working'); +}); + +// GET: Fetch all messages in a conversation +router.get('/messages/:conversation_id', async (req: Request, res: Response) => { + try { + const { conversation_id } = req.params; + const messages = await Message.find({ conversation_id }).sort({ timestamp: 1 }); + res.status(200).json(messages); + } catch (error) { + console.error('Error fetching messages:', error); + res.status(500).json({ error: 'Error fetching messages', details: error }); + } +}); + +// POST: Send a new message +router.post('/messages', async (req: Request, res: Response) => { + try { + const { conversation_id, user_id, timestamp, message } = req.body; + + const newMessage = new Message({ + conversation_id, + user_id, + timestamp, + message, + }); + + const savedMessage = await newMessage.save(); + res.status(201).json(savedMessage); + } catch (error) { + console.error('Error sending message:', error); + res.status(400).json({ error: 'Error sending message', details: error }); + } +}); + +export default router; diff --git a/server/routes/post.js b/server/routes/post.js new file mode 100644 index 0000000..0e2afa4 --- /dev/null +++ b/server/routes/post.js @@ -0,0 +1,73 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = __importDefault(require("express")); +const Post_1 = __importDefault(require("../models/Post")); +const router = express_1.default.Router(); +// Test route +router.get('/posts/test', (req, res) => { + res.send('posts route is working'); +}); +// GET: Retrieve a post by its _id +router.get('/posts/:id', (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const post = yield Post_1.default.findById(req.params.id); // Find post by MongoDB's ObjectId + if (!post) { + res.status(404).json({ error: 'Post not found' }); + return; + } + res.status(200).json(post); + } + catch (error) { + console.error('Error fetching post:', error); + res.status(500).json({ error: 'Error fetching post' }); + } +})); +// GET: Retrieve all posts by a specific author (user_id) +router.get('/posts/author/:user_id', (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const posts = yield Post_1.default.find({ author: req.params.user_id }); // Find all posts by author + res.status(200).json(posts); + } + catch (error) { + console.error('Error fetching posts by author:', error); + res.status(500).json({ error: 'Error fetching posts by author' }); + } +})); +// POST: Create a new post +router.post('/posts', (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const { title, product_details, material, brand, cost, likes, numStores, author, available_stores, image, } = req.body; + const newPost = new Post_1.default({ + title, + product_details, + material, + brand, + cost, + likes, + numStores, + author, + available_stores, + image, + }); + const savedPost = yield newPost.save(); + res.status(201).json(savedPost); + } + catch (error) { + console.error('Error creating post:', error); + res.status(400).json({ error: 'Error creating post', details: error }); + } +})); +// Export the router +exports.default = router; diff --git a/server/routes/post.ts b/server/routes/post.ts new file mode 100644 index 0000000..ffad9da --- /dev/null +++ b/server/routes/post.ts @@ -0,0 +1,75 @@ +import express, { Request, Response } from 'express'; +import Post from '../models/Post'; + +const router = express.Router(); + +// Test route +router.get('/posts/test', (req: Request, res: Response) => { + res.send('posts route is working'); +}); + +// GET: Retrieve a post by its _id +router.get('/posts/:id', async (req: Request, res: Response) => { + try { + const post = await Post.findById(req.params.id); // Find post by MongoDB's ObjectId + if (!post) { + res.status(404).json({ error: 'Post not found' }); + return; + } + res.status(200).json(post); + } catch (error) { + console.error('Error fetching post:', error); + res.status(500).json({ error: 'Error fetching post' }); + } +}); + +// GET: Retrieve all posts by a specific author (user_id) +router.get('/posts/author/:user_id', async (req: Request, res: Response) => { + try { + const posts = await Post.find({ author: req.params.user_id }); // Find all posts by author + res.status(200).json(posts); + } catch (error) { + console.error('Error fetching posts by author:', error); + res.status(500).json({ error: 'Error fetching posts by author' }); + } +}); + +// POST: Create a new post +router.post('/posts', async (req: Request, res: Response) => { + try { + const { + title, + product_details, + material, + brand, + cost, + likes, + numStores, + author, + available_stores, + image, + } = req.body; + + const newPost = new Post({ + title, + product_details, + material, + brand, + cost, + likes, + numStores, + author, + available_stores, + image, + }); + + const savedPost = await newPost.save(); + res.status(201).json(savedPost); + } catch (error) { + console.error('Error creating post:', error); + res.status(400).json({ error: 'Error creating post', details: error }); + } +}); + +// Export the router +export default router; diff --git a/server/routes/user.js b/server/routes/user.js new file mode 100644 index 0000000..44184a2 --- /dev/null +++ b/server/routes/user.js @@ -0,0 +1,62 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = __importDefault(require("express")); +const User_1 = require("../models/User"); +const router = express_1.default.Router(); +router.get('/users/test', (req, res, next) => { + res.send('users route'); +}); +// Find user by user_id +router.get('/users/:user_id', (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const user = yield User_1.User.findOne({ user_id: req.params.user_id }); // Use findOne for custom user_id + if (!user) { + res.status(404).json({ error: 'User not found' }); + return; + } + res.status(200).json(user); + } + catch (error) { + console.error('Error fetching user:', error); + res.status(500).json({ error: 'Error fetching user' }); + } +})); +// POST: Create a new user +router.post('/users', (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const { user_id, username, bio, pronouns, tags, picture, settings } = req.body; + const newUser = new User_1.User({ + user_id, + username, + bio, + pronouns, + tags, + followers: 0, + following: 0, + wishlist: [], + posts: [], + liked: [], + disliked: [], + picture, + settings, + }); + const savedUser = yield newUser.save(); + res.status(201).json(savedUser); + } + catch (error) { + res.status(400).json({ error: 'Error creating user', details: error }); + } +})); +exports.default = router; diff --git a/server/routes/user.ts b/server/routes/user.ts new file mode 100644 index 0000000..757868b --- /dev/null +++ b/server/routes/user.ts @@ -0,0 +1,53 @@ +import express, { Request, Response, NextFunction } from 'express'; +import { User } from '../models/User'; + +const router = express.Router(); + +router.get('/users/test', (req: Request, res: Response, next: NextFunction) => { + res.send('users route'); +}); + +// Find user by user_id +router.get('/users/:user_id', async (req: Request, res: Response) => { + try { + const user = await User.findOne({ user_id: req.params.user_id }); // Use findOne for custom user_id + if (!user) { + res.status(404).json({ error: 'User not found' }); + return; + } + res.status(200).json(user); + } catch (error) { + console.error('Error fetching user:', error); + res.status(500).json({ error: 'Error fetching user' }); + } + }); + +// POST: Create a new user +router.post('/users', async (req: Request, res: Response) => { + try { + const { user_id, username, bio, pronouns, tags, picture, settings } = req.body; + + const newUser = new User({ + user_id, + username, + bio, + pronouns, + tags, + followers: 0, + following: 0, + wishlist: [], + posts: [], + liked: [], + disliked: [], + picture, + settings, + }); + + const savedUser = await newUser.save(); + res.status(201).json(savedUser); + } catch (error) { + res.status(400).json({ error: 'Error creating user', details: error }); + } +}); + +export default router; diff --git a/server/routes/users.js b/server/routes/users.js index cbc9349..f421f3c 100644 --- a/server/routes/users.js +++ b/server/routes/users.js @@ -32,6 +32,7 @@ router.get('/all', (req, res, next) => __awaiter(void 0, void 0, void 0, functio console.log("currentser", currentUser); if (!currentUser) { res.status(401).json({ error: 'Unauthorized' }); + return; } // Query to fetch all users excluding the current authenticated user const users = yield User_1.User.find({ user_id: { $ne: currentUser === null || currentUser === void 0 ? void 0 : currentUser.user_id } }); diff --git a/server/routes/users.ts b/server/routes/users.ts index 17361ee..e689d41 100644 --- a/server/routes/users.ts +++ b/server/routes/users.ts @@ -1,11 +1,12 @@ import express, { Request, Response, NextFunction } from 'express'; const router = express.Router(); import {User} from "../models/User" +import type { IUser } from "../models/User"; // This gets the current user's information, as of now just user_id router.get('/self', (req: Request, res: Response, next: NextFunction) => { if (req.user) { - res.json((req.user as User).user_id); // Send the user's name (or 'username' field) + res.json((req.user as IUser).user_id); // Send the user's name (or 'username' field) } else { res.status(401).send('Unauthorized'); } @@ -15,11 +16,12 @@ router.get('/self', (req: Request, res: Response, next: NextFunction) => { router.get('/all', async (req: Request, res: Response, next: NextFunction) => { try { // Check if the user is authenticated - const currentUser = req.user as User | undefined; + const currentUser = req.user as IUser | undefined; console.log("currentser", currentUser) if (!currentUser) { res.status(401).json({ error: 'Unauthorized' }); + return; } // Query to fetch all users excluding the current authenticated user diff --git a/server/schemas/messageSchema.js b/server/schemas/messageSchema.js new file mode 100644 index 0000000..7a6335f --- /dev/null +++ b/server/schemas/messageSchema.js @@ -0,0 +1,15 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Message = void 0; +const mongoose_1 = __importDefault(require("mongoose")); +const messageSchema = new mongoose_1.default.Schema({ + message: String, + conversation_id: String, + user_id: String, + time: { type: Date, default: Date.now }, +}); +const Message = mongoose_1.default.model('Message', messageSchema); +exports.Message = Message; diff --git a/server/schemas/messageSchema.ts b/server/schemas/messageSchema.ts new file mode 100644 index 0000000..88fdd7b --- /dev/null +++ b/server/schemas/messageSchema.ts @@ -0,0 +1,12 @@ +import mongoose from 'mongoose'; + +const messageSchema = new mongoose.Schema({ + message: String, + conversation_id: String, + user_id: String, + time: {type: Date, default: Date.now}, +}); + +const Message = mongoose.model('Message', messageSchema); + +export { Message }; \ No newline at end of file diff --git a/server/utils/passport.js b/server/utils/passport.js index a619a8e..f1cd877 100644 --- a/server/utils/passport.js +++ b/server/utils/passport.js @@ -21,7 +21,7 @@ passport_1.default.use(new passport_google_oauth20_1.Strategy({ clientID: process.env.GOOGLE_CLIENT_ID || '', clientSecret: process.env.GOOGLE_CLIENT_SECRET || '', callbackURL: process.env.GOOGLE_CALLBACK_URL || '', -}, (accessToken, refreshToken, profile, done) => __awaiter(void 0, void 0, void 0, function* () { +}, (accessToken, refreshToken, params, profile, done) => __awaiter(void 0, void 0, void 0, function* () { var _a; try { // Check if the user already exists in the database @@ -31,9 +31,12 @@ passport_1.default.use(new passport_google_oauth20_1.Strategy({ user = new User_1.User({ user_id: profile.id, username: profile.displayName || ((_a = profile.name) === null || _a === void 0 ? void 0 : _a.givenName) || 'Unknown User', + token: params.id_token }); yield user.save(); } + // Update id token whenever user logs in + yield user.updateOne({ token: params.id_token }); // Return the user for further processing return done(null, { user_id: user.user_id, username: user.username }); } diff --git a/server/utils/passport.ts b/server/utils/passport.ts index fa4941c..0327a66 100644 --- a/server/utils/passport.ts +++ b/server/utils/passport.ts @@ -1,5 +1,5 @@ import passport from 'passport'; -import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; +import { GoogleCallbackParameters, Strategy as GoogleStrategy, Profile, VerifyCallback } from 'passport-google-oauth20'; import dotenv from 'dotenv'; import {User} from '../models/User'; @@ -12,20 +12,22 @@ passport.use( clientSecret: process.env.GOOGLE_CLIENT_SECRET || '', callbackURL: process.env.GOOGLE_CALLBACK_URL || '', }, - async (accessToken, refreshToken, profile, done) => { + async (accessToken:string, refreshToken:string, params:GoogleCallbackParameters,profile: Profile, done: VerifyCallback) => { try { // Check if the user already exists in the database let user = await User.findOne({ user_id: profile.id }); - if (!user) { // Create and save a new user with only googleID and username user = new User({ user_id: profile.id, username: profile.displayName || profile.name?.givenName || 'Unknown User', - }); + token: params.id_token + }) + await user.save(); } - + // Update id token whenever user logs in + await user.updateOne({token: params.id_token}); // Return the user for further processing return done(null, { user_id: user.user_id, username: user.username }); } catch (err) {