IB nav + fixes

This commit is contained in:
sneedmaster
2026-02-04 14:15:13 +01:00
parent 82d76b6d7b
commit f980e390af
32 changed files with 743 additions and 737 deletions

View File

@@ -1,5 +1,5 @@
import { TemplateCtx } from "../../ctx";
import { Board } from "../../db/schema";
import { TemplateCtx } from "../ctx";
import { Board } from "../db/schema";
import { Form, FormField } from "./form";
import { PropsWithChildren } from "@kitajs/html";

View File

@@ -1,21 +1,19 @@
import { TemplateCtx } from "../ctx";
import { PropsWithChildren } from "@kitajs/html";
const BoardLinks = ({ ctx }: PropsWithChildren<{ ctx: TemplateCtx }>) => {
return (
<>
[
{ctx.listedBoards.map((board, index) => (
<>
<a href={`/ib/${board}`} safe>
{board}
</a>
{index === ctx.listedBoards.length - 1 ? "" : " / "}
</>
))}
]
</>
);
};
const BoardLinks = ({ ctx }: PropsWithChildren<{ ctx: TemplateCtx }>) => (
<>
[
{ctx.listedBoards.map((board, index) => (
<>
<a href={`/ib/${board}`} safe>
{board}
</a>
{index === ctx.listedBoards.length - 1 ? "" : " / "}
</>
))}
]
</>
);
export default BoardLinks;

72
src/components/form.tsx Normal file
View File

@@ -0,0 +1,72 @@
import { PropsWithChildren } from "@kitajs/html";
const Form = ({
submit,
action,
id,
center = false,
fullWidth = false,
disabled = false,
children,
}: PropsWithChildren<{
submit: string;
action: string;
id?: string;
center?: boolean;
fullWidth?: boolean;
disabled?: boolean;
}>) => (
<table
id={id}
class={`form-table${center ? " auto-center" : ""}${fullWidth ? " full-width" : ""}`}
>
<tbody>
{children}
{disabled ? (
""
) : (
<tr>
<td colspan="2">
<div class="input-cell">
<button name="action" type="submit" value={action} safe>
{submit}
</button>
</div>
</td>
</tr>
)}
</tbody>
</table>
);
const FormField = ({
label,
fullWidth = false,
wrapper = false,
children,
}: PropsWithChildren<{
label: string | JSX.Element;
fullWidth?: boolean;
wrapper?: boolean;
}>) => (
<tr>
<th>{label}</th>
<td>
<div
class={`input-cell${fullWidth ? " full-width" : ""}${wrapper ? " input-wrapper" : ""}`}
>
{children}
</div>
</td>
</tr>
);
const FormHeader = ({ children }: PropsWithChildren) => (
<tr>
<th class="center" colspan="2">
{children}
</th>
</tr>
);
export { Form, FormField, FormHeader };

View File

@@ -1,76 +0,0 @@
import { PropsWithChildren } from "@kitajs/html";
const Form = ({
submit,
action,
id,
center = false,
fullWidth = false,
disabled = false,
children,
}: PropsWithChildren<{
submit: string;
action: string;
id?: string;
center?: boolean;
fullWidth?: boolean;
disabled?: boolean;
}>) => {
return (
<table
id={id}
class={`form-table${center ? " auto-center" : ""}${fullWidth ? " full-width" : ""}`}
>
<tbody>
{children}
{disabled ? (
""
) : (
<tr>
<td colspan="2">
<div class="input-cell">
<button name="action" type="submit" value={action} safe>
{submit}
</button>
</div>
</td>
</tr>
)}
</tbody>
</table>
);
};
const FormField = ({
label,
fullWidth = false,
wrapper = false,
children,
}: PropsWithChildren<{
label: string | JSX.Element;
fullWidth?: boolean;
wrapper?: boolean;
}>) => {
return (
<tr>
<th>{label}</th>
<td>
<div
class={`input-cell${fullWidth ? " full-width" : ""}${wrapper ? " input-wrapper" : ""}`}
>
{children}
</div>
</td>
</tr>
);
};
const FormHeader = ({ children }: PropsWithChildren) => (
<tr>
<th class="center" colspan="2">
{children}
</th>
</tr>
);
export { Form, FormField, FormHeader };

View File

