Files
IB/src/app/(main)/layout.tsx
tomosa.sarkar 8a194a5855 feat : In home page "get statement" worked.
feat : After 5 minutes session timeout automatically.
feat: realtime otp feature up
2025-10-09 14:22:39 +05:30

384 lines
17 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, Text, Title } from '@mantine/core';
import { IconBook, IconCurrencyRupee, IconHome, IconLogout, IconPhoneFilled, IconSettings } 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';
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 [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");
}
async function handleFetchUserName() {
try {
const token = localStorage.getItem("access_token");
const response = await fetch('/api/customer', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
});
if (!response.ok) {
notifications.show({
withBorder: true,
color: "red",
title: "Error",
message: "Internal Server Error",
autoClose: 5000,
});
localStorage.removeItem("access_token");
localStorage.removeItem("remitter_name");
return;
}
const data = await response.json();
if (response.ok && Array.isArray(data)) {
if (data.length > 0) {
const name = data[0].custname;
const mobileNumber = data[0].mobileno;
localStorage.setItem("remitter_name", name);
localStorage.setItem("remitter_mobile_no", mobileNumber);
setCustname(name);
}
} else {
throw new Error();
}
} catch {
notifications.show({
withBorder: true,
color: "red",
title: "Please try again later",
message: "Unable to Fetch, Please try again later",
autoClose: 5000,
});
}
}
// 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);
handleFetchUserName();
}
}, []);
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',
'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
style={{
width: "100%",
display: "flex",
height: "60px",
// flexDirection: "row",
alignItems: "center",
justifyContent: "flex-start",
// padding: isMobile ? "0.5rem" : "0.5rem 1rem",
background: "linear-gradient(15deg,rgba(10, 114, 40, 1) 55%, rgba(101, 101, 184, 1) 100%)",
position: "relative",
// position: "fixed",
}}
>
{/* Logo */}
<Box style={{ width: isMobile ? "48px" : "65px", height: isMobile ? "50px" : "60px" }}>
<Image src={logo} component={NextImage} alt="ebanking" style={{ width: "100%", height: "100%" }} />
</Box>
{/* Title & Phone */}
<Stack gap={isMobile ? 2 : 0} style={{ flex: 1, textAlign: isMobile ? "center" : "left", marginLeft: isMobile ? 0 : "1rem" }}>
<Title order={isMobile ? 5 : 2} style={{ color: "white", fontFamily: "Roboto", lineHeight: 1.2 }}>
THE KANGRA CENTRAL CO-OPERATIVE BANK LTD.
</Title>
<Text style={{ color: "white", fontSize: isMobile ? "0.8rem" : "0.9rem", textShadow: "1px 1px 2px black" }}>
<IconPhoneFilled size={isMobile ? 16 : 20} /> Toll Free No : 1800-180-8008
</Text>
</Stack>
</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
</Text>
</Box>
</Box>
</Providers>
</body>
</html >
);
}
}