add solid
This commit is contained in:
parent
9bde1a1aa7
commit
7598b1e103
40 changed files with 999 additions and 2353 deletions
|
@ -6,27 +6,13 @@ module.exports = {
|
||||||
extends: [
|
extends: [
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
"plugin:react/recommended",
|
"plugin:solid/recommended",
|
||||||
],
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
files: [".eslintrc.{js,cjs}"],
|
|
||||||
parserOptions: {
|
|
||||||
sourceType: "script",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
|
ignorePatterns: ["dist/**", ".eslintrc.cjs"],
|
||||||
parser: "@typescript-eslint/parser",
|
parser: "@typescript-eslint/parser",
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: "latest",
|
ecmaVersion: "latest",
|
||||||
sourceType: "module",
|
sourceType: "module",
|
||||||
},
|
},
|
||||||
plugins: ["@typescript-eslint", "react"],
|
plugins: ["@typescript-eslint", "solid"],
|
||||||
rules: {
|
|
||||||
"react/react-in-jsx-scope": "off",
|
|
||||||
"no-mixed-spaces-and-tabs": "off",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,11 +2,12 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Mercury</title>
|
<title>Vite + Solid + TS</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -6,47 +6,32 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"format": "prettier -w ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.11.1",
|
|
||||||
"@emotion/styled": "^11.11.0",
|
|
||||||
"@fontsource/roboto": "^5.0.8",
|
"@fontsource/roboto": "^5.0.8",
|
||||||
"@heroicons/react": "^2.0.18",
|
"@suid/icons-material": "^0.6.11",
|
||||||
"@mui/icons-material": "^5.14.9",
|
"@suid/material": "^0.15.1",
|
||||||
"@mui/material": "^5.14.9",
|
"@suid/vite-plugin": "^0.1.5",
|
||||||
"@reduxjs/toolkit": "^1.9.5",
|
"axios": "^1.6.2",
|
||||||
"axios": "^1.5.0",
|
"humanize-duration": "^3.31.0",
|
||||||
"humanize-duration": "^3.29.0",
|
"markdown-it": "^13.0.2",
|
||||||
"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",
|
"sanitize-html": "^2.11.0",
|
||||||
"ulid": "^2.3.0",
|
"solid-js": "^1.8.5"
|
||||||
"wouter-preact": "^2.11.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@preact/preset-vite": "^2.5.0",
|
"@types/humanize-duration": "^3.27.3",
|
||||||
"@types/humanize-duration": "^3.27.1",
|
"@types/markdown-it": "^13.0.7",
|
||||||
"@types/luxon": "^3.3.2",
|
"@types/node": "^20.9.4",
|
||||||
"@types/markdown-it": "^13.0.1",
|
"@types/sanitize-html": "^2.9.5",
|
||||||
"@types/node": "^20.6.0",
|
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
||||||
"@types/react-helmet": "^6.1.6",
|
"@typescript-eslint/parser": "^6.12.0",
|
||||||
"@types/sanitize-html": "^2.9.0",
|
"eslint": "^8.54.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.7.0",
|
"eslint-plugin-solid": "^0.13.0",
|
||||||
"@typescript-eslint/parser": "^6.7.0",
|
"prettier": "^3.1.0",
|
||||||
"autoprefixer": "^10.4.15",
|
"typescript": "^5.2.2",
|
||||||
"eslint": "^8.49.0",
|
"vite": "^5.0.0",
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"vite-plugin-solid": "^2.7.2"
|
||||||
"postcss": "^8.4.29",
|
|
||||||
"prettier": "^3.0.3",
|
|
||||||
"tailwindcss": "^3.3.3",
|
|
||||||
"typescript": "^5.0.2",
|
|
||||||
"vite": "^4.4.5"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
};
|
|
15
frontend/src/App.tsx
Normal file
15
frontend/src/App.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import accountStore from "./lib/store/account";
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ul>
|
||||||
|
<li>Username: {accountStore.me.username}</li>
|
||||||
|
<li>ID: {accountStore.me.id}</li>
|
||||||
|
<li>Domain: {accountStore.me.domain}</li>
|
||||||
|
<li>Email: {accountStore.me.email}</li>
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
|
@ -1,32 +0,0 @@
|
||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { Helmet } from "react-helmet";
|
|
||||||
|
|
||||||
export default function DefaultHead() {
|
|
||||||
return (
|
|
||||||
<Helmet>
|
|
||||||
<title>Mercury</title>
|
|
||||||
</Helmet>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
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 +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="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>
|
|
Before Width: | Height: | Size: 1.6 KiB |
1
frontend/src/assets/solid.svg
Normal file
1
frontend/src/assets/solid.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 166 155.3"><path d="M163 35S110-4 69 5l-3 1c-6 2-11 5-14 9l-2 3-15 26 26 5c11 7 25 10 38 7l46 9 18-30z" fill="#76b3e1"/><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="27.5" y1="3" x2="152" y2="63.5"><stop offset=".1" stop-color="#76b3e1"/><stop offset=".3" stop-color="#dcf2fd"/><stop offset="1" stop-color="#76b3e1"/></linearGradient><path d="M163 35S110-4 69 5l-3 1c-6 2-11 5-14 9l-2 3-15 26 26 5c11 7 25 10 38 7l46 9 18-30z" opacity=".3" fill="url(#a)"/><path d="M52 35l-4 1c-17 5-22 21-13 35 10 13 31 20 48 15l62-21S92 26 52 35z" fill="#518ac8"/><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="95.8" y1="32.6" x2="74" y2="105.2"><stop offset="0" stop-color="#76b3e1"/><stop offset=".5" stop-color="#4377bb"/><stop offset="1" stop-color="#1f3b77"/></linearGradient><path d="M52 35l-4 1c-17 5-22 21-13 35 10 13 31 20 48 15l62-21S92 26 52 35z" opacity=".3" fill="url(#b)"/><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="18.4" y1="64.2" x2="144.3" y2="149.8"><stop offset="0" stop-color="#315aa9"/><stop offset=".5" stop-color="#518ac8"/><stop offset="1" stop-color="#315aa9"/></linearGradient><path d="M134 80a45 45 0 00-48-15L24 85 4 120l112 19 20-36c4-7 3-15-2-23z" fill="url(#c)"/><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="75.2" y1="74.5" x2="24.4" y2="260.8"><stop offset="0" stop-color="#4377bb"/><stop offset=".5" stop-color="#1a336b"/><stop offset="1" stop-color="#1a336b"/></linearGradient><path d="M114 115a45 45 0 00-48-15L4 120s53 40 94 30l3-1c17-5 23-21 13-34z" fill="url(#d)"/></svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -1,62 +0,0 @@
|
||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
.post-Post--account {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-Post--names {
|
|
||||||
margin: 0 0.5rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
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;
|
|
11
frontend/src/index.tsx
Normal file
11
frontend/src/index.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
/* @refresh reload */
|
||||||
|
import { render } from "solid-js/web";
|
||||||
|
import "@fontsource/roboto/300.css";
|
||||||
|
import "@fontsource/roboto/400.css";
|
||||||
|
import "@fontsource/roboto/500.css";
|
||||||
|
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
|
const root = document.getElementById("app");
|
||||||
|
|
||||||
|
render(() => <App />, root!);
|
|
@ -1,10 +1,7 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import type { Error } from "./entities/error";
|
import type { Error } from "./entities/error";
|
||||||
|
|
||||||
export async function apiFetch<T>(
|
interface APIFetchData {
|
||||||
path: string,
|
|
||||||
data:
|
|
||||||
| {
|
|
||||||
method?: string;
|
method?: string;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
data?: any;
|
data?: any;
|
||||||
|
@ -12,13 +9,16 @@ export async function apiFetch<T>(
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
version?: number;
|
version?: number;
|
||||||
}
|
}
|
||||||
| undefined = undefined,
|
|
||||||
|
export async function apiFetch<T>(
|
||||||
|
path: string,
|
||||||
|
data: APIFetchData | undefined = undefined,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const resp = await axios<T>({
|
const resp = await axios<T>({
|
||||||
method: data?.method || "GET",
|
method: data?.method || "GET",
|
||||||
url: `/api/v${data?.version || 1}${path}`,
|
url: `/api/v${data?.version || 1}${path}`,
|
||||||
data: data,
|
data: data?.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
return resp.data;
|
return resp.data;
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
|
||||||
import type { Store, Dispatch } from "./store";
|
|
||||||
|
|
||||||
export const useAppDispatch: () => Dispatch = useDispatch;
|
|
||||||
export const useAppSelector: TypedUseSelectorHook<Store> = useSelector;
|
|
10
frontend/src/lib/store/account.ts
Normal file
10
frontend/src/lib/store/account.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { createStore } from "solid-js/store";
|
||||||
|
import { MeAccount } from "$lib/api/entities/account";
|
||||||
|
|
||||||
|
export const [accountStore, setAccountStore] = createStore({
|
||||||
|
me: JSON.parse(
|
||||||
|
document.getElementById("accountData")!.innerHTML,
|
||||||
|
) as MeAccount,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default accountStore;
|
|
@ -1,33 +0,0 @@
|
||||||
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;
|
|
|
@ -1,15 +0,0 @@
|
||||||
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;
|
|
|
@ -1,22 +0,0 @@
|
||||||
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;
|
|
|
@ -1,23 +0,0 @@
|
||||||
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;
|
|
|
@ -1,18 +0,0 @@
|
||||||
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;
|
|
|
@ -1,18 +0,0 @@
|
||||||
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;
|
|
||||||
};
|
|
|
@ -1,11 +0,0 @@
|
||||||
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;
|
|
||||||
};
|
|
|
@ -1,3 +0,0 @@
|
||||||
export { getPost } from "./posts";
|
|
||||||
export { getBlog, getBlogByBlogName } from "./blogs";
|
|
||||||
export { getAccount, getAccountByName, getCurrentAccount } from "./accounts";
|
|
|
@ -1,5 +0,0 @@
|
||||||
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,10 +0,0 @@
|
||||||
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")!);
|
|
|
@ -1,30 +0,0 @@
|
||||||
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} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"navbar": {
|
|
||||||
"homeTimeline": "Home timeline"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"navbar": {
|
|
||||||
"homeTimeline": "Yar friends' bottles"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,78 +0,0 @@
|
||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
export default {
|
|
||||||
content: [
|
|
||||||
"../web/frontend/app.html",
|
|
||||||
"./index.html",
|
|
||||||
"./src/**/*.{js,ts,jsx,tsx}",
|
|
||||||
],
|
|
||||||
darkMode: "class",
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
textDark: "#ffffff",
|
|
||||||
textLight: "#000000",
|
|
||||||
background: {
|
|
||||||
50: "#fefefe",
|
|
||||||
100: "#fdfdfd",
|
|
||||||
200: "#fbfbfb",
|
|
||||||
300: "#f9f9f9",
|
|
||||||
400: "#f7f7f7",
|
|
||||||
500: "#f5f5f5",
|
|
||||||
600: "#c4c4c4",
|
|
||||||
700: "#939393",
|
|
||||||
800: "#626262",
|
|
||||||
900: "#313131",
|
|
||||||
},
|
|
||||||
primary: {
|
|
||||||
50: "#e6f2f3",
|
|
||||||
100: "#cee5e8",
|
|
||||||
200: "#9ccbd1",
|
|
||||||
300: "#6bb2b9",
|
|
||||||
400: "#3998a2",
|
|
||||||
500: "#087e8b",
|
|
||||||
600: "#06656f",
|
|
||||||
700: "#054c53",
|
|
||||||
800: "#033238",
|
|
||||||
900: "#02191c",
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
50: "#f2f2f3",
|
|
||||||
100: "#e5e5e7",
|
|
||||||
200: "#cbccce",
|
|
||||||
300: "#b0b2b6",
|
|
||||||
400: "#96999d",
|
|
||||||
500: "#7c7f85",
|
|
||||||
600: "#63666a",
|
|
||||||
700: "#4a4c50",
|
|
||||||
800: "#323335",
|
|
||||||
900: "#19191b",
|
|
||||||
},
|
|
||||||
danger: {
|
|
||||||
50: "#ffefef",
|
|
||||||
100: "#ffdedf",
|
|
||||||
200: "#ffbdbf",
|
|
||||||
300: "#ff9c9f",
|
|
||||||
400: "#ff7b7f",
|
|
||||||
500: "#ff5a5f",
|
|
||||||
600: "#cc484c",
|
|
||||||
700: "#993639",
|
|
||||||
800: "#662426",
|
|
||||||
900: "#331213",
|
|
||||||
},
|
|
||||||
success: {
|
|
||||||
50: "#edf6f0",
|
|
||||||
100: "#dbece1",
|
|
||||||
200: "#b8d9c2",
|
|
||||||
300: "#94c7a4",
|
|
||||||
400: "#71b485",
|
|
||||||
500: "#4da167",
|
|
||||||
600: "#3e8152",
|
|
||||||
700: "#2e613e",
|
|
||||||
800: "#1f4029",
|
|
||||||
900: "#0f2015",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
};
|
|
|
@ -12,8 +12,8 @@
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "preserve",
|
||||||
"jsxImportSource": "preact",
|
"jsxImportSource": "solid-js",
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
@ -22,8 +22,6 @@
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"react": ["./node_modules/preact/compat/"],
|
|
||||||
"react-dom": ["./node_modules/preact/compat/"],
|
|
||||||
"$/*": ["./src/*"],
|
"$/*": ["./src/*"],
|
||||||
"$lib/*": ["src/lib/*"],
|
"$lib/*": ["src/lib/*"],
|
||||||
"$components/*": ["src/components/*"],
|
"$components/*": ["src/components/*"],
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import preact from "@preact/preset-vite";
|
import solid from "vite-plugin-solid";
|
||||||
|
import suid from "@suid/vite-plugin";
|
||||||
import { resolve } from "path";
|
import { resolve } from "path";
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [preact()],
|
plugins: [suid(), solid()],
|
||||||
build: {
|
build: {
|
||||||
manifest: "manifest.json",
|
manifest: "manifest.json",
|
||||||
},
|
},
|
||||||
|
|
2609
pnpm-lock.yaml
2609
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -40,7 +40,7 @@ func New(app *app.App) *Frontend {
|
||||||
glue, err := vueglue.NewVueGlue(&vueglue.ViteConfig{
|
glue, err := vueglue.NewVueGlue(&vueglue.ViteConfig{
|
||||||
Environment: "development",
|
Environment: "development",
|
||||||
AssetsPath: "frontend",
|
AssetsPath: "frontend",
|
||||||
EntryPoint: "src/main.tsx",
|
EntryPoint: "src/index.tsx",
|
||||||
Platform: "vue",
|
Platform: "vue",
|
||||||
FS: os.DirFS("frontend"),
|
FS: os.DirFS("frontend"),
|
||||||
})
|
})
|
||||||
|
@ -55,7 +55,7 @@ func New(app *app.App) *Frontend {
|
||||||
Environment: "production",
|
Environment: "production",
|
||||||
URLPrefix: "/assets/",
|
URLPrefix: "/assets/",
|
||||||
AssetsPath: "dist",
|
AssetsPath: "dist",
|
||||||
EntryPoint: "src/main.tsx",
|
EntryPoint: "src/index.tsx",
|
||||||
Platform: "vue",
|
Platform: "vue",
|
||||||
FS: frontend.Embed,
|
FS: frontend.Embed,
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue