mirror of
https://github.com/screentinker/screentinker.git
synced 2026-05-15 07:32:23 -06:00
i18n batch 2a: wire widgets.js (~107 keys)
- All widget types (clock/weather/rss/text/webpage/social/directory-board) with localized names + descriptions - Full Directory Board editor (categories, entries, logo, backgrounds) - Content picker overlay - Confirms, toasts, empty states - 532 keys total, 100% parity across en/es/fr/de/pt Designer.js follows in batch 2b. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
eccf4b7af1
commit
0743901e48
|
|
@ -440,4 +440,113 @@ export default {
|
||||||
'settings.user.confirm': 'Bestätigen?',
|
'settings.user.confirm': 'Bestätigen?',
|
||||||
'settings.user.count_one': '1 Benutzer registriert',
|
'settings.user.count_one': '1 Benutzer registriert',
|
||||||
'settings.user.count_other': '{n} Benutzer registriert',
|
'settings.user.count_other': '{n} Benutzer registriert',
|
||||||
|
|
||||||
|
// Widgets
|
||||||
|
'widget.title': 'Widgets',
|
||||||
|
'widget.subtitle': 'Fügen Sie Ihren Layouts dynamische Inhalte hinzu',
|
||||||
|
'widget.help_tip': 'Dynamische Inhaltselemente: Live-Uhren, Wetter, RSS-Ticker, Text, Webseiten und Social-Feeds. Erstellen Sie ein Widget und weisen Sie es einer Geräte-Playlist zu.',
|
||||||
|
'widget.new_widget': 'Neues Widget',
|
||||||
|
'widget.configure': 'Widget konfigurieren',
|
||||||
|
'widget.preview': 'Vorschau',
|
||||||
|
'widget.preview_title': 'Vorschau',
|
||||||
|
'widget.close': 'Schließen',
|
||||||
|
'widget.edit_x': '{type} bearbeiten',
|
||||||
|
'widget.new_x': 'Neues {type}',
|
||||||
|
'widget.empty_title': 'Noch keine Widgets',
|
||||||
|
'widget.empty_desc': 'Erstellen Sie ein Widget, um Ihren Layouts dynamische Inhalte hinzuzufügen.',
|
||||||
|
'widget.this_widget': 'dieses Widget',
|
||||||
|
'widget.confirm_delete': '„{name}" löschen? Dies kann nicht rückgängig gemacht werden.',
|
||||||
|
'widget.toast.saved': 'Widget gespeichert',
|
||||||
|
'widget.toast.deleted': 'Widget gelöscht',
|
||||||
|
'widget.toast.preview_failed': 'Vorschau fehlgeschlagen',
|
||||||
|
'widget.type.clock.name': 'Uhr',
|
||||||
|
'widget.type.clock.desc': 'Digitaluhr mit Datum',
|
||||||
|
'widget.type.weather.name': 'Wetter',
|
||||||
|
'widget.type.weather.desc': 'Aktuelle Wetterbedingungen',
|
||||||
|
'widget.type.rss.name': 'Nachrichten-Ticker',
|
||||||
|
'widget.type.rss.desc': 'Scrollender RSS-Feed',
|
||||||
|
'widget.type.text.name': 'Text/HTML',
|
||||||
|
'widget.type.text.desc': 'Benutzerdefinierter Text oder HTML-Inhalt',
|
||||||
|
'widget.type.webpage.name': 'Webseite',
|
||||||
|
'widget.type.webpage.desc': 'Webseite einbetten',
|
||||||
|
'widget.type.social.name': 'Social Feed',
|
||||||
|
'widget.type.social.desc': 'Social-Media-Feed',
|
||||||
|
'widget.type.directory_board.name': 'Verzeichnistafel',
|
||||||
|
'widget.type.directory_board.desc': 'Scrollendes Mieter-/Raumverzeichnis für Lobbys',
|
||||||
|
'widget.field.name': 'Widget-Name',
|
||||||
|
'widget.field.format': 'Format',
|
||||||
|
'widget.field.format_12h': '12 Stunden',
|
||||||
|
'widget.field.format_24h': '24 Stunden',
|
||||||
|
'widget.field.timezone': 'Zeitzone',
|
||||||
|
'widget.field.font_size': 'Schriftgröße',
|
||||||
|
'widget.field.font_size_px': 'Schriftgröße (px)',
|
||||||
|
'widget.field.color': 'Farbe',
|
||||||
|
'widget.field.background': 'Hintergrund',
|
||||||
|
'widget.field.location': 'Standort',
|
||||||
|
'widget.field.location_placeholder': 'Stadt, Land',
|
||||||
|
'widget.field.units': 'Einheiten',
|
||||||
|
'widget.field.units_imperial': 'Imperial (°F)',
|
||||||
|
'widget.field.units_metric': 'Metrisch (°C)',
|
||||||
|
'widget.field.feed_url': 'Feed-URL',
|
||||||
|
'widget.field.scroll_speed_seconds': 'Scroll-Geschwindigkeit (Sekunden)',
|
||||||
|
'widget.field.max_items': 'Max. Elemente',
|
||||||
|
'widget.field.html_content': 'HTML-Inhalt',
|
||||||
|
'widget.field.css_optional': 'CSS (optional)',
|
||||||
|
'widget.field.url': 'URL',
|
||||||
|
'widget.field.zoom_pct': 'Zoom (%)',
|
||||||
|
'widget.field.refresh_interval': 'Aktualisierungsintervall (Sekunden, 0 = nie)',
|
||||||
|
'widget.field.platform': 'Plattform',
|
||||||
|
'widget.field.platform_twitter': 'Twitter/X',
|
||||||
|
'widget.field.platform_instagram': 'Instagram',
|
||||||
|
'widget.field.query': 'Abfrage',
|
||||||
|
'widget.field.query_placeholder': '@handle oder #hashtag',
|
||||||
|
'widget.picker.default_title': 'Bild auswählen',
|
||||||
|
'widget.picker.select_logo': 'Logo auswählen',
|
||||||
|
'widget.picker.select_bg_images': 'Hintergrundbilder auswählen',
|
||||||
|
'widget.picker.search': 'Bilder suchen...',
|
||||||
|
'widget.picker.no_matches': 'Keine Treffer.',
|
||||||
|
'widget.picker.no_images': 'Keine Bilder in Ihrer Bibliothek. Laden Sie zuerst Bilder aus der Inhaltsbibliothek hoch.',
|
||||||
|
'widget.picker.selected_count': '{n} ausgewählt',
|
||||||
|
'widget.dir.title_label': 'Titel',
|
||||||
|
'widget.dir.title_placeholder': 'Lincoln Lager',
|
||||||
|
'widget.dir.logo_label': 'Logo (optional)',
|
||||||
|
'widget.dir.footer_text_label': 'Fußzeile',
|
||||||
|
'widget.dir.footer_placeholder': 'Mietanfragen: Kontakt...',
|
||||||
|
'widget.dir.bg_images_label': 'Hintergrundbilder (optional)',
|
||||||
|
'widget.dir.bg_images_hint': 'Bilder wechseln alle 15 Sekunden bei 30 % Deckkraft. Mehrere für Rotation hinzufügen.',
|
||||||
|
'widget.dir.add_bg_image': '+ Hintergrundbild hinzufügen',
|
||||||
|
'widget.dir.theme': 'Thema',
|
||||||
|
'widget.dir.theme_dark': 'Dunkel',
|
||||||
|
'widget.dir.theme_light': 'Hell',
|
||||||
|
'widget.dir.scroll_speed': 'Scroll-Geschwindigkeit',
|
||||||
|
'widget.dir.speed_slow': 'Langsam',
|
||||||
|
'widget.dir.speed_medium': 'Mittel',
|
||||||
|
'widget.dir.speed_fast': 'Schnell',
|
||||||
|
'widget.dir.columns': 'Spalten',
|
||||||
|
'widget.dir.columns_auto': 'Auto',
|
||||||
|
'widget.dir.categories': 'Kategorien',
|
||||||
|
'widget.dir.add_category': '+ Kategorie hinzufügen',
|
||||||
|
'widget.dir.add_entry': '+ Eintrag hinzufügen',
|
||||||
|
'widget.dir.empty_categories': 'Fügen Sie Ihre erste Etage oder Abteilung hinzu, um zu beginnen',
|
||||||
|
'widget.dir.no_entries': 'Noch keine Einträge',
|
||||||
|
'widget.dir.entry': 'Eintrag',
|
||||||
|
'widget.dir.entries': 'Einträge',
|
||||||
|
'widget.dir.collapse': 'Einklappen',
|
||||||
|
'widget.dir.expand': 'Aufklappen',
|
||||||
|
'widget.dir.move_up': 'Nach oben',
|
||||||
|
'widget.dir.move_down': 'Nach unten',
|
||||||
|
'widget.dir.delete_category': 'Kategorie löschen',
|
||||||
|
'widget.dir.delete_entry': 'Eintrag löschen',
|
||||||
|
'widget.dir.unnamed': '(unbenannt)',
|
||||||
|
'widget.dir.confirm_delete_category': 'Kategorie „{name}" und alle Einträge löschen?',
|
||||||
|
'widget.dir.category_name_placeholder': 'z. B. Erste Etage',
|
||||||
|
'widget.dir.entry_id_placeholder': '101',
|
||||||
|
'widget.dir.entry_name_placeholder': 'Mietername',
|
||||||
|
'widget.dir.entry_subtitle_placeholder': 'Details (optional)',
|
||||||
|
'widget.dir.available': 'Verfügbar',
|
||||||
|
'widget.dir.change': 'Ändern',
|
||||||
|
'widget.dir.choose_logo': 'Logo wählen',
|
||||||
|
'widget.dir.remove_logo': 'Entfernen',
|
||||||
|
'widget.dir.no_bg_images': 'Keine Hintergrundbilder ausgewählt',
|
||||||
|
'widget.dir.remove_bg': 'Entfernen',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -467,4 +467,117 @@ export default {
|
||||||
'settings.user.confirm': 'Confirm?',
|
'settings.user.confirm': 'Confirm?',
|
||||||
'settings.user.count_one': '1 user registered',
|
'settings.user.count_one': '1 user registered',
|
||||||
'settings.user.count_other': '{n} users registered',
|
'settings.user.count_other': '{n} users registered',
|
||||||
|
|
||||||
|
// Widgets
|
||||||
|
'widget.title': 'Widgets',
|
||||||
|
'widget.subtitle': 'Add dynamic content to your layouts',
|
||||||
|
'widget.help_tip': 'Dynamic content elements: live clocks, weather, RSS tickers, text, webpages, and social feeds. Create a widget then assign it to a device playlist.',
|
||||||
|
'widget.new_widget': 'New Widget',
|
||||||
|
'widget.configure': 'Configure Widget',
|
||||||
|
'widget.preview': 'Preview',
|
||||||
|
'widget.preview_title': 'Preview',
|
||||||
|
'widget.close': 'Close',
|
||||||
|
'widget.edit_x': 'Edit {type}',
|
||||||
|
'widget.new_x': 'New {type}',
|
||||||
|
'widget.empty_title': 'No widgets yet',
|
||||||
|
'widget.empty_desc': 'Create a widget to add dynamic content to your layouts.',
|
||||||
|
'widget.this_widget': 'this widget',
|
||||||
|
'widget.confirm_delete': 'Delete "{name}"? This cannot be undone.',
|
||||||
|
'widget.toast.saved': 'Widget saved',
|
||||||
|
'widget.toast.deleted': 'Widget deleted',
|
||||||
|
'widget.toast.preview_failed': 'Preview failed',
|
||||||
|
// Widget types
|
||||||
|
'widget.type.clock.name': 'Clock',
|
||||||
|
'widget.type.clock.desc': 'Digital clock with date',
|
||||||
|
'widget.type.weather.name': 'Weather',
|
||||||
|
'widget.type.weather.desc': 'Current weather conditions',
|
||||||
|
'widget.type.rss.name': 'News Ticker',
|
||||||
|
'widget.type.rss.desc': 'Scrolling RSS feed',
|
||||||
|
'widget.type.text.name': 'Text/HTML',
|
||||||
|
'widget.type.text.desc': 'Custom text or HTML content',
|
||||||
|
'widget.type.webpage.name': 'Webpage',
|
||||||
|
'widget.type.webpage.desc': 'Embed a webpage',
|
||||||
|
'widget.type.social.name': 'Social Feed',
|
||||||
|
'widget.type.social.desc': 'Social media feed',
|
||||||
|
'widget.type.directory_board.name': 'Directory Board',
|
||||||
|
'widget.type.directory_board.desc': 'Scrolling tenant/room directory for lobbies',
|
||||||
|
// Widget config form fields
|
||||||
|
'widget.field.name': 'Widget Name',
|
||||||
|
'widget.field.format': 'Format',
|
||||||
|
'widget.field.format_12h': '12 Hour',
|
||||||
|
'widget.field.format_24h': '24 Hour',
|
||||||
|
'widget.field.timezone': 'Timezone',
|
||||||
|
'widget.field.font_size': 'Font Size',
|
||||||
|
'widget.field.font_size_px': 'Font Size (px)',
|
||||||
|
'widget.field.color': 'Color',
|
||||||
|
'widget.field.background': 'Background',
|
||||||
|
'widget.field.location': 'Location',
|
||||||
|
'widget.field.location_placeholder': 'City, State',
|
||||||
|
'widget.field.units': 'Units',
|
||||||
|
'widget.field.units_imperial': 'Imperial (°F)',
|
||||||
|
'widget.field.units_metric': 'Metric (°C)',
|
||||||
|
'widget.field.feed_url': 'Feed URL',
|
||||||
|
'widget.field.scroll_speed_seconds': 'Scroll Speed (seconds)',
|
||||||
|
'widget.field.max_items': 'Max Items',
|
||||||
|
'widget.field.html_content': 'HTML Content',
|
||||||
|
'widget.field.css_optional': 'CSS (optional)',
|
||||||
|
'widget.field.url': 'URL',
|
||||||
|
'widget.field.zoom_pct': 'Zoom (%)',
|
||||||
|
'widget.field.refresh_interval': 'Refresh Interval (seconds, 0 = never)',
|
||||||
|
'widget.field.platform': 'Platform',
|
||||||
|
'widget.field.platform_twitter': 'Twitter/X',
|
||||||
|
'widget.field.platform_instagram': 'Instagram',
|
||||||
|
'widget.field.query': 'Query',
|
||||||
|
'widget.field.query_placeholder': '@handle or #hashtag',
|
||||||
|
// Content picker
|
||||||
|
'widget.picker.default_title': 'Select Image',
|
||||||
|
'widget.picker.select_logo': 'Select Logo',
|
||||||
|
'widget.picker.select_bg_images': 'Select Background Images',
|
||||||
|
'widget.picker.search': 'Search images...',
|
||||||
|
'widget.picker.no_matches': 'No matches.',
|
||||||
|
'widget.picker.no_images': 'No images in your content library. Upload images first from Content Library.',
|
||||||
|
'widget.picker.selected_count': '{n} selected',
|
||||||
|
// Directory Board
|
||||||
|
'widget.dir.title_label': 'Title',
|
||||||
|
'widget.dir.title_placeholder': 'Lincoln Warehouse',
|
||||||
|
'widget.dir.logo_label': 'Logo (optional)',
|
||||||
|
'widget.dir.footer_text_label': 'Footer Text',
|
||||||
|
'widget.dir.footer_placeholder': 'For Leasing Inquiries: Contact...',
|
||||||
|
'widget.dir.bg_images_label': 'Background Images (optional)',
|
||||||
|
'widget.dir.bg_images_hint': 'Images crossfade every 15 seconds at 30% opacity. Add multiple for rotation.',
|
||||||
|
'widget.dir.add_bg_image': '+ Add Background Image',
|
||||||
|
'widget.dir.theme': 'Theme',
|
||||||
|
'widget.dir.theme_dark': 'Dark',
|
||||||
|
'widget.dir.theme_light': 'Light',
|
||||||
|
'widget.dir.scroll_speed': 'Scroll Speed',
|
||||||
|
'widget.dir.speed_slow': 'Slow',
|
||||||
|
'widget.dir.speed_medium': 'Medium',
|
||||||
|
'widget.dir.speed_fast': 'Fast',
|
||||||
|
'widget.dir.columns': 'Columns',
|
||||||
|
'widget.dir.columns_auto': 'Auto',
|
||||||
|
'widget.dir.categories': 'Categories',
|
||||||
|
'widget.dir.add_category': '+ Add Category',
|
||||||
|
'widget.dir.add_entry': '+ Add Entry',
|
||||||
|
'widget.dir.empty_categories': 'Add your first floor or department to get started',
|
||||||
|
'widget.dir.no_entries': 'No entries yet',
|
||||||
|
'widget.dir.entry': 'entry',
|
||||||
|
'widget.dir.entries': 'entries',
|
||||||
|
'widget.dir.collapse': 'Collapse',
|
||||||
|
'widget.dir.expand': 'Expand',
|
||||||
|
'widget.dir.move_up': 'Move up',
|
||||||
|
'widget.dir.move_down': 'Move down',
|
||||||
|
'widget.dir.delete_category': 'Delete category',
|
||||||
|
'widget.dir.delete_entry': 'Delete entry',
|
||||||
|
'widget.dir.unnamed': '(unnamed)',
|
||||||
|
'widget.dir.confirm_delete_category': 'Delete category "{name}" and all its entries?',
|
||||||
|
'widget.dir.category_name_placeholder': 'e.g. First Floor',
|
||||||
|
'widget.dir.entry_id_placeholder': '101',
|
||||||
|
'widget.dir.entry_name_placeholder': 'Tenant name',
|
||||||
|
'widget.dir.entry_subtitle_placeholder': 'Details (optional)',
|
||||||
|
'widget.dir.available': 'Available',
|
||||||
|
'widget.dir.change': 'Change',
|
||||||
|
'widget.dir.choose_logo': 'Choose Logo',
|
||||||
|
'widget.dir.remove_logo': 'Remove',
|
||||||
|
'widget.dir.no_bg_images': 'No background images selected',
|
||||||
|
'widget.dir.remove_bg': 'Remove',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -439,4 +439,113 @@ export default {
|
||||||
'settings.user.confirm': '¿Confirmar?',
|
'settings.user.confirm': '¿Confirmar?',
|
||||||
'settings.user.count_one': '1 usuario registrado',
|
'settings.user.count_one': '1 usuario registrado',
|
||||||
'settings.user.count_other': '{n} usuarios registrados',
|
'settings.user.count_other': '{n} usuarios registrados',
|
||||||
|
|
||||||
|
// Widgets
|
||||||
|
'widget.title': 'Widgets',
|
||||||
|
'widget.subtitle': 'Agrega contenido dinámico a tus diseños',
|
||||||
|
'widget.help_tip': 'Elementos de contenido dinámico: relojes en vivo, clima, tickers RSS, texto, páginas web y feeds sociales. Crea un widget y asígnalo a la lista de un dispositivo.',
|
||||||
|
'widget.new_widget': 'Nuevo widget',
|
||||||
|
'widget.configure': 'Configurar widget',
|
||||||
|
'widget.preview': 'Previsualizar',
|
||||||
|
'widget.preview_title': 'Previsualización',
|
||||||
|
'widget.close': 'Cerrar',
|
||||||
|
'widget.edit_x': 'Editar {type}',
|
||||||
|
'widget.new_x': 'Nuevo {type}',
|
||||||
|
'widget.empty_title': 'Aún no hay widgets',
|
||||||
|
'widget.empty_desc': 'Crea un widget para agregar contenido dinámico a tus diseños.',
|
||||||
|
'widget.this_widget': 'este widget',
|
||||||
|
'widget.confirm_delete': '¿Eliminar "{name}"? Esto no se puede deshacer.',
|
||||||
|
'widget.toast.saved': 'Widget guardado',
|
||||||
|
'widget.toast.deleted': 'Widget eliminado',
|
||||||
|
'widget.toast.preview_failed': 'Falló la previsualización',
|
||||||
|
'widget.type.clock.name': 'Reloj',
|
||||||
|
'widget.type.clock.desc': 'Reloj digital con fecha',
|
||||||
|
'widget.type.weather.name': 'Clima',
|
||||||
|
'widget.type.weather.desc': 'Condiciones meteorológicas actuales',
|
||||||
|
'widget.type.rss.name': 'Ticker de noticias',
|
||||||
|
'widget.type.rss.desc': 'Feed RSS con desplazamiento',
|
||||||
|
'widget.type.text.name': 'Texto/HTML',
|
||||||
|
'widget.type.text.desc': 'Texto o HTML personalizado',
|
||||||
|
'widget.type.webpage.name': 'Página web',
|
||||||
|
'widget.type.webpage.desc': 'Incrustar una página web',
|
||||||
|
'widget.type.social.name': 'Feed social',
|
||||||
|
'widget.type.social.desc': 'Feed de redes sociales',
|
||||||
|
'widget.type.directory_board.name': 'Directorio',
|
||||||
|
'widget.type.directory_board.desc': 'Directorio de inquilinos/salas con desplazamiento para vestíbulos',
|
||||||
|
'widget.field.name': 'Nombre del widget',
|
||||||
|
'widget.field.format': 'Formato',
|
||||||
|
'widget.field.format_12h': '12 horas',
|
||||||
|
'widget.field.format_24h': '24 horas',
|
||||||
|
'widget.field.timezone': 'Zona horaria',
|
||||||
|
'widget.field.font_size': 'Tamaño de fuente',
|
||||||
|
'widget.field.font_size_px': 'Tamaño de fuente (px)',
|
||||||
|
'widget.field.color': 'Color',
|
||||||
|
'widget.field.background': 'Fondo',
|
||||||
|
'widget.field.location': 'Ubicación',
|
||||||
|
'widget.field.location_placeholder': 'Ciudad, Estado',
|
||||||
|
'widget.field.units': 'Unidades',
|
||||||
|
'widget.field.units_imperial': 'Imperial (°F)',
|
||||||
|
'widget.field.units_metric': 'Métrico (°C)',
|
||||||
|
'widget.field.feed_url': 'URL del feed',
|
||||||
|
'widget.field.scroll_speed_seconds': 'Velocidad de desplazamiento (segundos)',
|
||||||
|
'widget.field.max_items': 'Máx. elementos',
|
||||||
|
'widget.field.html_content': 'Contenido HTML',
|
||||||
|
'widget.field.css_optional': 'CSS (opcional)',
|
||||||
|
'widget.field.url': 'URL',
|
||||||
|
'widget.field.zoom_pct': 'Zoom (%)',
|
||||||
|
'widget.field.refresh_interval': 'Intervalo de actualización (segundos, 0 = nunca)',
|
||||||
|
'widget.field.platform': 'Plataforma',
|
||||||
|
'widget.field.platform_twitter': 'Twitter/X',
|
||||||
|
'widget.field.platform_instagram': 'Instagram',
|
||||||
|
'widget.field.query': 'Consulta',
|
||||||
|
'widget.field.query_placeholder': '@usuario o #hashtag',
|
||||||
|
'widget.picker.default_title': 'Seleccionar imagen',
|
||||||
|
'widget.picker.select_logo': 'Seleccionar logotipo',
|
||||||
|
'widget.picker.select_bg_images': 'Seleccionar imágenes de fondo',
|
||||||
|
'widget.picker.search': 'Buscar imágenes...',
|
||||||
|
'widget.picker.no_matches': 'Sin coincidencias.',
|
||||||
|
'widget.picker.no_images': 'No hay imágenes en tu biblioteca. Sube imágenes primero desde la Biblioteca de contenido.',
|
||||||
|
'widget.picker.selected_count': '{n} seleccionadas',
|
||||||
|
'widget.dir.title_label': 'Título',
|
||||||
|
'widget.dir.title_placeholder': 'Almacén Lincoln',
|
||||||
|
'widget.dir.logo_label': 'Logotipo (opcional)',
|
||||||
|
'widget.dir.footer_text_label': 'Texto del pie',
|
||||||
|
'widget.dir.footer_placeholder': 'Consultas de arrendamiento: Contacto...',
|
||||||
|
'widget.dir.bg_images_label': 'Imágenes de fondo (opcional)',
|
||||||
|
'widget.dir.bg_images_hint': 'Las imágenes se alternan cada 15 segundos al 30% de opacidad. Agrega varias para rotación.',
|
||||||
|
'widget.dir.add_bg_image': '+ Agregar imagen de fondo',
|
||||||
|
'widget.dir.theme': 'Tema',
|
||||||
|
'widget.dir.theme_dark': 'Oscuro',
|
||||||
|
'widget.dir.theme_light': 'Claro',
|
||||||
|
'widget.dir.scroll_speed': 'Velocidad de desplazamiento',
|
||||||
|
'widget.dir.speed_slow': 'Lenta',
|
||||||
|
'widget.dir.speed_medium': 'Media',
|
||||||
|
'widget.dir.speed_fast': 'Rápida',
|
||||||
|
'widget.dir.columns': 'Columnas',
|
||||||
|
'widget.dir.columns_auto': 'Auto',
|
||||||
|
'widget.dir.categories': 'Categorías',
|
||||||
|
'widget.dir.add_category': '+ Agregar categoría',
|
||||||
|
'widget.dir.add_entry': '+ Agregar entrada',
|
||||||
|
'widget.dir.empty_categories': 'Agrega tu primer piso o departamento para empezar',
|
||||||
|
'widget.dir.no_entries': 'Aún no hay entradas',
|
||||||
|
'widget.dir.entry': 'entrada',
|
||||||
|
'widget.dir.entries': 'entradas',
|
||||||
|
'widget.dir.collapse': 'Contraer',
|
||||||
|
'widget.dir.expand': 'Expandir',
|
||||||
|
'widget.dir.move_up': 'Subir',
|
||||||
|
'widget.dir.move_down': 'Bajar',
|
||||||
|
'widget.dir.delete_category': 'Eliminar categoría',
|
||||||
|
'widget.dir.delete_entry': 'Eliminar entrada',
|
||||||
|
'widget.dir.unnamed': '(sin nombre)',
|
||||||
|
'widget.dir.confirm_delete_category': '¿Eliminar la categoría "{name}" y todas sus entradas?',
|
||||||
|
'widget.dir.category_name_placeholder': 'p. ej. Primer piso',
|
||||||
|
'widget.dir.entry_id_placeholder': '101',
|
||||||
|
'widget.dir.entry_name_placeholder': 'Nombre del inquilino',
|
||||||
|
'widget.dir.entry_subtitle_placeholder': 'Detalles (opcional)',
|
||||||
|
'widget.dir.available': 'Disponible',
|
||||||
|
'widget.dir.change': 'Cambiar',
|
||||||
|
'widget.dir.choose_logo': 'Elegir logotipo',
|
||||||
|
'widget.dir.remove_logo': 'Quitar',
|
||||||
|
'widget.dir.no_bg_images': 'No se han seleccionado imágenes de fondo',
|
||||||
|
'widget.dir.remove_bg': 'Quitar',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -440,4 +440,113 @@ export default {
|
||||||
'settings.user.confirm': 'Confirmer ?',
|
'settings.user.confirm': 'Confirmer ?',
|
||||||
'settings.user.count_one': '1 utilisateur inscrit',
|
'settings.user.count_one': '1 utilisateur inscrit',
|
||||||
'settings.user.count_other': '{n} utilisateurs inscrits',
|
'settings.user.count_other': '{n} utilisateurs inscrits',
|
||||||
|
|
||||||
|
// Widgets
|
||||||
|
'widget.title': 'Widgets',
|
||||||
|
'widget.subtitle': 'Ajoutez du contenu dynamique à vos mises en page',
|
||||||
|
'widget.help_tip': 'Éléments de contenu dynamique : horloges, météo, tickers RSS, texte, pages web et flux sociaux. Créez un widget puis attribuez-le à la liste d\'un appareil.',
|
||||||
|
'widget.new_widget': 'Nouveau widget',
|
||||||
|
'widget.configure': 'Configurer le widget',
|
||||||
|
'widget.preview': 'Aperçu',
|
||||||
|
'widget.preview_title': 'Aperçu',
|
||||||
|
'widget.close': 'Fermer',
|
||||||
|
'widget.edit_x': 'Modifier {type}',
|
||||||
|
'widget.new_x': 'Nouveau {type}',
|
||||||
|
'widget.empty_title': 'Aucun widget pour le moment',
|
||||||
|
'widget.empty_desc': 'Créez un widget pour ajouter du contenu dynamique à vos mises en page.',
|
||||||
|
'widget.this_widget': 'ce widget',
|
||||||
|
'widget.confirm_delete': 'Supprimer « {name} » ? Cette action est irréversible.',
|
||||||
|
'widget.toast.saved': 'Widget enregistré',
|
||||||
|
'widget.toast.deleted': 'Widget supprimé',
|
||||||
|
'widget.toast.preview_failed': 'Échec de l\'aperçu',
|
||||||
|
'widget.type.clock.name': 'Horloge',
|
||||||
|
'widget.type.clock.desc': 'Horloge numérique avec date',
|
||||||
|
'widget.type.weather.name': 'Météo',
|
||||||
|
'widget.type.weather.desc': 'Conditions météo actuelles',
|
||||||
|
'widget.type.rss.name': 'Bandeau d\'actualités',
|
||||||
|
'widget.type.rss.desc': 'Flux RSS défilant',
|
||||||
|
'widget.type.text.name': 'Texte/HTML',
|
||||||
|
'widget.type.text.desc': 'Texte ou contenu HTML personnalisé',
|
||||||
|
'widget.type.webpage.name': 'Page web',
|
||||||
|
'widget.type.webpage.desc': 'Intégrer une page web',
|
||||||
|
'widget.type.social.name': 'Flux social',
|
||||||
|
'widget.type.social.desc': 'Flux de réseaux sociaux',
|
||||||
|
'widget.type.directory_board.name': 'Annuaire',
|
||||||
|
'widget.type.directory_board.desc': 'Annuaire défilant des locataires/salles pour halls',
|
||||||
|
'widget.field.name': 'Nom du widget',
|
||||||
|
'widget.field.format': 'Format',
|
||||||
|
'widget.field.format_12h': '12 heures',
|
||||||
|
'widget.field.format_24h': '24 heures',
|
||||||
|
'widget.field.timezone': 'Fuseau horaire',
|
||||||
|
'widget.field.font_size': 'Taille de police',
|
||||||
|
'widget.field.font_size_px': 'Taille de police (px)',
|
||||||
|
'widget.field.color': 'Couleur',
|
||||||
|
'widget.field.background': 'Fond',
|
||||||
|
'widget.field.location': 'Emplacement',
|
||||||
|
'widget.field.location_placeholder': 'Ville, Région',
|
||||||
|
'widget.field.units': 'Unités',
|
||||||
|
'widget.field.units_imperial': 'Impérial (°F)',
|
||||||
|
'widget.field.units_metric': 'Métrique (°C)',
|
||||||
|
'widget.field.feed_url': 'URL du flux',
|
||||||
|
'widget.field.scroll_speed_seconds': 'Vitesse de défilement (secondes)',
|
||||||
|
'widget.field.max_items': 'Éléments max',
|
||||||
|
'widget.field.html_content': 'Contenu HTML',
|
||||||
|
'widget.field.css_optional': 'CSS (facultatif)',
|
||||||
|
'widget.field.url': 'URL',
|
||||||
|
'widget.field.zoom_pct': 'Zoom (%)',
|
||||||
|
'widget.field.refresh_interval': 'Intervalle d\'actualisation (secondes, 0 = jamais)',
|
||||||
|
'widget.field.platform': 'Plateforme',
|
||||||
|
'widget.field.platform_twitter': 'Twitter/X',
|
||||||
|
'widget.field.platform_instagram': 'Instagram',
|
||||||
|
'widget.field.query': 'Requête',
|
||||||
|
'widget.field.query_placeholder': '@compte ou #hashtag',
|
||||||
|
'widget.picker.default_title': 'Sélectionner une image',
|
||||||
|
'widget.picker.select_logo': 'Sélectionner un logo',
|
||||||
|
'widget.picker.select_bg_images': 'Sélectionner des images de fond',
|
||||||
|
'widget.picker.search': 'Rechercher des images...',
|
||||||
|
'widget.picker.no_matches': 'Aucun résultat.',
|
||||||
|
'widget.picker.no_images': 'Aucune image dans votre bibliothèque. Téléversez d\'abord des images depuis la Bibliothèque.',
|
||||||
|
'widget.picker.selected_count': '{n} sélectionnées',
|
||||||
|
'widget.dir.title_label': 'Titre',
|
||||||
|
'widget.dir.title_placeholder': 'Entrepôt Lincoln',
|
||||||
|
'widget.dir.logo_label': 'Logo (facultatif)',
|
||||||
|
'widget.dir.footer_text_label': 'Texte de pied',
|
||||||
|
'widget.dir.footer_placeholder': 'Demandes de location : Contact...',
|
||||||
|
'widget.dir.bg_images_label': 'Images de fond (facultatif)',
|
||||||
|
'widget.dir.bg_images_hint': 'Les images alternent toutes les 15 secondes à 30 % d\'opacité. Ajoutez-en plusieurs pour la rotation.',
|
||||||
|
'widget.dir.add_bg_image': '+ Ajouter une image de fond',
|
||||||
|
'widget.dir.theme': 'Thème',
|
||||||
|
'widget.dir.theme_dark': 'Sombre',
|
||||||
|
'widget.dir.theme_light': 'Clair',
|
||||||
|
'widget.dir.scroll_speed': 'Vitesse de défilement',
|
||||||
|
'widget.dir.speed_slow': 'Lente',
|
||||||
|
'widget.dir.speed_medium': 'Moyenne',
|
||||||
|
'widget.dir.speed_fast': 'Rapide',
|
||||||
|
'widget.dir.columns': 'Colonnes',
|
||||||
|
'widget.dir.columns_auto': 'Auto',
|
||||||
|
'widget.dir.categories': 'Catégories',
|
||||||
|
'widget.dir.add_category': '+ Ajouter une catégorie',
|
||||||
|
'widget.dir.add_entry': '+ Ajouter une entrée',
|
||||||
|
'widget.dir.empty_categories': 'Ajoutez votre premier étage ou département pour commencer',
|
||||||
|
'widget.dir.no_entries': 'Pas encore d\'entrées',
|
||||||
|
'widget.dir.entry': 'entrée',
|
||||||
|
'widget.dir.entries': 'entrées',
|
||||||
|
'widget.dir.collapse': 'Réduire',
|
||||||
|
'widget.dir.expand': 'Développer',
|
||||||
|
'widget.dir.move_up': 'Monter',
|
||||||
|
'widget.dir.move_down': 'Descendre',
|
||||||
|
'widget.dir.delete_category': 'Supprimer la catégorie',
|
||||||
|
'widget.dir.delete_entry': 'Supprimer l\'entrée',
|
||||||
|
'widget.dir.unnamed': '(sans nom)',
|
||||||
|
'widget.dir.confirm_delete_category': 'Supprimer la catégorie « {name} » et toutes ses entrées ?',
|
||||||
|
'widget.dir.category_name_placeholder': 'ex. Premier étage',
|
||||||
|
'widget.dir.entry_id_placeholder': '101',
|
||||||
|
'widget.dir.entry_name_placeholder': 'Nom du locataire',
|
||||||
|
'widget.dir.entry_subtitle_placeholder': 'Détails (facultatif)',
|
||||||
|
'widget.dir.available': 'Disponible',
|
||||||
|
'widget.dir.change': 'Changer',
|
||||||
|
'widget.dir.choose_logo': 'Choisir un logo',
|
||||||
|
'widget.dir.remove_logo': 'Retirer',
|
||||||
|
'widget.dir.no_bg_images': 'Aucune image de fond sélectionnée',
|
||||||
|
'widget.dir.remove_bg': 'Retirer',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -440,4 +440,113 @@ export default {
|
||||||
'settings.user.confirm': 'Confirmar?',
|
'settings.user.confirm': 'Confirmar?',
|
||||||
'settings.user.count_one': '1 usuário registrado',
|
'settings.user.count_one': '1 usuário registrado',
|
||||||
'settings.user.count_other': '{n} usuários registrados',
|
'settings.user.count_other': '{n} usuários registrados',
|
||||||
|
|
||||||
|
// Widgets
|
||||||
|
'widget.title': 'Widgets',
|
||||||
|
'widget.subtitle': 'Adicione conteúdo dinâmico aos seus layouts',
|
||||||
|
'widget.help_tip': 'Elementos de conteúdo dinâmico: relógios ao vivo, clima, tickers RSS, texto, páginas web e feeds sociais. Crie um widget e atribua à playlist de um dispositivo.',
|
||||||
|
'widget.new_widget': 'Novo widget',
|
||||||
|
'widget.configure': 'Configurar widget',
|
||||||
|
'widget.preview': 'Pré-visualizar',
|
||||||
|
'widget.preview_title': 'Pré-visualização',
|
||||||
|
'widget.close': 'Fechar',
|
||||||
|
'widget.edit_x': 'Editar {type}',
|
||||||
|
'widget.new_x': 'Novo {type}',
|
||||||
|
'widget.empty_title': 'Nenhum widget ainda',
|
||||||
|
'widget.empty_desc': 'Crie um widget para adicionar conteúdo dinâmico aos seus layouts.',
|
||||||
|
'widget.this_widget': 'este widget',
|
||||||
|
'widget.confirm_delete': 'Excluir "{name}"? Isso não pode ser desfeito.',
|
||||||
|
'widget.toast.saved': 'Widget salvo',
|
||||||
|
'widget.toast.deleted': 'Widget excluído',
|
||||||
|
'widget.toast.preview_failed': 'Falha na pré-visualização',
|
||||||
|
'widget.type.clock.name': 'Relógio',
|
||||||
|
'widget.type.clock.desc': 'Relógio digital com data',
|
||||||
|
'widget.type.weather.name': 'Clima',
|
||||||
|
'widget.type.weather.desc': 'Condições climáticas atuais',
|
||||||
|
'widget.type.rss.name': 'Ticker de notícias',
|
||||||
|
'widget.type.rss.desc': 'Feed RSS com rolagem',
|
||||||
|
'widget.type.text.name': 'Texto/HTML',
|
||||||
|
'widget.type.text.desc': 'Texto ou conteúdo HTML personalizado',
|
||||||
|
'widget.type.webpage.name': 'Página web',
|
||||||
|
'widget.type.webpage.desc': 'Incorporar uma página web',
|
||||||
|
'widget.type.social.name': 'Feed social',
|
||||||
|
'widget.type.social.desc': 'Feed de redes sociais',
|
||||||
|
'widget.type.directory_board.name': 'Diretório',
|
||||||
|
'widget.type.directory_board.desc': 'Diretório rolante de inquilinos/salas para lobbies',
|
||||||
|
'widget.field.name': 'Nome do widget',
|
||||||
|
'widget.field.format': 'Formato',
|
||||||
|
'widget.field.format_12h': '12 horas',
|
||||||
|
'widget.field.format_24h': '24 horas',
|
||||||
|
'widget.field.timezone': 'Fuso horário',
|
||||||
|
'widget.field.font_size': 'Tamanho da fonte',
|
||||||
|
'widget.field.font_size_px': 'Tamanho da fonte (px)',
|
||||||
|
'widget.field.color': 'Cor',
|
||||||
|
'widget.field.background': 'Fundo',
|
||||||
|
'widget.field.location': 'Local',
|
||||||
|
'widget.field.location_placeholder': 'Cidade, Estado',
|
||||||
|
'widget.field.units': 'Unidades',
|
||||||
|
'widget.field.units_imperial': 'Imperial (°F)',
|
||||||
|
'widget.field.units_metric': 'Métrico (°C)',
|
||||||
|
'widget.field.feed_url': 'URL do feed',
|
||||||
|
'widget.field.scroll_speed_seconds': 'Velocidade de rolagem (segundos)',
|
||||||
|
'widget.field.max_items': 'Máx. itens',
|
||||||
|
'widget.field.html_content': 'Conteúdo HTML',
|
||||||
|
'widget.field.css_optional': 'CSS (opcional)',
|
||||||
|
'widget.field.url': 'URL',
|
||||||
|
'widget.field.zoom_pct': 'Zoom (%)',
|
||||||
|
'widget.field.refresh_interval': 'Intervalo de atualização (segundos, 0 = nunca)',
|
||||||
|
'widget.field.platform': 'Plataforma',
|
||||||
|
'widget.field.platform_twitter': 'Twitter/X',
|
||||||
|
'widget.field.platform_instagram': 'Instagram',
|
||||||
|
'widget.field.query': 'Consulta',
|
||||||
|
'widget.field.query_placeholder': '@usuario ou #hashtag',
|
||||||
|
'widget.picker.default_title': 'Selecionar imagem',
|
||||||
|
'widget.picker.select_logo': 'Selecionar logotipo',
|
||||||
|
'widget.picker.select_bg_images': 'Selecionar imagens de fundo',
|
||||||
|
'widget.picker.search': 'Buscar imagens...',
|
||||||
|
'widget.picker.no_matches': 'Sem correspondências.',
|
||||||
|
'widget.picker.no_images': 'Sem imagens na biblioteca. Envie imagens primeiro pela Biblioteca de conteúdo.',
|
||||||
|
'widget.picker.selected_count': '{n} selecionadas',
|
||||||
|
'widget.dir.title_label': 'Título',
|
||||||
|
'widget.dir.title_placeholder': 'Armazém Lincoln',
|
||||||
|
'widget.dir.logo_label': 'Logotipo (opcional)',
|
||||||
|
'widget.dir.footer_text_label': 'Texto do rodapé',
|
||||||
|
'widget.dir.footer_placeholder': 'Consultas de locação: Contato...',
|
||||||
|
'widget.dir.bg_images_label': 'Imagens de fundo (opcional)',
|
||||||
|
'widget.dir.bg_images_hint': 'As imagens alternam a cada 15 segundos com 30% de opacidade. Adicione várias para rotação.',
|
||||||
|
'widget.dir.add_bg_image': '+ Adicionar imagem de fundo',
|
||||||
|
'widget.dir.theme': 'Tema',
|
||||||
|
'widget.dir.theme_dark': 'Escuro',
|
||||||
|
'widget.dir.theme_light': 'Claro',
|
||||||
|
'widget.dir.scroll_speed': 'Velocidade de rolagem',
|
||||||
|
'widget.dir.speed_slow': 'Lenta',
|
||||||
|
'widget.dir.speed_medium': 'Média',
|
||||||
|
'widget.dir.speed_fast': 'Rápida',
|
||||||
|
'widget.dir.columns': 'Colunas',
|
||||||
|
'widget.dir.columns_auto': 'Auto',
|
||||||
|
'widget.dir.categories': 'Categorias',
|
||||||
|
'widget.dir.add_category': '+ Adicionar categoria',
|
||||||
|
'widget.dir.add_entry': '+ Adicionar entrada',
|
||||||
|
'widget.dir.empty_categories': 'Adicione seu primeiro andar ou departamento para começar',
|
||||||
|
'widget.dir.no_entries': 'Sem entradas ainda',
|
||||||
|
'widget.dir.entry': 'entrada',
|
||||||
|
'widget.dir.entries': 'entradas',
|
||||||
|
'widget.dir.collapse': 'Recolher',
|
||||||
|
'widget.dir.expand': 'Expandir',
|
||||||
|
'widget.dir.move_up': 'Mover para cima',
|
||||||
|
'widget.dir.move_down': 'Mover para baixo',
|
||||||
|
'widget.dir.delete_category': 'Excluir categoria',
|
||||||
|
'widget.dir.delete_entry': 'Excluir entrada',
|
||||||
|
'widget.dir.unnamed': '(sem nome)',
|
||||||
|
'widget.dir.confirm_delete_category': 'Excluir a categoria "{name}" e todas as entradas?',
|
||||||
|
'widget.dir.category_name_placeholder': 'ex. Primeiro andar',
|
||||||
|
'widget.dir.entry_id_placeholder': '101',
|
||||||
|
'widget.dir.entry_name_placeholder': 'Nome do inquilino',
|
||||||
|
'widget.dir.entry_subtitle_placeholder': 'Detalhes (opcional)',
|
||||||
|
'widget.dir.available': 'Disponível',
|
||||||
|
'widget.dir.change': 'Alterar',
|
||||||
|
'widget.dir.choose_logo': 'Escolher logotipo',
|
||||||
|
'widget.dir.remove_logo': 'Remover',
|
||||||
|
'widget.dir.no_bg_images': 'Nenhuma imagem de fundo selecionada',
|
||||||
|
'widget.dir.remove_bg': 'Remover',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,41 @@
|
||||||
import { showToast } from '../components/toast.js';
|
import { showToast } from '../components/toast.js';
|
||||||
|
import { t } from '../i18n.js';
|
||||||
|
|
||||||
const API = (url, opts = {}) => fetch('/api' + url, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}`, ...opts.headers }, ...opts }).then(r => r.json());
|
const API = (url, opts = {}) => fetch('/api' + url, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}`, ...opts.headers }, ...opts }).then(r => r.json());
|
||||||
|
|
||||||
const WIDGET_TYPES = [
|
// Widget type ids only — name + desc are looked up via t() so they switch
|
||||||
{ id: 'clock', name: 'Clock', icon: '🕓', desc: 'Digital clock with date' },
|
// language with the rest of the UI.
|
||||||
{ id: 'weather', name: 'Weather', icon: '⛅', desc: 'Current weather conditions' },
|
const WIDGET_TYPES = ['clock', 'weather', 'rss', 'text', 'webpage', 'social', 'directory-board'];
|
||||||
{ id: 'rss', name: 'News Ticker', icon: '📰', desc: 'Scrolling RSS feed' },
|
const WIDGET_ICONS = {
|
||||||
{ id: 'text', name: 'Text/HTML', icon: '📝', desc: 'Custom text or HTML content' },
|
clock: '🕓',
|
||||||
{ id: 'webpage', name: 'Webpage', icon: '🌐', desc: 'Embed a webpage' },
|
weather: '⛅',
|
||||||
{ id: 'social', name: 'Social Feed', icon: '💬', desc: 'Social media feed' },
|
rss: '📰',
|
||||||
{ id: 'directory-board', name: 'Directory Board', icon: '🏢', desc: 'Scrolling tenant/room directory for lobbies' },
|
text: '📝',
|
||||||
];
|
webpage: '🌐',
|
||||||
|
social: '💬',
|
||||||
|
'directory-board': '🏢',
|
||||||
|
};
|
||||||
|
const widgetTypeName = (id) => t(`widget.type.${id.replace(/-/g, '_')}.name`);
|
||||||
|
const widgetTypeDesc = (id) => t(`widget.type.${id.replace(/-/g, '_')}.desc`);
|
||||||
|
|
||||||
function escAttr(s) {
|
function escAttr(s) {
|
||||||
return String(s == null ? '' : s).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
return String(s == null ? '' : s).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||||
}
|
}
|
||||||
|
|
||||||
function openContentPicker({ multiple = false, title = 'Select Image' } = {}) {
|
function openContentPicker({ multiple = false, title } = {}) {
|
||||||
return new Promise(async (resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:10000;padding:16px';
|
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:10000;padding:16px';
|
||||||
overlay.innerHTML = `
|
overlay.innerHTML = `
|
||||||
<div style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px;width:100%;max-width:640px;max-height:90vh;display:flex;flex-direction:column">
|
<div style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px;width:100%;max-width:640px;max-height:90vh;display:flex;flex-direction:column">
|
||||||
<h3 style="margin:0 0 12px;color:var(--text-primary)">${title}</h3>
|
<h3 style="margin:0 0 12px;color:var(--text-primary)">${title || t('widget.picker.default_title')}</h3>
|
||||||
<input type="text" id="cpSearch" class="input" placeholder="Search images..." style="margin-bottom:12px">
|
<input type="text" id="cpSearch" class="input" placeholder="${t('widget.picker.search')}" style="margin-bottom:12px">
|
||||||
<div id="cpList" style="flex:1;overflow-y:auto;min-height:200px"></div>
|
<div id="cpList" style="flex:1;overflow-y:auto;min-height:200px"></div>
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:12px;gap:8px;flex-wrap:wrap">
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:12px;gap:8px;flex-wrap:wrap">
|
||||||
<div style="font-size:12px;color:var(--text-muted)" id="cpSelCount"></div>
|
<div style="font-size:12px;color:var(--text-muted)" id="cpSelCount"></div>
|
||||||
<div style="display:flex;gap:8px;margin-left:auto">
|
<div style="display:flex;gap:8px;margin-left:auto">
|
||||||
<button class="btn btn-secondary" id="cpCancel">Cancel</button>
|
<button class="btn btn-secondary" id="cpCancel">${t('common.cancel')}</button>
|
||||||
${multiple ? '<button class="btn btn-primary" id="cpDone">Done</button>' : ''}
|
${multiple ? `<button class="btn btn-primary" id="cpDone">${t('common.done')}</button>` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
@ -43,7 +49,7 @@ function openContentPicker({ multiple = false, title = 'Select Image' } = {}) {
|
||||||
const resolveUrl = (item) => item.remote_url || `/api/content/${item.id}/file`;
|
const resolveUrl = (item) => item.remote_url || `/api/content/${item.id}/file`;
|
||||||
const updateCount = () => {
|
const updateCount = () => {
|
||||||
const el = overlay.querySelector('#cpSelCount');
|
const el = overlay.querySelector('#cpSelCount');
|
||||||
if (el && multiple) el.textContent = `${selected.size} selected`;
|
if (el && multiple) el.textContent = t('widget.picker.selected_count', { n: selected.size });
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderList() {
|
function renderList() {
|
||||||
|
|
@ -51,7 +57,7 @@ function openContentPicker({ multiple = false, title = 'Select Image' } = {}) {
|
||||||
const filtered = items.filter(i => (i.filename || '').toLowerCase().includes(q));
|
const filtered = items.filter(i => (i.filename || '').toLowerCase().includes(q));
|
||||||
const list = overlay.querySelector('#cpList');
|
const list = overlay.querySelector('#cpList');
|
||||||
if (!filtered.length) {
|
if (!filtered.length) {
|
||||||
list.innerHTML = `<div style="color:var(--text-muted);padding:32px;text-align:center;font-size:13px">${items.length ? 'No matches.' : 'No images in your content library. Upload images first from Content Library.'}</div>`;
|
list.innerHTML = `<div style="color:var(--text-muted);padding:32px;text-align:center;font-size:13px">${items.length ? t('widget.picker.no_matches') : t('widget.picker.no_images')}</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
list.innerHTML = `<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px">${
|
list.innerHTML = `<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px">${
|
||||||
|
|
@ -105,8 +111,8 @@ function showPreviewModal(html) {
|
||||||
overlay.innerHTML = `
|
overlay.innerHTML = `
|
||||||
<div style="width:100%;max-width:1400px;height:90vh;background:var(--bg-card);border-radius:8px;display:flex;flex-direction:column;overflow:hidden;border:1px solid var(--border)">
|
<div style="width:100%;max-width:1400px;height:90vh;background:var(--bg-card);border-radius:8px;display:flex;flex-direction:column;overflow:hidden;border:1px solid var(--border)">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 16px;border-bottom:1px solid var(--border)">
|
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 16px;border-bottom:1px solid var(--border)">
|
||||||
<strong style="color:var(--text-primary)">Preview</strong>
|
<strong style="color:var(--text-primary)">${t('widget.preview_title')}</strong>
|
||||||
<button class="btn btn-secondary btn-sm" id="pvClose">Close</button>
|
<button class="btn btn-secondary btn-sm" id="pvClose">${t('widget.close')}</button>
|
||||||
</div>
|
</div>
|
||||||
<iframe id="pvIframe" style="flex:1;width:100%;border:0;background:#000"></iframe>
|
<iframe id="pvIframe" style="flex:1;width:100%;border:0;background:#000"></iframe>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
@ -128,19 +134,19 @@ function showPreviewModal(html) {
|
||||||
export async function render(container) {
|
export async function render(container) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div><h1>Widgets <span class="help-tip" data-tip="Dynamic content elements: live clocks, weather, RSS tickers, text, webpages, and social feeds. Create a widget then assign it to a device playlist.">?</span></h1><div class="subtitle">Add dynamic content to your layouts</div></div>
|
<div><h1>${t('widget.title')} <span class="help-tip" data-tip="${t('widget.help_tip')}">?</span></h1><div class="subtitle">${t('widget.subtitle')}</div></div>
|
||||||
<button class="btn btn-primary" id="newWidgetBtn">
|
<button class="btn btn-primary" id="newWidgetBtn">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||||
New Widget
|
${t('widget.new_widget')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="widgetTypeGrid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px;margin-bottom:24px;display:none">
|
<div id="widgetTypeGrid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px;margin-bottom:24px;display:none">
|
||||||
${WIDGET_TYPES.map(t => `
|
${WIDGET_TYPES.map(id => `
|
||||||
<div class="content-item" style="cursor:pointer" data-create-type="${t.id}">
|
<div class="content-item" style="cursor:pointer" data-create-type="${id}">
|
||||||
<div style="padding:20px;text-align:center">
|
<div style="padding:20px;text-align:center">
|
||||||
<div style="font-size:36px;margin-bottom:8px">${t.icon}</div>
|
<div style="font-size:36px;margin-bottom:8px">${WIDGET_ICONS[id]}</div>
|
||||||
<div style="font-weight:600;font-size:14px">${t.name}</div>
|
<div style="font-weight:600;font-size:14px">${widgetTypeName(id)}</div>
|
||||||
<div style="font-size:11px;color:var(--text-muted);margin-top:4px">${t.desc}</div>
|
<div style="font-size:11px;color:var(--text-muted);margin-top:4px">${widgetTypeDesc(id)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
|
|
@ -150,16 +156,16 @@ export async function render(container) {
|
||||||
<!-- Widget Config Modal -->
|
<!-- Widget Config Modal -->
|
||||||
<div class="modal-overlay" id="widgetModal" style="display:none">
|
<div class="modal-overlay" id="widgetModal" style="display:none">
|
||||||
<div class="modal" style="width:560px">
|
<div class="modal" style="width:560px">
|
||||||
<div class="modal-header"><h3 id="widgetModalTitle">Configure Widget</h3>
|
<div class="modal-header"><h3 id="widgetModalTitle">${t('widget.configure')}</h3>
|
||||||
<button class="btn-icon" onclick="document.getElementById('widgetModal').style.display='none'">
|
<button class="btn-icon" onclick="document.getElementById('widgetModal').style.display='none'">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" id="widgetConfigForm"></div>
|
<div class="modal-body" id="widgetConfigForm"></div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-secondary" onclick="document.getElementById('widgetModal').style.display='none'">Cancel</button>
|
<button class="btn btn-secondary" onclick="document.getElementById('widgetModal').style.display='none'">${t('common.cancel')}</button>
|
||||||
<button class="btn btn-secondary" id="previewWidgetBtn">Preview</button>
|
<button class="btn btn-secondary" id="previewWidgetBtn">${t('widget.preview')}</button>
|
||||||
<button class="btn btn-primary" id="saveWidgetBtn">Save</button>
|
<button class="btn btn-primary" id="saveWidgetBtn">${t('common.save')}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -184,76 +190,78 @@ export async function render(container) {
|
||||||
});
|
});
|
||||||
|
|
||||||
function showConfigForm(type, config) {
|
function showConfigForm(type, config) {
|
||||||
const typeName = WIDGET_TYPES.find(t => t.id === type)?.name || type;
|
const typeName = widgetTypeName(type);
|
||||||
document.getElementById('widgetModalTitle').textContent = editingWidget ? `Edit ${typeName}` : `New ${typeName}`;
|
document.getElementById('widgetModalTitle').textContent = editingWidget
|
||||||
|
? t('widget.edit_x', { type: typeName })
|
||||||
|
: t('widget.new_x', { type: typeName });
|
||||||
|
|
||||||
let html = '<div class="form-group"><label>Widget Name</label><input type="text" id="wName" class="input" value="' + (config._name || typeName) + '"></div>';
|
let html = `<div class="form-group"><label>${t('widget.field.name')}</label><input type="text" id="wName" class="input" value="${escAttr(config._name || typeName)}"></div>`;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'clock':
|
case 'clock':
|
||||||
html += `
|
html += `
|
||||||
<div class="form-group"><label>Format</label><select id="wFormat" class="input" style="background:var(--bg-input)"><option value="12h" ${config.format === '12h' ? 'selected' : ''}>12 Hour</option><option value="24h" ${config.format === '24h' ? 'selected' : ''}>24 Hour</option></select></div>
|
<div class="form-group"><label>${t('widget.field.format')}</label><select id="wFormat" class="input" style="background:var(--bg-input)"><option value="12h" ${config.format === '12h' ? 'selected' : ''}>${t('widget.field.format_12h')}</option><option value="24h" ${config.format === '24h' ? 'selected' : ''}>${t('widget.field.format_24h')}</option></select></div>
|
||||||
<div class="form-group"><label>Timezone</label><input type="text" id="wTimezone" class="input" value="${config.timezone || 'America/Chicago'}" placeholder="America/New_York"></div>
|
<div class="form-group"><label>${t('widget.field.timezone')}</label><input type="text" id="wTimezone" class="input" value="${config.timezone || 'America/Chicago'}" placeholder="America/New_York"></div>
|
||||||
<div class="form-group"><label>Font Size (px)</label><input type="number" id="wFontSize" class="input" value="${config.font_size || 64}"></div>
|
<div class="form-group"><label>${t('widget.field.font_size_px')}</label><input type="number" id="wFontSize" class="input" value="${config.font_size || 64}"></div>
|
||||||
<div class="form-group"><label>Color</label><input type="color" id="wColor" value="${config.color || '#FFFFFF'}" style="width:60px;height:32px;border:none"></div>
|
<div class="form-group"><label>${t('widget.field.color')}</label><input type="color" id="wColor" value="${config.color || '#FFFFFF'}" style="width:60px;height:32px;border:none"></div>
|
||||||
<div class="form-group"><label>Background</label><input type="color" id="wBg" value="${config.background || '#000000'}" style="width:60px;height:32px;border:none"></div>`;
|
<div class="form-group"><label>${t('widget.field.background')}</label><input type="color" id="wBg" value="${config.background || '#000000'}" style="width:60px;height:32px;border:none"></div>`;
|
||||||
break;
|
break;
|
||||||
case 'weather':
|
case 'weather':
|
||||||
html += `
|
html += `
|
||||||
<div class="form-group"><label>Location</label><input type="text" id="wLocation" class="input" value="${config.location || ''}" placeholder="City, State"></div>
|
<div class="form-group"><label>${t('widget.field.location')}</label><input type="text" id="wLocation" class="input" value="${config.location || ''}" placeholder="${t('widget.field.location_placeholder')}"></div>
|
||||||
<div class="form-group"><label>Units</label><select id="wUnits" class="input" style="background:var(--bg-input)"><option value="imperial" ${config.units !== 'metric' ? 'selected' : ''}>Imperial (°F)</option><option value="metric" ${config.units === 'metric' ? 'selected' : ''}>Metric (°C)</option></select></div>
|
<div class="form-group"><label>${t('widget.field.units')}</label><select id="wUnits" class="input" style="background:var(--bg-input)"><option value="imperial" ${config.units !== 'metric' ? 'selected' : ''}>${t('widget.field.units_imperial')}</option><option value="metric" ${config.units === 'metric' ? 'selected' : ''}>${t('widget.field.units_metric')}</option></select></div>
|
||||||
<div class="form-group"><label>Font Size</label><input type="number" id="wFontSize" class="input" value="${config.font_size || 48}"></div>
|
<div class="form-group"><label>${t('widget.field.font_size')}</label><input type="number" id="wFontSize" class="input" value="${config.font_size || 48}"></div>
|
||||||
<div class="form-group"><label>Color</label><input type="color" id="wColor" value="${config.color || '#FFFFFF'}" style="width:60px;height:32px;border:none"></div>`;
|
<div class="form-group"><label>${t('widget.field.color')}</label><input type="color" id="wColor" value="${config.color || '#FFFFFF'}" style="width:60px;height:32px;border:none"></div>`;
|
||||||
break;
|
break;
|
||||||
case 'rss':
|
case 'rss':
|
||||||
html += `
|
html += `
|
||||||
<div class="form-group"><label>Feed URL</label><input type="text" id="wFeedUrl" class="input" value="${config.feed_url || ''}" placeholder="https://example.com/feed.xml"></div>
|
<div class="form-group"><label>${t('widget.field.feed_url')}</label><input type="text" id="wFeedUrl" class="input" value="${config.feed_url || ''}" placeholder="https://example.com/feed.xml"></div>
|
||||||
<div class="form-group"><label>Scroll Speed (seconds)</label><input type="number" id="wScrollSpeed" class="input" value="${config.scroll_speed || 30}"></div>
|
<div class="form-group"><label>${t('widget.field.scroll_speed_seconds')}</label><input type="number" id="wScrollSpeed" class="input" value="${config.scroll_speed || 30}"></div>
|
||||||
<div class="form-group"><label>Max Items</label><input type="number" id="wMaxItems" class="input" value="${config.max_items || 10}"></div>
|
<div class="form-group"><label>${t('widget.field.max_items')}</label><input type="number" id="wMaxItems" class="input" value="${config.max_items || 10}"></div>
|
||||||
<div class="form-group"><label>Font Size</label><input type="number" id="wFontSize" class="input" value="${config.font_size || 24}"></div>
|
<div class="form-group"><label>${t('widget.field.font_size')}</label><input type="number" id="wFontSize" class="input" value="${config.font_size || 24}"></div>
|
||||||
<div class="form-group"><label>Color</label><input type="color" id="wColor" value="${config.color || '#FFFFFF'}" style="width:60px;height:32px;border:none"></div>
|
<div class="form-group"><label>${t('widget.field.color')}</label><input type="color" id="wColor" value="${config.color || '#FFFFFF'}" style="width:60px;height:32px;border:none"></div>
|
||||||
<div class="form-group"><label>Background</label><input type="color" id="wBg" value="${config.background || '#000000'}" style="width:60px;height:32px;border:none"></div>`;
|
<div class="form-group"><label>${t('widget.field.background')}</label><input type="color" id="wBg" value="${config.background || '#000000'}" style="width:60px;height:32px;border:none"></div>`;
|
||||||
break;
|
break;
|
||||||
case 'text':
|
case 'text':
|
||||||
html += `
|
html += `
|
||||||
<div class="form-group"><label>HTML Content</label><textarea id="wHtml" class="input" rows="6" style="font-family:monospace;font-size:12px">${config.html || '<h1 style="color:white;text-align:center;margin-top:40px">Hello World</h1>'}</textarea></div>
|
<div class="form-group"><label>${t('widget.field.html_content')}</label><textarea id="wHtml" class="input" rows="6" style="font-family:monospace;font-size:12px">${config.html || '<h1 style="color:white;text-align:center;margin-top:40px">Hello World</h1>'}</textarea></div>
|
||||||
<div class="form-group"><label>CSS (optional)</label><textarea id="wCss" class="input" rows="3" style="font-family:monospace;font-size:12px">${config.css || ''}</textarea></div>
|
<div class="form-group"><label>${t('widget.field.css_optional')}</label><textarea id="wCss" class="input" rows="3" style="font-family:monospace;font-size:12px">${config.css || ''}</textarea></div>
|
||||||
<div class="form-group"><label>Background</label><input type="color" id="wBg" value="${config.background || '#000000'}" style="width:60px;height:32px;border:none"></div>`;
|
<div class="form-group"><label>${t('widget.field.background')}</label><input type="color" id="wBg" value="${config.background || '#000000'}" style="width:60px;height:32px;border:none"></div>`;
|
||||||
break;
|
break;
|
||||||
case 'webpage':
|
case 'webpage':
|
||||||
html += `
|
html += `
|
||||||
<div class="form-group"><label>URL</label><input type="text" id="wUrl" class="input" value="${config.url || ''}" placeholder="https://example.com"></div>
|
<div class="form-group"><label>${t('widget.field.url')}</label><input type="text" id="wUrl" class="input" value="${config.url || ''}" placeholder="https://example.com"></div>
|
||||||
<div class="form-group"><label>Zoom (%)</label><input type="number" id="wZoom" class="input" value="${config.zoom || 100}"></div>
|
<div class="form-group"><label>${t('widget.field.zoom_pct')}</label><input type="number" id="wZoom" class="input" value="${config.zoom || 100}"></div>
|
||||||
<div class="form-group"><label>Refresh Interval (seconds, 0 = never)</label><input type="number" id="wRefresh" class="input" value="${config.refresh_interval || 0}"></div>`;
|
<div class="form-group"><label>${t('widget.field.refresh_interval')}</label><input type="number" id="wRefresh" class="input" value="${config.refresh_interval || 0}"></div>`;
|
||||||
break;
|
break;
|
||||||
case 'social':
|
case 'social':
|
||||||
html += `
|
html += `
|
||||||
<div class="form-group"><label>Platform</label><select id="wPlatform" class="input" style="background:var(--bg-input)"><option value="twitter">Twitter/X</option><option value="instagram">Instagram</option></select></div>
|
<div class="form-group"><label>${t('widget.field.platform')}</label><select id="wPlatform" class="input" style="background:var(--bg-input)"><option value="twitter">${t('widget.field.platform_twitter')}</option><option value="instagram">${t('widget.field.platform_instagram')}</option></select></div>
|
||||||
<div class="form-group"><label>Query</label><input type="text" id="wQuery" class="input" value="${config.query || ''}" placeholder="@handle or #hashtag"></div>`;
|
<div class="form-group"><label>${t('widget.field.query')}</label><input type="text" id="wQuery" class="input" value="${config.query || ''}" placeholder="${t('widget.field.query_placeholder')}"></div>`;
|
||||||
break;
|
break;
|
||||||
case 'directory-board':
|
case 'directory-board':
|
||||||
html += `
|
html += `
|
||||||
<div class="form-group"><label>Title</label><input type="text" id="wTitle" class="input" value="${escAttr(config.title)}" placeholder="Lincoln Warehouse"></div>
|
<div class="form-group"><label>${t('widget.dir.title_label')}</label><input type="text" id="wTitle" class="input" value="${escAttr(config.title)}" placeholder="${t('widget.dir.title_placeholder')}"></div>
|
||||||
<div class="form-group"><label>Logo (optional)</label><div id="wLogoBox"></div></div>
|
<div class="form-group"><label>${t('widget.dir.logo_label')}</label><div id="wLogoBox"></div></div>
|
||||||
<div class="form-group"><label>Footer Text</label><input type="text" id="wFooter" class="input" value="${escAttr(config.footer_text)}" placeholder="For Leasing Inquiries: Contact..."></div>
|
<div class="form-group"><label>${t('widget.dir.footer_text_label')}</label><input type="text" id="wFooter" class="input" value="${escAttr(config.footer_text)}" placeholder="${t('widget.dir.footer_placeholder')}"></div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Background Images (optional)</label>
|
<label>${t('widget.dir.bg_images_label')}</label>
|
||||||
<div style="font-size:11px;color:var(--text-muted);margin-bottom:8px">Images crossfade every 15 seconds at 30% opacity. Add multiple for rotation.</div>
|
<div style="font-size:11px;color:var(--text-muted);margin-bottom:8px">${t('widget.dir.bg_images_hint')}</div>
|
||||||
<div id="wBgList"></div>
|
<div id="wBgList"></div>
|
||||||
<button type="button" class="btn btn-secondary btn-sm" id="wBgAdd" style="margin-top:8px">+ Add Background Image</button>
|
<button type="button" class="btn btn-secondary btn-sm" id="wBgAdd" style="margin-top:8px">${t('widget.dir.add_bg_image')}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="display:flex;gap:12px;flex-wrap:wrap">
|
<div class="form-group" style="display:flex;gap:12px;flex-wrap:wrap">
|
||||||
<div style="flex:1;min-width:140px"><label>Theme</label><select id="wTheme" class="input" style="background:var(--bg-input)">
|
<div style="flex:1;min-width:140px"><label>${t('widget.dir.theme')}</label><select id="wTheme" class="input" style="background:var(--bg-input)">
|
||||||
<option value="dark" ${!config.theme || config.theme === 'dark' ? 'selected' : ''}>Dark</option>
|
<option value="dark" ${!config.theme || config.theme === 'dark' ? 'selected' : ''}>${t('widget.dir.theme_dark')}</option>
|
||||||
<option value="light" ${config.theme === 'light' ? 'selected' : ''}>Light</option>
|
<option value="light" ${config.theme === 'light' ? 'selected' : ''}>${t('widget.dir.theme_light')}</option>
|
||||||
</select></div>
|
</select></div>
|
||||||
<div style="flex:1;min-width:140px"><label>Scroll Speed</label><select id="wSpeed" class="input" style="background:var(--bg-input)">
|
<div style="flex:1;min-width:140px"><label>${t('widget.dir.scroll_speed')}</label><select id="wSpeed" class="input" style="background:var(--bg-input)">
|
||||||
<option value="slow" ${config.scroll_speed === 'slow' ? 'selected' : ''}>Slow</option>
|
<option value="slow" ${config.scroll_speed === 'slow' ? 'selected' : ''}>${t('widget.dir.speed_slow')}</option>
|
||||||
<option value="medium" ${!config.scroll_speed || config.scroll_speed === 'medium' ? 'selected' : ''}>Medium</option>
|
<option value="medium" ${!config.scroll_speed || config.scroll_speed === 'medium' ? 'selected' : ''}>${t('widget.dir.speed_medium')}</option>
|
||||||
<option value="fast" ${config.scroll_speed === 'fast' ? 'selected' : ''}>Fast</option>
|
<option value="fast" ${config.scroll_speed === 'fast' ? 'selected' : ''}>${t('widget.dir.speed_fast')}</option>
|
||||||
</select></div>
|
</select></div>
|
||||||
<div style="flex:1;min-width:140px"><label>Columns</label><select id="wCols" class="input" style="background:var(--bg-input)">
|
<div style="flex:1;min-width:140px"><label>${t('widget.dir.columns')}</label><select id="wCols" class="input" style="background:var(--bg-input)">
|
||||||
<option value="auto" ${!config.columns || config.columns === 'auto' ? 'selected' : ''}>Auto</option>
|
<option value="auto" ${!config.columns || config.columns === 'auto' ? 'selected' : ''}>${t('widget.dir.columns_auto')}</option>
|
||||||
<option value="1" ${config.columns === '1' ? 'selected' : ''}>1</option>
|
<option value="1" ${config.columns === '1' ? 'selected' : ''}>1</option>
|
||||||
<option value="2" ${config.columns === '2' ? 'selected' : ''}>2</option>
|
<option value="2" ${config.columns === '2' ? 'selected' : ''}>2</option>
|
||||||
<option value="3" ${config.columns === '3' ? 'selected' : ''}>3</option>
|
<option value="3" ${config.columns === '3' ? 'selected' : ''}>3</option>
|
||||||
|
|
@ -261,9 +269,9 @@ export async function render(container) {
|
||||||
</select></div>
|
</select></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Categories</label>
|
<label>${t('widget.dir.categories')}</label>
|
||||||
<div id="dbCategories"></div>
|
<div id="dbCategories"></div>
|
||||||
<button type="button" class="btn btn-secondary btn-sm" id="dbAddCategory" style="margin-top:10px">+ Add Category</button>
|
<button type="button" class="btn btn-secondary btn-sm" id="dbAddCategory" style="margin-top:10px">${t('widget.dir.add_category')}</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -301,40 +309,43 @@ export async function render(container) {
|
||||||
const cont = document.getElementById('dbCategories');
|
const cont = document.getElementById('dbCategories');
|
||||||
if (!cont) return;
|
if (!cont) return;
|
||||||
if (!dirState.categories.length) {
|
if (!dirState.categories.length) {
|
||||||
cont.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-muted);border:1px dashed var(--border);border-radius:6px;font-size:13px">Add your first floor or department to get started</div>';
|
cont.innerHTML = `<div style="padding:20px;text-align:center;color:var(--text-muted);border:1px dashed var(--border);border-radius:6px;font-size:13px">${t('widget.dir.empty_categories')}</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cont.innerHTML = dirState.categories.map((cat, i) => {
|
cont.innerHTML = dirState.categories.map((cat, i) => {
|
||||||
const entryRows = (cat.entries || []).map((e, j) => `
|
const entryRows = (cat.entries || []).map((e, j) => `
|
||||||
<div class="db-entry" style="display:flex;gap:6px;align-items:flex-start;margin-bottom:8px;flex-wrap:wrap">
|
<div class="db-entry" style="display:flex;gap:6px;align-items:flex-start;margin-bottom:8px;flex-wrap:wrap">
|
||||||
<input type="text" class="input" data-entry-id="${i}-${j}" value="${escAttr(e.identifier)}" placeholder="101" style="width:90px">
|
<input type="text" class="input" data-entry-id="${i}-${j}" value="${escAttr(e.identifier)}" placeholder="${t('widget.dir.entry_id_placeholder')}" style="width:90px">
|
||||||
<div style="display:flex;flex-direction:column;gap:4px;flex:1;min-width:140px">
|
<div style="display:flex;flex-direction:column;gap:4px;flex:1;min-width:140px">
|
||||||
<input type="text" class="input" data-entry-name="${i}-${j}" value="${escAttr(e.name)}" placeholder="Tenant name">
|
<input type="text" class="input" data-entry-name="${i}-${j}" value="${escAttr(e.name)}" placeholder="${t('widget.dir.entry_name_placeholder')}">
|
||||||
<input type="text" class="input" data-entry-subtitle="${i}-${j}" value="${escAttr(e.subtitle)}" placeholder="Details (optional)" style="font-size:12px">
|
<input type="text" class="input" data-entry-subtitle="${i}-${j}" value="${escAttr(e.subtitle)}" placeholder="${t('widget.dir.entry_subtitle_placeholder')}" style="font-size:12px">
|
||||||
</div>
|
</div>
|
||||||
<label style="display:flex;align-items:center;gap:4px;font-size:12px;white-space:nowrap;color:var(--text-muted);padding-top:8px">
|
<label style="display:flex;align-items:center;gap:4px;font-size:12px;white-space:nowrap;color:var(--text-muted);padding-top:8px">
|
||||||
<input type="checkbox" data-entry-avail="${i}-${j}" ${e.available ? 'checked' : ''}> Available
|
<input type="checkbox" data-entry-avail="${i}-${j}" ${e.available ? 'checked' : ''}> ${t('widget.dir.available')}
|
||||||
</label>
|
</label>
|
||||||
<button type="button" class="btn-icon" data-entry-up="${i}-${j}" ${j === 0 ? 'disabled' : ''} title="Move up" style="padding:4px 6px">↑</button>
|
<button type="button" class="btn-icon" data-entry-up="${i}-${j}" ${j === 0 ? 'disabled' : ''} title="${t('widget.dir.move_up')}" style="padding:4px 6px">↑</button>
|
||||||
<button type="button" class="btn-icon" data-entry-down="${i}-${j}" ${j === cat.entries.length - 1 ? 'disabled' : ''} title="Move down" style="padding:4px 6px">↓</button>
|
<button type="button" class="btn-icon" data-entry-down="${i}-${j}" ${j === cat.entries.length - 1 ? 'disabled' : ''} title="${t('widget.dir.move_down')}" style="padding:4px 6px">↓</button>
|
||||||
<button type="button" class="btn-icon" data-entry-delete="${i}-${j}" title="Delete entry" style="padding:4px 6px;color:#ff6b6b">×</button>
|
<button type="button" class="btn-icon" data-entry-delete="${i}-${j}" title="${t('widget.dir.delete_entry')}" style="padding:4px 6px;color:#ff6b6b">×</button>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
|
|
||||||
|
const entryCount = cat.entries.length;
|
||||||
|
const entriesLabel = entryCount === 1 ? t('widget.dir.entry') : t('widget.dir.entries');
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="db-category" style="border:1px solid var(--border);border-radius:6px;margin-bottom:8px;padding:8px;background:var(--bg-input)">
|
<div class="db-category" style="border:1px solid var(--border);border-radius:6px;margin-bottom:8px;padding:8px;background:var(--bg-input)">
|
||||||
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
|
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
|
||||||
<button type="button" class="btn-icon" data-cat-toggle="${i}" title="${cat._expanded ? 'Collapse' : 'Expand'}" style="padding:4px 8px">${cat._expanded ? '▼' : '▶'}</button>
|
<button type="button" class="btn-icon" data-cat-toggle="${i}" title="${cat._expanded ? t('widget.dir.collapse') : t('widget.dir.expand')}" style="padding:4px 8px">${cat._expanded ? '▼' : '▶'}</button>
|
||||||
<input type="text" class="input" data-cat-name="${i}" value="${escAttr(cat.name)}" placeholder="e.g. First Floor" style="flex:1;min-width:140px;font-weight:600">
|
<input type="text" class="input" data-cat-name="${i}" value="${escAttr(cat.name)}" placeholder="${t('widget.dir.category_name_placeholder')}" style="flex:1;min-width:140px;font-weight:600">
|
||||||
<span style="font-size:11px;color:var(--text-muted);white-space:nowrap">${cat.entries.length} ${cat.entries.length === 1 ? 'entry' : 'entries'}</span>
|
<span style="font-size:11px;color:var(--text-muted);white-space:nowrap">${entryCount} ${entriesLabel}</span>
|
||||||
<button type="button" class="btn-icon" data-cat-up="${i}" ${i === 0 ? 'disabled' : ''} title="Move up" style="padding:4px 6px">↑</button>
|
<button type="button" class="btn-icon" data-cat-up="${i}" ${i === 0 ? 'disabled' : ''} title="${t('widget.dir.move_up')}" style="padding:4px 6px">↑</button>
|
||||||
<button type="button" class="btn-icon" data-cat-down="${i}" ${i === dirState.categories.length - 1 ? 'disabled' : ''} title="Move down" style="padding:4px 6px">↓</button>
|
<button type="button" class="btn-icon" data-cat-down="${i}" ${i === dirState.categories.length - 1 ? 'disabled' : ''} title="${t('widget.dir.move_down')}" style="padding:4px 6px">↓</button>
|
||||||
<button type="button" class="btn-icon" data-cat-delete="${i}" title="Delete category" style="padding:4px 6px;color:#ff6b6b">×</button>
|
<button type="button" class="btn-icon" data-cat-delete="${i}" title="${t('widget.dir.delete_category')}" style="padding:4px 6px;color:#ff6b6b">×</button>
|
||||||
</div>
|
</div>
|
||||||
${cat._expanded ? `
|
${cat._expanded ? `
|
||||||
<div style="padding:10px 0 4px 4px;margin-top:8px;border-top:1px solid var(--border)">
|
<div style="padding:10px 0 4px 4px;margin-top:8px;border-top:1px solid var(--border)">
|
||||||
${entryRows || '<div style="font-size:12px;color:var(--text-muted);padding:4px 0 8px">No entries yet</div>'}
|
${entryRows || `<div style="font-size:12px;color:var(--text-muted);padding:4px 0 8px">${t('widget.dir.no_entries')}</div>`}
|
||||||
<button type="button" class="btn btn-secondary btn-sm" data-add-entry="${i}" style="margin-top:4px">+ Add Entry</button>
|
<button type="button" class="btn btn-secondary btn-sm" data-add-entry="${i}" style="margin-top:4px">${t('widget.dir.add_entry')}</button>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -370,8 +381,8 @@ export async function render(container) {
|
||||||
});
|
});
|
||||||
cont.querySelectorAll('[data-cat-delete]').forEach(b => b.onclick = () => {
|
cont.querySelectorAll('[data-cat-delete]').forEach(b => b.onclick = () => {
|
||||||
const i = +b.dataset.catDelete;
|
const i = +b.dataset.catDelete;
|
||||||
const label = dirState.categories[i].name || '(unnamed)';
|
const label = dirState.categories[i].name || t('widget.dir.unnamed');
|
||||||
if (!confirm(`Delete category "${label}" and all its entries?`)) return;
|
if (!confirm(t('widget.dir.confirm_delete_category', { name: label }))) return;
|
||||||
dirState.categories.splice(i, 1);
|
dirState.categories.splice(i, 1);
|
||||||
renderDirCategories();
|
renderDirCategories();
|
||||||
});
|
});
|
||||||
|
|
@ -435,19 +446,19 @@ export async function render(container) {
|
||||||
<div style="display:flex;align-items:center;gap:10px;padding:10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input)">
|
<div style="display:flex;align-items:center;gap:10px;padding:10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input)">
|
||||||
<img src="${escAttr(dirState.logo_url)}" style="max-height:50px;max-width:120px;object-fit:contain;background:#0003;border-radius:3px" onerror="this.style.opacity='0.3'">
|
<img src="${escAttr(dirState.logo_url)}" style="max-height:50px;max-width:120px;object-fit:contain;background:#0003;border-radius:3px" onerror="this.style.opacity='0.3'">
|
||||||
<div style="flex:1;min-width:0;font-size:11px;color:var(--text-muted);word-break:break-all;overflow:hidden;text-overflow:ellipsis">${escAttr(dirState.logo_url)}</div>
|
<div style="flex:1;min-width:0;font-size:11px;color:var(--text-muted);word-break:break-all;overflow:hidden;text-overflow:ellipsis">${escAttr(dirState.logo_url)}</div>
|
||||||
<button type="button" class="btn btn-secondary btn-sm" id="wLogoChange">Change</button>
|
<button type="button" class="btn btn-secondary btn-sm" id="wLogoChange">${t('widget.dir.change')}</button>
|
||||||
<button type="button" class="btn-icon" id="wLogoClear" title="Remove" style="color:#ff6b6b;padding:4px 8px">×</button>
|
<button type="button" class="btn-icon" id="wLogoClear" title="${t('widget.dir.remove_logo')}" style="color:#ff6b6b;padding:4px 8px">×</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
document.getElementById('wLogoChange').onclick = pickLogo;
|
document.getElementById('wLogoChange').onclick = pickLogo;
|
||||||
document.getElementById('wLogoClear').onclick = () => { dirState.logo_url = ''; renderLogoPicker(); };
|
document.getElementById('wLogoClear').onclick = () => { dirState.logo_url = ''; renderLogoPicker(); };
|
||||||
} else {
|
} else {
|
||||||
box.innerHTML = `<button type="button" class="btn btn-secondary btn-sm" id="wLogoChoose">Choose Logo</button>`;
|
box.innerHTML = `<button type="button" class="btn btn-secondary btn-sm" id="wLogoChoose">${t('widget.dir.choose_logo')}</button>`;
|
||||||
document.getElementById('wLogoChoose').onclick = pickLogo;
|
document.getElementById('wLogoChoose').onclick = pickLogo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pickLogo() {
|
async function pickLogo() {
|
||||||
const url = await openContentPicker({ multiple: false, title: 'Select Logo' });
|
const url = await openContentPicker({ multiple: false, title: t('widget.picker.select_logo') });
|
||||||
if (url) { dirState.logo_url = url; renderLogoPicker(); }
|
if (url) { dirState.logo_url = url; renderLogoPicker(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -455,14 +466,14 @@ export async function render(container) {
|
||||||
const list = document.getElementById('wBgList');
|
const list = document.getElementById('wBgList');
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
if (!dirState.background_images.length) {
|
if (!dirState.background_images.length) {
|
||||||
list.innerHTML = '<div style="font-size:12px;color:var(--text-muted);font-style:italic;padding:4px 0">No background images selected</div>';
|
list.innerHTML = `<div style="font-size:12px;color:var(--text-muted);font-style:italic;padding:4px 0">${t('widget.dir.no_bg_images')}</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
list.innerHTML = `<div style="display:flex;gap:8px;flex-wrap:wrap">${
|
list.innerHTML = `<div style="display:flex;gap:8px;flex-wrap:wrap">${
|
||||||
dirState.background_images.map((u, i) => `
|
dirState.background_images.map((u, i) => `
|
||||||
<div style="position:relative;width:90px;height:68px;border-radius:4px;overflow:hidden;background:var(--bg-input);border:1px solid var(--border)">
|
<div style="position:relative;width:90px;height:68px;border-radius:4px;overflow:hidden;background:var(--bg-input);border:1px solid var(--border)">
|
||||||
<img src="${escAttr(u)}" style="width:100%;height:100%;object-fit:cover" onerror="this.style.display='none'">
|
<img src="${escAttr(u)}" style="width:100%;height:100%;object-fit:cover" onerror="this.style.display='none'">
|
||||||
<button type="button" data-bg-remove="${i}" title="Remove" style="position:absolute;top:3px;right:3px;width:22px;height:22px;border-radius:50%;border:0;background:rgba(0,0,0,0.75);color:#fff;cursor:pointer;font-size:14px;line-height:1;padding:0">×</button>
|
<button type="button" data-bg-remove="${i}" title="${t('widget.dir.remove_bg')}" style="position:absolute;top:3px;right:3px;width:22px;height:22px;border-radius:50%;border:0;background:rgba(0,0,0,0.75);color:#fff;cursor:pointer;font-size:14px;line-height:1;padding:0">×</button>
|
||||||
</div>
|
</div>
|
||||||
`).join('')
|
`).join('')
|
||||||
}</div>`;
|
}</div>`;
|
||||||
|
|
@ -473,7 +484,7 @@ export async function render(container) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pickBgImages() {
|
async function pickBgImages() {
|
||||||
const urls = await openContentPicker({ multiple: true, title: 'Select Background Images' });
|
const urls = await openContentPicker({ multiple: true, title: t('widget.picker.select_bg_images') });
|
||||||
if (urls && urls.length) {
|
if (urls && urls.length) {
|
||||||
dirState.background_images.push(...urls);
|
dirState.background_images.push(...urls);
|
||||||
renderBgList();
|
renderBgList();
|
||||||
|
|
@ -523,7 +534,7 @@ export async function render(container) {
|
||||||
await API('/widgets', { method: 'POST', body: JSON.stringify({ widget_type: type, name, config }) });
|
await API('/widgets', { method: 'POST', body: JSON.stringify({ widget_type: type, name, config }) });
|
||||||
}
|
}
|
||||||
document.getElementById('widgetModal').style.display = 'none';
|
document.getElementById('widgetModal').style.display = 'none';
|
||||||
showToast('Widget saved', 'success');
|
showToast(t('widget.toast.saved'), 'success');
|
||||||
loadWidgets();
|
loadWidgets();
|
||||||
} catch (err) { showToast(err.message, 'error'); }
|
} catch (err) { showToast(err.message, 'error'); }
|
||||||
};
|
};
|
||||||
|
|
@ -538,7 +549,7 @@ export async function render(container) {
|
||||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}` },
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}` },
|
||||||
body: JSON.stringify({ widget_type: type, config }),
|
body: JSON.stringify({ widget_type: type, config }),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error('Preview failed');
|
if (!res.ok) throw new Error(t('widget.toast.preview_failed'));
|
||||||
const html = await res.text();
|
const html = await res.text();
|
||||||
showPreviewModal(html);
|
showPreviewModal(html);
|
||||||
} catch (err) { showToast(err.message, 'error'); }
|
} catch (err) { showToast(err.message, 'error'); }
|
||||||
|
|
@ -548,23 +559,24 @@ export async function render(container) {
|
||||||
const widgets = await API('/widgets');
|
const widgets = await API('/widgets');
|
||||||
const grid = document.getElementById('widgetGrid');
|
const grid = document.getElementById('widgetGrid');
|
||||||
if (!widgets.length) {
|
if (!widgets.length) {
|
||||||
grid.innerHTML = '<div class="empty-state" style="grid-column:1/-1"><h3>No widgets yet</h3><p>Create a widget to add dynamic content to your layouts.</p></div>';
|
grid.innerHTML = `<div class="empty-state" style="grid-column:1/-1"><h3>${t('widget.empty_title')}</h3><p>${t('widget.empty_desc')}</p></div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
grid.innerHTML = widgets.map(w => {
|
grid.innerHTML = widgets.map(w => {
|
||||||
const typeMeta = WIDGET_TYPES.find(t => t.id === w.widget_type) || {};
|
const icon = WIDGET_ICONS[w.widget_type] || '?';
|
||||||
|
const typeLabel = WIDGET_TYPES.includes(w.widget_type) ? widgetTypeName(w.widget_type) : w.widget_type;
|
||||||
return `
|
return `
|
||||||
<div class="content-item">
|
<div class="content-item">
|
||||||
<div class="content-item-preview" style="display:flex;align-items:center;justify-content:center;flex-direction:column;gap:4px">
|
<div class="content-item-preview" style="display:flex;align-items:center;justify-content:center;flex-direction:column;gap:4px">
|
||||||
<span style="font-size:36px">${typeMeta.icon || '?'}</span>
|
<span style="font-size:36px">${icon}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="content-item-body">
|
<div class="content-item-body">
|
||||||
<div class="content-item-name">${escAttr(w.name)}</div>
|
<div class="content-item-name">${escAttr(w.name)}</div>
|
||||||
<div class="content-item-size">${escAttr(typeMeta.name || w.widget_type)}</div>
|
<div class="content-item-size">${escAttr(typeLabel)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content-item-actions">
|
<div class="content-item-actions">
|
||||||
<button class="btn btn-secondary btn-sm" data-edit-widget="${escAttr(w.id)}">Edit</button>
|
<button class="btn btn-secondary btn-sm" data-edit-widget="${escAttr(w.id)}">${t('common.edit')}</button>
|
||||||
<button class="btn btn-danger btn-sm" data-delete-widget="${escAttr(w.id)}">Delete</button>
|
<button class="btn btn-danger btn-sm" data-delete-widget="${escAttr(w.id)}">${t('common.delete')}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -586,11 +598,11 @@ export async function render(container) {
|
||||||
const deleteBtn = e.target.closest('[data-delete-widget]');
|
const deleteBtn = e.target.closest('[data-delete-widget]');
|
||||||
if (deleteBtn) {
|
if (deleteBtn) {
|
||||||
const w = widgets.find(x => x.id === deleteBtn.dataset.deleteWidget);
|
const w = widgets.find(x => x.id === deleteBtn.dataset.deleteWidget);
|
||||||
const label = w ? w.name : 'this widget';
|
const label = w ? w.name : t('widget.this_widget');
|
||||||
if (!confirm(`Delete "${label}"? This cannot be undone.`)) return;
|
if (!confirm(t('widget.confirm_delete', { name: label }))) return;
|
||||||
try {
|
try {
|
||||||
await API(`/widgets/${deleteBtn.dataset.deleteWidget}`, { method: 'DELETE' });
|
await API(`/widgets/${deleteBtn.dataset.deleteWidget}`, { method: 'DELETE' });
|
||||||
showToast('Widget deleted', 'success');
|
showToast(t('widget.toast.deleted'), 'success');
|
||||||
loadWidgets();
|
loadWidgets();
|
||||||
} catch (err) { showToast(err.message, 'error'); }
|
} catch (err) { showToast(err.message, 'error'); }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue