done ui enhancement

This commit is contained in:
azeeee05
2026-04-25 08:23:16 +03:00
parent e763ad457c
commit fc0147f046
25 changed files with 355 additions and 392 deletions
+1 -16
View File
@@ -71,7 +71,6 @@
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/generator": "^7.28.6",
@@ -1794,7 +1793,6 @@
"integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -1805,7 +1803,6 @@
"integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1865,7 +1862,6 @@
"integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.53.0",
"@typescript-eslint/types": "8.53.0",
@@ -2113,7 +2109,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2236,7 +2231,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -2606,7 +2600,6 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -3118,7 +3111,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4"
},
@@ -3317,8 +3309,7 @@
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"license": "BSD-2-Clause",
"peer": true
"license": "BSD-2-Clause"
},
"node_modules/leaflet-control-geocoder": {
"version": "3.3.1",
@@ -3865,7 +3856,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -3943,7 +3933,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -3953,7 +3942,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -4279,7 +4267,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -4374,7 +4361,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -4505,7 +4491,6 @@
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+12 -14
View File
@@ -3,6 +3,7 @@ import { ExternalLink, Box, Search, Rocket } from 'lucide-react';
import { moduleApi, type Module } from '../modules/user/UserModuleApi';
import { useAuth } from '../../context/AuthContext';
import { useTranslation } from 'react-i18next';
import CustomInput from '../../components/custom/CustomInput';
const Dashboard = () => {
const { user } = useAuth();
@@ -83,29 +84,25 @@ const Dashboard = () => {
}, [modules, searchTerm, t]);
return (
<div className="p-8 space-y-10 min-h-screen bg-gray-50/50">
<div className="space-y-6 min-h-screen">
{/* Hero Section */}
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-6 animate-fade-in-up">
<div>
<h1 className="text-4xl font-extrabold text-(--text-primary) tracking-tight mb-2">
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold text-(--text-primary)">
{t('dashboard:title')}, {user?.first_name || 'Admin'}! 👋
</h1>
<p className="text-lg text-(--text-secondary)">
<p className="text-sm text-(--text-secondary)">
{t('dashboard:subtitle')}
</p>
</div>
<div className="flex items-center gap-4 w-full md:w-auto">
<div className="relative group w-full md:w-64">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Search className="h-5 w-5 text-gray-400 group-focus-within:text-blue-500 transition-colors" />
</div>
<input
type="text"
<div className="w-full md:w-80">
<CustomInput
placeholder={t('dashboard:searchPlaceholder')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="block w-full pl-10 pr-3 py-2.5 bg-white border border-gray-200 rounded-xl leading-5 text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm hover:shadow-md"
leftIcon={<Search size={18} />}
/>
</div>
@@ -116,11 +113,12 @@ const Dashboard = () => {
<div className="space-y-12">
{loading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="bg-white border border-gray-100 rounded-2xl p-6 h-48 animate-pulse shadow-sm">
{[1, 2, 3, 4, 5, 6, 7, 8].map((i) => (
<div key={i} className="bg-(--card-bg) border border-(--card-border) rounded-2xl p-6 h-48 animate-pulse shadow-sm flex flex-col">
<div className="h-12 w-12 bg-gray-200 rounded-xl mb-4"></div>
<div className="h-6 bg-gray-200 rounded w-3/4 mb-3"></div>
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
<div className="h-4 bg-gray-200 rounded w-1/2 mb-auto"></div>
<div className="h-4 bg-gray-200 rounded w-1/4 mt-4"></div>
</div>
))}
</div>
+9 -6
View File
@@ -1,7 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { History, RefreshCcw } from "lucide-react";
import { useTranslation } from "react-i18next";
import { CustomButton, CustomColumnFilter } from "../../components/custom";
import { CustomColumnFilter, CustomButton } from "../../components/custom";
import { DataTable } from "../../components/custom/CustomTable";
import type { ColumnDef } from "../../components/custom/CustomTable";
import {
@@ -17,6 +16,7 @@ import {
SHARED_ADMIN_TABLE_PAGE_SIZE_STORAGE_KEY,
resolveStoredTablePageSize,
} from "../../lib/tablePageSize";
import { RefreshCcw } from "lucide-react";
interface AuditLog extends Record<string, unknown> {
id: string;
@@ -267,11 +267,14 @@ const LogsPage = () => {
);
return (
<div className="p-6 space-y-6">
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold text-(--text-primary) flex items-center gap-2">
<History size={24} /> {t("title")}
<div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold text-(--text-primary)">
{t("title")}
</h1>
<p className="text-sm text-[var(--text-secondary)]">Track and audit all system activities and user actions.</p>
</div>
<CustomButton
variant="outlined"
onClick={fetchLogs}
@@ -63,7 +63,7 @@ const ModuleList = () => {
if (loading && modules.length === 0) {
return (
<div className="p-6">
<div>
<div className="animate-pulse space-y-4">
<div className="h-8 bg-gray-300 rounded w-1/4"></div>
<div className="h-64 bg-gray-300 rounded"></div>
@@ -73,11 +73,11 @@ const ModuleList = () => {
}
return (
<div className="p-6 space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold text-(--text-primary)">{t('registry.title')}</h1>
<p className="text-(--text-secondary) mt-1">{t('registry.subtitle')}</p>
<div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold text-(--text-primary)">{t('registry.title')}</h1>
<p className="text-sm text-(--text-secondary)">{t('registry.subtitle')}</p>
</div>
<CustomButton
onClick={handleCreate}
@@ -108,8 +108,7 @@ const ModuleList = () => {
</div>
</div>
<div
className={`flex items-center gap-1 transition-opacity ${
togglingId === module.id
className={`flex items-center gap-1 transition-opacity ${togglingId === module.id
? 'opacity-50 cursor-not-allowed'
: 'cursor-pointer hover:text-blue-600'
}`}
+2 -2
View File
@@ -239,8 +239,8 @@ const ProfilePage: React.FC = () => {
return (
<div className="space-y-6">
{/* Header */}
<div>
<h1 className="text-2xl font-semibold text-[var(--text-primary)]">{t('title')}</h1>
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold text-[var(--text-primary)]">{t('title')}</h1>
<p className="text-sm text-[var(--text-secondary)]">
{t('subTitle')}
</p>
@@ -196,9 +196,9 @@ const AddRoles = () => {
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-4">
<CustomBackButton to="/roles" tooltip={t('backToRoles')} />
<div>
<h1 className="text-2xl font-semibold text-gray-900">{t('add')}</h1>
<p className="text-sm text-gray-500">
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold text-[var(--text-primary)]">{t('add')}</h1>
<p className="text-sm text-[var(--text-secondary)]">
{t('subTitle')}
</p>
</div>
@@ -464,8 +464,9 @@ const AllRoles = () => {
return (
<div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-semibold text-[var(--text-primary)]">{t('title')}</h1>
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold text-[var(--text-primary)]">{t('title')}</h1>
<p className="text-sm text-[var(--text-secondary)]">Define and manage user roles and their associated permissions.</p>
</div>
<ProtectedComponent requiredAccess="admin.role.create">
<Link to="/roles/add">
@@ -476,16 +477,13 @@ const AllRoles = () => {
{isLoading ? (
<div className="rounded-lg border border-[var(--card-border)] bg-[var(--card-bg)] p-6">
<CustomLoader />
</div>
) : errorMessage ? (
{errorMessage ? (
<div className="rounded-lg border border-red-200 bg-red-50 p-6 text-sm text-red-600">
{errorMessage}
</div>
) : (
<CustomTable
isLoading={isLoading}
data={roles}
columns={columns}
columnVisibilityEnabled
+2 -3
View File
@@ -54,12 +54,11 @@ const SettingsPage: React.FC = () => {
return (
<div className="max-w-4xl mx-auto">
{/* Page Header */}
<div className="flex items-center gap-3 mb-6">
<div>
<div className="flex flex-col gap-1 mb-6">
<h1 className="text-2xl font-bold text-(--text-primary)">
{t('settings.title')}
</h1>
</div>
<p className="text-sm text-[var(--text-secondary)]">Manage your account settings and application preferences.</p>
</div>
{/* Language Settings Section */}
@@ -123,15 +123,17 @@ const AddSubscriptions = () => {
<div className="flex items-center gap-4">
<CustomBackButton to="/subscriptions" tooltip="Back to Subscriptions" />
<div>
<h1 className="text-2xl font-semibold text-gray-900">
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold text-[var(--text-primary)]">
Add Subscription Plan
</h1>
<p className="text-sm text-gray-500">
<p className="text-sm text-[var(--text-secondary)]">
Create a new plan with pricing and access permissions.
</p>
</div>
</div>
</div>
</div>
<form
onSubmit={handleSubmit}
@@ -529,10 +529,9 @@ const AllSubscriptions = () => {
return (
<div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-semibold text-[var(--text-primary)]">
Subscription Plans
</h1>
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold text-[var(--text-primary)]">Subscription Plans</h1>
<p className="text-sm text-[var(--text-secondary)]">Create and manage different subscription tiers and their permissions.</p>
</div>
<ProtectedComponent requiredAccess="superadmin.plan.create">
<Link to="/subscriptions/add">
@@ -541,16 +540,13 @@ const AllSubscriptions = () => {
</ProtectedComponent>
</div>
{isLoading ? (
<div className="rounded-lg border border-[var(--card-border)] bg-[var(--card-bg)] p-6">
<CustomLoader />
</div>
) : errorMessage ? (
{errorMessage ? (
<div className="rounded-lg border border-red-200 bg-red-50 p-6 text-sm text-red-600">
{errorMessage}
</div>
) : (
<CustomTable
isLoading={isLoading}
data={plans}
columns={columns}
columnVisibilityEnabled
@@ -239,11 +239,11 @@ const AddTenants = () => {
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-4">
<CustomBackButton to="/tenants" tooltip="Back to Tenants" />
<div>
<h1 className="text-2xl font-semibold text-gray-900">
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold text-[var(--text-primary)]">
{canCreateTenant ? "Add Tenant" : "Tenant Details"}
</h1>
<p className="text-sm text-gray-500">
<p className="text-sm text-[var(--text-secondary)]">
{canCreateTenant
? "Create a new tenant with domain and subscription plan."
: "Your account is assigned to this tenant."}
@@ -12,7 +12,6 @@ import {
CustomModal,
CustomTable,
CustomStatus,
CustomLoader,
CustomActionMenu,
CustomActionItem,
} from "../../../components/custom";
@@ -643,8 +642,9 @@ const AllTenants = () => {
return (
<div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-semibold text-[var(--text-primary)]">Tenants</h1>
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold text-[var(--text-primary)]">Tenants</h1>
<p className="text-sm text-[var(--text-secondary)]">Manage and monitor all tenant accounts in the system.</p>
</div>
<ProtectedComponent requiredAccess="superadmin.tenant.create">
<Link to="/tenants/add">
@@ -653,16 +653,13 @@ const AllTenants = () => {
</ProtectedComponent>
</div>
{isLoading ? (
<div className="rounded-lg border border-[var(--card-border)] bg-[var(--card-bg)] p-6">
<CustomLoader />
</div>
) : errorMessage ? (
{errorMessage ? (
<div className="rounded-lg border border-red-200 bg-red-50 p-6 text-sm text-red-600">
{errorMessage}
</div>
) : (
<CustomTable
isLoading={isLoading}
data={tenants}
columns={columns}
columnVisibilityEnabled
@@ -3,7 +3,7 @@ import { Plus, Edit2, Trash2 } from "lucide-react";
import { CustomButton } from "../../../components/custom";
import DataTable, { type ColumnDef } from "../../../components/custom/CustomTable";
import { CustomActionMenu, CustomActionItem, CustomStatus, CustomConfirmationModal } from "../../../components/custom";
import { Loader } from "../../../components/custom/CustomLoader";
import { paletteApi } from "../PaletteApi";
import type { ColorPalette, ColorPalettePayload } from "../ThemeTypes";
import PaletteForm from "./PaletteForm";
@@ -169,27 +169,24 @@ const AllPalettes: React.FC = () => {
<>
<div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-semibold text-(--text-primary)">{t('theme:title')}</h1>
{/* <p className="text-sm text-(--text-secondary) mt-1">
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold text-(--text-primary)">{t('theme:title')}</h1>
<p className="text-sm text-(--text-secondary)">
Manage system color themes and default palettes.
</p> */}
</p>
</div>
<CustomButton onClick={handleCreate} leftIcon={<Plus size={18} />}>
{t('theme:actions.create')}
</CustomButton>
</div>
{isLoading ? (
<div className="rounded-lg border border-[var(--card-border)] bg-[var(--card-bg)] p-6">
<Loader />
</div>
) : errorMessage ? (
{errorMessage ? (
<div className="rounded-lg border border-red-200 bg-red-50 p-6 text-sm text-red-600">
{errorMessage}
</div>
) : (
<DataTable
isLoading={isLoading}
data={palettes}
columns={columns}
columnVisibilityEnabled
@@ -268,8 +268,8 @@ const AddUsers = () => {
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-4">
<CustomBackButton to="/users" tooltip={t('backToUsers')} />
<div>
<h1 className="text-2xl font-semibold text-(--text-primary)">{t('add')}</h1>
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold text-(--text-primary)">{t('add')}</h1>
<p className="text-sm text-(--text-secondary)">
{t('subTitle')}
</p>
@@ -11,7 +11,7 @@ import {
CustomInput,
CustomModal,
CustomTable,
CustomLoader,
CustomActionMenu,
CustomActionItem,
CustomStatus,
@@ -587,8 +587,9 @@ const AllUsers = () => {
return (
<div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-semibold text-[var(--text-primary)]">{t('title')}</h1>
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold text-[var(--text-primary)]">{t('title')}</h1>
<p className="text-sm text-[var(--text-secondary)]">Manage system users, their roles, and account status.</p>
</div>
<ProtectedComponent requiredAccess="admin.user.create">
<Link to="/users/add">
@@ -599,16 +600,13 @@ const AllUsers = () => {
{isLoading ? (
<div className="rounded-lg border border-[var(--card-border)] bg-[var(--card-bg)] p-6">
<CustomLoader />
</div>
) : errorMessage ? (
{errorMessage ? (
<div className="rounded-lg border border-red-200 bg-red-50 p-6 text-sm text-red-600">
{errorMessage}
</div>
) : (
<CustomTable
isLoading={isLoading}
data={users}
columns={columns}
columnVisibilityEnabled
+3 -6
View File
@@ -8,6 +8,7 @@ import {
persistTablePageSize,
resolveStoredTablePageSize,
} from "../../lib/tablePageSize";
import Loader from "./CustomLoader";
function cn(...parts: Array<string | false | null | undefined>) {
return parts.filter(Boolean).join(" ");
@@ -701,12 +702,8 @@ export function DataTable<T extends Record<string, unknown>>(
colSpan={visibleColumns.length}
className="px-6 py-24 text-center text-[var(--text-secondary)]"
>
<div className="flex flex-col items-center justify-center">
<svg className="animate-spin h-8 w-8 text-blue-500 mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p>{t('common.loading')}</p>
<div className="flex flex-col items-center justify-center min-h-[100px]">
<Loader />
</div>
</td>
</tr>
+37 -79
View File
@@ -1,21 +1,23 @@
import {
Menu,
ChevronDown,
LogOut,
} from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useSidebar } from "../../context/SidebarContext";
import { useAuth } from "../../context/AuthContext";
import { useTranslation } from "react-i18next";
import { authApi } from "../../application/authentication/AuthApi";
import { clearAuthCookies } from "../../lib/authCookies";
import { useTranslation } from "react-i18next";
const AppHeader: React.FC = () => {
const { t } = useTranslation(['profile', 'common']);
const navigate = useNavigate();
const { user, } = useAuth();
const { user } = useAuth();
const { toggleMobileSidebar } = useSidebar();
const navigate = useNavigate();
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
const userMenuRef = useRef<HTMLDivElement>(null);
@@ -50,12 +52,12 @@ const AppHeader: React.FC = () => {
: "U";
return (
<header className="sticky top-0 z-40 flex w-full bg-[var(--header-bg)] border-b border-[var(--header-border)] transition-colors duration-200">
<header className="sticky top-0 z-40 flex w-full bg-white/80 backdrop-blur-md border-b border-gray-200 transition-all duration-200">
<div className="flex flex-grow items-center gap-4 px-4 py-3 lg:px-6">
{/* LEFT: Mobile Toggle & Brand (Mobile only) */}
<div className="flex items-center gap-3">
<button
className="flex items-center justify-center p-2 text-[var(--header-text)] hover:bg-[var(--header-border)] rounded-lg lg:hidden transition-colors"
className="flex items-center justify-center p-2 text-gray-500 hover:bg-gray-100 rounded-lg lg:hidden transition-colors"
onClick={toggleMobileSidebar}
aria-label="Toggle Sidebar"
>
@@ -63,67 +65,12 @@ const AppHeader: React.FC = () => {
</button>
<Link to="/" className="lg:hidden flex items-center gap-2">
<span className="font-bold text-[var(--header-text)] text-lg">SaaS</span>
<span className="font-bold text-gray-900 text-lg">SaaS</span>
</Link>
</div>
{/* Logistics Network Pattern - Multi-layer Design */}
<div className="hidden lg:flex items-center overflow-hidden">
{/* ... existing SVG content ... */}
<svg className="w-80 h-10" viewBox="0 0 320 40" fill="none" xmlns="http://www.w3.org/2000/svg">
{/* Background grid pattern */}
<pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
<path d="M 20 0 L 0 0 0 20" fill="none" stroke="currentColor" strokeOpacity="0.1" strokeWidth="0.5" className="text-[var(--header-text)]" />
</pattern>
<rect width="320" height="40" fill="url(#grid)" />
{/* Multiple route paths */}
<path d="M0 30 C40 30, 60 10, 100 10 S160 30, 200 20 S260 10, 320 15" stroke="currentColor" strokeOpacity="0.2" strokeWidth="2" fill="none" className="text-[var(--header-text)]" />
<path d="M0 20 C50 15, 80 25, 120 20 S180 10, 220 15 S280 25, 320 20" stroke="currentColor" strokeOpacity="0.3" strokeWidth="1.5" fill="none" className="text-[var(--header-text)]" />
<path d="M0 10 C30 15, 70 5, 110 12 S170 20, 210 12 S270 8, 320 12" stroke="currentColor" strokeOpacity="0.4" strokeWidth="1" strokeDasharray="4 2" fill="none" className="text-[var(--header-text)]" />
{/* Hub nodes with glow effect */}
<circle cx="40" cy="25" r="6" fill="currentColor" fillOpacity="0.1" stroke="currentColor" strokeOpacity="0.4" strokeWidth="1.5" className="text-[var(--header-text)]" />
<circle cx="40" cy="25" r="3" fill="currentColor" className="text-[var(--primary)]" />
<circle cx="120" cy="15" r="8" fill="currentColor" fillOpacity="0.1" stroke="currentColor" strokeOpacity="0.5" strokeWidth="2" className="text-[var(--header-text)]" />
<circle cx="120" cy="15" r="4" fill="currentColor" className="text-[var(--primary)]" />
<circle cx="200" cy="18" r="6" fill="currentColor" fillOpacity="0.1" stroke="currentColor" strokeOpacity="0.4" strokeWidth="1.5" className="text-[var(--header-text)]" />
<circle cx="200" cy="18" r="3" fill="currentColor" className="text-[var(--primary)]" />
<circle cx="280" cy="16" r="5" fill="currentColor" fillOpacity="0.1" stroke="currentColor" strokeOpacity="0.3" strokeWidth="1" className="text-[var(--header-text)]" />
<circle cx="280" cy="16" r="2.5" fill="currentColor" className="text-[var(--primary)]" />
{/* Package icons */}
<g transform="translate(70, 22)">
<rect width="10" height="8" rx="1" fill="currentColor" fillOpacity="0.2" stroke="currentColor" strokeWidth="0.8" className="text-[var(--header-text)]" />
<line x1="0" y1="3" x2="10" y2="3" stroke="currentColor" strokeWidth="0.5" className="text-[var(--header-text)]" />
<line x1="5" y1="3" x2="5" y2="8" stroke="currentColor" strokeWidth="0.5" className="text-[var(--header-text)]" />
</g>
<g transform="translate(160, 8)">
<rect width="10" height="8" rx="1" fill="currentColor" fillOpacity="0.2" stroke="currentColor" strokeWidth="0.8" className="text-(--header-text)" />
<line x1="0" y1="3" x2="10" y2="3" stroke="currentColor" strokeWidth="0.5" className="text-(--header-text)" />
<line x1="5" y1="3" x2="5" y2="8" stroke="currentColor" strokeWidth="0.5" className="text-(--header-text)" />
</g>
<g transform="translate(240, 20)">
<rect width="8" height="6" rx="1" fill="currentColor" fillOpacity="0.2" stroke="currentColor" strokeWidth="0.8" className="text-(--header-text)" />
<line x1="0" y1="2" x2="8" y2="2" stroke="currentColor" strokeWidth="0.5" className="text-(--header-text)" />
</g>
{/* Connecting dots */}
<circle cx="20" cy="22" r="1.5" fill="currentColor" fillOpacity="0.5" className="text-(--header-text)" />
<circle cx="90" cy="12" r="1.5" fill="currentColor" fillOpacity="0.5" className="text-(--header-text)" />
<circle cx="150" cy="22" r="1.5" fill="currentColor" fillOpacity="0.5" className="text-(--header-text)" />
<circle cx="230" cy="14" r="1.5" fill="currentColor" fillOpacity="0.5" className="text-(--header-text)" />
<circle cx="300" cy="18" r="1.5" fill="currentColor" fillOpacity="0.5" className="text-(--header-text)" />
</svg>
</div>
{/* RIGHT: Actions */}
<div className="flex items-center gap-2 sm:gap-4 ms-auto">
<div className="flex items-center gap-2 sm:gap-4 ltr:ml-auto rtl:mr-auto">
{/* User Menu */}
<div className="relative" ref={userMenuRef}>
@@ -132,11 +79,10 @@ const AppHeader: React.FC = () => {
onClick={() => setIsUserMenuOpen((prev) => !prev)}
className="
flex items-center gap-2
pl-2 pr-1 py-1.5
rounded-lg
hover:bg-(--header-border)
transition-colors
border border-transparent hover:border-(--header-border)
p-1 pr-3
rounded-full border border-gray-200
hover:border-gray-300 hover:bg-gray-50
transition-all bg-white/50
"
aria-expanded={isUserMenuOpen}
>
@@ -144,12 +90,12 @@ const AppHeader: React.FC = () => {
{initials}
</div>
<div className="hidden md:block text-left me-1">
<p className="text-sm font-medium text-(--header-text) leading-none">{fullName}</p>
<div className="hidden md:block ltr:text-left rtl:text-right ltr:mr-1 rtl:ml-1 leading-tight">
<p className="text-sm font-semibold text-gray-700">{fullName}</p>
</div>
<ChevronDown
size={16}
size={14}
className={`text-gray-400 transition-transform duration-200 ${isUserMenuOpen ? "rotate-180" : ""
}`}
/>
@@ -159,33 +105,45 @@ const AppHeader: React.FC = () => {
{isUserMenuOpen && (
<div
className="
absolute end-0 mt-2 w-56
rounded-xl border border-(--card-border)
bg-(--card-bg)
shadow-md
absolute ltr:right-0 rtl:left-0 mt-2 w-64
rounded-xl border border-gray-100
bg-white
shadow-xl
overflow-hidden
animate-in fade-in zoom-in-95 duration-150
origin-top-right
ltr:origin-top-right rtl:origin-top-left
z-50
"
>
<div className="px-4 py-3 border-b border-(--card-border) bg-(--background)">
<p className="text-xs text-(--text-secondary) truncate mt-0.5">
<div className="px-4 py-3 border-b border-gray-100 bg-gray-50/50">
<p className="text-sm font-semibold text-gray-900 truncate">
{user?.email}
</p>
<p className="text-xs text-gray-500 truncate mt-0.5 uppercase tracking-wider">
{user?.role?.role_name || "User"}
</p>
</div>
<div className="py-1">
<Link
to="/profile"
onClick={() => setIsUserMenuOpen(false)}
className="flex w-full items-center gap-2 px-4 py-2 text-sm text-(--text-primary) hover:bg-(--background-secondary) transition-colors"
className="flex w-full items-center gap-3 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
>
<div className="w-8 h-8 rounded-lg bg-gray-100 flex items-center justify-center text-gray-500">
<ChevronDown size={16} className="-rotate-90" />
</div>
{t('profile:header.profileSettings')}
</Link>
<div className="h-px bg-gray-100 my-1 mx-2"></div>
<button
onClick={handleLogout}
className="flex w-full items-center gap-2 px-4 py-2 text-sm text-red-600 hover:bg-red-50 transition-colors"
className="flex w-full items-center gap-3 px-4 py-2.5 text-sm text-red-600 hover:bg-red-50 transition-colors"
>
<div className="w-8 h-8 rounded-lg bg-red-50 flex items-center justify-center text-red-600">
<LogOut size={16} />
</div>
{t('profile:header.signOut')}
</button>
</div>
+6 -5
View File
@@ -2,15 +2,15 @@ import { SidebarProvider, useSidebar } from "../../context/SidebarContext";
import { Outlet } from "react-router-dom";
import AppHeader from "./AppHeader";
import AppSidebar from "./AppSidebar";
import Backdrop from "./Backdrop";
const LayoutContent: React.FC = () => {
const { isExpanded } = useSidebar();
return (
<div className="min-h-screen bg-[var(--background)] transition-colors duration-200 flex flex-col">
<div className="min-h-screen bg-gray-50 flex flex-col transition-all duration-300">
<AppSidebar />
<Backdrop />
<div
className={`
@@ -20,9 +20,10 @@ const LayoutContent: React.FC = () => {
>
<AppHeader />
<main className="flex-1 p-4 md:p-6 lg:p-6 animate-fade-in w-full max-w-[1920px] mx-auto">
{/* Outlet Container - Ensures content is constrained but responsive */}
<main className="flex-1 p-4 sm:p-6 lg:p-8 animate-in fade-in duration-500">
<div className="max-w-[1600px] mx-auto w-full">
<Outlet />
</div>
</main>
</div>
</div>
+93 -72
View File
@@ -15,6 +15,7 @@ import {
ChevronDown,
ChevronRight,
CreditCard,
FileText,
} from "lucide-react";
import { useSidebar } from "../../context/SidebarContext";
import { usePermission } from "../../lib/usePermission";
@@ -82,7 +83,7 @@ const navItems: NavItem[] = [
access: "modules.view",
},
{
icon: <LayoutGrid size={22} />,
icon: <FileText size={22} />,
name: "Logs",
path: "/logs",
access: "admin.logs.read",
@@ -124,6 +125,7 @@ const AppSidebar: React.FC = () => {
'Settings': 'settings',
'Modules': 'modules',
'Logs': 'logs',
'Subscriptions': 'subscriptions',
};
const key = keyMap[name];
@@ -131,7 +133,6 @@ const AppSidebar: React.FC = () => {
return t(`nav.${key}`, name);
}
// Try other namespaces if not in nav map
return t([
`modules:${name}`,
`dashboard:${name}`,
@@ -140,22 +141,6 @@ const AppSidebar: React.FC = () => {
]);
};
const isActive = useCallback(
(path: string) => location.pathname === path || location.pathname.startsWith(path + "/"),
[location.pathname]
);
const isSubmenuRouteActive = useCallback(
(submenu?: SubMenuItem[]) =>
submenu?.some(
(subItem) =>
location.pathname === subItem.path ||
location.pathname.startsWith(subItem.path + "/")
) ?? false,
[location.pathname]
);
const visibleNavItems = useMemo(
() => navItems.filter((item) => {
if (!item.access) return true;
@@ -169,11 +154,55 @@ const AppSidebar: React.FC = () => {
[can]
);
const isActive = useCallback(
(path: string) => location.pathname === path || location.pathname.startsWith(path + "/"),
[location.pathname]
);
const groupedNavItems = useMemo(() => {
const groups: { label: string; items: NavItem[] }[] = [];
const addedPaths = new Set<string>();
const getItems = (names: string[]) => {
const filtered = visibleNavItems.filter(item =>
names.includes(item.name) && !addedPaths.has(item.path)
);
filtered.forEach(item => addedPaths.add(item.path));
return filtered;
};
// 1. Overview
const dashboardItems = getItems(["Dashboard"]);
if (dashboardItems.length > 0) {
groups.push({ label: t('nav.groups.overview', 'Overview'), items: dashboardItems });
}
// 2. Management
const managementItems = getItems(["Tenants", "Subscriptions", "Roles", "Users"]);
if (managementItems.length > 0) {
groups.push({ label: t('nav.groups.management', 'Management'), items: managementItems });
}
// 3. System
const systemItems = getItems(["Themes", "Modules", "Logs", "Settings"]);
if (systemItems.length > 0) {
groups.push({ label: t('nav.groups.system', 'System'), items: systemItems });
}
// Catch-all
const remaining = visibleNavItems.filter(item => !addedPaths.has(item.path));
if (remaining.length > 0) {
groups.push({ label: t('nav.groups.other', 'Other'), items: remaining });
}
return groups;
}, [visibleNavItems, t]);
const [openSubmenus, setOpenSubmenus] = useState<Record<string, boolean>>({});
const toggleSubmenu = (name: string) => {
if (!isExpanded && !isMobile) {
toggleSidebar(); // Auto expand when clicking submenu in collapsed state
toggleSidebar();
}
setOpenSubmenus(prev => ({ ...prev, [name]: !prev[name] }));
};
@@ -192,21 +221,11 @@ const AppSidebar: React.FC = () => {
return null;
}
// Sidebar width logic
const sidebarWidth = isExpanded ? "w-[260px]" : "w-[80px]";
const mobileClasses = isMobileOpen ? "translate-x-0" : "-translate-x-full";
return (
<>
{/* Mobile Backdrop */}
{isMobileOpen && (
<div
className="fixed inset-0 bg-black/50 z-40 lg:hidden backdrop-blur-sm transition-opacity duration-300 animate-fade-in"
onClick={closeMobileSidebar}
aria-hidden="true"
/>
)}
<aside
className={`
fixed top-0 left-0 z-50 h-screen bg-[var(--sidebar-bg)] text-[var(--sidebar-text)] flex flex-col
@@ -218,8 +237,7 @@ const AppSidebar: React.FC = () => {
`}
>
{/* Header */}
<div className={`h-[70px] flex items-center px-4 border-b border-(--sidebar-border)/50 ${!isExpanded && !isMobile ? 'justify-center' : 'justify-between'}`}>
{/* Logo - Only show when expanded or mobile */}
<div className={`h-[70px] flex items-center px-4 border-b border-white/5 ${!isExpanded && !isMobile ? 'justify-center' : 'justify-between'}`}>
{(isExpanded || isMobile) && (
<Link to="/dashboard" className="flex items-center gap-3 overflow-hidden ml-1" onClick={isMobile ? closeMobileSidebar : undefined}>
{user?.role?.accesses?.includes("superadmin.tenant.read") ? (
@@ -235,44 +253,50 @@ const AppSidebar: React.FC = () => {
className="h-8 w-auto object-contain"
/>
) : (
<>
<div className="shrink-0 text-blue-400">
<Package size={32} />
<div className="flex items-center gap-2">
<div className="shrink-0 w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center text-white font-bold">S</div>
<span className="text-xl font-bold text-white whitespace-nowrap">SaaS</span>
</div>
<span className="text-xl font-bold text-(--sidebar-active-text) whitespace-nowrap">
SaaS
</span>
</>
)}
</Link>
)}
{/* Desktop Toggle Button */}
{!isMobile && (
<button
onClick={toggleSidebar}
className={`p-1.5 rounded-lg text-slate-400 hover:text-white hover:bg-white/10 transition-colors ${!isExpanded ? "" : ""}`}
className="p-1.5 rounded-lg text-slate-400 hover:text-white hover:bg-white/10 transition-colors"
aria-label="Toggle Sidebar"
>
<PanelLeft size={20} className={!isExpanded ? "rotate-180" : ""} />
</button>
)}
{/* Mobile Close Button */}
{isMobile && (
<button onClick={closeMobileSidebar} className="p-1 rounded-md text-slate-400 hover:text-white hover:bg-white/10">
<button
onClick={closeMobileSidebar}
className="p-1 rounded-md text-slate-400 hover:text-white hover:bg-white/10"
>
<X size={20} />
</button>
)}
</div>
{/* Navigation */}
<nav className="flex-1 overflow-y-auto overflow-x-hidden py-6 px-3 custom-scrollbar">
<ul className="space-y-2">
{visibleNavItems.map((nav) => {
<nav className="flex-1 overflow-y-auto overflow-x-hidden py-4 px-3 custom-scrollbar">
<div className="flex flex-col gap-6">
{groupedNavItems.map((group, idx) => (
<div key={idx} className="space-y-1.5">
{(isExpanded || isMobile) && (
<p className="px-3 text-[10px] font-bold text-slate-500 uppercase tracking-[0.15em] mb-2 mt-2">
{group.label}
</p>
)}
<ul className="space-y-1.5">
{group.items.map((nav) => {
const active = isActive(nav.path);
const hasSubmenu = nav.submenu && nav.submenu.length > 0;
const isMenuOpen = openSubmenus[nav.name] || isSubmenuRouteActive(nav.submenu);
const isMenuOpen = openSubmenus[nav.name];
if (hasSubmenu) {
return (
@@ -280,10 +304,10 @@ const AppSidebar: React.FC = () => {
<button
onClick={() => toggleSubmenu(nav.name)}
className={`
w-full relative flex items-center gap-3 px-3 py-3 rounded-lg transition-all duration-200 group
w-full relative flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-200 group
${isMenuOpen
? "bg-white/5 text-(--sidebar-active-text)"
: "text-(--sidebar-text) hover:bg-(--sidebar-hover-bg)"
? "bg-white/5 text-white"
: "text-slate-400 hover:bg-white/5 hover:text-white"
}
`}
>
@@ -293,24 +317,18 @@ const AppSidebar: React.FC = () => {
{(isExpanded || isMobile) && (
<>
<span className="flex-1 text-left whitespace-nowrap font-medium">
<span className="flex-1 text-left whitespace-nowrap font-medium text-sm">
{getNavTranslation(nav.name)}
</span>
<span className="flex-shrink-0 transition-transform duration-200">
{isMenuOpen ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
{isMenuOpen ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</span>
</>
)}
{!isExpanded && !isMobile && (
<div className="absolute left-full ml-4 px-2 py-1 bg-slate-800 text-white text-xs rounded shadow-lg opacity-0 group-hover:opacity-100 transition-opacity z-50 whitespace-nowrap pointer-events-none">
{getNavTranslation(nav.name)}
</div>
)}
</button>
{/* Submenu Items */}
<div className={`overflow-hidden transition-all duration-300 ${isMenuOpen && (isExpanded || isMobile) ? "max-h-96 opacity-100 mt-1" : "max-h-0 opacity-0"}`}>
<ul className="space-y-1 pl-9">
<div className={`overflow-hidden transition-all duration-300 ${isMenuOpen && (isExpanded || isMobile) ? "max-h-[800px] opacity-100 mt-1" : "max-h-0 opacity-0"}`}>
<ul className="space-y-1 pl-4 mt-1 border-l border-white/5 ml-4">
{nav.submenu?.filter(subItem => !subItem.access || can(subItem.access)).map((subItem) => {
const subActive = isActive(subItem.path || "");
return (
@@ -319,15 +337,15 @@ const AppSidebar: React.FC = () => {
to={subItem.path || "#"}
onClick={isMobile ? closeMobileSidebar : undefined}
className={`
flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors block
flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors block
${subActive
? "bg-(--sidebar-active-bg) text-(--sidebar-active-text) font-medium"
: "text-(--sidebar-text-muted) hover:text-(--sidebar-text) hover:bg-(--sidebar-hover-bg)"
? "text-white font-bold"
: "text-slate-500 hover:text-slate-300 hover:bg-white/5"
}
`}
>
{subItem.icon && (
<div className="flex-shrink-0 scale-90">
<div className="flex-shrink-0 scale-75">
{subItem.icon}
</div>
)}
@@ -348,10 +366,10 @@ const AppSidebar: React.FC = () => {
to={nav.path}
onClick={isMobile ? closeMobileSidebar : undefined}
className={`
relative flex items-center gap-3 px-3 py-3 rounded-lg transition-all duration-200 group
relative flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-200 group
${active
? "bg-(--sidebar-active-bg) text-(--sidebar-active-text)"
: "text-(--sidebar-text) hover:bg-(--sidebar-hover-bg)"
? "bg-white text-slate-900"
: "text-slate-400 hover:bg-white/5 hover:text-white"
}
`}
>
@@ -361,14 +379,13 @@ const AppSidebar: React.FC = () => {
<span
className={`
whitespace-nowrap font-medium transition-all duration-300
whitespace-nowrap font-semibold text-sm transition-all duration-300
${(isExpanded || isMobile) ? "opacity-100 w-auto" : "opacity-0 w-0 hidden"}
`}
>
{getNavTranslation(nav.name)}
</span>
{/* Tooltip for collapsed mode */}
{!isExpanded && !isMobile && (
<div className="absolute left-full ml-4 px-2 py-1 bg-slate-800 text-white text-xs rounded shadow-lg opacity-0 group-hover:opacity-100 transition-opacity z-50 whitespace-nowrap pointer-events-none">
{getNavTranslation(nav.name)}
@@ -379,19 +396,23 @@ const AppSidebar: React.FC = () => {
);
})}
</ul>
</div>
))}
</div>
</nav>
{/* Footer */}
<div className="p-4 border-t border-slate-700/50">
{/* Logout */}
<div className="p-4 border-t border-white/5">
<button
onClick={handleLogout}
className={`flex items-center gap-3 w-full px-3 py-2 text-red-400 hover:text-red-300 hover:bg-red-500/10 rounded-lg transition-colors overflow-hidden ${(isExpanded || isMobile) ? "justify-start" : "justify-center"}`}
className={`group flex items-center gap-3 w-full px-3 py-2.5 text-slate-400 hover:text-white hover:bg-white/5 rounded-xl transition-all overflow-hidden ${(isExpanded || isMobile) ? "justify-start" : "justify-center"}`}
title="Logout"
>
<div className="flex-shrink-0 transition-colors group-hover:text-white">
<LogOut size={20} />
</div>
{(isExpanded || isMobile) && (
<span className="font-medium">{t('actions.signout')}</span>
<span className="font-medium text-sm">Sign Out</span>
)}
</button>
</div>
+1 -1
View File
@@ -7,7 +7,7 @@ const Backdrop: React.FC = () => {
return (
<div
className="fixed inset-0 z-40 bg-gray-900/50 lg:hidden"
className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm lg:hidden animate-in fade-in duration-300"
onClick={toggleMobileSidebar}
/>
);
+12 -2
View File
@@ -1,4 +1,4 @@
@import url("https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Archivo:ital,wght@0,100..900;1,100..900&display=swap");
@import "tailwindcss";
@keyframes fadeIn {
@@ -40,16 +40,26 @@
--text-secondary: #64748B;
--card-bg: #FFFFFF;
--card-border: #E2E8F0;
/* Optional: smoother focus ring for any inputs inside your custom components */
--ui-ring: rgba(17, 24, 39, 0.12); /* gray-900/12 */
}
html,
body {
font-family: "Outfit", sans-serif;
font-family: "Archivo", sans-serif;
@apply text-base antialiased;
}
}
@layer utilities {
/* Optional: smoother focus ring for any inputs inside your custom components */
.ui-focus :is(input, textarea, select, button):focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--ui-ring);
border-radius: 12px;
}
/* Hide number input arrows/spinners */
.no-spinner::-webkit-inner-spin-button,
.no-spinner::-webkit-outer-spin-button {
+4
View File
@@ -7,6 +7,10 @@ export default defineConfig(({ mode }) => {
console.log(`\n 🔌 Backend API URL: \x1b[36m${env.VITE_API_BASE_URL}\x1b[0m\n`);
return {
server: {
port: 5174,
},
plugins: [react(), tailwindcss()],
}
})