switch frontend to preact
This commit is contained in:
		
							parent
							
								
									6f1b94c040
								
							
						
					
					
						commit
						0a94e2bf93
					
				
					 57 changed files with 3366 additions and 1803 deletions
				
			
		|  | @ -5,7 +5,7 @@ tmp_dir = "tmp" | |||
|   args_bin = ["web"] | ||||
|   bin = "./tmp/main" | ||||
|   cmd = "go build -o ./tmp/main ." | ||||
|   delay = 0 | ||||
|   delay = 1000 | ||||
|   exclude_dir = ["node_modules", "frontend", "assets", "tmp", "vendor", "testdata", "web/frontend/assets", "web/templates"] | ||||
|   exclude_file = [] | ||||
|   exclude_regex = ["_test.go"] | ||||
|  |  | |||
							
								
								
									
										31
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										31
									
								
								README.md
									
										
									
									
									
								
							|  | @ -1 +1,32 @@ | |||
| # Mercury | ||||
| 
 | ||||
| Maybe, one day, a social media server with ActivityPub support. | ||||
| Currently mostly just me trying to make an overengineered code structure work. | ||||
| 
 | ||||
| ## Stack | ||||
| 
 | ||||
| - Database: PostgreSQL | ||||
| - Backend language: Go 1.19+ | ||||
|   - Router: [chi](https://github.com/go-chi/chi) | ||||
|   - Templates: [pongo2](https://github.com/flosch/pongo2) | ||||
|   - Command line: [cli](https://github.com/urfave/cli) | ||||
| - Frontend language: TypeScript | ||||
|   - Framework: [Preact](https://preactjs.com/) | ||||
|   - Bundler: [Vite](https://vitejs.dev/) | ||||
| 
 | ||||
| ## License | ||||
| 
 | ||||
| Copyright (C) 2023 sam \<sleepycat.moe\> | ||||
| 
 | ||||
| This program is free software: you can redistribute it and/or modify | ||||
| it under the terms of the GNU Affero General Public License as | ||||
| published by the Free Software Foundation, either version 3 of the | ||||
| License, or (at your option) any later version. | ||||
| 
 | ||||
| This program is distributed in the hope that it will be useful, | ||||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| GNU Affero General Public License for more details. | ||||
| 
 | ||||
| You should have received a copy of the GNU Affero General Public License | ||||
| along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
|  |  | |||
							
								
								
									
										32
									
								
								frontend/.eslintrc.cjs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								frontend/.eslintrc.cjs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,32 @@ | |||
| module.exports = { | ||||
| 	env: { | ||||
| 		browser: true, | ||||
| 		es2021: true, | ||||
| 	}, | ||||
| 	extends: [ | ||||
| 		"eslint:recommended", | ||||
| 		"plugin:@typescript-eslint/recommended", | ||||
| 		"plugin:react/recommended", | ||||
| 	], | ||||
| 	overrides: [ | ||||
| 		{ | ||||
| 			env: { | ||||
| 				node: true, | ||||
| 			}, | ||||
| 			files: [".eslintrc.{js,cjs}"], | ||||
| 			parserOptions: { | ||||
| 				sourceType: "script", | ||||
| 			}, | ||||
| 		}, | ||||
| 	], | ||||
| 	parser: "@typescript-eslint/parser", | ||||
| 	parserOptions: { | ||||
| 		ecmaVersion: "latest", | ||||
| 		sourceType: "module", | ||||
| 	}, | ||||
| 	plugins: ["@typescript-eslint", "react"], | ||||
| 	rules: { | ||||
| 		"react/react-in-jsx-scope": "off", | ||||
| 		"no-mixed-spaces-and-tabs": "off", | ||||
| 	}, | ||||
| }; | ||||
							
								
								
									
										24
									
								
								frontend/.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								frontend/.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | |||
| # Logs | ||||
| logs | ||||
| *.log | ||||
| npm-debug.log* | ||||
| yarn-debug.log* | ||||
| yarn-error.log* | ||||
| pnpm-debug.log* | ||||
| lerna-debug.log* | ||||
| 
 | ||||
| node_modules | ||||
| dist | ||||
| dist-ssr | ||||
| *.local | ||||
| 
 | ||||
| # Editor directories and files | ||||
| .vscode/* | ||||
| !.vscode/extensions.json | ||||
| .idea | ||||
| .DS_Store | ||||
| *.suo | ||||
| *.ntvs* | ||||
| *.njsproj | ||||
| *.sln | ||||
| *.sw? | ||||
							
								
								
									
										5
									
								
								frontend/.prettierrc
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								frontend/.prettierrc
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| { | ||||
| 	"useTabs": true, | ||||
| 	"singleQuote": false, | ||||
| 	"trailingComma": "all" | ||||
| } | ||||
|  | @ -1,47 +0,0 @@ | |||
| # Svelte + TS + Vite | ||||
| 
 | ||||
| This template should help get you started developing with Svelte and TypeScript in Vite. | ||||
| 
 | ||||
| ## Recommended IDE Setup | ||||
| 
 | ||||
| [VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). | ||||
| 
 | ||||
| ## Need an official Svelte framework? | ||||
| 
 | ||||
| Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. | ||||
| 
 | ||||
| ## Technical considerations | ||||
| 
 | ||||
| **Why use this over SvelteKit?** | ||||
| 
 | ||||
| - It brings its own routing solution which might not be preferable for some users. | ||||
| - It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. | ||||
| 
 | ||||
| This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. | ||||
| 
 | ||||
| Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate. | ||||
| 
 | ||||
| **Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?** | ||||
| 
 | ||||
| Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information. | ||||
| 
 | ||||
| **Why include `.vscode/extensions.json`?** | ||||
| 
 | ||||
| Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project. | ||||
| 
 | ||||
| **Why enable `allowJs` in the TS template?** | ||||
| 
 | ||||
| While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant. | ||||
| 
 | ||||
| **Why is HMR not preserving my local component state?** | ||||
| 
 | ||||
| HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr). | ||||
| 
 | ||||
| If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR. | ||||
| 
 | ||||
| ```ts | ||||
| // store.ts | ||||
| // An extremely simple external store | ||||
| import { writable } from "svelte/store"; | ||||
| export default writable(0); | ||||
| ``` | ||||
|  | @ -2,12 +2,11 @@ | |||
| <html lang="en"> | ||||
| 	<head> | ||||
| 		<meta charset="UTF-8" /> | ||||
| 		<link rel="icon" type="image/svg+xml" href="/vite.svg" /> | ||||
| 		<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
| 		<title>Vite + Svelte + TS</title> | ||||
| 		<title>Mercury</title> | ||||
| 	</head> | ||||
| 	<body> | ||||
| 		<div id="app"></div> | ||||
| 		<script type="module" src="/src/main.ts"></script> | ||||
| 		<script type="module" src="/src/main.tsx"></script> | ||||
| 	</body> | ||||
| </html> | ||||
|  |  | |||
|  | @ -5,25 +5,48 @@ | |||
| 	"type": "module", | ||||
| 	"scripts": { | ||||
| 		"dev": "vite", | ||||
| 		"build": "vite build", | ||||
| 		"preview": "vite preview", | ||||
| 		"check": "svelte-check --tsconfig ./tsconfig.json" | ||||
| 	}, | ||||
| 	"devDependencies": { | ||||
| 		"@sveltejs/vite-plugin-svelte": "^2.4.2", | ||||
| 		"@tsconfig/svelte": "^5.0.0", | ||||
| 		"autoprefixer": "^10.4.15", | ||||
| 		"postcss": "^8.4.29", | ||||
| 		"prettier": "^3.0.3", | ||||
| 		"sass": "^1.66.1", | ||||
| 		"svelte": "^4.0.5", | ||||
| 		"svelte-check": "^3.4.6", | ||||
| 		"tailwindcss": "^3.3.3", | ||||
| 		"tslib": "^2.6.0", | ||||
| 		"typescript": "^5.0.2", | ||||
| 		"vite": "^4.4.5" | ||||
| 		"build": "tsc && vite build", | ||||
| 		"preview": "vite preview" | ||||
| 	}, | ||||
| 	"dependencies": { | ||||
| 		"normalize.css": "^8.0.1" | ||||
| 		"@emotion/react": "^11.11.1", | ||||
| 		"@emotion/styled": "^11.11.0", | ||||
| 		"@fontsource/roboto": "^5.0.8", | ||||
| 		"@heroicons/react": "^2.0.18", | ||||
| 		"@mui/icons-material": "^5.14.9", | ||||
| 		"@mui/material": "^5.14.9", | ||||
| 		"@reduxjs/toolkit": "^1.9.5", | ||||
| 		"axios": "^1.5.0", | ||||
| 		"humanize-duration": "^3.29.0", | ||||
| 		"i18next": "^23.5.1", | ||||
| 		"luxon": "^3.4.3", | ||||
| 		"markdown-it": "^13.0.1", | ||||
| 		"preact": "^10.16.0", | ||||
| 		"react-helmet": "^6.1.0", | ||||
| 		"react-i18next": "^13.2.2", | ||||
| 		"react-redux": "^8.1.2", | ||||
| 		"redux": "^4.2.1", | ||||
| 		"sanitize-html": "^2.11.0", | ||||
| 		"ulid": "^2.3.0", | ||||
| 		"wouter-preact": "^2.11.0" | ||||
| 	}, | ||||
| 	"devDependencies": { | ||||
| 		"@preact/preset-vite": "^2.5.0", | ||||
| 		"@types/humanize-duration": "^3.27.1", | ||||
| 		"@types/luxon": "^3.3.2", | ||||
| 		"@types/markdown-it": "^13.0.1", | ||||
| 		"@types/node": "^20.6.0", | ||||
| 		"@types/react-helmet": "^6.1.6", | ||||
| 		"@types/sanitize-html": "^2.9.0", | ||||
| 		"@typescript-eslint/eslint-plugin": "^6.7.0", | ||||
| 		"@typescript-eslint/parser": "^6.7.0", | ||||
| 		"autoprefixer": "^10.4.15", | ||||
| 		"eslint": "^8.49.0", | ||||
| 		"eslint-plugin-react": "^7.33.2", | ||||
| 		"postcss": "^8.4.29", | ||||
| 		"prettier": "^3.0.3", | ||||
| 		"tailwindcss": "^3.3.3", | ||||
| 		"typescript": "^5.0.2", | ||||
| 		"vite": "^4.4.5" | ||||
| 	} | ||||
| } | ||||
|  |  | |||
							
								
								
									
										1383
									
								
								frontend/pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										1383
									
								
								frontend/pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -1,6 +1,6 @@ | |||
| export default { | ||||
|   plugins: { | ||||
|     tailwindcss: {}, | ||||
|     autoprefixer: {}, | ||||
|   }, | ||||
| } | ||||
| 	plugins: { | ||||
| 		tailwindcss: {}, | ||||
| 		autoprefixer: {}, | ||||
| 	}, | ||||
| }; | ||||
|  |  | |||
|  | @ -1,9 +0,0 @@ | |||
| <script lang="ts"> | ||||
| 	import { meAccount } from "./lib/store"; | ||||
| </script> | ||||
| 
 | ||||
| <main> | ||||
| 	<p> | ||||
| 		Username: {$meAccount.username}, ID: {$meAccount.id} | ||||
| 	</p> | ||||
| </main> | ||||
							
								
								
									
										32
									
								
								frontend/src/AppRouter.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								frontend/src/AppRouter.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,32 @@ | |||
| import { Router, Route, Switch } from "wouter-preact"; | ||||
| 
 | ||||
| import PostPage from "$pages/blog/post"; | ||||
| import HomeTimeline from "$pages/timeline/home"; | ||||
| 
 | ||||
| export default function AppRouter() { | ||||
| 	return ( | ||||
| 		<> | ||||
| 			<Router base="/web"> | ||||
| 				<Switch> | ||||
| 					{/* Posts */} | ||||
| 					<Route path="/@:username/posts/:postId"> | ||||
| 						<PostPage /> | ||||
| 					</Route> | ||||
| 
 | ||||
| 					{/* User timelines */} | ||||
| 					<Route path="/@:username/with_replies">Replies!</Route> | ||||
| 					<Route path="/@:username/media">Media!</Route> | ||||
| 					<Route path="/@:username">User!</Route> | ||||
| 
 | ||||
| 					{/* Home */} | ||||
| 					<Route path="/local">Local timeline</Route> | ||||
| 					<Route path="/global">Global timeline</Route> | ||||
| 					<Route path="/notifications">Notifications</Route> | ||||
| 					<Route path="/"> | ||||
| 						<HomeTimeline /> | ||||
| 					</Route> | ||||
| 				</Switch> | ||||
| 			</Router> | ||||
| 		</> | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										9
									
								
								frontend/src/DefaultHead.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								frontend/src/DefaultHead.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | |||
| import { Helmet } from "react-helmet"; | ||||
| 
 | ||||
| export default function DefaultHead() { | ||||
| 	return ( | ||||
| 		<Helmet> | ||||
| 			<title>Mercury</title> | ||||
| 		</Helmet> | ||||
| 	); | ||||
| } | ||||
|  | @ -1,3 +0,0 @@ | |||
| @tailwind base; | ||||
| @tailwind components; | ||||
| @tailwind utilities; | ||||
							
								
								
									
										20
									
								
								frontend/src/app.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								frontend/src/app.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| import { Provider } from "react-redux"; | ||||
| import { CssBaseline } from "@mui/material"; | ||||
| 
 | ||||
| import store from "$lib/store"; | ||||
| import Layout from "$components/layout"; | ||||
| 
 | ||||
| import DefaultHead from "./DefaultHead"; | ||||
| 
 | ||||
| export default function App() { | ||||
| 	return ( | ||||
| 		<> | ||||
| 			<CssBaseline> | ||||
| 				<Provider store={store}> | ||||
| 					<DefaultHead /> | ||||
| 					<Layout /> | ||||
| 				</Provider> | ||||
| 			</CssBaseline> | ||||
| 		</> | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										1
									
								
								frontend/src/assets/preact.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/src/assets/preact.svg
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="27.68" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 296"><path fill="#673AB8" d="m128 0l128 73.9v147.8l-128 73.9L0 221.7V73.9z"></path><path fill="#FFF" d="M34.865 220.478c17.016 21.78 71.095 5.185 122.15-34.704c51.055-39.888 80.24-88.345 63.224-110.126c-17.017-21.78-71.095-5.184-122.15 34.704c-51.055 39.89-80.24 88.346-63.224 110.126Zm7.27-5.68c-5.644-7.222-3.178-21.402 7.573-39.253c11.322-18.797 30.541-39.548 54.06-57.923c23.52-18.375 48.303-32.004 69.281-38.442c19.922-6.113 34.277-5.075 39.92 2.148c5.644 7.223 3.178 21.403-7.573 39.254c-11.322 18.797-30.541 39.547-54.06 57.923c-23.52 18.375-48.304 32.004-69.281 38.441c-19.922 6.114-34.277 5.076-39.92-2.147Z"></path><path fill="#FFF" d="M220.239 220.478c17.017-21.78-12.169-70.237-63.224-110.126C105.96 70.464 51.88 53.868 34.865 75.648c-17.017 21.78 12.169 70.238 63.224 110.126c51.055 39.889 105.133 56.485 122.15 34.704Zm-7.27-5.68c-5.643 7.224-19.998 8.262-39.92 2.148c-20.978-6.437-45.761-20.066-69.28-38.441c-23.52-18.376-42.74-39.126-54.06-57.923c-10.752-17.851-13.218-32.03-7.575-39.254c5.644-7.223 19.999-8.261 39.92-2.148c20.978 6.438 45.762 20.067 69.281 38.442c23.52 18.375 42.739 39.126 54.06 57.923c10.752 17.85 13.218 32.03 7.574 39.254Z"></path><path fill="#FFF" d="M127.552 167.667c10.827 0 19.603-8.777 19.603-19.604c0-10.826-8.776-19.603-19.603-19.603c-10.827 0-19.604 8.777-19.604 19.603c0 10.827 8.777 19.604 19.604 19.604Z"></path></svg> | ||||
| After Width: | Height: | Size: 1.6 KiB | 
|  | @ -1 +0,0 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg> | ||||
| Before Width: | Height: | Size: 1.9 KiB | 
|  | @ -1 +0,0 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg> | ||||
| Before Width: | Height: | Size: 1.5 KiB | 
							
								
								
									
										62
									
								
								frontend/src/components/layout/DesktopNav.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								frontend/src/components/layout/DesktopNav.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,62 @@ | |||
| import { Home, Notifications, People, Public } from "@mui/icons-material"; | ||||
| import { | ||||
| 	Drawer, | ||||
| 	List, | ||||
| 	ListItemButton, | ||||
| 	ListItemIcon, | ||||
| 	ListItemText, | ||||
| } from "@mui/material"; | ||||
| import { useLocation } from "wouter-preact"; | ||||
| 
 | ||||
| export default function DesktopNav() { | ||||
| 	const [location, navigate] = useLocation(); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<> | ||||
| 			<Drawer | ||||
| 				sx={{ width: 200, display: { xs: "none", lg: "block" } }} | ||||
| 				variant="permanent" | ||||
| 				anchor="left" | ||||
| 			> | ||||
| 				<List> | ||||
| 					<ListItemButton | ||||
| 						onClick={() => navigate("/web")} | ||||
| 						selected={location === "/web"} | ||||
| 					> | ||||
| 						<ListItemIcon> | ||||
| 							<Home /> | ||||
| 						</ListItemIcon> | ||||
| 						<ListItemText primary="Home" /> | ||||
| 					</ListItemButton> | ||||
| 					<ListItemButton | ||||
| 						onClick={() => navigate("/web/notifications")} | ||||
| 						selected={location === "/web/notifications"} | ||||
| 					> | ||||
| 						<ListItemIcon> | ||||
| 							<Notifications /> | ||||
| 						</ListItemIcon> | ||||
| 						<ListItemText primary="Notifications" /> | ||||
| 					</ListItemButton> | ||||
| 					<ListItemButton | ||||
| 						onClick={() => navigate("/web/local")} | ||||
| 						selected={location === "/web/local"} | ||||
| 					> | ||||
| 						<ListItemIcon> | ||||
| 							<People /> | ||||
| 						</ListItemIcon> | ||||
| 						<ListItemText primary="Local timeline" /> | ||||
| 					</ListItemButton> | ||||
| 					<ListItemButton | ||||
| 						onClick={() => navigate("/web/global")} | ||||
| 						selected={location === "/web/global"} | ||||
| 					> | ||||
| 						<ListItemIcon> | ||||
| 							<Public /> | ||||
| 						</ListItemIcon> | ||||
| 						<ListItemText primary="Global timeline" /> | ||||
| 					</ListItemButton> | ||||
| 				</List> | ||||
| 			</Drawer> | ||||
| 		</> | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										43
									
								
								frontend/src/components/layout/MobileNav.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								frontend/src/components/layout/MobileNav.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,43 @@ | |||
| import { Paper, BottomNavigation, BottomNavigationAction } from "@mui/material"; | ||||
| import { useLocation } from "wouter-preact"; | ||||
| import { Home, Notifications, People, Public } from "@mui/icons-material"; | ||||
| 
 | ||||
| export default function MobileNav() { | ||||
| 	const [location, navigate] = useLocation(); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<Paper | ||||
| 			sx={{ | ||||
| 				position: "fixed", | ||||
| 				bottom: 0, | ||||
| 				left: 0, | ||||
| 				right: 0, | ||||
| 				display: { xs: "block", lg: "none" }, | ||||
| 			}} | ||||
| 			elevation={3} | ||||
| 		> | ||||
| 			<BottomNavigation | ||||
| 				showLabels={false} | ||||
| 				value={location} | ||||
| 				onChange={(_, newValue) => navigate(newValue)} | ||||
| 			> | ||||
| 				<BottomNavigationAction icon={<Home />} label="Home" value="/web" /> | ||||
| 				<BottomNavigationAction | ||||
| 					icon={<Notifications />} | ||||
| 					label="Notifications" | ||||
| 					value="/web/notifications" | ||||
| 				/> | ||||
| 				<BottomNavigationAction | ||||
| 					icon={<People />} | ||||
| 					label="Local" | ||||
| 					value="/web/local" | ||||
| 				/> | ||||
| 				<BottomNavigationAction | ||||
| 					icon={<Public />} | ||||
| 					label="Global" | ||||
| 					value="/web/global" | ||||
| 				/> | ||||
| 			</BottomNavigation> | ||||
| 		</Paper> | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										19
									
								
								frontend/src/components/layout/index.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								frontend/src/components/layout/index.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | |||
| import { Stack } from "@mui/material"; | ||||
| 
 | ||||
| import AppRouter from "$/AppRouter"; | ||||
| import DesktopNav from "./DesktopNav"; | ||||
| import MobileNav from "./MobileNav"; | ||||
| 
 | ||||
| export default function Layout() { | ||||
| 	return ( | ||||
| 		<> | ||||
| 			<Stack direction={{ xs: "column", lg: "row" }}> | ||||
| 				<DesktopNav /> | ||||
| 				<Stack direction="row"> | ||||
| 					<AppRouter /> | ||||
| 				</Stack> | ||||
| 				<MobileNav /> | ||||
| 			</Stack> | ||||
| 		</> | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										10
									
								
								frontend/src/components/post/Post.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/components/post/Post.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| .post-Post--account { | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
| } | ||||
| 
 | ||||
| .post-Post--names { | ||||
|   margin: 0 0.5rem; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
| } | ||||
							
								
								
									
										58
									
								
								frontend/src/components/post/Post.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								frontend/src/components/post/Post.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,58 @@ | |||
| import { Avatar, Card, CardContent, Tooltip, Typography } from "@mui/material"; | ||||
| import { decodeTime } from "ulid"; | ||||
| import { DateTime } from "luxon"; | ||||
| 
 | ||||
| import type { Post } from "$lib/api/entities/post"; | ||||
| import humanizeDuration from "$/lib/duration"; | ||||
| 
 | ||||
| import "./Post.css"; | ||||
| import { renderMarkdown } from "$/lib/markdown"; | ||||
| 
 | ||||
| interface Props { | ||||
| 	post: Post; | ||||
| } | ||||
| 
 | ||||
| export default function Post(props: Props) { | ||||
| 	return ( | ||||
| 		<Card> | ||||
| 			<CardContent> | ||||
| 				<div className="post-Post--account"> | ||||
| 					<Avatar sx={{ width: 48, height: 48 }}> | ||||
| 						{props.post.blog.name.slice(0, 1).toUpperCase()} | ||||
| 					</Avatar> | ||||
| 					<div className="post-Post--names"> | ||||
| 						<Typography>{props.post.blog.name}</Typography> | ||||
| 						<Typography color="grey">@{props.post.blog.name}</Typography> | ||||
| 					</div> | ||||
| 					<Timestamp timestamp={decodeTime(props.post.id)} /> | ||||
| 				</div> | ||||
| 				<PostContent post={props.post} /> | ||||
| 			</CardContent> | ||||
| 		</Card> | ||||
| 	); | ||||
| } | ||||
| 
 | ||||
| function Timestamp({ timestamp }: { timestamp: number }) { | ||||
| 	const msAgo = new Date().getTime() - timestamp; | ||||
| 	const timeString = DateTime.fromMillis(timestamp).toLocaleString( | ||||
| 		DateTime.DATETIME_SHORT, | ||||
| 	); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<> | ||||
| 			<Tooltip title={timeString}> | ||||
| 				<Typography variant="subtitle2">{humanizeDuration(msAgo)}</Typography> | ||||
| 			</Tooltip> | ||||
| 		</> | ||||
| 	); | ||||
| } | ||||
| 
 | ||||
| function PostContent({ post, small = true }: { post: Post; small?: boolean }) { | ||||
| 	return ( | ||||
| 		<> | ||||
| 			<Typography variant={small ? "body2" : "body1"}> | ||||
| 				<p dangerouslySetInnerHTML={{ __html: renderMarkdown(post.content) }} /> | ||||
| 			</Typography> | ||||
| 		</> | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										24
									
								
								frontend/src/i18n.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								frontend/src/i18n.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | |||
| import i18n from "i18next"; | ||||
| import { initReactI18next } from "react-i18next"; | ||||
| 
 | ||||
| import en from "$/translations/en.json"; | ||||
| import enPirate from "$/translations/en.pirate.json"; | ||||
| 
 | ||||
| const resources = { | ||||
| 	en: { | ||||
| 		translation: en, | ||||
| 	}, | ||||
| 	"en-PR": { | ||||
| 		translation: enPirate, | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| i18n.use(initReactI18next).init({ | ||||
| 	resources, | ||||
| 	lng: "en-PR", | ||||
| 	interpolation: { | ||||
| 		escapeValue: false, | ||||
| 	}, | ||||
| }); | ||||
| 
 | ||||
| export default i18n; | ||||
|  | @ -1,10 +0,0 @@ | |||
| <script lang="ts"> | ||||
|   let count: number = 0 | ||||
|   const increment = () => { | ||||
|     count += 2 | ||||
|   } | ||||
| </script> | ||||
| 
 | ||||
| <button on:click={increment}> | ||||
|   count == {count} | ||||
| </button> | ||||
							
								
								
									
										8
									
								
								frontend/src/lib/api/entities/blog.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								frontend/src/lib/api/entities/blog.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | |||
| import { Account } from "./account"; | ||||
| 
 | ||||
| export interface Blog { | ||||
| 	id: string; | ||||
| 	name: string; | ||||
| 	domain: string | null; | ||||
| 	bio: Account; | ||||
| } | ||||
							
								
								
									
										5
									
								
								frontend/src/lib/api/entities/error.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								frontend/src/lib/api/entities/error.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| export interface Error { | ||||
| 	code: number; | ||||
| 	message: string; | ||||
| 	details?: string; | ||||
| } | ||||
							
								
								
									
										9
									
								
								frontend/src/lib/api/entities/post.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								frontend/src/lib/api/entities/post.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | |||
| import { Blog } from "./blog"; | ||||
| 
 | ||||
| export interface Post { | ||||
| 	id: string; | ||||
| 	content: string | null; | ||||
| 	source: string | null; | ||||
| 	visibility: "public" | "unlisted" | "followers" | "direct"; | ||||
| 	blog: Blog; | ||||
| } | ||||
							
								
								
									
										33
									
								
								frontend/src/lib/api/fetch.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								frontend/src/lib/api/fetch.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | |||
| import axios from "axios"; | ||||
| import type { Error } from "./entities/error"; | ||||
| 
 | ||||
| export async function apiFetch<T>( | ||||
| 	path: string, | ||||
| 	data: | ||||
| 		| { | ||||
| 				method?: string; | ||||
| 				// eslint-disable-next-line @typescript-eslint/no-explicit-any
 | ||||
| 				data?: any; | ||||
| 				token?: string; | ||||
| 				headers?: Record<string, string>; | ||||
| 				version?: number; | ||||
| 		  } | ||||
| 		| undefined = undefined, | ||||
| ) { | ||||
| 	try { | ||||
| 		const resp = await axios<T>({ | ||||
| 			method: data?.method || "GET", | ||||
| 			url: `/api/v${data?.version || 1}${path}`, | ||||
| 			data: data, | ||||
| 		}); | ||||
| 
 | ||||
| 		return resp.data; | ||||
| 		// eslint-disable-next-line @typescript-eslint/no-explicit-any
 | ||||
| 	} catch (err: any) { | ||||
| 		if (err.response) { | ||||
| 			throw err as Error; | ||||
| 		} | ||||
| 
 | ||||
| 		throw err; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										23
									
								
								frontend/src/lib/duration.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								frontend/src/lib/duration.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | |||
| import { humanizer } from "humanize-duration"; | ||||
| 
 | ||||
| const defaultHumanizer = humanizer({ | ||||
| 	round: true, | ||||
| 	largest: 1, | ||||
| 	spacer: "", | ||||
| 	languages: { | ||||
| 		en: { | ||||
| 			y: () => "y", | ||||
| 			mo: () => "mo", | ||||
| 			w: () => "w", | ||||
| 			d: () => "d", | ||||
| 			h: () => "h", | ||||
| 			m: () => "m", | ||||
| 			s: () => "s", | ||||
| 			ms: () => "ms", | ||||
| 		}, | ||||
| 	}, | ||||
| }); | ||||
| 
 | ||||
| export default function humanizeDuration(ms: number) { | ||||
| 	return defaultHumanizer(ms); | ||||
| } | ||||
							
								
								
									
										5
									
								
								frontend/src/lib/hooks.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								frontend/src/lib/hooks.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; | ||||
| import type { Store, Dispatch } from "./store"; | ||||
| 
 | ||||
| export const useAppDispatch: () => Dispatch = useDispatch; | ||||
| export const useAppSelector: TypedUseSelectorHook<Store> = useSelector; | ||||
							
								
								
									
										14
									
								
								frontend/src/lib/markdown.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								frontend/src/lib/markdown.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,14 @@ | |||
| import MarkdownIt from "markdown-it"; | ||||
| import sanitize from "sanitize-html"; | ||||
| 
 | ||||
| const md = new MarkdownIt({ | ||||
| 	html: false, | ||||
| 	breaks: true, | ||||
| 	linkify: true, | ||||
| }).disable(["heading", "lheading", "table"]); | ||||
| 
 | ||||
| export function renderMarkdown(src: string | null) { | ||||
| 	return src ? sanitize(md.render(src)) : null; | ||||
| } | ||||
| 
 | ||||
| export const charCount = (str: string) => [...str].length; | ||||
|  | @ -1,6 +0,0 @@ | |||
| import { writable } from "svelte/store"; | ||||
| import type { MeAccount } from "./api/account"; | ||||
| 
 | ||||
| export const meAccount = writable( | ||||
| 	JSON.parse(document.getElementById("accountData").innerHTML) as MeAccount, | ||||
| ); | ||||
							
								
								
									
										33
									
								
								frontend/src/lib/store/accounts.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								frontend/src/lib/store/accounts.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | |||
| import { createSlice, type PayloadAction } from "@reduxjs/toolkit"; | ||||
| import type { Account, MeAccount } from "$lib/api/entities/account"; | ||||
| 
 | ||||
| const accountsSlice = createSlice({ | ||||
| 	name: "accounts", | ||||
| 	initialState: { | ||||
| 		currentAccount: JSON.parse( | ||||
| 			document.getElementById("accountData")!.innerHTML, | ||||
| 		) as MeAccount, | ||||
| 		accounts: {}, | ||||
| 		usernames: {}, | ||||
| 	} as { | ||||
| 		currentAccount: MeAccount; | ||||
| 		accounts: Record<string, Account>; | ||||
| 		usernames: Record<string, string>; | ||||
| 	}, | ||||
| 	reducers: { | ||||
| 		setAccount(state, action: PayloadAction<Account>) { | ||||
| 			state.usernames[action.payload.username] = action.payload.id; // TODO: change to acct-equivalent field
 | ||||
| 			state.accounts[action.payload.id] = action.payload; | ||||
| 		}, | ||||
| 		removeAccount(state, action: PayloadAction<string>) { | ||||
| 			delete state.accounts[action.payload]; | ||||
| 		}, | ||||
| 		setCurrentAccount(state, action: PayloadAction<MeAccount>) { | ||||
| 			state.currentAccount = action.payload; | ||||
| 			state.accounts[action.payload.id] = action.payload; | ||||
| 		}, | ||||
| 	}, | ||||
| }); | ||||
| 
 | ||||
| export const accounts = accountsSlice.reducer; | ||||
| export const { setAccount, removeAccount } = accountsSlice.actions; | ||||
							
								
								
									
										15
									
								
								frontend/src/lib/store/api.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								frontend/src/lib/store/api.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; | ||||
| 
 | ||||
| import type { Post } from "$lib/api/entities/post"; | ||||
| 
 | ||||
| export const mercuryApi = createApi({ | ||||
| 	reducerPath: "mercuryApi", | ||||
| 	baseQuery: fetchBaseQuery({ baseUrl: "/api/v1" }), | ||||
| 	endpoints: (builder) => ({ | ||||
| 		getPostById: builder.query<Post, string>({ | ||||
| 			query: (id) => `/posts/${id}`, | ||||
| 		}), | ||||
| 	}), | ||||
| }); | ||||
| 
 | ||||
| export const { useGetPostByIdQuery } = mercuryApi; | ||||
							
								
								
									
										22
									
								
								frontend/src/lib/store/blogs.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								frontend/src/lib/store/blogs.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,22 @@ | |||
| import { createSlice, type PayloadAction } from "@reduxjs/toolkit"; | ||||
| import type { Blog } from "$lib/api/entities/blog"; | ||||
| 
 | ||||
| const blogSlice = createSlice({ | ||||
| 	name: "blogs", | ||||
| 	initialState: { blogs: {}, blogNames: {} } as { | ||||
| 		blogs: Record<string, Blog>; | ||||
| 		blogNames: Record<string, string>; | ||||
| 	}, | ||||
| 	reducers: { | ||||
| 		setBlog(state, action: PayloadAction<Blog>) { | ||||
| 			state.blogNames[action.payload.name] = action.payload.id; | ||||
| 			state.blogs[action.payload.id] = action.payload; | ||||
| 		}, | ||||
| 		removeBlog(state, action: PayloadAction<string>) { | ||||
| 			delete state.blogs[action.payload]; | ||||
| 		}, | ||||
| 	}, | ||||
| }); | ||||
| 
 | ||||
| export const blogs = blogSlice.reducer; | ||||
| export const { setBlog, removeBlog } = blogSlice.actions; | ||||
							
								
								
									
										23
									
								
								frontend/src/lib/store/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								frontend/src/lib/store/index.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | |||
| import { configureStore } from "@reduxjs/toolkit"; | ||||
| import { setupListeners } from "@reduxjs/toolkit/query/react"; | ||||
| import { mercuryApi } from "./api"; | ||||
| import { accounts } from "./accounts"; | ||||
| import { blogs } from "./blogs"; | ||||
| import { posts } from "./posts"; | ||||
| 
 | ||||
| const store = configureStore({ | ||||
| 	reducer: { | ||||
| 		accounts, | ||||
| 		blogs, | ||||
| 		posts, | ||||
| 		[mercuryApi.reducerPath]: mercuryApi.reducer, | ||||
| 	}, | ||||
| 	middleware: (getDefaultMiddleware) => | ||||
| 		getDefaultMiddleware().concat(mercuryApi.middleware), | ||||
| }); | ||||
| 
 | ||||
| setupListeners(store.dispatch); | ||||
| 
 | ||||
| export default store; | ||||
| export type Store = ReturnType<typeof store.getState>; | ||||
| export type Dispatch = typeof store.dispatch; | ||||
							
								
								
									
										18
									
								
								frontend/src/lib/store/posts.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								frontend/src/lib/store/posts.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | |||
| import { createSlice, type PayloadAction } from "@reduxjs/toolkit"; | ||||
| import type { Post } from "$lib/api/entities/post"; | ||||
| 
 | ||||
| const postsSlice = createSlice({ | ||||
| 	name: "posts", | ||||
| 	initialState: {} as Record<string, Post>, | ||||
| 	reducers: { | ||||
| 		setPost(state, action: PayloadAction<Post>) { | ||||
| 			state[action.payload.id] = action.payload; | ||||
| 		}, | ||||
| 		removePost(state, action: PayloadAction<string>) { | ||||
| 			delete state[action.payload]; | ||||
| 		}, | ||||
| 	}, | ||||
| }); | ||||
| 
 | ||||
| export const posts = postsSlice.reducer; | ||||
| export const { setPost, removePost } = postsSlice.actions; | ||||
							
								
								
									
										18
									
								
								frontend/src/lib/store/selectors/accounts.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								frontend/src/lib/store/selectors/accounts.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | |||
| import type { Store } from "$lib/store"; | ||||
| 
 | ||||
| export const getAccount = (state: Store, id: string) => | ||||
| 	id in state.accounts.accounts ? state.accounts.accounts[id] : undefined; | ||||
| 
 | ||||
| export const getCurrentAccount = (state: Store) => | ||||
| 	state.accounts.currentAccount; | ||||
| 
 | ||||
| export const getAccountByName = (state: Store, username: string) => { | ||||
| 	const id = | ||||
| 		username in state.accounts.usernames | ||||
| 			? state.accounts.usernames[username] | ||||
| 			: undefined; | ||||
| 	if (!id) return undefined; | ||||
| 	return id in state.accounts.accounts | ||||
| 		? state.accounts.accounts[id] | ||||
| 		: undefined; | ||||
| }; | ||||
							
								
								
									
										11
									
								
								frontend/src/lib/store/selectors/blogs.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/src/lib/store/selectors/blogs.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | |||
| import type { Store } from "$lib/store"; | ||||
| 
 | ||||
| export const getBlog = (state: Store, id: string) => | ||||
| 	id in state.blogs.blogs ? state.blogs.blogs[id] : undefined; | ||||
| 
 | ||||
| export const getBlogByBlogName = (state: Store, name: string) => { | ||||
| 	const id = | ||||
| 		name in state.blogs.blogNames ? state.blogs.blogNames[name] : undefined; | ||||
| 	if (!id) return undefined; | ||||
| 	return id in state.blogs.blogs ? state.blogs.blogs[id] : undefined; | ||||
| }; | ||||
							
								
								
									
										3
									
								
								frontend/src/lib/store/selectors/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/src/lib/store/selectors/index.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| export { getPost } from "./posts"; | ||||
| export { getBlog, getBlogByBlogName } from "./blogs"; | ||||
| export { getAccount, getAccountByName, getCurrentAccount } from "./accounts"; | ||||
							
								
								
									
										5
									
								
								frontend/src/lib/store/selectors/posts.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								frontend/src/lib/store/selectors/posts.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| import type { Store } from "$lib/store"; | ||||
| 
 | ||||
| /** Gets a status from the store by ID. */ | ||||
| export const getPost = (state: Store, id: string) => | ||||
| 	id in state.posts ? state.posts[id] : undefined; | ||||
|  | @ -1,8 +0,0 @@ | |||
| import "./app.css"; | ||||
| import App from "./App.svelte"; | ||||
| 
 | ||||
| const app = new App({ | ||||
| 	target: document.getElementById("app"), | ||||
| }); | ||||
| 
 | ||||
| export default app; | ||||
							
								
								
									
										10
									
								
								frontend/src/main.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/main.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| import { render } from "preact"; | ||||
| import "@fontsource/roboto/300.css"; | ||||
| import "@fontsource/roboto/400.css"; | ||||
| import "@fontsource/roboto/500.css"; | ||||
| import "@fontsource/roboto/700.css"; | ||||
| 
 | ||||
| import App from "./app.tsx"; | ||||
| import "./i18n"; | ||||
| 
 | ||||
| render(<App />, document.getElementById("app")!); | ||||
							
								
								
									
										0
									
								
								frontend/src/pages/blog/index.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								frontend/src/pages/blog/index.tsx
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										30
									
								
								frontend/src/pages/blog/post/index.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								frontend/src/pages/blog/post/index.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | |||
| import { useGetPostByIdQuery } from "$/lib/store/api"; | ||||
| import { CircularProgress } from "@mui/material"; | ||||
| import { Redirect, useRoute } from "wouter-preact"; | ||||
| import Post from "$components/post/Post"; | ||||
| 
 | ||||
| export default function PostPage() { | ||||
| 	const [, params] = useRoute("/@:username/posts/:postId"); | ||||
| 	const { data, error, isLoading } = useGetPostByIdQuery(params!.postId); | ||||
| 
 | ||||
| 	if (error) { | ||||
| 		throw error; | ||||
| 	} else if (isLoading || !data) { | ||||
| 		return <CircularProgress />; | ||||
| 	} | ||||
| 
 | ||||
| 	if (data.blog.name !== params!.username) { | ||||
| 		return ( | ||||
| 			<> | ||||
| 				<Redirect to={`/web/@${data.blog.name}/posts/${data.id}`} /> | ||||
| 				<p>hi world!</p> | ||||
| 			</> | ||||
| 		); | ||||
| 	} | ||||
| 
 | ||||
| 	return ( | ||||
| 		<> | ||||
| 			<Post post={data} /> | ||||
| 		</> | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										17
									
								
								frontend/src/pages/timeline/home/index.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								frontend/src/pages/timeline/home/index.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | |||
| import { useAppSelector } from "$/lib/hooks"; | ||||
| import { getCurrentAccount } from "$/lib/store/selectors"; | ||||
| 
 | ||||
| export default function HomeTimeline() { | ||||
| 	const account = useAppSelector(getCurrentAccount); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<> | ||||
| 			<ul> | ||||
| 				<li>Username: {account.username}</li> | ||||
| 				<li>ID: {account.id}</li> | ||||
| 				<li>Domain: {account.domain}</li> | ||||
| 				<li>Email: {account.email}</li> | ||||
| 			</ul> | ||||
| 		</> | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										5
									
								
								frontend/src/translations/en.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								frontend/src/translations/en.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| { | ||||
| 	"navbar": { | ||||
| 		"homeTimeline": "Home timeline" | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										5
									
								
								frontend/src/translations/en.pirate.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								frontend/src/translations/en.pirate.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| { | ||||
| 	"navbar": { | ||||
| 		"homeTimeline": "Yar friends' bottles" | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										1
									
								
								frontend/src/vite-env.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								frontend/src/vite-env.d.ts
									
										
									
									
										vendored
									
									
								
							|  | @ -1,2 +1 @@ | |||
| /// <reference types="svelte" />
 | ||||
| /// <reference types="vite/client" />
 | ||||
|  |  | |||
|  | @ -1,7 +0,0 @@ | |||
| import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; | ||||
| 
 | ||||
| export default { | ||||
| 	// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
 | ||||
| 	// for more information about preprocessors
 | ||||
| 	preprocess: vitePreprocess(), | ||||
| }; | ||||
|  | @ -1,6 +1,11 @@ | |||
| /** @type {import('tailwindcss').Config} */ | ||||
| export default { | ||||
| 	content: ["./index.html", "./src/**/*.{svelte,js,ts,jsx,tsx}"], | ||||
| 	content: [ | ||||
| 		"../web/frontend/app.html", | ||||
| 		"./index.html", | ||||
| 		"./src/**/*.{js,ts,jsx,tsx}", | ||||
| 	], | ||||
| 	darkMode: "class", | ||||
| 	theme: { | ||||
| 		extend: { | ||||
| 			colors: { | ||||
|  |  | |||
|  | @ -1,20 +1,36 @@ | |||
| { | ||||
| 	"extends": "@tsconfig/svelte/tsconfig.json", | ||||
| 	"compilerOptions": { | ||||
| 		"target": "ESNext", | ||||
| 		"target": "ES2020", | ||||
| 		"useDefineForClassFields": true, | ||||
| 		"module": "ESNext", | ||||
| 		"lib": ["ES2020", "DOM", "DOM.Iterable"], | ||||
| 		"skipLibCheck": true, | ||||
| 
 | ||||
| 		/* Bundler mode */ | ||||
| 		"moduleResolution": "bundler", | ||||
| 		"allowImportingTsExtensions": true, | ||||
| 		"resolveJsonModule": true, | ||||
| 		/** | ||||
| 		 * Typecheck JS in `.svelte` and `.js` files by default. | ||||
| 		 * Disable checkJs if you'd like to use dynamic types in JS. | ||||
| 		 * Note that setting allowJs false does not prevent the use | ||||
| 		 * of JS in `.svelte` files. | ||||
| 		 */ | ||||
| 		"allowJs": true, | ||||
| 		"checkJs": true, | ||||
| 		"isolatedModules": true | ||||
| 		"isolatedModules": true, | ||||
| 		"noEmit": true, | ||||
| 		"jsx": "react-jsx", | ||||
| 		"jsxImportSource": "preact", | ||||
| 
 | ||||
| 		/* Linting */ | ||||
| 		"strict": true, | ||||
| 		"noUnusedLocals": true, | ||||
| 		"noUnusedParameters": true, | ||||
| 		"noFallthroughCasesInSwitch": true, | ||||
| 		"baseUrl": ".", | ||||
| 		"paths": { | ||||
| 			"react": ["./node_modules/preact/compat/"], | ||||
| 			"react-dom": ["./node_modules/preact/compat/"], | ||||
| 			"$/*": ["./src/*"], | ||||
| 			"$lib/*": ["src/lib/*"], | ||||
| 			"$components/*": ["src/components/*"], | ||||
| 			"$public/*": ["public/*"], | ||||
| 			"$pages/*": ["src/pages/*"] | ||||
| 		} | ||||
| 	}, | ||||
| 	"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], | ||||
| 	"include": ["src"], | ||||
| 	"references": [{ "path": "./tsconfig.node.json" }] | ||||
| } | ||||
|  |  | |||
|  | @ -3,7 +3,8 @@ | |||
| 		"composite": true, | ||||
| 		"skipLibCheck": true, | ||||
| 		"module": "ESNext", | ||||
| 		"moduleResolution": "bundler" | ||||
| 		"moduleResolution": "bundler", | ||||
| 		"allowSyntheticDefaultImports": true | ||||
| 	}, | ||||
| 	"include": ["vite.config.ts"] | ||||
| } | ||||
|  |  | |||
|  | @ -1,10 +1,20 @@ | |||
| import { defineConfig } from "vite"; | ||||
| import { svelte } from "@sveltejs/vite-plugin-svelte"; | ||||
| import preact from "@preact/preset-vite"; | ||||
| import { resolve } from "path"; | ||||
| 
 | ||||
| // https://vitejs.dev/config/
 | ||||
| export default defineConfig({ | ||||
| 	plugins: [svelte()], | ||||
| 	plugins: [preact()], | ||||
| 	build: { | ||||
| 		manifest: "manifest.json", | ||||
| 	}, | ||||
| 	resolve: { | ||||
| 		alias: { | ||||
| 			$: resolve(__dirname, "./src/"), | ||||
| 			$lib: resolve(__dirname, "./src/lib/"), | ||||
| 			$components: resolve(__dirname, "./src/components/"), | ||||
| 			$pages: resolve(__dirname, "./src/pages/"), | ||||
| 			$public: resolve(__dirname, "./public/"), | ||||
| 		}, | ||||
| 	}, | ||||
| }); | ||||
|  |  | |||
							
								
								
									
										2871
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										2871
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue