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…
Reference in a new issue