Virtualize (kind of) the list

This commit is contained in:
Thomas Camlong 2025-04-17 16:19:42 +02:00
parent b5e2cca8d9
commit e90d3c4b7f
No known key found for this signature in database
GPG Key ID: A678F374F428457B
3 changed files with 69 additions and 82 deletions

View File

@ -399,11 +399,8 @@ export function IconSearch({ icons }: IconSearchProps) {
<span>{getSortLabel(sortOption)}</span> <span>{getSortLabel(sortOption)}</span>
</div> </div>
</div> </div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4 mt-2">
{filteredIcons.map(({ name, data }) => ( <IconsGrid filteredIcons={filteredIcons} matchedAliases={matchedAliases} />
<IconCard key={name} name={name} data={data} matchedAlias={matchedAliases[name] || null} />
))}
</div>
</> </>
)} )}
</> </>
@ -419,7 +416,6 @@ function IconCard({
data: Icon data: Icon
matchedAlias?: string | null matchedAlias?: string | null
}) { }) {
return ( return (
<MagicCard className="rounded-md shadow-md"> <MagicCard className="rounded-md shadow-md">
<Link prefetch={false} href={`/icons/${name}`} className="group flex flex-col items-center p-3 sm:p-4 cursor-pointer"> <Link prefetch={false} href={`/icons/${name}`} className="group flex flex-col items-center p-3 sm:p-4 cursor-pointer">
@ -440,3 +436,21 @@ function IconCard({
</MagicCard> </MagicCard>
) )
} }
interface IconsGridProps {
filteredIcons: { name: string; data: Icon }[]
matchedAliases: Record<string, string>
}
function IconsGrid({ filteredIcons, matchedAliases }: IconsGridProps) {
return (
<>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4 mt-2">
{filteredIcons.slice(0, 120).map(({ name, data }) => (
<IconCard key={name} name={name} data={data} matchedAlias={matchedAliases[name] || null} />
))}
</div>
{filteredIcons.length > 120 && <p className="text-sm text-muted-foreground">And {filteredIcons.length - 120} more...</p>}
</>
)
}

View File

@ -1,19 +1,19 @@
"use client"; "use client"
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card"
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
import { motion, useAnimation, useInView } from "framer-motion"; import { motion, useAnimation, useInView } from "framer-motion"
import { DollarSign, Heart, Search, Star } from "lucide-react"; import { DollarSign, Heart, Search, Star } from "lucide-react"
import Link from "next/link"; import Link from "next/link"
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react"
import { AuroraText } from "./magicui/aurora-text"; import { AuroraText } from "./magicui/aurora-text"
import { InteractiveHoverButton } from "./magicui/interactive-hover-button"; import { InteractiveHoverButton } from "./magicui/interactive-hover-button"
interface IconCardProps { interface IconCardProps {
name: string; name: string
imageUrl: string; imageUrl: string
} }
function IconCard({ name, imageUrl }: IconCardProps) { function IconCard({ name, imageUrl }: IconCardProps) {
@ -22,11 +22,9 @@ function IconCard({ name, imageUrl }: IconCardProps) {
<div className="w-16 h-16 flex items-center justify-center"> <div className="w-16 h-16 flex items-center justify-center">
<img src={imageUrl} alt={name} className="max-w-full max-h-full" /> <img src={imageUrl} alt={name} className="max-w-full max-h-full" />
</div> </div>
<p className="text-sm text-center text-muted-foreground group-hover:text-foreground transition-colors"> <p className="text-sm text-center text-muted-foreground group-hover:text-foreground transition-colors">{name}</p>
{name}
</p>
</Card> </Card>
); )
} }
function ElegantShape({ function ElegantShape({
@ -39,28 +37,28 @@ function ElegantShape({
mobileWidth, mobileWidth,
mobileHeight, mobileHeight,
}: { }: {
className?: string; className?: string
delay?: number; delay?: number
width?: number; width?: number
height?: number; height?: number
rotate?: number; rotate?: number
gradient?: string; gradient?: string
mobileWidth?: number; mobileWidth?: number
mobileHeight?: number; mobileHeight?: number
}) { }) {
const controls = useAnimation(); const controls = useAnimation()
const [isMobile, setIsMobile] = useState(false); const [isMobile, setIsMobile] = useState(false)
const ref = useRef(null); const ref = useRef(null)
const isInView = useInView(ref, { once: true, amount: 0.1 }); const isInView = useInView(ref, { once: true, amount: 0.1 })
useEffect(() => { useEffect(() => {
const checkMobile = () => { const checkMobile = () => {
setIsMobile(window.innerWidth < 768); setIsMobile(window.innerWidth < 768)
}; }
checkMobile(); checkMobile()
window.addEventListener("resize", checkMobile); window.addEventListener("resize", checkMobile)
return () => window.removeEventListener("resize", checkMobile); return () => window.removeEventListener("resize", checkMobile)
}, []); }, [])
useEffect(() => { useEffect(() => {
if (isInView) { if (isInView) {
@ -77,9 +75,9 @@ function ElegantShape({
ease: [0.23, 0.86, 0.39, 0.96], ease: [0.23, 0.86, 0.39, 0.96],
opacity: { duration: 1.2 }, opacity: { duration: 1.2 },
}, },
}); })
} }
}, [controls, delay, isInView, rotate]); }, [controls, delay, isInView, rotate])
return ( return (
<motion.div <motion.div
@ -121,14 +119,11 @@ function ElegantShape({
/> />
</motion.div> </motion.div>
</motion.div> </motion.div>
); )
} }
export function HeroSection({ export function HeroSection({ totalIcons, stars }: { totalIcons: number; stars: number }) {
totalIcons, const [searchQuery, setSearchQuery] = useState("")
stars,
}: { totalIcons: number; stars: number }) {
const [searchQuery, setSearchQuery] = useState("");
return ( return (
<div className="relative my-20 w-full flex items-center justify-center overflow-hidden"> <div className="relative my-20 w-full flex items-center justify-center overflow-hidden">
@ -193,40 +188,26 @@ export function HeroSection({
<div className="relative z-10 container mx-auto px-4 md:px-6"> <div className="relative z-10 container mx-auto px-4 md:px-6">
<div className="max-w-4xl mx-auto text-center flex flex-col gap-4"> <div className="max-w-4xl mx-auto text-center flex flex-col gap-4">
<Link <Link prefetch href="https://github.com/homarr-labs" target="_blank" rel="noopener noreferrer" className="mx-auto">
prefetch
href="https://github.com/homarr-labs"
target="_blank"
rel="noopener noreferrer"
className="mx-auto"
>
<Card className="group p-2 px-4 flex flex-row items-center gap-2 border-2 z-10 relative glass-effect motion-safe:motion-preset-slide-up motion-duration-1500 hover:scale-105 transition-all duration-300"> <Card className="group p-2 px-4 flex flex-row items-center gap-2 border-2 z-10 relative glass-effect motion-safe:motion-preset-slide-up motion-duration-1500 hover:scale-105 transition-all duration-300">
<Heart <Heart
// Filled when hovered // Filled when hovered
className="h-4 w-4 text-primary group-hover:fill-primary transition-all duration-300" className="h-4 w-4 text-primary group-hover:fill-primary transition-all duration-300"
/> />
<span className="text-sm text-foreground/70 tracking-wide"> <span className="text-sm text-foreground/70 tracking-wide">Made with love by Homarr Labs</span>
Made with love by Homarr Labs
</span>
</Card> </Card>
</Link> </Link>
<h1 className="text-3xl sm:text-5xl md:text-7xl font-bold mb-4 md:mb-8 tracking-tight motion-safe:motion-preset-slide-up motion-duration-1500"> <h1 className="text-3xl sm:text-5xl md:text-7xl font-bold mb-4 md:mb-8 tracking-tight motion-safe:motion-preset-slide-up motion-duration-1500">
Your definitive source for Your definitive source for
<br /> <br />
<AuroraText colors={["#FA5352", "#FA5352", "orange"]}> <AuroraText colors={["#FA5352", "#FA5352", "orange"]}>dashboard icons</AuroraText>
dashboard icons
</AuroraText>
</h1> </h1>
<motion.div <motion.div custom={2} className="motion-safe:motion-preset-slide-up motion-duration-1500">
custom={2}
className="motion-safe:motion-preset-slide-up motion-duration-1500"
>
<p className="text-sm sm:text-base md:text-xl text-muted-foreground mb-6 md:mb-8 leading-relaxed font-light tracking-wide max-w-2xl mx-auto px-4"> <p className="text-sm sm:text-base md:text-xl text-muted-foreground mb-6 md:mb-8 leading-relaxed font-light tracking-wide max-w-2xl mx-auto px-4">
A collection of <span className="font-medium ">{totalIcons}</span>{" "} A collection of <span className="font-medium ">{totalIcons}</span> curated icons for services, applications and tools,
curated icons for services, applications and tools, designed designed specifically for dashboards and app directories.
specifically for dashboards and app directories.
</p> </p>
</motion.div> </motion.div>
@ -234,11 +215,7 @@ export function HeroSection({
custom={3} custom={3}
className="flex flex-col items-center gap-4 md:gap-6 mb-8 md:mb-12 motion-safe:motion-preset-slide-up motion-duration-1500" className="flex flex-col items-center gap-4 md:gap-6 mb-8 md:mb-12 motion-safe:motion-preset-slide-up motion-duration-1500"
> >
<form <form action="/icons" method="GET" className="relative w-full max-w-md group">
action="/icons"
method="GET"
className="relative w-full max-w-md group"
>
<Input <Input
name="q" name="q"
autoFocus autoFocus
@ -258,9 +235,7 @@ export function HeroSection({
</form> </form>
<div className="flex gap-3 md:gap-4 flex-wrap justify-center"> <div className="flex gap-3 md:gap-4 flex-wrap justify-center">
<Link href="/icons"> <Link href="/icons">
<InteractiveHoverButton className="rounded-md bg-input/30"> <InteractiveHoverButton className="rounded-md bg-input/30">Explore icons</InteractiveHoverButton>
Explore icons
</InteractiveHoverButton>
</Link> </Link>
<Link <Link
href="https://github.com/homarr-labs/dashboard-icons" href="https://github.com/homarr-labs/dashboard-icons"
@ -272,9 +247,7 @@ export function HeroSection({
<div> <div>
<p>Give us a star</p> <p>Give us a star</p>
<Star className="h-4 w-4 ml-1 text-yellow-500 fill-yellow-500" /> <Star className="h-4 w-4 ml-1 text-yellow-500 fill-yellow-500" />
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">{stars}</span>
{stars}
</span>
</div> </div>
</Button> </Button>
</Link> </Link>
@ -294,5 +267,5 @@ export function HeroSection({
<div className="absolute inset-0 bg-gradient-to-t from-background via-transparent to-background/80 pointer-events-none" /> <div className="absolute inset-0 bg-gradient-to-t from-background via-transparent to-background/80 pointer-events-none" />
</div> </div>
); )
} }