initial commit

This commit is contained in:
shafin-r
2025-07-03 01:50:10 +06:00
commit 913ec11bc7
49 changed files with 6784 additions and 0 deletions

41
.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

36
README.md Normal file
View File

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

109
app/(auth)/login/page.tsx Normal file
View File

@ -0,0 +1,109 @@
"use client";
import React, { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import Image from "next/image";
import BackgroundWrapper from "@/components/BackgroundWrapper";
import FormField from "@/components/FormField";
import { login } from "@/lib/auth";
import DestructibleAlert from "@/components/DestructibleAlert";
import { useAuth } from "@/context/AuthContext";
const page = () => {
const router = useRouter();
const { setToken } = useAuth();
const [form, setForm] = useState({
email: "",
password: "",
});
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
// For Rafeed
// Function to login a user. I've kept it in a barebones form right now, but you can just call the login function from /lib/auth.ts and pass on the form.
const loginUser = async () => {
try {
setIsLoading(true);
setError(null);
await login(form, setToken); // Call the login function
router.push("/home"); // Redirect on successful login
} catch (error) {
console.log(error);
setError(error.message); // Handle error messages
} finally {
setIsLoading(false);
}
};
return (
<BackgroundWrapper>
<div className="flex-1 min-h-screen">
<div className="min-h-screen overflow-y-auto">
<div className="min-h-full flex flex-col justify-center gap-10 mt-7 mx-6 py-8">
{/* Logo Container */}
<div
className="w-full self-center mt-7"
style={{ aspectRatio: "368/89" }}
>
<Image
src="/images/logo/logo.png"
alt="Logo"
width={368}
height={89}
className="w-full h-full object-contain"
priority
/>
</div>
{/* Form Container */}
<div className="flex flex-col justify-between gap-10">
<div className="flex flex-col w-full gap-5">
<FormField
title="Email Address"
value={form.email}
placeholder="Enter your email address..."
handleChangeText={(e) => setForm({ ...form, email: e })}
/>
<FormField
title="Password"
value={form.password}
placeholder="Enter a password"
handleChangeText={(e) => setForm({ ...form, password: e })}
/>
</div>
{error && <DestructibleAlert text={error} extraStyles="" />}
<button
onClick={() => router.push("/home")}
disabled={isLoading}
className="w-full h-14 flex justify-center items-center border border-[#113768] rounded-full bg-transparent hover:bg-[#113768] hover:text-white transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span
className="font-medium"
style={{ fontFamily: "Montserrat, sans-serif" }}
>
{isLoading ? "Logging in..." : "Login"}
</span>
</button>
</div>
{/* Register Link */}
<p
className="text-center mb-[70px]"
style={{ fontFamily: "Montserrat, sans-serif" }}
>
Don't have an account?{" "}
<Link href="/register" className="text-[#276ac0] hover:underline">
Register here.
</Link>
</p>
</div>
</div>
</div>
</BackgroundWrapper>
);
};
export default page;

View File

@ -0,0 +1,166 @@
"use client";
import { useState } from "react";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { register } from "@/lib/auth";
import { useAuth } from "@/context/AuthContext";
import BackgroundWrapper from "@/components/BackgroundWrapper";
import FormField from "@/components/FormField";
import DestructibleAlert from "@/components/DestructibleAlert";
export default function RegisterPage() {
const { setToken } = useAuth();
const router = useRouter();
const [form, setForm] = useState({
name: "",
institution: "",
sscRoll: "",
hscRoll: "",
email: "",
phone: "",
password: "",
});
const [error, setError] = useState<string | null>(null);
const handleError = (error: any) => {
if (error?.detail) {
const match = error.detail.match(/Key \((.*?)\)=\((.*?)\)/);
if (match) {
const field = match[1];
const value = match[2];
return `The ${field} already exists. Please use a different value.`;
}
}
return "An unexpected error occurred. Please try again.";
};
const validateForm = () => {
const { sscRoll, hscRoll, password } = form;
if (sscRoll === hscRoll) {
return "SSC Roll and HSC Roll must be unique.";
}
const passwordRegex =
/^(?=.*[A-Z])(?=.*[!@#$%^&*(),.?":{}|<>])[A-Za-z\d!@#$%^&*(),.?":{}|<>]{8,16}$/;
if (!passwordRegex.test(password)) {
return "Password must be 8-16 characters long, include at least one uppercase letter and one special character.";
}
return null;
};
const createUser = async () => {
const validationError = validateForm();
if (validationError) {
setError(validationError);
return;
}
try {
await register(form, setToken);
router.push("/tabs/home");
} catch (error: any) {
console.error("Error:", error.response || error.message);
if (error.response?.detail) {
const decodedError = handleError({ detail: error.response.detail });
setError(decodedError);
} else {
setError(error.message || "An unexpected error occurred.");
}
}
};
return (
<BackgroundWrapper>
<div className="min-h-screen flex flex-col items-center justify-center px-4 py-10">
<div className="w-full max-w-md space-y-6">
<div className="w-full aspect-[368/89] mx-auto">
<Image
src="/images/logo/logo.png"
alt="Logo"
width={368}
height={89}
className="w-full h-auto"
/>
</div>
<div className="space-y-10">
<div className="space-y-6">
<FormField
title="Full name"
value={form.name}
handleChangeText={(e) =>
setForm({ ...form, name: e.target.value })
}
/>
<FormField
title="Institution"
placeholder="Enter a institution"
value={form.institution}
handleChangeText={(e) =>
setForm({ ...form, institution: e.target.value })
}
/>
<FormField
title="SSC Roll No."
placeholder="Enter your SSC Roll No."
value={form.sscRoll}
handleChangeText={(e) =>
setForm({ ...form, sscRoll: e.target.value })
}
/>
<FormField
title="HSC Roll No."
placeholder="Enter your HSC Roll No."
value={form.hscRoll}
handleChangeText={(e) =>
setForm({ ...form, hscRoll: e.target.value })
}
/>
<FormField
title="Email Address"
placeholder="Enter your email address..."
value={form.email}
handleChangeText={(e) =>
setForm({ ...form, email: e.target.value })
}
/>
<FormField
title="Phone Number"
placeholder="Enter your phone number.."
value={form.phone}
handleChangeText={(e) =>
setForm({ ...form, phone: e.target.value })
}
/>
<FormField
title="Password"
placeholder="Enter a password"
value={form.password}
handleChangeText={(e) =>
setForm({ ...form, password: e.target.value })
}
/>
</div>
{error && <DestructibleAlert text={error} />}
<button
onClick={createUser}
className="w-full h-14 rounded-full border border-blue-900 text-center text-base font-medium"
>
Get started
</button>
<p className="text-center text-sm">
Already have an account?{" "}
<Link href="/login" className="text-blue-600">
Login here
</Link>
</p>
</div>
</div>
</div>
</BackgroundWrapper>
);
}

View File

@ -0,0 +1,7 @@
import React from "react";
const page = () => {
return <div>page</div>;
};
export default page;

View File

@ -0,0 +1,7 @@
import React from "react";
const page = () => {
return <div>page</div>;
};
export default page;

246
app/(tabs)/home/page.tsx Normal file
View File

@ -0,0 +1,246 @@
"use client";
import React, { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import Image from "next/image";
import Link from "next/link";
import Header from "@/components/Header";
import SlidingGallery from "@/components/SlidingGallery";
import BackgroundWrapper from "@/components/BackgroundWrapper";
import DestructibleAlert from "@/components/DestructibleAlert";
import { ChevronRight } from "lucide-react"; // Using Lucide React for icons
import styles from "@/css/Home.module.css";
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000/api";
const page = () => {
const profileImg = "/images/static/avatar.jpg";
const router = useRouter();
const [boardData, setBoardData] = useState([]);
const [boardError, setBoardError] = useState(null);
const performanceData = [
{ label: "Mock Test", progress: 20 },
{ label: "Topic Test", progress: 70 },
{ label: "Subject Test", progress: 50 },
];
const progressData = [
{ label: "Physics", progress: 25 },
{ label: "Chemistry", progress: 57 },
];
// Fetch function for leaderboard data
useEffect(() => {
let isMounted = true;
async function fetchBoardData() {
try {
const response = await fetch(`${API_URL}/leaderboard`);
if (!response.ok) {
throw new Error("Failed to fetch leaderboard data");
}
const data = await response.json();
if (isMounted) setBoardData(data);
} catch (error) {
if (isMounted) setBoardError(error.message || "An error occurred");
}
}
fetchBoardData();
return () => {
isMounted = false;
};
}, []);
const getTopThree = (boardData) => {
if (!boardData || boardData.length === 0) return [];
return boardData
.slice()
.sort((a, b) => b.points - a.points)
.slice(0, 3)
.map((player, index) => ({
...player,
rank: index + 1,
height: index === 0 ? 250 : index === 1 ? 200 : 170,
}));
};
return (
<BackgroundWrapper>
<div className={styles.container}>
<Header displayTabTitle={null} displayUser image={profileImg} />
<div className={styles.scrollContainer}>
<div className={styles.contentWrapper}>
<SlidingGallery />
<div className={styles.mainContent}>
{/* Categories Section */}
<div>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>Categories</h2>
<button
onClick={() => router.push("/categories")}
className={styles.arrowButton}
>
<ChevronRight size={24} color="#113768" />
</button>
</div>
<div className={styles.categoriesContainer}>
<div className={styles.categoryRow}>
<button
disabled
className={`${styles.categoryButton} ${styles.disabled}`}
>
<Image
src="/images/icons/topic-test.png"
alt="Topic Test"
width={70}
height={70}
/>
<span className={styles.categoryButtonText}>
Topic Test
</span>
</button>
<button
onClick={() => router.push("/unit")}
className={styles.categoryButton}
>
<Image
src="/images/icons/mock-test.png"
alt="Mock Test"
width={70}
height={70}
/>
<span className={styles.categoryButtonText}>
Mock Test
</span>
</button>
</div>
<div className={styles.categoryRow}>
<button
disabled
className={`${styles.categoryButton} ${styles.disabled}`}
>
<Image
src="/images/icons/past-paper.png"
alt="Past Papers"
width={62}
height={62}
/>
<span className={styles.categoryButtonText}>
Past Papers
</span>
</button>
<button
disabled
className={`${styles.categoryButton} ${styles.disabled}`}
>
<Image
src="/images/icons/subject-test.png"
alt="Subject Test"
width={70}
height={70}
/>
<span className={styles.categoryButtonText}>
Subject Test
</span>
</button>
</div>
</div>
</div>
{/* Leaderboard Section */}
<div className={styles.leaderboardWrapper}>
<h2 className={styles.sectionTitle}>Leaderboard</h2>
<div className={styles.leaderboardContainer}>
<div className={styles.topThreeHeader}>
<span className={styles.topThreeTitle}>Top 3</span>
<button
onClick={() => router.push("/leaderboard")}
className={styles.arrowButton}
>
<ChevronRight size={24} color="#113768" />
</button>
</div>
<div className={styles.divider}></div>
<div className={styles.topThreeList}>
{getTopThree(boardData).map((student, idx) => (
<div key={idx} className={styles.topThreeItem}>
<div className={styles.studentInfo}>
<span className={styles.rank}>{student.rank}</span>
<Image
src="/images/static/avatar.jpg"
alt="Avatar"
width={20}
height={20}
className={styles.avatar}
/>
<span className={styles.studentName}>
{student.name}
</span>
</div>
<span className={styles.points}>
{student.points}pt
</span>
</div>
))}
</div>
</div>
</div>
{/* Performance Summary Section */}
<div className={styles.section}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>Performance Summary</h2>
<button
disabled
onClick={() => router.push("/performance")}
className={styles.arrowButton}
>
<ChevronRight size={24} color="#113768" />
</button>
</div>
<div className={styles.comingSoonCard}>
<p className={styles.comingSoonText}>Coming soon.</p>
</div>
</div>
{/* Progress Tracker Section */}
<div className={styles.section}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>Progress Tracker</h2>
<button
disabled
onClick={() => router.push("/progress")}
className={styles.arrowButton}
>
<ChevronRight size={24} color="#113768" />
</button>
</div>
<div className={styles.comingSoonCard}>
<p className={styles.comingSoonText}>Coming soon.</p>
</div>
</div>
{/* Daily Quiz Section */}
<div className={styles.section}>
<h2 className={styles.sectionTitle}>Daily Quiz</h2>
<div className={styles.comingSoonCard}>
<p className={styles.comingSoonText}>Coming soon.</p>
</div>
</div>
{/* Live Exams Section */}
<div className={`${styles.section} ${styles.lastSection}`}>
<h2 className={styles.sectionTitle}>Live Exams</h2>
<div className={styles.comingSoonCard}>
<p className={styles.comingSoonText}>Coming soon.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</BackgroundWrapper>
);
};
export default page;

38
app/(tabs)/layout.tsx Normal file
View File

@ -0,0 +1,38 @@
// app/tabs/layout.tsx
"use client";
import Link from "next/link";
import { ReactNode } from "react";
import { usePathname } from "next/navigation";
import clsx from "clsx";
const tabs = [
{ name: "Home", href: "/tabs/home" },
{ name: "Profile", href: "/tabs/profile" },
{ name: "Leaderboard", href: "/tabs/leaderboard" },
];
export default function TabsLayout({ children }: { children: ReactNode }) {
const pathname = usePathname();
return (
<div className="min-h-screen flex flex-col">
<main className="flex-1">{children}</main>
<nav className="flex justify-around border-t p-4 bg-white">
{tabs.map((tab) => (
<Link
key={tab.name}
href={tab.href}
className={clsx(
"text-center text-sm font-medium",
pathname === tab.href ? "text-blue-600" : "text-gray-500"
)}
>
{tab.name}
</Link>
))}
</nav>
</div>
);
}

View File

0
app/(tabs)/live/page.tsx Normal file
View File

View File

@ -0,0 +1,7 @@
import React from "react";
const page = () => {
return <div>page</div>;
};
export default page;

View File

@ -0,0 +1,7 @@
import React from "react";
const page = () => {
return <div>page</div>;
};
export default page;

View File

@ -0,0 +1,7 @@
import React from "react";
const page = () => {
return <div>page</div>;
};
export default page;

63
app/(tabs)/unit/page.tsx Normal file
View File

@ -0,0 +1,63 @@
"use client";
import React from "react";
import { useRouter } from "next/navigation";
import Header from "@/components/Header";
import BackgroundWrapper from "@/components/BackgroundWrapper";
const units = [
{
id: 3,
name: "C Unit (Business Studies)",
rating: 9,
},
];
const Unit = () => {
const router = useRouter();
const handleUnitPress = (unit) => {
router.push(`/paper?name=${encodeURIComponent(unit.name)}`);
};
return (
<BackgroundWrapper>
<div className="flex flex-col min-h-screen">
<Header
displayExamInfo={null}
displayTabTitle={"Units"}
displaySubject={null}
displayUser={false}
title=""
image={""}
/>
<div className="flex-1">
<div className="overflow-y-auto">
<div className="border border-blue-200 gap-4 rounded-3xl p-6 mx-10 mt-10">
{units ? (
units.map((unit) => (
<button
key={unit.id}
onClick={() => handleUnitPress(unit)}
className="border-2 border-blue-300 py-4 rounded-xl px-6 gap-2 block w-full text-left hover:bg-blue-50 transition-colors duration-200"
>
<p className="text-lg font-medium">{unit.name}</p>
<p className="text-sm">Rating: {unit.rating} / 10</p>
</button>
))
) : (
<div className="flex flex-col items-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mb-4"></div>
<p className="font-bold text-2xl text-center">Loading...</p>
</div>
)}
</div>
</div>
</div>
</div>
{/* <CustomBackHandler fallbackRoute="home" useCustomHandler={false} /> */}
</BackgroundWrapper>
);
};
export default Unit;

7
app/exam/[id]/page.tsx Normal file
View File

@ -0,0 +1,7 @@
import React from "react";
const page = () => {
return <div>page</div>;
};
export default page;

View File

@ -0,0 +1,7 @@
import React from "react";
const page = () => {
return <div>page</div>;
};
export default page;

View File

@ -0,0 +1,7 @@
import React from "react";
const page = () => {
return <div>page</div>;
};
export default page;

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

5
app/globals.css Normal file
View File

@ -0,0 +1,5 @@
@import "tailwindcss";
@theme {
--font-sans: var(--font-montserrat), ui-sans-serif, system-ui, sans-serif;
}

33
app/layout.tsx Normal file
View File

@ -0,0 +1,33 @@
import type { Metadata } from "next";
import { Montserrat } from "next/font/google";
import "./globals.css";
import { AuthProvider } from "@/context/AuthContext";
import { TimerProvider } from "@/context/TimerContext";
const montserrat = Montserrat({
subsets: ["latin"],
weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
variable: "--font-montserrat",
display: "swap",
});
export const metadata: Metadata = {
title: "ExamJam",
description: "Your exam preparation platform",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className={montserrat.variable}>
<body className="font-sans">
<TimerProvider>
<AuthProvider>{children}</AuthProvider>
</TimerProvider>
</body>
</html>
);
}

90
app/page.tsx Normal file
View File

@ -0,0 +1,90 @@
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import Image from "next/image";
import { useState, useEffect } from "react";
import BackgroundWrapper from "@/components/BackgroundWrapper";
export default function Home() {
const router = useRouter();
const [windowDimensions, setWindowDimensions] = useState({
width: 0,
height: 0,
});
useEffect(() => {
function handleResize() {
setWindowDimensions({
width: window.innerWidth,
height: window.innerHeight,
});
}
// Set initial dimensions
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
const handleLogin = () => {
router.push("/login");
};
return (
<BackgroundWrapper>
<div className="mx-10 h-screen">
<div className="h-full flex flex-col justify-around pt-10">
{/* Logo Container */}
<div className="w-full" style={{ aspectRatio: "368/89" }}>
<Image
src="/images/logo/logo.png"
alt="Logo"
width={368}
height={89}
className="w-full h-full object-contain"
priority
/>
</div>
{/* Login Graphic */}
<div className="w-full h-1/2">
<Image
src="/images/static/login-graphic-1.png"
alt="Login illustration"
width={400}
height={300}
className="w-full h-full object-contain"
/>
</div>
{/* Action Buttons */}
<div className="flex flex-col gap-4">
<button
onClick={handleLogin}
className="w-full h-[60px] flex justify-center items-center border border-[#113768] rounded-full bg-transparent hover:bg-[#113768] hover:text-white transition-colors duration-200"
>
<span
className="font-medium"
style={{ fontFamily: "Montserrat, sans-serif" }}
>
Login
</span>
</button>
<p
className="text-center font-medium"
style={{ fontFamily: "Montserrat, sans-serif" }}
>
Don't have an account?{" "}
<Link href="/register" className="text-[#276ac0] hover:underline">
Register here
</Link>
</p>
</div>
</div>
</div>
</BackgroundWrapper>
);
}

BIN
bun.lockb Normal file

Binary file not shown.

View File

@ -0,0 +1,23 @@
import React from "react";
const BackgroundWrapper = ({ children }) => {
return (
<div
className="min-h-screen bg-cover bg-center bg-no-repeat relative"
style={{
backgroundImage: "url('/images/static/paper-background.png')",
}}
>
<div
className="min-h-screen"
style={{
backgroundColor: "rgba(0, 0, 0, 0)", // Optional overlay - adjust opacity as needed
}}
>
{children}
</div>
</div>
);
};
export default BackgroundWrapper;

View File

@ -0,0 +1,38 @@
import React from "react";
const DestructibleAlert = ({
text,
extraStyles = "",
}: {
text: string;
extraStyles?: string;
}) => {
return (
<div
className={`border bg-red-200 border-blue-200 rounded-3xl py-6 ${extraStyles}`}
style={{
borderWidth: 1,
backgroundColor: "#fecaca",
borderColor: "#c0dafc",
paddingTop: 24,
paddingBottom: 24,
}}
>
<p
className="text-center text-red-800"
style={{
fontSize: 17,
lineHeight: "28px",
fontFamily: "Montserrat, sans-serif",
fontWeight: "bold",
textAlign: "center",
color: "#991b1b",
}}
>
{text}
</p>
</div>
);
};
export default DestructibleAlert;

80
components/FormField.tsx Normal file
View File

@ -0,0 +1,80 @@
import React, { useState } from "react";
const FormField = ({
title,
placeholder,
value,
handleChangeText,
...props
}) => {
const [showPassword, setShowPassword] = useState(false);
const isPasswordField = title === "Password" || title === "Confirm Password";
return (
<div className="w-full">
<label
className="block mb-2"
style={{
color: "#666666",
fontFamily: "Montserrat, sans-serif",
fontWeight: "500",
fontSize: 18,
marginBottom: 8,
letterSpacing: "-0.5px",
}}
>
{title}
</label>
<div
className="h-16 px-4 bg-blue-200 rounded-3xl flex items-center justify-between"
style={{
height: 64,
paddingLeft: 16,
paddingRight: 16,
backgroundColor: "#D2DFF0",
borderRadius: 20,
}}
>
<input
type={isPasswordField && !showPassword ? "password" : "text"}
value={value}
placeholder={placeholder}
onChange={(e) => handleChangeText(e.target.value)}
className="flex-1 bg-transparent outline-none border-none text-blue-950"
style={{
color: "#0D47A1",
fontSize: 16,
fontFamily: "inherit",
backgroundColor: "transparent",
border: "none",
outline: "none",
}}
{...props}
/>
{isPasswordField && (
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="ml-2 text-gray-600 hover:text-gray-800 focus:outline-none"
style={{
fontFamily: "Montserrat, sans-serif",
fontWeight: "500",
fontSize: 16,
background: "none",
border: "none",
cursor: "pointer",
padding: 0,
}}
>
{showPassword ? "Hide" : "Show"}
</button>
)}
</div>
</div>
);
};
export default FormField;

169
components/Header.tsx Normal file
View File

@ -0,0 +1,169 @@
import React, { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import Image from "next/image";
import { ChevronLeft, Layers } from "lucide-react";
import { useTimer } from "@/context/TimerContext";
import styles from "@/css/Header.module.css";
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000/api";
// You'll need to implement getToken for Next.js - could use cookies, localStorage, etc.
const getToken = async () => {
// Replace with your token retrieval logic
if (typeof window !== "undefined") {
return localStorage.getItem("token") || sessionStorage.getItem("token");
}
return null;
};
const Header = ({
image,
displayUser,
displaySubject,
displayTabTitle,
examDuration,
}) => {
const router = useRouter();
const [totalSeconds, setTotalSeconds] = useState(
examDuration ? parseInt(examDuration) * 60 : 0
);
const { timeRemaining, stopTimer } = useTimer();
const [userData, setUserData] = useState(null);
useEffect(() => {
if (!examDuration) return;
const timer = setInterval(() => {
setTotalSeconds((prev) => {
if (prev <= 0) {
clearInterval(timer);
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(timer);
}, [examDuration]);
useEffect(() => {
async function fetchUser() {
try {
const token = await getToken();
if (!token) return;
const response = await fetch(`${API_URL}/me`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.ok) {
const fetchedUserData = await response.json();
setUserData(fetchedUserData);
}
} catch (error) {
console.error("Error fetching user data:", error);
}
}
if (displayUser) {
fetchUser();
}
}, [displayUser]);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const showExitDialog = () => {
const confirmed = window.confirm("Are you sure you want to quit the exam?");
if (confirmed) {
if (stopTimer) {
stopTimer();
}
router.push("/unit");
}
};
const handleBackClick = () => {
router.back();
};
return (
<header className={styles.header}>
{displayUser && (
<div className={styles.profile}>
{image && (
<Image
src={image}
alt="Profile"
width={40}
height={40}
className={styles.profileImg}
/>
)}
<span className={styles.text}>
Hello {userData?.name ? userData.name.split(" ")[0] : ""}
</span>
</div>
)}
{displaySubject && (
<div className={styles.profile}>
<button onClick={handleBackClick} className={styles.iconButton}>
<ChevronLeft size={24} color="white" />
</button>
<span className={styles.text}>{displaySubject}</span>
</div>
)}
{displayTabTitle && (
<div className={styles.profile}>
<span className={styles.text}>{displayTabTitle}</span>
</div>
)}
{examDuration && (
<div className={styles.examHeader}>
<button onClick={showExitDialog} className={styles.iconButton}>
<ChevronLeft size={30} color="white" />
</button>
<div className={styles.timer}>
<div className={styles.timeUnit}>
<span className={styles.timeValue}>
{String(hours).padStart(2, "0")}
</span>
<span className={styles.timeLabel}>Hrs</span>
</div>
<div className={styles.timeUnit}>
<span className={styles.timeValue}>
{String(minutes).padStart(2, "0")}
</span>
<span className={styles.timeLabel}>Mins</span>
</div>
<div className={styles.timeUnit}>
<span className={styles.timeValue}>
{String(seconds).padStart(2, "0")}
</span>
<span className={styles.timeLabel}>Secs</span>
</div>
</div>
<button
disabled
onClick={() => router.push("/exam/modal")}
className={`${styles.iconButton} ${styles.disabled}`}
>
<Layers size={30} color="white" />
</button>
</div>
)}
</header>
);
};
export default Header;

View File

@ -0,0 +1,71 @@
import React, { useState, useRef, useEffect } from "react";
import Link from "next/link";
import Image from "next/image";
import styles from "../css/SlidingGallery.module.css";
const views = [
{
id: "1",
content: (
<Link
href="https://www.facebook.com/share/g/15jdqESvWV/?mibextid=wwXIfr"
className={styles.link}
>
<div className={styles.facebook}>
<div className={styles.textView}>
<h3 className={styles.facebookOne}>Meet, Share, and Learn!</h3>
<p className={styles.facebookTwo}>Join Facebook Community</p>
</div>
<div className={styles.logoView}>
<Image
src="/images/static/facebook-logo.png"
alt="Facebook Logo"
width={120}
height={120}
/>
</div>
</div>
</Link>
),
},
];
const SlidingGallery = () => {
const [activeIdx, setActiveIdx] = useState(0);
const scrollRef = useRef(null);
const handleScroll = (event) => {
const scrollLeft = event.target.scrollLeft;
const slideWidth = event.target.clientWidth;
const index = Math.round(scrollLeft / slideWidth);
setActiveIdx(index);
};
return (
<div className={styles.gallery}>
<div
className={styles.scrollContainer}
ref={scrollRef}
onScroll={handleScroll}
>
{views.map((item) => (
<div key={item.id} className={styles.slide}>
{item.content}
</div>
))}
</div>
<div className={styles.pagination}>
{views.map((_, index) => (
<div
key={index}
className={`${styles.dot} ${
activeIdx === index ? styles.activeDot : styles.inactiveDot
}`}
/>
))}
</div>
</div>
);
};
export default SlidingGallery;

109
context/AuthContext.tsx Normal file
View File

@ -0,0 +1,109 @@
"use client";
import React, { createContext, useContext, useState, useEffect } from "react";
import { useRouter } from "next/navigation";
interface AuthContextType {
token: string | null;
setToken: (token: string | null) => void;
logout: () => void;
isLoading: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
// Cookie utility functions
const getCookie = (name: string): string | null => {
if (typeof document === "undefined") return null;
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) {
return parts.pop()?.split(";").shift() || null;
}
return null;
};
const setCookie = (
name: string,
value: string | null,
days: number = 7
): void => {
if (typeof document === "undefined") return;
if (value === null) {
// Delete cookie by setting expiration to past date
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=Strict; Secure`;
} else {
const expires = new Date();
expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
document.cookie = `${name}=${value}; expires=${expires.toUTCString()}; path=/; SameSite=Strict; Secure`;
}
};
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [token, setTokenState] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const router = useRouter();
// Custom setToken function that also updates cookies
const setToken = (newToken: string | null) => {
setTokenState(newToken);
setCookie("authToken", newToken);
};
// On app load, check if there's a token in cookies
useEffect(() => {
const initializeAuth = () => {
const storedToken = getCookie("authToken");
if (storedToken) {
setTokenState(storedToken);
// Only redirect if we're on login/register pages
if (
router.pathname === "/" ||
router.pathname === "/login" ||
router.pathname === "/register"
) {
router.replace("/home");
}
} else {
// Only redirect to login if we're on a protected page
const publicPages = ["/", "/login", "/register"];
if (!publicPages.includes(router.pathname)) {
router.replace("/");
}
}
setIsLoading(false);
};
// Small delay to ensure router is ready
const timer = setTimeout(initializeAuth, 100);
return () => clearTimeout(timer);
}, [router.pathname]);
// Function to log out
const logout = () => {
setTokenState(null);
setCookie("authToken", null); // Remove token from cookies
router.replace("/login"); // Redirect to login screen
};
return (
<AuthContext.Provider value={{ token, setToken, logout, isLoading }}>
{children}
</AuthContext.Provider>
);
};
// Hook to use the AuthContext
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
};

70
context/TimerContext.tsx Normal file
View File

@ -0,0 +1,70 @@
"use client";
import React, { createContext, useContext, useState, useEffect } from "react";
// Define the context type
interface TimerContextType {
timeRemaining: number;
resetTimer: (duration: number) => void;
stopTimer: () => void;
setInitialTime: (duration: number) => void; // New function to set the initial time
}
// Create the context with a default value of `undefined`
const TimerContext = createContext<TimerContextType | undefined>(undefined);
// Provider Component
export const TimerProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [timeRemaining, setTimeRemaining] = useState<number>(0); // Default is 0
let timer: NodeJS.Timeout;
useEffect(() => {
if (timeRemaining > 0) {
timer = setInterval(() => {
setTimeRemaining((prev) => {
if (prev <= 1) {
clearInterval(timer);
return 0;
}
return prev - 1;
});
}, 1000);
}
return () => {
clearInterval(timer); // Cleanup timer on unmount
};
}, [timeRemaining]);
const resetTimer = (duration: number) => {
clearInterval(timer);
setTimeRemaining(duration);
};
const stopTimer = () => {
clearInterval(timer);
};
const setInitialTime = (duration: number) => {
setTimeRemaining(duration);
};
return (
<TimerContext.Provider
value={{ timeRemaining, resetTimer, stopTimer, setInitialTime }}
>
{children}
</TimerContext.Provider>
);
};
// Hook to use the TimerContext
export const useTimer = (): TimerContextType => {
const context = useContext(TimerContext);
if (!context) {
throw new Error("useTimer must be used within a TimerProvider");
}
return context;
};

179
css/Header.module.css Normal file
View File

@ -0,0 +1,179 @@
.header {
background-color: #113768;
height: 130px;
width: 100%;
padding-top: 30px;
padding-left: 30px;
padding-right: 30px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 100;
}
.profile {
display: flex;
flex-direction: row;
align-items: center;
gap: 20px;
}
.profileImg {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.text {
font-size: 24px;
font-family: 'Montserrat', sans-serif;
font-weight: 700;
color: #fff;
}
.iconButton {
background: none;
border: none;
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.2s;
}
.iconButton:hover {
opacity: 0.8;
}
.iconButton.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.examHeader {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
align-items: center;
}
.timer {
width: 167px;
height: 55px;
background-color: #fff;
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
border-radius: 10px;
padding: 0 10px;
}
.timeUnit {
display: flex;
flex-direction: column;
align-items: center;
}
.timeValue {
font-family: 'Montserrat', sans-serif;
font-weight: 500;
font-size: 20px;
color: #082E5E;
}
.timeLabel {
font-family: 'Montserrat', sans-serif;
font-weight: 500;
font-size: 14px;
color: #082E5E;
}
/* Responsive Design */
@media (max-width: 768px) {
.header {
height: 100px;
padding-top: 20px;
padding-left: 20px;
padding-right: 20px;
}
.text {
font-size: 15px;
}
.profile {
gap: 15px;
}
.profileImg {
width: 35px;
height: 35px;
}
.timer {
width: 140px;
height: 45px;
}
.timeValue {
font-size: 16px;
}
.timeLabel {
font-size: 12px;
}
}
@media (max-width: 480px) {
.header {
height: 80px;
padding-top: 15px;
padding-left: 15px;
padding-right: 15px;
}
.text {
font-size: 14px;
}
.profile {
gap: 10px;
}
.profileImg {
width: 30px;
height: 30px;
}
.timer {
width: 120px;
height: 40px;
padding: 0 5px;
}
.timeValue {
font-size: 14px;
}
.timeLabel {
font-size: 10px;
}
.examHeader {
gap: 10px;
}
}
/* Safe area considerations for mobile */
@media (max-width: 480px) {
.header {
padding-top: max(15px, env(safe-area-inset-top));
}
}

267
css/Home.module.css Normal file
View File

@ -0,0 +1,267 @@
.container {
flex: 1;
min-height: 100vh;
}
.scrollContainer {
overflow-y: auto;
padding-top: 40px;
height: calc(100vh - 80px); /* Adjust based on header height */
}
.contentWrapper {
margin: 0 35px;
padding-bottom: 80px; /* Extra space at bottom for mobile */
}
.mainContent {
padding-top: 25px;
display: flex;
flex-direction: column;
gap: 35px;
}
.section {
display: flex;
flex-direction: column;
gap: 24px;
}
.lastSection {
margin-bottom: 80px;
}
.sectionHeader {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.sectionTitle {
font-size: 32px;
font-family: 'Montserrat', sans-serif;
font-weight: 700;
color: #113768;
margin: 0;
}
.arrowButton {
background: none;
border: none;
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.2s;
}
.arrowButton:hover {
opacity: 0.7;
}
.arrowButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Categories Styles */
.categoriesContainer {
display: flex;
flex-direction: column;
gap: 16px;
padding-top: 25px;
}
.categoryRow {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 16px;
}
.categoryButton {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1px solid #c5dbf8;
width: 48%;
height: 150px; /* Approximate scaled height */
border-radius: 25px;
background: white;
cursor: pointer;
transition: all 0.2s;
gap: 8px;
}
.categoryButton:hover:not(:disabled) {
background-color: #f8fbff;
transform: translateY(-2px);
}
.categoryButton.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.categoryButtonText {
font-size: 14px;
font-family: 'Montserrat', sans-serif;
font-weight: 500;
color: #113768;
}
/* Leaderboard Styles */
.leaderboardWrapper {
display: flex;
flex-direction: column;
gap: 20px;
}
.leaderboardContainer {
border: 1px solid #c5dbf8;
padding: 22px 15px;
border-radius: 20px;
display: flex;
flex-direction: column;
gap: 15px;
background: white;
}
.topThreeHeader {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.topThreeTitle {
font-family: 'Montserrat', sans-serif;
font-weight: 500;
font-size: 17px;
}
.divider {
border-top: 0.5px solid #c5dbf8;
}
.topThreeList {
display: flex;
flex-direction: column;
gap: 12px;
}
.topThreeItem {
display: flex;
flex-direction: row;
border: 1px solid #c5dbf8;
border-radius: 10px;
padding: 6px 12px;
justify-content: space-between;
align-items: center;
}
.studentInfo {
display: flex;
flex-direction: row;
gap: 12px;
align-items: center;
}
.rank {
font-family: 'Montserrat', sans-serif;
font-weight: 500;
font-size: 20px;
}
.avatar {
border-radius: 50%;
}
.studentName {
font-family: 'Montserrat', sans-serif;
font-weight: 500;
font-size: 14px;
}
.points {
font-family: 'Montserrat', sans-serif;
font-weight: 500;
color: rgba(0, 0, 0, 0.4);
}
/* Coming Soon Card */
.comingSoonCard {
border: 2px solid #c5dbf8;
width: 100%;
padding: 24px;
border-radius: 20px;
background: white;
display: flex;
align-items: center;
justify-content: center;
}
.comingSoonText {
font-family: 'Montserrat', sans-serif;
font-weight: 500;
font-size: 20px;
text-align: center;
margin: 0;
}
/* Responsive Design */
@media (max-width: 768px) {
.contentWrapper {
margin: 0 20px;
}
.sectionTitle {
font-size: 20px;
}
.categoryButton {
height: 120px;
}
.categoryButtonText {
font-size: 12px;
}
.categoryRow {
gap: 12px;
}
.comingSoonText {
font-size: 18px;
}
}
@media (max-width: 480px) {
.contentWrapper {
margin: 0 15px;
}
.sectionTitle {
font-size: 18px;
}
.categoryButton {
height: 100px;
}
.mainContent {
gap: 25px;
}
.categoryRow {
flex-direction: column;
}
.categoryButton {
width: 100%;
}
}

View File

@ -0,0 +1,119 @@
.gallery {
height: 200px;
width: 100%;
border: 1px solid #113768;
border-radius: 25px;
position: relative;
overflow: hidden;
}
.scrollContainer {
display: flex;
height: 100%;
overflow-x: auto;
scroll-behavior: smooth;
scroll-snap-type: x mandatory;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* Internet Explorer 10+ */
}
.scrollContainer::-webkit-scrollbar {
display: none; /* WebKit */
}
.slide {
min-width: calc(100% - 72px);
display: flex;
justify-content: center;
align-items: center;
scroll-snap-align: start;
padding: 0 36px;
}
.link {
text-decoration: none;
color: inherit;
width: 100%;
}
.facebook {
flex: 1;
display: flex;
justify-content: space-between;
flex-direction: row;
height: 100%;
background-color: #fff;
border-radius: 25px;
padding: 20px;
box-sizing: border-box;
}
.facebookOne {
font-family: 'Montserrat', sans-serif;
font-weight: 900;
color: #113768;
font-size: 20px;
margin: 0;
}
.facebookTwo {
font-family: 'Montserrat', sans-serif;
font-weight: 600;
color: #113768;
font-size: 13px;
margin: 0;
}
.pagination {
display: flex;
flex-direction: row;
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
}
.dot {
width: 10px;
height: 10px;
border-radius: 5px;
margin: 0 5px;
}
.activeDot {
background-color: #113768;
}
.inactiveDot {
background-color: #ccc;
}
.textView {
width: 60%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.logoView {
width: 40%;
display: flex;
justify-content: flex-end;
align-items: flex-end;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.slide {
min-width: calc(100% - 40px);
padding: 0 20px;
}
.facebookOne {
font-size: 18px;
}
.facebookTwo {
font-size: 12px;
}
}

16
eslint.config.mjs Normal file
View File

@ -0,0 +1,16 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;

90
lib/auth.ts Normal file
View File

@ -0,0 +1,90 @@
export const API_URL = "https://examjam-api.pptx704.com";
// Cookie utility functions
const setCookie = (name, value, days = 7) => {
if (typeof document === "undefined") return;
if (value === null) {
// Delete cookie by setting expiration to past date
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=Strict; Secure`;
} else {
const expires = new Date();
expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
document.cookie = `${name}=${value}; expires=${expires.toUTCString()}; path=/; SameSite=Strict; Secure`;
}
};
export const login = async (form, setToken) => {
const response = await fetch(`${API_URL}/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(form),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || "Login failed");
}
// Save the token to cookies instead of secure storage
setCookie("authToken", data.token);
setToken(data.token); // Update the token in context
};
const handleError = (error) => {
// Check if error has a "detail" property
if (error?.detail) {
// Match the field causing the issue
const match = error.detail.match(/Key \((.*?)\)=\((.*?)\)/);
if (match) {
const field = match[1]; // The field name, e.g., "phone"
const value = match[2]; // The duplicate value, e.g., "0987654321"
return `The ${field} already exists. Please use a different value.`;
}
}
return "An unexpected error occurred. Please try again.";
};
export const register = async (form, setToken) => {
const response = await fetch(`${API_URL}/auth/register`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(form),
});
const data = await response.json(); // Parse the response JSON
if (!response.ok) {
// Instead of throwing a string, include full error data for debugging
const error = new Error(data?.detail || "Registration failed");
error.response = data; // Attach the full response for later use
throw error;
}
// Save the token to cookies instead of secure storage
setCookie("authToken", data.token);
setToken(data.token); // Update the token in context
};
// Additional utility function to get token from cookies (if needed elsewhere)
export const getTokenFromCookie = () => {
if (typeof document === "undefined") return null;
const value = `; ${document.cookie}`;
const parts = value.split(`; authToken=`);
if (parts.length === 2) {
return parts.pop()?.split(";").shift() || null;
}
return null;
};
// Utility function to clear auth token (for logout)
export const clearAuthToken = () => {
setCookie("authToken", null);
};

7
next.config.ts Normal file
View File

@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

4599
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
package.json Normal file
View File

@ -0,0 +1,32 @@
{
"name": "examjam-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"capacitor-secure-storage-plugin": "^0.11.0",
"clsx": "^2.1.1",
"lucide-react": "^0.523.0",
"next": "15.3.2",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"autoprefixer": "^10.4.21",
"eslint": "^9",
"eslint-config-next": "15.3.2",
"postcss": "^8.5.6",
"tailwindcss": "^4",
"typescript": "^5"
}
}

5
postcss.config.mjs Normal file
View File

@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
public/images/logo/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

27
tsconfig.json Normal file
View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}