Skip to content

Commit

Permalink
feat: WakatimeGraph component
Browse files Browse the repository at this point in the history
  • Loading branch information
jktrn committed Aug 6, 2024
1 parent 78c01cb commit 97e31f6
Show file tree
Hide file tree
Showing 9 changed files with 853 additions and 17 deletions.
50 changes: 50 additions & 0 deletions app/api/wakatime-stats/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { NextRequest } from 'next/server'

type Language = {
name: string
hours: string
}

export async function GET(request: NextRequest) {
const url = request.nextUrl
const username = url.searchParams.get('username')

if (!username) {
return new Response(JSON.stringify({ error: 'Username is required' }), {
status: 400,
headers: {
'Content-Type': 'application/json',
},
})
}

try {
const response = await fetch(`https://wakatime.com/api/v1/users/${username}/stats/all_time`)

if (!response.ok) {
console.log(response.status)
throw new Error('Failed to fetch data from WakaTime API')
}

const data = await response.json()
const topLanguages: Language[] = data.data.languages.map((lang: any) => ({
name: lang.name,
hours: (lang.total_seconds / 3600).toFixed(2),
}))

return new Response(JSON.stringify(topLanguages), {
status: 200,
headers: {
'Content-Type': 'application/json',
},
})
} catch (error) {
console.error('WakaTime API error:', error)
return new Response(JSON.stringify({ error: 'Internal server error' }), {
status: 500,
headers: {
'Content-Type': 'application/json',
},
})
}
}
20 changes: 16 additions & 4 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@
--ring: 34 54% 45%;

--radius: 0.5rem;

--chart-1: 34 54% 81%;
--chart-2: 34 34% 73%;
--chart-3: 35 22% 65%;
--chart-4: 35 16% 57%;
--chart-5: 35 12% 41%;
--chart-6: 33 12% 33%;
--chart-7: 32 12% 25%;
}

.dark {
Expand Down Expand Up @@ -71,6 +79,14 @@
--ring: 34 54% 81%;

--radius: 0.5rem;

--chart-1: 34 54% 81%;
--chart-2: 34 34% 73%;
--chart-3: 35 22% 65%;
--chart-4: 35 16% 57%;
--chart-5: 35 12% 41%;
--chart-6: 33 12% 33%;
--chart-7: 32 12% 25%;
}

* {
Expand Down Expand Up @@ -241,10 +257,6 @@
background-image: url('/static/images/bento/github.webp');
}

.grid-item-j {
background-image: url('/static/images/bento/technologies.webp');
}

.grid-item-k-overlay {
background-image: url('/static/images/bento/twitter.webp');
}
Expand Down
13 changes: 8 additions & 5 deletions components/bento/BentoBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ExternalLink from './ExternalLink'
import GithubCalendar from './GithubCalendar'
import SpotifyPresence from './SpotifyPresence'
import { Skeleton } from '../shadcn/skeleton'
import WakatimeGraph from './WakatimeGraph'

