v2: In-app support chat with Telegram notifications (REVIEW — not deployed)
Backend: - New models: SupportThread, SupportMessage (models/support.py) - Support router: user thread/messages, admin threads/reply, Telegram outbound - Context auto-attached: active program, points, gate status, last workout - Rate limited: 10 messages/user/day - Admin auth: username == 'scot' check - Telegram: fire-and-forget sendMessage via @AureusGoldBot, never blocks - config.py: added telegram_bot_token, telegram_chat_id Frontend: - Support.jsx: chat-style thread, send messages, confirmation, auto-scroll - SupportAdmin.jsx: thread list with unread badges, thread detail with context sidebar, reply - App.jsx: floating '?' help button with unread badge (polls every 60s), routes - api.js: support endpoints (user + admin) Config: - docker-compose.yml: TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID env vars NOT DEPLOYED — requires Telegram env vars in Coolify
This commit is contained in:
parent
42632b1b50
commit
10f13a9f62
10 changed files with 988 additions and 4 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { Routes, Route, Navigate, NavLink, useNavigate } from 'react-router-dom'
|
||||
import { Routes, Route, Navigate, NavLink, useNavigate, useLocation } from 'react-router-dom'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { hasToken, clearToken, api } from './api'
|
||||
import Login from './pages/Login'
|
||||
|
|
@ -11,12 +11,65 @@ import Onboarding from './pages/Onboarding'
|
|||
import WeekView from './pages/WeekView'
|
||||
import ProgramSearch from './pages/ProgramSearch'
|
||||
import ProgramDetail from './pages/ProgramDetail'
|
||||
import Support from './pages/Support'
|
||||
import SupportAdmin from './pages/SupportAdmin'
|
||||
|
||||
function ProtectedRoute({ children }) {
|
||||
if (!hasToken()) return <Navigate to="/login" />
|
||||
return children
|
||||
}
|
||||
|
||||
function HelpButton() {
|
||||
const [unread, setUnread] = useState(0)
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
|
||||
// Don't show on login/onboarding/support pages
|
||||
const hidden = ['/login', '/onboarding', '/support'].some(p => location.pathname.startsWith(p))
|
||||
if (hidden) return null
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasToken()) return
|
||||
api.getUnreadCount()
|
||||
.then(d => setUnread(d.unread || 0))
|
||||
.catch(() => {})
|
||||
// Poll every 60s for new replies
|
||||
const interval = setInterval(() => {
|
||||
api.getUnreadCount()
|
||||
.then(d => setUnread(d.unread || 0))
|
||||
.catch(() => {})
|
||||
}, 60000)
|
||||
return () => clearInterval(interval)
|
||||
}, [location.pathname])
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => navigate('/support')}
|
||||
style={{
|
||||
position: 'fixed', top: '12px', right: '12px', zIndex: 90,
|
||||
width: '40px', height: '40px', borderRadius: '50%',
|
||||
background: 'var(--card)', boxShadow: 'var(--shadow-2)',
|
||||
border: '1px solid var(--divider)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: '1.1rem', padding: 0, cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
?
|
||||
{unread > 0 && (
|
||||
<span style={{
|
||||
position: 'absolute', top: '-4px', right: '-4px',
|
||||
background: 'var(--red)', color: '#fff', fontSize: '0.6rem', fontWeight: 800,
|
||||
width: '18px', height: '18px', borderRadius: '50%',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
border: '2px solid var(--bg)',
|
||||
}}>
|
||||
{unread}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function NavBar() {
|
||||
return (
|
||||
<nav className="nav-bar">
|
||||
|
|
@ -42,6 +95,7 @@ function NavBar() {
|
|||
export default function App() {
|
||||
return (
|
||||
<>
|
||||
{hasToken() && <HelpButton />}
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/onboarding" element={<ProtectedRoute><Onboarding /></ProtectedRoute>} />
|
||||
|
|
@ -53,6 +107,9 @@ export default function App() {
|
|||
<Route path="/rewards" element={<ProtectedRoute><Rewards /></ProtectedRoute>} />
|
||||
<Route path="/week" element={<ProtectedRoute><WeekView /></ProtectedRoute>} />
|
||||
<Route path="/settings" element={<ProtectedRoute><Settings /></ProtectedRoute>} />
|
||||
<Route path="/support" element={<ProtectedRoute><Support /></ProtectedRoute>} />
|
||||
<Route path="/support/admin" element={<ProtectedRoute><SupportAdmin /></ProtectedRoute>} />
|
||||
<Route path="/support/admin/:threadId" element={<ProtectedRoute><SupportAdmin /></ProtectedRoute>} />
|
||||
</Routes>
|
||||
{hasToken() && <NavBar />}
|
||||
</>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue