Skip to content

Commit d7addee

Browse files
committed
added pb backend logic, working on displaying errors
1 parent 4326390 commit d7addee

File tree

8 files changed

+257
-2
lines changed

8 files changed

+257
-2
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React, { createContext, useContext, useState, ReactNode } from 'react';
2+
import AsyncStorage from '@react-native-async-storage/async-storage';
3+
4+
type AuthContextType = {
5+
login: (email: string, password: string) => Promise<void>;
6+
logout: () => Promise<void>;
7+
token: string | null;
8+
};
9+
10+
const AuthContext = createContext<AuthContextType | undefined>(undefined);
11+
12+
export const AuthProvider = ({ children }: { children: ReactNode }) => {
13+
const [token, setToken] = useState<string | null>(null);
14+
15+
const login = async (email: string, password: string) => {
16+
const res = await fetch('http://localhost:8000/api/v1/login', {
17+
method: 'POST',
18+
headers: { 'Content-Type': 'application/json' },
19+
body: JSON.stringify({ email, password }),
20+
});
21+
22+
const data = await res.json();
23+
if (res.ok) {
24+
await AsyncStorage.setItem('access_token', data.access_token);
25+
setToken(data.access_token);
26+
} else {
27+
throw new Error(data.detail || 'Login failed');
28+
}
29+
};
30+
31+
const logout = async () => {
32+
await AsyncStorage.removeItem('access_token');
33+
setToken(null);
34+
};
35+
36+
return (
37+
<AuthContext.Provider value={{ login, logout, token }}>
38+
{children}
39+
</AuthContext.Provider>
40+
);
41+
};
42+
43+
export const useAuth = () => {
44+
const ctx = useContext(AuthContext);
45+
if (!ctx) throw new Error("useAuth must be used within an AuthProvider");
46+
return ctx;
47+
};
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { useEffect, useState } from "react";
2+
import { getAccessToken } from "@/scripts/auth"; // adjust based on your token handling
3+
4+
export const usePersonalBests = () => {
5+
const [pbs, setPbs] = useState([]);
6+
const [loading, setLoading] = useState(true);
7+
8+
useEffect(() => {
9+
console.log("🔁 usePersonalBests hook loaded");
10+
11+
const fetchPBs = async () => {
12+
console.log("📡 Fetching personal bests...");
13+
setLoading(true);
14+
try {
15+
console.log("🔑 Attempting to get token...");
16+
const token = await getAccessToken();
17+
console.log("Got token:", token);
18+
19+
const res = await fetch("http://localhost:8000/api/v1/personal-bests", {
20+
headers: {
21+
Authorization: `Bearer ${token}`,
22+
},
23+
});
24+
25+
const data = await res.json();
26+
console.log("📬 Fetched PBs:", data);
27+
setPbs(data.data); // Adjust if shape differs
28+
} catch (err) {
29+
console.error("Failed to fetch personal bests", err);
30+
} finally {
31+
setLoading(false);
32+
}
33+
};
34+
35+
fetchPBs();
36+
}, []);
37+
38+
39+
40+
return { pbs, loading };
41+
};

KonditionExpo/scripts/auth.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import AsyncStorage from '@react-native-async-storage/async-storage';
2+
3+
export const getAccessToken = async (): Promise<string | null> => {
4+
try {
5+
const token = await AsyncStorage.getItem('access_token');
6+
console.log("Token from AsyncStorage:", token);
7+
return token;
8+
} catch (err) {
9+
console.error("Error getting token", err);
10+
return null;
11+
}
12+
};

backend/app/api/routes/p_bests.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# TODO test PB dedicated router, 3 REST routes, 1 post & 2 gets
2+
3+
from typing import List
4+
from fastapi import APIRouter, Depends, HTTPException, status
5+
from sqlmodel import Session
6+
7+
from app import crud
8+
from app.core.db import get_session
9+
from app.core.security import get_current_active_user
10+
from app.models import PersonalBestCreate, PersonalBestRead, PersonalBestsList
11+
12+
router = APIRouter(tags=["personal-bests"], prefix="/personal-bests")
13+
14+
@router.post("/", response_model=PersonalBestRead, status_code=status.HTTP_201_CREATED)
15+
def record_personal_best(
16+
pb_in: PersonalBestCreate,
17+
session: Session = Depends(get_session),
18+
current_user=Depends(get_current_active_user),
19+
):
20+
"""
21+
Record or update a personal best for the current user.
22+
"""
23+
pb = crud.create_or_update_personal_best(
24+
session=session, user_id=current_user.id, pb_in=pb_in
25+
)
26+
return pb
27+
28+
@router.get("/", response_model=PersonalBestsList)
29+
def list_personal_bests(
30+
session: Session = Depends(get_session),
31+
current_user=Depends(get_current_active_user),
32+
):
33+
all_pbs = crud.get_personal_bests(session=session, user_id=current_user.id)
34+
return {"data": all_pbs, "count": len(all_pbs)}
35+
36+
@router.get("/{metric}", response_model=PersonalBestRead)
37+
def get_personal_best(
38+
metric: str,
39+
session: Session = Depends(get_session),
40+
current_user=Depends(get_current_active_user),
41+
):
42+
pb = crud.get_personal_best(
43+
session=session, user_id=current_user.id, metric=metric
44+
)
45+
if not pb:
46+
raise HTTPException(status_code=404, detail="No personal best for that metric")
47+
return pb

backend/app/api/routes/workouts.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
ExerciseUpdate,
2020
ExercisePublic,
2121
)
22+
from app.crud import update_personal_bests_after_workout
23+
2224

2325
router = APIRouter(prefix="/workouts", tags=["workouts"])
2426

