Merge pull request 'azeem' (#24) from azeem into dev

Reviewed-on: https://gitea.maskantech.in/gitea_admin/saas_frontend/pulls/24
This commit is contained in:
furqan
2026-04-25 05:46:31 +00:00
25 changed files with 419 additions and 433 deletions
+1 -16
View File
@@ -71,7 +71,6 @@
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.28.6", "@babel/code-frame": "^7.28.6",
"@babel/generator": "^7.28.6", "@babel/generator": "^7.28.6",
@@ -1794,7 +1793,6 @@
"integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
} }
@@ -1805,7 +1803,6 @@
"integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@@ -1865,7 +1862,6 @@
"integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==", "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/scope-manager": "8.53.0",
"@typescript-eslint/types": "8.53.0", "@typescript-eslint/types": "8.53.0",
@@ -2113,7 +2109,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -2236,7 +2231,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -2606,7 +2600,6 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -3118,7 +3111,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.28.4" "@babel/runtime": "^7.28.4"
}, },
@@ -3317,8 +3309,7 @@
"version": "1.9.4", "version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause"
"peer": true
}, },
"node_modules/leaflet-control-geocoder": { "node_modules/leaflet-control-geocoder": {
"version": "3.3.1", "version": "3.3.1",
@@ -3865,7 +3856,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -3943,7 +3933,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -3953,7 +3942,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@@ -4279,7 +4267,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -4374,7 +4361,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.27.0", "esbuild": "^0.27.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -4505,7 +4491,6 @@
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "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 { moduleApi, type Module } from '../modules/user/UserModuleApi';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import CustomInput from '../../components/custom/CustomInput';
const Dashboard = () => { const Dashboard = () => {
const { user } = useAuth(); const { user } = useAuth();
@@ -83,29 +84,25 @@ const Dashboard = () => {
}, [modules, searchTerm, t]); }, [modules, searchTerm, t]);
return ( 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 */} {/* Hero Section */}
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-6 animate-fade-in-up"> <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-6 animate-fade-in-up">
<div> <div className="flex flex-col gap-1">
<h1 className="text-4xl font-extrabold text-(--text-primary) tracking-tight mb-2"> <h1 className="text-2xl font-bold text-(--text-primary)">
{t('dashboard:title')}, {user?.first_name || 'Admin'}! 👋 {t('dashboard:title')}, {user?.first_name || 'Admin'}! 👋
</h1> </h1>
<p className="text-lg text-(--text-secondary)"> <p className="text-sm text-(--text-secondary)">
{t('dashboard:subtitle')} {t('dashboard:subtitle')}
</p> </p>
</div> </div>
<div className="flex items-center gap-4 w-full md:w-auto"> <div className="flex items-center gap-4 w-full md:w-auto">
<div className="relative group w-full md:w-64"> <div className="w-full md:w-80">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> <CustomInput
<Search className="h-5 w-5 text-gray-400 group-focus-within:text-blue-500 transition-colors" />
</div>
<input
type="text"
placeholder={t('dashboard:searchPlaceholder')} placeholder={t('dashboard:searchPlaceholder')}
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} 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> </div>
@@ -116,11 +113,12 @@ const Dashboard = () => {
<div className="space-y-12"> <div className="space-y-12">
{loading ? ( {loading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> <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) => ( {[1, 2, 3, 4, 5, 6, 7, 8].map((i) => (
<div key={i} className="bg-white border border-gray-100 rounded-2xl p-6 h-48 animate-pulse shadow-sm"> <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-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-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>
))} ))}
</div> </div>
+9 -6
View File
@@ -1,7 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { History, RefreshCcw } from "lucide-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { CustomButton, CustomColumnFilter } from "../../components/custom"; import { CustomColumnFilter, CustomButton } from "../../components/custom";
import { DataTable } from "../../components/custom/CustomTable"; import { DataTable } from "../../components/custom/CustomTable";
import type { ColumnDef } from "../../components/custom/CustomTable"; import type { ColumnDef } from "../../components/custom/CustomTable";
import { import {
@@ -17,6 +16,7 @@ import {
SHARED_ADMIN_TABLE_PAGE_SIZE_STORAGE_KEY, SHARED_ADMIN_TABLE_PAGE_SIZE_STORAGE_KEY,
resolveStoredTablePageSize, resolveStoredTablePageSize,
} from "../../lib/tablePageSize"; } from "../../lib/tablePageSize";
import { RefreshCcw } from "lucide-react";
interface AuditLog extends Record<string, unknown> { interface AuditLog extends Record<string, unknown> {
id: string; id: string;
@@ -267,11 +267,14 @@ const LogsPage = () => {
); );
return ( return (
<div className="p-6 space-y-6"> <div className="space-y-6">
<div className="flex justify-between items-center"> <div className="flex flex-wrap items-center justify-between gap-4">
<h1 className="text-2xl font-bold text-(--text-primary) flex items-center gap-2"> <div className="flex flex-col gap-1">
<History size={24} /> {t("title")} <h1 className="text-2xl font-bold text-(--text-primary)">
{t("title")}
</h1> </h1>
<p className="text-sm text-[var(--text-secondary)]">Track and audit all system activities and user actions.</p>
</div>
<CustomButton <CustomButton
variant="outlined" variant="outlined"
onClick={fetchLogs} onClick={fetchLogs}
@@ -3,6 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom';
import { ArrowLeft, Plus, Edit, Trash2, Check, X } from 'lucide-react'; import { ArrowLeft, Plus, Edit, Trash2, Check, X } from 'lucide-react';
import { adminModuleApi } from '../AdminModuleApi'; import { adminModuleApi } from '../AdminModuleApi';
import type { ModuleEnvironment, Module } from '../AdminModuleTypes'; import type { ModuleEnvironment, Module } from '../AdminModuleTypes';
import Loader from '../../../../components/custom/CustomLoader';
import EnvironmentForm from './EnvironmentForm'; import EnvironmentForm from './EnvironmentForm';
import { CustomConfirmationModal } from '../../../../components/custom'; import { CustomConfirmationModal } from '../../../../components/custom';
@@ -63,12 +64,10 @@ const ModuleEnvironments = () => {
} }
}; };
if (loading) {
return <div className="p-6">Loading...</div>;
}
return ( return (
<div className="p-6 space-y-6"> <div className="space-y-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<button <button
onClick={() => navigate('/admin/modules')} onClick={() => navigate('/admin/modules')}
@@ -76,12 +75,13 @@ const ModuleEnvironments = () => {
> >
<ArrowLeft size={20} /> <ArrowLeft size={20} />
</button> </button>
<div className="flex-1"> <div className="flex flex-col gap-1">
<h1 className="text-3xl font-bold text-(--text-primary)"> <h1 className="text-2xl font-bold text-(--text-primary)">
{module?.module_name} - Environments {module?.module_name || 'Loading...'} - Environments
</h1> </h1>
<p className="text-(--text-secondary) mt-1">Configure environment-specific settings</p> <p className="text-sm text-(--text-secondary)">Configure environment-specific settings</p>
</div> </div>
<div className="ml-auto">
<button <button
onClick={() => { setSelectedEnv(null); setShowForm(true); }} onClick={() => { setSelectedEnv(null); setShowForm(true); }}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700" className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
@@ -90,7 +90,13 @@ const ModuleEnvironments = () => {
Add Environment Add Environment
</button> </button>
</div> </div>
</div>
{loading ? (
<div className="p-12 flex flex-col items-center justify-center min-h-[300px]">
<Loader size="md" />
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{environments.map((env) => ( {environments.map((env) => (
<div key={env.id} className="bg-(--card-bg) border border-(--card-border) rounded-lg p-6 space-y-4"> <div key={env.id} className="bg-(--card-bg) border border-(--card-border) rounded-lg p-6 space-y-4">
@@ -160,8 +166,9 @@ const ModuleEnvironments = () => {
</div> </div>
))} ))}
</div> </div>
)}
{environments.length === 0 && ( {environments.length === 0 && !loading && (
<div className="bg-(--card-bg) border border-(--card-border) rounded-lg p-12 text-center"> <div className="bg-(--card-bg) border border-(--card-border) rounded-lg p-12 text-center">
<p className="text-(--text-secondary) mb-4">No environments configured</p> <p className="text-(--text-secondary) mb-4">No environments configured</p>
<button <button
@@ -63,7 +63,7 @@ const ModuleList = () => {
if (loading && modules.length === 0) { if (loading && modules.length === 0) {
return ( return (
<div className="p-6"> <div>
<div className="animate-pulse space-y-4"> <div className="animate-pulse space-y-4">
<div className="h-8 bg-gray-300 rounded w-1/4"></div> <div className="h-8 bg-gray-300 rounded w-1/4"></div>
<div className="h-64 bg-gray-300 rounded"></div> <div className="h-64 bg-gray-300 rounded"></div>
@@ -73,11 +73,11 @@ const ModuleList = () => {
} }
return ( return (
<div className="p-6 space-y-6"> <div className="space-y-6">
<div className="flex justify-between items-center"> <div className="flex flex-wrap items-center justify-between gap-4">
<div> <div className="flex flex-col gap-1">
<h1 className="text-3xl font-bold text-(--text-primary)">{t('registry.title')}</h1> <h1 className="text-2xl font-bold text-(--text-primary)">{t('registry.title')}</h1>
<p className="text-(--text-secondary) mt-1">{t('registry.subtitle')}</p> <p className="text-sm text-(--text-secondary)">{t('registry.subtitle')}</p>
</div> </div>
<CustomButton <CustomButton
onClick={handleCreate} onClick={handleCreate}
@@ -108,8 +108,7 @@ const ModuleList = () => {
</div> </div>
</div> </div>
<div <div
className={`flex items-center gap-1 transition-opacity ${ className={`flex items-center gap-1 transition-opacity ${togglingId === module.id
togglingId === module.id
? 'opacity-50 cursor-not-allowed' ? 'opacity-50 cursor-not-allowed'
: 'cursor-pointer hover:text-blue-600' : 'cursor-pointer hover:text-blue-600'
}`} }`}
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { ArrowLeft, RefreshCw, Shield } from 'lucide-react'; import { ArrowLeft, RefreshCw, Shield } from 'lucide-react';
import { useModuleApi } from '../hooks/useModuleApi'; import { useModuleApi } from '../hooks/useModuleApi';
import Loader from '../../../../components/custom/CustomLoader';
import type { ModulePermission, Module } from '../AdminModuleTypes'; import type { ModulePermission, Module } from '../AdminModuleTypes';
import CustomButton from '../../../../components/custom/CustomButton'; import CustomButton from '../../../../components/custom/CustomButton';
import { NodeGroupAccessViewer } from '../../../roles/components/NodeGroupAccessViewer'; import { NodeGroupAccessViewer } from '../../../roles/components/NodeGroupAccessViewer';
@@ -43,9 +44,7 @@ const ModulePermissions = () => {
setSyncing(false); setSyncing(false);
}; };
if (loading && !module) {
return <div className="p-6">Loading...</div>;
}
const viewerAccesses: RoleAccess[] = permissions.map(p => ({ const viewerAccesses: RoleAccess[] = permissions.map(p => ({
id: p.id, id: p.id,
@@ -56,7 +55,7 @@ const ModulePermissions = () => {
})); }));
return ( return (
<div className="p-6 space-y-6"> <div className="space-y-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<button <button
onClick={() => navigate('/admin/modules')} onClick={() => navigate('/admin/modules')}
@@ -64,12 +63,13 @@ const ModulePermissions = () => {
> >
<ArrowLeft size={20} /> <ArrowLeft size={20} />
</button> </button>
<div className="flex-1"> <div className="flex flex-col gap-1">
<h1 className="text-3xl font-bold text-(--text-primary)"> <h1 className="text-2xl font-bold text-(--text-primary)">
{module?.module_name} - Permissions {module?.module_name || 'Loading...'} - Permissions
</h1> </h1>
<p className="text-(--text-secondary) mt-1">View and sync permissions from module</p> <p className="text-sm text-(--text-secondary)">View and sync permissions from module</p>
</div> </div>
<div className="ml-auto">
<CustomButton <CustomButton
onClick={handleSync} onClick={handleSync}
loading={syncing} loading={syncing}
@@ -78,8 +78,13 @@ const ModulePermissions = () => {
{syncing ? 'Syncing...' : 'Sync Permissions'} {syncing ? 'Syncing...' : 'Sync Permissions'}
</CustomButton> </CustomButton>
</div> </div>
</div>
{permissions.length > 0 ? ( {loading ? (
<div className=" p-12 flex flex-col items-center justify-center min-h-[300px]">
<Loader size="md" />
</div>
) : permissions.length > 0 ? (
<div className="bg-(--card-bg) border border-(--card-border) rounded-lg p-6"> <div className="bg-(--card-bg) border border-(--card-border) rounded-lg p-6">
<NodeGroupAccessViewer accesses={viewerAccesses} /> <NodeGroupAccessViewer accesses={viewerAccesses} />
</div> </div>
@@ -3,6 +3,7 @@ import { useParams } from 'react-router-dom';
import { adminModuleApi } from '../AdminModuleApi'; import { adminModuleApi } from '../AdminModuleApi';
import type { TenantModule, Module } from '../AdminModuleTypes'; import type { TenantModule, Module } from '../AdminModuleTypes';
import { Plus, Trash2 } from 'lucide-react'; import { Plus, Trash2 } from 'lucide-react';
import Loader from '../../../../components/custom/CustomLoader';
const TenantModuleAssignment = () => { const TenantModuleAssignment = () => {
const { tenantId } = useParams<{ tenantId: string }>(); const { tenantId } = useParams<{ tenantId: string }>();
@@ -85,12 +86,15 @@ const TenantModuleAssignment = () => {
} }
}; };
if (loading) return <div className="p-6">Loading...</div>;
return ( return (
<div className="p-6 space-y-6"> <div className="space-y-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h1 className="text-3xl font-bold text-(--text-primary)">Tenant Modules</h1> <div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold text-(--text-primary)">Tenant Modules</h1>
<p className="text-sm text-(--text-secondary)">Manage module assignments for this tenant</p>
</div>
<button <button
onClick={() => setShowAssignDialog(true)} onClick={() => setShowAssignDialog(true)}
disabled={availableModules.length === 0} disabled={availableModules.length === 0}
@@ -101,6 +105,12 @@ const TenantModuleAssignment = () => {
</button> </button>
</div> </div>
{loading ? (
<div className="bg-(--card-bg) border border-(--card-border) rounded-lg p-12 flex flex-col items-center justify-center min-h-[300px]">
<Loader size="lg" />
<p className="mt-4 text-(--text-secondary)">Loading assignments...</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{assignedModules.map((tm) => ( {assignedModules.map((tm) => (
<div key={tm.id} className="bg-(--card-bg) border border-(--card-border) rounded-lg p-6 space-y-4"> <div key={tm.id} className="bg-(--card-bg) border border-(--card-border) rounded-lg p-6 space-y-4">
@@ -141,6 +151,7 @@ const TenantModuleAssignment = () => {
</div> </div>
))} ))}
</div> </div>
)}
{showAssignDialog && ( {showAssignDialog && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
+2 -2
View File
@@ -239,8 +239,8 @@ const ProfilePage: React.FC = () => {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Header */}
<div> <div className="flex flex-col gap-1">
<h1 className="text-2xl font-semibold text-[var(--text-primary)]">{t('title')}</h1> <h1 className="text-2xl font-bold text-[var(--text-primary)]">{t('title')}</h1>
<p className="text-sm text-[var(--text-secondary)]"> <p className="text-sm text-[var(--text-secondary)]">
{t('subTitle')} {t('subTitle')}
</p> </p>
@@ -196,9 +196,9 @@ const AddRoles = () => {
<div className="flex flex-wrap items-center justify-between gap-4"> <div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<CustomBackButton to="/roles" tooltip={t('backToRoles')} /> <CustomBackButton to="/roles" tooltip={t('backToRoles')} />
<div> <div className="flex flex-col gap-1">
<h1 className="text-2xl font-semibold text-gray-900">{t('add')}</h1> <h1 className="text-2xl font-bold text-[var(--text-primary)]">{t('add')}</h1>
<p className="text-sm text-gray-500"> <p className="text-sm text-[var(--text-secondary)]">
{t('subTitle')} {t('subTitle')}
</p> </p>
</div> </div>
@@ -464,8 +464,9 @@ const AllRoles = () => {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-4"> <div className="flex flex-wrap items-center justify-between gap-4">
<div> <div className="flex flex-col gap-1">
<h1 className="text-2xl font-semibold text-[var(--text-primary)]">{t('title')}</h1> <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> </div>
<ProtectedComponent requiredAccess="admin.role.create"> <ProtectedComponent requiredAccess="admin.role.create">
<Link to="/roles/add"> <Link to="/roles/add">
@@ -476,16 +477,13 @@ const AllRoles = () => {
{isLoading ? ( {errorMessage ? (
<div className="rounded-lg border border-[var(--card-border)] bg-[var(--card-bg)] p-6">
<CustomLoader />
</div>
) : errorMessage ? (
<div className="rounded-lg border border-red-200 bg-red-50 p-6 text-sm text-red-600"> <div className="rounded-lg border border-red-200 bg-red-50 p-6 text-sm text-red-600">
{errorMessage} {errorMessage}
</div> </div>
) : ( ) : (
<CustomTable <CustomTable
isLoading={isLoading}
data={roles} data={roles}
columns={columns} columns={columns}
columnVisibilityEnabled columnVisibilityEnabled
+2 -3
View File
@@ -54,12 +54,11 @@ const SettingsPage: React.FC = () => {
return ( return (
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
{/* Page Header */} {/* Page Header */}
<div className="flex items-center gap-3 mb-6"> <div className="flex flex-col gap-1 mb-6">
<div>
<h1 className="text-2xl font-bold text-(--text-primary)"> <h1 className="text-2xl font-bold text-(--text-primary)">
{t('settings.title')} {t('settings.title')}
</h1> </h1>
</div> <p className="text-sm text-[var(--text-secondary)]">Manage your account settings and application preferences.</p>
</div> </div>
{/* Language Settings Section */} {/* Language Settings Section */}
@@ -123,15 +123,17 @@ const AddSubscriptions = () => {
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<CustomBackButton to="/subscriptions" tooltip="Back to Subscriptions" /> <CustomBackButton to="/subscriptions" tooltip="Back to Subscriptions" />
<div> <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 Add Subscription Plan
</h1> </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. Create a new plan with pricing and access permissions.
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</div>
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
@@ -529,10 +529,9 @@ const AllSubscriptions = () => {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-4"> <div className="flex flex-wrap items-center justify-between gap-4">
<div> <div className="flex flex-col gap-1">
<h1 className="text-2xl font-semibold text-[var(--text-primary)]"> <h1 className="text-2xl font-bold text-[var(--text-primary)]">Subscription Plans</h1>
Subscription Plans <p className="text-sm text-[var(--text-secondary)]">Create and manage different subscription tiers and their permissions.</p>
</h1>
</div> </div>
<ProtectedComponent requiredAccess="superadmin.plan.create"> <ProtectedComponent requiredAccess="superadmin.plan.create">
<Link to="/subscriptions/add"> <Link to="/subscriptions/add">
@@ -541,16 +540,13 @@ const AllSubscriptions = () => {
</ProtectedComponent> </ProtectedComponent>
</div> </div>
{isLoading ? ( {errorMessage ? (
<div className="rounded-lg border border-[var(--card-border)] bg-[var(--card-bg)] p-6">
<CustomLoader />
</div>
) : errorMessage ? (
<div className="rounded-lg border border-red-200 bg-red-50 p-6 text-sm text-red-600"> <div className="rounded-lg border border-red-200 bg-red-50 p-6 text-sm text-red-600">
{errorMessage} {errorMessage}
</div> </div>
) : ( ) : (
<CustomTable <CustomTable
isLoading={isLoading}
data={plans} data={plans}
columns={columns} columns={columns}
columnVisibilityEnabled columnVisibilityEnabled
@@ -239,11 +239,11 @@ const AddTenants = () => {
<div className="flex flex-wrap items-center justify-between gap-4"> <div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<CustomBackButton to="/tenants" tooltip="Back to Tenants" /> <CustomBackButton to="/tenants" tooltip="Back to Tenants" />
<div> <div className="flex flex-col gap-1">
<h1 className="text-2xl font-semibold text-gray-900"> <h1 className="text-2xl font-bold text-[var(--text-primary)]">
{canCreateTenant ? "Add Tenant" : "Tenant Details"} {canCreateTenant ? "Add Tenant" : "Tenant Details"}
</h1> </h1>
<p className="text-sm text-gray-500"> <p className="text-sm text-[var(--text-secondary)]">
{canCreateTenant {canCreateTenant
? "Create a new tenant with domain and subscription plan." ? "Create a new tenant with domain and subscription plan."
: "Your account is assigned to this tenant."} : "Your account is assigned to this tenant."}
@@ -12,7 +12,6 @@ import {
CustomModal, CustomModal,
CustomTable, CustomTable,
CustomStatus, CustomStatus,
CustomLoader,
CustomActionMenu, CustomActionMenu,
CustomActionItem, CustomActionItem,
} from "../../../components/custom"; } from "../../../components/custom";
@@ -643,8 +642,9 @@ const AllTenants = () => {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-4"> <div className="flex flex-wrap items-center justify-between gap-4">
<div> <div className="flex flex-col gap-1">
<h1 className="text-2xl font-semibold text-[var(--text-primary)]">Tenants</h1> <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> </div>
<ProtectedComponent requiredAccess="superadmin.tenant.create"> <ProtectedComponent requiredAccess="superadmin.tenant.create">
<Link to="/tenants/add"> <Link to="/tenants/add">
@@ -653,16 +653,13 @@ const AllTenants = () => {
</ProtectedComponent> </ProtectedComponent>
</div> </div>
{isLoading ? ( {errorMessage ? (
<div className="rounded-lg border border-[var(--card-border)] bg-[var(--card-bg)] p-6">
<CustomLoader />
</div>
) : errorMessage ? (
<div className="rounded-lg border border-red-200 bg-red-50 p-6 text-sm text-red-600"> <div className="rounded-lg border border-red-200 bg-red-50 p-6 text-sm text-red-600">
{errorMessage} {errorMessage}
</div> </div>
) : ( ) : (
<CustomTable <CustomTable
isLoading={isLoading}
data={tenants} data={tenants}
columns={columns} columns={columns}
columnVisibilityEnabled columnVisibilityEnabled
@@ -3,7 +3,7 @@ import { Plus, Edit2, Trash2 } from "lucide-react";
import { CustomButton } from "../../../components/custom"; import { CustomButton } from "../../../components/custom";
import DataTable, { type ColumnDef } from "../../../components/custom/CustomTable"; import DataTable, { type ColumnDef } from "../../../components/custom/CustomTable";
import { CustomActionMenu, CustomActionItem, CustomStatus, CustomConfirmationModal } from "../../../components/custom"; import { CustomActionMenu, CustomActionItem, CustomStatus, CustomConfirmationModal } from "../../../components/custom";
import { Loader } from "../../../components/custom/CustomLoader";
import { paletteApi } from "../PaletteApi"; import { paletteApi } from "../PaletteApi";
import type { ColorPalette, ColorPalettePayload } from "../ThemeTypes"; import type { ColorPalette, ColorPalettePayload } from "../ThemeTypes";
import PaletteForm from "./PaletteForm"; import PaletteForm from "./PaletteForm";
@@ -169,27 +169,24 @@ const AllPalettes: React.FC = () => {
<> <>
<div className="space-y-6"> <div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-4"> <div className="flex flex-wrap items-center justify-between gap-4">
<div> <div className="flex flex-col gap-1">
<h1 className="text-2xl font-semibold text-(--text-primary)">{t('theme:title')}</h1> <h1 className="text-2xl font-bold text-(--text-primary)">{t('theme:title')}</h1>
{/* <p className="text-sm text-(--text-secondary) mt-1"> <p className="text-sm text-(--text-secondary)">
Manage system color themes and default palettes. Manage system color themes and default palettes.
</p> */} </p>
</div> </div>
<CustomButton onClick={handleCreate} leftIcon={<Plus size={18} />}> <CustomButton onClick={handleCreate} leftIcon={<Plus size={18} />}>
{t('theme:actions.create')} {t('theme:actions.create')}
</CustomButton> </CustomButton>
</div> </div>
{isLoading ? ( {errorMessage ? (
<div className="rounded-lg border border-[var(--card-border)] bg-[var(--card-bg)] p-6">
<Loader />
</div>
) : errorMessage ? (
<div className="rounded-lg border border-red-200 bg-red-50 p-6 text-sm text-red-600"> <div className="rounded-lg border border-red-200 bg-red-50 p-6 text-sm text-red-600">
{errorMessage} {errorMessage}
</div> </div>
) : ( ) : (
<DataTable <DataTable
isLoading={isLoading}
data={palettes} data={palettes}
columns={columns} columns={columns}
columnVisibilityEnabled columnVisibilityEnabled
@@ -268,8 +268,8 @@ const AddUsers = () => {
<div className="flex flex-wrap items-center justify-between gap-4"> <div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<CustomBackButton to="/users" tooltip={t('backToUsers')} /> <CustomBackButton to="/users" tooltip={t('backToUsers')} />
<div> <div className="flex flex-col gap-1">
<h1 className="text-2xl font-semibold text-(--text-primary)">{t('add')}</h1> <h1 className="text-2xl font-bold text-(--text-primary)">{t('add')}</h1>
<p className="text-sm text-(--text-secondary)"> <p className="text-sm text-(--text-secondary)">
{t('subTitle')} {t('subTitle')}
</p> </p>
@@ -11,7 +11,7 @@ import {
CustomInput, CustomInput,
CustomModal, CustomModal,
CustomTable, CustomTable,
CustomLoader,
CustomActionMenu, CustomActionMenu,
CustomActionItem, CustomActionItem,
CustomStatus, CustomStatus,
@@ -587,8 +587,9 @@ const AllUsers = () => {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-4"> <div className="flex flex-wrap items-center justify-between gap-4">
<div> <div className="flex flex-col gap-1">
<h1 className="text-2xl font-semibold text-[var(--text-primary)]">{t('title')}</h1> <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> </div>
<ProtectedComponent requiredAccess="admin.user.create"> <ProtectedComponent requiredAccess="admin.user.create">
<Link to="/users/add"> <Link to="/users/add">
@@ -599,16 +600,13 @@ const AllUsers = () => {
{isLoading ? ( {errorMessage ? (
<div className="rounded-lg border border-[var(--card-border)] bg-[var(--card-bg)] p-6">
<CustomLoader />
</div>
) : errorMessage ? (
<div className="rounded-lg border border-red-200 bg-red-50 p-6 text-sm text-red-600"> <div className="rounded-lg border border-red-200 bg-red-50 p-6 text-sm text-red-600">
{errorMessage} {errorMessage}
</div> </div>
) : ( ) : (
<CustomTable <CustomTable
isLoading={isLoading}
data={users} data={users}
columns={columns} columns={columns}
columnVisibilityEnabled columnVisibilityEnabled
+3 -6
View File
@@ -8,6 +8,7 @@ import {
persistTablePageSize, persistTablePageSize,
resolveStoredTablePageSize, resolveStoredTablePageSize,
} from "../../lib/tablePageSize"; } from "../../lib/tablePageSize";
import Loader from "./CustomLoader";
function cn(...parts: Array<string | false | null | undefined>) { function cn(...parts: Array<string | false | null | undefined>) {
return parts.filter(Boolean).join(" "); return parts.filter(Boolean).join(" ");
@@ -701,12 +702,8 @@ export function DataTable<T extends Record<string, unknown>>(
colSpan={visibleColumns.length} colSpan={visibleColumns.length}
className="px-6 py-24 text-center text-[var(--text-secondary)]" className="px-6 py-24 text-center text-[var(--text-secondary)]"
> >
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center min-h-[100px]">
<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"> <Loader />
<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> </div>
</td> </td>
</tr> </tr>
+37 -79
View File
@@ -1,21 +1,23 @@
import { import {
Menu, Menu,
ChevronDown, ChevronDown,
LogOut,
} from "lucide-react"; } from "lucide-react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { useSidebar } from "../../context/SidebarContext"; import { useSidebar } from "../../context/SidebarContext";
import { useAuth } from "../../context/AuthContext"; import { useAuth } from "../../context/AuthContext";
import { useTranslation } from "react-i18next";
import { authApi } from "../../application/authentication/AuthApi"; import { authApi } from "../../application/authentication/AuthApi";
import { clearAuthCookies } from "../../lib/authCookies"; import { clearAuthCookies } from "../../lib/authCookies";
import { useTranslation } from "react-i18next";
const AppHeader: React.FC = () => { const AppHeader: React.FC = () => {
const { t } = useTranslation(['profile', 'common']); const { t } = useTranslation(['profile', 'common']);
const navigate = useNavigate(); const { user } = useAuth();
const { user, } = useAuth();
const { toggleMobileSidebar } = useSidebar(); const { toggleMobileSidebar } = useSidebar();
const navigate = useNavigate();
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false); const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
const userMenuRef = useRef<HTMLDivElement>(null); const userMenuRef = useRef<HTMLDivElement>(null);
@@ -50,12 +52,12 @@ const AppHeader: React.FC = () => {
: "U"; : "U";
return ( 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"> <div className="flex flex-grow items-center gap-4 px-4 py-3 lg:px-6">
{/* LEFT: Mobile Toggle & Brand (Mobile only) */} {/* LEFT: Mobile Toggle & Brand (Mobile only) */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button <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} onClick={toggleMobileSidebar}
aria-label="Toggle Sidebar" aria-label="Toggle Sidebar"
> >
@@ -63,67 +65,12 @@ const AppHeader: React.FC = () => {
</button> </button>
<Link to="/" className="lg:hidden flex items-center gap-2"> <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> </Link>
</div> </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 */} {/* 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 */} {/* User Menu */}
<div className="relative" ref={userMenuRef}> <div className="relative" ref={userMenuRef}>
@@ -132,11 +79,10 @@ const AppHeader: React.FC = () => {
onClick={() => setIsUserMenuOpen((prev) => !prev)} onClick={() => setIsUserMenuOpen((prev) => !prev)}
className=" className="
flex items-center gap-2 flex items-center gap-2
pl-2 pr-1 py-1.5 p-1 pr-3
rounded-lg rounded-full border border-gray-200
hover:bg-(--header-border) hover:border-gray-300 hover:bg-gray-50
transition-colors transition-all bg-white/50
border border-transparent hover:border-(--header-border)
" "
aria-expanded={isUserMenuOpen} aria-expanded={isUserMenuOpen}
> >
@@ -144,12 +90,12 @@ const AppHeader: React.FC = () => {
{initials} {initials}
</div> </div>
<div className="hidden md:block text-left me-1"> <div className="hidden md:block ltr:text-left rtl:text-right ltr:mr-1 rtl:ml-1 leading-tight">
<p className="text-sm font-medium text-(--header-text) leading-none">{fullName}</p> <p className="text-sm font-semibold text-gray-700">{fullName}</p>
</div> </div>
<ChevronDown <ChevronDown
size={16} size={14}
className={`text-gray-400 transition-transform duration-200 ${isUserMenuOpen ? "rotate-180" : "" className={`text-gray-400 transition-transform duration-200 ${isUserMenuOpen ? "rotate-180" : ""
}`} }`}
/> />
@@ -159,33 +105,45 @@ const AppHeader: React.FC = () => {
{isUserMenuOpen && ( {isUserMenuOpen && (
<div <div
className=" className="
absolute end-0 mt-2 w-56 absolute ltr:right-0 rtl:left-0 mt-2 w-64
rounded-xl border border-(--card-border) rounded-xl border border-gray-100
bg-(--card-bg) bg-white
shadow-md shadow-xl
overflow-hidden overflow-hidden
animate-in fade-in zoom-in-95 duration-150 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)"> <div className="px-4 py-3 border-b border-gray-100 bg-gray-50/50">
<p className="text-xs text-(--text-secondary) truncate mt-0.5"> <p className="text-sm font-semibold text-gray-900 truncate">
{user?.email} {user?.email}
</p> </p>
<p className="text-xs text-gray-500 truncate mt-0.5 uppercase tracking-wider">
{user?.role?.role_name || "User"}
</p>
</div> </div>
<div className="py-1"> <div className="py-1">
<Link <Link
to="/profile" to="/profile"
onClick={() => setIsUserMenuOpen(false)} 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')} {t('profile:header.profileSettings')}
</Link> </Link>
<div className="h-px bg-gray-100 my-1 mx-2"></div>
<button <button
onClick={handleLogout} 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')} {t('profile:header.signOut')}
</button> </button>
</div> </div>
+6 -5
View File
@@ -2,15 +2,15 @@ import { SidebarProvider, useSidebar } from "../../context/SidebarContext";
import { Outlet } from "react-router-dom"; import { Outlet } from "react-router-dom";
import AppHeader from "./AppHeader"; import AppHeader from "./AppHeader";
import AppSidebar from "./AppSidebar"; import AppSidebar from "./AppSidebar";
import Backdrop from "./Backdrop";
const LayoutContent: React.FC = () => { const LayoutContent: React.FC = () => {
const { isExpanded } = useSidebar(); const { isExpanded } = useSidebar();
return ( 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 /> <AppSidebar />
<Backdrop />
<div <div
className={` className={`
@@ -20,9 +20,10 @@ const LayoutContent: React.FC = () => {
> >
<AppHeader /> <AppHeader />
<main className="flex-1 p-4 md:p-6 lg:p-6 animate-fade-in w-full max-w-[1920px] mx-auto"> <main className="flex-1 p-4 sm:p-6 lg:p-8 animate-in fade-in duration-500">
{/* Outlet Container - Ensures content is constrained but responsive */} <div className="max-w-[1600px] mx-auto w-full">
<Outlet /> <Outlet />
</div>
</main> </main>
</div> </div>
</div> </div>
+93 -72
View File
@@ -15,6 +15,7 @@ import {
ChevronDown, ChevronDown,
ChevronRight, ChevronRight,
CreditCard, CreditCard,
FileText,
} from "lucide-react"; } from "lucide-react";
import { useSidebar } from "../../context/SidebarContext"; import { useSidebar } from "../../context/SidebarContext";
import { usePermission } from "../../lib/usePermission"; import { usePermission } from "../../lib/usePermission";
@@ -82,7 +83,7 @@ const navItems: NavItem[] = [
access: "modules.view", access: "modules.view",
}, },
{ {
icon: <LayoutGrid size={22} />, icon: <FileText size={22} />,
name: "Logs", name: "Logs",
path: "/logs", path: "/logs",
access: "admin.logs.read", access: "admin.logs.read",
@@ -124,6 +125,7 @@ const AppSidebar: React.FC = () => {
'Settings': 'settings', 'Settings': 'settings',
'Modules': 'modules', 'Modules': 'modules',
'Logs': 'logs', 'Logs': 'logs',
'Subscriptions': 'subscriptions',
}; };
const key = keyMap[name]; const key = keyMap[name];
@@ -131,7 +133,6 @@ const AppSidebar: React.FC = () => {
return t(`nav.${key}`, name); return t(`nav.${key}`, name);
} }
// Try other namespaces if not in nav map
return t([ return t([
`modules:${name}`, `modules:${name}`,
`dashboard:${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( const visibleNavItems = useMemo(
() => navItems.filter((item) => { () => navItems.filter((item) => {
if (!item.access) return true; if (!item.access) return true;
@@ -169,11 +154,55 @@ const AppSidebar: React.FC = () => {
[can] [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 [openSubmenus, setOpenSubmenus] = useState<Record<string, boolean>>({});
const toggleSubmenu = (name: string) => { const toggleSubmenu = (name: string) => {
if (!isExpanded && !isMobile) { if (!isExpanded && !isMobile) {
toggleSidebar(); // Auto expand when clicking submenu in collapsed state toggleSidebar();
} }
setOpenSubmenus(prev => ({ ...prev, [name]: !prev[name] })); setOpenSubmenus(prev => ({ ...prev, [name]: !prev[name] }));
}; };
@@ -192,21 +221,11 @@ const AppSidebar: React.FC = () => {
return null; return null;
} }
// Sidebar width logic
const sidebarWidth = isExpanded ? "w-[260px]" : "w-[80px]"; const sidebarWidth = isExpanded ? "w-[260px]" : "w-[80px]";
const mobileClasses = isMobileOpen ? "translate-x-0" : "-translate-x-full"; const mobileClasses = isMobileOpen ? "translate-x-0" : "-translate-x-full";
return ( 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 <aside
className={` className={`
fixed top-0 left-0 z-50 h-screen bg-[var(--sidebar-bg)] text-[var(--sidebar-text)] flex flex-col 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 */} {/* Header */}
<div className={`h-[70px] flex items-center px-4 border-b border-(--sidebar-border)/50 ${!isExpanded && !isMobile ? 'justify-center' : 'justify-between'}`}> <div className={`h-[70px] flex items-center px-4 border-b border-white/5 ${!isExpanded && !isMobile ? 'justify-center' : 'justify-between'}`}>
{/* Logo - Only show when expanded or mobile */}
{(isExpanded || isMobile) && ( {(isExpanded || isMobile) && (
<Link to="/dashboard" className="flex items-center gap-3 overflow-hidden ml-1" onClick={isMobile ? closeMobileSidebar : undefined}> <Link to="/dashboard" className="flex items-center gap-3 overflow-hidden ml-1" onClick={isMobile ? closeMobileSidebar : undefined}>
{user?.role?.accesses?.includes("superadmin.tenant.read") ? ( {user?.role?.accesses?.includes("superadmin.tenant.read") ? (
@@ -235,44 +253,50 @@ const AppSidebar: React.FC = () => {
className="h-8 w-auto object-contain" className="h-8 w-auto object-contain"
/> />
) : ( ) : (
<> <div className="flex items-center gap-2">
<div className="shrink-0 text-blue-400"> <div className="shrink-0 w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center text-white font-bold">S</div>
<Package size={32} /> <span className="text-xl font-bold text-white whitespace-nowrap">SaaS</span>
</div> </div>
<span className="text-xl font-bold text-(--sidebar-active-text) whitespace-nowrap">
SaaS
</span>
</>
)} )}
</Link> </Link>
)} )}
{/* Desktop Toggle Button */}
{!isMobile && ( {!isMobile && (
<button <button
onClick={toggleSidebar} 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" aria-label="Toggle Sidebar"
> >
<PanelLeft size={20} className={!isExpanded ? "rotate-180" : ""} /> <PanelLeft size={20} className={!isExpanded ? "rotate-180" : ""} />
</button> </button>
)} )}
{/* Mobile Close Button */}
{isMobile && ( {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} /> <X size={20} />
</button> </button>
)} )}
</div> </div>
{/* Navigation */} {/* Navigation */}
<nav className="flex-1 overflow-y-auto overflow-x-hidden py-6 px-3 custom-scrollbar"> <nav className="flex-1 overflow-y-auto overflow-x-hidden py-4 px-3 custom-scrollbar">
<ul className="space-y-2"> <div className="flex flex-col gap-6">
{visibleNavItems.map((nav) => { {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 active = isActive(nav.path);
const hasSubmenu = nav.submenu && nav.submenu.length > 0; const hasSubmenu = nav.submenu && nav.submenu.length > 0;
const isMenuOpen = openSubmenus[nav.name] || isSubmenuRouteActive(nav.submenu); const isMenuOpen = openSubmenus[nav.name];
if (hasSubmenu) { if (hasSubmenu) {
return ( return (
@@ -280,10 +304,10 @@ const AppSidebar: React.FC = () => {
<button <button
onClick={() => toggleSubmenu(nav.name)} onClick={() => toggleSubmenu(nav.name)}
className={` 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 ${isMenuOpen
? "bg-white/5 text-(--sidebar-active-text)" ? "bg-white/5 text-white"
: "text-(--sidebar-text) hover:bg-(--sidebar-hover-bg)" : "text-slate-400 hover:bg-white/5 hover:text-white"
} }
`} `}
> >
@@ -293,24 +317,18 @@ const AppSidebar: React.FC = () => {
{(isExpanded || isMobile) && ( {(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)} {getNavTranslation(nav.name)}
</span> </span>
<span className="flex-shrink-0 transition-transform duration-200"> <span className="flex-shrink-0 transition-transform duration-200">
{isMenuOpen ? <ChevronDown size={16} /> : <ChevronRight size={16} />} {isMenuOpen ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</span> </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> </button>
{/* Submenu Items */} <div className={`overflow-hidden transition-all duration-300 ${isMenuOpen && (isExpanded || isMobile) ? "max-h-[800px] opacity-100 mt-1" : "max-h-0 opacity-0"}`}>
<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-4 mt-1 border-l border-white/5 ml-4">
<ul className="space-y-1 pl-9">
{nav.submenu?.filter(subItem => !subItem.access || can(subItem.access)).map((subItem) => { {nav.submenu?.filter(subItem => !subItem.access || can(subItem.access)).map((subItem) => {
const subActive = isActive(subItem.path || ""); const subActive = isActive(subItem.path || "");
return ( return (
@@ -319,15 +337,15 @@ const AppSidebar: React.FC = () => {
to={subItem.path || "#"} to={subItem.path || "#"}
onClick={isMobile ? closeMobileSidebar : undefined} onClick={isMobile ? closeMobileSidebar : undefined}
className={` 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 ${subActive
? "bg-(--sidebar-active-bg) text-(--sidebar-active-text) font-medium" ? "text-white font-bold"
: "text-(--sidebar-text-muted) hover:text-(--sidebar-text) hover:bg-(--sidebar-hover-bg)" : "text-slate-500 hover:text-slate-300 hover:bg-white/5"
} }
`} `}
> >
{subItem.icon && ( {subItem.icon && (
<div className="flex-shrink-0 scale-90"> <div className="flex-shrink-0 scale-75">
{subItem.icon} {subItem.icon}
</div> </div>
)} )}
@@ -348,10 +366,10 @@ const AppSidebar: React.FC = () => {
to={nav.path} to={nav.path}
onClick={isMobile ? closeMobileSidebar : undefined} onClick={isMobile ? closeMobileSidebar : undefined}
className={` 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 ${active
? "bg-(--sidebar-active-bg) text-(--sidebar-active-text)" ? "bg-white text-slate-900"
: "text-(--sidebar-text) hover:bg-(--sidebar-hover-bg)" : "text-slate-400 hover:bg-white/5 hover:text-white"
} }
`} `}
> >
@@ -361,14 +379,13 @@ const AppSidebar: React.FC = () => {
<span <span
className={` 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"} ${(isExpanded || isMobile) ? "opacity-100 w-auto" : "opacity-0 w-0 hidden"}
`} `}
> >
{getNavTranslation(nav.name)} {getNavTranslation(nav.name)}
</span> </span>
{/* Tooltip for collapsed mode */}
{!isExpanded && !isMobile && ( {!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"> <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)} {getNavTranslation(nav.name)}
@@ -379,19 +396,23 @@ const AppSidebar: React.FC = () => {
); );
})} })}
</ul> </ul>
</div>
))}
</div>
</nav> </nav>
{/* Footer */} {/* Footer */}
<div className="p-4 border-t border-slate-700/50"> <div className="p-4 border-t border-white/5">
{/* Logout */}
<button <button
onClick={handleLogout} 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" title="Logout"
> >
<div className="flex-shrink-0 transition-colors group-hover:text-white">
<LogOut size={20} /> <LogOut size={20} />
</div>
{(isExpanded || isMobile) && ( {(isExpanded || isMobile) && (
<span className="font-medium">{t('actions.signout')}</span> <span className="font-medium text-sm">Sign Out</span>
)} )}
</button> </button>
</div> </div>
+1 -1
View File
@@ -7,7 +7,7 @@ const Backdrop: React.FC = () => {
return ( return (
<div <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} 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"; @import "tailwindcss";
@keyframes fadeIn { @keyframes fadeIn {
@@ -40,16 +40,26 @@
--text-secondary: #64748B; --text-secondary: #64748B;
--card-bg: #FFFFFF; --card-bg: #FFFFFF;
--card-border: #E2E8F0; --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, html,
body { body {
font-family: "Outfit", sans-serif; font-family: "Archivo", sans-serif;
@apply text-base antialiased; @apply text-base antialiased;
} }
} }
@layer utilities { @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 */ /* Hide number input arrows/spinners */
.no-spinner::-webkit-inner-spin-button, .no-spinner::-webkit-inner-spin-button,
.no-spinner::-webkit-outer-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`); console.log(`\n 🔌 Backend API URL: \x1b[36m${env.VITE_API_BASE_URL}\x1b[0m\n`);
return { return {
server: {
port: 5174,
},
plugins: [react(), tailwindcss()], plugins: [react(), tailwindcss()],
} }
}) })