Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.
Open

dev #32

Show file tree
Hide file tree
Changes from 4 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
6 changes: 4 additions & 2 deletions src/app/api/dashboard/devlogs/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

let data: unknown;
let data: { project_id: string; content: string; media_url?: string; description?: string | null };
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

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

The type annotation on line 10 is unsafe because req.json() returns any, which doesn't guarantee the data will match this type at runtime. Consider adding runtime validation using a library like Zod, or keeping the type as unknown and performing explicit type checking before using the data properties.

Copilot uses AI. Check for mistakes.
try {
data = await req.json();
} catch {
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
}

if (data.description === "") {
data.description = null;
}
Comment on lines +21 to +23
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

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

The mutation of the parsed data on lines 16-18 occurs after type assertion but without proper type checking. Since the type annotation on line 10 doesn't provide runtime safety, the check data.description === "" could fail if description is undefined or missing. Consider restructuring this to avoid mutation and ensure type safety, for example: body: JSON.stringify({ ...data, description: data.description === "" ? null : data.description })

Copilot uses AI. Check for mistakes.
try {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/devlogs/`, {
method: "POST",
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/hackatime/projects/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export async function GET() {

// Fetch all Hackatime projects for this user
const hackatimeRes = await fetch(
`https://hackatime.hackclub.com/api/v1/users/${hackatimeId}/stats?features=projects&start_date=2025-12-01T00:00:00Z`,
`https://hackatime.hackclub.com/api/v1/users/${hackatimeId}/stats?features=projects&start_date=2025-12-21T00:00:00Z`,
{ cache: "no-store" }
);

Expand Down
5 changes: 3 additions & 2 deletions src/app/api/projects/[projectId]/ship/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";
import { NextRequest, NextResponse } from "next/server";

