Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions app/api/docs-tree/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { NextResponse } from "next/server";
import * as fs from "node:fs";
import * as path from "node:path";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export const revalidate = 0;

type DirNode = {
name: string;
path: string; // relative to docs root
children?: DirNode[];
};

type Diag = {
cwd: string;
node: string;
hasFs: boolean;
candidates: string[];
envHints: {
NEXT_RUNTIME: string | null;
NODE_ENV: string | null;
};
};

function hasFs() {
try {
return (
typeof fs.readdirSync === "function" &&
typeof fs.existsSync === "function"
);
} catch {
return false;
}
}

function safeListDir(dir: string): { entries: fs.Dirent[]; error?: string } {
try {
return { entries: fs.readdirSync(dir, { withFileTypes: true }) };
} catch (e) {
return { entries: [], error: String(e) };
}
}

function buildTree(root: string, maxDepth = 2, rel = ""): DirNode[] {
const { entries, error } = safeListDir(root);
if (error) throw new Error(`readdir failed at ${root}: ${error}`);

const dirs = entries.filter((d) => d.isDirectory());
const nodes: DirNode[] = [];

for (const e of dirs) {
if (e.name.startsWith(".") || e.name.startsWith("[")) continue;
const abs = path.join(root, e.name);
const nodeRel = rel ? `${rel}/${e.name}` : e.name;
const node: DirNode = { name: e.name, path: nodeRel };
if (maxDepth > 1) node.children = buildTree(abs, maxDepth - 1, nodeRel);
nodes.push(node);
}

try {
nodes.sort((a, b) => a.name.localeCompare(b.name, "zh-Hans"));
} catch {
nodes.sort((a, b) => a.name.localeCompare(b.name));
}
return nodes;
}

export async function GET() {
const cwd = process.cwd();
const candidates = [
path.join(cwd, "app", "docs"),
path.join(cwd, "src", "app", "docs"),
];

const diag: Diag = {
cwd,
node: process.version,
hasFs: hasFs(),
candidates,
envHints: {
NEXT_RUNTIME: process.env.NEXT_RUNTIME ?? null,
NODE_ENV: process.env.NODE_ENV ?? null,
},
};

try {
if (!diag.hasFs) {
return NextResponse.json(
{ ok: false, reason: "fs-unavailable", diag },
{ status: 500 },
);
}

// pick the first existing candidate
const docsRoot = candidates.find((p) => fs.existsSync(p));
if (!docsRoot) {
return NextResponse.json(
{
ok: false,
reason: "docs-root-not-found",
diag: {
...diag,
exists: Object.fromEntries(
candidates.map((p) => [p, fs.existsSync(p)]),
),
},
},
{ status: 500 },
);
}

// try to list
let tree: DirNode[] = [];
try {
tree = buildTree(docsRoot, 2);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return NextResponse.json(
{
ok: false,
reason: "buildTree-failed",
error: msg,
diag: { ...diag, docsRoot },
},
{ status: 500 },
);
}

return NextResponse.json({ ok: true, docsRoot, tree, diag });
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
return NextResponse.json(
{ ok: false, reason: "unhandled", error: msg, diag },
{ status: 500 },
);
}
}
4 changes: 2 additions & 2 deletions app/components/Community.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Button } from "./ui/button";
import { Card, CardContent } from "./ui/card";
import { Button } from "../../components/ui/button";
import { Card, CardContent } from "../../components/ui/card";
import {
ExternalLink,
MessageCircle,
Expand Down
59 changes: 59 additions & 0 deletions app/components/Contribute.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
@keyframes drift {
0% {
transform: translateX(-10%);
}
100% {
transform: translateX(110%);
}
}

@keyframes twinkle {
0% {
opacity: 0.35;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.15);
}
100% {
opacity: 0.35;
transform: scale(1);
}
}

.driftSlow {
animation: drift 42s linear infinite;
}

.driftFast {
animation: drift 26s linear infinite;
}

.twinkle {
animation: twinkle 2.8s ease-in-out infinite;
}

.twinkleDelay1 {
animation: twinkle 3.2s ease-in-out infinite;
animation-delay: 0.4s;
}

.twinkleDelay2 {
animation: twinkle 3.6s ease-in-out infinite;
animation-delay: 0.8s;
}

.twinkleDelay3 {
animation: twinkle 4s ease-in-out infinite;
animation-delay: 1.2s;
}

.textGlow {
/* 多层阴影增强对比度:深色描边 + 柔和外晕 */
text-shadow:
0 1px 2px rgba(0, 0, 0, 0.55),
0 2px 6px rgba(0, 0, 0, 0.35),
0 0 12px rgba(255, 255, 255, 0.12);
-webkit-text-stroke: 0.35px rgba(0, 0, 0, 0.35);
}
Loading