From 24bd0aefe68257d0a8db00da4a2234b2c5bf629d Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Mon, 7 Jul 2025 17:34:54 +0530 Subject: [PATCH 1/5] Added basic responsive layout --- src/components/dashboard/Dashboard.tsx | 30 +- src/components/dashboard/RecentActivity.tsx | 4 +- src/components/dashboard/RepositoryList.tsx | 12 +- src/components/layout/Header.tsx | 73 +- src/components/layout/MainLayout.tsx | 17 +- src/components/layout/Sidebar.tsx | 108 ++- src/components/repositories/Repository.tsx | 153 ++-- .../repositories/RepositoryComboboxes.tsx | 10 +- .../repositories/RepositoryTable.tsx | 836 ++++++++++-------- src/styles/global.css | 10 + 10 files changed, 715 insertions(+), 538 deletions(-) diff --git a/src/components/dashboard/Dashboard.tsx b/src/components/dashboard/Dashboard.tsx index 05514fb..2cc9e67 100644 --- a/src/components/dashboard/Dashboard.tsx +++ b/src/components/dashboard/Dashboard.tsx @@ -179,16 +179,16 @@ export function Dashboard() { return isLoading || !connected ? (
-
+
-
+
{/* Repository List Skeleton */} -
+
@@ -201,7 +201,7 @@ export function Dashboard() {
{/* Recent Activity Skeleton */} -
+
@@ -217,24 +217,24 @@ export function Dashboard() { ) : (
-
+
} - description="Repositories being mirrored" + description="Total in mirror queue" /> } - description="Successfully mirrored" + description="Synced to Gitea" /> } - description="GitHub organizations" + description="From GitHub" />
-
- +
+
+ +
- {/* the api already sends 10 activities only but slicing in case of realtime updates */} - +
+ {/* the api already sends 10 activities only but slicing in case of realtime updates */} + +
); diff --git a/src/components/dashboard/RecentActivity.tsx b/src/components/dashboard/RecentActivity.tsx index f1b3bac..b3b52c7 100644 --- a/src/components/dashboard/RecentActivity.tsx +++ b/src/components/dashboard/RecentActivity.tsx @@ -16,7 +16,7 @@ export function RecentActivity({ activities }: RecentActivityProps) { View All - +
{activities.length === 0 ? (

No recent activity

@@ -31,7 +31,7 @@ export function RecentActivity({ activities }: RecentActivityProps) { />
-

+

{activity.message}

diff --git a/src/components/dashboard/RepositoryList.tsx b/src/components/dashboard/RepositoryList.tsx index d2fc9ea..ecc5ee6 100644 --- a/src/components/dashboard/RepositoryList.tsx +++ b/src/components/dashboard/RepositoryList.tsx @@ -54,7 +54,7 @@ export function RepositoryList({ repositories }: RepositoryListProps) { View All - + {repositories.length === 0 ? (

@@ -71,11 +71,11 @@ export function RepositoryList({ repositories }: RepositoryListProps) { {repositories.map((repo, index) => (
-
-

{repo.name}

+
+

{repo.name}

{repo.isPrivate && ( Private @@ -99,13 +99,13 @@ export function RepositoryList({ repositories }: RepositoryListProps) {
-
+
- + {/* setting the minimum width to 3rem corresponding to the largest status (mirrored) so that all are left alligned */} {repo.status} diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index d002e84..e34b482 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -7,13 +7,15 @@ import { toast } from "sonner"; import { Skeleton } from "@/components/ui/skeleton"; import { useLiveRefresh } from "@/hooks/useLiveRefresh"; import { useConfigStatus } from "@/hooks/useConfigStatus"; +import { Menu } from "lucide-react"; interface HeaderProps { currentPage?: "dashboard" | "repositories" | "organizations" | "configuration" | "activity-log"; onNavigate?: (page: string) => void; + onMenuClick: () => void; } -export function Header({ currentPage, onNavigate }: HeaderProps) { +export function Header({ currentPage, onNavigate, onMenuClick }: HeaderProps) { const { user, logout, isLoading } = useAuth(); const { isLiveEnabled, toggleLive } = useLiveRefresh(); const { isFullyConfigured, isLoading: configLoading } = useConfigStatus(); @@ -54,35 +56,48 @@ export function Header({ currentPage, onNavigate }: HeaderProps) { return (
-
- +
+
+ {/* Hamburger Menu Button - Mobile Only */} + + + +
-
+
{showLiveButton && ( )} @@ -111,12 +126,12 @@ export function Header({ currentPage, onNavigate }: HeaderProps) { {user.username.charAt(0).toUpperCase()} - ) : ( - )} diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx index 4ed6607..9341a54 100644 --- a/src/components/layout/MainLayout.tsx +++ b/src/components/layout/MainLayout.tsx @@ -44,6 +44,7 @@ function AppWithProviders({ page: initialPage }: AppProps) { const { isLoading: configLoading } = useConfigStatus(); const [currentPage, setCurrentPage] = useState(initialPage); const [navigationKey, setNavigationKey] = useState(0); + const [sidebarOpen, setSidebarOpen] = useState(false); useRepoSync({ userId: user?.id, @@ -99,10 +100,18 @@ function AppWithProviders({ page: initialPage }: AppProps) { return (
-
-
- -
+
setSidebarOpen(!sidebarOpen)} + /> +
+ setSidebarOpen(false)} + /> +
{currentPage === "dashboard" && } {currentPage === "repositories" && } {currentPage === "organizations" && } diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 8ac6ac9..29b81cd 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -7,9 +7,11 @@ import { VersionInfo } from "./VersionInfo"; interface SidebarProps { className?: string; onNavigate?: (page: string) => void; + isOpen: boolean; + onClose: () => void; } -export function Sidebar({ className, onNavigate }: SidebarProps) { +export function Sidebar({ className, onNavigate, isOpen, onClose }: SidebarProps) { const [currentPath, setCurrentPath] = useState(""); useEffect(() => { @@ -50,53 +52,75 @@ export function Sidebar({ className, onNavigate }: SidebarProps) { const pageName = pageMap[href] || 'dashboard'; onNavigate?.(pageName); + + // Close sidebar on mobile after navigation + if (window.innerWidth < 1024) { + onClose(); + } }; return ( -
- + + ); } diff --git a/src/components/repositories/Repository.tsx b/src/components/repositories/Repository.tsx index 01b2202..5049590 100644 --- a/src/components/repositories/Repository.tsx +++ b/src/components/repositories/Repository.tsx @@ -560,10 +560,10 @@ export default function Repository() { const availableActions = getAvailableActions(); return ( -
- {/* Combine search and actions into a single flex row */} -
-
+
+ {/* Search and filters */} +
+
- + {/* Filter controls in a responsive row */} +
+ - + +
+
- {/* Context-aware action buttons */} + {/* Action buttons - separate row on mobile */} +
{selectedRepoIds.size === 0 ? ( ) : ( -
+ <>
{selectedRepoIds.size} selected @@ -652,42 +659,44 @@ export default function Repository() {
- {availableActions.includes('mirror') && ( - - )} - - {availableActions.includes('sync') && ( - - )} - - {availableActions.includes('retry') && ( - - )} -
+
+ {availableActions.includes('mirror') && ( + + )} + + {availableActions.includes('sync') && ( + + )} + + {availableActions.includes('retry') && ( + + )} +
+ )}
diff --git a/src/components/repositories/RepositoryComboboxes.tsx b/src/components/repositories/RepositoryComboboxes.tsx index f77c11a..56f3ae4 100644 --- a/src/components/repositories/RepositoryComboboxes.tsx +++ b/src/components/repositories/RepositoryComboboxes.tsx @@ -33,13 +33,13 @@ export function OwnerCombobox({ options, value, onChange, placeholder = "Owner" variant="outline" role="combobox" aria-expanded={open} - className="w-[160px] justify-between" + className="w-full sm:w-[160px] justify-between" > {value ? value : placeholder} - + @@ -86,13 +86,13 @@ export function OrganizationCombobox({ options, value, onChange, placeholder = " variant="outline" role="combobox" aria-expanded={open} - className="w-[160px] justify-between" + className="w-full sm:w-[160px] justify-between" > {value ? value : placeholder} - + @@ -128,4 +128,4 @@ export function OrganizationCombobox({ options, value, onChange, placeholder = " ); -} +} \ No newline at end of file diff --git a/src/components/repositories/RepositoryTable.tsx b/src/components/repositories/RepositoryTable.tsx index 0585500..9abfe67 100644 --- a/src/components/repositories/RepositoryTable.tsx +++ b/src/components/repositories/RepositoryTable.tsx @@ -1,7 +1,7 @@ import { useMemo, useRef } from "react"; import Fuse from "fuse.js"; import { useVirtualizer } from "@tanstack/react-virtual"; -import { FlipHorizontal, GitFork, RefreshCw, RotateCcw, Star } from "lucide-react"; +import { FlipHorizontal, GitFork, RefreshCw, RotateCcw, Star, Lock } from "lucide-react"; import { SiGithub, SiGitea } from "react-icons/si"; import type { Repository } from "@/lib/db/schema"; import { Button } from "@/components/ui/button"; @@ -17,6 +17,8 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { InlineDestinationEditor } from "./InlineDestinationEditor"; +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; interface RepositoryTableProps { repositories: Repository[]; @@ -166,283 +168,89 @@ export default function RepositoryTable({ filteredRepositories.every(repo => repo.id && selectedRepoIds.has(repo.id)); const isPartiallySelected = selectedRepoIds.size > 0 && !isAllSelected; - return isLoading ? ( -
-
-
- -
-
- Repository -
-
Owner
-
- Organization -
-
- Last Mirrored -
-
Status
-
- Actions -
-
- Links -
-
+ // Mobile card layout for repository + const RepositoryCard = ({ repo }: { repo: Repository }) => { + const isLoading = repo.id ? loadingRepoIds.has(repo.id) : false; + const isSelected = repo.id ? selectedRepoIds.has(repo.id) : false; + const giteaUrl = getGiteaRepoUrl(repo); - {Array.from({ length: 5 }).map((_, i) => ( -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- ))} -
- ) : filteredRepositories.length === 0 ? ( -
- -

No repositories found

-

- {hasAnyFilter - ? "Try adjusting your search or filter criteria." - : "Configure your GitHub connection to start mirroring repositories."} -

- {hasAnyFilter ? ( - - ) : ( - - )} -
- ) : ( -
- {/* table header */} -
-
- -
-
- Repository -
-
Owner
-
- Organization -
-
- Last Mirrored -
-
Status
-
- Actions -
-
- Links -
-
- - {/* table body wrapper (for a parent in virtualization) */} -
-
- {rowVirtualizer.getVirtualItems().map((virtualRow, index) => { - const repo = filteredRepositories[virtualRow.index]; - const isLoading = loadingRepoIds.has(repo.id ?? ""); - - return ( -
- {/* Checkbox */} -
- repo.id && handleSelectRepo(repo.id, !!checked)} - aria-label={`Select ${repo.name}`} - /> + return ( + + +
+ repo.id && handleSelectRepo(repo.id, checked as boolean)} + className="mt-1" + /> +
+ {/* Repository Info */} +
+

{repo.name}

+
+ {repo.isPrivate && Private} + {repo.isForked && Fork} + {repo.isStarred && Starred}
+
- {/* Repository */} -
- -
-
- {repo.name} - {repo.isStarred && ( - - )} -
-
- {repo.fullName} -
-
- {repo.isPrivate && ( - - Private - - )} - {repo.isForked && ( - - Fork - - )} + {/* Owner & Organization */} +
+
Owner: {repo.owner}
+ {repo.organization &&
Org: {repo.organization}
} + {repo.destinationOrg &&
Destination: {repo.destinationOrg}
} +
+ + {/* Status & Last Mirrored */} +
+
+
+ {repo.status}
+ + {repo.lastMirrored ? formatDate(repo.lastMirrored) : "Never"} + +
- {/* Owner */} -
-

{repo.owner}

-
- - {/* Organization */} -
- -
- - {/* Last Mirrored */} -
-

- {repo.lastMirrored - ? formatDate(new Date(repo.lastMirrored)) - : "Never"} -

-
- - {/* Status */} -
- {repo.status === "failed" && repo.errorMessage ? ( - - - -
-
- {repo.status} -
- - -

{repo.errorMessage}

-
- - - ) : ( - <> -
- {repo.status} - - )} -
- - {/* Actions */} -
- onMirror({ repoId: repo.id ?? "" })} - onSync={() => onSync({ repoId: repo.id ?? "" })} - onRetry={() => onRetry({ repoId: repo.id ?? "" })} - /> -
- - {/* Links */} -
- {(() => { - const giteaUrl = getGiteaRepoUrl(repo); - - // Determine tooltip based on status and configuration - let tooltip: string; - if (!giteaConfig?.url) { - tooltip = "Gitea not configured"; - } else if (repo.status === 'imported') { - tooltip = "Repository not yet mirrored to Gitea"; - } else if (repo.status === 'failed') { - tooltip = "Repository mirroring failed"; - } else if (repo.status === 'mirroring') { - tooltip = "Repository is being mirrored to Gitea"; - } else if (giteaUrl) { - tooltip = "View on Gitea"; - } else { - tooltip = "Gitea repository not available"; - } - - return giteaUrl ? ( - - ) : ( - - ); - })()} - + )} + {(repo.status === "mirrored" || repo.status === "synced") && ( + + )} + {repo.status === "failed" && ( + + )} + + {/* Links */} +
+ + {giteaUrl ? ( + + ) : ( + + )}
- ); - })} -
+
+
+ + + ); + }; + + return isLoading ? ( +
+ {/* Mobile skeleton */} +
+ {Array.from({ length: 5 }).map((_, i) => ( + + +
+ +
+ + + +
+ + +
+
+
+
+
+ ))}
- {/* Status Bar */} -
-
-
- - {hasAnyFilter - ? `Showing ${filteredRepositories.length} of ${repositories.length} repositories` - : `${repositories.length} ${repositories.length === 1 ? 'repository' : 'repositories'} total`} - + {/* Desktop skeleton */} +
+
+
+ +
+
+ Repository +
+
Owner
+
+ Organization +
+
+ Last Mirrored +
+
Status
+
+ Actions +
+
+ Links +
- {/* Center - Live active indicator */} - {isLiveActive && ( -
-
- - Live active - -
+ {Array.from({ length: 5 }).map((_, i) => ( +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
- )} - - {hasAnyFilter && ( - - Filters applied - - )} + ))}
- ); -} + ) : ( +
+ {hasAnyFilter && ( +
+ + Showing {filteredRepositories.length} of {repositories.length} repositories + + +
+ )} -function RepoActionButton({ - repo, - isLoading, - onMirror, - onSync, - onRetry, -}: { - repo: { id: string; status: string }; - isLoading: boolean; - onMirror: () => void; - onSync: () => void; - onRetry: () => void; -}) { - let label = ""; - let icon = <>; - let onClick = () => {}; - let disabled = isLoading; - - if (repo.status === "failed") { - label = "Retry"; - icon = ; - onClick = onRetry; - } else if (["mirrored", "synced", "syncing"].includes(repo.status)) { - label = "Sync"; - icon = ; - onClick = onSync; - disabled ||= repo.status === "syncing"; - } else if (["imported", "mirroring"].includes(repo.status)) { - label = "Mirror"; - icon = ; // Don't change this icon to GitFork. - onClick = onMirror; - disabled ||= repo.status === "mirroring"; - } else { - return null; // unsupported status - } - - return ( - + )} + {(repo.status === "mirrored" || repo.status === "synced") && ( + + )} + {repo.status === "failed" && ( + + )} +
+
+ + + + + + +

View on GitHub

+
+
+
+ + {giteaUrl ? ( + + + + + + +

View on Gitea

+
+
+
+ ) : ( + + + + + + +

Not mirrored to Gitea

+
+
+
+ )} +
+
+ ); + })} +
+
+
)} - +
); -} +} \ No newline at end of file diff --git a/src/styles/global.css b/src/styles/global.css index b95ba51..91bd818 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -146,3 +146,13 @@ .dark ::-webkit-scrollbar-thumb:hover { background-color: oklch(0.6 0 0); } + +/* ===== Animations ===== */ +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} From b984ff9af47e3d39588b84a8650f44695ed2b0d9 Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Mon, 7 Jul 2025 18:51:24 +0530 Subject: [PATCH 2/5] feat: improve mobile layout across components - Update ActivityLog component for better mobile responsiveness - Enhance Header layout for mobile devices - Improve mobile UX in AddOrganizationDialog - Optimize Organization component mobile display - Enhance AddRepositoryDialog mobile layout --- src/components/activity/ActivityLog.tsx | 179 +++++++++-------- src/components/layout/Header.tsx | 65 +++++-- src/components/layout/Sidebar.tsx | 14 +- .../organizations/AddOrganizationDialog.tsx | 4 +- src/components/organizations/Organization.tsx | 182 +++++++++--------- .../repositories/AddRepositoryDialog.tsx | 4 +- 6 files changed, 249 insertions(+), 199 deletions(-) diff --git a/src/components/activity/ActivityLog.tsx b/src/components/activity/ActivityLog.tsx index 001d05a..c3e5431 100644 --- a/src/components/activity/ActivityLog.tsx +++ b/src/components/activity/ActivityLog.tsx @@ -346,10 +346,10 @@ export function ActivityLog() { /* ------------------------------ UI ------------------------------ */ return ( -
-
+
+
{/* search input */} -
+
- {/* status select */} - + {/* Filter controls row */} +
+ {/* status select */} + - {/* repo/org name combobox */} - setFilter((p) => ({ ...p, name }))} - /> + {/* type select - hidden on mobile */} + +
- {/* type select */} - + {/* repo/org name combobox - hidden on mobile */} +
+ setFilter((p) => ({ ...p, name }))} + /> +
- {/* export dropdown */} - - - - - - - Export as CSV - - - Export as JSON - - - + {/* Action buttons row */} +
+ {/* export dropdown - text hidden on mobile */} + + + + + + + Export as CSV + + + Export as JSON + + + - {/* refresh */} - + {/* refresh */} + - {/* cleanup all activities */} - + {/* cleanup all activities */} + +
{/* activity list */} diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index e34b482..3eea9c1 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -7,7 +7,13 @@ import { toast } from "sonner"; import { Skeleton } from "@/components/ui/skeleton"; import { useLiveRefresh } from "@/hooks/useLiveRefresh"; import { useConfigStatus } from "@/hooks/useConfigStatus"; -import { Menu } from "lucide-react"; +import { Menu, LogOut } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; interface HeaderProps { currentPage?: "dashboard" | "repositories" | "organizations" | "configuration" | "activity-log"; @@ -60,9 +66,9 @@ export function Header({ currentPage, onNavigate, onMenuClick }: HeaderProps) {
{/* Hamburger Menu Button - Mobile Only */} )} @@ -120,15 +126,40 @@ export function Header({ currentPage, onNavigate, onMenuClick }: HeaderProps) { ) : user ? ( <> - - - - {user.username.charAt(0).toUpperCase()} - - - + {/* Desktop: Show avatar and logout button */} +
+ + + + {user.username.charAt(0).toUpperCase()} + + + +
+ + {/* Mobile: Avatar with dropdown */} +
+ + + + + + + + Logout + + + +
) : ( ); @@ -102,19 +102,19 @@ export function Sidebar({ className, onNavigate, isOpen, onClose }: SidebarProps
-
+

Need Help?

-

+

Check out the documentation for help with setup and configuration.

Documentation - +
diff --git a/src/components/organizations/AddOrganizationDialog.tsx b/src/components/organizations/AddOrganizationDialog.tsx index f2e50c3..8d85ab1 100644 --- a/src/components/organizations/AddOrganizationDialog.tsx +++ b/src/components/organizations/AddOrganizationDialog.tsx @@ -63,12 +63,12 @@ export default function AddOrganizationDialog({ return ( - - + Add Organization diff --git a/src/components/organizations/Organization.tsx b/src/components/organizations/Organization.tsx index 4c3c123..78b28fb 100644 --- a/src/components/organizations/Organization.tsx +++ b/src/components/organizations/Organization.tsx @@ -293,10 +293,10 @@ export function Organization() { return ( -
- {/* Combine search and actions into a single flex row */} -
-
+
+ {/* Search and filters */} +
+
- {/* Membership Role Filter */} - + {/* Filter controls */} +
+ {/* Membership Role Filter */} + - {/* Status Filter */} - + {/* Status Filter */} + +
- + {/* Action buttons */} +
+ - + +
- - + Add Repository From 1deaae4d34900d329dbd8aca770e46f40d63bd04 Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Mon, 7 Jul 2025 19:27:07 +0530 Subject: [PATCH 3/5] More responsive layout updates to Config Page --- src/components/config/ConfigTabs.tsx | 2 +- src/components/config/GitHubConfigForm.tsx | 17 +++- .../config/GitHubMirrorSettings.tsx | 20 ++--- src/components/config/GiteaConfigForm.tsx | 17 +++- .../config/OrganizationConfiguration.tsx | 2 +- .../config/OrganizationStrategy.tsx | 77 ++++++++++--------- src/components/layout/Sidebar.tsx | 12 +-- 7 files changed, 89 insertions(+), 58 deletions(-) diff --git a/src/components/config/ConfigTabs.tsx b/src/components/config/ConfigTabs.tsx index 4f3b17c..c75eb5e 100644 --- a/src/components/config/ConfigTabs.tsx +++ b/src/components/config/ConfigTabs.tsx @@ -563,7 +563,7 @@ export function ConfigTabs() { ) : (
{/* Header section */} -
+

Configuration Settings diff --git a/src/components/config/GitHubConfigForm.tsx b/src/components/config/GitHubConfigForm.tsx index 351b72c..cfcbd34 100644 --- a/src/components/config/GitHubConfigForm.tsx +++ b/src/components/config/GitHubConfigForm.tsx @@ -88,15 +88,17 @@ export function GitHubConfigForm({ return ( - + GitHub Configuration + {/* Desktop: Show button in header */} @@ -200,6 +202,17 @@ export function GitHubConfigForm({ if (onAdvancedOptionsAutoSave) onAdvancedOptionsAutoSave(newOptions); }} /> + + {/* Mobile: Show button at bottom */} + diff --git a/src/components/config/GitHubMirrorSettings.tsx b/src/components/config/GitHubMirrorSettings.tsx index 7799de8..9c6c779 100644 --- a/src/components/config/GitHubMirrorSettings.tsx +++ b/src/components/config/GitHubMirrorSettings.tsx @@ -124,7 +124,7 @@ export function GitHubMirrorSettings({

-
+
- {/* Starred repos content selection - inline to prevent layout shift */} + {/* Starred repos content selection - responsive layout */}
-
+
- {/* Metadata multi-select - inline to prevent layout shift */} + {/* Metadata multi-select - responsive layout */} - + diff --git a/src/components/config/OrganizationStrategy.tsx b/src/components/config/OrganizationStrategy.tsx index 4759cfc..8014393 100644 --- a/src/components/config/OrganizationStrategy.tsx +++ b/src/components/config/OrganizationStrategy.tsx @@ -24,7 +24,7 @@ const strategyConfig = { preserve: { title: "Preserve Structure", icon: FolderTree, - description: "Keep the exact same organization structure as GitHub", + description: "Keep the exact same org structure as GitHub", color: "text-blue-600 dark:text-blue-400", bgColor: "bg-blue-50 dark:bg-blue-950/20", borderColor: "border-blue-200 dark:border-blue-900", @@ -60,7 +60,7 @@ const strategyConfig = { "mixed": { title: "Mixed Mode", icon: GitBranch, - description: "user repos in single org, org repos preserve structure", + description: "Personal repos in single org, org repos preserve structure", color: "text-orange-600 dark:text-orange-400", bgColor: "bg-orange-50 dark:bg-orange-950/20", borderColor: "border-orange-200 dark:border-orange-900", @@ -281,7 +281,7 @@ export const OrganizationStrategy: React.FC = ({ }) => { return (
-
+

@@ -371,15 +371,16 @@ export const OrganizationStrategy: React.FC = ({ !isSelected && "border-muted" )} > -
-
+
+
= ({ )} />
-
-
-

{config.title}

-
-

- {config.description} -

-
- - - - e.stopPropagation()} - > - - - - -
-

Repository Mapping Preview

- +
+
+
+

{config.title}

+

+ {config.description} +

- - + + + + e.stopPropagation()} + > + + + + +
+

Repository Mapping Preview

+ +
+
+
+
+
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index edc2dc0..220e4fd 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -64,7 +64,7 @@ export function Sidebar({ className, onNavigate, isOpen, onClose }: SidebarProps {/* Mobile Backdrop */} {isOpen && (
)} @@ -72,13 +72,13 @@ export function Sidebar({ className, onNavigate, isOpen, onClose }: SidebarProps {/* Sidebar */}

+ + {/* Mobile FAB for Export - only visible on mobile */} + + + + + + + Export as CSV + + + Export as JSON + + +
); } diff --git a/src/components/activity/ActivityNameCombobox.tsx b/src/components/activity/ActivityNameCombobox.tsx index bdac6f7..77d24f2 100644 --- a/src/components/activity/ActivityNameCombobox.tsx +++ b/src/components/activity/ActivityNameCombobox.tsx @@ -41,9 +41,14 @@ export function ActivityNameCombobox({ activities, value, onChange }: ActivityNa variant="outline" role="combobox" aria-expanded={open} - className="w-[180px] justify-between" + className="w-full sm:w-[180px] justify-between h-10" > - {value ? value : "All Names"} + + {value || "All names"} + @@ -62,7 +67,7 @@ export function ActivityNameCombobox({ activities, value, onChange }: ActivityNa }} > - All Names + All names {names.map((name) => ( Automation & Maintenance + + + + + + +
+

Background Operations

+

+ These automated tasks run in the background to keep your mirrors up-to-date and maintain optimal database performance. + Choose intervals that match your workflow and repository update frequency. +

+
+
+
+
@@ -311,21 +330,6 @@ export function AutomationSettings({
- -
-
- -
-

- Background Operations -

-

- These automated tasks run in the background to keep your mirrors up-to-date and maintain optimal database performance. - Choose intervals that match your workflow and repository update frequency. -

-
-
-
); diff --git a/src/components/config/ConfigTabs.tsx b/src/components/config/ConfigTabs.tsx index c75eb5e..d6fdbbf 100644 --- a/src/components/config/ConfigTabs.tsx +++ b/src/components/config/ConfigTabs.tsx @@ -566,14 +566,14 @@ export function ConfigTabs() {

- Configuration Settings + Configuration

Configure your GitHub and Gitea connections, and set up automatic mirroring.

-
+
- +

Fine-tune Your Mirror Destinations

diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 3eea9c1..96e2ef2 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -125,42 +125,24 @@ export function Header({ currentPage, onNavigate, onMenuClick }: HeaderProps) { {isLoading ? ( ) : user ? ( - <> - {/* Desktop: Show avatar and logout button */} -
- - - - {user.username.charAt(0).toUpperCase()} - - - -
- - {/* Mobile: Avatar with dropdown */} -
- - - - - - - - Logout - - - -
- + + + + + Logout + + + ) : ( + + + + Filter Organizations + + Narrow down your organization list + + + +
+ {/* Active filters summary */} + {hasActiveFilters && ( +
+ + {activeFilterCount} filter{activeFilterCount > 1 ? 's' : ''} active + + +
+ )} - {/* Filter controls */} -
- {/* Membership Role Filter */} - + {/* Role Filter */} +
+ + +
- {/* Status Filter */} - -
- - {/* Action buttons */} -
+ {/* Status Filter */} +
+ + +
+
+ + + + + + + + + + + + - +
+ + {/* Desktop: Original layout */} +
+
+ + + setFilter((prev) => ({ ...prev, searchTerm: e.target.value })) + } + /> +
+ + {/* Filter controls */} +
+ {/* Membership Role Filter */} + + + {/* Status Filter */} + +
+ + {/* Action buttons */} +
+ + + +
+
{ + setFilter({ + searchTerm: filter.searchTerm, + status: "", + organization: "", + owner: "", + }); + }; + return (
{/* Search and filters */}
-
- - - setFilter((prev) => ({ ...prev, searchTerm: e.target.value })) - } - /> -
+ {/* Mobile: Search bar with filter button */} +
+
+ + + setFilter((prev) => ({ ...prev, searchTerm: e.target.value })) + } + /> +
+ + {/* Mobile Filter Drawer */} + + + + + + + Filter Repositories + + Narrow down your repository list + + + +
+ {/* Active filters summary */} + {hasActiveFilters && ( +
+ + {activeFilterCount} filter{activeFilterCount > 1 ? 's' : ''} active + + +
+ )} - {/* Owner Combobox */} - - setFilter((prev) => ({ ...prev, owner })) - } - /> + {/* Owner Filter */} +
+ + + setFilter((prev) => ({ ...prev, owner })) + } + /> +
- {/* Organization Combobox */} - - setFilter((prev) => ({ ...prev, organization })) - } - /> - - {/* Filter controls in a responsive row */} -
- + {/* Organization Filter */} +
+ + + setFilter((prev) => ({ ...prev, organization })) + } + /> +
+ {/* Status Filter */} +
+ + +
+
+ + + + + + + + + + + + + + {selectedRepoIds.size === 0 && ( + + )} +
+ + {/* Desktop: Original layout */} +
+
+ + + setFilter((prev) => ({ ...prev, searchTerm: e.target.value })) + } + /> +
+ + {/* Owner Combobox */} + + setFilter((prev) => ({ ...prev, owner })) + } + /> + + {/* Organization Combobox */} + + setFilter((prev) => ({ ...prev, organization })) + } + /> + + {/* Filter controls in a responsive row */} +
+ + + +
- {/* Action buttons - separate row on mobile */} -
+ {/* Action buttons - shows when items are selected or Mirror All on desktop */} +
{selectedRepoIds.size === 0 ? ( ) : ( - <> + <>
{selectedRepoIds.size} selected diff --git a/src/components/repositories/RepositoryComboboxes.tsx b/src/components/repositories/RepositoryComboboxes.tsx index 56f3ae4..c581e54 100644 --- a/src/components/repositories/RepositoryComboboxes.tsx +++ b/src/components/repositories/RepositoryComboboxes.tsx @@ -33,17 +33,22 @@ export function OwnerCombobox({ options, value, onChange, placeholder = "Owner" variant="outline" role="combobox" aria-expanded={open} - className="w-full sm:w-[160px] justify-between" + className="w-full sm:w-[160px] justify-between h-10" > - {value ? value : placeholder} + + {value || "All owners"} + - + - No {placeholder.toLowerCase()} found. + No owners found. - All + All owners {options.map((option) => ( - {value ? value : placeholder} + + {value || "All organizations"} + - + - No {placeholder.toLowerCase()} found. + No organizations found. - All + All organizations {options.map((option) => ( ) { + return +} + +function DrawerTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DrawerPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DrawerClose({ + ...props +}: React.ComponentProps) { + return +} + +function DrawerOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DrawerContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + +
+ {children} + + + ) +} + +function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DrawerTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DrawerDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +} From 472f67a6aecf5f74a1b8c52b757ce02d0c3816fb Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Mon, 7 Jul 2025 22:02:43 +0530 Subject: [PATCH 5/5] Updates to Repository and Org Pages for Responsive Layouts --- src/components/activity/ActivityList.tsx | 133 +++- .../organizations/MirrorDestinationEditor.tsx | 18 +- .../organizations/OrganizationsList.tsx | 371 ++++++++--- src/components/repositories/Repository.tsx | 187 ++++-- .../repositories/RepositoryTable.tsx | 612 +++++++++++------- 5 files changed, 886 insertions(+), 435 deletions(-) diff --git a/src/components/activity/ActivityList.tsx b/src/components/activity/ActivityList.tsx index f433436..64741fb 100644 --- a/src/components/activity/ActivityList.tsx +++ b/src/components/activity/ActivityList.tsx @@ -3,11 +3,17 @@ import { useVirtualizer } from '@tanstack/react-virtual'; import type { MirrorJob } from '@/lib/db/schema'; import Fuse from 'fuse.js'; import { Button } from '../ui/button'; -import { RefreshCw } from 'lucide-react'; +import { RefreshCw, Check, X, Loader2, Import } from 'lucide-react'; import { Card } from '../ui/card'; import { formatDate, getStatusColor } from '@/lib/utils'; import { Skeleton } from '../ui/skeleton'; import type { FilterParams } from '@/types/filter'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '../ui/tooltip'; type MirrorJobWithKey = MirrorJob & { _rowKey: string }; @@ -73,7 +79,7 @@ export default function ActivityList({ count: filteredActivities.length, getScrollElement: () => parentRef.current, estimateSize: (idx) => - expandedItems.has(filteredActivities[idx]._rowKey) ? 217 : 120, + expandedItems.has(filteredActivities[idx]._rowKey) ? 217 : 100, overscan: 5, measureElement: (el) => el.getBoundingClientRect().height + 8, }); @@ -155,8 +161,8 @@ export default function ActivityList({ }} className='border-b px-4 pt-4' > -
-
+
+
-
-
-

{activity.message}

-

+

+
+
+ {/* Mobile: Show simplified status-based message */} +
+

+ {activity.status === 'synced' ? ( + <> + + Sync successful + + ) : activity.status === 'mirrored' ? ( + <> + + Mirror successful + + ) : activity.status === 'failed' ? ( + <> + + Operation failed + + ) : activity.status === 'syncing' ? ( + <> + + Syncing in progress + + ) : activity.status === 'mirroring' ? ( + <> + + Mirroring in progress + + ) : activity.status === 'imported' ? ( + <> + + Imported + + ) : ( + {activity.message} + )} +

+
+ {/* Desktop: Show status with icon and full message in tooltip */} +
+ + + +

+ {activity.status === 'synced' ? ( + <> + + Sync successful + + ) : activity.status === 'mirrored' ? ( + <> + + Mirror successful + + ) : activity.status === 'failed' ? ( + <> + + Operation failed + + ) : activity.status === 'syncing' ? ( + <> + + Syncing in progress + + ) : activity.status === 'mirroring' ? ( + <> + + Mirroring in progress + + ) : activity.status === 'imported' ? ( + <> + + Imported + + ) : ( + {activity.message} + )} +

+
+ +

{activity.message}

+
+
+
+
+
+

{formatDate(activity.timestamp)}

- {activity.repositoryName && ( -

- Repository: {activity.repositoryName} -

- )} - - {activity.organizationName && ( -

- Organization: {activity.organizationName} -

- )} +
+ {activity.repositoryName && ( +

+ Repo: {activity.repositoryName} +

+ )} + {activity.organizationName && ( +

+ Org: {activity.organizationName} +

+ )} +
{activity.details && (
@@ -199,7 +292,7 @@ export default function ActivityList({ }) } > - {isExpanded ? 'Hide Details' : 'Show Details'} + {isExpanded ? 'Hide Details' : activity.status === 'failed' ? 'Show Error Details' : 'Show Details'} {isExpanded && ( diff --git a/src/components/organizations/MirrorDestinationEditor.tsx b/src/components/organizations/MirrorDestinationEditor.tsx index 0a610c0..e214d83 100644 --- a/src/components/organizations/MirrorDestinationEditor.tsx +++ b/src/components/organizations/MirrorDestinationEditor.tsx @@ -69,19 +69,19 @@ export function MirrorDestinationEditor({ }; return ( -
-
- - {organizationName} - +
+
+ + {organizationName} + {effectiveDestination} {hasOverride && ( - + custom )} @@ -92,11 +92,11 @@ export function MirrorDestinationEditor({ diff --git a/src/components/organizations/OrganizationsList.tsx b/src/components/organizations/OrganizationsList.tsx index 21efed0..0ff8f82 100644 --- a/src/components/organizations/OrganizationsList.tsx +++ b/src/components/organizations/OrganizationsList.tsx @@ -127,9 +127,9 @@ export function OrganizationList({ }, [organizations, filter]); return isLoading ? ( -
+
{Array.from({ length: 5 }).map((_, i) => ( - + ))}
) : filteredOrganizations.length === 0 ? ( @@ -161,7 +161,7 @@ export function OrganizationList({ )}
) : ( -
+
{filteredOrganizations.map((org, index) => { const isLoading = loadingOrgIds.has(org.id ?? ""); const statusBadge = getStatusBadge(org.status); @@ -171,20 +171,33 @@ export function OrganizationList({ -
-
-
- - - {org.name} - + {/* Mobile Layout */} +
+ {/* Header with org name and badges */} +
+
+ + + {StatusIcon && } + {statusBadge.label} + +
+
+
- {/* Destination override section */} -
- handleUpdateDestination(org.id!, newDestination)} - isUpdating={isLoading} - /> + {/* Destination override section */} +
+ handleUpdateDestination(org.id!, newDestination)} + isUpdating={isLoading} + /> +
+
+ + {/* Desktop Layout */} +
+ {/* Header with org icon, name, role badge and status */} +
+
+
+
+ + {org.name} + + + {org.membershipRole} + +
+
+
+ + {/* Status badge */} + + {StatusIcon && } + {statusBadge.label} + +
+ + {/* Destination override section */} +
+ handleUpdateDestination(org.id!, newDestination)} + isUpdating={isLoading} + /> +
+ + {/* Repository statistics */} +
+
+
+ {org.repositoryCount} + + {org.repositoryCount === 1 ? "repository" : "repositories"} + +
+ + {/* Repository breakdown */} + {isLoading || (org.status === "mirroring" && org.publicRepositoryCount === undefined) ? ( +
+ + +
+ ) : ( +
+ {org.publicRepositoryCount !== undefined && ( +
+
+ + {org.publicRepositoryCount} public + +
+ )} + {org.privateRepositoryCount !== undefined && org.privateRepositoryCount > 0 && ( +
+
+ + {org.privateRepositoryCount} private + +
+ )} +
+ )}
- - {StatusIcon && } - {statusBadge.label} -
-
-
- - {org.repositoryCount}{" "} - {org.repositoryCount === 1 ? "repository" : "repositories"} - -
- {/* Always render this section to prevent layout shift */} -
- {isLoading || (org.status === "mirroring" && org.publicRepositoryCount === undefined) ? ( - <> - - - - ) : ( - <> - {org.publicRepositoryCount !== undefined ? ( - -
- {org.publicRepositoryCount} public - - ) : null} - {org.privateRepositoryCount !== undefined && org.privateRepositoryCount > 0 ? ( - -
- {org.privateRepositoryCount} private - - ) : null} - {org.forkRepositoryCount !== undefined && org.forkRepositoryCount > 0 ? ( - -
- {org.forkRepositoryCount} fork{org.forkRepositoryCount !== 1 ? 's' : ''} - - ) : null} - {/* Show a placeholder if no counts are available to maintain height */} - {org.publicRepositoryCount === undefined && - org.privateRepositoryCount === undefined && - org.forkRepositoryCount === undefined && ( - Loading counts... - )} - - )} -
-
- -
+ {/* Mobile Actions */} +
{org.status === "imported" && ( )} {org.status === "mirroring" && ( - )} {org.status === "mirrored" && ( - )} {org.status === "failed" && ( )}
- -
+ +
{(() => { const giteaUrl = getGiteaOrgUrl(org); @@ -337,34 +388,166 @@ export function OrganizationList({ } return giteaUrl ? ( - ) : ( - ); })()} -
+ + {/* Desktop Actions */} +
+
+ {org.status === "imported" && ( + + )} + + {org.status === "mirroring" && ( + + )} + + {org.status === "mirrored" && ( + + )} + + {org.status === "failed" && ( + + )} +
+ +
+ {(() => { + const giteaUrl = getGiteaOrgUrl(org); + + // Determine tooltip based on status and configuration + let tooltip: string; + if (!giteaConfig?.url) { + tooltip = "Gitea not configured"; + } else if (org.status === 'imported') { + tooltip = "Organization not yet mirrored to Gitea"; + } else if (org.status === 'failed') { + tooltip = "Organization mirroring failed"; + } else if (org.status === 'mirroring') { + tooltip = "Organization is being mirrored to Gitea"; + } else if (giteaUrl) { + tooltip = "View on Gitea"; + } else { + tooltip = "Gitea organization not available"; + } + + return ( +
+ + +
+ ); + })()} +
+
); })} diff --git a/src/components/repositories/Repository.tsx b/src/components/repositories/Repository.tsx index 53912a9..22e746c 100644 --- a/src/components/repositories/Repository.tsx +++ b/src/components/repositories/Repository.tsx @@ -752,18 +752,16 @@ export default function Repository() { - {selectedRepoIds.size === 0 && ( - - )} +
{/* Desktop: Original layout */} @@ -844,24 +842,80 @@ export default function Repository() {
+ + {/* Bulk actions on desktop - integrated into the same line */} +
+ {selectedRepoIds.size === 0 ? ( + + ) : ( + <> +
+ + {selectedRepoIds.size} selected + + +
+ + {availableActions.includes('mirror') && ( + + )} + + {availableActions.includes('sync') && ( + + )} + + {availableActions.includes('retry') && ( + + )} + + )} +
- {/* Action buttons - shows when items are selected or Mirror All on desktop */} -
- {selectedRepoIds.size === 0 ? ( - - ) : ( - <> -
+ {/* Action buttons for mobile - only show when items are selected */} + {selectedRepoIds.size > 0 && ( +
+
{selectedRepoIds.size} selected @@ -877,44 +931,43 @@ export default function Repository() {
{availableActions.includes('mirror') && ( - - )} - - {availableActions.includes('sync') && ( - - )} - - {availableActions.includes('retry') && ( - - )} -
- - )} -
+ + )} + + {availableActions.includes('sync') && ( + + )} + + {availableActions.includes('retry') && ( + + )} +
+
+ )} {!isGitHubConfigured ? (
@@ -946,7 +999,9 @@ export default function Repository() { loadingRepoIds={loadingRepoIds} selectedRepoIds={selectedRepoIds} onSelectionChange={setSelectedRepoIds} - onRefresh={() => fetchRepositories(false)} + onRefresh={async () => { + await fetchRepositories(false); + }} /> )} diff --git a/src/components/repositories/RepositoryTable.tsx b/src/components/repositories/RepositoryTable.tsx index 9abfe67..8b810bc 100644 --- a/src/components/repositories/RepositoryTable.tsx +++ b/src/components/repositories/RepositoryTable.tsx @@ -177,106 +177,159 @@ export default function RepositoryTable({ return ( -
- repo.id && handleSelectRepo(repo.id, checked as boolean)} - className="mt-1" - /> -
- {/* Repository Info */} -
-

{repo.name}

+
+ {/* Header with checkbox and repo name */} +
+ repo.id && handleSelectRepo(repo.id, checked as boolean)} + className="mt-1 h-5 w-5" + aria-label={`Select ${repo.name}`} + /> +
+

{repo.name}

- {repo.isPrivate && Private} - {repo.isForked && Fork} - {repo.isStarred && Starred} + {repo.isPrivate && Private} + {repo.isForked && Fork} + {repo.isStarred && Starred}
+
+ {/* Repository details */} +
{/* Owner & Organization */} -
-
Owner: {repo.owner}
- {repo.organization &&
Org: {repo.organization}
} - {repo.destinationOrg &&
Destination: {repo.destinationOrg}
} +
+
+ Owner: + {repo.owner} +
+ {repo.organization && ( +
+ Org: + {repo.organization} +
+ )} + {repo.destinationOrg && ( +
+ Dest: + {repo.destinationOrg} +
+ )}
{/* Status & Last Mirrored */} -
+
-
- {repo.status} +
+ {repo.status}
- - {repo.lastMirrored ? formatDate(repo.lastMirrored) : "Never"} + + {repo.lastMirrored ? formatDate(repo.lastMirrored) : "Never mirrored"}
+
- {/* Actions */} -
- {(repo.status === "imported" || repo.status === "failed") && ( - + )} + {(repo.status === "mirrored" || repo.status === "synced") && ( + + )} + {repo.status === "failed" && ( + + )} + + {/* External links */} +
@@ -404,13 +457,14 @@ export default function RepositoryTable({ ) : ( <> {/* Mobile card view */} -
+
{/* Select all checkbox */} -
+
Select All ({filteredRepositories.length}) @@ -424,7 +478,7 @@ export default function RepositoryTable({
{/* Desktop table view */} -
+
{/* Table header */}
@@ -453,215 +507,281 @@ export default function RepositoryTable({
- {/* Table body with virtualization */} + {/* Table body wrapper (for a parent in virtualization) */}
- {rowVirtualizer.getVirtualItems().map((virtualRow) => { + {rowVirtualizer.getVirtualItems().map((virtualRow, index) => { const repo = filteredRepositories[virtualRow.index]; - const isLoading = repo.id ? loadingRepoIds.has(repo.id) : false; - const isSelected = repo.id ? selectedRepoIds.has(repo.id) : false; - const giteaUrl = getGiteaRepoUrl(repo); + const isLoading = loadingRepoIds.has(repo.id ?? ""); return (
+ {/* Checkbox */}
repo.id && handleSelectRepo(repo.id, checked as boolean)} + checked={repo.id ? selectedRepoIds.has(repo.id) : false} + onCheckedChange={(checked) => repo.id && handleSelectRepo(repo.id, !!checked)} + aria-label={`Select ${repo.name}`} />
-
-
- {repo.name} - {repo.isPrivate && ( - - - - - - -

Private repository

-
-
-
- )} - {repo.isForked && ( - - - - - - -

Forked repository

-
-
-
- )} - {repo.isStarred && ( - - - - - - -

Starred repository

-
-
-
- )} + + {/* Repository */} +
+ +
+
+ {repo.name} + {repo.isStarred && ( + + )} +
+
+ {repo.fullName} +
-

- {repo.fullName} + {repo.isPrivate && ( + + Private + + )} + {repo.isForked && ( + + Fork + + )} +

+ {/* Owner */} +
+

{repo.owner}

+
+ + {/* Organization */} +
+ +
+ + {/* Last Mirrored */} +
+

+ {repo.lastMirrored + ? formatDate(new Date(repo.lastMirrored)) + : "Never"}

-
- {repo.owner} -
-
-
- {repo.organization || "-"} - {repo.destinationOrg && repo.id && ( - - )} -
-
-
- {repo.lastMirrored ? formatDate(repo.lastMirrored) : "Never"} -
-
-
-
- {repo.status} -
-
-
- {(repo.status === "imported" || repo.status === "failed") && ( - - )} - {(repo.status === "mirrored" || repo.status === "synced") && ( - - )} - {repo.status === "failed" && ( - - )} -
-
- - - - - - -

View on GitHub

-
-
-
- {giteaUrl ? ( + {/* Status */} +
+ {repo.status === "failed" && repo.errorMessage ? ( - +
+
+ {repo.status} +
- -

View on Gitea

+ +

{repo.errorMessage}

) : ( - - - - - - -

Not mirrored to Gitea

-
-
-
+ <> +
+ {repo.status} + )}
+ {/* Actions */} +
+ onMirror({ repoId: repo.id ?? "" })} + onSync={() => onSync({ repoId: repo.id ?? "" })} + onRetry={() => onRetry({ repoId: repo.id ?? "" })} + /> +
+ {/* Links */} +
+ {(() => { + const giteaUrl = getGiteaRepoUrl(repo); + + // Determine tooltip based on status and configuration + let tooltip: string; + if (!giteaConfig?.url) { + tooltip = "Gitea not configured"; + } else if (repo.status === 'imported') { + tooltip = "Repository not yet mirrored to Gitea"; + } else if (repo.status === 'failed') { + tooltip = "Repository mirroring failed"; + } else if (repo.status === 'mirroring') { + tooltip = "Repository is being mirrored to Gitea"; + } else if (giteaUrl) { + tooltip = "View on Gitea"; + } else { + tooltip = "Gitea repository not available"; + } + + return giteaUrl ? ( + + ) : ( + + ); + })()} + +
); })}
+ + {/* Status Bar */} +
+
+
+ + {hasAnyFilter + ? `Showing ${filteredRepositories.length} of ${repositories.length} repositories` + : `${repositories.length} ${repositories.length === 1 ? 'repository' : 'repositories'} total`} + +
+ + {/* Center - Live active indicator */} + {isLiveActive && ( +
+
+ + Live active + +
+
+ )} + + {hasAnyFilter && ( + + Filters applied + + )} +
)}
); +} + +function RepoActionButton({ + repo, + isLoading, + onMirror, + onSync, + onRetry, +}: { + repo: { id: string; status: string }; + isLoading: boolean; + onMirror: () => void; + onSync: () => void; + onRetry: () => void; +}) { + let label = ""; + let icon = <>; + let onClick = () => {}; + let disabled = isLoading; + + if (repo.status === "failed") { + label = "Retry"; + icon = ; + onClick = onRetry; + } else if (["mirrored", "synced", "syncing"].includes(repo.status)) { + label = "Sync"; + icon = ; + onClick = onSync; + disabled ||= repo.status === "syncing"; + } else if (["imported", "mirroring"].includes(repo.status)) { + label = "Mirror"; + icon = ; + onClick = onMirror; + disabled ||= repo.status === "mirroring"; + } else { + return null; // unsupported status + } + + return ( + + ); } \ No newline at end of file