Add language translation system and weather widget with admin controls
This commit is contained in:
+162
-1
@@ -31,6 +31,7 @@ const NAV = [
|
||||
{ id: 'places', label: 'Places', icon: '🗺️' },
|
||||
{ id: 'reviews', label: 'Reviews', icon: '⭐' },
|
||||
{ id: 'users', label: 'Users', icon: '👥' },
|
||||
{ id: 'weather', label: 'Weather', icon: '🌤️' },
|
||||
];
|
||||
|
||||
function I({ label, value, onChange, type = 'text', placeholder, style }: any) {
|
||||
@@ -1314,6 +1315,165 @@ function UsersSection() {
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Weather Section ─── */
|
||||
function WeatherSection({ onToast }: { onToast: (msg: string, type: string) => void }) {
|
||||
const [enabled, setEnabled] = useState(true);
|
||||
const [cities, setCities] = useState<{ name: string; lat: string; lon: string }[]>([]);
|
||||
const [newCity, setNewCity] = useState({ name: '', lat: '', lon: '' });
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Load weather config
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/site-assets?key=weather_config');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data.data) {
|
||||
const config = JSON.parse(data.data);
|
||||
setEnabled(config.enabled ?? true);
|
||||
setCities(config.cities ?? []);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load weather config:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
loadConfig();
|
||||
}, []);
|
||||
|
||||
const saveConfig = async () => {
|
||||
try {
|
||||
const config = { enabled, cities };
|
||||
const res = await fetch('/api/site-assets', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ key: 'weather_config', value: JSON.stringify(config) }),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
onToast('Weather settings saved', 'success');
|
||||
} else {
|
||||
throw new Error('Failed to save');
|
||||
}
|
||||
} catch (err) {
|
||||
onToast('Error saving weather settings', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const addCity = () => {
|
||||
if (newCity.name && newCity.lat && newCity.lon) {
|
||||
setCities([...cities, newCity]);
|
||||
setNewCity({ name: '', lat: '', lon: '' });
|
||||
}
|
||||
};
|
||||
|
||||
const removeCity = (index: number) => {
|
||||
setCities(cities.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div>
|
||||
<SecHead title="🌤️ Weather Widget" />
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: C.textLight }}>
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SecHead title="🌤️ Weather Widget" />
|
||||
<div style={{ background: C.card, borderRadius: 12, padding: '24px', marginBottom: '24px', border: `1px solid ${C.border}` }}>
|
||||
<p style={{ color: C.text, marginBottom: '24px' }}>
|
||||
Configure the weather widget displayed on the homepage
|
||||
</p>
|
||||
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enabled}
|
||||
onChange={(e) => setEnabled(e.target.checked)}
|
||||
style={{ width: '18px', height: '18px' }}
|
||||
/>
|
||||
<span style={{ fontWeight: 500, color: C.text }}>Enable Weather Widget</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h3 style={{ margin: '0 0 16px', color: C.text, fontSize: '16px' }}>Cities</h3>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
{cities.map((city, index) => (
|
||||
<div key={index} style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '8px', padding: '8px', background: C.bg, borderRadius: '8px' }}>
|
||||
<span style={{ flex: 1, color: C.text }}>{city.name} ({city.lat}, {city.lon})</span>
|
||||
<button
|
||||
onClick={() => removeCity(index)}
|
||||
style={{
|
||||
background: C.danger,
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
padding: '4px 8px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', padding: '16px', background: C.bg, borderRadius: '8px' }}>
|
||||
<h4 style={{ margin: '0 0 8px', color: C.text, fontSize: '14px' }}>Add City</h4>
|
||||
<I label="City Name" value={newCity.name} onChange={(v: string) => setNewCity({...newCity, name: v})} />
|
||||
<div style={{ display: 'flex', gap: '12px' }}>
|
||||
<I label="Latitude" value={newCity.lat} onChange={(v: string) => setNewCity({...newCity, lat: v})} />
|
||||
<I label="Longitude" value={newCity.lon} onChange={(v: string) => setNewCity({...newCity, lon: v})} />
|
||||
</div>
|
||||
<button
|
||||
onClick={addCity}
|
||||
style={{
|
||||
alignSelf: 'flex-start',
|
||||
background: C.gold,
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
padding: '8px 16px',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 500
|
||||
}}
|
||||
>
|
||||
Add City
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '24px' }}>
|
||||
<button
|
||||
onClick={saveConfig}
|
||||
style={{
|
||||
background: C.gold,
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
padding: '12px 24px',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 600,
|
||||
fontSize: '14px'
|
||||
}}
|
||||
>
|
||||
Save Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════
|
||||
MAIN PAGE
|
||||
═══════════════════════════════════════════════════ */
|
||||
@@ -1386,7 +1546,7 @@ export default function AdminPage() {
|
||||
case 'gallery': return (
|
||||
<div>
|
||||
<SecHead title="🖼️ Image Gallery" count={images.length} />
|
||||
<GalleryFilter images={images} onDelete={deleteImage} filter={imageFilter} onFilter={setImageFilter} />
|
||||
<ImageGallery images={images} onDelete={deleteImage} />
|
||||
</div>
|
||||
);
|
||||
case 'slides': return <SlidesSection slides={slides} setSlides={setSlides} images={images} onToast={showToast} />;
|
||||
@@ -1397,6 +1557,7 @@ export default function AdminPage() {
|
||||
case 'places': return <PlacesSection places={places} setPlaces={setPlaces} images={images} onToast={showToast} />;
|
||||
case 'reviews': return <ReviewsSection reviews={reviews} setReviews={setReviews} onToast={showToast} />;
|
||||
case 'users': return <UsersSection />;
|
||||
case 'weather': return <WeatherSection onToast={showToast} />;
|
||||
default: return <Dashboard rooms={rooms} slides={slides} events={events} reviews={reviews} places={places} />;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user