diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
index 5c7b1bc..02118f9 100644
--- a/src/app/admin/page.tsx
+++ b/src/app/admin/page.tsx
@@ -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 (
+
+ );
+ }
+
+ return (
+
+
+
+
+ Configure the weather widget displayed on the homepage
+
+
+
+
+
+
+
Cities
+
+ {cities.map((city, index) => (
+
+ {city.name} ({city.lat}, {city.lon})
+
+
+ ))}
+
+
+
+
Add City
+
setNewCity({...newCity, name: v})} />
+
+ setNewCity({...newCity, lat: v})} />
+ setNewCity({...newCity, lon: v})} />
+
+
+
+
+
+
+
+
+
+ );
+}
+
/* ═══════════════════════════════════════════════════
MAIN PAGE
═══════════════════════════════════════════════════ */
@@ -1386,7 +1546,7 @@ export default function AdminPage() {
case 'gallery': return (
-
+
);
case 'slides': return ;
@@ -1397,6 +1557,7 @@ export default function AdminPage() {
case 'places': return ;
case 'reviews': return ;
case 'users': return ;
+ case 'weather': return ;
default: return ;
}
};
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 2044fbe..5373892 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -4,6 +4,9 @@ import { useEffect, useState } from 'react';
import Slideshow from '@/components/Slideshow';
import CalendarStrip from '@/components/CalendarStrip';
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 navy = '#010D1E';
@@ -23,6 +26,11 @@ export default function HomePage() {
return (
+
+
+
+
+
- Hotel · Restaurant · Andean Highlands
+ {t('common.hotel', getLanguage())} · {t('common.restaurant', getLanguage())} · {t('common.andean_highlands', getLanguage())}
diff --git a/src/app/rooms/page.tsx b/src/app/rooms/page.tsx
index 8f188e6..7dae44d 100644
--- a/src/app/rooms/page.tsx
+++ b/src/app/rooms/page.tsx
@@ -99,7 +99,9 @@ export default function RoomsPage() {
const postComment = async (roomId: number) => {
if (!draft.trim() || sending) return;
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`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
diff --git a/src/components/LanguageSwitcher.tsx b/src/components/LanguageSwitcher.tsx
new file mode 100644
index 0000000..70ff58d
--- /dev/null
+++ b/src/components/LanguageSwitcher.tsx
@@ -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 (
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/WeatherWidget.tsx b/src/components/WeatherWidget.tsx
new file mode 100644
index 0000000..7d4b030
--- /dev/null
+++ b/src/components/WeatherWidget.tsx
@@ -0,0 +1,127 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+
+export default function WeatherWidget() {
+ const [weather, setWeather] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [config, setConfig] = useState(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 (
+
+ );
+ }
+
+ if (error || !weather) {
+ return (
+
+ );
+ }
+
+ const current = weather.current;
+ const temp = current.celsius;
+ const icon = '🌤️';
+
+ return (
+
+
+
{icon}
+
+
+ {temp}°C
+
+ {config.cities[0] && (
+
+ {config.cities[0].name}
+
+ )}
+
+
+
+ Updated: {new Date(current.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/admin/WeatherSection.tsx b/src/components/admin/WeatherSection.tsx
new file mode 100644
index 0000000..9396762
--- /dev/null
+++ b/src/components/admin/WeatherSection.tsx
@@ -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>([
+ { 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 (
+
+
{t('admin.weather.title', 'en')}
+
{t('admin.weather.description', 'en')}
+
+
+
+
+
+
+
{t('admin.weather.cities', 'en')}
+ {cities.map((city, index) => (
+
+
+
+ updateCity(index, 'name', e.target.value)}
+ style={{ padding: '0.5rem', border: '1px solid #ddd', borderRadius: '4px' }}
+ />
+
+
+
+ updateCity(index, 'lat', parseFloat(e.target.value) || 0)}
+ style={{ padding: '0.5rem', border: '1px solid #ddd', borderRadius: '4px', width: '100px' }}
+ />
+
+
+
+ updateCity(index, 'lon', parseFloat(e.target.value) || 0)}
+ style={{ padding: '0.5rem', border: '1px solid #ddd', borderRadius: '4px', width: '100px' }}
+ />
+
+
+
+ ))}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/lib/i18n.ts b/src/lib/i18n.ts
new file mode 100644
index 0000000..6193ae7
--- /dev/null
+++ b/src/lib/i18n.ts
@@ -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);
+ }
+}
\ No newline at end of file