Files
Mebbling/app/page.tsx
T

21 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Link from "next/link";
import { db } from "@/lib/db";
import { PostCard, type PublicPost } from "./components/post-card";
export const dynamic = "force-dynamic";
const pageSize = 20;
type Query = { q?: string; tag?: string; source?: string; author?: string; from?: string; to?: string; attachments?: string; page?: string };
export default async function Home({ searchParams }: { searchParams: Promise<Query> }) {
const query = await searchParams; const q = query.q?.trim() || ""; const tag = query.tag?.trim() || ""; const author = query.author?.trim() || ""; const sourceId = Number(query.source) || 0; const from = query.from || ""; const to = query.to || ""; const attachments = query.attachments === "1"; const page = Math.max(1, Number(query.page) || 1);
const where = ["p.visibility='PUBLIC'", "p.hidden=0"]; const args: (string | number)[] = [];
const ftsQuery = q.split(/\s+/).filter(Boolean).map((term) => `"${term.replaceAll('"', '""')}"`).join(" AND ");
if (q) { where.push("posts_fts MATCH ?"); args.push(ftsQuery); } if (tag) { where.push("p.tags_json LIKE ?"); args.push(`%${JSON.stringify(tag).slice(1, -1)}%`); } if (author) { where.push("(u.username LIKE ? OR s.remote_user LIKE ?)"); args.push(`%${author}%`, `%${author}%`); } if (sourceId) { where.push("s.id=?"); args.push(sourceId); } if (from) { where.push("date(COALESCE(p.remote_created_at,p.created_at)) >= date(?)"); args.push(from); } if (to) { where.push("date(COALESCE(p.remote_created_at,p.created_at)) <= date(?)"); args.push(to); } if (attachments) where.push("p.attachments_json <> '[]'");
const joins = ` FROM posts p ${q ? "JOIN posts_fts ON posts_fts.rowid=p.id" : ""} JOIN users u ON u.id=p.author_id LEFT JOIN sources s ON s.id=p.source_id `; const predicate = ` WHERE ${where.join(" AND ")}`;
const total = Number((db.prepare(`SELECT count(*) count${joins}${predicate}`).get(...args) as { count: number }).count); const pages = Math.max(1, Math.ceil(total / pageSize)); const safePage = Math.min(page, pages);
const posts = db.prepare(`SELECT p.*,u.username,s.name,s.remote_user,s.remote_display_name,s.base_url AS source_base_url,(SELECT count(*) FROM comments c WHERE c.post_id=p.id AND c.hidden=0) comment_count,(SELECT count(*) FROM reactions r WHERE r.post_id=p.id) reaction_count${joins}${predicate} ORDER BY COALESCE(p.remote_created_at,p.created_at) DESC LIMIT ? OFFSET ?`).all(...args, pageSize, (safePage - 1) * pageSize) as PublicPost[];
const sources = db.prepare("SELECT id,name FROM sources WHERE is_enabled=1 ORDER BY name").all() as { id: number; name: string }[];
const params = new URLSearchParams(); for (const [key, value] of Object.entries(query)) if (value && key !== "page") params.set(key, value); const pageHref = (target: number) => { const next = new URLSearchParams(params); next.set("page", String(target)); return `/?${next}`; };
return <><section className="space"><div><h1>公開 Memos Hub</h1><p className="muted">聚合朋友們公開分享的筆記。</p></div><Link className="button" href="/dashboard">發佈/連接來源</Link></section><form className="search-form" method="get"><input name="q" defaultValue={q} placeholder="搜尋公開貼文" /><input name="tag" defaultValue={tag} placeholder="標籤" /><input name="author" defaultValue={author} placeholder="作者" /><select name="source" defaultValue={sourceId || ""}><option value="">所有來源</option>{sources.map((source) => <option key={source.id} value={source.id}>{source.name}</option>)}</select><label><input name="from" type="date" defaultValue={from} /></label><label><input name="to" type="date" defaultValue={to} /></label><label className="check"><input name="attachments" type="checkbox" value="1" defaultChecked={attachments} />只看附件</label><button>搜尋</button></form><p className="meta"> {total} 篇公開貼文</p>{posts.length ? posts.map((post) => <PostCard post={post} key={post.id} />) : <p className="muted">尚無符合的公開貼文。</p>}{pages > 1 && <nav className="pagination" aria-label="貼文分頁">{safePage > 1 && <Link href={pageHref(safePage - 1)}> 上一頁</Link>}<span> {safePage}{pages} </span>{safePage < pages && <Link href={pageHref(safePage + 1)}>下一頁 </Link>}</nav>}</>;
}