mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-29 14:18:26 +02:00
minor visual tweaks to member landing
This commit is contained in:
parent
bda2aa46b6
commit
67bae76048
3 changed files with 275 additions and 281 deletions
|
@ -4,21 +4,42 @@ import { useState, useEffect } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import { ExternalLink, Globe, ShieldCheck, Search, RefreshCw, AlertCircle, Plus, Shield, ShieldOff, ChevronLeft, ChevronRight, Building2, Key, KeyRound, Fingerprint, AtSign, Copy, InfoIcon } from "lucide-react";
|
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Select,
|
||||||
TooltipContent,
|
SelectContent,
|
||||||
TooltipProvider,
|
SelectItem,
|
||||||
TooltipTrigger,
|
SelectTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
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 { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { GetUserResourcesResponse } from "@server/routers/resource/getUserResources";
|
import { GetUserResourcesResponse } from "@server/routers/resource/getUserResources";
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { useToast } from "@app/hooks/useToast";
|
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
|
// Update Resource type to include site information
|
||||||
type Resource = {
|
type Resource = {
|
||||||
|
@ -42,12 +63,18 @@ type MemberResourcesPortalProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Favicon component with fallback
|
// Favicon component with fallback
|
||||||
const ResourceFavicon = ({ domain, enabled }: { domain: string; enabled: boolean }) => {
|
const ResourceFavicon = ({
|
||||||
|
domain,
|
||||||
|
enabled
|
||||||
|
}: {
|
||||||
|
domain: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}) => {
|
||||||
const [faviconError, setFaviconError] = useState(false);
|
const [faviconError, setFaviconError] = useState(false);
|
||||||
const [faviconLoaded, setFaviconLoaded] = useState(false);
|
const [faviconLoaded, setFaviconLoaded] = useState(false);
|
||||||
|
|
||||||
// Extract domain for favicon URL
|
// Extract domain for favicon URL
|
||||||
const cleanDomain = domain.replace(/^https?:\/\//, '').split('/')[0];
|
const cleanDomain = domain.replace(/^https?:\/\//, "").split("/")[0];
|
||||||
const faviconUrl = `https://www.google.com/s2/favicons?domain=${cleanDomain}&sz=32`;
|
const faviconUrl = `https://www.google.com/s2/favicons?domain=${cleanDomain}&sz=32`;
|
||||||
|
|
||||||
const handleFaviconLoad = () => {
|
const handleFaviconLoad = () => {
|
||||||
|
@ -61,7 +88,9 @@ const ResourceFavicon = ({ domain, enabled }: { domain: string; enabled: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
if (faviconError || !enabled) {
|
if (faviconError || !enabled) {
|
||||||
return <Globe className="h-4 w-4 text-muted-foreground flex-shrink-0" />;
|
return (
|
||||||
|
<Globe className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -72,7 +101,7 @@ const ResourceFavicon = ({ domain, enabled }: { domain: string; enabled: boolean
|
||||||
<img
|
<img
|
||||||
src={faviconUrl}
|
src={faviconUrl}
|
||||||
alt={`${cleanDomain} favicon`}
|
alt={`${cleanDomain} favicon`}
|
||||||
className={`h-4 w-4 rounded-sm transition-opacity ${faviconLoaded ? 'opacity-100' : 'opacity-0'}`}
|
className={`h-4 w-4 rounded-sm transition-opacity ${faviconLoaded ? "opacity-100" : "opacity-0"}`}
|
||||||
onLoad={handleFaviconLoad}
|
onLoad={handleFaviconLoad}
|
||||||
onError={handleFaviconError}
|
onError={handleFaviconError}
|
||||||
/>
|
/>
|
||||||
|
@ -80,105 +109,22 @@ const ResourceFavicon = ({ domain, enabled }: { domain: string; enabled: boolean
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Enhanced status badge component
|
|
||||||
const StatusBadge = ({ enabled, protected: isProtected, resource }: { enabled: boolean; protected: boolean; resource: Resource }) => {
|
|
||||||
if (!enabled) {
|
|
||||||
return (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<div className="h-7 w-7 rounded-full flex items-center justify-center bg-gray-100 dark:bg-gray-800">
|
|
||||||
<div className="h-2 w-2 bg-gray-400 dark:bg-gray-500 rounded-full"></div>
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Resource Disabled</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isProtected) {
|
|
||||||
return (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<div className="h-7 w-7 rounded-full flex items-center justify-center bg-green-50 dark:bg-green-950">
|
|
||||||
<ShieldCheck className="h-3.5 w-3.5 text-green-700 dark:text-green-300" />
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="flex flex-col gap-2">
|
|
||||||
<p className="font-medium">Protected Resource</p>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<p className="text-sm font-medium text-muted-foreground">Authentication Methods:</p>
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
{resource.sso && (
|
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
|
||||||
<div className="h-6 w-6 rounded-full flex items-center justify-center bg-blue-50 dark:bg-blue-950">
|
|
||||||
<Key className="h-3 w-3 text-blue-700 dark:text-blue-300" />
|
|
||||||
</div>
|
|
||||||
<span>Single Sign-On (SSO)</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{resource.password && (
|
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
|
||||||
<div className="h-6 w-6 rounded-full flex items-center justify-center bg-purple-50 dark:bg-purple-950">
|
|
||||||
<KeyRound className="h-3 w-3 text-purple-700 dark:text-purple-300" />
|
|
||||||
</div>
|
|
||||||
<span>Password Protected</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{resource.pincode && (
|
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
|
||||||
<div className="h-6 w-6 rounded-full flex items-center justify-center bg-emerald-50 dark:bg-emerald-950">
|
|
||||||
<Fingerprint className="h-3 w-3 text-emerald-700 dark:text-emerald-300" />
|
|
||||||
</div>
|
|
||||||
<span>PIN Code</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{resource.whitelist && (
|
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
|
||||||
<div className="h-6 w-6 rounded-full flex items-center justify-center bg-amber-50 dark:bg-amber-950">
|
|
||||||
<AtSign className="h-3 w-3 text-amber-700 dark:text-amber-300" />
|
|
||||||
</div>
|
|
||||||
<span>Email Whitelist</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-7 w-7 rounded-full flex items-center justify-center bg-orange-50 dark:bg-orange-950">
|
|
||||||
<ShieldOff className="h-3.5 w-3.5 text-orange-700 dark:text-orange-300" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Resource Info component
|
// Resource Info component
|
||||||
const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
||||||
const hasAuthMethods = resource.sso || resource.password || resource.pincode || resource.whitelist;
|
const hasAuthMethods =
|
||||||
|
resource.sso ||
|
||||||
|
resource.password ||
|
||||||
|
resource.pincode ||
|
||||||
|
resource.whitelist;
|
||||||
|
|
||||||
return (
|
const infoContent = (
|
||||||
<TooltipProvider>
|
<div className="flex flex-col gap-3">
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<div className="h-7 w-7 rounded-full flex items-center justify-center bg-muted hover:bg-muted/80 transition-colors">
|
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="flex flex-col gap-3 w-[280px] p-3 bg-card border-2">
|
|
||||||
{/* Site Information */}
|
{/* Site Information */}
|
||||||
{resource.siteName && (
|
{resource.siteName && (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-medium mb-1.5">Site</div>
|
<div className="text-xs font-medium mb-1.5">Site</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Building2 className="h-4 w-4 text-foreground shrink-0" />
|
<Combine className="h-4 w-4 text-foreground shrink-0" />
|
||||||
<span className="text-sm">{resource.siteName}</span>
|
<span className="text-sm">{resource.siteName}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -186,15 +132,23 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
||||||
|
|
||||||
{/* Authentication Methods */}
|
{/* Authentication Methods */}
|
||||||
{hasAuthMethods && (
|
{hasAuthMethods && (
|
||||||
<div className={resource.siteName ? "border-t border-border pt-2" : ""}>
|
<div
|
||||||
<div className="text-xs font-medium mb-1.5">Authentication Methods</div>
|
className={
|
||||||
|
resource.siteName ? "border-t border-border pt-2" : ""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="text-xs font-medium mb-1.5">
|
||||||
|
Authentication Methods
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
{resource.sso && (
|
{resource.sso && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="h-5 w-5 rounded-full flex items-center justify-center bg-blue-50/50 dark:bg-blue-950/50">
|
<div className="h-5 w-5 rounded-full flex items-center justify-center bg-blue-50/50 dark:bg-blue-950/50">
|
||||||
<Key className="h-3 w-3 text-blue-700 dark:text-blue-300" />
|
<Key className="h-3 w-3 text-blue-700 dark:text-blue-300" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm">Single Sign-On (SSO)</span>
|
<span className="text-sm">
|
||||||
|
Single Sign-On (SSO)
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{resource.password && (
|
{resource.password && (
|
||||||
|
@ -202,7 +156,9 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
||||||
<div className="h-5 w-5 rounded-full flex items-center justify-center bg-purple-50/50 dark:bg-purple-950/50">
|
<div className="h-5 w-5 rounded-full flex items-center justify-center bg-purple-50/50 dark:bg-purple-950/50">
|
||||||
<KeyRound className="h-3 w-3 text-purple-700 dark:text-purple-300" />
|
<KeyRound className="h-3 w-3 text-purple-700 dark:text-purple-300" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm">Password Protected</span>
|
<span className="text-sm">
|
||||||
|
Password Protected
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{resource.pincode && (
|
{resource.pincode && (
|
||||||
|
@ -227,39 +183,21 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
||||||
|
|
||||||
{/* Resource Status - if disabled */}
|
{/* Resource Status - if disabled */}
|
||||||
{!resource.enabled && (
|
{!resource.enabled && (
|
||||||
<div className={`${(resource.siteName || hasAuthMethods) ? "border-t border-border pt-2" : ""}`}>
|
<div
|
||||||
|
className={`${resource.siteName || hasAuthMethods ? "border-t border-border pt-2" : ""}`}
|
||||||
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<AlertCircle className="h-4 w-4 text-destructive shrink-0" />
|
<AlertCircle className="h-4 w-4 text-destructive shrink-0" />
|
||||||
<span className="text-sm text-destructive">Resource Disabled</span>
|
<span className="text-sm text-destructive">
|
||||||
|
Resource Disabled
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Site badge component
|
|
||||||
const SiteBadge = ({ resource }: { resource: Resource }) => {
|
|
||||||
if (!resource.siteName) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<div className="h-7 w-7 rounded-full flex items-center justify-center bg-muted/60 dark:bg-muted/80">
|
|
||||||
<Building2 className="h-3.5 w-3.5 text-muted-foreground" />
|
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>{resource.siteName}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return <InfoPopup>{infoContent}</InfoPopup>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pagination component
|
// Pagination component
|
||||||
|
@ -300,7 +238,8 @@ const PaginationControls = ({
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => {
|
{Array.from({ length: totalPages }, (_, i) => i + 1).map(
|
||||||
|
(page) => {
|
||||||
// Show first page, last page, current page, and 2 pages around current
|
// Show first page, last page, current page, and 2 pages around current
|
||||||
const showPage =
|
const showPage =
|
||||||
page === 1 ||
|
page === 1 ||
|
||||||
|
@ -309,13 +248,17 @@ const PaginationControls = ({
|
||||||
|
|
||||||
const showEllipsis =
|
const showEllipsis =
|
||||||
(page === 2 && currentPage > 4) ||
|
(page === 2 && currentPage > 4) ||
|
||||||
(page === totalPages - 1 && currentPage < totalPages - 3);
|
(page === totalPages - 1 &&
|
||||||
|
currentPage < totalPages - 3);
|
||||||
|
|
||||||
if (!showPage && !showEllipsis) return null;
|
if (!showPage && !showEllipsis) return null;
|
||||||
|
|
||||||
if (showEllipsis) {
|
if (showEllipsis) {
|
||||||
return (
|
return (
|
||||||
<span key={page} className="px-2 text-muted-foreground">
|
<span
|
||||||
|
key={page}
|
||||||
|
className="px-2 text-muted-foreground"
|
||||||
|
>
|
||||||
...
|
...
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
@ -324,7 +267,11 @@ const PaginationControls = ({
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
key={page}
|
key={page}
|
||||||
variant={currentPage === page ? "default" : "outline"}
|
variant={
|
||||||
|
currentPage === page
|
||||||
|
? "default"
|
||||||
|
: "outline"
|
||||||
|
}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onPageChange(page)}
|
onClick={() => onPageChange(page)}
|
||||||
className="w-8 h-8 p-0"
|
className="w-8 h-8 p-0"
|
||||||
|
@ -332,7 +279,8 @@ const PaginationControls = ({
|
||||||
{page}
|
{page}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
})}
|
}
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
@ -352,7 +300,7 @@ const PaginationControls = ({
|
||||||
|
|
||||||
// Loading skeleton component
|
// Loading skeleton component
|
||||||
const ResourceCardSkeleton = () => (
|
const ResourceCardSkeleton = () => (
|
||||||
<Card className="rounded-lg bg-card text-card-foreground border-2 flex flex-col w-full animate-pulse">
|
<Card className="rounded-lg bg-card text-card-foreground flex flex-col w-full animate-pulse">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="h-6 bg-muted rounded w-3/4"></div>
|
<div className="h-6 bg-muted rounded w-3/4"></div>
|
||||||
|
@ -377,7 +325,9 @@ const ResourceCardSkeleton = () => (
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalProps) {
|
export default function MemberResourcesPortal({
|
||||||
|
orgId
|
||||||
|
}: MemberResourcesPortalProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
const api = createApiClient({ env });
|
const api = createApiClient({ env });
|
||||||
|
@ -416,7 +366,9 @@ export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalPr
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error fetching user resources:", err);
|
console.error("Error fetching user resources:", err);
|
||||||
setError("Failed to load resources. Please check your connection and try again.");
|
setError(
|
||||||
|
"Failed to load resources. Please check your connection and try again."
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
|
@ -429,9 +381,14 @@ export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalPr
|
||||||
|
|
||||||
// Filter and sort resources
|
// Filter and sort resources
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let filtered = resources.filter(resource =>
|
const filtered = resources.filter(
|
||||||
resource.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
(resource) =>
|
||||||
resource.domain.toLowerCase().includes(searchQuery.toLowerCase())
|
resource.name
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchQuery.toLowerCase()) ||
|
||||||
|
resource.domain
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchQuery.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sort resources
|
// Sort resources
|
||||||
|
@ -467,11 +424,14 @@ export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalPr
|
||||||
// Calculate pagination
|
// Calculate pagination
|
||||||
const totalPages = Math.ceil(filteredResources.length / itemsPerPage);
|
const totalPages = Math.ceil(filteredResources.length / itemsPerPage);
|
||||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
const paginatedResources = filteredResources.slice(startIndex, startIndex + itemsPerPage);
|
const paginatedResources = filteredResources.slice(
|
||||||
|
startIndex,
|
||||||
|
startIndex + itemsPerPage
|
||||||
|
);
|
||||||
|
|
||||||
const handleOpenResource = (resource: Resource) => {
|
const handleOpenResource = (resource: Resource) => {
|
||||||
// Open the resource in a new tab
|
// Open the resource in a new tab
|
||||||
window.open(resource.domain, '_blank');
|
window.open(resource.domain, "_blank");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
|
@ -485,7 +445,7 @@ export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalPr
|
||||||
const handlePageChange = (page: number) => {
|
const handlePageChange = (page: number) => {
|
||||||
setCurrentPage(page);
|
setCurrentPage(page);
|
||||||
// Scroll to top when page changes
|
// Scroll to top when page changes
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
@ -523,7 +483,7 @@ export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalPr
|
||||||
title="Resources"
|
title="Resources"
|
||||||
description="Resources you have access to in this organization"
|
description="Resources you have access to in this organization"
|
||||||
/>
|
/>
|
||||||
<Card className="border-destructive/50 bg-destructive/5 dark:bg-destructive/10">
|
<Card>
|
||||||
<CardContent className="flex flex-col items-center justify-center py-20 text-center">
|
<CardContent className="flex flex-col items-center justify-center py-20 text-center">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<AlertCircle className="h-16 w-16 text-destructive/60" />
|
<AlertCircle className="h-16 w-16 text-destructive/60" />
|
||||||
|
@ -564,7 +524,7 @@ export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalPr
|
||||||
placeholder="Search resources..."
|
placeholder="Search resources..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="w-full pl-8"
|
className="w-full pl-8 bg-card"
|
||||||
/>
|
/>
|
||||||
<Search className="h-4 w-4 absolute left-2 top-1/2 transform -translate-y-1/2 text-muted-foreground" />
|
<Search className="h-4 w-4 absolute left-2 top-1/2 transform -translate-y-1/2 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -572,16 +532,28 @@ export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalPr
|
||||||
{/* Sort */}
|
{/* Sort */}
|
||||||
<div className="w-full sm:w-36">
|
<div className="w-full sm:w-36">
|
||||||
<Select value={sortBy} onValueChange={setSortBy}>
|
<Select value={sortBy} onValueChange={setSortBy}>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="bg-card">
|
||||||
<SelectValue placeholder="Sort by..." />
|
<SelectValue placeholder="Sort by..." />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="name-asc">Name A-Z</SelectItem>
|
<SelectItem value="name-asc">
|
||||||
<SelectItem value="name-desc">Name Z-A</SelectItem>
|
Name A-Z
|
||||||
<SelectItem value="domain-asc">Domain A-Z</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="domain-desc">Domain Z-A</SelectItem>
|
<SelectItem value="name-desc">
|
||||||
<SelectItem value="status-enabled">Enabled First</SelectItem>
|
Name Z-A
|
||||||
<SelectItem value="status-disabled">Disabled First</SelectItem>
|
</SelectItem>
|
||||||
|
<SelectItem value="domain-asc">
|
||||||
|
Domain A-Z
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="domain-desc">
|
||||||
|
Domain Z-A
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="status-enabled">
|
||||||
|
Enabled First
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="status-disabled">
|
||||||
|
Disabled First
|
||||||
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
@ -595,7 +567,9 @@ export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalPr
|
||||||
disabled={refreshing}
|
disabled={refreshing}
|
||||||
className="gap-2 shrink-0"
|
className="gap-2 shrink-0"
|
||||||
>
|
>
|
||||||
<RefreshCw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
|
<RefreshCw
|
||||||
|
className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`}
|
||||||
|
/>
|
||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -603,7 +577,7 @@ export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalPr
|
||||||
{/* Resources Content */}
|
{/* Resources Content */}
|
||||||
{filteredResources.length === 0 ? (
|
{filteredResources.length === 0 ? (
|
||||||
/* Enhanced Empty State */
|
/* Enhanced Empty State */
|
||||||
<Card className="border-muted/50 bg-muted/5 dark:bg-muted/10">
|
<Card>
|
||||||
<CardContent className="flex flex-col items-center justify-center py-20 text-center">
|
<CardContent className="flex flex-col items-center justify-center py-20 text-center">
|
||||||
<div className="mb-8 p-4 rounded-full bg-muted/20 dark:bg-muted/30">
|
<div className="mb-8 p-4 rounded-full bg-muted/20 dark:bg-muted/30">
|
||||||
{searchQuery ? (
|
{searchQuery ? (
|
||||||
|
@ -613,13 +587,14 @@ export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalPr
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-2xl font-semibold text-foreground mb-3">
|
<h3 className="text-2xl font-semibold text-foreground mb-3">
|
||||||
{searchQuery ? "No Resources Found" : "No Resources Available"}
|
{searchQuery
|
||||||
|
? "No Resources Found"
|
||||||
|
: "No Resources Available"}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-muted-foreground max-w-lg text-base mb-6">
|
<p className="text-muted-foreground max-w-lg text-base mb-6">
|
||||||
{searchQuery
|
{searchQuery
|
||||||
? `No resources match "${searchQuery}". Try adjusting your search terms or clearing the search to see all resources.`
|
? `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."
|
: "You don't have access to any resources yet. Contact your administrator to get access to resources you need."}
|
||||||
}
|
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row gap-3">
|
<div className="flex flex-col sm:flex-row gap-3">
|
||||||
{searchQuery ? (
|
{searchQuery ? (
|
||||||
|
@ -637,7 +612,9 @@ export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalPr
|
||||||
disabled={refreshing}
|
disabled={refreshing}
|
||||||
className="gap-2"
|
className="gap-2"
|
||||||
>
|
>
|
||||||
<RefreshCw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
|
<RefreshCw
|
||||||
|
className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`}
|
||||||
|
/>
|
||||||
Refresh Resources
|
Refresh Resources
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
@ -649,12 +626,15 @@ export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalPr
|
||||||
{/* Resources Grid */}
|
{/* Resources Grid */}
|
||||||
<div className="grid gap-5 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4 auto-cols-fr">
|
<div className="grid gap-5 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4 auto-cols-fr">
|
||||||
{paginatedResources.map((resource) => (
|
{paginatedResources.map((resource) => (
|
||||||
<Card key={resource.resourceId} className="rounded-lg bg-card text-card-foreground hover:shadow-lg transition-all duration-200 border-2 hover:border-primary/20 dark:hover:border-primary/30 flex flex-col w-full group min-h-[200px]">
|
<Card key={resource.resourceId}>
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div className="flex items-center min-w-0 flex-1 gap-3 overflow-hidden">
|
<div className="flex items-center min-w-0 flex-1 gap-3 overflow-hidden">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<ResourceFavicon domain={resource.domain} enabled={resource.enabled} />
|
<ResourceFavicon
|
||||||
|
domain={resource.domain}
|
||||||
|
enabled={resource.enabled}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
@ -664,7 +644,9 @@ export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalPr
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p className="max-w-xs break-words">{resource.name}</p>
|
<p className="max-w-xs break-words">
|
||||||
|
{resource.name}
|
||||||
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
@ -677,21 +659,29 @@ export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalPr
|
||||||
|
|
||||||
<div className="flex items-center gap-2 mt-3">
|
<div className="flex items-center gap-2 mt-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleOpenResource(resource)}
|
onClick={() =>
|
||||||
|
handleOpenResource(resource)
|
||||||
|
}
|
||||||
className="text-sm text-muted-foreground font-medium text-left truncate flex-1"
|
className="text-sm text-muted-foreground font-medium text-left truncate flex-1"
|
||||||
disabled={!resource.enabled}
|
disabled={!resource.enabled}
|
||||||
>
|
>
|
||||||
{resource.domain.replace(/^https?:\/\//, '')}
|
{resource.domain.replace(
|
||||||
|
/^https?:\/\//,
|
||||||
|
""
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8"
|
className="h-8 w-8 text-muted-foreground"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(resource.domain);
|
navigator.clipboard.writeText(
|
||||||
|
resource.domain
|
||||||
|
);
|
||||||
toast({
|
toast({
|
||||||
title: "Copied to clipboard",
|
title: "Copied to clipboard",
|
||||||
description: "Resource URL has been copied to your clipboard.",
|
description:
|
||||||
|
"Resource URL has been copied to your clipboard.",
|
||||||
duration: 2000
|
duration: 2000
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
@ -703,7 +693,9 @@ export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalPr
|
||||||
|
|
||||||
<div className="p-6 pt-0 mt-auto">
|
<div className="p-6 pt-0 mt-auto">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleOpenResource(resource)}
|
onClick={() =>
|
||||||
|
handleOpenResource(resource)
|
||||||
|
}
|
||||||
className="w-full h-9 transition-all group-hover:shadow-sm"
|
className="w-full h-9 transition-all group-hover:shadow-sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
|
@ -62,11 +62,7 @@ export default async function OrgPage(props: OrgPageProps) {
|
||||||
return (
|
return (
|
||||||
<UserProvider user={user}>
|
<UserProvider user={user}>
|
||||||
<Layout orgId={orgId} navItems={[]} orgs={orgs}>
|
<Layout orgId={orgId} navItems={[]} orgs={orgs}>
|
||||||
{overview && (
|
{overview && <MemberResourcesPortal orgId={orgId} />}
|
||||||
<div className="w-full px-4 py-6">
|
|
||||||
<MemberResourcesPortal orgId={orgId} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Layout>
|
</Layout>
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -11,11 +11,12 @@ import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
interface InfoPopupProps {
|
interface InfoPopupProps {
|
||||||
text?: string;
|
text?: string;
|
||||||
info: string;
|
info?: string;
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InfoPopup({ text, info, trigger }: InfoPopupProps) {
|
export function InfoPopup({ text, info, trigger, children }: InfoPopupProps) {
|
||||||
const defaultTrigger = (
|
const defaultTrigger = (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
@ -35,7 +36,12 @@ export function InfoPopup({ text, info, trigger }: InfoPopupProps) {
|
||||||
{trigger ?? defaultTrigger}
|
{trigger ?? defaultTrigger}
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-80">
|
<PopoverContent className="w-80">
|
||||||
<p className="text-sm text-muted-foreground">{info}</p>
|
{children ||
|
||||||
|
(info && (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{info}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue