Feat: Implement curated recommendations engine matching partnerGroup to AssetGroups
This commit is contained in:
parent
5254e347a8
commit
d30d8d6b1e
@ -441,6 +441,7 @@ export class AuthService {
|
|||||||
mfaEnabled: true,
|
mfaEnabled: true,
|
||||||
organizationId: true,
|
organizationId: true,
|
||||||
onboardingStatus: true,
|
onboardingStatus: true,
|
||||||
|
partnerGroup: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
assignedNdaId: true,
|
assignedNdaId: true,
|
||||||
@ -472,6 +473,7 @@ export class AuthService {
|
|||||||
mfaEnabled: true,
|
mfaEnabled: true,
|
||||||
organizationId: true,
|
organizationId: true,
|
||||||
onboardingStatus: true,
|
onboardingStatus: true,
|
||||||
|
partnerGroup: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
assignedNdaId: true,
|
assignedNdaId: true,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import type { Variants } from "framer-motion";
|
import type { Variants } from "framer-motion";
|
||||||
import { UploadCloud, Search, File, CheckCircle, Share2, Folder } from "lucide-react";
|
import { UploadCloud, Search, File, CheckCircle, Share2, Folder, Sparkles } from "lucide-react";
|
||||||
import { useAuthStore } from "../hooks/use-auth";
|
import { useAuthStore } from "../hooks/use-auth";
|
||||||
import { axiosInstance } from "../services/axios";
|
import { axiosInstance } from "../services/axios";
|
||||||
import {
|
import {
|
||||||
@ -97,12 +97,13 @@ export const AssetsPage = () => {
|
|||||||
setAssets(assetsData);
|
setAssets(assetsData);
|
||||||
setSelectedAssetIds([]);
|
setSelectedAssetIds([]);
|
||||||
|
|
||||||
|
// Load groups for both ADMIN (for managing) and PARTNER (for recommendations)
|
||||||
|
const groupsData = await getAssetGroups();
|
||||||
|
setGroups(groupsData);
|
||||||
|
|
||||||
if (user?.role === "ADMIN") {
|
if (user?.role === "ADMIN") {
|
||||||
const orgsData = await getOrganizations();
|
const orgsData = await getOrganizations();
|
||||||
setOrganizations(orgsData);
|
setOrganizations(orgsData);
|
||||||
|
|
||||||
const groupsData = await getAssetGroups();
|
|
||||||
setGroups(groupsData);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to fetch assets data", err);
|
console.error("Failed to fetch assets data", err);
|
||||||
@ -366,6 +367,24 @@ export const AssetsPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Find recommended assets based on user's partnerGroup matching any AssetGroup.name
|
||||||
|
const recommendedGroup = user?.partnerGroup && user.role === "PARTNER_USER"
|
||||||
|
? groups.find(g => g.name.trim().toLowerCase() === user.partnerGroup?.trim().toLowerCase())
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Only recommend assets that the partner actually has permission to access
|
||||||
|
const recommendedAssets = recommendedGroup
|
||||||
|
? recommendedGroup.assets.filter(recAsset =>
|
||||||
|
assets.some(allAsset => allAsset.id === recAsset.id)
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const showRecommendations =
|
||||||
|
user?.role === "PARTNER_USER" &&
|
||||||
|
recommendedAssets.length > 0 &&
|
||||||
|
searchQuery === "" &&
|
||||||
|
selectedCategory === "ALL";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout header={headerNode} toolbar={toolbarNode}>
|
<PageLayout header={headerNode} toolbar={toolbarNode}>
|
||||||
<div className="p-5 flex-1 min-h-0 overflow-y-auto">
|
<div className="p-5 flex-1 min-h-0 overflow-y-auto">
|
||||||
@ -373,7 +392,63 @@ export const AssetsPage = () => {
|
|||||||
<div className="py-20 flex justify-center items-center">
|
<div className="py-20 flex justify-center items-center">
|
||||||
<div className="w-8 h-8 border-4 border-ink-900/30 border-t-ink-900 rounded-full animate-spin" />
|
<div className="w-8 h-8 border-4 border-ink-900/30 border-t-ink-900 rounded-full animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
) : filteredAssets.length === 0 ? (
|
) : (
|
||||||
|
<>
|
||||||
|
{showRecommendations && (
|
||||||
|
<div className="mb-8 p-6 bg-gradient-to-r from-ink-950 via-slate-900 to-ink-950 border border-ink-800 rounded-2xl relative overflow-hidden shadow-xl">
|
||||||
|
<div className="absolute top-0 right-0 w-64 h-64 bg-ink-800/10 rounded-full blur-3xl -mr-16 -mt-16 pointer-events-none" />
|
||||||
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 relative z-10 mb-6">
|
||||||
|
<div>
|
||||||
|
<div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-amber-500/10 border border-amber-500/20 text-xs font-bold text-amber-400 uppercase tracking-wider mb-3">
|
||||||
|
<Sparkles className="w-3.5 h-3.5 animate-pulse" />
|
||||||
|
<span>Curated for {user?.partnerGroup}</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-ink-0 font-sans tracking-tight">Recommended for You</h2>
|
||||||
|
<p className="text-slate-400 text-sm mt-1 max-w-xl">
|
||||||
|
Hand-picked digital assets, guides, and templates selected specifically for your partner category.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid of Recommended Assets */}
|
||||||
|
<motion.div
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="show"
|
||||||
|
className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4"
|
||||||
|
>
|
||||||
|
{recommendedAssets.map((asset) => (
|
||||||
|
<motion.div key={`rec-${asset.id}`} variants={itemVariants} className="relative h-[410px] w-full flex flex-col bg-slate-950/40 rounded-xl overflow-hidden border border-slate-800/60 shadow-lg">
|
||||||
|
<AssetCard
|
||||||
|
asset={asset}
|
||||||
|
user={user}
|
||||||
|
activeMenuId={activeMenuId}
|
||||||
|
setActiveMenuId={setActiveMenuId}
|
||||||
|
onViewDetails={openDetailsModal}
|
||||||
|
onEdit={openEditModal}
|
||||||
|
onShare={openShareModal}
|
||||||
|
onDelete={handleDeleteAsset}
|
||||||
|
onOpenViewer={openViewerModal}
|
||||||
|
onDownload={handleDownload}
|
||||||
|
onRequestDownload={handleRequestDownload}
|
||||||
|
isSelected={selectedAssetIds.includes(asset.id)}
|
||||||
|
onToggleSelect={handleToggleSelectAsset}
|
||||||
|
isExpanded={expandedAssetId === asset.id}
|
||||||
|
onToggleExpand={() => setExpandedAssetId(expandedAssetId === asset.id ? null : asset.id)}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showRecommendations && (
|
||||||
|
<div className="mb-6">
|
||||||
|
<h3 className="text-sm font-bold text-ink-500 uppercase tracking-wider">All Resources</h3>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{filteredAssets.length === 0 ? (
|
||||||
<div className="py-12 text-center bg-ink-0 border border-ink-200 rounded-xl">
|
<div className="py-12 text-center bg-ink-0 border border-ink-200 rounded-xl">
|
||||||
<File className="w-12 h-12 text-ink-300 mx-auto mb-4" />
|
<File className="w-12 h-12 text-ink-300 mx-auto mb-4" />
|
||||||
<h3 className="text-lg font-bold text-ink-900">No assets found</h3>
|
<h3 className="text-lg font-bold text-ink-900">No assets found</h3>
|
||||||
@ -411,6 +486,8 @@ export const AssetsPage = () => {
|
|||||||
))}
|
))}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modals Container */}
|
{/* Modals Container */}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ export interface User {
|
|||||||
role: 'ADMIN' | 'PARTNER_USER';
|
role: 'ADMIN' | 'PARTNER_USER';
|
||||||
organizationId: string | null;
|
organizationId: string | null;
|
||||||
onboardingStatus?: string;
|
onboardingStatus?: string;
|
||||||
|
partnerGroup?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthResponse {
|
export interface AuthResponse {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user