first commit
This commit is contained in:
246
app/apps/[id]/config/page.tsx
Normal file
246
app/apps/[id]/config/page.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
import { getAppConfigs, upsertConfig, deleteConfig } from "../../../config/actions";
|
||||
import { getAppById } from "../../actions";
|
||||
import { Save, Trash2, Smartphone, Globe, Plus, Code, ArrowLeft } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
export default async function AppConfigPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const appId = parseInt(id);
|
||||
|
||||
if (isNaN(appId)) notFound();
|
||||
|
||||
const app = await getAppById(appId);
|
||||
if (!app) notFound();
|
||||
|
||||
const configToken = process.env.CONFIG_API_TOKEN ?? "";
|
||||
const jsonUrl = `/api/config/${app.bundleId}?token=${configToken}`;
|
||||
|
||||
const configs = await getAppConfigs(appId);
|
||||
|
||||
// --- Server Actions ---
|
||||
|
||||
async function handleAddConfig(formData: FormData) {
|
||||
"use server";
|
||||
const key = formData.get("key") as string;
|
||||
const value = formData.get("value") as string;
|
||||
const type = formData.get("type") as string;
|
||||
|
||||
let parsedValue: any = value;
|
||||
if (type === "boolean") parsedValue = value === "true";
|
||||
if (type === "number") parsedValue = parseFloat(value);
|
||||
|
||||
await upsertConfig(appId, key, parsedValue);
|
||||
revalidatePath(`/apps/${appId}/config`);
|
||||
}
|
||||
|
||||
async function handleUpdateConfig(formData: FormData) {
|
||||
"use server";
|
||||
const key = formData.get("key") as string;
|
||||
const rawValue = formData.get("value") as string;
|
||||
|
||||
// Auto-detect type
|
||||
let parsedValue: any = rawValue;
|
||||
if (rawValue === "true") parsedValue = true;
|
||||
else if (rawValue === "false") parsedValue = false;
|
||||
else if (rawValue !== "" && !isNaN(Number(rawValue))) parsedValue = Number(rawValue);
|
||||
|
||||
await upsertConfig(appId, key, parsedValue);
|
||||
revalidatePath(`/apps/${appId}/config`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-slate-50 dark:bg-black font-sans text-slate-900 dark:text-slate-100">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-64 bg-white dark:bg-zinc-950 border-r border-slate-200 dark:border-zinc-900 flex flex-col shrink-0 shadow-sm">
|
||||
<div className="p-6 border-b border-slate-100 dark:border-zinc-900 flex items-center gap-3">
|
||||
<Link href="/" className="w-7 h-7 bg-slate-900 dark:bg-white rounded-md flex items-center justify-center">
|
||||
<span className="text-white dark:text-black font-bold text-sm">A</span>
|
||||
</Link>
|
||||
<span className="font-bold text-lg uppercase tracking-wider">AppAdmin</span>
|
||||
</div>
|
||||
<nav className="flex-1 p-4 space-y-1">
|
||||
<Link href="/" className="flex items-center gap-2.5 px-3 py-2 text-slate-600 dark:text-zinc-400 hover:bg-slate-50 dark:hover:bg-zinc-900 rounded-lg text-sm font-medium">
|
||||
<Globe size={18} /> Dashboard
|
||||
</Link>
|
||||
<Link href="/apps" className="flex items-center gap-2.5 px-3 py-2 bg-slate-900 dark:bg-white text-white dark:text-black rounded-lg text-sm font-semibold">
|
||||
<Smartphone size={18} /> Uygulamalar
|
||||
</Link>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{/* Main */}
|
||||
<main className="flex-1 overflow-y-auto p-8">
|
||||
<div className="max-w-5xl mx-auto space-y-8">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-slate-200 dark:border-zinc-900 pb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/apps" className="p-2 bg-white dark:bg-zinc-950 border border-slate-200 dark:border-zinc-800 rounded-lg text-slate-500 hover:text-slate-900 transition-colors">
|
||||
<ArrowLeft size={20} />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">{app.name} - Remote Config</h1>
|
||||
<p className="text-sm text-slate-500 font-mono opacity-70">{app.bundleId}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
href={jsonUrl}
|
||||
target="_blank"
|
||||
className="px-4 py-2 bg-slate-100 dark:bg-zinc-900 rounded-lg text-xs font-bold flex items-center gap-2 hover:bg-slate-200 transition-all border border-slate-200 dark:border-zinc-800"
|
||||
>
|
||||
<Code size={14} /> JSON Çıktısı
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* API Erişim Bilgisi */}
|
||||
<div className="bg-slate-900 dark:bg-zinc-900 rounded-xl p-5 space-y-3 border border-slate-800">
|
||||
<p className="text-[10px] font-bold uppercase tracking-widest text-slate-400">Mobil Uygulama Entegrasyonu</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-[10px] font-bold uppercase text-slate-500 w-16 shrink-0">URL</span>
|
||||
<code className="flex-1 text-xs text-emerald-400 font-mono bg-black/30 px-3 py-1.5 rounded-lg truncate select-all">
|
||||
{`/api/config/${app.bundleId}`}
|
||||
</code>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-[10px] font-bold uppercase text-slate-500 w-16 shrink-0">Token</span>
|
||||
<code className="flex-1 text-xs text-yellow-400 font-mono bg-black/30 px-3 py-1.5 rounded-lg truncate select-all">
|
||||
{configToken || "— CONFIG_API_TOKEN tanımlı değil —"}
|
||||
</code>
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-500 pl-20">
|
||||
{`Authorization: Bearer <token> ya da ?token=<token> query parametresi ile erişin.`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
|
||||
{/* Add New Config Panel */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="bg-white dark:bg-zinc-950 p-6 rounded-xl border border-slate-200 dark:border-zinc-900 shadow-sm sticky top-8">
|
||||
<h3 className="font-bold text-sm mb-4 flex items-center gap-2 text-slate-900 dark:text-white">
|
||||
<Plus size={16} className="text-blue-500" /> Yeni Ayar Ekle
|
||||
</h3>
|
||||
<form action={handleAddConfig} className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-bold uppercase text-slate-400">Anahtar (Key)</label>
|
||||
<input
|
||||
name="key"
|
||||
required
|
||||
placeholder="örn: maintenance_mode"
|
||||
className="w-full px-3 py-2 bg-slate-50 dark:bg-zinc-900 border border-slate-200 dark:border-zinc-800 rounded-lg text-sm font-mono text-slate-900 dark:text-white outline-none focus:ring-2 focus:ring-slate-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-bold uppercase text-slate-400">Değer (Value)</label>
|
||||
<input
|
||||
name="value"
|
||||
required
|
||||
placeholder="örn: true veya 1.0.0"
|
||||
className="w-full px-3 py-2 bg-slate-50 dark:bg-zinc-900 border border-slate-200 dark:border-zinc-800 rounded-lg text-sm text-slate-900 dark:text-white outline-none focus:ring-2 focus:ring-slate-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-bold uppercase text-slate-400">Tip</label>
|
||||
<select
|
||||
name="type"
|
||||
className="w-full px-3 py-2 bg-slate-50 dark:bg-zinc-900 border border-slate-200 dark:border-zinc-800 rounded-lg text-sm text-slate-900 dark:text-white outline-none"
|
||||
>
|
||||
<option value="string">String</option>
|
||||
<option value="boolean">Boolean</option>
|
||||
<option value="number">Number</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full py-2.5 bg-slate-900 dark:bg-white text-white dark:text-black rounded-lg text-sm font-bold hover:opacity-90 transition-opacity"
|
||||
>
|
||||
Kaydet
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Config Table */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-white dark:bg-zinc-950 rounded-xl border border-slate-200 dark:border-zinc-900 shadow-sm overflow-hidden">
|
||||
<table className="w-full text-left">
|
||||
<thead className="bg-slate-50 dark:bg-zinc-900 border-b border-slate-200 dark:border-zinc-800 text-[10px] font-bold uppercase tracking-widest text-slate-400">
|
||||
<tr>
|
||||
<th className="px-5 py-4 w-2/5">Key</th>
|
||||
<th className="px-5 py-4 w-3/5">Value</th>
|
||||
<th className="px-5 py-4 text-right"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100 dark:divide-zinc-900">
|
||||
{configs.map((config) => (
|
||||
<tr
|
||||
key={config.id}
|
||||
className="hover:bg-slate-50/70 dark:hover:bg-zinc-900/50 transition-colors group"
|
||||
>
|
||||
{/* Key — read only */}
|
||||
<td className="px-5 py-3 font-mono text-xs font-semibold text-slate-600 dark:text-zinc-400 align-middle">
|
||||
{config.configKey}
|
||||
</td>
|
||||
|
||||
{/* Value — inline editable */}
|
||||
<td className="px-3 py-2 align-middle">
|
||||
<form action={handleUpdateConfig} className="flex items-center gap-1.5">
|
||||
<input type="hidden" name="key" value={config.configKey} />
|
||||
<input
|
||||
name="value"
|
||||
defaultValue={String(config.configValue)}
|
||||
className="w-full px-3 py-1.5 rounded-lg text-sm font-mono text-slate-900 dark:text-white bg-slate-50 dark:bg-zinc-900 border border-slate-200 dark:border-zinc-800 outline-none focus:ring-2 focus:ring-slate-400 dark:focus:ring-zinc-600 transition-all"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
title="Değeri güncelle"
|
||||
className="shrink-0 w-7 h-7 flex items-center justify-center rounded-lg text-slate-300 dark:text-zinc-600 hover:text-emerald-600 dark:hover:text-emerald-400 hover:bg-emerald-50 dark:hover:bg-emerald-950/50 transition-all opacity-0 group-hover:opacity-100 focus:opacity-100"
|
||||
>
|
||||
<Save size={14} />
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
|
||||
{/* Delete */}
|
||||
<td className="px-5 py-3 text-right align-middle">
|
||||
<form
|
||||
action={async () => {
|
||||
"use server";
|
||||
await deleteConfig(config.id);
|
||||
revalidatePath(`/apps/${appId}/config`);
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
title="Sil"
|
||||
className="p-1.5 rounded-lg text-slate-300 dark:text-zinc-700 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-950/40 transition-all"
|
||||
>
|
||||
<Trash2 size={15} />
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
{configs.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={3} className="px-6 py-12 text-center text-slate-400 italic text-sm">
|
||||
Henüz bir ayar eklenmemiş.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user