Add language translation system and weather widget with admin controls

This commit is contained in:
2026-06-28 15:35:02 -05:00
parent 295c024ced
commit 01ea9e391d
7 changed files with 655 additions and 3 deletions
+41
View File
@@ -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>
);
}
+127
View File
@@ -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>
);
}
+154
View File
@@ -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>
);
}