implemented logi module
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -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,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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user