Skip to content

Commit

Permalink
update hasAvatar state logic in auth context
Browse files Browse the repository at this point in the history
  • Loading branch information
thetiagogil committed Jan 24, 2025
1 parent a59ae76 commit 02feae5
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 119 deletions.
6 changes: 0 additions & 6 deletions src/api/mock-data.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
export const mockUser = {
isAuth: true,
hasAvatar: false,
isAvatarLoading: false
};

export const mockTopStrengths = [
{
number: 1,
Expand Down
37 changes: 25 additions & 12 deletions src/api/useUserApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,30 @@ import { UserModel } from "../models/user.model";
import { showToast } from "../utils/toast";

export const getUserByEmail = async (email: string) => {
try {
const { data, error } = await supabase.from("users").select().eq("email", email).single();
if (error) {
if (error.details === "The result contains 0 rows") {
showToast("error", "Invalid credential.");
} else {
showToast("error", "Failed to get user.");
}
}
return data as UserModel;
} catch (error) {
console.error("Failed to get user:", error);
const { data, error } = await supabase.from("users").select().eq("email", email).single();
if (error) {
showToast("error", "Invalid credential.");
}
return data as UserModel;
};

export const getUserById = async (userId: string | null) => {
const { data, error } = await supabase.from("users").select().eq("id", userId).single();
if (error) {
showToast("error", "Failed to get user.");
}
return data as UserModel;
};

export const updateUserAvatarState = async (userId: string | null) => {
const { error: updateError } = await supabase.from("users").update({ hasAvatar: true }).eq("id", userId).single();
if (updateError) {
showToast("error", "Failed to create avatar.");
}

const { data, error: getError } = await supabase.from("users").select().eq("id", userId).single();
if (getError) {
showToast("error", "Failed to get user.");
}
return data as UserModel;
};
2 changes: 1 addition & 1 deletion src/components/layout/avatar-loading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { colors } from "../../utils/colors";

export const AvatarLoading = () => {
return (
<Stack alignItems="center" py={10}>
<Stack alignItems="center">
<Stack bgcolor={colors.background.avatarLoading} alignItems="center" px={28} py={12} borderRadius={32} gap={4}>
<Stack>
<Typography level="h1" textColor="neutral.white">
Expand Down
4 changes: 2 additions & 2 deletions src/components/navigation/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type NavbarProps = {
};

export const Navbar = ({ hasSubvisualIcon }: NavbarProps) => {
const { handleLogout } = useContext(AuthContext);
const { handleLogout, user } = useContext(AuthContext);
const [isOpen, setIsOpen] = useState(false);

return (
Expand All @@ -30,7 +30,7 @@ export const Navbar = ({ hasSubvisualIcon }: NavbarProps) => {
<Stack direction="row" alignItems="center" gap={1.5}>
<Avatar sx={{ fontSize: 48, border: "2px solid", borderColor: "subvisual.pink" }} />
<Stack direction="row" alignItems="center" gap={1}>
<Typography>John</Typography>
<Typography level="body-md">{user?.name}</Typography>
<Dropdown open={isOpen} onOpenChange={() => setIsOpen(!isOpen)}>
<MenuButton variant="plain" size="sm">
{isOpen ? <ArrowUpOutlined sx={{ fontSize: 12 }} /> : <ArrowDownOutlined sx={{ fontSize: 12 }} />}
Expand Down
64 changes: 49 additions & 15 deletions src/contexts/auth.context.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { createContext, ReactNode, useEffect, useState } from "react";
import { getUserByEmail } from "../api/useUserApi";
import { getUserByEmail, getUserById, updateUserAvatarState } from "../api/useUserApi";
import { UserModel } from "../models/user.model";

type AuthContextProps = {
userId: string | null;
isAuthenticated: boolean;
hasAvatar: boolean;
user: UserModel | null;
isLoadingUserData: boolean;
handleLogin: (email: string) => Promise<void>;
handleHasAvatar: () => Promise<void>;
handleLogout: () => Promise<void>;
};

Expand All @@ -16,41 +18,73 @@ type AuthContextProvider = {
export const AuthContext = createContext({} as AuthContextProps);

export const AuthContextProvider = ({ children }: AuthContextProvider) => {
const [userId, setUserId] = useState<string | null>(null);
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
const [hasAvatar, setHasAvatar] = useState<boolean>(false);
const [user, setUser] = useState<UserModel | null>(null);
const [isLoadingUserData, setIsLoadingUserData] = useState(true);

useEffect(() => {
const userId = window.localStorage.getItem("userId");
if (userId) {
setUserId(userId);
setIsAuthenticated(true);
}
const loadUser = async () => {
const storedUserId = localStorage.getItem("userId");
if (storedUserId) {
try {
const user = await getUserById(storedUserId);
setUser(user);
setIsAuthenticated(true);
} catch (error) {
console.error("Failed to get user:", error);
handleLogout();
}
}
setIsLoadingUserData(false);
};

loadUser();
}, []);

const handleLogin = async (email: string) => {
try {
const user = await getUserByEmail(email);
if (user) {
setUserId(user.id);
setHasAvatar(user.hasAvatar);
window.localStorage.setItem("userId", user.id);
setUser(user);
setIsAuthenticated(true);
}
} catch (error) {
console.error("Login failed:", error);
}
};

const handleHasAvatar = async () => {
if (user && !user.hasAvatar) {
try {
await new Promise(res => setTimeout(res, 3000)); // this line is to prolong the loading state for testing purposes
const updatedUser = await updateUserAvatarState(user.id);
if (updatedUser) {
setUser(updatedUser);
}
} catch (error) {
console.error("Failed to create avatar:", error);
}
}
};

const handleLogout = async () => {
setIsAuthenticated(false);
setUserId(null);
setHasAvatar(false);
setUser(null);
window.localStorage.removeItem("userId");
};

return (
<AuthContext.Provider value={{ userId, isAuthenticated, hasAvatar, handleLogin, handleLogout }}>
<AuthContext.Provider
value={{
isAuthenticated,
user,
isLoadingUserData,
handleLogin,
handleHasAvatar,
handleLogout
}}
>
{children}
</AuthContext.Provider>
);
Expand Down
78 changes: 50 additions & 28 deletions src/pages/avatar-create.page.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,63 @@
import { Link as JoyLink, Stack, Typography } from "@mui/joy";
import { Link as ReactLink } from "react-router-dom";
import { useContext, useState } from "react";
import { MockAvatar } from "../api/mock-avatar";
import { ArrowRightOutlined } from "../assets/icons/arrow-right";
import { AvatarLoading } from "../components/layout/avatar-loading";
import { Layout } from "../components/shared/containers";
import { AuthContext } from "../contexts/auth.context";

export const AvatarCreatePage = () => {
return (
<Stack alignItems="center">
<Stack minHeight="100vh" width={1120} justifyContent="space-between" py={10} gap={4}>
<Stack maxWidth={720} alignItems="center" alignSelf="center" gap={4}>
<Typography level="h1" textAlign="center" textColor="neutral.dark">
Your Strength-Based Avatar is Almost Ready!
</Typography>
const { handleHasAvatar } = useContext(AuthContext);
const [isLoadingAvatar, setIsLoadingAvatar] = useState<boolean>(false);

<Typography level="body-md" textColor="neutral.dark">
Now, let's customize the core features to bring your Avatar to life.
</Typography>
</Stack>
const handleCreateAvatar = async () => {
setIsLoadingAvatar(true);
try {
await handleHasAvatar();
} catch (error) {
console.error("Failed to create avatar:", error);
} finally {
setIsLoadingAvatar(false);
}
};

return (
<Layout alignCenter>
{isLoadingAvatar ? (
<AvatarLoading />
) : (
<Stack alignItems="center">
<MockAvatar sx={{ fontSize: 280 }} />
</Stack>
<Stack width={1120} gap={4}>
<Stack maxWidth={720} alignItems="center" alignSelf="center" gap={4}>
<Typography level="h1" textAlign="center" textColor="neutral.dark">
Your Strength-Based Avatar is Almost Ready!
</Typography>

<Typography level="body-md" textColor="neutral.dark">
Now, let's customize the core features to bring your Avatar to life.
</Typography>
</Stack>

<Stack alignItems="center">
<MockAvatar sx={{ fontSize: 280 }} />
</Stack>

<Stack bgcolor="neutral.white" height={128} alignItems="center" borderRadius={8}></Stack>
<Stack bgcolor="neutral.white" height={128} alignItems="center" borderRadius={8}></Stack>

<Stack alignItems="end">
<JoyLink
component={ReactLink}
to="/avatar-results"
underline="none"
endDecorator={<ArrowRightOutlined sx={{ fontSize: 10 }} />}
textColor="neutral.black"
gap={0.5}
>
Next
</JoyLink>
<Stack alignItems="end">
<JoyLink
onClick={handleCreateAvatar}
underline="none"
endDecorator={<ArrowRightOutlined sx={{ fontSize: 10 }} />}
textColor="neutral.black"
gap={0.5}
>
Next
</JoyLink>
</Stack>
</Stack>
</Stack>
</Stack>
</Stack>
)}
</Layout>
);
};
60 changes: 25 additions & 35 deletions src/pages/avatar-results.page.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,40 @@
import { Button, IconButton, Stack, Typography } from "@mui/joy";
import { Link } from "react-router-dom";
import { MockAvatar } from "../api/mock-avatar";
import { mockUser } from "../api/mock-data";
import { ArrowRotateOutlined } from "../assets/icons/arrow-rotate";
import { AvatarLoading } from "../components/layout/avatar-loading";
import { CardTopStrengths } from "../components/layout/card-top-strengths";
import { Layout } from "../components/shared/containers";

export const AvatarResultsPage = () => {
const loading = mockUser.isAvatarLoading;

return (
<>
{loading ? (
<AvatarLoading />
) : (
<Layout alignCenter>
<Stack direction="row" justifyContent="center" gap={14}>
<Stack maxWidth={480}>
<Stack alignItems="center" gap={7}>
<Stack alignItems="center" gap={3}>
<Typography level="h1" fontFamily="Acta-Book">
Your <Typography textColor="subvisual.primary">Strengths</Typography>, brought to life.
</Typography>

<Typography level="body-lg">
Meet your personalized avatar—a reflection of your unique talents and abilities.
</Typography>
</Stack>
<Layout alignCenter>
<Stack direction="row" justifyContent="center" gap={14}>
<Stack maxWidth={480}>
<Stack alignItems="center" gap={7}>
<Stack alignItems="center" gap={3}>
<Typography level="h1" fontFamily="Acta-Book">
Your <Typography textColor="subvisual.primary">Strengths</Typography>, brought to life.
</Typography>

<Stack alignItems="center" gap={4}>
<MockAvatar sx={{ fontSize: 280 }} />
<IconButton>
<ArrowRotateOutlined sx={{ fontSize: 16 }} />
</IconButton>
<Button component={Link} to="/personal">
Go to dashboard
</Button>
</Stack>
</Stack>
<Typography level="body-lg">
Meet your personalized avatar—a reflection of your unique talents and abilities.
</Typography>
</Stack>

<CardTopStrengths />
<Stack alignItems="center" gap={4}>
<MockAvatar sx={{ fontSize: 280 }} />
<IconButton>
<ArrowRotateOutlined sx={{ fontSize: 16 }} />
</IconButton>
<Button component={Link} to="/personal">
Go to dashboard
</Button>
</Stack>
</Stack>
</Layout>
)}
</>
</Stack>

<CardTopStrengths />
</Stack>
</Layout>
);
};
10 changes: 10 additions & 0 deletions src/pages/loading.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { CircularProgress, Stack } from "@mui/joy";
import { SubvisualLogo } from "../assets/icons/subvisual-logo";

export const LoadingPage = () => (
<Stack height="100vh" alignItems="center" justifyContent="center">
<CircularProgress variant="plain" sx={{ "--CircularProgress-size": "200px" }}>
<SubvisualLogo sx={{ fontSize: 80 }} />
</CircularProgress>
</Stack>
);
2 changes: 1 addition & 1 deletion src/pages/signup.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { AuthContext } from "../contexts/auth.context";

export const SignupPage = () => {
const { handleLogin } = useContext(AuthContext);
const [email, setEmail] = useState<string>("[email protected]");
const [email, setEmail] = useState<string>("");
const [isLoadingSubmit, setIsLoadingSubmit] = useState<boolean>(false);

const handleSubmit = async (e: FormEvent) => {
Expand Down
Loading

0 comments on commit 02feae5

Please sign in to comment.