Skip to content

Commit

Permalink
Merge pull request #253 from boostcampwm-2024/feat/chart
Browse files Browse the repository at this point in the history
✨ feat: 차트 컴포넌트 구현, 차트 API연동
  • Loading branch information
jungmyunggi authored Dec 2, 2024
2 parents 880db32 + aa569ae commit 0302197
Show file tree
Hide file tree
Showing 15 changed files with 1,543 additions and 21 deletions.
899 changes: 891 additions & 8 deletions client/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.27.0",
"recharts": "^2.13.3",
"socket.io-client": "^4.8.1",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
Expand Down
20 changes: 20 additions & 0 deletions client/src/api/services/chart/chart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { axiosInstance } from "@/api/instance";
import { ChartResponse, ChartPlatforms } from "@/types/chart";

export const chart = {
//금일 조회수
getToday: async (): Promise<ChartResponse> => {
const response = await axiosInstance.get<ChartResponse>("/api/statistic/today?limit=5");
return response.data;
},
//전체 조회수
getAll: async (): Promise<ChartResponse> => {
const response = await axiosInstance.get<ChartResponse>("/api/statistic/all?limit=5");
return response.data;
},
//금일 조회수
getPlatform: async (): Promise<ChartPlatforms> => {
const response = await axiosInstance.get<ChartPlatforms>("/api/statistic/platform");
return response.data;
},
};
75 changes: 75 additions & 0 deletions client/src/components/chart/BarChartItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useState, useEffect, useRef } from "react";

import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";

import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";

import { ChartType } from "@/types/chart";

type BarType = {
data: ChartType[];
title: string;
description: string;
color: boolean;
};

export default function BarChartItem({ data, title, description, color }: BarType) {
const [componentWidth, setComponentWidth] = useState(0);
const cardRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
setComponentWidth(entry.contentRect.width);
}
});

if (cardRef.current) {
resizeObserver.observe(cardRef.current);
}
return () => {
if (cardRef.current) {
resizeObserver.unobserve(cardRef.current);
}
};
}, []);
const truncateText = (text: string) => {
const charWidth = 55;
const maxChars = Math.floor(componentWidth / charWidth);

return text.length > maxChars ? `${text.slice(0, Math.max(0, maxChars - 3))}...` : text;
};

const chartConfig = {
desktop: {
label: "Desktop",
color: color ? "hsl(200, 70%, 68%)" : "hsl(120, 70%, 68%)",
},
} satisfies ChartConfig;

return (
<Card ref={cardRef} className="w-[50%]">
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig}>
<BarChart accessibilityLayer data={data}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="title"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) => truncateText(value)}
/>
<ChartTooltip cursor={true} content={<ChartTooltipContent />} />
<Bar dataKey="viewCount" fill="var(--color-desktop)" radius={8} />
</BarChart>
</ChartContainer>
</CardContent>
</Card>
);
}
24 changes: 24 additions & 0 deletions client/src/components/chart/Chart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import BarChartItem from "@/components/chart/BarChartItem";
import ChartSkeleton from "@/components/chart/ChartSkeleton";
import PieChartItem from "@/components/chart/PieChartItem";

import { useChart } from "@/hooks/queries/useChart";

export default function Chart() {
const { data, isLoading, error } = useChart();
if (!data || isLoading) return <ChartSkeleton />;
if (error) return <p>Error loading data</p>;
const { chartAll, chartToday, chartPlatform } = data;

return (
<div className="p-8">
<div className="flex">
<BarChartItem title="전체 조회수" description="전체 조회수 TOP5" data={chartAll.data} color={true} />
<BarChartItem title="오늘의 조회수" description="금일 조회수 TOP5" data={chartToday.data} color={false} />
</div>
<div>
<PieChartItem data={chartPlatform.data} title="플랫폼별 블로그 수" />
</div>
</div>
);
}
16 changes: 16 additions & 0 deletions client/src/components/chart/ChartSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import BarChartItem from "@/components/chart/BarChartItem";
import PieChartItem from "@/components/chart/PieChartItem";

export default function ChartSkeleton() {
return (
<div className="p-8">
<div className="flex">
<BarChartItem title="전체 조회수" description="전체 조회수 TOP5" data={[]} color={true} />
<BarChartItem title="오늘의 조회수" description="금일 조회수 TOP5" data={[]} color={false} />
</div>
<div>
<PieChartItem data={[]} title="플랫폼별 블로그 수" />
</div>
</div>
);
}
55 changes: 55 additions & 0 deletions client/src/components/chart/PieChartItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { LabelList, Pie, PieChart, Cell } from "recharts";

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";