@@ -0,0 +1,24 @@
import { Board } from "../db/schema";
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`
}
>
Katalog
</a>
]
</>
);
export default IBLinks;

View File

@@ -15,11 +15,10 @@ const IPAddress = ({
</>
);
const cloakIP = (ctx: TemplateCtx, ip: string) => {
return createHash("sha256")
const cloakIP = (ctx: TemplateCtx, ip: string) =>
createHash("sha256")
.update(`${ip}$${ctx.config.site.cloak_secret}`)
.digest("hex")
.slice(0, 8);
};
export default IPAddress;

View File

@@ -1,13 +1,10 @@
import { PropsWithChildren } from "@kitajs/html";
const JsxComment = ({ children }: PropsWithChildren) => {
return (
<>
{"<!-- "}
{children}
{" --!>"}
</>
);
};
const JsxComment = ({ children }: PropsWithChildren) => (
<>
{"<!-- "}
{children}
{" --!>"}
</>
);
export default JsxComment;

View File

@@ -2,42 +2,40 @@ import { TemplateCtx } from "../ctx";
import { canUser, CzchanPerm } from "../permissions";
import { PropsWithChildren } from "@kitajs/html";
const ModNav = ({ ctx }: PropsWithChildren<{ ctx: TemplateCtx }>) => {
return (
<nav class="pagination">
[<a href="/mod/">Domov</a>] [<a href="/mod/users">Uživatelé</a>]{" "}
{canUser(ctx.user, CzchanPerm.MANAGE_BOARDS) && (
<>
[<a href="/mod/boards">Nástěnky</a>]{" "}
</>
)}
{canUser(ctx.user, CzchanPerm.MANAGE_BANS) && (
<>
[<a href="/mod/bans">Bany</a>]{" "}
</>
)}
{canUser(ctx.user, CzchanPerm.MANAGE_BANNERS) && (
<>
[<a href="/mod/banners">Bannery</a>]{" "}
</>
)}
{canUser(ctx.user, CzchanPerm.MANAGE_REPORTS) && (
<>
[<a href="/mod/reports">Hlášení</a>]{" "}
</>
)}
{canUser(ctx.user, CzchanPerm.MANAGE_NEWS) && (
<>
[<a href="/mod/news">Novinky</a>]{" "}
</>
)}
{canUser(ctx.user, CzchanPerm.VIEW_LOGS) && (
<>
[<a href="/mod/logs">Záznamy</a>]
</>
)}
</nav>
);
};
const ModNav = ({ ctx }: PropsWithChildren<{ ctx: TemplateCtx }>) => (
<nav class="pagination">
[<a href="/mod/">Domov</a>] [<a href="/mod/users">Uživatelé</a>]{" "}
{canUser(ctx.user, CzchanPerm.MANAGE_BOARDS) && (
<>
[<a href="/mod/boards">Nástěnky</a>]{" "}
</>
)}
{canUser(ctx.user, CzchanPerm.MANAGE_BANS) && (
<>
[<a href="/mod/bans">Bany</a>]{" "}
</>
)}
{canUser(ctx.user, CzchanPerm.MANAGE_BANNERS) && (
<>
[<a href="/mod/banners">Bannery</a>]{" "}
</>
)}
{canUser(ctx.user, CzchanPerm.MANAGE_REPORTS) && (
<>
[<a href="/mod/reports">Hlášení</a>]{" "}
</>
)}
{canUser(ctx.user, CzchanPerm.MANAGE_NEWS) && (
<>
[<a href="/mod/news">Novinky</a>]{" "}
</>
)}
{canUser(ctx.user, CzchanPerm.VIEW_LOGS) && (
<>
[<a href="/mod/logs">Záznamy</a>]
</>
)}
</nav>
);
export default ModNav;

View File

@@ -1,12 +1,9 @@
import { type PropsWithChildren } from "@kitajs/html";
const Page = ({ children }: PropsWithChildren) => {
return (
<>
{"<!DOCTYPE html>"}
<html lang="cs">{children}</html>
</>
);
};
const Page = ({ children }: PropsWithChildren) => (
<>
{"<!DOCTYPE html>"}
<html lang="cs">{children}</html>
</>
);
export default Page;

View File

@@ -6,47 +6,43 @@ import { type PropsWithChildren } from "@kitajs/html";
const PageBody = ({
ctx,
children,
}: PropsWithChildren<{ ctx: TemplateCtx }>) => {
return (
<body>
<div id="top"></div>
<header class="header clearfix">
<nav>
[<a href="/">domov</a>] <BoardLinks ctx={ctx} /> [
<a href="/news">novinky</a>]
<span class="float-r">
{ctx.user ? (
<>
{" "}
[<a href="/actions/logout">odhlásit</a>] [
<a href="/mod/">mod</a>]
</>
) : (
<>
{" "}
[<a href="/login">přihlásit</a>]
</>
)}
</span>
</nav>
</header>
<main>{children}</main>
<footer class="footer">
<div class="inner-footer">
<small>
<a href={pjson.repository.url}>czchan</a>{" "}
<span safe>{pjson.version}</span> - © 2025{" "}
<a href="mailto:dev@czchan.org">sneedmaster</a> - Licencováno{" "}
<a href="/LICENSE.txt">AGPL 3.0</a>
<br />
Všechny příspěvky na této stránce byly vytvořeny náhodnými
uživateli.
</small>
</div>
</footer>
<div id="bottom"></div>
</body>
);
};
}: PropsWithChildren<{ ctx: TemplateCtx }>) => (
<body>
<div id="top"></div>
<header class="header clearfix">
<nav>
[<a href="/">domov</a>] <BoardLinks ctx={ctx} /> [
<a href="/news">novinky</a>]
<span class="float-r">
{ctx.user ? (
<>
{" "}
[<a href="/actions/logout">odhlásit</a>] [<a href="/mod/">mod</a>]
</>
) : (
<>
{" "}
[<a href="/login">přihlásit</a>]
</>
)}
</span>
</nav>
</header>
<main>{children}</main>
<footer class="footer">
<div class="inner-footer">
<small>
<a href={pjson.repository.url}>czchan</a>{" "}
<span safe>{pjson.version}</span> - © 2025{" "}
<a href="mailto:dev@czchan.org">sneedmaster</a> - Licencováno{" "}
<a href="/LICENSE.txt">AGPL 3.0</a>
<br />
Všechny příspěvky na této stránce byly vytvořeny náhodnými uživateli.
</small>
</div>
</footer>
<div id="bottom"></div>
</body>
);
export default PageBody;

View File

@@ -12,23 +12,21 @@ const PageHead = ({
title: string;
theme?: string;
description?: string;
}>) => {
return (
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title safe>{title}</title>
{description !== undefined && (
<meta name="description" content={description} />
)}
<link rel="stylesheet" href="/public/css/base.css" />
<link
rel="stylesheet"
href={`/public/css/themes/${theme || ctx.config.site.global_theme}.css`}
/>
{children}
</head>
);
};
}>) => (
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title safe>{title}</title>
{description !== undefined && (
<meta name="description" content={description} />
)}
<link rel="stylesheet" href="/public/css/base.css" />
<link
rel="stylesheet"
href={`/public/css/themes/${theme || ctx.config.site.global_theme}.css`}
/>
{children}
</head>
);
export default PageHead;

View File

@@ -7,12 +7,14 @@ const Pagination = ({
entries,
current,
max,
otherLinks,
}: PropsWithChildren<{
ctx: TemplateCtx;
base: string;
entries: number;
current: number;
max?: number;
otherLinks?: JSX.Element;
}>) => {
const impliedPages = Math.ceil(entries / ctx.config.site.page_size);
const actualPages = Math.min(impliedPages, max || impliedPages);
@@ -52,6 +54,7 @@ const Pagination = ({
[<a href={`${base}?page=${current + 1}`}>Další</a>]
</>
)}
{otherLinks !== undefined && <> {otherLinks}</>}
</nav>
);
};

View File

@@ -18,173 +18,171 @@ const PostComponent = ({
board: Board;
post: Post;
reply?: boolean;
}>) => {
return (
<div
id={`${board.id}-${post.id}`}
class={`post${reply ? " reply" : ""}`}
data-board={board.id}
>
<div class="post-header">
<input name="posts" type="checkbox" value={`${board.id}$${post.id}`} />{" "}
{post.subject !== null && (
<>
<span class="subject" safe>
{post.subject}
</span>{" "}
</>
)}
{post.email !== null ? (
<a class="name" rel="nofollow" href={`mailto:${post.email}`} safe>
{post.name}
</a>
) : (
<span class="name" safe>
{post.name}
</span>
)}{" "}
{post.tripcode !== null && (
<>
<span class="tripcode" safe>
{post.tripcode}
</span>{" "}
</>
)}
{post.capcode !== null && (
<>
<Capcode capcode={post.capcode} />{" "}
</>
)}
{board.config.geo_flags && (
<>
<img
class="icon"
src={`/public/img/country_flags/${post.metadata.country || "xx"}.png`}
/>{" "}
</>
)}
<Datetime date={post.created} />{" "}
<span class="post-number">
<a href={canonicalPostURL(post)}>Č.</a>
<a
class="quote-link"
href={`${threadURL(post)}#post-form`}
data-thread-url={threadURL(post)}
>
{post.id}
</a>
</span>{" "}
{board.config.user_ids && (
<span
class="user-id"
style={{ backgroundColor: `#${post.user_id}` }}
safe
>
{post.user_id}
</span>
)}
{post.thread === null && (
<>
<a href={threadURL(post)}>[Otevřít]</a>{" "}
</>
)}
{post.f_sticky !== 0 && (
<>
<img src="/public/img/icons/sticky.png" />{" "}
</>
)}
{post.f_locked && (
<>
<img src="/public/img/icons/locked.png" />{" "}
</>
)}
{post.f_bumplocked && (
<>
<img src="/public/img/icons/bumplock.png" />{" "}
</>
)}
{post.f_looped && (
<>
<img src="/public/img/icons/loop.png" />{" "}
</>
)}
</div>
<div class="clearfix">
{
<div
class={`post-files${post.files.length > 1 ? " multiple-files" : ""}`}
>
{post.files.map((file) => (
<div class="post-file">
<div class="post-file-info">
<a
title={`Stáhnout ${file.original_filename}`}
download={file.original_filename}
href={file.url}
safe
>
{truncateFilename(file.original_filename, 16)}
</a>
<br />
<small>
(<span safe>{bytesToSize(file.size)}</span>
{file.dimensions !== null && (
<span>
, {file.dimensions[0]}x{file.dimensions[1]}
</span>
)}
{file.duration !== null && (
<span safe>, {secsToDuration(file.duration)}</span>
)}
)
</small>
</div>
<a class="thumb-link" target="_blank" href={file.url}>
<img
class={"thumb thumb-" + file.type}
src={(() => {
if (file.thumb_url) {
return file.thumb_url;
}
switch (file.type) {
case "image":
return file.url;
case "audio":
return "/public/img/thumb/audio.png";
default:
return "";
}
})()}
/>
</a>
</div>
))}
</div>
}
<div class="post-body post-content">{post.content as "safe"}</div>
</div>
{post.replies.length > 0 && (
<small>
Odpovědi:{" "}
{post.replies.map((reply) => (
<>
<a
class="quote"
href={canonicalPostURL({
board: board.id,
id: reply,
thread: post.id,
} as Post)}
>
&gt;&gt;{reply}
</a>{" "}
</>
))}
</small>
}>) => (
<div
id={`${board.id}-${post.id}`}
class={`post${reply ? " reply" : ""}`}
data-board={board.id}
>
<div class="post-header">
<input name="posts" type="checkbox" value={`${board.id}$${post.id}`} />{" "}
{post.subject !== null && (
<>
<span class="subject" safe>
{post.subject}
</span>{" "}
</>
)}
{post.email !== null ? (
<a class="name" rel="nofollow" href={`mailto:${post.email}`} safe>
{post.name}
</a>
) : (
<span class="name" safe>
{post.name}
</span>
)}{" "}
{post.tripcode !== null && (
<>
<span class="tripcode" safe>
{post.tripcode}
</span>{" "}
</>
)}
{post.capcode !== null && (
<>
<Capcode capcode={post.capcode} />{" "}
</>
)}
{board.config.geo_flags && (
<>
<img
class="icon"
src={`/public/img/country_flags/${post.metadata.country || "xx"}.png`}
/>{" "}
</>
)}
<Datetime date={post.created} />{" "}
<span class="post-number">
<a href={canonicalPostURL(post)}>Č.</a>
<a
class="quote-link"
href={`${threadURL(post)}#post-form`}
data-thread-url={threadURL(post)}
>
{post.id}
</a>
</span>{" "}
{board.config.user_ids && (
<span
class="user-id"
style={{ backgroundColor: `#${post.user_id}` }}
safe
>
{post.user_id}
</span>
)}
{post.thread === null && (
<>
<a href={threadURL(post)}>[Otevřít]</a>{" "}
</>
)}
{post.f_sticky !== 0 && (
<>
<img src="/public/img/icons/sticky.png" />{" "}
</>
)}
{post.f_locked && (
<>
<img src="/public/img/icons/locked.png" />{" "}
</>
)}
{post.f_bumplocked && (
<>
<img src="/public/img/icons/bumplock.png" />{" "}
</>
)}
{post.f_looped && (
<>
<img src="/public/img/icons/loop.png" />{" "}
</>
)}
</div>
);
};
<div class="clearfix">
{
<div
class={`post-files${post.files.length > 1 ? " multiple-files" : ""}`}
>
{post.files.map((file) => (
<div class="post-file">
<div class="post-file-info">
<a
title={`Stáhnout ${file.original_filename}`}
download={file.original_filename}
href={file.url}
safe
>
{truncateFilename(file.original_filename, 16)}
</a>
<br />
<small>
(<span safe>{bytesToSize(file.size)}</span>
{file.dimensions !== null && (
<span>
, {file.dimensions[0]}x{file.dimensions[1]}
</span>
)}
{file.duration !== null && (
<span safe>, {secsToDuration(file.duration)}</span>
)}
)
</small>
</div>
<a class="thumb-link" target="_blank" href={file.url}>
<img
class={"thumb thumb-" + file.type}
src={(() => {
if (file.thumb_url) {
return file.thumb_url;
}
switch (file.type) {
case "image":
return file.url;
case "audio":
return "/public/img/thumb/audio.png";
default:
return "";
}
})()}
/>
</a>
</div>
))}
</div>
}
<div class="post-body post-content">{post.content as "safe"}</div>
</div>
{post.replies.length > 0 && (
<small>
Odpovědi:{" "}
{post.replies.map((reply) => (
<>
<a
class="quote"
href={canonicalPostURL({
board: board.id,
id: reply,
thread: post.id,
} as Post)}
>
&gt;&gt;{reply}
</a>{" "}
</>
))}
</small>
)}
</div>
);
export default PostComponent;

