Skip to content

Commit

Permalink
feat: add user profile page, add redirect from /me to own user profile
Browse files Browse the repository at this point in the history
  • Loading branch information
tomcur committed Jan 19, 2024
1 parent 1652cd2 commit 2c68047
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 164 deletions.
13 changes: 12 additions & 1 deletion astroplant-frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import "leaflet/dist/leaflet.css";
import "./App.css";

import { Navigate, Route, Routes } from "react-router-dom";
import { Navigate, Route, Routes, useParams } from "react-router-dom";

import ConnectionStatus from "./Components/ConnectionStatus";
import Footer from "./Components/Footer";
Expand All @@ -18,6 +18,7 @@ import LogIn from "./scenes/LogIn";
import Map from "./scenes/map";
import Me from "./scenes/Me";
import SignUp from "./scenes/SignUp";
import User from "./scenes/user";

export default function App() {
return (
Expand All @@ -35,6 +36,7 @@ export default function App() {
<Route path="/log-in" element={<LogIn />} />
<Route path="/sign-up" element={<SignUp />} />
<Route path="/me" element={<Me />} />
<Route path="/user/:username" element={<User_ />} />
<Route path="/create-kit" element={<CreateKit />} />
<Route path="/kit/:kitSerial/*" element={<Kit />} />
<Route path="*" element={<NotFound />} />
Expand All @@ -49,3 +51,12 @@ export default function App() {
</>
);
}

function User_() {
const { username } = useParams<{ username: string }>();
if (username) {
return <User username={username} />;
} else {
return <NotFound />;
}
}
165 changes: 2 additions & 163 deletions astroplant-frontend/src/scenes/Me/index.tsx
Original file line number Diff line number Diff line change
@@ -1,174 +1,13 @@
import { useState } from "react";
import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { IconEdit } from "@tabler/icons-react";
import clsx from "clsx";

import { Navigate } from "react-router-dom";
import {
withAuthentication,
WithAuthentication,
} from "~/Components/AuthenticatedGuard";
import Gravatar from "~/Components/Gravatar";
import HeadTitle from "~/Components/HeadTitle";
import PlaceholderSegment from "~/Components/PlaceholderSegment";
import { useAppSelector } from "~/hooks";
import { selectMe } from "~/modules/me/reducer";
import { kitSelectors } from "~/modules/kit/reducer";

import commonStyle from "~/Common.module.css";
import style from "./index.module.css";
import { KitAvatar } from "~/Components/KitAvatar";
import { Badge } from "~/Components/Badge";
import { Input } from "~/Components/Input";
import { Button } from "~/Components/Button";
import { rtkApi } from "~/services/astroplant";

type Props = WithAuthentication;

function Me({ me }: Props) {
const { t } = useTranslation();

const { kitMemberships, loadingKitMemberships } = useAppSelector(selectMe);
const kitStates = useAppSelector(kitSelectors.selectEntities);

const [submitting, setSubmitting] = useState(false);
const [editingProfile, setEditingProfile] = useState(false);
const [displayName, setDisplayName] = useState("");

const [patchUser] = rtkApi.usePatchUserMutation();

return (
<>
<HeadTitle
main={t("me.header", {
displayName: me.displayName,
})}
/>
<article
className={clsx(commonStyle.containerWide, style.wrapper)}
style={{ marginTop: "1em" }}
>
<section className={style.profile}>
<section className={style.avatar}>
<Gravatar
size={200}
identifier={
me.useEmailAddressForGravatar
? me.emailAddress
: me.gravatarAlternative
}
/>
</section>
{editingProfile ? (
<form
onSubmit={(e) => {
e.preventDefault();
}}
>
<fieldset disabled={submitting}>
<label>
Name
<Input
value={displayName}
onChange={(e) => setDisplayName(e.currentTarget.value)}
fullWidth
/>
</label>
<section style={{ marginTop: "0.5rem" }}>
<Button
type="submit"
variant="positive"
size="small"
onClick={async () => {
setSubmitting(true);
try {
await patchUser({
username: me.username,
patch: { displayName },
});
setEditingProfile(false);
// TODO: show errors
} finally {
setSubmitting(false);
}
}}
>
Save
</Button>
<Button
variant="muted"
size="small"
onClick={() => setEditingProfile(false)}
>
Cancel
</Button>
</section>
</fieldset>
</form>
) : (
<header>
<h1 className={style.displayName}>{me.displayName}</h1>
<span>{me.username}</span>
<section className={style.emailAddress}>
{me.emailAddress}
</section>
<Button
variant="muted"
size="small"
leftAdornment={<IconEdit />}
style={{ width: "100%", marginTop: "1rem" }}
onClick={() => {
setDisplayName(me.displayName);
setEditingProfile(true);
}}
>
Edit profile
</Button>
</header>
)}
</section>
<section className={style.kits}>
<h2>Your kits</h2>
{Object.keys(kitMemberships).length > 0 || loadingKitMemberships ? (
<>
<p>
<Link to="/create-kit">Create another.</Link>
</p>
<ul className={style.kitList}>
{Object.keys(kitMemberships).map((serial) => {
const kitState = kitStates[serial];
return (
<li key={serial}>
<KitAvatar serial={serial} fontSize="1.25rem" />
<div>
<header className={style.itemHeader}>
<Link to={`/kit/${serial}`}>
<h3>
{kitState?.details?.name ?? t("kit.unnamed")}
</h3>
</Link>
{kitState?.details?.privacyPublicDashboard && (
<Badge variant="muted" size="small" text="Public" />
)}
</header>
<p>{serial}</p>
</div>
</li>
);
})}
</ul>
{loadingKitMemberships && <PlaceholderSegment />}
</>
) : (
<p>
You have no kits yet.{" "}
<Link to="/create-kit">You can create one!</Link>
</p>
)}
</section>
</article>
</>
);
return <Navigate to={`/user/${me.username}`} />;
}

export default withAuthentication()(Me);
Loading

0 comments on commit 2c68047

Please sign in to comment.