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 ( +
+ +
+ Loading... +
+
+ ); + } + + 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 ( +
+
Loading weather...
+
+ ); + } + + if (error || !weather) { + return ( +
+
Weather unavailable
+
+ ); + } + + 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