@jhits/plugin-website 0.0.12 β 0.0.14
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/dist/api/handler.d.ts.map +1 -1
- package/dist/api/handler.js +10 -0
- package/dist/types/settings.d.ts +18 -0
- package/dist/types/settings.d.ts.map +1 -1
- package/dist/views/SettingsView.d.ts.map +1 -1
- package/dist/views/SettingsView.js +120 -89
- package/package.json +4 -4
- package/src/api/handler.ts +10 -0
- package/src/types/settings.ts +22 -0
- package/src/views/SettingsView.tsx +381 -201
|
@@ -6,11 +6,17 @@
|
|
|
6
6
|
'use client';
|
|
7
7
|
|
|
8
8
|
import React, { useState, useEffect, useRef } from 'react';
|
|
9
|
-
import { Save, RefreshCw, Globe,
|
|
10
|
-
import { WebsiteSettings,
|
|
9
|
+
import { Save, RefreshCw, Globe, Globe2, Mail, Search, Settings2, Calendar, Plus, Trash2, X, Edit3 } from 'lucide-react';
|
|
10
|
+
import { WebsiteSettings, SupportedLocale, LocalizedSocialLink, DomainLocaleConfig } from '../types/settings';
|
|
11
11
|
import { FaFacebook, FaInstagram, FaLinkedin, FaPinterest, FaTiktok } from 'react-icons/fa';
|
|
12
12
|
import { FaXTwitter } from 'react-icons/fa6';
|
|
13
13
|
|
|
14
|
+
const AVAILABLE_LOCALES: { code: SupportedLocale; name: string; flag: string }[] = [
|
|
15
|
+
{ code: 'en', name: 'English', flag: 'π¬π§' },
|
|
16
|
+
{ code: 'sv', name: 'Swedish', flag: 'πΈπͺ' },
|
|
17
|
+
{ code: 'nl', name: 'Dutch', flag: 'π³π±' },
|
|
18
|
+
];
|
|
19
|
+
|
|
14
20
|
const AVAILABLE_PLATFORMS = [
|
|
15
21
|
{ name: 'Instagram', icon: <FaInstagram size={18} /> },
|
|
16
22
|
{ name: 'Facebook', icon: <FaFacebook size={18} /> },
|
|
@@ -25,10 +31,211 @@ export interface SettingsViewProps {
|
|
|
25
31
|
locale: string;
|
|
26
32
|
}
|
|
27
33
|
|
|
34
|
+
// =============================================================================
|
|
35
|
+
// Sub-components
|
|
36
|
+
// =============================================================================
|
|
37
|
+
|
|
38
|
+
interface DomainLanguagesCardProps {
|
|
39
|
+
config: DomainLocaleConfig[];
|
|
40
|
+
onEdit: () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function DomainLanguagesCard({ config, onEdit }: DomainLanguagesCardProps) {
|
|
44
|
+
return (
|
|
45
|
+
<section className="bg-dashboard-bg p-6 rounded-3xl border border-dashboard-border">
|
|
46
|
+
<div className="flex items-center justify-between border-b border-dashboard-border pb-4 mb-4">
|
|
47
|
+
<div className="flex items-center gap-2 font-bold text-dashboard-text">
|
|
48
|
+
<Globe2 size={18} className="text-primary" />
|
|
49
|
+
Domain Languages
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
<div className="space-y-2">
|
|
53
|
+
{config && config.length > 0 ? (
|
|
54
|
+
<div className="flex flex-wrap gap-2">
|
|
55
|
+
{config.map((entry, index) => (
|
|
56
|
+
<div key={index} className="inline-flex items-center gap-2 px-3 py-1.5 bg-dashboard-card rounded-full border border-dashboard-border text-xs">
|
|
57
|
+
<span className="font-medium text-dashboard-text">{entry.domain}</span>
|
|
58
|
+
<span className="text-dashboard-text-secondary">β</span>
|
|
59
|
+
<span>{AVAILABLE_LOCALES.find(l => l.code === entry.locale)?.flag}</span>
|
|
60
|
+
<span className="text-dashboard-text-secondary">
|
|
61
|
+
{entry.allowUserOverride ? '' : 'π'}
|
|
62
|
+
</span>
|
|
63
|
+
</div>
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
) : (
|
|
67
|
+
<p className="text-xs text-dashboard-text-secondary">
|
|
68
|
+
No domain languages configured.
|
|
69
|
+
</p>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
<button
|
|
73
|
+
onClick={onEdit}
|
|
74
|
+
className="mt-4 w-full flex items-center justify-center gap-2 px-4 py-2 bg-primary text-white rounded-xl text-xs font-bold uppercase tracking-wider hover:bg-primary/90 transition-colors"
|
|
75
|
+
>
|
|
76
|
+
<Edit3 size={14} />
|
|
77
|
+
{config?.length ? 'Edit Domains' : 'Add Domains'}
|
|
78
|
+
</button>
|
|
79
|
+
</section>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface DomainLanguagesModalProps {
|
|
84
|
+
isOpen: boolean;
|
|
85
|
+
config: DomainLocaleConfig[];
|
|
86
|
+
onSave: (config: DomainLocaleConfig[]) => void;
|
|
87
|
+
onClose: () => void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function DomainLanguagesModal({ isOpen, config, onSave, onClose }: DomainLanguagesModalProps) {
|
|
91
|
+
const [tempConfig, setTempConfig] = useState<DomainLocaleConfig[]>(config);
|
|
92
|
+
const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
setTempConfig(config);
|
|
96
|
+
setExpandedIndex(null);
|
|
97
|
+
}, [config, isOpen]);
|
|
98
|
+
|
|
99
|
+
if (!isOpen) return null;
|
|
100
|
+
|
|
101
|
+
const handleAdd = () => {
|
|
102
|
+
setTempConfig([...tempConfig, { domain: '', locale: 'en', allowUserOverride: true }]);
|
|
103
|
+
setExpandedIndex(tempConfig.length);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const handleUpdate = (index: number, field: keyof DomainLocaleConfig, value: any) => {
|
|
107
|
+
const updated = [...tempConfig];
|
|
108
|
+
updated[index] = { ...updated[index], [field]: value };
|
|
109
|
+
setTempConfig(updated);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const handleRemove = (index: number) => {
|
|
113
|
+
setTempConfig(tempConfig.filter((_, i) => i !== index));
|
|
114
|
+
if (expandedIndex === index) setExpandedIndex(null);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const toggleExpand = (index: number) => {
|
|
118
|
+
setExpandedIndex(expandedIndex === index ? null : index);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
123
|
+
<div className="bg-dashboard-bg rounded-3xl border border-dashboard-border w-full max-w-lg max-h-[80vh] overflow-hidden flex flex-col">
|
|
124
|
+
<div className="flex items-center justify-between p-6 border-b border-dashboard-border">
|
|
125
|
+
<h2 className="text-xl font-bold text-dashboard-text">Domain Languages</h2>
|
|
126
|
+
<button onClick={onClose} className="p-2 text-dashboard-text-secondary hover:text-dashboard-text transition-colors">
|
|
127
|
+
<X size={20} />
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
<div className="flex-1 overflow-y-auto p-6 space-y-4">
|
|
131
|
+
{/* Existing entries - collapsed by default */}
|
|
132
|
+
{tempConfig.length > 0 && (
|
|
133
|
+
<div className="space-y-2">
|
|
134
|
+
<p className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-wider">Current Config</p>
|
|
135
|
+
{tempConfig.map((entry, index) => (
|
|
136
|
+
<div key={index} className="bg-dashboard-card rounded-xl border border-dashboard-border overflow-hidden">
|
|
137
|
+
<div
|
|
138
|
+
onClick={() => toggleExpand(index)}
|
|
139
|
+
className="w-full flex items-center justify-between p-3 text-left hover:bg-dashboard-border/50 transition-colors cursor-pointer"
|
|
140
|
+
>
|
|
141
|
+
<div className="flex items-center gap-2">
|
|
142
|
+
<span className="font-medium text-dashboard-text">{entry.domain || '(no domain)'}</span>
|
|
143
|
+
<span className="text-dashboard-text-secondary">β</span>
|
|
144
|
+
<span>{AVAILABLE_LOCALES.find(l => l.code === entry.locale)?.flag}</span>
|
|
145
|
+
{entry.allowUserOverride === false && <span className="text-dashboard-text-secondary">π</span>}
|
|
146
|
+
</div>
|
|
147
|
+
<div className="flex items-center gap-2">
|
|
148
|
+
<button
|
|
149
|
+
onClick={(e) => { e.stopPropagation(); handleRemove(index); }}
|
|
150
|
+
className="p-1 text-red-500 hover:bg-red-500/10 rounded transition-colors"
|
|
151
|
+
title="Remove"
|
|
152
|
+
>
|
|
153
|
+
<Trash2 size={14} />
|
|
154
|
+
</button>
|
|
155
|
+
<span className="text-dashboard-text-secondary text-xs">
|
|
156
|
+
{expandedIndex === index ? 'β²' : 'βΌ'}
|
|
157
|
+
</span>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
{expandedIndex === index && (
|
|
161
|
+
<div className="p-3 pt-0 space-y-3 border-t border-dashboard-border">
|
|
162
|
+
<div className="space-y-1">
|
|
163
|
+
<label className="text-[10px] font-bold text-dashboard-text-secondary uppercase tracking-wider">Domain</label>
|
|
164
|
+
<input
|
|
165
|
+
type="text"
|
|
166
|
+
value={entry.domain}
|
|
167
|
+
onChange={(e) => handleUpdate(index, 'domain', e.target.value)}
|
|
168
|
+
placeholder="example.com"
|
|
169
|
+
className="w-full px-3 py-2 bg-dashboard-bg border border-dashboard-border rounded-lg outline-none focus:ring-2 focus:ring-primary text-dashboard-text text-sm"
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
172
|
+
<div className="flex items-center gap-2">
|
|
173
|
+
<div className="flex-1 space-y-1">
|
|
174
|
+
<label className="text-[10px] font-bold text-dashboard-text-secondary uppercase tracking-wider">Language</label>
|
|
175
|
+
<select
|
|
176
|
+
value={entry.locale}
|
|
177
|
+
onChange={(e) => handleUpdate(index, 'locale', e.target.value)}
|
|
178
|
+
className="w-full px-3 py-2 bg-dashboard-bg border border-dashboard-border rounded-lg outline-none focus:ring-2 focus:ring-primary text-dashboard-text text-sm"
|
|
179
|
+
>
|
|
180
|
+
{AVAILABLE_LOCALES.map(loc => (<option key={loc.code} value={loc.code}>{loc.flag} {loc.name}</option>))}
|
|
181
|
+
</select>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
<div className="flex items-center justify-between">
|
|
185
|
+
<label className="text-[10px] text-dashboard-text-secondary">Allow user to change</label>
|
|
186
|
+
<button
|
|
187
|
+
onClick={() => handleUpdate(index, 'allowUserOverride', !entry.allowUserOverride)}
|
|
188
|
+
className={`relative w-10 h-5 rounded-full transition-colors ${entry.allowUserOverride ? 'bg-primary' : 'bg-neutral-300 dark:bg-neutral-600'}`}
|
|
189
|
+
>
|
|
190
|
+
<div className={`absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full transition-transform duration-200 ${entry.allowUserOverride ? 'translate-x-5' : 'translate-x-0'}`} />
|
|
191
|
+
</button>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
))}
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
|
|
200
|
+
{/* Add new domain - always visible */}
|
|
201
|
+
<div className="border-t border-dashboard-border pt-4">
|
|
202
|
+
<p className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-wider mb-3">Add New Domain</p>
|
|
203
|
+
<button
|
|
204
|
+
onClick={handleAdd}
|
|
205
|
+
className="w-full flex items-center justify-center gap-2 px-4 py-3 border-2 border-dashed border-dashboard-border text-dashboard-text-secondary hover:border-primary hover:text-primary rounded-xl transition-colors"
|
|
206
|
+
>
|
|
207
|
+
<Plus size={16} />
|
|
208
|
+
Add Domain
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<p className="text-[10px] text-dashboard-text-secondary text-center pt-2">
|
|
213
|
+
Configure which language to use per domain. E.g., botanicsandyou.nl β Dutch, botanicsandyou.se β Swedish.
|
|
214
|
+
</p>
|
|
215
|
+
</div>
|
|
216
|
+
<div className="flex gap-3 p-6 border-t border-dashboard-border">
|
|
217
|
+
<button onClick={onClose} className="flex-1 px-4 py-2 bg-dashboard-card text-dashboard-text rounded-xl text-sm font-bold hover:bg-dashboard-border transition-colors">
|
|
218
|
+
Cancel
|
|
219
|
+
</button>
|
|
220
|
+
<button onClick={() => onSave(tempConfig)} className="flex-1 px-4 py-2 bg-primary text-white rounded-xl text-sm font-bold hover:bg-primary/90 transition-colors">
|
|
221
|
+
Save
|
|
222
|
+
</button>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// =============================================================================
|
|
230
|
+
// Main Component
|
|
231
|
+
// =============================================================================
|
|
232
|
+
|
|
28
233
|
export function SettingsView({ siteId, locale }: SettingsViewProps) {
|
|
29
234
|
const [isLoading, setIsLoading] = useState(true);
|
|
30
235
|
const [isSaving, setIsSaving] = useState(false);
|
|
31
236
|
const [showSuccess, setShowSuccess] = useState(false);
|
|
237
|
+
const [activeTab, setActiveTab] = useState<SupportedLocale>('en');
|
|
238
|
+
const [showDomainModal, setShowDomainModal] = useState(false);
|
|
32
239
|
const [settings, setSettings] = useState<WebsiteSettings>({
|
|
33
240
|
identifier: 'site_config',
|
|
34
241
|
siteName: '',
|
|
@@ -42,6 +249,17 @@ export function SettingsView({ siteId, locale }: SettingsViewProps) {
|
|
|
42
249
|
maintenanceMode: false,
|
|
43
250
|
launch_date: '',
|
|
44
251
|
socials: [],
|
|
252
|
+
localizedContactInfo: {
|
|
253
|
+
en: { physicalAddress: '', phoneNumber: '' },
|
|
254
|
+
sv: { physicalAddress: '', phoneNumber: '' },
|
|
255
|
+
nl: { physicalAddress: '', phoneNumber: '' },
|
|
256
|
+
},
|
|
257
|
+
localizedSocials: {
|
|
258
|
+
en: [],
|
|
259
|
+
sv: [],
|
|
260
|
+
nl: [],
|
|
261
|
+
},
|
|
262
|
+
domainLocaleConfig: [],
|
|
45
263
|
});
|
|
46
264
|
|
|
47
265
|
// Fetch settings on load
|
|
@@ -49,37 +267,21 @@ export function SettingsView({ siteId, locale }: SettingsViewProps) {
|
|
|
49
267
|
const fetchSettings = async () => {
|
|
50
268
|
try {
|
|
51
269
|
setIsLoading(true);
|
|
52
|
-
const response = await fetch('/api/plugin-website/settings', {
|
|
53
|
-
|
|
54
|
-
});
|
|
55
|
-
if (!response.ok) {
|
|
56
|
-
throw new Error('Failed to fetch settings');
|
|
57
|
-
}
|
|
270
|
+
const response = await fetch('/api/plugin-website/settings', { credentials: 'include' });
|
|
271
|
+
if (!response.ok) throw new Error('Failed to fetch settings');
|
|
58
272
|
const data = await response.json();
|
|
273
|
+
|
|
59
274
|
if (data.siteName !== undefined) {
|
|
60
|
-
// Convert launch_date to datetime-local format if it exists
|
|
61
275
|
let launchDate = '';
|
|
62
276
|
if (data.launch_date) {
|
|
63
|
-
// If it's a Date object or ISO string, convert to datetime-local format
|
|
64
277
|
const date = new Date(data.launch_date);
|
|
65
278
|
if (!isNaN(date.getTime())) {
|
|
66
|
-
// Format as YYYY-MM-DDTHH:mm for datetime-local input (24-hour format)
|
|
67
279
|
const year = date.getFullYear();
|
|
68
280
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
69
281
|
const day = String(date.getDate()).padStart(2, '0');
|
|
70
282
|
const hours = String(date.getHours()).padStart(2, '0');
|
|
71
283
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
72
284
|
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
285
|
}
|
|
84
286
|
}
|
|
85
287
|
|
|
@@ -96,6 +298,13 @@ export function SettingsView({ siteId, locale }: SettingsViewProps) {
|
|
|
96
298
|
maintenanceMode: data.maintenanceMode ?? false,
|
|
97
299
|
launch_date: launchDate,
|
|
98
300
|
socials: data.socials || [],
|
|
301
|
+
localizedContactInfo: data.localizedContactInfo || {
|
|
302
|
+
en: { physicalAddress: '', phoneNumber: '' },
|
|
303
|
+
sv: { physicalAddress: '', phoneNumber: '' },
|
|
304
|
+
nl: { physicalAddress: '', phoneNumber: '' },
|
|
305
|
+
},
|
|
306
|
+
localizedSocials: data.localizedSocials || { en: [], sv: [], nl: [] },
|
|
307
|
+
domainLocaleConfig: data.domainLocaleConfig || [],
|
|
99
308
|
});
|
|
100
309
|
}
|
|
101
310
|
} catch (error) {
|
|
@@ -109,39 +318,21 @@ export function SettingsView({ siteId, locale }: SettingsViewProps) {
|
|
|
109
318
|
|
|
110
319
|
// Use ref to always get latest state
|
|
111
320
|
const settingsRef = useRef(settings);
|
|
112
|
-
useEffect(() => {
|
|
113
|
-
settingsRef.current = settings;
|
|
114
|
-
}, [settings]);
|
|
321
|
+
useEffect(() => { settingsRef.current = settings; }, [settings]);
|
|
115
322
|
|
|
116
323
|
// Save settings
|
|
117
324
|
const handleSave = async () => {
|
|
118
325
|
try {
|
|
119
326
|
setIsSaving(true);
|
|
120
|
-
|
|
121
|
-
// Get the latest state from ref
|
|
122
327
|
const currentSettings = settingsRef.current;
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const settingsToSave: any = {
|
|
126
|
-
...currentSettings,
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
// Handle launch_date - keep it if it has a value, otherwise don't include it
|
|
328
|
+
const settingsToSave: any = { ...currentSettings };
|
|
329
|
+
|
|
130
330
|
if (currentSettings.launch_date && currentSettings.launch_date.trim() !== '') {
|
|
131
331
|
settingsToSave.launch_date = currentSettings.launch_date;
|
|
132
332
|
} else {
|
|
133
|
-
// Don't include launch_date if it's empty
|
|
134
333
|
delete settingsToSave.launch_date;
|
|
135
334
|
}
|
|
136
335
|
|
|
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
336
|
const response = await fetch('/api/plugin-website/settings', {
|
|
146
337
|
method: 'POST',
|
|
147
338
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -164,33 +355,60 @@ export function SettingsView({ siteId, locale }: SettingsViewProps) {
|
|
|
164
355
|
}
|
|
165
356
|
};
|
|
166
357
|
|
|
167
|
-
//
|
|
168
|
-
const
|
|
169
|
-
|
|
358
|
+
// Update localized contact info
|
|
359
|
+
const handleUpdateLocalizedContact = (field: 'physicalAddress' | 'phoneNumber', value: string) => {
|
|
360
|
+
setSettings({
|
|
361
|
+
...settings,
|
|
362
|
+
localizedContactInfo: {
|
|
363
|
+
...settings.localizedContactInfo,
|
|
364
|
+
[activeTab]: { ...settings.localizedContactInfo?.[activeTab], [field]: value },
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// Social links handlers
|
|
370
|
+
const handleAddLocalizedSocial = () => {
|
|
371
|
+
const currentSocials = settings.localizedSocials?.[activeTab] || [];
|
|
372
|
+
const newId = currentSocials.length ? Math.max(...currentSocials.map(s => s.id)) + 1 : 1;
|
|
170
373
|
setSettings({
|
|
171
374
|
...settings,
|
|
172
|
-
|
|
375
|
+
localizedSocials: {
|
|
376
|
+
...settings.localizedSocials,
|
|
377
|
+
[activeTab]: [...currentSocials, { id: newId, platform: '', url: '' }],
|
|
378
|
+
},
|
|
173
379
|
});
|
|
174
380
|
};
|
|
175
381
|
|
|
176
|
-
|
|
177
|
-
|
|
382
|
+
const handleUpdateLocalizedSocial = (id: number, field: 'platform' | 'url', value: string) => {
|
|
383
|
+
const currentSocials = settings.localizedSocials?.[activeTab] || [];
|
|
178
384
|
setSettings({
|
|
179
385
|
...settings,
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
386
|
+
localizedSocials: {
|
|
387
|
+
...settings.localizedSocials,
|
|
388
|
+
[activeTab]: currentSocials.map(s => s.id === id ? { ...s, [field]: value } : s),
|
|
389
|
+
},
|
|
183
390
|
});
|
|
184
391
|
};
|
|
185
392
|
|
|
186
|
-
|
|
187
|
-
|
|
393
|
+
const handleRemoveLocalizedSocial = (id: number) => {
|
|
394
|
+
const currentSocials = settings.localizedSocials?.[activeTab] || [];
|
|
188
395
|
setSettings({
|
|
189
396
|
...settings,
|
|
190
|
-
|
|
397
|
+
localizedSocials: {
|
|
398
|
+
...settings.localizedSocials,
|
|
399
|
+
[activeTab]: currentSocials.filter(s => s.id !== id),
|
|
400
|
+
},
|
|
191
401
|
});
|
|
192
402
|
};
|
|
193
403
|
|
|
404
|
+
// Domain locale handlers
|
|
405
|
+
const openDomainModal = () => setShowDomainModal(true);
|
|
406
|
+
const closeDomainModal = () => setShowDomainModal(false);
|
|
407
|
+
const saveDomainConfig = (config: DomainLocaleConfig[]) => {
|
|
408
|
+
setSettings({ ...settings, domainLocaleConfig: config });
|
|
409
|
+
setShowDomainModal(false);
|
|
410
|
+
};
|
|
411
|
+
|
|
194
412
|
if (isLoading) {
|
|
195
413
|
return (
|
|
196
414
|
<div className="h-full w-full bg-dashboard-card text-dashboard-text flex items-center justify-center">
|
|
@@ -218,29 +436,15 @@ export function SettingsView({ siteId, locale }: SettingsViewProps) {
|
|
|
218
436
|
<button
|
|
219
437
|
onClick={handleSave}
|
|
220
438
|
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 shadow-primary/20 ${
|
|
222
|
-
? 'bg-neutral-400 text-white cursor-not-allowed'
|
|
223
|
-
:
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}`}
|
|
439
|
+
className={`inline-flex items-center gap-2 px-6 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-all shadow-lg shadow-primary/20 ${
|
|
440
|
+
isSaving ? 'bg-neutral-400 text-white cursor-not-allowed' :
|
|
441
|
+
showSuccess ? 'bg-green-600 text-white' :
|
|
442
|
+
'bg-primary text-white hover:bg-primary/90'
|
|
443
|
+
}`}
|
|
227
444
|
>
|
|
228
|
-
{isSaving ?
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
)}
|
|
445
|
+
{isSaving ? <><RefreshCw className="w-4 h-4 animate-spin" /> Saving...</> :
|
|
446
|
+
showSuccess ? <><Settings2 className="w-4 h-4" /> Saved!</> :
|
|
447
|
+
<><Save className="w-4 h-4" /> Save Settings</>}
|
|
244
448
|
</button>
|
|
245
449
|
</div>
|
|
246
450
|
|
|
@@ -255,9 +459,7 @@ export function SettingsView({ siteId, locale }: SettingsViewProps) {
|
|
|
255
459
|
</div>
|
|
256
460
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
257
461
|
<div className="space-y-2">
|
|
258
|
-
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest">
|
|
259
|
-
Website Name
|
|
260
|
-
</label>
|
|
462
|
+
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest">Website Name</label>
|
|
261
463
|
<input
|
|
262
464
|
type="text"
|
|
263
465
|
value={settings.siteName || ''}
|
|
@@ -267,9 +469,7 @@ export function SettingsView({ siteId, locale }: SettingsViewProps) {
|
|
|
267
469
|
/>
|
|
268
470
|
</div>
|
|
269
471
|
<div className="space-y-2">
|
|
270
|
-
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest">
|
|
271
|
-
Website Tagline
|
|
272
|
-
</label>
|
|
472
|
+
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest">Website Tagline</label>
|
|
273
473
|
<input
|
|
274
474
|
type="text"
|
|
275
475
|
value={settings.siteTagline || ''}
|
|
@@ -280,9 +480,7 @@ export function SettingsView({ siteId, locale }: SettingsViewProps) {
|
|
|
280
480
|
</div>
|
|
281
481
|
</div>
|
|
282
482
|
<div className="mt-6 space-y-2">
|
|
283
|
-
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest">
|
|
284
|
-
Website Description
|
|
285
|
-
</label>
|
|
483
|
+
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest">Website Description</label>
|
|
286
484
|
<textarea
|
|
287
485
|
value={settings.siteDescription || ''}
|
|
288
486
|
onChange={(e) => setSettings({ ...settings, siteDescription: e.target.value })}
|
|
@@ -292,9 +490,7 @@ export function SettingsView({ siteId, locale }: SettingsViewProps) {
|
|
|
292
490
|
/>
|
|
293
491
|
</div>
|
|
294
492
|
<div className="mt-6 space-y-2">
|
|
295
|
-
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest">
|
|
296
|
-
Keywords (comma-separated)
|
|
297
|
-
</label>
|
|
493
|
+
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest">Keywords (comma-separated)</label>
|
|
298
494
|
<input
|
|
299
495
|
type="text"
|
|
300
496
|
value={settings.keywords || ''}
|
|
@@ -311,11 +507,23 @@ export function SettingsView({ siteId, locale }: SettingsViewProps) {
|
|
|
311
507
|
<Mail size={20} className="text-primary" />
|
|
312
508
|
Contact Information
|
|
313
509
|
</div>
|
|
510
|
+
<div className="flex gap-2 mb-6">
|
|
511
|
+
{AVAILABLE_LOCALES.map((loc) => (
|
|
512
|
+
<button
|
|
513
|
+
key={loc.code}
|
|
514
|
+
onClick={() => setActiveTab(loc.code)}
|
|
515
|
+
className={`px-4 py-2 rounded-full text-xs font-bold uppercase tracking-widest transition-all ${
|
|
516
|
+
activeTab === loc.code ? 'bg-primary text-white' : 'bg-dashboard-card text-dashboard-text-secondary hover:bg-dashboard-border'
|
|
517
|
+
}`}
|
|
518
|
+
>
|
|
519
|
+
<span className="mr-1">{loc.flag}</span>
|
|
520
|
+
{loc.code.toUpperCase()}
|
|
521
|
+
</button>
|
|
522
|
+
))}
|
|
523
|
+
</div>
|
|
314
524
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
315
525
|
<div className="space-y-2">
|
|
316
|
-
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest">
|
|
317
|
-
Contact Email
|
|
318
|
-
</label>
|
|
526
|
+
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest">Contact Email (global)</label>
|
|
319
527
|
<input
|
|
320
528
|
type="email"
|
|
321
529
|
value={settings.contactEmail || ''}
|
|
@@ -325,25 +533,21 @@ export function SettingsView({ siteId, locale }: SettingsViewProps) {
|
|
|
325
533
|
/>
|
|
326
534
|
</div>
|
|
327
535
|
<div className="space-y-2">
|
|
328
|
-
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest">
|
|
329
|
-
Phone Number
|
|
330
|
-
</label>
|
|
536
|
+
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest">Phone Number ({activeTab.toUpperCase()})</label>
|
|
331
537
|
<input
|
|
332
538
|
type="tel"
|
|
333
|
-
value={settings.phoneNumber || ''}
|
|
334
|
-
onChange={(e) =>
|
|
539
|
+
value={settings.localizedContactInfo?.[activeTab]?.phoneNumber || settings.phoneNumber || ''}
|
|
540
|
+
onChange={(e) => handleUpdateLocalizedContact('phoneNumber', e.target.value)}
|
|
335
541
|
placeholder="+31 6 12345678"
|
|
336
542
|
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
543
|
/>
|
|
338
544
|
</div>
|
|
339
545
|
</div>
|
|
340
546
|
<div className="mt-6 space-y-2">
|
|
341
|
-
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest">
|
|
342
|
-
Physical Address
|
|
343
|
-
</label>
|
|
547
|
+
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest">Physical Address ({activeTab.toUpperCase()})</label>
|
|
344
548
|
<textarea
|
|
345
|
-
value={settings.physicalAddress || ''}
|
|
346
|
-
onChange={(e) =>
|
|
549
|
+
value={settings.localizedContactInfo?.[activeTab]?.physicalAddress || settings.physicalAddress || ''}
|
|
550
|
+
onChange={(e) => handleUpdateLocalizedContact('physicalAddress', e.target.value)}
|
|
347
551
|
placeholder="Street address, City, Country"
|
|
348
552
|
rows={2}
|
|
349
553
|
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"
|
|
@@ -351,60 +555,69 @@ export function SettingsView({ siteId, locale }: SettingsViewProps) {
|
|
|
351
555
|
</div>
|
|
352
556
|
</section>
|
|
353
557
|
|
|
354
|
-
{/* Social Links */}
|
|
558
|
+
{/* Social Links (Localized) */}
|
|
355
559
|
<section className="bg-dashboard-bg p-8 rounded-3xl border border-dashboard-border">
|
|
356
560
|
<div className="flex items-center justify-between border-b border-dashboard-border pb-4 mb-6">
|
|
357
561
|
<div className="flex items-center gap-2 font-bold text-dashboard-text">
|
|
358
562
|
<Globe size={20} className="text-primary" />
|
|
359
|
-
Social Links
|
|
563
|
+
Social Links ({activeTab.toUpperCase()})
|
|
360
564
|
</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
|
-
>
|
|
565
|
+
<button onClick={handleAddLocalizedSocial} className="px-4 py-2 text-xs font-bold uppercase tracking-widest bg-primary text-white rounded-full hover:bg-primary/90 transition-all">
|
|
365
566
|
Add Social
|
|
366
567
|
</button>
|
|
367
568
|
</div>
|
|
569
|
+
<div className="flex gap-2 mb-6">
|
|
570
|
+
{AVAILABLE_LOCALES.map((loc) => (
|
|
571
|
+
<button
|
|
572
|
+
key={loc.code}
|
|
573
|
+
onClick={() => setActiveTab(loc.code)}
|
|
574
|
+
className={`px-4 py-2 rounded-full text-xs font-bold uppercase tracking-widest transition-all ${
|
|
575
|
+
activeTab === loc.code ? 'bg-primary text-white' : 'bg-dashboard-card text-dashboard-text-secondary hover:bg-dashboard-border'
|
|
576
|
+
}`}
|
|
577
|
+
>
|
|
578
|
+
<span className="mr-1">{loc.flag}</span>
|
|
579
|
+
{loc.code.toUpperCase()}
|
|
580
|
+
</button>
|
|
581
|
+
))}
|
|
582
|
+
</div>
|
|
368
583
|
<div className="space-y-4">
|
|
369
|
-
{
|
|
370
|
-
settings.
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
584
|
+
{(() => {
|
|
585
|
+
const currentSocials = settings.localizedSocials?.[activeTab] || [];
|
|
586
|
+
return currentSocials.length > 0 ? (
|
|
587
|
+
currentSocials.map((social) => {
|
|
588
|
+
const platform = AVAILABLE_PLATFORMS.find(p => p.name === social.platform);
|
|
589
|
+
return (
|
|
590
|
+
<div key={social.id} className="flex items-center gap-4 p-4 bg-dashboard-card rounded-2xl border border-dashboard-border">
|
|
591
|
+
<div className="flex-shrink-0 text-dashboard-text-secondary">
|
|
592
|
+
{platform?.icon || <Globe size={18} />}
|
|
593
|
+
</div>
|
|
594
|
+
<select
|
|
595
|
+
value={social.platform}
|
|
596
|
+
onChange={(e) => handleUpdateLocalizedSocial(social.id, 'platform', e.target.value)}
|
|
597
|
+
className="flex-1 px-4 py-2 bg-dashboard-bg border border-dashboard-border rounded-xl outline-none focus:ring-2 focus:ring-primary text-dashboard-text"
|
|
598
|
+
>
|
|
599
|
+
<option value="">Select Platform</option>
|
|
600
|
+
{AVAILABLE_PLATFORMS.map(p => (<option key={p.name} value={p.name}>{p.name}</option>))}
|
|
601
|
+
</select>
|
|
602
|
+
<input
|
|
603
|
+
type="url"
|
|
604
|
+
value={social.url}
|
|
605
|
+
onChange={(e) => handleUpdateLocalizedSocial(social.id, 'url', e.target.value)}
|
|
606
|
+
placeholder="https://..."
|
|
607
|
+
className="flex-1 px-4 py-2 bg-dashboard-bg border border-dashboard-border rounded-xl outline-none focus:ring-2 focus:ring-primary text-dashboard-text"
|
|
608
|
+
/>
|
|
609
|
+
<button onClick={() => handleRemoveLocalizedSocial(social.id)} className="px-4 py-2 text-xs font-bold text-red-500 hover:text-red-700 transition-colors">
|
|
610
|
+
Remove
|
|
611
|
+
</button>
|
|
376
612
|
</div>
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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-bg 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-dashboard-text-secondary text-center py-8">
|
|
405
|
-
No social links added yet. Click "Add Social" to get started.
|
|
406
|
-
</p>
|
|
407
|
-
)}
|
|
613
|
+
);
|
|
614
|
+
})
|
|
615
|
+
) : (
|
|
616
|
+
<p className="text-sm text-dashboard-text-secondary text-center py-8">
|
|
617
|
+
No social links added for {activeTab.toUpperCase()}. Click "Add Social" to get started.
|
|
618
|
+
</p>
|
|
619
|
+
);
|
|
620
|
+
})()}
|
|
408
621
|
</div>
|
|
409
622
|
</section>
|
|
410
623
|
</div>
|
|
@@ -419,21 +632,14 @@ export function SettingsView({ siteId, locale }: SettingsViewProps) {
|
|
|
419
632
|
</div>
|
|
420
633
|
<div className="flex items-center justify-between">
|
|
421
634
|
<div>
|
|
422
|
-
<label className="text-xs font-bold text-dashboard-text block mb-1">
|
|
423
|
-
|
|
424
|
-
</label>
|
|
425
|
-
<p className="text-[10px] text-dashboard-text-secondary">
|
|
426
|
-
Show maintenance page to visitors
|
|
427
|
-
</p>
|
|
635
|
+
<label className="text-xs font-bold text-dashboard-text block mb-1">Maintenance Mode</label>
|
|
636
|
+
<p className="text-[10px] text-dashboard-text-secondary">Show maintenance page to visitors</p>
|
|
428
637
|
</div>
|
|
429
638
|
<button
|
|
430
639
|
onClick={() => setSettings({ ...settings, maintenanceMode: !settings.maintenanceMode })}
|
|
431
|
-
className={`relative w-12 h-6 rounded-full transition-colors ${settings.maintenanceMode ? 'bg-primary' : 'bg-neutral-200 dark:bg-neutral-700'
|
|
432
|
-
}`}
|
|
640
|
+
className={`relative w-12 h-6 rounded-full transition-colors ${settings.maintenanceMode ? 'bg-primary' : 'bg-neutral-200 dark:bg-neutral-700'}`}
|
|
433
641
|
>
|
|
434
|
-
<div
|
|
435
|
-
className={`absolute top-1 left-1 w-4 h-4 bg-white rounded-full transition-transform duration-200 ${settings.maintenanceMode ? 'translate-x-6' : 'translate-x-0'}`}
|
|
436
|
-
/>
|
|
642
|
+
<div className={`absolute top-1 left-1 w-4 h-4 bg-white rounded-full transition-transform duration-200 ${settings.maintenanceMode ? 'translate-x-6' : 'translate-x-0'}`} />
|
|
437
643
|
</button>
|
|
438
644
|
</div>
|
|
439
645
|
</section>
|
|
@@ -446,75 +652,49 @@ export function SettingsView({ siteId, locale }: SettingsViewProps) {
|
|
|
446
652
|
</div>
|
|
447
653
|
<div className="space-y-4">
|
|
448
654
|
<div className="space-y-2">
|
|
449
|
-
<label className="text-xs font-bold text-dashboard-text block">
|
|
450
|
-
Date
|
|
451
|
-
</label>
|
|
655
|
+
<label className="text-xs font-bold text-dashboard-text block">Date</label>
|
|
452
656
|
<input
|
|
453
657
|
type="date"
|
|
454
658
|
value={settings.launch_date ? settings.launch_date.split('T')[0] : ''}
|
|
455
659
|
onChange={(e) => {
|
|
456
660
|
const dateValue = e.target.value;
|
|
457
|
-
const currentTime = settings.launch_date?.includes('T')
|
|
458
|
-
|
|
459
|
-
: '00:00';
|
|
460
|
-
|
|
461
|
-
const newLaunchDate = dateValue ? `${dateValue}T${currentTime}` : '';
|
|
462
|
-
|
|
463
|
-
console.log('[SettingsView] Date changed:', {
|
|
464
|
-
dateValue,
|
|
465
|
-
currentTime,
|
|
466
|
-
newLaunchDate,
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
setSettings(prev => ({
|
|
470
|
-
...prev,
|
|
471
|
-
launch_date: newLaunchDate,
|
|
472
|
-
}));
|
|
661
|
+
const currentTime = settings.launch_date?.includes('T') ? settings.launch_date.split('T')[1] : '00:00';
|
|
662
|
+
setSettings(prev => ({ ...prev, launch_date: dateValue ? `${dateValue}T${currentTime}` : '' }));
|
|
473
663
|
}}
|
|
474
664
|
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"
|
|
475
665
|
/>
|
|
476
666
|
</div>
|
|
477
667
|
<div className="space-y-2">
|
|
478
|
-
<label className="text-xs font-bold text-dashboard-text block">
|
|
479
|
-
Time (24-hour format)
|
|
480
|
-
</label>
|
|
668
|
+
<label className="text-xs font-bold text-dashboard-text block">Time (24-hour format)</label>
|
|
481
669
|
<input
|
|
482
670
|
type="time"
|
|
483
671
|
step="60"
|
|
484
|
-
value={settings.launch_date?.includes('T')
|
|
485
|
-
? settings.launch_date.split('T')[1]
|
|
486
|
-
: '00:00'}
|
|
672
|
+
value={settings.launch_date?.includes('T') ? settings.launch_date.split('T')[1] : '00:00'}
|
|
487
673
|
onChange={(e) => {
|
|
488
674
|
const timeValue = e.target.value;
|
|
489
|
-
const currentDate = settings.launch_date?.includes('T')
|
|
490
|
-
|
|
491
|
-
: new Date().toISOString().split('T')[0];
|
|
492
|
-
|
|
493
|
-
const newLaunchDate = timeValue ? `${currentDate}T${timeValue}` : '';
|
|
494
|
-
|
|
495
|
-
console.log('[SettingsView] Time changed:', {
|
|
496
|
-
timeValue,
|
|
497
|
-
currentDate,
|
|
498
|
-
newLaunchDate,
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
setSettings(prev => ({
|
|
502
|
-
...prev,
|
|
503
|
-
launch_date: newLaunchDate,
|
|
504
|
-
}));
|
|
675
|
+
const currentDate = settings.launch_date?.includes('T') ? settings.launch_date.split('T')[0] : new Date().toISOString().split('T')[0];
|
|
676
|
+
setSettings(prev => ({ ...prev, launch_date: timeValue ? `${currentDate}T${timeValue}` : '' }));
|
|
505
677
|
}}
|
|
506
678
|
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"
|
|
507
679
|
/>
|
|
508
680
|
</div>
|
|
509
|
-
<p className="text-[10px] text-dashboard-text-secondary">
|
|
510
|
-
Used for countdown timers. Time defaults to 00:00 if not specified.
|
|
511
|
-
</p>
|
|
681
|
+
<p className="text-[10px] text-dashboard-text-secondary">Used for countdown timers.</p>
|
|
512
682
|
</div>
|
|
513
683
|
</section>
|
|
684
|
+
|
|
685
|
+
{/* Domain Languages Card */}
|
|
686
|
+
<DomainLanguagesCard config={settings.domainLocaleConfig || []} onEdit={openDomainModal} />
|
|
514
687
|
</div>
|
|
515
688
|
</div>
|
|
516
689
|
</div>
|
|
690
|
+
|
|
691
|
+
{/* Domain Languages Modal */}
|
|
692
|
+
<DomainLanguagesModal
|
|
693
|
+
isOpen={showDomainModal}
|
|
694
|
+
config={settings.domainLocaleConfig || []}
|
|
695
|
+
onSave={saveDomainConfig}
|
|
696
|
+
onClose={closeDomainModal}
|
|
697
|
+
/>
|
|
517
698
|
</div>
|
|
518
699
|
);
|
|
519
700
|
}
|
|
520
|
-
|