Skip to content

Commit d031216

Browse files
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
1 parent 16e918e commit d031216

File tree

1 file changed

+78
-1
lines changed

1 file changed

+78
-1
lines changed

src/app/_components/ScriptsGrid.tsx

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { CategorySidebar } from './CategorySidebar';
99
import { FilterBar, type FilterState } from './FilterBar';
1010
import { ViewToggle } from './ViewToggle';
1111
import { Button } from './ui/button';
12+
import { Clock } from 'lucide-react';
1213
import type { ScriptCard as ScriptCardType } from '~/types/script';
1314

1415

@@ -220,6 +221,31 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
220221
});
221222
}, [combinedScripts, localScriptsData]);
222223

224+
// Check if any filters are active (excluding default state)
225+
const hasActiveFilters = React.useMemo(() => {
226+
return (
227+
filters.searchQuery?.trim() !== '' ||
228+
filters.showUpdatable !== null ||
229+
filters.selectedTypes.length > 0 ||
230+
filters.sortBy !== 'name' ||
231+
filters.sortOrder !== 'asc' ||
232+
selectedCategory !== null
233+
);
234+
}, [filters, selectedCategory]);
235+
236+
// Get the 6 newest scripts based on date_created field
237+
const newestScripts = React.useMemo((): ScriptCardType[] => {
238+
return scriptsWithStatus
239+
.filter(script => script?.date_created) // Only scripts with date_created
240+
.sort((a, b) => {
241+
const aCreated = a?.date_created ?? '';
242+
const bCreated = b?.date_created ?? '';
243+
// Sort by date descending (newest first)
244+
return bCreated.localeCompare(aCreated);
245+
})
246+
.slice(0, 6); // Take only the first 6
247+
}, [scriptsWithStatus]);
248+
223249
// Filter scripts based on all filters and category
224250
const filteredScripts = React.useMemo((): ScriptCardType[] => {
225251
let scripts = scriptsWithStatus;
@@ -270,6 +296,12 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
270296
});
271297
}
272298

299+
// Exclude newest scripts from main grid when no filters are active (they'll be shown in carousel)
300+
if (!hasActiveFilters) {
301+
const newestScriptSlugs = new Set(newestScripts.map(script => script.slug).filter(Boolean));
302+
scripts = scripts.filter(script => !newestScriptSlugs.has(script.slug));
303+
}
304+
273305
// Apply sorting
274306
scripts.sort((a, b) => {
275307
if (!a || !b) return 0;
@@ -309,7 +341,7 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
309341
});
310342

311343
return scripts;
312-
}, [scriptsWithStatus, filters, selectedCategory]);
344+
}, [scriptsWithStatus, filters, selectedCategory, hasActiveFilters, newestScripts]);
313345

314346
// Calculate filter counts for FilterBar
315347
const filterCounts = React.useMemo(() => {
@@ -619,6 +651,51 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
619651
onViewModeChange={setViewMode}
620652
/>
621653

654+
{/* Newest Scripts Carousel - Only show when no filters are active */}
655+
{!hasActiveFilters && newestScripts.length > 0 && (
656+
<div className="mb-8">
657+
<div className="bg-card border-l-4 border-l-primary border border-border rounded-lg p-6 shadow-lg">
658+
<div className="flex items-center justify-between mb-4">
659+
<h2 className="text-xl font-semibold text-foreground flex items-center gap-2">
660+
<Clock className="h-6 w-6 text-primary" />
661+
Newest Scripts
662+
</h2>
663+
<span className="text-sm text-muted-foreground">
664+
{newestScripts.length} recently added
665+
</span>
666+
</div>
667+
668+
<div className="overflow-x-auto scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-600 scrollbar-track-transparent">
669+
<div className="flex gap-4 pb-2" style={{ minWidth: 'max-content' }}>
670+
{newestScripts.map((script, index) => {
671+
if (!script || typeof script !== 'object') {
672+
return null;
673+
}
674+
675+
const uniqueKey = `newest-${script.slug ?? 'unknown'}-${script.name ?? 'unnamed'}-${index}`;
676+
677+
return (
678+
<div key={uniqueKey} className="flex-shrink-0 w-64 sm:w-72 md:w-80">
679+
<div className="relative">
680+
<ScriptCard
681+
script={script}
682+
onClick={handleCardClick}
683+
isSelected={selectedSlugs.has(script.slug ?? '')}
684+
onToggleSelect={toggleScriptSelection}
685+
/>
686+
{/* NEW badge */}
687+
<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">
688+
NEW
689+
</div>
690+
</div>
691+
</div>
692+
);
693+
})}
694+
</div>
695+
</div>
696+
</div>
697+
</div>
698+
)}
622699

623700
{/* Action Buttons */}
624701
<div className="flex flex-wrap gap-2 mb-4">

0 commit comments

Comments
 (0)