feat(frontend): incomplete port to next.js
This commit is contained in:
		
							parent
							
								
									b9c30379ee
								
							
						
					
					
						commit
						eec01dc070
					
				
					 50 changed files with 2874 additions and 3163 deletions
				
			
		
							
								
								
									
										20
									
								
								Makefile
									
										
									
									
									
								
							
							
						
						
									
										20
									
								
								Makefile
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,23 +1,7 @@
 | 
			
		|||
.PHONY: all
 | 
			
		||||
all: frontend css backend
 | 
			
		||||
	mv api pronouns
 | 
			
		||||
 | 
			
		||||
.PHONY: migrate
 | 
			
		||||
migrate:
 | 
			
		||||
	go run -v ./scripts/migrate
 | 
			
		||||
 | 
			
		||||
.PHONY: backend
 | 
			
		||||
backend: css
 | 
			
		||||
	CGO_ENABLED=0 go build -v -o api -ldflags="-buildid= -X codeberg.org/u1f320/pronouns.cc/backend/server.Revision=`git rev-parse --short HEAD`" ./backend
 | 
			
		||||
 | 
			
		||||
.PHONY: frontend
 | 
			
		||||
frontend:
 | 
			
		||||
	yarn build
 | 
			
		||||
 | 
			
		||||
.PHONY: css
 | 
			
		||||
css:
 | 
			
		||||
	yarn tailwindcss -m -o frontend/style.css
 | 
			
		||||
 | 
			
		||||
.PHONY: dev
 | 
			
		||||
dev:
 | 
			
		||||
	yarn dev
 | 
			
		||||
