Overboard
This commit is contained in:
@@ -15,9 +15,8 @@ const createUser = async (
|
||||
permissions: CzchanPerm[],
|
||||
noCahce: boolean = false,
|
||||
) => {
|
||||
if (username === "system") {
|
||||
if (username === "system")
|
||||
throw new CzchanError('Jméno "system" je rezervované.', 400);
|
||||
}
|
||||
|
||||
const result = await postgres.query<User>(
|
||||
"INSERT INTO users (username, password, rank, capcode, permissions) VALUES ($1, $2, $3, $4, $5) RETURNING *",
|
||||
|
||||
@@ -31,7 +31,7 @@ const PageBody = ({
|
||||
))
|
||||
.join(" / ") as "safe"
|
||||
}
|
||||
]
|
||||
] [<a href="/ib/overboard">overboard</a>]{" "}
|
||||
</>
|
||||
)}{" "}
|
||||
[<a href="/news">novinky</a>]
|
||||
|
||||
@@ -5,45 +5,52 @@ const IBHeader = ({
|
||||
banner,
|
||||
board,
|
||||
catalog = false,
|
||||
}: PropsWithChildren<{ banner: string; board: Board; catalog?: boolean }>) => (
|
||||
}: PropsWithChildren<{ banner: string; board?: Board; catalog?: boolean }>) => (
|
||||
<div class="center">
|
||||
<img class="banner" src={banner} />
|
||||
<h1 class="title">
|
||||
{catalog ? (
|
||||
<>
|
||||
Katalog (
|
||||
<a href={`/ib/${board.id}`} safe>
|
||||
/{board.id}/
|
||||
</a>
|
||||
{board ? (
|
||||
<a href={`/ib/${board.id}`} safe>
|
||||
/{board.id}/
|
||||
</a>
|
||||
) : (
|
||||
<>Overboard</>
|
||||
)}
|
||||
)
|
||||
</>
|
||||
) : board ? (
|
||||
<>
|
||||
<span safe>
|
||||
/{board.id}/ - {board.name}
|
||||
</span>
|
||||
{board.config.unlisted && (
|
||||
<>
|
||||
{" "}
|
||||
<img class="icon" src="/public/img/icons/unlisted.png" />
|
||||
</>
|
||||
)}
|
||||
{board.config.private && (
|
||||
<>
|
||||
{" "}
|
||||
<img class="icon" src="/public/img/icons/lock.png" />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<span safe>
|
||||
/{board.id}/ - {board.name}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{board.config.unlisted && (
|
||||
<>
|
||||
{" "}
|
||||
<img class="icon" src="/public/img/icons/unlisted.png" />
|
||||
</>
|
||||
)}
|
||||
{board.config.private && (
|
||||
<>
|
||||
{" "}
|
||||
<img class="icon" src="/public/img/icons/lock.png" />
|
||||
</>
|
||||
<>Overboard</>
|
||||
)}
|
||||
</h1>
|
||||
<p class="subtitle" safe>
|
||||
{board.description}
|
||||
<p class="subtitle">
|
||||
{board ? board.description : <>Čerstvé příspěvky ze všech nástěnek</>}
|
||||
</p>
|
||||
<p>
|
||||
{catalog ? (
|
||||
<a href={`/ib/${board.id}`}>Index</a>
|
||||
<a href={`/ib/${board?.id || "overboard"}`}>Index</a>
|
||||
) : (
|
||||
<a href={`/ib/${board.id}/catalog`}>Katalog</a>
|
||||
<a href={`/ib/${board?.id || "overboard"}/catalog`}>Katalog</a>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -4,17 +4,8 @@ import { PropsWithChildren } from "@kitajs/html";
|
||||
const IBLinks = ({ board }: PropsWithChildren<{ board?: Board }>) => (
|
||||
<>
|
||||
[<a href="#top">Nahoru</a>] [<a href="#bottom">Dolů</a>] [
|
||||
<a href={board !== undefined ? `/ib/${board.id}` : `/ib/overboard`}>
|
||||
Index
|
||||
</a>
|
||||
] [
|
||||
<a
|
||||
href={
|
||||
board !== undefined
|
||||
? `/ib/${board.id}/catalog`
|
||||
: `/ib/overboard/catalog`
|
||||
}
|
||||
>
|
||||
<a href={board ? `/ib/${board.id}` : `/ib/overboard`}>Index</a>] [
|
||||
<a href={board ? `/ib/${board.id}/catalog` : `/ib/overboard/catalog`}>
|
||||
Katalog
|
||||
</a>
|
||||
]
|
||||
|
||||
155
src/web/routes/ib/overboard.tsx
Normal file
155
src/web/routes/ib/overboard.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import { valkey } from "../../../cache";
|
||||
import { readRandomBanner } from "../../../db/banner";
|
||||
import { readAllBoards } from "../../../db/board";
|
||||
import {
|
||||
readOverboardThreadsPage,
|
||||
readPostReplies,
|
||||
readQPosts,
|
||||
} from "../../../db/post";
|
||||
import { csPlural } from "../../../lib/locale";
|
||||
import { threadURL } from "../../../lib/util";
|
||||
import { Board, Post } from "../../../schema/tables";
|
||||
import { zPage } from "../../../schema/validation/common";
|
||||
import Page from "../../components/chrome/page";
|
||||
import PageBody from "../../components/chrome/page_body";
|
||||
import PageHead from "../../components/chrome/page_head";
|
||||
import PostComponent from "../../components/content/post";
|
||||
import { PostActionsForm } from "../../components/forms/post_actions_form";
|
||||
import IBHeader from "../../components/navigation/ib_header";
|
||||
import IBLinks from "../../components/navigation/ib_links";
|
||||
import Pagination from "../../components/navigation/pagination";
|
||||
import { getCtx, TemplateCtx } from "../../ctx";
|
||||
import { Request, Response } from "express";
|
||||
|
||||
export default async (req: Request, res: Response) => {
|
||||
const ctx = await getCtx(req);
|
||||
const banner = await readRandomBanner();
|
||||
const boards = Object.fromEntries(
|
||||
(await readAllBoards()).map((board) => [board.id, board]),
|
||||
);
|
||||
|
||||
const totalThreads = await valkey.zcount(
|
||||
`idx:posts:threads:all:bumped`,
|
||||
"-inf",
|
||||
"+inf",
|
||||
);
|
||||
|
||||
const page = zPage.parse(req.query.page || "1");
|
||||
|
||||
const overboardBoards = Object.values(boards)
|
||||
.filter((board) => !(board.config.private || board.config.unlisted))
|
||||
.map((board) => board.id);
|
||||
|
||||
const ops = await readOverboardThreadsPage(req.config, page, overboardBoards);
|
||||
const threads = [];
|
||||
const posts = [];
|
||||
|
||||
for (const op of ops) {
|
||||
const replies = await readPostReplies(op);
|
||||
threads.push({ op, replies });
|
||||
posts.push(op, ...replies);
|
||||
}
|
||||
|
||||
const qposts = await readQPosts(posts);
|
||||
|
||||
const html = Template(
|
||||
ctx,
|
||||
banner,
|
||||
boards,
|
||||
page,
|
||||
threads,
|
||||
qposts,
|
||||
totalThreads,
|
||||
);
|
||||
|
||||
res.send(html);
|
||||
};
|
||||
|
||||
const Template = (
|
||||
ctx: TemplateCtx,
|
||||
banner: string,
|
||||
boards: { [key: string]: Board },
|
||||
page: number,
|
||||
threads: { op: Post; replies: Post[] }[],
|
||||
qposts: { [key: string]: Post },
|
||||
totalThreads: number,
|
||||
) => (
|
||||
<Page>
|
||||
<PageHead
|
||||
ctx={ctx}
|
||||
title={`Overboard - Strana ${page}`}
|
||||
description="Čerstvé příspěvky ze všech nástěnek"
|
||||
/>
|
||||
<PageBody ctx={ctx}>
|
||||
<IBHeader banner={banner} />
|
||||
<hr />
|
||||
<Pagination
|
||||
ctx={ctx}
|
||||
base={`/ib/overboard`}
|
||||
entries={totalThreads}
|
||||
current={page}
|
||||
otherLinks={<IBLinks />}
|
||||
/>
|
||||
<hr />
|
||||
<form class="ajax-form" method="post" action="/actions/post_actions">
|
||||
{threads.map((thread) => (
|
||||
<>
|
||||
<b>
|
||||
Vlákno z{" "}
|
||||
<a href={`/ib/boards/${thread.op.board}`} safe>
|
||||
/{thread.op.board}/
|
||||
</a>
|
||||
</b>
|
||||
<div class="thread">
|
||||
<PostComponent
|
||||
ctx={ctx}
|
||||
board={boards[thread.op.board]}
|
||||
post={thread.op}
|
||||
qposts={qposts}
|
||||
/>
|
||||
{thread.replies.length > ctx.config.site.ui.preview_replies && (
|
||||
<p>
|
||||
{csPlural(
|
||||
["Byla vynechána", "Byly vynechány", "Bylo vynecháno"],
|
||||
thread.replies.length - ctx.config.site.ui.preview_replies,
|
||||
)}{" "}
|
||||
{thread.replies.length - ctx.config.site.ui.preview_replies}{" "}
|
||||
{csPlural(
|
||||
["odpověď", "odpovědi", "odpovědí"],
|
||||
thread.replies.length - ctx.config.site.ui.preview_replies,
|
||||
)}{" "}
|
||||
<a href={threadURL(thread.op)}>Kliknutím se otevře vlákno.</a>
|
||||
</p>
|
||||
)}
|
||||
{thread.replies
|
||||
.slice(-ctx.config.site.ui.preview_replies)
|
||||
.map((reply) => (
|
||||
<>
|
||||
<PostComponent
|
||||
ctx={ctx}
|
||||
board={boards[reply.board]}
|
||||
post={reply}
|
||||
qposts={qposts}
|
||||
reply
|
||||
/>
|
||||
<br />
|
||||
</>
|
||||
))}
|
||||
<hr />
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
<Pagination
|
||||
ctx={ctx}
|
||||
base={`/ib/overboard`}
|
||||
entries={totalThreads}
|
||||
current={page}
|
||||
otherLinks={<IBLinks />}
|
||||
/>
|
||||
<hr />
|
||||
<PostActionsForm ctx={ctx} />
|
||||
<hr />
|
||||
</form>
|
||||
</PageBody>
|
||||
</Page>
|
||||
);
|
||||
62
src/web/routes/ib/overboard_catalog.tsx
Normal file
62
src/web/routes/ib/overboard_catalog.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { readRandomBanner } from "../../../db/banner";
|
||||
import { readAllBoards } from "../../../db/board";
|
||||
import { readOverboardThreads } from "../../../db/post";
|
||||
import { Post } from "../../../schema/tables";
|
||||
import Page from "../../components/chrome/page";
|
||||
import PageBody from "../../components/chrome/page_body";
|
||||
import PageHead from "../../components/chrome/page_head";
|
||||
import CatalogTile from "../../components/content/catalog_tile";
|
||||
import { PostActionsForm } from "../../components/forms/post_actions_form";
|
||||
import IBHeader from "../../components/navigation/ib_header";
|
||||
import IBLinks from "../../components/navigation/ib_links";
|
||||
import { getCtx, TemplateCtx } from "../../ctx";
|
||||
import { Request, Response } from "express";
|
||||
|
||||
export default async (req: Request, res: Response) => {
|
||||
const ctx = await getCtx(req);
|
||||
const banner = await readRandomBanner();
|
||||
const boards = Object.fromEntries(
|
||||
(await readAllBoards()).map((board) => [board.id, board]),
|
||||
);
|
||||
|
||||
const overboardBoards = Object.values(boards)
|
||||
.filter((board) => !(board.config.private || board.config.unlisted))
|
||||
.map((board) => board.id);
|
||||
|
||||
const posts = await readOverboardThreads(overboardBoards);
|
||||
const html = Template(ctx, banner, posts);
|
||||
|
||||
res.send(html);
|
||||
};
|
||||
|
||||
const Template = (ctx: TemplateCtx, banner: string, posts: Post[]) => (
|
||||
<Page>
|
||||
<PageHead
|
||||
ctx={ctx}
|
||||
title={`Overboard - Katalog`}
|
||||
description="Katalog overboardu"
|
||||
/>
|
||||
<PageBody ctx={ctx}>
|
||||
<IBHeader banner={banner} catalog />
|
||||
<hr />
|
||||
<nav class="pagination">
|
||||
<IBLinks />
|
||||
</nav>
|
||||
<hr />
|
||||
<form class="ajax-form" method="post" action="/actions/post_actions">
|
||||
<div class="center">
|
||||
{posts.map((post) => (
|
||||
<CatalogTile post={post} />
|
||||
))}
|
||||
</div>
|
||||
<hr />
|
||||
<nav class="pagination">
|
||||
<IBLinks />
|
||||
</nav>
|
||||
<hr />
|
||||
<PostActionsForm ctx={ctx} />
|
||||
<hr />
|
||||
</form>
|
||||
</PageBody>
|
||||
</Page>
|
||||
);
|
||||
@@ -14,6 +14,9 @@ export default async (req: Request, res: Response) => {
|
||||
const [fields, _] = await parser.parse(req);
|
||||
const { id, name, description } = CreateBoardForm.parse(fields);
|
||||
|
||||
if (id === "overboard")
|
||||
throw new CzchanError('ID "overboard" je rezervované.', 400);
|
||||
|
||||
try {
|
||||
await createBoard(
|
||||
req.config,
|
||||
|
||||
@@ -7,6 +7,8 @@ import actionUpdateUconfig from "../routes/actions/update_uconfig";
|
||||
import captcha from "../routes/captcha";
|
||||
import board from "../routes/ib/board";
|
||||
import catalog from "../routes/ib/catalog";
|
||||
import overboard from "../routes/ib/overboard";
|
||||
import overboardCatalog from "../routes/ib/overboard_catalog";
|
||||
import thread from "../routes/ib/thread";
|
||||
import index from "../routes/index";
|
||||
import login from "../routes/login";
|
||||
@@ -22,6 +24,8 @@ const registerPublicRoutes = (router: Router) => {
|
||||
router.get("/news", e(news));
|
||||
router.get("/captcha", e(captcha));
|
||||
|
||||
router.get("/ib/overboard", e(overboard));
|
||||
router.get("/ib/overboard/catalog", e(overboardCatalog));
|
||||
router.get("/ib/:board", e(board));
|
||||
router.get("/ib/:board/catalog", e(catalog));
|
||||
router.get("/ib/:board/:id", e(thread));
|
||||
|
||||
@@ -180,6 +180,7 @@ select,
|
||||
}
|
||||
|
||||
.post.highlighted,
|
||||
.catalog-tile:target,
|
||||
.post:target {
|
||||
border-right: 1px solid $hl-box-border;
|
||||
border-bottom: 1px solid $hl-box-border;
|
||||
|
||||
@@ -36,10 +36,14 @@ const hoverQuote = (quotes: JQuery<HTMLElement>) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const board = quote.closest(".post").attr("data-board") || null;
|
||||
const board =
|
||||
quote.closest(".post, .catalog-tile").attr("data-board") || null;
|
||||
|
||||
const id = parseQuote(board, quote.text());
|
||||
const existingPost = $(`#${id}.post`); // Catalog tiles don't count
|
||||
const url = quote.attr("href");
|
||||
const existingPost = $(`#${id}`);
|
||||
|
||||
console.log(id, existingPost[0]);
|
||||
|
||||
if (existingPost.length > 0 && isInViewport(existingPost)) {
|
||||
existingPost.toggleClass("highlighted", hovering);
|
||||
|
||||
Reference in New Issue
Block a user