switch to another frontend framework wheeeeeeeeeeee
This commit is contained in:
parent
fa3c1ccaa7
commit
c4adf6918c
58 changed files with 6246 additions and 1703 deletions
|
@ -1,4 +0,0 @@
|
||||||
# The API base that the server itself should call, this should not be behind a reverse proxy.
|
|
||||||
PRIVATE_API_BASE=http://localhost:5000/api
|
|
||||||
# The API base that clients should call, behind a reverse proxy.
|
|
||||||
PUBLIC_API_BASE=https://pronouns.cc/api
|
|
84
Foxnouns.Frontend/.eslintrc.cjs
Normal file
84
Foxnouns.Frontend/.eslintrc.cjs
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
/**
|
||||||
|
* This is intended to be a basic starting point for linting in your app.
|
||||||
|
* It relies on recommended configs out of the box for simplicity, but you can
|
||||||
|
* and should modify this configuration to best suit your team's needs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {import('eslint').Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
commonjs: true,
|
||||||
|
es6: true,
|
||||||
|
},
|
||||||
|
ignorePatterns: ["!**/.server", "!**/.client"],
|
||||||
|
|
||||||
|
// Base config
|
||||||
|
extends: ["eslint:recommended"],
|
||||||
|
|
||||||
|
overrides: [
|
||||||
|
// React
|
||||||
|
{
|
||||||
|
files: ["**/*.{js,jsx,ts,tsx}"],
|
||||||
|
plugins: ["react", "jsx-a11y"],
|
||||||
|
extends: [
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react/jsx-runtime",
|
||||||
|
"plugin:react-hooks/recommended",
|
||||||
|
"plugin:jsx-a11y/recommended",
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: "detect",
|
||||||
|
},
|
||||||
|
formComponents: ["Form"],
|
||||||
|
linkComponents: [
|
||||||
|
{ name: "Link", linkAttribute: "to" },
|
||||||
|
{ name: "NavLink", linkAttribute: "to" },
|
||||||
|
],
|
||||||
|
"import/resolver": {
|
||||||
|
typescript: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Typescript
|
||||||
|
{
|
||||||
|
files: ["**/*.{ts,tsx}"],
|
||||||
|
plugins: ["@typescript-eslint", "import"],
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
settings: {
|
||||||
|
"import/internal-regex": "^~/",
|
||||||
|
"import/resolver": {
|
||||||
|
node: {
|
||||||
|
extensions: [".ts", ".tsx"],
|
||||||
|
},
|
||||||
|
typescript: {
|
||||||
|
alwaysTryTypes: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:import/recommended",
|
||||||
|
"plugin:import/typescript",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Node
|
||||||
|
{
|
||||||
|
files: [".eslintrc.cjs"],
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
9
Foxnouns.Frontend/.gitignore
vendored
9
Foxnouns.Frontend/.gitignore
vendored
|
@ -1,10 +1,5 @@
|
||||||
.DS_Store
|
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
|
/.cache
|
||||||
/build
|
/build
|
||||||
/.svelte-kit
|
|
||||||
/package
|
|
||||||
.env
|
.env
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
vite.config.js.timestamp-*
|
|
||||||
vite.config.ts.timestamp-*
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
engine-strict=true
|
|
|
@ -1,4 +0,0 @@
|
||||||
# Ignore files for PNPM, NPM and YARN
|
|
||||||
pnpm-lock.yaml
|
|
||||||
package-lock.json
|
|
||||||
yarn.lock
|
|
|
@ -1,6 +1,3 @@
|
||||||
{
|
{
|
||||||
"useTabs": true,
|
"useTabs": true
|
||||||
"printWidth": 100,
|
|
||||||
"plugins": ["prettier-plugin-svelte"],
|
|
||||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +1,40 @@
|
||||||
# create-svelte
|
# Welcome to Remix!
|
||||||
|
|
||||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
|
- 📖 [Remix docs](https://remix.run/docs)
|
||||||
|
|
||||||
## Creating a project
|
## Development
|
||||||
|
|
||||||
If you're seeing this, you've probably already done this step. Congrats!
|
Run the dev server:
|
||||||
|
|
||||||
```bash
|
```shellscript
|
||||||
# create a new project in the current directory
|
|
||||||
npm create svelte@latest
|
|
||||||
|
|
||||||
# create a new project in my-app
|
|
||||||
npm create svelte@latest my-app
|
|
||||||
```
|
|
||||||
|
|
||||||
## Developing
|
|
||||||
|
|
||||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
npm run dev
|
||||||
|
|
||||||
# or start the server and open the app in a new browser tab
|
|
||||||
npm run dev -- --open
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building
|
## Deployment
|
||||||
|
|
||||||
To create a production version of your app:
|
First, build your app for production:
|
||||||
|
|
||||||
```bash
|
```sh
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
You can preview the production build with `npm run preview`.
|
Then run the app in production mode:
|
||||||
|
|
||||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
```sh
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you'll need to pick a host to deploy it to.
|
||||||
|
|
||||||
|
### DIY
|
||||||
|
|
||||||
|
If you're familiar with deploying Node applications, the built-in Remix app server is production-ready.
|
||||||
|
|
||||||
|
Make sure to deploy the output of `npm run build`
|
||||||
|
|
||||||
|
- `build/server`
|
||||||
|
- `build/client`
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever css framework you prefer. See the [Vite docs on css](https://vitejs.dev/guide/features.html#css) for more information.
|
||||||
|
|
21
Foxnouns.Frontend/app/app.scss
Normal file
21
Foxnouns.Frontend/app/app.scss
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
$font-family-sans-serif:
|
||||||
|
"FiraGO",
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
"Segoe UI",
|
||||||
|
Roboto,
|
||||||
|
"Helvetica Neue",
|
||||||
|
"Noto Sans",
|
||||||
|
"Liberation Sans",
|
||||||
|
Arial,
|
||||||
|
sans-serif,
|
||||||
|
"Apple Color Emoji",
|
||||||
|
"Segoe UI Emoji",
|
||||||
|
"Segoe UI Symbol",
|
||||||
|
"Noto Color Emoji" !default;
|
||||||
|
|
||||||
|
@import "bootstrap/scss/bootstrap";
|
||||||
|
|
||||||
|
@import "@fontsource/firago/400.css";
|
||||||
|
@import "@fontsource/firago/400-italic.css";
|
||||||
|
@import "@fontsource/firago/700.css";
|
41
Foxnouns.Frontend/app/components/nav/Logo.tsx
Normal file
41
Foxnouns.Frontend/app/components/nav/Logo.tsx
Normal file
File diff suppressed because one or more lines are too long
87
Foxnouns.Frontend/app/components/nav/Navbar.tsx
Normal file
87
Foxnouns.Frontend/app/components/nav/Navbar.tsx
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import { Link, useFetcher } from "@remix-run/react";
|
||||||
|
import Meta from "~/lib/api/meta";
|
||||||
|
import { User, UserSettings } from "~/lib/api/user";
|
||||||
|
import Logo from "./Logo";
|
||||||
|
|
||||||
|
import Nav from "react-bootstrap/Nav";
|
||||||
|
import Navbar from "react-bootstrap/Navbar";
|
||||||
|
import NavDropdown from "react-bootstrap/NavDropdown";
|
||||||
|
import {
|
||||||
|
BrightnessHigh,
|
||||||
|
BrightnessHighFill,
|
||||||
|
MoonFill,
|
||||||
|
} from "react-bootstrap-icons";
|
||||||
|
|
||||||
|
export default function MainNavbar({
|
||||||
|
user,
|
||||||
|
settings,
|
||||||
|
}: {
|
||||||
|
meta: Meta;
|
||||||
|
user?: User;
|
||||||
|
settings: UserSettings;
|
||||||
|
}) {
|
||||||
|
const fetcher = useFetcher();
|
||||||
|
|
||||||
|
const userMenu = user ? (
|
||||||
|
<NavDropdown title={<>@{user.username}</>} align="end">
|
||||||
|
<NavDropdown.Item as={Link} to={`/@${user.username}`}>
|
||||||
|
View profile
|
||||||
|
</NavDropdown.Item>
|
||||||
|
<NavDropdown.Item as={Link} to="/settings">
|
||||||
|
Settings
|
||||||
|
</NavDropdown.Item>
|
||||||
|
<NavDropdown.Divider />
|
||||||
|
<NavDropdown.Item as={Link} to="/auth/logout">
|
||||||
|
Log out
|
||||||
|
</NavDropdown.Item>
|
||||||
|
</NavDropdown>
|
||||||
|
) : (
|
||||||
|
<Nav.Link to="/auth/login" as={Link}>
|
||||||
|
Log in or sign up
|
||||||
|
</Nav.Link>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ThemeIcon =
|
||||||
|
settings.dark_mode === null
|
||||||
|
? BrightnessHigh
|
||||||
|
: settings.dark_mode
|
||||||
|
? MoonFill
|
||||||
|
: BrightnessHighFill;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Navbar expand="lg" className="mb-4 mx-2">
|
||||||
|
<Navbar.Brand to="/" as={Link}>
|
||||||
|
<Logo />
|
||||||
|
</Navbar.Brand>
|
||||||
|
<Navbar.Toggle aria-controls="main-navbar" />
|
||||||
|
<Navbar.Collapse id="main-navbar">
|
||||||
|
<Nav className="ms-auto">{userMenu}</Nav>
|
||||||
|
</Navbar.Collapse>
|
||||||
|
<fetcher.Form method="POST" action="/dark-mode">
|
||||||
|
<NavDropdown
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
<ThemeIcon /> Theme
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
align="end"
|
||||||
|
>
|
||||||
|
<NavDropdown.Item as="button" name="theme" value="auto" type="submit">
|
||||||
|
Automatic
|
||||||
|
</NavDropdown.Item>
|
||||||
|
<NavDropdown.Item as="button" name="theme" value="dark" type="submit">
|
||||||
|
Dark mode
|
||||||
|
</NavDropdown.Item>
|
||||||
|
<NavDropdown.Item
|
||||||
|
as="button"
|
||||||
|
name="theme"
|
||||||
|
value="light"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Light mode
|
||||||
|
</NavDropdown.Item>
|
||||||
|
</NavDropdown>
|
||||||
|
</fetcher.Form>
|
||||||
|
</Navbar>
|
||||||
|
);
|
||||||
|
}
|
18
Foxnouns.Frontend/app/entry.client.tsx
Normal file
18
Foxnouns.Frontend/app/entry.client.tsx
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
/**
|
||||||
|
* By default, Remix will handle hydrating your app on the client for you.
|
||||||
|
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
|
||||||
|
* For more information, see https://remix.run/file-conventions/entry.client
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { RemixBrowser } from "@remix-run/react";
|
||||||
|
import { startTransition, StrictMode } from "react";
|
||||||
|
import { hydrateRoot } from "react-dom/client";
|
||||||
|
|
||||||
|
startTransition(() => {
|
||||||
|
hydrateRoot(
|
||||||
|
document,
|
||||||
|
<StrictMode>
|
||||||
|
<RemixBrowser />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
|
});
|
140
Foxnouns.Frontend/app/entry.server.tsx
Normal file
140
Foxnouns.Frontend/app/entry.server.tsx
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
/**
|
||||||
|
* By default, Remix will handle generating the HTTP Response for you.
|
||||||
|
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
|
||||||
|
* For more information, see https://remix.run/file-conventions/entry.server
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { PassThrough } from "node:stream";
|
||||||
|
|
||||||
|
import type { AppLoadContext, EntryContext } from "@remix-run/node";
|
||||||
|
import { createReadableStreamFromReadable } from "@remix-run/node";
|
||||||
|
import { RemixServer } from "@remix-run/react";
|
||||||
|
import { isbot } from "isbot";
|
||||||
|
import { renderToPipeableStream } from "react-dom/server";
|
||||||
|
|
||||||
|
const ABORT_DELAY = 5_000;
|
||||||
|
|
||||||
|
export default function handleRequest(
|
||||||
|
request: Request,
|
||||||
|
responseStatusCode: number,
|
||||||
|
responseHeaders: Headers,
|
||||||
|
remixContext: EntryContext,
|
||||||
|
// This is ignored so we can keep it in the template for visibility. Feel
|
||||||
|
// free to delete this parameter in your app if you're not using it!
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
loadContext: AppLoadContext,
|
||||||
|
) {
|
||||||
|
return isbot(request.headers.get("user-agent") || "")
|
||||||
|
? handleBotRequest(
|
||||||
|
request,
|
||||||
|
responseStatusCode,
|
||||||
|
responseHeaders,
|
||||||
|
remixContext,
|
||||||
|
)
|
||||||
|
: handleBrowserRequest(
|
||||||
|
request,
|
||||||
|
responseStatusCode,
|
||||||
|
responseHeaders,
|
||||||
|
remixContext,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBotRequest(
|
||||||
|
request: Request,
|
||||||
|
responseStatusCode: number,
|
||||||
|
responseHeaders: Headers,
|
||||||
|
remixContext: EntryContext,
|
||||||
|
) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let shellRendered = false;
|
||||||
|
const { pipe, abort } = renderToPipeableStream(
|
||||||
|
<RemixServer
|
||||||
|
context={remixContext}
|
||||||
|
url={request.url}
|
||||||
|
abortDelay={ABORT_DELAY}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
onAllReady() {
|
||||||
|
shellRendered = true;
|
||||||
|
const body = new PassThrough();
|
||||||
|
const stream = createReadableStreamFromReadable(body);
|
||||||
|
|
||||||
|
responseHeaders.set("Content-Type", "text/html");
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
new Response(stream, {
|
||||||
|
headers: responseHeaders,
|
||||||
|
status: responseStatusCode,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
pipe(body);
|
||||||
|
},
|
||||||
|
onShellError(error: unknown) {
|
||||||
|
reject(error);
|
||||||
|
},
|
||||||
|
onError(error: unknown) {
|
||||||
|
responseStatusCode = 500;
|
||||||
|
// Log streaming rendering errors from inside the shell. Don't log
|
||||||
|
// errors encountered during initial shell rendering since they'll
|
||||||
|
// reject and get logged in handleDocumentRequest.
|
||||||
|
if (shellRendered) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
setTimeout(abort, ABORT_DELAY);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBrowserRequest(
|
||||||
|
request: Request,
|
||||||
|
responseStatusCode: number,
|
||||||
|
responseHeaders: Headers,
|
||||||
|
remixContext: EntryContext,
|
||||||
|
) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let shellRendered = false;
|
||||||
|
const { pipe, abort } = renderToPipeableStream(
|
||||||
|
<RemixServer
|
||||||
|
context={remixContext}
|
||||||
|
url={request.url}
|
||||||
|
abortDelay={ABORT_DELAY}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
onShellReady() {
|
||||||
|
shellRendered = true;
|
||||||
|
const body = new PassThrough();
|
||||||
|
const stream = createReadableStreamFromReadable(body);
|
||||||
|
|
||||||
|
responseHeaders.set("Content-Type", "text/html");
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
new Response(stream, {
|
||||||
|
headers: responseHeaders,
|
||||||
|
status: responseStatusCode,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
pipe(body);
|
||||||
|
},
|
||||||
|
onShellError(error: unknown) {
|
||||||
|
reject(error);
|
||||||
|
},
|
||||||
|
onError(error: unknown) {
|
||||||
|
responseStatusCode = 500;
|
||||||
|
// Log streaming rendering errors from inside the shell. Don't log
|
||||||
|
// errors encountered during initial shell rendering since they'll
|
||||||
|
// reject and get logged in handleDocumentRequest.
|
||||||
|
if (shellRendered) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
setTimeout(abort, ABORT_DELAY);
|
||||||
|
});
|
||||||
|
}
|
3
Foxnouns.Frontend/app/env.server.ts
Normal file
3
Foxnouns.Frontend/app/env.server.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { env } from "node:process";
|
||||||
|
|
||||||
|
export const API_BASE = env.API_BASE || "https://pronouns.localhost/api";
|
28
Foxnouns.Frontend/app/lib/api/error.ts
Normal file
28
Foxnouns.Frontend/app/lib/api/error.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
export type ApiError = {
|
||||||
|
status: number;
|
||||||
|
message: string;
|
||||||
|
code: ErrorCode;
|
||||||
|
errors?: ValidationError[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum ErrorCode {
|
||||||
|
InternalServerError = "INTERNAL_SERVER_ERROR",
|
||||||
|
Forbidden = "FORBIDDEN",
|
||||||
|
BadRequest = "BAD_REQUEST",
|
||||||
|
AuthenticationError = "AUTHENTICATION_ERROR",
|
||||||
|
AuthenticationRequired = "AUTHENTICATION_REQUIRED",
|
||||||
|
MissingScopes = "MISSING_SCOPES",
|
||||||
|
GenericApiError = "GENERIC_API_ERROR",
|
||||||
|
UserNotFound = "USER_NOT_FOUND",
|
||||||
|
MemberNotFound = "MEMBER_NOT_FOUND",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ValidationError = {
|
||||||
|
message: string;
|
||||||
|
min_length?: number;
|
||||||
|
max_length?: number;
|
||||||
|
actual_length?: number;
|
||||||
|
allowed_values?: any[];
|
||||||
|
actual_value?: any;
|
||||||
|
};
|
|
@ -7,3 +7,7 @@ export type User = {
|
||||||
avatar_url: string | null;
|
avatar_url: string | null;
|
||||||
links: string[];
|
links: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UserSettings = {
|
||||||
|
dark_mode: boolean | null;
|
||||||
|
};
|
66
Foxnouns.Frontend/app/lib/request.server.ts
Normal file
66
Foxnouns.Frontend/app/lib/request.server.ts
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import { parse as parseCookie, serialize as serializeCookie } from "cookie";
|
||||||
|
import { API_BASE } from "~/env.server";
|
||||||
|
import { ApiError, ErrorCode } from "./api/error";
|
||||||
|
|
||||||
|
export type RequestParams = {
|
||||||
|
token?: string;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
body?: any;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function serverRequest<T>(
|
||||||
|
method: string,
|
||||||
|
path: string,
|
||||||
|
params: RequestParams = {},
|
||||||
|
) {
|
||||||
|
const url = `${API_BASE}/v2${path}`;
|
||||||
|
const resp = await fetch(url, {
|
||||||
|
method,
|
||||||
|
body: params.body ? JSON.stringify(params.body) : undefined,
|
||||||
|
headers: {
|
||||||
|
...params.headers,
|
||||||
|
...(params.token ? { Authorization: params.token } : {}),
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resp.headers.get("Content-Type")?.indexOf("application/json") === -1) {
|
||||||
|
// If we don't get a JSON response, the server almost certainly encountered an internal error it couldn't recover from
|
||||||
|
// (that, or the reverse proxy, which should also be treated as a 500 error)
|
||||||
|
throw {
|
||||||
|
status: 500,
|
||||||
|
code: ErrorCode.InternalServerError,
|
||||||
|
message: "Internal server error",
|
||||||
|
} as ApiError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp.status < 200 || resp.status >= 400)
|
||||||
|
throw (await resp.json()) as ApiError;
|
||||||
|
return (await resp.json()) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCookie(
|
||||||
|
req: Request,
|
||||||
|
cookieName: string,
|
||||||
|
): string | undefined {
|
||||||
|
const header = req.headers.get("Cookie");
|
||||||
|
if (!header) return undefined;
|
||||||
|
|
||||||
|
const cookie = parseCookie(header);
|
||||||
|
return cookieName in cookie ? cookie[cookieName] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const YEAR = 365 * 86400;
|
||||||
|
|
||||||
|
export const writeCookie = (
|
||||||
|
cookieName: string,
|
||||||
|
value: string,
|
||||||
|
maxAge: number | undefined = YEAR,
|
||||||
|
) =>
|
||||||
|
serializeCookie(cookieName, value, {
|
||||||
|
maxAge,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "lax",
|
||||||
|
httpOnly: true,
|
||||||
|
});
|
23
Foxnouns.Frontend/app/lib/settings.server.ts
Normal file
23
Foxnouns.Frontend/app/lib/settings.server.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { UserSettings } from "./api/user";
|
||||||
|
import { getCookie } from "./request.server";
|
||||||
|
|
||||||
|
export default function getLocalSettings(req: Request): UserSettings {
|
||||||
|
const settings = { dark_mode: null } as UserSettings;
|
||||||
|
const theme = getCookie(req, "pronounscc-theme");
|
||||||
|
|
||||||
|
switch (theme) {
|
||||||
|
case "auto":
|
||||||
|
settings.dark_mode = null;
|
||||||
|
break;
|
||||||
|
case "light":
|
||||||
|
settings.dark_mode = false;
|
||||||
|
break;
|
||||||
|
case "dark":
|
||||||
|
settings.dark_mode = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
}
|
85
Foxnouns.Frontend/app/root.tsx
Normal file
85
Foxnouns.Frontend/app/root.tsx
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import {
|
||||||
|
json,
|
||||||
|
Links,
|
||||||
|
Meta as MetaComponent,
|
||||||
|
Outlet,
|
||||||
|
Scripts,
|
||||||
|
ScrollRestoration,
|
||||||
|
useLoaderData,
|
||||||
|
} from "@remix-run/react";
|
||||||
|
import { LoaderFunction } from "@remix-run/node";
|
||||||
|
import SSRProvider from "react-bootstrap/SSRProvider";
|
||||||
|
|
||||||
|
import serverRequest, { getCookie, writeCookie } from "./lib/request.server";
|
||||||
|
import Meta from "./lib/api/meta";
|
||||||
|
import Navbar from "./components/nav/Navbar";
|
||||||
|
import { User, UserSettings } from "./lib/api/user";
|
||||||
|
import { ApiError, ErrorCode } from "./lib/api/error";
|
||||||
|
|
||||||
|
import "./app.scss";
|
||||||
|
import getLocalSettings from "./lib/settings.server";
|
||||||
|
|
||||||
|
export const loader: LoaderFunction = async ({ request }) => {
|
||||||
|
const meta = await serverRequest<Meta>("GET", "/meta");
|
||||||
|
|
||||||
|
const token = getCookie(request, "pronounscc-token");
|
||||||
|
let setCookie = "";
|
||||||
|
|
||||||
|
let meUser: User | undefined;
|
||||||
|
let settings = getLocalSettings(request);
|
||||||
|
if (token) {
|
||||||
|
try {
|
||||||
|
const user = await serverRequest<User>("GET", "/users/@me", { token });
|
||||||
|
meUser = user;
|
||||||
|
|
||||||
|
settings = await serverRequest<UserSettings>(
|
||||||
|
"GET",
|
||||||
|
"/users/@me/settings",
|
||||||
|
{ token },
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// If we get an unauthorized error, clear the token, as it's not valid anymore.
|
||||||
|
if ((e as ApiError).code === ErrorCode.AuthenticationRequired) {
|
||||||
|
setCookie = writeCookie("pronounscc-token", token, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json(
|
||||||
|
{ meta, meUser, settings },
|
||||||
|
{
|
||||||
|
headers: { "Set-Cookie": setCookie },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
const { settings } = useLoaderData<typeof loader>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html lang="en" data-bs-theme={settings.dark_mode ? "dark" : "light"}>
|
||||||
|
<head>
|
||||||
|
<meta charSet="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<MetaComponent />
|
||||||
|
<Links />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<SSRProvider>{children}</SSRProvider>
|
||||||
|
<ScrollRestoration />
|
||||||
|
<Scripts />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const { meta, meUser, settings } = useLoaderData<typeof loader>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Navbar meta={meta} user={meUser} settings={settings} />
|
||||||
|
<Outlet />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
30
Foxnouns.Frontend/app/routes/$username/route.tsx
Normal file
30
Foxnouns.Frontend/app/routes/$username/route.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { json, LoaderFunction, MetaFunction } from "@remix-run/node";
|
||||||
|
import { redirect, useLoaderData } from "@remix-run/react";
|
||||||
|
import { User } from "~/lib/api/user";
|
||||||
|
import serverRequest from "~/lib/request.server";
|
||||||
|
|
||||||
|
export const meta: MetaFunction<typeof loader> = ({ data }) => {
|
||||||
|
const { user } = data!;
|
||||||
|
|
||||||
|
return [{ title: `@${user.username} - pronouns.cc` }];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loader: LoaderFunction = async ({ params }) => {
|
||||||
|
let username = params.username!;
|
||||||
|
if (!username.startsWith("@")) throw redirect(`/@${username}`);
|
||||||
|
username = username.substring("@".length);
|
||||||
|
|
||||||
|
const user = await serverRequest<User>("GET", `/users/${username}`);
|
||||||
|
|
||||||
|
return json({ user });
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function UserPage() {
|
||||||
|
const { user } = useLoaderData<typeof loader>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
hello! this is the user page for @{user.username}. their ID is {user.id}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
45
Foxnouns.Frontend/app/routes/_index.tsx
Normal file
45
Foxnouns.Frontend/app/routes/_index.tsx
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import type { MetaFunction } from "@remix-run/node";
|
||||||
|
|
||||||
|
export const meta: MetaFunction = () => {
|
||||||
|
return [{ title: "pronouns.cc" }];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
return (
|
||||||
|
<div className="font-sans p-4">
|
||||||
|
<h1 className="text-3xl">Welcome to Remix</h1>
|
||||||
|
<ul className="list-disc mt-4 pl-6 space-y-2">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className="text-blue-700 underline visited:text-purple-900"
|
||||||
|
target="_blank"
|
||||||
|
href="https://remix.run/start/quickstart"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
5m Quick Start
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className="text-blue-700 underline visited:text-purple-900"
|
||||||
|
target="_blank"
|
||||||
|
href="https://remix.run/start/tutorial"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
30m Tutorial
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className="text-blue-700 underline visited:text-purple-900"
|
||||||
|
target="_blank"
|
||||||
|
href="https://remix.run/docs"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Remix Docs
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
31
Foxnouns.Frontend/app/routes/dark-mode/route.tsx
Normal file
31
Foxnouns.Frontend/app/routes/dark-mode/route.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { ActionFunction } from "@remix-run/node";
|
||||||
|
import { UserSettings } from "~/lib/api/user";
|
||||||
|
import serverRequest, { getCookie, writeCookie } from "~/lib/request.server";
|
||||||
|
|
||||||
|
// Handles theme switching
|
||||||
|
// Remix itself handles redirecting back to the original page after the setting is set
|
||||||
|
export const action: ActionFunction = async ({ request }) => {
|
||||||
|
const body = await request.formData();
|
||||||
|
const theme = (body.get("theme") as string | null) || "auto";
|
||||||
|
|
||||||
|
const token = getCookie(request, "pronounscc-token");
|
||||||
|
if (token) {
|
||||||
|
await serverRequest<UserSettings>("PATCH", "/users/@me/settings", {
|
||||||
|
token,
|
||||||
|
body: {
|
||||||
|
dark_mode: theme === "auto" ? null : theme === "dark" ? true : false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(null, {
|
||||||
|
status: 204,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(null, {
|
||||||
|
headers: {
|
||||||
|
"Set-Cookie": writeCookie("pronounscc-theme", theme),
|
||||||
|
},
|
||||||
|
status: 204,
|
||||||
|
});
|
||||||
|
};
|
|
@ -1,33 +0,0 @@
|
||||||
import js from '@eslint/js';
|
|
||||||
import ts from 'typescript-eslint';
|
|
||||||
import svelte from 'eslint-plugin-svelte';
|
|
||||||
import prettier from 'eslint-config-prettier';
|
|
||||||
import globals from 'globals';
|
|
||||||
|
|
||||||
/** @type {import('eslint').Linter.FlatConfig[]} */
|
|
||||||
export default [
|
|
||||||
js.configs.recommended,
|
|
||||||
...ts.configs.recommended,
|
|
||||||
...svelte.configs['flat/recommended'],
|
|
||||||
prettier,
|
|
||||||
...svelte.configs['flat/prettier'],
|
|
||||||
{
|
|
||||||
languageOptions: {
|
|
||||||
globals: {
|
|
||||||
...globals.browser,
|
|
||||||
...globals.node
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ['**/*.svelte'],
|
|
||||||
languageOptions: {
|
|
||||||
parserOptions: {
|
|
||||||
parser: ts.parser
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ignores: ['build/', '.svelte-kit/', 'dist/']
|
|
||||||
}
|
|
||||||
];
|
|
|
@ -1,40 +1,58 @@
|
||||||
{
|
{
|
||||||
"name": "foxnouns.frontend",
|
"name": "foxnouns-fe",
|
||||||
"version": "0.0.1",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"sideEffects": false,
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"build": "remix vite:build",
|
||||||
"build": "vite build",
|
"dev": "node ./server.js",
|
||||||
"preview": "vite preview",
|
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"start": "cross-env NODE_ENV=production node ./server.js",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"typecheck": "tsc",
|
||||||
"lint": "prettier --check . && eslint .",
|
"format": "prettier -w ."
|
||||||
"format": "prettier --write ."
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@remix-run/express": "^2.11.2",
|
||||||
|
"@remix-run/node": "^2.11.2",
|
||||||
|
"@remix-run/react": "^2.11.2",
|
||||||
|
"@remix-run/serve": "^2.11.2",
|
||||||
|
"bootstrap": "^5.3.3",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
|
"compression": "^1.7.4",
|
||||||
|
"cookie": "^0.6.0",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"express": "^4.19.2",
|
||||||
|
"isbot": "^4.1.0",
|
||||||
|
"morgan": "^1.10.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-bootstrap": "^2.10.4",
|
||||||
|
"react-bootstrap-icons": "^1.11.4",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@fontsource/firago": "^5.0.11",
|
"@fontsource/firago": "^5.0.11",
|
||||||
"@sveltejs/adapter-node": "^5.0.1",
|
"@remix-run/dev": "^2.11.2",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@types/compression": "^1.7.5",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
"@types/cookie": "^0.6.0",
|
||||||
"@sveltestrap/sveltestrap": "^6.2.7",
|
"@types/express": "^4.17.21",
|
||||||
"@tabler/icons-svelte": "^3.5.0",
|
"@types/morgan": "^1.9.9",
|
||||||
"@types/eslint": "^8.56.7",
|
"@types/react": "^18.2.20",
|
||||||
"bootstrap-icons": "^1.11.3",
|
"@types/react-dom": "^18.2.7",
|
||||||
"bulma": "^1.0.1",
|
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||||
"eslint": "^9.0.0",
|
"@typescript-eslint/parser": "^6.7.4",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint": "^8.38.0",
|
||||||
"eslint-plugin-svelte": "^2.36.0",
|
"eslint-import-resolver-typescript": "^3.6.1",
|
||||||
"globals": "^15.0.0",
|
"eslint-plugin-import": "^2.28.1",
|
||||||
"prettier": "^3.1.1",
|
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||||
"prettier-plugin-svelte": "^3.1.2",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"sass": "^1.77.4",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"svelte": "^4.2.7",
|
"prettier": "^3.3.3",
|
||||||
"svelte-check": "^3.6.0",
|
"sass": "^1.78.0",
|
||||||
"tslib": "^2.4.1",
|
"typescript": "^5.1.6",
|
||||||
"typescript": "^5.0.0",
|
"vite": "^5.1.0",
|
||||||
"typescript-eslint": "^8.0.0-alpha.20",
|
"vite-tsconfig-paths": "^4.2.1"
|
||||||
"vite": "^5.0.3"
|
|
||||||
},
|
},
|
||||||
"type": "module",
|
"engines": {
|
||||||
"dependencies": {}
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
BIN
Foxnouns.Frontend/public/favicon.ico
Normal file
BIN
Foxnouns.Frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
52
Foxnouns.Frontend/server.js
Normal file
52
Foxnouns.Frontend/server.js
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import { env } from "node:process";
|
||||||
|
import { createRequestHandler } from "@remix-run/express";
|
||||||
|
import compression from "compression";
|
||||||
|
import express from "express";
|
||||||
|
import morgan from "morgan";
|
||||||
|
|
||||||
|
const viteDevServer =
|
||||||
|
env.NODE_ENV === "production"
|
||||||
|
? undefined
|
||||||
|
: await import("vite").then((vite) =>
|
||||||
|
vite.createServer({
|
||||||
|
server: { middlewareMode: true },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const remixHandler = createRequestHandler({
|
||||||
|
build: viteDevServer
|
||||||
|
? () => viteDevServer.ssrLoadModule("virtual:remix/server-build")
|
||||||
|
: await import("./build/server/index.js"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use(compression());
|
||||||
|
|
||||||
|
// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
|
||||||
|
app.disable("x-powered-by");
|
||||||
|
|
||||||
|
// handle asset requests
|
||||||
|
if (viteDevServer) {
|
||||||
|
app.use(viteDevServer.middlewares);
|
||||||
|
} else {
|
||||||
|
// Vite fingerprints its assets so we can cache forever.
|
||||||
|
app.use(
|
||||||
|
"/assets",
|
||||||
|
express.static("build/client/assets", { immutable: true, maxAge: "1y" }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything else (like favicon.ico) is cached for an hour. You may want to be
|
||||||
|
// more aggressive with this caching.
|
||||||
|
app.use(express.static("build/client", { maxAge: "1h" }));
|
||||||
|
|
||||||
|
app.use(morgan("tiny"));
|
||||||
|
|
||||||
|
// handle SSR requests
|
||||||
|
app.all("*", remixHandler);
|
||||||
|
|
||||||
|
const port = env.PORT || 3000;
|
||||||
|
app.listen(port, () =>
|
||||||
|
console.log(`Express server listening at http://localhost:${port}`),
|
||||||
|
);
|
16
Foxnouns.Frontend/src/app.d.ts
vendored
16
Foxnouns.Frontend/src/app.d.ts
vendored
|
@ -1,16 +0,0 @@
|
||||||
// See https://kit.svelte.dev/docs/types#app
|
|
||||||
// for information about these interfaces
|
|
||||||
declare global {
|
|
||||||
namespace App {
|
|
||||||
// interface Error {}
|
|
||||||
// interface Locals {}
|
|
||||||
interface Locals {
|
|
||||||
token?: string;
|
|
||||||
}
|
|
||||||
// interface PageData {}
|
|
||||||
// interface PageState {}
|
|
||||||
// interface Platform {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {};
|
|
|
@ -1,19 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<script>
|
|
||||||
let theme = localStorage.getItem("pronounscc-theme");
|
|
||||||
if (!theme)
|
|
||||||
theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
||||||
|
|
||||||
document.documentElement.setAttribute("data-theme", theme);
|
|
||||||
</script>
|
|
||||||
%sveltekit.head%
|
|
||||||
</head>
|
|
||||||
<body data-sveltekit-preload-data="hover">
|
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,8 +0,0 @@
|
||||||
@use "bulma/sass" with (
|
|
||||||
$family-primary: "FiraGO"
|
|
||||||
);
|
|
||||||
|
|
||||||
@import "@fontsource/firago/400.css";
|
|
||||||
@import "@fontsource/firago/400-italic.css";
|
|
||||||
@import "@fontsource/firago/700.css";
|
|
||||||
@import "bootstrap-icons/font/bootstrap-icons.css";
|
|
|
@ -1,73 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Internal error occurred</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-size: 1.2em;
|
|
||||||
font-family: sans-serif;
|
|
||||||
margin: 40px auto;
|
|
||||||
max-width: 650px;
|
|
||||||
|
|
||||||
background-color: #ffffff;
|
|
||||||
color: #212529;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3 {
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:link,
|
|
||||||
a:visited {
|
|
||||||
color: #0d6efd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
color: rgba(33, 37, 41, 0.75);
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
body {
|
|
||||||
background-color: #212529;
|
|
||||||
color: #adb5bd;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:link,
|
|
||||||
a:visited {
|
|
||||||
color: #6ea8fe;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
color: rgba(173, 181, 189, 0.75);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
<p class="logo">
|
|
||||||
<img src="/logo.svg" alt="pronouns.cc logo" width="50%" />
|
|
||||||
</p>
|
|
||||||
<h1>Internal error occurred</h1>
|
|
||||||
<p>An internal error has occurred. Don't worry, it's (probably) not your fault.</p>
|
|
||||||
<p>
|
|
||||||
If this is the first time this is happening, try reloading the page. Otherwise, check the
|
|
||||||
<a href="https://status.pronouns.cc/" target="_blank">status page</a> for updates.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<p class="info">
|
|
||||||
<strong>Status:</strong> %sveltekit.status%<br />
|
|
||||||
<strong>Error message:</strong> %sveltekit.error.message%
|
|
||||||
</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { PRIVATE_API_BASE } from "$env/static/private";
|
|
||||||
import { PUBLIC_API_BASE } from "$env/static/public";
|
|
||||||
|
|
||||||
export async function handle({ event, resolve }) {
|
|
||||||
event.locals.token = event.cookies.get("pronounscc-token");
|
|
||||||
return await resolve(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleFetch({ event, request, fetch }) {
|
|
||||||
if (request.url.startsWith(PUBLIC_API_BASE))
|
|
||||||
request = new Request(request.url.replace(PUBLIC_API_BASE, PRIVATE_API_BASE), request);
|
|
||||||
if (event.locals.token) request.headers.set("Authorization", event.locals.token);
|
|
||||||
|
|
||||||
return fetch(request);
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
import type { User } from "./user";
|
|
||||||
|
|
||||||
export type CallbackRequest = {
|
|
||||||
code: string;
|
|
||||||
state: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CallbackResponse = {
|
|
||||||
has_account: boolean;
|
|
||||||
ticket: string;
|
|
||||||
remote_username: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AuthResponse = {
|
|
||||||
user: User;
|
|
||||||
token: string;
|
|
||||||
expires_at: string;
|
|
||||||
};
|
|
|
@ -1 +0,0 @@
|
||||||
// place files you want to import through the `$lib` alias in this folder.
|
|
|
@ -1,11 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
let isOpen = false;
|
|
||||||
export let right = false;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class={"navbar-item has-dropdown" + (isOpen ? " is-active" : "") + (right ? " is-right" : "")}>
|
|
||||||
<button class="navbar-link" on:click={() => (isOpen = !isOpen)}><slot name="label" /></button>
|
|
||||||
<div class="navbar-dropdown">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,10 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
export let divider = false;
|
|
||||||
export let href: string | undefined = undefined;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if divider}
|
|
||||||
<hr class="navbar-divider" />
|
|
||||||
{:else}
|
|
||||||
<a {href} class="navbar-item"><slot /></a>
|
|
||||||
{/if}
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 7.9 KiB |
|
@ -1,66 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { IconSun, IconMoon } from "@tabler/icons-svelte";
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
import Dropdown from "./Dropdown.svelte";
|
|
||||||
import DropdownItem from "./DropdownItem.svelte";
|
|
||||||
import Logo from "./Logo.svelte";
|
|
||||||
import type { User } from "$lib/api/user";
|
|
||||||
import { themeStore } from "$lib/store";
|
|
||||||
|
|
||||||
export let user: User | undefined;
|
|
||||||
let navIsOpen = false;
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
let theme = localStorage.getItem("pronounscc-theme");
|
|
||||||
if (!theme)
|
|
||||||
theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
||||||
themeStore.set(theme);
|
|
||||||
});
|
|
||||||
|
|
||||||
const toggleTheme = () => {
|
|
||||||
themeStore.set($themeStore === "dark" ? "light" : "dark");
|
|
||||||
document.documentElement.setAttribute("data-theme", $themeStore);
|
|
||||||
localStorage.setItem("pronounscc-theme", $themeStore);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<nav class="navbar" aria-label="Main navigation">
|
|
||||||
<div class="navbar-brand">
|
|
||||||
<a href="/" class="navbar-item">
|
|
||||||
<Logo />
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
class={"navbar-burger" + (navIsOpen ? " is-active" : "")}
|
|
||||||
aria-label="menu"
|
|
||||||
aria-expanded="false"
|
|
||||||
on:click={() => (navIsOpen = !navIsOpen)}
|
|
||||||
>
|
|
||||||
<span aria-hidden="true"></span>
|
|
||||||
<span aria-hidden="true"></span>
|
|
||||||
<span aria-hidden="true"></span>
|
|
||||||
<span aria-hidden="true"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class={"navbar-menu" + (navIsOpen ? " is-active" : "")}>
|
|
||||||
<div class="navbar-end">
|
|
||||||
{#if user}
|
|
||||||
<Dropdown>
|
|
||||||
<span slot="label">@{user.username}</span>
|
|
||||||
<DropdownItem href="/@{user.username}">View profile</DropdownItem>
|
|
||||||
<DropdownItem href="/settings">Settings</DropdownItem>
|
|
||||||
<DropdownItem divider />
|
|
||||||
<DropdownItem href="/auth/logout">Log out</DropdownItem>
|
|
||||||
</Dropdown>
|
|
||||||
{:else}
|
|
||||||
<a href="/auth/login" class="navbar-item">Log in or sign up</a>
|
|
||||||
{/if}
|
|
||||||
<button class="navbar-item" on:click={() => toggleTheme()}>
|
|
||||||
{#if $themeStore === "dark"}
|
|
||||||
<IconSun size={20} /> Light theme
|
|
||||||
{:else}
|
|
||||||
<IconMoon size={20} /> Dark theme
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
|
@ -1,72 +0,0 @@
|
||||||
import { PUBLIC_API_BASE } from "$env/static/public";
|
|
||||||
|
|
||||||
export type RequestParams = {
|
|
||||||
token?: string;
|
|
||||||
body?: any;
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch a path from the API and parse the response.
|
|
||||||
* To make sure the request is authenticated in load functions,
|
|
||||||
* pass `fetch` from the request object into opts.
|
|
||||||
*
|
|
||||||
* @param fetchFn A function like `fetch`, such as from the `load` function
|
|
||||||
* @param method The HTTP method, i.e. GET, POST, PATCH
|
|
||||||
* @param path The path to request, minus the leading `/api/v2`
|
|
||||||
* @param params Extra options for this request
|
|
||||||
* @returns T
|
|
||||||
* @throws APIError
|
|
||||||
*/
|
|
||||||
export default async function request<T>(
|
|
||||||
fetchFn: typeof fetch,
|
|
||||||
method: string,
|
|
||||||
path: string,
|
|
||||||
params: RequestParams = {},
|
|
||||||
) {
|
|
||||||
const url = `${PUBLIC_API_BASE}/v2${path}`;
|
|
||||||
const resp = await fetchFn(url, {
|
|
||||||
method,
|
|
||||||
body: params.body ? JSON.stringify(params.body) : undefined,
|
|
||||||
headers: {
|
|
||||||
...params.headers,
|
|
||||||
...(params.token ? { Authorization: params.token } : {}),
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (resp.status < 200 || resp.status >= 400) throw await resp.json();
|
|
||||||
return (await resp.json()) as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch a path from the API and discard the response.
|
|
||||||
* To make sure the request is authenticated in load functions,
|
|
||||||
* pass `fetch` from the request object into opts.
|
|
||||||
*
|
|
||||||
* @param fetchFn A function like `fetch`, such as from the `load` function
|
|
||||||
* @param method The HTTP method, i.e. GET, POST, PATCH
|
|
||||||
* @param path The path to request, minus the leading `/api/v2`
|
|
||||||
* @param params Extra options for this request
|
|
||||||
* @returns T
|
|
||||||
* @throws APIError
|
|
||||||
*/
|
|
||||||
export async function fastRequest(
|
|
||||||
fetchFn: typeof fetch,
|
|
||||||
method: string,
|
|
||||||
path: string,
|
|
||||||
params: RequestParams = {},
|
|
||||||
): Promise<void> {
|
|
||||||
const url = `${PUBLIC_API_BASE}/v2${path}`;
|
|
||||||
const resp = await fetchFn(url, {
|
|
||||||
method,
|
|
||||||
body: params.body ? JSON.stringify(params.body) : undefined,
|
|
||||||
headers: {
|
|
||||||
...params.headers,
|
|
||||||
...(params.token ? { Authorization: params.token } : {}),
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (resp.status < 200 || resp.status >= 400) throw await resp.json();
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { writable } from "svelte/store";
|
|
||||||
import { browser } from "$app/environment";
|
|
||||||
|
|
||||||
const defaultThemeValue = "light";
|
|
||||||
const initialThemeValue = browser
|
|
||||||
? window.localStorage.getItem("pronouns-theme") ?? defaultThemeValue
|
|
||||||
: defaultThemeValue;
|
|
||||||
|
|
||||||
export const themeStore = writable<string>(initialThemeValue);
|
|
|
@ -1,14 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { page } from "$app/stores";
|
|
||||||
import neofox from "./neofox_confused_2048.png";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if $page.status === 404}
|
|
||||||
<div class="has-text-centered">
|
|
||||||
<img src={neofox} alt="A very confused-looking fox" width="25%" />
|
|
||||||
<h1 class="title">Not found</h1>
|
|
||||||
<p>Our foxes can't find the page you're looking for, sorry!</p>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
div.has-text-centered
|
|
||||||
{/if}
|
|
|
@ -1,13 +0,0 @@
|
||||||
import type Meta from "$lib/api/meta";
|
|
||||||
import type { User } from "$lib/api/user";
|
|
||||||
import request from "$lib/request";
|
|
||||||
|
|
||||||
export async function load({ fetch, locals }) {
|
|
||||||
const meta = await request<Meta>(fetch, "GET", "/meta");
|
|
||||||
let user: User | undefined;
|
|
||||||
try {
|
|
||||||
user = await request<User>(fetch, "GET", "/users/@me");
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
return { meta, currentUser: user, token: locals.token };
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import type { LayoutData } from "./$types";
|
|
||||||
import "../app.scss";
|
|
||||||
import Navbar from "$lib/nav/Navbar.svelte";
|
|
||||||
|
|
||||||
export let data: LayoutData;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Navbar user={data.currentUser} />
|
|
||||||
<slot />
|
|
|
@ -1,24 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import type { PageData } from "./$types";
|
|
||||||
export let data: PageData;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<title>pronouns.cc</title>
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<h1 class="title">Welcome to SvelteKit</h1>
|
|
||||||
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
are you logged in? {data.currentUser !== undefined}
|
|
||||||
{#if data.currentUser}
|
|
||||||
<br />hello, {data.currentUser.username}!
|
|
||||||
<br />your ID: {data.currentUser.id}
|
|
||||||
{/if}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>stats:</strong>
|
|
||||||
{data.meta.users.total} users, {data.meta.members} members
|
|
||||||
</p>
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { error } from "@sveltejs/kit";
|
|
||||||
import type { User } from "$lib/api/user";
|
|
||||||
import request from "$lib/request";
|
|
||||||
|
|
||||||
export const load = async ({ params, fetch }) => {
|
|
||||||
try {
|
|
||||||
const user = await request<User>(fetch, "GET", `/users/${params.username}`);
|
|
||||||
return { user };
|
|
||||||
} catch {
|
|
||||||
error(404, { message: "User not found" });
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,10 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import type { PageData } from "./$types";
|
|
||||||
export let data: PageData;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<title>@{data.user.username} • pronouns.cc</title>
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<h1>this is the user page for @{data.user.username}</h1>
|
|
|
@ -1,12 +0,0 @@
|
||||||
import request from "$lib/request.js";
|
|
||||||
|
|
||||||
type UrlsResponse = {
|
|
||||||
discord: string | null;
|
|
||||||
google: string | null;
|
|
||||||
tumblr: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const load = async ({ fetch }) => {
|
|
||||||
const urls = await request<UrlsResponse>(fetch, "POST", "/auth/urls");
|
|
||||||
return { urls };
|
|
||||||
};
|
|
|
@ -1,53 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import type { PageData } from "./$types";
|
|
||||||
export let data: PageData;
|
|
||||||
$: hasUrls = !!(data.urls.discord || data.urls.google || data.urls.tumblr);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="container mt-6">
|
|
||||||
<div class="fixed-grid has-1-cols has-2-cols-desktop">
|
|
||||||
<div class="grid">
|
|
||||||
<div class="cell">
|
|
||||||
<p class="title">Log in with email address</p>
|
|
||||||
<form method="POST" action="?/login">
|
|
||||||
<div class="field">
|
|
||||||
<label for="email" class="label">Email address</label>
|
|
||||||
<div class="control">
|
|
||||||
<input type="email" id="email" class="input" placeholder="Email address" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="password" class="label">Password</label>
|
|
||||||
<div class="control">
|
|
||||||
<input type="password" id="password" class="input" placeholder="Password" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field is-grouped">
|
|
||||||
<div class="control">
|
|
||||||
<button class="button is-primary">Log in</button>
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<a href="/auth/signup" class="button">Sign up</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{#if hasUrls}
|
|
||||||
<div class="cell">
|
|
||||||
<p class="title">Log in with third-party provider</p>
|
|
||||||
<ul>
|
|
||||||
{#if data.urls.discord}
|
|
||||||
<li><a href={data.urls.discord}>Log in with Discord</a></li>
|
|
||||||
{/if}
|
|
||||||
{#if data.urls.google}
|
|
||||||
<li><a href={data.urls.google}>Log in with Google</a></li>
|
|
||||||
{/if}
|
|
||||||
{#if data.urls.tumblr}
|
|
||||||
<li><a href={data.urls.tumblr}>Log in with Tumblr</a></li>
|
|
||||||
{/if}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,51 +0,0 @@
|
||||||
import request from "$lib/request";
|
|
||||||
import type { AuthResponse, CallbackResponse } from "$lib/api/auth";
|
|
||||||
|
|
||||||
export const load = async ({ fetch, url, cookies, parent }) => {
|
|
||||||
const data = await parent();
|
|
||||||
if (data.user) {
|
|
||||||
return { loggedIn: true, token: data.token, user: data.user };
|
|
||||||
}
|
|
||||||
|
|
||||||
const resp = await request<AuthResponse | CallbackResponse>(
|
|
||||||
fetch,
|
|
||||||
"POST",
|
|
||||||
"/auth/discord/callback",
|
|
||||||
{
|
|
||||||
body: {
|
|
||||||
code: url.searchParams.get("code"),
|
|
||||||
state: url.searchParams.get("state"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if ("token" in resp) {
|
|
||||||
const authResp = resp as AuthResponse;
|
|
||||||
cookies.set("pronounscc-token", authResp.token, { path: "/" });
|
|
||||||
return { loggedIn: true, token: authResp.token, user: authResp.user };
|
|
||||||
}
|
|
||||||
|
|
||||||
const callbackResp = resp as CallbackResponse;
|
|
||||||
return {
|
|
||||||
loggedIn: false,
|
|
||||||
hasAccount: callbackResp.has_account,
|
|
||||||
ticket: resp.ticket,
|
|
||||||
remoteUsername: resp.remote_username,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const actions = {
|
|
||||||
register: async ({ cookies, request: req, fetch, locals }) => {
|
|
||||||
const data = await req.formData();
|
|
||||||
const username = data.get("username");
|
|
||||||
const ticket = data.get("ticket");
|
|
||||||
|
|
||||||
const resp = await request<AuthResponse>(fetch, "POST", "/auth/discord/register", {
|
|
||||||
body: { username, ticket },
|
|
||||||
});
|
|
||||||
cookies.set("pronounscc-token", resp.token, { path: "/" });
|
|
||||||
locals.token = resp.token;
|
|
||||||
|
|
||||||
return { token: resp.token, user: resp.user };
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,79 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
import { goto } from "$app/navigation";
|
|
||||||
import { enhance } from "$app/forms";
|
|
||||||
import type { PageData, ActionData } from "./$types";
|
|
||||||
export let data: PageData;
|
|
||||||
|
|
||||||
export let form: ActionData;
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
if (data.user) {
|
|
||||||
await new Promise((r) => setTimeout(r, 3000));
|
|
||||||
await goto(`/@${data.user.username}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const redirectOnForm = async (action: ActionData) => {
|
|
||||||
if (form?.user) {
|
|
||||||
await new Promise((r) => setTimeout(r, 3000));
|
|
||||||
await goto(`/@${form.user.username}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$: redirectOnForm(form);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
{#if form?.user}
|
|
||||||
<h1 class="title">Successfully created account!</h1>
|
|
||||||
<p>Welcome, <strong>@{form.user.username}</strong>!</p>
|
|
||||||
<p>
|
|
||||||
You should automatically be redirected to your profile in a few seconds. If you're not
|
|
||||||
redirected, please press the link above.
|
|
||||||
</p>
|
|
||||||
{:else if data.loggedIn}
|
|
||||||
<h1 class="title">Successfully logged in!</h1>
|
|
||||||
<p>You are now logged in as <a href="/@{data.user?.username}">@{data.user?.username}</a>.</p>
|
|
||||||
<p>
|
|
||||||
You should automatically be redirected to your profile in a few seconds. If you're not
|
|
||||||
redirected, please press the link above.
|
|
||||||
</p>
|
|
||||||
{:else}
|
|
||||||
<h1 class="title">Finish signing up with a Discord account</h1>
|
|
||||||
<form method="POST" action="?/register" use:enhance>
|
|
||||||
<div class="field">
|
|
||||||
<label for="remote_username" class="label">Discord username</label>
|
|
||||||
<div class="control">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="remote_username"
|
|
||||||
id="remote_username"
|
|
||||||
class="input"
|
|
||||||
value={data.remoteUsername}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="username" class="label">Username</label>
|
|
||||||
<div class="control">
|
|
||||||
<input type="text" name="username" id="username" class="input" placeholder="Username" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="ticket"
|
|
||||||
id="ticket"
|
|
||||||
class="hidden"
|
|
||||||
style="display: hidden;"
|
|
||||||
value={data.ticket}
|
|
||||||
/>
|
|
||||||
<div class="field">
|
|
||||||
<div class="control">
|
|
||||||
<button class="button is-primary">Sign up</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
Binary file not shown.
Before Width: | Height: | Size: 143 KiB |
Binary file not shown.
Before Width: | Height: | Size: 43 KiB |
|
@ -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 |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 7.9 KiB |
|
@ -1,5 +0,0 @@
|
||||||
User-agent: *
|
|
||||||
Disallow: /@*
|
|
||||||
Disallow: /auth
|
|
||||||
Disallow: /settings
|
|
||||||
Disallow: /edit
|
|
|
@ -1,24 +0,0 @@
|
||||||
import adapter from "@sveltejs/adapter-node";
|
|
||||||
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
|
||||||
const config = {
|
|
||||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
|
||||||
// for more information about preprocessors
|
|
||||||
preprocess: vitePreprocess(),
|
|
||||||
|
|
||||||
kit: {
|
|
||||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
|
||||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
|
||||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
|
||||||
adapter: adapter(),
|
|
||||||
env: {
|
|
||||||
privatePrefix: "PRIVATE_",
|
|
||||||
},
|
|
||||||
csrf: {
|
|
||||||
checkOrigin: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
|
@ -1,19 +1,32 @@
|
||||||
{
|
{
|
||||||
"extends": "./.svelte-kit/tsconfig.json",
|
"include": [
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
"**/.server/**/*.ts",
|
||||||
|
"**/.server/**/*.tsx",
|
||||||
|
"**/.client/**/*.ts",
|
||||||
|
"**/.client/**/*.tsx"
|
||||||
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"allowJs": true,
|
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||||
"checkJs": true,
|
"types": ["@remix-run/node", "vite/client"],
|
||||||
|
"isolatedModules": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"jsx": "react-jsx",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"skipLibCheck": true,
|
"target": "ES2022",
|
||||||
"sourceMap": true,
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"moduleResolution": "bundler"
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["./app/*"]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Vite takes care of building everything, not tsc.
|
||||||
|
"noEmit": true
|
||||||
}
|
}
|
||||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
|
||||||
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
|
|
||||||
//
|
|
||||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
|
||||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,24 @@
|
||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
import { vitePlugin as remix } from "@remix-run/dev";
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from "vite";
|
||||||
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [sveltekit()]
|
plugins: [
|
||||||
|
remix({
|
||||||
|
future: {
|
||||||
|
v3_fetcherPersist: true,
|
||||||
|
v3_relativeSplatPath: true,
|
||||||
|
v3_throwAbortReason: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
tsconfigPaths(),
|
||||||
|
],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
"/api": {
|
||||||
|
target: "http://localhost:5000",
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue