mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-10 05:26:44 +03:00
refactor: enhance organization card display with status badges and improved layout
This commit is contained in:
@@ -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"
|
{org.status === "mirroring" && (
|
||||||
>
|
<Button size="sm" disabled variant="outline">
|
||||||
Enable mirroring
|
<RefreshCw className="h-3 w-3 animate-spin mr-2" />
|
||||||
</label>
|
Mirroring...
|
||||||
|
</Button>
|
||||||
{isLoading && (
|
)}
|
||||||
<RefreshCw className="opacity-50 h-4 w-4 animate-spin ml-4" />
|
|
||||||
|
{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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in New Issue
Block a user