Initial commit

This commit is contained in:
2025-10-04 18:22:24 +03:00
commit 2852c2c054
291 changed files with 38109 additions and 0 deletions

231
src/layouts/app/App.tsx Normal file
View File

@@ -0,0 +1,231 @@
import {
LogoutOutlined,
QuestionOutlined,
SettingOutlined,
UserOutlined,
} from "@ant-design/icons";
import {
Dropdown,
Flex,
FloatButton,
Layout,
MenuProps,
message,
theme,
Tooltip,
Typography,
} from "antd";
import { ReactNode, useEffect, useRef, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import dayjs from "dayjs";
import { logout } from "features/auth/authSlice.ts";
import { useTranslation } from "react-i18next";
import { useAppDispatch } from "redux/hooks.ts";
import { PATH_AUTH } from "utils/constants.ts";
import LangSelect from "./components/langSelect/LangSelect.tsx";
import HeaderNav from "./HeaderNav.tsx";
import SideNav from "./SideNav.tsx";
import "./styles.css";
const { Content } = Layout;
const { Title } = Typography;
type AppLayoutProps = {
withSidebar?: boolean;
children: ReactNode;
};
export const AppLayout = ({ children, withSidebar = true }: AppLayoutProps) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const {
token: { borderRadius },
} = theme.useToken();
const [navFill, setNavFill] = useState(false);
const navigate = useNavigate();
const nodeRef = useRef(null);
const floatBtnRef = useRef(null);
const location = useLocation();
const isMenu = location.pathname === "/menu";
const items: MenuProps["items"] = [
{
key: "user-profile-link",
label: t("profile"),
icon: <UserOutlined />,
},
{
key: "user-settings-link",
label: t("settings"),
icon: <SettingOutlined />,
},
{
key: "user-help-link",
label: t("help center"),
icon: <QuestionOutlined />,
},
{
type: "divider",
},
{
key: "user-logout-link",
label: t("logout"),
icon: <LogoutOutlined />,
danger: true,
onClick: () => {
message.open({
type: "loading",
content: t("msg-success-logout"),
});
dispatch(logout());
setTimeout(() => {
navigate(PATH_AUTH.signin);
}, 1000);
},
},
];
useEffect(() => {
window.addEventListener("scroll", () => {
if (window.scrollY > 5) {
setNavFill(true);
} else {
setNavFill(false);
}
});
}, []);
return (
<>
<Layout
style={{
minHeight: "100vh",
// backgroundColor: 'white',
}}
>
{withSidebar && (
<SideNav
trigger={null}
style={{
overflow: "auto",
background: "none",
border: "none",
transition: "all .2s",
}}
/>
)}
<HeaderNav
style={{
padding: "0px 20px 0px 20px",
background: navFill ? "rgba(255, 255, 255, .5)" : "none",
backdropFilter: navFill ? "blur(8px)" : "none",
boxShadow: navFill ? "0 0 8px 2px rgba(0, 0, 0, 0.05)" : "none",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
height: "7vh",
zIndex: 10,
transition: "all .25s",
}}
>
<Flex gap="middle" align="start" style={{ gap: 25 }}>
<Tooltip title={dayjs().format("DD/MM/YYYY")}>
<Title level={3} style={{ position: "relative", top: -2 }}>
{dayjs().format("DD/MM/YYYY")}{" "}
</Title>
</Tooltip>
<Tooltip title={t("language")}>
<LangSelect />
</Tooltip>
</Flex>
{/* <Flex align="center">
<Input.Search
placeholder="search"
style={{
width: isMobile ? "100%" : "400px",
marginLeft: isMobile ? 0 : ".5rem",
}}
size="middle"
/>
</Flex> */}
<Flex align="end" gap="small">
{/* <Tooltip title="Apps">
<Button icon={<AppstoreOutlined />} type="text" size="large" />
</Tooltip>
<Tooltip title="Messages">
<Button icon={<MessageOutlined />} type="text" size="large" />
</Tooltip> */}
{/* <Tooltip title="Theme">
<Switch
className=" hidden sm:inline py-1"
checkedChildren={<MoonOutlined />}
unCheckedChildren={<SunOutlined />}
checked={mytheme === "light" ? true : false}
onClick={() => dispatch(toggleTheme())}
/>
</Tooltip> */}
{/* <Badge
count={2}
size="small"
style={{
top: 15,
right: 5,
left: 5,
}}
>
<BellOutlined
style={{
fontSize: 20,
marginTop: 15,
marginLeft: 15,
marginRight: 15,
cursor: "pointer",
}}
onClick={() => setNotificationsDrawerOpen(true)}
/>
</Badge> */}
<Dropdown menu={isMenu ? {} : { items }} trigger={["click"]}>
<Flex>
{!isMenu && (
<img
src={"/avatar.png"}
alt="user profile photo"
height={36}
width={36}
style={{
borderRadius,
objectFit: "cover",
position: "relative",
top: 5,
}}
/>
)}
</Flex>
</Dropdown>
</Flex>
</HeaderNav>
<Content>
<div ref={nodeRef} style={{ background: "none" }}>
{children}
</div>
<div ref={floatBtnRef}>
<FloatButton.BackTop />
</div>
</Content>
{/* <FooterNav
style={{
textAlign: "center",
marginLeft: collapsed ? 0 : "200px",
background: "none",
}}
/> */}
</Layout>
</>
);
};

