"use client"; import { useState, useEffect } from "react"; import { useTranslations } from "next-intl"; import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { ExternalLink, Globe, Search, RefreshCw, AlertCircle, ChevronLeft, ChevronRight, Key, KeyRound, Fingerprint, AtSign, Copy, InfoIcon, Combine } from "lucide-react"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { GetUserResourcesResponse } from "@server/routers/resource/getUserResources"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { useToast } from "@app/hooks/useToast"; import { InfoPopup } from "@/components/ui/info-popup"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; // Update Resource type to include site information type Resource = { resourceId: number; name: string; domain: string; enabled: boolean; protected: boolean; protocol: string; // Auth method fields sso?: boolean; password?: boolean; pincode?: boolean; whitelist?: boolean; // Site information siteName?: string | null; }; type MemberResourcesPortalProps = { orgId: string; }; // Favicon component with fallback const ResourceFavicon = ({ domain, enabled }: { domain: string; enabled: boolean; }) => { const [faviconError, setFaviconError] = useState(false); const [faviconLoaded, setFaviconLoaded] = useState(false); // Extract domain for favicon URL const cleanDomain = domain.replace(/^https?:\/\//, "").split("/")[0]; const faviconUrl = `https://www.google.com/s2/favicons?domain=${cleanDomain}&sz=32`; const handleFaviconLoad = () => { setFaviconLoaded(true); setFaviconError(false); }; const handleFaviconError = () => { setFaviconError(true); setFaviconLoaded(false); }; if (faviconError || !enabled) { return ( ); } return (
{!faviconLoaded && (
)} {`${cleanDomain}
); }; // Resource Info component const ResourceInfo = ({ resource }: { resource: Resource }) => { const hasAuthMethods = resource.sso || resource.password || resource.pincode || resource.whitelist; const infoContent = (
{/* Site Information */} {resource.siteName && (
Site
{resource.siteName}
)} {/* Authentication Methods */} {hasAuthMethods && (
Authentication Methods
{resource.sso && (
Single Sign-On (SSO)
)} {resource.password && (
Password Protected
)} {resource.pincode && (
PIN Code
)} {resource.whitelist && (
Email Whitelist
)}
)} {/* Resource Status - if disabled */} {!resource.enabled && (
Resource Disabled
)}
); return {infoContent}; }; // Pagination component const PaginationControls = ({ currentPage, totalPages, onPageChange, totalItems, itemsPerPage }: { currentPage: number; totalPages: number; onPageChange: (page: number) => void; totalItems: number; itemsPerPage: number; }) => { const startItem = (currentPage - 1) * itemsPerPage + 1; const endItem = Math.min(currentPage * itemsPerPage, totalItems); if (totalPages <= 1) return null; return (
Showing {startItem}-{endItem} of {totalItems} resources
{Array.from({ length: totalPages }, (_, i) => i + 1).map( (page) => { // Show first page, last page, current page, and 2 pages around current const showPage = page === 1 || page === totalPages || Math.abs(page - currentPage) <= 1; const showEllipsis = (page === 2 && currentPage > 4) || (page === totalPages - 1 && currentPage < totalPages - 3); if (!showPage && !showEllipsis) return null; if (showEllipsis) { return ( ... ); } return ( ); } )}
); }; // Loading skeleton component const ResourceCardSkeleton = () => (
); export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalProps) { const t = useTranslations(); const { env } = useEnvContext(); const api = createApiClient({ env }); const { toast } = useToast(); const [resources, setResources] = useState([]); const [filteredResources, setFilteredResources] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [sortBy, setSortBy] = useState("name-asc"); const [refreshing, setRefreshing] = useState(false); // Pagination state const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 12; // 3x4 grid on desktop const fetchUserResources = async (isRefresh = false) => { try { if (isRefresh) { setRefreshing(true); } else { setLoading(true); } setError(null); const response = await api.get( `/org/${orgId}/user-resources` ); if (response.data.success) { setResources(response.data.data.resources); setFilteredResources(response.data.data.resources); } else { setError("Failed to load resources"); } } catch (err) { console.error("Error fetching user resources:", err); setError( "Failed to load resources. Please check your connection and try again." ); } finally { setLoading(false); setRefreshing(false); } }; useEffect(() => { fetchUserResources(); }, [orgId, api]); // Filter and sort resources useEffect(() => { const filtered = resources.filter( (resource) => resource.name .toLowerCase() .includes(searchQuery.toLowerCase()) || resource.domain .toLowerCase() .includes(searchQuery.toLowerCase()) ); // Sort resources filtered.sort((a, b) => { switch (sortBy) { case "name-asc": return a.name.localeCompare(b.name); case "name-desc": return b.name.localeCompare(a.name); case "domain-asc": return a.domain.localeCompare(b.domain); case "domain-desc": return b.domain.localeCompare(a.domain); case "status-enabled": // Enabled first, then protected vs unprotected if (a.enabled !== b.enabled) return b.enabled ? 1 : -1; return b.protected ? 1 : -1; case "status-disabled": // Disabled first, then unprotected vs protected if (a.enabled !== b.enabled) return a.enabled ? 1 : -1; return a.protected ? 1 : -1; default: return a.name.localeCompare(b.name); } }); setFilteredResources(filtered); // Reset to first page when search/sort changes setCurrentPage(1); }, [resources, searchQuery, sortBy]); // Calculate pagination const totalPages = Math.ceil(filteredResources.length / itemsPerPage); const startIndex = (currentPage - 1) * itemsPerPage; const paginatedResources = filteredResources.slice( startIndex, startIndex + itemsPerPage ); const handleOpenResource = (resource: Resource) => { // Open the resource in a new tab window.open(resource.domain, "_blank"); }; const handleRefresh = () => { fetchUserResources(true); }; const handleRetry = () => { fetchUserResources(); }; const handlePageChange = (page: number) => { setCurrentPage(page); // Scroll to top when page changes window.scrollTo({ top: 0, behavior: "smooth" }); }; if (loading) { return (
{/* Search and Sort Controls - Skeleton */}
{/* Loading Skeletons */}
{Array.from({ length: 12 }).map((_, index) => ( ))}
); } if (error) { return (

Unable to Load Resources

{error}

); } return (
{/* Search and Sort Controls with Refresh */}
{/* Search */}
setSearchQuery(e.target.value)} className="w-full pl-8 bg-card" />
{/* Sort */}
{/* Refresh Button */}
{/* Resources Content */} {filteredResources.length === 0 ? ( /* Enhanced Empty State */
{searchQuery ? ( ) : ( )}

{searchQuery ? "No Resources Found" : "No Resources Available"}

{searchQuery ? `No resources match "${searchQuery}". Try adjusting your search terms or clearing the search to see all resources.` : "You don't have access to any resources yet. Contact your administrator to get access to resources you need."}

{searchQuery ? ( ) : ( )}
) : ( <> {/* Resources Grid */}
{paginatedResources.map((resource) => (
{resource.name}

{resource.name}

))}
{/* Pagination Controls */} )}
); }