learning_ai_common_plat/dashboards/admin-web/src/components/ThemeEditor.tsx
saravanakumardb1 2d54795c30 feat(dashboards): migrate admin + tracker dashboards to common-plat as product-agnostic
- Copy admin-dashboard-web → dashboards/admin-web
- Copy tracker-dashboard-web → dashboards/tracker-web
- Update pnpm-workspace.yaml to include dashboards/*
- Replace file: refs with workspace:* for @bytelyst/* packages
- Replace all hardcoded LysnrAI/lysnn.com branding with generic platform refs
- Make telemetry use NEXT_PUBLIC_PRODUCT_ID / PRODUCT_ID env vars
- Update mock credentials, seed data, invitation codes, placeholders
- Update READMEs, e2e tests, unit tests for product-agnostic content
- Both dashboards pass tsc --noEmit clean
2026-02-28 02:17:35 -08:00

220 lines
6.7 KiB
TypeScript

'use client';
import { useState } from 'react';
import { Theme, PlatformTheme } from '@/types/theme';
function ColorInput({
label,
value,
onChange,
}: {
label: string;
value: string;
onChange: (value: string) => void;
}) {
return (
<div className="flex items-center gap-2">
<label className="text-sm font-medium w-32">{label}</label>
<input
type="color"
value={value}
onChange={e => onChange(e.target.value)}
className="w-12 h-8 border rounded cursor-pointer"
/>
<input
type="text"
value={value}
onChange={e => onChange(e.target.value)}
className="flex-1 px-2 py-1 border rounded text-sm font-mono"
placeholder="#000000"
/>
</div>
);
}
function PlatformSection({
title,
colors,
onChange,
}: {
title: string;
colors: PlatformTheme;
onChange: (key: keyof PlatformTheme, value: string) => void;
}) {
return (
<div className="bg-white rounded-lg border p-4">
<h3 className="text-lg font-semibold mb-4">{title}</h3>
<div className="space-y-2">
<ColorInput label="Primary" value={colors.primary} onChange={v => onChange('primary', v)} />
<ColorInput
label="Secondary"
value={colors.secondary}
onChange={v => onChange('secondary', v)}
/>
<ColorInput label="Accent" value={colors.accent} onChange={v => onChange('accent', v)} />
<ColorInput
label="Background"
value={colors.background}
onChange={v => onChange('background', v)}
/>
<ColorInput label="Surface" value={colors.surface} onChange={v => onChange('surface', v)} />
<ColorInput label="Error" value={colors.error} onChange={v => onChange('error', v)} />
<ColorInput label="Warning" value={colors.warning} onChange={v => onChange('warning', v)} />
<ColorInput label="Success" value={colors.success} onChange={v => onChange('success', v)} />
{title === 'Desktop' && (
<>
<ColorInput
label="Idle"
value={colors.idle || '#4caf50'}
onChange={v => onChange('idle', v)}
/>
<ColorInput
label="Listening"
value={colors.listening || '#e94560'}
onChange={v => onChange('listening', v)}
/>
<ColorInput
label="Processing"
value={colors.processing || '#f5a623'}
onChange={v => onChange('processing', v)}
/>
<ColorInput
label="Offline"
value={colors.offline || '#9e9e9e'}
onChange={v => onChange('offline', v)}
/>
</>
)}
</div>
</div>
);
}
interface ThemeEditorProps {
theme?: Theme;
onSave: (theme: Partial<Theme>) => void;
onCancel: () => void;
}
export default function ThemeEditor({ theme, onSave, onCancel }: ThemeEditorProps) {
const [name, setName] = useState(theme?.name || '');
const [description, setDescription] = useState(theme?.description || '');
const [iosColors, setIosColors] = useState<PlatformTheme>(
theme?.ios || {
primary: '#4caf50',
secondary: '#2e7d32',
accent: '#66bb6a',
background: '#ffffff',
surface: '#f5f5f5',
error: '#f44336',
warning: '#ff9800',
success: '#4caf50',
}
);
const [androidColors, setAndroidColors] = useState<PlatformTheme>(theme?.android || iosColors);
const [desktopColors, setDesktopColors] = useState<PlatformTheme>(
theme?.desktop || {
...iosColors,
idle: '#4caf50',
listening: '#e94560',
processing: '#f5a623',
offline: '#9e9e9e',
}
);
const handleColorChange = (
platform: 'ios' | 'android' | 'desktop',
colorKey: keyof PlatformTheme,
value: string
) => {
// Validate hex color format
const hexPattern = /^#[0-9A-Fa-f]{6}$/;
if (value && !hexPattern.test(value)) {
return; // Don't update if invalid
}
const setter =
platform === 'ios'
? setIosColors
: platform === 'android'
? setAndroidColors
: setDesktopColors;
setter(prev => ({ ...prev, [colorKey]: value }));
};
const handleSave = () => {
onSave({
name,
description,
ios: iosColors,
android: androidColors,
desktop: desktopColors,
});
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto m-4">
<div className="p-6">
<h2 className="text-2xl font-bold mb-6">{theme ? 'Edit Theme' : 'Create New Theme'}</h2>
<div className="space-y-4 mb-6">
<div>
<label className="block text-sm font-medium mb-1">Theme Name</label>
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
className="w-full px-3 py-2 border rounded-lg"
placeholder="Enter theme name"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Description</label>
<textarea
value={description}
onChange={e => setDescription(e.target.value)}
className="w-full px-3 py-2 border rounded-lg"
rows={2}
placeholder="Enter theme description (optional)"
/>
</div>
</div>
<div className="space-y-4 mb-6">
<PlatformSection
title="iOS"
colors={iosColors}
onChange={(key, value) => handleColorChange('ios', key, value)}
/>
<PlatformSection
title="Android"
colors={androidColors}
onChange={(key, value) => handleColorChange('android', key, value)}
/>
<PlatformSection
title="Desktop"
colors={desktopColors}
onChange={(key, value) => handleColorChange('desktop', key, value)}
/>
</div>
<div className="flex justify-end gap-2">
<button onClick={onCancel} className="px-4 py-2 text-gray-600 hover:text-gray-800">
Cancel
</button>
<button
onClick={handleSave}
disabled={!name}
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
>
{theme ? 'Update' : 'Create'}
</button>
</div>
</div>
</div>
</div>
);
}