Skip to content

Commit cda2cb1

Browse files
committed
feat: implement complete merchant settings page with all features
1 parent 204b9be commit cda2cb1

File tree

2 files changed

+365
-15
lines changed

2 files changed

+365
-15
lines changed
Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,5 @@
1-
export default function SettingsPage() {
2-
return (
3-
<div className="space-y-6">
4-
<div className="flex items-center justify-between">
5-
<div>
6-
<h2 className="text-2xl font-bold tracking-tight">Settings</h2>
7-
<p className="text-muted-foreground">Manage your account preferences and configurations.</p>
8-
</div>
9-
</div>
1+
import { SettingsPage } from "@/features/settings";
102

11-
{/* Pending Implementation: Settings Form */}
12-
<div className="rounded-xl border bg-card shadow p-12 text-center text-muted-foreground">
13-
Account Settings will be implemented here.
14-
</div>
15-
</div>
16-
);
3+
export default function Settings() {
4+
return <SettingsPage />;
175
}
Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
"use client";
2+
3+
import React, { useState } from "react";
4+
import Input from "@/components/Input";
5+
import { Button } from "@/components/Button";
6+
import { Modal } from "@/components/Modal";
7+
import { cn } from "@/lib/utils";
8+
import { Copy, Key, Webhook, Shield, CheckCircle2 } from "lucide-react";
9+
10+
export default function SettingsPage() {
11+
// Account Details State
12+
const [businessName, setBusinessName] = useState("Acme Corporation");
13+
const [contactEmail, setContactEmail] = useState("contact@acme.com");
14+
const [accountSaved, setAccountSaved] = useState(false);
15+
const [isSavingAccount, setIsSavingAccount] = useState(false);
16+
17+
// API Key State
18+
const [apiKey, setApiKey] = useState("fluxapay_live_xxxxxxxxxxxxxxxxxxxxxxxx");
19+
const [showRegenerateModal, setShowRegenerateModal] = useState(false);
20+
const [isRegenerating, setIsRegenerating] = useState(false);
21+
const [keyRegenerated, setKeyRegenerated] = useState(false);
22+
const [copied, setCopied] = useState(false);
23+
24+
// Webhook State
25+
const [webhookUrl, setWebhookUrl] = useState("https://api.acme.com/webhooks");
26+
const [webhookError, setWebhookError] = useState("");
27+
const [webhookSaved, setWebhookSaved] = useState(false);
28+
const [isSavingWebhook, setIsSavingWebhook] = useState(false);
29+
30+
// Security State
31+
const [twoFactorEnabled, setTwoFactorEnabled] = useState(false);
32+
33+
// Generate random API key
34+
const generateApiKey = () => {
35+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
36+
let key = 'fluxapay_live_';
37+
for (let i = 0; i < 24; i++) {
38+
key += chars.charAt(Math.floor(Math.random() * chars.length));
39+
}
40+
return key;
41+
};
42+
43+
// Handle Account Details Save
44+
const handleAccountSave = async () => {
45+
setIsSavingAccount(true);
46+
// Simulate API call
47+
await new Promise((resolve) => setTimeout(resolve, 800));
48+
console.log("Saving account details:", { businessName, contactEmail });
49+
setIsSavingAccount(false);
50+
setAccountSaved(true);
51+
setTimeout(() => setAccountSaved(false), 3000);
52+
};
53+
54+
// Handle API Key Copy
55+
const handleCopyApiKey = () => {
56+
navigator.clipboard.writeText(apiKey);
57+
setCopied(true);
58+
setTimeout(() => setCopied(false), 2000);
59+
};
60+
61+
// Handle API Key Regeneration
62+
const handleRegenerateApiKey = async () => {
63+
setIsRegenerating(true);
64+
// Simulate API call
65+
await new Promise((resolve) => setTimeout(resolve, 1500));
66+
67+
// Generate new API key
68+
const newKey = generateApiKey();
69+
setApiKey(newKey);
70+
console.log("API Key regenerated:", newKey);
71+
72+
setIsRegenerating(false);
73+
setShowRegenerateModal(false);
74+
75+
// Show success message
76+
setKeyRegenerated(true);
77+
setTimeout(() => setKeyRegenerated(false), 5000);
78+
};
79+
80+
// Handle Webhook URL Change
81+
const handleWebhookUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
82+
const value = e.target.value;
83+
setWebhookUrl(value);
84+
85+
// Validate HTTPS
86+
if (value && !value.startsWith("https://")) {
87+
setWebhookError("Webhook URL must start with https://");
88+
} else {
89+
setWebhookError("");
90+
}
91+
};
92+
93+
// Handle Webhook Save
94+
const handleWebhookSave = async () => {
95+
if (webhookError) return;
96+
setIsSavingWebhook(true);
97+
// Simulate API call
98+
await new Promise((resolve) => setTimeout(resolve, 800));
99+
console.log("Saving webhook URL:", webhookUrl);
100+
setIsSavingWebhook(false);
101+
setWebhookSaved(true);
102+
setTimeout(() => setWebhookSaved(false), 3000);
103+
};
104+
105+
return (
106+
<div className="space-y-6">
107+
{/* Page Header */}
108+
<div className="flex items-center justify-between">
109+
<div>
110+
<h2 className="text-2xl font-bold tracking-tight">Settings</h2>
111+
<p className="text-muted-foreground">
112+
Manage your account preferences and configurations.
113+
</p>
114+
</div>
115+
</div>
116+
117+
{/* Account Details Section */}
118+
<div className="space-y-4 p-6 rounded-2xl border bg-muted/20">
119+
<div className="flex items-center gap-2 text-primary font-semibold mb-4">
120+
<Shield className="h-5 w-5" />
121+
<h3 className="text-lg">Account Details</h3>
122+
</div>
123+
124+
<div className="space-y-4">
125+
<div>
126+
<label className="block text-sm font-medium mb-2">
127+
Business Name
128+
</label>
129+
<Input
130+
type="text"
131+
value={businessName}
132+
onChange={(e) => setBusinessName(e.target.value)}
133+
placeholder="Enter your business name"
134+
/>
135+
</div>
136+
137+
<div>
138+
<label className="block text-sm font-medium mb-2">
139+
Contact Email
140+
</label>
141+
<Input
142+
type="email"
143+
value={contactEmail}
144+
onChange={(e) => setContactEmail(e.target.value)}
145+
placeholder="contact@example.com"
146+
/>
147+
</div>
148+
149+
<div className="flex items-center gap-3 pt-2">
150+
<Button
151+
variant="dark"
152+
onClick={handleAccountSave}
153+
disabled={isSavingAccount}
154+
className="gap-2"
155+
>
156+
{isSavingAccount && (
157+
<svg
158+
className="h-4 w-4 animate-spin"
159+
viewBox="0 0 24 24"
160+
fill="none"
161+
stroke="currentColor"
162+
strokeWidth="3"
163+
>
164+
<circle cx="12" cy="12" r="10" className="opacity-30" />
165+
<path d="M22 12a10 10 0 0 1-10 10" />
166+
</svg>
167+
)}
168+
{accountSaved && <CheckCircle2 className="h-4 w-4" />}
169+
{isSavingAccount ? "Saving..." : accountSaved ? "Saved!" : "Save Changes"}
170+
</Button>
171+
</div>
172+
</div>
173+
</div>
174+
175+
{/* API Keys Section */}
176+
<div className="space-y-4 p-6 rounded-2xl border bg-muted/20">
177+
<div className="flex items-center gap-2 text-primary font-semibold mb-4">
178+
<Key className="h-5 w-5" />
179+
<h3 className="text-lg">API Keys</h3>
180+
</div>
181+
182+
<div className="space-y-4">
183+
<div>
184+
<label className="block text-sm font-medium mb-2">
185+
Live API Key
186+
</label>
187+
<div className="flex gap-2">
188+
<Input
189+
type="text"
190+
value={apiKey}
191+
readOnly
192+
className="font-mono text-sm bg-muted/50"
193+
/>
194+
<Button
195+
variant="outline"
196+
onClick={handleCopyApiKey}
197+
className="gap-2 shrink-0"
198+
>
199+
{copied ? (
200+
<>
201+
<CheckCircle2 className="h-4 w-4 text-green-600" />
202+
Copied
203+
</>
204+
) : (
205+
<>
206+
<Copy className="h-4 w-4" />
207+
Copy
208+
</>
209+
)}
210+
</Button>
211+
</div>
212+
<p className="text-xs text-muted-foreground mt-2">
213+
Keep your API key secure. Do not share it publicly.
214+
</p>
215+
</div>
216+
217+
<div className="pt-2">
218+
<Button
219+
variant="destructive"
220+
onClick={() => setShowRegenerateModal(true)}
221+
>
222+
Regenerate API Key
223+
</Button>
224+
</div>
225+
226+
{/* Success Message */}
227+
{keyRegenerated && (
228+
<div className="flex items-center gap-2 p-3 rounded-lg bg-green-50 border border-green-200 text-green-800 animate-in fade-in slide-in-from-top-2">
229+
<CheckCircle2 className="h-4 w-4" />
230+
<p className="text-sm font-medium">
231+
API key regenerated successfully! Make sure to update your integrations.
232+
</p>
233+
</div>
234+
)}
235+
</div>
236+
</div>
237+
238+
{/* Webhook Configuration Section */}
239+
<div className="space-y-4 p-6 rounded-2xl border bg-muted/20">
240+
<div className="flex items-center gap-2 text-primary font-semibold mb-4">
241+
<Webhook className="h-5 w-5" />
242+
<h3 className="text-lg">Webhook Configuration</h3>
243+
</div>
244+
245+
<div className="space-y-4">
246+
<div>
247+
<label className="block text-sm font-medium mb-2">
248+
Webhook URL
249+
</label>
250+
<Input
251+
type="url"
252+
value={webhookUrl}
253+
onChange={handleWebhookUrlChange}
254+
placeholder="https://your-domain.com/webhooks"
255+
error={webhookError}
256+
/>
257+
<p className="text-xs text-muted-foreground mt-2">
258+
We'll send payment notifications to this endpoint.
259+
</p>
260+
</div>
261+
262+
<div className="flex items-center gap-3 pt-2">
263+
<Button
264+
variant="dark"
265+
onClick={handleWebhookSave}
266+
disabled={!!webhookError || isSavingWebhook}
267+
className="gap-2"
268+
>
269+
{isSavingWebhook && (
270+
<svg
271+
className="h-4 w-4 animate-spin"
272+
viewBox="0 0 24 24"
273+
fill="none"
274+
stroke="currentColor"
275+
strokeWidth="3"
276+
>
277+
<circle cx="12" cy="12" r="10" className="opacity-30" />
278+
<path d="M22 12a10 10 0 0 1-10 10" />
279+
</svg>
280+
)}
281+
{webhookSaved && <CheckCircle2 className="h-4 w-4" />}
282+
{isSavingWebhook ? "Saving..." : webhookSaved ? "Saved!" : "Save Webhook URL"}
283+
</Button>
284+
</div>
285+
</div>
286+
</div>
287+
288+
{/* Security Settings Section */}
289+
<div className="space-y-4 p-6 rounded-2xl border bg-muted/20">
290+
<div className="flex items-center gap-2 text-primary font-semibold mb-4">
291+
<Shield className="h-5 w-5" />
292+
<h3 className="text-lg">Security Settings</h3>
293+
</div>
294+
295+
<div className="space-y-4">
296+
<div className="flex items-center justify-between p-4 rounded-lg border bg-background">
297+
<div>
298+
<p className="font-medium">Two-Factor Authentication</p>
299+
<p className="text-sm text-muted-foreground">
300+
Add an extra layer of security to your account
301+
</p>
302+
</div>
303+
<label className="relative inline-flex items-center cursor-pointer">
304+
<input
305+
type="checkbox"
306+
checked={twoFactorEnabled}
307+
onChange={(e) => setTwoFactorEnabled(e.target.checked)}
308+
className="sr-only peer"
309+
/>
310+
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[#5649DF]/20 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[#5649DF]"></div>
311+
</label>
312+
</div>
313+
</div>
314+
</div>
315+
316+
{/* API Key Regeneration Modal */}
317+
<Modal
318+
isOpen={showRegenerateModal}
319+
onClose={() => setShowRegenerateModal(false)}
320+
title="Regenerate API Key"
321+
>
322+
<div className="space-y-4">
323+
<p className="text-sm text-muted-foreground">
324+
Are you sure you want to regenerate your API key? Your current API
325+
key will be immediately invalidated and any integrations using it
326+
will stop working.
327+
</p>
328+
<div className="flex gap-3 pt-4">
329+
<Button
330+
variant="outline"
331+
onClick={() => setShowRegenerateModal(false)}
332+
className="flex-1"
333+
disabled={isRegenerating}
334+
>
335+
Cancel
336+
</Button>
337+
<Button
338+
variant="destructive"
339+
onClick={handleRegenerateApiKey}
340+
className="flex-1 gap-2"
341+
disabled={isRegenerating}
342+
>
343+
{isRegenerating && (
344+
<svg
345+
className="h-4 w-4 animate-spin"
346+
viewBox="0 0 24 24"
347+
fill="none"
348+
stroke="currentColor"
349+
strokeWidth="3"
350+
>
351+
<circle cx="12" cy="12" r="10" className="opacity-30" />
352+
<path d="M22 12a10 10 0 0 1-10 10" />
353+
</svg>
354+
)}
355+
{isRegenerating ? "Regenerating..." : "Regenerate"}
356+
</Button>
357+
</div>
358+
</div>
359+
</Modal>
360+
</div>
361+
);
362+
}

0 commit comments

Comments
 (0)