feat: Add newest scripts highlighting section (#179)
- Add horizontal scrollable carousel for 6 newest scripts - Only show when no filters are active to avoid duplication - Exclude newest scripts from main grid when carousel is visible - Add Clock icon and subtle left border accent for visual distinction - Include NEW badges on script cards in carousel - Responsive design for mobile, tablet, and desktop - Sort by date_created field in descending order
This commit is contained in:
committed by
GitHub
parent
16e918e9b4
commit
d0312165bd
@@ -9,6 +9,7 @@ import { CategorySidebar } from './CategorySidebar';
|
|||||||
import { FilterBar, type FilterState } from './FilterBar';
|
import { FilterBar, type FilterState } from './FilterBar';
|
||||||
import { ViewToggle } from './ViewToggle';
|
import { ViewToggle } from './ViewToggle';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
|
import { Clock } from 'lucide-react';
|
||||||
import type { ScriptCard as ScriptCardType } from '~/types/script';
|
import type { ScriptCard as ScriptCardType } from '~/types/script';
|
||||||
|
|
||||||
|
|
||||||
@@ -220,6 +221,31 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
|||||||
});
|
});
|
||||||
}, [combinedScripts, localScriptsData]);
|
}, [combinedScripts, localScriptsData]);
|
||||||
|
|
||||||
|
// Check if any filters are active (excluding default state)
|
||||||
|
const hasActiveFilters = React.useMemo(() => {
|
||||||
|
return (
|
||||||
|
filters.searchQuery?.trim() !== '' ||
|
||||||
|
filters.showUpdatable !== null ||
|
||||||
|
filters.selectedTypes.length > 0 ||
|
||||||
|
filters.sortBy !== 'name' ||
|
||||||
|
filters.sortOrder !== 'asc' ||
|
||||||
|
selectedCategory !== null
|
||||||
|
);
|
||||||
|
}, [filters, selectedCategory]);
|
||||||
|
|
||||||
|
// Get the 6 newest scripts based on date_created field
|
||||||
|
const newestScripts = React.useMemo((): ScriptCardType[] => {
|
||||||
|
return scriptsWithStatus
|
||||||
|
.filter(script => script?.date_created) // Only scripts with date_created
|
||||||
|
.sort((a, b) => {
|
||||||
|
const aCreated = a?.date_created ?? '';
|
||||||
|
const bCreated = b?.date_created ?? '';
|
||||||
|
// Sort by date descending (newest first)
|
||||||
|
return bCreated.localeCompare(aCreated);
|
||||||
|
})
|
||||||
|
.slice(0, 6); // Take only the first 6
|
||||||
|
}, [scriptsWithStatus]);
|
||||||
|
|
||||||
// Filter scripts based on all filters and category
|
// Filter scripts based on all filters and category
|
||||||
const filteredScripts = React.useMemo((): ScriptCardType[] => {
|
const filteredScripts = React.useMemo((): ScriptCardType[] => {
|
||||||
let scripts = scriptsWithStatus;
|
let scripts = scriptsWithStatus;
|
||||||
@@ -270,6 +296,12 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Exclude newest scripts from main grid when no filters are active (they'll be shown in carousel)
|
||||||
|
if (!hasActiveFilters) {
|
||||||
|
const newestScriptSlugs = new Set(newestScripts.map(script => script.slug).filter(Boolean));
|
||||||
|
scripts = scripts.filter(script => !newestScriptSlugs.has(script.slug));
|
||||||
|
}
|
||||||
|
|
||||||
// Apply sorting
|
// Apply sorting
|
||||||
scripts.sort((a, b) => {
|
scripts.sort((a, b) => {
|
||||||
if (!a || !b) return 0;
|
if (!a || !b) return 0;
|
||||||
@@ -309,7 +341,7 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return scripts;
|
return scripts;
|
||||||
}, [scriptsWithStatus, filters, selectedCategory]);
|
}, [scriptsWithStatus, filters, selectedCategory, hasActiveFilters, newestScripts]);
|
||||||
|
|
||||||
// Calculate filter counts for FilterBar
|
// Calculate filter counts for FilterBar
|
||||||
const filterCounts = React.useMemo(() => {
|
const filterCounts = React.useMemo(() => {
|
||||||
@@ -619,6 +651,51 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
|||||||
onViewModeChange={setViewMode}
|
onViewModeChange={setViewMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Newest Scripts Carousel - Only show when no filters are active */}
|
||||||
|
{!hasActiveFilters && newestScripts.length > 0 && (
|
||||||
|
<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="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-xl font-semibold text-foreground flex items-center gap-2">
|
||||||
|
<Clock className="h-6 w-6 text-primary" />
|
||||||
|
Newest Scripts
|
||||||
|
</h2>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{newestScripts.length} recently added
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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-green-600 text-white text-xs font-semibold px-2 py-1 rounded-md shadow-md z-10">
|
||||||
|
NEW
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<div className="flex flex-wrap gap-2 mb-4">
|
<div className="flex flex-wrap gap-2 mb-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user