add solid

This commit is contained in:
sam 2023-11-23 21:33:30 +01:00
parent 9bde1a1aa7
commit 7598b1e103
40 changed files with 999 additions and 2353 deletions

View file

@ -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",
},
}; };

View file

@ -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>

View file

@ -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"
} }
} }

View file

@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

15
frontend/src/App.tsx Normal file
View 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>
</>
);
}

View file

@ -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>
</>
);
}

View file

@ -1,9 +0,0 @@
import { Helmet } from "react-helmet";
export default function DefaultHead() {
return (
<Helmet>
<title>Mercury</title>
</Helmet>
);
}

View file

@ -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>
</>
);
}

View file

@ -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

View 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

View file

@ -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>
</>
);
}

View file

@ -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>
);
}

View file

@ -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>
</>
);
}

View file

@ -1,10 +0,0 @@
.post-Post--account {
display: flex;
flex-direction: row;
}
.post-Post--names {
margin: 0 0.5rem;
display: flex;
flex-direction: column;
}

View file

@ -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>
</>
);
}

View file

@ -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
View 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!);

View file

@ -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;

View file

@ -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;

View 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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;
};

View file

@ -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;
};

View file

@ -1,3 +0,0 @@
export { getPost } from "./posts";
export { getBlog, getBlogByBlogName } from "./blogs";
export { getAccount, getAccountByName, getCurrentAccount } from "./accounts";

View file

@ -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;

View file

@ -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")!);

View file

@ -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} />
</>
);
}

View file

@ -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>
</>
);
}

View file

@ -1,5 +0,0 @@
{
"navbar": {
"homeTimeline": "Home timeline"
}
}

View file

@ -1,5 +0,0 @@
{
"navbar": {
"homeTimeline": "Yar friends' bottles"
}
}

View file

@ -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: [],
};

View file

@ -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/*"],

View file

@ -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",
}, },

File diff suppressed because it is too large Load diff

View file

@ -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,
}) })