mirror of
https://github.com/screentinker/screentinker.git
synced 2026-05-15 07:32:23 -06:00
i18n batch 6: wire teams + activity + help (~62 keys)
- teams.js: list, detail with members + shared devices, invite/role controls, all toasts - activity.js: page chrome, action verb/noun mapping translated through t() so the audit log reads naturally in each language - help.js: page chrome translated; guides and FAQ body content kept in English with a comment explaining why (machine-translated docs read worse than English source) - 1008 keys total, parity 100% across en/es/fr/de/pt All 16 dashboard views now use t(). index.html modal, player overlay, and Android resources still pending. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
7a17bb5079
commit
6d6f901ef4
|
|
@ -983,4 +983,69 @@ export default {
|
||||||
'billing.toast.checkout_failed': 'Bezahlung konnte nicht gestartet werden: {error}',
|
'billing.toast.checkout_failed': 'Bezahlung konnte nicht gestartet werden: {error}',
|
||||||
'billing.toast.portal_failed': 'Abrechnungsportal konnte nicht geöffnet werden: {error}',
|
'billing.toast.portal_failed': 'Abrechnungsportal konnte nicht geöffnet werden: {error}',
|
||||||
'billing.toast.payment_success': 'Bezahlung erfolgreich! Ihr Plan wurde aktualisiert.',
|
'billing.toast.payment_success': 'Bezahlung erfolgreich! Ihr Plan wurde aktualisiert.',
|
||||||
|
|
||||||
|
// Teams
|
||||||
|
'team.title': 'Teams',
|
||||||
|
'team.subtitle': 'Teams und gemeinsamen Zugriff verwalten',
|
||||||
|
'team.help_tip': 'Erstellen Sie Teams, um Geräte mit anderen Nutzern zu teilen. Eigentümer verwalten das Team, Editoren ändern Inhalte/Playlists, Betrachter überwachen nur.',
|
||||||
|
'team.new_team': 'Neues Team',
|
||||||
|
'team.prompt_name': 'Teamname:',
|
||||||
|
'team.empty_title': 'Noch keine Teams',
|
||||||
|
'team.empty_desc': 'Erstellen Sie ein Team, um Geräte mit anderen Nutzern zu teilen.',
|
||||||
|
'team.your_role': 'Ihre Rolle: {role}',
|
||||||
|
'team.member_count_one': '1 Mitglied',
|
||||||
|
'team.member_count_other': '{n} Mitglieder',
|
||||||
|
'team.not_found': 'Team nicht gefunden',
|
||||||
|
'team.back': 'Zurück zu Teams',
|
||||||
|
'team.delete_team': 'Team löschen',
|
||||||
|
'team.members_count': 'Mitglieder ({n})',
|
||||||
|
'team.invite': '+ Einladen',
|
||||||
|
'team.role_viewer': 'Betrachter',
|
||||||
|
'team.role_editor': 'Editor',
|
||||||
|
'team.role_owner': 'Eigentümer',
|
||||||
|
'team.remove': 'Entfernen',
|
||||||
|
'team.remove_from_team': 'Aus Team entfernen',
|
||||||
|
'team.no_members': 'Keine Mitglieder',
|
||||||
|
'team.shared_devices': 'Geteilte Geräte ({n})',
|
||||||
|
'team.add_device': '+ Gerät hinzufügen...',
|
||||||
|
'team.no_devices': 'Keine Geräte mit diesem Team geteilt',
|
||||||
|
'team.prompt_email': 'E-Mail-Adresse zum Einladen:',
|
||||||
|
'team.prompt_role': 'Rolle (viewer, editor oder owner):',
|
||||||
|
'team.toast.invalid_role': 'Ungültige Rolle',
|
||||||
|
'team.toast.invitation_sent': 'Einladung gesendet',
|
||||||
|
'team.toast.role_updated': 'Rolle aktualisiert',
|
||||||
|
'team.toast.member_removed': 'Mitglied entfernt',
|
||||||
|
'team.toast.device_added': 'Gerät zum Team hinzugefügt',
|
||||||
|
'team.toast.device_removed': 'Gerät aus Team entfernt',
|
||||||
|
'team.toast.deleted': 'Team gelöscht',
|
||||||
|
|
||||||
|
// Activity
|
||||||
|
'activity.title': 'Aktivitätsprotokoll',
|
||||||
|
'activity.subtitle': 'Audit-Trail aller Aktionen',
|
||||||
|
'activity.load_more': 'Mehr laden',
|
||||||
|
'activity.empty_title': 'Noch keine Aktivität',
|
||||||
|
'activity.empty_desc': 'Aktionen erscheinen hier, sobald Sie das System nutzen.',
|
||||||
|
'activity.system': 'System',
|
||||||
|
'activity.verb_created': 'hat erstellt',
|
||||||
|
'activity.verb_updated': 'hat aktualisiert',
|
||||||
|
'activity.verb_deleted': 'hat gelöscht',
|
||||||
|
'activity.action_paired_device': 'hat ein Gerät gekoppelt',
|
||||||
|
'activity.action_added_remote_content': 'hat Remote-Inhalt hinzugefügt',
|
||||||
|
'activity.noun_content': 'Inhalt',
|
||||||
|
'activity.noun_device': 'Gerät',
|
||||||
|
'activity.noun_playlist_assignment': 'Playlist-Zuweisung',
|
||||||
|
'activity.noun_assignment': 'Zuweisung',
|
||||||
|
'activity.noun_layout': 'Layout',
|
||||||
|
'activity.noun_widget': 'Widget',
|
||||||
|
'activity.noun_schedule': 'Zeitplan',
|
||||||
|
'activity.noun_video_wall': 'Videowand',
|
||||||
|
'activity.alert_device_offline': 'Alarm: Gerät offline',
|
||||||
|
|
||||||
|
// Help
|
||||||
|
'help.title': 'Hilfe-Center',
|
||||||
|
'help.subtitle': 'Schnellanleitungen und FAQ',
|
||||||
|
'help.faq': 'Häufig gestellte Fragen',
|
||||||
|
'help.shortcuts': 'Tastaturkürzel',
|
||||||
|
'help.shortcut_esc': 'Web-Player zurücksetzen (auf Player-Seite)',
|
||||||
|
'help.shortcut_f': 'Vollbild umschalten (Web-Player)',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1019,4 +1019,69 @@ export default {
|
||||||
'billing.toast.checkout_failed': 'Failed to start checkout: {error}',
|
'billing.toast.checkout_failed': 'Failed to start checkout: {error}',
|
||||||
'billing.toast.portal_failed': 'Failed to open billing portal: {error}',
|
'billing.toast.portal_failed': 'Failed to open billing portal: {error}',
|
||||||
'billing.toast.payment_success': 'Payment successful! Your plan has been upgraded.',
|
'billing.toast.payment_success': 'Payment successful! Your plan has been upgraded.',
|
||||||
|
|
||||||
|
// Teams
|
||||||
|
'team.title': 'Teams',
|
||||||
|
'team.subtitle': 'Manage teams and shared access',
|
||||||
|
'team.help_tip': 'Create teams to share devices with other users. Owners manage the team, editors can change content/playlists, viewers can only monitor.',
|
||||||
|
'team.new_team': 'New Team',
|
||||||
|
'team.prompt_name': 'Team name:',
|
||||||
|
'team.empty_title': 'No teams yet',
|
||||||
|
'team.empty_desc': 'Create a team to share devices with other users.',
|
||||||
|
'team.your_role': 'Your role: {role}',
|
||||||
|
'team.member_count_one': '1 member',
|
||||||
|
'team.member_count_other': '{n} members',
|
||||||
|
'team.not_found': 'Team not found',
|
||||||
|
'team.back': 'Back to Teams',
|
||||||
|
'team.delete_team': 'Delete Team',
|
||||||
|
'team.members_count': 'Members ({n})',
|
||||||
|
'team.invite': '+ Invite',
|
||||||
|
'team.role_viewer': 'Viewer',
|
||||||
|
'team.role_editor': 'Editor',
|
||||||
|
'team.role_owner': 'Owner',
|
||||||
|
'team.remove': 'Remove',
|
||||||
|
'team.remove_from_team': 'Remove from team',
|
||||||
|
'team.no_members': 'No members yet',
|
||||||
|
'team.shared_devices': 'Shared Devices ({n})',
|
||||||
|
'team.add_device': '+ Add device...',
|
||||||
|
'team.no_devices': 'No devices shared with this team',
|
||||||
|
'team.prompt_email': 'Email address to invite:',
|
||||||
|
'team.prompt_role': 'Role (viewer, editor, or owner):',
|
||||||
|
'team.toast.invalid_role': 'Invalid role',
|
||||||
|
'team.toast.invitation_sent': 'Invitation sent',
|
||||||
|
'team.toast.role_updated': 'Role updated',
|
||||||
|
'team.toast.member_removed': 'Member removed',
|
||||||
|
'team.toast.device_added': 'Device added to team',
|
||||||
|
'team.toast.device_removed': 'Device removed from team',
|
||||||
|
'team.toast.deleted': 'Team deleted',
|
||||||
|
|
||||||
|
// Activity log
|
||||||
|
'activity.title': 'Activity Log',
|
||||||
|
'activity.subtitle': 'Audit trail of all actions',
|
||||||
|
'activity.load_more': 'Load More',
|
||||||
|
'activity.empty_title': 'No activity yet',
|
||||||
|
'activity.empty_desc': 'Actions will appear here as you use the system.',
|
||||||
|
'activity.system': 'System',
|
||||||
|
'activity.verb_created': 'created',
|
||||||
|
'activity.verb_updated': 'updated',
|
||||||
|
'activity.verb_deleted': 'deleted',
|
||||||
|
'activity.action_paired_device': 'paired a device',
|
||||||
|
'activity.action_added_remote_content': 'added remote content',
|
||||||
|
'activity.noun_content': 'content',
|
||||||
|
'activity.noun_device': 'device',
|
||||||
|
'activity.noun_playlist_assignment': 'playlist assignment',
|
||||||
|
'activity.noun_assignment': 'assignment',
|
||||||
|
'activity.noun_layout': 'layout',
|
||||||
|
'activity.noun_widget': 'widget',
|
||||||
|
'activity.noun_schedule': 'schedule',
|
||||||
|
'activity.noun_video_wall': 'video wall',
|
||||||
|
'activity.alert_device_offline': 'alert: device went offline',
|
||||||
|
|
||||||
|
// Help
|
||||||
|
'help.title': 'Help Center',
|
||||||
|
'help.subtitle': 'Quick guides and FAQ',
|
||||||
|
'help.faq': 'Frequently Asked Questions',
|
||||||
|
'help.shortcuts': 'Keyboard Shortcuts',
|
||||||
|
'help.shortcut_esc': 'Reset web player (on player page)',
|
||||||
|
'help.shortcut_f': 'Toggle fullscreen (web player)',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -982,4 +982,69 @@ export default {
|
||||||
'billing.toast.checkout_failed': 'Error al iniciar el pago: {error}',
|
'billing.toast.checkout_failed': 'Error al iniciar el pago: {error}',
|
||||||
'billing.toast.portal_failed': 'Error al abrir el portal de facturación: {error}',
|
'billing.toast.portal_failed': 'Error al abrir el portal de facturación: {error}',
|
||||||
'billing.toast.payment_success': '¡Pago exitoso! Tu plan ha sido actualizado.',
|
'billing.toast.payment_success': '¡Pago exitoso! Tu plan ha sido actualizado.',
|
||||||
|
|
||||||
|
// Teams
|
||||||
|
'team.title': 'Equipos',
|
||||||
|
'team.subtitle': 'Gestiona equipos y acceso compartido',
|
||||||
|
'team.help_tip': 'Crea equipos para compartir dispositivos con otros usuarios. Los propietarios gestionan el equipo, los editores pueden cambiar contenido/listas, los espectadores solo monitorean.',
|
||||||
|
'team.new_team': 'Nuevo equipo',
|
||||||
|
'team.prompt_name': 'Nombre del equipo:',
|
||||||
|
'team.empty_title': 'Aún no hay equipos',
|
||||||
|
'team.empty_desc': 'Crea un equipo para compartir dispositivos con otros usuarios.',
|
||||||
|
'team.your_role': 'Tu rol: {role}',
|
||||||
|
'team.member_count_one': '1 miembro',
|
||||||
|
'team.member_count_other': '{n} miembros',
|
||||||
|
'team.not_found': 'Equipo no encontrado',
|
||||||
|
'team.back': 'Volver a equipos',
|
||||||
|
'team.delete_team': 'Eliminar equipo',
|
||||||
|
'team.members_count': 'Miembros ({n})',
|
||||||
|
'team.invite': '+ Invitar',
|
||||||
|
'team.role_viewer': 'Espectador',
|
||||||
|
'team.role_editor': 'Editor',
|
||||||
|
'team.role_owner': 'Propietario',
|
||||||
|
'team.remove': 'Eliminar',
|
||||||
|
'team.remove_from_team': 'Quitar del equipo',
|
||||||
|
'team.no_members': 'Sin miembros',
|
||||||
|
'team.shared_devices': 'Dispositivos compartidos ({n})',
|
||||||
|
'team.add_device': '+ Agregar dispositivo...',
|
||||||
|
'team.no_devices': 'No hay dispositivos compartidos con este equipo',
|
||||||
|
'team.prompt_email': 'Email para invitar:',
|
||||||
|
'team.prompt_role': 'Rol (viewer, editor o owner):',
|
||||||
|
'team.toast.invalid_role': 'Rol no válido',
|
||||||
|
'team.toast.invitation_sent': 'Invitación enviada',
|
||||||
|
'team.toast.role_updated': 'Rol actualizado',
|
||||||
|
'team.toast.member_removed': 'Miembro eliminado',
|
||||||
|
'team.toast.device_added': 'Dispositivo agregado al equipo',
|
||||||
|
'team.toast.device_removed': 'Dispositivo quitado del equipo',
|
||||||
|
'team.toast.deleted': 'Equipo eliminado',
|
||||||
|
|
||||||
|
// Activity
|
||||||
|
'activity.title': 'Registro de actividad',
|
||||||
|
'activity.subtitle': 'Historial de auditoría de todas las acciones',
|
||||||
|
'activity.load_more': 'Cargar más',
|
||||||
|
'activity.empty_title': 'Aún no hay actividad',
|
||||||
|
'activity.empty_desc': 'Las acciones aparecerán aquí a medida que uses el sistema.',
|
||||||
|
'activity.system': 'Sistema',
|
||||||
|
'activity.verb_created': 'creó',
|
||||||
|
'activity.verb_updated': 'actualizó',
|
||||||
|
'activity.verb_deleted': 'eliminó',
|
||||||
|
'activity.action_paired_device': 'vinculó un dispositivo',
|
||||||
|
'activity.action_added_remote_content': 'agregó contenido remoto',
|
||||||
|
'activity.noun_content': 'contenido',
|
||||||
|
'activity.noun_device': 'dispositivo',
|
||||||
|
'activity.noun_playlist_assignment': 'asignación de lista',
|
||||||
|
'activity.noun_assignment': 'asignación',
|
||||||
|
'activity.noun_layout': 'diseño',
|
||||||
|
'activity.noun_widget': 'widget',
|
||||||
|
'activity.noun_schedule': 'horario',
|
||||||
|
'activity.noun_video_wall': 'muro de video',
|
||||||
|
'activity.alert_device_offline': 'alerta: dispositivo desconectado',
|
||||||
|
|
||||||
|
// Help
|
||||||
|
'help.title': 'Centro de ayuda',
|
||||||
|
'help.subtitle': 'Guías rápidas y preguntas frecuentes',
|
||||||
|
'help.faq': 'Preguntas frecuentes',
|
||||||
|
'help.shortcuts': 'Atajos de teclado',
|
||||||
|
'help.shortcut_esc': 'Reiniciar reproductor web (en la página del reproductor)',
|
||||||
|
'help.shortcut_f': 'Alternar pantalla completa (reproductor web)',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -983,4 +983,69 @@ export default {
|
||||||
'billing.toast.checkout_failed': 'Échec du paiement : {error}',
|
'billing.toast.checkout_failed': 'Échec du paiement : {error}',
|
||||||
'billing.toast.portal_failed': 'Échec d\'ouverture du portail : {error}',
|
'billing.toast.portal_failed': 'Échec d\'ouverture du portail : {error}',
|
||||||
'billing.toast.payment_success': 'Paiement réussi ! Votre plan a été mis à niveau.',
|
'billing.toast.payment_success': 'Paiement réussi ! Votre plan a été mis à niveau.',
|
||||||
|
|
||||||
|
// Teams
|
||||||
|
'team.title': 'Équipes',
|
||||||
|
'team.subtitle': 'Gérez les équipes et l\'accès partagé',
|
||||||
|
'team.help_tip': 'Créez des équipes pour partager des appareils. Les propriétaires gèrent l\'équipe, les éditeurs modifient le contenu/listes, les spectateurs ne font que surveiller.',
|
||||||
|
'team.new_team': 'Nouvelle équipe',
|
||||||
|
'team.prompt_name': 'Nom de l\'équipe :',
|
||||||
|
'team.empty_title': 'Aucune équipe',
|
||||||
|
'team.empty_desc': 'Créez une équipe pour partager des appareils avec d\'autres utilisateurs.',
|
||||||
|
'team.your_role': 'Votre rôle : {role}',
|
||||||
|
'team.member_count_one': '1 membre',
|
||||||
|
'team.member_count_other': '{n} membres',
|
||||||
|
'team.not_found': 'Équipe introuvable',
|
||||||
|
'team.back': 'Retour aux équipes',
|
||||||
|
'team.delete_team': 'Supprimer l\'équipe',
|
||||||
|
'team.members_count': 'Membres ({n})',
|
||||||
|
'team.invite': '+ Inviter',
|
||||||
|
'team.role_viewer': 'Spectateur',
|
||||||
|
'team.role_editor': 'Éditeur',
|
||||||
|
'team.role_owner': 'Propriétaire',
|
||||||
|
'team.remove': 'Retirer',
|
||||||
|
'team.remove_from_team': 'Retirer de l\'équipe',
|
||||||
|
'team.no_members': 'Aucun membre',
|
||||||
|
'team.shared_devices': 'Appareils partagés ({n})',
|
||||||
|
'team.add_device': '+ Ajouter un appareil...',
|
||||||
|
'team.no_devices': 'Aucun appareil partagé avec cette équipe',
|
||||||
|
'team.prompt_email': 'E-mail à inviter :',
|
||||||
|
'team.prompt_role': 'Rôle (viewer, editor ou owner) :',
|
||||||
|
'team.toast.invalid_role': 'Rôle invalide',
|
||||||
|
'team.toast.invitation_sent': 'Invitation envoyée',
|
||||||
|
'team.toast.role_updated': 'Rôle mis à jour',
|
||||||
|
'team.toast.member_removed': 'Membre retiré',
|
||||||
|
'team.toast.device_added': 'Appareil ajouté à l\'équipe',
|
||||||
|
'team.toast.device_removed': 'Appareil retiré de l\'équipe',
|
||||||
|
'team.toast.deleted': 'Équipe supprimée',
|
||||||
|
|
||||||
|
// Activity
|
||||||
|
'activity.title': 'Journal d\'activité',
|
||||||
|
'activity.subtitle': 'Audit de toutes les actions',
|
||||||
|
'activity.load_more': 'Charger plus',
|
||||||
|
'activity.empty_title': 'Aucune activité',
|
||||||
|
'activity.empty_desc': 'Les actions apparaîtront ici au fur et à mesure de votre utilisation.',
|
||||||
|
'activity.system': 'Système',
|
||||||
|
'activity.verb_created': 'a créé',
|
||||||
|
'activity.verb_updated': 'a mis à jour',
|
||||||
|
'activity.verb_deleted': 'a supprimé',
|
||||||
|
'activity.action_paired_device': 'a apparié un appareil',
|
||||||
|
'activity.action_added_remote_content': 'a ajouté du contenu distant',
|
||||||
|
'activity.noun_content': 'contenu',
|
||||||
|
'activity.noun_device': 'appareil',
|
||||||
|
'activity.noun_playlist_assignment': 'attribution de liste',
|
||||||
|
'activity.noun_assignment': 'attribution',
|
||||||
|
'activity.noun_layout': 'mise en page',
|
||||||
|
'activity.noun_widget': 'widget',
|
||||||
|
'activity.noun_schedule': 'plage horaire',
|
||||||
|
'activity.noun_video_wall': 'mur vidéo',
|
||||||
|
'activity.alert_device_offline': 'alerte : appareil hors ligne',
|
||||||
|
|
||||||
|
// Help
|
||||||
|
'help.title': 'Centre d\'aide',
|
||||||
|
'help.subtitle': 'Guides rapides et FAQ',
|
||||||
|
'help.faq': 'Questions fréquentes',
|
||||||
|
'help.shortcuts': 'Raccourcis clavier',
|
||||||
|
'help.shortcut_esc': 'Réinitialiser le lecteur web (sur la page du lecteur)',
|
||||||
|
'help.shortcut_f': 'Basculer le plein écran (lecteur web)',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -983,4 +983,69 @@ export default {
|
||||||
'billing.toast.checkout_failed': 'Falha ao iniciar pagamento: {error}',
|
'billing.toast.checkout_failed': 'Falha ao iniciar pagamento: {error}',
|
||||||
'billing.toast.portal_failed': 'Falha ao abrir portal de cobrança: {error}',
|
'billing.toast.portal_failed': 'Falha ao abrir portal de cobrança: {error}',
|
||||||
'billing.toast.payment_success': 'Pagamento bem-sucedido! Seu plano foi atualizado.',
|
'billing.toast.payment_success': 'Pagamento bem-sucedido! Seu plano foi atualizado.',
|
||||||
|
|
||||||
|
// Teams
|
||||||
|
'team.title': 'Equipes',
|
||||||
|
'team.subtitle': 'Gerencie equipes e acesso compartilhado',
|
||||||
|
'team.help_tip': 'Crie equipes para compartilhar dispositivos com outros usuários. Proprietários gerenciam a equipe, editores podem alterar conteúdo/playlists, visualizadores apenas monitoram.',
|
||||||
|
'team.new_team': 'Nova equipe',
|
||||||
|
'team.prompt_name': 'Nome da equipe:',
|
||||||
|
'team.empty_title': 'Sem equipes ainda',
|
||||||
|
'team.empty_desc': 'Crie uma equipe para compartilhar dispositivos com outros usuários.',
|
||||||
|
'team.your_role': 'Sua função: {role}',
|
||||||
|
'team.member_count_one': '1 membro',
|
||||||
|
'team.member_count_other': '{n} membros',
|
||||||
|
'team.not_found': 'Equipe não encontrada',
|
||||||
|
'team.back': 'Voltar para equipes',
|
||||||
|
'team.delete_team': 'Excluir equipe',
|
||||||
|
'team.members_count': 'Membros ({n})',
|
||||||
|
'team.invite': '+ Convidar',
|
||||||
|
'team.role_viewer': 'Visualizador',
|
||||||
|
'team.role_editor': 'Editor',
|
||||||
|
'team.role_owner': 'Proprietário',
|
||||||
|
'team.remove': 'Remover',
|
||||||
|
'team.remove_from_team': 'Remover da equipe',
|
||||||
|
'team.no_members': 'Sem membros',
|
||||||
|
'team.shared_devices': 'Dispositivos compartilhados ({n})',
|
||||||
|
'team.add_device': '+ Adicionar dispositivo...',
|
||||||
|
'team.no_devices': 'Sem dispositivos compartilhados com esta equipe',
|
||||||
|
'team.prompt_email': 'E-mail para convidar:',
|
||||||
|
'team.prompt_role': 'Função (viewer, editor ou owner):',
|
||||||
|
'team.toast.invalid_role': 'Função inválida',
|
||||||
|
'team.toast.invitation_sent': 'Convite enviado',
|
||||||
|
'team.toast.role_updated': 'Função atualizada',
|
||||||
|
'team.toast.member_removed': 'Membro removido',
|
||||||
|
'team.toast.device_added': 'Dispositivo adicionado à equipe',
|
||||||
|
'team.toast.device_removed': 'Dispositivo removido da equipe',
|
||||||
|
'team.toast.deleted': 'Equipe excluída',
|
||||||
|
|
||||||
|
// Activity
|
||||||
|
'activity.title': 'Registro de atividades',
|
||||||
|
'activity.subtitle': 'Trilha de auditoria de todas as ações',
|
||||||
|
'activity.load_more': 'Carregar mais',
|
||||||
|
'activity.empty_title': 'Sem atividade ainda',
|
||||||
|
'activity.empty_desc': 'As ações aparecerão aqui conforme você usa o sistema.',
|
||||||
|
'activity.system': 'Sistema',
|
||||||
|
'activity.verb_created': 'criou',
|
||||||
|
'activity.verb_updated': 'atualizou',
|
||||||
|
'activity.verb_deleted': 'excluiu',
|
||||||
|
'activity.action_paired_device': 'pareou um dispositivo',
|
||||||
|
'activity.action_added_remote_content': 'adicionou conteúdo remoto',
|
||||||
|
'activity.noun_content': 'conteúdo',
|
||||||
|
'activity.noun_device': 'dispositivo',
|
||||||
|
'activity.noun_playlist_assignment': 'atribuição de playlist',
|
||||||
|
'activity.noun_assignment': 'atribuição',
|
||||||
|
'activity.noun_layout': 'layout',
|
||||||
|
'activity.noun_widget': 'widget',
|
||||||
|
'activity.noun_schedule': 'agenda',
|
||||||
|
'activity.noun_video_wall': 'parede de vídeo',
|
||||||
|
'activity.alert_device_offline': 'alerta: dispositivo offline',
|
||||||
|
|
||||||
|
// Help
|
||||||
|
'help.title': 'Central de ajuda',
|
||||||
|
'help.subtitle': 'Guias rápidos e perguntas frequentes',
|
||||||
|
'help.faq': 'Perguntas frequentes',
|
||||||
|
'help.shortcuts': 'Atalhos de teclado',
|
||||||
|
'help.shortcut_esc': 'Reiniciar player web (na página do player)',
|
||||||
|
'help.shortcut_f': 'Alternar tela cheia (player web)',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
import { showToast } from '../components/toast.js';
|
import { showToast } from '../components/toast.js';
|
||||||
import { esc } from '../utils.js';
|
import { esc } from '../utils.js';
|
||||||
|
import { t } from '../i18n.js';
|
||||||
|
|
||||||
const API = (url) => fetch('/api' + url, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }}).then(r => r.json());
|
const API = (url) => fetch('/api' + url, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }}).then(r => r.json());
|
||||||
|
|
||||||
export async function render(container) {
|
export async function render(container) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div><h1>Activity Log</h1><div class="subtitle">Audit trail of all actions</div></div>
|
<div><h1>${t('activity.title')}</h1><div class="subtitle">${t('activity.subtitle')}</div></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="activityList"><div class="empty-state"><h3>Loading...</h3></div></div>
|
<div id="activityList"><div class="empty-state"><h3>${t('common.loading')}</h3></div></div>
|
||||||
<div style="text-align:center;margin-top:16px">
|
<div style="text-align:center;margin-top:16px">
|
||||||
<button class="btn btn-secondary btn-sm" id="loadMoreBtn" style="display:none">Load More</button>
|
<button class="btn btn-secondary btn-sm" id="loadMoreBtn" style="display:none">${t('activity.load_more')}</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -25,14 +26,14 @@ export async function render(container) {
|
||||||
if (!append) list.innerHTML = '';
|
if (!append) list.innerHTML = '';
|
||||||
|
|
||||||
if (items.length === 0 && offset === 0) {
|
if (items.length === 0 && offset === 0) {
|
||||||
list.innerHTML = '<div class="empty-state"><h3>No activity yet</h3><p>Actions will appear here as you use the system.</p></div>';
|
list.innerHTML = `<div class="empty-state"><h3>${t('activity.empty_title')}</h3><p>${t('activity.empty_desc')}</p></div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const html = items.map(item => {
|
const html = items.map(item => {
|
||||||
const time = new Date(item.created_at * 1000);
|
const time = new Date(item.created_at * 1000);
|
||||||
const timeStr = time.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + ' ' +
|
const timeStr = time.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) + ' ' +
|
||||||
time.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
time.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
|
||||||
const icon = getActionIcon(item.action);
|
const icon = getActionIcon(item.action);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
|
|
@ -40,7 +41,7 @@ export async function render(container) {
|
||||||
<div style="width:32px;height:32px;border-radius:50%;background:var(--bg-card);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:14px">${icon}</div>
|
<div style="width:32px;height:32px;border-radius:50%;background:var(--bg-card);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:14px">${icon}</div>
|
||||||
<div style="flex:1;min-width:0">
|
<div style="flex:1;min-width:0">
|
||||||
<div style="font-size:13px">
|
<div style="font-size:13px">
|
||||||
<strong>${esc(item.user_name || item.user_email || 'System')}</strong>
|
<strong>${esc(item.user_name || item.user_email || t('activity.system'))}</strong>
|
||||||
<span style="color:var(--text-secondary)"> ${esc(formatAction(item.action))}</span>
|
<span style="color:var(--text-secondary)"> ${esc(formatAction(item.action))}</span>
|
||||||
</div>
|
</div>
|
||||||
${item.details ? `<div style="font-size:12px;color:var(--text-muted);margin-top:2px">${esc(item.details)}</div>` : ''}
|
${item.details ? `<div style="font-size:12px;color:var(--text-muted);margin-top:2px">${esc(item.details)}</div>` : ''}
|
||||||
|
|
@ -81,22 +82,29 @@ function getActionIcon(action) {
|
||||||
return '📄';
|
return '📄';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Action verbs are user-visible; translate them through t() so they switch
|
||||||
|
// languages with the rest of the UI. The mapping below preserves the original
|
||||||
|
// verb-then-noun structure of the English version.
|
||||||
function formatAction(action) {
|
function formatAction(action) {
|
||||||
return action
|
// Verbs
|
||||||
.replace('POST /api/', 'created ')
|
let s = action
|
||||||
.replace('PUT /api/', 'updated ')
|
.replace('POST /api/', t('activity.verb_created') + ' ')
|
||||||
.replace('DELETE /api/', 'deleted ')
|
.replace('PUT /api/', t('activity.verb_updated') + ' ')
|
||||||
.replace('/provision/pair', 'paired a device')
|
.replace('DELETE /api/', t('activity.verb_deleted') + ' ');
|
||||||
.replace('/content/remote', 'added remote content')
|
// Specific endpoints
|
||||||
.replace('/content', 'content')
|
s = s
|
||||||
.replace('/devices/:id', 'device')
|
.replace('/provision/pair', t('activity.action_paired_device'))
|
||||||
.replace('/assignments/device/:deviceId', 'playlist assignment')
|
.replace('/content/remote', t('activity.action_added_remote_content'))
|
||||||
.replace('/assignments/:id', 'assignment')
|
.replace('/content', t('activity.noun_content'))
|
||||||
.replace('/layouts', 'layout')
|
.replace('/devices/:id', t('activity.noun_device'))
|
||||||
.replace('/widgets', 'widget')
|
.replace('/assignments/device/:deviceId', t('activity.noun_playlist_assignment'))
|
||||||
.replace('/schedules', 'schedule')
|
.replace('/assignments/:id', t('activity.noun_assignment'))
|
||||||
.replace('/walls', 'video wall')
|
.replace('/layouts', t('activity.noun_layout'))
|
||||||
.replace('alert:device_offline', 'alert: device went offline');
|
.replace('/widgets', t('activity.noun_widget'))
|
||||||
|
.replace('/schedules', t('activity.noun_schedule'))
|
||||||
|
.replace('/walls', t('activity.noun_video_wall'))
|
||||||
|
.replace('alert:device_offline', t('activity.alert_device_offline'));
|
||||||
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cleanup() {}
|
export function cleanup() {}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
|
import { t } from '../i18n.js';
|
||||||
|
|
||||||
|
// Help guides + FAQ are documentation. Page chrome is translated; the body
|
||||||
|
// content is intentionally left in English because partial machine
|
||||||
|
// translation of multi-paragraph docs reads worse than a single source of
|
||||||
|
// truth. A native-language docs site is the right long-term answer.
|
||||||
export function render(container) {
|
export function render(container) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div><h1>Help Center</h1><div class="subtitle">Quick guides and FAQ</div></div>
|
<div><h1>${t('help.title')}</h1><div class="subtitle">${t('help.subtitle')}</div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px;margin-bottom:32px">
|
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px;margin-bottom:32px">
|
||||||
|
|
@ -25,7 +31,7 @@ export function render(container) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>Frequently Asked Questions</h3>
|
<h3>${t('help.faq')}</h3>
|
||||||
${[
|
${[
|
||||||
{ q: 'What devices are supported?', a: 'Android TV/tablets (APK), Raspberry Pi, Windows, ChromeOS, LG webOS, Samsung Tizen, Fire TV, and any device with a web browser.' },
|
{ q: 'What devices are supported?', a: 'Android TV/tablets (APK), Raspberry Pi, Windows, ChromeOS, LG webOS, Samsung Tizen, Fire TV, and any device with a web browser.' },
|
||||||
{ q: 'How does the free trial work?', a: 'New accounts get a 14-day free trial of the Pro plan (15 devices, all features). After 14 days, you\'re moved to the Free plan (1 device) unless you upgrade.' },
|
{ q: 'How does the free trial work?', a: 'New accounts get a 14-day free trial of the Pro plan (15 devices, all features). After 14 days, you\'re moved to the Free plan (1 device) unless you upgrade.' },
|
||||||
|
|
@ -46,10 +52,10 @@ export function render(container) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>Keyboard Shortcuts</h3>
|
<h3>${t('help.shortcuts')}</h3>
|
||||||
<div style="display:grid;grid-template-columns:auto 1fr;gap:8px 16px;font-size:13px">
|
<div style="display:grid;grid-template-columns:auto 1fr;gap:8px 16px;font-size:13px">
|
||||||
<kbd style="background:var(--bg-input);padding:2px 8px;border-radius:4px;font-family:monospace">Esc</kbd> <span style="color:var(--text-secondary)">Reset web player (on player page)</span>
|
<kbd style="background:var(--bg-input);padding:2px 8px;border-radius:4px;font-family:monospace">Esc</kbd> <span style="color:var(--text-secondary)">${t('help.shortcut_esc')}</span>
|
||||||
<kbd style="background:var(--bg-input);padding:2px 8px;border-radius:4px;font-family:monospace">F</kbd> <span style="color:var(--text-secondary)">Toggle fullscreen (web player)</span>
|
<kbd style="background:var(--bg-input);padding:2px 8px;border-radius:4px;font-family:monospace">F</kbd> <span style="color:var(--text-secondary)">${t('help.shortcut_f')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { api } from '../api.js';
|
import { api } from '../api.js';
|
||||||
import { showToast } from '../components/toast.js';
|
import { showToast } from '../components/toast.js';
|
||||||
|
import { t, tn } 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());
|
||||||
|
|
||||||
|
|
@ -15,17 +16,17 @@ export async function render(container) {
|
||||||
async function renderList(container) {
|
async function renderList(container) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div><h1>Teams <span class="help-tip" data-tip="Create teams to share devices with other users. Owners manage the team, editors can change content/playlists, viewers can only monitor.">?</span></h1><div class="subtitle">Manage teams and shared access</div></div>
|
<div><h1>${t('team.title')} <span class="help-tip" data-tip="${t('team.help_tip')}">?</span></h1><div class="subtitle">${t('team.subtitle')}</div></div>
|
||||||
<button class="btn btn-primary" id="newTeamBtn">
|
<button class="btn btn-primary" id="newTeamBtn">
|
||||||
<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 Team
|
${t('team.new_team')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="teamsList"></div>
|
<div id="teamsList"></div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
document.getElementById('newTeamBtn').onclick = async () => {
|
document.getElementById('newTeamBtn').onclick = async () => {
|
||||||
const name = prompt('Team name:');
|
const name = prompt(t('team.prompt_name'));
|
||||||
if (!name) return;
|
if (!name) return;
|
||||||
const team = await API('/teams', { method: 'POST', body: JSON.stringify({ name }) });
|
const team = await API('/teams', { method: 'POST', body: JSON.stringify({ name }) });
|
||||||
window.location.hash = `#/team/${team.id}`;
|
window.location.hash = `#/team/${team.id}`;
|
||||||
|
|
@ -36,19 +37,19 @@ async function renderList(container) {
|
||||||
const list = document.getElementById('teamsList');
|
const list = document.getElementById('teamsList');
|
||||||
|
|
||||||
if (!teams.length) {
|
if (!teams.length) {
|
||||||
list.innerHTML = '<div class="empty-state"><h3>No teams yet</h3><p>Create a team to share devices with other users.</p></div>';
|
list.innerHTML = `<div class="empty-state"><h3>${t('team.empty_title')}</h3><p>${t('team.empty_desc')}</p></div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
list.innerHTML = `<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px">
|
list.innerHTML = `<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px">
|
||||||
${teams.map(t => `
|
${teams.map(team => `
|
||||||
<div class="content-item" style="cursor:pointer" onclick="window.location.hash='#/team/${t.id}'">
|
<div class="content-item" style="cursor:pointer" onclick="window.location.hash='#/team/${team.id}'">
|
||||||
<div style="padding:20px">
|
<div style="padding:20px">
|
||||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px">
|
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px">
|
||||||
<div style="width:40px;height:40px;border-radius:50%;background:var(--accent);display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:700;color:white">${t.name[0].toUpperCase()}</div>
|
<div style="width:40px;height:40px;border-radius:50%;background:var(--accent);display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:700;color:white">${team.name[0].toUpperCase()}</div>
|
||||||
<div>
|
<div>
|
||||||
<div style="font-weight:600;font-size:16px">${t.name}</div>
|
<div style="font-weight:600;font-size:16px">${team.name}</div>
|
||||||
<div style="font-size:12px;color:var(--text-muted)">Your role: ${t.my_role} · ${t.member_count} member(s)</div>
|
<div style="font-size:12px;color:var(--text-muted)">${t('team.your_role', { role: team.my_role })} · ${tn('team.member_count', team.member_count)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -66,28 +67,27 @@ async function renderTeamDetail(container, teamId) {
|
||||||
API(`/teams/${teamId}/devices`),
|
API(`/teams/${teamId}/devices`),
|
||||||
api.getDevices()
|
api.getDevices()
|
||||||
]);
|
]);
|
||||||
} catch { container.innerHTML = '<div class="empty-state"><h3>Team not found</h3></div>'; return; }
|
} catch { container.innerHTML = `<div class="empty-state"><h3>${t('team.not_found')}</h3></div>`; return; }
|
||||||
|
|
||||||
const unassignedDevices = allDevices.filter(d => !d.team_id || d.team_id !== teamId);
|
const unassignedDevices = allDevices.filter(d => !d.team_id || d.team_id !== teamId);
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<a href="#/teams" class="back-link" style="display:inline-flex;align-items:center;gap:6px;color:var(--text-secondary);margin-bottom:16px;font-size:13px">
|
<a href="#/teams" class="back-link" style="display:inline-flex;align-items:center;gap:6px;color:var(--text-secondary);margin-bottom:16px;font-size:13px">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
|
||||||
Back to Teams
|
${t('team.back')}
|
||||||
</a>
|
</a>
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1>${team.name}</h1>
|
<h1>${team.name}</h1>
|
||||||
<div style="display:flex;gap:8px">
|
<div style="display:flex;gap:8px">
|
||||||
<button class="btn btn-danger btn-sm" id="deleteTeamBtn">Delete Team</button>
|
<button class="btn btn-danger btn-sm" id="deleteTeamBtn">${t('team.delete_team')}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:24px">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:24px">
|
||||||
<!-- Members -->
|
|
||||||
<div class="settings-section" style="margin:0">
|
<div class="settings-section" style="margin:0">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||||||
<h3 style="font-size:15px">Members (${team.members?.length || 0})</h3>
|
<h3 style="font-size:15px">${t('team.members_count', { n: team.members?.length || 0 })}</h3>
|
||||||
<button class="btn btn-secondary btn-sm" id="inviteMemberBtn">+ Invite</button>
|
<button class="btn btn-secondary btn-sm" id="inviteMemberBtn">${t('team.invite')}</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="membersList">
|
<div id="membersList">
|
||||||
${(team.members || []).map(m => `
|
${(team.members || []).map(m => `
|
||||||
|
|
@ -98,22 +98,21 @@ async function renderTeamDetail(container, teamId) {
|
||||||
<div style="font-size:11px;color:var(--text-muted)">${m.email}</div>
|
<div style="font-size:11px;color:var(--text-muted)">${m.email}</div>
|
||||||
</div>
|
</div>
|
||||||
<select class="input" style="max-width:100px;width:100%;background:var(--bg-input);font-size:12px;padding:4px 8px" data-member-id="${m.user_id}" ${m.role === 'owner' ? 'disabled' : ''}>
|
<select class="input" style="max-width:100px;width:100%;background:var(--bg-input);font-size:12px;padding:4px 8px" data-member-id="${m.user_id}" ${m.role === 'owner' ? 'disabled' : ''}>
|
||||||
<option value="viewer" ${m.role === 'viewer' ? 'selected' : ''}>Viewer</option>
|
<option value="viewer" ${m.role === 'viewer' ? 'selected' : ''}>${t('team.role_viewer')}</option>
|
||||||
<option value="editor" ${m.role === 'editor' ? 'selected' : ''}>Editor</option>
|
<option value="editor" ${m.role === 'editor' ? 'selected' : ''}>${t('team.role_editor')}</option>
|
||||||
<option value="owner" ${m.role === 'owner' ? 'selected' : ''}>Owner</option>
|
<option value="owner" ${m.role === 'owner' ? 'selected' : ''}>${t('team.role_owner')}</option>
|
||||||
</select>
|
</select>
|
||||||
${m.role !== 'owner' ? `<button class="btn-icon" data-remove-member="${m.user_id}" style="color:var(--danger)" title="Remove">✕</button>` : ''}
|
${m.role !== 'owner' ? `<button class="btn-icon" data-remove-member="${m.user_id}" style="color:var(--danger)" title="${t('team.remove')}">✕</button>` : ''}
|
||||||
</div>
|
</div>
|
||||||
`).join('') || '<p style="color:var(--text-muted);font-size:13px">No members yet</p>'}
|
`).join('') || `<p style="color:var(--text-muted);font-size:13px">${t('team.no_members')}</p>`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Devices -->
|
|
||||||
<div class="settings-section" style="margin:0">
|
<div class="settings-section" style="margin:0">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||||||
<h3 style="font-size:15px">Shared Devices (${devices.length})</h3>
|
<h3 style="font-size:15px">${t('team.shared_devices', { n: devices.length })}</h3>
|
||||||
<select id="addDeviceToTeam" class="input" style="max-width:200px;width:100%;background:var(--bg-input);font-size:12px">
|
<select id="addDeviceToTeam" class="input" style="max-width:200px;width:100%;background:var(--bg-input);font-size:12px">
|
||||||
<option value="">+ Add device...</option>
|
<option value="">${t('team.add_device')}</option>
|
||||||
${unassignedDevices.map(d => `<option value="${d.id}">${d.name}</option>`).join('')}
|
${unassignedDevices.map(d => `<option value="${d.id}">${d.name}</option>`).join('')}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -125,75 +124,69 @@ async function renderTeamDetail(container, teamId) {
|
||||||
<div style="font-size:13px;font-weight:500">${d.name}</div>
|
<div style="font-size:13px;font-weight:500">${d.name}</div>
|
||||||
<div style="font-size:11px;color:var(--text-muted)">${d.status}</div>
|
<div style="font-size:11px;color:var(--text-muted)">${d.status}</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-icon" data-remove-device="${d.id}" style="color:var(--danger)" title="Remove from team">✕</button>
|
<button class="btn-icon" data-remove-device="${d.id}" style="color:var(--danger)" title="${t('team.remove_from_team')}">✕</button>
|
||||||
</div>
|
</div>
|
||||||
`).join('') || '<p style="color:var(--text-muted);font-size:13px">No devices shared with this team</p>'}
|
`).join('') || `<p style="color:var(--text-muted);font-size:13px">${t('team.no_devices')}</p>`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Invite member
|
|
||||||
document.getElementById('inviteMemberBtn').onclick = async () => {
|
document.getElementById('inviteMemberBtn').onclick = async () => {
|
||||||
const email = prompt('Email address to invite:');
|
const email = prompt(t('team.prompt_email'));
|
||||||
if (!email) return;
|
if (!email) return;
|
||||||
const role = prompt('Role (viewer, editor, or owner):', 'editor');
|
const role = prompt(t('team.prompt_role'), 'editor');
|
||||||
if (!['viewer', 'editor', 'owner'].includes(role)) { showToast('Invalid role', 'error'); return; }
|
if (!['viewer', 'editor', 'owner'].includes(role)) { showToast(t('team.toast.invalid_role'), 'error'); return; }
|
||||||
try {
|
try {
|
||||||
await API(`/teams/${teamId}/invite`, { method: 'POST', body: JSON.stringify({ email, role }) });
|
await API(`/teams/${teamId}/invite`, { method: 'POST', body: JSON.stringify({ email, role }) });
|
||||||
showToast('Invitation sent', 'success');
|
showToast(t('team.toast.invitation_sent'), 'success');
|
||||||
renderTeamDetail(container, teamId);
|
renderTeamDetail(container, teamId);
|
||||||
} catch (err) { showToast(err.message, 'error'); }
|
} catch (err) { showToast(err.message, 'error'); }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Change member role
|
|
||||||
container.querySelectorAll('[data-member-id]').forEach(select => {
|
container.querySelectorAll('[data-member-id]').forEach(select => {
|
||||||
select.onchange = async () => {
|
select.onchange = async () => {
|
||||||
try {
|
try {
|
||||||
await API(`/teams/${teamId}/members/${select.dataset.memberId}`, { method: 'PUT', body: JSON.stringify({ role: select.value }) });
|
await API(`/teams/${teamId}/members/${select.dataset.memberId}`, { method: 'PUT', body: JSON.stringify({ role: select.value }) });
|
||||||
showToast('Role updated', 'success');
|
showToast(t('team.toast.role_updated'), 'success');
|
||||||
} catch (err) { showToast(err.message, 'error'); }
|
} catch (err) { showToast(err.message, 'error'); }
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove member
|
|
||||||
container.querySelectorAll('[data-remove-member]').forEach(btn => {
|
container.querySelectorAll('[data-remove-member]').forEach(btn => {
|
||||||
btn.onclick = async () => {
|
btn.onclick = async () => {
|
||||||
try {
|
try {
|
||||||
await API(`/teams/${teamId}/members/${btn.dataset.removeMember}`, { method: 'DELETE' });
|
await API(`/teams/${teamId}/members/${btn.dataset.removeMember}`, { method: 'DELETE' });
|
||||||
showToast('Member removed', 'success');
|
showToast(t('team.toast.member_removed'), 'success');
|
||||||
renderTeamDetail(container, teamId);
|
renderTeamDetail(container, teamId);
|
||||||
} catch (err) { showToast(err.message, 'error'); }
|
} catch (err) { showToast(err.message, 'error'); }
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add device to team
|
|
||||||
document.getElementById('addDeviceToTeam').onchange = async (e) => {
|
document.getElementById('addDeviceToTeam').onchange = async (e) => {
|
||||||
const deviceId = e.target.value;
|
const deviceId = e.target.value;
|
||||||
if (!deviceId) return;
|
if (!deviceId) return;
|
||||||
try {
|
try {
|
||||||
await API(`/teams/${teamId}/devices`, { method: 'POST', body: JSON.stringify({ device_id: deviceId }) });
|
await API(`/teams/${teamId}/devices`, { method: 'POST', body: JSON.stringify({ device_id: deviceId }) });
|
||||||
showToast('Device added to team', 'success');
|
showToast(t('team.toast.device_added'), 'success');
|
||||||
renderTeamDetail(container, teamId);
|
renderTeamDetail(container, teamId);
|
||||||
} catch (err) { showToast(err.message, 'error'); }
|
} catch (err) { showToast(err.message, 'error'); }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove device from team
|
|
||||||
container.querySelectorAll('[data-remove-device]').forEach(btn => {
|
container.querySelectorAll('[data-remove-device]').forEach(btn => {
|
||||||
btn.onclick = async () => {
|
btn.onclick = async () => {
|
||||||
try {
|
try {
|
||||||
await API(`/teams/${teamId}/devices/${btn.dataset.removeDevice}`, { method: 'DELETE' });
|
await API(`/teams/${teamId}/devices/${btn.dataset.removeDevice}`, { method: 'DELETE' });
|
||||||
showToast('Device removed from team', 'success');
|
showToast(t('team.toast.device_removed'), 'success');
|
||||||
renderTeamDetail(container, teamId);
|
renderTeamDetail(container, teamId);
|
||||||
} catch (err) { showToast(err.message, 'error'); }
|
} catch (err) { showToast(err.message, 'error'); }
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete team
|
|
||||||
document.getElementById('deleteTeamBtn').onclick = async () => {
|
document.getElementById('deleteTeamBtn').onclick = async () => {
|
||||||
try {
|
try {
|
||||||
await API(`/teams/${teamId}`, { method: 'DELETE' });
|
await API(`/teams/${teamId}`, { method: 'DELETE' });
|
||||||
showToast('Team deleted', 'success');
|
showToast(t('team.toast.deleted'), 'success');
|
||||||
window.location.hash = '#/teams';
|
window.location.hash = '#/teams';
|
||||||
} catch (err) { showToast(err.message, 'error'); }
|
} catch (err) { showToast(err.message, 'error'); }
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue