From f94ad939eafc78a282247a035f5f4e956e76f712 Mon Sep 17 00:00:00 2001 From: Arunit Date: Sun, 3 Aug 2025 15:40:57 +0530 Subject: [PATCH 1/3] feat: added user routes and centralized fetching logic in frontend - added user routes to fetch all, fetch single, update, add and delete user. - added a centralized user fetching in frontend. Closes #41 --- backend/app.py | 3 + backend/routes/users.py | 123 ++++++++++++++++++ backend/supabase_client.py | 7 +- .../infinite-carousel/EmblaCarousel.jsx | 33 ++--- .../mobile-carousel/MobCarousel.jsx | 27 +--- src/pages/Home/Home.jsx | 31 ++++- src/utils/fetchProfiles.js | 14 -- src/utils/fetchUsers.js | 19 +++ 8 files changed, 191 insertions(+), 66 deletions(-) create mode 100644 backend/routes/users.py delete mode 100644 src/utils/fetchProfiles.js create mode 100644 src/utils/fetchUsers.js diff --git a/backend/app.py b/backend/app.py index df2bc38..b5b9cc6 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,5 +1,6 @@ import os +from routes.users import users_bp from config import Config from flask import Flask from flask_cors import CORS @@ -17,3 +18,5 @@ app.register_blueprint(auth_bp, url_prefix='/auth') +app.register_blueprint(users_bp, url_prefix='/api/users') + diff --git a/backend/routes/users.py b/backend/routes/users.py new file mode 100644 index 0000000..03356c2 --- /dev/null +++ b/backend/routes/users.py @@ -0,0 +1,123 @@ +from collections import defaultdict + +from flask import Blueprint, request, jsonify, g +from supabase_client import supabase +from functools import wraps + +users_bp = Blueprint("users",__name__) + +def auth_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + try: + auth_header = request.headers.get("Authorization") + if not auth_header or not auth_header.startswith("Bearer "): + return jsonify({"error": "Authorization header is missing or invalid"}), 401 + access_token = auth_header.split(" ")[1] + user_response = supabase.auth.get_user(access_token) + if not user_response.data: + return jsonify({"error": "User not authenticated"}), 401 + g.user = user_response.data + g.user_id = user_response.data["id"] + return f(*args, **kwargs) + except Exception as e: + return jsonify({"error": str(e)}), 401 + return decorated_function + +@users_bp.route("/all", methods=["GET"]) +@auth_required +def get_users(): + try: + users_response = supabase.table("user_info").select("*").execute() + if not users_response.data: + return jsonify({"message":"No users found"}), 404 + users=users_response.data + user_ids = [user["id"] for user in users] + tags_response = supabase.table("tags_user_table")\ + .select("user_id, tag_id, tags_table(name)")\ + .in_("user_id",user_ids)\ + .execute() + user_tags_map = defaultdict(list) + for entry in tags_response.data: + user_id = entry["user_id"] + tag_name = entry["tags_table"]["name"] + user_tags_map[user_id].append(tag_name) + for user in users: + user["tags"] = user_tags_map.get(user["id"], []) + + return jsonify(users), 200 + except Exception as e: + return jsonify({"error getting profiles from database": str(e)}), 500 + + +@users_bp.route("/me",methods=["GET"]) +@auth_required +def get_user(): + try: + user_response = supabase.table("user_info").select("*").eq("id", g.user_id).single().execute() + if not user_response.data: + return jsonify({"message":f"User with id {g.user_id} not found"}), 404 + user = user_response.data + tags_response = supabase.table("tags_user_table") \ + .select("tag_id, tags_table(name)") \ + .eq("user_id", g.user_id) \ + .execute() + tags=[entry["tags_table"]["name"] for entry in tags_response.data] + user["tags"]=tags + return jsonify(user), 200 + except Exception as e: + return jsonify({"error fetching user profile": str(e)}), 500 + +@users_bp.route("/me",methods=["DELETE"]) +@auth_required +def delete_user(): + try: + supabase.table("tags_user_table").delete().eq("user_id",g.user_id).execute() + supabase.table("user_info").delete().eq("id",g.user_id).execute() + return jsonify({"message":"User deleted successfully"}), 200 + except Exception as e: + return jsonify({"error deleting user":str(e)}), 500 + +#add a single user +@users_bp.route("/register",methods=["POST"]) +@auth_required +def add_user(): + try: + data = request.get_json() + if not data: + return jsonify({"message":"No data provided"}), 400 + data["id"] = g.user_id + tags = data.pop("tags",[]) + existing = supabase.table("user_info").select("id").eq("id", g.user_id).maybe_single().execute() + if existing.data: + return jsonify({"message": "User already registered"}), 409 + response = supabase.table("user_info").insert(data).execute() + if not response.data: + return jsonify({"message":"Error adding user"}), 500 + user_id = response.data[0]["id"] + for tag in tags: + tag_insert = {"user_id":user_id, "tag_id":tag} + supabase.table("tags_user_table").insert(tag_insert).execute() + return jsonify({"message":"User added successfully"}), 201 + except Exception as e: + return jsonify({"error adding user":str(e)}), 500 + + +#modify a user +@users_bp.route("/me", methods=["PUT"]) +@auth_required +def modify_user(): + try: + data = request.get_json() + if not data: + return jsonify({"message":"No data provided"}), 400 + tags = data.pop("tags",[]) + supabase.table("user_info").update(data).eq("id",g.user_id).execute() + if tags is not None: + supabase.table("tags_user_table").delete().eq("user_id", g.user_id).execute() + for tag in tags: + tag_insert = {"user_id": g.user_id, "tag_id": tag} + supabase.table("tags_user_table").insert(tag_insert).execute() + return jsonify({"message":"User updated successfully"}), 200 + except Exception as e: + return jsonify({"error updating user":str(e)}), 500 \ No newline at end of file diff --git a/backend/supabase_client.py b/backend/supabase_client.py index 2d2bc06..aacc3b4 100644 --- a/backend/supabase_client.py +++ b/backend/supabase_client.py @@ -8,8 +8,11 @@ load_dotenv() -url = os.environ.get("SUPABASE_URL", "") -key = os.environ.get("SUPABASE_KEY", "") +# url = os.environ.get("SUPABASE_URL", "") +# key = os.environ.get("SUPABASE_KEY", "") + +url="https://nbllkutffqgadnaniqtq.supabase.co" +key="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im5ibGxrdXRmZnFnYWRuYW5pcXRxIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTM4MTU2NjUsImV4cCI6MjA2OTM5MTY2NX0.IdgV4Flch2pvjtK6Bpqc0Jbmut4PnwzuGpuXJHiDTDg" def get_supabase() -> Client: if "supabase" not in g: diff --git a/src/components/infinite-carousel/EmblaCarousel.jsx b/src/components/infinite-carousel/EmblaCarousel.jsx index ddbee41..5814749 100644 --- a/src/components/infinite-carousel/EmblaCarousel.jsx +++ b/src/components/infinite-carousel/EmblaCarousel.jsx @@ -2,7 +2,6 @@ import React, { useEffect, useState, useCallback } from "react"; import useEmblaCarousel from "embla-carousel-react"; import nextArrow from "../../assets/carousel/nextArrow.svg"; import prevArrow from "../../assets/carousel/prevArrow.svg"; -import fetchUsers from "../../utils/fetchProfiles"; import "./EmblaCarousel.css"; const ProfileCard = ({ @@ -82,38 +81,28 @@ const SkeletonCard = ({ index, selectedSlide }) => { ); }; -const EmblaCarousel = () => { +const EmblaCarousel = ({profiles, setProfiles, error, loading}) => { const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true, align: "center", }); //Backend integration - - const [profiles, setProfiles] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); const [fadeComplete, setFadeComplete] = useState(false); const [contentVisible, setContentVisible] = useState(false); - useEffect(() => { - const controller = new AbortController(); - - const loadProfiles = async () => { - const { data, error } = await fetchUsers(controller.signal); - if (error) { - setError(error); - } else { - setProfiles(data); - } - setLoading(false); + useEffect(() => { + if (!loading) { setTimeout(() => setContentVisible(true), 100); - }; - - loadProfiles(); + } + }, [loading]); - return () => controller.abort(); - }, []); + useEffect(() => { + if (contentVisible && !fadeComplete) { + const timer = setTimeout(() => setFadeComplete(true), 600); + return () => clearTimeout(timer); + } + }, [contentVisible, fadeComplete]); useEffect(() => { if (contentVisible && !fadeComplete) { diff --git a/src/components/mobile-carousel/MobCarousel.jsx b/src/components/mobile-carousel/MobCarousel.jsx index a0db6b5..860e385 100644 --- a/src/components/mobile-carousel/MobCarousel.jsx +++ b/src/components/mobile-carousel/MobCarousel.jsx @@ -5,7 +5,6 @@ import { useMotionValue, useTransform, } from "framer-motion"; -import fetchUsers from "../../utils/fetchProfiles"; import "./MobCarousel.css"; const SkeletonCard = () => ( @@ -121,32 +120,10 @@ const UserCard = ({ ); }; -export default function MobCarousel() { - const [profiles, setProfiles] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); +export default function MobCarousel({profiles, loading, error, setProfiles}) { const [liked, setLiked] = useState([]); const [disliked, setDisliked] = useState([]); - useEffect(() => { - const controller = new AbortController(); - const loadProfiles = async () => { - try { - setLoading(true); - const { data, error } = await fetchUsers(controller.signal); - if (error) throw new Error(error); - setProfiles(data); - } catch (error) { - setError("Could not load profiles"); - console.error("Failed to fetch profiles:", error); - } finally { - setLoading(false); - } - }; - loadProfiles(); - return () => controller.abort(); - }, []); - const handleSwipe = (profile, direction) => { if (direction === "right") { setLiked((prev) => [...prev, profile]); @@ -182,7 +159,7 @@ export default function MobCarousel() { ); } - if (profiles.lenth === 0) { + if (profiles.length === 0) { return (

No more profiles 💔

diff --git a/src/pages/Home/Home.jsx b/src/pages/Home/Home.jsx index 999e5f7..719d985 100644 --- a/src/pages/Home/Home.jsx +++ b/src/pages/Home/Home.jsx @@ -8,12 +8,37 @@ import MobNav from "../../components/mobile-nav/MobNav.jsx"; import MobFoot from "../../components/mobile-footer/MobFoot.jsx"; import MobCarousel from "../../components/mobile-carousel/MobCarousel.jsx"; import "@fontsource/sacramento"; +import {useEffect, useState} from "react"; +import fetchUsers from "../../utils/fetchUsers.js"; const Home = () => { + const [profiles, setProfiles] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const controller = new AbortController(); + const loadProfiles = async () => { + try { + setLoading(true); + const { data, error } = await fetchUsers(controller.signal); + if (error) throw new Error(error); + setProfiles(data); + } catch (error) { + console.error("Failed to fetch profiles:", error); + setError("Could not load profiles"); + } finally { + setLoading(false); + } + }; + loadProfiles(); + return () => controller.abort(); + }, []); + return ( <>
- +

@@ -21,7 +46,7 @@ const Home = () => {
blush today?🥰

- +
pass @@ -46,7 +71,7 @@ const Home = () => {

Ready to make someone blush today?🥰

- +
pass diff --git a/src/utils/fetchProfiles.js b/src/utils/fetchProfiles.js deleted file mode 100644 index bb115a9..0000000 --- a/src/utils/fetchProfiles.js +++ /dev/null @@ -1,14 +0,0 @@ -const API_BASE_URL = import.meta.env.VITE_API_URL; - -const fetchUsers = async (signal) => { - try { - const res = await fetch(`${API_BASE_URL}/api/profiles`, { signal }); - if (!res.ok) throw new Error("Failed to fetch profiles"); - const data = await res.json(); - return { data }; - } catch (error) { - return { error: error.message }; - } -}; - -export default fetchUsers; diff --git a/src/utils/fetchUsers.js b/src/utils/fetchUsers.js new file mode 100644 index 0000000..63e145e --- /dev/null +++ b/src/utils/fetchUsers.js @@ -0,0 +1,19 @@ +const API_BASE_URL = import.meta.env.VITE_API_URL; + +const fetchUsers = async (signal) => { + try { + const res = await fetch(`${API_BASE_URL}/api/users/all`, { signal }); + if (!res.ok) throw new Error("Failed to fetch profiles"); + const data = await res.json(); + return { data }; + } catch (error) { + if (error.name === "AbortError") { + console.warn("Fetch aborted by user or unmount."); + return { error: "aborted" }; + } + console.error("Fetch error:", error); + return { error: error.message }; + } +}; + +export default fetchUsers; From 3612a6dad055279ac7af4fe3b1e6371355d329ff Mon Sep 17 00:00:00 2001 From: Arunit Date: Sun, 3 Aug 2025 15:45:59 +0530 Subject: [PATCH 2/3] fix: removed some comments --- backend/supabase_client.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/backend/supabase_client.py b/backend/supabase_client.py index aacc3b4..2d2bc06 100644 --- a/backend/supabase_client.py +++ b/backend/supabase_client.py @@ -8,11 +8,8 @@ load_dotenv() -# url = os.environ.get("SUPABASE_URL", "") -# key = os.environ.get("SUPABASE_KEY", "") - -url="https://nbllkutffqgadnaniqtq.supabase.co" -key="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im5ibGxrdXRmZnFnYWRuYW5pcXRxIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTM4MTU2NjUsImV4cCI6MjA2OTM5MTY2NX0.IdgV4Flch2pvjtK6Bpqc0Jbmut4PnwzuGpuXJHiDTDg" +url = os.environ.get("SUPABASE_URL", "") +key = os.environ.get("SUPABASE_KEY", "") def get_supabase() -> Client: if "supabase" not in g: From 39aafcee9cba4aa9d5337761c0d02e2f73d6f304 Mon Sep 17 00:00:00 2001 From: Arunit Date: Fri, 8 Aug 2025 22:20:11 +0530 Subject: [PATCH 3/3] feat: added a route to get user roll number i.e id --- backend/routes/users.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/backend/routes/users.py b/backend/routes/users.py index 03356c2..3c1dfc9 100644 --- a/backend/routes/users.py +++ b/backend/routes/users.py @@ -120,4 +120,16 @@ def modify_user(): supabase.table("tags_user_table").insert(tag_insert).execute() return jsonify({"message":"User updated successfully"}), 200 except Exception as e: - return jsonify({"error updating user":str(e)}), 500 \ No newline at end of file + return jsonify({"error updating user":str(e)}), 500 + + #get only id of a user +@users_bp.route("/roll-number", methods=["GET"]) +@auth_required +def roll_number_user(): + try: + return jsonify({"Roll Number of user": g.user_id}), 200 + except Exception as e: + return jsonify({"error getting roll number": str(e)}), 500 + + +