@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 +45 -0
- package/src/api/handler.ts +157 -0
- package/src/api/router.ts +47 -0
- package/src/index.server.ts +10 -0
- package/src/index.tsx +30 -0
- package/src/types/settings.ts +36 -0
- package/src/views/SettingsView.tsx +519 -0
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
|
+
|