View File

@@ -1,6 +1,6 @@
import { TemplateCtx } from "../../ctx";
import { Board } from "../../db/schema";
import { canUser, CzchanPerm } from "../../permissions";
import { TemplateCtx } from "../ctx";
import { Board } from "../db/schema";
import { canUser, CzchanPerm } from "../permissions";
import { Form, FormField, FormHeader } from "./form";
import { PropsWithChildren } from "@kitajs/html";

View File

@@ -1,11 +1,12 @@
import { valkey } from "../../cache";
import PostForm from "../../components/forms/post_form";
import IBHeader from "../../components/ib_header";
import IBLinks from "../../components/ib_links";
import Page from "../../components/page";
import PageBody from "../../components/page_body";
import PageHead from "../../components/page_head";
import Pagination from "../../components/pagination";
import PostComponent from "../../components/post";
import PostForm from "../../components/post_form";
import { getCtx, TemplateCtx } from "../../ctx";
import { readRanomBanner } from "../../db/banner";
import { readBoard } from "../../db/board";
@@ -65,51 +66,64 @@ const Template = (
page: number,
threads: { op: Post; replies: Post[] }[],
totalThreads: number,
) => {
return (
<Page>
<PageHead
) => (
<Page>
<PageHead
ctx={ctx}
title={`/${board.id}/ - ${board.name} - Strana ${page}`}
theme={board.config.theme}
description={board.description}
/>
<PageBody ctx={ctx}>
<IBHeader banner={banner} board={board} />
<hr />
<PostForm ctx={ctx} board={board} />
<hr />
<Pagination
ctx={ctx}
title={`/${board.id}/ - ${board.name} - Strana ${page}`}
theme={board.config.theme}
description={board.description}
base={`/ib/${board.id}`}
entries={totalThreads}
current={page}
max={board.config.pages}
otherLinks={<IBLinks board={board} />}
/>
<PageBody ctx={ctx}>
<IBHeader banner={banner} board={board} />
<hr />
<PostForm ctx={ctx} board={board} />
<hr />
{threads.map((thread) => (
<div class="thread">
<PostComponent board={board} post={thread.op} />
{thread.replies.length > ctx.config.site.preview_replies && (
<p>
Bylo vynecháno {thread.replies.length} odpovědí.{" "}
<a href={threadURL(thread.op)}>Kliknutím se otevře vlákno.</a>
</p>
)}
{thread.replies
.reverse()
.slice(thread.replies.length - ctx.config.site.preview_replies)
.reverse()
.map((reply) => (
<>
<PostComponent board={board} post={reply} reply />
<br />
</>
))}
<hr />
</div>
))}
<Pagination
ctx={ctx}
base={`/ib/${board.id}`}
entries={totalThreads}
current={page}
max={board.config.pages}
/>
<hr />
</PageBody>
</Page>
);
};
<hr />
{threads.map((thread) => (
<div class="thread">
<PostComponent board={board} post={thread.op} />
{thread.replies.length > ctx.config.site.preview_replies && (
<p>
Bylo vynecháno {thread.replies.length} odpovědí.{" "}
<a href={threadURL(thread.op)}>Kliknutím se otevře vlákno.</a>
</p>
)}
{thread.replies
.reverse()
.slice(
Math.max(
0,
thread.replies.length - ctx.config.site.preview_replies,
),
)
.reverse()
.map((reply) => (
<>
<PostComponent board={board} post={reply} reply />
<br />
</>
))}
<hr />
</div>
))}
<Pagination
ctx={ctx}
base={`/ib/${board.id}`}
entries={totalThreads}
current={page}
max={board.config.pages}
otherLinks={<IBLinks board={board} />}
/>{" "}
<hr />
</PageBody>
</Page>
);

View File

@@ -1,13 +1,14 @@
import CatalogTile from "../../components/catalog_tile";
import PostForm from "../../components/forms/post_form";
import IBHeader from "../../components/ib_header";
import IBLinks from "../../components/ib_links";
import Page from "../../components/page";
import PageBody from "../../components/page_body";
import PageHead from "../../components/page_head";
import PostForm from "../../components/post_form";
import { getCtx, TemplateCtx } from "../../ctx";
import { readRanomBanner } from "../../db/banner";
import { readBoard } from "../../db/board";
import { readAllBoardThreads } from "../../db/post";
import { readBoardCatalog } from "../../db/post";
import { Board, Post } from "../../db/schema";
import { CzchanError } from "../../error";
import { canUser, CzchanPerm } from "../../permissions";
@@ -26,7 +27,7 @@ export default async (req: Request, res: Response) => {
throw new CzchanError(`K nástěnce /${board.id}/ nemáš přístup.`, 403);
}
const posts = await readAllBoardThreads(board.id);
const posts = await readBoardCatalog(board.id);
const html = Template(ctx, banner, board, posts);
res.send(html);
@@ -37,27 +38,33 @@ const Template = (
banner: string,
board: Board,
posts: Post[],
) => {
return (
<Page>
<PageHead
ctx={ctx}
title={`/${board.id}/ - ${board.name} - Katalog`}
theme={board.config.theme}
description={board.description}
/>
<PageBody ctx={ctx}>
<IBHeader banner={banner} board={board} catalog />
<hr />
<PostForm ctx={ctx} board={board} />
<hr />
<div class="center">
{posts.map((post) => (
<CatalogTile post={post} />
))}
</div>
<hr />
</PageBody>
</Page>
);
};
) => (
<Page>
<PageHead
ctx={ctx}
title={`/${board.id}/ - ${board.name} - Katalog`}
theme={board.config.theme}
description={board.description}
/>
<PageBody ctx={ctx}>
<IBHeader banner={banner} board={board} catalog />
<hr />
<PostForm ctx={ctx} board={board} />
<hr />
<div class="pagination">
<IBLinks board={board} />
</div>
<hr />
<div class="center">
{posts.map((post) => (
<CatalogTile post={post} />
))}
</div>
<hr />
<div class="pagination">
<IBLinks board={board} />
</div>
<hr />
</PageBody>
</Page>
);

View File

@@ -1,9 +1,10 @@
import PostForm from "../../components/forms/post_form";
import IBHeader from "../../components/ib_header";
import IBLinks from "../../components/ib_links";
import Page from "../../components/page";
import PageBody from "../../components/page_body";
import PageHead from "../../components/page_head";
import PostComponent from "../../components/post";
import PostForm from "../../components/post_form";
import { getCtx, TemplateCtx } from "../../ctx";
import { readRanomBanner } from "../../db/banner";
import { readBoard } from "../../db/board";
@@ -47,31 +48,37 @@ const Template = (
board: Board,
op: Post,
replies: Post[],
) => {
return (
<Page>
<PageHead
ctx={ctx}
title={`/${board.id}/ - ${op.subject || truncate(op.content_unformatted, 64)}`}
theme={board.config.theme}
description={truncate(op.content_unformatted, 128)}
/>
<PageBody ctx={ctx}>
<IBHeader banner={banner} board={board} />
<hr />
<PostForm ctx={ctx} board={board} thread={op.id} />
<hr />
) => (
<Page>
<PageHead
ctx={ctx}
title={`/${board.id}/ - ${op.subject || truncate(op.content_unformatted, 64)}`}
theme={board.config.theme}
description={truncate(op.content_unformatted, 128)}
/>
<PageBody ctx={ctx}>
<IBHeader banner={banner} board={board} />
<hr />
<PostForm ctx={ctx} board={board} thread={op.id} />
<hr />
<div class="pagination">
<IBLinks board={board} />
</div>
<hr />
<div class="thread">
<PostComponent board={board} post={op} />
<div class="thread">
{replies.map((reply) => (
<>
<PostComponent board={board} post={reply} reply />
<br />
</>
))}
</div>
<hr />
</PageBody>
</Page>
);
};
{replies.map((reply) => (
<>
<PostComponent board={board} post={reply} reply />
<br />
</>
))}
</div>
<hr />
<div class="pagination">
<IBLinks board={board} />
</div>
<hr />
</PageBody>
</Page>
);

View File

@@ -11,22 +11,20 @@ export default async (req: Request, res: Response) => {
res.send(html);
};
const Template = (ctx: TemplateCtx) => {
return (
<Page>
<PageHead
ctx={ctx}
title="czchan"
description="Český anonymní imageboard"
/>
<PageBody ctx={ctx}>
<div class="center">
<img class="logo" src="/public/img/logo.png" />
<p class="subtitle">Český anonymní imageboard</p>
<hr />
<hr />
</div>
</PageBody>
</Page>
);
};
const Template = (ctx: TemplateCtx) => (
<Page>
<PageHead
ctx={ctx}
title="czchan"
description="Český anonymní imageboard"
/>
<PageBody ctx={ctx}>
<div class="center">
<img class="logo" src="/public/img/logo.png" />
<p class="subtitle">Český anonymní imageboard</p>
<hr />
<hr />
</div>
</PageBody>
</Page>
);

View File

