feat: frontend layout skeleton
This commit is contained in:
parent
2e4b8b9823
commit
9c5a9a72d0
20 changed files with 1401 additions and 87 deletions
5
frontend/src/lib/Container.tsx
Normal file
5
frontend/src/lib/Container.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import React from "react";
|
||||
|
||||
export default function Container(props: React.PropsWithChildren<{}>) {
|
||||
return <div className="m-2 lg:m-4">{props.children}</div>;
|
||||
}
|
91
frontend/src/lib/Navigation.tsx
Normal file
91
frontend/src/lib/Navigation.tsx
Normal file
|
@ -0,0 +1,91 @@
|
|||
import Logo from "./logo";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { MoonStars, Sun, List } from "react-bootstrap-icons";
|
||||
|
||||
function Navigation() {
|
||||
const [darkTheme, setDarkTheme] = useState<boolean>(
|
||||
localStorage.theme === "dark" ||
|
||||
(!("theme" in localStorage) &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
);
|
||||
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
|
||||
if (darkTheme) {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
|
||||
const storeTheme = (system: boolean) => {
|
||||
if (system) {
|
||||
localStorage.removeItem("theme");
|
||||
} else {
|
||||
localStorage.setItem("theme", darkTheme ? "dark" : "light");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white/75 dark:bg-slate-800/75 w-full backdrop-blur border-b-slate-200 dark:border-b-slate-900">
|
||||
<div className="max-w-8xl mx-auto">
|
||||
<div className="py-4 mx-4">
|
||||
<div className="flex items-center">
|
||||
<Link to="/">
|
||||
<Logo />
|
||||
</Link>
|
||||
<div className="ml-auto flex items-center">
|
||||
<nav className="hidden lg:flex">
|
||||
<ul className="flex space-x-8 font-bold">
|
||||
<li>
|
||||
<Link
|
||||
className="hover:text-sky-500 dark:hover:text-sky-400"
|
||||
to="/login"
|
||||
>
|
||||
Log in
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="flex border-l border-slate-200 ml-4 pl-4 lg:ml-6 lg:pl-6 lg:mr-2 dark:border-slate-500 space-x-2 lg:space-x-4">
|
||||
<div
|
||||
onClick={() => {
|
||||
storeTheme(false);
|
||||
setDarkTheme(!darkTheme);
|
||||
}}
|
||||
title={
|
||||
darkTheme ? "Switch to light mode" : "Switch to dark mode"
|
||||
}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{darkTheme ? (
|
||||
<Sun className="hover:text-sky-400" size={24} />
|
||||
) : (
|
||||
<MoonStars size={24} className="hover:text-sky-500" />
|
||||
)}
|
||||
</div>
|
||||
<div onClick={() => setShowMenu(!showMenu)} title="Show menu" className="cursor-pointer flex lg:hidden">
|
||||
<List className="dark:hover:text-sky-400 hover:text-sky-500" size={24} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav className={`lg:hidden my-2 p-4 border-slate-200 dark:border-slate-500 border-t border-b ${showMenu ? "flex" : "hidden"}`}>
|
||||
<ul className="flex space-x-8 font-bold">
|
||||
<li>
|
||||
<Link
|
||||
className="hover:text-sky-500 dark:hover:text-sky-400"
|
||||
to="/login"
|
||||
>
|
||||
Log in
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Navigation;
|
16
frontend/src/lib/logo.svg
Normal file
16
frontend/src/lib/logo.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 11 KiB |
45
frontend/src/lib/logo.tsx
Normal file
45
frontend/src/lib/logo.tsx
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,20 +1,26 @@
|
|||
import axios from "axios";
|
||||
import { atom } from "recoil";
|
||||
import type { APIError, MeUser } from "./types";
|
||||
import { atom, useRecoilState, useRecoilValue } from "recoil";
|
||||
import { APIError, ErrorCode, MeUser } from "./types";
|
||||
|
||||
export const userState = atom<MeUser | null>({
|
||||
export const userState = atom<MeUser>({
|
||||
key: "userState",
|
||||
default: getCurrentUser(),
|
||||
});
|
||||
|
||||
async function getCurrentUser(): Promise<MeUser | null> {
|
||||
async function getCurrentUser() {
|
||||
const token = localStorage.getItem("pronouns-token");
|
||||
if (!token) return null;
|
||||
|
||||
try {
|
||||
const resp = await axios.get<MeUser | APIError>("/api/v1/users/@me");
|
||||
if ("id" in resp.data) return resp.data as MeUser;
|
||||
return null;
|
||||
if (resp.status === 200) {
|
||||
return resp.data as MeUser;
|
||||
}
|
||||
|
||||
// if we got a forbidden error, the token is invalid
|
||||
if ((resp.data as APIError).code === ErrorCode.Forbidden) {
|
||||
localStorage.removeItem("pronouns-token");
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Error fetching /users/@me:", e);
|
||||
}
|
||||
|
|
|
@ -10,6 +10,22 @@ export interface MeUser {
|
|||
discord_username: string | null;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
display_name: string | null;
|
||||
bio: string | null;
|
||||
avatar_source: string | null;
|
||||
links: string[] | null;
|
||||
members: PartialMember[];
|
||||
}
|
||||
|
||||
export interface PartialMember {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar_url: string | null;
|
||||
}
|
||||
|
||||
export interface APIError {
|
||||
code: ErrorCode;
|
||||
message?: string;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue