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
84 changes: 83 additions & 1 deletion src/app/(web)/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,85 @@
'use client';
import Link from 'next/link';
import { loginAction } from '@/lib/auth/auth-service';
import { useActionState, useEffect } from 'react';

export interface LoginState {
success: boolean;
errors: {
email?: string[];
password?: string[];
};
message: string;
}

Comment thread
cherman23 marked this conversation as resolved.
const initialState: LoginState = {
success: true,
errors: {},
message: '',
};

export default function LoginPage() {
return <div className="">login</div>;
const [state, formAction] = useActionState(loginAction, initialState);

useEffect(() => {
if (state && !state.success && Object.keys(state.errors).length === 0 && state.message) {
alert(state.message);
}
Comment on lines +25 to +27

Copilot AI Oct 17, 2025

Copy link

Choose a reason for hiding this comment

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

Using alert() for error messages provides poor user experience. Consider using a toast notification or inline error display instead.

Copilot uses AI. Check for mistakes.
}, [state]);

return (
<div className="flex min-h-screen items-center">
<div className="border-s-lightgrey container mx-auto max-w-md rounded-lg border-2 p-9">
<h1 className="mb-6 text-center text-xl font-medium">Sign In</h1>

<form action={formAction} className="flex flex-col gap-y-4">
<div className="flex flex-col gap-y-4">
<div className="flex flex-col gap-0.5">
<label htmlFor="email" className="font-semibold">
Email
</label>
<input
type="email"
name="email"
id="email"
className="bg-s-lightgrey rounded-lg px-2 py-3"
/>
{state?.errors?.email && (
<p className="text-sm text-red-500">{state.errors.email[0]}</p>
)}
</div>

<div className="flex flex-col gap-0.5">
<label htmlFor="password" className="font-semibold">
Password
</label>
<input
type="password"
name="password"
id="password"
className="bg-s-lightgrey rounded-lg px-2 py-3"
/>
{state?.errors?.password && (
<p className="text-sm text-red-500">{state.errors.password[0]}</p>
)}
</div>
</div>

<button
type="submit"
className="bg-s-purple hover:bg-s-purple/90 rounded-lg py-3 text-white transition-colors duration-200"
>
Continue
</button>

<div className="flex gap-x-1 text-sm">
<p>Don&apos;t have an account?</p>
<Link href={'/signup'} className="text-s-purple">
Sign Up
</Link>
</div>
</form>
</div>
</div>
);
}
101 changes: 100 additions & 1 deletion src/app/(web)/(auth)/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,102 @@
'use client';
import Link from 'next/link';
import { signup } from '@/lib/auth/auth-service';
import { useFormState } from 'react-dom';
import { useEffect } from 'react';

export interface SignupState {
success: boolean;
errors: {
name?: string[];
email?: string[];
password?: string[];
};
message: string;
}

Comment thread
cherman23 marked this conversation as resolved.
const initialState: SignupState = {
success: true,
errors: {},
message: '',
};

export default function SignupPage() {
return <div className="">signup</div>;
const [state, formAction] = useFormState(signup, initialState);

useEffect(() => {
if (state && !state.success && Object.keys(state.errors).length === 0 && state.message) {
alert(state.message);
}
Comment thread
cherman23 marked this conversation as resolved.
}, [state]);

return (
<div className="flex min-h-screen items-center">
<div className="border-s-lightgrey container mx-auto max-w-md rounded-lg border-2 p-9">
<h1 className="mb-6 text-center text-xl font-medium">Create an account</h1>

<form action={formAction} className="flex flex-col gap-y-4">
<div className="flex flex-col gap-y-4">
<div className="flex flex-col gap-0.5">
<label htmlFor="fullName" className="font-semibold">
Full Name
</label>
<input
type="text"
name="fullName"
id="fullName"
className="bg-s-lightgrey rounded-lg px-2 py-3"
/>
{state?.errors?.name && (
<p className="text-sm text-red-500">{state.errors.name[0]}</p>
)}
</div>

<div className="flex flex-col gap-0.5">
<label htmlFor="email" className="font-semibold">
Email
</label>
<input
type="email"
name="email"
id="email"
className="bg-s-lightgrey rounded-lg px-2 py-3"
/>
{state?.errors?.email && (
<p className="text-sm text-red-500">{state.errors.email[0]}</p>
)}
</div>

<div className="flex flex-col gap-0.5">
<label htmlFor="password" className="font-semibold">
Password
</label>
<input
type="password"
name="password"
id="password"
className="bg-s-lightgrey rounded-lg px-2 py-3"
/>
{state?.errors?.password && (
<p className="text-sm text-red-500">{state.errors.password[0]}</p>
)}
</div>
</div>

<button
type="submit"
className="bg-s-purple hover:bg-s-purple/90 rounded-lg py-3 text-white transition-colors duration-200"
>
Continue
</button>

<div className="flex gap-x-1 text-sm">
<p>Already have an account?</p>
<Link href={'/login'} className="text-s-purple">
Sign In
</Link>
</div>
</form>
</div>
</div>
);
}
7 changes: 7 additions & 0 deletions src/app/(web)/(crm)/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { getCurrentUser } from '@/lib/auth/auth-service';

