implemented logi module

This commit is contained in:
liyaqath
2026-06-29 15:37:14 +05:30
parent 865c289445
commit 3541e546c9
12 changed files with 702 additions and 134 deletions
+384
View File
@@ -0,0 +1,384 @@
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useFormik } from "formik";
import { ArrowLeft, Image as ImageIcon, Video, FileText, Award, Megaphone, HelpCircle, X, Plus, AlertCircle, Settings2, Eye } from "lucide-react";
import { useAssetType } from "../hook/useAssetType";
import { assetTypeSchema } from "../validation/asset-types.schema";
import type { AssetTypeCreateRequest } from "../types/asset-types.types";
const CATEGORIES = [
{ id: 'image', label: 'Image', icon: ImageIcon, desc: 'jpg, jpeg, png...' },
{ id: 'video', label: 'Video', icon: Video, desc: 'mp4, mov, avi...' },
{ id: 'document', label: 'Document', icon: FileText, desc: 'pdf, docx, doc...' },
{ id: 'certificate', label: 'Certificate', icon: Award, desc: 'pdf, jpg, png' },
{ id: 'marketing', label: 'Marketing', icon: Megaphone, desc: 'jpg, png, svg...' },
{ id: 'other', label: 'Other', icon: HelpCircle, desc: 'pdf, zip, csv...' },
] as const;
export default function NewAssetType() {
const navigate = useNavigate();
const { id } = useParams<{ id?: string }>();
const isEdit = Boolean(id);
const { createItem, updateItem, items, fetchItems } = useAssetType();
const [newFileType, setNewFileType] = useState('');
useEffect(() => {
fetchItems();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const formik = useFormik({
initialValues: {
name: "",
code: "",
description: "",
status: "active" as "active" | "inactive",
isRequired: false,
category: "" as typeof CATEGORIES[number]['id'] | "",
validation: {
allowedFileTypes: [] as string[],
maxFileSize: 10,
minUploadCount: 0,
maxUploadCount: 1,
}
},
validationSchema: assetTypeSchema,
onSubmit: async (values, { setSubmitting }) => {
try {
if (isEdit && id) {
await updateItem(id, values as any);
} else {
await createItem(values as AssetTypeCreateRequest);
}
navigate("..");
} catch {
// Error handled in hook
} finally {
setSubmitting(false);
}
},
});
useEffect(() => {
if (isEdit && id && items.length > 0) {
const match = items.find((item) => item.id === id);
if (match) {
formik.setValues({
name: match.name,
code: match.code || '',
description: match.description || '',
status: match.status as "active" | "inactive",
isRequired: match.isRequired || false,
category: match.category || '',
validation: match.validation || {
allowedFileTypes: [],
maxFileSize: 10,
minUploadCount: 0,
maxUploadCount: 1,
}
});
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isEdit, id, items]);
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
formik.handleChange(e);
if (!isEdit && !formik.touched.code) {
const generatedCode = e.target.value.toLowerCase().replace(/[^a-z0-9]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '');
formik.setFieldValue('code', generatedCode);
}
};
const handleAddFileType = () => {
if (newFileType.trim() && !formik.values.validation.allowedFileTypes.includes(newFileType.trim().toLowerCase())) {
formik.setFieldValue('validation.allowedFileTypes', [...formik.values.validation.allowedFileTypes, newFileType.trim().toLowerCase()]);
setNewFileType('');
}
};
const removeFileType = (type: string) => {
formik.setFieldValue('validation.allowedFileTypes', formik.values.validation.allowedFileTypes.filter(t => t !== type));
};
const SectionBadge = ({ num, title, subtitle }: { num: number, title: string, subtitle?: string }) => (
<div className="flex items-center gap-3 mb-6 pb-4 border-b border-gray-100">
<div className="w-6 h-6 rounded-full bg-purple-600 text-white flex items-center justify-center text-xs font-bold">
{num}
</div>
<h2 className="text-base font-bold text-gray-900">
{title}
{subtitle && <span className="text-xs font-normal text-gray-400 ml-2">{subtitle}</span>}
</h2>
</div>
);
return (
<div className="min-h-screen bg-gray-50 flex flex-col">
{/* Top Header */}
<div className="bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between sticky top-0 z-10">
<div className="flex items-center gap-4">
<button onClick={() => navigate("..")} className="text-gray-400 hover:text-gray-900 transition-colors">
<ArrowLeft className="w-5 h-5" />
</button>
<div className="flex items-center gap-2 text-sm">
<span className="text-gray-500">Asset Types</span>
<span className="text-gray-300"></span>
<span className="font-medium text-gray-900">{isEdit ? 'Edit Asset Type' : 'Create Asset Type'}</span>
</div>
</div>
<div className="flex items-center gap-3">
<button onClick={() => navigate("..")} type="button" className="px-4 py-2 text-sm font-medium text-gray-600 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
Cancel
</button>
<button onClick={() => formik.handleSubmit()} disabled={formik.isSubmitting} className="px-4 py-2 text-sm font-semibold text-white bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50">
{isEdit ? 'Save Changes' : 'Create Asset Type'}
</button>
</div>
</div>
{/* Main Content Area - Left Aligned */}
<div className="flex-1 overflow-y-auto p-6">
<div className="w-full max-w-4xl mx-0">
<div className="space-y-6 pb-20">
{/* SECTION 1: Basic Information */}
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
<SectionBadge num={1} title="Basic Information" />
<div className="grid grid-cols-2 gap-6 mb-6">
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1.5">Asset Type Name <span className="text-red-500">*</span></label>
<input
name="name"
value={formik.values.name}
onChange={handleNameChange}
onBlur={formik.handleBlur}
placeholder="e.g. Primary Image"
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
{formik.touched.name && formik.errors.name && <p className="text-red-500 text-xs mt-1">{formik.errors.name}</p>}
</div>
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1.5">Asset Code <span className="text-red-500">*</span></label>
<input
name="code"
value={formik.values.code}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
placeholder="e.g. primary_image"
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 bg-gray-50"
/>
<p className="text-[11px] text-gray-400 mt-1">Unique identifier — auto-generated from name</p>
</div>
</div>
<div className="mb-6">
<label className="block text-xs font-semibold text-gray-700 mb-1.5">Description</label>
<textarea
name="description"
value={formik.values.description}
onChange={formik.handleChange}
rows={3}
placeholder="Describe the purpose and usage guidelines for this asset type..."
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 resize-none"
/>
</div>
<div className="grid grid-cols-2 gap-6">
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1.5">Status</label>
<select
name="status"
value={formik.values.status}
onChange={formik.handleChange}
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1.5">Required Asset</label>
<label className="flex items-center gap-3 p-2.5 border border-gray-200 rounded-lg cursor-pointer hover:bg-gray-50 transition-colors">
<input
type="checkbox"
name="isRequired"
checked={formik.values.isRequired}
onChange={formik.handleChange}
className="w-4 h-4 text-purple-600 rounded border-gray-300 focus:ring-purple-500"
/>
<div>
<div className="text-sm font-medium text-gray-900">Mark as required</div>
<div className="text-xs text-gray-400">Products must upload this asset type</div>
</div>
</label>
</div>
</div>
</div>
{/* SECTION 2: Asset Category */}
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
<SectionBadge num={2} title="Asset Category" />
<p className="text-sm text-gray-600 mb-4">Select the media category. This determines default validation rules and file type presets.</p>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
{CATEGORIES.map(cat => (
<div
key={cat.id}
onClick={() => formik.setFieldValue('category', cat.id)}
className={`
cursor-pointer p-4 rounded-xl border transition-all flex flex-col items-center justify-center gap-2 text-center
${formik.values.category === cat.id
? 'bg-purple-50 border-purple-500 shadow-sm ring-1 ring-purple-500'
: 'bg-white border-gray-200 hover:border-purple-300 hover:bg-purple-50/30'
}
`}
>
<div className={`
w-10 h-10 rounded-lg flex items-center justify-center
${formik.values.category === cat.id ? 'bg-purple-600 text-white' : 'bg-gray-100 text-gray-600'}
`}>
<cat.icon className="w-5 h-5" />
</div>
<div>
<div className={`font-semibold text-sm ${formik.values.category === cat.id ? 'text-purple-900' : 'text-gray-900'}`}>
{cat.label}
</div>
<div className="text-[10px] text-gray-500 mt-1">{cat.desc}</div>
</div>
</div>
))}
</div>
</div>
{/* SECTION 3: Validation Rules */}
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
<SectionBadge num={3} title="Validation Rules" subtitle="Enforced when assets are uploaded" />
<div className="mb-6">
<label className="block text-xs font-semibold text-gray-700 mb-1.5">Allowed File Types <span className="text-red-500">*</span></label>
<div className="min-h-[42px] p-2 border border-gray-200 rounded-lg mb-2 flex flex-wrap gap-2 bg-gray-50">
{formik.values.validation.allowedFileTypes.length === 0 ? (
<span className="text-sm text-gray-400 py-1 px-2">No file types added yet</span>
) : (
formik.values.validation.allowedFileTypes.map(type => (
<span key={type} className="inline-flex items-center gap-1 px-2 py-1 bg-white border border-gray-200 rounded text-xs font-medium text-gray-700">
.{type}
<button type="button" onClick={() => removeFileType(type)} className="text-gray-400 hover:text-red-500"><X className="w-3 h-3" /></button>
</span>
))
)}
</div>
<div className="flex gap-2">
<input
value={newFileType}
onChange={(e) => setNewFileType(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleAddFileType(); } }}
placeholder="Type extension and press Enter (e.g. jpg)"
className="flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
<button type="button" onClick={handleAddFileType} className="px-3 py-2 border border-gray-200 rounded-lg hover:bg-gray-50">
<Plus className="w-4 h-4 text-gray-600" />
</button>
</div>
</div>
<div className="grid grid-cols-3 gap-6 mb-6">
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1.5">Max File Size</label>
<div className="flex">
<input
type="number"
name="validation.maxFileSize"
value={formik.values.validation.maxFileSize}
onChange={formik.handleChange}
className="w-full px-3 py-2 border border-gray-200 rounded-l-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 border-r-0"
/>
<span className="px-3 py-2 bg-gray-50 border border-gray-200 rounded-r-lg text-sm text-gray-500">MB</span>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1.5">Min Upload Count</label>
<input
type="number"
name="validation.minUploadCount"
value={formik.values.validation.minUploadCount}
onChange={formik.handleChange}
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
<div className="text-[10px] text-gray-400 mt-1">0 = optional</div>
</div>
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1.5">Max Upload Count</label>
<input
type="number"
name="validation.maxUploadCount"
value={formik.values.validation.maxUploadCount}
onChange={formik.handleChange}
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
</div>
</div>
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3 flex gap-2">
<AlertCircle className="w-4 h-4 text-amber-500 shrink-0 mt-0.5" />
<p className="text-xs text-amber-800">
Products using this asset type must upload up to {formik.values.validation.maxUploadCount} file{formik.values.validation.maxUploadCount !== 1 && 's'} (max {formik.values.validation.maxFileSize} MB each).
Accepted formats: {formik.values.validation.allowedFileTypes.length > 0 ? formik.values.validation.allowedFileTypes.map(t => '.' + t).join(', ') : '—'}.
</p>
</div>
</div>
{/* SECTION 4: Preview */}
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
<SectionBadge num={4} title="Preview" subtitle="Validation summary as shown to content editors" />
<div className="border border-gray-200 rounded-xl overflow-hidden mt-4">
<div className="bg-purple-600 p-4 flex items-center justify-between text-white">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-white/20 rounded-lg flex items-center justify-center backdrop-blur-sm">
<ImageIcon className="w-5 h-5" />
</div>
<div>
<div className="font-bold">{formik.values.name || 'Asset Type Name'}</div>
<div className="text-xs text-purple-200 font-mono">{formik.values.code || 'asset_code'}</div>
</div>
</div>
<div className="px-2 py-1 bg-white/20 rounded-full text-xs font-medium flex items-center gap-1.5 backdrop-blur-sm">
<div className={`w-2 h-2 rounded-full ${formik.values.status === 'active' ? 'bg-green-400' : 'bg-gray-400'}`} />
{formik.values.status === 'active' ? 'Active' : 'Inactive'}
</div>
</div>
<div className="bg-gray-50 p-4 grid grid-cols-2 gap-4 text-sm">
<div>
<div className="text-xs font-semibold text-gray-500 uppercase mb-2">Accepted Formats</div>
<div className="flex flex-wrap gap-1">
{formik.values.validation.allowedFileTypes.length === 0 ? (
<span className="text-gray-400 text-xs">No file types specified</span>
) : (
formik.values.validation.allowedFileTypes.map(type => (
<span key={type} className="px-2 py-0.5 bg-gray-200 text-gray-700 rounded text-xs">.{type}</span>
))
)}
</div>
</div>
<div>
<div className="text-xs font-semibold text-gray-500 uppercase mb-2">Constraints</div>
<div className="space-y-1 text-xs text-gray-600">
<div className="flex items-center justify-between"><span>Max size:</span> <span className="font-medium text-gray-900">{formik.values.validation.maxFileSize} MB</span></div>
<div className="flex items-center justify-between"><span>Upload count:</span> <span className="font-medium text-gray-900">{formik.values.validation.minUploadCount > 0 ? formik.values.validation.minUploadCount : 'Optional'}, max {formik.values.validation.maxUploadCount}</span></div>
<div className="flex items-center justify-between"><span>Required:</span> <span className="font-medium text-gray-900">{formik.values.isRequired ? 'Yes' : 'No - optional'}</span></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
BIN
View File
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,4 +1,4 @@
import { ReactNode } from "react";
import type { ReactNode } from "react";
import { Database, GitBranch, Share2, ShieldCheck, Package } from "lucide-react";
import { HeroIllustration } from "./HeroIllustration";
+1 -1
View File
@@ -1,5 +1,5 @@
import { useState } from "react";
import { Briefcase, X, CheckCircle2, Layers, Tag, Globe, GitBranch, ArrowRight, Laptop, Headphones, Home, Box } from "lucide-react";
import { X, CheckCircle2, Layers, Tag, Globe, GitBranch, ArrowRight, Laptop, Headphones, Home, Box } from "lucide-react";
export interface FamilyCardData {
id: string;
+22 -13
View File
@@ -1,28 +1,37 @@
import { useLocation, useNavigate } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { Bell, ChevronDown, Building2, Globe, LogOut } from "lucide-react";
import { Bell, ChevronDown, Building2, Globe } from "lucide-react";
import { useLanguage, type Language } from "../../contexts/LanguageContext";
import { useHeader } from "../../contexts/HeaderContext";
import { useAppDispatch, useAppSelector } from "../../store";
import { logout } from "../../store/slices/authSlice";
import { useState, useRef, useEffect } from "react";
const PAGE_TITLES: Record<string, { title: string; subtitle?: string }> = {
"/dashboard": { title: "Dashboard", subtitle: "Overview of your PIM platform" },
"/products": { title: "Products", subtitle: "Manage your product catalog" },
"/families": { title: "Product Families", subtitle: "Manage product families" },
"/reports": { title: "Reports & Analytics", subtitle: "Comprehensive insights into your product catalog performance" },
"/users": { title: "Users & Roles", subtitle: "Manage team members, permissions, and access controls" },
"/settings": { title: "Settings", subtitle: "System configurations and preferences" },
};
export function Header() {
const { title, subtitle } = useHeader();
const { language, setLanguage } = useLanguage();
const navigate = useNavigate();
const dispatch = useAppDispatch();
const { user } = useAppSelector((state) => state.auth);
const [showProfileMenu, setShowProfileMenu] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const defaultPage = PAGE_TITLES[location.pathname] || { title: "PIM Platform" };
const displayTitle = title || defaultPage.title;
const displaySubtitle = title ? subtitle : defaultPage.subtitle;
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setShowProfileMenu(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const handleLogout = () => {
dispatch(logout());
navigate("/login");
};
return (
<header className="h-16 w-full flex items-center justify-between px-6 bg-surface border-b border-border sticky top-0 z-30">
@@ -1,7 +1,7 @@
import { useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useFormik } from "formik";
import { ArrowLeft, Save, Info, Library, Settings2, ShieldCheck } from "lucide-react";
import { Save, Library } from "lucide-react";
import { PageWrapper } from "../../../components/layouts/PageWrapper";
import { Button } from "../../../components/customs/Button";
import { useAssetFamily } from "../hook/useAssetFamily";
@@ -78,39 +78,40 @@ export default function NewAssetFamily() {
</>
}
/>
<form id="asset-family-form" onSubmit={formik.handleSubmit} className="max-w-4xl space-y-10">
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-6 border-b border-gray-200 pb-2">Information</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="md:col-span-2">
<label className={labelClass}>Name *</label>
<input
name="name"
className={inputClass(formik.touched.name && Boolean(formik.errors.name))}
value={formik.values.name}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
placeholder="e.g. Documents"
/>
{formik.touched.name && formik.errors.name && <p className={errorClass}>{formik.errors.name}</p>}
</div>
<div className="md:col-span-2">
<label className={labelClass}>Status</label>
<select
name="status"
className={inputClass()}
value={formik.values.status}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
>
{STATUS_OPTIONS.map((s) => (
<option key={s.value} value={s.value}>{s.label}</option>
))}
</select>
<div className="flex flex-col lg:flex-row gap-8">
<form id="asset-family-form" onSubmit={formik.handleSubmit} className="flex-1 max-w-4xl space-y-10">
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-6 border-b border-gray-200 pb-2">Information</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="md:col-span-2">
<label className={labelClass}>Name *</label>
<input
name="name"
className={inputClass(formik.touched.name && Boolean(formik.errors.name))}
value={formik.values.name}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
placeholder="e.g. Media Assets"
/>
{formik.touched.name && formik.errors.name && <p className={errorClass}>{formik.errors.name}</p>}
</div>
<div className="md:col-span-2">
<label className={labelClass}>Status</label>
<select
name="status"
className={inputClass()}
value={formik.values.status}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
>
{STATUS_OPTIONS.map((s) => (
<option key={s.value} value={s.value}>{s.label}</option>
))}
</select>
</div>
</div>
</div>
</div>
{/* Actions */}
<div className="flex items-center justify-end gap-3 pt-4">
+237 -34
View File
@@ -1,11 +1,13 @@
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useFormik } from "formik";
import { ArrowLeft, Image as ImageIcon, Video, FileText, Award, Megaphone, HelpCircle, X, Plus, AlertCircle, Settings2, Eye } from "lucide-react";
import { Save, Image as ImageIcon, Video, FileText, Award, Megaphone, HelpCircle, X, Plus, AlertCircle } from "lucide-react";
import { PageWrapper } from "../../../components/layouts/PageWrapper";
import { Breadcrumb } from "../../../components/layouts/Breadcrumb";
import { Button } from "../../../components/customs/Button";
import { useAssetType } from "../hook/useAssetType";
import { assetTypeSchema } from "../validation/asset-types.schema";
import type { AssetTypeCreateRequest } from "../types/asset-types.types";
import { Breadcrumb } from "../../../components/layouts/Breadcrumb";
const CATEGORIES = [
{ id: 'image', label: 'Image', icon: ImageIcon, desc: 'jpg, jpeg, png...' },
@@ -128,40 +130,241 @@ export default function NewAssetType() {
}
/>
<form id="asset-type-form" onSubmit={formik.handleSubmit} className="max-w-4xl space-y-10">
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-6 border-b border-gray-200 pb-2">Information</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="md:col-span-2">
<label className={labelClass}>Name *</label>
<input
name="name"
className={inputClass(formik.touched.name && Boolean(formik.errors.name))}
value={formik.values.name}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
placeholder="e.g. Image"
/>
{formik.touched.name && formik.errors.name && <p className={errorClass}>{formik.errors.name}</p>}
</div>
<div className="md:col-span-2">
<label className={labelClass}>Status</label>
<select
name="status"
className={inputClass()}
value={formik.values.status}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
>
{STATUS_OPTIONS.map((s) => (
<option key={s.value} value={s.value}>{s.label}</option>
))}
</select>
{/* SECTION 1: Basic Information */}
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
<SectionBadge num={1} title="Basic Information" />
<div className="grid grid-cols-2 gap-6 mb-6">
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1.5">Asset Type Name <span className="text-red-500">*</span></label>
<input
name="name"
value={formik.values.name}
onChange={handleNameChange}
onBlur={formik.handleBlur}
placeholder="e.g. Primary Image"
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
{formik.touched.name && formik.errors.name && <p className="text-red-500 text-xs mt-1">{formik.errors.name}</p>}
</div>
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1.5">Asset Code <span className="text-red-500">*</span></label>
<input
name="code"
value={formik.values.code}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
placeholder="e.g. primary_image"
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 bg-gray-50"
/>
<p className="text-[11px] text-gray-400 mt-1">Unique identifier auto-generated from name</p>
</div>
</div>
<div className="mb-6">
<label className="block text-xs font-semibold text-gray-700 mb-1.5">Description</label>
<textarea
name="description"
value={formik.values.description}
onChange={formik.handleChange}
rows={3}
placeholder="Describe the purpose and usage guidelines for this asset type..."
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 resize-none"
/>
</div>
<div className="grid grid-cols-2 gap-6">
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1.5">Status</label>
<select
name="status"
value={formik.values.status}
onChange={formik.handleChange}
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1.5">Required Asset</label>
<label className="flex items-center gap-3 p-2.5 border border-gray-200 rounded-lg cursor-pointer hover:bg-gray-50 transition-colors">
<input
type="checkbox"
name="isRequired"
checked={formik.values.isRequired}
onChange={formik.handleChange}
className="w-4 h-4 text-purple-600 rounded border-gray-300 focus:ring-purple-500"
/>
<div>
<div className="text-sm font-medium text-gray-900">Mark as required</div>
<div className="text-xs text-gray-400">Products must upload this asset type</div>
</div>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* SECTION 2: Asset Category */}
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
<SectionBadge num={2} title="Asset Category" />
<p className="text-sm text-gray-600 mb-4">Select the media category. This determines default validation rules and file type presets.</p>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
{CATEGORIES.map(cat => (
<div
key={cat.id}
onClick={() => formik.setFieldValue('category', cat.id)}
className={`
cursor-pointer p-4 rounded-xl border transition-all flex flex-col items-center justify-center gap-2 text-center
${formik.values.category === cat.id
? 'bg-purple-50 border-purple-500 shadow-sm ring-1 ring-purple-500'
: 'bg-white border-gray-200 hover:border-purple-300 hover:bg-purple-50/30'
}
`}
>
<div className={`
w-10 h-10 rounded-lg flex items-center justify-center
${formik.values.category === cat.id ? 'bg-purple-600 text-white' : 'bg-gray-100 text-gray-600'}
`}>
<cat.icon className="w-5 h-5" />
</div>
<div>
<div className={`font-semibold text-sm ${formik.values.category === cat.id ? 'text-purple-900' : 'text-gray-900'}`}>
{cat.label}
</div>
<div className="text-[10px] text-gray-500 mt-1">{cat.desc}</div>
</div>
</div>
))}
</div>
</div>
{/* SECTION 3: Validation Rules */}
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
<SectionBadge num={3} title="Validation Rules" subtitle="Enforced when assets are uploaded" />
<div className="mb-6">
<label className="block text-xs font-semibold text-gray-700 mb-1.5">Allowed File Types <span className="text-red-500">*</span></label>
<div className="min-h-[42px] p-2 border border-gray-200 rounded-lg mb-2 flex flex-wrap gap-2 bg-gray-50">
{formik.values.validation.allowedFileTypes.length === 0 ? (
<span className="text-sm text-gray-400 py-1 px-2">No file types added yet</span>
) : (
formik.values.validation.allowedFileTypes.map(type => (
<span key={type} className="inline-flex items-center gap-1 px-2 py-1 bg-white border border-gray-200 rounded text-xs font-medium text-gray-700">
.{type}
<button type="button" onClick={() => removeFileType(type)} className="text-gray-400 hover:text-red-500"><X className="w-3 h-3" /></button>
</span>
))
)}
</div>
<div className="flex gap-2">
<input
value={newFileType}
onChange={(e) => setNewFileType(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleAddFileType(); } }}
placeholder="Type extension and press Enter (e.g. jpg)"
className="flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
<button type="button" onClick={handleAddFileType} className="px-3 py-2 border border-gray-200 rounded-lg hover:bg-gray-50">
<Plus className="w-4 h-4 text-gray-600" />
</button>
</div>
</div>
<div className="grid grid-cols-3 gap-6 mb-6">
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1.5">Max File Size</label>
<div className="flex">
<input
type="number"
name="validation.maxFileSize"
value={formik.values.validation.maxFileSize}
onChange={formik.handleChange}
className="w-full px-3 py-2 border border-gray-200 rounded-l-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 border-r-0"
/>
<span className="px-3 py-2 bg-gray-50 border border-gray-200 rounded-r-lg text-sm text-gray-500">MB</span>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1.5">Min Upload Count</label>
<input
type="number"
name="validation.minUploadCount"
value={formik.values.validation.minUploadCount}
onChange={formik.handleChange}
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
<div className="text-[10px] text-gray-400 mt-1">0 = optional</div>
</div>
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1.5">Max Upload Count</label>
<input
type="number"
name="validation.maxUploadCount"
value={formik.values.validation.maxUploadCount}
onChange={formik.handleChange}
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
</div>
</div>
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3 flex gap-2">
<AlertCircle className="w-4 h-4 text-amber-500 shrink-0 mt-0.5" />
<p className="text-xs text-amber-800">
Products using this asset type must upload up to {formik.values.validation.maxUploadCount} file{formik.values.validation.maxUploadCount !== 1 && 's'} (max {formik.values.validation.maxFileSize} MB each).
Accepted formats: {formik.values.validation.allowedFileTypes.length > 0 ? formik.values.validation.allowedFileTypes.map(t => '.' + t).join(', ') : '—'}.
</p>
</div>
</div>
{/* SECTION 4: Preview */}
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
<SectionBadge num={4} title="Preview" subtitle="Validation summary as shown to content editors" />
<div className="border border-gray-200 rounded-xl overflow-hidden mt-4">
<div className="bg-purple-600 p-4 flex items-center justify-between text-white">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-white/20 rounded-lg flex items-center justify-center backdrop-blur-sm">
<ImageIcon className="w-5 h-5" />
</div>
<div>
<div className="font-bold">{formik.values.name || 'Asset Type Name'}</div>
<div className="text-xs text-purple-200 font-mono">{formik.values.code || 'asset_code'}</div>
</div>
</div>
<div className="px-2 py-1 bg-white/20 rounded-full text-xs font-medium flex items-center gap-1.5 backdrop-blur-sm">
<div className={`w-2 h-2 rounded-full ${formik.values.status === 'active' ? 'bg-green-400' : 'bg-gray-400'}`} />
{formik.values.status === 'active' ? 'Active' : 'Inactive'}
</div>
</div>
<div className="bg-gray-50 p-4 grid grid-cols-2 gap-4 text-sm">
<div>
<div className="text-xs font-semibold text-gray-500 uppercase mb-2">Accepted Formats</div>
<div className="flex flex-wrap gap-1">
{formik.values.validation.allowedFileTypes.length === 0 ? (
<span className="text-gray-400 text-xs">No file types specified</span>
) : (
formik.values.validation.allowedFileTypes.map(type => (
<span key={type} className="px-2 py-0.5 bg-gray-200 text-gray-700 rounded text-xs">.{type}</span>
))
)}
</div>
</div>
<div>
<div className="text-xs font-semibold text-gray-500 uppercase mb-2">Constraints</div>
<div className="space-y-1 text-xs text-gray-600">
<div className="flex items-center justify-between"><span>Max size:</span> <span className="font-medium text-gray-900">{formik.values.validation.maxFileSize} MB</span></div>
<div className="flex items-center justify-between"><span>Upload count:</span> <span className="font-medium text-gray-900">{formik.values.validation.minUploadCount > 0 ? formik.values.validation.minUploadCount : 'Optional'}, max {formik.values.validation.maxUploadCount}</span></div>
<div className="flex items-center justify-between"><span>Required:</span> <span className="font-medium text-gray-900">{formik.values.isRequired ? 'Yes' : 'No - optional'}</span></div>
</div>
</div>
</div>
</div>
</div>
</form>
</PageWrapper>
);
}
+3 -15
View File
@@ -2,22 +2,13 @@ import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Plus, Edit2, Trash2, RefreshCw, Radio, CheckCircle, Layers, TrendingUp, ShoppingCart, ShoppingBag, Server, Warehouse, Store, Smartphone, Globe, Monitor } from "lucide-react";
import { PageWrapper } from "../../../components/layouts/PageWrapper";
import { Breadcrumb } from "../../../components/layouts/Breadcrumb";
import { Button } from "../../../components/customs/Button";
import { DataTable } from "../../../components/customs/DataTable";
import { Table } from "../../../components/customs/Table";
import { SearchBar } from "../../../components/customs/SearchBar";
import { KPIGrid } from "../../../components/customs/KPI";
import { StatusBadge } from "../../../components/customs/StatusBadge";
import { ChannelStatsCard } from "../components/ChannelStatsCard";
// Mock Data
const MOCK_CHANNELS = [
{ id: "1", name: "Shopify Storefront", desc: "Primary ecommerce storefront powered by Shopify", code: "shopify_main", type: "Ecommerce", status: "active", families: 24, products: "8,942", updated: "2025-03-15", author: "Sarah Chen", icon: ShoppingCart, typeColor: "text-blue-600", typeBg: "bg-blue-50" },
{ id: "2", name: "Amazon Marketplace", desc: "Amazon.com US marketplace listing channel", code: "amazon_us", type: "Marketplace", status: "active", families: 18, products: "6,241", updated: "2025-03-12", author: "Michael Torres", icon: ShoppingBag, typeColor: "text-amber-600", typeBg: "bg-amber-50" },
{ id: "3", name: "WooCommerce Store", desc: "European WooCommerce store for EU market", code: "woocommerce_eu", type: "Ecommerce", status: "active", families: 15, products: "4,312", updated: "2025-02-28", author: "Emma Wilson", icon: ShoppingCart, typeColor: "text-blue-600", typeBg: "bg-blue-50" },
{ id: "4", name: "Company Website", desc: "Corporate marketing website product catalog", code: "website_main", type: "Website", status: "active", families: 30, products: "12,840", updated: "2025-03-14", author: "Admin User", icon: Globe, typeColor: "text-purple-600", typeBg: "bg-purple-50" },
{ id: "5", name: "Retail POS", desc: "In-store point-of-sale terminals", code: "pos_retail", type: "POS", status: "active", families: 20, products: "9,840", updated: "2025-03-10", author: "James Park", icon: Smartphone, typeColor: "text-emerald-600", typeBg: "bg-emerald-50" },
{ id: "6", name: "SAP ERP System", desc: "SAP S/4HANA enterprise resource planning integration", code: "sap_erp", type: "ERP", status: "active", families: 28, products: "11,200", updated: "2025-03-05", author: "Tech Team", icon: Monitor, typeColor: "text-red-600", typeBg: "bg-red-50" },
];
import { Breadcrumb } from "../../../components/layouts/Breadcrumb";
import { useChannel } from "../hook/useChannel";
import type { Channel } from "../types/channels.types";
@@ -33,7 +24,6 @@ const CHANNEL_TYPES_META: Record<string, { label: string, icon: any, typeColor:
};
export default function ChannelList() {
usePageHeader("Channel Master", "Manage publication destinations for product information");
const [searchQuery, setSearchQuery] = useState("");
const navigate = useNavigate();
const { items, fetchItems, loading, deleteItem } = useChannel();
@@ -120,8 +110,6 @@ export default function ChannelList() {
const filteredItems = items.filter(item => item.name.toLowerCase().includes(searchQuery.toLowerCase()) || (item.code && item.code.toLowerCase().includes(searchQuery.toLowerCase())));
const activeCount = items.filter(i => i.status === 'active').length;
return (
<PageWrapper>
<Breadcrumb
@@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
import { Plus, Edit2, Trash2, RefreshCw, AlertCircle, Package, Clock, Plug, CheckCircle, ShoppingCart, ShoppingBag, Server, Warehouse, Store, Globe, Smartphone, Monitor, Code2 } from "lucide-react";
import { Plus, Edit2, Trash2, RefreshCw, ShoppingCart, ShoppingBag, Server, Warehouse, Store, Globe, Smartphone, Monitor, Code2 } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { PageWrapper } from "../../../components/layouts/PageWrapper";
import { Button } from "../../../components/customs/Button";
@@ -142,9 +142,6 @@ export default function IntegrationList() {
);
const filteredItems = items.filter(item => {
const matchesSearch = item.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
(item.description && item.description.toLowerCase().includes(searchQuery.toLowerCase()));
const matchesStatus = statusFilter === "All Status" ||
(statusFilter === "Connected" && (item.status === "Connected" || item.status === "active")) ||
(statusFilter === "Pending" && (item.status === "Pending" || item.status === "pending")) ||
@@ -153,10 +150,9 @@ export default function IntegrationList() {
const matchesType = typeFilter === "All Types" ||
(typeFilter.toLowerCase() === item.integrationType.toLowerCase());
return matchesSearch && matchesStatus && matchesType;
return matchesStatus && matchesType;
});
const connectedCount = items.filter(i => i.status === "Connected" || i.status === "active").length;
const failedCount = items.filter(i => i.syncErrors && i.syncErrors > 0).length;
return (
@@ -214,17 +210,25 @@ export default function IntegrationList() {
<div className="w-full">
<DataTable
columns={columns}
data={MOCK_INTEGRATIONS}
data={filteredItems}
actions={actions}
searchPlaceholder="Search integrations..."
toolbarLeft={
<div className="flex gap-2">
<select className="h-9 px-3 py-1.5 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 text-gray-700 bg-white">
<select
className="h-9 px-3 py-1.5 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 text-gray-700 bg-white"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
>
<option>All Status</option>
<option>Connected</option>
<option>Disconnected</option>
</select>
<select className="h-9 px-3 py-1.5 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 text-gray-700 bg-white">
<select
className="h-9 px-3 py-1.5 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 text-gray-700 bg-white"
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
>
<option>All Types</option>
<option>Marketplace</option>
<option>E-Commerce</option>
@@ -233,14 +237,6 @@ export default function IntegrationList() {
}
/>
</div>
{/* Table */}
<Table
columns={columns}
data={filteredItems}
actions={actions}
variant="flat"
/>
</div>
</PageWrapper>
);
@@ -252,26 +252,12 @@ export default function NewIntegration() {
}
return (
<div className="min-h-screen bg-gray-50 flex flex-col">
{/* Top Header */}
<div className="bg-white border-b border-gray-200 px-6 py-4 sticky top-0 z-10">
<nav className="text-xs text-gray-500 mb-2 flex items-center gap-1.5">
<span className="cursor-pointer hover:text-gray-700" onClick={() => navigate("..")}>Integration Hub</span>
<span></span>
<span className="text-gray-900">{isEdit ? "Edit Integration" : "Create Integration"}</span>
</nav>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<button
type="button"
onClick={() => navigate("..")}
className="p-1.5 rounded-lg hover:bg-gray-100 transition-colors text-gray-500"
>
<ArrowLeft className="w-5 h-5" />
</button>
<h1 className="text-xl font-bold text-gray-900">{isEdit ? "Edit Integration" : "New Integration"}</h1>
</div>
<div className="flex items-center gap-3">
<PageWrapper>
<Breadcrumb
items={[{ label: 'Home' }, { label: 'Integration Hub', href: '/integrations' }, { label: isEdit ? 'Edit Integration' : 'Create Integration' }]}
backTo="/integrations"
actions={
<>
<button
type="button"
className="flex items-center gap-2 py-2 px-4 border border-amber-300 bg-amber-50 text-amber-700 rounded-lg text-sm font-medium hover:bg-amber-100 transition-colors"
+2 -1
View File
@@ -1,8 +1,9 @@
import { useState } from 'react';
import { useParams } from 'react-router-dom';
import { Box, LayoutGrid, Tags, Globe, Eye, Settings2, Save, Send, CheckCircle2, Copy, Upload, Image as ImageIcon, Video, FileText, Link as LinkIcon, Info, FolderTree, AlertCircle, RefreshCw, Briefcase } from 'lucide-react';
import { Box, LayoutGrid, Tags, Globe, Eye, Settings2, Save, Send, CheckCircle2, Copy, Upload, Image as ImageIcon, Video, FileText, Link as LinkIcon, Info, FolderTree, AlertCircle, RefreshCw } from 'lucide-react';
import { PageWrapper } from '../../../components/layouts/PageWrapper';
import { Breadcrumb } from '../../../components/layouts/Breadcrumb';
import { FamilyCard } from '../../../components/customs/FamilyCard';
const MOCK_FAMILIES = [