mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-08 12:36:44 +03:00
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
This commit is contained in:
@@ -346,10 +346,10 @@ export function ActivityLog() {
|
|||||||
/* ------------------------------ UI ------------------------------ */
|
/* ------------------------------ UI ------------------------------ */
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-y-8'>
|
<div className='flex flex-col gap-y-4 sm:gap-y-8'>
|
||||||
<div className='flex w-full flex-row items-center gap-4'>
|
<div className='flex w-full flex-col sm:flex-row items-start sm:items-center gap-2 sm:gap-4'>
|
||||||
{/* search input */}
|
{/* search input */}
|
||||||
<div className='relative flex-1'>
|
<div className='relative w-full sm:flex-1'>
|
||||||
<Search className='absolute left-2 top-2.5 h-4 w-4 text-muted-foreground' />
|
<Search className='absolute left-2 top-2.5 h-4 w-4 text-muted-foreground' />
|
||||||
<input
|
<input
|
||||||
type='text'
|
type='text'
|
||||||
@@ -365,6 +365,8 @@ export function ActivityLog() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Filter controls row */}
|
||||||
|
<div className='flex flex-row items-center gap-2 w-full sm:w-auto'>
|
||||||
{/* status select */}
|
{/* status select */}
|
||||||
<Select
|
<Select
|
||||||
value={filter.status || 'all'}
|
value={filter.status || 'all'}
|
||||||
@@ -375,7 +377,7 @@ export function ActivityLog() {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className='h-9 w-[140px] max-h-9'>
|
<SelectTrigger className='h-9 w-full sm:w-[140px] max-h-9'>
|
||||||
<SelectValue placeholder='All Status' />
|
<SelectValue placeholder='All Status' />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -387,21 +389,14 @@ export function ActivityLog() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
{/* repo/org name combobox */}
|
{/* type select - hidden on mobile */}
|
||||||
<ActivityNameCombobox
|
|
||||||
activities={activities}
|
|
||||||
value={filter.name || ''}
|
|
||||||
onChange={(name) => setFilter((p) => ({ ...p, name }))}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* type select */}
|
|
||||||
<Select
|
<Select
|
||||||
value={filter.type || 'all'}
|
value={filter.type || 'all'}
|
||||||
onValueChange={(v) =>
|
onValueChange={(v) =>
|
||||||
setFilter((p) => ({ ...p, type: v === 'all' ? '' : v }))
|
setFilter((p) => ({ ...p, type: v === 'all' ? '' : v }))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className='h-9 w-[140px] max-h-9'>
|
<SelectTrigger className='h-9 w-[140px] max-h-9 hidden sm:flex'>
|
||||||
<SelectValue placeholder='All Types' />
|
<SelectValue placeholder='All Types' />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -412,14 +407,26 @@ export function ActivityLog() {
|
|||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* export dropdown */}
|
{/* repo/org name combobox - hidden on mobile */}
|
||||||
|
<div className='hidden sm:block'>
|
||||||
|
<ActivityNameCombobox
|
||||||
|
activities={activities}
|
||||||
|
value={filter.name || ''}
|
||||||
|
onChange={(name) => setFilter((p) => ({ ...p, name }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action buttons row */}
|
||||||
|
<div className='flex items-center gap-2 ml-auto'>
|
||||||
|
{/* export dropdown - text hidden on mobile */}
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant='outline' className='flex items-center gap-1'>
|
<Button variant='outline' size='sm' className='flex items-center gap-1'>
|
||||||
<Download className='mr-1 h-4 w-4' />
|
<Download className='h-4 w-4' />
|
||||||
Export
|
<span className='hidden sm:inline'>Export</span>
|
||||||
<ChevronDown className='ml-1 h-4 w-4' />
|
<ChevronDown className='h-4 w-4 hidden sm:inline' />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
@@ -438,6 +445,7 @@ export function ActivityLog() {
|
|||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => fetchActivities(false)} // Manual refresh, show loading skeleton
|
onClick={() => fetchActivities(false)} // Manual refresh, show loading skeleton
|
||||||
title="Refresh activity log"
|
title="Refresh activity log"
|
||||||
|
className='h-8 w-8 sm:h-9 sm:w-9'
|
||||||
>
|
>
|
||||||
<RefreshCw className='h-4 w-4' />
|
<RefreshCw className='h-4 w-4' />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -448,11 +456,12 @@ export function ActivityLog() {
|
|||||||
size="icon"
|
size="icon"
|
||||||
onClick={handleCleanupClick}
|
onClick={handleCleanupClick}
|
||||||
title="Delete all activities"
|
title="Delete all activities"
|
||||||
className="text-destructive hover:text-destructive"
|
className="text-destructive hover:text-destructive h-8 w-8 sm:h-9 sm:w-9"
|
||||||
>
|
>
|
||||||
<Trash2 className='h-4 w-4' />
|
<Trash2 className='h-4 w-4' />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* activity list */}
|
{/* activity list */}
|
||||||
<ActivityList
|
<ActivityList
|
||||||
|
|||||||
@@ -7,7 +7,13 @@ import { toast } from "sonner";
|
|||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { useLiveRefresh } from "@/hooks/useLiveRefresh";
|
import { useLiveRefresh } from "@/hooks/useLiveRefresh";
|
||||||
import { useConfigStatus } from "@/hooks/useConfigStatus";
|
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 {
|
interface HeaderProps {
|
||||||
currentPage?: "dashboard" | "repositories" | "organizations" | "configuration" | "activity-log";
|
currentPage?: "dashboard" | "repositories" | "organizations" | "configuration" | "activity-log";
|
||||||
@@ -60,9 +66,9 @@ export function Header({ currentPage, onNavigate, onMenuClick }: HeaderProps) {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* Hamburger Menu Button - Mobile Only */}
|
{/* Hamburger Menu Button - Mobile Only */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="outline"
|
||||||
size="sm"
|
size="lg"
|
||||||
className="lg:hidden p-2"
|
className="lg:hidden"
|
||||||
onClick={onMenuClick}
|
onClick={onMenuClick}
|
||||||
>
|
>
|
||||||
<Menu className="h-5 w-5" />
|
<Menu className="h-5 w-5" />
|
||||||
@@ -96,12 +102,12 @@ export function Header({ currentPage, onNavigate, onMenuClick }: HeaderProps) {
|
|||||||
{showLiveButton && (
|
{showLiveButton && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="lg"
|
||||||
className="flex items-center gap-2 sm:px-4"
|
className="flex items-center gap-1.5 px-3 sm:px-4"
|
||||||
onClick={toggleLive}
|
onClick={toggleLive}
|
||||||
title={getTooltip()}
|
title={getTooltip()}
|
||||||
>
|
>
|
||||||
<div className={`w-3 h-3 rounded-full ${
|
<div className={`size-4 sm:size-3 rounded-full ${
|
||||||
configLoading
|
configLoading
|
||||||
? 'bg-yellow-400 animate-pulse'
|
? 'bg-yellow-400 animate-pulse'
|
||||||
: isLiveActive
|
: isLiveActive
|
||||||
@@ -110,7 +116,7 @@ export function Header({ currentPage, onNavigate, onMenuClick }: HeaderProps) {
|
|||||||
? 'bg-orange-400'
|
? 'bg-orange-400'
|
||||||
: 'bg-gray-500'
|
: 'bg-gray-500'
|
||||||
}`} />
|
}`} />
|
||||||
<span className="hidden sm:inline">LIVE</span>
|
<span className="text-sm font-medium hidden sm:inline">LIVE</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -120,15 +126,40 @@ export function Header({ currentPage, onNavigate, onMenuClick }: HeaderProps) {
|
|||||||
<AuthButtonsSkeleton />
|
<AuthButtonsSkeleton />
|
||||||
) : user ? (
|
) : user ? (
|
||||||
<>
|
<>
|
||||||
|
{/* Desktop: Show avatar and logout button */}
|
||||||
|
<div className="hidden sm:flex sm:items-center sm:gap-4">
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AvatarImage src="" alt="@shadcn" />
|
<AvatarImage src="" alt="@shadcn" />
|
||||||
<AvatarFallback>
|
<AvatarFallback>
|
||||||
{user.username.charAt(0).toUpperCase()}
|
{user.username.charAt(0).toUpperCase()}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Button variant="outline" size="sm" onClick={handleLogout} className="hidden sm:inline-flex">
|
<Button variant="outline" size="sm" onClick={handleLogout}>
|
||||||
Logout
|
Logout
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile: Avatar with dropdown */}
|
||||||
|
<div className="sm:hidden">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline" size="lg" className="relative h-10 w-10 rounded-full p-0">
|
||||||
|
<Avatar className="h-full w-full">
|
||||||
|
<AvatarImage src="" alt="@shadcn" />
|
||||||
|
<AvatarFallback>
|
||||||
|
{user.username.charAt(0).toUpperCase()}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-48">
|
||||||
|
<DropdownMenuItem onClick={handleLogout} className="cursor-pointer">
|
||||||
|
<LogOut className="h-4 w-4 mr-2" />
|
||||||
|
Logout
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Button variant="outline" size="sm" asChild>
|
<Button variant="outline" size="sm" asChild>
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export function Sidebar({ className, onNavigate, isOpen, onClose }: SidebarProps
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col h-full pt-4">
|
<div className="flex flex-col h-full pt-4">
|
||||||
<nav className="flex flex-col gap-y-1 pl-2 pr-3">
|
<nav className="flex flex-col gap-y-1 lg:gap-y-1 pl-2 pr-3">
|
||||||
{links.map((link, index) => {
|
{links.map((link, index) => {
|
||||||
const isActive = currentPath === link.href;
|
const isActive = currentPath === link.href;
|
||||||
const Icon = link.icon;
|
const Icon = link.icon;
|
||||||
@@ -88,13 +88,13 @@ export function Sidebar({ className, onNavigate, isOpen, onClose }: SidebarProps
|
|||||||
key={index}
|
key={index}
|
||||||
onClick={(e) => handleNavigation(link.href, e)}
|
onClick={(e) => handleNavigation(link.href, e)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors w-full text-left",
|
"flex items-center gap-3 rounded-md px-3 py-3 lg:py-2 text-sm lg:text-sm font-medium transition-colors w-full text-left",
|
||||||
isActive
|
isActive
|
||||||
? "bg-primary text-primary-foreground"
|
? "bg-primary text-primary-foreground"
|
||||||
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon className="h-4 w-4" />
|
<Icon className="h-5 w-5 lg:h-4 lg:w-4" />
|
||||||
{link.label}
|
{link.label}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
@@ -102,19 +102,19 @@ export function Sidebar({ className, onNavigate, isOpen, onClose }: SidebarProps
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="mt-auto px-4 py-4">
|
<div className="mt-auto px-4 py-4">
|
||||||
<div className="rounded-md bg-muted p-3">
|
<div className="rounded-md bg-muted p-3 lg:p-3">
|
||||||
<h4 className="text-sm font-medium mb-2">Need Help?</h4>
|
<h4 className="text-sm font-medium mb-2">Need Help?</h4>
|
||||||
<p className="text-xs text-muted-foreground mb-2">
|
<p className="text-xs text-muted-foreground mb-3 lg:mb-2">
|
||||||
Check out the documentation for help with setup and configuration.
|
Check out the documentation for help with setup and configuration.
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
href="/docs"
|
href="/docs"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-1 text-xs text-primary hover:underline"
|
className="inline-flex items-center gap-1.5 text-xs lg:text-xs text-primary hover:underline py-2 lg:py-0"
|
||||||
>
|
>
|
||||||
Documentation
|
Documentation
|
||||||
<ExternalLink className="h-3 w-3" />
|
<ExternalLink className="h-3.5 w-3.5 lg:h-3 lg:w-3" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<VersionInfo />
|
<VersionInfo />
|
||||||
|
|||||||
@@ -63,12 +63,12 @@ export default function AddOrganizationDialog({
|
|||||||
return (
|
return (
|
||||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button className="fixed bottom-6 right-6 rounded-full h-12 w-12 shadow-lg p-0">
|
<Button className="fixed bottom-4 right-4 sm:bottom-6 sm:right-6 rounded-full h-12 w-12 shadow-lg p-0 z-10">
|
||||||
<Plus className="h-6 w-6" />
|
<Plus className="h-6 w-6" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|
||||||
<DialogContent className="sm:max-w-[425px] gap-0 gap-y-6">
|
<DialogContent className="w-[calc(100%-2rem)] sm:max-w-[425px] gap-0 gap-y-6 mx-4 sm:mx-0">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Add Organization</DialogTitle>
|
<DialogTitle>Add Organization</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
@@ -293,10 +293,10 @@ export function Organization() {
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-y-8">
|
<div className="flex flex-col gap-y-4 sm:gap-y-8">
|
||||||
{/* Combine search and actions into a single flex row */}
|
{/* Search and filters */}
|
||||||
<div className="flex flex-row items-center gap-4 w-full flex-wrap">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-2 sm:gap-4 w-full">
|
||||||
<div className="relative flex-grow">
|
<div className="relative w-full sm:flex-grow">
|
||||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -309,6 +309,8 @@ export function Organization() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Filter controls */}
|
||||||
|
<div className="flex items-center gap-2 w-full sm:w-auto">
|
||||||
{/* Membership Role Filter */}
|
{/* Membership Role Filter */}
|
||||||
<Select
|
<Select
|
||||||
value={filter.membershipRole || "all"}
|
value={filter.membershipRole || "all"}
|
||||||
@@ -319,7 +321,7 @@ export function Organization() {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[140px] h-9 max-h-9">
|
<SelectTrigger className="w-full sm:w-[140px] h-9 max-h-9">
|
||||||
<SelectValue placeholder="All Roles" />
|
<SelectValue placeholder="All Roles" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -355,7 +357,7 @@ export function Organization() {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[140px] h-9 max-h-9">
|
<SelectTrigger className="w-full sm:w-[140px] h-9 max-h-9">
|
||||||
<SelectValue placeholder="All Statuses" />
|
<SelectValue placeholder="All Statuses" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -376,25 +378,33 @@ export function Organization() {
|
|||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div className="flex items-center gap-2 ml-auto">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
title="Refresh organizations"
|
title="Refresh organizations"
|
||||||
|
className="h-8 w-8 sm:h-9 sm:w-9"
|
||||||
>
|
>
|
||||||
<RefreshCw className="h-4 w-4" />
|
<RefreshCw className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
|
size="sm"
|
||||||
onClick={handleMirrorAllOrgs}
|
onClick={handleMirrorAllOrgs}
|
||||||
disabled={isLoading || loadingOrgIds.size > 0}
|
disabled={isLoading || loadingOrgIds.size > 0}
|
||||||
|
className="sm:size-default"
|
||||||
>
|
>
|
||||||
<FlipHorizontal className="h-4 w-4 mr-2" />
|
<FlipHorizontal className="h-4 w-4 mr-2" />
|
||||||
Mirror All
|
<span className="hidden sm:inline">Mirror All</span>
|
||||||
|
<span className="sm:hidden">Mirror</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<OrganizationList
|
<OrganizationList
|
||||||
organizations={organizations}
|
organizations={organizations}
|
||||||
|
|||||||
@@ -60,12 +60,12 @@ export default function AddRepositoryDialog({
|
|||||||
return (
|
return (
|
||||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button className="fixed bottom-6 right-6 rounded-full h-12 w-12 shadow-lg p-0">
|
<Button className="fixed bottom-4 right-4 sm:bottom-6 sm:right-6 rounded-full h-12 w-12 shadow-lg p-0 z-10">
|
||||||
<Plus className="h-6 w-6" />
|
<Plus className="h-6 w-6" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|
||||||
<DialogContent className="sm:max-w-[425px] gap-0 gap-y-6">
|
<DialogContent className="w-[calc(100%-2rem)] sm:max-w-[425px] gap-0 gap-y-6 mx-4 sm:mx-0">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Add Repository</DialogTitle>
|
<DialogTitle>Add Repository</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
Reference in New Issue
Block a user