first commit
ci / build (push) Failing after 23s
deploy / deploy (push) Failing after 10m12s

This commit is contained in:
soroush.asadi
2026-05-31 12:47:02 +03:30
commit add78d8460
100 changed files with 15221 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
import 'server-only';
import Database from 'better-sqlite3';
import { existsSync, mkdirSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
/**
* Persistent content store for the CMS.
*
* A single `sections` table holds JSON overrides keyed by section name
* (e.g. "hero", "services", "portfolio"). The stored JSON is a bilingual
* payload — `{ fa: <sectionObject>, en: <sectionObject> }` — that mirrors the
* shape of the matching key inside `dict`. At request time the content loader
* merges these overrides on top of the in-code `dict` defaults, so editing a
* section in the admin panel transparently rewrites what every public section
* renders without touching any component.
*
* The database file lives under DATA_DIR (default ./data) which on the
* self-hosted deployment is a mounted Docker volume, so content survives
* container rebuilds.
*/
export const DATA_DIR = resolve(process.env.DATA_DIR ?? join(process.cwd(), 'data'));
export const UPLOADS_DIR = join(DATA_DIR, 'uploads');
const DB_PATH = join(DATA_DIR, 'cms.db');
let _db: Database.Database | null = null;
function db(): Database.Database {
if (_db) return _db;
if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
if (!existsSync(UPLOADS_DIR)) mkdirSync(UPLOADS_DIR, { recursive: true });
const handle = new Database(DB_PATH);
handle.pragma('journal_mode = WAL');
handle.exec(`
CREATE TABLE IF NOT EXISTS sections (
key TEXT PRIMARY KEY,
data TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
`);
_db = handle;
return handle;
}
export type SectionRow = {
key: string;
/** JSON-encoded `{ fa, en }` payload. */
data: string;
updated_at: number;
};
export type SectionOverride = {
key: string;
data: unknown;
updatedAt: number;
};
/** Every stored override, used by the content loader to merge onto defaults. */
export function getAllSections(): SectionRow[] {
try {
return db()
.prepare('SELECT key, data, updated_at FROM sections')
.all() as SectionRow[];
} catch {
// A missing/locked DB must never crash a public render — fall back to dict.
return [];
}
}
/** A single override, or null when the section has never been edited. */
export function getSection(key: string): SectionOverride | null {
const row = db()
.prepare('SELECT key, data, updated_at FROM sections WHERE key = ?')
.get(key) as SectionRow | undefined;
if (!row) return null;
return { key: row.key, data: JSON.parse(row.data), updatedAt: row.updated_at };
}
/** Insert or replace a section override (admin only). */
export function setSection(key: string, data: unknown): void {
db()
.prepare(
`INSERT INTO sections (key, data, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(key) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`,
)
.run(key, JSON.stringify(data), Date.now());
}
/** Drop an override so the section reverts to its in-code default. */
export function resetSection(key: string): void {
db().prepare('DELETE FROM sections WHERE key = ?').run(key);
}
/** Map of key → updatedAt for showing edit status in the dashboard. */
export function sectionStatus(): Record<string, number> {
const out: Record<string, number> = {};
for (const row of getAllSections()) out[row.key] = row.updated_at;
return out;
}
export { dirname };