Skip to content

Commit

Permalink
Link user profile settings to backend (firebase and supabase) (#210)
Browse files Browse the repository at this point in the history
Fixes #189 and fixes #124
  • Loading branch information
chunweii authored Nov 1, 2023
1 parent e09d0c5 commit e78450b
Show file tree
Hide file tree
Showing 25 changed files with 709 additions and 222 deletions.
2 changes: 1 addition & 1 deletion frontend/src/components/profile/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const columns: ColumnDef<Attempt>[] = [
id: "actions",
header: "Actions",
cell: ({ row }) => {
const attemptId = row.id;
const attemptId = row.original.id;
return (
<Button
variant="outline"
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/firebase-client/useUpdateProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getAuth, updateProfile } from "firebase/auth";

export const useUpdateProfile = () => {

const updateUserProfile = async ({displayName, photoURL}: {displayName?: string, photoURL?: string}) => {
const auth = getAuth();
try {
await updateProfile(auth.currentUser!, { displayName, photoURL });
} catch (error) {
console.log(error);
}
};

return { updateUserProfile };
};
11 changes: 9 additions & 2 deletions frontend/src/hooks/useHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { AuthContext } from "@/contexts/AuthContext";
import {
getAttemptsOfUser,
createAttemptOfUser,
} from "./../pages/api/historyHandler";
getAttemptById,
} from "@/pages/api/historyHandler";

type AttemptData = {
uid: string;
Expand All @@ -27,5 +28,11 @@ export const useHistory = () => {
}
};

return { fetchAttempts, postAttempt };
const fetchAttempt = async (attemptId: string) => {
if (authIsReady) {
return getAttemptById(currentUser, attemptId);
}
}
;
return { fetchAttempts, fetchAttempt, postAttempt };
};
10 changes: 8 additions & 2 deletions frontend/src/hooks/useUser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useContext } from "react";
import { updateUserByUid as updateUserApi } from "./../pages/api/userHandler";
import { updateUserByUid as updateUserApi, getUserByUid as getUserApi } from "./../pages/api/userHandler";
import { AuthContext } from "@/contexts/AuthContext";
import { EditableUser } from "@/types/UserTypes";

Expand All @@ -12,5 +12,11 @@ export const useUser = () => {
}
};

return { updateUser };
const getAppUser = async (userId?: string) => {
if (authIsReady) {
return getUserApi(userId || currentUser?.uid || "", currentUser);
}
};

return { updateUser, getAppUser };
};
42 changes: 39 additions & 3 deletions frontend/src/pages/api/historyHandler.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { userApiPathAddress } from "@/gateway-address/gateway-address";
import { Attempt } from "@/types/UserTypes";

export const getAttemptsOfUser = async (user: any, uid: string) => {
try {
const url = `${userApiPathAddress}api/${uid}/attempts`;
const url = `${userApiPathAddress}${uid}/attempts`;
const idToken = await user.getIdToken(true);

const response = await fetch(url, {
Expand All @@ -16,16 +17,51 @@ export const getAttemptsOfUser = async (user: any, uid: string) => {
if (!response.ok) {
throw new Error(`Unable to get attempts: ${await response.text()}`);
}
return response.json();
return response.json().then((arr: Array<any>) => {
return arr.map(obj => {
obj["time_saved_at"] = new Date(obj["time_saved_at"]);
obj["time_updated"] = new Date(obj["time_updated"]);
obj["time_created"] = new Date(obj["time_created"]);
return obj
});
});
} catch (error) {
console.error("There was an error fetching the attempts", error);
throw error;
}
};

export const getAttemptById = async (user: any, attemptId: string) => {
try {
const url = `${userApiPathAddress}attempt/${attemptId}`;
const idToken = await user.getIdToken(true);

const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
"User-Id-Token": idToken,
},
});

if (!response.ok) {
throw new Error(`Unable to get attempt: ${await response.text()}`);
}
return response.json().then(val => {
val["time_saved_at"] = new Date(val["time_saved_at"]);
val["time_updated"] = new Date(val["time_updated"]);
val["time_created"] = new Date(val["time_created"]);
return val
});
} catch (error) {
console.error("There was an error fetching the attempt", error);
throw error;
}
};

