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
10 changes: 9 additions & 1 deletion next.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
/* config options here */
async redirects() {
return [
{
source: "/",
destination: "/home",
permanent: true,
},
];
},
};

export default nextConfig;
9 changes: 9 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"dependencies": {
"axios": "^1.10.0",
"bcryptjs": "^3.0.2",
"email-validator": "^2.0.4",
"jsonwebtoken": "^9.0.2",
"next": "15.3.2",
"nodemailer": "^7.0.3",
Expand Down
64 changes: 64 additions & 0 deletions src/app/(main)/forgotpassword/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"use client";

import React from "react";
import axios from "axios";
import toast, { Toaster } from "react-hot-toast";
import { useState } from "react";
import * as EmailValidator from "email-validator";

export default function ForgotPassword() {
const [email, setEmail] = useState<string>("");

const handleSubmit = async () => {
try {
if (EmailValidator.validate(email)) {
const res = await axios.post("/api/users/forgotpassword", { email });
toast.success(res.data.message);
} else {
toast.error("Invalid email address");
}
} catch (error: any) {
if (error.response) {
toast.error(error.response.data.error || "An error occurred");
} else {
toast.error("An error occurred");
}
}
};

return (
<div>
<Toaster position="top-left" reverseOrder={false} />
<div className="w-full max-w-xl bg-gradient-to-b from-black-900 to-purple-600 mx-auto p-6 rounded-lg shadow-xl">
<form className="space-y-8">
<h1 className="text-4xl text-black flex justify-center">
Forgot your Password? 😭
</h1>
<div>
<label htmlFor="email" className="block mb-2 text-2xl text-black">
Email
</label>
<input
type="email"
onChange={(e) => setEmail(e.target.value)}
value={email}
placeholder="youremail@example.com"
className="rounded bg-purple-500 w-full px-3 py-1 font-mono text-black"
></input>
</div>

<div className="flex justify-center">
<button
type="button"
className="bg-purple-500 hover:bg-purple-300 transition-colors duration-200 text-black text-xl py-2 px-4 rounded flex justify-center"
onClick={handleSubmit}
disabled={!email}
>
Send Reset Link
</button>
</div>
</form>
</div>
</div>
);
}
67 changes: 67 additions & 0 deletions src/app/(main)/logout/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"use client";

import React from "react";
import axios from "axios";
import { toast, Toaster } from "react-hot-toast";
import { useRouter } from "next/navigation";

export default function Logout() {
const [processing, setProcessing] = React.useState(false);
const [buttonDisabled, setButtonDisabled] = React.useState(false);
const [loggedout, setLoggedout] = React.useState(false);
const [error, setError] = React.useState(false);
const router = useRouter();

//call logout function onClick
const onLogout = async () => {
setProcessing(true);
setButtonDisabled(true);
try {
const userdata = await axios.get("/api/users/logout");
toast.success(userdata.data.message);
setLoggedout(true);
setTimeout(() => {
router.push("/home");
}, 5000);
} catch (error: any) {
console.log(error);
setError(true);
toast.error("An error occurred during logout.");
} finally {
setProcessing(false);
}
};
return (
<main className="flex flex-col">
<Toaster position="top-left" reverseOrder={false} />
<div className="w-full max-w-xl bg-gradient-to-b from-black-900 to-purple-600 mx-auto mt-[40%] p-12 rounded-lg shadow-xl">
<div className="flex flex-col items-center">
{processing ? (
<p className="text-lg text-yellow-600">
Logging out of your account...
</p>
) : loggedout ? (
<p className="text-lg text-green-600">Logged out successfully 😔</p>
) : error ? (
<p className="text-lg text-red-600">
Logging out failed. Please try again or contact support.
</p>
) : (
<p className="text-lg text-black-500">
Please stay pretty please with a cherry on top 🥺.
</p>
)}
<button
type="button"
className="bg-purple-500 hover:bg-purple-300 transition-colors duration-200 text-black text-xl py-2 px-4 rounded flex justify-center"
onClick={onLogout}
// make sure button is disabled by wiping tokens and user data from client
disabled={buttonDisabled || processing}
>
Logout
</button>
</div>
</div>
</main>
);
}
128 changes: 128 additions & 0 deletions src/app/(main)/resetpassword/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"use client";

import axios from "axios";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import toast, { Toaster } from "react-hot-toast";

export default function ResetPasswordPage() {
const router = useRouter();
const [password, setPassword] = useState("");
const [confirmpassword, setConfirmPassword] = useState("");
const [loading, setLoading] = useState(false);
const [token, setToken] = useState("");

//search URL for the token that can only be received from nodemailer
useEffect(() => {
const urlToken = new URLSearchParams(window.location.search).get("token");
console.log(urlToken);
setToken(urlToken || "");
if (!urlToken) {
toast.error("Invalid reset request");
}
}, []);

const handleResetPassword = async () => {
if (password !== confirmpassword) {
toast.error("Passwords don't match");
return;
}

if (password.length < 8) {
toast.error("Password must be at least 8 characters");
return;
}

if (!/\d/.test(password)) {
toast.error("Password must contain at least one number");
return;
}

try {
setLoading(true);
const response = await axios.post("/api/users/resetpassword", {
token,
password,
});

if (response.data.success) {
toast.success("Password reset successfully!");
setTimeout(() => router.push("/login"), 2000);
} else {
throw new Error(response.data.error || "Password reset failed");
}
} catch (error: any) {
const errorMessage =
error.response?.data?.error ||
error.message ||
"Failed to reset password";
toast.error(errorMessage);

// If token is invalid, redirect after showing error
if (error.response?.status === 400) {
setTimeout(() => router.push("/forgotpassword"), 3000);
}
} finally {
setLoading(false);
}
};

return (
<div>
<Toaster position="top-left" reverseOrder={false} />
<div className="w-full max-w-xl bg-gradient-to-b from-black-900 to-purple-600 mx-auto p-6 rounded-lg shadow-xl">
<form className="space-y-8">
<h1 className="text-4xl text-black flex justify-center">
Reset your Password 🤡
</h1>
<div>
<label
htmlFor="password"
className="block mb-2 text-2xl text-black"
>
New Password
</label>
<input
type="password"
name="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••••••••••"
className="rounded bg-purple-500 w-full px-3 py-1 font-mono text-black"
></input>
</div>

<div>
<label
htmlFor="confirmpassword"
className="block mb-2 text-2xl text-black"
>
Confirm Password
</label>
<input
type="password"
name="confirmpassword"
id="confirmpassword"
value={confirmpassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="••••••••••••••••"
className="rounded bg-purple-500 w-full px-3 py-1 font-mono text-black"
></input>
</div>

<div className="flex justify-center">
<button
type="button"
className="bg-purple-500 hover:bg-purple-300 transition-colors duration-200 text-black text-xl py-2 px-4 rounded flex justify-center"
onClick={handleResetPassword}
disabled={loading}
>
{loading ? "Processing..." : "Reset Password"}
</button>
</div>
</form>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default function VerifyEmail() {

const VerifyUserEmail = async () => {
try {
// user token from email sent from nodemailer
const res = await axios.post("api/users/verifyemail", { token });
console.log(res.data);
toast.success(res.data.message);
Expand All @@ -23,6 +24,7 @@ export default function VerifyEmail() {
}
};

//search the URL for the token
useEffect(() => {
const params = window.location.search.split("=")[1];
setToken(params || "");
Expand All @@ -36,7 +38,7 @@ export default function VerifyEmail() {
}, [token]);

return (
<div className="flex flex-col max-w-xl bg-gradient-to-b from-black-900 to-purple-600 mx-auto mt-20 p-6 rounded-lg text-center">
<div className="flex flex-col max-h-screen max-w-xl bg-gradient-to-b from-black-900 to-purple-600 mx-auto p-6 rounded-lg text-center justify-center">
<h1 className="text-2xl text-black mb-4">Email Verification</h1>

{loading ? (
Expand Down
12 changes: 12 additions & 0 deletions src/app/(navgroup)/games/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use client";
import React from "react";
import { Toaster } from "react-hot-toast";

export default function Games() {
return (
<main className="flex flex-col">
<Toaster position="top-left" reverseOrder={false} />
<div></div>
</main>
);
}
3 changes: 2 additions & 1 deletion src/app/page.tsx → src/app/(navgroup)/home/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"use client";
import React from "react";
import { Toaster } from "react-hot-toast";

export default function Home() {
return (
<main className="flex flex-col justify-center min-h-screen bg-gradient-to-t from-black to-purple-600 py-12">
<main className="flex flex-col">
<Toaster position="top-left" reverseOrder={false} />
<div></div>
</main>
Expand Down
36 changes: 36 additions & 0 deletions src/app/(navgroup)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use client";

import { useEffect, useState } from "react";
import { usePathname } from "next/navigation";
import Navbar from "./navbar";
import axios from "axios";

export default function NavbarLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const [isUserVerified, setIsUserVerified] = useState(false);
const pathname = usePathname();

useEffect(() => {
async function checkAuthStatus() {
try {
const response = await axios.get("/api/users/checkVerification");
setIsUserVerified(response.data.isVerified);
} catch (error) {
console.error("Failed to fetch auth status:", error);
setIsUserVerified(false);
}
}

checkAuthStatus();
}, [pathname]); // Re-run when route changes

return (
<div className="min-h-screen bg-gradient-to-t from-black to-purple-600">
<Navbar isVerified={isUserVerified} />
{children}
</div>
);
}
Loading