Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 17 additions & 24 deletions src/components/DailyVisitsChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import {
type ChartConfig,
} from "@/components/ui/chart"
import { Skeleton } from "@/components/ui/skeleton"
import { usePageViews, type DailyData } from "@/hooks/usePageViews"
import type { DailyData } from "@/hooks/usePageViews"
import { useT } from "@/i18n"
import { buildDailySeries, sumViews } from "@/lib/analytics-data"

const chartConfig = {
views: {
Expand All @@ -17,31 +18,23 @@ const chartConfig = {
},
} satisfies ChartConfig

function generateLast30Days(): DailyData[] {
const days: DailyData[] = []
const today = new Date()
for (let i = 29; i >= 0; i--) {
const d = new Date(today)
d.setDate(d.getDate() - i)
const yyyy = d.getFullYear()
const mm = String(d.getMonth() + 1).padStart(2, "0")
const dd = String(d.getDate()).padStart(2, "0")
days.push({ date: `${yyyy}-${mm}-${dd}`, views: 0 })
}
return days
}

function mergeData(base: DailyData[], fetched: DailyData[]): DailyData[] {
const map = new Map(fetched.map((d) => [d.date, d.views]))
return base.map((d) => ({ ...d, views: map.get(d.date) ?? 0 }))
interface DailyVisitsChartProps {
totalViews: number | null
daily: DailyData[]
isLoading: boolean
rangeDays?: number
}

export function DailyVisitsChart() {
const { totalViews, daily, isLoading } = usePageViews()
export function DailyVisitsChart({ totalViews, daily, isLoading, rangeDays = 30 }: DailyVisitsChartProps) {
const t = useT()

const data = mergeData(generateLast30Days(), daily)
const monthlyTotal = data.reduce((sum, d) => sum + d.views, 0)
const data = buildDailySeries(rangeDays, daily)
const rangeTotal = sumViews(data)
const rangeLabel = rangeDays === 7
? t.analytics.range7d
: rangeDays === 14
? t.analytics.range14d
: t.analytics.range30d

if (isLoading) {
return (
Expand Down Expand Up @@ -77,10 +70,10 @@ export function DailyVisitsChart() {
<div>
<p className="text-xs text-muted-foreground mb-1 flex items-center gap-1">
<TrendingUp className="size-3" />
{t.components.last30Days}
{rangeLabel}
</p>
<p className="text-2xl font-bold tabular-nums">
{monthlyTotal.toLocaleString()}
{rangeTotal.toLocaleString()}
</p>
</div>
</div>
Expand Down
51 changes: 51 additions & 0 deletions src/i18n/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,23 @@ export interface Translations {
loadError: string
showingCachedData: string
refresh: string
range7d: string
range14d: string
range30d: string
trafficMomentum: string
viewsInRange: string
avgDailyViews: string
peakDay: string
periodChange: string
compareUnavailable: string
contentInsights: string
fastestPosts: string
tagPerformance: string
seriesPerformance: string
viewsPerDay: string
averageViews: string
allTimeBasis: string
noSeriesData: string
}
about: {
description: string
Expand Down Expand Up @@ -305,6 +322,23 @@ export const ko: Translations = {
loadError: "조회수 데이터를 불러올 수 없습니다",
showingCachedData: "캐시된 데이터를 표시합니다",
refresh: "새로고침",
range7d: "7일",
range14d: "14일",
range30d: "30일",
trafficMomentum: "트래픽 모멘텀",
viewsInRange: "선택 기간 조회수",
avgDailyViews: "일평균 조회수",
peakDay: "최고 유입일",
periodChange: "이전 기간 대비",
compareUnavailable: "비교 데이터 부족",
contentInsights: "콘텐츠 인사이트",
fastestPosts: "빠르게 읽히는 글",
tagPerformance: "태그별 성과",
seriesPerformance: "시리즈별 성과",
viewsPerDay: "일평균",
averageViews: "평균 조회수",
allTimeBasis: "전체 기간 기준",
noSeriesData: "시리즈 데이터 없음",
},
about: {
description: "비즈니스 흐름을 제품 기능으로 만들고, 안정적으로 확장되는 서비스 구조까지 설계하는 백엔드 엔지니어입니다.",
Expand Down Expand Up @@ -481,6 +515,23 @@ export const en: Translations = {
loadError: "Could not load view data",
showingCachedData: "Showing cached data",
refresh: "Refresh",
range7d: "7 days",
range14d: "14 days",
range30d: "30 days",
trafficMomentum: "Traffic Momentum",
viewsInRange: "Views in range",
avgDailyViews: "Daily average",
peakDay: "Peak day",
periodChange: "vs previous period",
compareUnavailable: "Not enough comparison data",
contentInsights: "Content Insights",
fastestPosts: "Fastest Posts",
tagPerformance: "Tag Performance",
seriesPerformance: "Series Performance",
viewsPerDay: "views/day",
averageViews: "Avg views",
allTimeBasis: "All-time basis",
noSeriesData: "No series data",
},
about: {
description: "Backend engineer who turns business flows into product features and designs service structures that scale reliably.",
Expand Down
62 changes: 62 additions & 0 deletions src/lib/analytics-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { DailyData } from "@/hooks/usePageViews"

export interface DailySeriesPoint extends DailyData {
label: string
}

function formatDateKey(date: Date): string {
const yyyy = date.getFullYear()
const mm = String(date.getMonth() + 1).padStart(2, "0")
const dd = String(date.getDate()).padStart(2, "0")
return `${yyyy}-${mm}-${dd}`
}

export function formatShortDate(dateKey: string): string {
const [, month = "0", day = "0"] = dateKey.split("-")
return `${Number(month)}/${Number(day)}`
}

export function buildDailySeries(days: number, fetched: DailyData[]): DailySeriesPoint[] {
const map = new Map(fetched.map((d) => [d.date, d.views]))
const today = new Date()

return Array.from({ length: days }, (_, index) => {
const d = new Date(today)
d.setDate(d.getDate() - (days - 1 - index))
const date = formatDateKey(d)

return {
date,
label: formatShortDate(date),
views: map.get(date) ?? 0,
}
})
}

export function sumViews(data: Pick<DailyData, "views">[]): number {
return data.reduce((sum, d) => sum + d.views, 0)
}

export function averageViews(data: Pick<DailyData, "views">[]): number {
if (data.length === 0) return 0
return sumViews(data) / data.length
}

export function getPeakDay(data: DailySeriesPoint[]): DailySeriesPoint | null {
if (data.length === 0) return null
return data.reduce((peak, d) => (d.views > peak.views ? d : peak), data[0]!)
}

export function getPreviousComparableRange(days: number, fetched: DailyData[]): DailySeriesPoint[] {
if (fetched.length < days * 2) return []

const sorted = [...fetched].sort((a, b) => a.date.localeCompare(b.date))
return sorted
.slice(Math.max(0, sorted.length - days * 2), sorted.length - days)
.map((d) => ({ ...d, label: formatShortDate(d.date) }))
}

export function getPercentChange(current: number, previous: number): number | null {
if (previous <= 0) return null
return ((current - previous) / previous) * 100
}
Loading
Loading