@@ -1,4 +1,4 @@
import { Form, FormField } from "../components/forms/form";
import { Form, FormField } from "../components/form";
import Page from "../components/page";
import PageBody from "../components/page_body";
import PageHead from "../components/page_head";
@@ -12,25 +12,23 @@ export default async (req: Request, res: Response) => {
res.send(html);
};
const Template = (ctx: TemplateCtx) => {
return (
<Page>
<PageHead ctx={ctx} title="Přihlásit se" description="" />
<PageBody ctx={ctx}>
<h1 class="title center">Přihlásit se</h1>
<hr />
<form method="post" action="/actions/login">
<Form submit="Přihlásit se" action="$login" center>
<FormField label="Jméno">
<input name="username" type="text" />
</FormField>
<FormField label="Heslo">
<input name="password" type="password" />
</FormField>
</Form>
</form>
<hr />
</PageBody>
</Page>
);
};
const Template = (ctx: TemplateCtx) => (
<Page>
<PageHead ctx={ctx} title="Přihlásit se" description="" />
<PageBody ctx={ctx}>
<h1 class="title center">Přihlásit se</h1>
<hr />
<form method="post" action="/actions/login">
<Form submit="Přihlásit se" action="$login" center>
<FormField label="Jméno">
<input name="username" type="text" />
</FormField>
<FormField label="Heslo">
<input name="password" type="password" />
</FormField>
</Form>
</form>
<hr />
</PageBody>
</Page>
);

View File

