Feat: Implement curated recommendations engine matching partnerGroup to AssetGroups

This commit is contained in:
kenilkb 2026-07-16 12:04:56 +05:30
parent 5254e347a8
commit d30d8d6b1e
3 changed files with 119 additions and 39 deletions

View File

@ -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,

View File

@ -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>

View File

@ -4,6 +4,7 @@ export interface User {
role: 'ADMIN' | 'PARTNER_USER';
organizationId: string | null;
onboardingStatus?: string;
partnerGroup?: string | null;
}
export interface AuthResponse {