Files
IB/src/app/(main)/layout.tsx

411 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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 />
Youll 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 >
);
}
}