first commit. added CI, Docker compose, blank client and server. used unit tests, they pass
CI / Go (build + vet + test) (push) Successful in 10m49s
CI / Web (build + lint) (push) Successful in 10m31s

This commit is contained in:
vantablackTea
2026-09-05 01:04:12 +03:00
commit 1a669c948a
30 changed files with 2208 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
import { useEffect, useState } from 'react'
import { pingApi, type PingResponse } from './api'
import './index.css'
type ApiState =
| { status: 'loading' }
| { status: 'ok'; data: PingResponse }
| { status: 'error'; error: string }
function App() {
const [api, setApi] = useState<ApiState>({ status: 'loading' })
useEffect(() => {
let cancelled = false
pingApi()
.then((data) => {
if (!cancelled) setApi({ status: 'ok', data })
})
.catch((err: unknown) => {
if (!cancelled) setApi({ status: 'error', error: String(err) })
})
return () => {
cancelled = true
}
}, [])
return (
<main className="shell">
<h1>Messenger</h1>
<div className="card">
<div className="status-row">
<span className={`dot ${api.status}`} aria-hidden />
<span className="status-label">
{api.status === 'loading' && 'API: connecting…'}
{api.status === 'ok' && 'API: connected'}
{api.status === 'error' && 'API: unreachable'}
</span>
</div>
{api.status === 'ok' && (
<p className="detail">
{api.data.message} · server time {api.data.time}
</p>
)}
{api.status === 'error' && <p className="detail">{api.error}</p>}
</div>
</main>
)
}
export default App
+12
View File
@@ -0,0 +1,12 @@
export interface PingResponse {
message: string
time: string
}
export async function pingApi(): Promise<PingResponse> {
const res = await fetch('/api/v1/ping')
if (!res.ok) {
throw new Error(`API responded with ${res.status}`)
}
return res.json()
}
+84
View File
@@ -0,0 +1,84 @@
:root {
color-scheme: dark;
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
--bg: #0f1115;
--card: #1a1d24;
--text: #e8eaf0;
--muted: #9aa1ae;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: grid;
place-items: center;
}
.shell {
text-align: center;
padding: 2rem;
}
.shell h1 {
margin-bottom: 2rem;
font-size: 2.25rem;
letter-spacing: 0.06em;
}
.card {
background: var(--card);
border: 1px solid #262b35;
border-radius: 12px;
padding: 1.5rem 2.5rem;
min-width: 280px;
}
.status-row {
display: flex;
align-items: center;
justify-content: center;
gap: 0.6rem;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--muted);
}
.dot.ok {
background: #34d399;
box-shadow: 0 0 8px #34d39988;
}
.dot.error {
background: #f87171;
box-shadow: 0 0 8px #f8717188;
}
.dot.loading {
animation: pulse 1s ease-in-out infinite;
}
.status-label {
font-weight: 600;
}
.detail {
margin: 0.75rem 0 0;
color: var(--muted);
font-size: 0.875rem;
}
@keyframes pulse {
50% {
opacity: 0.35;
}
}
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)