export async function POST(
request: NextRequest,
_req: NextRequest,
{ params }: { params: Promise<{ projectId: string }> }
) {
const { projectId } = await params;
Expand All @@ -19,6 +19,7 @@ export async function POST(
{
method: "POST",
headers: {
"Content-Type": "application/json",
Cookie: `sessionId=${sessionId}`,
},
}
Expand Down
19 changes: 19 additions & 0 deletions src/app/api/shop/form-url/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { NextResponse } from "next/server";

// TODO: This is a placeholder implementation. Backend needs to be updated to:
// 1. Add a ShopConfig model/table to store form URLs and other shop settings
// 2. Create /api/v1/shop/config endpoint to fetch/update shop configuration
// 3. This route should then proxy to the backend instead of using env vars

export async function GET() {
const formUrl = process.env.NEXT_PUBLIC_SHOP_FORM_URL;

if (!formUrl) {
return NextResponse.json(
{ error: "Shop form URL not configured" },
{ status: 503 }
);
}

return NextResponse.json({ formUrl });
}
2 changes: 1 addition & 1 deletion src/app/dashboard/projects/[projectId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export default async function ProjectPage({

return (
<div className="max-w-4xl mx-auto">
<ProjectDetailsClient project={project} />
<ProjectDetailsClient project={project} devlogCount={devlogs.length} />

<div>
<div className="flex items-center gap-3 mb-6">
Expand Down
93 changes: 93 additions & 0 deletions src/app/dashboard/shop/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"use client";

import { useSearchParams } from "next/navigation";
import { useEffect, useState, Suspense } from "react";

function ShopRedirect() {
const searchParams = useSearchParams();
const item = searchParams.get("item");

const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
if (!item) {
setError("Missing item parameter. Please specify what you want to purchase.");
setLoading(false);
return;
}

async function fetchAndRedirect() {
try {
const res = await fetch("/api/shop/form-url");

if (!res.ok) {
const data = await res.json();
setError(data.error || "Failed to load shop configuration");
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

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

The error response on line 26 uses a generic "Failed to load shop configuration" message without including the backend's error detail. Consider extracting and including the error from the backend response to provide more context: setError(data.error || data.detail || "Failed to load shop configuration")

Suggested change
setError(data.error || "Failed to load shop configuration");
setError((data && (data.error || data.detail)) || "Failed to load shop configuration");

Copilot uses AI. Check for mistakes.
setLoading(false);
return;
}

const { formUrl } = await res.json();
const redirectUrl = `${formUrl}?item=${encodeURIComponent(item!)}`;
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

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

The non-null assertion operator (!) on line 32 is unnecessary because the item parameter is already checked for null/undefined on line 14, and the function returns early if it's falsy. You can safely use item without the assertion: ${formUrl}?item=${encodeURIComponent(item)}

Suggested change
const redirectUrl = `${formUrl}?item=${encodeURIComponent(item!)}`;
const redirectUrl = `${formUrl}?item=${encodeURIComponent(item)}`;

Copilot uses AI. Check for mistakes.
window.location.href = redirectUrl;
Comment on lines +33 to +35
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

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

The redirectUrl constructed on line 32 should be validated to ensure the formUrl from the API is a trusted URL before redirecting. While the backend should control this, client-side validation would add defense in depth. Consider validating that formUrl is a known trusted domain before performing the redirect.

Copilot uses AI. Check for mistakes.
} catch (err) {
console.error("Error fetching form URL", err);
setError("Something went wrong. Please try again later.");
setLoading(false);
}
}

fetchAndRedirect();
}, [item]);

if (error) {
return (
<div className="flex flex-col items-center justify-center min-h-[60vh] text-center">
<h1 className="text-4xl font-black mb-4 text-gray-900 font-[family-name:var(--font-righteous)]">
Oops!
</h1>
<p className="text-lg text-gray-700 mb-6">{error}</p>
<a
href="/dashboard"
className="px-6 py-3 bg-[#DC143C] text-white font-bold rounded-xl hover:bg-[#8B0000] transition-colors"
>
Back to Dashboard
</a>
</div>
);
}

if (loading) {
return (
<div className="flex flex-col items-center justify-center min-h-[60vh]">
<h1 className="text-4xl font-black mb-6 text-gray-900 font-[family-name:var(--font-righteous)]">
Shop
</h1>
<div className="flex flex-col items-center gap-4">
<div className="w-12 h-12 border-4 border-[#DC143C] border-t-transparent rounded-full animate-spin" />
<p className="text-lg text-gray-700">Redirecting to shop...</p>
</div>
</div>
);
}

return null;
}

export default function ShopPage() {
return (
<Suspense
fallback={
<div className="flex flex-col items-center justify-center min-h-[60vh]">
<h1 className="text-4xl font-black mb-6 text-gray-900 font-[family-name:var(--font-righteous)]">
Shop
</h1>
<div className="w-12 h-12 border-4 border-[#DC143C] border-t-transparent rounded-full animate-spin" />
</div>
}
>
<ShopRedirect />
</Suspense>
);
}
8 changes: 4 additions & 4 deletions src/components/Meta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,26 @@ const Meta = () => (
<link
key="safari_icon"
rel="mask-icon"
href="favicons/safari-pinned-tab.svg"
href="/favicons/safari-pinned-tab.svg"
color="#cc0000"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="favicons/favicon-16x16.png"
href="/favicons/favicon-16x16.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="favicons/favicon-32x32.png"
href="/favicons/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="48x48"
href="favicons/favicon-48x48.png"
href="/favicons/favicon-48x48.png"
/>
<link
rel="icon"
Expand Down
12 changes: 6 additions & 6 deletions src/components/dashboard/EditProject.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState, useEffect } from "react";
import React, { useState, useEffect } from "react";
import { useRouter } from "next/navigation";

type HackatimeProject = {
Expand All @@ -20,11 +20,11 @@ type ProjectData = {
export default function EditProject({
projectId,
initialData,
onCancel,
onCancelAction,
}: {
projectId: number;
initialData: ProjectData;
onCancel: () => void;
onCancelAction: () => void;
Comment on lines +24 to +28
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

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

The prop name "onCancelAction" is unconventional. Standard React naming convention for callback props is "on[EventName]" without the "Action" suffix (e.g., "onCancel", "onClick"). While custom component props can have any name, following the conventional pattern makes the codebase more intuitive. Consider using just "onCancel" instead.

Copilot uses AI. Check for mistakes.
}) {
const router = useRouter();
const [formData, setFormData] = useState(initialData);
Expand Down Expand Up @@ -73,7 +73,7 @@ export default function EditProject({
}

router.refresh();
onCancel();
onCancelAction();
} catch (err) {
setError(err instanceof Error ? err.message : "An error occurred");
} finally {
Expand All @@ -97,7 +97,7 @@ export default function EditProject({
<h2 className="text-2xl font-bold text-gray-900">Edit Project</h2>
<button
type="button"
onClick={onCancel}
onClick={onCancelAction}
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-gray-400"
aria-label="Close"
>
Expand Down Expand Up @@ -228,7 +228,7 @@ export default function EditProject({
<div className="flex gap-3 pt-4 border-t-2 border-gray-100">
<button
type="button"
onClick={onCancel}
onClick={onCancelAction}
className="flex-1 px-6 py-3 border-2 border-gray-300 text-gray-700 rounded-lg font-bold hover:bg-gray-50 transition-all focus:outline-none focus:ring-2 focus:ring-gray-400"
disabled={loading}
>
Expand Down
50 changes: 48 additions & 2 deletions src/components/dashboard/ProjectDetailsClient.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";

import EditProject from "./EditProject";

Expand All @@ -17,8 +18,41 @@ type Project = {
shipped: boolean;
};

export default function ProjectDetailsClient({ project }: { project: Project }) {
type Props = {
project: Project;
devlogCount: number;
};

export default function ProjectDetailsClient({ project, devlogCount }: Props) {
const router = useRouter();
const [isEditing, setIsEditing] = useState(false);
const [isShipping, setIsShipping] = useState(false);
const [shipError, setShipError] = useState<string | null>(null);

const canShip = !project.shipped && devlogCount > 0;

async function handleShip() {
setIsShipping(true);
setShipError(null);

try {
const res = await fetch(`/api/projects/${project.project_id}/ship`, {
method: "POST",
credentials: "include",
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

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

The credentials: "include" option on line 41 is unnecessary for same-origin requests in Next.js. The cookies are automatically sent with same-origin requests. This option is typically only needed for cross-origin requests with credentials. Consider removing it for clarity.

Suggested change
credentials: "include",

Copilot uses AI. Check for mistakes.
});

if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.detail || data.error || "Failed to ship project");
}

router.refresh();
} catch (err) {
setShipError(err instanceof Error ? err.message : "Something went wrong");
} finally {
setIsShipping(false);
}
}

return (
<>
Expand Down Expand Up @@ -52,13 +86,25 @@ export default function ProjectDetailsClient({ project }: { project: Project })
Shipped
</span>
)}
{canShip && (
<button
onClick={handleShip}
disabled={isShipping}
className="px-4 py-2 bg-rose-700 hover:bg-rose-800 text-white rounded-lg font-semibold transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-rose-500 focus:ring-offset-2 shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
>
{isShipping ? "Shipping..." : "Ship Project"}
</button>
)}
<button
onClick={() => setIsEditing(true)}
className="px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg font-semibold transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 shadow-sm hover:shadow-md"
>
Edit
</button>
</div>
{shipError && (
<p className="text-red-600 text-sm mt-2">{shipError}</p>
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

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

The error message displayed on lines 105-107 lacks accessibility attributes. Consider adding role="alert" and aria-live="polite" to the error container so screen readers announce the error when it appears.

Suggested change
<p className="text-red-600 text-sm mt-2">{shipError}</p>
<p
className="text-red-600 text-sm mt-2"
role="alert"
aria-live="polite"
>
{shipError}
</p>

Copilot uses AI. Check for mistakes.
)}
Comment on lines +107 to +109
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

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

The error message (lines 105-107) is placed as a third child in the flex container with justify-between (line 69). This will position the error horizontally alongside the project info and buttons, which is likely not the intended layout. Consider moving the error message outside this flex container (after line 108) or restructuring the layout to display the error below the buttons on a new line.

Copilot uses AI. Check for mistakes.
</div>

{(project.repo || project.demo_url) && (
Expand Down Expand Up @@ -126,7 +172,7 @@ export default function ProjectDetailsClient({ project }: { project: Project })
description: project.description || "",
hackatime_projects: project.hackatime_projects,
}}
onCancel={() => setIsEditing(false)}
onCancelAction={() => setIsEditing(false)}
/>
)}
</>
Expand Down
Loading