Skip to content

Commit 648b574

Browse files
committed
Added pop up modal and edit functions for promote modal
1 parent 110310d commit 648b574

8 files changed

Lines changed: 635 additions & 1 deletion

File tree

‎apps/codebility/app/home/page.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import H1 from "@/components/shared/dashboard/H1";
33
import PageContainer from "./_components/PageContainer";
44
import DashboardClient from "./_components/DashboardClient";
55
import NewsBanner from "./_components/NewsBanner";
6+
import { fetchActiveModal } from "./promote-modal/actions";
7+
import FeaturePromoModal from "./promote-modal/_components/FeaturePromoModal";
68

79
// Loading component for better UX
810
function DashboardLoading() {
@@ -26,9 +28,13 @@ function DashboardLoading() {
2628

2729
export const revalidate = 3600; // Revalidate every hour for fresh data
2830

29-
export default function DashboardPage() {
31+
export default async function DashboardPage() {
32+
const modal = await fetchActiveModal();
33+
3034
return (
3135
<div className="w-full">
36+
{modal && <FeaturePromoModal data={modal} />}
37+
3238
<div className="flex flex-col gap-4 pt-4 relative mb-8">
3339
{/* Background decorations - contained within content area */}
3440
<div className="absolute inset-0 overflow-hidden pointer-events-none">
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import * as Icons from "lucide-react";
5+
import { FeatureModal, ModalFeature } from "../type";
6+
import { upsertActiveModal } from "../actions";
7+
import IconPicker from "./IconPicker";
8+
9+
interface EditPromoModalProps {
10+
data: FeatureModal | null;
11+
}
12+
13+
export default function EditPromoModal({ data }: EditPromoModalProps) {
14+
const [form, setForm] = useState<Partial<FeatureModal>>({
15+
id: data?.id,
16+
badge: data?.badge ?? "",
17+
headline: data?.headline ?? "",
18+
subheadline: data?.subheadline ?? "",
19+
cta_label: data?.cta_label ?? "",
20+
cta_href: data?.cta_href ?? "",
21+
dismiss_label: data?.dismiss_label ?? "Maybe later",
22+
features: data?.features ?? [],
23+
is_active: data?.is_active ?? true,
24+
});
25+
const [saving, setSaving] = useState(false);
26+
const [message, setMessage] = useState<string | null>(null);
27+
28+
function handleChange(field: keyof FeatureModal, value: unknown) {
29+
setForm((prev) => ({ ...prev, [field]: value }));
30+
}
31+
32+
function handleFeatureChange(
33+
index: number,
34+
field: keyof ModalFeature,
35+
value: string
36+
) {
37+
const updated = (form.features ?? []).map((f, i) =>
38+
i === index ? { ...f, [field]: value } : f
39+
) as ModalFeature[];
40+
handleChange("features", updated);
41+
}
42+
43+
function addFeature() {
44+
handleChange("features", [
45+
...(form.features ?? []),
46+
{ icon: "", title: "", description: "" } satisfies ModalFeature,
47+
]);
48+
}
49+
50+
function removeFeature(index: number) {
51+
handleChange(
52+
"features",
53+
(form.features ?? []).filter((_, i) => i !== index)
54+
);
55+
}
56+
57+
async function handleSubmit() {
58+
setSaving(true);
59+
setMessage(null);
60+
const { error } = await upsertActiveModal(form);
61+
setSaving(false);
62+
setMessage(error ?? "Changes saved successfully.");
63+
}
64+
65+
return (
66+
<div className="space-y-6">
67+
{/* ── Two-column grid ── */}
68+
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
69+
70+
{/* ── LEFT COLUMN: Content fields ── */}
71+
<div className="rounded-2xl border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-900">
72+
<div className="mb-5 flex items-center justify-between">
73+
<h2 className="text-sm font-semibold text-gray-700 dark:text-gray-200">
74+
Modal Content
75+
</h2>
76+
{/* Active toggle */}
77+
<div className="flex items-center gap-2">
78+
<span className="text-xs text-gray-500 dark:text-gray-400">
79+
{form.is_active ? "Visible" : "Hidden"}
80+
</span>
81+
<button
82+
onClick={() => handleChange("is_active", !form.is_active)}
83+
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
84+
form.is_active
85+
? "bg-violet-600"
86+
: "bg-gray-300 dark:bg-gray-600"
87+
}`}
88+
>
89+
<span
90+
className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${
91+
form.is_active ? "translate-x-6" : "translate-x-1"
92+
}`}
93+
/>
94+
</button>
95+
</div>
96+
</div>
97+
98+
<div className="space-y-4">
99+
{(
100+
[
101+
{ field: "badge", label: "Badge" },
102+
{ field: "headline", label: "Headline" },
103+
{ field: "subheadline", label: "Subheadline" },
104+
] as const
105+
).map(({ field, label }) => (
106+
<div key={field} className="flex flex-col gap-1.5">
107+
<label className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
108+
{label}
109+
</label>
110+
{field === "subheadline" ? (
111+
<textarea
112+
rows={3}
113+
value={(form[field] as string) ?? ""}
114+
onChange={(e) => handleChange(field, e.target.value)}
115+
className="rounded-xl border border-gray-200 bg-gray-50 px-4 py-2.5 text-sm outline-none focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-white resize-none"
116+
/>
117+
) : (
118+
<input
119+
value={(form[field] as string) ?? ""}
120+
onChange={(e) => handleChange(field, e.target.value)}
121+
className="rounded-xl border border-gray-200 bg-gray-50 px-4 py-2.5 text-sm outline-none focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
122+
/>
123+
)}
124+
</div>
125+
))}
126+
</div>
127+
128+
<hr className="my-5 border-gray-200 dark:border-gray-700" />
129+
130+
{/* CTA + Dismiss */}
131+
<h3 className="mb-4 text-sm font-semibold text-gray-700 dark:text-gray-200">
132+
Call to Action
133+
</h3>
134+
<div className="space-y-4">
135+
{(
136+
[
137+
{ field: "cta_label", label: "Button Label" },
138+
{ field: "cta_href", label: "Button URL" },
139+
{ field: "dismiss_label", label: "Dismiss Text" },
140+
] as const
141+
).map(({ field, label }) => (
142+
<div key={field} className="flex flex-col gap-1.5">
143+
<label className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
144+
{label}
145+
</label>
146+
<input
147+
value={(form[field] as string) ?? ""}
148+
onChange={(e) => handleChange(field, e.target.value)}
149+
className="rounded-xl border border-gray-200 bg-gray-50 px-4 py-2.5 text-sm outline-none focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
150+
/>
151+
</div>
152+
))}
153+
</div>
154+
</div>
155+
156+
{/* ── RIGHT COLUMN: Features + Preview ── */}
157+
<div className="flex flex-col gap-6">
158+
159+
{/* Features editor */}
160+
<div className="rounded-2xl border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-900">
161+
<h2 className="mb-4 text-sm font-semibold text-gray-700 dark:text-gray-200">
162+
Features
163+
</h2>
164+
<div className="space-y-4">
165+
{form.features?.map((f, i) => (
166+
<div
167+
key={i}
168+
className="relative rounded-xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800"
169+
>
170+
<button
171+
onClick={() => removeFeature(i)}
172+
className="absolute right-3 top-3 text-xs text-red-400 hover:text-red-600"
173+
>
174+
Remove
175+
</button>
176+
<div className="grid grid-cols-4 gap-2 mb-2">
177+
<div className="flex flex-col gap-1 col-span-4">
178+
<label className="text-xs text-gray-500 dark:text-gray-400">
179+
Icon
180+
</label>
181+
<IconPicker
182+
value={f.icon}
183+
onChange={(name) => handleFeatureChange(i, "icon", name)}
184+
/>
185+
</div>
186+
<div className="col-span-4 flex flex-col gap-1">
187+
<label className="text-xs text-gray-500 dark:text-gray-400">
188+
Title
189+
</label>
190+
<input
191+
value={f.title}
192+
onChange={(e) =>
193+
handleFeatureChange(i, "title", e.target.value)
194+
}
195+
className="rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm outline-none focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20 dark:border-gray-700 dark:bg-gray-900 dark:text-white"
196+
/>
197+
</div>
198+
</div>
199+
<div className="flex flex-col gap-1">
200+
<label className="text-xs text-gray-500 dark:text-gray-400">
201+
Description
202+
</label>
203+
<input
204+
value={f.description}
205+
onChange={(e) =>
206+
handleFeatureChange(i, "description", e.target.value)
207+
}
208+
className="rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm outline-none focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20 dark:border-gray-700 dark:bg-gray-900 dark:text-white"
209+
/>
210+
</div>
211+
</div>
212+
))}
213+
</div>
214+
<button
215+
onClick={addFeature}
216+
className="mt-3 w-full rounded-xl border border-dashed border-violet-400 px-4 py-2 text-sm text-violet-600 hover:bg-violet-50 dark:hover:bg-violet-900/20 transition-colors"
217+
>
218+
+ Add Feature
219+
</button>
220+
</div>
221+
222+
{/* Live preview */}
223+
<div className="rounded-2xl border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-900">
224+
<h2 className="mb-4 text-sm font-semibold text-gray-700 dark:text-gray-200">
225+
Preview
226+
</h2>
227+
<div className="overflow-hidden rounded-xl border border-gray-200 dark:border-gray-700 shadow-lg">
228+
<div className="h-1.5 w-full bg-gradient-to-r from-violet-500 via-blue-500 to-cyan-400" />
229+
<div className="bg-white dark:bg-gray-900 px-5 py-4">
230+
{form.badge && (
231+
<span className="mb-2 inline-flex items-center gap-1.5 rounded-full bg-violet-100 px-3 py-1 text-xs font-semibold text-violet-700 dark:bg-violet-900/40 dark:text-violet-300">
232+
{form.badge}
233+
</span>
234+
)}
235+
<p className="text-base font-bold text-gray-900 dark:text-white">
236+
{form.headline || (
237+
<span className="text-gray-300 dark:text-gray-600">Headline...</span>
238+
)}
239+
</p>
240+
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
241+
{form.subheadline || (
242+
<span className="text-gray-300 dark:text-gray-600">Subheadline...</span>
243+
)}
244+
</p>
245+
{(form.features ?? []).length > 0 && (
246+
<ul className="mt-3 space-y-1.5">
247+
{form.features?.map((f, i) => (
248+
<li
249+
key={i}
250+
className="flex items-center gap-2 rounded-lg bg-gray-50 px-3 py-2 dark:bg-gray-800"
251+
>
252+
{(() => {
253+
const Icon = f.icon ? (Icons[f.icon as keyof typeof Icons] as Icons.LucideIcon) : null;
254+
return Icon ? <Icon className="h-4 w-4 shrink-0 text-violet-600" /> : null;
255+
})()}
256+
<div>
257+
<p className="text-xs font-semibold text-gray-800 dark:text-gray-100">
258+
{f.title}
259+
</p>
260+
<p className="text-xs text-gray-500 dark:text-gray-400">
261+
{f.description}
262+
</p>
263+
</div>
264+
</li>
265+
))}
266+
</ul>
267+
)}
268+
<div className="mt-3 flex gap-2">
269+
<div className="flex-1 rounded-lg bg-gradient-to-r from-violet-600 to-blue-600 px-3 py-1.5 text-center text-xs font-semibold text-white">
270+
{form.cta_label || "CTA Button"}
271+
</div>
272+
<div className="flex-1 rounded-lg px-3 py-1.5 text-center text-xs text-gray-500">
273+
{form.dismiss_label || "Maybe later"}
274+
</div>
275+
</div>
276+
</div>
277+
</div>
278+
</div>
279+
</div>
280+
</div>
281+
282+
{/* ── Save bar ── */}
283+
<div className="flex items-center justify-between rounded-2xl border border-gray-200 bg-white px-6 py-4 dark:border-gray-700 dark:bg-gray-900">
284+
{message ? (
285+
<p
286+
className={`text-sm font-medium ${
287+
message.includes("Failed") ? "text-red-500" : "text-green-500"
288+
}`}
289+
>
290+
{message}
291+
</p>
292+
) : (
293+
<p className="text-sm text-gray-400">Unsaved changes</p>
294+
)}
295+
<button
296+
onClick={handleSubmit}
297+
disabled={saving}
298+
className="rounded-xl bg-gradient-to-r from-violet-600 to-blue-600 px-6 py-2.5 text-sm font-semibold text-white shadow-md hover:brightness-110 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-50 transition-all"
299+
>
300+
{saving ? "Saving..." : "Save Changes"}
301+
</button>
302+
</div>
303+
</div>
304+
);
305+
}

0 commit comments

Comments
 (0)