const BentoBox = ({ posts }) => {
const lanyard = useLanyard({
Expand Down Expand Up @@ -105,11 +106,13 @@ const BentoBox = ({ posts }) => {
/>
</div>

<div className="grid-item-j aspect-square">
<p className="sr-only">
Technologies used (in order of comfort): NextJS, Tailwind CSS, React, Hexo,
TypeScript, Unity, C#, Python, Svelte, Astro, JavaScript, Vercel
</p>
<div className="grid-item-j relative aspect-square">
<WakatimeGraph username="jktrn" />
<ExternalLink
href="https://wakatime.com/@jktrn"
aria-label="View enscribe's Wakatime profile"
title="Wakatime Profile"
/>
</div>

<div className="grid-item-k has-overlay relative flex aspect-square items-center justify-center hover:bg-none">
Expand Down
4 changes: 2 additions & 2 deletions components/bento/ExternalLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ const ExternalLink = ({
}: ExternalLinkProps & AnchorHTMLAttributes<HTMLAnchorElement>) => {
return newTab ? (
<Link {...props}>
<div className="absolute bottom-0 right-0 m-3 flex w-fit items-end rounded-full border border-border bg-tertiary/50 p-3 text-primary transition-all duration-300 hover:brightness-125">
<div className="absolute bottom-0 right-0 m-3 flex w-fit items-end rounded-full border border-border bg-tertiary p-3 text-primary transition-all duration-300 hover:brightness-125">
<MoveUpRight size={16} />
</div>
</Link>
) : (
<a {...props}>
<div className="absolute bottom-0 right-0 m-3 flex w-fit items-end rounded-full border border-border bg-tertiary/50 p-3 text-primary transition-all duration-300 hover:brightness-125">
<div className="absolute bottom-0 right-0 m-3 flex w-fit items-end rounded-full border border-border bg-tertiary p-3 text-primary transition-all duration-300 hover:brightness-125">
<MoveUpRight size={16} />
</div>
</a>
Expand Down
149 changes: 149 additions & 0 deletions components/bento/WakatimeGraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
'use client'

import React, { useState, useEffect, useMemo, useCallback } from 'react'
import { Bar, BarChart, CartesianGrid, LabelList, XAxis, YAxis } from 'recharts'
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from '@/components/shadcn/chart'
import { Skeleton } from '@/components/shadcn/skeleton'
import { getLanguageIcon } from '@/scripts/utils/language-icons'

interface Language {
name: string
hours: number
fill: string
}

interface Props {
username: string
omitLanguages?: string[]
}

const colors = [
'hsl(var(--chart-1))',
'hsl(var(--chart-2))',
'hsl(var(--chart-3))',
'hsl(var(--chart-4))',
'hsl(var(--chart-5))',
'hsl(var(--chart-6))',
'hsl(var(--chart-7))',
]

const chartConfig: ChartConfig = {
hours: {
label: 'Hours',
color: 'hsl(var(--primary))',
},
label: {
color: 'hsl(var(--muted-foreground))',
},
}

colors.forEach((color, index) => {
chartConfig[`language${index}`] = {
label: `Language ${index + 1}`,
color: color,
}
})

const WakatimeGraph = ({ username, omitLanguages = [] }: Props) => {
const [languages, setLanguages] = useState<Language[]>([])
const [isLoading, setIsLoading] = useState<boolean>(true)
const [error, setError] = useState<string | null>(null)

const fetchData = useCallback(async () => {
try {
const response = await fetch(`/api/wakatime-stats?username=${username}`)
if (!response.ok) {
throw new Error('Failed to fetch data')
}
const data = await response.json()

const filteredLanguages = data
.filter((lang) => !omitLanguages.includes(lang.name))
.slice(0, 7)
.map((lang, index) => ({
name: lang.name,
hours: parseFloat(lang.hours),
fill: colors[index % colors.length],
}))

setLanguages(filteredLanguages)
setIsLoading(false)
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred')
setIsLoading(false)
}
}, [username, omitLanguages])

useEffect(() => {
fetchData()
}, [fetchData])

const CustomYAxisTick = useCallback((props: any) => {
const { x, y, payload } = props
const icon = getLanguageIcon(payload.value.toLowerCase())
return (
<g transform={`translate(${x},${y})`}>
<title>{payload.value}</title>
<circle cx="-18" cy="0" r="14" fill="#1A1A1A" />
<foreignObject width={16} height={16} x={-26} y={-8}>
{icon ? (
React.cloneElement(icon, {
size: 16,
color: '#E9D3B6',
})
) : (
<text
x={8}
y={12}
fill="#E9D3B6"
fontSize="12"
textAnchor="middle"
dominantBaseline="central"
>
{payload.value.charAt(0).toUpperCase()}
</text>
)}
</foreignObject>
</g>
)
}, [])

const memoizedLanguages = useMemo(() => languages, [languages])

if (isLoading) return <Skeleton className="size-full rounded-3xl" />
if (error) return <div>Error: {error}</div>

return (
<ChartContainer config={chartConfig} className="h-full w-full p-4">
<BarChart accessibilityLayer data={memoizedLanguages} layout="vertical">
<CartesianGrid horizontal={false} />
<YAxis
dataKey="name"
type="category"
tickLine={false}
axisLine={false}
width={50}
tick={<CustomYAxisTick />}
/>
<XAxis type="number" hide />
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
<Bar dataKey="hours" fill="var(--color-hours)" radius={[8, 8, 8, 8]}>
<LabelList
dataKey="hours"
position="right"
formatter={(value: number) => `${value.toFixed(1)}h`}
className="fill-foreground"
fontSize={12}
/>
</Bar>
</BarChart>
</ChartContainer>
)
}

export default React.memo(WakatimeGraph)
Loading

0 comments on commit 97e31f6

Please sign in to comment.