@@ -145,7 +145,7 @@ const updateCapcode = async (
const segments = capcode_icon.split("$");
if (
segments[0] === "file" &&
segments[0] === "icon" &&
!req.config.assets.capcodeIcons.includes(segments[1])
) {
throw new CzchanError(`Ikona ${segments[1]} neexistuje.`, 400);
@@ -172,7 +172,7 @@ const updateCapcode = async (
case "none":
icon = null;
break;
case "file":
case "icon":
icon = segments[1];
break;
}

View File

@@ -1,5 +1,5 @@
import Datetime from "../../components/datetime";
import { Form, FormField } from "../../components/forms/form";
import { Form, FormField } from "../../components/form";
import ModNav from "../../components/mod_nav";
import Page from "../../components/page";
import PageBody from "../../components/page_body";
@@ -24,32 +24,30 @@ export default async (req: Request, res: Response) => {
res.send(html);
};
const Template = (ctx: TemplateCtx, banners: Banner[]) => {
return (
<Page>
<PageHead ctx={ctx} title="Bannery" description="" />
<PageBody ctx={ctx}>
<h1 class="title center">Bannery</h1>
<hr />
<ModNav ctx={ctx} />
<hr />
<form method="post" action="/mod/actions/banner_actions">
<BannerTable ctx={ctx} banners={banners} />
{canUser(ctx.user, CzchanPerm.MANAGE_BANNERS) && (
<>
<hr />
<button name="action" type="submit" value="$delete_banner">
Odstranit
</button>
</>
)}
</form>
<CreateForm />
<hr />
</PageBody>
</Page>
);
};
const Template = (ctx: TemplateCtx, banners: Banner[]) => (
<Page>
<PageHead ctx={ctx} title="Bannery" description="" />
<PageBody ctx={ctx}>
<h1 class="title center">Bannery</h1>
<hr />
<ModNav ctx={ctx} />
<hr />
<form method="post" action="/mod/actions/banner_actions">
<BannerTable ctx={ctx} banners={banners} />
{canUser(ctx.user, CzchanPerm.MANAGE_BANNERS) && (
<>
<hr />
<button name="action" type="submit" value="$delete_banner">
Odstranit
</button>
</>
)}
</form>
<CreateForm />
<hr />
</PageBody>
</Page>
);
const BannerTable = ({
ctx,

View File

@@ -25,27 +25,25 @@ export default async (req: Request, res: Response) => {
res.send(html);
};
const Template = (ctx: TemplateCtx, bans: Ban[]) => {
return (
<Page>
<PageHead ctx={ctx} title="Bany" description="" />
<PageBody ctx={ctx}>
<h1 class="title center">Bany</h1>
const Template = (ctx: TemplateCtx, bans: Ban[]) => (
<Page>
<PageHead ctx={ctx} title="Bany" description="" />
<PageBody ctx={ctx}>
<h1 class="title center">Bany</h1>
<hr />
<ModNav ctx={ctx} />
<hr />
<form method="post" action="/mod/actions/ban_actions">
<BanTable ctx={ctx} bans={bans} />
<hr />
<ModNav ctx={ctx} />
<hr />
<form method="post" action="/mod/actions/ban_actions">
<BanTable ctx={ctx} bans={bans} />
<hr />
<button name="action" type="submit" value="$delete_ban">
Odstranit
</button>
</form>
<hr />
</PageBody>
</Page>
);
};
<button name="action" type="submit" value="$delete_ban">
Odstranit
</button>
</form>
<hr />
</PageBody>
</Page>
);
const BanTable = ({
ctx,
@@ -89,41 +87,37 @@ const BanRow = ({
}: PropsWithChildren<{ ctx: TemplateCtx; ban: Ban }>) => (
<tr>
<td>
{(() => {
return (
<input
name="bans"
type="checkbox"
value={ban.id.toString()}
disabled={
!(
ctx.user?.username === ban.created_by ||
canUser(ctx.user, CzchanPerm.ALL_BANS)
)
}
/>
);
})()}
<input
name="bans"
type="checkbox"
value={ban.id.toString()}
disabled={
!(
ctx.user?.username === ban.created_by ||
canUser(ctx.user, CzchanPerm.ALL_BANS)
)
}
/>
</td>
<td>
<IPAddress ctx={ctx} ip={ban.ip_range} />
</td>
<td>
{ban.board === null ? (
<i>Všechny</i>
) : (
{ban.board !== null ? (
<span safe>{`/${ban.board}/`}</span>
) : (
<i>Všechny</i>
)}
</td>
<td>
<BoolIcon value={ban.appealable} />
</td>
<td>{ban.appeal === null ? "-" : <span safe>{ban.appeal}</span>}</td>
<td>{ban.appeal !== null ? <span safe>{ban.appeal}</span> : "-"}</td>
<td>
{ban.appeal_response === null ? (
"-"
) : (
{ban.appeal_response !== null ? (
<span safe>{ban.appeal_response}</span>
) : (
"-"
)}
</td>
<td safe>{ban.created_by}</td>

View File

@@ -1,4 +1,4 @@
import BoardConfigForm from "../../components/forms/board_config_form";
import BoardConfigForm from "../../components/board_config_form";
import Page from "../../components/page";
import PageBody from "../../components/page_body";
import PageHead from "../../components/page_head";

View File

@@ -1,5 +1,5 @@
import Datetime from "../../components/datetime";
import { Form, FormField } from "../../components/forms/form";
import { Form, FormField } from "../../components/form";
import ModNav from "../../components/mod_nav";
import Page from "../../components/page";
import PageBody from "../../components/page_body";
@@ -24,29 +24,27 @@ export default async (req: Request, res: Response) => {
res.send(html);
};
const Template = (ctx: TemplateCtx, boards: Board[]) => {
return (
<Page>
<PageHead ctx={ctx} title="Nástěnky" description="" />
<PageBody ctx={ctx}>
<h1 class="title center">Nástěnky</h1>
const Template = (ctx: TemplateCtx, boards: Board[]) => (
<Page>
<PageHead ctx={ctx} title="Nástěnky" description="" />
<PageBody ctx={ctx}>
<h1 class="title center">Nástěnky</h1>
<hr />
<ModNav ctx={ctx} />
<hr />
<form method="post" action="/mod/actions/board_actions">
<BoardTable ctx={ctx} boards={boards} />
<hr />
<ModNav ctx={ctx} />
<hr />
<form method="post" action="/mod/actions/board_actions">
<BoardTable ctx={ctx} boards={boards} />
<hr />
<button name="action" type="submit" value="$delete_board">
Odstranit
</button>
<UpdateForm />
</form>
<CreateForm />
<hr />
</PageBody>
</Page>
);
};
<button name="action" type="submit" value="$delete_board">
Odstranit
</button>
<UpdateForm />
</form>
<CreateForm />
<hr />
</PageBody>
</Page>
);
const BoardTable = ({
ctx,

View File

@@ -1,5 +1,5 @@
import Capcode from "../../components/capcode";
import { Form, FormField } from "../../components/forms/form";
import { Form, FormField } from "../../components/form";
import ModNav from "../../components/mod_nav";
import Page from "../../components/page";
import PageBody from "../../components/page_body";
@@ -19,24 +19,22 @@ export default async (req: Request, res: Response) => {
res.send(html);
};
const Template = (ctx: TemplateCtx, boards: Board[]) => {
return (
<Page>
<PageHead ctx={ctx} title="Domov" description="" />
<PageBody ctx={ctx}>
<h1 class="title center">Domov</h1>
<hr />
<ModNav ctx={ctx} />
<hr />
<UserTable ctx={ctx} />
<hr />
<BoardTable ctx={ctx} boards={boards} />
<PasswordChangeForm />
<hr />
</PageBody>
</Page>
);
};
const Template = (ctx: TemplateCtx, boards: Board[]) => (
<Page>
<PageHead ctx={ctx} title="Domov" description="" />
<PageBody ctx={ctx}>
<h1 class="title center">Domov</h1>
<hr />
<ModNav ctx={ctx} />
<hr />
<UserTable ctx={ctx} />
<hr />
<BoardTable ctx={ctx} boards={boards} />
<PasswordChangeForm />
<hr />
</PageBody>
</Page>
);
const UserTable = ({ ctx }: PropsWithChildren<{ ctx: TemplateCtx }>) => (
<div class="table-wrapper">

View File

@@ -18,17 +18,15 @@ export default async (req: Request, res: Response) => {
res.send(html);
};
const Template = (ctx: TemplateCtx) => {
return (
<Page>
<PageHead ctx={ctx} title="Záznamy" description="" />
<PageBody ctx={ctx}>
<h1 class="title center">Záznamy</h1>
<p class="subtitle center">TODO</p>
<hr />
<ModNav ctx={ctx} />
<hr />
</PageBody>
</Page>
);
};
const Template = (ctx: TemplateCtx) => (
<Page>
<PageHead ctx={ctx} title="Záznamy" description="" />
<PageBody ctx={ctx}>
<h1 class="title center">Záznamy</h1>
<p class="subtitle center">TODO</p>
<hr />
<ModNav ctx={ctx} />
<hr />
</PageBody>
</Page>
);

View File

@@ -1,5 +1,5 @@
import Datetime from "../../components/datetime";
import { Form, FormField } from "../../components/forms/form";
import { Form, FormField } from "../../components/form";
import ModNav from "../../components/mod_nav";
import Page from "../../components/page";
import PageBody from "../../components/page_body";
@@ -24,28 +24,26 @@ export default async (req: Request, res: Response) => {
res.send(html);
};
const Template = (ctx: TemplateCtx, news: News[]) => {
return (
<Page>
<PageHead ctx={ctx} title="Novinky" description="" />
<PageBody ctx={ctx}>
<h1 class="title center">Novinky</h1>
const Template = (ctx: TemplateCtx, news: News[]) => (
<Page>
<PageHead ctx={ctx} title="Novinky" description="" />
<PageBody ctx={ctx}>
<h1 class="title center">Novinky</h1>
<hr />
<ModNav ctx={ctx} />
<hr />
<form method="post" action="/mod/actions/news_actions">
<NewsTable ctx={ctx} news={news} />
<hr />
<ModNav ctx={ctx} />
<hr />
<form method="post" action="/mod/actions/news_actions">
<NewsTable ctx={ctx} news={news} />
<hr />
<button name="action" type="submit" value="$delete_news">
Odstranit
</button>
</form>
<CreateForm />
<hr />
</PageBody>
</Page>
);
};
<button name="action" type="submit" value="$delete_news">
Odstranit
</button>
</form>
<CreateForm />
<hr />
</PageBody>
</Page>
);
const NewsTable = ({
ctx,

View File

@@ -1,4 +1,4 @@
import { Form, FormField } from "../../components/forms/form";
import { Form, FormField } from "../../components/form";
import Page from "../../components/page";
import PageBody from "../../components/page_body";
import PageHead from "../../components/page_head";

View File

@@ -18,17 +18,15 @@ export default async (req: Request, res: Response) => {
res.send(html);
};
const Template = (ctx: TemplateCtx) => {
return (
<Page>
<PageHead ctx={ctx} title="Hlášení" description="" />
<PageBody ctx={ctx}>
<h1 class="title center">Hlášení</h1>
<p class="subtitle center">TODO</p>
<hr />
<ModNav ctx={ctx} />
<hr />
</PageBody>
</Page>
);
};
const Template = (ctx: TemplateCtx) => (
<Page>
<PageHead ctx={ctx} title="Hlášení" description="" />
<PageBody ctx={ctx}>
<h1 class="title center">Hlášení</h1>
<p class="subtitle center">TODO</p>
<hr />
<ModNav ctx={ctx} />
<hr />
</PageBody>
</Page>
);

View File

@@ -1,4 +1,4 @@
import { Form, FormField } from "../../components/forms/form";
import { Form, FormField } from "../../components/form";
import Page from "../../components/page";
import PageBody from "../../components/page_body";
import PageHead from "../../components/page_head";

View File

@@ -1,6 +1,6 @@
import Capcode from "../../components/capcode";
import Datetime from "../../components/datetime";
import { Form, FormField } from "../../components/forms/form";
import { Form, FormField } from "../../components/form";
import ModNav from "../../components/mod_nav";
import Page from "../../components/page";
import PageBody from "../../components/page_body";
@@ -25,36 +25,34 @@ export default async (req: Request, res: Response) => {
res.send(html);
};
const Template = (ctx: TemplateCtx, users: User[]) => {
return (
<Page>
<PageHead ctx={ctx} title="Uživatelé" description="" />
<PageBody ctx={ctx}>
<h1 class="title center">Uživatelé</h1>
const Template = (ctx: TemplateCtx, users: User[]) => (
<Page>
<PageHead ctx={ctx} title="Uživatelé" description="" />
<PageBody ctx={ctx}>
<h1 class="title center">Uživatelé</h1>
<hr />
<ModNav ctx={ctx} />
<hr />
<form method="post" action="/mod/actions/user_actions">
<UserTable ctx={ctx} users={users} />
<hr />
<ModNav ctx={ctx} />
<hr />
<form method="post" action="/mod/actions/user_actions">
<UserTable ctx={ctx} users={users} />
<hr />
<button name="action" type="submit" value="$delete_user">
Odstranit
</button>
{canUser(ctx.user, CzchanPerm.MANAGE_USERS) && (
<UpdateRankForm ctx={ctx} />
)}
{(canUser(ctx.user, CzchanPerm.MANAGE_USERS) ||
canUser(ctx.user, CzchanPerm.CUSTOM_CAPCODE)) && (
<UpdateCapcodeForm ctx={ctx} />
)}
{canUser(ctx.user, CzchanPerm.MANAGE_USERS) && <RoleForm ctx={ctx} />}
</form>
{canUser(ctx.user, CzchanPerm.MANAGE_USERS) && <CreateForm ctx={ctx} />}
<hr />
</PageBody>
</Page>
);
};
<button name="action" type="submit" value="$delete_user">
Odstranit
</button>
{canUser(ctx.user, CzchanPerm.MANAGE_USERS) && (
<UpdateRankForm ctx={ctx} />
)}
{(canUser(ctx.user, CzchanPerm.MANAGE_USERS) ||
canUser(ctx.user, CzchanPerm.CUSTOM_CAPCODE)) && (
<UpdateCapcodeForm ctx={ctx} />
)}
{canUser(ctx.user, CzchanPerm.MANAGE_USERS) && <RoleForm ctx={ctx} />}
</form>
{canUser(ctx.user, CzchanPerm.MANAGE_USERS) && <CreateForm ctx={ctx} />}
<hr />
</PageBody>
</Page>
);
const UserTable = ({
ctx,

View File

@@ -16,21 +16,19 @@ export default async (req: Request, res: Response) => {
res.send(html);
};
const Template = (ctx: TemplateCtx, news: News[]) => {
return (
<Page>
<PageHead ctx={ctx} title="Novinky" />
<PageBody ctx={ctx}>
<div class="container">
<h1 class="title center">Novinky</h1>
{news.map((news) => (
<NewsPost news={news} />
))}
</div>
</PageBody>
</Page>
);
};
const Template = (ctx: TemplateCtx, news: News[]) => (
<Page>
<PageHead ctx={ctx} title="Novinky" />
<PageBody ctx={ctx}>
<div class="container">
<h1 class="title center">Novinky</h1>
{news.map((news) => (
<NewsPost news={news} />
))}
</div>
</PageBody>
</Page>
);
const NewsPost = ({ news }: PropsWithChildren<{ news: News }>) => (
<article class="infobox">