backend:
 | 
			
		||||
	CGO_ENABLED=0 go build -v -o pronouns -ldflags="-buildid= -X codeberg.org/u1f320/pronouns.cc/backend/server.Revision=`git rev-parse --short HEAD`" ./backend
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										3
									
								
								frontend/.eslintrc.json
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										3
									
								
								frontend/.eslintrc.json
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
{
 | 
			
		||||
  "extends": "next/core-web-vitals"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										35
									
								
								frontend/.gitignore
									
										
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										35
									
								
								frontend/.gitignore
									
										
									
									
										vendored
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,35 @@
 | 
			
		|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 | 
			
		||||
 | 
			
		||||
# dependencies
 | 
			
		||||
/node_modules
 | 
			
		||||
/.pnp
 | 
			
		||||
.pnp.js
 | 
			
		||||
 | 
			
		||||
# testing
 | 
			
		||||
/coverage
 | 
			
		||||
 | 
			
		||||
# next.js
 | 
			
		||||
/.next/
 | 
			
		||||
/out/
 | 
			
		||||
 | 
			
		||||
# production
 | 
			
		||||
/build
 | 
			
		||||
 | 
			
		||||
# misc
 | 
			
		||||
.DS_Store
 | 
			
		||||
*.pem
 | 
			
		||||
 | 
			
		||||
# debug
 | 
			
		||||
npm-debug.log*
 | 
			
		||||
yarn-debug.log*
 | 
			
		||||
yarn-error.log*
 | 
			
		||||
.pnpm-debug.log*
 | 
			
		||||
 | 
			
		||||
# local env files
 | 
			
		||||
.env*.local
 | 
			
		||||
 | 
			
		||||
# vercel
 | 
			
		||||
.vercel
 | 
			
		||||
 | 
			
		||||
# typescript
 | 
			
		||||
*.tsbuildinfo
 | 
			
		||||
							
								
								
									
										34
									
								
								frontend/README.md
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										34
									
								
								frontend/README.md
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,34 @@
 | 
			
		|||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
 | 
			
		||||
 | 
			
		||||
## Getting Started
 | 
			
		||||
 | 
			
		||||
First, run the development server:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
npm run dev
 | 
			
		||||
# or
 | 
			
		||||
yarn dev
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
 | 
			
		||||
 | 
			
		||||
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
 | 
			
		||||
 | 
			
		||||
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
 | 
			
		||||
 | 
			
		||||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
 | 
			
		||||
 | 
			
		||||
## Learn More
 | 
			
		||||
 | 
			
		||||
To learn more about Next.js, take a look at the following resources:
 | 
			
		||||
 | 
			
		||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
 | 
			
		||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
 | 
			
		||||
 | 
			
		||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
 | 
			
		||||
 | 
			
		||||
## Deploy on Vercel
 | 
			
		||||
 | 
			
		||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
 | 
			
		||||
 | 
			
		||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
 | 
			
		||||
							
								
								
									
										24
									
								
								frontend/components/NavItem.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								frontend/components/NavItem.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
import { ReactNode } from "react";
 | 
			
		||||
import Link from "next/link";
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  children?: ReactNode | undefined;
 | 
			
		||||
  href: string;
 | 
			
		||||
  plain?: boolean | undefined; // Do not wrap in <li></li>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function NavItem(props: Props) {
 | 
			
		||||
  const ret = (
 | 
			
		||||
    <Link
 | 
			
		||||
      className="hover:text-sky-500 dark:hover:text-sky-400"
 | 
			
		||||
      href={props.href}
 | 
			
		||||
    >
 | 
			
		||||
      {props.children}
 | 
			
		||||
    </Link>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (props.plain) {
 | 
			
		||||
    return ret;
 | 
			
		||||
  }
 | 
			
		||||
  return <li>{ret}</li>;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,15 +1,15 @@
 | 
			
		|||
import { useEffect, useState } from "react";
 | 
			
		||||
import { Link } from "react-router-dom";
 | 
			
		||||
import { MoonStars, Sun, List } from "react-bootstrap-icons";
 | 
			
		||||
 | 
			
		||||
import Link from "next/link";
 | 
			
		||||
import NavItem from "./NavItem";
 | 
			
		||||
import Logo from "./logo";
 | 
			
		||||
import { useRecoilState } from "recoil";
 | 
			
		||||
import { userState } from "./store";
 | 
			
		||||
import fetchAPI from "./fetch";
 | 
			
		||||
import { APIError, ErrorCode, MeUser } from "./types";
 | 
			
		||||
import { userState } from "../lib/state";
 | 
			
		||||
import fetchAPI from "../lib/fetch";
 | 
			
		||||
import { APIError, ErrorCode, MeUser } from "../lib/types";
 | 
			
		||||
 | 
			
		||||
function Navigation() {
 | 
			
		||||
export default function Navigation() {
 | 
			
		||||
  const [user, setUser] = useRecoilState(userState);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -27,21 +27,24 @@ function Navigation() {
 | 
			
		|||
        }
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const [darkTheme, setDarkTheme] = useState<boolean>(
 | 
			
		||||
    localStorage.theme === "dark" ||
 | 
			
		||||
      (!("theme" in localStorage) &&
 | 
			
		||||
        window.matchMedia("(prefers-color-scheme: dark)").matches)
 | 
			
		||||
  );
 | 
			
		||||
  }, [user, setUser]);
 | 
			
		||||
 | 
			
		||||
  const [darkTheme, setDarkTheme] = useState<boolean>(false);
 | 
			
		||||
  const [showMenu, setShowMenu] = useState(false);
 | 
			
		||||
 | 
			
		||||
  if (darkTheme) {
 | 
			
		||||
    document.documentElement.classList.add("dark");
 | 
			
		||||
  } else {
 | 
			
		||||
    document.documentElement.classList.remove("dark");
 | 
			
		||||
  }
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setDarkTheme(
 | 
			
		||||
      localStorage.theme === "dark" ||
 | 
			
		||||
        (!("theme" in localStorage) &&
 | 
			
		||||
          window.matchMedia("(prefers-color-scheme: dark)").matches)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (darkTheme) {
 | 
			
		||||
      document.documentElement.classList.add("dark");
 | 
			
		||||
    } else {
 | 
			
		||||
      document.documentElement.classList.remove("dark");
 | 
			
		||||
    }
 | 
			
		||||
  }, [darkTheme]);
 | 
			
		||||
 | 
			
		||||
  const storeTheme = (useDarkTheme: boolean | null) => {
 | 
			
		||||
    if (useDarkTheme === null) {
 | 
			
		||||
| 
						 | 
				
			
			@ -53,13 +56,13 @@ function Navigation() {
 | 
			
		|||
 | 
			
		||||
  const nav = user ? (
 | 
			
		||||
    <>
 | 
			
		||||
      <NavItem to={`/u/${user.username}`}>@{user.username}</NavItem>
 | 
			
		||||
      <NavItem to="/settings">Settings</NavItem>
 | 
			
		||||
      <NavItem to="/logout">Log out</NavItem>
 | 
			
		||||
      <NavItem href={`/u/${user.username}`}>@{user.username}</NavItem>
 | 
			
		||||
      <NavItem href="/settings">Settings</NavItem>
 | 
			
		||||
      <NavItem href="/logout">Log out</NavItem>
 | 
			
		||||
    </>
 | 
			
		||||
  ) : (
 | 
			
		||||
    <>
 | 
			
		||||
      <NavItem to="/login">Log in</NavItem>
 | 
			
		||||
      <NavItem href="/login">Log in</NavItem>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -69,7 +72,7 @@ function Navigation() {
 | 
			
		|||
        <div className="max-w-8xl mx-auto">
 | 
			
		||||
          <div className="py-4 mx-4">
 | 
			
		||||
            <div className="flex items-center">
 | 
			
		||||
              <Link to="/">
 | 
			
		||||
              <Link href="/">
 | 
			
		||||
                <Logo />
 | 
			
		||||
              </Link>
 | 
			
		||||
              <div className="ml-auto flex items-center">
 | 
			
		||||
| 
						 | 
				
			
			@ -119,5 +122,3 @@ function Navigation() {
 | 
			
		|||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Navigation;
 | 
			
		||||
| 
						 | 
				
			
			@ -17,7 +17,7 @@ function Logo() {
 | 
			
		|||
      <g transform="translate(-49.754 -142.45)">
 | 
			
		||||
        <g
 | 
			
		||||
          transform="matrix(.33073 0 0 -.33073 50.093 154.62)"
 | 
			
		||||
          clip-path="url(#clipPath16)"
 | 
			
		||||
          clipPath="url(#clipPath16)"
 | 
			
		||||
        >
 | 
			
		||||
          <path
 | 
			
		||||
            d="m35.347 20.107-8.899 3.294-3.323 10.891c-0.128 0.42-0.516 0.708-0.956 0.708-0.439 0-0.828-0.288-0.956-0.708l-3.322-10.891-8.9-3.294c-0.393-0.146-0.653-0.52-0.653-0.938s0.26-0.793 0.653-0.937l8.896-3.293 3.323-11.223c0.126-0.425 0.516-0.716 0.959-0.716s0.833 0.291 0.959 0.716l3.324 11.223 8.896 3.293c0.392 0.144 0.652 0.519 0.652 0.937s-0.26 0.792-0.653 0.938"
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +0,0 @@
 | 
			
		|||
package frontend
 | 
			
		||||
 | 
			
		||||
import "embed"
 | 
			
		||||
 | 
			
		||||
//go:embed dist/*
 | 
			
		||||
var Data embed.FS
 | 
			
		||||
 | 
			
		||||
//go:embed style.css
 | 
			
		||||
var CSS []byte
 | 
			
		||||
| 
						 | 
				
			
			@ -1,13 +0,0 @@
 | 
			
		|||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="UTF-8" />
 | 
			
		||||
    <link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
			
		||||
    <title>pronouns.cc</title>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body class="bg-white dark:bg-slate-800 text-black dark:text-white">
 | 
			
		||||
    <div id="root"></div>
 | 
			
		||||
    <script type="module" src="/src/main.tsx"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,3 @@
 | 
			
		|||
import axios from "axios";
 | 
			
		||||
import type { APIError } from "./types";
 | 
			
		||||
 | 
			
		||||
export default async function fetchAPI<T>(
 | 
			
		||||
							
								
								
									
										7
									
								
								frontend/lib/state.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								frontend/lib/state.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
import { atom } from "recoil";
 | 
			
		||||
import { MeUser } from "./types";
 | 
			
		||||
 | 
			
		||||
export const userState = atom<MeUser | null>({
 | 
			
		||||
  key: "userState",
 | 
			
		||||
  default: null,
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										5
									
								
								frontend/next-env.d.ts
									
										
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										5
									
								
								frontend/next-env.d.ts
									
										
									
									
										vendored
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
/// <reference types="next" />
 | 
			
		||||
/// <reference types="next/image-types/global" />
 | 
			
		||||
 | 
			
		||||
// NOTE: This file should not be edited
 | 
			
		||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
 | 
			
		||||
							
								
								
									
										7
									
								
								frontend/next.config.js
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										7
									
								
								frontend/next.config.js
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
/** @type {import('next').NextConfig} */
 | 
			
		||||
const nextConfig = {
 | 
			
		||||
  reactStrictMode: true,
 | 
			
		||||
  swcMinify: true,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = nextConfig
 | 
			
		||||
							
								
								
									
										34
									
								
								frontend/package.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								frontend/package.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,34 @@
 | 
			
		|||
{
 | 
			
		||||
  "name": "pronouns",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "next dev",
 | 
			
		||||
    "build": "next build",
 | 
			
		||||
    "start": "next start",
 | 
			
		||||
    "lint": "next lint"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "next": "12.2.2",
 | 
			
		||||
    "react": "18.2.0",
 | 
			
		||||
    "react-bootstrap-icons": "^1.8.4",
 | 
			
		||||
    "react-dom": "18.2.0",
 | 
			
		||||
    "react-markdown": "^8.0.3",
 | 
			
		||||
    "react-sortablejs": "^6.1.4",
 | 
			
		||||
    "recoil": "^0.7.5",
 | 
			
		||||
    "sortablejs": "^1.15.0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@tailwindcss/forms": "^0.5.2",
 | 
			
		||||
    "@tailwindcss/typography": "^0.5.3",
 | 
			
		||||
    "@types/node": "18.0.3",
 | 
			
		||||
    "@types/react": "18.0.15",
 | 
			
		||||
    "@types/react-dom": "18.0.6",
 | 
			
		||||
    "@types/sortablejs": "^1.13.0",
 | 
			
		||||
    "autoprefixer": "^10.4.7",
 | 
			
		||||
    "eslint": "8.19.0",
 | 
			
		||||
    "eslint-config-next": "12.2.2",
 | 
			
		||||
    "postcss": "^8.4.14",
 | 
			
		||||
    "tailwindcss": "^3.1.6",
 | 
			
		||||
    "typescript": "4.7.4"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								frontend/pages/_app.tsx
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										18
									
								
								frontend/pages/_app.tsx
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
import "../styles/globals.css";
 | 
			
		||||
import type { AppProps } from "next/app";
 | 
			
		||||
import Container from "../components/Container";
 | 
			
		||||
import Navigation from "../components/Navigation";
 | 
			
		||||
import { RecoilRoot } from "recoil";
 | 
			
		||||
 | 
			
		||||
function MyApp({ Component, pageProps }: AppProps) {
 | 
			
		||||
  return (
 | 
			
		||||
    <RecoilRoot>
 | 
			
		||||
      <Navigation />
 | 
			
		||||
      <Container>
 | 
			
		||||
        <Component {...pageProps} />
 | 
			
		||||
      </Container>
 | 
			
		||||
    </RecoilRoot>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default MyApp;
 | 
			
		||||
							
								
								
									
										13
									
								
								frontend/pages/_document.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								frontend/pages/_document.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
import { Html, Head, Main, NextScript } from "next/document";
 | 
			
		||||
 | 
			
		||||
export default function Document() {
 | 
			
		||||
  return (
 | 
			
		||||
    <Html>
 | 
			
		||||
      <Head />
 | 
			
		||||
      <body className="bg-white dark:bg-slate-800 text-black dark:text-white">
 | 
			
		||||
        <Main />
 | 
			
		||||
        <NextScript />
 | 
			
		||||
      </body>
 | 
			
		||||
    </Html>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								frontend/pages/api/hello.ts
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										13
									
								
								frontend/pages/api/hello.ts
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
 | 
			
		||||
import type { NextApiRequest, NextApiResponse } from 'next'
 | 
			
		||||
 | 
			
		||||
type Data = {
 | 
			
		||||
  name: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function handler(
 | 
			
		||||
  req: NextApiRequest,
 | 
			
		||||
  res: NextApiResponse<Data>
 | 
			
		||||
) {
 | 
			
		||||
  res.status(200).json({ name: 'John Doe' })
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										0
									
								
								frontend/src/pages/Home.tsx → frontend/pages/index.tsx
									
										
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								frontend/src/pages/Home.tsx → frontend/pages/index.tsx
									
										
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										8
									
								
								frontend/postcss.config.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								frontend/postcss.config.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
// If you want to use other PostCSS plugins, see the following:
 | 
			
		||||
// https://tailwindcss.com/docs/using-with-preprocessors
 | 
			
		||||
module.exports = {
 | 
			
		||||
  plugins: {
 | 
			
		||||
    tailwindcss: {},
 | 
			
		||||
    autoprefixer: {},
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								frontend/public/favicon.ico
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								frontend/public/favicon.ico
									
										
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 25 KiB  | 
							
								
								
									
										4
									
								
								frontend/public/vercel.svg
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										4
									
								
								frontend/public/vercel.svg
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
<svg width="283" height="64" viewBox="0 0 283 64" fill="none" 
 | 
			
		||||
    xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
    <path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.1 KiB  | 
| 
						 | 
				
			
			@ -1,42 +0,0 @@
 | 
			
		|||
.App {
 | 
			
		||||
  text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.App-logo {
 | 
			
		||||
  height: 40vmin;
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (prefers-reduced-motion: no-preference) {
 | 
			
		||||
  .App-logo {
 | 
			
		||||
    animation: App-logo-spin infinite 20s linear;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.App-header {
 | 
			
		||||
  background-color: #282c34;
 | 
			
		||||
  min-height: 100vh;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  font-size: calc(10px + 2vmin);
 | 
			
		||||
  color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.App-link {
 | 
			
		||||
  color: #61dafb;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes App-logo-spin {
 | 
			
		||||
  from {
 | 
			
		||||
    transform: rotate(0deg);
 | 
			
		||||
  }
 | 
			
		||||
  to {
 | 
			
		||||
    transform: rotate(360deg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button {
 | 
			
		||||
  font-size: calc(10px + 2vmin);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,30 +0,0 @@
 | 
			
		|||
import { Routes, Route } from "react-router-dom";
 | 
			
		||||
import "./App.css";
 | 
			
		||||
import Container from "./lib/Container";
 | 
			
		||||
import Navigation from "./lib/Navigation";
 | 
			
		||||
import EditMe from "./pages/EditMe";
 | 
			
		||||
import Home from "./pages/Home";
 | 
			
		||||
import Discord from "./pages/login/Discord";
 | 
			
		||||
import Login from "./pages/login/Login";
 | 
			
		||||
import User from "./pages/User";
 | 
			
		||||
 | 
			
		||||
function App() {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Navigation />
 | 
			
		||||
      <Container>
 | 
			
		||||
        <Routes>
 | 
			
		||||
          <Route path="/" element={<Home />} />
 | 
			
		||||
          <Route path="/u/:username" element={<User />} />
 | 
			
		||||
          <Route path="/u/:username/:member" element={<User />} />
 | 
			
		||||
          <Route path="/edit" element={<EditMe />} />
 | 
			
		||||
          <Route path="/edit/:member" element={<EditMe />} />
 | 
			
		||||
          <Route path="/login" element={<Login />} />
 | 
			
		||||
          <Route path="/login/discord" element={<Discord />} />
 | 
			
		||||
        </Routes>
 | 
			
		||||
      </Container>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default App;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,2 +0,0 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 47.5 47.5" style="enable-background:new 0 0 47.5 47.5;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,38 38,38 38,0 0,0 0,38 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,47.5)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(35.3467,20.1069)" id="g20"><path id="path22" style="fill:#aa8ed6;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 -8.899,3.294 -3.323,10.891 c -0.128,0.42 -0.516,0.708 -0.956,0.708 -0.439,0 -0.828,-0.288 -0.956,-0.708 L -17.456,3.294 -26.356,0 c -0.393,-0.146 -0.653,-0.52 -0.653,-0.938 0,-0.418 0.26,-0.793 0.653,-0.937 l 8.896,-3.293 3.323,-11.223 c 0.126,-0.425 0.516,-0.716 0.959,-0.716 0.443,0 0.833,0.291 0.959,0.716 l 3.324,11.223 8.896,3.293 c 0.392,0.144 0.652,0.519 0.652,0.937 C 0.653,-0.52 0.393,-0.146 0,0"/></g><g transform="translate(15.3472,9.1064)" id="g24"><path id="path26" style="fill:#fcab40;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 -2.313,0.856 -0.9,3.3 c -0.119,0.436 -0.514,0.738 -0.965,0.738 -0.451,0 -0.846,-0.302 -0.965,-0.738 l -0.9,-3.3 L -8.356,0 c -0.393,-0.145 -0.653,-0.52 -0.653,-0.937 0,-0.418 0.26,-0.793 0.653,-0.938 l 2.301,-0.853 0.907,-3.622 c 0.111,-0.444 0.511,-0.756 0.97,-0.756 0.458,0 0.858,0.312 0.97,0.756 L -2.301,-2.728 0,-1.875 c 0.393,0.145 0.653,0.52 0.653,0.938 C 0.653,-0.52 0.393,-0.145 0,0"/></g><g transform="translate(11.0093,30.769)" id="g28"><path id="path30" style="fill:#5dadec;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 -2.365,0.875 -3.24,3.24 c -0.146,0.393 -0.52,0.653 -0.938,0.653 -0.419,0 -0.793,-0.26 -0.938,-0.653 L -5.992,0.875 -8.356,0 c -0.393,-0.146 -0.653,-0.52 -0.653,-0.938 0,-0.418 0.26,-0.792 0.653,-0.938 l 2.364,-0.875 0.876,-2.365 c 0.145,-0.393 0.519,-0.653 0.938,-0.653 0.418,0 0.792,0.26 0.938,0.653 L -2.365,-2.751 0,-1.876 c 0.393,0.146 0.653,0.52 0.653,0.938 C 0.653,-0.52 0.393,-0.146 0,0"/></g></g></g></g></svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 2.4 KiB  | 
| 
						 | 
				
			
			@ -1,14 +0,0 @@
 | 
			
		|||
import { Link } from "react-router-dom";
 | 
			
		||||
 | 
			
		||||
export type Props = {
 | 
			
		||||
  to: string;
 | 
			
		||||
  children?: React.ReactNode;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default function BlueLink({ to, children }: Props) {
 | 
			
		||||
  return (
 | 
			
		||||
    <Link to={to} className="hover:underline text-sky-500 dark:text-sky-400">
 | 
			
		||||
      {children}
 | 
			
		||||
    </Link>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,28 +0,0 @@
 | 
			
		|||
import React, { ReactNode } from "react";
 | 
			
		||||
 | 
			
		||||
export type Props = {
 | 
			
		||||
  children?: ReactNode | undefined;
 | 
			
		||||
  title: string;
 | 
			
		||||
  draggable?: boolean;
 | 
			
		||||
  footer?: ReactNode | undefined;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default function Card({ title, draggable, children, footer }: 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${
 | 
			
		||||
          draggable && " handle hover:cursor-grab"
 | 
			
		||||
        }`}
 | 
			
		||||
      >
 | 
			
		||||
        {title}
 | 
			
		||||
      </h1>
 | 
			
		||||
      <div className="flex flex-col p-2">{children}</div>
 | 
			
		||||
      {footer && (
 | 
			
		||||
        <div className="p-2 border-t border-zinc-200 dark:border-slate-800">
 | 
			
		||||
          {footer}
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,62 +0,0 @@
 | 
			
		|||
import {
 | 
			
		||||
  HeartFill,
 | 
			
		||||
  HandThumbsUp,
 | 
			
		||||
  HandThumbsDown,
 | 
			
		||||
  People,
 | 
			
		||||
  EmojiLaughing,
 | 
			
		||||
} from "react-bootstrap-icons";
 | 
			
		||||
import BlueLink from "./BlueLink";
 | 
			
		||||
 | 
			
		||||
import Card from "./Card";
 | 
			
		||||
import type { Field } from "./types";
 | 
			
		||||
 | 
			
		||||
function linkPronoun(input: string) {
 | 
			
		||||
  if (input.includes(" ") || input.split("/").length !== 5)
 | 
			
		||||
    return <span>{input}</span>;
 | 
			
		||||
 | 
			
		||||
  const [sub, obj, possDet, possPro, reflexive] = input.split("/");
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <BlueLink to={`/pronouns/${sub}/${obj}/${possDet}/${possPro}/${reflexive}`}>
 | 
			
		||||
      {sub}/{obj}/{possDet}
 | 
			
		||||
    </BlueLink>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function FieldCard({
 | 
			
		||||
  field,
 | 
			
		||||
  draggable,
 | 
			
		||||
}: {
 | 
			
		||||
  field: Field;
 | 
			
		||||
  draggable?: boolean;
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <Card title={field.name} draggable={draggable}>
 | 
			
		||||
      {field.favourite.map((entry) => (
 | 
			
		||||
        <p className="text-lg font-bold">
 | 
			
		||||
          <HeartFill className="inline" /> {linkPronoun(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>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,10 +0,0 @@
 | 
			
		|||
import { ThreeDots } from "react-bootstrap-icons";
 | 
			
		||||
 | 
			
		||||
export default function Loading() {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex flex-col pt-32 items-center">
 | 
			
		||||
      <ThreeDots size={64} className="animate-bounce" aria-hidden="true" />
 | 
			
		||||
      <span className="font-bold text-xl">Loading...</span>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,22 +0,0 @@
 | 
			
		|||
import { ReactNode, PropsWithChildren } from "react";
 | 
			
		||||
import { Link } from "react-router-dom";
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
    children?: ReactNode | undefined;
 | 
			
		||||
    to: string;
 | 
			
		||||
    plain?: boolean | undefined; // Do not wrap in <li></li>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function NavItem(props: Props) {
 | 
			
		||||
    const ret = <Link
 | 
			
		||||
        className="hover:text-sky-500 dark:hover:text-sky-400"
 | 
			
		||||
        to={props.to}
 | 
			
		||||
    >
 | 
			
		||||
        {props.children}
 | 
			
		||||
    </Link>
 | 
			
		||||
 | 
			
		||||
    if (props.plain) {
 | 
			
		||||
        return ret
 | 
			
		||||
    }
 | 
			
		||||
    return <li>{ret}</li>;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,19 +0,0 @@
 | 
			
		|||
import { ChangeEventHandler } from "react";
 | 
			
		||||
 | 
			
		||||
export type Props = {
 | 
			
		||||
  defaultValue?: string;
 | 
			
		||||
  value?: string;
 | 
			
		||||
  onChange?: ChangeEventHandler<HTMLInputElement>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default function TextInput(props: Props) {
 | 
			
		||||
  return (
 | 
			
		||||
    <input
 | 
			
		||||
      type="text"
 | 
			
		||||
      className="p-1 lg:p-2 rounded-md bg-white border-slate-300 text-black dark:bg-slate-800 dark:border-slate-900 dark:text-white"
 | 
			
		||||
      defaultValue={props.defaultValue}
 | 
			
		||||
      value={props.value}
 | 
			
		||||
      onChange={props.onChange}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
		 Before Width: | Height: | Size: 11 KiB  | 
| 
						 | 
				
			
			@ -1,34 +0,0 @@
 | 
			
		|||
import axios from "axios";
 | 
			
		||||
import { atom, useRecoilState, useRecoilValue } from "recoil";
 | 
			
		||||
import fetchAPI from "./fetch";
 | 
			
		||||
import { APIError, ErrorCode, MeUser } from "./types";
 | 
			
		||||
 | 
			
		||||
export const userState = atom<MeUser>({
 | 
			
		||||
  key: "userState",
 | 
			
		||||
  default: getCurrentUser(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function getCurrentUser() {
 | 
			
		||||
  const token = localStorage.getItem("pronouns-token");
 | 
			
		||||
  if (!token) return null;
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    return await fetchAPI<MeUser>("/users/@me");
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    if (
 | 
			
		||||
      (e as APIError).code === ErrorCode.Forbidden ||
 | 
			
		||||
      (e as APIError).code === ErrorCode.InvalidToken
 | 
			
		||||
    ) {
 | 
			
		||||
      localStorage.removeItem("pronouns-token");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.log("Error fetching /users/@me:", e);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isMeUser(id: string): boolean {
 | 
			
		||||
  const meUser = useRecoilValue(userState);
 | 
			
		||||
  return meUser && meUser.id === id;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,26 +0,0 @@
 | 
			
		|||
import React from "react";
 | 
			
		||||
import ReactDOM from "react-dom/client";
 | 
			
		||||
import { BrowserRouter } from "react-router-dom";
 | 
			
		||||
import { RecoilRoot } from "recoil";
 | 
			
		||||
import * as Sentry from "@sentry/react";
 | 
			
		||||
import { BrowserTracing } from "@sentry/tracing";
 | 
			
		||||
 | 
			
		||||
import App from "./App";
 | 
			
		||||
import "./index.css";
 | 
			
		||||
 | 
			
		||||
if (import.meta.env.VITE_SENTRY_DSN) {
 | 
			
		||||
  Sentry.init({
 | 
			
		||||
    dsn: import.meta.env.VITE_SENTRY_DSN,
 | 
			
		||||
    integrations: [new BrowserTracing()],
 | 
			
		||||
 | 
			
		||||
    tracesSampleRate: 1.0,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ReactDOM.createRoot(document.getElementById("root")!).render(
 | 
			
		||||
  <RecoilRoot>
 | 
			
		||||
    <BrowserRouter>
 | 
			
		||||
      <App />
 | 
			
		||||
    </BrowserRouter>
 | 
			
		||||
  </RecoilRoot>
 | 
			
		||||
);
 | 
			
		||||
| 
						 | 
				
			
			@ -1,295 +0,0 @@
 | 
			
		|||
import cloneDeep from "lodash/cloneDeep";
 | 
			
		||||
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import { useNavigate } from "react-router-dom";
 | 
			
		||||
import { ReactSortable } from "react-sortablejs";
 | 
			
		||||
import { useRecoilValue } from "recoil";
 | 
			
		||||
import {
 | 
			
		||||
  EmojiLaughing,
 | 
			
		||||
  HandThumbsDown,
 | 
			
		||||
  HandThumbsUp,
 | 
			
		||||
  Heart,
 | 
			
		||||
  People,
 | 
			
		||||
  Trash3,
 | 
			
		||||
} from "react-bootstrap-icons";
 | 
			
		||||
 | 
			
		||||
import { userState } from "../lib/store";
 | 
			
		||||
import Loading from "../lib/Loading";
 | 
			
		||||
import Card from "../lib/Card";
 | 
			
		||||
import TextInput from "../lib/TextInput";
 | 
			
		||||
import fetchAPI from "../lib/fetch";
 | 
			
		||||
import { MeUser, Field } from "../lib/types";
 | 
			
		||||
 | 
			
		||||
interface EditField {
 | 
			
		||||
  id: number;
 | 
			
		||||
  name: string;
 | 
			
		||||
  pronouns: Record<string, PronounChoice>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum PronounChoice {
 | 
			
		||||
  favourite,
 | 
			
		||||
  okay,
 | 
			
		||||
  jokingly,
 | 
			
		||||
  friendsOnly,
 | 
			
		||||
  avoid,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function fieldsEqual(arr1: EditField[], arr2: EditField[]) {
 | 
			
		||||
  if (arr1?.length !== arr2?.length) return false;
 | 
			
		||||
 | 
			
		||||
  if (!arr1.every((_, i) => arr1[i].id === arr2[i].id)) return false;
 | 
			
		||||
 | 
			
		||||
  return arr1.every((_, i) =>
 | 
			
		||||
    Object.keys(arr1[i].pronouns).every(
 | 
			
		||||
      (val) => arr1[i].pronouns[val] === arr2[i].pronouns[val]
 | 
			
		||||
    )
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function updateUser(args: {
 | 
			
		||||
  displayName: string;
 | 
			
		||||
  bio: string;
 | 
			
		||||
  fields: EditField[];
 | 
			
		||||
}) {
 | 
			
		||||
  const newFields = args.fields.map((editField) => {
 | 
			
		||||
    const field: Field = {
 | 
			
		||||
      name: editField.name,
 | 
			
		||||
      favourite: [],
 | 
			
		||||
      okay: [],
 | 
			
		||||
      jokingly: [],
 | 
			
		||||
      friends_only: [],
 | 
			
		||||
      avoid: [],
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    Object.keys(editField).forEach((pronoun) => {
 | 
			
		||||
      switch (editField.pronouns[pronoun]) {
 | 
			
		||||
        case PronounChoice.favourite:
 | 
			
		||||
          field.favourite?.push(pronoun);
 | 
			
		||||
          break;
 | 
			
		||||
        case PronounChoice.okay:
 | 
			
		||||
          field.okay?.push(pronoun);
 | 
			
		||||
          break;
 | 
			
		||||
        case PronounChoice.jokingly:
 | 
			
		||||
          field.jokingly?.push(pronoun);
 | 
			
		||||
          break;
 | 
			
		||||
        case PronounChoice.friendsOnly:
 | 
			
		||||
          field.friends_only?.push(pronoun);
 | 
			
		||||
          break;
 | 
			
		||||
        case PronounChoice.avoid:
 | 
			
		||||
          field.avoid?.push(pronoun);
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return field;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return await fetchAPI<MeUser>("/users/@me", "PATCH", {
 | 
			
		||||
    display_name: args.displayName,
 | 
			
		||||
    bio: args.bio,
 | 
			
		||||
    fields: newFields,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function EditMe() {
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
 | 
			
		||||
  const meUser = useRecoilValue(userState);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!meUser) {
 | 
			
		||||
      navigate("/");
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  if (!meUser) {
 | 
			
		||||
    return <Loading />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const [state, setState] = useState(cloneDeep(meUser));
 | 
			
		||||
  // convert all fields to EditFields
 | 
			
		||||
  const originalOrder = state.fields.map((f, i) => {
 | 
			
		||||
    const field: EditField = {
 | 
			
		||||
      id: i,
 | 
			
		||||
      name: f.name,
 | 
			
		||||
      pronouns: {},
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    f.favourite?.forEach((val) => {
 | 
			
		||||
      field.pronouns[val] = PronounChoice.favourite;
 | 
			
		||||
    });
 | 
			
		||||
    f.okay?.forEach((val) => {
 | 
			
		||||
      field.pronouns[val] = PronounChoice.okay;
 | 
			
		||||
    });
 | 
			
		||||
    f.jokingly?.forEach((val) => {
 | 
			
		||||
      field.pronouns[val] = PronounChoice.jokingly;
 | 
			
		||||
    });
 | 
			
		||||
    f.friends_only?.forEach((val) => {
 | 
			
		||||
      field.pronouns[val] = PronounChoice.friendsOnly;
 | 
			
		||||
    });
 | 
			
		||||
    f.avoid?.forEach((val) => {
 | 
			
		||||
      field.pronouns[val] = PronounChoice.avoid;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return field;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const [fields, setFields] = useState(cloneDeep(originalOrder));
 | 
			
		||||
  const fieldsUpdated = !fieldsEqual(fields, originalOrder);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="container mx-auto">
 | 
			
		||||
      <div>{`fieldsUpdated: ${fieldsUpdated}`}</div>
 | 
			
		||||
      {/* @ts-ignore: This component isn't updated to have a "children" prop yet, but it accepts it just fine. */}
 | 
			
		||||
      <ReactSortable
 | 
			
		||||
        handle=".handle"
 | 
			
		||||
        list={fields}
 | 
			
		||||
        setList={setFields}
 | 
			
		||||
        className="grid grid-cols-1 xl:grid-cols-2 gap-4 py-2"
 | 
			
		||||
      >
 | 
			
		||||
        {fields.map((field, i) => (
 | 
			
		||||
          <EditableCard
 | 
			
		||||
            key={i}
 | 
			
		||||
            field={field}
 | 
			
		||||
            onChangeName={(e) => {
 | 
			
		||||
              field.name = e.target.value;
 | 
			
		||||
              setFields([...fields]);
 | 
			
		||||
            }}
 | 
			
		||||
            onChangeFavourite={(e, entry: string) => {
 | 
			
		||||
              field.pronouns[entry] = PronounChoice.favourite;
 | 
			
		||||
              setFields([...fields]);
 | 
			
		||||
            }}
 | 
			
		||||
            onChangeOkay={(e, entry: string) => {
 | 
			
		||||
              field.pronouns[entry] = PronounChoice.okay;
 | 
			
		||||
              setFields([...fields]);
 | 
			
		||||
            }}
 | 
			
		||||
            onChangeJokingly={(e, entry: string) => {
 | 
			
		||||
              field.pronouns[entry] = PronounChoice.jokingly;
 | 
			
		||||
              setFields([...fields]);
 | 
			
		||||
            }}
 | 
			
		||||
            onChangeFriends={(e, entry: string) => {
 | 
			
		||||
              field.pronouns[entry] = PronounChoice.friendsOnly;
 | 
			
		||||
              setFields([...fields]);
 | 
			
		||||
            }}
 | 
			
		||||
            onChangeAvoid={(e, entry: string) => {
 | 
			
		||||
              field.pronouns[entry] = PronounChoice.avoid;
 | 
			
		||||
              setFields([...fields]);
 | 
			
		||||
            }}
 | 
			
		||||
            onClickDelete={(_) => {
 | 
			
		||||
              const newFields = [...fields];
 | 
			
		||||
              newFields.splice(i, 1);
 | 
			
		||||
              setFields(newFields);
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        ))}
 | 
			
		||||
      </ReactSortable>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type EditableCardProps = {
 | 
			
		||||
  field: EditField;
 | 
			
		||||
  onChangeName: React.ChangeEventHandler<HTMLInputElement>;
 | 
			
		||||
  onChangeFavourite(
 | 
			
		||||
    e: React.MouseEvent<HTMLButtonElement>,
 | 
			
		||||
    entry: string
 | 
			
		||||
  ): void;
 | 
			
		||||
  onChangeOkay(e: React.MouseEvent<HTMLButtonElement>, entry: string): void;
 | 
			
		||||
  onChangeJokingly(e: React.MouseEvent<HTMLButtonElement>, entry: string): void;
 | 
			
		||||
  onChangeFriends(e: React.MouseEvent<HTMLButtonElement>, entry: string): void;
 | 
			
		||||
  onChangeAvoid(e: React.MouseEvent<HTMLButtonElement>, entry: string): void;
 | 
			
		||||
  onClickDelete: React.MouseEventHandler<HTMLButtonElement>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function EditableCard(props: EditableCardProps) {
 | 
			
		||||
  const footer = (
 | 
			
		||||
    <div className="flex justify-between">
 | 
			
		||||
      <TextInput value={props.field.name} onChange={props.onChangeName} />
 | 
			
		||||
      <button
 | 
			
		||||
        type="button"
 | 
			
		||||
        onClick={props.onClickDelete}
 | 
			
		||||
        className="bg-red-600 dark:bg-red-700 hover:bg-red-700 hover:dark:bg-red-800 p-2 rounded-md"
 | 
			
		||||
      >
 | 
			
		||||
        <Trash3 aria-hidden className="inline" />{" "}
 | 
			
		||||
        <span className="font-bold">Delete</span>
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Card title={props.field.name} draggable footer={footer}>
 | 
			
		||||
      <ul>
 | 
			
		||||
        {Object.keys(props.field.pronouns).map((pronoun, index) => {
 | 
			
		||||
          const choice = props.field.pronouns[pronoun];
 | 
			
		||||
          return (
 | 
			
		||||
            <li className="flex justify-between my-1" key={index}>
 | 
			
		||||
              <div>{pronoun}</div>
 | 
			
		||||
              <div className="rounded-md">
 | 
			
		||||
                <button
 | 
			
		||||
                  type="button"
 | 
			
		||||
                  onClick={(e) => props.onChangeFavourite(e, pronoun)}
 | 
			
		||||
                  className={`${
 | 
			
		||||
                    choice == PronounChoice.favourite
 | 
			
		||||
                      ? "bg-slate-500"
 | 
			
		||||
                      : "bg-slate-600"
 | 
			
		||||
                  } hover:bg-slate-400 p-2`}
 | 
			
		||||
                >
 | 
			
		||||
                  <Heart />
 | 
			
		||||
                </button>
 | 
			
		||||
                <button
 | 
			
		||||
                  type="button"
 | 
			
		||||
                  onClick={(e) => props.onChangeOkay(e, pronoun)}
 | 
			
		||||
                  className={`${
 | 
			
		||||
                    choice == PronounChoice.okay
 | 
			
		||||
                      ? "bg-slate-500"
 | 
			
		||||
                      : "bg-slate-600"
 | 
			
		||||
                  } hover:bg-slate-400 p-2`}
 | 
			
		||||
                >
 | 
			
		||||
                  <HandThumbsUp />
 | 
			
		||||
                </button>
 | 
			
		||||
                <button
 | 
			
		||||
                  type="button"
 | 
			
		||||
                  onClick={(e) => props.onChangeJokingly(e, pronoun)}
 | 
			
		||||
                  className={`${
 | 
			
		||||
                    choice == PronounChoice.jokingly
 | 
			
		||||
                      ? "bg-slate-500"
 | 
			
		||||
                      : "bg-slate-600"
 | 
			
		||||
                  } hover:bg-slate-400 p-2`}
 | 
			
		||||
                >
 | 
			
		||||
                  <EmojiLaughing />
 | 
			
		||||
                </button>
 | 
			
		||||
                <button
 | 
			
		||||
                  type="button"
 | 
			
		||||
                  onClick={(e) => props.onChangeFriends(e, pronoun)}
 | 
			
		||||
                  className={`${
 | 
			
		||||
                    choice == PronounChoice.friendsOnly
 | 
			
		||||
                      ? "bg-slate-500"
 | 
			
		||||
                      : "bg-slate-600"
 | 
			
		||||
                  } hover:bg-slate-400 p-2`}
 | 
			
		||||
                >
 | 
			
		||||
                  <People />
 | 
			
		||||
                </button>
 | 
			
		||||
                <button
 | 
			
		||||
                  type="button"
 | 
			
		||||
                  onClick={(e) => props.onChangeAvoid(e, pronoun)}
 | 
			
		||||
                  className={`${
 | 
			
		||||
                    choice == PronounChoice.avoid
 | 
			
		||||
                      ? "bg-slate-500"
 | 
			
		||||
                      : "bg-slate-600"
 | 
			
		||||
                  } hover:bg-slate-400 p-2`}
 | 
			
		||||
                >
 | 
			
		||||
                  <HandThumbsDown />
 | 
			
		||||
                </button>
 | 
			
		||||
                <button
 | 
			
		||||
                  type="button"
 | 
			
		||||
                  className="bg-red-600 dark:bg-red-700 hover:bg-red-700 hover:dark:bg-red-800 p-2"
 | 
			
		||||
                >
 | 
			
		||||
                  <Trash3 />
 | 
			
		||||
                </button>
 | 
			
		||||
              </div>
 | 
			
		||||
            </li>
 | 
			
		||||
          );
 | 
			
		||||
        })}
 | 
			
		||||
      </ul>
 | 
			
		||||
    </Card>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,114 +0,0 @@
 | 
			
		|||
import React, { useState, useEffect } from "react";
 | 
			
		||||
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 { 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";
 | 
			
		||||
import Loading from "../lib/Loading";
 | 
			
		||||
 | 
			
		||||
function UserPage() {
 | 
			
		||||
  const params = useParams();
 | 
			
		||||
 | 
			
		||||
  const [user, setUser] = useState<User>(null);
 | 
			
		||||
  const meUser = useRecoilValue(userState);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    fetchAPI<User>(`/users/${params.username}`).then((res) => {
 | 
			
		||||
      setUser(res);
 | 
			
		||||
    });
 | 
			
		||||
  }, [params.username]);
 | 
			
		||||
 | 
			
		||||
  if (user == null) {
 | 
			
		||||
    return <Loading />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <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 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-xs rounded-full" src={user.avatar_url} />
 | 
			
		||||
          )}
 | 
			
		||||
          <div className="flex flex-col">
 | 
			
		||||
            {user.display_name && (
 | 
			
		||||
              <h1 className="text-2xl font-bold">{user.display_name}</h1>
 | 
			
		||||
            )}
 | 
			
		||||
            <h3
 | 
			
		||||
              className={`${
 | 
			
		||||
                user.display_name
 | 
			
		||||
                  ? "text-xl italic text-slate-600 dark:text-slate-400"
 | 
			
		||||
                  : "text-2xl font-bold"
 | 
			
		||||
              }`}
 | 
			
		||||
            >
 | 
			
		||||
              @{user.username}
 | 
			
		||||
            </h3>
 | 
			
		||||
            {user.bio && (
 | 
			
		||||
              <ReactMarkdown className="prose dark:prose-invert prose-slate">
 | 
			
		||||
                {user.bio}
 | 
			
		||||
              </ReactMarkdown>
 | 
			
		||||
            )}
 | 
			
		||||
            {user.links?.length && user.fields?.length && (
 | 
			
		||||
              <div className="flex flex-col mx-auto lg:ml-auto">
 | 
			
		||||
                {user.links.map((link, index) => (
 | 
			
		||||
                  <a
 | 
			
		||||
                    key={index}
 | 
			
		||||
                    href={link}
 | 
			
		||||
                    rel="nofollow noopener noreferrer me"
 | 
			
		||||
                    className="hover:underline text-sky-500 dark:text-sky-400"
 | 
			
		||||
                  >
 | 
			
		||||
                    {link}
 | 
			
		||||
                  </a>
 | 
			
		||||
                ))}
 | 
			
		||||
              </div>
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="grid grid-cols-1 md:grid-cols-3 gap-4 py-2">
 | 
			
		||||
          {user.fields?.map((field, index) => (
 | 
			
		||||
            <FieldCard key={index} field={field}></FieldCard>
 | 
			
		||||
          ))}
 | 
			
		||||
          {user.links?.length && (
 | 
			
		||||
            <Card title="Links">
 | 
			
		||||
              {user.links.map((link, index) => (
 | 
			
		||||
                <a
 | 
			
		||||
                  key={index}
 | 
			
		||||
                  href={link}
 | 
			
		||||
                  rel="nofollow noopener noreferrer me"
 | 
			
		||||
                  className="hover:underline text-sky-500 dark:text-sky-400"
 | 
			
		||||
                >
 | 
			
		||||
                  {link}
 | 
			
		||||
                </a>
 | 
			
		||||
              ))}
 | 
			
		||||
            </Card>
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default UserPage;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,77 +0,0 @@
 | 
			
		|||
import { useEffect, useState } from "react";
 | 
			
		||||
import { useNavigate } from "react-router-dom";
 | 
			
		||||
import { useRecoilState } from "recoil";
 | 
			
		||||
import fetchAPI from "../../lib/fetch";
 | 
			
		||||
import Loading from "../../lib/Loading";
 | 
			
		||||
import { userState } from "../../lib/store";
 | 
			
		||||
import { MeUser } from "../../lib/types";
 | 
			
		||||
 | 
			
		||||
interface CallbackResponse {
 | 
			
		||||
  has_account: boolean;
 | 
			
		||||
  token?: string;
 | 
			
		||||
  user?: MeUser;
 | 
			
		||||
 | 
			
		||||
  discord?: string;
 | 
			
		||||
  ticket?: string;
 | 
			
		||||
  require_invite?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function Discord() {
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
  const params = new URLSearchParams(window.location.search);
 | 
			
		||||
 | 
			
		||||
  const [state, setState] = useState({
 | 
			
		||||
    hasAccount: false,
 | 
			
		||||
    isLoading: false,
 | 
			
		||||
    token: null,
 | 
			
		||||
    user: null,
 | 
			
		||||
    discord: null,
 | 
			
		||||
    ticket: null,
 | 
			
		||||
    error: null,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const [user, setUser] = useRecoilState(userState);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (state.isLoading) return;
 | 
			
		||||
    setState({ ...state, isLoading: true });
 | 
			
		||||
 | 
			
		||||
    fetchAPI<CallbackResponse>("/auth/discord/callback", "POST", {
 | 
			
		||||
      callback_domain: window.location.origin,
 | 
			
		||||
      code: params.get("code"),
 | 
			
		||||
      state: params.get("state"),
 | 
			
		||||
    }).then(
 | 
			
		||||
      (resp) => {
 | 
			
		||||
        setState({
 | 
			
		||||
          hasAccount: resp.has_account,
 | 
			
		||||
          isLoading: false,
 | 
			
		||||
          token: resp.token,
 | 
			
		||||
          user: resp.user,
 | 
			
		||||
          discord: resp.discord,
 | 
			
		||||
          ticket: resp.ticket,
 | 
			
		||||
          error: null,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        console.log("token:", resp.token);
 | 
			
		||||
        localStorage.setItem("pronouns-token", resp.token);
 | 
			
		||||
 | 
			
		||||
        setUser(resp.user);
 | 
			
		||||
      },
 | 
			
		||||
      (err) => {
 | 
			
		||||
        console.log(err);
 | 
			
		||||
        setState({ ...state, error: err, isLoading: false });
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  if (user) {
 | 
			
		||||
    // we got a token + user, save it and return to the home page
 | 
			
		||||
    navigate("/");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (user || state.isLoading) {
 | 
			
		||||
    return <Loading />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return <>wow such login</>;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,52 +0,0 @@
 | 
			
		|||
import { useEffect, useState } from "react";
 | 
			
		||||
import { useNavigate } from "react-router-dom";
 | 
			
		||||
import { useRecoilValue } from "recoil";
 | 
			
		||||
import fetchAPI from "../../lib/fetch";
 | 
			
		||||
import Loading from "../../lib/Loading";
 | 
			
		||||
import { userState } from "../../lib/store";
 | 
			
		||||
 | 
			
		||||
interface URLsResponse {
 | 
			
		||||
  discord: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function Login() {
 | 
			
		||||
  const [state, setState] = useState({
 | 
			
		||||
    loading: false,
 | 
			
		||||
    error: null,
 | 
			
		||||
    discord: "",
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  if (useRecoilValue(userState) !== null) {
 | 
			
		||||
    const nav = useNavigate();
 | 
			
		||||
    nav("/");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (state.loading) return;
 | 
			
		||||
    setState({ ...state, loading: true });
 | 
			
		||||
 | 
			
		||||
    fetchAPI<URLsResponse>("/auth/urls", "POST", {
 | 
			
		||||
      callback_domain: window.location.origin,
 | 
			
		||||
    }).then(
 | 
			
		||||
      (resp) => {
 | 
			
		||||
        setState({ loading: false, error: null, discord: resp.discord });
 | 
			
		||||
      },
 | 
			
		||||
      (err) => {
 | 
			
		||||
        console.log(err);
 | 
			
		||||
        setState({ ...state, loading: false, error: err });
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  if (state.loading) {
 | 
			
		||||
    return <Loading />;
 | 
			
		||||
  } else if (state.error) {
 | 
			
		||||
    return <>Error: {`${state.error}`}</>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <a href={state.discord}>Login with Discord</a>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										0
									
								
								frontend/src/index.css → frontend/styles/globals.css
									
										
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								frontend/src/index.css → frontend/styles/globals.css
									
										
									
									
									
										
										
										Normal file → Executable file
									
								
							| 
						 | 
				
			
			@ -1,6 +1,9 @@
 | 
			
		|||
module.exports = {
 | 
			
		||||
  darkMode: "class",
 | 
			
		||||
  content: ["./frontend/index.html", "./frontend/src/**/*.{vue,js,ts,jsx,tsx}"],
 | 
			
		||||
  content: [
 | 
			
		||||
    "./pages/**/*.{js,ts,jsx,tsx}",
 | 
			
		||||
    "./components/**/*.{js,ts,jsx,tsx}",
 | 
			
		||||
  ],
 | 
			
		||||
  theme: {
 | 
			
		||||
    extend: {},
 | 
			
		||||
  },
 | 
			
		||||
							
								
								
									
										20
									
								
								frontend/tsconfig.json
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										20
									
								
								frontend/tsconfig.json
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,20 @@
 | 
			
		|||
{
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "target": "es5",
 | 
			
		||||
    "lib": ["dom", "dom.iterable", "esnext"],
 | 
			
		||||
    "allowJs": true,
 | 
			
		||||
    "skipLibCheck": true,
 | 
			
		||||
    "strict": true,
 | 
			
		||||
    "forceConsistentCasingInFileNames": true,
 | 
			
		||||
    "noEmit": true,
 | 
			
		||||
    "esModuleInterop": true,
 | 
			
		||||
    "module": "esnext",
 | 
			
		||||
    "moduleResolution": "node",
 | 
			
		||||
    "resolveJsonModule": true,
 | 
			
		||||
    "isolatedModules": true,
 | 
			
		||||
    "jsx": "preserve",
 | 
			
		||||
    "incremental": true
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
 | 
			
		||||
  "exclude": ["node_modules"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2617
									
								
								frontend/yarn.lock
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										2617
									
								
								frontend/yarn.lock
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										41
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										41
									
								
								package.json
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,41 +0,0 @@
 | 
			
		|||
{
 | 
			
		||||
  "name": "pronouns",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "version": "0.0.0",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "vite",
 | 
			
		||||
    "build": "tsc && vite build",
 | 
			
		||||
    "preview": "vite preview"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@sentry/react": "^6.19.7",
 | 
			
		||||
    "@sentry/tracing": "^6.19.7",
 | 
			
		||||
    "axios": "^0.27.2",
 | 
			
		||||
    "lodash": "^4.17.21",
 | 
			
		||||
    "react": "^18.0.0",
 | 
			
		||||
    "react-bootstrap-icons": "^1.8.2",
 | 
			
		||||
    "react-dom": "^18.0.0",
 | 
			
		||||
    "react-helmet": "^6.1.0",
 | 
			
		||||
    "react-markdown": "^8.0.3",
 | 
			
		||||
    "react-router-dom": "6",
 | 
			
		||||
    "react-select": "^5.3.2",
 | 
			
		||||
    "react-sortablejs": "^6.1.1",
 | 
			
		||||
    "recoil": "^0.7.2",
 | 
			
		||||
    "sortablejs": "^1.15.0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@tailwindcss/forms": "^0.5.1",
 | 
			
		||||
    "@tailwindcss/typography": "^0.5.2",
 | 
			
		||||
    "@types/lodash": "^4.14.182",
 | 
			
		||||
    "@types/react": "^18.0.0",
 | 
			
		||||
    "@types/react-dom": "^18.0.0",
 | 
			
		||||
    "@types/react-helmet": "^6.1.5",
 | 
			
		||||
    "@types/sortablejs": "^1.13.0",
 | 
			
		||||
    "@vitejs/plugin-react": "^1.3.0",
 | 
			
		||||
    "autoprefixer": "^10.4.7",
 | 
			
		||||
    "postcss": "^8.4.13",
 | 
			
		||||
    "tailwindcss": "^3.0.24",
 | 
			
		||||
    "typescript": "^4.6.3",
 | 
			
		||||
    "vite": "^2.9.7"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
module.exports = {
 | 
			
		||||
  plugins: {
 | 
			
		||||
    tailwindcss: {},
 | 
			
		||||
    autoprefixer: {},
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,21 +0,0 @@
 | 
			
		|||
{
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "target": "ESNext",
 | 
			
		||||
    "useDefineForClassFields": true,
 | 
			
		||||
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
 | 
			
		||||
    "allowJs": false,
 | 
			
		||||
    "skipLibCheck": true,
 | 
			
		||||
    "esModuleInterop": false,
 | 
			
		||||
    "allowSyntheticDefaultImports": true,
 | 
			
		||||
    "strict": true,
 | 
			
		||||
    "forceConsistentCasingInFileNames": true,
 | 
			
		||||
    "module": "ESNext",
 | 
			
		||||
    "moduleResolution": "Node",
 | 
			
		||||
    "resolveJsonModule": true,
 | 
			
		||||
    "isolatedModules": true,
 | 
			
		||||
    "noEmit": true,
 | 
			
		||||
    "jsx": "react-jsx"
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["src"],
 | 
			
		||||
  "references": [{ "path": "./tsconfig.node.json" }]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,8 +0,0 @@
 | 
			
		|||
{
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "composite": true,
 | 
			
		||||
    "module": "esnext",
 | 
			
		||||
    "moduleResolution": "node"
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["vite.config.ts"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								vite-env.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								vite-env.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -1 +0,0 @@
 | 
			
		|||
/// <reference types="vite/client" />
 | 
			
		||||
| 
						 | 
				
			
			@ -1,17 +0,0 @@
 | 
			
		|||
import { defineConfig } from "vite";
 | 
			
		||||
import react from "@vitejs/plugin-react";
 | 
			
		||||
 | 
			
		||||
// https://vitejs.dev/config/
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  root: "frontend",
 | 
			
		||||
  plugins: [react()],
 | 
			
		||||
  server: {
 | 
			
		||||
    proxy: {
 | 
			
		||||
      "/api": {
 | 
			
		||||
        // assumes port 8080 in .env for development
 | 
			
		||||
        target: "http://localhost:8080",
 | 
			
		||||
        changeOrigin: true,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue