feat: add /users/@me endpoint, add edit button to profile page
This commit is contained in:
		
							parent
							
								
									d2f4e09a01
								
							
						
					
					
						commit
						15797b679c
					
				
					 8 changed files with 115 additions and 40 deletions
				
			
		|  | @ -90,3 +90,27 @@ func (s *Server) getUser(w http.ResponseWriter, r *http.Request) error { | |||
| 	render.JSON(w, r, dbUserToResponse(u, fields)) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (s *Server) getMeUser(w http.ResponseWriter, r *http.Request) error { | ||||
| 	ctx := r.Context() | ||||
| 	claims, _ := server.ClaimsFromContext(ctx) | ||||
| 
 | ||||
| 	u, err := s.DB.User(ctx, claims.UserID) | ||||
| 	if err != nil { | ||||
| 		log.Errorf("Error getting user: %v", err) | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	fields, err := s.DB.UserFields(ctx, u.ID) | ||||
| 	if err != nil { | ||||
| 		log.Errorf("Error getting user fields: %v", err) | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	render.JSON(w, r, GetMeResponse{ | ||||
| 		GetUserResponse: dbUserToResponse(u, fields), | ||||
| 		Discord:         u.Discord, | ||||
| 		DiscordUsername: u.DiscordUsername, | ||||
| 	}) | ||||
| 	return nil | ||||
| } | ||||
|  |  | |||
|  | @ -13,7 +13,7 @@ func Mount(srv *server.Server, r chi.Router) { | |||
| 	s := &Server{srv} | ||||
| 
 | ||||
| 	r.Route("/users", func(r chi.Router) { | ||||
| 		r.With(server.MustAuth).Get("/@me", server.WrapHandler(nil)) | ||||
| 		r.With(server.MustAuth).Get("/@me", server.WrapHandler(s.getMeUser)) | ||||
| 
 | ||||
| 		r.Get("/{userRef}", server.WrapHandler(s.getUser)) | ||||
| 	}) | ||||
|  |  | |||
							
								
								
									
										14
									
								
								frontend/src/lib/Card.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								frontend/src/lib/Card.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,14 @@ | |||
| import React, { PropsWithChildren } from "react"; | ||||
| 
 | ||||
| export type Props = PropsWithChildren<{ title: string }>; | ||||
| 
 | ||||
| export default function Card({ title, children }: Props) { | ||||
|   return ( | ||||
|     <div className="bg-slate-100 dark:bg-slate-700 rounded-md shadow"> | ||||
|       <h1 className="text-2xl p-2 border-b border-zinc-200 dark:border-slate-800"> | ||||
|         {title} | ||||
|       </h1> | ||||
|       <div className="flex flex-col p-2">{children}</div> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
|  | @ -6,39 +6,37 @@ import { | |||
|   EmojiLaughing, | ||||
| } from "react-bootstrap-icons"; | ||||
| 
 | ||||
| import Card from "./Card"; | ||||
| import type { Field } from "./types"; | ||||
| 
 | ||||
| export default function FieldCard({ field }: { field: Field }) { | ||||
|   return ( | ||||
|     <div className=" bg-slate-100 dark:bg-slate-700 rounded-md shadow"> | ||||
|       <h1 className="text-2xl p-2 border-b border-zinc-200 dark:border-slate-800">{field.name}</h1> | ||||
|       <div className="flex flex-col p-2"> | ||||
|         {field.favourite.map((entry) => ( | ||||
|           <p className="text-lg font-bold"> | ||||
|             <HeartFill className="inline" /> {entry} | ||||
|           </p> | ||||
|         ))} | ||||
|         {field.okay.length !== 0 && ( | ||||
|           <p> | ||||
|             <HandThumbsUp className="inline" /> {field.okay.join(", ")} | ||||
|           </p> | ||||
|         )} | ||||
|         {field.jokingly.length !== 0 && ( | ||||
|           <p> | ||||
|             <EmojiLaughing className="inline" /> {field.jokingly.join(", ")} | ||||
|           </p> | ||||
|         )} | ||||
|         {field.friends_only.length !== 0 && ( | ||||
|           <p> | ||||
|             <People className="inline" /> {field.friends_only.join(", ")} | ||||
|           </p> | ||||
|         )} | ||||
|         {field.avoid.length !== 0 && ( | ||||
|           <p className="text-slate-600 dark:text-slate-400"> | ||||
|             <HandThumbsDown className="inline" /> {field.avoid.join(", ")} | ||||
|           </p> | ||||
|         )} | ||||
|       </div> | ||||
|     </div> | ||||
|     <Card title={field.name}> | ||||
|       {field.favourite.map((entry) => ( | ||||
|         <p className="text-lg font-bold"> | ||||
|           <HeartFill className="inline" /> {entry} | ||||
|         </p> | ||||
|       ))} | ||||
|       {field.okay.length !== 0 && ( | ||||
|         <p> | ||||
|           <HandThumbsUp className="inline" /> {field.okay.join(", ")} | ||||
|         </p> | ||||
|       )} | ||||
|       {field.jokingly.length !== 0 && ( | ||||
|         <p> | ||||
|           <EmojiLaughing className="inline" /> {field.jokingly.join(", ")} | ||||
|         </p> | ||||
|       )} | ||||
|       {field.friends_only.length !== 0 && ( | ||||
|         <p> | ||||
|           <People className="inline" /> {field.friends_only.join(", ")} | ||||
|         </p> | ||||
|       )} | ||||
|       {field.avoid.length !== 0 && ( | ||||
|         <p className="text-slate-600 dark:text-slate-400"> | ||||
|           <HandThumbsDown className="inline" /> {field.avoid.join(", ")} | ||||
|         </p> | ||||
|       )} | ||||
|     </Card> | ||||
|   ); | ||||
| } | ||||
|  |  | |||
|  | @ -45,7 +45,7 @@ function Navigation() { | |||
| 
 | ||||
|   const nav = user ? ( | ||||
|     <> | ||||
|       <NavItem to="/me">@{user.username}</NavItem> | ||||
|       <NavItem to={`/u/${user.username}`}>@{user.username}</NavItem> | ||||
|       <NavItem to="/settings">Settings</NavItem> | ||||
|       <NavItem to="/logout">Log out</NavItem> | ||||
|     </> | ||||
|  |  | |||
|  | @ -27,3 +27,8 @@ async function getCurrentUser() { | |||
| 
 | ||||
|   return null; | ||||
| } | ||||
| 
 | ||||
| export function isMeUser(id: string): boolean { | ||||
|   const meUser = useRecoilValue(userState); | ||||
|   return meUser && meUser.id === id; | ||||
| } | ||||
|  |  | |||
|  | @ -1,17 +1,22 @@ | |||
| import React, { useState, useEffect } from "react"; | ||||
| import { useParams } from "react-router-dom"; | ||||
| import { Link, useParams } from "react-router-dom"; | ||||
| import { ArrowClockwise } from "react-bootstrap-icons"; | ||||
| import ReactMarkdown from "react-markdown"; | ||||
| import { Helmet } from "react-helmet"; | ||||
| 
 | ||||
| import type { APIError, User } from "../lib/types"; | ||||
| import NavItem from "../lib/NavItem"; | ||||
| import type { User } from "../lib/types"; | ||||
| import fetchAPI from "../lib/fetch"; | ||||
| import FieldCard from "../lib/FieldCard"; | ||||
| import Card from "../lib/Card"; | ||||
| import { userState } from "../lib/store"; | ||||
| import { useRecoilValue } from "recoil"; | ||||
| 
 | ||||
| function UserPage() { | ||||
|   const params = useParams(); | ||||
| 
 | ||||
|   const [user, setUser] = useState<User>(null); | ||||
|   const meUser = useRecoilValue(userState); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     fetchAPI<User>(`/users/${params.username}`).then((res) => { | ||||
|  | @ -33,13 +38,25 @@ function UserPage() { | |||
|       <Helmet> | ||||
|         <title>@{user.username} - pronouns.cc</title> | ||||
|       </Helmet> | ||||
|       {meUser && meUser.id === user.id && ( | ||||
|         <div className="lg:w-1/3 mx-auto bg-slate-100 dark:bg-slate-700 shadow rounded-md p-2"> | ||||
|           <span> | ||||
|             You are currently viewing your{" "} | ||||
|             <span className="font-bold">public</span> profile. | ||||
|           </span> | ||||
|           <br /> | ||||
|           <Link | ||||
|             to="/edit" | ||||
|             className="hover:underline text-sky-500 dark:text-sky-400" | ||||
|           > | ||||
|             Edit your profile | ||||
|           </Link> | ||||
|         </div> | ||||
|       )} | ||||
|       <div className="container mx-auto"> | ||||
|         <div className="flex flex-col lg:flex-row justify-center lg:justify-start items-center space-y-4 lg:space-y-0 lg:space-x-16 lg:items-start"> | ||||
|         <div className="flex flex-col m-2 p-2 lg:flex-row justify-center lg:justify-start items-center space-y-4 lg:space-y-0 lg:space-x-16 lg:items-start border-b border-slate-200 dark:border-slate-700"> | ||||
|           {user.avatar_url && ( | ||||
|             <img | ||||
|               className="max-w-max lg:max-w-lg rounded-full" | ||||
|               src={user.avatar_url} | ||||
|             /> | ||||
|             <img className="max-w-xs rounded-full" src={user.avatar_url} /> | ||||
|           )} | ||||
|           <div className="flex flex-col lg:mx-auto"> | ||||
|             {user.display_name && ( | ||||
|  | @ -59,7 +76,7 @@ function UserPage() { | |||
|                 {user.bio} | ||||
|               </ReactMarkdown> | ||||
|             )} | ||||
|             {user.links.length !== 0 && ( | ||||
|             {user.links.length !== 0 && user.fields.length === 0 && ( | ||||
|               <div className="flex flex-col mx-auto lg:ml-auto"> | ||||
|                 {user.links.map((link) => ( | ||||
|                   <a | ||||
|  | @ -78,6 +95,19 @@ function UserPage() { | |||
|           {user.fields.map((field) => ( | ||||
|             <FieldCard field={field}></FieldCard> | ||||
|           ))} | ||||
|           {user.links.length !== 0 && ( | ||||
|             <Card title="Links"> | ||||
|               {user.links.map((link) => ( | ||||
|                 <a | ||||
|                   href={link} | ||||
|                   rel="nofollow noopener noreferrer me" | ||||
|                   className="hover:underline text-sky-500 dark:text-sky-400" | ||||
|                 > | ||||
|                   {link} | ||||
|                 </a> | ||||
|               ))} | ||||
|             </Card> | ||||
|           )} | ||||
|         </div> | ||||
|       </div> | ||||
|     </> | ||||
|  |  | |||
|  | @ -67,5 +67,9 @@ export default function Discord() { | |||
|     navigate("/"); | ||||
|   } | ||||
| 
 | ||||
|   if (user || state.isLoading) { | ||||
|     return <>Loading...</>; | ||||
|   } | ||||
| 
 | ||||
|   return <>wow such login</>; | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue