From d0312165bde311139e930f82fe3d7dbfc2018955 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner <73236783+michelroegl-brunner@users.noreply.github.com> Date: Fri, 17 Oct 2025 14:29:32 +0200 Subject: [PATCH] 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 --- src/app/_components/ScriptsGrid.tsx | 79 ++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/src/app/_components/ScriptsGrid.tsx b/src/app/_components/ScriptsGrid.tsx index d2d22a6..8182b55 100644 --- a/src/app/_components/ScriptsGrid.tsx +++ b/src/app/_components/ScriptsGrid.tsx @@ -9,6 +9,7 @@ import { CategorySidebar } from './CategorySidebar'; import { FilterBar, type FilterState } from './FilterBar'; import { ViewToggle } from './ViewToggle'; import { Button } from './ui/button'; +import { Clock } from 'lucide-react'; import type { ScriptCard as ScriptCardType } from '~/types/script'; @@ -220,6 +221,31 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) { }); }, [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 const filteredScripts = React.useMemo((): ScriptCardType[] => { 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 scripts.sort((a, b) => { if (!a || !b) return 0; @@ -309,7 +341,7 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) { }); return scripts; - }, [scriptsWithStatus, filters, selectedCategory]); + }, [scriptsWithStatus, filters, selectedCategory, hasActiveFilters, newestScripts]); // Calculate filter counts for FilterBar const filterCounts = React.useMemo(() => { @@ -619,6 +651,51 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) { onViewModeChange={setViewMode} /> + {/* Newest Scripts Carousel - Only show when no filters are active */} + {!hasActiveFilters && newestScripts.length > 0 && ( +
+
+
+

+ + Newest Scripts +

+ + {newestScripts.length} recently added + +
+ +
+
+ {newestScripts.map((script, index) => { + if (!script || typeof script !== 'object') { + return null; + } + + const uniqueKey = `newest-${script.slug ?? 'unknown'}-${script.name ?? 'unnamed'}-${index}`; + + return ( +
+
+ + {/* NEW badge */} +
+ NEW +
+
+
+ ); + })} +
+
+
+
+ )} {/* Action Buttons */}