export const createAttemptOfUser = async (user: any, data: any) => {
try {
const url = `${userApiPathAddress}api/attempt`;
const url = `${userApiPathAddress}attempt`;
const idToken = await user.getIdToken(true);

const response = await fetch(url, {
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/pages/api/userHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,30 @@ export const updateUserByUid = async (user: EditableUser, currentUser: any) => {
throw error;
}
};

export const getUserByUid = async (uid: string, currentUser: any) => {
try {
const url = `${userApiPathAddress}${uid}`;
const idToken = await currentUser.getIdToken(true);

const response = await fetch(url, {
method: "GET",
mode: "cors",
headers: {
"Content-Type": "application/json",
"User-Id-Token": idToken,
"User-Id": currentUser.uid
},
});

const data = await response.json();
if (response.status === 200) {
return data;
} else {
throw new Error(response.statusText);
}
} catch (error) {
console.error("There was an error getting the user", error);
throw error;
}
};
60 changes: 54 additions & 6 deletions frontend/src/pages/attempt/[id]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,51 @@ import { Textarea } from "@/components/ui/textarea";
import { TypographyBody, TypographyCode, TypographyH2 } from "@/components/ui/typography";
import { ArrowLeft } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { Attempt } from "@/types/UserTypes";
import { useHistory } from "@/hooks/useHistory";
import { useQuestions } from "@/hooks/useQuestions";
import { Question } from "@/types/QuestionTypes";
import { DotWave } from "@uiball/loaders";

export default function Page() {
const router = useRouter();
const attemptId = router.query.id;
const { fetchAttempt } = useHistory();
const { fetchQuestion } = useQuestions();
const [attempt, setAttempt] = useState<Attempt>();
const [question, setQuestion] = useState<Question>();
const [loadingState, setLoadingState] = useState<"loading" | "error" | "success">("loading");

useEffect(() => {
if (attemptId === undefined || Array.isArray(attemptId)) {
router.push("/profile");
return;
}
fetchAttempt(attemptId).then((attempt) => {
if (attempt) {
setAttempt(attempt);
return fetchQuestion(attempt.question_id);
} else {
throw new Error("Attempt not found");
}
}).then((question) => {
if (question) {
setQuestion(question);
setLoadingState("success");
} else {
throw new Error("Question not found");
}
}).catch((err: any) => {
setLoadingState("error");
console.log(err);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [attemptId]);

if (attemptId === undefined || Array.isArray(attemptId)) {
return null;
}

return (
<div className="min-h-screen p-12 mx-auto max-w-3xl flex flex-col gap-8">
Expand All @@ -17,27 +59,33 @@ export default function Page() {
<TypographyH2>Attempt</TypographyH2>
</div>

{ loadingState === "loading" ? <div className="h-32 flex items-center justify-center">
<DotWave
size={47}
speed={1}
color="white"
/></div> : loadingState === "error" ? <TypographyBody>Error</TypographyBody> : <>
<div>
<Label className="text-primary">Question</Label>
<TypographyBody>Two Sum</TypographyBody>
<TypographyBody>{question?.title}</TypographyBody>
</div>

<div>
<Label className="text-primary">Attempted At</Label>
<TypographyBody>23/10/2023 15:00</TypographyBody>
<TypographyBody>{attempt?.time_updated.toLocaleString()}</TypographyBody>
</div>

<div>
<Label className="text-primary">Mode of Attempt</Label>
<TypographyBody>Interview • with Chun Wei</TypographyBody>
<TypographyBody>{attempt?.room_id ? "Interview" : "Solo"}</TypographyBody>
</div>

<div>
<Label className="text-primary">Solution</Label>
<TypographyBody>Solved</TypographyBody>
<Textarea disabled={true} className="my-4" defaultValue={'class Solution: def twoSum(self, nums: List[int], target: int) -> List[int]: numToIndex = {} for i in range(len(nums)): if target - nums[i] in numToIndex: return [numToIndex[target - nums[i]], i] numToIndex[nums[i]] = i return []'}>
<TypographyBody>{attempt?.solved ? "Solved": "Unsolved"}</TypographyBody>
<Textarea disabled={true} className="my-4" defaultValue={attempt?.answer || ""}>
</Textarea>
</div>
</div></>}
</div>
)
}
43 changes: 35 additions & 8 deletions frontend/src/pages/profile/[id]/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,44 @@
import Profile from "../_profile";
import { useContext, useEffect, useState } from "react";
import { AuthContext } from "@/contexts/AuthContext";
import { Attempt } from "@/types/UserTypes";
import { useHistory } from "@/hooks/useHistory";
import { useRouter } from "next/router";
import { User } from "firebase/auth";
import { useUser } from "@/hooks/useUser";

export default function Page() {
// TODO: retrieve selected user from user id in url
const router = useRouter();
const id = router.query.id;
const { getAppUser } = useUser();
const { user: currentUser } = useContext(AuthContext);
const { fetchAttempts } = useHistory();

const selectedUser = {
displayName: "John Doe",
email: "[email protected]",
photoURL: "https://www.gravatar.com/avatar/00",
}
const [attempts, setAttempts] = useState<Attempt[]>([]);
const [user, setUser] = useState<User>();
const [loadingState, setLoadingState] = useState<"loading" | "error" | "success">("loading");

useEffect(() => {
if (currentUser && (typeof id === "string")) {
Promise.all([getAppUser(id), fetchAttempts(id)]).then(([user, attempts]) => {
if (user && attempts) {
user["photoURL"] = user["photoUrl"];
console.log(user);
setUser(user);
setAttempts(attempts);
setLoadingState("success");
} else {
throw new Error("User or attempts not found");
}
}).catch((err: any) => {
setLoadingState("error");
console.log(err);
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentUser]);

// TODO: if selected user is null, redirect to 404 page
return (
<Profile selectedUser={selectedUser as User} isCurrentUser={false} attempts={[]}/>
(user && <Profile selectedUser={user} isCurrentUser={user.uid === currentUser?.uid} loadingState={loadingState} attempts={attempts}/>)
)
}
Loading

0 comments on commit e78450b

Please sign in to comment.