Merge pull request #281 from community-scripts/fix/270
Add minimize buttons to FilterBar and Newest Scripts sections
This commit is contained in:
@@ -41,6 +41,7 @@ export function FilterBar({
|
|||||||
}: FilterBarProps) {
|
}: FilterBarProps) {
|
||||||
const [isTypeDropdownOpen, setIsTypeDropdownOpen] = useState(false);
|
const [isTypeDropdownOpen, setIsTypeDropdownOpen] = useState(false);
|
||||||
const [isSortDropdownOpen, setIsSortDropdownOpen] = useState(false);
|
const [isSortDropdownOpen, setIsSortDropdownOpen] = useState(false);
|
||||||
|
const [isMinimized, setIsMinimized] = useState(false);
|
||||||
|
|
||||||
const updateFilters = (updates: Partial<FilterState>) => {
|
const updateFilters = (updates: Partial<FilterState>) => {
|
||||||
onFiltersChange({ ...filters, ...updates });
|
onFiltersChange({ ...filters, ...updates });
|
||||||
@@ -98,44 +99,17 @@ export function FilterBar({
|
|||||||
{!isLoadingFilters && (
|
{!isLoadingFilters && (
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<h3 className="text-lg font-medium text-foreground">Filter Scripts</h3>
|
<h3 className="text-lg font-medium text-foreground">Filter Scripts</h3>
|
||||||
<ContextualHelpIcon section="available-scripts" tooltip="Help with filtering and searching" />
|
<div className="flex items-center gap-2">
|
||||||
</div>
|
<ContextualHelpIcon section="available-scripts" tooltip="Help with filtering and searching" />
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Search Bar */}
|
|
||||||
<div className="mb-4">
|
|
||||||
<div className="relative max-w-md w-full">
|
|
||||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
|
||||||
<svg
|
|
||||||
className="h-5 w-5 text-muted-foreground"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search scripts..."
|
|
||||||
value={filters.searchQuery}
|
|
||||||
onChange={(e) => updateFilters({ searchQuery: e.target.value })}
|
|
||||||
className="block w-full rounded-lg border border-input bg-background py-3 pr-10 pl-10 text-sm leading-5 text-foreground placeholder-muted-foreground focus:border-primary focus:placeholder-muted-foreground focus:ring-2 focus:ring-primary focus:outline-none"
|
|
||||||
/>
|
|
||||||
{filters.searchQuery && (
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => updateFilters({ searchQuery: "" })}
|
onClick={() => setIsMinimized(!isMinimized)}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="absolute inset-y-0 right-0 pr-3 text-muted-foreground hover:text-foreground"
|
className="h-8 w-8 text-muted-foreground hover:text-foreground"
|
||||||
|
title={isMinimized ? "Expand filters" : "Minimize filters"}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
className="h-5 w-5"
|
className={`h-4 w-4 transition-transform ${isMinimized ? "" : "rotate-180"}`}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -144,16 +118,69 @@ export function FilterBar({
|
|||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
d="M6 18L18 6M6 6l12 12"
|
d="M5 15l7-7 7 7"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Filter Buttons */}
|
{/* Filter Content - Conditionally rendered based on minimized state */}
|
||||||
<div className="mb-4 flex flex-col sm:flex-row flex-wrap gap-2 sm:gap-3">
|
{!isMinimized && !isLoadingFilters && (
|
||||||
|
<>
|
||||||
|
{/* Search Bar */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="relative max-w-md w-full">
|
||||||
|
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 text-muted-foreground"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search scripts..."
|
||||||
|
value={filters.searchQuery}
|
||||||
|
onChange={(e) => updateFilters({ searchQuery: e.target.value })}
|
||||||
|
className="block w-full rounded-lg border border-input bg-background py-3 pr-10 pl-10 text-sm leading-5 text-foreground placeholder-muted-foreground focus:border-primary focus:placeholder-muted-foreground focus:ring-2 focus:ring-primary focus:outline-none"
|
||||||
|
/>
|
||||||
|
{filters.searchQuery && (
|
||||||
|
<Button
|
||||||
|
onClick={() => updateFilters({ searchQuery: "" })}
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="absolute inset-y-0 right-0 pr-3 text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter Buttons */}
|
||||||
|
<div className="mb-4 flex flex-col sm:flex-row flex-wrap gap-2 sm:gap-3">
|
||||||
{/* Updateable Filter */}
|
{/* Updateable Filter */}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -431,6 +458,8 @@ export function FilterBar({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Click outside to close dropdowns */}
|
{/* Click outside to close dropdowns */}
|
||||||
{(isTypeDropdownOpen || isSortDropdownOpen) && (
|
{(isTypeDropdownOpen || isSortDropdownOpen) && (
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
|||||||
});
|
});
|
||||||
const [saveFiltersEnabled, setSaveFiltersEnabled] = useState(false);
|
const [saveFiltersEnabled, setSaveFiltersEnabled] = useState(false);
|
||||||
const [isLoadingFilters, setIsLoadingFilters] = useState(true);
|
const [isLoadingFilters, setIsLoadingFilters] = useState(true);
|
||||||
|
const [isNewestMinimized, setIsNewestMinimized] = useState(false);
|
||||||
const gridRef = useRef<HTMLDivElement>(null);
|
const gridRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { data: scriptCardsData, isLoading: githubLoading, error: githubError, refetch } = api.scripts.getScriptCardsWithCategories.useQuery();
|
const { data: scriptCardsData, isLoading: githubLoading, error: githubError, refetch } = api.scripts.getScriptCardsWithCategories.useQuery();
|
||||||
@@ -689,8 +690,8 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
|||||||
onViewModeChange={setViewMode}
|
onViewModeChange={setViewMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Newest Scripts Carousel - Always show when there are newest scripts */}
|
{/* Newest Scripts Carousel - Only show when no search, filters, or category is active */}
|
||||||
{newestScripts.length > 0 && (
|
{newestScripts.length > 0 && !hasActiveFilters && !selectedCategory && (
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="bg-card border-l-4 border-l-primary border border-border rounded-lg p-6 shadow-lg">
|
<div className="bg-card border-l-4 border-l-primary border border-border rounded-lg p-6 shadow-lg">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
@@ -698,39 +699,64 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
|||||||
<Clock className="h-6 w-6 text-primary" />
|
<Clock className="h-6 w-6 text-primary" />
|
||||||
Newest Scripts
|
Newest Scripts
|
||||||
</h2>
|
</h2>
|
||||||
<span className="text-sm text-muted-foreground">
|
<div className="flex items-center gap-2">
|
||||||
{newestScripts.length} recently added
|
<span className="text-sm text-muted-foreground">
|
||||||
</span>
|
{newestScripts.length} recently added
|
||||||
</div>
|
</span>
|
||||||
|
<Button
|
||||||
<div className="overflow-x-auto scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-600 scrollbar-track-transparent">
|
onClick={() => setIsNewestMinimized(!isNewestMinimized)}
|
||||||
<div className="flex gap-4 pb-2" style={{ minWidth: 'max-content' }}>
|
variant="ghost"
|
||||||
{newestScripts.map((script, index) => {
|
size="icon"
|
||||||
if (!script || typeof script !== 'object') {
|
className="h-8 w-8 text-muted-foreground hover:text-foreground"
|
||||||
return null;
|
title={isNewestMinimized ? "Expand newest scripts" : "Minimize newest scripts"}
|
||||||
}
|
>
|
||||||
|
<svg
|
||||||
const uniqueKey = `newest-${script.slug ?? 'unknown'}-${script.name ?? 'unnamed'}-${index}`;
|
className={`h-4 w-4 transition-transform ${isNewestMinimized ? "" : "rotate-180"}`}
|
||||||
|
fill="none"
|
||||||
return (
|
stroke="currentColor"
|
||||||
<div key={uniqueKey} className="flex-shrink-0 w-64 sm:w-72 md:w-80">
|
viewBox="0 0 24 24"
|
||||||
<div className="relative">
|
>
|
||||||
<ScriptCard
|
<path
|
||||||
script={script}
|
strokeLinecap="round"
|
||||||
onClick={handleCardClick}
|
strokeLinejoin="round"
|
||||||
isSelected={selectedSlugs.has(script.slug ?? '')}
|
strokeWidth={2}
|
||||||
onToggleSelect={toggleScriptSelection}
|
d="M5 15l7-7 7 7"
|
||||||
/>
|
/>
|
||||||
{/* NEW badge */}
|
</svg>
|
||||||
<div className="absolute top-2 right-2 bg-success text-success-foreground text-xs font-semibold px-2 py-1 rounded-md shadow-md z-10">
|
</Button>
|
||||||
NEW
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!isNewestMinimized && (
|
||||||
|
<div className="overflow-x-auto scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-600 scrollbar-track-transparent">
|
||||||
|
<div className="flex gap-4 pb-2" style={{ minWidth: 'max-content' }}>
|
||||||
|
{newestScripts.map((script, index) => {
|
||||||
|
if (!script || typeof script !== 'object') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueKey = `newest-${script.slug ?? 'unknown'}-${script.name ?? 'unnamed'}-${index}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={uniqueKey} className="flex-shrink-0 w-64 sm:w-72 md:w-80">
|
||||||
|
<div className="relative">
|
||||||
|
<ScriptCard
|
||||||
|
script={script}
|
||||||
|
onClick={handleCardClick}
|
||||||
|
isSelected={selectedSlugs.has(script.slug ?? '')}
|
||||||
|
onToggleSelect={toggleScriptSelection}
|
||||||
|
/>
|
||||||
|
{/* NEW badge */}
|
||||||
|
<div className="absolute top-2 right-2 bg-success text-success-foreground text-xs font-semibold px-2 py-1 rounded-md shadow-md z-10">
|
||||||
|
NEW
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user