diligence/frontend/src/pages/Support.jsx
Claude c075eb9257 Stream A: DW brand theme — Instrument Sans + IBM Plex Mono, light/dark mode, all hardcoded colors replaced
- Replace Solar Momentum palette with DiligenceWorks brand identity
- Typography: Outfit → Instrument Sans, Plus Jakarta Sans → IBM Plex Mono (data/stats)
- CSS variables: --orange* → --accent*, new DW blue #2952CC / #6B9BFF
- Dark mode: prefers-color-scheme media query + manual data-theme override
- Border radii: 14/10/20px → 8/6/12px (professional, less playful)
- Font weights: 800/900 → 600/700 (DW uses lighter weights)
- Fixed 30+ hardcoded hex values across 16 files (UI-06)
- Gate banners, progress bars, category chips all use CSS variables
2026-06-21 23:12:25 +00:00

162 lines
5.6 KiB
JavaScript

import { useState, useEffect, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import { api } from '../api'
export default function Support() {
const [messages, setMessages] = useState([])
const [loading, setLoading] = useState(true)
const [sending, setSending] = useState(false)
const [input, setInput] = useState('')
const [confirmation, setConfirmation] = useState(null)
const bottomRef = useRef(null)
const navigate = useNavigate()
useEffect(() => { loadThread() }, [])
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages])
async function loadThread() {
try {
const data = await api.getThread()
setMessages(data.messages || [])
} catch (err) { console.error(err) }
finally { setLoading(false) }
}
async function handleSend(e) {
e.preventDefault()
const body = input.trim()
if (!body || sending) return
setSending(true)
setConfirmation(null)
try {
const result = await api.sendSupportMessage(body)
setInput('')
setConfirmation(result.message)
await loadThread()
} catch (err) {
setConfirmation(err.message)
}
finally { setSending(false) }
}
function formatTime(iso) {
const d = new Date(iso)
const now = new Date()
const diff = now - d
if (diff < 60000) return 'Just now'
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
+ ' ' + d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
}
if (loading) return <div style={{ padding: '2rem', textAlign: 'center', color: 'var(--text-3)' }}>Loading...</div>
return (
<div style={{ display: 'flex', flexDirection: 'column', height: 'calc(100dvh - 64px)', maxWidth: '600px', margin: '0 auto' }}>
{/* Header */}
<div style={{
padding: '16px', borderBottom: '1px solid var(--divider)',
display: 'flex', alignItems: 'center', gap: '12px',
}}>
<button onClick={() => navigate('/')} style={{
background: 'transparent', color: 'var(--text-3)', padding: '4px 0', fontSize: '1.2rem',
}}></button>
<div>
<h1 style={{ fontFamily: 'var(--font-display)', fontSize: '1.2rem', fontWeight: 800, margin: 0 }}>
Support
</h1>
<p style={{ color: 'var(--text-3)', fontSize: '0.75rem', margin: 0 }}>
Ask a question we'll get back to you
</p>
</div>
</div>
{/* Messages */}
<div style={{ flex: 1, overflow: 'auto', padding: '16px', display: 'flex', flexDirection: 'column', gap: '12px' }}>
{messages.length === 0 && (
<div style={{ textAlign: 'center', color: 'var(--text-3)', fontSize: '0.85rem', marginTop: '2rem' }}>
<div style={{ fontSize: '2rem', marginBottom: '8px' }}>💬</div>
No messages yet. Ask anything about your workouts, program, or the app.
</div>
)}
{messages.map(m => (
<div key={m.id} style={{
display: 'flex',
justifyContent: m.sender === 'user' ? 'flex-end' : 'flex-start',
}}>
<div style={{
maxWidth: '80%',
padding: '10px 14px',
borderRadius: m.sender === 'user'
? 'var(--r) var(--r) 4px var(--r)'
: 'var(--r) var(--r) var(--r) 4px',
background: m.sender === 'user' ? 'var(--accent)' : 'var(--card)',
color: m.sender === 'user' ? '#fff' : 'var(--text)',
boxShadow: 'var(--shadow-1)',
fontSize: '0.88rem',
lineHeight: 1.5,
}}>
<div>{m.body}</div>
<div style={{
fontSize: '0.68rem', marginTop: '4px',
opacity: 0.7, textAlign: 'right',
}}>
{formatTime(m.created_at)}
{m.sender === 'user' && m.read_at && ' '}
</div>
</div>
</div>
))}
{confirmation && (
<div style={{
textAlign: 'center', fontSize: '0.8rem', color: 'var(--green-dark)',
padding: '8px', background: 'var(--green-bg)', borderRadius: 'var(--r-sm)',
}}>
{confirmation}
</div>
)}
<div ref={bottomRef} />
</div>
{/* Input */}
<form onSubmit={handleSend} style={{
padding: '12px 16px', borderTop: '1px solid var(--divider)',
display: 'flex', gap: '8px', background: 'var(--card)',
}}>
<input
type="text"
value={input}
onChange={e => setInput(e.target.value)}
placeholder="Type your question..."
maxLength={2000}
style={{
flex: 1, padding: '12px 14px', borderRadius: 'var(--r-full)',
border: '1px solid var(--divider)', fontFamily: 'var(--font)',
fontSize: '0.88rem', background: 'var(--bg)',
}}
/>
<button
type="submit"
disabled={!input.trim() || sending}
style={{
background: sending ? 'var(--text-3)' : 'var(--accent)',
color: '#fff', borderRadius: 'var(--r-full)',
width: '44px', height: '44px', padding: 0,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: '1.1rem', boxShadow: 'var(--shadow-accent)',
opacity: !input.trim() ? 0.5 : 1,
}}
>
</button>
</form>
</div>
)
}