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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ And that's it! Your `SkateHub Frontend` should now be up and running locally on

### 2024

- 2024-12-07 - Implement user profile update functionality in AuthContext [#89](https://github.com/jpcmf/Frontend-GraduateProgram-FullStack-2024/pull/89) _(v0.1.27)_
- 2024-12-07 - Create `textarea` form component [#88](https://github.com/jpcmf/Frontend-GraduateProgram-FullStack-2024/pull/88) _(v0.1.26)_
- 2024-11-19 - Add reCAPTCHA verification to sign-up process [#70](https://github.com/jpcmf/Frontend-GraduateProgram-FullStack-2024/pull/70) _(v0.1.25)_
- 2024-11-19 - Add Lint-Staged to enhance pre-commit validations [#65](https://github.com/jpcmf/Frontend-GraduateProgram-FullStack-2024/pull/65) _(v0.1.24)_
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "frontend",
"version": "0.1.26",
"name": "skatehub-frontend",
"version": "0.1.27",
"private": true,
"scripts": {
"dev": "next dev",
Expand Down
16 changes: 10 additions & 6 deletions src/components/Header/Profile.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useRouter } from "next/router";
import { useContext } from "react";
import { RiLogoutCircleLine } from "react-icons/ri";
import {
Expand All @@ -20,6 +21,7 @@ interface ProfileProps {
}

export function Profile({ showProfileData = true }: ProfileProps) {
const router = useRouter();
const { user, signOut } = useContext(AuthContext);

return (
Expand All @@ -39,14 +41,16 @@ export function Profile({ showProfileData = true }: ProfileProps) {

<MenuList bg="gray.900" borderColor="gray.800">
<MenuGroup title="Minha conta">
<MenuItem color="gray.600" bg="gray.900" _hover={{ color: "white" }}>
{user?.about}
</MenuItem>
<MenuItem color="gray.600" bg="gray.900" _hover={{ color: "white" }}>
My Account
<MenuItem
color="gray.600"
bg="gray.900"
_hover={{ color: "white" }}
onClick={() => router.push("/user/edit")}
>
Editar
</MenuItem>
<MenuItem color="gray.600" bg="gray.900" _hover={{ color: "white" }}>
Payments{" "}
{user?.about}
</MenuItem>
<MenuDivider />
<MenuItem
Expand Down
73 changes: 62 additions & 11 deletions src/contexts/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Router from "next/router";
import { createContext, useEffect, useState } from "react";
import { destroyCookie, parseCookies, setCookie } from "nookies";

import { signInRequest, userMe } from "../services/auth";
import { signInRequest, updateUserProfile, userMe } from "../services/auth";

type SignInData = {
email: string;
Expand All @@ -11,37 +11,70 @@ type SignInData = {
};

type User = {
id: string;
name: string;
email: string;
about: string;
username: string;
avatar_url: string;
website_url: string;
};

type UpdateUserData = Pick<User, "id" | "name" | "email" | "about" | "website_url">;

type AuthContextType = {
isAuthenticated: boolean;
user: User | null;
signIn: (data: SignInData) => Promise<void>;
signOut: () => void;
updateUser: (data: UpdateUserData) => Promise<void>;
token: string | null;
isLoading: boolean;
};

export const AuthContext = createContext({} as AuthContextType);

export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);

const isAuthenticated = !!user;

useEffect(() => {
const { "nextauth.token": token } = parseCookies();

if (token) {
userMe(token)
.then(response => {
setUser(response);
})
.catch(() => {
async function loadUserData() {
if (token) {
try {
setToken(token);
userMe(token)
.then(response => {
const userData = response.user || response;
setUser({
id: userData.id,
name: userData.name || userData.username || "User",
email: userData.email,
about: userData.about || "",
username: userData.username,
avatar_url: userData.avatar_url || "",
website_url: userData.website_url || ""
});
})
.catch(() => {
signOut();
});
} catch (error) {
signOut();
});
} finally {
setIsLoading(false);
}
} else {
setIsLoading(false);
}
}

loadUserData();
}, []);

async function signIn({ email, password }: SignInData) {
Expand All @@ -53,16 +86,34 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
});

setUser(user);
console.table(user);

setToken(jwt);
Router.push("/dashboard");
}

async function updateUser(data: UpdateUserData) {
if (!user || !token) {
throw new Error("No authenticated user.");
}

try {
const updatedUser = await updateUserProfile(token, data);
setUser(prevUser => (prevUser ? { ...prevUser, ...updatedUser } : null));
} catch (error) {
console.error("Failed to update user.", error);
throw error;
}
}

function signOut() {
setUser(null);
setToken("");
destroyCookie(undefined, "nextauth.token");
Router.push("/auth/signin");
}

return <AuthContext.Provider value={{ user, isAuthenticated, signIn, signOut }}>{children}</AuthContext.Provider>;
return (
<AuthContext.Provider value={{ user, isAuthenticated, signIn, signOut, token, isLoading, updateUser }}>
{children}
</AuthContext.Provider>
);
}
131 changes: 131 additions & 0 deletions src/features/user/edit/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { useRouter } from "next/router";
import { useContext, useEffect } from "react";
import { SubmitHandler, useForm } from "react-hook-form";
import { Box, Button, Flex, Heading, Divider, SimpleGrid, VStack, HStack } from "@chakra-ui/react";

import { Input } from "@/shared/components/Form/Input";
import { Toast } from "@/components/Toast";
import { Layout } from "@/shared/components/Layout";
import { Textarea } from "@/shared/components/Form/Textarea";
import { AuthContext } from "@/contexts/AuthContext";

type RegisterForm = {
name: string;
email: string;
about: string;
website_url: string;
// password: string;
// password_confirmation: string;
};

export function UserEdit() {
const router = useRouter();
const { addToast } = Toast();

const { user, updateUser } = useContext(AuthContext);

const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset
} = useForm<RegisterForm>();

useEffect(() => {
if (user) {
reset(user);
}
}, [user, reset]);

const handleEditUser: SubmitHandler<RegisterForm> = async values => {
try {
await updateUser({
id: user ? user.id : "",
name: values.name,
email: values.email,
about: values.about,
website_url: values.website_url
});

addToast({
title: "Usuário editado com sucesso.",
message: "Seu perfil foi atualizado.",
type: "success"
});
} catch (error) {
addToast({
title: "Erro ao editar usuário.",
message: `Ocorreu um erro ao editar seu perfil: ${error}`,
type: "error"
});
console.log("Erro ao editar usuário:", error);
}
};

return (
<Layout>
<Box as="form" onSubmit={handleSubmit(handleEditUser)} flex="1" borderRadius={8} bg="gray.800" p={["6", "8"]}>
<Flex mb="8" direction="column">
<Heading size="lg" fontWeight="normal">
Editar
</Heading>
<Divider my="6" borderColor="gray.700" />
<VStack spacing="4">
<SimpleGrid minChildWidth="240px" spacing="4" w="100%">
<Input label="Nome completo" {...register("name")} error={errors.name} isDisabled />
<Input type="email" label="E-mail" {...register("email")} error={errors.email} />
</SimpleGrid>
<SimpleGrid minChildWidth="240px" spacing="4" w="100%">
<Flex flexDirection="column">
<Textarea label="Sobre você" placeholder="Sobre você..." {...register("about")} error={errors.about} />
</Flex>
</SimpleGrid>
<SimpleGrid minChildWidth="240px" spacing="4" w="100%">
<Input
isInputGroup
InputLeftAddonText="instagram.com/"
label="Perfil Instagram"
placeholder="Ex. nome_do_usuário"
/>
<Input
isInputGroup
InputLeftAddonText="https://"
label="Website Pessoal"
placeholder="Ex. www.site.com.br"
{...register("website_url")}
error={errors.website_url}
/>
</SimpleGrid>

{/* <SimpleGrid minChildWidth="240px" spacing={["6", "8"]} w="100%">
<Input type="password" label="Senha" {...register("password")} error={errors.password} />
<Input
type="password"
label="Confirmar senha"
{...register("password_confirmation")}
error={errors.password_confirmation}
/>
</SimpleGrid> */}
</VStack>
<Flex mt="8" justify="flex-end">
<HStack spacing="4">
<Button as="a" size="lg" fontSize="sm" colorScheme="whiteAlpha" onClick={() => router.push("/dashboard")}>
Cancelar
</Button>
<Button
type="submit"
isLoading={isSubmitting}
size="lg"
fontSize="sm"
colorScheme="green"
isDisabled={!user?.email}
>
Salvar
</Button>
</HStack>
</Flex>
</Flex>
</Box>
</Layout>
);
}
5 changes: 5 additions & 0 deletions src/pages/user/edit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { UserEdit } from "@/features/user/edit";

export default function UserEditPage() {
return <UserEdit />;
}
38 changes: 37 additions & 1 deletion src/services/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ type SignInData = {
password: string;
};

type UpdateUserData = {
id: string;
name: string;
email: string;
about: string;
// username: string;
// avatar_url: string;
website_url: string;
};

export async function signInRequest({ email, password }: SignInData) {
const res = await axios.post(`${API}/api/auth/local`, {
identifier: email,
Expand All @@ -18,9 +28,35 @@ export async function signInRequest({ email, password }: SignInData) {
export async function userMe(token: string) {
const res = await axios.get(`${API}/api/users/me`, {
headers: {
Authorization: `Bearer ${token}`
Authorization: `Bearer ${token}`,
"Content-Type": "application/json"
}
});

if (!res.data) {
throw new Error("Failed to fetch user.");
}

return res.data;
}

export async function updateUserProfile(token: string, data: UpdateUserData) {
const formData = new FormData();
formData.append("name", data.name);
formData.append("email", data.email);
formData.append("about", data.about);
formData.append("website_url", data.website_url);

const res = await axios.put(`${API}/api/users/${data.id}`, formData, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "multipart/form-data"
}
});

if (!res.data) {
throw new Error("Failed to update user.");
}

return res.data;
}
Loading
Loading