You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
128 lines
3.8 KiB
TypeScript
128 lines
3.8 KiB
TypeScript
// frontend/components/dashboard/DashboardClient.tsx
|
|
"use client";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { Icon } from "@/components/Icon";
|
|
import type { IconName } from "@/components/icons/paths";
|
|
import { useDashboard } from "@/lib/dashboard/useDashboard";
|
|
import { useProactive } from "@/lib/hooks/useProactive";
|
|
import { acceptProactive, dismissProactive } from "@/lib/proactive/api";
|
|
import { getWeeklyReview } from "@/lib/weekly/api";
|
|
import type { WeeklyReview as WeeklyReviewT } from "@/lib/types";
|
|
import { ProactiveBanner } from "@/components/proactive/ProactiveBanner";
|
|
import { WeeklyReview } from "@/components/weekly/WeeklyReview";
|
|
import { ApprovalSummaryCard } from "./ApprovalSummaryCard";
|
|
import { DashboardSkeleton } from "./DashboardSkeleton";
|
|
import { GoalsCard } from "./GoalsCard";
|
|
import { HeroBriefing } from "./HeroBriefing";
|
|
import { InboxSummaryCard } from "./InboxSummaryCard";
|
|
import { ScheduleCard } from "./ScheduleCard";
|
|
import { TaskSummaryCard } from "./TaskSummaryCard";
|
|
|
|
export function DashboardClient() {
|
|
const { data, isLoading, error, mutate } = useDashboard();
|
|
const { data: proactive, refresh: refreshProactive } = useProactive();
|
|
const [weekly, setWeekly] = useState<WeeklyReviewT | null>(null);
|
|
const shellRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
let live = true;
|
|
getWeeklyReview()
|
|
.then((wr) => {
|
|
if (live) setWeekly(wr);
|
|
})
|
|
.catch(() => {});
|
|
return () => {
|
|
live = false;
|
|
};
|
|
}, []);
|
|
|
|
const onAccept = async (id: string) => {
|
|
try {
|
|
await acceptProactive(id);
|
|
} finally {
|
|
refreshProactive();
|
|
}
|
|
};
|
|
const onDismiss = async (id: string) => {
|
|
try {
|
|
await dismissProactive(id);
|
|
} finally {
|
|
refreshProactive();
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const el = shellRef.current;
|
|
if (!el) return;
|
|
const id = requestAnimationFrame(() => el.classList.add("entered"));
|
|
return () => cancelAnimationFrame(id);
|
|
}, [data]);
|
|
|
|
if (isLoading) return <DashboardSkeleton />;
|
|
if (error || !data) {
|
|
return (
|
|
<div className="dash-error" role="alert">
|
|
<p>대시보드를 불러오지 못했어요.</p>
|
|
<button className="mini-cta" onClick={() => mutate()}>
|
|
<Icon name="swap" /> 다시 시도
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const {
|
|
user,
|
|
briefing,
|
|
saved_today,
|
|
today_routed,
|
|
schedule,
|
|
task_summary,
|
|
goals,
|
|
approvals_summary,
|
|
inbox_recent,
|
|
badges,
|
|
} = data;
|
|
|
|
return (
|
|
<div className="dash-page" ref={shellRef}>
|
|
<div className="pagehead">
|
|
<div>
|
|
<div className="ph-eyebrow">
|
|
<span>{briefing.today}</span>
|
|
<span className="sep" />
|
|
<span className="wx">
|
|
<Icon name={briefing.weather.icon as IconName} />
|
|
{briefing.weather.cond}
|
|
</span>
|
|
</div>
|
|
<h1 className="ph-title">
|
|
좋은 아침이에요, {user.name}님 <em>☀</em>
|
|
</h1>
|
|
</div>
|
|
<div className="ph-search">
|
|
<Icon name="search" />
|
|
<input placeholder="아리에게 무엇이든…" aria-label="검색" />
|
|
</div>
|
|
</div>
|
|
|
|
<ProactiveBanner cards={proactive ?? []} onAccept={onAccept} onDismiss={onDismiss} />
|
|
|
|
<div className="work">
|
|
<div className="board">
|
|
<HeroBriefing briefing={briefing} />
|
|
<ApprovalSummaryCard
|
|
items={approvals_summary}
|
|
savedToday={saved_today}
|
|
pending={badges.appr}
|
|
/>
|
|
<InboxSummaryCard items={inbox_recent} todayRouted={today_routed} />
|
|
<ScheduleCard items={schedule} />
|
|
<TaskSummaryCard summary={task_summary} />
|
|
<GoalsCard goals={goals} />
|
|
{weekly && <WeeklyReview review={weekly} />}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|