diff --git a/app/admin/settings/page.tsx b/app/admin/settings/page.tsx index 3deeef2..3c87329 100644 --- a/app/admin/settings/page.tsx +++ b/app/admin/settings/page.tsx @@ -20,6 +20,7 @@ import { Share2, MapPin, Image as ImageIcon, + EyeOff, } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; @@ -580,6 +581,69 @@ export default function AdminSettingsPage() { + {/* Coming Soon — landing CTA */} +
+
+ +
+
+
+
+

+ Coming Soon (หน้า Landing) +

+

+ เมื่อเปิด ปุ่มเข้าสู่ระบบบนหน้าแรกจะถูกปิด และแสดงข้อความแทน +

+
+ +
+ + {settings.comingSoonEnabled ? ( +
+ + + setSettings({ + ...settings, + comingSoonMessage: e.target.value, + }) + } + placeholder="พบกันเร็วๆนี้" + className="w-full px-4 py-2 bg-white dark:bg-gray-600 border border-gray-200 dark:border-gray-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-line-green" + /> +
+ ) : null} +
+
+ {/* Map & GPS — managed on dedicated page */} mapArticle(row as Record)); } catch (error) { - console.error("[blog] listPublishedArticles:", error); + if (!isMissingRelationError(error)) { + console.error("[blog] listPublishedArticles:", error); + } return []; } } @@ -58,13 +63,17 @@ export async function getPublishedArticleBySlug( const { data, error } = await query.maybeSingle(); if (error) { - console.error("[blog] getPublishedArticleBySlug:", error); + if (!isMissingRelationError(error)) { + console.error("[blog] getPublishedArticleBySlug:", error); + } return null; } if (!data) return null; return mapArticle(data as Record); } catch (error) { - console.error("[blog] getPublishedArticleBySlug:", error); + if (!isMissingRelationError(error)) { + console.error("[blog] getPublishedArticleBySlug:", error); + } return null; } } @@ -81,13 +90,17 @@ export async function getArticleById(id: string): Promise
{ .eq("id", id) .maybeSingle(); if (error) { - console.error("[blog] getArticleById:", error); + if (!isMissingRelationError(error)) { + console.error("[blog] getArticleById:", error); + } return null; } if (!data) return null; return mapArticle(data as Record); } catch (error) { - console.error("[blog] getArticleById:", error); + if (!isMissingRelationError(error)) { + console.error("[blog] getArticleById:", error); + } return null; } } diff --git a/lib/help/data.ts b/lib/help/data.ts index 849c42d..4e04497 100644 --- a/lib/help/data.ts +++ b/lib/help/data.ts @@ -1,6 +1,7 @@ import { createAdminClient } from "@/lib/supabase/admin"; import { hasSupabaseAdminEnv } from "@/lib/setup/db-url"; import { getPublishedArticleBySlug } from "@/lib/blog/data"; +import { isMissingRelationError } from "@/lib/supabase/missing-relation"; import type { Article } from "@/lib/blog/types"; import type { HelpAudience, @@ -61,8 +62,12 @@ export async function getHelpPageWithSections( .order("sort_order", { ascending: true }), ]); - if (pageError) console.error("[help] page fetch error:", pageError); - if (sectionError) console.error("[help] sections fetch error:", sectionError); + if (pageError && !isMissingRelationError(pageError)) { + console.error("[help] page fetch error:", pageError); + } + if (sectionError && !isMissingRelationError(sectionError)) { + console.error("[help] sections fetch error:", sectionError); + } if (pageError || sectionError || !pageRow) return null; return { @@ -72,7 +77,9 @@ export async function getHelpPageWithSections( ), }; } catch (error) { - console.error("[help] getHelpPageWithSections:", error); + if (!isMissingRelationError(error)) { + console.error("[help] getHelpPageWithSections:", error); + } return null; } } @@ -110,8 +117,12 @@ export async function listHelpPages(): Promise { .order("published_at", { ascending: false }), ]); - if (legacyError) console.error("[help] listHelpPages legacy:", legacyError); - if (articleError) console.error("[help] listHelpPages articles:", articleError); + if (legacyError && !isMissingRelationError(legacyError)) { + console.error("[help] listHelpPages legacy:", legacyError); + } + if (articleError && !isMissingRelationError(articleError)) { + console.error("[help] listHelpPages articles:", articleError); + } const legacy = (legacyRows ?? []).map((row) => mapPage(row as Record) @@ -130,7 +141,9 @@ export async function listHelpPages(): Promise { return [...legacy, ...fromArticles]; } catch (error) { - console.error("[help] listHelpPages:", error); + if (!isMissingRelationError(error)) { + console.error("[help] listHelpPages:", error); + } return []; } } diff --git a/lib/landing-public-data.ts b/lib/landing-public-data.ts index f3f6550..5ae6559 100644 --- a/lib/landing-public-data.ts +++ b/lib/landing-public-data.ts @@ -88,7 +88,7 @@ export async function getPublicLandingSettings(): Promise }; } catch { return { - comingSoonEnabled: DEFAULT_APP_SETTINGS.comingSoonEnabled ?? true, + comingSoonEnabled: DEFAULT_APP_SETTINGS.comingSoonEnabled ?? false, comingSoonMessage: normalizeComingSoonMessage( DEFAULT_APP_SETTINGS.comingSoonMessage, "พบกันเร็วๆนี้" diff --git a/lib/setup/ensure-content-cms.ts b/lib/setup/ensure-content-cms.ts new file mode 100644 index 0000000..f8795c3 --- /dev/null +++ b/lib/setup/ensure-content-cms.ts @@ -0,0 +1,48 @@ +import postgres from "postgres"; +import { resolvePostgresUrl } from "@/lib/setup/db-url"; +import { tableExists } from "@/lib/setup/probe"; +import { + loadArticlesMigrationSql, + loadHelpPagesMigrationSql, +} from "@/lib/setup/schemas"; + +/** + * Applies help_pages + articles CMS migrations when missing. + * Needed because hydrateDatabase() often returns early in "skipped" mode + * after lost_items + system_config already exist (Vercel/Supabase new school). + */ +export async function ensureContentCmsWithSql(sql: postgres.Sql): Promise { + const hasHelp = await tableExists(sql, "public", "help_pages"); + if (!hasHelp) { + const helpSql = loadHelpPagesMigrationSql(); + if (helpSql) { + await sql.unsafe(helpSql); + } + } + + const hasArticles = await tableExists(sql, "public", "articles"); + if (!hasArticles) { + const articlesSql = loadArticlesMigrationSql(); + if (articlesSql) { + await sql.unsafe(articlesSql); + } + } +} + +export async function ensureContentCms(): Promise { + const connectionString = resolvePostgresUrl(); + if (!connectionString) return; + + const sql = postgres(connectionString, { + max: 1, + idle_timeout: 5, + connect_timeout: 15, + prepare: false, + }); + + try { + await ensureContentCmsWithSql(sql); + } finally { + await sql.end({ timeout: 5 }); + } +} diff --git a/lib/setup/hydrator.ts b/lib/setup/hydrator.ts index 58e4a88..a257fd2 100644 --- a/lib/setup/hydrator.ts +++ b/lib/setup/hydrator.ts @@ -4,6 +4,7 @@ import { RESET_FALSE_SETUP_WITHOUT_ADMIN_SQL, } from "@/lib/setup/backfill-sql"; import { ensureAccountsTableWithSql } from "@/lib/setup/ensure-accounts-table"; +import { ensureContentCmsWithSql } from "@/lib/setup/ensure-content-cms"; import { ensureStorageBucketsWithSql } from "@/lib/setup/ensure-storage-buckets"; import { SETUP_ADVISORY_LOCK_ID } from "@/lib/setup/constants"; import { resolvePostgresUrl } from "@/lib/setup/db-url"; @@ -44,6 +45,7 @@ export async function hydrateDatabase(): Promise { if (state.hasSystemConfig && state.hasLostItems) { await ensureAccountsTableWithSql(sql); + await ensureContentCmsWithSql(sql); await backfillSetupStatusIfNeeded(sql); await ensureStorageBucketsWithSql(sql); return { ok: true, mode: "skipped" }; @@ -56,6 +58,7 @@ export async function hydrateDatabase(): Promise { } await runSqlBatch(sql, systemConfigSql); await ensureAccountsTableWithSql(sql); + await ensureContentCmsWithSql(sql); await backfillSetupStatusIfNeeded(sql); await ensureStorageBucketsWithSql(sql); return { ok: true, mode: "system_config_only" }; @@ -71,6 +74,7 @@ export async function hydrateDatabase(): Promise { } await ensureAccountsTableWithSql(sql); + await ensureContentCmsWithSql(sql); await ensureStorageBucketsWithSql(sql); await backfillSetupStatusIfNeeded(sql); diff --git a/lib/setup/schemas/index.ts b/lib/setup/schemas/index.ts index 5239f82..6e31174 100644 --- a/lib/setup/schemas/index.ts +++ b/lib/setup/schemas/index.ts @@ -40,3 +40,17 @@ export function loadAccountsMigrationSql(): string | null { ); return file ? readMigrationSql(file) : null; } + +export function loadHelpPagesMigrationSql(): string | null { + const file = listMigrationFiles().find((name) => + name.includes("help_pages_cms") + ); + return file ? readMigrationSql(file) : null; +} + +export function loadArticlesMigrationSql(): string | null { + const file = listMigrationFiles().find((name) => + name.includes("articles_blog_cms") + ); + return file ? readMigrationSql(file) : null; +} diff --git a/lib/supabase/missing-relation.ts b/lib/supabase/missing-relation.ts new file mode 100644 index 0000000..9a18f01 --- /dev/null +++ b/lib/supabase/missing-relation.ts @@ -0,0 +1,11 @@ +/** PostgREST / Supabase error when a table is not in the schema yet */ +export function isMissingRelationError(error: unknown): boolean { + if (!error || typeof error !== "object") return false; + const e = error as { code?: string; message?: string }; + if (e.code === "PGRST205" || e.code === "42P01") return true; + const message = String(e.message ?? ""); + return ( + message.includes("Could not find the table") || + message.includes("does not exist") + ); +} diff --git a/lib/types.ts b/lib/types.ts index 04fbe85..d1b0b3f 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -163,7 +163,7 @@ export const DEFAULT_APP_SETTINGS: AppSettings = { compressionQuality: 0.8, nfcEnabled: true, nfcRequireLoginToReport: true, - comingSoonEnabled: true, + comingSoonEnabled: false, comingSoonMessage: "พบกันเร็วๆนี้", }; diff --git a/supabase/migrations/20260613193519_seed_coming_soon_and_admin_flag.sql b/supabase/migrations/20260613193519_seed_coming_soon_and_admin_flag.sql index befafaf..8238d71 100644 --- a/supabase/migrations/20260613193519_seed_coming_soon_and_admin_flag.sql +++ b/supabase/migrations/20260613193519_seed_coming_soon_and_admin_flag.sql @@ -1,7 +1,7 @@ --- Enable coming soon on landing page +-- Coming soon is off by default (admins can enable from Settings) UPDATE public.app_settings SET settings = COALESCE(settings, '{}'::jsonb) || jsonb_build_object( - 'comingSoonEnabled', true, + 'comingSoonEnabled', false, 'comingSoonMessage', 'พบกันเร็วๆนี้' ), updated_at = now() @@ -10,7 +10,7 @@ WHERE id = 'default'; -- Ensure default row exists if missing INSERT INTO public.app_settings (id, settings, updated_at) SELECT 'default', jsonb_build_object( - 'comingSoonEnabled', true, + 'comingSoonEnabled', false, 'comingSoonMessage', 'พบกันเร็วๆนี้' ), now() WHERE NOT EXISTS (SELECT 1 FROM public.app_settings WHERE id = 'default');; diff --git a/supabase/migrations/20260718010000_coming_soon_default_off.sql b/supabase/migrations/20260718010000_coming_soon_default_off.sql new file mode 100644 index 0000000..0d095b6 --- /dev/null +++ b/supabase/migrations/20260718010000_coming_soon_default_off.sql @@ -0,0 +1,8 @@ +-- Flip coming-soon off for existing installs (was seeded true). +-- Admins can re-enable from /admin/settings. +UPDATE public.app_settings +SET settings = COALESCE(settings, '{}'::jsonb) || jsonb_build_object( + 'comingSoonEnabled', false +), +updated_at = now() +WHERE id = 'default';