diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bf506ef --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/AGENTS.md b/AGENTS.md index 8bd0e39..c153a9b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,7 @@ + # 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. + diff --git a/app/(shared-layout)/layout.tsx b/app/(shared-layout)/layout.tsx new file mode 100644 index 0000000..7efb839 --- /dev/null +++ b/app/(shared-layout)/layout.tsx @@ -0,0 +1,14 @@ +import Navbar from "@/components/web/navbar"; + +export default function SharedLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + <> + + {children} + + ); +} diff --git a/app/page.tsx b/app/(shared-layout)/page.tsx similarity index 51% rename from app/page.tsx rename to app/(shared-layout)/page.tsx index 2a34784..fdf7bbd 100644 --- a/app/page.tsx +++ b/app/(shared-layout)/page.tsx @@ -1,3 +1,3 @@ export default function Page() { - return

Home Page

-} \ No newline at end of file + return

Home Page

; +} diff --git a/app/auth/login/page.tsx b/app/auth/login/page.tsx index 095a345..d50f837 100644 --- a/app/auth/login/page.tsx +++ b/app/auth/login/page.tsx @@ -1,3 +1,3 @@ export default function LoginPage() { - return

Login Page

-} \ No newline at end of file + return

Login Page

; +} diff --git a/app/auth/sign-up/layout.tsx b/app/auth/sign-up/layout.tsx new file mode 100644 index 0000000..7f2137a --- /dev/null +++ b/app/auth/sign-up/layout.tsx @@ -0,0 +1,21 @@ +import { buttonVariants } from "@/components/ui/button"; +import { ArrowLeft } from "lucide-react"; +import Link from "next/link"; + +export default function authLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+
+ + + Back to Home + +
+
{children}
+
+ ); +} diff --git a/app/auth/sign-up/page.tsx b/app/auth/sign-up/page.tsx index 6828276..f99cb57 100644 --- a/app/auth/sign-up/page.tsx +++ b/app/auth/sign-up/page.tsx @@ -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

Sign Up Page

