yslootahrobotics/src/components/ConfigPanel.tsx
Najjar\NajjarV02 05b540997e
Some checks are pending
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
feat: add admin authentication and management features
- Implemented Prisma schema with models for AdminUser, AppSettings, and Snapshot.
- Created seed script to initialize the database with an admin user and JWT secret.
- Developed admin login page with form handling and error management.
- Added API routes for admin login, logout, change password, and JWT verification.
- Integrated Stripe for payment intent management in admin orders.
- Established middleware for protecting admin routes with JWT authentication.
- Created Zustand stores for managing persona and snapshot states.
2026-04-13 17:57:59 +04:00

208 lines
6.9 KiB
TypeScript

'use client';
import { useCallback, useEffect, useRef } from 'react';
import { configStore, useConfigStore } from '@/store/useConfigStore';
import { personaStore, usePersonaStore } from '@/store/usePersonaStore';
import { PricingEngine } from './PricingEngine';
export function ConfigPanel() {
const activeColors = useConfigStore((s) => s.activeColors);
const activePersona = useConfigStore((s) => s.activePersonaAttire);
const personas = usePersonaStore((s) => s.personas);
const colorsSectionRef = useRef<HTMLElement>(null);
const personaSectionRef = useRef<HTMLElement>(null);
useEffect(() => {
personaStore.getState().hydrate();
}, []);
const handleColorChange = useCallback((key: 'primary' | 'secondary' | 'accent', value: string) => {
configStore.getState().setColors({ [key]: value });
}, []);
const handlePersonaSelect = useCallback((attire: string) => {
configStore.getState().setPersonaAttire(attire);
}, []);
const handleReset = useCallback(() => {
configStore.getState().reset();
configStore.getState().setHydrated(true);
}, []);
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
{/* --- COLORS SECTION --- */}
<section
ref={colorsSectionRef}
id="section-colors"
style={{
borderRadius: '0.5rem',
padding: '0.75rem',
}}
>
<h3 style={sectionTitleStyle}>Colors</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<ColorInput label="Primary" value={activeColors.primary} onChange={(v) => handleColorChange('primary', v)} />
</div>
</section>
{/* --- PERSONA SECTION --- */}
<section
ref={personaSectionRef}
id="section-persona"
style={{
borderRadius: '0.5rem',
padding: '0.75rem',
}}
>
<h3 style={sectionTitleStyle}>Persona Attire</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
{personas.map((persona) => {
const isActive = activePersona === persona.id;
return (
<button
key={persona.id}
onClick={() => handlePersonaSelect(persona.id)}
style={{
display: 'flex',
alignItems: 'center',
gap: '0.75rem',
padding: '0.6rem 0.75rem',
borderRadius: '0.5rem',
border: isActive
? '1px solid rgba(59, 130, 246, 0.5)'
: '1px solid rgba(0, 0, 0, 0.06)',
background: isActive
? 'rgba(59, 130, 246, 0.06)'
: 'rgba(248, 248, 246, 0.4)',
cursor: 'pointer',
transition: 'all 0.25s ease',
textAlign: 'left',
width: '100%',
}}
aria-pressed={isActive}
>
{/* Color preview swatch */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px', flexShrink: 0 }}>
<div style={{
width: '36px',
height: '12px',
borderRadius: '3px 3px 0 0',
backgroundColor: persona.colors.torso,
border: '1px solid rgba(255,255,255,0.1)',
borderBottom: 'none',
}} />
<div style={{
width: '36px',
height: '12px',
borderRadius: '0 0 3px 3px',
backgroundColor: persona.colors.legs,
border: '1px solid rgba(255,255,255,0.1)',
borderTop: 'none',
}} />
</div>
{/* Text */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
fontSize: '0.8rem',
fontWeight: isActive ? 600 : 400,
color: isActive ? '#374151' : '#94a3b8',
marginBottom: '2px',
}}>
{persona.label}
</div>
<div style={{
fontSize: '0.65rem',
color: '#64748b',
lineHeight: 1.3,
}}>
{persona.description}
</div>
</div>
{/* Active checkmark */}
{isActive && (
<div style={{
width: '20px',
height: '20px',
borderRadius: '50%',
background: 'rgba(59, 130, 246, 0.2)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
}}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#2563eb" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12" />
</svg>
</div>
)}
</button>
);
})}
</div>
</section>
{/* --- PRICING --- */}
<PricingEngine />
{/* --- RESET --- */}
<button
onClick={handleReset}
style={{
padding: '0.6rem',
borderRadius: '0.375rem',
border: '1px solid rgba(239, 68, 68, 0.2)',
background: 'rgba(239, 68, 68, 0.05)',
color: '#ef4444',
cursor: 'pointer',
fontSize: '0.8rem',
transition: 'all 0.2s ease',
}}
>
Reset Configuration
</button>
</div>
);
}
function ColorInput({ label, value, onChange }: { label: string; value: string; onChange: (v: string) => void }) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
<input
type="color"
value={value}
onChange={(e) => onChange(e.target.value)}
style={{
width: '32px',
height: '32px',
border: '2px solid rgba(0, 0, 0, 0.08)',
borderRadius: '0.375rem',
background: 'transparent',
cursor: 'pointer',
padding: 0,
}}
aria-label={`${label} color`}
/>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '0.8rem', color: '#374151', marginBottom: '2px' }}>{label}</div>
<div style={{ fontSize: '0.7rem', color: '#64748b', fontFamily: 'monospace' }}>{value}</div>
</div>
</div>
);
}
const sectionTitleStyle: React.CSSProperties = {
fontSize: '0.75rem',
fontWeight: 500,
color: '#94a3b8',
textTransform: 'uppercase',
letterSpacing: '0.05em',
margin: 0,
paddingBottom: '0.5rem',
borderBottom: '1px solid rgba(0, 0, 0, 0.06)',
marginBottom: '0.75rem',
};