View File

@@ -0,0 +1,13 @@
import { Layout } from 'antd';
const { Footer } = Layout;
type FooterNavProps = React.HTMLAttributes<HTMLDivElement>;
const FooterNav = ({ ...others }: FooterNavProps) => {
return (
<Footer {...others}>AntD Dashboard © 2023 Created by Design Sparx</Footer>
);
};
export default FooterNav;

View File

@@ -0,0 +1,135 @@
import { CloseOutlined } from "@ant-design/icons";
import { Button, Drawer, Grid } from "antd";
import BackIcon from "components/Icons/BackIcon";
import NextIcon from "components/Icons/NextIcon";
import ProText from "components/ProText";
import { useState } from "react";
import { useAppSelector } from "redux/hooks";
import useHeaderMenu from "./hooks/useHeaderMenu";
const {useBreakpoint} = Grid;
export default function HeaderMenuDrawer() {
const { isRTL, locale } = useAppSelector((state) => state.locale);
const {themeName} = useAppSelector(state => state.theme)
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const { menuItems } = useHeaderMenu();
const {xs } = useBreakpoint();
const isMobile = xs
const closeMobileMenu = () => setMobileMenuOpen(false);
const mobileMenuLinkStyle = (isActive: boolean): React.CSSProperties => ({
display: "flex",
alignItems: "center",
gap: 16,
padding: "10px 20px",
borderRadius: 12,
fontSize: 14,
fontWeight: isActive ? 600 : 400,
marginBottom: 4,
color: "#1f2937",
});
const actionButtonStyle: React.CSSProperties = {
color: themeName === "dark" ? "white" : "#1f2937",
height: 32, // isMobile ? 44 : 40,
minWidth: 32, //isMobile ? 44 : "auto",
width: 32, //isMobile ? 44 : "auto",
borderRadius: 8,
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: isMobile ? 0 : 8,
};
return (
<>
<div
style={{
display: "flex",
justifyContent: "space-between",
position: "fixed",
zIndex: 999,
[isRTL ? "left" : "right"]: 0,
top: "30%",
backgroundColor: themeName === "dark" ? "#111827" : "#FFF",
[isRTL ? "borderTopRightRadius" : "borderTopLeftRadius"]: 10,
[isRTL ? "borderBottomRightRadius" : "borderBottomLeftRadius"]: 10,
border:
themeName === "dark" ? "1px solid #374151" : "1px solid #FDF1D9",
}}
>
<Button
type="text"
icon={isRTL ? <NextIcon /> : <BackIcon />}
onClick={() => setMobileMenuOpen(true)}
style={actionButtonStyle}
/>
</div>
<Drawer
placement={isRTL ? "left" : "right"}
maskClosable={false}
onClose={closeMobileMenu}
open={mobileMenuOpen}
width={"50%"}
styles={{
body: {
padding: 0,
},
header: {
padding: "5px 24px",
borderBottom: `1px solid ${
themeName === "dark" ? "#374151" : "#e5e7eb"
}`,
},
}}
closeIcon={null}
>
<div className="mobile-drawer-content">
<div
style={{
width: 32,
height: 32,
backgroundColor: "#5F6C7B1F",
borderRadius: "50%",
gap: 10,
[isRTL ? "marginRight" : "marginLeft"]: "80%",
marginTop: "50px",
display: "flex",
flexDirection: "row",
justifyContent: "center",
}}
>
<CloseOutlined
onClick={() => {
closeMobileMenu();
}}
/>
</div>
{/* Navigation Links */}
<div className="mobile-drawer-section">
<div style={{ display: "flex", flexDirection: "column" }}>
{menuItems.map((item) => (
<div
key={item.key}
onClick={() => {
closeMobileMenu();
item.onClick?.();
}}
>
<div
key={item.key}
style={mobileMenuLinkStyle(locale === item.key)}
>
{item.icon}
<ProText>{item.label.props.children}</ProText>
</div>
</div>
))}
</div>
</div>
</div>
</Drawer>
</>
);
}

View File

@@ -0,0 +1,16 @@
import { Layout } from 'antd';
import { useRef } from 'react';
const { Header } = Layout;
type HeaderNavProps = {
navFill?: boolean;
} & React.HTMLAttributes<HTMLDivElement>;
const HeaderNav = ({ ...others }: HeaderNavProps) => {
const nodeRef = useRef(null);
return <Header ref={nodeRef} {...others} />;
};
export default HeaderNav;

View File

@@ -0,0 +1,68 @@
import { LoginOutlined } from "@ant-design/icons";
import { ConfigProvider, Layout, Menu, SiderProps } from "antd";
import { useEffect, useRef, useState } from "react";
import { useLocation } from "react-router-dom";
import { COLOR } from "ThemeConstants";
import useSidebarItems from "./useSidebarItems";
const { Sider } = Layout;
type SideNavProps = SiderProps;
const SideNav = ({ ...others }: SideNavProps) => {
const nodeRef = useRef(null);
const { pathname } = useLocation();
const [current, setCurrent] = useState("");
useEffect(() => {
const paths = pathname.split("/");
setCurrent(paths[paths.length - 1]);
}, [pathname]);
const items = useSidebarItems();
return (
<Sider
ref={nodeRef}
collapsedWidth={105}
breakpoint="lg"
trigger={null}
collapsed={true}
{...others}
style={{ background: "#FFF", overflow: "hidden" }}
className="side-container"
>
<LoginOutlined
color="blue"
// asLink
// href={PATHS.menu}
// justify="center"
// gap="small"
// imgSize={{ h: 105, w: 105 }}
// style={{ padding: "1rem 0" }}
// src="/sidebar-logo.jpg"
/>
<ConfigProvider
theme={{
components: {
Menu: {
itemBg: "none",
itemSelectedBg: COLOR["100"],
itemHoverBg: COLOR["50"],
itemSelectedColor: COLOR["600"],
},
},
}}
>
<Menu
mode="inline"
items={items}
selectedKeys={[current]}
style={{ border: "none" }}
className="sidebar-menu-container"
/>
</ConfigProvider>
</Sider>
);
};
export default SideNav;

View File

