Bramucci Candela
Juan Castro
ProfesorEsta es una GRAN solución, Cande. Felicitaciones. :clap::clap: :star2:
Bramucci Candela
Muchísimas gracias!!
Si llegaste hasta aquí, ya manejas autenticación, autorización y rutas básicas con React Router. Ahora viene el reto final del módulo: construir un sistema de rutas dinámicas con permisos avanzados al estilo de Platzi, Twitter o cualquier red social donde cada usuario tiene su propio perfil público, pero solo el dueño (o un administrador) puede editarlo.
Este desafío te obliga a integrar todo lo aprendido hasta ahora: hooks de React, custom hooks, estado global, patrones de render y las APIs de React Router DOM v6.
¿Cómo funcionan las rutas de perfil públicas en apps tipo Platzi?
La idea es replicar el comportamiento real de plataformas como Platzi, Twitter o cualquier red social. No existe una ruta genérica /profile, sino una ruta única por cada usuario.
Cualquier persona, esté logueada o no, puede entrar a platzi.com/p/juandc y ver la información pública de ese perfil. La aplicación renderiza esa ruta sin importar quién la visite. Pero la edición es otra historia: solo el dueño de la cuenta puede modificar sus datos.
¿Qué es una ruta dinámica en React Router? Es una ruta cuyo segmento cambia según un parámetro, por ejemplo
/p/:username. ConuseParamslees ese valor y cargas la información correspondiente. [01:08]
¿Qué reglas de permisos debes implementar en el reto?
Aquí está el corazón del desafío. La lógica de permisos tiene tres capas que debes resolver con claridad.
La validación se hace comparando la identidad autenticada con el parámetro de la URL. Si los usernames o correos coinciden, se muestra la opción de edición. Si no coinciden y no hay rol de administrador, se oculta.
¿Cómo verifico si el usuario logueado es el dueño del perfil? Comparas el identificador del usuario autenticado (guardado en tu estado global) con el parámetro que obtienes de
useParams. Si son iguales, renderizas el botón de editar. [02:15]
¿Qué herramientas de React necesitas combinar para resolverlo?
Este reto no se resuelve con una sola técnica. Necesitas orquestar varias piezas que ya viste en clases anteriores.
useParams para capturar el username desde la URL.La clave está en pensar la solución de forma filosófica antes de tocar código. Ya tuviste un reto similar antes, así que aplica esa misma manera de razonar a este caso. [03:42]
¿Por qué compartir tu intento, incluso si falla, es lo más valioso?
Aquí va una invitación honesta. Los comentarios favoritos no son los de soluciones perfectas, son los de intentos fallidos bien documentados.
Algo como: "intenté esto, pero pasó aquello, entonces probé esta otra ruta y casi funciona excepto por este detalle". Ese tipo de aporte abre las mejores discusiones porque obliga a pensar a toda la comunidad. La comunidad es el valor más sagrado de Platzi, y se construye justo en esos momentos donde alguien se atreve a mostrar su proceso, no solo el resultado.
Intenta resolver el reto con todas tus fuerzas. Si lo logras, genial. Si no, también. Lo importante es que cuentes tu paso a paso, el cómo, el porqué, las dudas que te surgieron. En los comentarios te ayudamos a llegar a la solución o a descubrir un ángulo que no habías considerado.
¿Qué viene después de este reto en el curso?
Una vez completes el desafío, ya tendrás todas las herramientas de React Router que necesitas para cualquier proyecto. Pero el curso no termina ahí.
El siguiente módulo es una migración real: vas a tomar el proyecto de to do machine, que hasta ahora vive en una sola ruta con modales, y lo vas a convertir en una aplicación con rutas reales. Eso implica también añadir funcionalidades como editar to dos, lo que vuelve la app un poco más compleja y mucho más cercana a un producto real.
Cuéntame en los comentarios cómo te fue con el reto, qué intentaste primero, qué te trabó y qué decisión filosófica tomaste para resolver los permisos.
Saludos, compañeros! (0,0)/
Aquí les comparto mi progreso del curso y los retos aplicados. Si alguien le sirve de referencia o si alguien gusta proporcionar algo de feedback, ya sea en buenas practicas o sugerencias, se agradece y mucho.
Repo. GitHub page.
Advierto que sólo trabajé funcionalidad, por lo que no esperen un trabajo estético xD. Los usuarios admin del sistema son: Irisval, RetaxMaster, freddier, alex. Los usuarios "normales" son juandc, nameless.
Hardcoded data.
Para manejar los datos "hardcodeados" opté por crear archivos para la información del usuario (user.js), blogs (blogdata.js) y los roles de usuario (roles.js). Todos ubicados en src/data/.
Inspirandome en aportaciones dadas por compañeros en clases anteriores, removí la lista de Admins y agregué nuevas propiedades como phonenumber, description y roles, este último siendo un objeto el cual guarda un arreglo de roles.
Para el manejo de roles implementé el uso de constantes. En este caso sólo creé dos roles: ADMIN (admin) y USER (user).
Agregué a los blogs una propeidad comments el cual, obviamente, guarda un arreglo de comentarios.
La estructura de un comentario es la de un objeto con las propiedades content y author.
Services.
Las funcionalidades de Auth del archivo auth.js visto en el curso las trabajé como un servicio. Por lo que creé la carpeta src/services/.
Por comodidad creé un servicio user.js. Esto con el fin de manipular la data del usuario y desarrollar funciones acorde a este. Buscando mantener auth.js lo más posible a sólo cosas relacionadas a funciones de autentificación.
Creé un servicio que funciona como la BD de usuarios. usersDB.js. Esto con el fin de servir de apoyo para las funciones de actualización de usuarios.
Para la manipulación de los blogs opté por crear un servicio al igual que auth: blog.js. Siendo este una copia del auth.js ya que utilizo la mismas funciones o características. Un provider, un useBlog y BlogRoute (ojo en este último).
BlogRoute. Durante el proceso me di cuenta que si accedía a un blog que no existe la app truena. Por lo que redirecciono al usuario en caso de que no exista el blog que indica en la url.
BlogProvider/BlogContext. Además de gestionar los blogs, también sus comentarios. Dejé todos los métodos dentro de este servicio, aunque otra opción sería crear un servicio de comentarios para manipularlos desde ahí utilizando useBlog. Algo parecido a useUser.
Gestión de blogs.
Habilité las funciones de eliminar blogs. Permitiendo dicha función unicamente al propietario del blog o los usuarios admin.
Añadí un formulario para la creación de nuevos blogs (sólo para usuarios con una sesión iniciada).
Añadí una sección de comentarior en el detalle de un blog y la función de agregar un nuevo comentario (sólo para usuarios con una sesión iniciada).
Al igual que los blogs, los usuarios pueden borrar sus comentarios. Pero un usuario admin puede cualquier comentario.
UX login.
En caso de no tener una sesión iniciada, la vista blog cuenta con un botón para dirigir al usuario a la vista de login. Una vez iniciada la sesión, el usuario volverá a la vista de blog.
La funcionalidad anterior también fue implementada desde la sección de comentarios de un blog.
Editar perfil de usuario.
Además de agregar más datos para el perfil de los usuarios, también agregué la posibilidad de cambiar estos datos.
Únicamente permito actualizar el número de teléfono y la descripción del usuario.
username no es un campo válido a modificar (al menos en este proyecto) debido a que funciona como el identificador del usuario y los blogs y comentarios los relaciono a partir de este.
Es posible ver el perfil de otros usuarios ingresando su username en la ruta: e.j. profile/juandc.
Los usuarios admin pueden modificar el perfil de otros usuarios.
Posibles mejoras.
Agregar estilos (por supuesto xD).
Implementar función de Editar (blogs y comentarios).
Guardar comentarios con algún identificador y/o datetime para facilitar la gestión de estos.
El propietario del blog sea capaz de eliminar u ocultar comentarios de sus blogs.
Uso de algún ID para los usuarios (number, hash, etc.).
Editar roles de usuario.
Ajustes en los servicios.
Está muy bueno Martín! Un montón de funcionalidades. Algunas mejoras que espero te ayuden:
De nuevo, muy buen trabajo! Y, me podrías decir como solucionaste el reto de esta clase? Ya que no encuentro más usuarios, no puedo acceder a sus perfiles.
Bramucci Candela
Juan Castro
ProfesorEsta es una GRAN solución, Cande. Felicitaciones. :clap::clap: :star2:
Bramucci Candela
Muchísimas gracias!!
Diego Reyes Cabrera
Jesús Manuel Alarcón Maldonado
Llegue a tu misma solucion! 👩💻
Diego Reyes Cabrera
Jaja me alegro! Viendo mis comentarios 3 años despues
Ulqernesh Karvenae
Reto completado , muy muyyyy retador pero se logro
Trabaje con Tailwind un diseño responsivo
Posibles mejoras
Cualquier feedback es bien recibido, os invito a comentan si tienten dudas de como realice algo o como lo podría mejorar , comenten, saludos compañeros de Platzi
Ulqernesh Karvenae
Ulqernesh Karvenae
Por si desean usar el diseño este es el figma que diseñe
Edier Gama
Hola amigos, les comparto el deploy de mi app y el repositorio por si a alguien le sirve
Github Pages - Deploy
Repositorio
La lista de admins son [ JuanDC - Freddier - Retaxmaster ]
La aplicación tiene persistencia en local storage de los blogs y usuarios, puedes crear tus propios blogs, puedes eliminar tus blogs pero si inicias sesión con otro user ya no podrás hacerlo, a no ser que sean un administrador, un admin puede hacer todo
Ulqernesh Karvenae
Reto 1 😰 Reto 2 😃 Reto 3 😰
Juan Pablo Cortés
David Rangel
David Rangel
David Rangel
Ahhh! Esto era para el reto anterior!
Neil Ruiz
Nicolás García López
Nicolás García López
Para la variable de quien puede editar o no los perfiles tuve que realizar la siguiente modificación
const canChangeName = Users && (auth.user?.role === 'admin') || (Users?.user === auth.user?.userName);
Gonzalo Gutiérrez Castillo
Armando Cruz Vidal
Sebastian Meneses
Buenas, me demore un poco pero lo logre. Le puse algo de amor con bootstrap ❤️ My Blog
Max Andy Diaz Neyra
Brandon James Huamán Mallcco
he aquí mi solución REPO: https://github.com/Brandon328/curso-react-router LIVE: https://rr-blog.netlify.app/
Juan Mercado
UFFFFFFFFFFFFFFFFFFF! DESPUES DE 3 DIAS! Tuve que refactorizar todo, estaba muy perdido. Entonces comenze un nuevo proyecto desde 0.
Implemente una pagina Profiles que solo tiene acceso el rol admin. apartir de esto una ruta publica de profiles/:user para ver el perfil de un usuario.
En resumen: CREE LAS FUNCIONALIDADES DE: -Crear un usuario. -Editar un usuario -Ver la informacion del usuario -Crear un blog -Editar un blog
Dejo el repositorio. Me quedan los estilos y proteger algunas rutas. pero esta es mi forma de solucionar hasta el momento los retos dados hasta ahora:
https://github.com/TheJB4/proyect-blog-react-router-thejb29
Que mas... pues nada, se me complico un poco implementar el componente ProtectedRoute y algunas protecciones son dadads en el mismo componente.
César Augusto Cortés Labrada
Andres Eduardo Maneiro Antunez
Saludos, playzinautas
les comparto un proyecto que hice con los retos de las clases, me faltan detalles por mejorar, pero espero con ansias su feedback
GitHub Page.
usuario admin clave admin123 usuario con role creator juandc clave admin123
usuarios con role editor nicobytes clave admin123 diannerd clave123
Jvalencia
se me ocurrió separar los componentes del blog, y hacer lo mismo que hicimos en las rutas cuando usamos "AuthRoute" pero lo tome como un AuthAccess con el valido los posibles roles y ya sé que componente mostrar y cual no, pero me parece que se complica ya que se debe generar uno para cada componente
Felipe de Jesús Isidro
Aquí mi repositorio de lo que hice se pueden editar y eliminar los blogs solo si tienes los permisos se puede acceder a el de forma publica cree un custom hook para la info de los blogs, acciones y comparto la información en base a los patrones de render del curso anterior no se si es exactamente lo que se buscaba pero me gusto como quedo
Lo logré!
Después de muchos intentos y muchas pruebas, pude hacerlo sin buscar la respuesta en internet. . Lo primero que hice fue cambiar la forma en la que se logeaban los usuarios. Agregue la clase User para que todos tengan la misma estructura.
auth.js
class User { constructor(username) { this.username = username this.roles = {} } } export function AuthProvider({ children }) { const navigate = useNavigate() const [user, setUser] = useState(null) const login = ({ username, locationAfterLogin }) => { const existingUser = users.find(user => user.username === username) if (existingUser) setUser(existingUser) else { const newUser = new User(username) users.push(newUser) setUser(newUser) } navigate(locationAfterLogin) } const logout = () => { setUser(null) } const auth = { user, users, login, logout } return <AuthContext.Provider value={auth}>{children}</AuthContext.Provider> }
Esa fue la parte fácil. Lo segundo fue usar useParams() en ProfilePage. Cambie la ruta de '/profile' a '/profile/:username', luego hice comprobaciones para saber si se trataba de la cuenta del usuario logeado o no.
ProfilePage.js
export function ProfilePage() { const { user, users } = useAuth() const { username } = useParams() let isAuthor const userProfile = users.find(user => user.username === username) if (!userProfile) return <p>sorry! {username} isn't a logged user.</p> if (userProfile === user) isAuthor = true if (isAuthor) return ( <> <h2>Hello, {username}</h2> <button>Edit your profile here!</button> </> ) return <h2>Profile of {username}</h2> }
Surgió el problema que más me costó resolver: el link en Menu redirigia a '/profile', pero esa ruta ya no existía. Probé muchas cosas.
ProfilePage.js
export function ProfileRedirect() { const { user } = useAuth() if (user) return <Navigate to={`/profile/${user.username}`} /> return <NotFound /> }
App.js
<Route path="/profile/:username" element={<ProfilePage />} /> <Route path="/login" element={<LoginPage />} />
Siento que no quería llegar a esta solución, no me parece la más elegante, pero es la que conseguí después de muchos intentos. Y funciona perfecto! . Por último, cualquier sugerencia va a ser agradecida. Gracias por leer :D
Mi solución
Cuando Juan mencionó todo el reto, por mi mente pasó un "Aaaaihhggg.... por dónde empiezo...." . Les describiré cómo se fue creando mi solución en mi cabeza: .
Cambiar estructura de profiles
Antes solo podíamos mirar nuestro perfil, ahora tenemos que hacer que cualquier perfil pueda ver cualquier otro perfil. La ruta "/profile" contiene un input con un buscador de perfiles (hice un array con perfiles hardcodeados). La ruta "/profile/:username" contiene la vista del perfil del usuario.
<Route path="/profile/" element={<SearchProfilePage />} /> <Route path="/profile/:username" element={<ProfilePage />} />
.
Aplicar lógica de búsqueda y navegación
En el buscador de perfiles, verifico si el username ingresado en el input existe; si no existe, lanzo un alert(), si sí existe, navego hacia la ruta "/profile/${username}"
function SearchUserPage() { const navigate = useNavigate(); const [username, setUsername] = useState(""); const searchUser = (ev) => { ev.preventDefault(); if (!users.some((user) => user.username === username)) { alert("This user does not exist. Try another user."); return; } navigate(`/profile/${username}`); }; return ( <> <h3>Search User</h3> <form onSubmit={searchUser}> <label htmlFor="username">Type an username</label> <input type="text" name="username" id="username" value={username} onChange={(ev) => setUsername(ev.target.value)} /> <button type="submit">Search</button> </form> </> ); }
.
Renderizar y Autorización en Profile Page
En Profile Page, recojo el user autenticado y el user enviado por la URL. Si los dos son el mismo user, tiene permiso para editar el perfil. Si el user autenticado tiene el rol de admin, puede editar el perfil.
function ProfilePage() { const { user } = useAuth(); const params = useParams(); const data = users.find((user) => user.username === params.username); const canEditProfile = () => { if (user.role.name === "admin") return true; if (user.username === data.username) return true; return false; }; return ( <> <h2>ProfilePage</h2> <h3>{data.username}</h3> <img src={data.img.src} alt={data.img.alt} width="400" /> <p>{data.bio}</p> {canEditProfile() && <button>Edit Profile</button>} </> ); }
. .
Mi filosofía para este reto
Lo que hago es simular una base de datos teniendo un array de usuarios en un archivo aparte, y en cada búsqueda o autenticación, lo consulto. Y dentro de cada componente hago la lógica necesaria trabajando los datos que tengo: array de usuarios y user autenticado (o no autenticado)
Estas son las vistas del admin
No lo logré. Comparto una forma de no hacerlo. Yo creé un array aparte para simular un "backend" y creé las rutas de la misma forma que en Blogpage y BlogPost
//Este es el "backend" const publicData = []; publicData.push({ name: 'Jhonny', slug_Url: '@jhonny-c', nickname: '@jhonny-c', }); ....
Utilize el contexto Para compartir el "backend". También creé un método signUp para crear una cuenta.
function AuthProvider({ children }) { const [newUser, setNewUser] = React.useState(publicData); ... const signup = ({ username, nickname }) => { un objeto que representa a un nuevo usuario const newSignUp = { name: username, slug_Url: '@' + nickname, nickname: '@' + nickname, } //Crear una copia del "backend" const createUser = [...newUser] //concatenar el nuevo usuario al "backend" createUser.push(newSignUp); //cambiar el "backend" para toda la app setNewUser(createUser); //esta parte es para comprobar si el usuario existe const findUser = newUser.find(user => user.name === username); //el hook useLocation const from = location.state?.from?.pathname || -1; //si el usuario no existe findUser === undefined ? //crearlo setUser({ name: username, nickname: nickname, role: roles.visitor }) //si existe es auth.user : setUser(findUser) //el hook useLocation navigate(from , { replace : true }); } const auth = { user, ... newUser, };
La sub-ruta de ProfilePage es PublicProfile asi se ve desde App.js
function App () { return( ... <Route path="/profile" element={ <ProfilePage/> } > <Route path=":slug" element={ <PublicProfile/> } /> </Route> ... ) }
Asi queda PublicProfile.js
function PublicProfile() { const { slug } = useParams(); const { user, newUser}= useAuth(); const profile = newUser?.filter(person => person.slug_Url === slug); const verified = user === profile.name; return( <> {profile && !verified && profile.map( p => <ProfilesPub props={p} />) } {profile && verified && profile.map( p => <Profiles props={p} />)} </> ); } //este componente es para cuando hace login y le permite modificar el perfil function Profiles({props}) { return( <> <div> <h2>Profile</h2> <h4>{props.name}</h4><button>editar</button> <div></div><button>editar</button> <div>Nickname: {props.nickname}</div><button>editar</button> </div> </> ); } //este componente solo muestra el perfil público y no permite modificaciones. function ProfilesPub({ props }) { return( <> <div> <h2>Profile</h2> <h4>{props.name}</h4> <div></div> <div>Nickname: {props.nickname}</div> </div> </> ); }
Reitero esta es una forma que no funciona. Desde LoginPage o SignUpPage se crea la sub-ruta directamente. Además, la sub-ruta solo es disponible después de hacer logout.
Esto fue lo que hice para el reto (agregué una sección de comments donde sólo puedes comentar si estás loggeado):
import { useState } from 'react'; import { Link } from 'react-router'; import { commentsData } from '../chore/commentsDb'; import { useAuth } from '../utils/auth'; export const Comments = () => { const [currentComment, setCurrentComment] = useState({ title: '', content: '', date: '', author: '', wasSent: false, }); const auth = useAuth(); const canAddComments: boolean = !!auth?.user?.name; const canDisplayCurrentComment: boolean = !!currentComment.title.trim() && !!currentComment.content.trim() && currentComment.wasSent; const onHandleComment = () => { setCurrentComment((prev) => ({ ...prev, wasSent: !prev.wasSent, date: new Date().toISOString().split('T')[0], author: auth?.user?.name as string, })); }; return ( <> <h3>Comentarios:</h3> {canAddComments && !currentComment.wasSent && ( <> <input placeholder='Título' value={currentComment.title} onChange={(e) => setCurrentComment((prev) => ({ ...prev, title: e.target.value, })) } /> <input placeholder='Comentarios' value={currentComment.content} onChange={(e) => setCurrentComment((prev) => ({ ...prev, content: e.target.value, })) } /> <button onClick={onHandleComment}> Enviar </button> </> )} {canDisplayCurrentComment && ( <> <h4> {currentComment.title} </h4> <p> {currentComment.content} </p> <p> {currentComment.date} </p> <h5> {currentComment.author} </h5> </> )} {!auth?.user?.name && ( <> <p>Haz loggin para agregar comentarios: </p> <Link to='/login?redirected=true'> Loggin </Link> </> )} {commentsData.map((comment) => ( <> <h4> {comment.title} </h4> <p> {comment.content} </p> <p> {comment.date} </p> <h5> {comment.author} </h5> </> ))} <p></p> </> ); };
Adicionalmente hice una ligera modificación en el hook de Auth para redirigir de forma inteligentemente si el login fue a través de la sección de comments o si fue de alguna otra parte del sitio:
const AuthProvider = ({ children }: { children: ReactNode }) => { const [searchParams] = useSearchParams(); const navigate = useNavigate(); const [user, setUser] = useState<UserType | null>(null); const wasRedirected: boolean = searchParams.get('redirected') === 'true'; const login = (name: string) => { const isAdmin: boolean = adminList.includes(name); setUser({ name, isAdmin }); if (wasRedirected) navigate(-1); if (!wasRedirected) navigate('profile'); }; const logout = () => { setUser(null); navigate('/'); }; const auth = useMemo(() => ({ user, login, logout }), [user, login, logout]); return <AuthContext.Provider value={auth}>{children}</AuthContext.Provider>; };
La solución que logré construir está basada en un arreglo que contiene la hipotética información privada de cada usuario registrado en la App, la cual alojé en un componente al que le dí el nombre de userdata.jsx:
const userdata = []; userdata.push({ name: "Hugo", email: "Hugo@gmail.com", city: "San Francisco", age: 21, role:"frontend" }); userdata.push({ name: "Paco", email: "Paco@gmail.com", city: "Seattle", age: 34, role:"backend" }); userdata.push({ name: "Luis", email: "Luis@gmail.com", city: "New York", age: 47, role:"full stack" }); export {userdata};
Inicialmente construí el botón "Editar Perfil" dentro del componente ProfilePage.jsx, que al final de todo el proceso, me quedó de la siguiente forma:
import { useAuth } from "./auth"; import { useNavigate, useParams, Outlet } from "react-router-dom"; function ProfilePage() { const auth = useAuth(); const navigate = useNavigate(); const {username} = useParams(); const isOwnProfile = username === auth.user?.username; const canEdit = (isOwnProfile || auth.user?.isAdmin) && auth.user?.validUser; const editProfile = () => { navigate(`/profile/${username}/edit`); }; return ( <> <h1>ProfilePage</h1> {isOwnProfile ? ( <> <p>Welcome, {auth.user.username}</p> </> ) : ( <> <p>Este es el perfil público de {username}</p> </> )} {canEdit && <button onClick={editProfile}>Editar perfil</button>} <Outlet /> </> ); } export { ProfilePage };
Para validar la existencia del usuario logueado en alguna de las 2 listas (adminList o userdate), fue necesario replantear el login dentro del componente auth.jsx, que al final de todo el proceso, me quedó así:
import { createContext, useContext, useState } from "react"; import { useNavigate, Navigate, useLocation } from "react-router-dom"; import { userdata } from "./userdata"; const adminList = ['Irisval', 'Retaxmaster', 'freddier']; const AuthContext = createContext(); function AuthProvider({ children }) { const navigate = useNavigate(); const [user, setUser] = useState(null); const login = ({ username }) => { const isAdmin = adminList.includes(username); const validUser = isAdmin || userdata.some(u => u.name === username); setUser({ username, validUser, isAdmin }); navigate("/profile"); }; const logout = () => { setUser(null); navigate("/"); }; const auth = { user, login, logout }; return <AuthContext.Provider value={auth}>{children}</AuthContext.Provider>; } function useAuth() { const auth = useContext(AuthContext); return auth; } function AuthRoute(props) { const auth = useAuth(); const location = useLocation(); if (!auth.user) { return <Navigate to="/login" state={{ from: location }} replace/>; } return props.children; } export { AuthProvider, useAuth, AuthRoute, };
Para que el parámetro dinámico del username sea aceptado, fue necesario ajustar el sistema de rutas del componente App.jsx, que al final del proceso, lucía así:
import { HashRouter, Routes, Route } from "react-router-dom"; import { Menu } from "./Menu"; import { AuthProvider, AuthRoute } from "./auth"; import { HomePage } from "./HomePage"; import { BlogPage } from "./BlogPage"; import { BlogPost } from "./BlogPost"; import { ProfilePage } from "./ProfilePage"; import { EditProfile } from "./EditProfile"; import { LoginPage } from "./LoginPage"; import { LogoutPage } from "./LogoutPage"; function App() { return ( <> <HashRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}> <AuthProvider> <Menu /> <Routes> <Route path="/" element={<HomePage />} /> <Route path="/blog" element={<AuthRoute><BlogPage /></AuthRoute>}> <Route path=":slug" element={<BlogPost />} /> </Route> <Route path="/login" element={<LoginPage />} /> <Route path="/logout" element={<LogoutPage />} /> <Route path="/profile/:username" element={<ProfilePage />}> <Route path="/profile/:username/edit" element={<EditProfile />} /> </Route> <Route path="*" element={<p>Not found<p></p>} /> </Routes> </AuthProvider> </HashRouter> ); } export default App;
Para poder agregar el username a la URL, fue necesario ajustar el componente LoginPage.jsx, que después de construir la solución, me quedó así:
import { useState } from 'react'; import { useAuth } from './auth'; import { useLocation, useNavigate} from 'react-router-dom'; function LoginPage () { const auth = useAuth(); const [username, setUsername] = useState ('') const navigate = useNavigate(); const location = useLocation(); const from = location.state?.from?.pathname || '/profile'; const login = (e)=>{ e.preventDefault(); auth.login ({username}); if (from.startsWith("/profile")) { navigate(`/profile/${encodeURIComponent(username)}`, { replace: true, }); } else { navigate(from, { replace: true }); } } return ( <> <h1>LoginPage</h1> <form onSubmit={login}> <label>Escribe tu nombre de usuario:</label> <input value={username} onChange={e => setUsername (e.target.value)} /> <button type='submit'>Entrar</button> </form> ) } export {LoginPage};
La utilización del username en la URL, también hizo necesario ajustar el componente Menu.jsx, que al final del proceso se ve así:
import { NavLink } from "react-router-dom"; import { useAuth } from "./auth"; function Menu() { const auth = useAuth(); return ( <nav> <ul> {routes.map((route) => { if (route.publicOnly && auth.user) return null; if (route.private && !auth.user) return null; let to = route.to; if (to === "/profile" && auth.user?.username) { to = `/profile/${encodeURIComponent(auth.user.username)}`; } return ( <li> <NavLink style={({ isActive }) => ({ color: isActive ? "red" : "blue" })} to={to} > {route.text} </NavLink> </li> ); })} </ul> </nav> ); } const routes = []; routes.push({ to: "/", text: "Home", }); routes.push({ to: "/blog", text: "Blog", }); routes.push({ to: "/profile", text: "Profile", private: true, }); routes.push({ to: "/login", text: "Login", publicOnly: true }); routes.push({ to: "/logout", text: "Logout", private: true, }); export { Menu };
Finalmente, al componente que renderiza la información privada de los usuarios registrados en la App se le asignó el nombre de EditProfile.jsx y después de construir toda la solución, luce de la sigiente forma:
import {useParams, useNavigate } from 'react-router-dom'; import { userdata } from "./userdata"; function EditProfile() { const navigate = useNavigate(); const {username} = useParams(); const user = userdata.find((u) => u.name === username); const goBack = () => navigate(-1); // volver a la vista anterior return ( <> <h2>Editando perfil de {user.name}</h2> <p><strong>Email:</strong> {user.email}</p> <p><strong>Ciudad:</strong> {user.city}</p> <p><strong>Edad:</strong> {user.age}</p> <p><strong>Rol:</strong> {user.role}</p> <button onClick={goBack}>Volver</button> ); } export { EditProfile };
Mi solución: 1) Para crear las rutas dinámicas, es básicamente mirar lo que hicimos anteriormente con el contenido de los blogs. Primero crear un componente donde renderizaremos las paginas dinámicas. y con ello hacer la modificación permitente en nuestro Router
<Route path="/profile" element={ <AuthRoute> <Profile /> </AuthRoute> } > <Route path=":users" element={<ProfileUser />} /> </Route> ```2) Modificar el valor de nuestra ruta profile que tenemos en menu.jsx ```js { to: `/profile/${auth.user?.userName}`, text: 'Profile', private: true, } ```Con esto ya logramos el tener paginas dinámicas al ir a profile según el usuario con el que ingresemos en login. 3\) ahora es hora de dar cierta personalización en cada pagina, para ello elegí darle mas información a cada usuario, este array decidí agregarlo un estado, pero al tener mas de 3 estados en auth.jsx opte por crear un estado compuesto para tener un mejor manejo: ```js const userList = [ { user: 'SrPizza', nickname: 'Sr_Pizza', location: 'Bogotá, Col', role: 'admin' }, { user: 'Percy', nickname: 'Perrito_Percy', location: 'Bogotá, Col', role: 'admin' }, { user: 'Nicolas', nickname: 'XnicolasG', location: 'NY, US', role: 'poster' } ];
const [authState, setAuthState] = useState({ user: null, dataBlog: Blogdata, prevPath: null, userList: userList }) ```con esto podemos renderizar la información en cada perfil según el usuario primero ubicando los datos en el array segun nuestro slug en este caso lo llame 'users' ```js const { users } = useParams(); let Users = auth.userList.find(us => us.user === users) ```con esto ya podemos renderizar la información de nuestro usuario especifico dandole cierta diferencia de si el usuario se encuentra registrado en nuestro array o no ```js return ( <section> { Users ? ( <> <h2>{Users.user} - ({Users.role})</h2> <h4>{Users.nickname}</h4> <p>{Users.location}</p> </>) : ( <h2>{auth.user.userName} - (Visitor)</h2>) } </section> ) ```4) Por último decidí que se pueda editar el nickname, pero solamente si es la persona es dueña del perfil, o si su rol es admin. Lo primero es dictar un condicional inicialmente creando una variable que use para la condición  ```js const canChangeName = Users || auth.userList.some((us) => us.user === auth.user?.userName && us.role === 'admin') ```una vez creada ya podemos generar la condición en la cual genere un pequeño formulario con un input y un boton: ```js return ( <section> { Users ? ( <> <h2>{Users.user} - ({Users.role})</h2> <h4>{Users.nickname}</h4> <p>{Users.location}</p> { canChangeName && ( <form onSubmit={changeName}> <label htmlFor="Nuevo nombre"></label> <input value={newName} onChange={(e)=>setNewName(e.target.value)} type="text" /> <button type='submit'>Cambiar nombre</button> </form> ) } </>) : ( <h2>{auth.user.userName} - (Visitor)</h2>) } </section> ) } ```5) para generar la acción de editar el nickname y vincularlo al formulario lo hice de esta manera: ```js const changeName = (e) =>{ e.preventDefault(); const profileUser = Users.user; const findUser = auth.userList.find((us)=> us.user === profileUser); if (findUser) { findUser.nickname = newName; auth.setAuthState((prevState)=>({ ...prevState, userList:Array.isArray(prevState.userList) ? [...prevState.userList] : [] })) } }
Complete los 3 retos, la parte a la que agregé mayor funcionalidad fue al blog, ya que se puede editar y eliminar comentarios, el titulo y el contenido del BlogPost. Para lograr todas las validaciones de los roles y las acciones permitidas y no permitidas en el blog use un nuevo custom hook que hice yo mismo. Espero les agrade mi solución. No creo que sea la forma más optima de hacerlo, pero me sirvió para reforzar mas cosas sobre el estado, la composición de componentes, useReducer, y los closures.import React, { useReducer } from "react"; const initialState = { //role:"visitor", read:true, selfWrite:false, selfDelete:false, elseWrite:false, elseDelete:false,}; //Action Typesconst actionTypes = { admin: 'admin', editor: 'editor', customer: 'customer', visitor: 'visitor',} const reducerObject = (state, action) => { const actions = { [actionTypes.admin]: { ...state, userName:action.payload, role:actionTypes.admin, selfWrite:true, selfDelete:true, elseWrite:true, elseDelete:true, }, [actionTypes.editor]: { ...state, userName:action.payload, role:actionTypes.editor, selfWrite:true, selfDelete:true, elseWrite:false, elseDelete:false, }, [actionTypes.customer]: { ...state, userName:action.payload, role:actionTypes.customer, selfWrite:true, selfDelete:true, elseWrite:false, elseDelete:false, }, }; return actions[action.type] || initialState;}; export function useAuthReducer() { const [state, dispatch] = useReducer(reducerObject, initialState); //Action creators const onAdmin = (userName) => {dispatch({type: actionTypes.admin, payload:userName});} const onEditor = (userName) => {dispatch({type: actionTypes.editor, payload:userName});} const onCustomer = (userName) => {dispatch({type: actionTypes.customer, payload:userName});} const onvisitor = () => {dispatch(initialState);} // const onWrite = (eventValue) => { // dispatch({type: actionTypes.write, payload:eventValue}); // } //create a switch expression executing the 3 above functions according to the userRole parameter const dispatchUser = (user)=>{ if(!!user){ switch (user.userRole) { case 'admin': onAdmin(user.userName); break; case 'editor': onEditor(user.userName); break; case 'customer': onCustomer(user.userName); break; default: break; }}else{ onvisitor(); } } return [state, dispatchUser] // const onDelete = (eventValue) => {}
Custom hook, useBlogContent,js:
import React from 'react' export const useBlogContent = ({ contentToChange, actions, setBlogdataLocal }) => { const { onEdit, onDelete } = actions const [edit, setEdit] = React.useState(false); const [inputContent, setInputContent] = React.useState(contentToChange); const [loading, setLoading] = React.useState(false); const handleEdit = () => { setEdit(true); }; const handleSave = () => { setLoading(true); setInputContent(inputContent); setBlogdataLocal(onEdit); setEdit(false); setLoading(false); }; const handleDelete = () => { setLoading(true); setBlogdataLocal(onDelete); setLoading(false); }; const handleCancelEdit = () => { setEdit(false); const currentValue = inputContent; setInputContent(currentValue); }; return { edit, inputContent, loading, setInputContent, handleEdit, handleSave, handleDelete, handleCancelEdit, }; }; ```Componente BlogPost (Composicion de componente, permisos, usa el customhook useBlogContent): ```js import React from 'react' import { useParams, useNavigate } from 'react-router-dom' //import { blogdata } from './blogData' import { useAuth } from './auth' import Comments from './Comments/Comments' import Comment from './Comments/Comment' import RequestButton from './Buttons/RequestButton' import { useBlogContent } from './useBlogContent' function BlogPost({blogDataStates}) { const [blogdataLocal, setBlogdataLocal] = blogDataStates const navigate = useNavigate() const auth = useAuth() const { user } = auth const { slug } = useParams() const { title, content, author, comments } = blogdataLocal.find(post => post.slug === slug) const returnToBlog = () => { navigate('/blog') } const updateActions = { 'EDIT': 'EDIT', } const updateTitlePost = (action, inputContent) => ( blogdataLocal.map(post => { if (post.slug === slug) { if(action === updateActions.EDIT){ return { ...post, title: inputContent }; } } return post; }) ) const updateContentPost = (action, inputContent) => ( blogdataLocal.map(post => { if (post.slug === slug) { if(action === updateActions.EDIT){ return { ...post, content: inputContent }; } } return post; }) ) const deletePost = () => { const updatedBlogData = blogdataLocal.filter(post => post.slug !== slug); setBlogdataLocal(updatedBlogData); navigate('/blog') } const { edit:editTitle, inputContent: inputContentTitle, loading: loadingTitle, setInputContent: setInputContentTitle, handleEdit: handleEditTitle, handleCancelEdit: handleCancelEditTitle, handleSave: handleSaveTitle, } = useBlogContent({ contentToChange: title, actions: { onEdit: () => updateTitlePost(updateActions.EDIT, inputContentTitle), }, setBlogdataLocal, }); const { edit:editContent, inputContent: inputContentPostContent, loading: loadingContent, setInputContent: setInputContentPostContent, handleEdit: handleEditContent, handleCancelEdit: handleCancelEditContent, handleSave: handleSaveContent, } = useBlogContent({ contentToChange: content, actions: { onEdit: () => updateContentPost(updateActions.EDIT, inputContentPostContent), }, setBlogdataLocal, }); return ( <> <header> {!editTitle && <h3>{title}</h3>} {editTitle && ( <input type="text" value={inputContentTitle} onChange={(e) => setInputContentTitle(e.target.value)} /> )} {!editTitle && ( <div> {(user?.elseWrite || user?.userName === author) && ( <button onClick={handleEditTitle}>Editar título</button> )} </div> )} {editTitle && ( <div> <RequestButton loading={loadingTitle} requestType={"EDIT"} onClick={handleSaveTitle} > Guardar </RequestButton> <button onClick={handleCancelEditTitle}>Cancelar</button> </div> )} </header> <br /> <button onClick={returnToBlog}>Volver al blog</button> <p>{author}</p> {!editContent && <p>{content}</p>} {editContent && ( <textarea value={inputContentPostContent} onChange={(e) => setInputContentPostContent(e.target.value)} /> )} {!editContent && ( <> {user?.elseWrite && <button onClick={handleEditContent}>Editar Contenido</button>} {user?.userName === author && (!user?.elseWrite || !user?.elseDelete) && <button onClick={handleEditContent}>Editar Contenido</button> } </> )} {editContent && ( <div> <RequestButton loading={loadingContent} requestType={"EDIT"} onClick={handleSaveContent} > Guardar </RequestButton> <button onClick={handleCancelEditContent}>Cancelar</button> </div> )} <br /> {user?.elseDelete && <RequestButton requestType={"DELETE"} onClick={deletePost}>Borrar Post</RequestButton>} {user?.userName === author && (!user?.elseWrite || !user?.elseDelete) && <RequestButton requestType={"DELETE"} onClick={deletePost}>Borrar Post</RequestButton> } <br /> <Comments> {comments.map(({ idUser, content }, index) => ( <Comment key={index} idUser={idUser} content={content} slug={slug} setBlogdataLocal={setBlogdataLocal} blogdataLocal={blogdataLocal} /> ))} </Comments> {/*crea */} </> ); } export default BlogPost ```Componente Comment (Composicion de componente, permisos, usa el customhook useBlogContent): ```js import React from 'react' import { users, useAuth } from '../auth' import RequestButton from '../Buttons/RequestButton'; import { useBlogContent } from '../useBlogContent.js'; function Comment({idUser, content, slug, blogdataLocal, setBlogdataLocal}) { const author = findUser(idUser) const { user } = useAuth() const updateActions = { 'EDIT': 'EDIT', 'DELETE': 'DELETE' } const updatedBlogData = (action, inputContent) => ( blogdataLocal.map(post => { if (post.slug === slug) { let updatedComments; if(action === updateActions.DELETE){ updatedComments = post.comments.filter(comment => comment.idUser !== idUser); } else if(action === updateActions.EDIT){ updatedComments = post.comments.map(comment => { if(comment.idUser === idUser){ return { ...comment, content: inputContent }; } return comment; }); } return { ...post, comments: updatedComments }; } return post; }) ) const { edit, inputContent, loading, setInputContent, handleEdit, handleCancelEdit, handleDelete: handleDeleteComment, handleSave, } = useBlogContent({ contentToChange: content, actions: { onEdit:()=> updatedBlogData(updateActions.EDIT,inputContent), onDelete:()=> updatedBlogData(updateActions.DELETE, inputContent), }, setBlogdataLocal, }); return ( <article> <h5>{author.userName}</h5> <div> {!edit && <p>{content}</p>} {edit && <textarea value={inputContent} onChange={(e) => setInputContent(e.target.value)} />} </div> {!edit && <div> {user?.elseWrite && <button onClick={handleEdit}>Editar</button>} {user?.elseDelete && <RequestButton requestType={'DELETE'} onClick={handleDeleteComment} loading={loading}>Borrar</RequestButton>} {user?.userName === author.userName && (!user?.elseWrite||!user?.elseDelete) && ( <> <button onClick={handleEdit}>Editar</button> <RequestButton requestType={'DELETE'} loading={loading} onClick={handleDeleteComment}>Borrar</RequestButton> </> )} </div>} { edit && <div> <RequestButton loading={loading} requestType={'EDIT'} onClick={handleSave}>Guardar</RequestButton> <button onClick={handleCancelEdit}>Cancelar</button> </div> } </article> ); } //a function that iterates over a list of objects and return the object that have the same userName that i am looking for export function findUser(idUser){ let user = users.find(user => user.id === idUser) user = user ? user: {userName: 'Anónimo'} return user } export default Comment
auth.js (aqui resuelvo el segundo y tercer reto, además uso el reducer para permitir la escalabilidad de mas roles):
import React from 'react' import { useNavigate, Navigate, useLocation } from 'react-router-dom'; import { useAuthReducer } from './AuthReducer' const AuthContext = React.createContext(); const roles = { admin: 'admin', editor: 'editor', customer: 'customer', visitor: 'visitor', }; export const users = [ { id: 1, userName: "Andres", role: roles.admin, }, { id: 2, userName: "Felipe", role: roles.editor, }, { id: 3, userName: "Pedro", role: roles.customer, }, ]; export function AuthProvider({children}) { //const [user, setUser] = useState(null) const [user, dispatchUser] = useAuthReducer(); const navigate = useNavigate() const login = ({ userName, callback }) => { //revisar si el usuario existe o lo crea como visitante const userFound = users.find((usu) => usu.userName === userName); if (userFound !== undefined) { dispatchUser({ userName: userFound.userName, userRole: userFound.role })}else{ dispatchUser(null); } if (callback){ callback(); } else{ navigate("/profile"); } } const logout = () => { dispatchUser(null); navigate('/') } const auth = { user, login, logout} return ( <AuthContext.Provider value={auth}> {children} </AuthContext.Provider> ) } export function useAuth() { const auth = React.useContext(AuthContext) return auth } export function RequireAuth(props) { const auth = useAuth(); let location = useLocation(); if (!auth.user.role) { return <Navigate to="/login" state={{ from: location }} replace/> } return props.children } ```AuthReducer.js: ```js import React, { useReducer } from "react"; const initialState = { //role:"visitor", read:true, selfWrite:false, selfDelete:false, elseWrite:false, elseDelete:false, }; //Action Types const actionTypes = { admin: 'admin', editor: 'editor', customer: 'customer', visitor: 'visitor', } const reducerObject = (state, action) => { const actions = { [actionTypes.admin]: { ...state, userName:action.payload, role:actionTypes.admin, selfWrite:true, selfDelete:true, elseWrite:true, elseDelete:true, }, [actionTypes.editor]: { ...state, userName:action.payload, role:actionTypes.editor, selfWrite:true, selfDelete:true, elseWrite:false, elseDelete:false, }, [actionTypes.customer]: { ...state, userName:action.payload, role:actionTypes.customer, selfWrite:true, selfDelete:true, elseWrite:false, elseDelete:false, }, }; return actions[action.type] || initialState; }; export function useAuthReducer() { const [state, dispatch] = useReducer(reducerObject, initialState); //Action creators const onAdmin = (userName) => {dispatch({type: actionTypes.admin, payload:userName});} const onEditor = (userName) => {dispatch({type: actionTypes.editor, payload:userName});} const onCustomer = (userName) => {dispatch({type: actionTypes.customer, payload:userName});} const onvisitor = () => {dispatch(initialState);} //a switch expression executing the 3 above functions according to the userRole parameter const dispatchUser = (user)=>{ if(!!user){ switch (user.userRole) { case 'admin': onAdmin(user.userName); break; case 'editor': onEditor(user.userName); break; case 'customer': onCustomer(user.userName); break; default: break; }}else{ onvisitor(); } } return [state, dispatchUser] // const onDelete = (eventValue) => { }
Hola a comunidad, les dejo mi código del proyecto en typescript. Tome varias ideas interesantes de otros comentarios, espero el mio tambien les sea de ayuda:
import { NavLink } from 'react-router-dom' import { useAuth } from '../hooks/useAuth'; const Menu = () => { const { user } = useAuth(); return( <nav> <ul> { routes.map(route=>{ if(route.to.includes('/profile')) route.to = `/profile/${user?.username.toLowerCase()}` if(route.private && !user) return null if(route.to === '/login' && user) return null return ( <li key={route.id}> <NavLink style={({ isActive })=>({ color: isActive? 'red': 'blue'} )} to={route.to} > {route.text} </NavLink> </li> ) }) } </ul> </nav> ) } interface IRoute { id: number, to: string, text: string, private: boolean, } const routes:IRoute[] = [ { id: 1, to: '/', text: 'Home', private: false, }, { id: 2, to: '/blog', text: 'Blog', private: false, }, { id: 3, to: '/profile', text: 'Profile', private: true, }, { id: 4, to: '/login', text: 'Login', private: false, }, { id: 5, to: '/logout', text: 'Logout', private: true, } ]; export { Menu }
function App() { return ( <> <HashRouter> <UsersDBProvider> <AuthProvider> <BlogProvider> <Menu /> <Routes> <Route path='/' element={<HomePage />} /> <Route path='/blog' element={<BlogPage />}> <Route path=':slug' element={<BlogPost />} /> </Route> <Route path='/login' element={<LoginPage />} /> <Route path='/logout' element={ <AuthRoute> <LogoutPage /> </AuthRoute> } /> <Route path='/profile/:username' element={<ProfilePage />} /> <Route path='*' element={<p>Not found</p>} /> </Routes> </BlogProvider> </AuthProvider> </UsersDBProvider> </HashRouter> </> ) }
export interface IUser { username: string, alias: string, description: string, rol: Rol, } export enum Rol { Admin = 'admin', User = 'user', } export const usersData:IUser[] = [ { username: 'Steve', alias: 'Capitan America', description: 'I am Steve Rogers', rol: Rol.User, }, { username: 'Tony', alias: 'Iron Man', description: 'I am Tony Stark', rol: Rol.Admin, }, { username: 'Bruce', alias: 'Hulk', description: 'I am Bruce Banner', rol: Rol.User, }, { username: 'Hank', alias: 'Hombre Hormiga', description: 'I am Hank Pym', rol: Rol.Admin, } ]
import { useState, useEffect, FC, PropsWithChildren } from 'react'; import { createContext } from "react"; import { IUser, usersData } from "../data/usersData"; interface UsersDB { users : IUser[]; findUser: (username:string) => IUser | undefined; updateUser: (user: Partial<IUser>)=>void } const UsersDBContext = createContext({} as UsersDB ); const UsersDBProvider:FC<PropsWithChildren> = ({ children }) => { const [users, setUsers] = useState([] as UsersDB['users']); useEffect(()=>{ setUsers(usersData); }, []) const findUser = (username: string) => { return users.find(user=>user.username.toLowerCase() === username.toLowerCase()); } const updateUser = (user:Partial<IUser>) => { const idx = users.findIndex((u) => u.username === user.username); if(idx >= 0){ const newUsers = [...users]; newUsers[idx] = { ...newUsers[idx], ...user } setUsers(newUsers); } } return( <UsersDBContext.Provider value={{ users, findUser, updateUser}}> { children } </UsersDBContext.Provider> ) } export { UsersDBContext, UsersDBProvider}
import { useContext } from "react" import { UsersDBContext } from "../store/UsersStore" const useUsersDB = () => { const usersDB = useContext(UsersDBContext); return usersDB; } export { useUsersDB }
import { useState, FormEvent } from "react"; import { useParams } from "react-router-dom"; import { useAuth } from "../hooks/useAuth"; import { Rol } from "../data/usersData"; import { useUsersDB } from "../hooks/useUsersDB"; type IProfileParams = { username: string } const ProfilePage = () => { const { username } = useParams<IProfileParams>(); const { user: userAuth } = useAuth(); const usersDB = useUsersDB(); const [edit, setEdit] = useState(false); const userProfile = usersDB.findUser(username as string); const [description, setDescription] = useState(userProfile?.description || ''); const onToggleEdit = () => { setEdit(!edit) } const onSave = (e:FormEvent<HTMLFormElement>) => { e.preventDefault(); usersDB.updateUser({ ...userProfile, description}); setEdit(false); } if (!userProfile) return <p>sorry! {username} isn't a logged user.</p> return( <> <h2>Perfil</h2> <p>Welcome, { userProfile?.username?.split('').map((elem, i)=>i===0?elem.toUpperCase():elem).join('') } </p> <p>Nombre de héroe: { userProfile?.alias }</p> {edit?( <form onSubmit={onSave}> <span>Descripion: </span> <input type="text" value={description} onChange={(e)=>setDescription(e.target.value)} /> <button>Guardar</button> </form> ):( <> <p>Descripción: { userProfile.description }</p> <p>Rol: { userProfile.alias }</p> </> )} {(userAuth?.username === userProfile?.username || userAuth?.rol === Rol.Admin) && ( <button onClick={onToggleEdit} style={{ color: 'red', marginTop: '4px' }}>{edit? 'Cancelar':' Editar'}</button> )} </> ) } export { ProfilePage }
Condiciones:
Para realizar el reto: Cambié la ruta del componente <ProfilePage> a una ruta dinámica:
{ path: "/profile/:slug", element:( <AuthRoute> <ProfilePage/> </AuthRoute>), },
En mi custom hook useAuth.jsx tengo los roles de los usuarios y manejo el estado del username logueado. Los envío en el return del hook.
Espero haber entendido lo que se pedía en el reto. Pero queda algo pendiente, hacer que el botón actualice datos. La cosa es que mi información es hardcodeada en su mayoría ya que no uso un objeto literal que tenga todos esos datos y luego pueda cambiar sus propiedades, tampoco uso una API, base de datos o localstorage. Pero si tuviera la forma de cambiar los datos que está debajo de Count information:
<h4>Count information</h4> <p><b>Interests:</b></p> <p>....</p> <p><b>Posts created:</b><span>{` ...`}</span></p> <p><b>Contributions:</b><span>{` ...`}</span></p>
Sería actualizando un estado que contenga un objeto literal con la información de ese perfil. La actualización sería con un componente formulario que actualice el estado y luego navegue al profilepage, el profilpage renderizaría datos nuevos.