Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
9c51d84
feat: refactor WikiArticleViewer to simplify page view tracking; add …
halilibrahimcelik Feb 20, 2026
55d27ac
feat: add LinkButton component for navigation; refactor Navbar and r…
halilibrahimcelik Feb 20, 2026
a8d61d4
feat: refactor Navbar component; remove SignInButton and clean up imp…
halilibrahimcelik Feb 20, 2026
80a3419
feat: enhance Navbar and Articles components; add ShinyText component…
halilibrahimcelik Feb 20, 2026
69eb0d0
feat: implement article summarization feature in WikiArticleViewer; a…
halilibrahimcelik Feb 21, 2026
f3ed570
feat: enhance AI response handling in route.ts; add validation error …
halilibrahimcelik Feb 21, 2026
eda69f4
feat: update content validation in RequestBodySchema; add Tooltip com…
halilibrahimcelik Feb 25, 2026
a9bff8e
feat: integrate Sonner for toast notifications in WikiArticleViewer a…
halilibrahimcelik Mar 2, 2026
bc07e2d
feat: enhance article deletion handling; improve error management in …
halilibrahimcelik Mar 2, 2026
4a46a16
feat: add AI gateway API key to CI/CD workflow for enhanced integration
halilibrahimcelik Mar 2, 2026
f35f9d0
feat: refactor imports and formatting in multiple files; enhance tool…
halilibrahimcelik Mar 2, 2026
e70c0b4
Initial plan
Copilot Mar 2, 2026
39c5981
fix: address PR review comments - auth, deps, UX, abort, dedup, types
Copilot Mar 2, 2026
8646ae4
fix: sync pnpm-lock.yaml with package.json (remove @openrouter/sdk)
Copilot Mar 2, 2026
47ca5ab
Merge pull request #4 from halilibrahimcelik/copilot/sub-pr-3
halilibrahimcelik Mar 2, 2026
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
1 change: 1 addition & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ jobs:
UPSTASH_REDIS_REST_URL: ${{ secrets.UPSTASH_REDIS_REST_URL }}
UPSTASH_REDIS_REST_TOKEN: ${{ secrets.UPSTASH_REDIS_REST_TOKEN }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
AI_GATEWAY_API_KEY: ${{ secrets.AI_GATEWAY_API_KEY }}
- name: log build success
run: echo "Build and tests completed successfully!"
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"cSpell.words": ["neondatabase", "Wikimasters"]
"cSpell.words": ["gsap", "neondatabase", "Wikimasters"]
}
16 changes: 13 additions & 3 deletions app/actions/articles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,23 @@ export async function deleteArticle(id: string) {
}

