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)) | 	render.JSON(w, r, dbUserToResponse(u, fields)) | ||||||
| 	return nil | 	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} | 	s := &Server{srv} | ||||||
| 
 | 
 | ||||||
| 	r.Route("/users", func(r chi.Router) { | 	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)) | 		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, |   EmojiLaughing, | ||||||
| } from "react-bootstrap-icons"; | } from "react-bootstrap-icons"; | ||||||
| 
 | 
 | ||||||
|  | import Card from "./Card"; | ||||||
| import type { Field } from "./types"; | import type { Field } from "./types"; | ||||||
| 
 | 
 | ||||||
| export default function FieldCard({ field }: { field: Field }) { | export default function FieldCard({ field }: { field: Field }) { | ||||||
|   return ( |   return ( | ||||||
|     <div className=" bg-slate-100 dark:bg-slate-700 rounded-md shadow"> |     <Card title={field.name}> | ||||||
|       <h1 className="text-2xl p-2 border-b border-zinc-200 dark:border-slate-800">{field.name}</h1> |       {field.favourite.map((entry) => ( | ||||||
|       <div className="flex flex-col p-2"> |         <p className="text-lg font-bold"> | ||||||
|         {field.favourite.map((entry) => ( |           <HeartFill className="inline" /> {entry} | ||||||
|           <p className="text-lg font-bold"> |         </p> | ||||||
|             <HeartFill className="inline" /> {entry} |       ))} | ||||||
|           </p> |       {field.okay.length !== 0 && ( | ||||||
|         ))} |         <p> | ||||||
|         {field.okay.length !== 0 && ( |           <HandThumbsUp className="inline" /> {field.okay.join(", ")} | ||||||
|           <p> |         </p> | ||||||
|             <HandThumbsUp className="inline" /> {field.okay.join(", ")} |       )} | ||||||
|           </p> |       {field.jokingly.length !== 0 && ( | ||||||
|         )} |         <p> | ||||||
|         {field.jokingly.length !== 0 && ( |           <EmojiLaughing className="inline" /> {field.jokingly.join(", ")} | ||||||
|           <p> |         </p> | ||||||
|             <EmojiLaughing className="inline" /> {field.jokingly.join(", ")} |       )} | ||||||
|           </p> |       {field.friends_only.length !== 0 && ( | ||||||
|         )} |         <p> | ||||||
|         {field.friends_only.length !== 0 && ( |           <People className="inline" /> {field.friends_only.join(", ")} | ||||||
|           <p> |         </p> | ||||||
|             <People className="inline" /> {field.friends_only.join(", ")} |       )} | ||||||
|           </p> |       {field.avoid.length !== 0 && ( | ||||||
|         )} |         <p className="text-slate-600 dark:text-slate-400"> | ||||||
|         {field.avoid.length !== 0 && ( |           <HandThumbsDown className="inline" /> {field.avoid.join(", ")} | ||||||
|           <p className="text-slate-600 dark:text-slate-400"> |         </p> | ||||||
|             <HandThumbsDown className="inline" /> {field.avoid.join(", ")} |       )} | ||||||
|           </p> |     </Card> | ||||||
|         )} |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -45,7 +45,7 @@ function Navigation() { | ||||||
| 
 | 
 | ||||||
|   const nav = user ? ( |   const nav = user ? ( | ||||||
|     <> |     <> | ||||||
|       <NavItem to="/me">@{user.username}</NavItem> |       <NavItem to={`/u/${user.username}`}>@{user.username}</NavItem> | ||||||
|       <NavItem to="/settings">Settings</NavItem> |       <NavItem to="/settings">Settings</NavItem> | ||||||
|       <NavItem to="/logout">Log out</NavItem> |       <NavItem to="/logout">Log out</NavItem> | ||||||
|     </> |     </> | ||||||
|  |  | ||||||
|  | @ -27,3 +27,8 @@ async function getCurrentUser() { | ||||||
| 
 | 
 | ||||||
|   return null; |   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 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 { ArrowClockwise } from "react-bootstrap-icons"; | ||||||
| import ReactMarkdown from "react-markdown"; | import ReactMarkdown from "react-markdown"; | ||||||
| import { Helmet } from "react-helmet"; | 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 fetchAPI from "../lib/fetch"; | ||||||
| import FieldCard from "../lib/FieldCard"; | import FieldCard from "../lib/FieldCard"; | ||||||
|  | import Card from "../lib/Card"; | ||||||
|  | import { userState } from "../lib/store"; | ||||||
|  | import { useRecoilValue } from "recoil"; | ||||||
| 
 | 
 | ||||||
| function UserPage() { | function UserPage() { | ||||||
|   const params = useParams(); |   const params = useParams(); | ||||||
| 
 | 
 | ||||||
|   const [user, setUser] = useState<User>(null); |   const [user, setUser] = useState<User>(null); | ||||||
|  |   const meUser = useRecoilValue(userState); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     fetchAPI<User>(`/users/${params.username}`).then((res) => { |     fetchAPI<User>(`/users/${params.username}`).then((res) => { | ||||||
|  | @ -33,13 +38,25 @@ function UserPage() { | ||||||
|       <Helmet> |       <Helmet> | ||||||
|         <title>@{user.username} - pronouns.cc</title> |         <title>@{user.username} - pronouns.cc</title> | ||||||
|       </Helmet> |       </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="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 && ( |           {user.avatar_url && ( | ||||||
|             <img |             <img className="max-w-xs rounded-full" src={user.avatar_url} /> | ||||||
|               className="max-w-max lg:max-w-lg rounded-full" |  | ||||||
|               src={user.avatar_url} |  | ||||||
|             /> |  | ||||||
|           )} |           )} | ||||||
|           <div className="flex flex-col lg:mx-auto"> |           <div className="flex flex-col lg:mx-auto"> | ||||||
|             {user.display_name && ( |             {user.display_name && ( | ||||||
|  | @ -59,7 +76,7 @@ function UserPage() { | ||||||
|                 {user.bio} |                 {user.bio} | ||||||
|               </ReactMarkdown> |               </ReactMarkdown> | ||||||
|             )} |             )} | ||||||
|             {user.links.length !== 0 && ( |             {user.links.length !== 0 && user.fields.length === 0 && ( | ||||||
|               <div className="flex flex-col mx-auto lg:ml-auto"> |               <div className="flex flex-col mx-auto lg:ml-auto"> | ||||||
|                 {user.links.map((link) => ( |                 {user.links.map((link) => ( | ||||||
|                   <a |                   <a | ||||||
|  | @ -78,6 +95,19 @@ function UserPage() { | ||||||
|           {user.fields.map((field) => ( |           {user.fields.map((field) => ( | ||||||
|             <FieldCard field={field}></FieldCard> |             <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> | ||||||
|       </div> |       </div> | ||||||
|     </> |     </> | ||||||
|  |  | ||||||
|  | @ -67,5 +67,9 @@ export default function Discord() { | ||||||
|     navigate("/"); |     navigate("/"); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   if (user || state.isLoading) { | ||||||
|  |     return <>Loading...</>; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   return <>wow such login</>; |   return <>wow such login</>; | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue