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
3 changes: 1 addition & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
name: Nextblog CI

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

Expand Down Expand Up @@ -47,3 +45,4 @@ jobs:
NEXT_PUBLIC_CONVEX_URL: "https://insightful-pheasant-237.convex.cloud"
NEXT_PUBLIC_CONVEX_SITE_URL: "https://insightful-pheasant-237.convex.site"
NEXT_PUBLIC_SITE_URL: "http://localhost:3000"
BETTER_AUTH_SECRET: "dummy-secret-for-ci-build"
11 changes: 11 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"printWidth": 80,
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "lf"
}
121 changes: 121 additions & 0 deletions app/(shared-layout)/create/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"use client";

import { useTransition } from "react";
import { Loader2 } from "lucide-react";
import { Controller, useForm } from "react-hook-form";
import { useMutation } from "convex/react";
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { CardContent } from "@/components/ui/card";
import {
Field,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { zodResolver } from "@hookform/resolvers/zod";
import { postSchema } from "@/app/schemas/blog";
import { api } from "@/convex/_generated/api";
import { Textarea } from "@/components/ui/textarea";
import { toast } from "sonner";
import { z } from "zod/v3";

export default function CreatePage() {
const form = useForm<z.infer<typeof postSchema>>({
resolver: zodResolver(postSchema),
defaultValues: {
title: "",
content: "",
},
});
const createPost = useMutation(api.posts.createPost);
const [isPending, startTransition] = useTransition();

const onSubmit = (data: z.infer<typeof postSchema>) => {
startTransition(async () => {
try {
await createPost(data);
toast.success("Post created successfully!");
form.reset();
} catch {
toast.error("Failed to create post");
}
});
};
return (
<div className="py-12">
<div className="text-center mb-12">
<h1 className="text-3xl font-extrabold tracking-tight sm:text-5xl">
Create Blog
</h1>
<p className="text-xl text-muted-foreground pt-4">
Share your knowledge and ideas with the world.
</p>
</div>

<Card className="w-full max-w-xl mx-auto">
<CardHeader>
<CardTitle>Create Blog Article</CardTitle>
<CardDescription>Create a new blog article</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup className="gap-y-4">
<Controller
control={form.control}
name="title"
render={({ field, fieldState }) => (
<Field>
<FieldLabel>Title</FieldLabel>
<Input
aria-invalid={fieldState.invalid}
placeholder="Title"
{...field}
/>
{fieldState.error && (
<FieldError>{fieldState.error.message}</FieldError>
)}
</Field>
)}
/>
<Controller
control={form.control}
name="content"
render={({ field, fieldState }) => (
<Field>
<FieldLabel>Content</FieldLabel>
<Textarea
aria-invalid={fieldState.invalid}
placeholder="Content"
{...field}
/>
{fieldState.error && (
<FieldError>{fieldState.error.message}</FieldError>
)}
</Field>
)}
/>

<Button type="submit" className="w-full" disabled={isPending}>
{isPending ? (
<>
<Loader2 className="size-4 animate-spin" />{" "}
<span>Loading...</span>
</>
) : (
<span>Create Blog</span>
)}
</Button>
</FieldGroup>
</form>
</CardContent>
</Card>
</div>
);
}
15 changes: 0 additions & 15 deletions app/(shared-layout)/test/page.tsx

This file was deleted.

144 changes: 143 additions & 1 deletion app/auth/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,145 @@
"use client";

import { useTransition } from "react";
import { useRouter } from "next/navigation";
import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod/v3";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription,
} from "@/components/ui/card";
import { toast } from "sonner";
import { authClient } from "@/lib/auth-client";
import {
Field,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
import { Loader2 } from "lucide-react";

const loginSchema = z.object({
email: z.string().email("Invalid email address"),
password: z.string().min(1, "Password is required"),
});

type LoginFormValues = z.infer<typeof loginSchema>;

export default function LoginPage() {
return <h1>Login Page</h1>;
const router = useRouter();
const [isPending, startTransition] = useTransition();

const form = useForm<LoginFormValues>({
resolver: zodResolver(loginSchema),
defaultValues: {
email: "",
password: "",
},
});

const onSubmit = (data: LoginFormValues) => {
startTransition(async () => {
try {
const result = await authClient.signIn.email({
email: data.email,
password: data.password,
});

if (result.error) {
toast.error(result.error.message || "Failed to sign in");
} else {
toast.success("Logged in successfully!");
router.push("/");
router.refresh();
}
} catch {
toast.error("An unexpected error occurred. Please try again.");
}
});
};

return (
<div className="flex items-center justify-center min-h-screen p-4">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Sign In</CardTitle>
<CardDescription>
Enter your credentials to access your account
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
<Controller
control={form.control}
name="email"
render={({ field, fieldState }) => (
<Field>
<FieldLabel>Email</FieldLabel>
<Input
aria-invalid={fieldState.invalid}
placeholder="john@example.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" className="w-full" disabled={isPending}>
{isPending ? (
<>
<Loader2 className="size-4 animate-spin" />{" "}
<span>Loading...</span>
</>
) : (
<span>Sign In</span>
)}
</Button>
</FieldGroup>
</form>

<div className="mt-6 text-center">
<p className="text-sm text-muted-foreground">
Don&apos;t have an account?{" "}
<Button
variant="link"
onClick={() => router.push("/auth/sign-up")}
className="h-auto p-0"
>
Sign up
</Button>
</p>
</div>
</CardContent>
</Card>
</div>
);
}
45 changes: 36 additions & 9 deletions app/auth/sign-up/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { signUpSchema } from "@/app/schemas/auth";
import { z } from "zod";
import { z } from "zod/v3";
import {
Card,
CardContent,
Expand All @@ -20,23 +20,41 @@ import {
} from "@/components/ui/field";
import { Button } from "@/components/ui/button";
import { authClient } from "@/lib/auth-client";
import { toast } from "sonner";
import { useTransition } from "react";
import { useRouter } from "next/navigation";
import { Loader2 } from "lucide-react";

export default function SignupPage() {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const form = useForm<z.infer<typeof signUpSchema>>({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
resolver: zodResolver(signUpSchema as any),
resolver: zodResolver(signUpSchema),
defaultValues: {
name: "",
email: "",
password: "",
},
});

const onSubmit = async (data: z.infer<typeof signUpSchema>) => {
await authClient.signUp.email({
email: data.email,
name: data.name,
password: data.password,
const onSubmit = (data: z.infer<typeof signUpSchema>) => {
startTransition(async () => {
try {
const result = await authClient.signUp.email({
email: data.email,
name: data.name,
password: data.password,
});

if (result.error) {
toast.error(result.error.message || "Failed to sign up");
} else {
toast.success("Account created successfully!");
router.push("/auth/login");
}
} catch {
toast.error("An unexpected error occurred. Please try again.");
}
});
};

Expand Down Expand Up @@ -103,7 +121,16 @@ export default function SignupPage() {
)}
/>

<Button type="submit">Sign up</Button>
<Button type="submit" disabled={isPending}>
{isPending ? (
<>
<Loader2 className="size-4 animate-spin" />{" "}
<span>Loading...</span>
</>
) : (
<span>Sign up</span>
)}
</Button>
</FieldGroup>
</form>
</CardContent>
Expand Down
Loading
Loading