Merge pull request 'furqan' (#15) from furqan into dev
Reviewed-on: https://gitea.maskantech.in/gitea_admin/saas_frontend/pulls/15
This commit is contained in:
-3
@@ -21,7 +21,6 @@ export default function SignUpForm() {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
if (!isChecked) {
|
if (!isChecked) {
|
||||||
// Require terms acceptance before sending the signup request.
|
|
||||||
setErrorMessage("Please accept the terms and conditions to continue.");
|
setErrorMessage("Please accept the terms and conditions to continue.");
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
@@ -35,10 +34,8 @@ export default function SignUpForm() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Call the backend signup API.
|
|
||||||
await authApi.signup(payload);
|
await authApi.signup(payload);
|
||||||
|
|
||||||
// Signup does not return tokens, so take the user to signin.
|
|
||||||
navigate("/signin");
|
navigate("/signin");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message =
|
const message =
|
||||||
@@ -3,9 +3,11 @@ import axios from 'axios';
|
|||||||
import { ExternalLink, Box, Search, Rocket } from 'lucide-react';
|
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';
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const { t } = useTranslation(['dashboard', 'common']);
|
||||||
|
|
||||||
const [modules, setModules] = useState<Module[]>([]);
|
const [modules, setModules] = useState<Module[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -54,7 +56,7 @@ const Dashboard = () => {
|
|||||||
if (filtered.length === 0) return {};
|
if (filtered.length === 0) return {};
|
||||||
|
|
||||||
filtered.forEach(module => {
|
filtered.forEach(module => {
|
||||||
const category = module.category || 'All Modules';
|
const category = module.category || t('dashboard:allModules');
|
||||||
if (!groups[category]) {
|
if (!groups[category]) {
|
||||||
groups[category] = [];
|
groups[category] = [];
|
||||||
}
|
}
|
||||||
@@ -68,7 +70,7 @@ const Dashboard = () => {
|
|||||||
},
|
},
|
||||||
{} as Record<string, Module[]>
|
{} as Record<string, Module[]>
|
||||||
);
|
);
|
||||||
}, [modules, searchTerm]);
|
}, [modules, searchTerm, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 space-y-10 min-h-screen bg-gray-50/50">
|
<div className="p-8 space-y-10 min-h-screen bg-gray-50/50">
|
||||||
@@ -76,10 +78,10 @@ const Dashboard = () => {
|
|||||||
<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>
|
||||||
<h1 className="text-4xl font-extrabold text-(--text-primary) tracking-tight mb-2">
|
<h1 className="text-4xl font-extrabold text-(--text-primary) tracking-tight mb-2">
|
||||||
Welcome back, {user?.first_name || 'Admin'}! 👋
|
{t('dashboard:title')}, {user?.first_name || 'Admin'}! 👋
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-lg text-(--text-secondary)">
|
<p className="text-lg text-(--text-secondary)">
|
||||||
Manage your SaaS ecosystem from one central hub.
|
{t('dashboard:subtitle')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -90,7 +92,7 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search available modules..."
|
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"
|
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"
|
||||||
@@ -160,7 +162,7 @@ const Dashboard = () => {
|
|||||||
className="w-full flex items-center justify-center gap-2 py-2.5 px-4 bg-gray-50 hover:bg-blue-600 text-gray-700 hover:text-white rounded-xl font-medium transition-all duration-200 group-hover:shadow-md"
|
className="w-full flex items-center justify-center gap-2 py-2.5 px-4 bg-gray-50 hover:bg-blue-600 text-gray-700 hover:text-white rounded-xl font-medium transition-all duration-200 group-hover:shadow-md"
|
||||||
>
|
>
|
||||||
<Rocket size={18} className="transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5" />
|
<Rocket size={18} className="transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5" />
|
||||||
Launch Module
|
{t('dashboard:actions.launch')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -173,16 +175,16 @@ const Dashboard = () => {
|
|||||||
<div className="w-16 h-16 bg-gray-50 rounded-full flex items-center justify-center mb-4">
|
<div className="w-16 h-16 bg-gray-50 rounded-full flex items-center justify-center mb-4">
|
||||||
<Box className="w-8 h-8 text-gray-400" />
|
<Box className="w-8 h-8 text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-1">No modules found</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-1">{t('common:common.noData')}</h3>
|
||||||
<p className="text-gray-500 max-w-sm mx-auto">
|
<p className="text-gray-500 max-w-sm mx-auto">
|
||||||
{searchTerm ? `No results found for "${searchTerm}". Try a different keyword.` : "Your dashboard is currently empty. Contact your administrator to assign modules."}
|
{searchTerm ? t('common:common.noData') : "Your dashboard is currently empty. Contact your administrator to assign modules."}
|
||||||
</p>
|
</p>
|
||||||
{searchTerm && (
|
{searchTerm && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setSearchTerm('')}
|
onClick={() => setSearchTerm('')}
|
||||||
className="mt-4 text-blue-600 font-medium hover:underline"
|
className="mt-4 text-blue-600 font-medium hover:underline"
|
||||||
>
|
>
|
||||||
Clear Search
|
{t('common:actions.reset')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import CustomButton from '../../../../components/custom/CustomButton';
|
|||||||
|
|
||||||
const ModuleList = () => {
|
const ModuleList = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation('modules');
|
const { t } = useTranslation(['modules', 'common']);
|
||||||
const { listModules, deleteModule, loading } = useModuleApi();
|
const { listModules, deleteModule, loading } = useModuleApi();
|
||||||
const [modules, setModules] = useState<Module[]>([]);
|
const [modules, setModules] = useState<Module[]>([]);
|
||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
@@ -125,14 +125,14 @@ const ModuleList = () => {
|
|||||||
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors text-sm text-(--text-primary)"
|
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors text-sm text-(--text-primary)"
|
||||||
>
|
>
|
||||||
<Settings size={16} />
|
<Settings size={16} />
|
||||||
Environments
|
{t('registry.card.environments')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate(`/admin/modules/${module.id}/permissions`)}
|
onClick={() => navigate(`/admin/modules/${module.id}/permissions`)}
|
||||||
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors text-sm text-(--text-primary)"
|
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors text-sm text-(--text-primary)"
|
||||||
>
|
>
|
||||||
<Shield size={16} />
|
<Shield size={16} />
|
||||||
Permissions
|
{t('registry.card.permissions')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -142,14 +142,14 @@ const ModuleList = () => {
|
|||||||
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-blue-50 hover:bg-blue-100 text-blue-600 rounded-lg transition-colors text-sm"
|
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-blue-50 hover:bg-blue-100 text-blue-600 rounded-lg transition-colors text-sm"
|
||||||
>
|
>
|
||||||
<Edit size={16} />
|
<Edit size={16} />
|
||||||
Edit
|
{t('common:actions.edit')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => confirmDelete(module)}
|
onClick={() => confirmDelete(module)}
|
||||||
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-red-50 hover:bg-red-100 text-red-600 rounded-lg transition-colors text-sm"
|
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-red-50 hover:bg-red-100 text-red-600 rounded-lg transition-colors text-sm"
|
||||||
>
|
>
|
||||||
<Trash2 size={16} />
|
<Trash2 size={16} />
|
||||||
Delete
|
{t('common:actions.delete')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import { paletteApi } from "../PaletteApi";
|
|||||||
import type { ColorPalette } from "../ThemeTypes";
|
import type { ColorPalette } from "../ThemeTypes";
|
||||||
import PaletteForm from "./PaletteForm";
|
import PaletteForm from "./PaletteForm";
|
||||||
import { useTheme } from "../../../context/ThemeContext";
|
import { useTheme } from "../../../context/ThemeContext";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const AllPalettes: React.FC = () => {
|
const AllPalettes: React.FC = () => {
|
||||||
|
const { t } = useTranslation(['theme', 'common']);
|
||||||
const [palettes, setPalettes] = useState<ColorPalette[]>([]);
|
const [palettes, setPalettes] = useState<ColorPalette[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
@@ -25,7 +27,7 @@ const AllPalettes: React.FC = () => {
|
|||||||
const data = await paletteApi.getAllPalettes();
|
const data = await paletteApi.getAllPalettes();
|
||||||
setPalettes(data);
|
setPalettes(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Failed to fetch palettes";
|
const message = error instanceof Error ? error.message : t('theme:errors.fetchFailed');
|
||||||
setErrorMessage(message);
|
setErrorMessage(message);
|
||||||
console.error("Failed to fetch palettes", error);
|
console.error("Failed to fetch palettes", error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -48,10 +50,10 @@ const AllPalettes: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const handleDelete = async (id: string) => {
|
||||||
if (!window.confirm("Are you sure you want to delete this palette?")) return;
|
if (!window.confirm(t('theme:confirmDelete'))) return;
|
||||||
try {
|
try {
|
||||||
await paletteApi.deletePalette(id);
|
await paletteApi.deletePalette(id);
|
||||||
toast.success("Palette deleted successfully");
|
toast.success(t('theme:success.deleted'));
|
||||||
fetchPalettes();
|
fetchPalettes();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// toast handled by api client
|
// toast handled by api client
|
||||||
@@ -63,10 +65,10 @@ const AllPalettes: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
if (editingPalette) {
|
if (editingPalette) {
|
||||||
await paletteApi.updatePalette(editingPalette.id, data);
|
await paletteApi.updatePalette(editingPalette.id, data);
|
||||||
toast.success("Palette updated successfully");
|
toast.success(t('theme:success.updated'));
|
||||||
} else {
|
} else {
|
||||||
await paletteApi.createPalette(data);
|
await paletteApi.createPalette(data);
|
||||||
toast.success("Palette created successfully");
|
toast.success(t('theme:success.created'));
|
||||||
}
|
}
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
fetchPalettes();
|
fetchPalettes();
|
||||||
@@ -81,7 +83,7 @@ const AllPalettes: React.FC = () => {
|
|||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
key: "name",
|
key: "name",
|
||||||
header: "Name",
|
header: t('theme:table.name'),
|
||||||
searchable: true,
|
searchable: true,
|
||||||
render: (row) => (
|
render: (row) => (
|
||||||
<div>
|
<div>
|
||||||
@@ -94,43 +96,43 @@ const AllPalettes: React.FC = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "is_default",
|
key: "is_default",
|
||||||
header: "Default",
|
header: t('theme:table.default'),
|
||||||
render: (row) => (
|
render: (row) => (
|
||||||
row.is_default ? (
|
row.is_default ? (
|
||||||
<CustomStatus status="Default" variant="success" />
|
<CustomStatus status={t('common:status.default')} variant="success" />
|
||||||
) : null
|
) : null
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "colors",
|
key: "colors",
|
||||||
header: "Preview",
|
header: t('theme:table.preview'),
|
||||||
render: (row) => (
|
render: (row) => (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<div
|
<div
|
||||||
className="w-6 h-6 rounded-full border border-(--card-border) shadow-sm"
|
className="w-6 h-6 rounded-full border border-(--card-border) shadow-sm"
|
||||||
style={{ backgroundColor: row.colors.primary }}
|
style={{ backgroundColor: row.colors.primary }}
|
||||||
title="Primary"
|
title={t('theme:colors.primary')}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="w-6 h-6 rounded-full border border-(--card-border) shadow-sm"
|
className="w-6 h-6 rounded-full border border-(--card-border) shadow-sm"
|
||||||
style={{ backgroundColor: row.colors.sidebar_bg }}
|
style={{ backgroundColor: row.colors.sidebar_bg }}
|
||||||
title="Sidebar"
|
title={t('theme:colors.sidebar')}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="w-6 h-6 rounded-full border border-(--card-border) shadow-sm"
|
className="w-6 h-6 rounded-full border border-(--card-border) shadow-sm"
|
||||||
style={{ backgroundColor: row.colors.header_bg }}
|
style={{ backgroundColor: row.colors.header_bg }}
|
||||||
title="Header"
|
title={t('theme:colors.header')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "actions",
|
key: "actions",
|
||||||
header: "Actions",
|
header: t('common:columns.actions'),
|
||||||
render: (row) => (
|
render: (row) => (
|
||||||
<CustomActionMenu>
|
<CustomActionMenu>
|
||||||
<CustomActionItem onClick={() => handleEdit(row)}>
|
<CustomActionItem onClick={() => handleEdit(row)}>
|
||||||
<Edit2 size={16} className="mr-2" /> Edit Palette
|
<Edit2 size={16} className="mr-2" /> {t('theme:actions.edit')}
|
||||||
</CustomActionItem>
|
</CustomActionItem>
|
||||||
|
|
||||||
<CustomActionItem
|
<CustomActionItem
|
||||||
@@ -146,7 +148,7 @@ const AllPalettes: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 size={16} className="mr-2" /> Delete Palette
|
<Trash2 size={16} className="mr-2" /> {t('theme:actions.delete')}
|
||||||
</div>
|
</div>
|
||||||
</CustomActionItem>
|
</CustomActionItem>
|
||||||
</CustomActionMenu>
|
</CustomActionMenu>
|
||||||
@@ -160,13 +162,13 @@ 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>
|
||||||
<h1 className="text-2xl font-semibold text-(--text-primary)">Theme Palettes</h1>
|
<h1 className="text-2xl font-semibold text-(--text-primary)">{t('theme:title')}</h1>
|
||||||
{/* <p className="text-sm text-(--text-secondary) mt-1">
|
{/* <p className="text-sm text-(--text-secondary) mt-1">
|
||||||
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} />}>
|
||||||
Create Palette
|
{t('theme:actions.create')}
|
||||||
</CustomButton>
|
</CustomButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { Check } from "lucide-react";
|
import { Check } from "lucide-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import CustomModal from "../../../components/custom/CustomModal";
|
import CustomModal from "../../../components/custom/CustomModal";
|
||||||
import CustomInput from "../../../components/custom/CustomInput";
|
import CustomInput from "../../../components/custom/CustomInput";
|
||||||
import {CustomButton} from "../../../components/custom";
|
import {CustomButton} from "../../../components/custom";
|
||||||
@@ -15,27 +16,27 @@ interface PaletteFormProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const colorFields = [
|
const colorFields = [
|
||||||
{ label: "Primary Color", key: "primary" },
|
{ key: "primary" },
|
||||||
{ label: "Primary Hover", key: "primary_hover" },
|
{ key: "primary_hover" },
|
||||||
{ label: "Sidebar Background", key: "sidebar_bg" },
|
{ key: "sidebar_bg" },
|
||||||
{ label: "Sidebar Text", key: "sidebar_text" },
|
{ key: "sidebar_text" },
|
||||||
{ label: "Sidebar Text Muted", key: "sidebar_text_muted" },
|
{ key: "sidebar_text_muted" },
|
||||||
{ label: "Sidebar Active BG", key: "sidebar_active_bg" },
|
{ key: "sidebar_active_bg" },
|
||||||
{ label: "Sidebar Active Text", key: "sidebar_active_text" },
|
{ key: "sidebar_active_text" },
|
||||||
{ label: "Sidebar Hover BG", key: "sidebar_hover_bg" },
|
{ key: "sidebar_hover_bg" },
|
||||||
{ label: "Sidebar Border", key: "sidebar_border" },
|
{ key: "sidebar_border" },
|
||||||
{ label: "Header Background", key: "header_bg" },
|
{ key: "header_bg" },
|
||||||
{ label: "Header Text", key: "header_text" },
|
{ key: "header_text" },
|
||||||
{ label: "Header Border", key: "header_border" },
|
{ key: "header_border" },
|
||||||
{ label: "App Background", key: "background" },
|
{ key: "background" },
|
||||||
{ label: "App Secondary BG", key: "background_secondary" },
|
{ key: "background_secondary" },
|
||||||
{ label: "Text Primary", key: "text_primary" },
|
{ key: "text_primary" },
|
||||||
{ label: "Text Secondary", key: "text_secondary" },
|
{ key: "text_secondary" },
|
||||||
{ label: "Card Background", key: "card_bg" },
|
{ key: "card_bg" },
|
||||||
{ label: "Card Border", key: "card_border" },
|
{ key: "card_border" },
|
||||||
{ label: "Table Header BG", key: "table_header_bg" },
|
{ key: "table_header_bg" },
|
||||||
{ label: "Table Row Hover", key: "table_row_hover" },
|
{ key: "table_row_hover" },
|
||||||
{ label: "Table Border", key: "table_border" },
|
{ key: "table_border" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const emptyColors = colorFields.reduce((acc, field) => {
|
const emptyColors = colorFields.reduce((acc, field) => {
|
||||||
@@ -50,6 +51,7 @@ const PaletteForm: React.FC<PaletteFormProps> = ({
|
|||||||
isLoading,
|
isLoading,
|
||||||
isOpen,
|
isOpen,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation(['theme', 'common']);
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -96,7 +98,7 @@ const PaletteForm: React.FC<PaletteFormProps> = ({
|
|||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
Cancel
|
{t('theme:actions.cancel')}
|
||||||
</CustomButton>
|
</CustomButton>
|
||||||
<CustomButton
|
<CustomButton
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -105,7 +107,7 @@ const PaletteForm: React.FC<PaletteFormProps> = ({
|
|||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
leftIcon={<Check size={16} />}
|
leftIcon={<Check size={16} />}
|
||||||
>
|
>
|
||||||
{initialData ? "Update Palette" : "Create Palette"}
|
{initialData ? t('theme:actions.update') : t('theme:actions.create')}
|
||||||
</CustomButton>
|
</CustomButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -114,7 +116,7 @@ const PaletteForm: React.FC<PaletteFormProps> = ({
|
|||||||
<CustomModal
|
<CustomModal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={onCancel}
|
onClose={onCancel}
|
||||||
title={initialData ? "Edit Palette" : "Create New Palette"}
|
title={initialData ? t('theme:form.title.edit') : t('theme:form.title.create')}
|
||||||
size="lg"
|
size="lg"
|
||||||
footer={footer}
|
footer={footer}
|
||||||
>
|
>
|
||||||
@@ -126,15 +128,15 @@ const PaletteForm: React.FC<PaletteFormProps> = ({
|
|||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<CustomInput
|
<CustomInput
|
||||||
label="Palette Name"
|
label={t('theme:form.fields.name.label')}
|
||||||
{...register("name", { required: "Name is required" })}
|
{...register("name", { required: t('theme:form.fields.name.required') })}
|
||||||
error={errors.name?.message as string}
|
error={errors.name?.message as string}
|
||||||
placeholder="e.g. Dark Mode"
|
placeholder={t('theme:form.fields.name.placeholder')}
|
||||||
/>
|
/>
|
||||||
<CustomInput
|
<CustomInput
|
||||||
label="Description"
|
label={t('theme:form.fields.description.label')}
|
||||||
{...register("description")}
|
{...register("description")}
|
||||||
placeholder="Optional description"
|
placeholder={t('theme:form.fields.description.placeholder')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -146,13 +148,13 @@ const PaletteForm: React.FC<PaletteFormProps> = ({
|
|||||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="is_default" className="text-sm font-medium text-gray-700">
|
<label htmlFor="is_default" className="text-sm font-medium text-gray-700">
|
||||||
Set as Default Palette
|
{t('theme:form.fields.isDefault')}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-gray-200 pt-4">
|
<div className="border-t border-gray-200 pt-4">
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-4">
|
<h3 className="text-lg font-medium text-gray-900 mb-4">
|
||||||
Color Configuration
|
{t('theme:form.fields.sectionTitle')}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="max-h-[60vh] overflow-y-auto pr-2 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="max-h-[60vh] overflow-y-auto pr-2 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
@@ -175,9 +177,9 @@ const PaletteForm: React.FC<PaletteFormProps> = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<CustomInput
|
<CustomInput
|
||||||
label={field.label}
|
label={t(`theme:colors.${field.key}`)}
|
||||||
{...register(`colors.${field.key}` as any, {
|
{...register(`colors.${field.key}` as any, {
|
||||||
required: "Required",
|
required: t('theme:form.fields.required'),
|
||||||
})}
|
})}
|
||||||
placeholder="#000000"
|
placeholder="#000000"
|
||||||
containerClassName="flex-1 mb-0"
|
containerClassName="flex-1 mb-0"
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ const AppHeader: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* RIGHT: Actions */}
|
{/* RIGHT: Actions */}
|
||||||
<div className="flex items-center gap-2 sm:gap-4 ml-auto">
|
<div className="flex items-center gap-2 sm:gap-4 ms-auto">
|
||||||
|
|
||||||
{/* User Menu */}
|
{/* User Menu */}
|
||||||
<div className="relative" ref={userMenuRef}>
|
<div className="relative" ref={userMenuRef}>
|
||||||
@@ -144,7 +144,7 @@ const AppHeader: React.FC = () => {
|
|||||||
{initials}
|
{initials}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden md:block text-left mr-1">
|
<div className="hidden md:block text-left me-1">
|
||||||
<p className="text-sm font-medium text-(--header-text) leading-none">{fullName}</p>
|
<p className="text-sm font-medium text-(--header-text) leading-none">{fullName}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ const AppHeader: React.FC = () => {
|
|||||||
{isUserMenuOpen && (
|
{isUserMenuOpen && (
|
||||||
<div
|
<div
|
||||||
className="
|
className="
|
||||||
absolute right-0 mt-2 w-56
|
absolute end-0 mt-2 w-56
|
||||||
rounded-xl border border-(--card-border)
|
rounded-xl border border-(--card-border)
|
||||||
bg-(--card-bg)
|
bg-(--card-bg)
|
||||||
shadow-md
|
shadow-md
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ const navItems: NavItem[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const AppSidebar: React.FC = () => {
|
const AppSidebar: React.FC = () => {
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation(['common', 'modules', 'dashboard']);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const {
|
const {
|
||||||
isExpanded,
|
isExpanded,
|
||||||
@@ -114,12 +114,17 @@ const AppSidebar: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const key = keyMap[name];
|
const key = keyMap[name];
|
||||||
if (!key) {
|
if (key) {
|
||||||
console.warn(`Missing translation key for: ${name}`);
|
return t(`nav.${key}`, name);
|
||||||
return name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return t(`nav.${key}`, name);
|
// Try other namespaces if not in nav map
|
||||||
|
return t([
|
||||||
|
`modules:${name}`,
|
||||||
|
`dashboard:${name}`,
|
||||||
|
`common:nav.${name}`,
|
||||||
|
name
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import arDashboard from './locales/ar/dashboard.json';
|
|||||||
import enModules from './locales/en/modules.json';
|
import enModules from './locales/en/modules.json';
|
||||||
import arModules from './locales/ar/modules.json';
|
import arModules from './locales/ar/modules.json';
|
||||||
|
|
||||||
|
import enTheme from './locales/en/theme.json';
|
||||||
|
import arTheme from './locales/ar/theme.json';
|
||||||
|
|
||||||
export const languages = {
|
export const languages = {
|
||||||
en: { name: 'English', dir: 'ltr' },
|
en: { name: 'English', dir: 'ltr' },
|
||||||
ar: { name: 'العربية', dir: 'rtl' },
|
ar: { name: 'العربية', dir: 'rtl' },
|
||||||
@@ -30,6 +33,7 @@ const resources = {
|
|||||||
profile: enProfile,
|
profile: enProfile,
|
||||||
dashboard: enDashboard,
|
dashboard: enDashboard,
|
||||||
modules: enModules,
|
modules: enModules,
|
||||||
|
theme: enTheme,
|
||||||
},
|
},
|
||||||
ar: {
|
ar: {
|
||||||
common: arCommon,
|
common: arCommon,
|
||||||
@@ -38,6 +42,7 @@ const resources = {
|
|||||||
profile: arProfile,
|
profile: arProfile,
|
||||||
dashboard: arDashboard,
|
dashboard: arDashboard,
|
||||||
modules: arModules,
|
modules: arModules,
|
||||||
|
theme: arTheme,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
"profile": "الملف الشخصي",
|
"profile": "الملف الشخصي",
|
||||||
"settings": "الإعدادات",
|
"settings": "الإعدادات",
|
||||||
"configuration": "التكوين",
|
"configuration": "التكوين",
|
||||||
"dropdowns": "القوائم المنسدلة"
|
"dropdowns": "القوائم المنسدلة",
|
||||||
|
"modules": "الوحدات"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"save": "حفظ",
|
"save": "حفظ",
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
{
|
{
|
||||||
"title": "لوحة المعلومات قيد التطوير",
|
"title": "مرحباً بعودتك",
|
||||||
"message": "نحن نعمل على إنشاء تجربة لوحة معلومات قوية لمنحك رؤى أفضل. يرجى التحقق مرة أخرى قريبا!"
|
"subtitle": "أدِر نظام SaaS الخاص بك من مركز واحد.",
|
||||||
|
"allModules": "جميع الوحدات",
|
||||||
|
"searchPlaceholder": "البحث في الوحدات المتاحة...",
|
||||||
|
"actions": {
|
||||||
|
"launch": "تشغيل الوحدة"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
{
|
{
|
||||||
"registry": {
|
"registry": {
|
||||||
"title": "Module Registry",
|
"title": "سجل الوحدات",
|
||||||
"subtitle": "Manage platform modules and their configurations",
|
"subtitle": "إدارة وحدات المنصة وتكويناتها",
|
||||||
"create_button": "Create Module",
|
"create_button": "إنشاء وحدة",
|
||||||
"empty_state": {
|
"empty_state": {
|
||||||
"title": "No Modules Yet",
|
"title": "لا توجد وحدات بعد",
|
||||||
"subtitle": "Create your first module to get started"
|
"subtitle": "قم بإنشاء وحدتك الأولى للبدء"
|
||||||
|
},
|
||||||
|
"card": {
|
||||||
|
"environments": "البيئات",
|
||||||
|
"permissions": "الأذونات"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
@@ -55,9 +59,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"delete_modal": {
|
"delete_modal": {
|
||||||
"title": "Delete Module",
|
"title": "حذف الوحدة",
|
||||||
"description": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone and will remove all associated environments and tenant assignments.",
|
"description": "هل أنت متأكد أنك تريد حذف \"{{name}}\"؟ لا يمكن التراجع عن هذا الإجراء وسيتم إزالة جميع البيئات وتعيينات المستأجرين المرتبطة.",
|
||||||
"confirm": "Delete Module",
|
"confirm": "حذف الوحدة",
|
||||||
"cancel": "Cancel"
|
"cancel": "إلغاء"
|
||||||
}
|
},
|
||||||
|
"Inventory": "المخزون",
|
||||||
|
"Fulfillment & Logistics": "التنفيذ والخدمات اللوجستية",
|
||||||
|
"User Management": "إدارة المستخدمين",
|
||||||
|
"Tenant Management": "إدارة المستأجرين",
|
||||||
|
"Role Management": "إدارة الأدوار"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"title": "أنظمة الألوان",
|
||||||
|
"errors": {
|
||||||
|
"fetchFailed": "فشل جلب الأنظمة"
|
||||||
|
},
|
||||||
|
"confirmDelete": "هل أنت متأكد أنك تريد حذف هذا النظام؟",
|
||||||
|
"success": {
|
||||||
|
"deleted": "تم حذف النظام بنجاح",
|
||||||
|
"updated": "تم تحديث النظام بنجاح",
|
||||||
|
"created": "تم إنشاء النظام بنجاح"
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"name": "الاسم",
|
||||||
|
"default": "الافتراضي",
|
||||||
|
"preview": "المعاينة"
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"primary": "اللون الأساسي",
|
||||||
|
"primary_hover": "تأثير مرور الماوس الأساسي",
|
||||||
|
"sidebar_bg": "خلفية الشريط الجانبي",
|
||||||
|
"sidebar_text": "نص الشريط الجانبي",
|
||||||
|
"sidebar_text_muted": "نص الشريط الجانبي (باهت)",
|
||||||
|
"sidebar_active_bg": "خلفية الشريط الجانبي (نشط)",
|
||||||
|
"sidebar_active_text": "نص الشريط الجانبي (نشط)",
|
||||||
|
"sidebar_hover_bg": "خلفية مرور الماوس للشريط الجانبي",
|
||||||
|
"sidebar_border": "حدود الشريط الجانبي",
|
||||||
|
"header_bg": "خلفية الرأس",
|
||||||
|
"header_text": "نص الرأس",
|
||||||
|
"header_border": "حدود الرأس",
|
||||||
|
"background": "خلفية التطبيق",
|
||||||
|
"background_secondary": "خلفية ثانوية للتطبيق",
|
||||||
|
"text_primary": "النص الأساسي",
|
||||||
|
"text_secondary": "النص الثانوي",
|
||||||
|
"card_bg": "خلفية البطاقة",
|
||||||
|
"card_border": "حدود البطاقة",
|
||||||
|
"table_header_bg": "خلفية رأس الجدول",
|
||||||
|
"table_row_hover": "تأثير مرور الوموس للجدول",
|
||||||
|
"table_border": "حدود الجدول"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"edit": "تعديل النظام",
|
||||||
|
"delete": "حذف النظام",
|
||||||
|
"create": "إنشاء نظام جديد",
|
||||||
|
"update": "تحديث النظام",
|
||||||
|
"cancel": "إلغاء"
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"title": {
|
||||||
|
"create": "إنشاء نظام جديد",
|
||||||
|
"edit": "تعديل النظام"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"name": {
|
||||||
|
"label": "اسم النظام",
|
||||||
|
"placeholder": "مثال: الوضع الداكن",
|
||||||
|
"required": "الاسم مطلوب"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"label": "الوصف",
|
||||||
|
"placeholder": "وصف اختياري"
|
||||||
|
},
|
||||||
|
"isDefault": "تعيين كنظام افتراضي",
|
||||||
|
"sectionTitle": "تكوين الألوان",
|
||||||
|
"required": "مطلوب"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
{
|
{
|
||||||
"title": "Dashboard"
|
"title": "Welcome back",
|
||||||
|
"subtitle": "Manage your SaaS ecosystem from one central hub.",
|
||||||
|
"allModules": "All Modules",
|
||||||
|
"searchPlaceholder": "Search available modules...",
|
||||||
|
"actions": {
|
||||||
|
"launch": "Launch Module"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,10 @@
|
|||||||
"empty_state": {
|
"empty_state": {
|
||||||
"title": "No Modules Yet",
|
"title": "No Modules Yet",
|
||||||
"subtitle": "Create your first module to get started"
|
"subtitle": "Create your first module to get started"
|
||||||
|
},
|
||||||
|
"card": {
|
||||||
|
"environments": "Environments",
|
||||||
|
"permissions": "Permissions"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
@@ -59,5 +63,10 @@
|
|||||||
"description": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone and will remove all associated environments and tenant assignments.",
|
"description": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone and will remove all associated environments and tenant assignments.",
|
||||||
"confirm": "Delete Module",
|
"confirm": "Delete Module",
|
||||||
"cancel": "Cancel"
|
"cancel": "Cancel"
|
||||||
}
|
},
|
||||||
|
"Inventory": "Inventory",
|
||||||
|
"Fulfillment & Logistics": "Fulfillment & Logistics",
|
||||||
|
"User Management": "User Management",
|
||||||
|
"Tenant Management": "Tenant Management",
|
||||||
|
"Role Management": "Role Management"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"title": "Theme Palettes",
|
||||||
|
"errors": {
|
||||||
|
"fetchFailed": "Failed to fetch palettes"
|
||||||
|
},
|
||||||
|
"confirmDelete": "Are you sure you want to delete this palette?",
|
||||||
|
"success": {
|
||||||
|
"deleted": "Palette deleted successfully",
|
||||||
|
"updated": "Palette updated successfully",
|
||||||
|
"created": "Palette created successfully"
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"name": "Name",
|
||||||
|
"default": "Default",
|
||||||
|
"preview": "Preview"
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"primary": "Primary Color",
|
||||||
|
"primary_hover": "Primary Hover",
|
||||||
|
"sidebar_bg": "Sidebar Background",
|
||||||
|
"sidebar_text": "Sidebar Text",
|
||||||
|
"sidebar_text_muted": "Sidebar Text Muted",
|
||||||
|
"sidebar_active_bg": "Sidebar Active BG",
|
||||||
|
"sidebar_active_text": "Sidebar Active Text",
|
||||||
|
"sidebar_hover_bg": "Sidebar Hover BG",
|
||||||
|
"sidebar_border": "Sidebar Border",
|
||||||
|
"header_bg": "Header Background",
|
||||||
|
"header_text": "Header Text",
|
||||||
|
"header_border": "Header Border",
|
||||||
|
"background": "App Background",
|
||||||
|
"background_secondary": "App Secondary BG",
|
||||||
|
"text_primary": "Text Primary",
|
||||||
|
"text_secondary": "Text Secondary",
|
||||||
|
"card_bg": "Card Background",
|
||||||
|
"card_border": "Card Border",
|
||||||
|
"table_header_bg": "Table Header BG",
|
||||||
|
"table_row_hover": "Table Row Hover",
|
||||||
|
"table_border": "Table Border"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"edit": "Edit Palette",
|
||||||
|
"delete": "Delete Palette",
|
||||||
|
"create": "Create Palette",
|
||||||
|
"update": "Update Palette",
|
||||||
|
"cancel": "Cancel"
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"title": {
|
||||||
|
"create": "Create New Palette",
|
||||||
|
"edit": "Edit Palette"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"name": {
|
||||||
|
"label": "Palette Name",
|
||||||
|
"placeholder": "e.g. Dark Mode",
|
||||||
|
"required": "Name is required"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"label": "Description",
|
||||||
|
"placeholder": "Optional description"
|
||||||
|
},
|
||||||
|
"isDefault": "Set as Default Palette",
|
||||||
|
"sectionTitle": "Color Configuration",
|
||||||
|
"required": "Required"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user