"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"; import CopyToClipboard from "@app/components/CopyToClipboard"; // 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 SiteResource = { siteResourceId: number; name: string; destination: string; mode: string; protocol: string | null; ssl: boolean; fullDomain: string | null; enabled: boolean; alias: string | null; aliasAddress: string | null; type: "site"; }; 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 t = useTranslations(); const hasAuthMethods = resource.sso || resource.password || resource.pincode || resource.whitelist; const hasAnyInfo = Boolean(resource.siteName) || Boolean(hasAuthMethods) || !resource.enabled; if (!hasAnyInfo) return null; const infoContent = (
{/* Site Information */} {resource.siteName && (
{t("site")}
{resource.siteName}
)} {/* Authentication Methods */} {hasAuthMethods && (
{t("memberPortalAuthMethods")}
{resource.sso && (
{t("memberPortalSso")}
)} {resource.password && (
{t("memberPortalPasswordProtected")}
)} {resource.pincode && (
{t("memberPortalPinCode")}
)} {resource.whitelist && (
{t("memberPortalEmailWhitelist")}
)}
)} {/* Resource Status - if disabled */} {!resource.enabled && (
{t("memberPortalResourceDisabled")}
)}
); return {infoContent}; }; // Pagination component const PaginationControls = ({ currentPage, totalPages, onPageChange, totalItems, itemsPerPage }: { currentPage: number; totalPages: number; onPageChange: (page: number) => void; totalItems: number; itemsPerPage: number; }) => { const t = useTranslations(); const startItem = (currentPage - 1) * itemsPerPage + 1; const endItem = Math.min(currentPage * itemsPerPage, totalItems); if (totalPages <= 1) return null; return (
{t("memberPortalShowingResources", { start: startItem, end: endItem, total: totalItems })}
{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 [siteResources, setSiteResources] = useState([]); const [filteredResources, setFilteredResources] = useState([]); const [filteredSiteResources, setFilteredSiteResources] = useState< SiteResource[] >([]); 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); setSiteResources(response.data.data.siteResources || []); setFilteredResources(response.data.data.resources); setFilteredSiteResources( response.data.data.siteResources || [] ); } else { setError(t("memberPortalFailedToLoad")); } } catch (err) { console.error("Error fetching user resources:", err); setError(t("memberPortalFailedToLoadDescription")); } 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); // Filter and sort site resources const filteredSites = siteResources.filter( (resource) => resource.name .toLowerCase() .includes(searchQuery.toLowerCase()) || resource.destination .toLowerCase() .includes(searchQuery.toLowerCase()) ); // Sort site resources filteredSites.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": case "domain-desc": // Sort by destination for site resources const destCompare = sortBy === "domain-asc" ? a.destination.localeCompare(b.destination) : b.destination.localeCompare(a.destination); return destCompare; case "status-enabled": return b.enabled ? 1 : -1; case "status-disabled": return a.enabled ? 1 : -1; default: return a.name.localeCompare(b.name); } }); setFilteredSiteResources(filteredSites); // Reset to first page when search/sort changes setCurrentPage(1); }, [resources, siteResources, searchQuery, sortBy]); // Calculate pagination const totalItems = filteredResources.length + filteredSiteResources.length; const totalPages = Math.ceil(totalItems / itemsPerPage); const startIndex = (currentPage - 1) * itemsPerPage; const paginatedResources = filteredResources.slice( startIndex, startIndex + itemsPerPage ); const remainingSlots = itemsPerPage - paginatedResources.length; const paginatedSiteResources = remainingSlots > 0 ? filteredSiteResources.slice( Math.max(0, startIndex - filteredResources.length), Math.max(0, startIndex - filteredResources.length) + remainingSlots ) : []; 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 (

{t("memberPortalUnableToLoad")}

{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 && filteredSiteResources.length === 0 ? ( /* Enhanced Empty State */
{searchQuery ? ( ) : ( )}

{searchQuery ? t("memberPortalNoResourcesFound") : t("memberPortalNoResourcesAvailable")}

{searchQuery ? t("memberPortalNoResourcesMatchSearch", { query: searchQuery }) : t("memberPortalNoResourcesAccess")}

{searchQuery ? ( ) : ( )}
) : ( <> {/* Public Resources Section */} {paginatedResources.length > 0 && ( <>

{t("memberPortalPublicResources")}

{t( "memberPortalPublicResourcesDescription" )}

{paginatedResources.map((resource) => (
{ resource.name }

{ resource.name }

))}
)} {/* Private Resources (Site Resources) Section */} {paginatedSiteResources.length > 0 && ( <>

{t("memberPortalPrivateResources")}

{t( "memberPortalPrivateResourcesDescription" )}

{paginatedSiteResources.map((siteResource) => (
{ siteResource.name }

{ siteResource.name }

{t( "memberPortalResourceDetails" )}
{t( "memberPortalMode" )} : { siteResource.mode }
{siteResource.protocol && (
{t( "protocol" )} : { siteResource.protocol }
)}
{t( "memberPortalDestination" )} : { siteResource.destination }
{siteResource.alias && (
{t( "memberPortalAlias" )} : { siteResource.alias }
)}
{t( "status" )} : {siteResource.enabled ? t( "enabled" ) : t( "disabled" )}
{siteResource.mode === "http" && siteResource.fullDomain ? ( /* HTTP mode - show as clickable link */ ) : siteResource.alias ? ( <> {/* Alias as primary */}
{ siteResource.alias }
{/* Destination as secondary */}
{ siteResource.destination }
) : ( /* Destination as primary when no alias */
{ siteResource.destination }
)}
{siteResource.mode === "http" && siteResource.fullDomain ? ( ) : null}
{t( "memberPortalRequiresClientConnection" )}
))}
)} {/* Pagination Controls */} )}
); }