generated from muhtadeetaron/nextjs-template
chore: refactor code for type safety
This commit is contained in:
@ -1,24 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, ReactNode } 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";
|
||||
import facebookStyles from "@/css/SlidingGallery.module.css";
|
||||
import { API_URL } from "@/lib/auth";
|
||||
import { Avatar, AvatarFallback } from "@radix-ui/react-avatar";
|
||||
import { Avatar } from "@/components/ui/avatar";
|
||||
import { getLinkedViews } from "@/lib/gallery-views";
|
||||
import { getTopThree } from "@/lib/leaderboard";
|
||||
|
||||
interface LinkedView {
|
||||
id: string;
|
||||
content: ReactNode;
|
||||
}
|
||||
|
||||
const page = () => {
|
||||
const profileImg = "/images/static/avatar.jpg";
|
||||
const router = useRouter();
|
||||
const [boardData, setBoardData] = useState([]);
|
||||
const [boardError, setBoardError] = useState(null);
|
||||
const [linkedViews, setLinkedViews] = useState<LinkedView[]>();
|
||||
|
||||
const performanceData = [
|
||||
{ label: "Mock Test", progress: 20 },
|
||||
@ -31,38 +36,6 @@ const page = () => {
|
||||
{ label: "Chemistry", progress: 57 },
|
||||
];
|
||||
|
||||
const facebookViews = [
|
||||
{
|
||||
id: "1",
|
||||
content: (
|
||||
<Link
|
||||
href="https://www.facebook.com/share/g/15jdqESvWV/?mibextid=wwXIfr"
|
||||
className="w-full h-full block text-inherit box-border"
|
||||
>
|
||||
<div className="w-full h-full p-6 flex text-black bg-blue-50 rounded-4xl border-[0.5px] border-[#113768]/30">
|
||||
<div className="">
|
||||
<h3 className="text-2xl text-[#113768] font-black">
|
||||
Meet, Share, and Learn!
|
||||
</h3>
|
||||
<p className="font-bold text-sm text-[#113768] ">
|
||||
Join Facebook Community
|
||||
</p>
|
||||
</div>
|
||||
<div className={facebookStyles.logoView}>
|
||||
<Image
|
||||
src="/images/static/facebook-logo.png"
|
||||
alt="Facebook Logo"
|
||||
width={150}
|
||||
height={150}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// Fetch function for leaderboard data
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
async function fetchBoardData() {
|
||||
@ -77,32 +50,21 @@ const page = () => {
|
||||
if (isMounted) setBoardError(error.message || "An error occurred");
|
||||
}
|
||||
}
|
||||
fetchBoardData();
|
||||
const fetchedLinkedViews = getLinkedViews();
|
||||
setLinkedViews(fetchedLinkedViews);
|
||||
|
||||
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} />
|
||||
<Header displayUser />
|
||||
<div className={styles.scrollContainer}>
|
||||
<div className={styles.contentWrapper}>
|
||||
<SlidingGallery views={facebookViews} height="23vh" />
|
||||
<SlidingGallery views={linkedViews} height="23vh" />
|
||||
<div className={styles.mainContent}>
|
||||
{/* Categories Section */}
|
||||
<div>
|
||||
|
||||
@ -5,13 +5,7 @@ import Link from "next/link";
|
||||
import { ReactNode } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
House,
|
||||
LayoutGrid,
|
||||
Bookmark,
|
||||
CircleUser,
|
||||
Settings,
|
||||
} from "lucide-react";
|
||||
import { House, LayoutGrid, Bookmark, Settings } from "lucide-react";
|
||||
|
||||
const tabs = [
|
||||
{ name: "Home", href: "/home", component: <House size={30} /> },
|
||||
|
||||
@ -4,15 +4,39 @@ import BackgroundWrapper from "@/components/BackgroundWrapper";
|
||||
import Header from "@/components/Header";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { API_URL, getToken } from "@/lib/auth";
|
||||
import { BoardData, getLeaderboard, getUserData } from "@/lib/leaderboard";
|
||||
import { UserData } from "@/types/auth";
|
||||
import Image from "next/image";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
const page = () => {
|
||||
const LeaderboardPage = () => {
|
||||
const [boardError, setBoardError] = useState<string | null>(null);
|
||||
const [boardData, setBoardData] = useState([]);
|
||||
const [userData, setUserData] = useState(null);
|
||||
const [boardData, setBoardData] = useState<BoardData[]>([]);
|
||||
const [userData, setUserData] = useState<UserData>();
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchUser() {
|
||||
try {
|
||||
const token = await getToken();
|
||||
if (!token) throw new Error("User is not authenticated");
|
||||
|
||||
const response = await fetch(`${API_URL}/me`, {
|
||||
method: "get",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Failed to fetch user data");
|
||||
|
||||
const fetchedUserData = await response.json();
|
||||
setLoading(false);
|
||||
setUserData(fetchedUserData);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setUserData(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchBoardData() {
|
||||
try {
|
||||
const boardResponse = await fetch(`${API_URL}/leaderboard`, {
|
||||
@ -37,33 +61,11 @@ const page = () => {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchUser() {
|
||||
try {
|
||||
const token = await getToken();
|
||||
if (!token) throw new Error("User is not authenticated");
|
||||
|
||||
const response = await fetch(`${API_URL}/me`, {
|
||||
method: "get",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Failed to fetch user data");
|
||||
|
||||
const fetchedUserData = await response.json();
|
||||
setLoading(false);
|
||||
setUserData(fetchedUserData);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setUserData(null);
|
||||
}
|
||||
}
|
||||
|
||||
fetchUser();
|
||||
fetchBoardData();
|
||||
}, []);
|
||||
|
||||
const getTopThree = (boardData) => {
|
||||
const getTopThree = (boardData: BoardData[]) => {
|
||||
if (!boardData || !Array.isArray(boardData)) return [];
|
||||
const sortedData = boardData
|
||||
.filter((player) => player?.points !== undefined) // Ensure `points` exists
|
||||
@ -78,24 +80,10 @@ const page = () => {
|
||||
return [topThree[1], topThree[0], topThree[2]].filter(Boolean); // Handle missing players
|
||||
};
|
||||
|
||||
const getLeaderboard = (boardData) => {
|
||||
return boardData.slice().sort((a, b) => b.points - a.points);
|
||||
};
|
||||
|
||||
const getUserData = (boardData, name) => {
|
||||
if (!boardData || !Array.isArray(boardData)) return [];
|
||||
const sortedData = boardData
|
||||
.filter((player) => player?.name && player?.points !== undefined)
|
||||
.sort((a, b) => b.points - a.points);
|
||||
|
||||
const result = sortedData.find((player) => player.name === name);
|
||||
return result ? [{ ...result, rank: sortedData.indexOf(result) + 1 }] : [];
|
||||
};
|
||||
|
||||
return (
|
||||
<BackgroundWrapper>
|
||||
<section>
|
||||
<Header displaySubject={"Leaderboard"} displayTabTitle={null} />
|
||||
<Header displayTabTitle={"Leaderboard"} />
|
||||
<section className="flex flex-col mx-10 pt-10 space-y-4">
|
||||
<section className="flex justify-evenly items-end">
|
||||
{getTopThree(boardData).map((student, idx) =>
|
||||
@ -124,7 +112,7 @@ const page = () => {
|
||||
<div className="w-full border-[0.5px] border-[#c5dbf8] bg-[#c5dbf8]"></div>
|
||||
<section className="border-[1px] border-[#c0dafc] w-full rounded-3xl p-6 space-y-4 mb-20">
|
||||
<section>
|
||||
{getUserData(boardData, userData?.name).map((user, idx) => (
|
||||
{getUserData(boardData, userData!.name).map((user, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex bg-[#113768] rounded-[8] py-2 px-4 justify-between items-center"
|
||||
@ -179,4 +167,4 @@ const page = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default page;
|
||||
export default LeaderboardPage;
|
||||
|
||||
@ -6,15 +6,7 @@ import { getToken, API_URL } from "@/lib/auth";
|
||||
import { ChevronLeft, Edit2, Lock, Save } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
interface UserData {
|
||||
name: string;
|
||||
institution: string;
|
||||
sscRoll: string;
|
||||
hscRoll: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
}
|
||||
import { UserData } from "@/types/auth";
|
||||
|
||||
const ProfilePage = () => {
|
||||
const router = useRouter();
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Montserrat } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { AuthProvider } from "@/context/AuthContext";
|
||||
import { TimerProvider } from "@/context/TimerContext";
|
||||
import { ExamProvider } from "@/context/ExamContext";
|
||||
|
||||
import { Providers } from "./providers";
|
||||
|
||||
const montserrat = Montserrat({
|
||||
|
||||
20
app/page.tsx
20
app/page.tsx
@ -3,30 +3,10 @@
|
||||
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);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BackgroundWrapper>
|
||||
|
||||
@ -1,33 +1,27 @@
|
||||
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";
|
||||
import { useExam } from "@/context/ExamContext";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { useModal } from "@/context/ModalContext";
|
||||
import { API_URL, getToken } from "@/lib/auth";
|
||||
import { UserData } from "@/types/auth";
|
||||
|
||||
const API_URL = "https://examjam-api.pptx704.com";
|
||||
|
||||
// You'll need to implement getToken for Next.js - could use cookies, localStorage, etc.
|
||||
const getToken = async () => {
|
||||
if (typeof window === "undefined") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract authToken from cookies
|
||||
const match = document.cookie.match(/(?:^|;\s*)authToken=([^;]*)/);
|
||||
return match ? decodeURIComponent(match[1]) : null;
|
||||
};
|
||||
interface HeaderProps {
|
||||
displayUser?: boolean;
|
||||
displaySubject?: string;
|
||||
displayTabTitle: string;
|
||||
examDuration?: string;
|
||||
}
|
||||
|
||||
const Header = ({
|
||||
image,
|
||||
displayUser,
|
||||
displaySubject,
|
||||
displayTabTitle,
|
||||
examDuration,
|
||||
}) => {
|
||||
}: HeaderProps) => {
|
||||
const router = useRouter();
|
||||
const { open } = useModal();
|
||||
const { clearExam } = useExam();
|
||||
@ -35,7 +29,7 @@ const Header = ({
|
||||
examDuration ? parseInt(examDuration) * 60 : 0
|
||||
);
|
||||
const { timeRemaining, stopTimer } = useTimer();
|
||||
const [userData, setUserData] = useState(null);
|
||||
const [userData, setUserData] = useState<UserData>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!examDuration) return;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
// lib/resultViews.tsx
|
||||
// lib/gallery-views.tsx
|
||||
import { Link } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
|
||||
interface ExamResults {
|
||||
@ -97,3 +98,34 @@ export const getResultViews = (examResults: ExamResults | null) => [
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export const getLinkedViews = () => [
|
||||
{
|
||||
id: "1",
|
||||
content: (
|
||||
<Link
|
||||
href="https://www.facebook.com/share/g/15jdqESvWV/?mibextid=wwXIfr"
|
||||
className="w-full h-full block text-inherit box-border"
|
||||
>
|
||||
<div className="w-full h-full p-6 flex text-black bg-blue-50 rounded-4xl border-[0.5px] border-[#113768]/30">
|
||||
<div className="">
|
||||
<h3 className="text-2xl text-[#113768] font-black">
|
||||
Meet, Share, and Learn!
|
||||
</h3>
|
||||
<p className="font-bold text-sm text-[#113768] ">
|
||||
Join Facebook Community
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-center items-center shrink-0">
|
||||
<Image
|
||||
src="/images/static/facebook-logo.png"
|
||||
alt="Facebook Logo"
|
||||
width={150}
|
||||
height={150}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
];
|
||||
32
lib/leaderboard.ts
Normal file
32
lib/leaderboard.ts
Normal file
@ -0,0 +1,32 @@
|
||||
export interface BoardData {
|
||||
id: string;
|
||||
name: string;
|
||||
points: number;
|
||||
}
|
||||
|
||||
export const getTopThree = (boardData: 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,
|
||||
}));
|
||||
};
|
||||
|
||||
export const getLeaderboard = (boardData: BoardData[]) => {
|
||||
return boardData.slice().sort((a, b) => b.points - a.points);
|
||||
};
|
||||
|
||||
export const getUserData = (boardData: BoardData[], name: string) => {
|
||||
if (!boardData || !Array.isArray(boardData)) return [];
|
||||
const sortedData = boardData
|
||||
.filter((player) => player?.name && player?.points !== undefined)
|
||||
.sort((a, b) => b.points - a.points);
|
||||
|
||||
const result = sortedData.find((player) => player.name === name);
|
||||
return result ? [{ ...result, rank: sortedData.indexOf(result) + 1 }] : [];
|
||||
};
|
||||
8
types/auth.d.ts
vendored
Normal file
8
types/auth.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
export interface UserData {
|
||||
name: string;
|
||||
institution: string;
|
||||
sscRoll: string;
|
||||
hscRoll: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
}
|
||||
Reference in New Issue
Block a user