Files
IB/src/app/(main)/layout.tsx
2025-12-06 13:54:20 +05:30

508 lines
25 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 { Anchor, Box, Button, Container, Divider, Group, Image, Modal, Popover, Stack, Switch, Text, Title, Grid } from '@mantine/core';
import { IconHome, IconLogout, IconMoon, IconSend, IconSettings, IconSun, IconUserCircle, IconUsers, IconWallet } 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: IconWallet },
{ href: "/funds_transfer", label: "Fund Transfer", icon: IconSend },
{ href: "/beneficiary", label: "Beneficiaries", icon: IconUsers },
{ href: "/settings", label: "Settings", icon: IconSettings },
];
if (authorized) {
return (
<html lang="en">
<body>
<Providers>
<Box style={{ minHeight: "100%", display: "flex", flexDirection: "column" }}>
{/* HEADER */}
<Box
component="header"
className={styles.header}
style={{
width: "100%",
padding: isMobile ? "0.6rem 1rem" : "0.8rem 2rem",
background: darkMode
? "linear-gradient(15deg, rgba(229, 101, 22, 1) 55%, rgba(28, 28, 30, 1) 100%)"
: "linear-gradient(15deg, rgba(10, 114, 40, 1) 55%, rgba(101, 101, 184, 1) 100%)",
alignItems: "center",
justifyContent: "space-between",
color: "white",
boxShadow: "0 2px 6px rgba(0,0,0,0.15)",
position: "sticky",
top: 0,
zIndex: 200, // ↑ Increased for stability
}}
>
<Group gap="md" wrap="nowrap">
<Image
src={logo}
component={NextImage}
fit="contain"
alt="ebanking"
style={{
width: isMobile ? "40px" : "60px",
height: "auto",
}}
/>
<div>
<Title
order={isMobile ? 4 : 3}
style={{
fontFamily: "Roboto",
color: "white",
marginBottom: 2,
fontSize: isMobile ? "14px" : "22px",
lineHeight: 1.2,
}}
>
THE KANGRA CENTRAL CO-OPERATIVE BANK LTD.
</Title>
{!isMobile && (
<Text size="xs" c="white" style={{ opacity: 0.85 }}>
Head Office: Dharmshala, District Kangra (H.P), Pin: 176215
</Text>
)}
</div>
</Group>
{/* USER BUTTON */}
<Popover
opened={userOpened}
onChange={setUserOpened}
position="bottom-end"
withArrow
shadow="md"
>
<Popover.Target>
<Button
leftSection={<IconUserCircle size={isMobile ? 18 : 22} />}
variant="subtle"
onClick={() => setUserOpened((prev) => !prev)}
color='white'
style={{
fontWeight: 500,
padding: isMobile ? "4px 8px" : "6px 12px",
fontSize: isMobile ? "12px" : "14px",
}}
>
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={handleLogout}
>
Logout
</Button>
</Stack>
</Popover.Dropdown>
</Popover>
</Box>
{/* NAVBAR — desktop unchanged, mobile scrollable */}
<Group
style={{
background: "#c8eeacff",
boxShadow: "0 6px 6px rgba(0,0,0,0.06)",
position: "sticky",
top: isMobile ? 60 : 85,
zIndex: 150,
/* MOBILE FIX make it scrollable */
overflowX: isMobile ? "auto" : "visible",
whiteSpace: isMobile ? "nowrap" : "normal",
padding: isMobile ? "6px 4px" : "0.05rem",
}}
>
{navItems.map((item) => {
const isActive = pathname.startsWith(item.href);
const Icon = item.icon;
return (
<Link key={item.href} href={item.href} style={{ textDecoration: "none" }}>
<Group
gap={8}
style={{
padding: isMobile ? "10px 14px" : "14px 16px",
// borderRadius: isMobile ? 6 : 8,
width: "100%",
transition: "0.2s ease",
background: isActive ? "rgba(50, 159, 81, 1)" : "transparent",
color: isActive ? "white" : "#3b3b3b",
fontWeight: isActive ? 600 : 500,
}}
>
<Icon
size={isMobile ? 16 : 18}
color={isActive ? "white" : "#3b3b3b"}
/>
<Text size={isMobile ? "xs" : "sm"} fw={500}>
{item.label}
</Text>
</Group>
</Link>
);
})}
</Group>
{/* CONTENT */}
<Box
style={{
flex: 1,
backgroundColor: "#f5f7fa",
padding: isMobile ? "0.8rem" : "1.5rem",
}}
>
<Box
style={{
margin: "0 auto",
background: "white",
padding: isMobile ? "1rem" : "1.8rem",
borderRadius: "12px",
boxShadow: "0 4px 12px rgba(0,0,0,0.08)",
minHeight: "75vh",
}}
>
{children}
</Box>
</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>
{/* FOOTER (desktop same, mobile stacked) */}
<Box
component="footer"
style={{
backgroundColor: "rgba(60, 54, 74, 1)",
paddingTop: "2rem",
paddingBottom: "2rem",
color: "white",
}}
>
<Container size="xl">
<Grid gutter="xl">
<Grid.Col span={{ base: 12, md: 4 }}>
<Group mb="md">
<Box
style={{
width: 40,
height: 40,
background: 'linear-gradient(135deg, #16a34a 0%, #10b981 100%)',
borderRadius: '50%',
overflow: 'hidden',
}}
>
<Image
src={logo}
component={NextImage}
fit="cover"
alt="ebanking"
style={{
width: "100%",
height: "100%",
borderRadius: "50%",
}}
/>
</Box>
<div>
<Text size="sm" fw={500}>The Kangra Central</Text>
<Text size="sm" fw={500}>Co-operative Bank Ltd</Text>
</div>
</Group>
<Text size="sm" c="dimmed">
Serving the community since inception.
</Text>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 4 }}>
<Text size="sm" fw={500} mb="md">Quick Links</Text>
<Stack gap="xs">
<Anchor
href="https://kccbhp.bank.in/about-us/history-of-kccb/"
size="sm"
c="dimmed"
target="_blank"
rel="noopener noreferrer"
>
About Us
</Anchor>
<Anchor
href="https://kccbhp.bank.in/products/service-products/service-charges/"
size="sm"
c="dimmed"
target="_blank"
rel="noopener noreferrer"
>
Products & Services
</Anchor>
<Anchor
href="/CustomerCare"
size="sm"
c="dimmed"
target="_blank"
rel="noopener noreferrer"
>
Help & Support
</Anchor>
</Stack>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 4 }}>
<Text size="sm" fw={500} mb="md">Contact Us</Text>
<Stack gap="xs">
<Text size="sm" c="dimmed">Phone: +91-1800-1808008 </Text>
<Text size="sm" c="dimmed">MonSat 10 AM 5 PM</Text>
<Text size="sm" c="dimmed">(The Second and fourth Saturdays are holidays)</Text>
</Stack>
</Grid.Col>
</Grid>
<Text
size="sm"
c="dimmed"
ta="center"
style={{ borderTop: "1px solid rgba(255,255,255,0.2)", marginTop: 20, paddingTop: 20 }}
>
© 2025 The Kangra Central Co-operative Bank Ltd. All rights reserved.
</Text>
</Container>
</Box>
</Box>
</Providers>
</body>
</html>
);
}
}