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: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
DATABASE_URL=
DATABASE_URL=
RESEND_API_KEY=
947 changes: 936 additions & 11 deletions package-lock.json

Large diffs are not rendered by default.

18 changes: 14 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,41 +13,51 @@
},
"dependencies": {
"@floating-ui/dom": "^1.7.4",
"@hookform/resolvers": "^5.2.2",
"@mantine/core": "^8.3.4",
"@mantine/dates": "^8.3.5",
"@mantine/hooks": "^8.3.4",
"@neondatabase/serverless": "^1.0.2",
"@next/eslint-plugin-next": "^15.5.4",
"d3": "^7.9.0",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@tanstack/react-table": "^8.21.3",
"better-auth": "^1.4.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"d3": "^7.9.0",
"dotenv": "^17.2.3",
"drizzle-orm": "^0.44.7",
"input-otp": "^1.4.2",
"lucide": "^0.544.0",
"lucide-react": "^0.545.0",
"next": "15.5.4",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-excel-renderer": "^1.1.0",
"react-hook-form": "^7.67.0",
"react-icons": "^5.5.0",
"resend": "^6.6.0",
"tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7",
"react-icons": "^5.5.0",
"vercel": "^48.2.0",
"xlsx": "^0.18.5"
"xlsx": "^0.18.5",
"zod": "^4.1.13"
},
"devDependencies": {
"@eslint/js": "^9.37.0",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"drizzle-kit": "^0.31.6",
"drizzle-kit": "^0.31.7",
"eslint": "^9.37.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.4.0",
"husky": "^9.1.7",
"knip": "^5.79.0",
"lint-staged": "^16.2.3",
"prettier": "3.6.2",
"tailwindcss": "^4",
Expand Down
Binary file added public/mhs-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/app/api/auth/[...all]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";

export const { POST, GET } = toNextJsHandler(auth);
7 changes: 7 additions & 0 deletions src/app/dashboard/dummyPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function DummyPage() {
return (
<div>
<h1>Hello World</h1>
</div>
);
}
18 changes: 18 additions & 0 deletions src/app/signin/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import AuthForm from "@/components/AuthForm";
import WarpShader from "@/components/WarpShader";

export default function SignInPage() {
return (
<div className="flex h-screen flex-col items-center justify-center">
<div className="w-full h-full flex flex-row">
<AuthForm />
<div className="w-1/2 h-full">
<WarpShader
colorFront={{ r: 0.784, g: 0.192, b: 0.22, a: 1 }}
colorBack={{ r: 1.0, g: 0.498, b: 0.525, a: 1 }}
/>
</div>
</div>
</div>
);
}
187 changes: 187 additions & 0 deletions src/components/AuthForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
"use client";

import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { authClient } from "@/lib/auth-client";
import { useRouter } from "next/navigation";
import Image from "next/image";

export default function AuthForm() {
const [step, setStep] = useState<"email" | "otp">("email");
const [email, setEmail] = useState("");
const [otp, setOtp] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");

const router = useRouter();

async function handleSendCode(e: React.FormEvent) {
e.preventDefault();
setIsLoading(true);
setError(""); // TO DO: Change error handling to toast
try {
await authClient.emailOtp.sendVerificationOtp({
email,
type: "sign-in",
});
setStep("otp");
} catch (error) {
setError("Failed to send verification code. Please try again.");
console.error(error);
} finally {
setIsLoading(false);
}
}

async function handleVerifyOtp(e: React.FormEvent) {
e.preventDefault();
setIsLoading(true);
setError("");
try {
await authClient.signIn.emailOtp({
email,
otp,
});
router.push("/");
} catch (error) {
setError("Invalid or expired code. Please try again.");
console.error(error);
} finally {
setIsLoading(false);
}
}

async function handleResendCode() {
setIsLoading(true);
setError("");
try {
await authClient.emailOtp.sendVerificationOtp({
email,
type: "sign-in",
});
setOtp("");
} catch (err) {
setError("Failed to resend code. Please try again.");
console.error(err);
} finally {
setIsLoading(false);
}
}

return (
<div className="w-1/2 max-w-md mx-auto p-6 mt-16 flex flex-col gap-16 items-center">
<Image
src="/mhs-logo.png"
alt="MHS Logo Image"
width={256}
height={128}
/>
<div className="flex flex-col gap-2">
<h1 className="text-3xl font-bold">Sign In</h1>
<p className="text-[#646464]">
Enter your email and a one time password will be sent to
you. If you do not have an account yet, speak to an
appropriate administrator at MHD.
</p>
</div>

<div className="w-full">
{step === "email" ? (
<form onSubmit={handleSendCode} className="space-y-4">
<div>
<label
htmlFor="email"
className="block text-sm text-[#646464] mb-2"
>
Email
</label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="[email protected]"
required
/>
</div>
{error && (
<div className="p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded">
{error}
</div>
)}
<Button
type="submit"
className="w-48 bg-[#1447E6]"
disabled={isLoading}
>
{isLoading ? "Sending..." : "Sign in"}
</Button>
</form>
) : (
<div className="space-y-4">
<form onSubmit={handleVerifyOtp} className="space-y-4">
<div>
<label
htmlFor="otp"
className="block text-sm font-medium mb-2 text-[#646464]"
>
Code
</label>
<Input
id="otp"
type="text"
value={otp}
onChange={(e) =>
setOtp(
e.target.value
.replace(/\D/g, "")
.slice(0, 6),
)
}
placeholder="123456"
maxLength={6}
required
className="text-center text-2xl tracking-widest"
/>
</div>
<Button
type="submit"
className="w-full bg-[#1447E6]"
disabled={isLoading}
>
{isLoading ? "Verifying..." : "Verify"}
</Button>
</form>
<div className="flex gap-2">
<Button
type="button"
variant="outline"
className="flex-1"
onClick={handleResendCode}
disabled={isLoading}
>
Resend Code
</Button>
<Button
type="button"
variant="outline"
className="flex-1"
onClick={() => {
setStep("email");
setOtp("");
setError("");
}}
>
Change Email
</Button>
</div>
</div>
)}
</div>
<div className="mt-auto">
<p className="text-[#646464]">Created with ❤️ by JumboCode</p>
</div>
</div>
);
}
Loading