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