mirror of
				https://github.com/walkxcode/dashboard-icons.git
				synced 2025-10-31 16:57:58 +01:00 
			
		
		
		
	Merge branch 'main' into feat/ph-capture-missing-icons
Signed-off-by: Thomas Camlong <thomas@ajnart.fr>
This commit is contained in:
		| @@ -1,7 +1,7 @@ | ||||
| import { getAllIcons } from "@/lib/api" | ||||
| import { ImageResponse } from "next/og" | ||||
| import { readFile } from "node:fs/promises" | ||||
| import { join } from "node:path" | ||||
| import { getAllIcons } from "@/lib/api" | ||||
| import { ImageResponse } from "next/og" | ||||
|  | ||||
| export const dynamic = "force-static" | ||||
|  | ||||
|   | ||||
| @@ -1,49 +1,44 @@ | ||||
| import { IconDetails } from "@/components/icon-details"; | ||||
| import { BASE_URL, WEB_URL } from "@/constants"; | ||||
| import { getAllIcons, getAuthorData } from "@/lib/api"; | ||||
| import type { Metadata, ResolvingMetadata } from "next"; | ||||
| import { notFound } from "next/navigation"; | ||||
| import { IconDetails } from "@/components/icon-details" | ||||
| import { BASE_URL, WEB_URL } from "@/constants" | ||||
| import { getAllIcons, getAuthorData } from "@/lib/api" | ||||
| import type { Metadata, ResolvingMetadata } from "next" | ||||
| import { notFound } from "next/navigation" | ||||
|  | ||||
| export const dynamicParams = false; | ||||
| export const dynamicParams = false | ||||
|  | ||||
| export async function generateStaticParams() { | ||||
| 	const iconsData = await getAllIcons(); | ||||
| 	const iconsData = await getAllIcons() | ||||
| 	return Object.keys(iconsData).map((icon) => ({ | ||||
| 		icon, | ||||
| 	})); | ||||
| 	})) | ||||
| } | ||||
|  | ||||
| export const dynamic = "force-static"; | ||||
| export const dynamic = "force-static" | ||||
|  | ||||
| type Props = { | ||||
| 	params: Promise<{ icon: string }>; | ||||
| 	searchParams: Promise<{ [key: string]: string | string[] | undefined }>; | ||||
| }; | ||||
| 	params: Promise<{ icon: string }> | ||||
| 	searchParams: Promise<{ [key: string]: string | string[] | undefined }> | ||||
| } | ||||
|  | ||||
| export async function generateMetadata( | ||||
| 	{ params, searchParams }: Props, | ||||
| 	parent: ResolvingMetadata, | ||||
| ): Promise<Metadata> { | ||||
| 	const { icon } = await params; | ||||
| 	const iconsData = await getAllIcons(); | ||||
| export async function generateMetadata({ params, searchParams }: Props, parent: ResolvingMetadata): Promise<Metadata> { | ||||
| 	const { icon } = await params | ||||
| 	const iconsData = await getAllIcons() | ||||
| 	if (!iconsData[icon]) { | ||||
| 		notFound(); | ||||
| 		notFound() | ||||
| 	} | ||||
| 	const authorData = await getAuthorData(iconsData[icon].update.author.id); | ||||
| 	const authorName = authorData.name || authorData.login; | ||||
| 	const updateDate = new Date(iconsData[icon].update.timestamp); | ||||
| 	const totalIcons = Object.keys(iconsData).length; | ||||
| 	const authorData = await getAuthorData(iconsData[icon].update.author.id) | ||||
| 	const authorName = authorData.name || authorData.login | ||||
| 	const updateDate = new Date(iconsData[icon].update.timestamp) | ||||
| 	const totalIcons = Object.keys(iconsData).length | ||||
|  | ||||
| 	console.debug( | ||||
| 		`Generated metadata for ${icon} by ${authorName} (${authorData.html_url}) updated at ${updateDate.toLocaleString()}`, | ||||
| 	); | ||||
| 	console.debug(`Generated metadata for ${icon} by ${authorName} (${authorData.html_url}) updated at ${updateDate.toLocaleString()}`) | ||||
|  | ||||
| 	const iconImageUrl = `${BASE_URL}/png/${icon}.png`; | ||||
| 	const pageUrl = `${WEB_URL}/icons/${icon}`; | ||||
| 	const iconImageUrl = `${BASE_URL}/png/${icon}.png` | ||||
| 	const pageUrl = `${WEB_URL}/icons/${icon}` | ||||
| 	const formattedIconName = icon | ||||
| 		.split("-") | ||||
| 		.map((word) => word.charAt(0).toUpperCase() + word.slice(1)) | ||||
| 		.join(" "); | ||||
| 		.join(" ") | ||||
|  | ||||
| 	return { | ||||
| 		title: `${formattedIconName} Icon | Dashboard Icons`, | ||||
| @@ -77,15 +72,7 @@ export async function generateMetadata( | ||||
| 			publishedTime: updateDate.toISOString(), | ||||
| 			modifiedTime: updateDate.toISOString(), | ||||
| 			section: "Icons", | ||||
| 			tags: [ | ||||
| 				formattedIconName, | ||||
| 				"dashboard icon", | ||||
| 				"service icon", | ||||
| 				"application icon", | ||||
| 				"tool icon", | ||||
| 				"web dashboard", | ||||
| 				"app directory", | ||||
| 			], | ||||
| 			tags: [formattedIconName, "dashboard icon", "service icon", "application icon", "tool icon", "web dashboard", "app directory"], | ||||
| 		}, | ||||
| 		twitter: { | ||||
| 			card: "summary_large_image", | ||||
| @@ -101,27 +88,19 @@ export async function generateMetadata( | ||||
| 				webp: `${BASE_URL}/webp/${icon}.webp`, | ||||
| 			}, | ||||
| 		}, | ||||
| 	}; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| export default async function IconPage({ | ||||
| 	params, | ||||
| }: { params: Promise<{ icon: string }> }) { | ||||
| 	const { icon } = await params; | ||||
| 	const iconsData = await getAllIcons(); | ||||
| 	const originalIconData = iconsData[icon]; | ||||
| export default async function IconPage({ params }: { params: Promise<{ icon: string }> }) { | ||||
| 	const { icon } = await params | ||||
| 	const iconsData = await getAllIcons() | ||||
| 	const originalIconData = iconsData[icon] | ||||
|  | ||||
| 	if (!originalIconData) { | ||||
| 		notFound(); | ||||
| 		notFound() | ||||
| 	} | ||||
|  | ||||
| 	const authorData = await getAuthorData(originalIconData.update.author.id); | ||||
| 	const authorData = await getAuthorData(originalIconData.update.author.id) | ||||
|  | ||||
| 	return ( | ||||
| 		<IconDetails | ||||
| 			icon={icon} | ||||
| 			iconData={originalIconData} | ||||
| 			authorData={authorData} | ||||
| 		/> | ||||
| 	); | ||||
| 	return <IconDetails icon={icon} iconData={originalIconData} authorData={authorData} /> | ||||
| } | ||||
|   | ||||
| @@ -156,6 +156,8 @@ export function IconSearch({ icons }: IconSearchProps) { | ||||
| 		[pathname, router, initialSort], | ||||
| 	) | ||||
|  | ||||
| 	 | ||||
|  | ||||
| 	const handleSearch = useCallback( | ||||
| 		(query: string) => { | ||||
| 			setSearchQuery(query) | ||||
| @@ -211,10 +213,7 @@ export function IconSearch({ icons }: IconSearchProps) { | ||||
| 	}, []) | ||||
|  | ||||
| 	useEffect(() => { | ||||
| 		if (filteredIcons.length === 0 && searchQuery && searchQuery.length > 3) { | ||||
| 			console.log("no icons found", { | ||||
| 				query: searchQuery, | ||||
| 			}) | ||||
| 		if (filteredIcons.length === 0) { | ||||
| 			posthog.capture("no icons found", { | ||||
| 				query: searchQuery, | ||||
| 			}) | ||||
|   | ||||
| @@ -7,8 +7,8 @@ import type { Metadata, Viewport } from "next" | ||||
| import { Inter } from "next/font/google" | ||||
| import { Toaster } from "sonner" | ||||
| import "./globals.css" | ||||
| import { ThemeProvider } from "./theme-provider" | ||||
| import { getDescription, websiteTitle } from "@/constants" | ||||
| import { ThemeProvider } from "./theme-provider" | ||||
|  | ||||
| const inter = Inter({ | ||||
| 	variable: "--font-inter", | ||||
|   | ||||
| @@ -58,11 +58,7 @@ export function Header() { | ||||
| 				<div className="flex items-center gap-2 md:gap-4"> | ||||
| 					{/* Desktop search button */} | ||||
| 					<div className="hidden md:block"> | ||||
| 						<Button | ||||
| 							variant="outline" | ||||
| 							className="gap-2 cursor-pointer   transition-all duration-300" | ||||
| 							onClick={openCommandMenu} | ||||
| 						> | ||||
| 						<Button variant="outline" className="gap-2 cursor-pointer   transition-all duration-300" onClick={openCommandMenu}> | ||||
| 							<Search className="h-4 w-4 transition-all duration-300" /> | ||||
| 							<span>Find icons</span> | ||||
| 							<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border border-border/80 bg-muted/80 px-1.5 font-mono text-[10px] font-medium opacity-100"> | ||||
|   | ||||
| @@ -207,21 +207,22 @@ export function HeroSection({ totalIcons, stars }: { totalIcons: number; stars: | ||||
|  | ||||
| 			<div className="relative z-10 container mx-auto px-4 md:px-6 mt-4 py-20"> | ||||
| 				<div className="max-w-4xl mx-auto text-center flex flex-col gap-4 "> | ||||
| 					<h1 className="relative text-3xl sm:text-5xl md:text-7xl font-bold mb-4 md:mb-8 tracking-tight motion-preset-slide-up motion-duration-2000 "> | ||||
| 					<h1 className="relative text-3xl sm:text-5xl md:text-7xl font-bold mb-4 md:mb-8 tracking-tight motion-preset-slide-up motion-duration-500 "> | ||||
| 						Your definitive source for | ||||
| 						<Sparkles className="absolute -right-1 -bottom-3 text-rose-500 h-8 w-8 sm:h-12 sm:w-12 md:h-16 md:w-12 motion-delay-300 motion-preset-seesaw-lg motion-scale-in-[0.5] motion-translate-x-in-[-120%] motion-translate-y-in-[-60%] motion-opacity-in-[33%] motion-rotate-in-[-1080deg] motion-blur-in-[10px] motion-duration-[1s] motion-delay-[0.13s]/scale motion-duration-[0.13s]/opacity motion-duration-[0.40s]/rotate motion-duration-[0.05s]/blur motion-delay-[0.20s]/blur motion-ease-spring-bouncier" /> | ||||
| 						<Sparkles className="absolute -right-1 -bottom-3 text-rose-500 h-8 w-8 sm:h-12 sm:w-12 md:h-16 md:w-12 motion-delay-300 motion-preset-seesaw-lg motion-scale-in-[0.5] motion-translate-x-in-[-120%] motion-translate-y-in-[-60%] motion-opacity-in-[33%] motion-rotate-in-[-1080deg] motion-blur-in-[10px] motion-duration-500 motion-delay-[0.13s]/scale motion-duration-[0.13s]/opacity motion-duration-[0.40s]/rotate motion-duration-[0.05s]/blur motion-delay-[0.20s]/blur motion-ease-spring-bouncier" /> | ||||
| 						<br /> | ||||
| 						<Sparkles className="absolute -left-1 -top-3 text-rose-500 h-5 w-5 sm:h-8 sm:w-8 md:h-12 md:w-12 motion-delay-300 motion-preset-seesaw-lg motion-scale-in-[0.5] motion-translate-x-in-[159%] motion-translate-y-in-[-60%] motion-opacity-in-[33%] motion-rotate-in-[-1080deg] motion-blur-in-[10px] motion-duration-[1s] motion-delay-[0.13s]/scale motion-duration-[0.13s]/opacity motion-duration-[0.40s]/rotate motion-duration-[0.05s]/blur motion-delay-[0.20s]/blur motion-ease-spring-bouncier" /> | ||||
| 						<Sparkles className="absolute -left-1 -top-3 text-rose-500 h-5 w-5 sm:h-8 sm:w-8 md:h-12 md:w-12 motion-delay-300 motion-preset-seesaw-lg motion-scale-in-[0.5] motion-translate-x-in-[159%] motion-translate-y-in-[-60%] motion-opacity-in-[33%] motion-rotate-in-[-1080deg] motion-blur-in-[10px] motion-duration-500 motion-delay-[0.13s]/scale motion-duration-[0.13s]/opacity motion-duration-[0.40s]/rotate motion-duration-[0.05s]/blur motion-delay-[0.20s]/blur motion-ease-spring-bouncier" /> | ||||
| 						<AuroraText colors={["#FA5352", "#FA5352", "orange"]}>dashboard icons</AuroraText> | ||||
| 					</h1> | ||||
|  | ||||
| 					<p className="text-sm sm:text-base md:text-xl text-muted-foreground leading-relaxed mb-8 font-light tracking-wide max-w-2xl mx-auto px-4 motion-preset-slide-down motion-duration-2000"> | ||||
| 						A collection of <NumberTicker value={totalIcons} className="font-bold tracking-tighter text-muted-foreground" /> curated icons | ||||
| 					<p className="text-sm sm:text-base md:text-xl text-muted-foreground leading-relaxed mb-8 font-light tracking-wide max-w-2xl mx-auto px-4 motion-preset-slide-down motion-duration-500"> | ||||
| 						A collection of{" "} | ||||
| 						<NumberTicker value={totalIcons} startValue={1000} className="font-bold tracking-tighter text-muted-foreground" /> curated icons | ||||
| 						for services, applications and tools, designed specifically for dashboards and app directories. | ||||
| 					</p> | ||||
| 					<div className="flex flex-col gap-4 max-w-3xl mx-auto"> | ||||
| 						<SearchInput searchQuery={searchQuery} setSearchQuery={setSearchQuery} totalIcons={totalIcons} /> | ||||
| 						<div className="w-full flex gap-3 md:gap-4 flex-wrap justify-center motion-preset-slide-down motion-duration-2000"> | ||||
| 						<div className="w-full flex gap-3 md:gap-4 flex-wrap justify-center motion-preset-slide-down motion-duration-500"> | ||||
| 							<Link href="/icons"> | ||||
| 								<InteractiveHoverButton className="rounded-md bg-input/30">Explore icons</InteractiveHoverButton> | ||||
| 							</Link> | ||||
|   | ||||
| @@ -72,10 +72,7 @@ export function IconSubmissionForm() { | ||||
| 	return ( | ||||
| 		<Dialog open={open} onOpenChange={setOpen}> | ||||
| 			<DialogTrigger asChild> | ||||
| 				<Button | ||||
| 					variant="outline" | ||||
| 					className="hidden md:inline-flex cursor-pointer transition-all duration-300" | ||||
| 				> | ||||
| 				<Button variant="outline" className="hidden md:inline-flex cursor-pointer transition-all duration-300"> | ||||
| 					<PlusCircle className="h-4 w-4 transition-all duration-300" /> Contribute new icon | ||||
| 				</Button> | ||||
| 			</DialogTrigger> | ||||
|   | ||||
| @@ -1,67 +1,57 @@ | ||||
| "use client"; | ||||
| "use client" | ||||
|  | ||||
| import { useInView, useMotionValue, useSpring } from "motion/react"; | ||||
| import { ComponentPropsWithoutRef, useEffect, useRef } from "react"; | ||||
| import { useInView, useMotionValue, useSpring } from "motion/react" | ||||
| import { type ComponentPropsWithoutRef, useEffect, useRef } from "react" | ||||
|  | ||||
| import { cn } from "@/lib/utils"; | ||||
| import { cn } from "@/lib/utils" | ||||
|  | ||||
| interface NumberTickerProps extends ComponentPropsWithoutRef<"span"> { | ||||
|   value: number; | ||||
|   startValue?: number; | ||||
|   direction?: "up" | "down"; | ||||
|   delay?: number; | ||||
|   decimalPlaces?: number; | ||||
| 	value: number | ||||
| 	startValue?: number | ||||
| 	direction?: "up" | "down" | ||||
| 	delay?: number | ||||
| 	decimalPlaces?: number | ||||
| } | ||||
|  | ||||
| export function NumberTicker({ | ||||
|   value, | ||||
|   startValue = 0, | ||||
|   direction = "up", | ||||
|   delay = 0, | ||||
|   className, | ||||
|   decimalPlaces = 0, | ||||
|   ...props | ||||
| 	value, | ||||
| 	startValue = 0, | ||||
| 	direction = "up", | ||||
| 	delay = 0, | ||||
| 	className, | ||||
| 	decimalPlaces = 0, | ||||
| 	...props | ||||
| }: NumberTickerProps) { | ||||
|   const ref = useRef<HTMLSpanElement>(null); | ||||
|   const motionValue = useMotionValue(direction === "down" ? value : startValue); | ||||
|   const springValue = useSpring(motionValue, { | ||||
|     damping: 30, | ||||
|     stiffness: 100, | ||||
|   }); | ||||
|   const isInView = useInView(ref, { once: true, margin: "0px" }); | ||||
| 	const ref = useRef<HTMLSpanElement>(null) | ||||
| 	const motionValue = useMotionValue(direction === "down" ? value : startValue) | ||||
| 	const springValue = useSpring(motionValue, { | ||||
| 		damping: 30, | ||||
| 		stiffness: 200, | ||||
| 	}) | ||||
| 	const isInView = useInView(ref, { once: true, margin: "0px" }) | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (isInView) { | ||||
|       const timer = setTimeout(() => { | ||||
|         motionValue.set(direction === "down" ? startValue : value); | ||||
|       }, delay * 1000); | ||||
|       return () => clearTimeout(timer); | ||||
|     } | ||||
|   }, [motionValue, isInView, delay, value, direction, startValue]); | ||||
| 	useEffect(() => { | ||||
| 		if (isInView) { | ||||
| 			const timer = setTimeout(() => { | ||||
| 				motionValue.set(direction === "down" ? startValue : value) | ||||
| 			}, delay * 1000) | ||||
| 			return () => clearTimeout(timer) | ||||
| 		} | ||||
| 	}, [motionValue, isInView, delay, value, direction, startValue]) | ||||
|  | ||||
|   useEffect( | ||||
|     () => | ||||
|       springValue.on("change", (latest) => { | ||||
|         if (ref.current) { | ||||
|           ref.current.textContent = Intl.NumberFormat("en-US", { | ||||
|             minimumFractionDigits: decimalPlaces, | ||||
|             maximumFractionDigits: decimalPlaces, | ||||
|           }).format(Number(latest.toFixed(decimalPlaces))); | ||||
|         } | ||||
|       }), | ||||
|     [springValue, decimalPlaces], | ||||
|   ); | ||||
| 	useEffect( | ||||
| 		() => | ||||
| 			springValue.on("change", (latest) => { | ||||
| 				if (ref.current) { | ||||
| 					ref.current.textContent = Number(latest.toFixed(decimalPlaces)).toString() | ||||
| 				} | ||||
| 			}), | ||||
| 		[springValue, decimalPlaces], | ||||
| 	) | ||||
|  | ||||
|   return ( | ||||
|     <span | ||||
|       ref={ref} | ||||
|       className={cn( | ||||
|         "inline-block tabular-nums tracking-wider", | ||||
|         className, | ||||
|       )} | ||||
|       {...props} | ||||
|     > | ||||
|       {startValue} | ||||
|     </span> | ||||
|   ); | ||||
| 	return ( | ||||
| 		<span ref={ref} className={cn("inline-block tabular-nums tracking-wider", className)} {...props}> | ||||
| 			{startValue} | ||||
| 		</span> | ||||
| 	) | ||||
| } | ||||
|   | ||||
| @@ -32,7 +32,7 @@ export function RecentlyAddedIcons({ icons }: { icons: IconWithName[] }) { | ||||
|  | ||||
| 			<div className="mx-auto px-6 lg:px-8"> | ||||
| 				<div className="mx-auto max-w-2xl text-center my-4"> | ||||
| 					<h2 className="text-3xl font-bold tracking-tight sm:text-4xl bg-clip-text text-transparent bg-gradient-to-r from-rose-600 to-rose-500  motion-safe:motion-preset-fade-lg motion-duration-2000"> | ||||
| 					<h2 className="text-3xl font-bold tracking-tight sm:text-4xl bg-clip-text text-transparent bg-gradient-to-r from-rose-600 to-rose-500  motion-safe:motion-preset-fade-lg motion-duration-500"> | ||||
| 						Recently Added Icons | ||||
| 					</h2> | ||||
| 				</div> | ||||
|   | ||||
| @@ -18,11 +18,7 @@ export function ThemeSwitcher() { | ||||
| 				<Tooltip> | ||||
| 					<TooltipTrigger asChild> | ||||
| 						<DropdownMenuTrigger asChild> | ||||
| 							<Button | ||||
| 								className=" transition-colors duration-200 group hover:ring-2 rounded-lg cursor-pointer" | ||||
| 								variant="ghost" | ||||
| 								size="icon" | ||||
| 							> | ||||
| 							<Button className=" transition-colors duration-200 group hover:ring-2 rounded-lg cursor-pointer" variant="ghost" size="icon"> | ||||
| 								<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0 group-hover:" /> | ||||
| 								<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100 group-hover:" /> | ||||
| 								<span className="sr-only">Toggle theme</span> | ||||
|   | ||||
| @@ -7,4 +7,4 @@ export const REPO_NAME = "homarr-labs/dashboard-icons" | ||||
| export const getDescription = (totalIcons: number) => | ||||
| 	`A collection of ${totalIcons} curated icons for services, applications and tools, designed specifically for dashboards and app directories.` | ||||
|  | ||||
| export const websiteTitle = "Free Dashboard Icons - Download High-Quality UI & App Icons" | ||||
| export const websiteTitle = "Free Dashboard Icons - Download High-Quality UI & App Icons" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user