first commit. added CI, Docker compose, blank client and server. used unit tests, they pass
This commit is contained in:
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>,
|
||||
)
|
||||
Reference in New Issue
Block a user