@@ -247,6 +249,7 @@ def delete_workout(
247249

248250
return Message(message="Workout deleted successfully")
249251

252+
# TODO - Kush, added PB updating, test
250253

251254
@router.post("/{workout_id}/complete", response_model=WorkoutPublic)
252255
def complete_workout(
@@ -289,6 +292,10 @@ def complete_workout(
289292
session.add(workout)
290293
session.commit()
291294
session.refresh(workout)
295+
296+
#added for updating personalbests
297+
update_personal_bests_after_workout(session=session, workout=workout)
298+
292299

293300
return workout
294301

backend/app/crud.py

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,20 @@
22
from typing import Any
33

44
from sqlmodel import Session, select
5-
65
from app.core.security import get_password_hash, verify_password
76
from app.models import Item, ItemCreate, User, UserCreate, UserUpdate
7+
from app.models import Workout, Exercise, PersonalBest, PersonalBestCreate
8+
from datetime import date
89

10+
# Define metric keys for exercises we want to track personal bests for
11+
TRACKED_EXERCISES = {
12+
"bench press": "bench-press",
13+
"squat": "squat",
14+
"deadlift": "deadlift",
15+
"push-ups": "pushups",
16+
"pull-ups": "pullups",
17+
# extend as needed
18+
}
919

1020
def create_user(*, session: Session, user_create: UserCreate) -> User:
1121
db_obj = User.model_validate(
@@ -52,3 +62,71 @@ def create_item(*, session: Session, item_in: ItemCreate, owner_id: uuid.UUID) -
5262
session.commit()
5363
session.refresh(db_item)
5464
return db_item
65+
66+
67+
68+
#TODO test, create_or_update_personal_best will upsert only if the new value is strictly better.
69+
# Two getters: one for all metrics, one for a single metric.
70+
71+
def create_or_update_personal_best(
72+
*, session: Session, user_id: uuid.UUID, pb_in: PersonalBestCreate
73+
) -> PersonalBest:
74+
# see if user already has a PB on this metric
75+
stmt = select(PersonalBest).where(
76+
PersonalBest.user_id == user_id,
77+
PersonalBest.metric == pb_in.metric
78+
)
79+
existing = session.exec(stmt).one_or_none()
80+
81+
# update if new value is "better" (you define the logic per metric)
82+
if existing:
83+
if pb_in.value > existing.value:
84+
existing.value = pb_in.value
85+
existing.date = pb_in.date
86+
session.add(existing)
87+
else:
88+
existing = PersonalBest.model_validate(pb_in, update={"user_id": user_id})
89+
session.add(existing)
90+
91+
session.commit()
92+
session.refresh(existing)
93+
return existing
94+
95+
def get_personal_bests(
96+
*, session: Session, user_id: uuid.UUID
97+
) -> list[PersonalBest]:
98+
stmt = select(PersonalBest).where(PersonalBest.user_id == user_id)
99+
return session.exec(stmt).all()
100+
101+
def get_personal_best(
102+
*, session: Session, user_id: uuid.UUID, metric: str
103+
) -> PersonalBest | None:
104+
stmt = select(PersonalBest).where(
105+
PersonalBest.user_id == user_id,
106+
PersonalBest.metric == metric
107+
)
108+
return session.exec(stmt).one_or_none()
109+
110+
def update_personal_bests_after_workout(*, session: Session, workout: Workout):
111+
user_id = workout.user_id
112+
113+
# Fetch all exercises associated with this workout
114+
exercises = workout.exercises
115+
116+
for exercise in exercises:
117+
name = exercise.name.lower()
118+
metric_key = TRACKED_EXERCISES.get(name)
119+
if not metric_key:
120+
#
121+
continue # skip exercises we don't track
122+
123+
# Use weight * reps as performance metric for now
124+
value = (exercise.weight or 0) * (exercise.reps or 0)
125+
if value <= 0:
126+
#continue
127+
value = 0
128+
129+
pb_in = PersonalBestCreate(metric=metric_key, value=value, date=date.today())
130+
131+
from app.crud import create_or_update_personal_best # to avoid circular imports
132+
create_or_update_personal_best(session=session, user_id=user_id, pb_in=pb_in)

backend/app/models.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from pydantic import EmailStr
44
from sqlmodel import Field, Relationship, SQLModel
5+
from dates import date
56

67

78
# Shared properties
@@ -45,6 +46,9 @@ class User(UserBase, table=True):
4546
hashed_password: str
4647
items: list["Item"] = Relationship(back_populates="owner", cascade_delete=True)
4748

49+
# TODO test
50+
personal_bests: list["PB"] = Relationship(back_populates="user", cascade_delete=True)
51+
4852

4953
# Properties to return via API, id is always required
5054
class UserPublic(UserBase):
@@ -81,6 +85,25 @@ class Item(ItemBase, table=True):
8185
owner: User | None = Relationship(back_populates="items")
8286

8387

88+
89+
90+
# TODO test, one-to-many User.personal_bests relationship.
91+
# expose PersonalBestBase for writes, PersonalBest for reads, and wrapper list.
92+
93+
class PersonalBestBase(SQLModel):
94+
metric: str = Field(max_length=100, description = "e.g. deadlift, 5k-run, pushups")
95+
value: float = Field(..., description="numeric value of the best (e.g. kg, seconds, reps)")
96+
date: date = Field(default_factory=date.today)
97+
98+
class PersonalBest(PersonalBestBase, table=True):
99+
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
100+
user_id: uuid.UUID = Field(foreign_key="user.id", nullable=False, ondelete="CASCADE")
101+
user: User = Relationship(back_populates="personal_bests")
102+
103+
class PersonalBestsList(SQLModel):
104+
data: list[PersonalBest]
105+
count: int
106+
84107
# Properties to return via API, id is always required
85108
class ItemPublic(ItemBase):
86109
id: uuid.UUID

backend/app/models/user.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class UserBase(SQLModel):
1414

1515
# Properties to receive via API on creation
1616
class UserCreate(UserBase):
17-
password: str = Field(min_length=8, max_length=40)
17+
password: str = Field(min_length=8, max_length=50)
1818

1919

2020
class UserRegister(SQLModel):

0 commit comments

Comments
 (0)