@@ -0,0 +1,14 @@
.lang-item {
margin: 8px 0px;
padding: 0px 16px;
}
.lang-item .avatar {
width: 30px;
height: 30px;
border-radius: 35%;
}
.lang-dropdown-container ul {
padding: 10px 0px !important;
}

View File

@@ -0,0 +1,79 @@
import type { MenuProps } from "antd";
import { Avatar, Dropdown } from "antd";
import { setLocalesThunk } from "features/locale/localeSlice";
import { LOCALES } from "i18n/helper";
import { useCallback, useEffect, useMemo, useState } from "react";
import i18n from "i18n/i18n";
import { useAppDispatch } from "redux/hooks";
import { DEFAULT_LANGUAGE } from "utils/constants";
import "./LangSelect.css";
type Locale = {
id: string;
title: string;
iconPath: string;
};
export default function LangSelect() {
const dispatch = useAppDispatch();
const [currentLanguage, setCurrentLanguage] = useState<Locale>(LOCALES[0]);
useEffect(() => {
const selectedLocale = LOCALES.find(
(item) => item.id === localStorage.getItem(DEFAULT_LANGUAGE)
);
if (selectedLocale && selectedLocale.id !== currentLanguage.id)
setCurrentLanguage(selectedLocale);
}, [currentLanguage?.id]);
const changeLocale = useCallback(
(key: string) => {
dispatch(setLocalesThunk(key));
setCurrentLanguage(LOCALES.find((item) => item.id === key) || LOCALES[0]);
i18n.changeLanguage(key);
},
[dispatch]
);
const items: MenuProps["items"] = useMemo(
() =>
LOCALES.map((_locale) => ({
className: "lang-item",
key: _locale.id,
icon: (
<span>
<Avatar className="avatar" src={_locale.iconPath} />
</span>
),
label: _locale.title,
})),
[]
);
return (
<Dropdown
menu={{
items,
onClick: ({ key }) => {
changeLocale(key as string);
},
}}
overlayClassName="lang-dropdown-container"
>
<Avatar
style={{
position: "relative",
top: 25,
borderRadius: "35%",
width: 30,
height: 30,
}}
src={currentLanguage?.iconPath}
/>
</Dropdown>
);
}

View File

@@ -0,0 +1,90 @@
import {
BgColorsOutlined,
HomeOutlined,
LoginOutlined,
MenuOutlined,
TranslationOutlined,
} from "@ant-design/icons";
import { setLocale, setLocalesThunk } from "features/locale/localeSlice";
import { toggleTheme } from "features/theme/themeSlice";
import i18n from "i18n/i18n";
import { useTranslation } from "react-i18next";
import { Link, useNavigate, useParams } from "react-router-dom";
import { useAppDispatch, useAppSelector } from "redux/hooks";
export default function useHeaderMenu() {
const { id } = useParams();
const dispatch = useAppDispatch();
const { t } = useTranslation();
const { isRTL } = useAppSelector((state) => state.locale);
const { themeName } = useAppSelector((state) => state.theme);
const navigate = useNavigate();
const menuItems = [
{
key: "language",
icon: (
<TranslationOutlined
style={{ color: themeName === "dark" ? "white" : "#1f2937" }}
/>
),
label: <div>{isRTL ? t("common.arabic") : t("common.english")}</div>,
onClick: () => {
const key = isRTL ? "en" : "ar";
dispatch(setLocale(key));
dispatch(setLocalesThunk(key));
i18n.changeLanguage(key);
},
},
{
key: "theme",
icon: (
<BgColorsOutlined
style={{ color: themeName === "dark" ? "white" : "#1f2937" }}
/>
),
label: (
<div>
{themeName === "light" ? t("common.dark") : t("common.light")}
</div>
),
onClick: () => {
dispatch(toggleTheme());
},
},
{
key: "orders",
icon: (
<HomeOutlined
style={{ color: themeName === "dark" ? "white" : "#1f2937" }}
/>
),
label: <Link to={`/${id}/orders`}>{t("common.myOrder")}</Link>,
onClick: () => {
navigate(`/${id}/orders`);
},
},
{
key: "branches",
icon: (
<MenuOutlined
style={{ color: themeName === "dark" ? "white" : "#1f2937" }}
/>
),
label: <Link to={`/${id}/menu`}>{t("common.branches")}</Link>,
},
{
key: "login",
icon: (
<LoginOutlined
style={{ color: themeName === "dark" ? "white" : "#1f2937" }}
/>
),
label: <Link to={`/${id}/login`}>{t("common.login")}</Link>,
onClick: () => {
navigate(`/${id}/login`);
},
},
];
return { menuItems };
}

