production V2
This commit is contained in:
parent
61f3bd8057
commit
8687d72394
@ -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>
|
||||
)}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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>
|
||||
))}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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"
|
||||
>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user