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
45 changes: 45 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Nextblog CI

on:
push:
branches: [main]
pull_request:
branches: [main]

# This cancels previous runs if you push again to the same PR
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
quality-check:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 9

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"

- name: Install Dependencies
run: pnpm install --frozen-lockfile

- name: Run Format Check
run: pnpm run format:check

- name: Run Lint
run: pnpm run lint

- name: Type Check
run: pnpm exec tsc --noEmit

- name: Build Project
run: pnpm run build
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<!-- BEGIN:nextjs-agent-rules -->

# This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.

<!-- END:nextjs-agent-rules -->
14 changes: 14 additions & 0 deletions app/(shared-layout)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Navbar from "@/components/web/navbar";

export default function SharedLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<Navbar />
{children}
</>
);
}
4 changes: 2 additions & 2 deletions app/page.tsx → app/(shared-layout)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export default function Page() {
return <h1>Home Page</h1>
}
return <h1>Home Page</h1>;
}
4 changes: 2 additions & 2 deletions app/auth/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export default function LoginPage() {
return <h1>Login Page</h1>
}
return <h1>Login Page</h1>;
}
21 changes: 21 additions & 0 deletions app/auth/sign-up/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { buttonVariants } from "@/components/ui/button";
import { ArrowLeft } from "lucide-react";
import Link from "next/link";

export default function authLayout({
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Rename default export to PascalCase.

React component convention (and ESLint react/* rules commonly used with Next.js) requires component names to start with an uppercase letter. authLayout should be AuthLayout.

-export default function authLayout({
+export default function AuthLayout({
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export default function authLayout({
export default function AuthLayout({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/auth/sign-up/layout.tsx` at line 5, The default exported React component
named authLayout should be renamed to PascalCase: AuthLayout; update the
function declaration/export from authLayout to AuthLayout and update any
imports/usages that reference authLayout to import or reference AuthLayout
instead, and ensure the component name inside JSX/Next layout reference matches
the new AuthLayout identifier to satisfy React/ESLint conventions.

children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="absolute top-5 left-5">
<Link href="/" className={buttonVariants({ variant: "secondary" })}>
<ArrowLeft className="size-4" />
Back to Home
</Link>
</div>
<div className="w-full max-w-md mx-auto">{children}</div>
</div>
Comment on lines +11 to +19
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

absolute without a positioned ancestor.

The wrapper is not relative, so the "Back to Home" link positions against the nearest positioned ancestor (likely the viewport/<body>). If the intent is to anchor the back link to this layout's container (e.g., for future nesting or modal contexts), add relative to the outer div.

-    <div className="min-h-screen flex items-center justify-center">
+    <div className="relative min-h-screen flex items-center justify-center">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/auth/sign-up/layout.tsx` around lines 11 - 19, The outer layout div uses
an absolutely positioned "Back to Home" Link but the container div (the one
rendering children) lacks a positioning context; update the outer <div
className="min-h-screen flex items-center justify-center"> to include relative
positioning (e.g., add "relative") so the absolute positioning of the Link
anchors to this layout component; change the className where children are
rendered (the container wrapping Link, ArrowLeft, buttonVariants and children)
to include "relative".

);
}
108 changes: 106 additions & 2 deletions app/auth/sign-up/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,107 @@
"use client";

import { signUpSchema } from "@/app/schemas/auth";
import { z } from "zod";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { zodResolver } from "@hookform/resolvers/zod";
import { Controller, useForm } from "react-hook-form";
import { Input } from "@/components/ui/input";
import {
Field,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
import { Button } from "@/components/ui/button";

export default function SignupPage() {
return <h1>Sign Up Page</h1>
}
const form = useForm<z.infer<typeof signUpSchema>>({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
resolver: zodResolver(signUpSchema as any),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check installed versions of hookform resolvers, react-hook-form, and zod
fd -t f 'package.json' -d 2 --exec cat {} \; | jq '.dependencies + .devDependencies | with_entries(select(.key | test("react-hook-form|@hookform/resolvers|zod")))'

Repository: thyuhtooaung-dev/NextBlog

Length of output: 157


🌐 Web query:

Does @hookform/resolvers latest zodResolver require a cast for Zod 4 schemas?

💡 Result:

No, the latest @hookform/resolvers zodResolver does not require a cast (like schema.cast from Zod 3) for Zod 4 schemas. It fully supports Zod 4 natively since version 5.1.0 (June 2025), with ongoing fixes in later releases up to 5.2.2 (Sep 2025). Usage is direct: import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; // or 'zod/v4' const schema = z.object({ ... }); useForm({ resolver: zodResolver(schema) }); Official docs and examples explicitly show import { z } from 'zod'; // or 'zod/v4' with no casting needed. Types are automatically inferred from the schema (input/output distinction handled internally). Zod 4 changes (no preprocess/castDraft) are adapted in the resolver source, using schema.parse/parseAsync on form values directly. Zod 3 sometimes needed schema.cast for loose parsing, but Zod 4/ZodResolver combo uses strict parse by default, with raw mode option for untransformed values.

Citations:


Remove the as any cast on zodResolver.

ESLint flags this (@typescript-eslint/no-explicit-any). The installed versions (@hookform/resolvers 5.2.2 and Zod 4.3.6) support zodResolver(signUpSchema) directly without the cast. The type inference from z.infer<typeof signUpSchema> works correctly on useForm when the cast is removed.

Proposed fix
-    resolver: zodResolver(signUpSchema as any),
+    resolver: zodResolver(signUpSchema),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
resolver: zodResolver(signUpSchema as any),
resolver: zodResolver(signUpSchema),
🧰 Tools
🪛 ESLint

[error] 25-25: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/auth/sign-up/page.tsx` at line 25, Remove the explicit any cast from the
zodResolver usage: replace the call using "zodResolver(signUpSchema as any)"
with "zodResolver(signUpSchema)" so TypeScript can infer types; update the
resolver argument in the useForm invocation (the resolver passed to useForm in
page.tsx) to use zodResolver(signUpSchema) directly and confirm the form type is
inferred from z.infer<typeof signUpSchema> (no other code changes required).

defaultValues: {
name: "",
email: "",
password: "",
},
});

const onSubmit = () => {
console.log("yoo");
};
Comment on lines +34 to +36
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Placeholder onSubmit and missing submit-state handling.

A few things to address before this is merge-ready:

  1. onSubmit just logs "yoo" — needs a real implementation (and the handler should accept the validated data parameter typed as z.infer<typeof signUpSchema>).
  2. The <form> has no noValidate, so the browser’s native validation (triggered by type="email") will run alongside Zod and can short-circuit your resolver’s error messages.
  3. The submit button isn’t guarded by form.formState.isSubmitting, allowing duplicate submissions once async work is wired in.
🛠 Suggested shape
-  const onSubmit = () => {
-    console.log("yoo");
-  };
+  const onSubmit = async (data: z.infer<typeof signUpSchema>) => {
+    // TODO: submit to auth endpoint
+    console.log(data);
+  };
@@
-        <form onSubmit={form.handleSubmit(onSubmit)}>
+        <form onSubmit={form.handleSubmit(onSubmit)} noValidate>
@@
-            <Button type="submit">Sign up</Button>
+            <Button type="submit" disabled={form.formState.isSubmitting}>
+              Sign up
+            </Button>

Want me to open a follow-up issue to track the real submit implementation (API call, error surface, redirect)?

Also applies to: 44-44, 100-100

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/auth/sign-up/page.tsx` around lines 33 - 35, Replace the placeholder
onSubmit with a real handler that accepts the validated data parameter typed as
z.infer<typeof signUpSchema> (e.g., const onSubmit = async (data: z.infer<typeof
signUpSchema>) => { ... }) and perform the actual submit logic there; add the
noValidate attribute to the <form> element so browser native validation does not
short-circuit the Zod/resolver flow; and guard the submit button using
form.formState.isSubmitting (disable the button when isSubmitting is true) to
prevent duplicate submits once async work is wired in.


return (
<Card>
<CardHeader>
<CardTitle>Sign Up</CardTitle>
<CardDescription>Create a new account to get started</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
<Controller
control={form.control}
name="name"
render={({ field, fieldState }) => (
<Field>
<FieldLabel>Full Name</FieldLabel>
<Input
aria-invalid={fieldState.invalid}
placeholder="Jhon Doe"
{...field}
/>
{fieldState.error && (
<FieldError>{fieldState.error.message}</FieldError>
)}
</Field>
Comment on lines +51 to +61
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Associate FieldLabel with its Input (accessibility).

FieldLabel wraps Radix Label, which associates with a control either by nesting or via htmlFor/id. Here the label is a sibling of Input with neither set, so clicking the label won’t focus the input and assistive tech won’t announce the label with the field. The same applies to the email and password fields.

🛠 Proposed fix (apply to all three fields)
               render={({ field, fieldState }) => (
                 <Field>
-                  <FieldLabel>Full Name</FieldLabel>
+                  <FieldLabel htmlFor="name">Full Name</FieldLabel>
                   <Input
+                    id="name"
                     aria-invalid={fieldState.invalid}
-                    placeholder="Jhon Doe"
+                    placeholder="John Doe"
                     {...field}
                   />

Note: the "Jhon" placeholders on lines 54 and 71 are also typos for "John".

Also applies to: 67-78, 85-96

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/auth/sign-up/page.tsx` around lines 50 - 60, The FieldLabel and Input are
currently siblings so the label isn't associated with the control; update each
field (the Full Name, Email, and Password blocks using Field, FieldLabel, Input,
FieldError) to either nest Input inside FieldLabel or give the Input a unique id
and set FieldLabel's htmlFor to that id so clicking the label focuses the input
and assistive tech announces it; also correct the placeholder typos from "Jhon"
to "John".

)}
/>
<Controller
control={form.control}
name="email"
render={({ field, fieldState }) => (
<Field>
<FieldLabel>Email</FieldLabel>
<Input
aria-invalid={fieldState.invalid}
placeholder="jhon@doe.com"
type="email"
{...field}
/>
{fieldState.error && (
<FieldError>{fieldState.error.message}</FieldError>
)}
</Field>
)}
/>
<Controller
control={form.control}
name="password"
render={({ field, fieldState }) => (
<Field>
<FieldLabel>Password</FieldLabel>
<Input
aria-invalid={fieldState.invalid}
placeholder="********"
type="password"
{...field}
/>
{fieldState.error && (
<FieldError>{fieldState.error.message}</FieldError>
)}
</Field>
)}
/>

<Button type="submit">Sign up</Button>
</FieldGroup>
</form>
</CardContent>
</Card>
);
}
2 changes: 1 addition & 1 deletion app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,4 @@
html {
@apply font-sans;
}
}
}
10 changes: 4 additions & 6 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import type { Metadata } from "next";
import React from "react";
import { Geist, Geist_Mono } from "next/font/google";
import { Outfit, Geist_Mono } from "next/font/google";
import "./globals.css";
import Navbar from "@/components/web/navbar";
import { ThemeProvider } from "@/components/ui/theme-provider";

const geistSans = Geist({
variable: "--font-geist-sans",
const outfit = Outfit({
variable: "--font-sans",
subsets: ["latin"],
});

Expand All @@ -27,7 +26,7 @@ export default function RootLayout({
return (
<html lang="en" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
className={`${outfit.variable} ${geistMono.variable} font-sans h-full antialiased`}
>
<ThemeProvider
attribute="class"
Expand All @@ -36,7 +35,6 @@ export default function RootLayout({
disableTransitionOnChange
>
<main className={"w-full max-auto px-4 md:px-6 lg:px-8"}>
<Navbar />
{children}
</main>
</ThemeProvider>
Expand Down
10 changes: 10 additions & 0 deletions app/schemas/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import z from "zod";

export const signUpSchema = z.object({
name: z
.string()
.min(3, "Name must be at least 3 characters long")
.max(30, "Name must be at most 30 characters long"),
email: z.string().email("Invalid email address"),
password: z.string().min(6, "Password must be at least 6 characters long"),
});
20 changes: 10 additions & 10 deletions components/ui/button.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { Slot } from "radix-ui";

import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";

const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
Expand Down Expand Up @@ -38,8 +38,8 @@ const buttonVariants = cva(
variant: "default",
size: "default",
},
}
)
},
);

function Button({
className,
Expand All @@ -49,9 +49,9 @@ function Button({
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
asChild?: boolean;
}) {
const Comp = asChild ? Slot.Root : "button"
const Comp = asChild ? Slot.Root : "button";

return (
<Comp
Expand All @@ -61,7 +61,7 @@ function Button({
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
);
}

export { Button, buttonVariants }
export { Button, buttonVariants };
Loading
Loading