🎉 Reinital Commit
This commit is contained in:
parent
1e04f2029f
commit
178f80c707
@ -5,7 +5,7 @@ RUN apk add nodejs npm
|
||||
|
||||
WORKDIR /source
|
||||
COPY . .
|
||||
WORKDIR /source/pkg/view
|
||||
WORKDIR /source/pkg/views
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
WORKDIR /source
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.smartsheep.studio/hydrogen/interactive/pkg/view"
|
||||
"code.smartsheep.studio/hydrogen/identity/pkg/views"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cache"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
@ -99,7 +99,7 @@ func NewServer() {
|
||||
Expiration: 24 * time.Hour,
|
||||
CacheControl: true,
|
||||
}), filesystem.New(filesystem.Config{
|
||||
Root: http.FS(view.FS),
|
||||
Root: http.FS(views.FS),
|
||||
PathPrefix: "dist",
|
||||
Index: "index.html",
|
||||
NotFoundFile: "dist/index.html",
|
||||
|
7
pkg/view/.gitignore
vendored
7
pkg/view/.gitignore
vendored
@ -1,7 +0,0 @@
|
||||
/dist
|
||||
/node_modules
|
||||
|
||||
.DS_Store
|
||||
|
||||
package-lock.json
|
||||
yarn.lock
|
@ -1,28 +0,0 @@
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
$ npm install # or pnpm install or yarn install
|
||||
```
|
||||
|
||||
### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm run dev`
|
||||
|
||||
Runs the app in the development mode.<br>
|
||||
Open [http://localhost:5173](http://localhost:5173) to view it in the browser.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `dist` folder.<br>
|
||||
It correctly bundles Solid in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.<br>
|
||||
Your app is ready to be deployed!
|
||||
|
||||
## Deployment
|
||||
|
||||
Learn more about deploying your application with the [documentations](https://vitejs.dev/guide/static-deploy.html)
|
@ -1,6 +0,0 @@
|
||||
package view
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed all:dist
|
||||
var FS embed.FS
|
@ -1,27 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Embedded Interactive</title>
|
||||
|
||||
<style>
|
||||
body, html {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
iframe {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: block;
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<iframe src="http://localhost:8445/realms/1?noTitle=1"></iframe>
|
||||
</body>
|
||||
</html>
|
@ -1,13 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Goatplaza</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
@ -1,36 +0,0 @@
|
||||
{
|
||||
"name": "@hydrogen/interactive-web",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||
"@solidjs/router": "^0.10.10",
|
||||
"artplayer": "^5.1.1",
|
||||
"cherry-markdown": "^0.8.38",
|
||||
"dompurify": "^3.0.8",
|
||||
"flv.js": "^1.6.2",
|
||||
"hls.js": "^1.5.3",
|
||||
"marked": "^12.0.0",
|
||||
"medium-zoom": "^1.1.0",
|
||||
"solid-js": "^1.8.7",
|
||||
"universal-cookie": "^7.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"daisyui": "^4.6.1",
|
||||
"postcss": "^8.4.33",
|
||||
"solid-devtools": "^0.29.3",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.8",
|
||||
"vite-plugin-solid": "^2.8.0"
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 12 KiB |
@ -1,5 +0,0 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": false
|
||||
}
|
@ -1,184 +0,0 @@
|
||||
:root {
|
||||
--bs-body-font-family: "IBM Plex Sans", "Noto Serif SC", sans-serif !important;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: var(--bs-body-font-family);
|
||||
}
|
||||
|
||||
/* ibm-plex-sans-100 - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'IBM Plex Sans';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
src: url('./ibm-plex-sans-v19-latin-100.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
/* ibm-plex-sans-100italic - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'IBM Plex Sans';
|
||||
font-style: italic;
|
||||
font-weight: 100;
|
||||
src: url('./ibm-plex-sans-v19-latin-100italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
/* ibm-plex-sans-200 - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'IBM Plex Sans';
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
src: url('./ibm-plex-sans-v19-latin-200.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
/* ibm-plex-sans-200italic - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'IBM Plex Sans';
|
||||
font-style: italic;
|
||||
font-weight: 200;
|
||||
src: url('./ibm-plex-sans-v19-latin-200italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
/* ibm-plex-sans-300 - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'IBM Plex Sans';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url('./ibm-plex-sans-v19-latin-300.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
/* ibm-plex-sans-300italic - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'IBM Plex Sans';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: url('./ibm-plex-sans-v19-latin-300italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
/* ibm-plex-sans-regular - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'IBM Plex Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('./ibm-plex-sans-v19-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
/* ibm-plex-sans-italic - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'IBM Plex Sans';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url('./ibm-plex-sans-v19-latin-italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
/* ibm-plex-sans-500 - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'IBM Plex Sans';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url('./ibm-plex-sans-v19-latin-500.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
/* ibm-plex-sans-500italic - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'IBM Plex Sans';
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
src: url('./ibm-plex-sans-v19-latin-500italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
/* ibm-plex-sans-600 - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'IBM Plex Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url('./ibm-plex-sans-v19-latin-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
/* ibm-plex-sans-600italic - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'IBM Plex Sans';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: url('./ibm-plex-sans-v19-latin-600italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
/* ibm-plex-sans-700 - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'IBM Plex Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url('./ibm-plex-sans-v19-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
/* ibm-plex-sans-700italic - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'IBM Plex Sans';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url('./ibm-plex-sans-v19-latin-700italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* noto-serif-sc-200 - chinese-simplified */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "Noto Serif SC";
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
src: url("./noto-serif-sc-v22-chinese-simplified-200.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* noto-serif-sc-300 - chinese-simplified */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "Noto Serif SC";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url("./noto-serif-sc-v22-chinese-simplified-300.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* noto-serif-sc-regular - chinese-simplified */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "Noto Serif SC";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url("./noto-serif-sc-v22-chinese-simplified-regular.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* noto-serif-sc-500 - chinese-simplified */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "Noto Serif SC";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url("./noto-serif-sc-v22-chinese-simplified-500.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* noto-serif-sc-600 - chinese-simplified */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "Noto Serif SC";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url("./noto-serif-sc-v22-chinese-simplified-600.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* noto-serif-sc-700 - chinese-simplified */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "Noto Serif SC";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url("./noto-serif-sc-v22-chinese-simplified-700.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* noto-serif-sc-900 - chinese-simplified */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "Noto Serif SC";
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
src: url("./noto-serif-sc-v22-chinese-simplified-900.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,22 +0,0 @@
|
||||
import { Show } from "solid-js";
|
||||
|
||||
export default function Avatar(props: { user: any }) {
|
||||
return (
|
||||
<Show
|
||||
when={props.user?.avatar}
|
||||
fallback={
|
||||
<div class="avatar placeholder">
|
||||
<div class="w-12 h-12 bg-neutral text-neutral-content">
|
||||
<span class="text-xl uppercase">{props.user?.name?.substring(0, 1)}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="avatar">
|
||||
<div class="w-12">
|
||||
<img alt="avatar" src={props.user?.avatar} />
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
);
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
export default function LoadingAnimation() {
|
||||
return (
|
||||
<div class="w-full border-b border-base-200 pt-5 pb-7 text-center">
|
||||
<p class="loading loading-lg loading-infinity"></p>
|
||||
<p>Listening to the latest news...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
.description {
|
||||
color: var(--fallback-bc, oklch(var(--bc)/.8));
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
import { createSignal, Show } from "solid-js";
|
||||
|
||||
import styles from "./NameCard.module.css";
|
||||
import { getAtk } from "../stores/userinfo.tsx";
|
||||
import { request } from "../scripts/request.ts";
|
||||
|
||||
export default function NameCard(props: { accountId: string, onError: (messasge: string | null) => void }) {
|
||||
const [info, setInfo] = createSignal<any>(null);
|
||||
const [isFollowing, setIsFollowing] = createSignal(false);
|
||||
|
||||
const [_, setLoading] = createSignal(true);
|
||||
const [submitting, setSubmitting] = createSignal(false);
|
||||
|
||||
async function readInfo() {
|
||||
setLoading(true);
|
||||
const res = await request(`/api/users/${props.accountId}`);
|
||||
if (res.status !== 200) {
|
||||
props.onError(await res.text());
|
||||
} else {
|
||||
setInfo(await res.json());
|
||||
props.onError(null);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
async function readIsFollowing() {
|
||||
setLoading(true);
|
||||
const res = await request(`/api/users/${props.accountId}/follow`, {
|
||||
method: "GET",
|
||||
headers: { Authorization: `Bearer ${getAtk()}` }
|
||||
});
|
||||
if (res.status === 200) {
|
||||
const data = await res.json();
|
||||
setIsFollowing(data["is_followed"]);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
async function follow() {
|
||||
setSubmitting(true);
|
||||
const res = await request(`/api/users/${props.accountId}/follow`, {
|
||||
method: "POST",
|
||||
headers: { "Authorization": `Bearer ${getAtk()}` }
|
||||
});
|
||||
if (res.status !== 201 && res.status !== 204) {
|
||||
props.onError(await res.text());
|
||||
} else {
|
||||
await readIsFollowing();
|
||||
props.onError(null);
|
||||
}
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
readInfo();
|
||||
readIsFollowing();
|
||||
|
||||
return (
|
||||
<div class="relative">
|
||||
<figure id="banner">
|
||||
<img class="object-cover w-full h-36" src="https://images.unsplash.com/photo-1464822759023-fed622ff2c3b"
|
||||
alt="banner" />
|
||||
</figure>
|
||||
|
||||
<div id="avatar" class="avatar absolute border-4 border-base-200 left-[20px] top-[4.5rem]">
|
||||
<div class="w-24">
|
||||
<img src={info()?.avatar} alt="avatar" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="actions" class="flex justify-end">
|
||||
<div>
|
||||
<Show when={isFollowing()} fallback={
|
||||
<button type="button" class="btn btn-primary" disabled={submitting()} onClick={() => follow()}>
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
Follow
|
||||
</button>
|
||||
}>
|
||||
<button type="button" class="btn btn-accent" disabled={submitting()} onClick={() => follow()}>
|
||||
<i class="fa-solid fa-check"></i>
|
||||
Followed
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="description" class="px-6 pb-7">
|
||||
<h2 class="text-2xl font-bold">{info()?.name}</h2>
|
||||
<p class="text-md">{info()?.description}</p>
|
||||
<div class={`mt-2 ${styles.description}`}>
|
||||
<p class="text-xs">
|
||||
<i class="fa-solid fa-calendar-days me-2"></i>
|
||||
Joined at {new Date(info()?.created_at).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
.attachmentsControl {
|
||||
background-color: transparent !important;
|
||||
}
|
@ -1,158 +0,0 @@
|
||||
import { createEffect, createMemo, createSignal, Match, Switch } from "solid-js";
|
||||
import mediumZoom from "medium-zoom";
|
||||
|
||||
import styles from "./PostAttachments.module.css";
|
||||
|
||||
import Artplayer from "artplayer";
|
||||
import HlsJs from "hls.js";
|
||||
import FlvJs from "flv.js";
|
||||
|
||||
function Video({ url, ...rest }: any) {
|
||||
let container: any;
|
||||
|
||||
function playM3u8(video: HTMLVideoElement, url: string, art: Artplayer) {
|
||||
if (HlsJs.isSupported()) {
|
||||
if (art.hls) art.hls.destroy();
|
||||
const hls = new HlsJs();
|
||||
hls.loadSource(url);
|
||||
hls.attachMedia(video);
|
||||
art.hls = hls;
|
||||
art.on("destroy", () => hls.destroy());
|
||||
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
|
||||
video.src = url;
|
||||
} else {
|
||||
art.notice.show = "Unsupported playback format: m3u8";
|
||||
}
|
||||
}
|
||||
|
||||
function playFlv(video: HTMLVideoElement, url: string, art: Artplayer) {
|
||||
if (FlvJs.isSupported()) {
|
||||
if (art.flv) art.flv.destroy();
|
||||
const flv = FlvJs.createPlayer({ type: "flv", url });
|
||||
flv.attachMediaElement(video);
|
||||
flv.load();
|
||||
art.flv = flv;
|
||||
art.on("destroy", () => flv.destroy());
|
||||
} else {
|
||||
art.notice.show = "Unsupported playback format: flv";
|
||||
}
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
new Artplayer({
|
||||
container: container as HTMLDivElement,
|
||||
url: url,
|
||||
setting: true,
|
||||
flip: true,
|
||||
loop: true,
|
||||
playbackRate: true,
|
||||
aspectRatio: true,
|
||||
subtitleOffset: true,
|
||||
fullscreen: true,
|
||||
fullscreenWeb: true,
|
||||
theme: "#49509e",
|
||||
customType: {
|
||||
m3u8: playM3u8,
|
||||
flv: playFlv
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={container} {...rest}></div>
|
||||
);
|
||||
}
|
||||
|
||||
function Audio({ url, caption, ...rest }: any) {
|
||||
|
||||
return (
|
||||
<figure {...rest}>
|
||||
<figcaption>{caption}</figcaption>
|
||||
<audio controls src={url} />
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default function PostAttachments(props: { attachments: any[] }) {
|
||||
if (props.attachments.length <= 0) return null;
|
||||
|
||||
const [focus, setFocus] = createSignal(0);
|
||||
const item = createMemo(() => props.attachments[focus()]);
|
||||
|
||||
function getRenderType(item: any): string {
|
||||
return item.mimetype.split("/")[0];
|
||||
}
|
||||
|
||||
function getUrl(item: any): string {
|
||||
return item.external_url ? item.external_url : `/api/attachments/o/${item.file_id}`;
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
mediumZoom(document.querySelectorAll(".attachment-image img"), {
|
||||
background: "var(--fallback-b1,oklch(var(--b1)/1))"
|
||||
});
|
||||
}, [focus()]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p class="text-xs mt-3 mb-2">
|
||||
<i class="fa-solid fa-paperclip me-2"></i>
|
||||
Attached {props.attachments.length} file{props.attachments.length > 1 ? "s" : null}
|
||||
</p>
|
||||
<div class="border border-base-200">
|
||||
<Switch fallback={
|
||||
<div class="py-16 flex justify-center items-center">
|
||||
<div class="text-center">
|
||||
<i class="fa-solid fa-circle-question text-3xl"></i>
|
||||
<p class="mt-3">{item().filename}</p>
|
||||
|
||||
<div class="flex gap-2 w-full">
|
||||
<p class="text-sm">{item().filesize <= 0 ? "Unknown" : item().filesize} Bytes</p>
|
||||
<p class="text-sm">{item().mimetype}</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<a class="link" href={getUrl(item())} target="_blank">Open in browser</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}>
|
||||
<Match when={getRenderType(item()) === "image"}>
|
||||
<figure class="attachment-image">
|
||||
<img class="object-cover" src={getUrl(item())} alt={item().filename} />
|
||||
</figure>
|
||||
</Match>
|
||||
<Match when={getRenderType(item()) === "audio"}>
|
||||
<Audio class="p-5 flex flex-col items-center justify-center gap-2 w-full" url={getUrl(item())}
|
||||
caption={item().filename} />
|
||||
</Match>
|
||||
<Match when={getRenderType(item()) === "video"}>
|
||||
<Video class="h-[360px] w-full" url={getUrl(item())} caption={item().filename} />
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
<div id="attachments-control" class="flex justify-between border-t border-base-200">
|
||||
<div class="flex">
|
||||
<button class={`w-12 h-12 btn btn-ghost ${styles.attachmentsControl}`}
|
||||
disabled={focus() - 1 < 0}
|
||||
onClick={() => setFocus(focus() - 1)}>
|
||||
<i class="fa-solid fa-caret-left"></i>
|
||||
</button>
|
||||
<button class={`w-12 h-12 btn btn-ghost ${styles.attachmentsControl}`}
|
||||
disabled={focus() + 1 >= props.attachments.length}
|
||||
onClick={() => setFocus(focus() + 1)}>
|
||||
<i class="fa-solid fa-caret-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="h-12 px-5 py-3.5 text-sm">
|
||||
File {focus() + 1}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,399 +0,0 @@
|
||||
import { closeModel, openModel } from "../../scripts/modals.ts";
|
||||
import { createSignal, For, Match, Show, Switch } from "solid-js";
|
||||
import { getAtk, useUserinfo } from "../../stores/userinfo.tsx";
|
||||
import { request } from "../../scripts/request.ts";
|
||||
|
||||
import styles from "./PostPublish.module.css";
|
||||
|
||||
export default function PostEditActions(props: {
|
||||
editing?: any;
|
||||
onInputAlias: (value: string) => void;
|
||||
onInputPublish: (value: string) => void;
|
||||
onInputAttachments: (value: any[]) => void;
|
||||
onInputCategories: (categories: any[]) => void;
|
||||
onInputTags: (tags: any[]) => void;
|
||||
onError: (message: string | null) => void;
|
||||
}) {
|
||||
const userinfo = useUserinfo();
|
||||
|
||||
const [uploading, setUploading] = createSignal(false);
|
||||
|
||||
const [attachments, setAttachments] = createSignal<any[]>(props.editing?.attachments ?? []);
|
||||
const [categories, setCategories] = createSignal<{ alias: string; name: string }[]>(props.editing?.categories ?? []);
|
||||
const [tags, setTags] = createSignal<{ alias: string; name: string }[]>(props.editing?.tags ?? []);
|
||||
|
||||
const [availableCategories, setAvailableCategories] = createSignal<any[]>([]);
|
||||
const [attachmentMode, setAttachmentMode] = createSignal(0);
|
||||
|
||||
async function readCategories() {
|
||||
const res = await request("/api/categories");
|
||||
if (res.status === 200) {
|
||||
setAvailableCategories(await res.json());
|
||||
}
|
||||
}
|
||||
|
||||
readCategories();
|
||||
|
||||
async function uploadAttachment(evt: SubmitEvent) {
|
||||
evt.preventDefault();
|
||||
|
||||
const form = evt.target as HTMLFormElement;
|
||||
const data = new FormData(form);
|
||||
if (!data.get("attachment")) return;
|
||||
|
||||
setUploading(true);
|
||||
const res = await request("/api/attachments", {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${getAtk()}` },
|
||||
body: data,
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
props.onError(await res.text());
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setAttachments(attachments().concat([data.info]));
|
||||
props.onInputAttachments(attachments());
|
||||
props.onError(null);
|
||||
form.reset();
|
||||
}
|
||||
setUploading(false);
|
||||
}
|
||||
|
||||
function addAttachment(evt: SubmitEvent) {
|
||||
evt.preventDefault();
|
||||
|
||||
const form = evt.target as HTMLFormElement;
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
|
||||
setAttachments(
|
||||
attachments().concat([
|
||||
{
|
||||
...data,
|
||||
author_id: userinfo?.profiles?.id,
|
||||
},
|
||||
]),
|
||||
);
|
||||
props.onInputAttachments(attachments());
|
||||
form.reset();
|
||||
}
|
||||
|
||||
function removeAttachment(idx: number) {
|
||||
const data = attachments().slice();
|
||||
data.splice(idx, 1);
|
||||
setAttachments(data);
|
||||
props.onInputAttachments(attachments());
|
||||
}
|
||||
|
||||
function addCategory(evt: SubmitEvent) {
|
||||
evt.preventDefault();
|
||||
|
||||
const form = evt.target as HTMLFormElement;
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
if (!data.category) return;
|
||||
|
||||
const item = availableCategories().find((item) => item.alias === data.category);
|
||||
|
||||
setCategories(categories().concat([item]));
|
||||
props.onInputCategories(categories());
|
||||
form.reset();
|
||||
}
|
||||
|
||||
function removeCategory(idx: number) {
|
||||
const data = categories().slice();
|
||||
data.splice(idx, 1);
|
||||
setCategories(data);
|
||||
props.onInputCategories(categories());
|
||||
}
|
||||
|
||||
function addTag(evt: SubmitEvent) {
|
||||
evt.preventDefault();
|
||||
|
||||
const form = evt.target as HTMLFormElement;
|
||||
const data = Object.fromEntries(new FormData(evt.target as HTMLFormElement));
|
||||
if (!data.alias) data.alias = crypto.randomUUID().replace(/-/g, "");
|
||||
if (!data.name) return;
|
||||
|
||||
setTags(tags().concat([data as any]));
|
||||
props.onInputTags(tags());
|
||||
form.reset();
|
||||
}
|
||||
|
||||
function removeTag(idx: number) {
|
||||
const data = tags().slice();
|
||||
data.splice(idx, 1);
|
||||
setTags(data);
|
||||
props.onInputTags(tags());
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="flex pl-[20px]">
|
||||
<button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#alias")}>
|
||||
<i class="fa-solid fa-link"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#attachments")}>
|
||||
<i class="fa-solid fa-paperclip"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#planning-publish")}>
|
||||
<i class="fa-solid fa-calendar-day"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#categories-and-tags")}>
|
||||
<i class="fa-solid fa-tag"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<dialog id="alias" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mx-1">Permalink</h3>
|
||||
<label class="form-control w-full mt-3">
|
||||
<div class="label">
|
||||
<span class="label-text">Alias</span>
|
||||
</div>
|
||||
<input
|
||||
name="alias"
|
||||
type="text"
|
||||
placeholder="Type here"
|
||||
class="input input-bordered w-full"
|
||||
value={props.editing?.alias ?? ""}
|
||||
onInput={(evt) => props.onInputAlias(evt.target.value)}
|
||||
/>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Leave blank to generate a random string.</span>
|
||||
</div>
|
||||
</label>
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn" onClick={() => closeModel("#alias")}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="planning-publish" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mx-1">Planning Publish</h3>
|
||||
<label class="form-control w-full mt-3">
|
||||
<div class="label">
|
||||
<span class="label-text">Published At</span>
|
||||
</div>
|
||||
<input
|
||||
name="published_at"
|
||||
type="datetime-local"
|
||||
placeholder="Pick a date"
|
||||
class="input input-bordered w-full"
|
||||
value={props.editing?.published_at ?? ""}
|
||||
onInput={(evt) => props.onInputAlias(evt.target.value)}
|
||||
/>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">
|
||||
Before this time, your post will not be visible for everyone. You can modify this plan on Creator Hub.
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn" onClick={() => closeModel("#planning-publish")}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="attachments" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mx-1">Attachments</h3>
|
||||
|
||||
<div role="tablist" class="tabs tabs-boxed mt-3">
|
||||
<input
|
||||
type="radio"
|
||||
name="attachment"
|
||||
role="tab"
|
||||
class="tab"
|
||||
aria-label="File picker"
|
||||
checked={attachmentMode() === 0}
|
||||
onClick={() => setAttachmentMode(0)}
|
||||
/>
|
||||
<input
|
||||
type="radio"
|
||||
name="attachment"
|
||||
role="tab"
|
||||
class="tab"
|
||||
aria-label="External link"
|
||||
checked={attachmentMode() === 1}
|
||||
onClick={() => setAttachmentMode(1)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Switch>
|
||||
<Match when={attachmentMode() === 0}>
|
||||
<form class="w-full mt-2" onSubmit={uploadAttachment}>
|
||||
<label class="form-control">
|
||||
<div class="label">
|
||||
<span class="label-text">Pick a file</span>
|
||||
</div>
|
||||
<div class="join">
|
||||
<input
|
||||
required
|
||||
type="file"
|
||||
name="attachment"
|
||||
class="join-item file-input file-input-bordered w-full"
|
||||
/>
|
||||
<button type="submit" class="join-item btn btn-primary" disabled={uploading()}>
|
||||
<i class="fa-solid fa-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Click upload to add this file into list</span>
|
||||
</div>
|
||||
</label>
|
||||
</form>
|
||||
</Match>
|
||||
<Match when={attachmentMode() === 1}>
|
||||
<form class="w-full mt-2" onSubmit={addAttachment}>
|
||||
<label class="form-control">
|
||||
<div class="label">
|
||||
<span class="label-text">Attach an external file</span>
|
||||
</div>
|
||||
<div class="join">
|
||||
<input
|
||||
required
|
||||
type="text"
|
||||
name="mimetype"
|
||||
class="join-item input input-bordered w-full"
|
||||
placeholder="Mimetype"
|
||||
/>
|
||||
<input
|
||||
required
|
||||
type="text"
|
||||
name="filename"
|
||||
class="join-item input input-bordered w-full"
|
||||
placeholder="Name"
|
||||
/>
|
||||
</div>
|
||||
<div class="join">
|
||||
<input
|
||||
required
|
||||
type="text"
|
||||
name="external_url"
|
||||
class="join-item input input-bordered w-full"
|
||||
placeholder="External URL"
|
||||
/>
|
||||
<button type="submit" class="join-item btn btn-primary">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Click add button to add it into list</span>
|
||||
</div>
|
||||
</label>
|
||||
</form>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
<Show when={attachments().length > 0}>
|
||||
<h3 class="font-bold mt-3 mx-1">Attachment list</h3>
|
||||
<ol class="mt-2 mx-1 text-sm">
|
||||
<For each={attachments()}>
|
||||
{(item, idx) => (
|
||||
<li>
|
||||
<i class="fa-regular fa-file me-2"></i>
|
||||
{item.filename}
|
||||
<button class="ml-2" onClick={() => removeAttachment(idx())}>
|
||||
<i class="fa-solid fa-delete-left"></i>
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ol>
|
||||
</Show>
|
||||
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn" onClick={() => closeModel("#attachments")}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="categories-and-tags" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mx-1">Categories & Tags</h3>
|
||||
<form class="w-full mt-3" onSubmit={addCategory}>
|
||||
<label class="form-control">
|
||||
<div class="label">
|
||||
<span class="label-text">Add a category</span>
|
||||
</div>
|
||||
<div class="join">
|
||||
<select name="category" class="join-item select select-bordered w-full">
|
||||
<For each={availableCategories()}>{(item) => <option value={item.alias}>{item.name}</option>}</For>
|
||||
</select>
|
||||
<button type="submit" class="join-item btn btn-primary">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
</form>
|
||||
|
||||
<Show when={categories().length > 0}>
|
||||
<h3 class="font-bold mt-3 mx-1">Category list</h3>
|
||||
<ol class="mt-2 mx-1 text-sm">
|
||||
<For each={categories()}>
|
||||
{(item, idx) => (
|
||||
<li>
|
||||
<i class="fa-solid fa-layer-group me-2"></i>
|
||||
{item.name} <span class={styles.description}>#{item.alias}</span>
|
||||
<button class="ml-2" onClick={() => removeCategory(idx())}>
|
||||
<i class="fa-solid fa-delete-left"></i>
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ol>
|
||||
</Show>
|
||||
|
||||
<form class="w-full mt-3" onSubmit={addTag}>
|
||||
<label class="form-control">
|
||||
<div class="label">
|
||||
<span class="label-text">Add a tag</span>
|
||||
</div>
|
||||
<div class="join">
|
||||
<input type="text" name="alias" placeholder="Alias" class="join-item input input-bordered w-full" />
|
||||
<input type="text" name="name" placeholder="Name" class="join-item input input-bordered w-full" />
|
||||
<button type="submit" class="join-item btn btn-primary">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">
|
||||
Alias is the url key of this tag. Lowercase only, required length 4-24. Leave blank for auto generate.
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</form>
|
||||
|
||||
<Show when={tags().length > 0}>
|
||||
<h3 class="font-bold mt-3 mx-1">Category list</h3>
|
||||
<ol class="mt-2 mx-1 text-sm">
|
||||
<For each={tags()}>
|
||||
{(item, idx) => (
|
||||
<li>
|
||||
<i class="fa-solid fa-tag me-2"></i>
|
||||
{item.name} <span class={styles.description}>#{item.alias}</span>
|
||||
<button class="ml-2" onClick={() => removeTag(idx())}>
|
||||
<i class="fa-solid fa-delete-left"></i>
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ol>
|
||||
</Show>
|
||||
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn" onClick={() => closeModel("#categories-and-tags")}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,223 +0,0 @@
|
||||
import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js";
|
||||
|
||||
import Cherry from "cherry-markdown";
|
||||
import "cherry-markdown/dist/cherry-markdown.min.css";
|
||||
import { getAtk } from "../../stores/userinfo.tsx";
|
||||
import { request } from "../../scripts/request.ts";
|
||||
import PostEditActions from "./PostEditActions.tsx";
|
||||
|
||||
export default function PostEditor(props: {
|
||||
editing?: any,
|
||||
onError: (message: string | null) => void,
|
||||
onPost: () => void
|
||||
}) {
|
||||
let editorContainer: any;
|
||||
const [editor, setEditor] = createSignal<Cherry>();
|
||||
const [realmList, setRealmList] = createSignal<any[]>([]);
|
||||
|
||||
const [submitting, setSubmitting] = createSignal(false);
|
||||
|
||||
const [alias, setAlias] = createSignal("");
|
||||
const [publishedAt, setPublishedAt] = createSignal("");
|
||||
const [attachments, setAttachments] = createSignal<any[]>([]);
|
||||
const [categories, setCategories] = createSignal<{ alias: string, name: string }[]>([]);
|
||||
const [tags, setTags] = createSignal<{ alias: string, name: string }[]>([]);
|
||||
|
||||
const theme = createMemo(() => {
|
||||
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
return "dark";
|
||||
} else {
|
||||
return "light";
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
editor()?.setTheme(theme());
|
||||
}, [editor(), theme()]);
|
||||
|
||||
onMount(() => {
|
||||
if (editorContainer) {
|
||||
setEditor(new Cherry({
|
||||
el: editorContainer,
|
||||
value: "Welcome to the creator hub! " +
|
||||
"We provide a better editor than normal mode for you! " +
|
||||
"So you can tell us your mind clearly. " +
|
||||
"Delete this paragraph and getting start!"
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
setAttachments(props.editing?.attachments ?? []);
|
||||
setCategories(props.editing?.categories ?? []);
|
||||
setTags(props.editing?.tags ?? []);
|
||||
editor()?.setValue(props.editing?.content);
|
||||
}, [props.editing]);
|
||||
|
||||
async function listRealm() {
|
||||
const res = await request("/api/realms/me/available", {
|
||||
headers: { "Authorization": `Bearer ${getAtk()}` }
|
||||
});
|
||||
if (res.status === 200) {
|
||||
setRealmList(await res.json());
|
||||
}
|
||||
}
|
||||
|
||||
listRealm();
|
||||
|
||||
async function doPost(evt: SubmitEvent) {
|
||||
evt.preventDefault();
|
||||
|
||||
const form = evt.target as HTMLFormElement;
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
if (!editor()?.getValue()) return;
|
||||
|
||||
setSubmitting(true);
|
||||
const res = await request("/api/posts", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${getAtk()}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
alias: alias() ? alias() : crypto.randomUUID().replace(/-/g, ""),
|
||||
title: data.title,
|
||||
content: editor()?.getValue(),
|
||||
attachments: attachments(),
|
||||
categories: categories(),
|
||||
tags: tags(),
|
||||
realm_id: parseInt(data.realm as string) !== 0 ? parseInt(data.realm as string) : undefined,
|
||||
published_at: publishedAt() ? new Date(publishedAt()) : new Date()
|
||||
})
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
props.onError(await res.text());
|
||||
} else {
|
||||
form.reset();
|
||||
props.onError(null);
|
||||
props.onPost();
|
||||
}
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
async function doEdit(evt: SubmitEvent) {
|
||||
evt.preventDefault();
|
||||
|
||||
const form = evt.target as HTMLFormElement;
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
if (!editor()?.getValue()) return;
|
||||
|
||||
setSubmitting(true);
|
||||
const res = await request(`/api/posts/${props.editing?.id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${getAtk()}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
alias: alias() ? alias() : crypto.randomUUID().replace(/-/g, ""),
|
||||
title: data.title,
|
||||
content: editor()?.getValue(),
|
||||
attachments: attachments(),
|
||||
categories: categories(),
|
||||
tags: tags(),
|
||||
published_at: publishedAt() ? new Date(publishedAt()) : new Date()
|
||||
})
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
props.onError(await res.text());
|
||||
} else {
|
||||
form.reset();
|
||||
props.onError(null);
|
||||
props.onPost();
|
||||
}
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
setAttachments([]);
|
||||
setCategories([]);
|
||||
setTags([]);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onReset={resetForm} onSubmit={(evt) => props.editing ? doEdit(evt) : doPost(evt)}>
|
||||
<div>
|
||||
<div ref={editorContainer}></div>
|
||||
</div>
|
||||
|
||||
<div class="border-y border-base-200">
|
||||
<PostEditActions
|
||||
editing={props.editing}
|
||||
onInputAlias={setAlias}
|
||||
onInputPublish={setPublishedAt}
|
||||
onInputAttachments={setAttachments}
|
||||
onInputCategories={setCategories}
|
||||
onInputTags={setTags}
|
||||
onError={props.onError}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pt-3 pb-7 px-7">
|
||||
<Show when={!props.editing} fallback={
|
||||
<label class="form-control w-full mb-3">
|
||||
<div class="label">
|
||||
<span class="label-text">Publish region</span>
|
||||
</div>
|
||||
<input readonly type="text" class="input input-bordered"
|
||||
value={`You published this post in realm #${props.editing?.realm_id ?? "global"}`} />
|
||||
</label>
|
||||
}>
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">Publish region</span>
|
||||
</div>
|
||||
<select name="realm" class="select select-bordered">
|
||||
<option value={0} selected>Global</option>
|
||||
<For each={realmList()}>
|
||||
{item => <option value={item.id}>{item.name}</option>}
|
||||
</For>
|
||||
</select>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Will show realms you joined or created.</span>
|
||||
</div>
|
||||
</label>
|
||||
</Show>
|
||||
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">Post title</span>
|
||||
</div>
|
||||
<input value={props.editing?.title ?? ""} name="title" type="text" placeholder="Type here"
|
||||
class="input input-bordered w-full" />
|
||||
</label>
|
||||
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">Post description</span>
|
||||
</div>
|
||||
<textarea value={props.editing?.description ?? ""} disabled name="description"
|
||||
placeholder="Not available now"
|
||||
class="textarea textarea-bordered w-full" />
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Won't display in the post list when your post is too long.</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">Post thumbnail</span>
|
||||
</div>
|
||||
<input disabled name="thumbnail" type="file" placeholder="Not available now"
|
||||
class="file-input file-input-bordered w-full" />
|
||||
</label>
|
||||
|
||||
<button type="submit" class="btn btn-primary mt-7" disabled={submitting()}>
|
||||
<Show when={submitting()} fallback={"Submit"}>
|
||||
<span class="loading"></span>
|
||||
</Show>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
@ -1,215 +0,0 @@
|
||||
import { createSignal, For, Show } from "solid-js";
|
||||
import { getAtk, useUserinfo } from "../../stores/userinfo.tsx";
|
||||
import { request } from "../../scripts/request.ts";
|
||||
import PostAttachments from "./PostAttachments.tsx";
|
||||
import * as marked from "marked";
|
||||
import DOMPurify from "dompurify";
|
||||
import Avatar from "../Avatar.tsx";
|
||||
|
||||
export default function PostItem(props: {
|
||||
post: any;
|
||||
noClick?: boolean;
|
||||
noAuthor?: boolean;
|
||||
noControl?: boolean;
|
||||
noRelated?: boolean;
|
||||
noContent?: boolean;
|
||||
onRepost?: (post: any) => void;
|
||||
onReply?: (post: any) => void;
|
||||
onEdit?: (post: any) => void;
|
||||
onDelete?: (post: any) => void;
|
||||
onSearch?: (filter: any) => void;
|
||||
onError: (message: string | null) => void;
|
||||
onReact: () => void;
|
||||
}) {
|
||||
const [reacting, setReacting] = createSignal(false);
|
||||
|
||||
const userinfo = useUserinfo();
|
||||
|
||||
async function reactPost(item: any, type: string) {
|
||||
setReacting(true);
|
||||
const res = await request(`/api/posts/${item.id}/react/${type}`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${getAtk()}` }
|
||||
});
|
||||
if (res.status !== 201 && res.status !== 204) {
|
||||
props.onError(await res.text());
|
||||
} else {
|
||||
props.onReact();
|
||||
props.onError(null);
|
||||
}
|
||||
setReacting(false);
|
||||
}
|
||||
|
||||
const content = <article class="prose" innerHTML={DOMPurify.sanitize(marked.parse(props.post.content) as string)} />;
|
||||
|
||||
return (
|
||||
<div class="post-item">
|
||||
<Show when={!props.noAuthor}>
|
||||
<a href={`/accounts/${props.post.author.name}`}>
|
||||
<div class="flex bg-base-200">
|
||||
<div class="pl-[20px]">
|
||||
<Avatar user={props.post.author} />
|
||||
</div>
|
||||
<div class="flex items-center px-5">
|
||||
<div>
|
||||
<h3 class="font-bold text-sm">{props.post.author.nick}</h3>
|
||||
<p class="text-xs">{props.post.author.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</Show>
|
||||
|
||||
<Show when={!props.noContent}>
|
||||
<div class="px-7 py-5">
|
||||
<h2 class="card-title">{props.post.title}</h2>
|
||||
<Show when={!props.noClick} fallback={content}>
|
||||
<a href={`/posts/${props.post.alias}`}>{content}</a>
|
||||
</Show>
|
||||
|
||||
<div class="mt-2 flex gap-2">
|
||||
<For each={props.post.categories}>
|
||||
{(item) => (
|
||||
<a href={`/search?category=${item.alias}`} class="badge badge-primary">
|
||||
<i class="fa-solid fa-layer-group me-1.5"></i>
|
||||
{item.name}
|
||||
</a>
|
||||
)}
|
||||
</For>
|
||||
<For each={props.post.tags}>
|
||||
{(item) => (
|
||||
<a href={`/search?tag=${item.alias}`} class="badge badge-accent">
|
||||
<i class="fa-solid fa-tag me-1.5"></i>
|
||||
{item.name}
|
||||
</a>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<Show when={props.post.attachments?.length > 0}>
|
||||
<div>
|
||||
<PostAttachments attachments={props.post.attachments ?? []} />
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={!props.noRelated && props.post.repost_to}>
|
||||
<p class="text-xs mt-3 mb-2">
|
||||
<i class="fa-solid fa-retweet me-2"></i>
|
||||
Reposted a post
|
||||
</p>
|
||||
<div class="border border-base-200 mb-5">
|
||||
<PostItem noControl post={props.post.repost_to} onError={props.onError} onReact={props.onReact} />
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!props.noRelated && props.post.reply_to}>
|
||||
<p class="text-xs mt-3 mb-2">
|
||||
<i class="fa-solid fa-reply me-2"></i>
|
||||
Replied a post
|
||||
</p>
|
||||
<div class="border border-base-200 mb-5">
|
||||
<PostItem noControl post={props.post.reply_to} onError={props.onError} onReact={props.onReact} />
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={!props.noControl}>
|
||||
<div class="relative">
|
||||
<Show when={!userinfo?.isLoggedIn}>
|
||||
<div
|
||||
class="px-7 py-2.5 h-12 w-full opacity-0 transition-opacity hover:opacity-100 bg-base-100 border-t border-base-200 z-[1] absolute top-0 left-0">
|
||||
<b>Login!</b> To access entire platform.
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="grid grid-cols-3 border-y border-base-200">
|
||||
<div class="max-md:col-span-2 md:col-span-1 grid grid-cols-2">
|
||||
<div class="tooltip" data-tip="Daisuki">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-block"
|
||||
disabled={reacting()}
|
||||
onClick={() => reactPost(props.post, "like")}
|
||||
>
|
||||
<i class="fa-solid fa-thumbs-up"></i>
|
||||
<code class="font-mono">{props.post.like_count}</code>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tooltip" data-tip="Daikirai">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-block"
|
||||
disabled={reacting()}
|
||||
onClick={() => reactPost(props.post, "dislike")}
|
||||
>
|
||||
<i class="fa-solid fa-thumbs-down"></i>
|
||||
<code class="font-mono">{props.post.dislike_count}</code>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-md:col-span-1 md:col-span-2 flex justify-end">
|
||||
<section class="max-md:hidden">
|
||||
<div class="tooltip" data-tip="Reply">
|
||||
<button
|
||||
type="button"
|
||||
class="indicator btn btn-ghost btn-block"
|
||||
onClick={() => props.onReply && props.onReply(props.post)}
|
||||
>
|
||||
<span class="indicator-item badge badge-sm badge-primary">{props.post.reply_count}</span>
|
||||
<i class="fa-solid fa-reply"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tooltip" data-tip="Repost">
|
||||
<button
|
||||
type="button"
|
||||
class="indicator btn btn-ghost btn-block"
|
||||
onClick={() => props.onRepost && props.onRepost(props.post)}
|
||||
>
|
||||
<span class="indicator-item badge badge-sm badge-secondary">{props.post.repost_count}</span>
|
||||
<i class="fa-solid fa-retweet"></i>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="dropdown dropdown-end">
|
||||
<div tabIndex="0" role="button" class="btn btn-ghost w-12">
|
||||
<i class="fa-solid fa-ellipsis-vertical"></i>
|
||||
</div>
|
||||
<ul tabIndex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
|
||||
<li class="md:hidden">
|
||||
<a class="flex justify-between" onClick={() => props.onReply && props.onReply(props.post)}>
|
||||
<span>Reply</span>
|
||||
<span class="badge badge-primary">{props.post.reply_count}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="md:hidden">
|
||||
<a class="flex justify-between" onClick={() => props.onRepost && props.onRepost(props.post)}>
|
||||
<span>Repost</span>
|
||||
<span class="badge badge-secondary">{props.post.repost_count}</span>
|
||||
</a>
|
||||
</li>
|
||||
<Show when={userinfo?.profiles?.id === props.post.author_id}>
|
||||
<li>
|
||||
<a onClick={() => props.onDelete && props.onDelete(props.post)}>Delete</a>
|
||||
</li>
|
||||
</Show>
|
||||
<Show when={userinfo?.profiles?.id === props.post.author_id}>
|
||||
<li>
|
||||
<a onClick={() => props.onEdit && props.onEdit(props.post)}>Edit</a>
|
||||
</li>
|
||||
</Show>
|
||||
<li>
|
||||
<a>Report</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
.paginationControl {
|
||||
background-color: transparent !important;
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
import { createMemo, createSignal, For, Show } from "solid-js";
|
||||
|
||||
import styles from "./PostList.module.css";
|
||||
import PostItem from "./PostItem.tsx";
|
||||
import LoadingAnimation from "../LoadingAnimation.tsx";
|
||||
import { getAtk } from "../../stores/userinfo.tsx";
|
||||
import { request } from "../../scripts/request.ts";
|
||||
|
||||
export default function PostList(props: {
|
||||
noRelated?: boolean,
|
||||
info: { data: any[], count: number } | null,
|
||||
onRepost?: (post: any) => void,
|
||||
onReply?: (post: any) => void,
|
||||
onEdit?: (post: any) => void,
|
||||
onUpdate: (pn: number, filter?: any) => Promise<void>,
|
||||
onError: (message: string | null) => void
|
||||
}) {
|
||||
const [loading, setLoading] = createSignal(true);
|
||||
|
||||
const posts = createMemo(() => props.info?.data);
|
||||
const postCount = createMemo<number>(() => props.info?.count ?? 0);
|
||||
|
||||
const [page, setPage] = createSignal(1);
|
||||
const pageCount = createMemo(() => Math.ceil(postCount() / 10));
|
||||
|
||||
async function readPosts(filter?: any) {
|
||||
setLoading(true);
|
||||
await props.onUpdate(page(), filter);
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
readPosts();
|
||||
|
||||
async function deletePost(item: any) {
|
||||
if (!confirm(`Are you sure to delete post#${item.id}?`)) return;
|
||||
|
||||
setLoading(true);
|
||||
const res = await request(`/api/posts/${item.id}`, {
|
||||
method: "DELETE",
|
||||
headers: { "Authorization": `Bearer ${getAtk()}` }
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
props.onError(await res.text());
|
||||
} else {
|
||||
await readPosts();
|
||||
props.onError(null);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
function changePage(pn: number) {
|
||||
setPage(pn);
|
||||
readPosts().then(() => {
|
||||
setTimeout(() => window.scrollTo({ top: 0, behavior: "smooth" }), 16);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="post-list">
|
||||
<div id="posts">
|
||||
<For each={posts()}>
|
||||
{item =>
|
||||
<PostItem
|
||||
post={item}
|
||||
noRelated={props.noRelated}
|
||||
onRepost={props.onRepost}
|
||||
onReply={props.onReply}
|
||||
onEdit={props.onEdit}
|
||||
onDelete={deletePost}
|
||||
onReact={() => readPosts()}
|
||||
onError={props.onError}
|
||||
/>
|
||||
}
|
||||
</For>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<div class="join">
|
||||
<button class={`join-item btn btn-ghost ${styles.paginationControl}`} disabled={page() <= 1}
|
||||
onClick={() => changePage(page() - 1)}>
|
||||
<i class="fa-solid fa-caret-left"></i>
|
||||
</button>
|
||||
<button class="join-item btn btn-ghost">Page {page()}</button>
|
||||
<button class={`join-item btn btn-ghost ${styles.paginationControl}`} disabled={page() >= pageCount()}
|
||||
onClick={() => changePage(page() + 1)}>
|
||||
<i class="fa-solid fa-caret-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={loading()}>
|
||||
<LoadingAnimation />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
.publishInput {
|
||||
outline-style: none !important;
|
||||
outline-width: 0 !important;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--fallback-bc, oklch(var(--bc)/.8));
|
||||
}
|
@ -1,210 +0,0 @@
|
||||
import { createEffect, createSignal, Show } from "solid-js";
|
||||
import { getAtk, useUserinfo } from "../../stores/userinfo.tsx";
|
||||
import { request } from "../../scripts/request.ts";
|
||||
|
||||
import styles from "./PostPublish.module.css";
|
||||
import PostEditActions from "./PostEditActions.tsx";
|
||||
import Avatar from "../Avatar.tsx";
|
||||
|
||||
export default function PostPublish(props: {
|
||||
replying?: any,
|
||||
reposting?: any,
|
||||
editing?: any,
|
||||
realmId?: number,
|
||||
onReset: () => void,
|
||||
onError: (message: string | null) => void,
|
||||
onPost: () => void
|
||||
}) {
|
||||
const userinfo = useUserinfo();
|
||||
|
||||
if (!userinfo?.isLoggedIn) {
|
||||
return (
|
||||
<div class="py-9 flex justify-center items-center">
|
||||
<div class="text-center">
|
||||
<h2 class="text-lg font-bold">Login!</h2>
|
||||
<p>Or keep silent.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const [submitting, setSubmitting] = createSignal(false);
|
||||
|
||||
const [alias, setAlias] = createSignal("");
|
||||
const [publishedAt, setPublishedAt] = createSignal("");
|
||||
const [attachments, setAttachments] = createSignal<any[]>([]);
|
||||
const [categories, setCategories] = createSignal<{ alias: string, name: string }[]>([]);
|
||||
const [tags, setTags] = createSignal<{ alias: string, name: string }[]>([]);
|
||||
|
||||
createEffect(() => {
|
||||
setAttachments(props.editing?.attachments ?? []);
|
||||
setCategories(props.editing?.categories ?? []);
|
||||
setTags(props.editing?.tags ?? []);
|
||||
}, [props.editing]);
|
||||
|
||||
async function doPost(evt: SubmitEvent) {
|
||||
evt.preventDefault();
|
||||
|
||||
const form = evt.target as HTMLFormElement;
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
if (!data.content) return;
|
||||
|
||||
setSubmitting(true);
|
||||
const res = await request("/api/posts", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${getAtk()}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
alias: alias() ? alias() : crypto.randomUUID().replace(/-/g, ""),
|
||||
title: data.title,
|
||||
content: data.content,
|
||||
attachments: attachments(),
|
||||
categories: categories(),
|
||||
tags: tags(),
|
||||
realm_id: data.publish_in_realm ? props.realmId : undefined,
|
||||
published_at: publishedAt() ? new Date(publishedAt()) : new Date(),
|
||||
repost_to: props.reposting?.id,
|
||||
reply_to: props.replying?.id
|
||||
})
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
props.onError(await res.text());
|
||||
} else {
|
||||
form.reset();
|
||||
props.onError(null);
|
||||
props.onPost();
|
||||
}
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
async function doEdit(evt: SubmitEvent) {
|
||||
evt.preventDefault();
|
||||
|
||||
const form = evt.target as HTMLFormElement;
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
if (!data.content) return;
|
||||
|
||||
setSubmitting(true);
|
||||
const res = await request(`/api/posts/${props.editing?.id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${getAtk()}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
alias: alias() ? alias() : crypto.randomUUID().replace(/-/g, ""),
|
||||
title: data.title,
|
||||
content: data.content,
|
||||
attachments: attachments(),
|
||||
categories: categories(),
|
||||
tags: tags(),
|
||||
realm_id: props.realmId,
|
||||
published_at: publishedAt() ? new Date(publishedAt()) : new Date()
|
||||
})
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
props.onError(await res.text());
|
||||
} else {
|
||||
form.reset();
|
||||
props.onError(null);
|
||||
props.onPost();
|
||||
}
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
setAttachments([]);
|
||||
setCategories([]);
|
||||
setTags([]);
|
||||
props.onReset();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form id="publish" onSubmit={(evt) => (props.editing ? doEdit : doPost)(evt)} onReset={() => resetForm()}>
|
||||
<div id="publish-identity" class="flex border-y border-base-200">
|
||||
<div class="pl-[20px]">
|
||||
<Avatar user={userinfo?.profiles} />
|
||||
</div>
|
||||
<div class="flex flex-grow">
|
||||
<input name="title" value={props.editing?.title ?? ""}
|
||||
class={`${styles.publishInput} input w-full`}
|
||||
placeholder="The describe for a long content" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={props.reposting}>
|
||||
<div role="alert" class="bg-base-200 flex justify-between">
|
||||
<div class="px-5 py-3">
|
||||
<i class="fa-solid fa-circle-info me-3"></i>
|
||||
You are reposting a post from <b>{props.reposting?.author?.nick}</b>
|
||||
</div>
|
||||
<button type="reset" class="btn btn-ghost w-12" disabled={submitting()}>
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.replying}>
|
||||
<div role="alert" class="bg-base-200 flex justify-between">
|
||||
<div class="px-5 py-3">
|
||||
<i class="fa-solid fa-circle-info me-3"></i>
|
||||
You are replying a post from <b>{props.replying?.author?.nick}</b>
|
||||
</div>
|
||||
<button type="reset" class="btn btn-ghost w-12" disabled={submitting()}>
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.editing}>
|
||||
<div role="alert" class="bg-base-200 flex justify-between">
|
||||
<div class="px-5 py-3">
|
||||
<i class="fa-solid fa-circle-info me-3"></i>
|
||||
You are editing a post published at{" "}
|
||||
<b>{new Date(props.editing?.created_at).toLocaleString()}</b>
|
||||
</div>
|
||||
<button type="reset" class="btn btn-ghost w-12" disabled={submitting()}>
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={props.realmId && !props.editing}>
|
||||
<div class="border-b border-base-200 px-5 h-[48px] flex items-center">
|
||||
<div class="form-control flex-grow">
|
||||
<label class="label cursor-pointer">
|
||||
<span class="label-text">Publish in this realm</span>
|
||||
<input name="publish_in_realm" type="checkbox" checked class="checkbox checkbox-primary" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<textarea required name="content" value={props.editing?.content ?? ""}
|
||||
class={`${styles.publishInput} textarea w-full`}
|
||||
placeholder="What's happened?! (Support markdown)" />
|
||||
|
||||
<div id="publish-actions" class="flex justify-between border-y border-base-200">
|
||||
<PostEditActions
|
||||
editing={props.editing}
|
||||
onInputAlias={setAlias}
|
||||
onInputPublish={setPublishedAt}
|
||||
onInputAttachments={setAttachments}
|
||||
onInputCategories={setCategories}
|
||||
onInputTags={setTags}
|
||||
onError={props.onError}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<button type="submit" class="btn btn-primary" disabled={submitting()}>
|
||||
<Show when={submitting()} fallback={props.editing ? "Save changes" : "Post a post"}>
|
||||
<span class="loading"></span>
|
||||
</Show>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html, body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.medium-zoom-image--opened {
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
.medium-zoom-overlay {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.scrollbar-hidden {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.scrollbar-hidden::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.cherry, .cherry-toolbar, .cherry-editor, .cherry-previewer, .cherry-drag {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.cherry-drag {
|
||||
width: 2px !important;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.cherry-drag {
|
||||
background: oklch(var(--b2)) !important;
|
||||
}
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
import "solid-devtools";
|
||||
|
||||
/* @refresh reload */
|
||||
import { render } from "solid-js/web";
|
||||
|
||||
import "./index.css";
|
||||
import "./assets/fonts/fonts.css";
|
||||
import { lazy } from "solid-js";
|
||||
import { Route, Router } from "@solidjs/router";
|
||||
|
||||
import "@fortawesome/fontawesome-free/css/all.css";
|
||||
|
||||
import RootLayout from "./layouts/RootLayout.tsx";
|
||||
import FeedView from "./pages/view.tsx";
|
||||
import Global from "./pages/global.tsx";
|
||||
import PostReference from "./pages/post.tsx";
|
||||
import CreatorView from "./pages/creators/view.tsx";
|
||||
import { UserinfoProvider } from "./stores/userinfo.tsx";
|
||||
import { WellKnownProvider } from "./stores/wellKnown.tsx";
|
||||
|
||||
const root = document.getElementById("root");
|
||||
|
||||
const router = (basename?: string) => (
|
||||
<WellKnownProvider>
|
||||
<UserinfoProvider>
|
||||
<Router root={RootLayout} base={basename}>
|
||||
<Route path="/" component={FeedView}>
|
||||
<Route path="/" component={Global} />
|
||||
<Route path="/posts/:postId" component={PostReference} />
|
||||
<Route path="/search" component={lazy(() => import("./pages/search.tsx"))} />
|
||||
<Route path="/realms" component={lazy(() => import("./pages/realms"))} />
|
||||
<Route path="/realms/:realmId" component={lazy(() => import("./pages/realms/realm.tsx"))} />
|
||||
<Route path="/accounts/:accountId" component={lazy(() => import("./pages/account.tsx"))} />
|
||||
</Route>
|
||||
<Route path="/creators" component={CreatorView}>
|
||||
<Route path="/" component={lazy(() => import("./pages/creators"))} />
|
||||
<Route path="/publish" component={lazy(() => import("./pages/creators/publish.tsx"))} />
|
||||
<Route path="/edit/:postId" component={lazy(() => import("./pages/creators/edit.tsx"))} />
|
||||
</Route>
|
||||
</Router>
|
||||
</UserinfoProvider>
|
||||
</WellKnownProvider>
|
||||
);
|
||||
|
||||
declare const __GARFISH_EXPORTS__: {
|
||||
provider: Object;
|
||||
registerProvider?: (provider: any) => void;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__GARFISH__: boolean;
|
||||
__LAUNCHPAD_TARGET__?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export const provider = () => ({
|
||||
render: ({ dom, basename }: { dom: any, basename: string }) => {
|
||||
render(
|
||||
() => router(basename),
|
||||
dom.querySelector("#root")
|
||||
);
|
||||
},
|
||||
destroy: () => {
|
||||
}
|
||||
});
|
||||
|
||||
if (!window.__GARFISH__) {
|
||||
console.log("Running directly!")
|
||||
render(router, root!);
|
||||
} else if (typeof __GARFISH_EXPORTS__ !== "undefined") {
|
||||
console.log("Running in launchpad container!")
|
||||
console.log("Launchpad target:", window.__LAUNCHPAD_TARGET__)
|
||||
if (__GARFISH_EXPORTS__.registerProvider) {
|
||||
__GARFISH_EXPORTS__.registerProvider(provider);
|
||||
} else {
|
||||
__GARFISH_EXPORTS__.provider = provider;
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
import Navigator from "./shared/Navigator.tsx";
|
||||
import { readProfiles, useUserinfo } from "../stores/userinfo.tsx";
|
||||
import { createEffect, createMemo, createSignal, Show } from "solid-js";
|
||||
import { readWellKnown } from "../stores/wellKnown.tsx";
|
||||
import { BeforeLeaveEventArgs, useLocation, useNavigate, useSearchParams } from "@solidjs/router";
|
||||
|
||||
export default function RootLayout(props: any) {
|
||||
const [ready, setReady] = createSignal(false);
|
||||
|
||||
Promise.all([readWellKnown(), readProfiles()]).then(() => setReady(true));
|
||||
|
||||
const navigate = useNavigate();
|
||||
const userinfo = useUserinfo();
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const location = useLocation();
|
||||
|
||||
createEffect(() => {
|
||||
if (ready()) {
|
||||
keepGate(location.pathname + location.search, searchParams["embedded"] != null);
|
||||
}
|
||||
}, [ready, userinfo]);
|
||||
|
||||
function keepGate(path: string, embedded: boolean, e?: BeforeLeaveEventArgs) {
|
||||
const blacklist = ["/creators"];
|
||||
|
||||
if (!userinfo?.isLoggedIn && blacklist.includes(path)) {
|
||||
if (!e?.defaultPrevented) e?.preventDefault();
|
||||
if (embedded) {
|
||||
navigate(`/auth?redirect_uri=${path}&embedded=${location.query["embedded"]}`);
|
||||
} else {
|
||||
navigate(`/auth?redirect_uri=${path}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mainContentStyles = createMemo(() => {
|
||||
if (!searchParams["embedded"]) {
|
||||
return "h-[calc(100vh-64px)] max-md:mb-[64px] md:mt-[64px]";
|
||||
} else {
|
||||
return "h-[100vh]";
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={ready()}
|
||||
fallback={
|
||||
<div class="h-screen w-screen flex justify-center items-center">
|
||||
<div>
|
||||
<span class="loading loading-lg loading-infinity"></span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Show when={!searchParams["embedded"]}>
|
||||
<Navigator />
|
||||
</Show>
|
||||
|
||||
<main class={`${mainContentStyles()} scrollbar-hidden`}>{props.children}</main>
|
||||
</Show>
|
||||
);
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
import { createMemo, For, Match, Switch } from "solid-js";
|
||||
import { clearUserinfo, useUserinfo } from "../../stores/userinfo.tsx";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { useWellKnown } from "../../stores/wellKnown.tsx";
|
||||
|
||||
interface MenuItem {
|
||||
icon: string;
|
||||
label: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
export default function Navigator() {
|
||||
const nav: MenuItem[] = [
|
||||
{ icon: "fa-solid fa-pen-nib", label: "Creators", href: "/creators" },
|
||||
{ icon: "fa-solid fa-newspaper", label: "Feed", href: "/" },
|
||||
{ icon: "fa-solid fa-people-group", label: "Realms", href: "/realms" },
|
||||
];
|
||||
|
||||
const wellKnown = useWellKnown();
|
||||
const userinfo = useUserinfo();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const endpoint = createMemo(() => wellKnown?.components?.identity)
|
||||
|
||||
function logout() {
|
||||
clearUserinfo();
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="max-md:hidden navbar bg-base-100 shadow-md px-5 z-10 h-[64px] fixed top-0">
|
||||
<div class="navbar-start">
|
||||
<a href="/" class="btn btn-ghost text-xl">
|
||||
{wellKnown?.name ?? "Interactive"}
|
||||
</a>
|
||||
</div>
|
||||
<div class="navbar-center hidden md:flex">
|
||||
<ul class="menu menu-horizontal px-1">
|
||||
<For each={nav}>
|
||||
{(item) => (
|
||||
<li class="tooltip tooltip-bottom" data-tip={item.label}>
|
||||
<a href={item.href}>
|
||||
<i class={item.icon}></i>
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="navbar-end pe-5">
|
||||
<Switch>
|
||||
<Match when={userinfo?.isLoggedIn}>
|
||||
<button type="button" class="btn btn-sm btn-ghost" onClick={() => logout()}>
|
||||
Logout
|
||||
</button>
|
||||
</Match>
|
||||
<Match when={!userinfo?.isLoggedIn}>
|
||||
<a href={`${endpoint()}/auth/login?redirect_uri=${window.location}`} class="btn btn-sm btn-primary">
|
||||
Login
|
||||
</a>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden btm-nav fixed bottom-0 bg-base-100 border-t border-base-200 z-10 h-[64px]">
|
||||
<For each={nav}>
|
||||
{(item) => (
|
||||
<a href={item.href}>
|
||||
<div class="tooltip" data-tip={item.label}>
|
||||
<i class={item.icon}></i>
|
||||
</div>
|
||||
</a>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
import { createEffect, createSignal, Show } from "solid-js";
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { useSearchParams } from "@solidjs/router";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { closeModel, openModel } from "../scripts/modals.ts";
|
||||
import { request } from "../scripts/request.ts";
|
||||
|
||||
import PostList from "../components/posts/PostList.tsx";
|
||||
import NameCard from "../components/NameCard.tsx";
|
||||
import PostPublish from "../components/posts/PostPublish.tsx";
|
||||
|
||||
export default function AccountPage() {
|
||||
const [error, setError] = createSignal<string | null>(null);
|
||||
|
||||
const [page, setPage] = createSignal(0);
|
||||
const [info, setInfo] = createSignal<any>(null);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const params = useParams();
|
||||
|
||||
createEffect(() => {
|
||||
setPage(parseInt(searchParams["page"] ?? "1"));
|
||||
}, [searchParams]);
|
||||
|
||||
async function readPosts(pn?: number) {
|
||||
if (pn) setSearchParams({ page: pn });
|
||||
const res = await request(
|
||||
"/api/posts?" +
|
||||
new URLSearchParams({
|
||||
take: searchParams["take"] ? searchParams["take"] : (10).toString(),
|
||||
offset: searchParams["offset"] ? searchParams["offset"] : ((page() - 1) * 10).toString(),
|
||||
authorId: params["accountId"],
|
||||
}),
|
||||
);
|
||||
if (res.status !== 200) {
|
||||
setError(await res.text());
|
||||
} else {
|
||||
setError(null);
|
||||
setInfo(await res.json());
|
||||
}
|
||||
}
|
||||
|
||||
function setMeta(data: any, field: string, open = true) {
|
||||
const meta: { [id: string]: any } = {
|
||||
reposting: null,
|
||||
replying: null,
|
||||
editing: null,
|
||||
};
|
||||
meta[field] = data;
|
||||
setPublishMeta(meta);
|
||||
|
||||
if (open) openModel("#post-publish");
|
||||
else closeModel("#post-publish");
|
||||
}
|
||||
|
||||
const [publishMeta, setPublishMeta] = createStore<any>({
|
||||
replying: null,
|
||||
reposting: null,
|
||||
editing: null,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id="alerts">
|
||||
<Show when={error()}>
|
||||
<div role="alert" class="alert alert-error">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="capitalize">{error()}</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<NameCard accountId={params["accountId"]} onError={setError} />
|
||||
|
||||
<dialog id="post-publish" class="modal">
|
||||
<div class="modal-box p-0 w-[540px]">
|
||||
<PostPublish
|
||||
reposting={publishMeta.reposting}
|
||||
replying={publishMeta.replying}
|
||||
editing={publishMeta.editing}
|
||||
onReset={() => setMeta(null, "none", false)}
|
||||
onError={setError}
|
||||
onPost={() => readPosts()}
|
||||
/>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<PostList
|
||||
info={info()}
|
||||
onUpdate={readPosts}
|
||||
onError={setError}
|
||||
onRepost={(item) => setMeta(item, "reposting")}
|
||||
onReply={(item) => setMeta(item, "replying")}
|
||||
onEdit={(item) => setMeta(item, "editing")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
import PostEdit from "../../components/posts/PostEditor.tsx";
|
||||
import { useNavigate, useParams } from "@solidjs/router";
|
||||
import { createSignal, Show } from "solid-js";
|
||||
import { getAtk } from "../../stores/userinfo.tsx";
|
||||
import { request } from "../../scripts/request.ts";
|
||||
|
||||
export default function PublishPost() {
|
||||
const navigate = useNavigate();
|
||||
const params = useParams();
|
||||
|
||||
const [error, setError] = createSignal<string | null>(null);
|
||||
const [post, setPost] = createSignal<any>();
|
||||
|
||||
async function readPost() {
|
||||
const res = await request(`/api/creators/posts/${params["postId"]}`, {
|
||||
headers: { "Authorization": `Bearer ${getAtk()}` }
|
||||
});
|
||||
if (res.status === 200) {
|
||||
setPost((await res.json())["data"]);
|
||||
} else {
|
||||
setError(await res.text());
|
||||
}
|
||||
}
|
||||
|
||||
readPost();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="flex pt-1 border-b border-base-200">
|
||||
<a class="btn btn-ghost ml-[20px] w-12 h-12" href="/creators">
|
||||
<i class="fa-solid fa-angle-left"></i>
|
||||
</a>
|
||||
<div class="px-5 flex items-center">
|
||||
<p>Edit「{post()?.title ? post()?.title : "Untitled"}」</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alerts">
|
||||
<Show when={error()}>
|
||||
<div role="alert" class="alert alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="capitalize">{error()}</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<PostEdit
|
||||
editing={post()}
|
||||
onError={setError}
|
||||
onPost={() => navigate("/creators")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
import { createMemo, createSignal, For, Show } from "solid-js";
|
||||
import { getAtk } from "../../stores/userinfo.tsx";
|
||||
import LoadingAnimation from "../../components/LoadingAnimation.tsx";
|
||||
import styles from "../../components/posts/PostList.module.css";
|
||||
import { request } from "../../scripts/request.ts";
|
||||
|
||||
export default function CreatorHub() {
|
||||
const [error, setError] = createSignal<string | null>(null);
|
||||
|
||||
const [posts, setPosts] = createSignal<any[]>([]);
|
||||
const [postCount, setPostCount] = createSignal(0);
|
||||
|
||||
const [page, setPage] = createSignal(1);
|
||||
const [loading, setLoading] = createSignal(false);
|
||||
|
||||
const pageCount = createMemo(() => Math.ceil(postCount() / 10));
|
||||
|
||||
async function readPosts(pn?: number) {
|
||||
if (pn) setPage(pn);
|
||||
setLoading(true);
|
||||
const res = await request("/api/creators/posts?" + new URLSearchParams({
|
||||
take: (10).toString(),
|
||||
offset: ((page() - 1) * 10).toString()
|
||||
}), { headers: { "Authorization": `Bearer ${getAtk()}` } });
|
||||
if (res.status !== 200) {
|
||||
setError(await res.text());
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setError(null);
|
||||
setPosts(data["data"]);
|
||||
setPostCount(data["count"]);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
readPosts();
|
||||
|
||||
function changePage(pn: number) {
|
||||
readPosts(pn).then(() => {
|
||||
setTimeout(() => window.scrollTo({ top: 0, behavior: "smooth" }), 16);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id="alerts">
|
||||
<Show when={error()}>
|
||||
<div role="alert" class="alert alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="capitalize">{error()}</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 px-7 flex items-center justify-between border-b border-base-200">
|
||||
<h3 class="py-3 font-bold">Your posts</h3>
|
||||
<a class="btn btn-primary" href="/creators/publish">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="grid justify-items-strench">
|
||||
<For each={posts()}>
|
||||
{item =>
|
||||
<a href={`/creators/edit/${item.alias}`}>
|
||||
<div class="card sm:card-side hover:bg-base-200 transition-colors sm:max-w-none">
|
||||
<div class="card-body">
|
||||
<Show when={item?.title} fallback={
|
||||
<div class="line-clamp-3">
|
||||
{item?.content?.replaceAll("#", "").replaceAll("*", "").trim()}
|
||||
</div>
|
||||
}>
|
||||
<h2 class="text-xl">{item?.title}</h2>
|
||||
<div class="mx-[-2px] mt-[-4px]">
|
||||
{item?.categories?.map((category: any) => (
|
||||
<span class="badge badge-primary">{category.name}</span>
|
||||
))}
|
||||
{item?.tags?.map((tag: any) => (
|
||||
<span class="badge badge-secondary">{tag.name}</span>
|
||||
))}
|
||||
</div>
|
||||
<div class="text-sm opacity-80 line-clamp-3">
|
||||
{item?.content?.substring(0, 160).replaceAll("#", "").replaceAll("*", "").trim() + "……"}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="text-xs opacity-70 flex gap-2">
|
||||
<span>Post #{item?.id}</span>
|
||||
<span>Published at {new Date(item?.published_at).toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<div class="join">
|
||||
<button class={`join-item btn btn-ghost ${styles.paginationControl}`} disabled={page() <= 1}
|
||||
onClick={() => changePage(page() - 1)}>
|
||||
<i class="fa-solid fa-caret-left"></i>
|
||||
</button>
|
||||
<button class="join-item btn btn-ghost">Page {page()}</button>
|
||||
<button class={`join-item btn btn-ghost ${styles.paginationControl}`} disabled={page() >= pageCount()}
|
||||
onClick={() => changePage(page() + 1)}>
|
||||
<i class="fa-solid fa-caret-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={loading()}>
|
||||
<LoadingAnimation />
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
import PostEdit from "../../components/posts/PostEditor.tsx";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { createSignal, Show } from "solid-js";
|
||||
|
||||
export default function PublishPost() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [error, setError] = createSignal<string | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="flex pt-1 border-b border-base-200">
|
||||
<a class="btn btn-ghost ml-[20px] w-12 h-12" href="/creators">
|
||||
<i class="fa-solid fa-angle-left"></i>
|
||||
</a>
|
||||
<div class="px-5 flex items-center">
|
||||
<p>Publish a new post</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alerts">
|
||||
<Show when={error()}>
|
||||
<div role="alert" class="alert alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="capitalize">{error()}</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<PostEdit
|
||||
onError={setError}
|
||||
onPost={() => navigate("/creators")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
.wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
column-gap: 20px;
|
||||
|
||||
max-height: calc(100vh - 64px);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.wrapper {
|
||||
grid-template-columns: 1fr 2fr;
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import { createMemo } from "solid-js";
|
||||
import { useSearchParams } from "@solidjs/router";
|
||||
|
||||
import styles from "./view.module.css";
|
||||
|
||||
export default function CreatorView(props: any) {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const scrollContentStyles = createMemo(() => {
|
||||
if (!searchParams["embedded"]) {
|
||||
return "max-md:mb-[64px]";
|
||||
} else {
|
||||
return "h-[100vh]";
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div class={`${styles.wrapper} container mx-auto`}>
|
||||
<div id="nav" class="card shadow-xl h-fit">
|
||||
<h2 class="text-xl font-bold mt-1 py-5 px-7">Creator Hub</h2>
|
||||
</div>
|
||||
|
||||
<div id="content" class={`${scrollContentStyles()} card shadow-xl`}>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
import { createEffect, createSignal, Show } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { useSearchParams } from "@solidjs/router";
|
||||
import { request } from "../scripts/request.ts";
|
||||
|
||||
import PostList from "../components/posts/PostList.tsx";
|
||||
import PostPublish from "../components/posts/PostPublish.tsx";
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [error, setError] = createSignal<string | null>(null);
|
||||
|
||||
const [page, setPage] = createSignal(0);
|
||||
const [info, setInfo] = createSignal<any>(null);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
createEffect(() => {
|
||||
setPage(parseInt(searchParams["page"] ?? "1"));
|
||||
}, [searchParams]);
|
||||
|
||||
async function readPosts(pn?: number) {
|
||||
if (pn) setSearchParams({ page: pn });
|
||||
const res = await request(
|
||||
"/api/posts?" +
|
||||
new URLSearchParams({
|
||||
take: searchParams["take"] ? searchParams["take"] : (10).toString(),
|
||||
offset: searchParams["offset"] ? searchParams["offset"] : ((page() - 1) * 10).toString(),
|
||||
reply: false.toString(),
|
||||
}),
|
||||
);
|
||||
if (res.status !== 200) {
|
||||
setError(await res.text());
|
||||
} else {
|
||||
setError(null);
|
||||
setInfo(await res.json());
|
||||
}
|
||||
}
|
||||
|
||||
function setMeta(data: any, field: string, scroll = true) {
|
||||
const meta: { [id: string]: any } = {
|
||||
reposting: null,
|
||||
replying: null,
|
||||
editing: null,
|
||||
};
|
||||
meta[field] = data;
|
||||
setPublishMeta(meta);
|
||||
|
||||
if (scroll) window.scroll({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
|
||||
const [publishMeta, setPublishMeta] = createStore<any>({
|
||||
replying: null,
|
||||
reposting: null,
|
||||
editing: null,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id="alerts">
|
||||
<Show when={error()}>
|
||||
<div role="alert" class="alert alert-error">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="capitalize">{error()}</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<PostPublish
|
||||
replying={publishMeta.replying}
|
||||
reposting={publishMeta.reposting}
|
||||
editing={publishMeta.editing}
|
||||
onReset={() => setMeta(null, "none", false)}
|
||||
onPost={() => readPosts()}
|
||||
onError={setError}
|
||||
/>
|
||||
|
||||
<PostList
|
||||
info={info()}
|
||||
onUpdate={readPosts}
|
||||
onError={setError}
|
||||
onRepost={(item) => setMeta(item, "reposting")}
|
||||
onReply={(item) => setMeta(item, "replying")}
|
||||
onEdit={(item) => setMeta(item, "editing")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,161 +0,0 @@
|
||||
import { createSignal, Show } from "solid-js";
|
||||
import { useNavigate, useParams, useSearchParams } from "@solidjs/router";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { closeModel, openModel } from "../scripts/modals.ts";
|
||||
import { getAtk } from "../stores/userinfo.tsx";
|
||||
import { request } from "../scripts/request.ts";
|
||||
import PostPublish from "../components/posts/PostPublish.tsx";
|
||||
import PostList from "../components/posts/PostList.tsx";
|
||||
import PostItem from "../components/posts/PostItem.tsx";
|
||||
|
||||
export default function PostPage() {
|
||||
const [error, setError] = createSignal<string | null>(null);
|
||||
|
||||
const [page, setPage] = createSignal(0);
|
||||
const [related, setRelated] = createSignal<any>(null);
|
||||
const [info, setInfo] = createSignal<any>(null);
|
||||
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
async function readPost(pn?: number) {
|
||||
if (pn) setPage(pn);
|
||||
const res = await request(`/api/posts/${params["postId"]}?` + new URLSearchParams({
|
||||
take: (10).toString(),
|
||||
offset: ((page() - 1) * 10).toString()
|
||||
}));
|
||||
if (res.status !== 200) {
|
||||
setError(await res.text());
|
||||
} else {
|
||||
setError(null);
|
||||
const data = await res.json();
|
||||
setInfo(data["data"]);
|
||||
setRelated({
|
||||
count: data["count"],
|
||||
data: data["related"]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
readPost();
|
||||
|
||||
async function deletePost(item: any) {
|
||||
if (!confirm(`Are you sure to delete post#${item.id}?`)) return;
|
||||
|
||||
const res = await request(`/api/posts/${item.id}`, {
|
||||
method: "DELETE",
|
||||
headers: { "Authorization": `Bearer ${getAtk()}` }
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
setError(await res.text());
|
||||
} else {
|
||||
back();
|
||||
setError(null);
|
||||
}
|
||||
}
|
||||
|
||||
function setMeta(data: any, field: string, open = true) {
|
||||
const meta: { [id: string]: any } = {
|
||||
reposting: null,
|
||||
replying: null,
|
||||
editing: null
|
||||
};
|
||||
meta[field] = data;
|
||||
setPublishMeta(meta);
|
||||
|
||||
if (open) openModel("#post-publish");
|
||||
else closeModel("#post-publish");
|
||||
}
|
||||
|
||||
const [publishMeta, setPublishMeta] = createStore<any>({
|
||||
replying: null,
|
||||
reposting: null,
|
||||
editing: null
|
||||
});
|
||||
|
||||
function back() {
|
||||
if (window.history.length > 0) {
|
||||
window.history.back();
|
||||
} else {
|
||||
navigate("/");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id="alerts">
|
||||
<Show when={error()}>
|
||||
<div role="alert" class="alert alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="capitalize">{error()}</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="flex pt-1">
|
||||
<Show when={searchParams["embedded"]} fallback={
|
||||
<button class="btn btn-ghost ml-[20px] w-12 h-12" onClick={() => back()}>
|
||||
<i class="fa-solid fa-angle-left"></i>
|
||||
</button>
|
||||
}>
|
||||
<div class="w-12 h-12 ml-[20px] flex justify-center items-center">
|
||||
<i class="fa-solid fa-comments mb-1"></i>
|
||||
</div>
|
||||
</Show>
|
||||
<div class="px-5 flex items-center">
|
||||
<p>{searchParams["title"] ?? "Post details"}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dialog id="post-publish" class="modal">
|
||||
<div class="modal-box p-0 w-[540px]">
|
||||
<PostPublish
|
||||
reposting={publishMeta.reposting}
|
||||
replying={publishMeta.replying}
|
||||
editing={publishMeta.editing}
|
||||
onReset={() => setMeta(null, "none", false)}
|
||||
onError={setError}
|
||||
onPost={() => readPost()}
|
||||
/>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<Show when={info()} fallback={
|
||||
<div class="w-full border-b border-base-200 pt-5 pb-7 text-center">
|
||||
<p class="loading loading-lg loading-infinity"></p>
|
||||
<p>Creating fake news...</p>
|
||||
</div>
|
||||
}>
|
||||
<PostItem
|
||||
noClick
|
||||
post={info()}
|
||||
onError={setError}
|
||||
onReact={readPost}
|
||||
onDelete={deletePost}
|
||||
noAuthor={searchParams["noAuthor"] != null}
|
||||
noContent={searchParams["noContent"] != null}
|
||||
noControl={searchParams["noControl"] != null}
|
||||
onRepost={(item) => setMeta(item, "reposting")}
|
||||
onReply={(item) => setMeta(item, "replying")}
|
||||
onEdit={(item) => setMeta(item, "editing")}
|
||||
/>
|
||||
|
||||
<PostList
|
||||
noRelated
|
||||
info={related()}
|
||||
onUpdate={readPost}
|
||||
onError={setError}
|
||||
onRepost={(item) => setMeta(item, "reposting")}
|
||||
onReply={(item) => setMeta(item, "replying")}
|
||||
onEdit={(item) => setMeta(item, "editing")}
|
||||
/>
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
import { createSignal, For, Show } from "solid-js";
|
||||
import { closeModel, openModel } from "../../scripts/modals.ts";
|
||||
import { getAtk } from "../../stores/userinfo.tsx";
|
||||
import { request } from "../../scripts/request.ts";
|
||||
|
||||
export default function RealmDirectoryPage() {
|
||||
const [error, setError] = createSignal<string | null>(null);
|
||||
const [submitting, setSubmitting] = createSignal(false);
|
||||
|
||||
const [realms, setRealms] = createSignal<any>(null);
|
||||
|
||||
async function readRealms() {
|
||||
const res = await request(`/api/realms`);
|
||||
if (res.status !== 200) {
|
||||
setError(await res.text());
|
||||
} else {
|
||||
setRealms(await res.json());
|
||||
}
|
||||
}
|
||||
|
||||
readRealms();
|
||||
|
||||
async function createRealm(evt: SubmitEvent) {
|
||||
evt.preventDefault();
|
||||
|
||||
const form = evt.target as HTMLFormElement;
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
|
||||
setSubmitting(true);
|
||||
const res = await request("/api/realms", {
|
||||
method: "POST",
|
||||
headers: { "Authorization": `Bearer ${getAtk()}`, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
is_public: data.is_public != null
|
||||
})
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
setError(await res.text());
|
||||
} else {
|
||||
await readRealms();
|
||||
closeModel("#create-realm");
|
||||
form.reset();
|
||||
}
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id="alerts">
|
||||
<Show when={error()}>
|
||||
<div role="alert" class="alert alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="capitalize">{error()}</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 px-7 flex items-center justify-between">
|
||||
<h3 class="py-3 font-bold">Realms directory</h3>
|
||||
<button type="button" class="btn btn-primary" onClick={() => openModel("#create-realm")}>
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<For each={realms()}>
|
||||
{item => <div class="px-7 pt-7 pb-5 border-t border-base-200">
|
||||
<h2 class="text-xl font-bold">{item.name}</h2>
|
||||
<p>{item.description}</p>
|
||||
|
||||
<div class="mt-2">
|
||||
<a href={`/realms/${item.id}`} class="link">Jump in</a>
|
||||
</div>
|
||||
</div>}
|
||||
</For>
|
||||
|
||||
<dialog id="create-realm" class="modal">
|
||||
<div class="modal-box">
|
||||
<h2 class="card-title px-1">Create a realm</h2>
|
||||
<form class="mt-2" onSubmit={createRealm}>
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">Realm name</span>
|
||||
</div>
|
||||
<input name="name" type="text" placeholder="Type here" class="input input-bordered w-full" />
|
||||
</label>
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">Realm description</span>
|
||||
</div>
|
||||
<textarea name="description" placeholder="Type here" class="textarea textarea-bordered w-full" />
|
||||
</label>
|
||||
<div class="form-control mt-2">
|
||||
<label class="label cursor-pointer">
|
||||
<span class="label-text">Make it public</span>
|
||||
<input type="checkbox" name="is_public" class="checkbox checkbox-primary" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mt-2" disabled={submitting()}>
|
||||
<Show when={submitting()} fallback={"Submit"}>
|
||||
<span class="loading"></span>
|
||||
</Show>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
.description {
|
||||
color: var(--fallback-bc, oklch(var(--bc)/.8));
|
||||
}
|
@ -1,291 +0,0 @@
|
||||
import { createSignal, Show } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { useNavigate, useParams } from "@solidjs/router";
|
||||
import { request } from "../../scripts/request.ts";
|
||||
|
||||
import PostList from "../../components/posts/PostList.tsx";
|
||||
import PostPublish from "../../components/posts/PostPublish.tsx";
|
||||
|
||||
import styles from "./realm.module.css";
|
||||
import { getAtk, useUserinfo } from "../../stores/userinfo.tsx";
|
||||
import { closeModel, openModel } from "../../scripts/modals.ts";
|
||||
|
||||
export default function RealmPage() {
|
||||
const userinfo = useUserinfo();
|
||||
|
||||
const [error, setError] = createSignal<string | null>(null);
|
||||
const [submitting, setSubmitting] = createSignal(false);
|
||||
|
||||
const [realm, setRealm] = createSignal<any>(null);
|
||||
const [page, setPage] = createSignal(0);
|
||||
const [info, setInfo] = createSignal<any>(null);
|
||||
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
async function readRealm() {
|
||||
const res = await request(`/api/realms/${params["realmId"]}`);
|
||||
if (res.status !== 200) {
|
||||
setError(await res.text());
|
||||
} else {
|
||||
setRealm(await res.json());
|
||||
}
|
||||
}
|
||||
|
||||
readRealm();
|
||||
|
||||
async function readPosts(pn?: number) {
|
||||
if (pn) setPage(pn);
|
||||
const res = await request(`/api/posts?` + new URLSearchParams({
|
||||
take: (10).toString(),
|
||||
offset: ((page() - 1) * 10).toString(),
|
||||
realmId: params["realmId"]
|
||||
}));
|
||||
if (res.status !== 200) {
|
||||
setError(await res.text());
|
||||
} else {
|
||||
setError(null);
|
||||
setInfo(await res.json());
|
||||
}
|
||||
}
|
||||
|
||||
async function editRealm(evt: SubmitEvent) {
|
||||
evt.preventDefault();
|
||||
|
||||
const form = evt.target as HTMLFormElement;
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
|
||||
setSubmitting(true);
|
||||
const res = await request(`/api/realms/${params["realmId"]}`, {
|
||||
method: "PUT",
|
||||
headers: { "Authorization": `Bearer ${getAtk()}`, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
is_public: data.is_public != null
|
||||
})
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
setError(await res.text());
|
||||
} else {
|
||||
await readRealm();
|
||||
closeModel("#edit-realm");
|
||||
form.reset();
|
||||
}
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
async function inviteMember(evt: SubmitEvent) {
|
||||
evt.preventDefault();
|
||||
|
||||
const form = evt.target as HTMLFormElement;
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
|
||||
setSubmitting(true);
|
||||
const res = await request(`/api/realms/${params["realmId"]}/invite`, {
|
||||
method: "POST",
|
||||
headers: { "Authorization": `Bearer ${getAtk()}`, "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
setError(await res.text());
|
||||
} else {
|
||||
await readRealm();
|
||||
closeModel("#invite-member");
|
||||
form.reset();
|
||||
}
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
async function kickMember(evt: SubmitEvent) {
|
||||
evt.preventDefault();
|
||||
|
||||
const form = evt.target as HTMLFormElement;
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
|
||||
setSubmitting(true);
|
||||
const res = await request(`/api/realms/${params["realmId"]}/kick`, {
|
||||
method: "POST",
|
||||
headers: { "Authorization": `Bearer ${getAtk()}`, "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
setError(await res.text());
|
||||
} else {
|
||||
await readRealm();
|
||||
closeModel("#kick-member");
|
||||
form.reset();
|
||||
}
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
async function breakRealm() {
|
||||
if (!confirm("Are you sure about that? All posts in this realm will disappear forever.")) return;
|
||||
|
||||
const res = await request(`/api/realms/${params["realmId"]}`, {
|
||||
method: "DELETE",
|
||||
headers: { "Authorization": `Bearer ${getAtk()}` }
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
setError(await res.text());
|
||||
} else {
|
||||
navigate("/realms");
|
||||
}
|
||||
}
|
||||
|
||||
function setMeta(data: any, field: string, scroll = true) {
|
||||
const meta: { [id: string]: any } = {
|
||||
reposting: null,
|
||||
replying: null,
|
||||
editing: null
|
||||
};
|
||||
meta[field] = data;
|
||||
setPublishMeta(meta);
|
||||
|
||||
if (scroll) window.scroll({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
|
||||
const [publishMeta, setPublishMeta] = createStore<any>({
|
||||
replying: null,
|
||||
reposting: null,
|
||||
editing: null
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id="alerts">
|
||||
<Show when={error()}>
|
||||
<div role="alert" class="alert alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="capitalize">{error()}</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="px-7 pt-7 pb-5">
|
||||
<h2 class="text-2xl font-bold">{realm()?.name}</h2>
|
||||
<p>{realm()?.description}</p>
|
||||
|
||||
<div class={`${styles.description} text-sm mt-3`}>
|
||||
<p>Realm #{realm()?.id}</p>
|
||||
<Show when={realm()?.account_id === userinfo?.profiles?.id}>
|
||||
<div class="flex gap-2">
|
||||
<button class="link" onClick={() => openModel("#edit-realm")}>Edit</button>
|
||||
<button class="link" onClick={() => openModel("#invite-member")}>Invite</button>
|
||||
<button class="link" onClick={() => openModel("#kick-member")}>Kick</button>
|
||||
<button class="link" onClick={() => breakRealm()}>Break-up</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PostPublish
|
||||
realmId={parseInt(params["realmId"])}
|
||||
replying={publishMeta.replying}
|
||||
reposting={publishMeta.reposting}
|
||||
editing={publishMeta.editing}
|
||||
onReset={() => setMeta(null, "none", false)}
|
||||
onPost={() => readPosts()}
|
||||
onError={setError}
|
||||
/>
|
||||
|
||||
<PostList
|
||||
info={info()}
|
||||
onUpdate={readPosts}
|
||||
onError={setError}
|
||||
onRepost={(item) => setMeta(item, "reposting")}
|
||||
onReply={(item) => setMeta(item, "replying")}
|
||||
onEdit={(item) => setMeta(item, "editing")}
|
||||
/>
|
||||
|
||||
<dialog id="edit-realm" class="modal">
|
||||
<div class="modal-box">
|
||||
<h2 class="card-title px-1">Edit your realm</h2>
|
||||
<form class="mt-2" onSubmit={editRealm}>
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">Realm name</span>
|
||||
</div>
|
||||
<input value={realm()?.name} name="name" type="text" placeholder="Type here"
|
||||
class="input input-bordered w-full" />
|
||||
</label>
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">Realm description</span>
|
||||
</div>
|
||||
<textarea value={realm()?.description} name="description" placeholder="Type here"
|
||||
class="textarea textarea-bordered w-full" />
|
||||
</label>
|
||||
<div class="form-control mt-2">
|
||||
<label class="label cursor-pointer">
|
||||
<span class="label-text">Make it public</span>
|
||||
<input checked={realm()?.is_public} type="checkbox" name="is_public"
|
||||
class="checkbox checkbox-primary" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mt-2" disabled={submitting()}>
|
||||
<Show when={submitting()} fallback={"Submit"}>
|
||||
<span class="loading"></span>
|
||||
</Show>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="invite-member" class="modal">
|
||||
<div class="modal-box">
|
||||
<h2 class="card-title px-1">Invite someone as a member</h2>
|
||||
<form class="mt-2" onSubmit={inviteMember}>
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">Username</span>
|
||||
</div>
|
||||
<input name="account_name" type="text" placeholder="Type here" class="input input-bordered w-full" />
|
||||
<div class="label">
|
||||
<span class="label-text-alt">
|
||||
Invite someone via their username so that they can publish content in non-public realm.
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<button type="submit" class="btn btn-primary mt-2" disabled={submitting()}>
|
||||
<Show when={submitting()} fallback={"Submit"}>
|
||||
<span class="loading"></span>
|
||||
</Show>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="kick-member" class="modal">
|
||||
<div class="modal-box">
|
||||
<h2 class="card-title px-1">Kick someone out of your realm</h2>
|
||||
<form class="mt-2" onSubmit={kickMember}>
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">Username</span>
|
||||
</div>
|
||||
<input name="account_name" type="text" placeholder="Type here" class="input input-bordered w-full" />
|
||||
<div class="label">
|
||||
<span class="label-text-alt">
|
||||
Remove someone out of your realm.
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<button type="submit" class="btn btn-primary mt-2" disabled={submitting()}>
|
||||
<Show when={submitting()} fallback={"Submit"}>
|
||||
<span class="loading"></span>
|
||||
</Show>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
import { useNavigate, useSearchParams } from "@solidjs/router";
|
||||
import { createSignal, Show } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { closeModel, openModel } from "../scripts/modals.ts";
|
||||
import { request } from "../scripts/request.ts";
|
||||
import PostPublish from "../components/posts/PostPublish.tsx";
|
||||
import PostList from "../components/posts/PostList.tsx";
|
||||
|
||||
export default function SearchPage() {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const [error, setError] = createSignal<string | null>(null);
|
||||
|
||||
const [page, setPage] = createSignal(0);
|
||||
const [info, setInfo] = createSignal<any>(null);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
async function readPosts(pn?: number) {
|
||||
if (pn) setPage(pn);
|
||||
const res = await request("/api/posts?" + new URLSearchParams({
|
||||
take: (10).toString(),
|
||||
offset: ((page() - 1) * 10).toString(),
|
||||
...searchParams
|
||||
}));
|
||||
if (res.status !== 200) {
|
||||
setError(await res.text());
|
||||
} else {
|
||||
setError(null);
|
||||
setInfo(await res.json());
|
||||
}
|
||||
}
|
||||
|
||||
function setMeta(data: any, field: string, open = true) {
|
||||
const meta: { [id: string]: any } = {
|
||||
reposting: null,
|
||||
replying: null,
|
||||
editing: null
|
||||
};
|
||||
meta[field] = data;
|
||||
setPublishMeta(meta);
|
||||
|
||||
if (open) openModel("#post-publish");
|
||||
else closeModel("#post-publish");
|
||||
}
|
||||
|
||||
const [publishMeta, setPublishMeta] = createStore<any>({
|
||||
replying: null,
|
||||
reposting: null,
|
||||
editing: null
|
||||
});
|
||||
|
||||
function getDescribe() {
|
||||
let builder = [];
|
||||
if (searchParams["category"]) {
|
||||
builder.push("category is #" + searchParams["category"]);
|
||||
} else if (searchParams["tag"]) {
|
||||
builder.push("tag is #" + searchParams["tag"]);
|
||||
}
|
||||
|
||||
return builder.join(" and ");
|
||||
}
|
||||
|
||||
function back() {
|
||||
if (window.history.length > 0) {
|
||||
window.history.back();
|
||||
} else {
|
||||
navigate("/");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="flex pt-1">
|
||||
<button class="btn btn-ghost ml-[20px] w-12 h-12" onClick={() => back()}>
|
||||
<i class="fa-solid fa-angle-left"></i>
|
||||
</button>
|
||||
<div class="px-5 flex items-center">
|
||||
<p>Search</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alerts">
|
||||
<Show when={error()}>
|
||||
<div role="alert" class="alert alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="capitalize">{error()}</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div role="alert" class="alert alert-info px-[20px]">
|
||||
<i class="fa-solid fa-magnifying-glass pl-[13px]"></i>
|
||||
<span>You will only see posts with <b>{getDescribe()}</b></span>
|
||||
</div>
|
||||
|
||||
<dialog id="post-publish" class="modal">
|
||||
<div class="modal-box p-0 w-[540px]">
|
||||
<PostPublish
|
||||
reposting={publishMeta.reposting}
|
||||
replying={publishMeta.replying}
|
||||
editing={publishMeta.editing}
|
||||
onReset={() => setMeta(null, "none", false)}
|
||||
onError={setError}
|
||||
onPost={() => readPosts()}
|
||||
/>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<PostList
|
||||
info={info()}
|
||||
onUpdate={readPosts}
|
||||
onError={setError}
|
||||
onRepost={(item) => setMeta(item, "reposting")}
|
||||
onReply={(item) => setMeta(item, "replying")}
|
||||
onEdit={(item) => setMeta(item, "editing")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
.wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
column-gap: 20px;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.wrapper {
|
||||
grid-template-columns: 1fr 2fr 1fr;
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import { createMemo } from "solid-js";
|
||||
import { useSearchParams } from "@solidjs/router";
|
||||
|
||||
import styles from "./view.module.css";
|
||||
|
||||
export default function FeedView(props: any) {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const scrollContentStyles = createMemo(() => {
|
||||
if (!searchParams["embedded"]) {
|
||||
return "max-md:mb-[64px]";
|
||||
} else {
|
||||
return "h-[100vh]";
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div class={`${styles.wrapper} container mx-auto`}>
|
||||
<div id="trending" class="card shadow-xl h-fit"></div>
|
||||
|
||||
<div id="content" class={`${scrollContentStyles()} card shadow-xl`}>
|
||||
{props.children}
|
||||
</div>
|
||||
|
||||
<div id="well-known" class="card shadow-xl h-fit"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
export function openModel(selector: string) {
|
||||
document.querySelector<HTMLDialogElement>(selector)?.showModal()
|
||||
}
|
||||
|
||||
export function closeModel(selector: string) {
|
||||
document.querySelector<HTMLDialogElement>(selector)?.close()
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
export async function request(input: string, init?: RequestInit) {
|
||||
const prefix = window.__LAUNCHPAD_TARGET__ ?? "";
|
||||
return await fetch(prefix + input, init)
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
import Cookie from "universal-cookie";
|
||||
import { createContext, useContext } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { request } from "../scripts/request.ts";
|
||||
|
||||
export interface Userinfo {
|
||||
isLoggedIn: boolean,
|
||||
displayName: string,
|
||||
profiles: any,
|
||||
}
|
||||
|
||||
const UserinfoContext = createContext<Userinfo>();
|
||||
|
||||
const defaultUserinfo: Userinfo = {
|
||||
isLoggedIn: false,
|
||||
displayName: "Citizen",
|
||||
profiles: null
|
||||
};
|
||||
|
||||
const [userinfo, setUserinfo] = createStore<Userinfo>(structuredClone(defaultUserinfo));
|
||||
|
||||
export function getAtk(): string {
|
||||
return new Cookie().get("identity_auth_key");
|
||||
}
|
||||
|
||||
function checkLoggedIn(): boolean {
|
||||
return new Cookie().get("identity_auth_key");
|
||||
}
|
||||
|
||||
export async function readProfiles() {
|
||||
if (!checkLoggedIn()) return;
|
||||
|
||||
const res = await request("/api/users/me", {
|
||||
headers: { "Authorization": `Bearer ${getAtk()}` }
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
clearUserinfo();
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
setUserinfo({
|
||||
isLoggedIn: true,
|
||||
displayName: data["name"],
|
||||
profiles: data
|
||||
});
|
||||
}
|
||||
|
||||
export function clearUserinfo() {
|
||||
const cookies = document.cookie.split(";");
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i];
|
||||
const eqPos = cookie.indexOf("=");
|
||||
const name = eqPos > -1 ? cookie.substring(0, eqPos) : cookie;
|
||||
document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
|
||||
}
|
||||
|
||||
setUserinfo(defaultUserinfo);
|
||||
}
|
||||
|
||||
export function UserinfoProvider(props: any) {
|
||||
return (
|
||||
<UserinfoContext.Provider value={userinfo}>
|
||||
{props.children}
|
||||
</UserinfoContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useUserinfo() {
|
||||
return useContext(UserinfoContext);
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
import { createContext, useContext } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { request } from "../scripts/request.ts";
|
||||
|
||||
const WellKnownContext = createContext<any>();
|
||||
|
||||
const [wellKnown, setWellKnown] = createStore<any>(null);
|
||||
|
||||
export async function readWellKnown() {
|
||||
const res = await request("/.well-known")
|
||||
setWellKnown(await res.json())
|
||||
}
|
||||
|
||||
export function WellKnownProvider(props: any) {
|
||||
return (
|
||||
<WellKnownContext.Provider value={wellKnown}>
|
||||
{props.children}
|
||||
</WellKnownContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useWellKnown() {
|
||||
return useContext(WellKnownContext);
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
/** @type {import("tailwindcss").Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./src/**/*.{js,jsx,ts,tsx}"
|
||||
],
|
||||
daisyui: {
|
||||
themes: [
|
||||
{
|
||||
light: {
|
||||
...require("daisyui/src/theming/themes")["light"],
|
||||
primary: "#4750a3",
|
||||
secondary: "#93c5fd",
|
||||
accent: "#0f766e",
|
||||
info: "#67e8f9",
|
||||
success: "#15803d",
|
||||
warning: "#f97316",
|
||||
error: "#dc2626",
|
||||
"--rounded-box": "0",
|
||||
"--rounded-btn": "0",
|
||||
"--rounded-badge": "0",
|
||||
"--tab-radius": "0"
|
||||
}
|
||||
},
|
||||
{
|
||||
dark: {
|
||||
...require("daisyui/src/theming/themes")["dark"],
|
||||
primary: "#4750a3",
|
||||
secondary: "#93c5fd",
|
||||
accent: "#0f766e",
|
||||
info: "#67e8f9",
|
||||
success: "#15803d",
|
||||
warning: "#f97316",
|
||||
error: "#dc2626",
|
||||
"--rounded-box": "0",
|
||||
"--rounded-btn": "0",
|
||||
"--rounded-badge": "0",
|
||||
"--tab-radius": "0"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [require("daisyui"), require("@tailwindcss/typography")]
|
||||
};
|
||||
|
@ -1,26 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "ES2015", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import { defineConfig } from "vite";
|
||||
import solid from "vite-plugin-solid";
|
||||
import devtools from "solid-devtools/vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [devtools({ autoname: true }), solid()],
|
||||
server: {
|
||||
proxy: {
|
||||
"/api": "http://localhost:8445",
|
||||
"/.well-known": "http://localhost:8445"
|
||||
}
|
||||
}
|
||||
});
|
15
pkg/views/.eslintrc.cjs
Normal file
15
pkg/views/.eslintrc.cjs
Normal file
@ -0,0 +1,15 @@
|
||||
/* eslint-env node */
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
'extends': [
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'@vue/eslint-config-typescript',
|
||||
'@vue/eslint-config-prettier/skip-formatting'
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest'
|
||||
}
|
||||
}
|
30
pkg/views/.gitignore
vendored
Normal file
30
pkg/views/.gitignore
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
8
pkg/views/.prettierrc.json
Normal file
8
pkg/views/.prettierrc.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": false,
|
||||
"printWidth": 120,
|
||||
"trailingComma": "none"
|
||||
}
|
8
pkg/views/.vscode/extensions.json
vendored
Normal file
8
pkg/views/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"Vue.vscode-typescript-vue-plugin",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
46
pkg/views/README.md
Normal file
46
pkg/views/README.md
Normal file
@ -0,0 +1,46 @@
|
||||
# views
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
|
||||
## Type Support for `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||
|
||||
1. Disable the built-in TypeScript Extension
|
||||
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Type-Check, Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Lint with [ESLint](https://eslint.org/)
|
||||
|
||||
```sh
|
||||
npm run lint
|
||||
```
|
BIN
pkg/views/bun.lockb
Executable file
BIN
pkg/views/bun.lockb
Executable file
Binary file not shown.
13
pkg/views/index.html
Normal file
13
pkg/views/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Goatplaza</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
41
pkg/views/package.json
Normal file
41
pkg/views/package.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "views",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build --force",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@unocss/reset": "^0.58.5",
|
||||
"pinia": "^2.1.7",
|
||||
"unocss": "^0.58.5",
|
||||
"vue": "^3.4.15",
|
||||
"vue-router": "^4.2.5",
|
||||
"vuetify": "^3.5.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.3.3",
|
||||
"@tsconfig/node20": "^20.1.2",
|
||||
"@types/node": "^20.11.10",
|
||||
"@vitejs/plugin-vue": "^5.0.3",
|
||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"npm-run-all2": "^6.1.1",
|
||||
"prettier": "^3.0.3",
|
||||
"typescript": "~5.3.0",
|
||||
"vite": "^5.0.11",
|
||||
"vue-tsc": "^1.8.27"
|
||||
}
|
||||
}
|
5
pkg/views/src/index.vue
Normal file
5
pkg/views/src/index.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<router-view />
|
||||
</v-app>
|
||||
</template>
|
27
pkg/views/src/layouts/master.vue
Normal file
27
pkg/views/src/layouts/master.vue
Normal file
@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<v-navigation-drawer v-model="drawerOpen" color="grey-lighten-5" floating>
|
||||
<div class="d-flex text-center justify-center items-center h-[64px]">
|
||||
<h1>Goatplaza</h1>
|
||||
</div>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<v-app-bar height="64" color="primary" scroll-behavior="elevate" flat>
|
||||
<div class="container mx-auto px-5">
|
||||
<v-app-bar-nav-icon variant="text" @click.stop="toggleDrawer"></v-app-bar-nav-icon>
|
||||
</div>
|
||||
</v-app-bar>
|
||||
|
||||
<v-main>
|
||||
<router-view />
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue"
|
||||
|
||||
const drawerOpen = ref(true)
|
||||
|
||||
function toggleDrawer() {
|
||||
drawerOpen.value = !drawerOpen.value
|
||||
}
|
||||
</script>
|
41
pkg/views/src/main.ts
Normal file
41
pkg/views/src/main.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import "virtual:uno.css"
|
||||
|
||||
import { createApp } from "vue"
|
||||
import { createPinia } from "pinia"
|
||||
|
||||
import "vuetify/styles"
|
||||
import { createVuetify } from "vuetify"
|
||||
import * as components from "vuetify/components"
|
||||
import * as directives from "vuetify/directives"
|
||||
|
||||
import "@mdi/font/css/materialdesignicons.min.css"
|
||||
|
||||
import index from "./index.vue"
|
||||
import router from "./router"
|
||||
|
||||
const app = createApp(index)
|
||||
|
||||
app.use(
|
||||
createVuetify({
|
||||
components,
|
||||
directives,
|
||||
theme: {
|
||||
themes: {
|
||||
light: {
|
||||
primary: "#4a5099",
|
||||
secondary: "#2196f3",
|
||||
accent: "#009688",
|
||||
error: "#f44336",
|
||||
warning: "#ff9800",
|
||||
info: "#03a9f4",
|
||||
success: "#4caf50"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
app.mount("#app")
|
22
pkg/views/src/router/index.ts
Normal file
22
pkg/views/src/router/index.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { createRouter, createWebHistory } from "vue-router"
|
||||
import MasterLayout from "@/layouts/master.vue"
|
||||
import LandingPage from "@/views/landing.vue"
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
component: MasterLayout,
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
name: "landing",
|
||||
component: LandingPage
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export default router
|
3
pkg/views/src/views/landing.vue
Normal file
3
pkg/views/src/views/landing.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div>Good morning!</div>
|
||||
</template>
|
14
pkg/views/tsconfig.app.json
Normal file
14
pkg/views/tsconfig.app.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
11
pkg/views/tsconfig.json
Normal file
11
pkg/views/tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
19
pkg/views/tsconfig.node.json
Normal file
19
pkg/views/tsconfig.node.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "@tsconfig/node20/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
5
pkg/views/uno.config.ts
Normal file
5
pkg/views/uno.config.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { defineConfig, presetUno } from "unocss"
|
||||
|
||||
export default defineConfig({
|
||||
presets: [presetUno({ preflight: false })]
|
||||
})
|
16
pkg/views/vite.config.ts
Normal file
16
pkg/views/vite.config.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { fileURLToPath, URL } from "node:url"
|
||||
|
||||
import { defineConfig } from "vite"
|
||||
import vue from "@vitejs/plugin-vue"
|
||||
import vueJsx from "@vitejs/plugin-vue-jsx"
|
||||
import unocss from "unocss/vite"
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue(), vueJsx(), unocss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url))
|
||||
}
|
||||
}
|
||||
})
|
Loading…
Reference in New Issue
Block a user