OverlayBrandingControls.tsx 4.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. import { useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { api } from '../api/client';
  4. import { useAuth } from '../contexts/AuthContext';
  5. import { useToast } from '../contexts/ToastContext';
  6. import { useOverlayLogo } from '../hooks/useOverlayLogo';
  7. import { isOverlayColour, type OverlayBranding } from '../utils/overlayBranding';
  8. export function OverlayBrandingControls({ value, onChange, onBusyChange }: {
  9. value: OverlayBranding;
  10. onChange: (value: OverlayBranding) => void;
  11. onBusyChange?: (busy: boolean) => void;
  12. }) {
  13. const { t } = useTranslation();
  14. const { showToast } = useToast();
  15. const { authEnabled, hasPermission } = useAuth();
  16. const canEdit = !authEnabled || hasPermission('settings:update');
  17. const [busy, setBusy] = useState(false);
  18. const [draft, setDraft] = useState({ from: value.from || '#00ae42', to: value.to || '#00ae42' });
  19. const logo = useOverlayLogo(true, null, value.logoRevision);
  20. const saveLogo = async (file: File | null) => {
  21. setBusy(true);
  22. onBusyChange?.(true);
  23. try {
  24. if (file) await api.uploadOverlayLogo(file);
  25. else await api.deleteOverlayLogo();
  26. onChange({ ...value, logo: file !== null, logoRevision: value.logoRevision + 1 });
  27. } catch (error) {
  28. showToast(error instanceof Error ? error.message : t('streamOverlay.branding.failed'), 'error');
  29. } finally {
  30. setBusy(false);
  31. onBusyChange?.(false);
  32. }
  33. };
  34. const setColour = (key: 'from' | 'to', colour: string) => {
  35. setDraft((current) => ({ ...current, [key]: colour }));
  36. if (isOverlayColour(colour)) {
  37. onChange({ ...value, from: value.from || '#00ae42', to: value.to || '#00ae42', [key]: colour });
  38. }
  39. };
  40. return <fieldset disabled={busy} className="my-4 space-y-3 rounded-lg border border-bambu-dark-tertiary p-4">
  41. <legend className="px-2 text-sm font-medium text-white">{t('streamOverlay.branding.title')}</legend>
  42. <p className="text-xs text-bambu-gray">{t('streamOverlay.branding.hint')}</p>
  43. <label className="block text-sm text-bambu-gray">
  44. {t('streamOverlay.branding.upload')}
  45. <input type="file" accept="image/png,image/webp" disabled={!canEdit}
  46. className="mt-1 block w-full text-sm"
  47. onChange={(event) => {
  48. const file = event.target.files?.[0];
  49. event.target.value = '';
  50. if (file) void saveLogo(file);
  51. }} />
  52. </label>
  53. {logo && <div className="flex flex-wrap items-center gap-3">
  54. <img src={logo} alt={t('streamOverlay.branding.logo')} className="h-16 max-w-40 object-contain" />
  55. <label className="flex items-center gap-2 text-sm text-bambu-gray">
  56. <input type="checkbox" checked={value.logo} onChange={(event) => onChange({ ...value, logo: event.target.checked })} />
  57. {t('streamOverlay.branding.logo')}
  58. </label>
  59. <button type="button" disabled={!canEdit} onClick={() => void saveLogo(null)} className="text-sm text-red-400 disabled:opacity-50">{t('common.remove')}</button>
  60. </div>}
  61. <div className="grid gap-3 sm:grid-cols-2">
  62. {(['from', 'to'] as const).map((key) => <div key={key}>
  63. <label htmlFor={`overlay-colour-${key}`} className="block text-sm text-bambu-gray mb-1">{t(`streamOverlay.branding.${key}`)}</label>
  64. <div className="flex items-center gap-2">
  65. <input id={`overlay-colour-${key}`} type="color" value={value[key] || '#00ae42'} onChange={(event) => setColour(key, event.target.value)} className="h-9 w-12 bg-transparent" />
  66. <input aria-label={`${t(`streamOverlay.branding.${key}`)} (hex)`} value={draft[key]} onChange={(event) => setColour(key, event.target.value)}
  67. aria-invalid={!isOverlayColour(draft[key])} maxLength={7} spellCheck={false}
  68. className="w-28 rounded border border-bambu-dark-tertiary bg-bambu-dark px-2 py-1 text-white" />
  69. </div>
  70. </div>)}
  71. </div>
  72. <button type="button" className="text-sm text-bambu-green" onClick={() => {
  73. setDraft({ from: '#00ae42', to: '#00ae42' });
  74. onChange({ ...value, from: '', to: '' });
  75. }}>{t('streamOverlay.branding.reset')}</button>
  76. </fieldset>;
  77. }