diff --git a/.gitignore b/.gitignore
index e13325f..7f523b9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,9 @@ dist/
.DS_Store
.tmp/
build/
+
+# Game servers (cloned repos / large data)
+servers/maple2/
+servers/openfusion/fusion
+servers/openfusion/tdata/
+servers/openfusion/data/
diff --git a/apps/cms/Dockerfile b/apps/cms/Dockerfile
index 3765b7d..62740b8 100644
--- a/apps/cms/Dockerfile
+++ b/apps/cms/Dockerfile
@@ -2,9 +2,12 @@ FROM node:20-alpine AS base
WORKDIR /app
COPY package.json package-lock.json* ./
-RUN npm ci
+RUN npm install
COPY . .
-RUN npm run build
+RUN npm run build && \
+ find src/api -name "schema.json" | while read f; do \
+ mkdir -p "dist/$(dirname "$f")" && cp "$f" "dist/$f"; \
+ done
FROM node:20-alpine AS production
WORKDIR /app
diff --git a/apps/cms/package.json b/apps/cms/package.json
index 3ecb108..ca2ecd9 100644
--- a/apps/cms/package.json
+++ b/apps/cms/package.json
@@ -15,10 +15,15 @@
"@strapi/plugin-cloud": "^5.36.0",
"@strapi/plugin-users-permissions": "^5.36.0",
"pg": "^8.13.0",
- "better-sqlite3": "^11.0.0"
+ "better-sqlite3": "^11.0.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "react-router-dom": "^6.0.0",
+ "styled-components": "^6.0.0"
},
"devDependencies": {
- "typescript": "^5.3.0"
+ "typescript": "^5.3.0",
+ "esbuild": "^0.25.0"
},
"engines": {
"node": ">=20.0.0 <=24.x.x",
diff --git a/apps/cms/src/api/documentary/content-types/documentary/schema.json b/apps/cms/src/api/documentary/content-types/documentary/schema.json
index 1eeca44..f0a1981 100644
--- a/apps/cms/src/api/documentary/content-types/documentary/schema.json
+++ b/apps/cms/src/api/documentary/content-types/documentary/schema.json
@@ -40,9 +40,6 @@
"relation": "oneToMany",
"target": "api::chapter.chapter",
"mappedBy": "documentary"
- },
- "publishedAt": {
- "type": "datetime"
}
}
}
diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile
index e2c773d..fb8cea7 100644
--- a/apps/web/Dockerfile
+++ b/apps/web/Dockerfile
@@ -14,13 +14,14 @@ WORKDIR /app/apps/web
RUN npm run build
FROM node:20-alpine AS production
-WORKDIR /app/apps/web
-COPY --from=base /app/apps/web/.next ./.next
-COPY --from=base /app/apps/web/public ./public
-COPY --from=base /app/apps/web/package.json ./
-COPY --from=base /app/apps/web/node_modules ./node_modules
-COPY --from=base /app/node_modules /app/node_modules
-COPY --from=base /app/packages /app/packages
+WORKDIR /app
+COPY --from=base /app/package.json ./
+COPY --from=base /app/node_modules ./node_modules
+COPY --from=base /app/packages ./packages
+COPY --from=base /app/apps/web/.next ./apps/web/.next
+COPY --from=base /app/apps/web/package.json ./apps/web/
+COPY --from=base /app/apps/web/next.config.ts ./apps/web/
+WORKDIR /app/apps/web
EXPOSE 3000
CMD ["npm", "start"]
diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts
index 86e7332..67d4bf9 100644
--- a/apps/web/next.config.ts
+++ b/apps/web/next.config.ts
@@ -2,6 +2,10 @@ import createNextIntlPlugin from "next-intl/plugin";
const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");
-const nextConfig = {};
+const nextConfig = {
+ eslint: {
+ ignoreDuringBuilds: true,
+ },
+};
export default withNextIntl(nextConfig);
diff --git a/apps/web/src/app/[locale]/games/[slug]/page.tsx b/apps/web/src/app/[locale]/games/[slug]/page.tsx
index fd4f388..8ad6b19 100644
--- a/apps/web/src/app/[locale]/games/[slug]/page.tsx
+++ b/apps/web/src/app/[locale]/games/[slug]/page.tsx
@@ -24,7 +24,7 @@ export default async function GamePage({
return (
<>
-
+
{game.screenshots && (
diff --git a/apps/web/src/app/[locale]/layout.tsx b/apps/web/src/app/[locale]/layout.tsx
index 41b0e5c..da1d110 100644
--- a/apps/web/src/app/[locale]/layout.tsx
+++ b/apps/web/src/app/[locale]/layout.tsx
@@ -1,11 +1,29 @@
import type { Metadata } from "next";
+import { Playfair_Display, Source_Serif_4, DM_Sans } from "next/font/google";
import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";
import { notFound } from "next/navigation";
import { routing } from "@/i18n/routing";
import { Navbar } from "@/components/layout/Navbar";
import { Footer } from "@/components/layout/Footer";
-import "./globals.css";
+
+const playfair = Playfair_Display({
+ subsets: ["latin"],
+ variable: "--font-playfair",
+ display: "swap",
+});
+
+const sourceSerif = Source_Serif_4({
+ subsets: ["latin", "latin-ext"],
+ variable: "--font-source-serif",
+ display: "swap",
+});
+
+const dmSans = DM_Sans({
+ subsets: ["latin"],
+ variable: "--font-dm-sans",
+ display: "swap",
+});
export const metadata: Metadata = {
title: {
@@ -30,8 +48,11 @@ export default async function LocaleLayout({
const messages = await getMessages();
return (
-
-
+
+
{children}
diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css
new file mode 100644
index 0000000..99512da
--- /dev/null
+++ b/apps/web/src/app/globals.css
@@ -0,0 +1,54 @@
+@import "tailwindcss";
+
+@theme {
+ --font-sans: var(--font-dm-sans), system-ui, sans-serif;
+ --font-display: var(--font-playfair), Georgia, serif;
+ --font-body: var(--font-source-serif), Georgia, serif;
+}
+
+/* ── Editorial prose — game descriptions ────────────────── */
+
+.prose-editorial p {
+ font-family: var(--font-body);
+ font-size: 1.125rem;
+ line-height: 1.85;
+ color: #d1d5db;
+ margin-bottom: 1.5em;
+}
+
+.prose-editorial p:last-child {
+ margin-bottom: 0;
+}
+
+/* ── Chapter reading experience ─────────────────────────── */
+
+.chapter-prose p {
+ font-family: var(--font-body);
+ font-size: 1.1875rem;
+ line-height: 1.9;
+ color: #e5e7eb;
+ margin-bottom: 1.75em;
+ letter-spacing: 0.005em;
+}
+
+.chapter-prose > p:first-of-type::first-letter {
+ float: left;
+ font-family: var(--font-display);
+ font-size: 3.5rem;
+ line-height: 1;
+ padding-right: 0.5rem;
+ margin-top: 0.1rem;
+ font-weight: 700;
+ color: #f59e0b;
+}
+
+.chapter-prose p:last-child {
+ margin-bottom: 0;
+}
+
+/* ── Em-dash and quotation styling inside prose ─────────── */
+
+.chapter-prose p em {
+ font-style: italic;
+ color: #fbbf24;
+}
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
new file mode 100644
index 0000000..f9334d3
--- /dev/null
+++ b/apps/web/src/app/layout.tsx
@@ -0,0 +1,9 @@
+import "./globals.css";
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return children;
+}
diff --git a/apps/web/src/app/not-found.tsx b/apps/web/src/app/not-found.tsx
new file mode 100644
index 0000000..d8d0f6e
--- /dev/null
+++ b/apps/web/src/app/not-found.tsx
@@ -0,0 +1,13 @@
+export default function RootNotFound() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/documentary/ChapterContent.tsx b/apps/web/src/components/documentary/ChapterContent.tsx
index 472c967..dfd8128 100644
--- a/apps/web/src/components/documentary/ChapterContent.tsx
+++ b/apps/web/src/components/documentary/ChapterContent.tsx
@@ -1,5 +1,6 @@
import Image from "next/image";
import type { Chapter } from "@afterlife/shared";
+import { formatTextToHtml } from "@/lib/format";
interface ChapterContentProps {
chapter: Chapter;
@@ -7,9 +8,22 @@ interface ChapterContentProps {
export function ChapterContent({ chapter }: ChapterContentProps) {
return (
-
+
+ {/* Chapter indicator */}
+
+
+
+ {String(chapter.order).padStart(2, "0")}
+
+
+
+
+ {chapter.title}
+
+
+
{chapter.coverImage && (
-
+
)}
-
{chapter.title}
+
);
diff --git a/apps/web/src/components/documentary/ChapterNav.tsx b/apps/web/src/components/documentary/ChapterNav.tsx
index 827dc34..15b4b42 100644
--- a/apps/web/src/components/documentary/ChapterNav.tsx
+++ b/apps/web/src/components/documentary/ChapterNav.tsx
@@ -9,27 +9,39 @@ interface ChapterNavProps {
onSelectChapter: (id: number, index: number) => void;
}
-export function ChapterNav({ chapters, activeChapterId, onSelectChapter }: ChapterNavProps) {
+export function ChapterNav({
+ chapters,
+ activeChapterId,
+ onSelectChapter,
+}: ChapterNavProps) {
const t = useTranslations("documentary");
return (
-