Files
hanarang-dashboard/frontend/components/live/LiveWorkboard.tsx

224 lines
5.6 KiB
TypeScript

'use client';
import { useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import SisterActivityLane from './SisterActivityLane';
import WorkflowTimeline from './WorkflowTimeline';
import ActiveTaskFocus from './ActiveTaskFocus';
import { mockLiveWorkboard, rotateEvents } from '@/lib/control/mock-events';
const Root = styled.div`
display: flex;
flex-direction: column;
gap: var(--space-lg);
`;
const Hero = styled.header`
border: 1px solid rgba(255,255,255,0.12);
border-radius: 28px;
padding: 24px;
background:
radial-gradient(circle at 16% 18%, rgba(255,143,112,0.18), transparent 34%),
radial-gradient(circle at 82% 12%, rgba(139,211,255,0.16), transparent 28%),
linear-gradient(135deg, rgba(255,255,255,0.075), rgba(255,255,255,0.02));
`;
const HeroTop = styled.div`
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
@media (max-width: 767px) {
flex-direction: column;
}
`;
const TitleBlock = styled.div`
display: grid;
gap: 8px;
`;
const Label = styled.div`
font-family: var(--font-mono);
color: var(--text-secondary);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
`;
const Title = styled.h1`
margin: 0;
font-size: clamp(30px, 5vw, 58px);
line-height: 0.95;
letter-spacing: -0.055em;
`;
const Summary = styled.p`
margin: 0;
max-width: 760px;
color: var(--text-secondary);
font-size: 14px;
`;
const SourceBadge = styled.div`
display: inline-flex;
align-items: center;
gap: 8px;
border: 1px solid rgba(255, 204, 102, 0.35);
border-radius: 999px;
padding: 8px 12px;
color: #ffcc66;
font-family: var(--font-mono);
font-size: 11px;
white-space: nowrap;
`;
const Dot = styled.span`
width: 7px;
height: 7px;
border-radius: 50%;
background: #ffcc66;
box-shadow: 0 0 18px rgba(255, 204, 102, 0.95);
`;
const PhaseGrid = styled.div`
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 8px;
margin-top: 22px;
@media (max-width: 767px) {
grid-template-columns: 1fr;
}
`;
const PhaseCell = styled.div<{ $active: boolean }>`
border: 1px solid ${({ $active }) => ($active ? 'rgba(255,255,255,0.36)' : 'rgba(255,255,255,0.09)')};
border-radius: 14px;
padding: 10px;
color: ${({ $active }) => ($active ? 'var(--text-primary)' : 'var(--text-secondary)')};
background: ${({ $active }) => ($active ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.16)')};
font-family: var(--font-mono);
font-size: 11px;
`;
const MainGrid = styled.div`
display: grid;
grid-template-columns: minmax(0, 1fr) 360px;
gap: var(--space-lg);
@media (max-width: 1100px) {
grid-template-columns: 1fr;
}
`;
const LanesGrid = styled.section`
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-md);
@media (max-width: 900px) {
grid-template-columns: 1fr;
}
`;
const BottomGrid = styled.div`
display: grid;
grid-template-columns: minmax(0, 1.35fr) minmax(280px, 0.65fr);
gap: var(--space-lg);
@media (max-width: 1100px) {
grid-template-columns: 1fr;
}
`;
const NoteCard = styled.section`
border: 1px solid var(--border-color);
border-radius: 22px;
padding: 18px;
background: rgba(255,255,255,0.025);
`;
const NoteTitle = styled.h2`
margin: 0 0 8px;
font-size: 15px;
`;
const NoteText = styled.p`
margin: 0;
color: var(--text-secondary);
font-size: 13px;
`;
const phases = [
['planning', 'PLAN'],
['building', 'BUILD'],
['reviewing', 'REVIEW'],
['waiting_approval', 'APPROVAL'],
['deploying', 'DEPLOY'],
] as const;
export default function LiveWorkboard() {
const [tick, setTick] = useState(0);
const [selectedId, setSelectedId] = useState(mockLiveWorkboard.events[0]?.id ?? '');
const events = useMemo(() => rotateEvents(mockLiveWorkboard.events, tick), [tick]);
const selected = events.find((event) => event.id === selectedId) ?? events[0];
useEffect(() => {
const timer = window.setInterval(() => {
setTick((value) => value + 1);
}, 4200);
return () => window.clearInterval(timer);
}, []);
return (
<Root>
<Hero>
<HeroTop>
<TitleBlock>
<Label>4 Sisters Live Workboard</Label>
<Title> 4 </Title>
<Summary>{mockLiveWorkboard.workflow.summary}</Summary>
</TitleBlock>
<SourceBadge aria-label="data source mock">
<Dot /> source: {mockLiveWorkboard.source} · v0
</SourceBadge>
</HeroTop>
<PhaseGrid>
{phases.map(([phase, label]) => (
<PhaseCell key={phase} $active={mockLiveWorkboard.workflow.phase === phase}>
{label}
</PhaseCell>
))}
</PhaseGrid>
</Hero>
<MainGrid>
<LanesGrid>
{mockLiveWorkboard.sisters.map((lane) => (
<SisterActivityLane key={lane.id} lane={lane} />
))}
</LanesGrid>
<ActiveTaskFocus selectedEvent={selected} approvals={mockLiveWorkboard.approvals} />
</MainGrid>
<BottomGrid>
<WorkflowTimeline
events={events}
selectedId={selected.id}
onSelect={(event) => setSelectedId(event.id)}
/>
<NoteCard>
<NoteTitle>v0 </NoteTitle>
<NoteText>
mock event stream이야. 4 . Socket.IO live event와 `/control/summary` .
</NoteText>
</NoteCard>
</BottomGrid>
</Root>
);
}