Spaces:
Running
Running
| <!DOCTYPE html> | |
| <html lang="pt-BR"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Assistente de Bolso - Onboarding</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <script src="https://unpkg.com/react@18/umd/react.development.js"></script> | |
| <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script> | |
| <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> | |
| <style> | |
| :root { | |
| --bg-cream: #fefdf3; | |
| --green-primary: #0e3628; | |
| --green-light: #1a5a42; | |
| --green-accent: #22c55e; | |
| --white: #ffffff; | |
| --black: #111111; | |
| --gray-100: #f5f5f4; | |
| --gray-200: #e7e5e4; | |
| --gray-300: #d6d3d1; | |
| --gray-400: #a8a29e; | |
| --gray-500: #78716c; | |
| --gray-600: #57534e; | |
| --gray-700: #44403c; | |
| --error: #dc2626; | |
| --font-serif: 'DM Serif Display', Georgia, serif; | |
| --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: var(--font-sans); | |
| background-color: var(--bg-cream); | |
| color: var(--black); | |
| min-height: 100vh; | |
| line-height: 1.6; | |
| } | |
| #root { | |
| min-height: 100vh; | |
| } | |
| ::selection { | |
| background-color: var(--green-primary); | |
| color: var(--white); | |
| } | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: var(--gray-100); | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--gray-400); | |
| border-radius: 3px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: var(--gray-500); | |
| } | |
| input::placeholder, textarea::placeholder { | |
| color: var(--gray-400); | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| @keyframes slideIn { | |
| from { opacity: 0; transform: translateX(-20px); } | |
| to { opacity: 1; transform: translateX(0); } | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { transform: scale(1); } | |
| 50% { transform: scale(1.02); } | |
| } | |
| .animate-fade-in { | |
| animation: fadeIn 0.4s ease-out forwards; | |
| } | |
| .animate-slide-in { | |
| animation: slideIn 0.3s ease-out forwards; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="root"></div> | |
| <script type="text/babel"> | |
| const { useState, useEffect } = React; | |
| // ===== COMPONENTS ===== | |
| const Logo = ({ size = 'default' }) => { | |
| const sizes = { | |
| small: { width: 28, height: 28 }, | |
| default: { width: 36, height: 36 }, | |
| large: { width: 56, height: 56 } | |
| }; | |
| const s = sizes[size]; | |
| // Logo do Assistente de Bolso - trevo de 4 folhas (formato correto) | |
| return ( | |
| <svg width={s.width} height={s.height} viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
| {/* Trevo de 4 folhas - cada pétala é uma forma arredondada */} | |
| <path d="M50 5 C70 5, 85 20, 85 40 C85 50, 75 55, 50 50 C25 55, 15 50, 15 40 C15 20, 30 5, 50 5Z" fill="#22c55e"/> | |
| <path d="M95 50 C95 70, 80 85, 60 85 C50 85, 45 75, 50 50 C45 25, 50 15, 60 15 C80 15, 95 30, 95 50Z" fill="#22c55e"/> | |
| <path d="M50 95 C30 95, 15 80, 15 60 C15 50, 25 45, 50 50 C75 45, 85 50, 85 60 C85 80, 70 95, 50 95Z" fill="#22c55e"/> | |
| <path d="M5 50 C5 30, 20 15, 40 15 C50 15, 55 25, 50 50 C55 75, 50 85, 40 85 C20 85, 5 70, 5 50Z" fill="#22c55e"/> | |
| </svg> | |
| ); | |
| }; | |
| const Header = ({ currentStep, totalSteps, onBack, showBack = true }) => ( | |
| <header style={{ | |
| position: 'sticky', | |
| top: 0, | |
| background: 'var(--bg-cream)', | |
| borderBottom: '1px solid var(--gray-200)', | |
| zIndex: 100, | |
| padding: '16px 24px', | |
| }}> | |
| <div style={{ | |
| maxWidth: '600px', | |
| margin: '0 auto', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'space-between', | |
| }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}> | |
| {showBack && ( | |
| <button | |
| onClick={onBack} | |
| style={{ | |
| background: 'transparent', | |
| border: 'none', | |
| cursor: 'pointer', | |
| padding: '8px', | |
| margin: '-8px', | |
| display: 'flex', | |
| alignItems: 'center', | |
| color: 'var(--green-primary)', | |
| transition: 'opacity 0.2s', | |
| }} | |
| onMouseOver={(e) => e.currentTarget.style.opacity = '0.7'} | |
| onMouseOut={(e) => e.currentTarget.style.opacity = '1'} | |
| > | |
| <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"> | |
| <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/> | |
| </svg> | |
| </button> | |
| )} | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> | |
| <Logo size="small" /> | |
| <span style={{ | |
| fontFamily: 'var(--font-serif)', | |
| fontSize: '18px', | |
| color: 'var(--green-primary)', | |
| fontWeight: 400, | |
| }}> | |
| Assistente de Bolso | |
| </span> | |
| </div> | |
| </div> | |
| <span style={{ | |
| fontSize: '14px', | |
| color: 'var(--gray-500)', | |
| fontWeight: 500, | |
| }}> | |
| {currentStep}/{totalSteps} | |
| </span> | |
| </div> | |
| </header> | |
| ); | |
| const ProgressBar = ({ current, total }) => ( | |
| <div style={{ | |
| maxWidth: '600px', | |
| margin: '0 auto', | |
| padding: '0 24px 24px', | |
| }}> | |
| <div style={{ | |
| height: '4px', | |
| background: 'var(--gray-200)', | |
| borderRadius: '2px', | |
| overflow: 'hidden', | |
| }}> | |
| <div style={{ | |
| height: '100%', | |
| width: `${(current / total) * 100}%`, | |
| background: 'var(--green-primary)', | |
| borderRadius: '2px', | |
| transition: 'width 0.4s ease', | |
| }} /> | |
| </div> | |
| </div> | |
| ); | |
| const Title = ({ children }) => ( | |
| <h1 style={{ | |
| fontFamily: 'var(--font-serif)', | |
| fontSize: 'clamp(24px, 5vw, 32px)', | |
| fontWeight: 400, | |
| color: 'var(--green-primary)', | |
| marginBottom: '12px', | |
| lineHeight: 1.2, | |
| }}> | |
| {children} | |
| </h1> | |
| ); | |
| const Subtitle = ({ children, style = {} }) => ( | |
| <p style={{ | |
| fontSize: '16px', | |
| color: 'var(--gray-600)', | |
| marginBottom: '32px', | |
| lineHeight: 1.6, | |
| ...style, | |
| }}> | |
| {children} | |
| </p> | |
| ); | |
| const TextInput = ({ label, value, onChange, placeholder, error, hint, type = 'text', required = true }) => ( | |
| <div style={{ marginBottom: '24px' }} className="animate-fade-in"> | |
| <label style={{ | |
| display: 'block', | |
| fontSize: '14px', | |
| fontWeight: 600, | |
| color: 'var(--black)', | |
| marginBottom: '8px', | |
| }}> | |
| {label} {required && <span style={{ color: 'var(--error)' }}>*</span>} | |
| </label> | |
| <input | |
| type={type} | |
| value={value} | |
| onChange={(e) => onChange(e.target.value)} | |
| placeholder={placeholder} | |
| style={{ | |
| width: '100%', | |
| padding: '16px 20px', | |
| fontSize: '16px', | |
| border: `2px solid ${error ? 'var(--error)' : 'var(--gray-200)'}`, | |
| borderRadius: '12px', | |
| background: 'var(--white)', | |
| color: 'var(--black)', | |
| outline: 'none', | |
| transition: 'border-color 0.2s, box-shadow 0.2s', | |
| }} | |
| onFocus={(e) => { | |
| e.target.style.borderColor = 'var(--green-primary)'; | |
| e.target.style.boxShadow = '0 0 0 4px rgba(14, 54, 40, 0.1)'; | |
| }} | |
| onBlur={(e) => { | |
| e.target.style.borderColor = error ? 'var(--error)' : 'var(--gray-200)'; | |
| e.target.style.boxShadow = 'none'; | |
| }} | |
| /> | |
| {error && <p style={{ color: 'var(--error)', fontSize: '13px', marginTop: '6px' }}>{error}</p>} | |
| {hint && !error && <p style={{ color: 'var(--gray-500)', fontSize: '13px', marginTop: '6px' }}>{hint}</p>} | |
| </div> | |
| ); | |
| const TextArea = ({ label, value, onChange, placeholder, error, rows = 3, required = true }) => ( | |
| <div style={{ marginBottom: '24px' }} className="animate-fade-in"> | |
| <label style={{ | |
| display: 'block', | |
| fontSize: '14px', | |
| fontWeight: 600, | |
| color: 'var(--black)', | |
| marginBottom: '8px', | |
| }}> | |
| {label} {required && <span style={{ color: 'var(--error)' }}>*</span>} | |
| </label> | |
| <textarea | |
| value={value} | |
| onChange={(e) => onChange(e.target.value)} | |
| placeholder={placeholder} | |
| rows={rows} | |
| style={{ | |
| width: '100%', | |
| padding: '16px 20px', | |
| fontSize: '16px', | |
| border: `2px solid ${error ? 'var(--error)' : 'var(--gray-200)'}`, | |
| borderRadius: '12px', | |
| background: 'var(--white)', | |
| color: 'var(--black)', | |
| outline: 'none', | |
| resize: 'none', | |
| fontFamily: 'inherit', | |
| transition: 'border-color 0.2s, box-shadow 0.2s', | |
| }} | |
| onFocus={(e) => { | |
| e.target.style.borderColor = 'var(--green-primary)'; | |
| e.target.style.boxShadow = '0 0 0 4px rgba(14, 54, 40, 0.1)'; | |
| }} | |
| onBlur={(e) => { | |
| e.target.style.borderColor = error ? 'var(--error)' : 'var(--gray-200)'; | |
| e.target.style.boxShadow = 'none'; | |
| }} | |
| /> | |
| {error && <p style={{ color: 'var(--error)', fontSize: '13px', marginTop: '6px' }}>{error}</p>} | |
| </div> | |
| ); | |
| const RadioGroup = ({ label, options, selected, onChange, error }) => ( | |
| <div style={{ marginBottom: '24px' }} className="animate-fade-in"> | |
| <label style={{ | |
| display: 'block', | |
| fontSize: '14px', | |
| fontWeight: 600, | |
| color: 'var(--black)', | |
| marginBottom: '12px', | |
| }}> | |
| {label} <span style={{ color: 'var(--error)' }}>*</span> | |
| </label> | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}> | |
| {options.map((opt, idx) => ( | |
| <button | |
| key={opt.value} | |
| type="button" | |
| onClick={() => onChange(opt.value)} | |
| className="animate-slide-in" | |
| style={{ | |
| animationDelay: `${idx * 50}ms`, | |
| padding: '16px 20px', | |
| background: selected === opt.value ? 'var(--green-primary)' : 'var(--white)', | |
| border: `2px solid ${selected === opt.value ? 'var(--green-primary)' : 'var(--gray-200)'}`, | |
| borderRadius: '12px', | |
| color: selected === opt.value ? 'var(--white)' : 'var(--black)', | |
| fontSize: '15px', | |
| textAlign: 'left', | |
| cursor: 'pointer', | |
| display: 'flex', | |
| alignItems: 'center', | |
| gap: '14px', | |
| transition: 'all 0.2s ease', | |
| }} | |
| onMouseOver={(e) => { | |
| if (selected !== opt.value) { | |
| e.currentTarget.style.borderColor = 'var(--green-primary)'; | |
| e.currentTarget.style.background = 'rgba(14, 54, 40, 0.03)'; | |
| } | |
| }} | |
| onMouseOut={(e) => { | |
| if (selected !== opt.value) { | |
| e.currentTarget.style.borderColor = 'var(--gray-200)'; | |
| e.currentTarget.style.background = 'var(--white)'; | |
| } | |
| }} | |
| > | |
| <div style={{ | |
| width: '22px', | |
| height: '22px', | |
| borderRadius: '50%', | |
| border: `2px solid ${selected === opt.value ? 'var(--white)' : 'var(--gray-300)'}`, | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| flexShrink: 0, | |
| transition: 'all 0.2s', | |
| }}> | |
| {selected === opt.value && ( | |
| <div style={{ | |
| width: '10px', | |
| height: '10px', | |
| borderRadius: '50%', | |
| background: 'var(--white)', | |
| }} /> | |
| )} | |
| </div> | |
| {opt.label} | |
| </button> | |
| ))} | |
| </div> | |
| {error && <p style={{ color: 'var(--error)', fontSize: '13px', marginTop: '8px' }}>{error}</p>} | |
| </div> | |
| ); | |
| const CheckboxGroup = ({ label, options, selected, onChange, error }) => ( | |
| <div style={{ marginBottom: '24px' }} className="animate-fade-in"> | |
| <label style={{ | |
| display: 'block', | |
| fontSize: '14px', | |
| fontWeight: 600, | |
| color: 'var(--black)', | |
| marginBottom: '12px', | |
| }}> | |
| {label} <span style={{ color: 'var(--error)' }}>*</span> | |
| </label> | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}> | |
| {options.map((opt, idx) => ( | |
| <button | |
| key={opt.value} | |
| type="button" | |
| onClick={() => onChange(opt.value)} | |
| className="animate-slide-in" | |
| style={{ | |
| animationDelay: `${idx * 50}ms`, | |
| padding: '16px 20px', | |
| background: selected.includes(opt.value) ? 'var(--green-primary)' : 'var(--white)', | |
| border: `2px solid ${selected.includes(opt.value) ? 'var(--green-primary)' : 'var(--gray-200)'}`, | |
| borderRadius: '12px', | |
| color: selected.includes(opt.value) ? 'var(--white)' : 'var(--black)', | |
| fontSize: '15px', | |
| textAlign: 'left', | |
| cursor: 'pointer', | |
| display: 'flex', | |
| alignItems: 'center', | |
| gap: '14px', | |
| transition: 'all 0.2s ease', | |
| }} | |
| onMouseOver={(e) => { | |
| if (!selected.includes(opt.value)) { | |
| e.currentTarget.style.borderColor = 'var(--green-primary)'; | |
| e.currentTarget.style.background = 'rgba(14, 54, 40, 0.03)'; | |
| } | |
| }} | |
| onMouseOut={(e) => { | |
| if (!selected.includes(opt.value)) { | |
| e.currentTarget.style.borderColor = 'var(--gray-200)'; | |
| e.currentTarget.style.background = 'var(--white)'; | |
| } | |
| }} | |
| > | |
| <div style={{ | |
| width: '22px', | |
| height: '22px', | |
| borderRadius: '6px', | |
| border: `2px solid ${selected.includes(opt.value) ? 'var(--white)' : 'var(--gray-300)'}`, | |
| background: selected.includes(opt.value) ? 'transparent' : 'transparent', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| flexShrink: 0, | |
| transition: 'all 0.2s', | |
| }}> | |
| {selected.includes(opt.value) && ( | |
| <svg width="14" height="12" viewBox="0 0 14 12" fill="none"> | |
| <path d="M1 6L5 10L13 2" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/> | |
| </svg> | |
| )} | |
| </div> | |
| {opt.label} | |
| </button> | |
| ))} | |
| </div> | |
| {error && <p style={{ color: 'var(--error)', fontSize: '13px', marginTop: '8px' }}>{error}</p>} | |
| </div> | |
| ); | |
| const Dropdown = ({ label, options, selected, onChange, placeholder, error, required = true, id }) => ( | |
| <div style={{ marginBottom: '24px' }} className="animate-fade-in" id={id}> | |
| {label && ( | |
| <label style={{ | |
| display: 'block', | |
| fontSize: '14px', | |
| fontWeight: 600, | |
| color: 'var(--black)', | |
| marginBottom: '8px', | |
| }}> | |
| {label} {required && <span style={{ color: 'var(--error)' }}>*</span>} | |
| </label> | |
| )} | |
| <select | |
| value={selected} | |
| onChange={(e) => onChange(e.target.value)} | |
| style={{ | |
| width: '100%', | |
| padding: '16px 20px', | |
| fontSize: '16px', | |
| border: `2px solid ${error ? 'var(--error)' : 'var(--gray-200)'}`, | |
| borderRadius: '12px', | |
| background: 'var(--white)', | |
| color: selected ? 'var(--black)' : 'var(--gray-400)', | |
| outline: 'none', | |
| cursor: 'pointer', | |
| appearance: 'none', | |
| backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpath fill='%2378716c' d='M8 11L3 6h10z'/%3E%3C/svg%3E")`, | |
| backgroundRepeat: 'no-repeat', | |
| backgroundPosition: 'right 16px center', | |
| }} | |
| > | |
| <option value="" disabled>{placeholder}</option> | |
| {options.map((opt) => ( | |
| <option key={opt.value} value={opt.value}>{opt.label}</option> | |
| ))} | |
| </select> | |
| {error && <p style={{ color: 'var(--error)', fontSize: '13px', marginTop: '6px' }}>{error}</p>} | |
| </div> | |
| ); | |
| const Checkbox = ({ checked, onChange, label, error }) => ( | |
| <div style={{ marginBottom: '20px' }} className="animate-fade-in"> | |
| <button | |
| type="button" | |
| onClick={() => onChange(!checked)} | |
| style={{ | |
| display: 'flex', | |
| alignItems: 'flex-start', | |
| gap: '14px', | |
| background: 'transparent', | |
| border: 'none', | |
| cursor: 'pointer', | |
| textAlign: 'left', | |
| padding: 0, | |
| }} | |
| > | |
| <div style={{ | |
| width: '24px', | |
| height: '24px', | |
| borderRadius: '6px', | |
| border: `2px solid ${checked ? 'var(--green-primary)' : error ? 'var(--error)' : 'var(--gray-300)'}`, | |
| background: checked ? 'var(--green-primary)' : 'transparent', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| flexShrink: 0, | |
| marginTop: '2px', | |
| transition: 'all 0.2s', | |
| }}> | |
| {checked && ( | |
| <svg width="14" height="12" viewBox="0 0 14 12" fill="none"> | |
| <path d="M1 6L5 10L13 2" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/> | |
| </svg> | |
| )} | |
| </div> | |
| <span style={{ color: 'var(--gray-600)', fontSize: '14px', lineHeight: 1.5 }}>{label}</span> | |
| </button> | |
| {error && <p style={{ color: 'var(--error)', fontSize: '13px', marginTop: '6px', marginLeft: '38px' }}>{error}</p>} | |
| </div> | |
| ); | |
| const InfoBox = ({ children, type = 'info', icon }) => { | |
| const styles = { | |
| info: { bg: 'rgba(14, 54, 40, 0.06)', border: 'var(--green-primary)', iconDefault: 'ℹ️' }, | |
| warning: { bg: '#FEF3C7', border: '#F59E0B', iconDefault: '⚠️' }, | |
| success: { bg: 'rgba(34, 197, 94, 0.1)', border: 'var(--green-accent)', iconDefault: '🔒' }, | |
| }; | |
| const s = styles[type]; | |
| return ( | |
| <div style={{ | |
| background: s.bg, | |
| border: `1px solid ${s.border}`, | |
| borderRadius: '12px', | |
| padding: '16px 18px', | |
| marginBottom: '20px', | |
| display: 'flex', | |
| gap: '12px', | |
| alignItems: 'flex-start', | |
| }} className="animate-fade-in"> | |
| <span style={{ fontSize: '18px', flexShrink: 0 }}>{icon || s.iconDefault}</span> | |
| <p style={{ color: 'var(--black)', fontSize: '14px', lineHeight: 1.6, margin: 0 }}>{children}</p> | |
| </div> | |
| ); | |
| }; | |
| const PrimaryButton = ({ text, onClick, disabled }) => ( | |
| <button | |
| type="button" | |
| onClick={onClick} | |
| disabled={disabled} | |
| style={{ | |
| width: '100%', | |
| padding: '18px 32px', | |
| background: disabled ? 'var(--gray-300)' : 'var(--green-primary)', | |
| border: 'none', | |
| borderRadius: '12px', | |
| color: disabled ? 'var(--gray-500)' : 'var(--white)', | |
| fontSize: '16px', | |
| fontWeight: 600, | |
| cursor: disabled ? 'not-allowed' : 'pointer', | |
| transition: 'all 0.2s ease', | |
| boxShadow: disabled ? 'none' : '0 4px 14px rgba(14, 54, 40, 0.25)', | |
| }} | |
| onMouseOver={(e) => { | |
| if (!disabled) { | |
| e.currentTarget.style.transform = 'translateY(-2px)'; | |
| e.currentTarget.style.boxShadow = '0 6px 20px rgba(14, 54, 40, 0.3)'; | |
| } | |
| }} | |
| onMouseOut={(e) => { | |
| if (!disabled) { | |
| e.currentTarget.style.transform = 'translateY(0)'; | |
| e.currentTarget.style.boxShadow = '0 4px 14px rgba(14, 54, 40, 0.25)'; | |
| } | |
| }} | |
| > | |
| {text} | |
| </button> | |
| ); | |
| const FeatureCard = ({ emoji, title, examples }) => ( | |
| <div style={{ | |
| background: 'var(--white)', | |
| borderRadius: '16px', | |
| padding: '20px', | |
| marginBottom: '16px', | |
| border: '1px solid var(--gray-200)', | |
| boxShadow: '0 2px 8px rgba(0,0,0,0.04)', | |
| }} className="animate-fade-in"> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '14px' }}> | |
| <span style={{ fontSize: '28px' }}>{emoji}</span> | |
| <span style={{ fontWeight: 600, fontSize: '16px', color: 'var(--black)' }}>{title}</span> | |
| </div> | |
| <div style={{ marginLeft: '40px', display: 'flex', flexDirection: 'column', gap: '8px' }}> | |
| {examples.map((ex, i) => ( | |
| <div key={i} style={{ | |
| background: 'var(--gray-100)', | |
| borderRadius: '8px', | |
| padding: '12px 14px', | |
| color: 'var(--gray-700)', | |
| fontSize: '14px', | |
| fontStyle: 'italic', | |
| }}> | |
| "{ex}" | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| const TimelineItem = ({ time, description, isLast }) => ( | |
| <div style={{ display: 'flex', gap: '14px', marginBottom: isLast ? 0 : '20px' }}> | |
| <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}> | |
| <div style={{ | |
| width: '36px', | |
| height: '36px', | |
| borderRadius: '50%', | |
| background: 'var(--green-primary)', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| color: 'var(--white)', | |
| fontSize: '13px', | |
| fontWeight: 600, | |
| }}> | |
| {time} | |
| </div> | |
| {!isLast && <div style={{ width: '2px', height: '28px', background: 'var(--gray-200)', marginTop: '6px' }} />} | |
| </div> | |
| <div style={{ flex: 1, paddingTop: '8px' }}> | |
| <p style={{ color: 'var(--gray-700)', fontSize: '14px', lineHeight: 1.5 }}>{description}</p> | |
| </div> | |
| </div> | |
| ); | |
| const StepItem = ({ number, title, description }) => ( | |
| <div style={{ display: 'flex', gap: '16px', marginBottom: '20px' }} className="animate-slide-in"> | |
| <div style={{ | |
| width: '32px', | |
| height: '32px', | |
| borderRadius: '50%', | |
| background: 'var(--green-primary)', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| color: 'var(--white)', | |
| fontSize: '15px', | |
| fontWeight: 600, | |
| flexShrink: 0, | |
| }}> | |
| {number} | |
| </div> | |
| <div style={{ flex: 1 }}> | |
| <p style={{ fontWeight: 600, fontSize: '15px', color: 'var(--black)', marginBottom: '4px' }}>{title}</p> | |
| <p style={{ color: 'var(--gray-600)', fontSize: '14px', lineHeight: 1.5 }}>{description}</p> | |
| </div> | |
| </div> | |
| ); | |
| const ContentWrapper = ({ children }) => ( | |
| <div style={{ | |
| maxWidth: '600px', | |
| margin: '0 auto', | |
| padding: '32px 24px', | |
| }}> | |
| {children} | |
| </div> | |
| ); | |
| const Footer = ({ children }) => ( | |
| <div style={{ | |
| position: 'sticky', | |
| bottom: 0, | |
| background: 'var(--bg-cream)', | |
| borderTop: '1px solid var(--gray-200)', | |
| padding: '20px 24px', | |
| }}> | |
| <div style={{ maxWidth: '600px', margin: '0 auto' }}> | |
| {children} | |
| </div> | |
| </div> | |
| ); | |
| // ===== MAIN APP ===== | |
| const OnboardingApp = () => { | |
| const [currentScreen, setCurrentScreen] = useState(0); | |
| const [errors, setErrors] = useState({}); | |
| const [isAnimating, setIsAnimating] = useState(false); | |
| const [showWelcome, setShowWelcome] = useState(true); | |
| const [formData, setFormData] = useState({ | |
| nome: '', cpf: '', email: '', aceitaTermos: false, | |
| expectativa: '', tipoControle: [], | |
| atividadeEmpresa: '', faturamentoEmpresa: '', | |
| profissao: '', experienciaApp: '', qualApp: '', comoConheceu: '', | |
| metodoRegistro: '', | |
| gastoMensal: '', ganhoMensal: '', | |
| cartoes: [], cartaoPrincipal: '', outroCartao: '', | |
| temDividas: '', dividas: [{ tipo: '', valor: '', prazo: '' }], | |
| metas: [{ descricao: '', prazo: '' }] | |
| }); | |
| const formatCPF = (value) => { | |
| const n = value.replace(/\D/g, ''); | |
| return n.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4').slice(0, 14); | |
| }; | |
| const formatCurrency = (value) => { | |
| const n = value.replace(/\D/g, ''); | |
| if (!n) return ''; | |
| return 'R$ ' + parseInt(n).toLocaleString('pt-BR'); | |
| }; | |
| const usaRegistroAutomatico = formData.metodoRegistro === 'automatico' || formData.metodoRegistro === 'ambos'; | |
| const controlaEmpresa = formData.tipoControle.includes('empresa'); | |
| const controlaApenasEmpresa = formData.tipoControle.length === 1 && formData.tipoControle.includes('empresa'); | |
| const getScreenSequence = () => { | |
| let screens = ['dados', 'expectativas']; | |
| if (controlaEmpresa) screens.push('empresa'); | |
| if (!controlaApenasEmpresa) screens.push('sobreVoce'); | |
| else screens.push('sobreVoceEmpresa'); | |
| screens.push('metodoRegistro', 'rendaGastos', 'cartoes', 'dividas'); | |
| if (formData.temDividas === 'sim') screens.push('detalhesDividas'); | |
| screens.push('metas', 'tutorialRegistro'); | |
| if (usaRegistroAutomatico) { | |
| screens.push('tutorialAutomatico'); | |
| screens.push('tutorialConexao'); | |
| } | |
| screens.push('tutorialDashboard', 'tutorialLembretes', 'pronto'); | |
| return screens; | |
| }; | |
| const screenSequence = getScreenSequence(); | |
| const currentScreenName = screenSequence[currentScreen] || 'dados'; | |
| const totalSteps = screenSequence.length; | |
| const validateScreen = () => { | |
| const newErrors = {}; | |
| if (currentScreenName === 'dados') { | |
| if (!formData.nome.trim()) newErrors.nome = 'Campo obrigatório'; | |
| if (!formData.cpf.trim()) newErrors.cpf = 'Campo obrigatório'; | |
| else if (formData.cpf.replace(/\D/g, '').length !== 11) newErrors.cpf = 'CPF inválido'; | |
| if (!formData.email.trim()) newErrors.email = 'Campo obrigatório'; | |
| else if (!/\S+@\S+\.\S+/.test(formData.email)) newErrors.email = 'E-mail inválido'; | |
| if (!formData.aceitaTermos) newErrors.aceitaTermos = 'Você precisa aceitar os termos'; | |
| } else if (currentScreenName === 'expectativas') { | |
| if (!formData.expectativa.trim()) newErrors.expectativa = 'Campo obrigatório'; | |
| if (formData.tipoControle.length === 0) newErrors.tipoControle = 'Selecione pelo menos uma opção'; | |
| } else if (currentScreenName === 'empresa') { | |
| if (!formData.atividadeEmpresa.trim()) newErrors.atividadeEmpresa = 'Campo obrigatório'; | |
| if (!formData.faturamentoEmpresa) newErrors.faturamentoEmpresa = 'Selecione uma opção'; | |
| } else if (currentScreenName === 'sobreVoce') { | |
| if (!formData.profissao.trim()) newErrors.profissao = 'Campo obrigatório'; | |
| if (!formData.experienciaApp) newErrors.experienciaApp = 'Selecione uma opção'; | |
| if ((formData.experienciaApp === 'desisti' || formData.experienciaApp === 'uso') && !formData.qualApp.trim()) newErrors.qualApp = 'Campo obrigatório'; | |
| if (!formData.comoConheceu) newErrors.comoConheceu = 'Selecione uma opção'; | |
| } else if (currentScreenName === 'sobreVoceEmpresa') { | |
| if (!formData.experienciaApp) newErrors.experienciaApp = 'Selecione uma opção'; | |
| if ((formData.experienciaApp === 'desisti' || formData.experienciaApp === 'uso') && !formData.qualApp.trim()) newErrors.qualApp = 'Campo obrigatório'; | |
| if (!formData.comoConheceu) newErrors.comoConheceu = 'Selecione uma opção'; | |
| } else if (currentScreenName === 'metodoRegistro') { | |
| if (!formData.metodoRegistro) newErrors.metodoRegistro = 'Selecione uma opção'; | |
| } else if (currentScreenName === 'rendaGastos') { | |
| if (!formData.gastoMensal.trim()) newErrors.gastoMensal = 'Campo obrigatório'; | |
| if (!formData.ganhoMensal.trim()) newErrors.ganhoMensal = 'Campo obrigatório'; | |
| } else if (currentScreenName === 'cartoes') { | |
| if (formData.cartoes.length === 0) newErrors.cartoes = 'Selecione pelo menos uma opção'; | |
| if (formData.cartoes.length > 0 && !formData.cartoes.includes('nenhum') && !formData.cartaoPrincipal) { | |
| newErrors.cartaoPrincipal = 'Selecione seu cartão principal'; | |
| // Scroll automático para o campo de cartão principal | |
| setTimeout(() => { | |
| const cartaoPrincipalElement = document.getElementById('cartao-principal-dropdown'); | |
| if (cartaoPrincipalElement) { | |
| cartaoPrincipalElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| } | |
| }, 100); | |
| } | |
| } else if (currentScreenName === 'dividas') { | |
| if (!formData.temDividas) newErrors.temDividas = 'Selecione uma opção'; | |
| } else if (currentScreenName === 'detalhesDividas') { | |
| formData.dividas.forEach((d, i) => { | |
| if (!d.tipo) newErrors[`divida_tipo_${i}`] = 'Obrigatório'; | |
| if (!d.valor) newErrors[`divida_valor_${i}`] = 'Obrigatório'; | |
| if (!d.prazo) newErrors[`divida_prazo_${i}`] = 'Obrigatório'; | |
| }); | |
| } else if (currentScreenName === 'metas') { | |
| formData.metas.forEach((m, i) => { | |
| if (!m.descricao.trim()) newErrors[`meta_desc_${i}`] = 'Obrigatório'; | |
| }); | |
| } | |
| setErrors(newErrors); | |
| return Object.keys(newErrors).length === 0; | |
| }; | |
| const handleNext = () => { | |
| if (!validateScreen()) return; | |
| setIsAnimating(true); | |
| setTimeout(() => { | |
| if (currentScreen < screenSequence.length - 1) { | |
| setCurrentScreen(prev => prev + 1); | |
| } else { | |
| setCurrentScreen(100); | |
| } | |
| setIsAnimating(false); | |
| setErrors({}); | |
| window.scrollTo(0, 0); | |
| }, 200); | |
| }; | |
| const handleBack = () => { | |
| setIsAnimating(true); | |
| setTimeout(() => { | |
| if (currentScreen === 0) { | |
| setShowWelcome(true); | |
| } else { | |
| setCurrentScreen(prev => prev - 1); | |
| } | |
| setIsAnimating(false); | |
| setErrors({}); | |
| window.scrollTo(0, 0); | |
| }, 200); | |
| }; | |
| const handleStartOnboarding = () => { | |
| setShowWelcome(false); | |
| }; | |
| const handleFinish = () => setCurrentScreen(100); | |
| const updateFormData = (field, value) => { | |
| setFormData(prev => ({ ...prev, [field]: value })); | |
| setErrors(prev => ({ ...prev, [field]: null })); | |
| }; | |
| const toggleArrayField = (field, value) => { | |
| setFormData(prev => { | |
| let newArray; | |
| if (value === 'nenhum') { | |
| newArray = prev[field].includes('nenhum') ? [] : ['nenhum']; | |
| } else { | |
| newArray = prev[field].filter(v => v !== 'nenhum'); | |
| newArray = newArray.includes(value) ? newArray.filter(v => v !== value) : [...newArray, value]; | |
| } | |
| return { ...prev, [field]: newArray }; | |
| }); | |
| setErrors(prev => ({ ...prev, [field]: null })); | |
| }; | |
| const addDivida = () => setFormData(prev => ({ ...prev, dividas: [...prev.dividas, { tipo: '', valor: '', prazo: '' }] })); | |
| const removeDivida = (index) => { if (formData.dividas.length > 1) setFormData(prev => ({ ...prev, dividas: prev.dividas.filter((_, i) => i !== index) })); }; | |
| const updateDivida = (index, field, value) => setFormData(prev => ({ ...prev, dividas: prev.dividas.map((d, i) => i === index ? { ...d, [field]: value } : d) })); | |
| const addMeta = () => setFormData(prev => ({ ...prev, metas: [...prev.metas, { descricao: '', prazo: '' }] })); | |
| const removeMeta = (index) => { if (formData.metas.length > 1) setFormData(prev => ({ ...prev, metas: prev.metas.filter((_, i) => i !== index) })); }; | |
| const updateMeta = (index, field, value) => setFormData(prev => ({ ...prev, metas: prev.metas.map((m, i) => i === index ? { ...m, [field]: value } : m) })); | |
| // Welcome Screen | |
| if (showWelcome) { | |
| return ( | |
| <div style={{ | |
| minHeight: '100vh', | |
| display: 'flex', | |
| flexDirection: 'column', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| padding: '40px 24px', | |
| textAlign: 'center', | |
| }}> | |
| <div className="animate-fade-in" style={{ maxWidth: '480px' }}> | |
| <Logo size="large" /> | |
| <h1 style={{ | |
| fontFamily: 'var(--font-serif)', | |
| fontSize: 'clamp(32px, 6vw, 42px)', | |
| color: 'var(--green-primary)', | |
| marginTop: '32px', | |
| marginBottom: '16px', | |
| fontWeight: 400, | |
| lineHeight: 1.1, | |
| }}> | |
| Bem-vindo ao Assistente de Bolso | |
| </h1> | |
| <p style={{ | |
| fontSize: '18px', | |
| color: 'var(--gray-600)', | |
| marginBottom: '48px', | |
| lineHeight: 1.6, | |
| }}> | |
| A partir de hoje, vou te ajudar a cuidar das suas finanças e compromissos. Vamos começar? | |
| </p> | |
| <PrimaryButton text="Começar cadastro" onClick={handleStartOnboarding} /> | |
| <p style={{ | |
| fontSize: '13px', | |
| color: 'var(--gray-500)', | |
| marginTop: '24px', | |
| }}> | |
| Leva apenas 5 minutos ⏱️ | |
| </p> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // Final Screen | |
| if (currentScreen === 100) { | |
| return ( | |
| <div style={{ | |
| minHeight: '100vh', | |
| display: 'flex', | |
| flexDirection: 'column', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| padding: '40px 24px', | |
| textAlign: 'center', | |
| }}> | |
| <div className="animate-fade-in" style={{ maxWidth: '480px' }}> | |
| <div style={{ | |
| width: '100px', | |
| height: '100px', | |
| borderRadius: '50%', | |
| background: 'rgba(34, 197, 94, 0.15)', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| margin: '0 auto 32px', | |
| }}> | |
| <svg width="50" height="50" viewBox="0 0 24 24" fill="var(--green-accent)"> | |
| <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/> | |
| </svg> | |
| </div> | |
| <h1 style={{ | |
| fontFamily: 'var(--font-serif)', | |
| fontSize: 'clamp(28px, 5vw, 36px)', | |
| color: 'var(--green-primary)', | |
| marginBottom: '16px', | |
| fontWeight: 400, | |
| }}> | |
| Tudo pronto, {formData.nome.split(' ')[0] || 'você'}! 🎉 | |
| </h1> | |
| <p style={{ | |
| fontSize: '16px', | |
| color: 'var(--gray-600)', | |
| marginBottom: '40px', | |
| lineHeight: 1.6, | |
| }}> | |
| Seu cadastro foi concluído com sucesso. A partir de agora, você pode me enviar mensagens pelo WhatsApp. | |
| </p> | |
| <div style={{ | |
| background: 'var(--white)', | |
| borderRadius: '16px', | |
| padding: '24px', | |
| textAlign: 'left', | |
| border: '1px solid var(--gray-200)', | |
| marginBottom: '32px', | |
| }}> | |
| <p style={{ | |
| fontSize: '12px', | |
| color: 'var(--gray-500)', | |
| textTransform: 'uppercase', | |
| fontWeight: 600, | |
| marginBottom: '12px', | |
| }}> | |
| 💡 Dica rápida | |
| </p> | |
| <p style={{ | |
| fontSize: '15px', | |
| color: 'var(--black)', | |
| marginBottom: '16px', | |
| lineHeight: 1.5, | |
| }}> | |
| Experimente registrar sua primeira despesa agora! | |
| </p> | |
| <div style={{ | |
| background: 'var(--gray-100)', | |
| borderRadius: '8px', | |
| padding: '12px 16px', | |
| color: 'var(--gray-700)', | |
| fontSize: '15px', | |
| fontStyle: 'italic', | |
| }}> | |
| "Almoço 35 reais" | |
| </div> | |
| </div> | |
| <PrimaryButton | |
| text="Voltar para o WhatsApp" | |
| onClick={() => { | |
| // Em produção, isso redirecionaria para o WhatsApp | |
| // Por enquanto, reinicia a demonstração | |
| setCurrentScreen(0); | |
| setShowWelcome(true); | |
| setFormData({ | |
| nome: '', cpf: '', email: '', aceitaTermos: false, | |
| expectativa: '', tipoControle: [], | |
| atividadeEmpresa: '', faturamentoEmpresa: '', | |
| profissao: '', experienciaApp: '', qualApp: '', comoConheceu: '', | |
| metodoRegistro: '', | |
| gastoMensal: '', ganhoMensal: '', | |
| cartoes: [], cartaoPrincipal: '', outroCartao: '', | |
| temDividas: '', dividas: [{ tipo: '', valor: '', prazo: '' }], | |
| metas: [{ descricao: '', prazo: '' }] | |
| }); | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // Main Form Screens | |
| return ( | |
| <div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}> | |
| <Header | |
| currentStep={currentScreen + 1} | |
| totalSteps={totalSteps} | |
| onBack={handleBack} | |
| showBack={true} | |
| /> | |
| <ProgressBar current={currentScreen + 1} total={totalSteps} /> | |
| <div style={{ flex: 1, opacity: isAnimating ? 0.5 : 1, transition: 'opacity 0.2s' }}> | |
| <ContentWrapper> | |
| {/* TELA: DADOS */} | |
| {currentScreenName === 'dados' && ( | |
| <> | |
| <Title>Preencha seus dados básicos</Title> | |
| <Subtitle>Precisamos dessas informações para criar seu cadastro.</Subtitle> | |
| <TextInput label="Nome completo" value={formData.nome} onChange={(v) => updateFormData('nome', v)} placeholder="Digite seu nome" error={errors.nome} /> | |
| <TextInput label="CPF" value={formData.cpf} onChange={(v) => updateFormData('cpf', formatCPF(v))} placeholder="000.000.000-00" error={errors.cpf} /> | |
| <TextInput label="E-mail" type="email" value={formData.email} onChange={(v) => updateFormData('email', v)} placeholder="seu@email.com" error={errors.email} hint="Usado para conectar sua agenda Google." /> | |
| <InfoBox type="success">Todas as suas informações são protegidas com <strong>criptografia de ponta a ponta</strong>. Seus dados estão seguros.</InfoBox> | |
| <Checkbox | |
| checked={formData.aceitaTermos} | |
| onChange={(v) => updateFormData('aceitaTermos', v)} | |
| label={<span>Li e aceito os <a href="#" style={{ color: 'var(--green-primary)', textDecoration: 'underline' }}>Termos de Uso</a> e a <a href="#" style={{ color: 'var(--green-primary)', textDecoration: 'underline' }}>Política de Privacidade</a></span>} | |
| error={errors.aceitaTermos} | |
| /> | |
| </> | |
| )} | |
| {/* TELA: EXPECTATIVAS */} | |
| {currentScreenName === 'expectativas' && ( | |
| <> | |
| <Title>O que você espera do Assistente?</Title> | |
| <Subtitle>Conte suas expectativas e o que você quer controlar.</Subtitle> | |
| <TextArea label="Em poucas palavras, o que você espera do Assistente de Bolso?" value={formData.expectativa} onChange={(v) => updateFormData('expectativa', v)} placeholder="Ex: parar de estourar o cartão todo mês, organizar minhas contas..." error={errors.expectativa} /> | |
| <CheckboxGroup label="O que você quer controlar?" options={[{ value: 'pessoal', label: 'Finanças pessoais' }, { value: 'empresa', label: 'Finanças da empresa' }, { value: 'familia', label: 'Finanças da família' }, { value: 'outro', label: 'Outro' }]} selected={formData.tipoControle} onChange={(v) => toggleArrayField('tipoControle', v)} error={errors.tipoControle} /> | |
| </> | |
| )} | |
| {/* TELA: EMPRESA */} | |
| {currentScreenName === 'empresa' && ( | |
| <> | |
| <Title>Conte sobre sua empresa</Title> | |
| <Subtitle>Essas informações me ajudam a personalizar as orientações para o seu negócio.</Subtitle> | |
| <TextArea label="O que sua empresa faz?" value={formData.atividadeEmpresa} onChange={(v) => updateFormData('atividadeEmpresa', v)} placeholder="Ex: Vendo roupas online, tenho uma consultoria de marketing, sou dono de um restaurante..." rows={3} error={errors.atividadeEmpresa} /> | |
| <Dropdown label="Qual o faturamento mensal médio?" options={[{ value: 'ate5k', label: 'Até R$ 5.000' }, { value: '5k-15k', label: 'R$ 5.000 a R$ 15.000' }, { value: '15k-50k', label: 'R$ 15.000 a R$ 50.000' }, { value: '50k-100k', label: 'R$ 50.000 a R$ 100.000' }, { value: '100k-500k', label: 'R$ 100.000 a R$ 500.000' }, { value: '500k+', label: 'Acima de R$ 500.000' }, { value: 'naosei', label: 'Prefiro não informar' }]} selected={formData.faturamentoEmpresa} onChange={(v) => updateFormData('faturamentoEmpresa', v)} placeholder="Selecione o faturamento" error={errors.faturamentoEmpresa} /> | |
| <InfoBox type="info">Essas informações são confidenciais e serão usadas apenas para personalizar sua experiência.</InfoBox> | |
| </> | |
| )} | |
| {/* TELA: SOBRE VOCÊ */} | |
| {currentScreenName === 'sobreVoce' && ( | |
| <> | |
| <Title>Conte um pouco sobre você</Title> | |
| <Subtitle>Queremos te conhecer melhor para personalizar sua experiência.</Subtitle> | |
| <TextInput label="Profissão ou atividade principal" value={formData.profissao} onChange={(v) => updateFormData('profissao', v)} placeholder="Ex: Empresário, Desenvolvedor..." error={errors.profissao} /> | |
| <RadioGroup label="Você já usou algum app de controle financeiro antes?" options={[{ value: 'nunca', label: 'Nunca usei' }, { value: 'desisti', label: 'Já usei, mas não continuei' }, { value: 'uso', label: 'Uso outro atualmente' }]} selected={formData.experienciaApp} onChange={(v) => updateFormData('experienciaApp', v)} error={errors.experienciaApp} /> | |
| {(formData.experienciaApp === 'desisti' || formData.experienciaApp === 'uso') && ( | |
| <TextInput label="Qual app você usou/usa? O que achou?" value={formData.qualApp} onChange={(v) => updateFormData('qualApp', v)} placeholder="Ex: Mobills, achei complicado..." error={errors.qualApp} /> | |
| )} | |
| <Dropdown label="Como você conheceu o Assistente de Bolso?" options={[{ value: 'amigo', label: 'Indicação de amigo' }, { value: 'instagram', label: 'Instagram' }, { value: 'google', label: 'Google' }, { value: 'tiktok', label: 'TikTok' }, { value: 'outro', label: 'Outro' }]} selected={formData.comoConheceu} onChange={(v) => updateFormData('comoConheceu', v)} placeholder="Selecione uma opção" error={errors.comoConheceu} /> | |
| </> | |
| )} | |
| {/* TELA: SOBRE VOCÊ (EMPRESA) */} | |
| {currentScreenName === 'sobreVoceEmpresa' && ( | |
| <> | |
| <Title>Conte um pouco sobre você</Title> | |
| <Subtitle>Queremos te conhecer melhor para personalizar sua experiência.</Subtitle> | |
| <RadioGroup label="Você já usou algum app de controle financeiro antes?" options={[{ value: 'nunca', label: 'Nunca usei' }, { value: 'desisti', label: 'Já usei, mas não continuei' }, { value: 'uso', label: 'Uso outro atualmente' }]} selected={formData.experienciaApp} onChange={(v) => updateFormData('experienciaApp', v)} error={errors.experienciaApp} /> | |
| {(formData.experienciaApp === 'desisti' || formData.experienciaApp === 'uso') && ( | |
| <TextInput label="Qual app você usou/usa? O que achou?" value={formData.qualApp} onChange={(v) => updateFormData('qualApp', v)} placeholder="Ex: Mobills, achei complicado..." error={errors.qualApp} /> | |
| )} | |
| <Dropdown label="Como você conheceu o Assistente de Bolso?" options={[{ value: 'amigo', label: 'Indicação de amigo' }, { value: 'instagram', label: 'Instagram' }, { value: 'google', label: 'Google' }, { value: 'tiktok', label: 'TikTok' }, { value: 'outro', label: 'Outro' }]} selected={formData.comoConheceu} onChange={(v) => updateFormData('comoConheceu', v)} placeholder="Selecione uma opção" error={errors.comoConheceu} /> | |
| </> | |
| )} | |
| {/* TELA: MÉTODO DE REGISTRO */} | |
| {currentScreenName === 'metodoRegistro' && ( | |
| <> | |
| <Title>Como você quer registrar suas finanças?</Title> | |
| <Subtitle>Escolha como prefere registrar suas despesas e receitas.</Subtitle> | |
| <RadioGroup label="Escolha uma opção" options={[{ value: 'automatico', label: 'Conexão automática com contas bancárias' }, { value: 'manual', label: 'Registro manual por texto, foto ou áudio' }, { value: 'ambos', label: 'Quero usar as duas formas acima' }]} selected={formData.metodoRegistro} onChange={(v) => updateFormData('metodoRegistro', v)} error={errors.metodoRegistro} /> | |
| {(formData.metodoRegistro === 'automatico' || formData.metodoRegistro === 'ambos') && ( | |
| <InfoBox>Para conexão automática, usamos um parceiro seguro chamado <strong>Pluggy</strong>. Você receberá um link para conectar suas contas ao final do cadastro.</InfoBox> | |
| )} | |
| <InfoBox type="info">Se mudar de ideia no futuro, é só avisar o assistente!</InfoBox> | |
| </> | |
| )} | |
| {/* TELA: RENDA E GASTOS */} | |
| {currentScreenName === 'rendaGastos' && ( | |
| <> | |
| <Title>Qual sua renda e seus gastos mensais?</Title> | |
| <Subtitle>Informe uma estimativa para eu entender sua situação financeira.</Subtitle> | |
| <TextInput label="Quanto você ganha por mês?" value={formData.ganhoMensal} onChange={(v) => updateFormData('ganhoMensal', formatCurrency(v))} placeholder="R$ 0" error={errors.ganhoMensal} /> | |
| <TextInput label="Quanto você gasta por mês?" value={formData.gastoMensal} onChange={(v) => updateFormData('gastoMensal', formatCurrency(v))} placeholder="R$ 0" error={errors.gastoMensal} hint="Depois eu te mostro se está batendo com a realidade." /> | |
| </> | |
| )} | |
| {/* TELA: CARTÕES */} | |
| {currentScreenName === 'cartoes' && ( | |
| <> | |
| <Title>Quais cartões de crédito você usa?</Title> | |
| <Subtitle>Com essa informação vou conseguir te ajudar a organizar seus gastos.</Subtitle> | |
| <CheckboxGroup label="Selecione seus cartões" options={[{ value: 'nubank', label: 'Nubank' }, { value: 'inter', label: 'Inter' }, { value: 'c6', label: 'C6 Bank' }, { value: 'itau', label: 'Itaú' }, { value: 'bradesco', label: 'Bradesco' }, { value: 'santander', label: 'Santander' }, { value: 'bb', label: 'Banco do Brasil' }, { value: 'caixa', label: 'Caixa' }, { value: 'xp', label: 'XP' }, { value: 'outro', label: 'Outro' }, { value: 'nenhum', label: 'Não uso cartão de crédito' }]} selected={formData.cartoes} onChange={(v) => toggleArrayField('cartoes', v)} error={errors.cartoes} /> | |
| {formData.cartoes.includes('outro') && ( | |
| <TextInput label="Qual outro cartão?" value={formData.outroCartao} onChange={(v) => updateFormData('outroCartao', v)} placeholder="Ex: Neon, PicPay..." required={false} /> | |
| )} | |
| {formData.cartoes.length > 0 && !formData.cartoes.includes('nenhum') && ( | |
| <Dropdown id="cartao-principal-dropdown" label="Qual é seu cartão principal?" options={formData.cartoes.filter(c => c !== 'outro' && c !== 'nenhum').map(c => ({ value: c, label: { nubank: 'Nubank', inter: 'Inter', c6: 'C6 Bank', itau: 'Itaú', bradesco: 'Bradesco', santander: 'Santander', bb: 'Banco do Brasil', caixa: 'Caixa', xp: 'XP' }[c] || c })).concat(formData.cartoes.includes('outro') && formData.outroCartao ? [{ value: 'outro', label: formData.outroCartao }] : [])} selected={formData.cartaoPrincipal} onChange={(v) => updateFormData('cartaoPrincipal', v)} placeholder="Selecione o cartão principal" error={errors.cartaoPrincipal} /> | |
| )} | |
| </> | |
| )} | |
| {/* TELA: DÍVIDAS */} | |
| {currentScreenName === 'dividas' && ( | |
| <> | |
| <Title>Você possui dívidas atualmente?</Title> | |
| <Subtitle>Saber sobre suas dívidas me ajuda a criar um plano para você quitá-las mais rápido.</Subtitle> | |
| <InfoBox type="info" icon="💡">Perguntamos sobre dívidas para te ajudar a se organizar e criar estratégias de pagamento. Essas informações são 100% confidenciais.</InfoBox> | |
| <RadioGroup label="Você tem dívidas hoje?" options={[{ value: 'nao', label: 'Não tenho dívidas' }, { value: 'sim', label: 'Sim, tenho dívidas em andamento' }]} selected={formData.temDividas} onChange={(v) => updateFormData('temDividas', v)} error={errors.temDividas} /> | |
| </> | |
| )} | |
| {/* TELA: DETALHES DAS DÍVIDAS */} | |
| {currentScreenName === 'detalhesDividas' && ( | |
| <> | |
| <Title>Detalhe suas dívidas</Title> | |
| <Subtitle>Informe cada dívida para que eu possa te ajudar a criar um plano de pagamento.</Subtitle> | |
| {formData.dividas.map((divida, index) => ( | |
| <div key={index} style={{ | |
| background: 'var(--white)', | |
| borderRadius: '16px', | |
| padding: '20px', | |
| marginBottom: '20px', | |
| border: '1px solid var(--gray-200)', | |
| }}> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}> | |
| <span style={{ fontWeight: 600, fontSize: '15px', color: 'var(--black)' }}>Dívida {index + 1}</span> | |
| {formData.dividas.length > 1 && ( | |
| <button type="button" onClick={() => removeDivida(index)} style={{ background: 'transparent', border: 'none', color: 'var(--error)', fontSize: '13px', cursor: 'pointer', padding: '4px 8px' }}>Remover</button> | |
| )} | |
| </div> | |
| <Dropdown label="Tipo de dívida" options={[{ value: 'cartao', label: 'Cartão de crédito' }, { value: 'financiamento', label: 'Financiamento' }, { value: 'consignado', label: 'Empréstimo consignado' }, { value: 'pessoal', label: 'Empréstimo pessoal' }, { value: 'cheque', label: 'Cheque especial' }, { value: 'outro', label: 'Outro' }]} selected={divida.tipo} onChange={(v) => updateDivida(index, 'tipo', v)} placeholder="Selecione o tipo" error={errors[`divida_tipo_${index}`]} /> | |
| <TextInput label="Valor total aproximado" value={divida.valor} onChange={(v) => updateDivida(index, 'valor', formatCurrency(v))} placeholder="R$ 0" error={errors[`divida_valor_${index}`]} /> | |
| <Dropdown label="Prazo restante" options={[{ value: '6m', label: 'Menos de 6 meses' }, { value: '6-12m', label: 'Entre 6 e 12 meses' }, { value: '1-3a', label: 'Entre 1 e 3 anos' }, { value: '3a+', label: 'Mais de 3 anos' }, { value: 'naosei', label: 'Não sei' }]} selected={divida.prazo} onChange={(v) => updateDivida(index, 'prazo', v)} placeholder="Selecione o prazo" error={errors[`divida_prazo_${index}`]} /> | |
| </div> | |
| ))} | |
| <button type="button" onClick={addDivida} style={{ | |
| width: '100%', | |
| padding: '14px', | |
| background: 'transparent', | |
| border: '2px dashed var(--green-primary)', | |
| borderRadius: '12px', | |
| color: 'var(--green-primary)', | |
| fontSize: '14px', | |
| fontWeight: 600, | |
| cursor: 'pointer', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| gap: '8px', | |
| transition: 'all 0.2s', | |
| }} | |
| onMouseOver={(e) => e.currentTarget.style.background = 'rgba(14, 54, 40, 0.05)'} | |
| onMouseOut={(e) => e.currentTarget.style.background = 'transparent'} | |
| > | |
| <span style={{ fontSize: '18px' }}>+</span> Adicionar outra dívida | |
| </button> | |
| </> | |
| )} | |
| {/* TELA: METAS */} | |
| {currentScreenName === 'metas' && ( | |
| <> | |
| <Title>Defina suas metas financeiras</Title> | |
| <Subtitle>Ter metas claras te ajuda a manter o foco. Vou te lembrar e acompanhar seu progresso!</Subtitle> | |
| {formData.metas.map((meta, index) => ( | |
| <div key={index} style={{ | |
| background: 'var(--white)', | |
| borderRadius: '16px', | |
| padding: '20px', | |
| marginBottom: '20px', | |
| border: '1px solid var(--gray-200)', | |
| }}> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}> | |
| <span style={{ fontWeight: 600, fontSize: '15px', color: 'var(--black)' }}>Meta {index + 1}</span> | |
| {formData.metas.length > 1 && ( | |
| <button type="button" onClick={() => removeMeta(index)} style={{ background: 'transparent', border: 'none', color: 'var(--error)', fontSize: '13px', cursor: 'pointer', padding: '4px 8px' }}>Remover</button> | |
| )} | |
| </div> | |
| <TextArea label="Descreva sua meta" value={meta.descricao} onChange={(v) => updateMeta(index, 'descricao', v)} placeholder="Ex: Juntar R$ 5.000 para emergências..." rows={2} error={errors[`meta_desc_${index}`]} /> | |
| <TextInput label="Prazo desejado" value={meta.prazo} onChange={(v) => updateMeta(index, 'prazo', v)} placeholder="Ex: 6 meses, até dezembro..." required={false} /> | |
| </div> | |
| ))} | |
| <button type="button" onClick={addMeta} style={{ | |
| width: '100%', | |
| padding: '14px', | |
| background: 'transparent', | |
| border: '2px dashed var(--green-primary)', | |
| borderRadius: '12px', | |
| color: 'var(--green-primary)', | |
| fontSize: '14px', | |
| fontWeight: 600, | |
| cursor: 'pointer', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| gap: '8px', | |
| transition: 'all 0.2s', | |
| }} | |
| onMouseOver={(e) => e.currentTarget.style.background = 'rgba(14, 54, 40, 0.05)'} | |
| onMouseOut={(e) => e.currentTarget.style.background = 'transparent'} | |
| > | |
| <span style={{ fontSize: '18px' }}>+</span> Adicionar outra meta | |
| </button> | |
| </> | |
| )} | |
| {/* TELA: TUTORIAL REGISTRO */} | |
| {currentScreenName === 'tutorialRegistro' && ( | |
| <> | |
| <Title>Veja como registrar movimentações</Title> | |
| <Subtitle>É muito fácil! Basta me enviar uma mensagem como se estivesse falando com um amigo.</Subtitle> | |
| <FeatureCard emoji="💸" title="Registrar uma despesa" examples={['Almoço 35 reais', 'Uber 22,50 hoje', 'Mercado 180 no débito']} /> | |
| <FeatureCard emoji="💰" title="Registrar uma receita" examples={['Recebi 3.500 do salário', 'Freela de 800 reais', 'PIX de 150 do João']} /> | |
| <FeatureCard emoji="🔁" title="Registrar algo recorrente" examples={['Aluguel 1.200 todo dia 5', 'Netflix 55,90 todo mês']} /> | |
| </> | |
| )} | |
| {/* TELA: TUTORIAL AUTOMÁTICO */} | |
| {currentScreenName === 'tutorialAutomatico' && ( | |
| <> | |
| <Title>Como funciona o registro automático</Title> | |
| <Subtitle>Suas transações bancárias serão importadas e categorizadas automaticamente.</Subtitle> | |
| <div style={{ | |
| background: 'rgba(34, 197, 94, 0.08)', | |
| border: '1px solid var(--green-accent)', | |
| borderRadius: '16px', | |
| padding: '24px', | |
| marginBottom: '24px', | |
| }}> | |
| <p style={{ fontWeight: 600, fontSize: '16px', color: 'var(--black)', marginBottom: '20px' }}>⏰ Horários de atualização</p> | |
| <div style={{ display: 'flex', gap: '14px', alignItems: 'flex-start' }}> | |
| <div style={{ | |
| width: '36px', | |
| height: '36px', | |
| borderRadius: '50%', | |
| background: 'var(--green-primary)', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| color: 'var(--white)', | |
| fontSize: '12px', | |
| fontWeight: 600, | |
| flexShrink: 0, | |
| }}> | |
| 24h | |
| </div> | |
| <p style={{ color: 'var(--gray-700)', fontSize: '14px', lineHeight: 1.5, paddingTop: '8px' }}> | |
| A atualização com suas movimentações bancárias acontece a cada 24h, a partir do horário que você conectou suas contas. | |
| </p> | |
| </div> | |
| </div> | |
| <div style={{ | |
| background: 'var(--white)', | |
| borderRadius: '16px', | |
| padding: '20px', | |
| marginBottom: '20px', | |
| border: '1px solid var(--gray-200)', | |
| }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '10px' }}> | |
| <span style={{ fontSize: '20px' }}>🏷️</span> | |
| <span style={{ fontWeight: 600, color: 'var(--black)' }}>Categorização inteligente</span> | |
| </div> | |
| <p style={{ color: 'var(--gray-600)', fontSize: '14px', lineHeight: 1.6 }}>Todas as entradas e saídas são categorizadas automaticamente (alimentação, transporte, lazer, etc.) e registradas no seu dashboard.</p> | |
| </div> | |
| <InfoBox type="info">Você pode combinar o registro automático com registros manuais sempre que quiser!</InfoBox> | |
| <InfoBox type="warning" icon="📌">Por enquanto, só é possível conectar uma conta bancária por usuário. Se você quiser conectar mais contas, é só manifestar o interesse ao Assistente de Bolso!</InfoBox> | |
| </> | |
| )} | |
| {/* TELA: TUTORIAL CONEXÃO */} | |
| {currentScreenName === 'tutorialConexao' && ( | |
| <> | |
| <Title>Como conectar suas contas</Title> | |
| <Subtitle>A conexão é feita via Open Finance, de forma segura e regulamentada pelo Banco Central.</Subtitle> | |
| <div style={{ | |
| background: 'rgba(34, 197, 94, 0.08)', | |
| border: '1px solid var(--green-accent)', | |
| borderRadius: '16px', | |
| padding: '24px', | |
| marginBottom: '24px', | |
| }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '20px' }}> | |
| <span style={{ fontSize: '24px' }}>🔗</span> | |
| <span style={{ fontWeight: 600, fontSize: '17px', color: 'var(--black)' }}>Passo a passo</span> | |
| </div> | |
| <StepItem number="1" title="Peça o link de conexão" description='Envie uma mensagem pedindo para conectar sua conta. Ex: "Quero conectar minha conta"' /> | |
| <StepItem number="2" title="Acesse o link da Pluggy" description="Você receberá um link seguro do nosso parceiro Pluggy para fazer a autorização." /> | |
| <StepItem number="3" title="Autorize suas contas" description="Selecione os bancos que deseja conectar e siga o passo a passo de autorização." /> | |
| <StepItem number="4" title="Pronto!" description="Suas transações serão importadas automaticamente." /> | |
| </div> | |
| <div style={{ | |
| background: 'var(--white)', | |
| borderRadius: '16px', | |
| padding: '20px', | |
| marginBottom: '20px', | |
| border: '1px solid var(--gray-200)', | |
| }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '10px' }}> | |
| <span style={{ fontSize: '20px' }}>🔒</span> | |
| <span style={{ fontWeight: 600, color: 'var(--black)' }}>Segurança garantida</span> | |
| </div> | |
| <p style={{ color: 'var(--gray-600)', fontSize: '14px', lineHeight: 1.6, marginBottom: '8px' }}>A conexão é feita através da <strong>Pluggy</strong>, parceiro certificado pelo Banco Central para Open Finance.</p> | |
| <p style={{ color: 'var(--gray-600)', fontSize: '14px', lineHeight: 1.6 }}>Você pode revogar o acesso a qualquer momento diretamente no app do seu banco.</p> | |
| </div> | |
| <InfoBox type="info" icon="💡">Você pode conectar suas contas agora ou a qualquer momento depois. É só me pedir o link!</InfoBox> | |
| </> | |
| )} | |
| {/* TELA: TUTORIAL DASHBOARD */} | |
| {currentScreenName === 'tutorialDashboard' && ( | |
| <> | |
| <Title>Acompanhe suas finanças no dashboard</Title> | |
| <Subtitle>Todas as suas movimentações ficam organizadas em um dashboard que você pode acessar a qualquer momento.</Subtitle> | |
| <div style={{ | |
| background: 'var(--white)', | |
| borderRadius: '16px', | |
| padding: '24px', | |
| marginBottom: '20px', | |
| border: '1px solid var(--gray-200)', | |
| }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '18px' }}> | |
| <span style={{ fontSize: '24px' }}>📊</span> | |
| <span style={{ fontWeight: 600, fontSize: '17px', color: 'var(--black)' }}>Seu dashboard financeiro</span> | |
| </div> | |
| <p style={{ color: 'var(--gray-600)', fontSize: '14px', lineHeight: 1.6, marginBottom: '16px' }}>No dashboard você encontra:</p> | |
| {['Todas as despesas e receitas (manuais e automáticas)', 'Gráficos de gastos por categoria', 'Evolução do seu saldo', 'Progresso das suas metas'].map((item, i) => ( | |
| <div key={i} style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '10px' }}> | |
| <span style={{ color: 'var(--green-accent)', fontSize: '18px' }}>✓</span> | |
| <span style={{ color: 'var(--black)', fontSize: '14px' }}>{item}</span> | |
| </div> | |
| ))} | |
| </div> | |
| <div style={{ | |
| background: 'var(--gray-100)', | |
| borderRadius: '12px', | |
| padding: '16px', | |
| marginBottom: '20px', | |
| }}> | |
| <p style={{ fontSize: '12px', color: 'var(--gray-500)', textTransform: 'uppercase', fontWeight: 600, marginBottom: '8px' }}>🔗 Como acessar</p> | |
| <p style={{ color: 'var(--black)', fontSize: '14px', lineHeight: 1.5 }}>Você receberá um link exclusivo para acessar seu dashboard quando quiser.</p> | |
| </div> | |
| <FeatureCard emoji="💬" title="Ou pergunte direto pra mim" examples={['Quanto gastei hoje?', 'Quanto gastei no mês?', 'Meus gastos com alimentação']} /> | |
| </> | |
| )} | |
| {/* TELA: TUTORIAL LEMBRETES */} | |
| {currentScreenName === 'tutorialLembretes' && ( | |
| <> | |
| <Title>Crie lembretes e nunca esqueça nada</Title> | |
| <Subtitle>Posso te lembrar de pagamentos, compromissos e criar eventos na sua agenda.</Subtitle> | |
| <FeatureCard emoji="💳" title="Lembrete de pagamento" examples={['Me lembra de pagar o cartão dia 15', 'Avisa sobre o aluguel todo dia 5']} /> | |
| <FeatureCard emoji="📅" title="Lembrete de compromisso" examples={['Me lembra da reunião amanhã às 14h', 'Dentista dia 20 às 9h']} /> | |
| <FeatureCard emoji="📆" title="Evento na agenda Google" examples={['Cria evento: viagem SP dia 10 a 12', 'Agenda reunião quinta às 15h']} /> | |
| <InfoBox type="warning">Os lembretes podem levar até 3h para serem registrados. Você será avisado quando estiver tudo certo! ✅</InfoBox> | |
| </> | |
| )} | |
| {/* TELA: PRONTO */} | |
| {currentScreenName === 'pronto' && ( | |
| <> | |
| <Title>Você está pronto para começar!</Title> | |
| <Subtitle>Agora é só me enviar mensagens por texto, áudio ou foto. Estou aqui para te ajudar!</Subtitle> | |
| <div style={{ | |
| background: 'rgba(34, 197, 94, 0.08)', | |
| border: '1px solid var(--green-accent)', | |
| borderRadius: '16px', | |
| padding: '24px', | |
| marginBottom: '24px', | |
| }}> | |
| <p style={{ fontWeight: 600, fontSize: '16px', color: 'var(--black)', marginBottom: '16px' }}>✨ O que você pode fazer:</p> | |
| {[ | |
| { i: '💸', t: 'Registrar despesas e receitas' }, | |
| { i: '📊', t: 'Ver relatórios e análises' }, | |
| { i: '⏰', t: 'Criar lembretes' }, | |
| { i: '🎯', t: 'Acompanhar suas metas' }, | |
| { i: '💬', t: 'Tirar dúvidas sobre finanças' } | |
| ].map((item, idx) => ( | |
| <div key={idx} style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: idx < 4 ? '10px' : 0 }}> | |
| <span style={{ fontSize: '18px' }}>{item.i}</span> | |
| <span style={{ color: 'var(--black)', fontSize: '15px' }}>{item.t}</span> | |
| </div> | |
| ))} | |
| </div> | |
| <div style={{ | |
| background: 'var(--white)', | |
| borderRadius: '12px', | |
| padding: '16px', | |
| border: '1px solid var(--gray-200)', | |
| }}> | |
| <p style={{ fontSize: '12px', color: 'var(--gray-500)', textTransform: 'uppercase', fontWeight: 600, marginBottom: '8px' }}>💡 Dica</p> | |
| <p style={{ color: 'var(--black)', fontSize: '14px', lineHeight: 1.6 }}>Se não tiver certeza se eu faço alguma coisa, é só perguntar! Estou aqui pra ajudar 😉</p> | |
| </div> | |
| </> | |
| )} | |
| </ContentWrapper> | |
| </div> | |
| <Footer> | |
| <PrimaryButton | |
| text={currentScreenName === 'pronto' ? 'Concluir cadastro' : 'Continuar'} | |
| onClick={currentScreenName === 'pronto' ? handleFinish : handleNext} | |
| disabled={currentScreenName === 'dados' && !formData.aceitaTermos} | |
| /> | |
| </Footer> | |
| </div> | |
| ); | |
| }; | |
| ReactDOM.createRoot(document.getElementById('root')).render(<OnboardingApp />); | |
| </script> | |
| </body> | |
| </html> |