@jhits/plugin-website 0.0.16 → 0.0.17
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/131._entry.js +2 -0
- package/dist/131._entry.js.LICENSE.txt +6 -0
- package/dist/303._entry.js +1 -0
- package/dist/_entry.js +2 -0
- package/dist/_entry.js.LICENSE.txt +6 -0
- package/dist/api/handler.d.ts.map +1 -1
- package/dist/api/handler.js +15 -4
- package/dist/remoteEntry.js +1 -0
- package/dist/server.js +232 -0
- package/dist/types/settings.d.ts +18 -11
- package/dist/types/settings.d.ts.map +1 -1
- package/dist/views/Settings/components/ContactInfoSection.d.ts +12 -0
- package/dist/views/Settings/components/ContactInfoSection.d.ts.map +1 -0
- package/dist/views/Settings/components/ContactInfoSection.js +14 -0
- package/dist/views/Settings/components/DomainLanguagesCard.d.ts +8 -0
- package/dist/views/Settings/components/DomainLanguagesCard.d.ts.map +1 -0
- package/dist/views/Settings/components/DomainLanguagesCard.js +12 -0
- package/dist/views/Settings/components/DomainLanguagesModal.d.ts +12 -0
- package/dist/views/Settings/components/DomainLanguagesModal.d.ts.map +1 -0
- package/dist/views/Settings/components/DomainLanguagesModal.js +38 -0
- package/dist/views/Settings/components/FlagIcon.d.ts +6 -0
- package/dist/views/Settings/components/FlagIcon.d.ts.map +1 -0
- package/dist/views/Settings/components/FlagIcon.js +11 -0
- package/dist/views/Settings/components/LaunchDateSection.d.ts +8 -0
- package/dist/views/Settings/components/LaunchDateSection.d.ts.map +1 -0
- package/dist/views/Settings/components/LaunchDateSection.js +22 -0
- package/dist/views/Settings/components/LocalesManagementCard.d.ts +8 -0
- package/dist/views/Settings/components/LocalesManagementCard.d.ts.map +1 -0
- package/dist/views/Settings/components/LocalesManagementCard.js +66 -0
- package/dist/views/Settings/components/LocalizedContentSection.d.ts +12 -0
- package/dist/views/Settings/components/LocalizedContentSection.d.ts.map +1 -0
- package/dist/views/Settings/components/LocalizedContentSection.js +17 -0
- package/dist/views/Settings/components/SeoIdentitySection.d.ts +9 -0
- package/dist/views/Settings/components/SeoIdentitySection.d.ts.map +1 -0
- package/dist/views/Settings/components/SeoIdentitySection.js +12 -0
- package/dist/views/Settings/components/SocialLinksSection.d.ts +13 -0
- package/dist/views/Settings/components/SocialLinksSection.d.ts.map +1 -0
- package/dist/views/Settings/components/SocialLinksSection.js +13 -0
- package/dist/views/Settings/components/StatusSection.d.ts +8 -0
- package/dist/views/Settings/components/StatusSection.d.ts.map +1 -0
- package/dist/views/Settings/components/StatusSection.js +27 -0
- package/dist/views/Settings/constants.d.ts +12 -0
- package/dist/views/Settings/constants.d.ts.map +1 -0
- package/dist/views/Settings/constants.js +23 -0
- package/dist/views/Settings/hooks/useWebsiteSettings.d.ts +24 -0
- package/dist/views/Settings/hooks/useWebsiteSettings.d.ts.map +1 -0
- package/dist/views/Settings/hooks/useWebsiteSettings.js +217 -0
- package/dist/views/Settings/types.d.ts +5 -0
- package/dist/views/Settings/types.d.ts.map +1 -0
- package/dist/views/Settings/types.js +1 -0
- package/dist/views/SettingsView.d.ts +3 -6
- package/dist/views/SettingsView.d.ts.map +1 -1
- package/dist/views/SettingsView.js +59 -262
- package/package.json +14 -10
- package/src/api/handler.ts +41 -4
- package/src/types/settings.ts +28 -12
- package/src/views/Settings/components/ContactInfoSection.tsx +109 -0
- package/src/views/Settings/components/DomainLanguagesCard.tsx +91 -0
- package/src/views/Settings/components/DomainLanguagesModal.tsx +216 -0
- package/src/views/Settings/components/FlagIcon.tsx +20 -0
- package/src/views/Settings/components/LaunchDateSection.tsx +111 -0
- package/src/views/Settings/components/LocalesManagementCard.tsx +198 -0
- package/src/views/Settings/components/LocalizedContentSection.tsx +155 -0
- package/src/views/Settings/components/SeoIdentitySection.tsx +98 -0
- package/src/views/Settings/components/SocialLinksSection.tsx +98 -0
- package/src/views/Settings/components/StatusSection.tsx +181 -0
- package/src/views/Settings/constants.ts +26 -0
- package/src/views/Settings/hooks/useWebsiteSettings.ts +240 -0
- package/src/views/Settings/types.ts +4 -0
- package/src/views/SettingsView.tsx +226 -674
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
import { Globe, Plus, Search, Loader2, X, Sparkles, Info } from 'lucide-react';
|
|
4
|
+
import { LocaleDefinition } from '../../../types/settings';
|
|
5
|
+
import { GLOBAL_LOCALE_DATABASE } from '../constants';
|
|
6
|
+
import { FlagIcon } from './FlagIcon';
|
|
7
|
+
import { clsx, type ClassValue } from 'clsx';
|
|
8
|
+
import { twMerge } from 'tailwind-merge';
|
|
9
|
+
|
|
10
|
+
function cn(...inputs: ClassValue[]) {
|
|
11
|
+
return twMerge(clsx(inputs));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface LocalesManagementCardProps {
|
|
15
|
+
supportedLocales: LocaleDefinition[];
|
|
16
|
+
onUpdate: (locales: LocaleDefinition[]) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function LocalesManagementCard({ supportedLocales, onUpdate }: LocalesManagementCardProps) {
|
|
20
|
+
const [searchCode, setSearchCode] = useState('');
|
|
21
|
+
const [isSearching, setIsSearching] = useState(false);
|
|
22
|
+
const [searchError, setSearchError] = useState('');
|
|
23
|
+
|
|
24
|
+
const handleToggleLocale = (locale: LocaleDefinition) => {
|
|
25
|
+
const isEnabled = supportedLocales.some(l => l.code === locale.code);
|
|
26
|
+
if (isEnabled) {
|
|
27
|
+
if (supportedLocales.length <= 1) {
|
|
28
|
+
alert('You must have at least one language enabled.');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
onUpdate(supportedLocales.filter(l => l.code !== locale.code));
|
|
32
|
+
} else {
|
|
33
|
+
onUpdate([...supportedLocales, locale]);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleSearchAndAdd = async () => {
|
|
38
|
+
const code = searchCode.trim().toLowerCase();
|
|
39
|
+
if (!code) return;
|
|
40
|
+
|
|
41
|
+
if (supportedLocales.some(l => l.code === code || l.countryCode === code)) {
|
|
42
|
+
setSearchError('Language already added');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setIsSearching(true);
|
|
47
|
+
setSearchError('');
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const response = await fetch(`https://restcountries.com/v3.1/alpha/${code}`);
|
|
51
|
+
if (!response.ok) throw new Error('Country not found');
|
|
52
|
+
|
|
53
|
+
const data = await response.json();
|
|
54
|
+
const country = data[0];
|
|
55
|
+
|
|
56
|
+
if (country) {
|
|
57
|
+
const newLocale: LocaleDefinition = {
|
|
58
|
+
code: code,
|
|
59
|
+
name: country.name.common,
|
|
60
|
+
countryCode: country.cca2.toLowerCase()
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
onUpdate([...supportedLocales, newLocale]);
|
|
64
|
+
setSearchCode('');
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
setSearchError('Could not find country code');
|
|
68
|
+
} finally {
|
|
69
|
+
setIsSearching(false);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<section className="space-y-8">
|
|
75
|
+
<div className="flex items-center gap-4">
|
|
76
|
+
<div className="size-10 rounded-xl bg-primary/10 flex items-center justify-center text-primary border border-primary/20 shadow-inner">
|
|
77
|
+
<Globe size={20} />
|
|
78
|
+
</div>
|
|
79
|
+
<h3 className="text-2xl lg:text-3xl font-bold text-dashboard-text leading-none tracking-tight">
|
|
80
|
+
Localization <span className="text-primary">Matrix</span>
|
|
81
|
+
</h3>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<motion.div
|
|
85
|
+
initial={{ opacity: 0, y: 20 }}
|
|
86
|
+
animate={{ opacity: 1, y: 0 }}
|
|
87
|
+
className="bg-dashboard-card/50 border border-dashboard-border/40 p-8 rounded-[2rem] space-y-8 relative overflow-hidden"
|
|
88
|
+
>
|
|
89
|
+
{/* Active / Enabled Languages */}
|
|
90
|
+
<div className="space-y-4">
|
|
91
|
+
<div className="flex items-center gap-3 ml-2">
|
|
92
|
+
<Sparkles size={12} className="text-primary animate-pulse" />
|
|
93
|
+
<span className="text-[10px] font-bold uppercase tracking-[0.2em] text-primary/60">Active Translation Nodes</span>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
97
|
+
<AnimatePresence mode="popLayout">
|
|
98
|
+
{supportedLocales.length > 0 ? (
|
|
99
|
+
supportedLocales.map((loc) => (
|
|
100
|
+
<motion.div
|
|
101
|
+
layout
|
|
102
|
+
initial={{ opacity: 0, scale: 0.95 }}
|
|
103
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
104
|
+
exit={{ opacity: 0, scale: 0.95 }}
|
|
105
|
+
key={loc.code}
|
|
106
|
+
className="flex items-center justify-between p-4 bg-dashboard-bg/30 border border-dashboard-border/40 rounded-xl group hover:border-primary/30 transition-all duration-500 shadow-sm"
|
|
107
|
+
>
|
|
108
|
+
<div className="flex items-center gap-4">
|
|
109
|
+
<div className="size-10 rounded-lg bg-dashboard-card/50 flex items-center justify-center border border-dashboard-border/40 group-hover:bg-primary/10 group-hover:border-primary/20 transition-all duration-500 shadow-inner">
|
|
110
|
+
<FlagIcon countryCode={loc.countryCode} />
|
|
111
|
+
</div>
|
|
112
|
+
<div className="flex flex-col">
|
|
113
|
+
<span className="text-sm font-bold text-dashboard-text/90 group-hover:text-primary transition-colors">{loc.name}</span>
|
|
114
|
+
<span className="text-[10px] text-dashboard-text-secondary/70 font-bold tracking-widest uppercase mt-0.5">{loc.code}</span>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
<button
|
|
118
|
+
onClick={() => handleToggleLocale(loc)}
|
|
119
|
+
className="p-2 text-dashboard-text-secondary/40 hover:text-red-500 hover:bg-red-500/10 rounded-lg opacity-0 group-hover:opacity-100 transition-all active:scale-90"
|
|
120
|
+
>
|
|
121
|
+
<X size={16} />
|
|
122
|
+
</button>
|
|
123
|
+
</motion.div>
|
|
124
|
+
))
|
|
125
|
+
) : (
|
|
126
|
+
<div className="col-span-2 py-12 text-center bg-dashboard-card/30 border-dashed border border-dashboard-border/40 rounded-2xl">
|
|
127
|
+
<p className="text-xs text-dashboard-text-secondary/60 font-medium">No active localization nodes detected.</p>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
</AnimatePresence>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{/* Search & Add New */}
|
|
135
|
+
<div className="space-y-4 pt-8 border-t border-dashboard-border/40">
|
|
136
|
+
<div className="flex items-center justify-between px-2">
|
|
137
|
+
<span className="text-[10px] font-bold uppercase tracking-[0.2em] text-primary/60">Initialize New Region</span>
|
|
138
|
+
{searchError && <p className="text-[10px] font-bold text-red-500 animate-pulse">{searchError}</p>}
|
|
139
|
+
</div>
|
|
140
|
+
<div className="flex gap-3">
|
|
141
|
+
<div className="relative flex-1">
|
|
142
|
+
<input
|
|
143
|
+
type="text"
|
|
144
|
+
value={searchCode}
|
|
145
|
+
onChange={(e) => setSearchCode(e.target.value.toUpperCase())}
|
|
146
|
+
onKeyDown={(e) => e.key === 'Enter' && handleSearchAndAdd()}
|
|
147
|
+
placeholder="COUNTRY CODE (e.g. BE, JP, FR)"
|
|
148
|
+
className="w-full pl-6 pr-12 py-3 bg-dashboard-bg/50 border border-dashboard-border/40 rounded-xl text-xs font-bold uppercase tracking-wider outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary/30 transition-all text-dashboard-text placeholder:text-dashboard-text-secondary/40"
|
|
149
|
+
maxLength={3}
|
|
150
|
+
/>
|
|
151
|
+
{isSearching && (
|
|
152
|
+
<Loader2 className="absolute right-4 top-1/2 -translate-y-1/2 size-4 animate-spin text-primary" />
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
<button
|
|
156
|
+
onClick={handleSearchAndAdd}
|
|
157
|
+
disabled={isSearching || !searchCode}
|
|
158
|
+
className="px-6 bg-primary text-white rounded-xl hover:bg-primary/90 transition-all shadow-lg shadow-primary/10 disabled:opacity-50 active:scale-95 flex items-center justify-center shrink-0"
|
|
159
|
+
>
|
|
160
|
+
<Plus size={20} strokeWidth={2.5} />
|
|
161
|
+
</button>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
{/* Quick Suggestions */}
|
|
166
|
+
<div className="space-y-4 pt-8 border-t border-dashboard-border/40">
|
|
167
|
+
<span className="text-[10px] font-bold uppercase tracking-[0.2em] text-primary/60 ml-2">Recommended Protocols</span>
|
|
168
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-3">
|
|
169
|
+
{GLOBAL_LOCALE_DATABASE
|
|
170
|
+
.filter(loc => !supportedLocales.some(s => s.code === loc.code))
|
|
171
|
+
.slice(0, 6)
|
|
172
|
+
.map((loc) => (
|
|
173
|
+
<button
|
|
174
|
+
key={loc.code}
|
|
175
|
+
onClick={() => handleToggleLocale(loc)}
|
|
176
|
+
className="flex flex-col items-center gap-2 p-3 bg-dashboard-bg/30 border border-dashboard-border/40 hover:border-primary/40 hover:bg-primary/5 group transition-all duration-500 rounded-xl"
|
|
177
|
+
>
|
|
178
|
+
<div className="size-8 rounded-lg bg-dashboard-card/50 flex items-center justify-center border border-dashboard-border/40 group-hover:border-primary/20 transition-all shadow-inner">
|
|
179
|
+
<FlagIcon countryCode={loc.countryCode} />
|
|
180
|
+
</div>
|
|
181
|
+
<span className="text-[10px] font-bold text-dashboard-text-secondary/60 group-hover:text-primary transition-colors truncate w-full text-center">{loc.name}</span>
|
|
182
|
+
</button>
|
|
183
|
+
))}
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<div className="bg-primary/5 border border-primary/10 p-6 rounded-2xl flex items-start gap-4 group">
|
|
188
|
+
<div className="size-8 rounded-lg bg-primary/10 flex items-center justify-center text-primary shrink-0 border border-primary/20">
|
|
189
|
+
<Info size={16} />
|
|
190
|
+
</div>
|
|
191
|
+
<p className="text-xs text-dashboard-text-secondary/70 font-medium leading-relaxed">
|
|
192
|
+
JHITS operates on a modular localization engine. Provisioning a region here will automatically synchronize translation schemas and localized data structures across the entire infrastructure.
|
|
193
|
+
</p>
|
|
194
|
+
</div>
|
|
195
|
+
</motion.div>
|
|
196
|
+
</section>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
import { Mail, Globe, Plus, Trash2, Smartphone, MapPin, Share2 } from 'lucide-react';
|
|
4
|
+
import { WebsiteSettings, LocaleDefinition, LocalizedContactInfo } from '../../../types/settings';
|
|
5
|
+
import { FlagIcon } from './FlagIcon';
|
|
6
|
+
import { AVAILABLE_PLATFORMS } from '../constants';
|
|
7
|
+
import { clsx, type ClassValue } from 'clsx';
|
|
8
|
+
import { twMerge } from 'tailwind-merge';
|
|
9
|
+
|
|
10
|
+
function cn(...inputs: ClassValue[]) {
|
|
11
|
+
return twMerge(clsx(inputs));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface LocalizedContentSectionProps {
|
|
15
|
+
settings: WebsiteSettings;
|
|
16
|
+
activeTab: string;
|
|
17
|
+
onUpdateContact: (field: keyof LocalizedContactInfo, value: string) => void;
|
|
18
|
+
onAddSocial: () => void;
|
|
19
|
+
onUpdateSocial: (id: number, field: string, value: string) => void;
|
|
20
|
+
onRemoveSocial: (id: number) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function LocalizedContentSection({
|
|
24
|
+
settings,
|
|
25
|
+
activeTab,
|
|
26
|
+
onUpdateContact,
|
|
27
|
+
onAddSocial,
|
|
28
|
+
onUpdateSocial,
|
|
29
|
+
onRemoveSocial
|
|
30
|
+
}: LocalizedContentSectionProps) {
|
|
31
|
+
const contact = settings.localizedContactInfo?.[activeTab] || {};
|
|
32
|
+
const socials = settings.localizedSocials?.[activeTab] || [];
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<section className="space-y-8">
|
|
36
|
+
<div className="flex items-center gap-4">
|
|
37
|
+
<div className="size-10 rounded-xl bg-primary/10 flex items-center justify-center text-primary border border-primary/20 shadow-inner">
|
|
38
|
+
<Share2 size={20} />
|
|
39
|
+
</div>
|
|
40
|
+
<h3 className="text-2xl lg:text-3xl font-bold text-dashboard-text leading-none tracking-tight">
|
|
41
|
+
Localized <span className="text-primary">Content</span>
|
|
42
|
+
</h3>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<motion.div
|
|
46
|
+
initial={{ opacity: 0, y: 20 }}
|
|
47
|
+
animate={{ opacity: 1, y: 0 }}
|
|
48
|
+
className="bg-dashboard-card/50 border border-dashboard-border/40 p-8 rounded-[2rem] space-y-8 relative overflow-hidden"
|
|
49
|
+
>
|
|
50
|
+
{/* 1. Contact Info Sub-section */}
|
|
51
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
52
|
+
<div className="space-y-2">
|
|
53
|
+
<label className="text-[10px] font-bold text-dashboard-text-secondary/70 uppercase tracking-[0.15em] ml-2 opacity-60">Communication Vector ({activeTab})</label>
|
|
54
|
+
<div className="relative group">
|
|
55
|
+
<Smartphone className="absolute left-5 top-1/2 -translate-y-1/2 size-4 text-dashboard-text-secondary/40 group-focus-within:text-primary transition-colors" />
|
|
56
|
+
<input
|
|
57
|
+
type="tel"
|
|
58
|
+
value={contact.phoneNumber || ''}
|
|
59
|
+
onChange={(e) => onUpdateContact('phoneNumber', e.target.value)}
|
|
60
|
+
placeholder="INITIALIZE_PHONE"
|
|
61
|
+
className="w-full pl-12 pr-6 py-3 bg-dashboard-bg/50 border border-dashboard-border/40 rounded-xl text-sm font-bold outline-none focus:ring-2 focus:ring-primary/10 focus:border-primary/30 transition-all text-dashboard-text placeholder:text-dashboard-text-secondary/30"
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<div className="space-y-2">
|
|
66
|
+
<label className="text-[10px] font-bold text-dashboard-text-secondary/70 uppercase tracking-[0.15em] ml-2 opacity-60">Geospatial Coordinates ({activeTab})</label>
|
|
67
|
+
<div className="relative group">
|
|
68
|
+
<MapPin className="absolute left-5 top-1/2 -translate-y-1/2 size-4 text-dashboard-text-secondary/40 group-focus-within:text-primary transition-colors" />
|
|
69
|
+
<input
|
|
70
|
+
type="text"
|
|
71
|
+
value={contact.physicalAddress || ''}
|
|
72
|
+
onChange={(e) => onUpdateContact('physicalAddress', e.target.value)}
|
|
73
|
+
placeholder="STREET_ADDRESS_PROTOCOL"
|
|
74
|
+
className="w-full pl-12 pr-6 py-3 bg-dashboard-bg/50 border border-dashboard-border/40 rounded-xl text-sm font-bold outline-none focus:ring-2 focus:ring-primary/10 focus:border-primary/30 transition-all text-dashboard-text placeholder:text-dashboard-text-secondary/30"
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{/* 2. Social Links Sub-section */}
|
|
81
|
+
<div className="pt-8 border-t border-dashboard-border/40">
|
|
82
|
+
<div className="flex items-center justify-between mb-4 ml-2">
|
|
83
|
+
<label className="text-[10px] font-bold text-dashboard-text-secondary/70 uppercase tracking-[0.15em] opacity-60">Social Ecosystem Nodes ({activeTab})</label>
|
|
84
|
+
<button
|
|
85
|
+
onClick={onAddSocial}
|
|
86
|
+
className="flex items-center gap-2 px-4 py-2 bg-primary text-white rounded-xl text-[10px] font-bold uppercase tracking-[0.15em] hover:bg-primary/90 transition-all shadow-lg shadow-primary/10 active:scale-95 group/btn overflow-hidden relative"
|
|
87
|
+
>
|
|
88
|
+
<div className="absolute inset-0 bg-linear-to-br from-white/20 to-transparent opacity-0 group-hover/btn:opacity-100 transition-opacity" />
|
|
89
|
+
<Plus size={12} strokeWidth={3} /> <span>Link Node</span>
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div className="space-y-3">
|
|
94
|
+
<AnimatePresence mode="popLayout">
|
|
95
|
+
{socials.length > 0 ? (
|
|
96
|
+
socials.map((social, idx) => {
|
|
97
|
+
const platform = AVAILABLE_PLATFORMS.find(p => p.name === social.platform);
|
|
98
|
+
return (
|
|
99
|
+
<motion.div
|
|
100
|
+
layout
|
|
101
|
+
initial={{ opacity: 0, x: -20 }}
|
|
102
|
+
animate={{ opacity: 1, x: 0 }}
|
|
103
|
+
exit={{ opacity: 0, x: 20 }}
|
|
104
|
+
transition={{ delay: idx * 0.05 }}
|
|
105
|
+
key={social.id}
|
|
106
|
+
className="flex items-center gap-4 p-3 bg-dashboard-bg/30 border border-dashboard-border/40 rounded-xl group hover:border-primary/30 transition-all duration-500"
|
|
107
|
+
>
|
|
108
|
+
<div className="shrink-0 size-10 rounded-lg bg-dashboard-card/50 flex items-center justify-center border border-dashboard-border/40 text-dashboard-text-secondary/40 group-hover:text-primary group-hover:bg-primary/10 group-hover:border-primary/20 transition-all duration-500 shadow-inner">
|
|
109
|
+
{platform?.icon || <Globe size={18} />}
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div className="flex-1 flex flex-col md:flex-row items-center gap-4">
|
|
113
|
+
<div className="w-full md:w-32 relative">
|
|
114
|
+
<select
|
|
115
|
+
value={social.platform}
|
|
116
|
+
onChange={(e) => onUpdateSocial(social.id, 'platform', e.target.value)}
|
|
117
|
+
className="w-full bg-transparent border-none outline-none text-[10px] font-bold uppercase tracking-wider text-dashboard-text/90 cursor-pointer focus:text-primary transition-colors appearance-none"
|
|
118
|
+
>
|
|
119
|
+
<option value="" className="bg-neutral-900">PLATFORM</option>
|
|
120
|
+
{AVAILABLE_PLATFORMS.map(p => (<option key={p.name} value={p.name} className="bg-neutral-900">{p.name}</option>))}
|
|
121
|
+
</select>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div className="hidden md:block h-5 w-px bg-dashboard-border/40" />
|
|
125
|
+
|
|
126
|
+
<input
|
|
127
|
+
type="url"
|
|
128
|
+
value={social.url}
|
|
129
|
+
onChange={(e) => onUpdateSocial(social.id, 'url', e.target.value)}
|
|
130
|
+
placeholder="URL_ENDPOINT"
|
|
131
|
+
className="flex-1 bg-transparent border-none outline-none text-xs font-bold text-dashboard-text/90 placeholder:text-dashboard-text-secondary/30 w-full"
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<button
|
|
136
|
+
onClick={() => onRemoveSocial(social.id)}
|
|
137
|
+
className="p-2 text-dashboard-text-secondary/40 hover:text-red-500 hover:bg-red-500/10 rounded-lg opacity-0 group-hover:opacity-100 transition-all active:scale-90"
|
|
138
|
+
>
|
|
139
|
+
<Trash2 size={16} />
|
|
140
|
+
</button>
|
|
141
|
+
</motion.div>
|
|
142
|
+
);
|
|
143
|
+
})
|
|
144
|
+
) : (
|
|
145
|
+
<div className="text-center py-12 bg-dashboard-card/20 border border-dashed border-dashboard-border/40 rounded-2xl">
|
|
146
|
+
<p className="text-[10px] text-dashboard-text-secondary/40 font-bold uppercase tracking-[0.2em]">No social platforms initialized</p>
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
</AnimatePresence>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</motion.div>
|
|
153
|
+
</section>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { motion } from 'framer-motion';
|
|
3
|
+
import { Search, Sparkles, Fingerprint } from 'lucide-react';
|
|
4
|
+
import { WebsiteSettings, LocalizedIdentity } from '../../../types/settings';
|
|
5
|
+
import { FlagIcon } from './FlagIcon';
|
|
6
|
+
import { clsx, type ClassValue } from 'clsx';
|
|
7
|
+
import { twMerge } from 'tailwind-merge';
|
|
8
|
+
|
|
9
|
+
function cn(...inputs: ClassValue[]) {
|
|
10
|
+
return twMerge(clsx(inputs));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface SeoIdentitySectionProps {
|
|
14
|
+
settings: WebsiteSettings;
|
|
15
|
+
activeTab: string;
|
|
16
|
+
onUpdateLocalized: (field: keyof LocalizedIdentity, value: string) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function SeoIdentitySection({
|
|
20
|
+
settings,
|
|
21
|
+
activeTab,
|
|
22
|
+
onUpdateLocalized
|
|
23
|
+
}: SeoIdentitySectionProps) {
|
|
24
|
+
const identity = settings.localizedIdentity?.[activeTab] || {};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<section className="space-y-8">
|
|
28
|
+
<div className="flex items-center gap-4">
|
|
29
|
+
<div className="size-10 rounded-xl bg-primary/10 flex items-center justify-center text-primary border border-primary/20 shadow-inner">
|
|
30
|
+
<Fingerprint size={20} />
|
|
31
|
+
</div>
|
|
32
|
+
<h3 className="text-2xl lg:text-3xl font-bold text-dashboard-text leading-none tracking-tight">
|
|
33
|
+
Identity <span className="text-primary">& SEO</span>
|
|
34
|
+
</h3>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<motion.div
|
|
38
|
+
initial={{ opacity: 0, y: 20 }}
|
|
39
|
+
animate={{ opacity: 1, y: 0 }}
|
|
40
|
+
className="bg-dashboard-card/50 border border-dashboard-border/40 p-8 rounded-[2rem] space-y-6 relative overflow-hidden"
|
|
41
|
+
>
|
|
42
|
+
<div className="absolute top-0 right-0 w-64 h-64 blur-3xl opacity-5 bg-primary pointer-events-none" />
|
|
43
|
+
|
|
44
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
45
|
+
<div className="space-y-2">
|
|
46
|
+
<div className="flex items-center justify-between ml-2">
|
|
47
|
+
<label className="text-[10px] font-bold text-dashboard-text-secondary/70 uppercase tracking-[0.15em] opacity-60">System Name ({activeTab})</label>
|
|
48
|
+
<Sparkles size={10} className="text-primary animate-pulse" />
|
|
49
|
+
</div>
|
|
50
|
+
<input
|
|
51
|
+
type="text"
|
|
52
|
+
value={identity.siteName || ''}
|
|
53
|
+
onChange={(e) => onUpdateLocalized('siteName', e.target.value)}
|
|
54
|
+
placeholder="INITIALIZE_NAME"
|
|
55
|
+
className="w-full px-6 py-3 bg-dashboard-bg/50 border border-dashboard-border/40 rounded-xl text-sm font-bold outline-none focus:ring-2 focus:ring-primary/10 focus:border-primary/30 transition-all text-dashboard-text placeholder:text-dashboard-text-secondary/30"
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
<div className="space-y-2">
|
|
59
|
+
<label className="text-[10px] font-bold text-dashboard-text-secondary/70 uppercase tracking-[0.15em] ml-2 opacity-60">Strategic Tagline ({activeTab})</label>
|
|
60
|
+
<input
|
|
61
|
+
type="text"
|
|
62
|
+
value={identity.siteTagline || ''}
|
|
63
|
+
onChange={(e) => onUpdateLocalized('siteTagline', e.target.value)}
|
|
64
|
+
placeholder="DEFINE_TAGLINE"
|
|
65
|
+
className="w-full px-6 py-3 bg-dashboard-bg/50 border border-dashboard-border/40 rounded-xl text-sm font-bold outline-none focus:ring-2 focus:ring-primary/10 focus:border-primary/30 transition-all text-dashboard-text placeholder:text-dashboard-text-secondary/30"
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div className="space-y-2">
|
|
71
|
+
<label className="text-[10px] font-bold text-dashboard-text-secondary/70 uppercase tracking-[0.15em] ml-2 opacity-60">Global Description Protocol ({activeTab})</label>
|
|
72
|
+
<textarea
|
|
73
|
+
value={identity.siteDescription || ''}
|
|
74
|
+
onChange={(e) => onUpdateLocalized('siteDescription', e.target.value)}
|
|
75
|
+
placeholder="A comprehensive architectural description for search engine crawlers..."
|
|
76
|
+
rows={3}
|
|
77
|
+
className="w-full px-6 py-4 bg-dashboard-bg/50 border border-dashboard-border/40 rounded-xl text-sm font-medium leading-relaxed outline-none focus:ring-2 focus:ring-primary/10 focus:border-primary/30 transition-all text-dashboard-text placeholder:text-dashboard-text-secondary/30 resize-none"
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div className="space-y-2">
|
|
82
|
+
<label className="text-[10px] font-bold text-dashboard-text-secondary/70 uppercase tracking-[0.15em] ml-2 opacity-60">Indexing Keywords ({activeTab})</label>
|
|
83
|
+
<div className="relative group">
|
|
84
|
+
<Search className="absolute left-5 top-1/2 -translate-y-1/2 size-4 text-dashboard-text-secondary/40 group-focus-within:text-primary transition-colors" />
|
|
85
|
+
<input
|
|
86
|
+
type="text"
|
|
87
|
+
value={identity.keywords || ''}
|
|
88
|
+
onChange={(e) => onUpdateLocalized('keywords', e.target.value)}
|
|
89
|
+
placeholder="comma, separated, identifiers"
|
|
90
|
+
className="w-full pl-12 pr-6 py-3 bg-dashboard-bg/50 border border-dashboard-border/40 rounded-xl text-sm font-bold outline-none focus:ring-2 focus:ring-primary/10 focus:border-primary/30 transition-all text-dashboard-text placeholder:text-dashboard-text-secondary/30"
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
<p className="text-[10px] text-dashboard-text-secondary/60 ml-4 font-medium opacity-60">Separated by commas. These tokens facilitate high-signal discovery during search engine scanning.</p>
|
|
94
|
+
</div>
|
|
95
|
+
</motion.div>
|
|
96
|
+
</section>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Globe, X } from 'lucide-react';
|
|
3
|
+
import { WebsiteSettings, LocaleDefinition, LocalizedSocialLink } from '../../../types/settings';
|
|
4
|
+
import { AVAILABLE_PLATFORMS } from '../constants';
|
|
5
|
+
import { FlagIcon } from './FlagIcon';
|
|
6
|
+
|
|
7
|
+
interface SocialLinksSectionProps {
|
|
8
|
+
settings: WebsiteSettings;
|
|
9
|
+
activeTab: string;
|
|
10
|
+
onTabChange: (code: string) => void;
|
|
11
|
+
activeLocales: LocaleDefinition[];
|
|
12
|
+
onAdd: () => void;
|
|
13
|
+
onUpdate: (id: number, field: string, value: string) => void;
|
|
14
|
+
onRemove: (id: number) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function SocialLinksSection({
|
|
18
|
+
settings,
|
|
19
|
+
activeTab,
|
|
20
|
+
onTabChange,
|
|
21
|
+
activeLocales,
|
|
22
|
+
onAdd,
|
|
23
|
+
onUpdate,
|
|
24
|
+
onRemove
|
|
25
|
+
}: SocialLinksSectionProps) {
|
|
26
|
+
const currentSocials = settings.localizedSocials?.[activeTab] || [];
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<section className="bg-dashboard-card/50 p-6 rounded-[2rem] border border-dashboard-border/40">
|
|
30
|
+
<div className="flex items-center justify-between border-b border-dashboard-border/40 pb-4 mb-4">
|
|
31
|
+
<div className="flex items-center gap-2 text-xl font-bold text-dashboard-text leading-none tracking-tight">
|
|
32
|
+
<Globe size={18} className="text-primary" />
|
|
33
|
+
Social <span className="text-primary">Links</span> ({activeTab.toUpperCase()})
|
|
34
|
+
</div>
|
|
35
|
+
<button
|
|
36
|
+
onClick={onAdd}
|
|
37
|
+
className="px-3 py-1.5 text-[10px] font-bold uppercase tracking-wider bg-primary text-white rounded-xl hover:bg-primary/90 transition-all shadow-lg shadow-primary/10"
|
|
38
|
+
>
|
|
39
|
+
Add Social
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div className="flex gap-2 mb-4 overflow-x-auto pb-2 custom-scrollbar">
|
|
44
|
+
{activeLocales.map((loc) => (
|
|
45
|
+
<button
|
|
46
|
+
key={loc.code}
|
|
47
|
+
onClick={() => onTabChange(loc.code)}
|
|
48
|
+
className={`px-3 py-1.5 rounded-xl text-[10px] font-bold uppercase tracking-wider transition-all flex items-center gap-2 shrink-0 border ${
|
|
49
|
+
activeTab === loc.code
|
|
50
|
+
? 'bg-primary text-white border-primary shadow-lg shadow-primary/10'
|
|
51
|
+
: 'bg-dashboard-bg/30 text-dashboard-text-secondary/50 border-dashboard-border/40 hover:border-primary/30'
|
|
52
|
+
}`}
|
|
53
|
+
>
|
|
54
|
+
<FlagIcon code={loc.code} countryCode={loc.countryCode} />
|
|
55
|
+
{loc.code.toUpperCase()}
|
|
56
|
+
</button>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div className="space-y-3">
|
|
61
|
+
{currentSocials.length > 0 ? (
|
|
62
|
+
currentSocials.map((social) => {
|
|
63
|
+
const platform = AVAILABLE_PLATFORMS.find(p => p.name === social.platform);
|
|
64
|
+
return (
|
|
65
|
+
<div key={social.id} className="flex items-center gap-3 p-3 bg-dashboard-bg/30 rounded-xl border border-dashboard-border/40">
|
|
66
|
+
<div className="flex-shrink-0 text-dashboard-text-secondary/40">
|
|
67
|
+
{platform?.icon || <Globe size={16} />}
|
|
68
|
+
</div>
|
|
69
|
+
<select
|
|
70
|
+
value={social.platform}
|
|
71
|
+
onChange={(e) => onUpdate(social.id, 'platform', e.target.value)}
|
|
72
|
+
className="flex-1 px-3 py-1.5 bg-dashboard-bg/50 border border-dashboard-border/40 rounded-lg outline-none focus:ring-2 focus:ring-primary/10 focus:border-primary/30 text-sm font-bold text-dashboard-text/90 cursor-pointer appearance-none"
|
|
73
|
+
>
|
|
74
|
+
<option value="" className="bg-neutral-900">PLATFORM</option>
|
|
75
|
+
{AVAILABLE_PLATFORMS.map(p => (<option key={p.name} value={p.name} className="bg-neutral-900">{p.name}</option>))}
|
|
76
|
+
</select>
|
|
77
|
+
<input
|
|
78
|
+
type="url"
|
|
79
|
+
value={social.url}
|
|
80
|
+
onChange={(e) => onUpdate(social.id, 'url', e.target.value)}
|
|
81
|
+
placeholder="https://..."
|
|
82
|
+
className="flex-[2] px-3 py-1.5 bg-dashboard-bg/50 border border-dashboard-border/40 rounded-lg outline-none focus:ring-2 focus:ring-primary/10 focus:border-primary/30 text-sm font-bold text-dashboard-text/90 placeholder:text-dashboard-text-secondary/30"
|
|
83
|
+
/>
|
|
84
|
+
<button onClick={() => onRemove(social.id)} className="p-2 text-dashboard-text-secondary/40 hover:text-red-500 hover:bg-red-500/10 rounded-lg transition-all">
|
|
85
|
+
<X size={14} />
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
})
|
|
90
|
+
) : (
|
|
91
|
+
<p className="text-[10px] text-dashboard-text-secondary/50 font-bold uppercase tracking-wider text-center py-8 bg-dashboard-card/20 border border-dashed border-dashboard-border/40 rounded-xl">
|
|
92
|
+
No social links added for {activeTab.toUpperCase()}
|
|
93
|
+
</p>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
</section>
|
|
97
|
+
);
|
|
98
|
+
}
|