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
Binary file modified .gitignore
Binary file not shown.
10 changes: 2 additions & 8 deletions app/(public)/join/[code]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,7 @@ async function getLeaderboard(code: string) {
return data;
}

async function getMemberCount(leaderboardId: string) {
const supabase = await createClient();
const { count } = await supabase
.from("leaderboard_members")
.select("*", { count: "exact", head: true })
.eq("leaderboard_id", leaderboardId);
return count ?? 0;
}


export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { code } = await params;
Expand Down Expand Up @@ -64,3 +57,4 @@ export default async function JoinPage({ params }: Props) {
const { code } = await params;
redirect(`/join?id=${encodeURIComponent(code)}`);
}

5 changes: 4 additions & 1 deletion app/(user)/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,8 @@ export default async function Dashboard() {
return <DashboardWithoutKey email={profile?.email || user.email!} />;
}

return <Stats />;
const email = profile?.email || user.email!;
const name = user?.user_metadata?.name || email.split("@")[0];

return <Stats name={name} email={email} />;
}
68 changes: 49 additions & 19 deletions app/api/wakatime/sync/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,27 +63,51 @@ export async function GET(request: Request) {
}
}

// Fetch from WakaTime
const response = await fetch(
"https://wakatime.com/api/v1/users/current/stats/last_7_days",
{
headers: {
Authorization: `Basic ${Buffer.from(profile$.wakatime_api_key).toString(
"base64",
)}`,
},
},
);
// Fetch from WakaTime API endpoints
const endDate = new Date();
const startDate = new Date();
startDate.setDate(endDate.getDate() - 6);
const endStr = endDate.toISOString().split("T")[0];
const startStr = startDate.toISOString().split("T")[0];

const authHeader = `Basic ${Buffer.from(profile$.wakatime_api_key).toString("base64")}`;

const [statsResponse, summariesResponse] = await Promise.all([
fetch("https://wakatime.com/api/v1/users/current/stats/last_7_days", {
headers: { Authorization: authHeader },
}),
fetch(
`https://wakatime.com/api/v1/users/current/summaries?start=${startStr}&end=${endStr}`,
{
headers: { Authorization: authHeader },
}
),
]);

const data = await response.json();
const statsData = await statsResponse.json();
const summariesData = await summariesResponse.json();

if (!response.ok) {
if (!statsResponse.ok || !summariesResponse.ok) {
return NextResponse.json(
{ error: "Failed to fetch WakaTime" },
{ error: "Failed to fetch data from WakaTime" },
{ status: 500 },
);
Comment on lines +75 to 94
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

statsResponse.json() / summariesResponse.json() are called before checking ok. If WakaTime returns a non-JSON error body (or an empty body), this will throw and bypass your intended error response. Check ok (or wrap JSON parsing in try/catch) before parsing, and include the upstream status/body in the error for easier debugging.

Copilot uses AI. Check for mistakes.
}

const wakaStats = statsData.data;
const wakaSummaries = summariesData.data;

// Process daily summaries
const daily_stats = wakaSummaries.map(
(day: {
range: { date: string };
grand_total: { total_seconds: number };
}) => ({
date: day.range.date,
total_seconds: day.grand_total.total_seconds,
}),
);

if (apiKey) {
const { error } = await supabase
.from("profiles")
Expand All @@ -109,10 +133,16 @@ export async function GET(request: Request) {
.from("user_stats")
.upsert({
user_id: user.id,
total_seconds: Math.floor(data.data.total_seconds),
languages: data.data.languages,
operating_systems: data.data.operating_systems,
editors: data.data.editors,
total_seconds: Math.floor(wakaStats.total_seconds),
daily_average: Math.floor(wakaStats.daily_average || 0),
languages: wakaStats.languages,
operating_systems: wakaStats.operating_systems,
editors: wakaStats.editors,
machines: wakaStats.machines,
categories: wakaStats.categories,
dependencies: wakaStats.dependencies || [],
best_day: wakaStats.best_day || {},
daily_stats: daily_stats,
last_fetched_at: new Date().toISOString(),
})
.select()
Expand All @@ -122,7 +152,7 @@ export async function GET(request: Request) {
.from("user_projects")
.upsert({
user_id: user.id,
projects: data.data.projects,
projects: wakaStats.projects,
last_fetched_at: new Date().toISOString(),
})
.select()
Expand Down
45 changes: 2 additions & 43 deletions app/components/dashboard/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,20 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faChartLine,
faTrophy,
faRightFromBracket,
faChevronLeft,
faChevronRight,
faGear,
} from "@fortawesome/free-solid-svg-icons";
import type { IconDefinition } from "@fortawesome/free-solid-svg-icons";

const SidebarContext = createContext({ collapsed: false });

function Sidebar({ email, name }: { email: string; name: string }) {
function Sidebar() {
const pathname = usePathname();
const { collapsed } = useContext(SidebarContext);

const navItems: { href: string; label: string; icon: IconDefinition }[] = [
{ href: "/dashboard", label: "Dashboard", icon: faChartLine },
{ href: "/dashboard/leaderboards", label: "Leaderboards", icon: faTrophy },
{ href: "/dashboard/settings", label: "Settings", icon: faGear },
];

return (
Expand All @@ -46,26 +43,6 @@ function Sidebar({ email, name }: { email: string; name: string }) {
)}
</div>

<div
className={`px-4 py-4 border-b border-white/5 ${collapsed ? "flex justify-center" : ""}`}
>
<div
className={`flex items-center gap-3 ${collapsed ? "justify-center" : ""}`}
>
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-xs font-bold text-white shrink-0">
{email.charAt(0).toUpperCase()}
</div>
{!collapsed && (
<div className="min-w-0">
<p className="text-sm font-medium text-gray-200 truncate">
{name}
</p>
<p className="text-[11px] text-gray-600 truncate">{email}</p>
</div>
)}
</div>
</div>

<nav className="flex-1 px-3 py-5 space-y-1">
{!collapsed && (
<p className="text-[10px] uppercase tracking-[0.15em] text-gray-700 font-semibold px-3 mb-3">
Expand Down Expand Up @@ -95,29 +72,11 @@ function Sidebar({ email, name }: { email: string; name: string }) {
</Link>
))}
</nav>

<div className="px-3 py-4 border-t border-white/5 space-y-1">
<Link
href="/logout"
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-gray-600 hover:text-red-400 hover:bg-red-500/5 transition ${
collapsed ? "justify-center" : ""
}`}
title={collapsed ? "Logout" : undefined}
>
<FontAwesomeIcon
icon={faRightFromBracket}
className="w-4 h-4 shrink-0"
/>
{!collapsed && "Logout"}
</Link>
</div>
</aside>
);
}

export default function DashboardLayout({
email,
name,
children,
}: {
email: string;
Expand Down Expand Up @@ -146,7 +105,7 @@ export default function DashboardLayout({
return (
<SidebarContext.Provider value={{ collapsed }}>
<div className="min-h-screen bg-[#0a0a1a] text-white">
<Sidebar email={email} name={name} />
<Sidebar />

{/* Collapse Toggle */}
<button
Expand Down
Loading
Loading