// Form-friendly server action: accepts FormData from a client form and calls deleteArticle
export async function deleteArticleForm(formData: FormData): Promise<void> {
export async function deleteArticleForm(
_prevState: { error: string } | null,
formData: FormData,
): Promise<{ error: string } | null> {
const id = formData.get("id");
if (!id) {
throw new Error("Missing article id");
return { error: "Missing article id" };
}

await deleteArticle(String(id));
try {
await deleteArticle(String(id));
} catch (error) {
return {
error:
error instanceof Error ? error.message : "Failed to delete article",
};
}

// After deleting, redirect the user back to the homepage.
redirect("/");
Expand Down
157 changes: 157 additions & 0 deletions app/api/ai/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
"use server";
import { NextRequest, NextResponse } from "next/server";
import { ZodError, z } from "zod";

const RequestBodySchema = z.object({
prompt: z.object({
text: z.string(),
content: z
.string()
.min(10, "Content is too short")
.max(7000, "Content too large"), // ✅ validation rules
}),
});
// ✅ define response types
export type AISuccessResponse = {
created: number;
content: string;
};

export type AIErrorResponse = {
error: string;
};

type AIResponse = AISuccessResponse | AIErrorResponse;

type OpenRouterMessage = {
role: string;
content: string;
};

type OpenRouterChoice = {
index: number;
finish_reason: string;
message: OpenRouterMessage;
logprobs: null | object;
};
type OpenRouterResponse = {
id: string;
model: string;
object: string;
created: number;
choices: OpenRouterChoice[];
usage: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
cost: number;
};
};
export const POST = async (
request: NextRequest,
): Promise<NextResponse<AIResponse>> => {
try {
const {
prompt: { text, content },
} = RequestBodySchema.parse(await request.json()); // ✅ validated + typed
const response = await fetch(
"https://openrouter.ai/api/v1/chat/completions",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.AI_GATEWAY_API_KEY}`,
"HTTP-Referer": process.env.SITE_URL ?? "", // Optional. Site URL for rankings on openrouter.ai.
"X-Title": process.env.SITE_NAME ?? "", // Optional. Site title for rankings on openrouter.ai.
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "openai/gpt-5-nano",
messages: [
{
role: "user",
content: [
{
type: "text",
text,
},
{
type: "text",
text: content,
},
],
},
],
}),
},
);

if (!response.ok) {
let errorText = "";
try {
errorText = await response.text();
} catch {
// ignore body parsing errors for non-OK responses
}
const status =
response.status >= 400 && response.status <= 599
? response.status
: 502;
return NextResponse.json<AIErrorResponse>(
{
error:
errorText ||
`Upstream OpenRouter error: ${response.status} ${response.statusText}`,
},
{ status },
);
}

let rawData: unknown;
try {
rawData = await response.json();
} catch {
return NextResponse.json<AIErrorResponse>(
{ error: "Failed to parse response from AI provider" },
{ status: 502 },
);
}

const data = rawData as Partial<OpenRouterResponse>;
const choice = data.choices?.[0];
const messageContent =
choice?.message && typeof choice.message.content === "string"
? choice.message.content
: undefined;

if (typeof data.created !== "number" || !messageContent) {
return NextResponse.json<AIErrorResponse>(
{ error: "Invalid response from AI provider" },
{ status: 502 },
);
}

return NextResponse.json(
{
created: data.created,
content: messageContent,
},
{ status: 200 },
);
} catch (error) {
if (error instanceof ZodError) {
return NextResponse.json<AIErrorResponse>(
{
error: error.message,
},
{ status: 400 },
);
}
console.error("API Error:", error);
return NextResponse.json<AIErrorResponse>(
{
error: "Failed to process the prompt",
},
{ status: 500 },
);
}
};
5 changes: 4 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { StackProvider, StackTheme } from "@stackframe/stack";
import type { Metadata } from "next";
import { JetBrains_Mono } from "next/font/google";
import { Toaster } from "sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { stackClientApp } from "../stack/client";
import "./globals.css";
import { Navbar } from "@/components/features/navbar";
Expand Down Expand Up @@ -29,7 +31,8 @@ export default function RootLayout({
<StackProvider app={stackClientApp}>
<StackTheme>
<Navbar />
{children}
<TooltipProvider>{children}</TooltipProvider>
<Toaster position="bottom-left" richColors />
</StackTheme>
</StackProvider>
</StoreProvider>
Expand Down
11 changes: 7 additions & 4 deletions app/wiki/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import WikiArticleViewer from "@/components/features/wikicards/wiki-article-viewer";
import { authorizeUserToEditArticle } from "@/db/authz";
import { getArticleByIdFromDB } from "@/lib/data/articles";
import { stackServerApp } from "@/stack/server";

Expand All @@ -12,10 +13,12 @@ export default async function ViewArticlePage({
params,
}: ViewArticlePageProps) {
const { id } = await params;
await stackServerApp.getUser({ or: "redirect" });
// Mock permission check - in a real app, this would come from auth/user context
const canEdit = true; // Set to true for demonstration
const article = await getArticleByIdFromDB(id);
const user = await stackServerApp.getUser({ or: "redirect" });
const userId = user.id;
const [article, canEdit] = await Promise.all([
getArticleByIdFromDB(id),
authorizeUserToEditArticle(userId, id),
]);
Comment on lines +16 to +21
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

stackServerApp.getUser({ or: "redirect" }) should guarantee a user, but this still falls back to a hard-coded "mockUserId". That can mask auth issues and may cause incorrect authorization results.

Prefer treating user as non-null after redirect (or explicitly handle the null case with a redirect/throw) and pass user.id directly.

Copilot uses AI. Check for mistakes.

if (!article) {
return (
Expand Down
8 changes: 5 additions & 3 deletions components.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@
},
"iconLibrary": "tabler",
"rtl": false,
"menuColor": "default",
"menuAccent": "bold",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "bold",
"registries": {}
"registries": {
"@react-bits": "https://reactbits.dev/r/{name}.json"
}
}
2 changes: 0 additions & 2 deletions components/features/navbar/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
export { Navbar } from "./navbar";
export { SignInButton } from "./signin-button";
export { SignOutButton } from "./signup-button";
19 changes: 12 additions & 7 deletions components/features/navbar/navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { UserButton } from "@stackframe/stack";
import Link from "next/link";
import LinkButton from "@/components/ui/link-button";
import {
NavigationMenu,
NavigationMenuItem,
NavigationMenuList,
} from "@/components/ui/navigation-menu";
import { stackServerApp } from "@/stack/server";
import { SignInButton } from "./signin-button";
import { SignOutButton } from "./signup-button";
import { Routes } from "@/types";

export const Navbar: React.FC = async () => {
const user = await stackServerApp.getUser();
Expand All @@ -27,16 +27,21 @@ export const Navbar: React.FC = async () => {
<NavigationMenu>
<NavigationMenuList className={"gap-2"}>
{user ? (
<NavigationMenuItem>
<UserButton />
</NavigationMenuItem>
<>
<NavigationMenuItem>
<LinkButton href={Routes.NEW_ARTICLE} text="New Article" />
</NavigationMenuItem>
<NavigationMenuItem>
<UserButton />
</NavigationMenuItem>
</>
) : (
<>
<NavigationMenuItem>
<SignOutButton />
<LinkButton href={Routes.SIGNIN} text="Sign In" />
</NavigationMenuItem>
<NavigationMenuItem>
<SignInButton />
<LinkButton href={Routes.SIGNUP} text="Sign Up" />
</NavigationMenuItem>
</>
)}
Expand Down
21 changes: 0 additions & 21 deletions components/features/navbar/signup-button.tsx

This file was deleted.

11 changes: 1 addition & 10 deletions components/features/wikicards/articles-list.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
"use client";

import Link from "next/link";
import { useMemo } from "react";
import { Button } from "@/components/ui/button";
import { useGetArticlesQuery } from "@/lib/redux/features/articles/articlesApiSlice";
import type { ArticleWikiData } from "@/types/api";
import WikiCard from "./wiki-card";
Expand All @@ -13,6 +10,7 @@ type Props = {

export function ArticlesList({ serverData }: Props) {
// Skip RTK Query if we have server data

const {
data: clientArticles,
isLoading,
Expand Down Expand Up @@ -53,13 +51,6 @@ export function ArticlesList({ serverData }: Props) {
<div>
<div className="flex items-center justify-between">
<h1 className="text-4xl font-bold mb-8">All Articles</h1>

<Button
variant="primary"
render={(props) => <Link href={"/wiki/edit/new"} {...props} />}
>
New Article
</Button>
</div>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{articles.map((article) => (
Expand Down
Loading