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,
|
||||
organizationId: true,
|
||||
onboardingStatus: true,
|
||||
partnerGroup: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
assignedNdaId: true,
|
||||
@ -472,6 +473,7 @@ export class AuthService {
|
||||
mfaEnabled: true,
|
||||
organizationId: true,
|
||||
onboardingStatus: true,
|
||||
partnerGroup: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
assignedNdaId: true,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { motion } 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 { axiosInstance } from "../services/axios";
|
||||
import {
|
||||
@ -97,12 +97,13 @@ export const AssetsPage = () => {
|
||||
setAssets(assetsData);
|
||||
setSelectedAssetIds([]);
|
||||
|
||||
// Load groups for both ADMIN (for managing) and PARTNER (for recommendations)
|
||||
const groupsData = await getAssetGroups();
|
||||
setGroups(groupsData);
|
||||
|
||||
if (user?.role === "ADMIN") {
|
||||
const orgsData = await getOrganizations();
|
||||
setOrganizations(orgsData);
|
||||
|
||||
const groupsData = await getAssetGroups();
|
||||
setGroups(groupsData);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch assets data", err);
|
||||
@ -366,6 +367,24 @@ export const AssetsPage = () => {
|
||||
</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 (
|
||||
<PageLayout header={headerNode} toolbar={toolbarNode}>
|
||||
<div className="p-5 flex-1 min-h-0 overflow-y-auto">
|
||||
@ -373,43 +392,101 @@ export const AssetsPage = () => {
|
||||
<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>
|
||||
) : filteredAssets.length === 0 ? (
|
||||
<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" />
|
||||
<h3 className="text-lg font-bold text-ink-900">No assets found</h3>
|
||||
<p className="text-ink-500 text-sm mt-1">
|
||||
There are no assets matching your criteria.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4 items-start"
|
||||
>
|
||||
{filteredAssets.map((asset) => (
|
||||
<motion.div key={asset.id} variants={itemVariants} className="relative h-[410px] w-full flex flex-col">
|
||||
<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)}
|
||||
/>
|
||||
<>
|
||||
{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">
|
||||
<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>
|
||||
<p className="text-ink-500 text-sm mt-1">
|
||||
There are no assets matching your criteria.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4 items-start"
|
||||
>
|
||||
{filteredAssets.map((asset) => (
|
||||
<motion.div key={asset.id} variants={itemVariants} className="relative h-[410px] w-full flex flex-col">
|
||||
<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>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ export interface User {
|
||||
role: 'ADMIN' | 'PARTNER_USER';
|
||||
organizationId: string | null;
|
||||
onboardingStatus?: string;
|
||||
partnerGroup?: string | null;
|
||||
}
|
||||
|
||||
export interface AuthResponse {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user