Created
April 25, 2026 09:41
-
-
Save annguyenwasd/0e890feb9309bbdf8afd97797fe2672d to your computer and use it in GitHub Desktop.
Chung Chi — Budget Multiple Periods mockup (screens 19–22)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="vi"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Chung Chi — Budget Multiple Periods (screens 19–22)</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;500;600;700&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| html, body { margin:0; padding:0; font-family:'Outfit', system-ui, sans-serif; background:#f0eee9; } | |
| .canvas { display:flex; gap:48px; padding:60px; align-items:flex-start; min-height:100vh; flex-wrap:wrap; } | |
| .artboard { display:flex; flex-direction:column; align-items:center; gap:12px; } | |
| .artboard-label { font-size:13px; font-weight:600; color:rgba(60,50,40,0.7); letter-spacing:0.02em; } | |
| .section-title { font-size:22px; font-weight:700; color:rgba(40,30,20,0.85); letter-spacing:-0.3px; margin-bottom:4px; width:100%; padding:0 60px; } | |
| .section-sub { font-size:14px; color:rgba(60,50,40,0.6); padding:0 60px; margin-bottom:32px; } | |
| .section { width:100%; margin-bottom:60px; } | |
| .row { display:flex; gap:48px; padding:0 60px; align-items:flex-start; flex-wrap:wrap; } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="root"></div> | |
| <script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script> | |
| <script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script> | |
| <script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script> | |
| <script type="text/babel" data-presets="env,react"> | |
| // ── IOSDevice ────────────────────────────────────────────────── | |
| function IOSStatusBar({ dark = false }) { | |
| const c = dark ? '#fff' : '#000'; | |
| return ( | |
| <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'21px 24px 19px', boxSizing:'border-box', position:'relative', zIndex:20, width:'100%' }}> | |
| <span style={{ fontFamily:'-apple-system,"SF Pro",system-ui', fontWeight:590, fontSize:17, color:c }}>9:41</span> | |
| <div style={{ display:'flex', alignItems:'center', gap:7 }}> | |
| <svg width="19" height="12" viewBox="0 0 19 12"><rect x="0" y="7.5" width="3.2" height="4.5" rx="0.7" fill={c}/><rect x="4.8" y="5" width="3.2" height="7" rx="0.7" fill={c}/><rect x="9.6" y="2.5" width="3.2" height="9.5" rx="0.7" fill={c}/><rect x="14.4" y="0" width="3.2" height="12" rx="0.7" fill={c}/></svg> | |
| <svg width="27" height="13" viewBox="0 0 27 13"><rect x="0.5" y="0.5" width="23" height="12" rx="3.5" stroke={c} strokeOpacity="0.35" fill="none"/><rect x="2" y="2" width="20" height="9" rx="2" fill={c}/><path d="M25 4.5V8.5C25.8 8.2 26.5 7.2 26.5 6.5C26.5 5.8 25.8 4.8 25 4.5Z" fill={c} fillOpacity="0.4"/></svg> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function IOSDevice({ children, width=390, height=844, dark=false }) { | |
| return ( | |
| <div style={{ width, height, borderRadius:48, overflow:'hidden', position:'relative', | |
| background: dark?'#000':'#F2F2F7', | |
| boxShadow:'0 40px 80px rgba(0,0,0,0.18), 0 0 0 1px rgba(0,0,0,0.12)', | |
| fontFamily:'-apple-system,system-ui,sans-serif', WebkitFontSmoothing:'antialiased', | |
| }}> | |
| <div style={{ position:'absolute', top:11, left:'50%', transform:'translateX(-50%)', width:126, height:37, borderRadius:24, background:'#000', zIndex:50 }}/> | |
| <div style={{ position:'absolute', top:0, left:0, right:0, zIndex:10 }}><IOSStatusBar dark={dark}/></div> | |
| <div style={{ height:'100%', display:'flex', flexDirection:'column' }}> | |
| <div style={{ flex:1, overflow:'auto' }}>{children}</div> | |
| </div> | |
| <div style={{ position:'absolute', bottom:0, left:0, right:0, zIndex:60, height:34, display:'flex', justifyContent:'center', alignItems:'flex-end', paddingBottom:8, pointerEvents:'none' }}> | |
| <div style={{ width:139, height:5, borderRadius:100, background: dark?'rgba(255,255,255,0.7)':'rgba(0,0,0,0.25)' }}/> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // ── Budget screens ───────────────────────────────────────────── | |
| const T = { | |
| crimson:'#AE2F34', coral:'#FF6B6B', | |
| cream:'#FFF8F6', warm:'#FFF3F0', tonal:'#F9F2F0', muted:'#F5EDE9', | |
| surface:'#FFFFFF', ink:'#1A1210', inkSoft:'#7A5C56', inkMuted:'#B89890', | |
| border:'#EDD9D3', | |
| fontDisplay:"'Chakra Petch', system-ui, sans-serif", | |
| fontBody:"'Outfit', system-ui, sans-serif", | |
| gradient:'linear-gradient(135deg, #AE2F34 0%, #FF6B6B 100%)', | |
| shadowCard:'0 2px 16px rgba(255,107,107,0.06)', | |
| shadowFab:'0 8px 24px rgba(255,107,107,0.40)', | |
| shadowModal:'0 -8px 40px rgba(26,18,16,0.18)', | |
| }; | |
| const DARK = { cream:'#1A1210', warm:'#231815', tonal:'#2A1A17', muted:'#2D1F1C', surface:'#231815', ink:'#FFF0EC', inkSoft:'#C4908A', inkMuted:'#7A5550', border:'#3D2420' }; | |
| const palette = (dark) => dark ? {...T,...DARK} : T; | |
| const fmt = (n) => n.toLocaleString('vi-VN'); | |
| const short = (n) => { if(n>=1_000_000) return (n/1_000_000).toFixed(n%1_000_000===0?0:1).replace('.0','')+'tr'; if(n>=1_000) return (n/1_000)+'k'; return String(n); }; | |
| const CATS = { | |
| food:{ name:'Ăn uống', color:'#FF9F43', emoji:'🍜' }, | |
| transport:{ name:'Di chuyển', color:'#54A0FF', emoji:'🚗' }, | |
| bills:{ name:'Hoá đơn', color:'#AE2F34', emoji:'💡' }, | |
| entertainment:{ name:'Giải trí', color:'#5F27CD', emoji:'🎬' }, | |
| health:{ name:'Sức khoẻ', color:'#00D2D3', emoji:'💊' }, | |
| shopping:{ name:'Mua sắm', color:'#1DD1A1', emoji:'🛒' }, | |
| family:{ name:'Nuôi bé ❤', color:'#FF6B6B', emoji:'👶' }, | |
| travel:{ name:'Về Đồng Nai 🌸', color:'#E91E8C', emoji:'✈️' }, | |
| }; | |
| function BottomNav({ active, dark }) { | |
| const P = palette(dark); | |
| const tabs = [ | |
| { id:'expenses', label:'Chi tiêu', d:'M19.5 3.5L18 2l-1.5 1.5L15 2l-1.5 1.5L12 2l-1.5 1.5L9 2 7.5 3.5 6 2v14H3v3c0 1.66 1.34 3 3 3h12c1.66 0 3-1.34 3-3V2l-1.5 1.5zM19 19c0 .55-.45 1-1 1s-1-.45-1-1v-3H8V5h11v14z' }, | |
| { id:'report', label:'Báo cáo', d:'M5 9.2h3V19H5zM10.6 5h2.8v14h-2.8zm5.6 8H19v6h-2.8z' }, | |
| { id:'budget', label:'Ngân sách', d:'M21 7.5V18a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h11L21 7.5zM17 14a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z' }, | |
| { id:'account', label:'Tài khoản', d:'M12 12c2.2 0 4-1.8 4-4s-1.8-4-4-4-4 1.8-4 4 1.8 4 4 4zm0 2c-2.7 0-8 1.3-8 4v2h16v-2c0-2.7-5.3-4-8-4z' }, | |
| ]; | |
| return ( | |
| <div style={{ position:'absolute', bottom:0, left:0, right:0, height:78, background:P.surface, display:'flex', paddingBottom:14, borderTop:`0.5px solid ${P.border}` }}> | |
| {tabs.map(t => { | |
| const on = t.id === active; | |
| return ( | |
| <div key={t.id} style={{ flex:1, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', gap:2, color:on?T.crimson:P.inkMuted, fontFamily:T.fontBody, fontSize:11, fontWeight:on?600:500, transform:on?'translateY(-2px)':'none' }}> | |
| <svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d={t.d}/></svg> | |
| {t.label} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| ); | |
| } | |
| function Donut({ size=180, segments, remaining, total, dark, innerText=true }) { | |
| const P = palette(dark); | |
| const R = size/2-14; | |
| const C = 2*Math.PI*R; | |
| const totalSpent = segments.reduce((s,x)=>s+x.value,0); | |
| const cap = Math.max(totalSpent,total); | |
| const gap = size>=140?1.8:0.8; | |
| let acc = 0; | |
| return ( | |
| <div style={{ position:'relative', width:size, height:size, margin:'0 auto' }}> | |
| {size>=140 && <div style={{ position:'absolute', inset:12, borderRadius:'50%', background:'radial-gradient(circle, rgba(255,107,107,0.18), transparent 70%)', filter:'blur(8px)' }}/>} | |
| <svg width={size} height={size} style={{ transform:'rotate(-90deg)', filter:size>=140?'drop-shadow(0 4px 10px rgba(174,47,52,0.15))':'none' }}> | |
| <circle cx={size/2} cy={size/2} r={R} fill="none" stroke={P.muted} strokeWidth={size>=140?18:14}/> | |
| {segments.map((seg,i) => { | |
| const frac=seg.value/cap; const len=Math.max(0,frac*C-gap); | |
| const el=(<circle key={i} cx={size/2} cy={size/2} r={R} fill="none" stroke={seg.color} strokeWidth={size>=140?18:14} strokeDasharray={`${len} ${C-len}`} strokeDashoffset={-acc} strokeLinecap="round"/>); | |
| acc+=frac*C; return el; | |
| })} | |
| </svg> | |
| {innerText && ( | |
| <div style={{ position:'absolute', inset:0, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center' }}> | |
| {size>=140 && (<> | |
| <div style={{ fontFamily:T.fontBody, fontSize:11, color:P.inkSoft, marginBottom:2, whiteSpace:'nowrap', letterSpacing:'0.04em' }}>Còn lại</div> | |
| <div style={{ fontFamily:T.fontDisplay, fontWeight:700, fontSize:34, letterSpacing:'-0.015em', whiteSpace:'nowrap', lineHeight:1, background:remaining<0?T.crimson:T.gradient, WebkitBackgroundClip:'text', WebkitTextFillColor:'transparent', backgroundClip:'text' }}>{remaining<0?'-':''}{short(Math.abs(remaining))}</div> | |
| <div style={{ fontFamily:T.fontBody, fontSize:10, color:P.inkMuted, marginTop:4, whiteSpace:'nowrap' }}>/ {short(total)}</div> | |
| </>)} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| // ── Screen 19 · Add budget step 1 ──────────────────────────── | |
| function CCBudgetAddStep1({ dark=false }) { | |
| const P = palette(dark); | |
| return ( | |
| <div style={{ position:'absolute', inset:0, background:P.cream, overflow:'hidden' }}> | |
| <div style={{ position:'absolute', inset:0, background:'#000', opacity:0.25 }}/> | |
| <div style={{ position:'absolute', left:0, right:0, bottom:0, background:P.cream, borderTopLeftRadius:24, borderTopRightRadius:24, boxShadow:T.shadowModal, height:'92%', display:'flex', flexDirection:'column', overflow:'hidden' }}> | |
| <div style={{ display:'flex', justifyContent:'center', padding:'10px 0 4px' }}> | |
| <div style={{ width:38, height:4, borderRadius:9999, background:P.inkMuted, opacity:0.5 }}/> | |
| </div> | |
| <div style={{ height:48, display:'flex', alignItems:'center', padding:'0 16px' }}> | |
| <button style={{ border:0, background:'transparent', color:T.crimson, fontSize:15, fontWeight:500, fontFamily:T.fontBody }}>Huỷ</button> | |
| <div style={{ flex:1, fontFamily:T.fontDisplay, fontWeight:600, fontSize:17, textAlign:'center', color:P.ink }}>Ngân sách mới</div> | |
| <button style={{ border:0, background:'transparent', color:P.inkMuted, fontSize:15, fontWeight:500, fontFamily:T.fontBody }}>Tiếp</button> | |
| </div> | |
| <div style={{ padding:'8px 20px', display:'flex', flexDirection:'column', gap:18, overflow:'auto' }}> | |
| <div style={{ display:'flex', gap:6, alignItems:'center', justifyContent:'center', padding:'2px 0 4px' }}> | |
| <span style={{ width:22, height:4, borderRadius:9999, background:T.crimson }}/> | |
| <span style={{ width:22, height:4, borderRadius:9999, background:P.muted }}/> | |
| <span style={{ fontSize:11, color:P.inkMuted, marginLeft:4 }}>1 / 2 · Thời gian & tổng</span> | |
| </div> | |
| {/* Budget name (NEW field) */} | |
| <div> | |
| <div style={{ fontSize:11, color:P.inkSoft, paddingLeft:14, marginBottom:6, fontWeight:600, letterSpacing:'0.05em', textTransform:'uppercase' }}>Tên ngân sách</div> | |
| <div style={{ background:P.surface, border:`1px solid ${P.border}`, borderRadius:12, padding:'12px 14px', fontSize:15, color:P.inkMuted, fontFamily:T.fontBody, fontWeight:500 }}>Du lịch Đà Nẵng</div> | |
| </div> | |
| {/* Period */} | |
| <div> | |
| <div style={{ fontSize:11, color:P.inkSoft, paddingLeft:14, marginBottom:6, fontWeight:600, letterSpacing:'0.05em', textTransform:'uppercase' }}>Khoảng thời gian</div> | |
| <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:10, marginBottom:10 }}> | |
| <div style={{ background:P.surface, border:`1.5px solid ${T.crimson}`, borderRadius:12, padding:'10px 14px' }}> | |
| <div style={{ fontSize:10, color:P.inkSoft, marginBottom:2 }}>Bắt đầu</div> | |
| <div style={{ fontSize:14, fontWeight:600, color:P.ink, fontFamily:T.fontDisplay, fontVariantNumeric:'tabular-nums' }}>02/05/2025</div> | |
| </div> | |
| <div style={{ background:P.surface, border:`1px solid ${P.border}`, borderRadius:12, padding:'10px 14px' }}> | |
| <div style={{ fontSize:10, color:P.inkSoft, marginBottom:2 }}>Kết thúc</div> | |
| <div style={{ fontSize:14, fontWeight:600, color:P.ink, fontFamily:T.fontDisplay, fontVariantNumeric:'tabular-nums' }}>09/05/2025</div> | |
| </div> | |
| </div> | |
| {/* Recurring toggle + cadence selector */} | |
| <div style={{ background:P.surface, borderRadius:12, boxShadow:T.shadowCard, padding:'12px 14px', display:'flex', alignItems:'center', gap:12 }}> | |
| <div style={{ width:30, height:30, borderRadius:8, background:T.crimson+'15', color:T.crimson, display:'flex', alignItems:'center', justifyContent:'center' }}> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M17.65 6.35A7.958 7.958 0 0 0 12 4a8 8 0 1 0 7.75 10h-2.08A6 6 0 1 1 12 6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg> | |
| </div> | |
| <div style={{ flex:1 }}> | |
| <div style={{ fontSize:13, fontWeight:500, color:P.ink }}>Lặp lại tự động</div> | |
| <div style={{ fontSize:11, color:P.inkSoft, marginTop:1 }}> | |
| <span style={{ background:T.crimson+'15', color:T.crimson, borderRadius:6, padding:'2px 8px', fontSize:11, fontWeight:600 }}>Hàng tháng ▾</span> | |
| </div> | |
| </div> | |
| {/* Toggle ON */} | |
| <div style={{ width:42, height:24, borderRadius:9999, background:T.crimson, position:'relative' }}> | |
| <span style={{ position:'absolute', top:2, left:20, width:20, height:20, borderRadius:'50%', background:'#fff' }}/> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Category scope (NEW field) */} | |
| <div> | |
| <div style={{ fontSize:11, color:P.inkSoft, paddingLeft:14, marginBottom:6, fontWeight:600, letterSpacing:'0.05em', textTransform:'uppercase' }}>Phạm vi danh mục</div> | |
| <div style={{ background:P.surface, border:`1px solid ${P.border}`, borderRadius:12, padding:'12px 14px', display:'flex', alignItems:'center', justifyContent:'space-between' }}> | |
| <div style={{ display:'flex', gap:6, flexWrap:'wrap' }}> | |
| {['Du lịch','Ăn uống'].map(c => ( | |
| <span key={c} style={{ background:T.crimson+'15', color:T.crimson, borderRadius:99, padding:'3px 10px', fontSize:12, fontWeight:600 }}>{c}</span> | |
| ))} | |
| </div> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill={P.inkMuted}><path d="M19 6.4L17.6 5 12 10.6 6.4 5 5 6.4 10.6 12 5 17.6 6.4 19 12 13.4 17.6 19 19 17.6 13.4 12z"/></svg> | |
| </div> | |
| <div style={{ fontSize:11, color:P.inkMuted, padding:'6px 14px 0', lineHeight:1.5 }}>Chỉ tính chi tiêu thuộc các danh mục này.</div> | |
| </div> | |
| {/* Total */} | |
| <div> | |
| <div style={{ fontSize:11, color:P.inkSoft, paddingLeft:14, marginBottom:6, fontWeight:600, letterSpacing:'0.05em', textTransform:'uppercase' }}>Tổng ngân sách</div> | |
| <div style={{ background:P.surface, borderRadius:14, padding:'18px 20px', boxShadow:T.shadowCard, textAlign:'center' }}> | |
| <div style={{ fontFamily:T.fontDisplay, fontWeight:700, fontSize:34, color:T.crimson, letterSpacing:'-0.01em', fontVariantNumeric:'tabular-nums' }}>8.000.000</div> | |
| <div style={{ fontSize:11, color:P.inkSoft, marginTop:4, letterSpacing:'0.08em', textTransform:'uppercase', fontWeight:600 }}>đồng</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // ── Screen 20 · Add budget step 2 ──────────────────────────── | |
| function CCBudgetAddStep2({ dark=false }) { | |
| const P = palette(dark); | |
| const rows = [ | |
| { cat:'family', amount:4_500_000, active:false }, | |
| { cat:'travel', amount:3_000_000, active:true }, | |
| { cat:'food', amount:null, active:false }, | |
| { cat:'bills', amount:null, active:false }, | |
| { cat:'transport', amount:null, active:false }, | |
| ]; | |
| const allocated = rows.reduce((s,r)=>s+(r.amount||0),0); | |
| const total = 30_000_000; | |
| const remain = total - allocated; | |
| return ( | |
| <div style={{ position:'absolute', inset:0, background:P.cream, overflow:'hidden' }}> | |
| <div style={{ height:'46%', overflow:'hidden', position:'relative', background:P.cream }}> | |
| <div style={{ height:60 }}/> | |
| <div style={{ height:48, display:'flex', alignItems:'center', padding:'0 16px' }}> | |
| <button style={{ border:0, background:'transparent', color:P.ink, width:30, height:30, display:'flex', alignItems:'center', justifyContent:'center' }}> | |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M19 6.4L17.6 5 12 10.6 6.4 5 5 6.4 10.6 12 5 17.6 6.4 19 12 13.4 17.6 19 19 17.6 13.4 12z"/></svg> | |
| </button> | |
| <div style={{ flex:1, fontFamily:T.fontDisplay, fontWeight:600, fontSize:17, color:P.ink, marginLeft:4 }}>Ngân sách danh mục</div> | |
| <button style={{ background:T.crimson, color:'#fff', border:0, padding:'7px 18px', borderRadius:9999, fontFamily:T.fontBody, fontWeight:600, fontSize:13 }}>Xong</button> | |
| </div> | |
| <div style={{ padding:'4px 20px 10px', display:'flex', alignItems:'center', gap:16 }}> | |
| <div style={{ width:80, height:80 }}> | |
| <Donut size={80} segments={rows.filter(r=>r.amount).map(r=>({ color:CATS[r.cat].color, value:r.amount }))} remaining={remain} total={total} dark={dark} innerText={false}/> | |
| </div> | |
| <div style={{ flex:1 }}> | |
| <div style={{ fontSize:10, color:P.inkSoft, letterSpacing:'0.08em', textTransform:'uppercase', fontWeight:600, marginBottom:3 }}>Chưa phân bổ</div> | |
| <div style={{ fontFamily:T.fontDisplay, fontWeight:700, fontSize:22, color:T.crimson, fontVariantNumeric:'tabular-nums', letterSpacing:'-0.01em' }}>{fmt(remain)} đ</div> | |
| <div style={{ fontSize:11, color:P.inkMuted, marginTop:2 }}>/ {fmt(total)} đ tổng</div> | |
| </div> | |
| </div> | |
| <div style={{ padding:'0 20px', display:'flex', flexDirection:'column', gap:0 }}> | |
| {rows.slice(0,2).map((r,i) => { | |
| const c = CATS[r.cat]; | |
| return ( | |
| <div key={i} style={{ display:'flex', alignItems:'center', gap:10, padding:'10px 2px', borderBottom:`0.5px solid ${P.border}` }}> | |
| <span style={{ width:10, height:10, borderRadius:'50%', background:c.color }}/> | |
| <div style={{ flex:1, fontSize:14, color:r.active?T.crimson:P.ink, fontWeight:r.active?600:500 }}>{c.name}</div> | |
| {r.amount ? <div style={{ fontSize:14, fontWeight:600, color:r.active?T.crimson:P.ink, fontVariantNumeric:'tabular-nums' }}>{short(r.amount)}</div> : <div style={{ fontSize:13, color:P.inkMuted }}>Đặt giới hạn</div>} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| <div style={{ position:'absolute', left:0, right:0, bottom:0, height:'54%', background:P.surface, borderTopLeftRadius:20, borderTopRightRadius:20, boxShadow:T.shadowModal, display:'flex', flexDirection:'column' }}> | |
| <div style={{ padding:'10px 20px 6px' }}> | |
| <div style={{ display:'flex', justifyContent:'center', marginBottom:8 }}> | |
| <div style={{ width:38, height:4, borderRadius:9999, background:P.inkMuted, opacity:0.4 }}/> | |
| </div> | |
| <div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:10 }}> | |
| <span style={{ width:8, height:8, borderRadius:'50%', background:CATS.travel.color }}/> | |
| <div style={{ fontSize:14, fontWeight:600, color:P.ink }}>{CATS.travel.name}</div> | |
| </div> | |
| <div style={{ background:P.muted, borderRadius:14, padding:'14px 18px', textAlign:'right', fontFamily:T.fontDisplay, fontWeight:700, fontSize:30, color:P.ink, fontVariantNumeric:'tabular-nums', letterSpacing:'-0.005em' }}>3.000.000</div> | |
| </div> | |
| <div style={{ flex:1, padding:'4px 14px 10px', display:'grid', gridTemplateColumns:'repeat(4, 1fr)', gap:6 }}> | |
| {[{l:'C',kind:'op'},{l:'÷',kind:'op'},{l:'×',kind:'op'},{l:'⌫',kind:'op'},{l:'7'},{l:'8'},{l:'9'},{l:'−',kind:'op'},{l:'4'},{l:'5'},{l:'6'},{l:'+',kind:'op'},{l:'1'},{l:'2'},{l:'3'},{l:'Xong',kind:'done',rowSpan:2},{l:'0'},{l:'000'},{l:'.'}].map((k,i) => { | |
| if(k.kind==='done') return ( | |
| <div key={i} style={{ gridRow:'span 2', background:T.crimson, color:'#fff', borderRadius:12, display:'flex', alignItems:'center', justifyContent:'center', fontFamily:T.fontDisplay, fontWeight:700, fontSize:15, letterSpacing:'0.02em' }}>{k.l}</div> | |
| ); | |
| const isOp = k.kind==='op'; | |
| return (<div key={i} style={{ background:P.surface, border:`1px solid ${P.border}`, borderRadius:12, display:'flex', alignItems:'center', justifyContent:'center', fontFamily:isOp?T.fontDisplay:T.fontBody, fontWeight:isOp?600:500, fontSize:isOp?18:17, color:isOp?T.crimson:P.ink, height:44 }}>{k.l}</div>); | |
| })} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // ── Screen 21 · Budget swipe to delete ─────────────────────── | |
| function CCBudgetSwipeDelete({ dark=false }) { | |
| const P = palette(dark); | |
| const rows = [ | |
| { cat:'family', budget:4_500_000, spent:3_200_000, swipe:0 }, | |
| { cat:'travel', budget:3_000_000, spent:4_215_000, swipe:110 }, | |
| { cat:'food', budget:6_000_000, spent:4_439_000, swipe:0 }, | |
| ]; | |
| return ( | |
| <div style={{ position:'absolute', inset:0, background:P.cream, overflow:'hidden' }}> | |
| <div style={{ height:60 }}/> | |
| <div style={{ height:52, display:'flex', alignItems:'center', padding:'0 20px' }}> | |
| <div style={{ flex:1, fontFamily:T.fontDisplay, fontWeight:700, fontSize:24, color:P.ink, letterSpacing:'-0.01em' }}>Ngân sách</div> | |
| <button style={{ border:0, background:T.gradient, color:'#fff', height:34, padding:'0 16px', borderRadius:9999, display:'flex', alignItems:'center', justifyContent:'center', gap:5, boxShadow:T.shadowFab, fontFamily:T.fontDisplay, fontWeight:600, fontSize:13 }}> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg> | |
| Thêm | |
| </button> | |
| </div> | |
| <div style={{ padding:'4px 20px 10px', fontSize:13, color:P.inkSoft, textAlign:'center', fontVariantNumeric:'tabular-nums' }}>25/03 – 25/04 · còn 1.1tr</div> | |
| <div style={{ padding:'4px 20px', display:'flex', flexDirection:'column', gap:10 }}> | |
| {rows.map((r,i) => { | |
| const c = CATS[r.cat]; | |
| const pct = Math.round((r.spent/r.budget)*100); | |
| const over = pct>100; | |
| return ( | |
| <div key={i} style={{ position:'relative', height:88 }}> | |
| <div style={{ position:'absolute', inset:0, background:'#E53935', borderRadius:14, display:'flex', alignItems:'center', justifyContent:'flex-end', paddingRight:24, gap:10, color:'#fff', fontFamily:T.fontBody, fontWeight:600, fontSize:13 }}> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M6 19a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7H6zM19 4h-3.5l-1-1h-5l-1 1H5v2h14z"/></svg> | |
| <span>Xoá</span> | |
| </div> | |
| <div style={{ position:'absolute', inset:0, width:r.swipe?`calc(100% - ${r.swipe}px)`:'100%', overflow:'hidden', borderTopLeftRadius:14, borderBottomLeftRadius:14, borderTopRightRadius:r.swipe?0:14, borderBottomRightRadius:r.swipe?0:14 }}> | |
| <div style={{ width:350, boxSizing:'border-box', background:P.surface, borderRadius:14, padding:'12px 14px', boxShadow:T.shadowCard, height:88 }}> | |
| <div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:8 }}> | |
| <div style={{ width:10, height:10, borderRadius:'50%', background:c.color, flexShrink:0 }}/> | |
| <div style={{ flex:1, fontSize:14, fontWeight:500, color:P.ink }}>{c.name}</div> | |
| <div style={{ fontSize:13, fontWeight:600, color:over?T.crimson:'#F5A623', fontVariantNumeric:'tabular-nums' }}>{pct}%</div> | |
| </div> | |
| <div style={{ height:6, borderRadius:9999, background:P.muted, overflow:'hidden', marginBottom:6 }}> | |
| <div style={{ height:'100%', width:`${Math.min(100,pct)}%`, background:over?T.crimson:c.color, borderRadius:9999 }}/> | |
| </div> | |
| <div style={{ display:'flex', justifyContent:'space-between' }}> | |
| <div style={{ fontSize:11, color:P.inkSoft, fontVariantNumeric:'tabular-nums' }}>{fmt(r.spent)} đ</div> | |
| <div style={{ fontSize:10, color:P.inkMuted, letterSpacing:'0.05em', textTransform:'uppercase', fontWeight:600 }}>Tối đa {short(r.budget)}</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| <div style={{ marginTop:6, alignSelf:'flex-end', background:P.ink, color:P.cream, borderRadius:10, padding:'7px 13px', fontSize:11, fontFamily:T.fontBody, fontWeight:500, whiteSpace:'nowrap', boxShadow:'0 4px 14px rgba(0,0,0,0.24)', position:'relative' }}> | |
| ← Vuốt để xoá ngân sách | |
| <div style={{ position:'absolute', right:22, top:-4, width:8, height:8, background:P.ink, transform:'rotate(45deg)' }}/> | |
| </div> | |
| </div> | |
| <BottomNav active="budget" dark={dark}/> | |
| </div> | |
| ); | |
| } | |
| // ── Screen 22 · Budget multiple periods ────────────────────── | |
| function CCBudgetMulti({ dark=false }) { | |
| const P = palette(dark); | |
| const budgets = [ | |
| { id:'apr', title:'Tháng 4 · Cùng nhau', period:'25/03 – 25/04', recurring:true, total:16_000_000, spent:13_882_000, accent:T.crimson, accent2:T.coral, gradient:T.gradient }, | |
| { id:'travel', title:'Du lịch Đà Nẵng', period:'02/05 – 09/05', recurring:false, total:8_000_000, spent:1_240_000, accent:'#5F27CD', accent2:'#54A0FF', gradient:'linear-gradient(135deg, #5F27CD 0%, #54A0FF 100%)' }, | |
| { id:'wedding', title:'Quỹ cưới', period:'Đến 12/12', recurring:false, total:80_000_000, spent:22_500_000, accent:'#1DD1A1', accent2:'#00D2D3', gradient:'linear-gradient(135deg, #1DD1A1 0%, #00D2D3 100%)' }, | |
| ]; | |
| const active = budgets[0]; | |
| const remaining = active.total - active.spent; | |
| const pct = Math.round((active.spent/active.total)*100); | |
| const allocations = [ | |
| { name:'Nuôi bé ❤', budget:4_500_000, spent:3_200_000, color:'#FF6B6B' }, | |
| { name:'Về Đồng Nai 🌸', budget:3_000_000, spent:4_215_000, color:'#E91E8C' }, | |
| { name:'Ăn uống', budget:6_000_000, spent:4_439_000, color:'#FF9F43' }, | |
| ]; | |
| return ( | |
| <div style={{ position:'absolute', inset:0, background:P.cream, overflow:'hidden', fontFamily:T.fontBody, color:P.ink }}> | |
| <div style={{ height:60 }}/> | |
| <div style={{ height:52, display:'flex', alignItems:'center', padding:'0 20px' }}> | |
| <div style={{ flex:1, fontFamily:T.fontDisplay, fontWeight:700, fontSize:26, color:P.ink, letterSpacing:'-0.015em' }}>Ngân sách</div> | |
| <button style={{ border:0, background:T.gradient, color:'#fff', height:34, padding:'0 16px', borderRadius:9999, display:'flex', alignItems:'center', justifyContent:'center', gap:5, boxShadow:T.shadowFab, fontFamily:T.fontDisplay, fontWeight:600, fontSize:13 }}> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg> | |
| Thêm | |
| </button> | |
| </div> | |
| <div style={{ padding:'8px 0 14px', overflow:'hidden', position:'relative' }}> | |
| <div style={{ display:'flex', gap:12, padding:'0 20px' }}> | |
| {/* Active card */} | |
| <div style={{ flex:'0 0 calc(100% - 56px)', background:active.gradient, color:'#fff', borderRadius:22, padding:'18px 20px 20px', boxShadow:'0 12px 32px rgba(174,47,52,0.30)', position:'relative', overflow:'hidden' }}> | |
| <div style={{ position:'absolute', right:-16, bottom:-50, fontFamily:T.fontDisplay, fontSize:180, fontWeight:700, opacity:0.10, lineHeight:1, color:'#fff', pointerEvents:'none' }}>共</div> | |
| <div style={{ display:'flex', alignItems:'center', gap:6, marginBottom:10 }}> | |
| {active.recurring && ( | |
| <span style={{ display:'inline-flex', alignItems:'center', gap:4, background:'rgba(255,255,255,0.22)', color:'#fff', fontSize:10, fontWeight:600, letterSpacing:'0.06em', padding:'3px 8px', borderRadius:9999, fontFamily:T.fontBody }}> | |
| <svg width="9" height="9" viewBox="0 0 24 24" fill="currentColor"><path d="M17.65 6.35A7.958 7.958 0 0 0 12 4a8 8 0 1 0 7.75 10h-2.08A6 6 0 1 1 12 6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg> | |
| HÀNG THÁNG | |
| </span> | |
| )} | |
| <span style={{ fontSize:11, opacity:0.85, fontVariantNumeric:'tabular-nums' }}>{active.period}</span> | |
| </div> | |
| <div style={{ fontFamily:T.fontDisplay, fontWeight:700, fontSize:19, marginBottom:14, letterSpacing:'-0.01em' }}>{active.title}</div> | |
| <div style={{ display:'flex', alignItems:'flex-end', gap:10, marginBottom:8 }}> | |
| <div> | |
| <div style={{ fontSize:10, opacity:0.78, marginBottom:2, letterSpacing:'0.06em', textTransform:'uppercase', fontWeight:600 }}>Còn lại</div> | |
| <div style={{ fontFamily:T.fontDisplay, fontSize:32, fontWeight:700, letterSpacing:'-0.015em', fontVariantNumeric:'tabular-nums', lineHeight:1 }}>{short(remaining)}</div> | |
| </div> | |
| <div style={{ flex:1 }}/> | |
| <div style={{ textAlign:'right' }}> | |
| <div style={{ fontSize:10, opacity:0.78, marginBottom:2, letterSpacing:'0.06em', textTransform:'uppercase', fontWeight:600 }}>Đã chi</div> | |
| <div style={{ fontFamily:T.fontDisplay, fontSize:16, fontWeight:600, fontVariantNumeric:'tabular-nums' }}>{short(active.spent)} / {short(active.total)}</div> | |
| </div> | |
| </div> | |
| <div style={{ height:8, borderRadius:9999, background:'rgba(255,255,255,0.22)', overflow:'hidden' }}> | |
| <div style={{ height:'100%', width:`${pct}%`, background:'#fff', borderRadius:9999, boxShadow:'0 0 8px rgba(255,255,255,0.6)' }}/> | |
| </div> | |
| </div> | |
| {/* Peek of next card */} | |
| <div style={{ flex:'0 0 44px', height:200, background:budgets[1].gradient, borderRadius:22, boxShadow:'0 8px 20px rgba(95,39,205,0.20)', opacity:0.85 }}/> | |
| </div> | |
| {/* Pagination dots */} | |
| <div style={{ display:'flex', justifyContent:'center', gap:6, marginTop:12 }}> | |
| {budgets.map((b,i) => ( | |
| <span key={b.id} style={{ width:i===0?18:6, height:6, borderRadius:9999, background:i===0?T.crimson:P.inkMuted, opacity:i===0?1:0.4 }}/> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Allocations list */} | |
| <div style={{ padding:'0 20px 12px', display:'flex', flexDirection:'column', gap:8, overflow:'hidden', flex:1 }}> | |
| <div style={{ display:'flex', alignItems:'baseline', justifyContent:'space-between', padding:'0 2px 4px' }}> | |
| <div style={{ fontSize:11, fontWeight:700, color:P.inkSoft, letterSpacing:'0.08em', textTransform:'uppercase' }}>Theo danh mục</div> | |
| <div style={{ fontSize:11, color:P.inkMuted }}>{allocations.length} danh mục</div> | |
| </div> | |
| {allocations.slice(0,2).map((a,i) => { | |
| const p = Math.round((a.spent/a.budget)*100); | |
| const over = p>100; | |
| return ( | |
| <div key={i} style={{ background:P.surface, borderRadius:14, padding:'11px 14px', boxShadow:'0 2px 10px rgba(174,47,52,0.05)', border:`1px solid ${P.border}` }}> | |
| <div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:7 }}> | |
| <div style={{ width:10, height:10, borderRadius:'50%', background:a.color }}/> | |
| <div style={{ flex:1, fontSize:13, fontWeight:600, color:P.ink }}>{a.name}</div> | |
| <div style={{ fontSize:12, fontWeight:700, color:over?T.crimson:'#F5A623', fontFamily:T.fontDisplay, fontVariantNumeric:'tabular-nums' }}>{p}%</div> | |
| </div> | |
| <div style={{ height:5, borderRadius:9999, background:P.muted, overflow:'hidden' }}> | |
| <div style={{ height:'100%', width:`${Math.min(100,p)}%`, background:over?T.crimson:`linear-gradient(90deg, ${a.color}, ${a.color}cc)`, borderRadius:9999 }}/> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| <BottomNav active="budget" dark={dark}/> | |
| </div> | |
| ); | |
| } | |
| // ── App ──────────────────────────────────────────────────────── | |
| function Phone({ dark, children, label }) { | |
| return ( | |
| <div className="artboard"> | |
| <IOSDevice width={390} height={844} dark={dark}>{children}</IOSDevice> | |
| <div className="artboard-label">{label}</div> | |
| </div> | |
| ); | |
| } | |
| function App() { | |
| const [dark, setDark] = React.useState(false); | |
| return ( | |
| <div> | |
| {/* Tweaks bar */} | |
| <div style={{ position:'fixed', top:16, right:16, zIndex:100, background:'#FFF8F6', border:'1px solid #EDD9D3', borderRadius:12, padding:'10px 14px', display:'flex', alignItems:'center', gap:10, boxShadow:'0 4px 16px rgba(174,47,52,0.12)' }}> | |
| <span style={{ fontFamily:"'Chakra Petch',sans-serif", fontSize:11, fontWeight:600, letterSpacing:'0.12em', textTransform:'uppercase', color:'#AE2F34' }}>Theme</span> | |
| {['light','dark'].map(v => ( | |
| <button key={v} onClick={() => setDark(v==='dark')} style={{ border:0, background:(dark?'dark':'light')===v?'#AE2F34':'transparent', color:(dark?'dark':'light')===v?'#fff':'#7A5C56', fontFamily:"'Outfit',sans-serif", fontSize:11, fontWeight:500, padding:'4px 10px', borderRadius:9999, cursor:'pointer' }}>{v}</button> | |
| ))} | |
| </div> | |
| <div style={{ padding:'60px 60px 80px' }}> | |
| <div style={{ marginBottom:60 }}> | |
| <div className="section-title">Budget · add flow</div> | |
| <div className="section-sub">Step 1: period, name, scope, cadence → Step 2: per-category allocations with keypad</div> | |
| <div className="row" style={{ marginTop:32 }}> | |
| <Phone dark={dark} label="19 · Add budget · period + total"><CCBudgetAddStep1 dark={dark}/></Phone> | |
| <Phone dark={dark} label="20 · Add budget · allocate + keypad"><CCBudgetAddStep2 dark={dark}/></Phone> | |
| </div> | |
| </div> | |
| <div> | |
| <div className="section-title">Budget · multiple periods</div> | |
| <div className="section-sub">Carousel switcher when N≥2 active budgets · swipe-to-delete on individual cards</div> | |
| <div className="row" style={{ marginTop:32 }}> | |
| <Phone dark={dark} label="22 · Budget · multiple periods"><CCBudgetMulti dark={dark}/></Phone> | |
| <Phone dark={dark} label="21 · Budget · swipe to delete"><CCBudgetSwipeDelete dark={dark}/></Phone> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| ReactDOM.createRoot(document.getElementById('root')).render(<App/>); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment