@jhits/plugin-website 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@jhits/plugin-website",
3
+ "version": "0.0.1",
4
+ "description": "Website management and configuration plugin for the JHITS ecosystem",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "main": "./src/index.tsx",
9
+ "types": "./src/index.tsx",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./src/index.tsx",
13
+ "default": "./src/index.tsx"
14
+ },
15
+ "./server": {
16
+ "types": "./src/index.server.ts",
17
+ "default": "./src/index.server.ts"
18
+ }
19
+ },
20
+ "dependencies": {
21
+ "@jhits/plugin-core": "^0.0.1",
22
+ "lucide-react": "^0.562.0",
23
+ "mongodb": "^7.0.0",
24
+ "next-auth": "^4.24.13",
25
+ "react-icons": "^5.5.0"
26
+ },
27
+ "peerDependencies": {
28
+ "next": ">=15.0.0",
29
+ "react": ">=18.0.0",
30
+ "react-dom": ">=18.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^20.19.27",
34
+ "@types/react": "^19",
35
+ "@types/react-dom": "^19",
36
+ "next": "16.1.1",
37
+ "react": "19.2.3",
38
+ "react-dom": "19.2.3",
39
+ "typescript": "^5"
40
+ },
41
+ "files": [
42
+ "src",
43
+ "package.json"
44
+ ]
45
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Website Settings API Handler
3
+ * Handles GET and POST requests for website settings
4
+ */
5
+
6
+ 'use server';
7
+
8
+ import { NextRequest, NextResponse } from 'next/server';
9
+ import { WebsiteApiConfig } from '../types/settings';
10
+
11
+ // Re-export for router
12
+ export type { WebsiteApiConfig } from '../types/settings';
13
+
14
+ /**
15
+ * GET /api/plugin-website/settings - Get website settings
16
+ */
17
+ export async function GET_SETTINGS(
18
+ req: NextRequest,
19
+ config: WebsiteApiConfig
20
+ ): Promise<NextResponse> {
21
+ try {
22
+ const dbConnection = await config.getDb();
23
+ const db = dbConnection.db();
24
+ const settings = db.collection('settings');
25
+
26
+ const siteConfig = await settings.findOne({ identifier: 'site_config' });
27
+
28
+ if (!siteConfig) {
29
+ // Return default settings if none exist
30
+ return NextResponse.json({
31
+ identifier: 'site_config',
32
+ siteName: '',
33
+ siteTagline: '',
34
+ siteDescription: '',
35
+ keywords: '',
36
+ contactEmail: '',
37
+ phoneNumber: '',
38
+ physicalAddress: '',
39
+ smtpUser: '',
40
+ maintenanceMode: false,
41
+ launch_date: '',
42
+ socials: [],
43
+ });
44
+ }
45
+
46
+ // Convert launch_date from Date to ISO string if it exists
47
+ const responseData: any = { ...siteConfig };
48
+ if (responseData.launch_date instanceof Date) {
49
+ responseData.launch_date = responseData.launch_date.toISOString();
50
+ } else if (responseData.launch_date) {
51
+ // If it's already a string, keep it as is
52
+ responseData.launch_date = responseData.launch_date;
53
+ } else {
54
+ responseData.launch_date = '';
55
+ }
56
+
57
+ return NextResponse.json(responseData);
58
+ } catch (error: any) {
59
+ console.error('[WebsiteAPI] GET_SETTINGS error:', error);
60
+ return NextResponse.json(
61
+ { error: 'Failed to fetch settings', detail: error.message },
62
+ { status: 500 }
63
+ );
64
+ }
65
+ }
66
+
67
+ /**
68
+ * POST /api/plugin-website/settings - Update website settings
69
+ */
70
+ export async function POST_SETTINGS(
71
+ req: NextRequest,
72
+ config: WebsiteApiConfig
73
+ ): Promise<NextResponse> {
74
+ try {
75
+ const userId = await config.getUserId?.(req);
76
+ if (!userId) {
77
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
78
+ }
79
+
80
+ const body = await req.json();
81
+ console.log('[WebsiteAPI] POST_SETTINGS received body:', {
82
+ launch_date: body.launch_date,
83
+ launch_date_type: typeof body.launch_date,
84
+ has_launch_date: 'launch_date' in body,
85
+ body_keys: Object.keys(body),
86
+ });
87
+
88
+ const dbConnection = await config.getDb();
89
+ const db = dbConnection.db();
90
+ const settings = db.collection('settings');
91
+
92
+ // Remove _id and launch_date from updateData (we'll handle launch_date separately)
93
+ const { _id, launch_date, ...updateData } = body;
94
+
95
+ // Prepare base update operations
96
+ const updateOps: any = {
97
+ $set: {
98
+ identifier: 'site_config',
99
+ ...updateData,
100
+ updatedAt: new Date(),
101
+ }
102
+ };
103
+
104
+ // Handle launch_date separately
105
+ if (launch_date !== undefined && launch_date !== null && launch_date !== '') {
106
+ // Convert launch_date from datetime-local string to Date if provided
107
+ const date = new Date(launch_date);
108
+ console.log('[WebsiteAPI] Date conversion:', {
109
+ input: launch_date,
110
+ parsed: date,
111
+ isValid: !isNaN(date.getTime()),
112
+ iso: !isNaN(date.getTime()) ? date.toISOString() : 'invalid',
113
+ });
114
+
115
+ if (!isNaN(date.getTime())) {
116
+ updateOps.$set.launch_date = date;
117
+ console.log('[WebsiteAPI] Setting launch_date to:', date.toISOString());
118
+ } else {
119
+ console.warn('[WebsiteAPI] Invalid date, removing launch_date field');
120
+ updateOps.$unset = { launch_date: '' };
121
+ }
122
+ } else {
123
+ // Remove launch_date field if it's empty, undefined, or null
124
+ console.log('[WebsiteAPI] launch_date is empty/undefined, removing field');
125
+ updateOps.$unset = { launch_date: '' };
126
+ }
127
+
128
+ console.log('[WebsiteAPI] Update operations:', JSON.stringify(updateOps, (key, value) => {
129
+ if (value instanceof Date) {
130
+ return value.toISOString();
131
+ }
132
+ return value;
133
+ }, 2));
134
+
135
+ // Upsert settings
136
+ const result = await settings.updateOne(
137
+ { identifier: 'site_config' },
138
+ updateOps,
139
+ { upsert: true }
140
+ );
141
+
142
+ console.log('[WebsiteAPI] Update result:', {
143
+ matchedCount: result.matchedCount,
144
+ modifiedCount: result.modifiedCount,
145
+ upsertedCount: result.upsertedCount,
146
+ });
147
+
148
+ return NextResponse.json({ success: true, message: 'Settings updated successfully' });
149
+ } catch (error: any) {
150
+ console.error('[WebsiteAPI] POST_SETTINGS error:', error);
151
+ return NextResponse.json(
152
+ { error: 'Failed to update settings', detail: error.message },
153
+ { status: 500 }
154
+ );
155
+ }
156
+ }
157
+
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Website Plugin API Router
3
+ * Routes API requests to appropriate handlers
4
+ */
5
+
6
+ 'use server';
7
+
8
+ import { NextRequest, NextResponse } from 'next/server';
9
+ import { WebsiteApiConfig } from '../types/settings';
10
+ import { GET_SETTINGS, POST_SETTINGS } from './handler';
11
+
12
+ /**
13
+ * Handle website API requests
14
+ */
15
+ export async function handleWebsiteApi(
16
+ req: NextRequest,
17
+ path: string[],
18
+ config: WebsiteApiConfig
19
+ ): Promise<NextResponse> {
20
+ const method = req.method;
21
+ const route = path[0] || '';
22
+
23
+ try {
24
+ // Route: /api/plugin-website/settings
25
+ if (route === 'settings') {
26
+ if (method === 'GET') {
27
+ return await GET_SETTINGS(req, config);
28
+ }
29
+ if (method === 'POST' || method === 'PUT') {
30
+ return await POST_SETTINGS(req, config);
31
+ }
32
+ }
33
+
34
+ // Method not allowed
35
+ return NextResponse.json(
36
+ { error: `Method ${method} not allowed for route: ${route || '/'}` },
37
+ { status: 405 }
38
+ );
39
+ } catch (error: any) {
40
+ console.error('[WebsiteApiRouter] Error:', error);
41
+ return NextResponse.json(
42
+ { error: error.message || 'Internal server error' },
43
+ { status: 500 }
44
+ );
45
+ }
46
+ }
47
+
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Plugin Website - Server-Only Entry Point
3
+ * This file exports only server-side API handlers
4
+ * Used by the dynamic plugin router via @jhits/plugin-website/server
5
+ */
6
+
7
+ export { handleWebsiteApi as handleApi } from './api/router';
8
+ export { handleWebsiteApi } from './api/router'; // Keep original export for backward compatibility
9
+ export type { WebsiteApiConfig } from './types/settings';
10
+
package/src/index.tsx ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Plugin Website - Client Entry Point
3
+ * Main settings management interface for website configuration
4
+ */
5
+
6
+ 'use client';
7
+
8
+ import React from 'react';
9
+ import { SettingsView } from './views/SettingsView';
10
+
11
+ export interface PluginProps {
12
+ subPath: string[];
13
+ siteId: string;
14
+ locale: string;
15
+ }
16
+
17
+ export default function WebsitePlugin({ subPath, siteId, locale }: PluginProps) {
18
+ const route = subPath[0] || 'settings';
19
+
20
+ switch (route) {
21
+ case 'settings':
22
+ return <SettingsView siteId={siteId} locale={locale} />;
23
+ default:
24
+ return <SettingsView siteId={siteId} locale={locale} />;
25
+ }
26
+ }
27
+
28
+ // Export for use as default
29
+ export { WebsitePlugin as Index };
30
+
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Website Settings Types
3
+ */
4
+
5
+ export interface WebsiteSettings {
6
+ identifier: string;
7
+ siteName?: string;
8
+ siteTagline?: string;
9
+ siteDescription?: string;
10
+ keywords?: string;
11
+ contactEmail?: string;
12
+ phoneNumber?: string;
13
+ physicalAddress?: string;
14
+ smtpUser?: string;
15
+ maintenanceMode?: boolean;
16
+ launch_date?: string;
17
+ socials?: SocialLink[];
18
+ updatedAt?: Date;
19
+ createdAt?: Date;
20
+ }
21
+
22
+ export interface SocialLink {
23
+ id: number;
24
+ platform: string;
25
+ url: string;
26
+ }
27
+
28
+ export interface WebsiteApiConfig {
29
+ getDb: () => Promise<{ db: () => any }>;
30
+ getUserId?: (req: any) => Promise<string | null>;
31
+ mongoClient?: Promise<any>;
32
+ authOptions?: any;
33
+ jwtSecret?: string;
34
+ [key: string]: any; // Allow additional config properties
35
+ }
36
+
@@ -0,0 +1,519 @@
1
+ /**
2
+ * Website Settings View
3
+ * Main interface for managing website settings
4
+ */
5
+
6
+ 'use client';
7
+
8
+ import React, { useState, useEffect, useRef } from 'react';
9
+ import { Save, RefreshCw, Globe, Mail, Phone, MapPin, Search, Settings2, Calendar } from 'lucide-react';
10
+ import { WebsiteSettings, SocialLink } from '../types/settings';
11
+ import { FaFacebook, FaInstagram, FaLinkedin, FaPinterest, FaTiktok } from 'react-icons/fa';
12
+ import { FaXTwitter } from 'react-icons/fa6';
13
+
14
+ const AVAILABLE_PLATFORMS = [
15
+ { name: 'Instagram', icon: <FaInstagram size={18} /> },
16
+ { name: 'Facebook', icon: <FaFacebook size={18} /> },
17
+ { name: 'Twitter', icon: <FaXTwitter size={18} /> },
18
+ { name: 'Pinterest', icon: <FaPinterest size={18} /> },
19
+ { name: 'LinkedIn', icon: <FaLinkedin size={18} /> },
20
+ { name: 'TikTok', icon: <FaTiktok size={18} /> },
21
+ ];
22
+
23
+ export interface SettingsViewProps {
24
+ siteId: string;
25
+ locale: string;
26
+ }
27
+
28
+ export function SettingsView({ siteId, locale }: SettingsViewProps) {
29
+ const [isLoading, setIsLoading] = useState(true);
30
+ const [isSaving, setIsSaving] = useState(false);
31
+ const [showSuccess, setShowSuccess] = useState(false);
32
+ const [settings, setSettings] = useState<WebsiteSettings>({
33
+ identifier: 'site_config',
34
+ siteName: '',
35
+ siteTagline: '',
36
+ siteDescription: '',
37
+ keywords: '',
38
+ contactEmail: '',
39
+ phoneNumber: '',
40
+ physicalAddress: '',
41
+ smtpUser: '',
42
+ maintenanceMode: false,
43
+ launch_date: '',
44
+ socials: [],
45
+ });
46
+
47
+ // Fetch settings on load
48
+ useEffect(() => {
49
+ const fetchSettings = async () => {
50
+ try {
51
+ setIsLoading(true);
52
+ const response = await fetch('/api/plugin-website/settings', {
53
+ credentials: 'include',
54
+ });
55
+ if (!response.ok) {
56
+ throw new Error('Failed to fetch settings');
57
+ }
58
+ const data = await response.json();
59
+ if (data.siteName !== undefined) {
60
+ // Convert launch_date to datetime-local format if it exists
61
+ let launchDate = '';
62
+ if (data.launch_date) {
63
+ // If it's a Date object or ISO string, convert to datetime-local format
64
+ const date = new Date(data.launch_date);
65
+ if (!isNaN(date.getTime())) {
66
+ // Format as YYYY-MM-DDTHH:mm for datetime-local input (24-hour format)
67
+ const year = date.getFullYear();
68
+ const month = String(date.getMonth() + 1).padStart(2, '0');
69
+ const day = String(date.getDate()).padStart(2, '0');
70
+ const hours = String(date.getHours()).padStart(2, '0');
71
+ const minutes = String(date.getMinutes()).padStart(2, '0');
72
+ launchDate = `${year}-${month}-${day}T${hours}:${minutes}`;
73
+ } else {
74
+ // If it's already a string in the correct format, use it
75
+ // Ensure it has time component, default to 00:00 if missing
76
+ if (data.launch_date.includes('T') && data.launch_date.split('T')[1]) {
77
+ launchDate = data.launch_date;
78
+ } else if (data.launch_date.includes('T')) {
79
+ launchDate = `${data.launch_date.split('T')[0]}T00:00`;
80
+ } else {
81
+ launchDate = `${data.launch_date}T00:00`;
82
+ }
83
+ }
84
+ }
85
+
86
+ setSettings({
87
+ identifier: 'site_config',
88
+ siteName: data.siteName || '',
89
+ siteTagline: data.siteTagline || '',
90
+ siteDescription: data.siteDescription || '',
91
+ keywords: data.keywords || '',
92
+ contactEmail: data.contactEmail || '',
93
+ phoneNumber: data.phoneNumber || '',
94
+ physicalAddress: data.physicalAddress || '',
95
+ smtpUser: data.smtpUser || '',
96
+ maintenanceMode: data.maintenanceMode ?? false,
97
+ launch_date: launchDate,
98
+ socials: data.socials || [],
99
+ });
100
+ }
101
+ } catch (error) {
102
+ console.error('Failed to load settings:', error);
103
+ } finally {
104
+ setIsLoading(false);
105
+ }
106
+ };
107
+ fetchSettings();
108
+ }, []);
109
+
110
+ // Use ref to always get latest state
111
+ const settingsRef = useRef(settings);
112
+ useEffect(() => {
113
+ settingsRef.current = settings;
114
+ }, [settings]);
115
+
116
+ // Save settings
117
+ const handleSave = async () => {
118
+ try {
119
+ setIsSaving(true);
120
+
121
+ // Get the latest state from ref
122
+ const currentSettings = settingsRef.current;
123
+
124
+ // Prepare settings for API - ensure launch_date is properly formatted
125
+ const settingsToSave: any = {
126
+ ...currentSettings,
127
+ };
128
+
129
+ // Handle launch_date - keep it if it has a value, otherwise don't include it
130
+ if (currentSettings.launch_date && currentSettings.launch_date.trim() !== '') {
131
+ settingsToSave.launch_date = currentSettings.launch_date;
132
+ } else {
133
+ // Don't include launch_date if it's empty
134
+ delete settingsToSave.launch_date;
135
+ }
136
+
137
+ console.log('[SettingsView] Saving settings:', {
138
+ launch_date: settingsToSave.launch_date,
139
+ launch_date_type: typeof settingsToSave.launch_date,
140
+ has_launch_date: 'launch_date' in settingsToSave,
141
+ currentState_launch_date: currentSettings.launch_date,
142
+ all_keys: Object.keys(settingsToSave),
143
+ });
144
+
145
+ const response = await fetch('/api/plugin-website/settings', {
146
+ method: 'POST',
147
+ headers: { 'Content-Type': 'application/json' },
148
+ credentials: 'include',
149
+ body: JSON.stringify(settingsToSave),
150
+ });
151
+
152
+ if (!response.ok) {
153
+ const error = await response.json();
154
+ throw new Error(error.error || 'Failed to save settings');
155
+ }
156
+
157
+ setShowSuccess(true);
158
+ setTimeout(() => setShowSuccess(false), 3000);
159
+ } catch (error: any) {
160
+ console.error('Failed to save settings:', error);
161
+ alert(error.message || 'Failed to save settings');
162
+ } finally {
163
+ setIsSaving(false);
164
+ }
165
+ };
166
+
167
+ // Add social link
168
+ const handleAddSocial = () => {
169
+ const newId = settings.socials?.length ? Math.max(...settings.socials.map(s => s.id)) + 1 : 1;
170
+ setSettings({
171
+ ...settings,
172
+ socials: [...(settings.socials || []), { id: newId, platform: '', url: '' }],
173
+ });
174
+ };
175
+
176
+ // Update social link
177
+ const handleUpdateSocial = (id: number, field: 'platform' | 'url', value: string) => {
178
+ setSettings({
179
+ ...settings,
180
+ socials: settings.socials?.map(social =>
181
+ social.id === id ? { ...social, [field]: value } : social
182
+ ) || [],
183
+ });
184
+ };
185
+
186
+ // Remove social link
187
+ const handleRemoveSocial = (id: number) => {
188
+ setSettings({
189
+ ...settings,
190
+ socials: settings.socials?.filter(social => social.id !== id) || [],
191
+ });
192
+ };
193
+
194
+ if (isLoading) {
195
+ return (
196
+ <div className="h-full w-full bg-dashboard-card text-dashboard-text flex items-center justify-center">
197
+ <div className="text-center">
198
+ <RefreshCw className="w-8 h-8 animate-spin text-primary mx-auto mb-4" />
199
+ <p className="text-sm text-neutral-500 dark:text-neutral-400">Loading settings...</p>
200
+ </div>
201
+ </div>
202
+ );
203
+ }
204
+
205
+ return (
206
+ <div className="h-full rounded-[2.5rem] w-full bg-dashboard-card text-dashboard-text overflow-y-auto">
207
+ <div className="max-w-6xl mx-auto p-8">
208
+ {/* Header */}
209
+ <div className="flex items-center justify-between mb-8">
210
+ <div>
211
+ <h1 className="text-3xl font-black text-neutral-950 dark:text-white uppercase tracking-tighter mb-2">
212
+ Website Settings
213
+ </h1>
214
+ <p className="text-sm text-neutral-500 dark:text-neutral-400">
215
+ Manage your website identity, contact information, and social links
216
+ </p>
217
+ </div>
218
+ <button
219
+ onClick={handleSave}
220
+ disabled={isSaving}
221
+ className={`inline-flex items-center gap-2 px-6 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-all shadow-lg ${isSaving
222
+ ? 'bg-neutral-400 text-white cursor-not-allowed'
223
+ : showSuccess
224
+ ? 'bg-green-600 text-white'
225
+ : 'bg-primary text-white hover:bg-primary/90'
226
+ }`}
227
+ >
228
+ {isSaving ? (
229
+ <>
230
+ <RefreshCw className="w-4 h-4 animate-spin" />
231
+ Saving...
232
+ </>
233
+ ) : showSuccess ? (
234
+ <>
235
+ <Settings2 className="w-4 h-4" />
236
+ Saved!
237
+ </>
238
+ ) : (
239
+ <>
240
+ <Save className="w-4 h-4" />
241
+ Save Settings
242
+ </>
243
+ )}
244
+ </button>
245
+ </div>
246
+
247
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
248
+ {/* Main Content */}
249
+ <div className="lg:col-span-2 space-y-8">
250
+ {/* SEO & Website Identity */}
251
+ <section className="bg-dashboard-sidebar p-8 rounded-3xl border border-dashboard-border">
252
+ <div className="flex items-center gap-2 font-bold text-neutral-950 dark:text-white border-b border-dashboard-border pb-4 mb-6">
253
+ <Search size={20} className="text-primary" />
254
+ Website Identity & SEO
255
+ </div>
256
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
257
+ <div className="space-y-2">
258
+ <label className="text-xs font-bold text-neutral-500 dark:text-neutral-400 uppercase tracking-widest">
259
+ Website Name
260
+ </label>
261
+ <input
262
+ type="text"
263
+ value={settings.siteName || ''}
264
+ onChange={(e) => setSettings({ ...settings, siteName: e.target.value })}
265
+ placeholder="e.g., Botanics & You"
266
+ className="w-full px-4 py-3 bg-dashboard-card border border-dashboard-border rounded-2xl outline-none focus:ring-2 focus:ring-primary transition-all text-dashboard-text"
267
+ />
268
+ </div>
269
+ <div className="space-y-2">
270
+ <label className="text-xs font-bold text-neutral-500 dark:text-neutral-400 uppercase tracking-widest">
271
+ Website Tagline
272
+ </label>
273
+ <input
274
+ type="text"
275
+ value={settings.siteTagline || ''}
276
+ onChange={(e) => setSettings({ ...settings, siteTagline: e.target.value })}
277
+ placeholder="e.g., Sharing my knowledge with you"
278
+ className="w-full px-4 py-3 bg-dashboard-card border border-dashboard-border rounded-2xl outline-none focus:ring-2 focus:ring-primary transition-all text-dashboard-text"
279
+ />
280
+ </div>
281
+ </div>
282
+ <div className="mt-6 space-y-2">
283
+ <label className="text-xs font-bold text-neutral-500 dark:text-neutral-400 uppercase tracking-widest">
284
+ Website Description
285
+ </label>
286
+ <textarea
287
+ value={settings.siteDescription || ''}
288
+ onChange={(e) => setSettings({ ...settings, siteDescription: e.target.value })}
289
+ placeholder="A brief description of your website"
290
+ rows={3}
291
+ className="w-full px-4 py-3 bg-dashboard-card border border-dashboard-border rounded-2xl outline-none focus:ring-2 focus:ring-primary transition-all text-dashboard-text resize-none"
292
+ />
293
+ </div>
294
+ <div className="mt-6 space-y-2">
295
+ <label className="text-xs font-bold text-neutral-500 dark:text-neutral-400 uppercase tracking-widest">
296
+ Keywords (comma-separated)
297
+ </label>
298
+ <input
299
+ type="text"
300
+ value={settings.keywords || ''}
301
+ onChange={(e) => setSettings({ ...settings, keywords: e.target.value })}
302
+ placeholder="keyword1, keyword2, keyword3"
303
+ className="w-full px-4 py-3 bg-dashboard-card border border-dashboard-border rounded-2xl outline-none focus:ring-2 focus:ring-primary transition-all text-dashboard-text"
304
+ />
305
+ </div>
306
+ </section>
307
+
308
+ {/* Contact Information */}
309
+ <section className="bg-dashboard-sidebar p-8 rounded-3xl border border-dashboard-border">
310
+ <div className="flex items-center gap-2 font-bold text-neutral-950 dark:text-white border-b border-dashboard-border pb-4 mb-6">
311
+ <Mail size={20} className="text-primary" />
312
+ Contact Information
313
+ </div>
314
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
315
+ <div className="space-y-2">
316
+ <label className="text-xs font-bold text-neutral-500 dark:text-neutral-400 uppercase tracking-widest">
317
+ Contact Email
318
+ </label>
319
+ <input
320
+ type="email"
321
+ value={settings.contactEmail || ''}
322
+ onChange={(e) => setSettings({ ...settings, contactEmail: e.target.value })}
323
+ placeholder="contact@example.com"
324
+ className="w-full px-4 py-3 bg-dashboard-card border border-dashboard-border rounded-2xl outline-none focus:ring-2 focus:ring-primary transition-all text-dashboard-text"
325
+ />
326
+ </div>
327
+ <div className="space-y-2">
328
+ <label className="text-xs font-bold text-neutral-500 dark:text-neutral-400 uppercase tracking-widest">
329
+ Phone Number
330
+ </label>
331
+ <input
332
+ type="tel"
333
+ value={settings.phoneNumber || ''}
334
+ onChange={(e) => setSettings({ ...settings, phoneNumber: e.target.value })}
335
+ placeholder="+31 6 12345678"
336
+ className="w-full px-4 py-3 bg-dashboard-card border border-dashboard-border rounded-2xl outline-none focus:ring-2 focus:ring-primary transition-all text-dashboard-text"
337
+ />
338
+ </div>
339
+ </div>
340
+ <div className="mt-6 space-y-2">
341
+ <label className="text-xs font-bold text-neutral-500 dark:text-neutral-400 uppercase tracking-widest">
342
+ Physical Address
343
+ </label>
344
+ <textarea
345
+ value={settings.physicalAddress || ''}
346
+ onChange={(e) => setSettings({ ...settings, physicalAddress: e.target.value })}
347
+ placeholder="Street address, City, Country"
348
+ rows={2}
349
+ className="w-full px-4 py-3 bg-dashboard-card border border-dashboard-border rounded-2xl outline-none focus:ring-2 focus:ring-primary transition-all text-dashboard-text resize-none"
350
+ />
351
+ </div>
352
+ </section>
353
+
354
+ {/* Social Links */}
355
+ <section className="bg-dashboard-sidebar p-8 rounded-3xl border border-dashboard-border">
356
+ <div className="flex items-center justify-between border-b border-dashboard-border pb-4 mb-6">
357
+ <div className="flex items-center gap-2 font-bold text-neutral-950 dark:text-white">
358
+ <Globe size={20} className="text-primary" />
359
+ Social Links
360
+ </div>
361
+ <button
362
+ onClick={handleAddSocial}
363
+ className="px-4 py-2 text-xs font-bold uppercase tracking-widest bg-primary text-white rounded-full hover:bg-primary/90 transition-all"
364
+ >
365
+ Add Social
366
+ </button>
367
+ </div>
368
+ <div className="space-y-4">
369
+ {settings.socials && settings.socials.length > 0 ? (
370
+ settings.socials.map((social) => {
371
+ const platform = AVAILABLE_PLATFORMS.find(p => p.name === social.platform);
372
+ return (
373
+ <div key={social.id} className="flex items-center gap-4 p-4 bg-dashboard-card rounded-2xl border border-dashboard-border">
374
+ <div className="flex-shrink-0 text-neutral-500 dark:text-neutral-400">
375
+ {platform?.icon || <Globe size={18} />}
376
+ </div>
377
+ <select
378
+ value={social.platform}
379
+ onChange={(e) => handleUpdateSocial(social.id, 'platform', e.target.value)}
380
+ className="flex-1 px-4 py-2 bg-dashboard-sidebar border border-dashboard-border rounded-xl outline-none focus:ring-2 focus:ring-primary text-dashboard-text"
381
+ >
382
+ <option value="">Select Platform</option>
383
+ {AVAILABLE_PLATFORMS.map(p => (
384
+ <option key={p.name} value={p.name}>{p.name}</option>
385
+ ))}
386
+ </select>
387
+ <input
388
+ type="url"
389
+ value={social.url}
390
+ onChange={(e) => handleUpdateSocial(social.id, 'url', e.target.value)}
391
+ placeholder="https://..."
392
+ className="flex-1 px-4 py-2 bg-dashboard-sidebar border border-dashboard-border rounded-xl outline-none focus:ring-2 focus:ring-primary text-dashboard-text"
393
+ />
394
+ <button
395
+ onClick={() => handleRemoveSocial(social.id)}
396
+ className="px-4 py-2 text-xs font-bold text-red-500 hover:text-red-700 dark:hover:text-red-400 transition-colors"
397
+ >
398
+ Remove
399
+ </button>
400
+ </div>
401
+ );
402
+ })
403
+ ) : (
404
+ <p className="text-sm text-neutral-500 dark:text-neutral-400 text-center py-8">
405
+ No social links added yet. Click "Add Social" to get started.
406
+ </p>
407
+ )}
408
+ </div>
409
+ </section>
410
+ </div>
411
+
412
+ {/* Sidebar */}
413
+ <div className="space-y-8">
414
+ {/* Maintenance Mode */}
415
+ <section className="bg-dashboard-sidebar p-6 rounded-3xl border border-dashboard-border">
416
+ <div className="flex items-center gap-2 font-bold text-neutral-950 dark:text-white border-b border-dashboard-border pb-4 mb-6">
417
+ <Settings2 size={18} className="text-primary" />
418
+ Maintenance
419
+ </div>
420
+ <div className="flex items-center justify-between">
421
+ <div>
422
+ <label className="text-xs font-bold text-neutral-700 dark:text-neutral-300 block mb-1">
423
+ Maintenance Mode
424
+ </label>
425
+ <p className="text-[10px] text-neutral-500 dark:text-neutral-400">
426
+ Show maintenance page to visitors
427
+ </p>
428
+ </div>
429
+ <button
430
+ onClick={() => setSettings({ ...settings, maintenanceMode: !settings.maintenanceMode })}
431
+ className={`relative w-12 h-6 rounded-full transition-colors ${settings.maintenanceMode ? 'bg-primary' : 'bg-neutral-300 dark:bg-neutral-700'
432
+ }`}
433
+ >
434
+ <div className={`absolute top-1 left-1 w-4 h-4 bg-white rounded-full transition-transform ${settings.maintenanceMode ? 'translate-x-6' : 'translate-x-0'
435
+ }`} />
436
+ </button>
437
+ </div>
438
+ </section>
439
+
440
+ {/* Launch Date */}
441
+ <section className="bg-dashboard-sidebar p-6 rounded-3xl border border-dashboard-border">
442
+ <div className="flex items-center gap-2 font-bold text-neutral-950 dark:text-white border-b border-dashboard-border pb-4 mb-6">
443
+ <Calendar size={18} className="text-primary" />
444
+ Launch Date
445
+ </div>
446
+ <div className="space-y-4">
447
+ <div className="space-y-2">
448
+ <label className="text-xs font-bold text-neutral-700 dark:text-neutral-300 block">
449
+ Date
450
+ </label>
451
+ <input
452
+ type="date"
453
+ value={settings.launch_date ? settings.launch_date.split('T')[0] : ''}
454
+ onChange={(e) => {
455
+ const dateValue = e.target.value;
456
+ const currentTime = settings.launch_date?.includes('T')
457
+ ? settings.launch_date.split('T')[1]
458
+ : '00:00';
459
+
460
+ const newLaunchDate = dateValue ? `${dateValue}T${currentTime}` : '';
461
+
462
+ console.log('[SettingsView] Date changed:', {
463
+ dateValue,
464
+ currentTime,
465
+ newLaunchDate,
466
+ });
467
+
468
+ setSettings(prev => ({
469
+ ...prev,
470
+ launch_date: newLaunchDate,
471
+ }));
472
+ }}
473
+ className="w-full px-4 py-2 bg-dashboard-card border border-dashboard-border rounded-xl outline-none focus:ring-2 focus:ring-primary text-dashboard-text"
474
+ />
475
+ </div>
476
+ <div className="space-y-2">
477
+ <label className="text-xs font-bold text-neutral-700 dark:text-neutral-300 block">
478
+ Time (24-hour format)
479
+ </label>
480
+ <input
481
+ type="time"
482
+ step="60"
483
+ value={settings.launch_date?.includes('T')
484
+ ? settings.launch_date.split('T')[1]
485
+ : '00:00'}
486
+ onChange={(e) => {
487
+ const timeValue = e.target.value;
488
+ const currentDate = settings.launch_date?.includes('T')
489
+ ? settings.launch_date.split('T')[0]
490
+ : new Date().toISOString().split('T')[0];
491
+
492
+ const newLaunchDate = timeValue ? `${currentDate}T${timeValue}` : '';
493
+
494
+ console.log('[SettingsView] Time changed:', {
495
+ timeValue,
496
+ currentDate,
497
+ newLaunchDate,
498
+ });
499
+
500
+ setSettings(prev => ({
501
+ ...prev,
502
+ launch_date: newLaunchDate,
503
+ }));
504
+ }}
505
+ className="w-full px-4 py-2 bg-dashboard-card border border-dashboard-border rounded-xl outline-none focus:ring-2 focus:ring-primary text-dashboard-text"
506
+ />
507
+ </div>
508
+ <p className="text-[10px] text-neutral-500 dark:text-neutral-400">
509
+ Used for countdown timers. Time defaults to 00:00 if not specified.
510
+ </p>
511
+ </div>
512
+ </section>
513
+ </div>
514
+ </div>
515
+ </div>
516
+ </div>
517
+ );
518
+ }
519
+