import { ChartPlatform } from "@/types/chart";

type BarType = {
data: ChartPlatform[];
title: string;
};

const chartConfig: ChartConfig = {
count: {
label: "Count",
},
tistory: {
label: "Tistory",
color: "hsl(210, 60%, 70%)",
},
velog: {
label: "Velog",
color: "hsl(150, 60%, 70%)",
},
etc: {
label: "etc",
color: "hsl(30, 60%, 70%)",
},
} satisfies ChartConfig;

export default function PieChartItem({ data, title }: BarType) {
return (
<Card className="flex flex-col">
<CardHeader className="items-center pb-0">
<CardTitle>{title}</CardTitle>
</CardHeader>
<CardContent className="flex-1 pb-0">
<ChartContainer
config={chartConfig}
className="mx-auto aspect-square max-h-[250px] [&_.recharts-text]:fill-background"
>
<PieChart>
<ChartTooltip content={<ChartTooltipContent hideLabel />} />
<Pie data={data} dataKey="count" nameKey="platform">
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={chartConfig[entry.platform]?.color || "#ccc"} />
))}
<LabelList dataKey="platform" className="fill-background" stroke="none" fontSize={12} />
</Pie>
</PieChart>
</ChartContainer>
</CardContent>
</Card>
);
}
2 changes: 1 addition & 1 deletion client/src/components/chat/ChatButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function OpenChat() {
return (
<button
onClick={toggleSidebar}
className="fixed text-white bottom-[18.5rem] right-7 bg-[#3498DB] hover:bg-[#2980B9] !rounded-full p-3"
className="fixed text-white bottom-[14.5rem] right-7 bg-[#3498DB] hover:bg-[#2980B9] !rounded-full p-3"
>
<MessageCircleMore size={25} />
</button>
Expand Down
9 changes: 9 additions & 0 deletions client/src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ function DesktopNavigation({ toggleModal }: { toggleModal: (modalType: "search"
<NavigationMenuItem>
<SideButton />
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink
className={`${navigationMenuTriggerStyle()} hover:text-primary hover:bg-primary/10`}
onClick={() => toggleModal("login")}
href="#"
>
서비스 소개
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink
className={`${navigationMenuTriggerStyle()} hover:text-primary hover:bg-primary/10`}
Expand Down
22 changes: 15 additions & 7 deletions client/src/components/layout/SideButton.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,43 @@
import { Home } from "lucide-react";
import { ArrowUp } from "lucide-react";
import { ChartArea } from "lucide-react";
import { CircleHelp } from "lucide-react";

import { Chat } from "@/components/chat/Chat";
import { OpenChat } from "@/components/chat/ChatButton";
import { SidebarProvider } from "@/components/ui/sidebar";

import { useTapStore } from "@/store/useTapStore";

export default function SideButton() {
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
};

const { setTap } = useTapStore();
return (
<div className="flex h-full items-center ">
<SidebarProvider defaultOpen={false}>
<Chat />
<OpenChat />
</SidebarProvider>
<button className="fixed text-white bottom-[6.5rem] right-7 bg-primary hover:bg-secondary !rounded-full p-3">
<button
className="fixed text-white bottom-[6.5rem] right-7 bg-primary hover:bg-secondary !rounded-full p-3"
onClick={() => {
setTap("main");
}}
>
<Home size={25} />
</button>
<button className="fixed text-white bottom-[10.5rem] right-7 bg-[#1ABC9C] hover:bg-[#16A085] !rounded-full p-3">
<button
className="fixed text-white bottom-[10.5rem] right-7 bg-[#1ABC9C] hover:bg-[#16A085] !rounded-full p-3"
onClick={() => {
setTap("chart");
}}
>
<ChartArea size={25} />
</button>
<button className="fixed text-white bottom-[14.5rem] right-7 bg-[#F1C40F] hover:bg-[#D4AC0D] !rounded-full p-3">
<CircleHelp size={25} />
</button>
<button
className="fixed text-white bottom-[2.5rem] right-7 bg-[#9B59B6] hover:bg-[#8E44AD] !rounded-full p-3"
onClick={scrollToTop}
Expand Down
Loading

0 comments on commit 0302197

Please sign in to comment.