production V2

This commit is contained in:
kenilkb 2026-07-17 10:18:36 +05:30
parent 61f3bd8057
commit 8687d72394
7 changed files with 168 additions and 54 deletions

View File

@ -18,7 +18,7 @@ export const PageHeader: React.FC<PageHeaderProps> = ({ title, subtitle, badge,
{badge && <div className="shrink-0">{badge}</div>}
</div>
{subtitle && (
<p className="text-xs page-subtitle font-medium text-ink-500 truncate max-w-3xl">
<p className="text-xs page-subtitle font-medium text-ink-500 max-w-3xl leading-relaxed">
{subtitle}
</p>
)}

View File

@ -274,12 +274,12 @@ export const AssetsPage = () => {
<PageHeader
title="Asset Library"
subtitle="Securely manage, distribute, and track marketing collateral and partner resources."
badge={
<div className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full bg-ink-100 border border-ink-200 text-[10px] font-bold text-ink-900 tracking-wider uppercase shrink-0">
<CheckCircle className="w-3.5 h-3.5 text-ink-950" />
<span>Global CDN Active</span>
</div>
}
// badge={
// <div className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full bg-ink-100 border border-ink-200 text-[10px] font-bold text-ink-900 tracking-wider uppercase shrink-0">
// <CheckCircle className="w-3.5 h-3.5 text-ink-950" />
// <span>Global CDN Active</span>
// </div>
// }
/>
);
@ -306,21 +306,19 @@ export const AssetsPage = () => {
<div className="flex items-center gap-1 bg-ink-50/50 p-1 rounded-xl border border-ink-200 shadow-sm shrink-0">
<button
onClick={() => setSelectedCategory("ALL")}
className={`px-3.5 py-1.5 rounded-lg text-[10px] uppercase tracking-wider font-extrabold transition-all cursor-pointer whitespace-nowrap ${
selectedCategory === "ALL"
className={`px-3.5 py-1.5 rounded-lg text-[10px] uppercase tracking-wider font-extrabold transition-all cursor-pointer whitespace-nowrap ${selectedCategory === "ALL"
? "bg-ink-900 text-ink-0 shadow-sm"
: "text-ink-600 hover:text-ink-900 hover:bg-ink-100/50"
}`}
}`}
>
All Assets
</button>
<button
onClick={() => setSelectedCategory("RECOMMENDED")}
className={`px-3.5 py-1.5 rounded-lg text-[10px] uppercase tracking-wider font-extrabold transition-all cursor-pointer whitespace-nowrap flex items-center gap-1.5 ${
selectedCategory === "RECOMMENDED"
className={`px-3.5 py-1.5 rounded-lg text-[10px] uppercase tracking-wider font-extrabold transition-all cursor-pointer whitespace-nowrap flex items-center gap-1.5 ${selectedCategory === "RECOMMENDED"
? "bg-gradient-to-r from-amber-500 to-amber-600 text-zinc-950 shadow-md shadow-amber-500/25"
: "text-amber-700 dark:text-amber-400 hover:bg-amber-500/10"
}`}
}`}
>
<Sparkles className="w-3.5 h-3.5 animate-pulse" />
<span>Recommended for You</span>
@ -398,7 +396,7 @@ export const AssetsPage = () => {
{/* Background decorative elements */}
<div className="absolute top-0 right-0 w-80 h-80 bg-amber-500/[0.04] rounded-full blur-3xl -mr-16 -mt-16 pointer-events-none" />
<div className="absolute -left-10 -bottom-10 w-60 h-60 bg-amber-600/[0.02] rounded-full blur-3xl pointer-events-none" />
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 relative z-10">
<div className="space-y-2">
<div className="flex flex-wrap items-center gap-2">

View File

@ -65,11 +65,16 @@ export const DashboardPage = () => {
}
];
const isAdmin = user?.role === 'ADMIN';
// Header component
const headerNode = (
<PageHeader
title={`Welcome back, ${user?.email?.split('@')[0]}`}
subtitle={`You are authenticated as ${user?.role}. Manage your channel network, monitor compliance, and distribute assets globally.`}
subtitle={isAdmin
? 'Manage your channel network, monitor compliance, and distribute assets globally.'
: `Access your shared resources, legal agreements, and explore our ecosystem of products and services.`
}
badge={
<div className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-ink-900 border border-ink-800 text-[10px] font-bold text-ink-0 tracking-wider uppercase shrink-0">
<div className="w-1 h-1 rounded-full bg-ink-0 animate-pulse" />

View File

@ -1,13 +1,12 @@
import React, { useEffect, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Code,
Briefcase,
Shield,
Cloud,
Globe,
ExternalLink,
Sparkles,
import {
Code,
Briefcase,
Shield,
Cloud,
Globe,
ExternalLink,
Layers
} from 'lucide-react';
import { PageHeader } from '../components/ui/PageHeader';
@ -52,12 +51,6 @@ export const EcosystemPage: React.FC = () => {
<PageHeader
title="Ecosystem Explorer"
subtitle="Unlock access to Tech4Biz's complete network of industry-leading products, platforms, and specialized services."
badge={
<div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-ink-950/5 dark:bg-ink-0/10 border border-ink-300 dark:border-ink-700 text-[10px] font-black text-ink-900 dark:text-ink-300 tracking-wider uppercase shrink-0">
<Sparkles className="w-3.5 h-3.5" />
<span>Ecosystem Active</span>
</div>
}
/>
);
@ -71,11 +64,10 @@ export const EcosystemPage: React.FC = () => {
<button
key={type}
onClick={() => setFilter(type)}
className={`px-4 py-2 rounded-lg text-xs font-black tracking-wider uppercase transition-all duration-300 cursor-pointer ${
filter === type
className={`px-4 py-2 rounded-lg text-xs font-black tracking-wider uppercase transition-all duration-300 cursor-pointer ${filter === type
? 'bg-ink-0 text-ink-950 shadow-sm border border-ink-200/50'
: 'text-ink-500 hover:text-ink-900'
}`}
}`}
>
{type}s
</button>
@ -91,7 +83,7 @@ export const EcosystemPage: React.FC = () => {
<div className="w-8 h-8 border-4 border-ink-900/30 border-t-ink-900 rounded-full animate-spin"></div>
</div>
) : (
<motion.div
<motion.div
layout
className="grid grid-cols-1 md:grid-cols-2 gap-8"
>
@ -123,11 +115,10 @@ export const EcosystemPage: React.FC = () => {
)}
</div>
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[9px] font-black tracking-widest uppercase border ${
isProduct
? 'bg-blue-500/10 text-blue-600 border-blue-500/20'
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[9px] font-black tracking-widest uppercase border ${isProduct
? 'bg-blue-500/10 text-blue-600 border-blue-500/20'
: 'bg-emerald-500/10 text-emerald-600 border-emerald-500/20'
}`}>
}`}>
<Layers className="w-3 h-3" />
{offering.type}
</span>
@ -153,9 +144,8 @@ export const EcosystemPage: React.FC = () => {
<ul className="grid grid-cols-1 gap-2.5 pt-1">
{offering.benefits.map((benefit, bIdx) => (
<li key={bIdx} className="flex items-start gap-2.5 text-xs font-medium text-ink-600">
<span className={`w-1.5 h-1.5 rounded-full shrink-0 mt-1.5 ${
isProduct ? 'bg-blue-500' : 'bg-emerald-500'
}`} />
<span className={`w-1.5 h-1.5 rounded-full shrink-0 mt-1.5 ${isProduct ? 'bg-blue-500' : 'bg-emerald-500'
}`} />
<span>{benefit}</span>
</li>
))}

View File

@ -139,12 +139,12 @@ export const LoginPage = () => {
<label className="block text-[10px] font-bold text-ink-500 uppercase tracking-wider">
Password
</label>
<a
{/* <a
href="#"
className="text-[10px] font-bold text-ink-500 hover:text-ink-900 transition-colors tracking-wider"
>
RECOVERY?
</a>
</a> */}
</div>
<div className="relative group">
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none">

View File

@ -950,6 +950,127 @@ export const DirectoryPage: React.FC = () => {
size="lg"
>
<div className="space-y-4 font-sans">
{/* ── Group Assignment Section ── */}
{!isAddingPartnerAssets && (
<div className="bg-ink-50/60 border border-ink-200 rounded-xl p-4 space-y-3">
<div className="flex items-center justify-between">
<h4 className="text-[10px] font-extrabold uppercase tracking-widest text-ink-500 flex items-center gap-1.5">
<Folder className="w-3.5 h-3.5 text-ink-600" />
<span>Recommendation Groups</span>
</h4>
</div>
{/* Current Groups as Tags */}
<div className="flex flex-wrap gap-2 items-center min-h-[28px]">
{(() => {
const currentGroups = selectedPartnerForAssets?.partnerGroup
? selectedPartnerForAssets.partnerGroup.split(',').map((s: string) => s.trim()).filter(Boolean)
: [];
if (currentGroups.length === 0) {
return (
<span className="text-[10px] text-ink-400 font-medium italic">No recommendation groups assigned</span>
);
}
return currentGroups.map((groupName: string) => (
<span
key={groupName}
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-ink-900 text-ink-0 text-[10px] font-bold tracking-wide"
>
<span>{groupName}</span>
<button
type="button"
onClick={async () => {
if (!selectedPartnerForAssets) return;
const updatedGroups = currentGroups
.filter((g: string) => g.toLowerCase() !== groupName.toLowerCase())
.join(', ');
try {
const currentAssetIds = selectedPartnerForAssets.sharedAssets?.map((sa: any) => sa.assetId) || [];
const updated = await updateMutation.mutateAsync({
partnerId: selectedPartnerForAssets.id,
params: {
partnerGroup: updatedGroups || null as any,
sharedAssetIds: currentAssetIds,
assignedNdaId: selectedPartnerForAssets.assignedNdaId === null ? "NONE" : selectedPartnerForAssets.assignedNdaId || undefined,
assignedMsaId: selectedPartnerForAssets.assignedMsaId === null ? "NONE" : selectedPartnerForAssets.assignedMsaId || undefined,
mfaEnabled: selectedPartnerForAssets.mfaEnabled,
showEcosystemTab: selectedPartnerForAssets.showEcosystemTab,
}
});
setSelectedPartnerForAssets(updated);
fetchPartners();
success("Group removed", `"${groupName}" removed from this partner's recommendations.`);
} catch (err: any) {
error("Failed to update", err.response?.data?.error || "Something went wrong.");
}
}}
className="hover:bg-ink-0/20 rounded-full p-0.5 transition-colors cursor-pointer"
title={`Remove ${groupName}`}
>
<svg className="w-2.5 h-2.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</span>
));
})()}
{/* Add Group Dropdown */}
{(() => {
const currentGroups = selectedPartnerForAssets?.partnerGroup
? selectedPartnerForAssets.partnerGroup.split(',').map((s: string) => s.trim().toLowerCase()).filter(Boolean)
: [];
const availableGroups = assetGroups.filter(
g => !currentGroups.includes(g.name.trim().toLowerCase())
);
if (availableGroups.length === 0) return null;
return (
<select
value=""
onChange={async (e) => {
const groupName = e.target.value;
if (!groupName || !selectedPartnerForAssets) return;
const existingGroups = selectedPartnerForAssets.partnerGroup
? selectedPartnerForAssets.partnerGroup.split(',').map((s: string) => s.trim()).filter(Boolean)
: [];
const updatedGroups = [...existingGroups, groupName].join(', ');
try {
const currentAssetIds = selectedPartnerForAssets.sharedAssets?.map((sa: any) => sa.assetId) || [];
const updated = await updateMutation.mutateAsync({
partnerId: selectedPartnerForAssets.id,
params: {
partnerGroup: updatedGroups,
sharedAssetIds: currentAssetIds,
assignedNdaId: selectedPartnerForAssets.assignedNdaId === null ? "NONE" : selectedPartnerForAssets.assignedNdaId || undefined,
assignedMsaId: selectedPartnerForAssets.assignedMsaId === null ? "NONE" : selectedPartnerForAssets.assignedMsaId || undefined,
mfaEnabled: selectedPartnerForAssets.mfaEnabled,
showEcosystemTab: selectedPartnerForAssets.showEcosystemTab,
}
});
setSelectedPartnerForAssets(updated);
fetchPartners();
success("Group added", `"${groupName}" added to this partner's recommendations.`);
} catch (err: any) {
error("Failed to update", err.response?.data?.error || "Something went wrong.");
}
}}
className="text-[10px] font-bold bg-ink-0 border border-dashed border-ink-300 hover:border-ink-400 rounded-lg px-2 py-1 text-ink-600 cursor-pointer outline-none transition-colors"
>
<option value="">+ Add Group</option>
{availableGroups.map(g => (
<option key={g.id} value={g.name}>{g.name} ({g.assets.length} assets)</option>
))}
</select>
);
})()}
</div>
</div>
)}
{/* Header Action */}
<div className="flex justify-between items-center">
<h4 className="text-xs font-bold uppercase tracking-wider text-ink-500">

View File

@ -39,10 +39,10 @@ export const LegalTemplatesPage: React.FC = () => {
axiosInstance.get('/legal/documents/active/NDA').catch(() => null),
axiosInstance.get('/legal/documents/active/MSA').catch(() => null)
]);
setNdaDoc(ndaRes?.data || null);
setMsaDoc(msaRes?.data || null);
// Populate defaults for form based on active selection
const activeDoc = activeTab === 'NDA' ? ndaRes?.data : msaRes?.data;
if (activeDoc) {
@ -92,7 +92,7 @@ export const LegalTemplatesPage: React.FC = () => {
const formData = new FormData();
formData.append('file', pdfFile);
formData.append('title', `${activeTab} Agreement Template v${version}`);
const assetRes = await axiosInstance.post('/assets/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
@ -113,7 +113,7 @@ export const LegalTemplatesPage: React.FC = () => {
success(`Successfully published ${activeTab} version ${version}!`, 'The new legal template version is now active.');
setPdfFile(null);
if (fileInputRef.current) fileInputRef.current.value = '';
await fetchActiveDocuments();
} catch (err: any) {
console.error('Failed to publish legal document template:', err);
@ -132,12 +132,12 @@ export const LegalTemplatesPage: React.FC = () => {
<PageHeader
title="Legal Agreements"
subtitle="Configure active documents required during partner onboarding."
badge={
<div className="flex items-center gap-1.5 px-2.5 py-0.5 rounded-full bg-ink-100 border border-ink-200 text-[10px] font-bold text-ink-900 tracking-wider uppercase shrink-0">
<Shield className="w-3.5 h-3.5 text-ink-950" />
<span>Compliance Panel</span>
</div>
}
// badge={
// <div className="flex items-center gap-1.5 px-2.5 py-0.5 rounded-full bg-ink-100 border border-ink-200 text-[10px] font-bold text-ink-900 tracking-wider uppercase shrink-0">
// <Shield className="w-3.5 h-3.5 text-ink-950" />
// <span>Compliance Panel</span>
// </div>
// }
/>
);
@ -170,7 +170,7 @@ export const LegalTemplatesPage: React.FC = () => {
<div className="lg:col-span-1 flex flex-col gap-6">
<div className="p-4 bg-ink-0 border border-ink-200 shadow-sm rounded-xl">
<h2 className="text-base font-bold text-ink-900 mb-4">Active Configuration</h2>
{loading ? (
<div className="flex flex-col items-center justify-center py-12">
<RefreshCw className="w-6 h-6 text-ink-400 animate-spin mb-3" />
@ -261,7 +261,7 @@ export const LegalTemplatesPage: React.FC = () => {
<div className="md:col-span-3">
<label className="block text-xs font-semibold text-ink-500 mb-2 uppercase tracking-wider">PDF Template (Optional)</label>
<div
<div
onClick={() => fileInputRef.current?.click()}
className="flex items-center gap-3 p-2 rounded-lg border border-dashed border-ink-200 hover:border-ink-400 cursor-pointer bg-ink-50 hover:bg-ink-100/50 transition-colors"
>