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 | ||||
| 
 | ||||
| /.cache | ||||
| /build | ||||
| /.svelte-kit | ||||
| /package | ||||
| .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, | ||||
| 	"printWidth": 100, | ||||
| 	"plugins": ["prettier-plugin-svelte"], | ||||
| 	"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] | ||||
| 	"useTabs": true | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
| # 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 | ||||
| ```shellscript | ||||
| 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 | ||||
| ``` | ||||
| 
 | ||||
| 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; | ||||
| 	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", | ||||
| 	"version": "0.0.1", | ||||
| 	"name": "foxnouns-fe", | ||||
| 	"private": true, | ||||
| 	"sideEffects": false, | ||||
| 	"type": "module", | ||||
| 	"scripts": { | ||||
| 		"dev": "vite dev", | ||||
| 		"build": "vite build", | ||||
| 		"preview": "vite preview", | ||||
| 		"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", | ||||
| 		"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", | ||||
| 		"lint": "prettier --check . && eslint .", | ||||
| 		"format": "prettier --write ." | ||||
| 		"build": "remix vite:build", | ||||
| 		"dev": "node ./server.js", | ||||
| 		"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", | ||||
| 		"start": "cross-env NODE_ENV=production node ./server.js", | ||||
| 		"typecheck": "tsc", | ||||
| 		"format": "prettier -w ." | ||||
| 	}, | ||||
| 	"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": { | ||||
| 		"@fontsource/firago": "^5.0.11", | ||||
| 		"@sveltejs/adapter-node": "^5.0.1", | ||||
| 		"@sveltejs/kit": "^2.0.0", | ||||
| 		"@sveltejs/vite-plugin-svelte": "^3.0.0", | ||||
| 		"@sveltestrap/sveltestrap": "^6.2.7", | ||||
| 		"@tabler/icons-svelte": "^3.5.0", | ||||
| 		"@types/eslint": "^8.56.7", | ||||
| 		"bootstrap-icons": "^1.11.3", | ||||
| 		"bulma": "^1.0.1", | ||||
| 		"eslint": "^9.0.0", | ||||
| 		"eslint-config-prettier": "^9.1.0", | ||||
| 		"eslint-plugin-svelte": "^2.36.0", | ||||
| 		"globals": "^15.0.0", | ||||
| 		"prettier": "^3.1.1", | ||||
| 		"prettier-plugin-svelte": "^3.1.2", | ||||
| 		"sass": "^1.77.4", | ||||
| 		"svelte": "^4.2.7", | ||||
| 		"svelte-check": "^3.6.0", | ||||
| 		"tslib": "^2.4.1", | ||||
| 		"typescript": "^5.0.0", | ||||
| 		"typescript-eslint": "^8.0.0-alpha.20", | ||||
| 		"vite": "^5.0.3" | ||||
| 		"@remix-run/dev": "^2.11.2", | ||||
| 		"@types/compression": "^1.7.5", | ||||
| 		"@types/cookie": "^0.6.0", | ||||
| 		"@types/express": "^4.17.21", | ||||
| 		"@types/morgan": "^1.9.9", | ||||
| 		"@types/react": "^18.2.20", | ||||
| 		"@types/react-dom": "^18.2.7", | ||||
| 		"@typescript-eslint/eslint-plugin": "^6.7.4", | ||||
| 		"@typescript-eslint/parser": "^6.7.4", | ||||
| 		"eslint": "^8.38.0", | ||||
| 		"eslint-import-resolver-typescript": "^3.6.1", | ||||
| 		"eslint-plugin-import": "^2.28.1", | ||||
| 		"eslint-plugin-jsx-a11y": "^6.7.1", | ||||
| 		"eslint-plugin-react": "^7.33.2", | ||||
| 		"eslint-plugin-react-hooks": "^4.6.0", | ||||
| 		"prettier": "^3.3.3", | ||||
| 		"sass": "^1.78.0", | ||||
| 		"typescript": "^5.1.6", | ||||
| 		"vite": "^5.1.0", | ||||
| 		"vite-tsconfig-paths": "^4.2.1" | ||||
| 	}, | ||||
| 	"type": "module", | ||||
| 	"dependencies": {} | ||||
| 	"engines": { | ||||
| 		"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": { | ||||
| 		"allowJs": true, | ||||
| 		"checkJs": true, | ||||
| 		"lib": ["DOM", "DOM.Iterable", "ES2022"], | ||||
| 		"types": ["@remix-run/node", "vite/client"], | ||||
| 		"isolatedModules": true, | ||||
| 		"esModuleInterop": true, | ||||
| 		"forceConsistentCasingInFileNames": true, | ||||
| 		"jsx": "react-jsx", | ||||
| 		"module": "ESNext", | ||||
| 		"moduleResolution": "Bundler", | ||||
| 		"resolveJsonModule": true, | ||||
| 		"skipLibCheck": true, | ||||
| 		"sourceMap": true, | ||||
| 		"target": "ES2022", | ||||
| 		"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 { defineConfig } from 'vite'; | ||||
| import { vitePlugin as remix } from "@remix-run/dev"; | ||||
| import { defineConfig } from "vite"; | ||||
| import tsconfigPaths from "vite-tsconfig-paths"; | ||||
| 
 | ||||
| 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…
	
	Add table
		Add a link
		
	
		Reference in a new issue