Skip to content
Merged
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
3 changes: 3 additions & 0 deletions backend/app.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -17,3 +18,5 @@

app.register_blueprint(auth_bp, url_prefix='/auth')

app.register_blueprint(users_bp, url_prefix='/api/users')

135 changes: 135 additions & 0 deletions backend/routes/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
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

#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



33 changes: 11 additions & 22 deletions src/components/infinite-carousel/EmblaCarousel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ({
Expand Down Expand Up @@ -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) {
Expand Down
27 changes: 2 additions & 25 deletions src/components/mobile-carousel/MobCarousel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
useMotionValue,
useTransform,
} from "framer-motion";
import fetchUsers from "../../utils/fetchProfiles";
import "./MobCarousel.css";

const SkeletonCard = () => (
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -182,7 +159,7 @@ export default function MobCarousel() {
);
}

if (profiles.lenth === 0) {
if (profiles.length === 0) {
return (
<div className="card-stack-empty">
<p>No more profiles 💔</p>
Expand Down
31 changes: 28 additions & 3 deletions src/pages/Home/Home.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,45 @@ 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 (
<>
<div className="mobile">
<MobNav />
<MobNav/>
<div className="main--mobile">
<div className="text-wrapper--mobile">
<h2 className="text--mobile">
Ready to make <br /> someone
<br /> blush today?🥰
</h2>
</div>
<MobCarousel />
<MobCarousel profiles={profiles} loading={loading} error={error} setProfiles={setProfiles}/>
<div className="icons--mob">
<div className="pass-icon--mob">
<img src={pass} alt="pass" className="pass-icon__image--mob" />
Expand All @@ -46,7 +71,7 @@ const Home = () => {
<div className="text-wrapper">
<h2 className="text">Ready to make someone blush today?🥰</h2>
</div>
<EmblaCarousel />
<EmblaCarousel profiles={profiles} loading={loading} error={error} setProfiles={setProfiles}/>
<div className="icons">
<div className="pass-icon">
<img src={pass} alt="pass" className="pass-icon__image" />
Expand Down
14 changes: 0 additions & 14 deletions src/utils/fetchProfiles.js

This file was deleted.

19 changes: 19 additions & 0 deletions src/utils/fetchUsers.js
Original file line number Diff line number Diff line change
@@ -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;