feat: add footer with copyright, live dev ticker, and Gitea repo link

This commit is contained in:
2026-03-08 00:35:35 -06:00
parent dc45cb7d83
commit 981fa3bea4

View File

@@ -1,17 +1,78 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import ViolationForm from './components/ViolationForm';
import Dashboard from './components/Dashboard';
import ReadmeModal from './components/ReadmeModal';
import ToastProvider from './components/ToastProvider';
const REPO_URL = 'https://git.alwisp.com/jason/cpas';
const PROJECT_START = new Date('2026-03-06T11:33:32-06:00');
function elapsed(from) {
const totalSec = Math.floor((Date.now() - from.getTime()) / 1000);
const d = Math.floor(totalSec / 86400);
const h = Math.floor((totalSec % 86400) / 3600);
const m = Math.floor((totalSec % 3600) / 60);
const s = totalSec % 60;
return `${d}d ${String(h).padStart(2,'0')}h ${String(m).padStart(2,'0')}m ${String(s).padStart(2,'0')}s`;
}
function DevTicker() {
const [tick, setTick] = useState(() => elapsed(PROJECT_START));
useEffect(() => {
const id = setInterval(() => setTick(elapsed(PROJECT_START)), 1000);
return () => clearInterval(id);
}, []);
return (
<span title="Time since first commit" style={{ display: 'inline-flex', alignItems: 'center', gap: '5px' }}>
<span style={{
width: '7px', height: '7px', borderRadius: '50%',
background: '#22c55e', display: 'inline-block',
animation: 'cpas-pulse 1.4s ease-in-out infinite',
}} />
{tick}
</span>
);
}
function GiteaIcon() {
return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style={{ verticalAlign: 'middle' }}>
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"/>
</svg>
);
}
function AppFooter() {
const year = new Date().getFullYear();
return (
<>
<style>{`
@keyframes cpas-pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(0.75); }
}
`}</style>
<footer style={sf.footer}>
<span style={sf.copy}>© {year} Jason Stedwell</span>
<span style={sf.sep}>·</span>
<DevTicker />
<span style={sf.sep}>·</span>
<a href={REPO_URL} target="_blank" rel="noopener noreferrer" style={sf.link}>
<GiteaIcon /> cpas
</a>
</footer>
</>
);
}
const tabs = [
{ id: 'dashboard', label: '📊 Dashboard' },
{ id: 'violation', label: '+ New Violation' },
];
const s = {
app: { minHeight: '100vh', background: '#050608', fontFamily: "'Segoe UI', Arial, sans-serif", color: '#f8f9fa' },
nav: { background: '#000000', padding: '0 40px', display: 'flex', alignItems: 'center', gap: 0, borderBottom: '1px solid #333' },
app: { minHeight: '100vh', background: '#050608', fontFamily: "'Segoe UI', Arial, sans-serif", color: '#f8f9fa', display: 'flex', flexDirection: 'column' },
nav: { background: '#000000', padding: '0 40px', display: 'flex', alignItems: 'center', gap: 0, borderBottom: '1px solid #333' },
logoWrap: { display: 'flex', alignItems: 'center', marginRight: '32px', padding: '14px 0' },
logoImg: { height: '28px', marginRight: '10px' },
logoText: { color: '#f8f9fa', fontWeight: 800, fontSize: '18px', letterSpacing: '0.5px' },
@@ -22,7 +83,6 @@ const s = {
cursor: 'pointer', fontWeight: active ? 700 : 400, fontSize: '14px',
background: 'none', border: 'none',
}),
// Docs button sits flush-right in the nav
docsBtn: {
marginLeft: 'auto',
background: 'none',
@@ -38,9 +98,34 @@ const s = {
alignItems: 'center',
gap: '6px',
},
main: { flex: 1 },
card: { maxWidth: '1100px', margin: '30px auto', background: '#111217', borderRadius: '10px', boxShadow: '0 2px 16px rgba(0,0,0,0.6)', border: '1px solid #222' },
};
const sf = {
footer: {
borderTop: '1px solid #1a1b22',
padding: '12px 40px',
display: 'flex',
alignItems: 'center',
gap: '12px',
fontSize: '11px',
color: 'rgba(248,249,250,0.35)',
background: '#000',
flexShrink: 0,
},
copy: { color: 'rgba(248,249,250,0.35)' },
sep: { color: 'rgba(248,249,250,0.15)' },
link: {
color: 'rgba(248,249,250,0.35)',
textDecoration: 'none',
display: 'inline-flex',
alignItems: 'center',
gap: '4px',
transition: 'color 0.15s',
},
};
export default function App() {
const [tab, setTab] = useState('dashboard');
const [showReadme, setShowReadme] = useState(false);
@@ -65,10 +150,14 @@ export default function App() {
</button>
</nav>
<div style={s.card}>
{tab === 'dashboard' ? <Dashboard /> : <ViolationForm />}
<div style={s.main}>
<div style={s.card}>
{tab === 'dashboard' ? <Dashboard /> : <ViolationForm />}
</div>
</div>
<AppFooter />
{showReadme && <ReadmeModal onClose={() => setShowReadme(false)} />}
</div>
</ToastProvider>