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: 'places', label: 'Places', icon: '🗺️' },
|
||||||
{ id: 'reviews', label: 'Reviews', icon: '⭐' },
|
{ id: 'reviews', label: 'Reviews', icon: '⭐' },
|
||||||
{ id: 'users', label: 'Users', icon: '👥' },
|
{ id: 'users', label: 'Users', icon: '👥' },
|
||||||
|
{ id: 'weather', label: 'Weather', icon: '🌤️' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function I({ label, value, onChange, type = 'text', placeholder, style }: any) {
|
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
|
MAIN PAGE
|
||||||
═══════════════════════════════════════════════════ */
|
═══════════════════════════════════════════════════ */
|
||||||
@@ -1386,7 +1546,7 @@ export default function AdminPage() {
|
|||||||
case 'gallery': return (
|
case 'gallery': return (
|
||||||
<div>
|
<div>
|
||||||
<SecHead title="🖼️ Image Gallery" count={images.length} />
|
<SecHead title="🖼️ Image Gallery" count={images.length} />
|
||||||
<GalleryFilter images={images} onDelete={deleteImage} filter={imageFilter} onFilter={setImageFilter} />
|
<ImageGallery images={images} onDelete={deleteImage} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'slides': return <SlidesSection slides={slides} setSlides={setSlides} images={images} onToast={showToast} />;
|
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 'places': return <PlacesSection places={places} setPlaces={setPlaces} images={images} onToast={showToast} />;
|
||||||
case 'reviews': return <ReviewsSection reviews={reviews} setReviews={setReviews} onToast={showToast} />;
|
case 'reviews': return <ReviewsSection reviews={reviews} setReviews={setReviews} onToast={showToast} />;
|
||||||
case 'users': return <UsersSection />;
|
case 'users': return <UsersSection />;
|
||||||
|
case 'weather': return <WeatherSection onToast={showToast} />;
|
||||||
default: return <Dashboard rooms={rooms} slides={slides} events={events} reviews={reviews} places={places} />;
|
default: return <Dashboard rooms={rooms} slides={slides} events={events} reviews={reviews} places={places} />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+9
-1
@@ -4,6 +4,9 @@ import { useEffect, useState } from 'react';
|
|||||||
import Slideshow from '@/components/Slideshow';
|
import Slideshow from '@/components/Slideshow';
|
||||||
import CalendarStrip from '@/components/CalendarStrip';
|
import CalendarStrip from '@/components/CalendarStrip';
|
||||||
import MeasuredText from '@/components/MeasuredText';
|
import MeasuredText from '@/components/MeasuredText';
|
||||||
|
import WeatherWidget from '@/components/WeatherWidget';
|
||||||
|
import LanguageSwitcher from '@/components/LanguageSwitcher';
|
||||||
|
import { t, getLanguage } from '@/lib/i18n';
|
||||||
|
|
||||||
const gold = '#E8A849';
|
const gold = '#E8A849';
|
||||||
const navy = '#010D1E';
|
const navy = '#010D1E';
|
||||||
@@ -23,6 +26,11 @@ export default function HomePage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main style={{ minHeight: 'calc(100vh - 56px)', background: navy, color: cream }}>
|
<main style={{ minHeight: 'calc(100vh - 56px)', background: navy, color: cream }}>
|
||||||
|
<LanguageSwitcher />
|
||||||
|
<div style={{ position: 'absolute', top: '1rem', right: '1rem', zIndex: 10 }}>
|
||||||
|
<WeatherWidget />
|
||||||
|
</div>
|
||||||
|
|
||||||
<Slideshow />
|
<Slideshow />
|
||||||
|
|
||||||
<section
|
<section
|
||||||
@@ -59,7 +67,7 @@ export default function HomePage() {
|
|||||||
La Huasca
|
La Huasca
|
||||||
</h1>
|
</h1>
|
||||||
<p style={{ fontSize: '18px', lineHeight: 1.55, color: 'rgba(245,240,232,0.78)', maxWidth: '52ch', margin: '0 0 2rem' }}>
|
<p style={{ fontSize: '18px', lineHeight: 1.55, color: 'rgba(245,240,232,0.78)', maxWidth: '52ch', margin: '0 0 2rem' }}>
|
||||||
Hotel · Restaurant · Andean Highlands
|
{t('common.hotel', getLanguage())} · {t('common.restaurant', getLanguage())} · {t('common.andean_highlands', getLanguage())}
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,9 @@ export default function RoomsPage() {
|
|||||||
const postComment = async (roomId: number) => {
|
const postComment = async (roomId: number) => {
|
||||||
if (!draft.trim() || sending) return;
|
if (!draft.trim() || sending) return;
|
||||||
setSending(true);
|
setSending(true);
|
||||||
const photo = getPhoto(roomId, activePhotoByRoom[roomId] || 0);
|
const room = rooms.find(r => r.id === roomId);
|
||||||
|
if (!room) return;
|
||||||
|
const photo = getPhoto(room, activePhotoByRoom[roomId] || 0);
|
||||||
await fetch(`/api/rooms/${roomId}/comments`, {
|
await fetch(`/api/rooms/${roomId}/comments`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { getLanguage, setLanguage } from '@/lib/i18n';
|
||||||
|
|
||||||
|
export default function LanguageSwitcher() {
|
||||||
|
const [currentLang, setCurrentLang] = useState<'en' | 'es'>('en');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentLang(getLanguage());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleLanguage = () => {
|
||||||
|
const newLang = currentLang === 'en' ? 'es' : 'en';
|
||||||
|
setLanguage(newLang);
|
||||||
|
setCurrentLang(newLang);
|
||||||
|
// Reload the page to apply the language change
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={toggleLanguage}
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: '1rem',
|
||||||
|
left: '1rem',
|
||||||
|
zIndex: 1000,
|
||||||
|
background: 'rgba(255, 255, 255, 0.9)',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
borderRadius: '4px',
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentLang === 'en' ? 'ES' : 'EN'}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
export default function WeatherWidget() {
|
||||||
|
const [weather, setWeather] = useState<any>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [config, setConfig] = useState<any>(null);
|
||||||
|
|
||||||
|
// Load weather configuration
|
||||||
|
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 parsedConfig = JSON.parse(data.data);
|
||||||
|
setConfig(parsedConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load weather config:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadConfig();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Fetch weather data when config is loaded and enabled
|
||||||
|
useEffect(() => {
|
||||||
|
if (!config || !config.enabled || config.cities.length === 0) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchWeather = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// Use the first city in the list for now
|
||||||
|
const city = config.cities[0];
|
||||||
|
const res = await fetch(`/api/weather?lat=${city.lat}&lon=${city.lon}`);
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
setWeather(data);
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to fetch weather data');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Weather API error:', err);
|
||||||
|
setError('Failed to load weather data');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchWeather();
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
if (!config?.enabled) return null;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
background: 'rgba(255, 255, 255, 0.9)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '12px 16px',
|
||||||
|
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.2)'
|
||||||
|
}}>
|
||||||
|
<div style={{ fontSize: '14px', color: '#666' }}>Loading weather...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !weather) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
background: 'rgba(255, 255, 255, 0.9)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '12px 16px',
|
||||||
|
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.2)'
|
||||||
|
}}>
|
||||||
|
<div style={{ fontSize: '14px', color: '#666' }}>Weather unavailable</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = weather.current;
|
||||||
|
const temp = current.celsius;
|
||||||
|
const icon = '🌤️';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
background: 'rgba(255, 255, 255, 0.9)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '12px 16px',
|
||||||
|
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.2)',
|
||||||
|
minWidth: '180px'
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
|
||||||
|
<span style={{ fontSize: '20px' }}>{icon}</span>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '18px', fontWeight: '600', color: '#333' }}>
|
||||||
|
{temp}°C
|
||||||
|
</div>
|
||||||
|
{config.cities[0] && (
|
||||||
|
<div style={{ fontSize: '12px', color: '#666', marginTop: '2px' }}>
|
||||||
|
{config.cities[0].name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '11px', color: '#888', marginTop: '4px' }}>
|
||||||
|
Updated: {new Date(current.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { t } from '@/lib/i18n';
|
||||||
|
|
||||||
|
export default function WeatherSection({ onToast }: { onToast: (msg: string, type: string) => void }) {
|
||||||
|
const [enabled, setEnabled] = useState(true);
|
||||||
|
const [cities, setCities] = useState<Array<{name: string, lat: number, lon: number}>>([
|
||||||
|
{ name: 'La Huasca', lat: -31.525, lon: -65.125 }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Load weather config from API
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/site-assets?key=weather_config')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.data) {
|
||||||
|
try {
|
||||||
|
const config = JSON.parse(data.data);
|
||||||
|
setEnabled(config.enabled ?? true);
|
||||||
|
setCities(config.cities ?? [{ name: 'La Huasca', lat: -31.525, lon: -65.125 }]);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse weather config:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Failed to load weather config:', err);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const addCity = () => {
|
||||||
|
setCities([...cities, { name: '', lat: 0, lon: 0 }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateCity = (index: number, field: string, value: string | number) => {
|
||||||
|
const updated = [...cities];
|
||||||
|
if (field === 'name') {
|
||||||
|
updated[index].name = value as string;
|
||||||
|
} else if (field === 'lat') {
|
||||||
|
updated[index].lat = value as number;
|
||||||
|
} else if (field === 'lon') {
|
||||||
|
updated[index].lon = value as number;
|
||||||
|
}
|
||||||
|
setCities(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeCity = (index: number) => {
|
||||||
|
setCities(cities.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveConfig = async () => {
|
||||||
|
try {
|
||||||
|
const config = { enabled, cities };
|
||||||
|
const response = await fetch('/api/site-assets', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
key: 'weather_config',
|
||||||
|
data: JSON.stringify(config)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
onToast(t('admin.weather.save_success', 'en'), 'success');
|
||||||
|
} else {
|
||||||
|
onToast(t('admin.weather.save_error', 'en'), 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to save weather config:', err);
|
||||||
|
onToast(t('admin.weather.save_error', 'en'), 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '2rem' }}>
|
||||||
|
<h2 style={{ marginBottom: '1.5rem' }}>{t('admin.weather.title', 'en')}</h2>
|
||||||
|
<p style={{ marginBottom: '2rem', color: '#666' }}>{t('admin.weather.description', 'en')}</p>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '2rem' }}>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={enabled}
|
||||||
|
onChange={(e) => setEnabled(e.target.checked)}
|
||||||
|
/>
|
||||||
|
{t('admin.weather.enabled', 'en')}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '2rem' }}>
|
||||||
|
<h3 style={{ marginBottom: '1rem' }}>{t('admin.weather.cities', 'en')}</h3>
|
||||||
|
{cities.map((city, index) => (
|
||||||
|
<div key={index} style={{ display: 'flex', gap: '1rem', marginBottom: '1rem', alignItems: 'end' }}>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.25rem', fontSize: '0.9rem' }}>
|
||||||
|
{t('admin.weather.city_name', 'en')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={city.name}
|
||||||
|
onChange={(e) => updateCity(index, 'name', e.target.value)}
|
||||||
|
style={{ padding: '0.5rem', border: '1px solid #ddd', borderRadius: '4px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.25rem', fontSize: '0.9rem' }}>
|
||||||
|
{t('admin.weather.latitude', 'en')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.001"
|
||||||
|
value={city.lat}
|
||||||
|
onChange={(e) => updateCity(index, 'lat', parseFloat(e.target.value) || 0)}
|
||||||
|
style={{ padding: '0.5rem', border: '1px solid #ddd', borderRadius: '4px', width: '100px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.25rem', fontSize: '0.9rem' }}>
|
||||||
|
{t('admin.weather.longitude', 'en')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.001"
|
||||||
|
value={city.lon}
|
||||||
|
onChange={(e) => updateCity(index, 'lon', parseFloat(e.target.value) || 0)}
|
||||||
|
style={{ padding: '0.5rem', border: '1px solid #ddd', borderRadius: '4px', width: '100px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => removeCity(index)}
|
||||||
|
style={{ padding: '0.5rem 1rem', background: '#ff4444', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
onClick={addCity}
|
||||||
|
style={{ padding: '0.5rem 1rem', background: '#0070f3', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
{t('admin.weather.add_city', 'en')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={saveConfig}
|
||||||
|
style={{ padding: '0.75rem 1.5rem', background: '#0070f3', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '1rem' }}
|
||||||
|
>
|
||||||
|
{t('admin.weather.save', 'en')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+159
@@ -0,0 +1,159 @@
|
|||||||
|
// Simple translation utility for La Huasca website
|
||||||
|
|
||||||
|
// Define languages
|
||||||
|
export type Language = 'en' | 'es';
|
||||||
|
|
||||||
|
// Translation data
|
||||||
|
export const translations = {
|
||||||
|
en: {
|
||||||
|
// Common
|
||||||
|
'common.hotel': 'Hotel',
|
||||||
|
'common.restaurant': 'Restaurant',
|
||||||
|
'common.andean_highlands': 'Andean Highlands',
|
||||||
|
'common.rooms_suites': 'Rooms & Suites',
|
||||||
|
'common.social_events': 'Social Events',
|
||||||
|
'common.coffee': 'Coffee',
|
||||||
|
'common.landscape': 'Landscape',
|
||||||
|
'common.comments': 'Comments',
|
||||||
|
'common.user_portal': 'User Portal',
|
||||||
|
'common.login': 'Login',
|
||||||
|
'common.logout': 'Logout',
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
'nav.brand': 'Brand',
|
||||||
|
'nav.gallery': 'Images',
|
||||||
|
'nav.slideshow': 'Slideshow',
|
||||||
|
'nav.calendar': 'Calendar',
|
||||||
|
'nav.social_events': 'Social Events',
|
||||||
|
'nav.rooms': 'Rooms',
|
||||||
|
'nav.menu': 'Menu',
|
||||||
|
'nav.places': 'Places',
|
||||||
|
'nav.reviews': 'Reviews',
|
||||||
|
'nav.users': 'Users',
|
||||||
|
'nav.weather': 'Weather',
|
||||||
|
|
||||||
|
// Weather widget
|
||||||
|
'weather.currently': 'Currently',
|
||||||
|
'weather.today': 'Today',
|
||||||
|
'weather.max': 'Max',
|
||||||
|
'weather.min': 'Min',
|
||||||
|
'weather.wind': 'Wind',
|
||||||
|
'weather.kmh': 'km/h',
|
||||||
|
'weather_updated': 'Updated',
|
||||||
|
|
||||||
|
// Admin panel
|
||||||
|
'admin.brand': 'Brand',
|
||||||
|
'admin.gallery': 'Images',
|
||||||
|
'admin.slideshow': 'Slideshow',
|
||||||
|
'admin.calendar': 'Calendar',
|
||||||
|
'admin.social_events': 'Social Events',
|
||||||
|
'admin.rooms': 'Rooms',
|
||||||
|
'admin.menu': 'Menu',
|
||||||
|
'admin.places': 'Places',
|
||||||
|
'admin.reviews': 'Reviews',
|
||||||
|
'admin.users': 'Users',
|
||||||
|
'admin.weather': 'Weather',
|
||||||
|
|
||||||
|
// Weather admin
|
||||||
|
'admin.weather.title': 'Weather Widget',
|
||||||
|
'admin.weather.description': 'Configure the weather widget displayed on the homepage',
|
||||||
|
'admin.weather.enabled': 'Enable Weather Widget',
|
||||||
|
'admin.weather.cities': 'Cities',
|
||||||
|
'admin.weather.add_city': 'Add City',
|
||||||
|
'admin.weather.city_name': 'City Name',
|
||||||
|
'admin.weather.latitude': 'Latitude',
|
||||||
|
'admin.weather.longitude': 'Longitude',
|
||||||
|
'admin.weather.save': 'Save Settings',
|
||||||
|
},
|
||||||
|
es: {
|
||||||
|
// Common
|
||||||
|
'common.hotel': 'Hotel',
|
||||||
|
'common.restaurant': 'Restaurante',
|
||||||
|
'common.andean_highlands': 'Tierras Altas Andinas',
|
||||||
|
'common.rooms_suites': 'Habitaciones & Suites',
|
||||||
|
'common.social_events': 'Eventos Sociales',
|
||||||
|
'common.coffee': 'Café',
|
||||||
|
'common.landscape': 'Paisaje',
|
||||||
|
'common.comments': 'Comentarios',
|
||||||
|
'common.user_portal': 'Portal de Usuario',
|
||||||
|
'common.login': 'Iniciar Sesión',
|
||||||
|
'common.logout': 'Cerrar Sesión',
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
'nav.brand': 'Marca',
|
||||||
|
'nav.gallery': 'Imágenes',
|
||||||
|
'nav.slideshow': 'Presentación',
|
||||||
|
'nav.calendar': 'Calendario',
|
||||||
|
'nav.social_events': 'Eventos Sociales',
|
||||||
|
'nav.rooms': 'Habitaciones',
|
||||||
|
'nav.menu': 'Menú',
|
||||||
|
'nav.places': 'Lugares',
|
||||||
|
'nav.reviews': 'Reseñas',
|
||||||
|
'nav.users': 'Usuarios',
|
||||||
|
'nav.weather': 'Clima',
|
||||||
|
|
||||||
|
// Weather widget
|
||||||
|
'weather.currently': 'Actualmente',
|
||||||
|
'weather.today': 'Hoy',
|
||||||
|
'weather.max': 'Máx',
|
||||||
|
'weather.min': 'Mín',
|
||||||
|
'weather.wind': 'Viento',
|
||||||
|
'weather.kmh': 'km/h',
|
||||||
|
'weather_updated': 'Actualizado',
|
||||||
|
|
||||||
|
// Admin panel
|
||||||
|
'admin.brand': 'Marca',
|
||||||
|
'admin.gallery': 'Imágenes',
|
||||||
|
'admin.slideshow': 'Presentación',
|
||||||
|
'admin.calendar': 'Calendario',
|
||||||
|
'admin.social_events': 'Eventos Sociales',
|
||||||
|
'admin.rooms': 'Habitaciones',
|
||||||
|
'admin.menu': 'Menú',
|
||||||
|
'admin.places': 'Lugares',
|
||||||
|
'admin.reviews': 'Reseñas',
|
||||||
|
'admin.users': 'Usuarios',
|
||||||
|
'admin.weather': 'Clima',
|
||||||
|
|
||||||
|
// Weather admin
|
||||||
|
'admin.weather.title': 'Widget del Clima',
|
||||||
|
'admin.weather.description': 'Configurar el widget del clima mostrado en la página de inicio',
|
||||||
|
'admin.weather.enabled': 'Habilitar Widget del Clima',
|
||||||
|
'admin.weather.cities': 'Ciudades',
|
||||||
|
'admin.weather.add_city': 'Agregar Ciudad',
|
||||||
|
'admin.weather.city_name': 'Nombre de la Ciudad',
|
||||||
|
'admin.weather.latitude': 'Latitud',
|
||||||
|
'admin.weather.longitude': 'Longitud',
|
||||||
|
'admin.weather.save': 'Guardar Configuración',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simple translation function
|
||||||
|
export function t(key: string, language: Language = 'en'): string {
|
||||||
|
if (language === 'es' && key in translations.es) {
|
||||||
|
return translations.es[key as keyof typeof translations.es];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key in translations.en) {
|
||||||
|
return translations.en[key as keyof typeof translations.en];
|
||||||
|
}
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get language from localStorage or default to 'en'
|
||||||
|
export function getLanguage(): Language {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const savedLang = localStorage.getItem('language');
|
||||||
|
if (savedLang && (savedLang === 'en' || savedLang === 'es')) {
|
||||||
|
return savedLang as Language;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set language in localStorage
|
||||||
|
export function setLanguage(lang: Language) {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem('language', lang);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user