411 lines
20 KiB
TypeScript
411 lines
20 KiB
TypeScript
"use client";
|
||
import React, { useEffect, useState } from 'react';
|
||
import { Box, Button, Divider, Group, Image, Modal, Popover, Stack, Switch, Text, Title } from '@mantine/core';
|
||
import { IconBook, IconCurrencyRupee, IconHome, IconLogout, IconMoon, IconPhoneFilled, IconSettings, IconSun, IconUserCircle } from '@tabler/icons-react';
|
||
import Link from 'next/link';
|
||
import { useRouter, usePathname } from "next/navigation";
|
||
import { Providers } from '../providers';
|
||
import logo from '@/app/image/logo1.jpg';
|
||
import NextImage from 'next/image';
|
||
import { notifications } from '@mantine/notifications';
|
||
import { useDisclosure, useMediaQuery } from '@mantine/hooks';
|
||
import { fetchAndStoreUserName } from '../_util/userdetails';
|
||
import styles from './page.module.css';
|
||
|
||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||
const router = useRouter();
|
||
const pathname = usePathname();
|
||
const [authorized, SetAuthorized] = useState<boolean | null>(null);
|
||
const [userLastLoginDetails, setUserLastLoginDetails] = useState(null);
|
||
const [custname, setCustname] = useState<string | null>(null);
|
||
const isMobile = useMediaQuery("(max-width: 768px)");
|
||
const [sessionModal, setSessionModal] = useState(false);
|
||
const [countdown, setCountdown] = useState(30); // 30 sec countdown before auto logout
|
||
const [darkMode, setDarkMode] = useState(false);
|
||
const firstName = custname ? custname.split(" ")[0] : "";
|
||
const [userOpened, setUserOpened] = useState(false); // Manage dropdown visibility
|
||
|
||
const [opened, { open, close }] = useDisclosure(false);
|
||
|
||
function doLogout() {
|
||
localStorage.removeItem("access_token");
|
||
sessionStorage.removeItem("access_token");
|
||
localStorage.removeItem("remitter_name");
|
||
localStorage.removeItem("pswExpiryDate");
|
||
localStorage.clear();
|
||
sessionStorage.clear();
|
||
router.push("/login");
|
||
}
|
||
|
||
async function handleLogout(e: React.FormEvent) {
|
||
e.preventDefault();
|
||
doLogout()
|
||
router.push("/login");
|
||
}
|
||
// Toggle Dark/Light Mode
|
||
const toggleDarkMode = () => {
|
||
setDarkMode((prevMode) => !prevMode);
|
||
};
|
||
|
||
// When reload and click on back then logout
|
||
// useEffect(() => {
|
||
// // Push fake history state to trap navigation
|
||
// window.history.pushState(null, "", window.location.href);
|
||
// const handlePopState = () => {
|
||
// doLogout(); // logout when back/forward pressed
|
||
// };
|
||
// const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
||
// // logout on tab close / refresh
|
||
// localStorage.removeItem("access_token");
|
||
// sessionStorage.removeItem("access_token");
|
||
// localStorage.clear();
|
||
// sessionStorage.clear();
|
||
// };
|
||
// window.addEventListener("popstate", handlePopState);
|
||
// window.addEventListener("beforeunload", handleBeforeUnload);
|
||
// return () => {
|
||
// window.removeEventListener("popstate", handlePopState);
|
||
// window.addEventListener("beforeunload", handleBeforeUnload);
|
||
// };
|
||
// }, []);
|
||
|
||
useEffect(() => {
|
||
const token = localStorage.getItem("access_token");
|
||
if (!token) {
|
||
SetAuthorized(false);
|
||
router.push("/login");
|
||
}
|
||
else {
|
||
SetAuthorized(true);
|
||
fetchAndStoreUserName(token).then(() => {
|
||
const name = localStorage.getItem("remitter_name");
|
||
if (name) setCustname(name);
|
||
});
|
||
}
|
||
}, []);
|
||
|
||
async function handleFetchUserDetails(e: React.FormEvent) {
|
||
e.preventDefault();
|
||
const token = localStorage.getItem("access_token");
|
||
const response = await fetch('/api/auth/user_details', {
|
||
method: 'GET',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
"X-Login-Type": "IB",
|
||
'Authorization': `Bearer ${token}`
|
||
},
|
||
});
|
||
const data = await response.json();
|
||
if (response.ok) {
|
||
return data;
|
||
}
|
||
else if (response.status === 401 || data.message === 'invalid or expired token') {
|
||
// console.log(data);
|
||
localStorage.removeItem("access_token");
|
||
localStorage.clear();
|
||
sessionStorage.clear();
|
||
router.push('/login');
|
||
}
|
||
else {
|
||
notifications.show({
|
||
withBorder: true,
|
||
color: "red",
|
||
title: "Please try again later",
|
||
message: "Unable to fetch timestamp, please try again later",
|
||
autoClose: 5000,
|
||
});
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
const fetchLoginTime = async () => {
|
||
const result = await handleFetchUserDetails({ preventDefault: () => { } } as React.FormEvent);
|
||
if (result) {
|
||
setUserLastLoginDetails(result.last_login);
|
||
}
|
||
};
|
||
fetchLoginTime();
|
||
}, []);
|
||
|
||
// LOGOUT AFTER 5 MINUTES OF INACTIVITY OR TAB SWITCH
|
||
useEffect(() => {
|
||
const INACTIVITY_LIMIT = 5 * 60 * 1000; // 5 minutes
|
||
let inactiveSince: number | null = null;
|
||
let countdownTimer: NodeJS.Timeout;
|
||
|
||
const startCountdown = () => {
|
||
setSessionModal(true);
|
||
setCountdown(30); // start from 30 seconds
|
||
|
||
countdownTimer = setInterval(() => {
|
||
setCountdown((prev) => {
|
||
if (prev <= 1) {
|
||
clearInterval(countdownTimer);
|
||
doLogout(); // auto logout after countdown
|
||
return 0;
|
||
}
|
||
return prev - 1;
|
||
});
|
||
}, 1000);
|
||
};
|
||
|
||
const handleVisibilityChange = () => {
|
||
if (document.hidden) {
|
||
// User switched tab → mark inactive time
|
||
inactiveSince = Date.now();
|
||
} else {
|
||
// User returned to tab
|
||
if (inactiveSince && Date.now() - inactiveSince >= INACTIVITY_LIMIT) {
|
||
// Inactive for ≥ 5 min → show modal
|
||
startCountdown();
|
||
}
|
||
inactiveSince = null; // reset inactiveSince
|
||
}
|
||
};
|
||
|
||
const handleUserActivity = () => {
|
||
// Reset inactivity timestamp if user interacts
|
||
inactiveSince = null;
|
||
};
|
||
|
||
const activityEvents = ["mousemove", "keydown", "click", "scroll", "touchstart"];
|
||
activityEvents.forEach((event) => window.addEventListener(event, handleUserActivity));
|
||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||
|
||
return () => {
|
||
activityEvents.forEach((event) => window.removeEventListener(event, handleUserActivity));
|
||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||
clearInterval(countdownTimer);
|
||
};
|
||
}, []);
|
||
|
||
const navItems = [
|
||
{ href: "/home", label: "Home", icon: IconHome },
|
||
{ href: "/accounts", label: "Accounts", icon: IconBook },
|
||
{ href: "/funds_transfer", label: "Send Money", icon: IconCurrencyRupee },
|
||
{ href: "/settings", label: "Settings", icon: IconSettings },
|
||
];
|
||
|
||
if (authorized) {
|
||
return (
|
||
<html lang="en">
|
||
<body>
|
||
<Providers>
|
||
<Box style={{ backgroundColor: "#e6ffff", minHeight: "100vh", display: "flex", flexDirection: "column", padding: 0, margin: 0 }}>
|
||
|
||
{/* HEADER */}
|
||
<Box
|
||
component="header"
|
||
className={styles.header}
|
||
style={{
|
||
width: "100%",
|
||
padding: "0.8rem 2rem",
|
||
background: darkMode
|
||
? "linear-gradient(15deg, rgba(229, 101, 22, 1) 55%, rgba(28, 28, 30, 1) 100%)" // Dark Mode Gradient
|
||
: "linear-gradient(15deg, rgba(10, 114, 40, 1) 55%, rgba(101, 101, 184, 1) 100%)", // Light Mode Gradient
|
||
alignItems: "center",
|
||
justifyContent: "space-between",
|
||
color: "white",
|
||
boxShadow: "0 2px 6px rgba(0,0,0,0.15)",
|
||
position: "sticky",
|
||
top: 0,
|
||
zIndex: 100,
|
||
}}
|
||
>
|
||
<Group gap="md">
|
||
<Image
|
||
src={logo}
|
||
component={NextImage}
|
||
fit="contain"
|
||
alt="ebanking"
|
||
style={{ width: "60px", height: "auto" }}
|
||
/>
|
||
<div>
|
||
<Title order={3} style={{ fontFamily: "Roboto", color: "white", marginBottom: 2 }}>
|
||
THE KANGRA CENTRAL CO-OPERATIVE BANK LTD.
|
||
</Title>
|
||
<Text size="xs" c="white" style={{ opacity: 0.85 }}>
|
||
Head Office: Dharmshala, District Kangra (H.P), Pin: 176215
|
||
</Text>
|
||
</div>
|
||
</Group>
|
||
|
||
{/* Dark/Light Mode Toggle using Moon and Sun Icons */}
|
||
<Group>
|
||
{/* <Button
|
||
onClick={toggleDarkMode}
|
||
variant="subtle"
|
||
color={darkMode ? "yellow" : "blue"}
|
||
style={{ marginRight: 12 }}
|
||
>
|
||
{darkMode ? <IconSun size={20} /> : <IconMoon size={20} />}
|
||
</Button> */}
|
||
<Popover opened={userOpened} onChange={setUserOpened} position="bottom-end" withArrow shadow="md">
|
||
<Popover.Target>
|
||
<Button
|
||
leftSection={<IconUserCircle size={22} />}
|
||
variant="subtle"
|
||
onClick={() => setUserOpened((prev) => !prev)}
|
||
color='white'
|
||
style={{ fontWeight: 500 }}
|
||
>
|
||
Welcome, {firstName}
|
||
</Button>
|
||
</Popover.Target>
|
||
|
||
<Popover.Dropdown style={{ minWidth: 230, padding: 15 }}>
|
||
<Stack gap="xs">
|
||
<Box>
|
||
<Text size="sm" fw={700}>{custname}</Text>
|
||
<Text size="xs" c="dimmed">Full Name</Text>
|
||
</Box>
|
||
<Box>
|
||
<Text size="sm">
|
||
{userLastLoginDetails
|
||
? new Date(userLastLoginDetails).toLocaleString()
|
||
: "N/A"}
|
||
</Text>
|
||
<Text size="xs" c="dimmed">Last Login</Text>
|
||
</Box>
|
||
|
||
<Divider />
|
||
|
||
<Button
|
||
leftSection={<IconLogout size={18} />}
|
||
onClick={doLogout}
|
||
>
|
||
Logout
|
||
</Button>
|
||
</Stack>
|
||
</Popover.Dropdown>
|
||
</Popover>
|
||
</Group>
|
||
|
||
</Box>
|
||
|
||
{/* WELCOME + NAV */}
|
||
<Box
|
||
style={{
|
||
flexShrink: 0,
|
||
padding: isMobile ? "0.5rem" : "0.5rem 1rem",
|
||
display: "flex",
|
||
flexDirection: isMobile ? "column" : "row",
|
||
justifyContent: "space-between",
|
||
alignItems: isMobile ? "flex-start" : "center",
|
||
gap: isMobile ? "0.5rem" : 0,
|
||
}}
|
||
>
|
||
<Stack gap={isMobile ? 2 : 0} align={isMobile ? "flex-start" : "flex-start"}>
|
||
<Title order={isMobile ? 5 : 4} style={{ fontFamily: "inter", fontSize: isMobile ? "18px" : "22px" }}>
|
||
Welcome, {custname ?? null}
|
||
</Title>
|
||
<Text size="xs" c="gray" style={{ fontFamily: "inter", fontSize: isMobile ? "11px" : "13px" }}>
|
||
Last logged in at {userLastLoginDetails ? new Date(userLastLoginDetails).toLocaleString() : "N/A"}
|
||
</Text>
|
||
</Stack>
|
||
|
||
<Group mt={isMobile ? "sm" : "md"} gap="sm" style={{ flexWrap: isMobile ? "wrap" : "nowrap" }}>
|
||
{navItems.map((item) => {
|
||
const isActive = pathname.startsWith(item.href);
|
||
const Icon = item.icon;
|
||
return (
|
||
<Link key={item.href} href={item.href}>
|
||
<Button
|
||
leftSection={<Icon size={isMobile ? 16 : 20} />}
|
||
variant={isActive ? "dark" : "subtle"}
|
||
color={isActive ? "blue" : undefined}
|
||
size={isMobile ? "xs" : "sm"}
|
||
>
|
||
{item.label}
|
||
</Button>
|
||
</Link>
|
||
);
|
||
})}
|
||
|
||
{/* <Popover opened={opened} onChange={close} position="bottom-end" withArrow shadow="md">
|
||
<Popover.Target>
|
||
<Button leftSection={<IconLogout size={isMobile ? 16 : 20} />} variant="subtle" size={isMobile ? "xs" : "sm"} onClick={open}>
|
||
Logout
|
||
</Button>
|
||
</Popover.Target>
|
||
<Popover.Dropdown>
|
||
<Text size="sm" mb="sm">
|
||
Are you sure you want to logout?
|
||
</Text>
|
||
<Group justify="flex-end" gap="sm">
|
||
<Button variant="default" onClick={close}>
|
||
Cancel
|
||
</Button>
|
||
<Button onClick={handleLogout}>Logout</Button>
|
||
</Group>
|
||
</Popover.Dropdown>
|
||
</Popover> */}
|
||
</Group>
|
||
</Box>
|
||
|
||
<Divider size="xs" color="#99c2ff" />
|
||
|
||
{/* CHILDREN */}
|
||
<Box
|
||
style={{
|
||
flex: 1,
|
||
overflowY: "auto",
|
||
borderTop: "1px solid #ddd",
|
||
borderBottom: "1px solid #ddd",
|
||
// padding: isMobile ? "0.5rem" : "1rem",
|
||
}}
|
||
>
|
||
{children}
|
||
</Box>
|
||
{/* this model for session logout */}
|
||
<Modal
|
||
opened={sessionModal}
|
||
onClose={() => setSessionModal(false)}
|
||
withCloseButton={false}
|
||
centered
|
||
closeOnClickOutside={false} // <--- prevents clicking outside to close
|
||
closeOnEscape={false} // <--- prevents ESC key
|
||
title="Session Timeout Warning"
|
||
>
|
||
<Stack align="center" gap="md">
|
||
<Text ta="center" c="red">
|
||
You have been inactive for a while.
|
||
<br />
|
||
You’ll be logged out automatically in <b>{countdown}</b> seconds.
|
||
</Text>
|
||
<Group justify="center" mt="sm">
|
||
{/* <Button color="gray" variant="default" onClick={() => setSessionModal(false)}>
|
||
Stay Logged In
|
||
</Button> */}
|
||
<Button color="red" onClick={doLogout}>
|
||
Logout Now
|
||
</Button>
|
||
</Group>
|
||
</Stack>
|
||
</Modal>
|
||
|
||
<Divider size="xs" color="blue" />
|
||
|
||
{/* FOOTER */}
|
||
<Box
|
||
style={{
|
||
flexShrink: 0,
|
||
display: "flex",
|
||
justifyContent: "center",
|
||
alignItems: "center",
|
||
backgroundColor: "#f8f9fa",
|
||
// padding: isMobile ? "0.25rem" : "0.5rem",
|
||
}}
|
||
>
|
||
<Text c="dimmed" size={isMobile ? "xs" : "sm"}>
|
||
© 2025 The Kangra Central Co-Operative Bank Ltd.
|
||
</Text>
|
||
</Box>
|
||
</Box>
|
||
</Providers>
|
||
</body>
|
||
</html >
|
||
);
|
||
}
|
||
}
|