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
72 changes: 72 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@
"prisma:reset": "prisma migrate reset"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@prisma/client": "^6.15.0",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
"@rainbow-me/rainbowkit": "^2.2.8",
"@tanstack/react-query": "^5.87.1",
"bcryptjs": "^3.0.2",
Expand All @@ -29,7 +32,9 @@
"next-safe-action": "^8.0.11",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.63.0",
"react-icons": "^5.5.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"viem": "^2.37.5",
"wagmi": "^2.16.9",
Expand Down
4 changes: 2 additions & 2 deletions src/actions/auth/signin/logic.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import 'server-only';

import { prisma } from "@/lib/prisma";
import { signinInput } from "./schema";
import bcrypt from "bcryptjs";
import { error, Result, success } from "@/lib/result";
import { getSession } from "@/lib/session";
import type { User } from "@prisma/client";
import { SigninInput } from './schema';

export async function signin(input: signinInput): Promise<Result<Omit<User, 'password'>>> {
export async function signin(input: SigninInput): Promise<Result<Omit<User, 'password'>>> {
const { email, password } = input;

const normalisedEmail = email.toLowerCase().trim();
Expand Down
2 changes: 1 addition & 1 deletion src/actions/auth/signin/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ export const signinSchema = z.object({
password: z.string().min(1,'Password is required')
});

export type signinInput = z.infer<typeof signinSchema>
export type SigninInput = z.infer<typeof signinSchema>
8 changes: 4 additions & 4 deletions src/actions/auth/signup/action.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
'use server';

import { actionClient } from '@/lib/action';
import { Signup} from './logic';
import { SignupSchema} from './schema';
import { signup} from './logic';
import { signupSchema} from './schema';

export const signupAction = actionClient
.inputSchema(SignupSchema)
.inputSchema(signupSchema)
.metadata({ actionName: 'signup' })
.action(async ({ parsedInput }) => {
try {
const result = await Signup(parsedInput);
const result = await signup(parsedInput);

if (result.success) {
return result.data;
Expand Down
9 changes: 6 additions & 3 deletions src/actions/auth/signup/logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import 'server-only';

import { prisma } from "@/lib/prisma";
import bcrypt from "bcryptjs";
import { signupInput } from "./schema";
import { error, Result, success } from "@/lib/result";
import { getSession } from "@/lib/session";
import type { User } from "@prisma/client";
import { SignupInput } from './schema';

export async function Signup(input: signupInput): Promise<Result<User>> {
export async function signup(input: SignupInput): Promise<Result<Omit<User, 'password'>>> {
const { email, name, password } = input;

const normalisedEmail = email.toLowerCase().trim();
Expand All @@ -34,5 +34,8 @@ export async function Signup(input: signupInput): Promise<Result<User>> {
session.user = { id: user.id };
await session.save();

return success(user);
// Omit password before returning
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { password: _password, ...userWithoutPassword } = user;
return success(userWithoutPassword);
}
30 changes: 19 additions & 11 deletions src/actions/auth/signup/schema.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { z } from "zod";

export const signupSchema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email format'),
password: z.string()
.min(8, 'Password must be at least 8 characters long')
.regex(/(?=.*[a-z])/, 'Password must contain at least one lowercase letter')
.regex(/(?=.*[A-Z])/, 'Password must contain at least one uppercase letter')
.regex(/(?=.*\d)/, 'Password must contain at least one number')
export const signupSchema = z
.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email format'),
password: z.string()
.min(8, 'Password must be at least 8 characters long')
.regex(/(?=.*[a-z])/, 'Password must contain at least one lowercase letter')
.regex(/(?=.*[A-Z])/, 'Password must contain at least one uppercase letter')
.regex(/(?=.*\d)/, 'Password must contain at least one number'),
confirmPassword: z.string().min(1, 'Please confirm your password'),
terms: z.literal(true, {
errorMap: () => ({ message: 'You must accept the terms' })
}),
})
.refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match',
path: ['confirmPassword'],
});

})

export type signupInput = z.infer<typeof signupSchema>
export type SignupInput = z.infer<typeof signupSchema>
9 changes: 9 additions & 0 deletions src/app/(auth)/signin/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"use client";

import { SigninPageContainer } from "@/components/pages/signin/SigninPageContainer";



export default function SignInPage() {
return <SigninPageContainer />;
}
7 changes: 7 additions & 0 deletions src/app/(auth)/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { SignupPageContainer } from "@/components/pages/signup/SignupPageContainer";

export default function Signup(){
return(
<SignupPageContainer/>
)
}
5 changes: 0 additions & 5 deletions src/app/auth/signin/page.tsx

This file was deleted.

5 changes: 0 additions & 5 deletions src/app/auth/signup/page.tsx

This file was deleted.

115 changes: 115 additions & 0 deletions src/components/pages/signin/SigninPageContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"use client";

import Link from "next/link";
import { useRouter } from "next/navigation";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { signinAction } from "@/actions/auth/signin/action";
import { useForm } from "react-hook-form";
import { useAction } from "next-safe-action/hooks";
import { Form } from "@/components/ui/form";
import FormInput from "@/components/shared/Form/FormInput";
import { signinSchema } from "@/actions/auth/signin/schema";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { toast } from "sonner";
import { FormButton } from "@/components/shared/Form/FormButton";

type SignInFormValues = z.infer<typeof signinSchema>;

export function SigninPageContainer() {
const form = useForm<SignInFormValues>({
mode: "onChange",
resolver: zodResolver(signinSchema),
defaultValues: {
email: "",
password: "",
},
});
const router = useRouter();

const { execute, isExecuting } = useAction(signinAction, {
onSuccess: () => {
toast.success("Signed in successfully!");
router.push("/");
},
onError: (error) => {
const fieldErrors = error.error.validationErrors?.fieldErrors;
const errorMessage =
error.error.serverError ??
(fieldErrors
? Object.entries(fieldErrors)
.map(([key, value]) => `${key}: ${value}`)
.join(", ")
: "An unknown error occurred");
toast.error(errorMessage);
},
});

return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<Card className="w-full max-w-md">
<CardHeader className="space-y-1">
<CardTitle className="text-2xl font-bold text-center">
Sign in to Sentiopulse
</CardTitle>
<CardDescription className="text-center">
Enter your credentials to access your account
</CardDescription>
</CardHeader>

<Form {...form}>
<form onSubmit={form.handleSubmit(execute)}>
<CardContent className="space-y-4">
<FormInput
control={form.control}
name="email"
label="Email"
placeholder="john@example.com"
type="email"
required
/>
<FormInput
control={form.control}
name="password"
label="Password"
placeholder="Enter your password"
type="password"
required
/>
</CardContent>

<CardFooter className="flex flex-col space-y-4">
<FormButton
className="w-full"
loading={isExecuting}
disabled={
!form.formState.isValid ||
!form.formState.isDirty ||
isExecuting
}
>
Sign in
</FormButton>
<p className="text-center text-sm text-gray-600">
Don&apos;t have an account?{" "}
<Link
href="/signup"
className="text-blue-600 hover:underline font-medium"
>
Sign up
</Link>
</p>
</CardFooter>
</form>
</Form>
</Card>
</div>
);
}
Loading