generated from muhtadeetaron/nextjs-template
initial commit
This commit is contained in:
109
app/(auth)/login/page.tsx
Normal file
109
app/(auth)/login/page.tsx
Normal 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;
|
||||
166
app/(auth)/register/page.tsx
Normal file
166
app/(auth)/register/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
7
app/(tabs)/bookmark/page.tsx
Normal file
7
app/(tabs)/bookmark/page.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return <div>page</div>;
|
||||
};
|
||||
|
||||
export default page;
|
||||
7
app/(tabs)/categories/page.tsx
Normal file
7
app/(tabs)/categories/page.tsx
Normal 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
246
app/(tabs)/home/page.tsx
Normal 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
38
app/(tabs)/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
0
app/(tabs)/leaderboard/page.tsx
Normal file
0
app/(tabs)/leaderboard/page.tsx
Normal file
0
app/(tabs)/live/page.tsx
Normal file
0
app/(tabs)/live/page.tsx
Normal file
7
app/(tabs)/performance/page.tsx
Normal file
7
app/(tabs)/performance/page.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return <div>page</div>;
|
||||
};
|
||||
|
||||
export default page;
|
||||
7
app/(tabs)/profile/page.tsx
Normal file
7
app/(tabs)/profile/page.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return <div>page</div>;
|
||||
};
|
||||
|
||||
export default page;
|
||||
7
app/(tabs)/progress/page.tsx
Normal file
7
app/(tabs)/progress/page.tsx
Normal 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
63
app/(tabs)/unit/page.tsx
Normal 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
7
app/exam/[id]/page.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return <div>page</div>;
|
||||
};
|
||||
|
||||
export default page;
|
||||
7
app/exam/pretest/page.tsx
Normal file
7
app/exam/pretest/page.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return <div>page</div>;
|
||||
};
|
||||
|
||||
export default page;
|
||||
7
app/exam/results/page.tsx
Normal file
7
app/exam/results/page.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return <div>page</div>;
|
||||
};
|
||||
|
||||
export default page;
|
||||
BIN
app/favicon.ico
Normal file
BIN
app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
5
app/globals.css
Normal file
5
app/globals.css
Normal 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
33
app/layout.tsx
Normal 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
90
app/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user