1
src/layouts/app/index.ts Normal file
View File

@@ -0,0 +1 @@
export { AppLayout } from './App.tsx';

View File

@@ -0,0 +1,66 @@
.trigger {
padding: 0 8px;
font-size: 18px;
line-height: 45px;
cursor: pointer;
transition: color 0.3s;
background-color: #005488;
border-radius: 50%;
width: 35px;
height: 35px;
font-weight: 400;
font-size: 16px;
line-height: 18px;
display: inline-block;
margin-left: 10px;
margin-right: 10px;
vertical-align: middle;
}
.trigger:hover {
color: #fff;
background-color: rgba(255, 255, 255, 0.36) !;
}
.icon-container {
width: 40px;
height: 40px;
/* margin-left: 15px; */
/* padding-left: 11px; */
position: absolute;
right: 30px;
margin-top: 10px;
}
.sidebar-menu-container .ant-menu-item,
.sidebar-menu-container .ant-menu-submenu {
margin-top: 25px !important;
}
.home-icon {
margin-top: -10px;
}
.translations-icon {
right: 25px !important;
}
.menu-icon {
margin-top: -17px;
right: 30px !important;
width: 38px;
}
.warehouse-icon {
color: black !important;
width: 35px !important;
}
.tax-icon {
right: 18px !important;
margin-top: -17px;
}
.report-icon {
right: 30px !important;
width: 32px;
}

View File

@@ -0,0 +1,70 @@
import { BugFilled } from "@ant-design/icons";
import { MenuProps } from "antd";
import { PATHS } from "utils/constants";
// import WarehouseIcon from "components/Icons/WarehouseIcon";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
export default function useSidebarItems() {
type MenuItem = Required<MenuProps>["items"][number] & {
permission: string;
children?: MenuItem[];
};
const { t } = useTranslation();
// const [isAuth] = useAuth();
const getItem = (
label: React.ReactNode,
key: React.Key,
permission?: string,
icon?: React.ReactNode,
children?: MenuItem[],
type?: "group"
): MenuItem => {
return {
key,
icon,
children,
label,
type,
permission,
} as MenuItem;
};
// Recursive function to filter items based on permissions
const getGrantedItems = (items: any[]): MenuItem[] => {
return items
.filter(() => true)// Filter out items without permission
.map((item: any) => {
if (item.children) {
// Recursively filter children
return {
...item,
children: getGrantedItems(item.children),
};
}
return item;
});
};
const items: MenuProps["items"] = [
getItem(
t("menu"),
PATHS.menu,
undefined,
<div style={{ marginTop: 10 }}>
<Link style={{}} to={PATHS.menu}>
<BugFilled className="icon-container pos-icon" />
</Link>
</div>
),
];
// if we have a menu with empty children after applying "getGrantedItems"
// we going to remove it
const grantedItems = getGrantedItems(items).filter(
(i) => (i.children && i.children.length !== 0) || !i.children
);
return grantedItems;
}

View File

@@ -0,0 +1,10 @@
import { AppLayout } from "layouts/app";
import { Outlet } from "react-router-dom";
export const DashboardLayout = () => {
return (
<AppLayout>
<Outlet />
</AppLayout>
);
};

3
src/layouts/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export { AppLayout } from './app';
export { DashboardLayout } from './dashboards';