refactor: enhance organization card display with status badges and improved layout

This commit is contained in:
Arunavo Ray
2025-06-17 16:52:15 +05:30
parent 818ba77693
commit 13d4257c4f

View File

@@ -1,14 +1,14 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Plus, RefreshCw, Building2 } from "lucide-react"; import { Badge } from "@/components/ui/badge";
import { Plus, RefreshCw, Building2, Check, AlertCircle, Clock } from "lucide-react";
import { SiGithub } from "react-icons/si"; import { SiGithub } from "react-icons/si";
import type { Organization } from "@/lib/db/schema"; import type { Organization } from "@/lib/db/schema";
import type { FilterParams } from "@/types/filter"; import type { FilterParams } from "@/types/filter";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { Checkbox } from "@/components/ui/checkbox"; import { cn } from "@/lib/utils";
import { getStatusColor } from "@/lib/utils";
interface OrganizationListProps { interface OrganizationListProps {
organizations: Organization[]; organizations: Organization[];
@@ -20,6 +20,22 @@ interface OrganizationListProps {
onAddOrganization?: () => void; onAddOrganization?: () => void;
} }
// Helper function to get status badge variant and icon
const getStatusBadge = (status: string | null) => {
switch (status) {
case "imported":
return { variant: "secondary" as const, label: "Not Mirrored", icon: null };
case "mirroring":
return { variant: "outline" as const, label: "Mirroring", icon: Clock };
case "mirrored":
return { variant: "default" as const, label: "Mirrored", icon: Check };
case "failed":
return { variant: "destructive" as const, label: "Failed", icon: AlertCircle };
default:
return { variant: "secondary" as const, label: "Unknown", icon: null };
}
};
export function OrganizationList({ export function OrganizationList({
organizations, organizations,
isLoading, isLoading,
@@ -93,29 +109,45 @@ export function OrganizationList({
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredOrganizations.map((org, index) => { {filteredOrganizations.map((org, index) => {
const isLoading = loadingOrgIds.has(org.id ?? ""); const isLoading = loadingOrgIds.has(org.id ?? "");
const statusBadge = getStatusBadge(org.status);
const StatusIcon = statusBadge.icon;
return ( return (
<Card key={index} className="overflow-hidden p-4"> <Card
<div className="flex items-center justify-between mb-2"> key={index}
<div className="flex items-center gap-2"> className={cn(
<Building2 className="h-5 w-5 text-muted-foreground" /> "overflow-hidden p-4 transition-all hover:shadow-md min-h-[160px]",
<a isLoading && "opacity-75"
href={`/repositories?organization=${encodeURIComponent(org.name || '')}`} )}
className="font-medium hover:underline cursor-pointer" >
> <div className="flex items-start justify-between mb-3">
{org.name} <div className="flex-1">
</a> <div className="flex items-center gap-2 mb-1">
<Building2 className="h-5 w-5 text-muted-foreground" />
<a
href={`/repositories?organization=${encodeURIComponent(org.name || '')}`}
className="font-medium hover:underline cursor-pointer"
>
{org.name}
</a>
<span
className={`text-xs px-2 py-0.5 rounded-full capitalize ${
org.membershipRole === "member"
? "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
: "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200"
}`}
>
{org.membershipRole}
</span>
</div>
</div> </div>
<span <Badge variant={statusBadge.variant} className="ml-2">
className={`text-xs px-2 py-1 rounded-full capitalize ${ {StatusIcon && <StatusIcon className={cn(
org.membershipRole === "member" "h-3 w-3",
? "bg-blue-100 text-blue-800" org.status === "mirroring" && "animate-pulse"
: "bg-purple-100 text-purple-800" )} />}
}`} {statusBadge.label}
> </Badge>
{org.membershipRole}
{/* needs to be updated */}
</span>
</div> </div>
<div className="text-sm text-muted-foreground mb-4"> <div className="text-sm text-muted-foreground mb-4">
@@ -125,58 +157,96 @@ export function OrganizationList({
{org.repositoryCount === 1 ? "repository" : "repositories"} {org.repositoryCount === 1 ? "repository" : "repositories"}
</span> </span>
</div> </div>
{(org.publicRepositoryCount !== undefined || {/* Always render this section to prevent layout shift */}
org.privateRepositoryCount !== undefined || <div className="flex gap-4 mt-2 text-xs min-h-[20px]">
org.forkRepositoryCount !== undefined) && ( {isLoading || (org.status === "mirroring" && org.publicRepositoryCount === undefined) ? (
<div className="flex gap-4 mt-2 text-xs"> <>
{org.publicRepositoryCount !== undefined && ( <Skeleton className="h-3 w-16" />
<span className="flex items-center gap-1"> <Skeleton className="h-3 w-16" />
<div className="h-2 w-2 rounded-full bg-green-500" /> </>
{org.publicRepositoryCount} public ) : (
</span> <>
)} {org.publicRepositoryCount !== undefined ? (
{org.privateRepositoryCount !== undefined && org.privateRepositoryCount > 0 && ( <span className="flex items-center gap-1">
<span className="flex items-center gap-1"> <div className="h-2 w-2 rounded-full bg-green-500" />
<div className="h-2 w-2 rounded-full bg-orange-500" /> {org.publicRepositoryCount} public
{org.privateRepositoryCount} private </span>
</span> ) : null}
)} {org.privateRepositoryCount !== undefined && org.privateRepositoryCount > 0 ? (
{org.forkRepositoryCount !== undefined && org.forkRepositoryCount > 0 && ( <span className="flex items-center gap-1">
<span className="flex items-center gap-1"> <div className="h-2 w-2 rounded-full bg-orange-500" />
<div className="h-2 w-2 rounded-full bg-blue-500" /> {org.privateRepositoryCount} private
{org.forkRepositoryCount} fork{org.forkRepositoryCount !== 1 ? 's' : ''} </span>
</span> ) : null}
)} {org.forkRepositoryCount !== undefined && org.forkRepositoryCount > 0 ? (
</div> <span className="flex items-center gap-1">
)} <div className="h-2 w-2 rounded-full bg-blue-500" />
{org.forkRepositoryCount} fork{org.forkRepositoryCount !== 1 ? 's' : ''}
</span>
) : null}
{/* Show a placeholder if no counts are available to maintain height */}
{org.publicRepositoryCount === undefined &&
org.privateRepositoryCount === undefined &&
org.forkRepositoryCount === undefined && (
<span className="invisible">Loading counts...</span>
)}
</>
)}
</div>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center"> <div className="flex items-center gap-2">
<Checkbox {org.status === "imported" && (
id={`include-${org.id}`} <Button
name={`include-${org.id}`} size="sm"
checked={org.status === "mirrored"} onClick={() => org.id && onMirror({ orgId: org.id })}
disabled={ disabled={isLoading}
loadingOrgIds.has(org.id ?? "") || >
org.status === "mirrored" || {isLoading ? (
org.status === "mirroring" <>
} <RefreshCw className="h-3 w-3 animate-spin mr-2" />
onCheckedChange={async (checked) => { Starting...
if (checked && !org.isIncluded && org.id) { </>
onMirror({ orgId: org.id }); ) : (
} "Mirror"
}} )}
/> </Button>
<label )}
htmlFor={`include-${org.id}`}
className="ml-2 text-sm select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Enable mirroring
</label>
{isLoading && ( {org.status === "mirroring" && (
<RefreshCw className="opacity-50 h-4 w-4 animate-spin ml-4" /> <Button size="sm" disabled variant="outline">
<RefreshCw className="h-3 w-3 animate-spin mr-2" />
Mirroring...
</Button>
)}
{org.status === "mirrored" && (
<Button size="sm" disabled variant="secondary">
<Check className="h-3 w-3 mr-2" />
Mirrored
</Button>
)}
{org.status === "failed" && (
<Button
size="sm"
variant="destructive"
onClick={() => org.id && onMirror({ orgId: org.id })}
disabled={isLoading}
>
{isLoading ? (
<>
<RefreshCw className="h-3 w-3 animate-spin mr-2" />
Retrying...
</>
) : (
<>
<AlertCircle className="h-3 w-3 mr-2" />
Retry
</>
)}
</Button>
)} )}
</div> </div>
@@ -185,19 +255,12 @@ export function OrganizationList({
href={`https://github.com/${org.name}`} href={`https://github.com/${org.name}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
title="View on GitHub"
> >
<SiGithub className="h-4 w-4" /> <SiGithub className="h-4 w-4" />
</a> </a>
</Button> </Button>
</div> </div>
{/* dont know if this looks good. maybe revised */}
<div className="flex items-center gap-2 justify-end mt-4">
<div
className={`h-2 w-2 rounded-full ${getStatusColor(org.status)}`}
/>
<span className="text-sm capitalize">{org.status}</span>
</div>
</Card> </Card>
); );
})} })}