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
64 changes: 64 additions & 0 deletions app/admin/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
Share2,
MapPin,
Image as ImageIcon,
EyeOff,
} from "lucide-react";
import Image from "next/image";
import Link from "next/link";
Expand Down Expand Up @@ -580,6 +581,69 @@ export default function AdminSettingsPage() {
</div>
</div>

{/* Coming Soon — landing CTA */}
<div className="flex items-start gap-4 p-4 bg-gray-50 dark:bg-gray-700 rounded-xl">
<div className="w-10 h-10 rounded-lg bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center flex-shrink-0">
<EyeOff className="w-5 h-5 text-amber-600" />
</div>
<div className="flex-1 space-y-4">
<div className="flex items-start justify-between gap-3">
<div>
<h3 className="font-medium text-gray-900 dark:text-white">
Coming Soon (หน้า Landing)
</h3>
<p className="text-sm text-gray-500 mt-1">
เมื่อเปิด ปุ่มเข้าสู่ระบบบนหน้าแรกจะถูกปิด และแสดงข้อความแทน
</p>
</div>
<button
type="button"
onClick={() =>
setSettings({
...settings,
comingSoonEnabled: !settings.comingSoonEnabled,
})
}
className={cn(
"w-14 h-8 rounded-full transition-colors relative flex-shrink-0",
settings.comingSoonEnabled
? "bg-line-green"
: "bg-gray-300 dark:bg-gray-600"
)}
aria-pressed={Boolean(settings.comingSoonEnabled)}
aria-label="สลับ Coming Soon"
>
<span
className={cn(
"absolute top-1 w-6 h-6 rounded-full bg-white shadow transition-transform",
settings.comingSoonEnabled ? "right-1" : "left-1"
)}
/>
</button>
</div>

{settings.comingSoonEnabled ? (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
ข้อความที่แสดง
</label>
<input
type="text"
value={settings.comingSoonMessage || ""}
onChange={(e) =>
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"
/>
</div>
) : null}
</div>
</div>

{/* Map & GPS — managed on dedicated page */}
<Link
href="/admin/maps"
Expand Down
2 changes: 1 addition & 1 deletion app/api/public-settings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export async function GET() {
} catch (err) {
console.error("public-settings error:", err);
return NextResponse.json({
comingSoonEnabled: true,
comingSoonEnabled: false,
comingSoonMessage: "พบกันเร็วๆนี้",
});
}
Expand Down
25 changes: 19 additions & 6 deletions lib/blog/data.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createAdminClient } from "@/lib/supabase/admin";
import { hasSupabaseAdminEnv } from "@/lib/setup/db-url";
import { mapArticle } from "@/lib/blog/map";
import { isMissingRelationError } from "@/lib/supabase/missing-relation";
import type { Article, ArticleSection } from "@/lib/blog/types";

export { mapArticle } from "@/lib/blog/map";
Expand Down Expand Up @@ -28,12 +29,16 @@ export async function listPublishedArticles(options?: {

const { data, error } = await query;
if (error) {
console.error("[blog] listPublishedArticles:", error);
if (!isMissingRelationError(error)) {
console.error("[blog] listPublishedArticles:", error);
}
return [];
}
return (data ?? []).map((row) => mapArticle(row as Record<string, unknown>));
} catch (error) {
console.error("[blog] listPublishedArticles:", error);
if (!isMissingRelationError(error)) {
console.error("[blog] listPublishedArticles:", error);
}
return [];
}
}
Expand All @@ -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<string, unknown>);
} catch (error) {
console.error("[blog] getPublishedArticleBySlug:", error);
if (!isMissingRelationError(error)) {
console.error("[blog] getPublishedArticleBySlug:", error);
}
return null;
}
}
Expand All @@ -81,13 +90,17 @@ export async function getArticleById(id: string): Promise<Article | null> {
.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<string, unknown>);
} catch (error) {
console.error("[blog] getArticleById:", error);
if (!isMissingRelationError(error)) {
console.error("[blog] getArticleById:", error);
}
return null;
}
}
25 changes: 19 additions & 6 deletions lib/help/data.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -110,8 +117,12 @@ export async function listHelpPages(): Promise<HelpPage[]> {
.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<string, unknown>)
Expand All @@ -130,7 +141,9 @@ export async function listHelpPages(): Promise<HelpPage[]> {

return [...legacy, ...fromArticles];
} catch (error) {
console.error("[help] listHelpPages:", error);
if (!isMissingRelationError(error)) {
console.error("[help] listHelpPages:", error);
}
return [];
}
}
2 changes: 1 addition & 1 deletion lib/landing-public-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export async function getPublicLandingSettings(): Promise<PublicLandingSettings>
};
} catch {
return {
comingSoonEnabled: DEFAULT_APP_SETTINGS.comingSoonEnabled ?? true,
comingSoonEnabled: DEFAULT_APP_SETTINGS.comingSoonEnabled ?? false,
comingSoonMessage: normalizeComingSoonMessage(
DEFAULT_APP_SETTINGS.comingSoonMessage,
"พบกันเร็วๆนี้"
Expand Down
48 changes: 48 additions & 0 deletions lib/setup/ensure-content-cms.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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<void> {
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 });
}
}
4 changes: 4 additions & 0 deletions lib/setup/hydrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -44,6 +45,7 @@ export async function hydrateDatabase(): Promise<HydrationResult> {

if (state.hasSystemConfig && state.hasLostItems) {
await ensureAccountsTableWithSql(sql);
await ensureContentCmsWithSql(sql);
await backfillSetupStatusIfNeeded(sql);
await ensureStorageBucketsWithSql(sql);
return { ok: true, mode: "skipped" };
Expand All @@ -56,6 +58,7 @@ export async function hydrateDatabase(): Promise<HydrationResult> {
}
await runSqlBatch(sql, systemConfigSql);
await ensureAccountsTableWithSql(sql);
await ensureContentCmsWithSql(sql);
await backfillSetupStatusIfNeeded(sql);
await ensureStorageBucketsWithSql(sql);
return { ok: true, mode: "system_config_only" };
Expand All @@ -71,6 +74,7 @@ export async function hydrateDatabase(): Promise<HydrationResult> {
}

await ensureAccountsTableWithSql(sql);
await ensureContentCmsWithSql(sql);
await ensureStorageBucketsWithSql(sql);
await backfillSetupStatusIfNeeded(sql);

Expand Down
14 changes: 14 additions & 0 deletions lib/setup/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
11 changes: 11 additions & 0 deletions lib/supabase/missing-relation.ts
Original file line number Diff line number Diff line change
@@ -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")
);
}
2 changes: 1 addition & 1 deletion lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export const DEFAULT_APP_SETTINGS: AppSettings = {
compressionQuality: 0.8,
nfcEnabled: true,
nfcRequireLoginToReport: true,
comingSoonEnabled: true,
comingSoonEnabled: false,
comingSoonMessage: "พบกันเร็วๆนี้",
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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');;
Original file line number Diff line number Diff line change
@@ -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';