export default async function DashboardPage() {
const name = await getCurrentUser();

return <div className="">{name?.name}</div>;
}
3 changes: 0 additions & 3 deletions src/app/(web)/dashboard/page.tsx

This file was deleted.

37 changes: 0 additions & 37 deletions src/app/api/auth/login/route.ts

This file was deleted.

17 changes: 11 additions & 6 deletions src/app/globals.css
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
@import 'tailwindcss';
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap');

:root {
--background: #ffffff;
--foreground: #171717;
--background: #171717;
--foreground: #ffffff;
--sarge-purple: #5d5bf7;
--sarge-lightgrey: #f3f3f5;
}

@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--color-s-purple: var(--sarge-purple);
--color-s-lightgrey: var(--sarge-lightgrey);
--font-sans: 'Inter', 'system-ui';
--font-mono: var(--font-geist-mono);
}

@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
--background: #ffffff;
--foreground: #0a0a0a;
}
}

body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
font-family: var(--font-sans);
}
4 changes: 2 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ const geistMono = Geist_Mono({
});

export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
title: 'Sarge',
description: 'Standardized Assessment Review, Grading, & Evaluation',
};

export default function RootLayout({
Expand Down
3 changes: 3 additions & 0 deletions src/lib/auth/auth-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use client';

// We need to create a React context to get the current user on the frontend in the authenticated pages
81 changes: 76 additions & 5 deletions src/lib/auth/auth-service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'server-only';
'use server';

import { createSession, deleteSession, verifySession } from './auth';
import { redirect } from 'next/navigation';
Expand All @@ -7,13 +7,84 @@ import { prisma } from '../prisma';
import bcrypt from 'bcrypt';
import { AuthorizationError } from '../schemas/errors';
import { type User } from '@/generated/prisma';
import { createUserSchema, loginUserSchema } from '../schemas/user.schema';
import { type SignupState } from '@/app/(web)/(auth)/signup/page';
import { type LoginState } from '@/app/(web)/(auth)/login/page';

export interface LoginCredentials {
email: string;
password: string;
export async function signup(
prevState: SignupState | null,
formData: FormData
): Promise<SignupState> {
const parsedCreds = createUserSchema.safeParse({
name: formData.get('fullName'),
email: formData.get('email'),
password: formData.get('password'),
});

if (!parsedCreds.success) {
return {
success: false,
errors: parsedCreds.error.flatten().fieldErrors,
message: 'Please check your input and try again.',
};
}

try {
const hashedPassword = await bcrypt.hash(parsedCreds.data.password, 10);

const user = await prisma.user.create({
data: {
name: parsedCreds.data.name,
email: parsedCreds.data.email,
hashedPassword,
},
});

await createSession({ userId: user.id, email: user.email });
} catch (error) {
console.error('Signup error:', error);
return {
success: false,
errors: {},
message: 'There was an error creating your account. Please try again.',
};
}

redirect('/dashboard');
}

export async function loginAction(
prevState: LoginState | null,
formData: FormData
): Promise<LoginState> {
const parsedCreds = loginUserSchema.safeParse({
email: formData.get('email'),
password: formData.get('password'),
});

if (!parsedCreds.success) {
return {
success: false,
errors: parsedCreds.error.flatten().fieldErrors,
message: 'Please check your input and try again.',
};
}

try {
await login(parsedCreds.data);
} catch (error) {
console.error('Login error:', error);
return {
success: false,
errors: {},
message: 'Invalid email or password. Please try again.',
};
}

redirect('/dashboard');
}

export async function login(_credentials: LoginCredentials): Promise<User> {
export async function login(_credentials: { email: string; password: string }): Promise<User> {
const user = await prisma.user.findUnique({
where: {
email: _credentials.email,
Expand Down
Loading