-} \ No newline at end of file + const form = useForm>({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + resolver: zodResolver(signUpSchema as any), + defaultValues: { + name: "", + email: "", + password: "", + }, + }); + + const onSubmit = () => { + console.log("yoo"); + }; + + return ( + + + Sign Up + Create a new account to get started + + +
+ + ( + + Full Name + + {fieldState.error && ( + {fieldState.error.message} + )} + + )} + /> + ( + + Email + + {fieldState.error && ( + {fieldState.error.message} + )} + + )} + /> + ( + + Password + + {fieldState.error && ( + {fieldState.error.message} + )} + + )} + /> + + + +
+
+
+ ); +} diff --git a/app/globals.css b/app/globals.css index c56032b..6a25b1b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -127,4 +127,4 @@ html { @apply font-sans; } -} \ No newline at end of file +} diff --git a/app/layout.tsx b/app/layout.tsx index 846b50b..bf63635 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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"], }); @@ -27,7 +26,7 @@ export default function RootLayout({ return (
- {children}
diff --git a/app/schemas/auth.ts b/app/schemas/auth.ts new file mode 100644 index 0000000..90b1d56 --- /dev/null +++ b/app/schemas/auth.ts @@ -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"), +}); diff --git a/components/ui/button.tsx b/components/ui/button.tsx index c0d08ef..c34204d 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -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", @@ -38,8 +38,8 @@ const buttonVariants = cva( variant: "default", size: "default", }, - } -) + }, +); function Button({ className, @@ -49,9 +49,9 @@ function Button({ ...props }: React.ComponentProps<"button"> & VariantProps & { - asChild?: boolean + asChild?: boolean; }) { - const Comp = asChild ? Slot.Root : "button" + const Comp = asChild ? Slot.Root : "button"; return ( - ) + ); } -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx index c263ad5..3e3e145 100644 --- a/components/ui/dropdown-menu.tsx +++ b/components/ui/dropdown-menu.tsx @@ -1,15 +1,15 @@ -"use client" +"use client"; -import * as React from "react" -import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui" +import * as React from "react"; +import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"; -import { cn } from "@/lib/utils" -import { CheckIcon, ChevronRightIcon } from "lucide-react" +import { cn } from "@/lib/utils"; +import { CheckIcon, ChevronRightIcon } from "lucide-react"; function DropdownMenu({ ...props }: React.ComponentProps) { - return + return ; } function DropdownMenuPortal({ @@ -17,7 +17,7 @@ function DropdownMenuPortal({ }: React.ComponentProps) { return ( - ) + ); } function DropdownMenuTrigger({ @@ -28,7 +28,7 @@ function DropdownMenuTrigger({ data-slot="dropdown-menu-trigger" {...props} /> - ) + ); } function DropdownMenuContent({ @@ -43,11 +43,14 @@ function DropdownMenuContent({ data-slot="dropdown-menu-content" sideOffset={sideOffset} align={align} - className={cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )} + className={cn( + "z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", + className, + )} {...props} /> - ) + ); } function DropdownMenuGroup({ @@ -55,7 +58,7 @@ function DropdownMenuGroup({ }: React.ComponentProps) { return ( - ) + ); } function DropdownMenuItem({ @@ -64,8 +67,8 @@ function DropdownMenuItem({ variant = "default", ...props }: React.ComponentProps & { - inset?: boolean - variant?: "default" | "destructive" + inset?: boolean; + variant?: "default" | "destructive"; }) { return ( - ) + ); } function DropdownMenuCheckboxItem({ @@ -88,7 +91,7 @@ function DropdownMenuCheckboxItem({ inset, ...props }: React.ComponentProps & { - inset?: boolean + inset?: boolean; }) { return ( - + {children} - ) + ); } function DropdownMenuRadioGroup({ @@ -123,7 +125,7 @@ function DropdownMenuRadioGroup({ data-slot="dropdown-menu-radio-group" {...props} /> - ) + ); } function DropdownMenuRadioItem({ @@ -132,7 +134,7 @@ function DropdownMenuRadioItem({ inset, ...props }: React.ComponentProps & { - inset?: boolean + inset?: boolean; }) { return ( @@ -149,13 +151,12 @@ function DropdownMenuRadioItem({ data-slot="dropdown-menu-radio-item-indicator" > - + {children} - ) + ); } function DropdownMenuLabel({ @@ -163,7 +164,7 @@ function DropdownMenuLabel({ inset, ...props }: React.ComponentProps & { - inset?: boolean + inset?: boolean; }) { return ( - ) + ); } function DropdownMenuSeparator({ @@ -188,7 +189,7 @@ function DropdownMenuSeparator({ className={cn("-mx-1 my-1 h-px bg-border", className)} {...props} /> - ) + ); } function DropdownMenuShortcut({ @@ -200,17 +201,17 @@ function DropdownMenuShortcut({ data-slot="dropdown-menu-shortcut" className={cn( "ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground", - className + className, )} {...props} /> - ) + ); } function DropdownMenuSub({ ...props }: React.ComponentProps) { - return + return ; } function DropdownMenuSubTrigger({ @@ -219,7 +220,7 @@ function DropdownMenuSubTrigger({ children, ...props }: React.ComponentProps & { - inset?: boolean + inset?: boolean; }) { return ( {children} - ) + ); } function DropdownMenuSubContent({ @@ -244,10 +245,13 @@ function DropdownMenuSubContent({ return ( - ) + ); } export { @@ -266,4 +270,4 @@ export { DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, -} +}; diff --git a/components/ui/field.tsx b/components/ui/field.tsx new file mode 100644 index 0000000..9871b0d --- /dev/null +++ b/components/ui/field.tsx @@ -0,0 +1,238 @@ +"use client"; + +import { useMemo } from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; +import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; + +function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { + return ( +
[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", + className, + )} + {...props} + /> + ); +} + +function FieldLegend({ + className, + variant = "legend", + ...props +}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) { + return ( + + ); +} + +function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +const fieldVariants = cva( + "group/field flex w-full gap-2 data-[invalid=true]:text-destructive", + { + variants: { + orientation: { + vertical: "flex-col *:w-full [&>.sr-only]:w-auto", + horizontal: + "flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + responsive: + "flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + }, + }, + defaultVariants: { + orientation: "vertical", + }, + }, +); + +function Field({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ); +} + +function FieldContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function FieldLabel({ + className, + ...props +}: React.ComponentProps) { + return ( +