mirror of
				https://github.com/walkxcode/dashboard-icons.git
				synced 2025-10-31 08:47:57 +01:00 
			
		
		
		
	feat(website): visually enhance website
- Update UI with refined rose-themed styling throughout the site - Add mobile-responsive improvements to header and hero sections - Create new 'Recently Added Icons' component with animated cards - Improve icon details view with better visual hierarchy and theme indicators - Implement better hover effects and transitions for interactive elements - Add mobile menu for better navigation on smaller screens - Update license notice wording - Remove grid background in favor of refined blur effects
This commit is contained in:
		
				
					committed by
					
						 Thomas Camlong
						Thomas Camlong
					
				
			
			
				
	
			
			
			
						parent
						
							15f841cb09
						
					
				
				
					commit
					86b89f5518
				
			| @@ -1 +1,26 @@ | ||||
| {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#FA5252","background_color":"#1B1B1D","display":"standalone"} | ||||
| { | ||||
| 	"name": "Dashboard Icons", | ||||
| 	"short_name": "DashIcons", | ||||
| 	"description": "A collection of curated icons for services, applications and tools, designed specifically for dashboards and app directories.", | ||||
| 	"icons": [ | ||||
| 		{ | ||||
| 			"src": "/android-chrome-192x192.png", | ||||
| 			"sizes": "192x192", | ||||
| 			"type": "image/png", | ||||
| 			"purpose": "any maskable" | ||||
| 		}, | ||||
| 		{ | ||||
| 			"src": "/android-chrome-512x512.png", | ||||
| 			"sizes": "512x512", | ||||
| 			"type": "image/png", | ||||
| 			"purpose": "any maskable" | ||||
| 		} | ||||
| 	], | ||||
| 	"theme_color": "#FA5252", | ||||
| 	"background_color": "#1B1B1D", | ||||
| 	"start_url": "/", | ||||
| 	"display": "standalone", | ||||
| 	"orientation": "portrait", | ||||
| 	"scope": "/", | ||||
| 	"categories": ["tools", "utilities", "productivity"] | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { IconDetails } from "@/components/icon-details" | ||||
| import { BASE_URL } from "@/constants" | ||||
| import { getAllIcons, getAuthorData } from "@/lib/api" | ||||
| import { getAllIcons, getAuthorData, getTotalIcons } from "@/lib/api" | ||||
| import type { Metadata, ResolvingMetadata } from "next" | ||||
| import { notFound } from "next/navigation" | ||||
|  | ||||
| @@ -36,7 +36,10 @@ export async function generateMetadata({ params, searchParams }: Props, parent: | ||||
|  | ||||
| 	const iconImageUrl = `${BASE_URL}/png/${icon}.png` | ||||
| 	const pageUrl = `${BASE_URL}/icons/${icon}` | ||||
| 	const formattedIconName = icon.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ') | ||||
| 	const formattedIconName = icon | ||||
| 		.split("-") | ||||
| 		.map((word) => word.charAt(0).toUpperCase() + word.slice(1)) | ||||
| 		.join(" ") | ||||
|  | ||||
| 	return { | ||||
| 		title: `${formattedIconName} Icon | Dashboard Icons`, | ||||
| @@ -101,9 +104,65 @@ export default async function IconPage({ params }: { params: Promise<{ icon: str | ||||
| 		notFound() | ||||
| 	} | ||||
|  | ||||
| 	// Pass originalIconData directly, assuming IconDetails can handle it | ||||
| 	const iconData = originalIconData | ||||
| 	// Fetch total icons | ||||
| 	const { totalIcons } = await getTotalIcons() | ||||
| 	// Format icon name | ||||
| 	const formattedIconName = icon | ||||
| 		.split("-") | ||||
| 		.map((word) => word.charAt(0).toUpperCase() + word.slice(1)) | ||||
| 		.join(" ") | ||||
|  | ||||
| 	const authorData = await getAuthorData(originalIconData.update.author.id) | ||||
| 	return <IconDetails icon={icon} iconData={originalIconData} authorData={authorData} /> | ||||
|  | ||||
| 	return ( | ||||
| 		<div className="relative isolate overflow-hidden pt-14"> | ||||
| 			{/* Background glow effect */} | ||||
| 			<div className="absolute inset-x-0 -top-40 -z-10 transform-gpu overflow-hidden blur-3xl sm:-top-80" aria-hidden="true"> | ||||
| 				<div | ||||
| 					className="relative left-[calc(50%-11rem)] aspect-[1155/678] w-[36.125rem] -translate-x-1/2 rotate-[30deg] bg-gradient-to-tr from-rose-400/50 to-red-300/50 dark:from-red-600/70 dark:to-red-900/70 opacity-50 dark:opacity-60 sm:left-[calc(50%-30rem)] sm:w-[72.1875rem]" | ||||
| 					style={{ | ||||
| 						clipPath: | ||||
| 							"polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)", | ||||
| 					}} | ||||
| 				/> | ||||
| 			</div> | ||||
|  | ||||
| 			{/* Secondary glow for additional effect */} | ||||
| 			<div | ||||
| 				className="absolute inset-x-0 top-[calc(100%-13rem)] -z-10 transform-gpu overflow-hidden blur-3xl sm:top-[calc(100%-30rem)]" | ||||
| 				aria-hidden="true" | ||||
| 			> | ||||
| 				<div | ||||
| 					className="relative left-[calc(50%+3rem)] aspect-[1155/678] w-[36.125rem] -translate-x-1/2 bg-gradient-to-tr from-red-300/50 to-rose-500/50 dark:from-red-700/50 dark:to-red-500/50 opacity-50 dark:opacity-50 sm:left-[calc(50%+36rem)] sm:w-[72.1875rem]" | ||||
| 					style={{ | ||||
| 						clipPath: | ||||
| 							"polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)", | ||||
| 					}} | ||||
| 				/> | ||||
| 			</div> | ||||
|  | ||||
| 			{/* Additional central glow */} | ||||
| 			<div className="absolute inset-x-0 top-1/2 -z-10 transform-gpu overflow-hidden blur-3xl" aria-hidden="true"> | ||||
| 				<div | ||||
| 					className="relative left-[calc(50%)] aspect-[1155/678] w-[36.125rem] -translate-x-1/2 bg-gradient-to-tr from-rose-300/50 to-red-400/50 dark:from-red-800/40 dark:to-red-600/40 opacity-50 dark:opacity-40 sm:w-[50.1875rem]" | ||||
| 					style={{ | ||||
| 						clipPath: | ||||
| 							"polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)", | ||||
| 					}} | ||||
| 				/> | ||||
| 			</div> | ||||
|  | ||||
| 			{/* Title and Description Section */} | ||||
| 			<div className="mx-auto max-w-7xl px-6 py-8 lg:px-8 text-center"> | ||||
| 				<h1 className="text-2xl md:text-4xl font-bold tracking-tight text-foreground sm:text-6xl">{formattedIconName} Icon</h1> | ||||
| 				<p className="mt-3 md:mt-6 text-sm md:text-lg leading-6 md:leading-8 text-muted-foreground"> | ||||
| 					Part of a collection of {totalIcons} curated icons for services, applications and tools, designed specifically for dashboards and | ||||
| 					app directories. | ||||
| 				</p> | ||||
| 			</div> | ||||
|  | ||||
| 			{/* Existing Icon Details */} | ||||
| 			<IconDetails icon={icon} iconData={originalIconData} authorData={authorData} /> | ||||
| 		</div> | ||||
| 	) | ||||
| } | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import { IconSubmissionContent } from "@/components/icon-submission-form" | ||||
| import { Input } from "@/components/ui/input" | ||||
| import { BASE_URL } from "@/constants" | ||||
| import type { IconSearchProps } from "@/types/icons" | ||||
| import { motion } from "framer-motion" | ||||
| import { Search } from "lucide-react" | ||||
| import Image from "next/image" | ||||
| import Link from "next/link" | ||||
| @@ -82,43 +83,67 @@ export function IconSearch({ icons }: IconSearchProps) { | ||||
|  | ||||
| 	return ( | ||||
| 		<> | ||||
| 			<div className="relative w-full sm:max-w-md"> | ||||
| 			<motion.div | ||||
| 				className="relative w-full sm:max-w-md" | ||||
| 				initial={{ opacity: 0, y: 10 }} | ||||
| 				animate={{ opacity: 1, y: 0 }} | ||||
| 				transition={{ duration: 0.5 }} | ||||
| 			> | ||||
| 				<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" /> | ||||
| 				<Input | ||||
| 					type="search" | ||||
| 					placeholder="Search icons by name, aliases, or categories..." | ||||
| 					className="w-full pl-8" | ||||
| 					className="w-full pl-8 cursor-text" | ||||
| 					value={searchQuery} | ||||
| 					onChange={(e) => handleSearch(e.target.value)} | ||||
| 				/> | ||||
| 			</div> | ||||
| 			</motion.div> | ||||
|  | ||||
| 			{filteredIcons.length === 0 ? ( | ||||
| 				<div className="flex flex-col gap-8 py-12 max-w-2xl mx-auto"> | ||||
| 				<motion.div | ||||
| 					className="flex flex-col gap-8 py-12 max-w-2xl mx-auto" | ||||
| 					initial={{ opacity: 0 }} | ||||
| 					animate={{ opacity: 1 }} | ||||
| 					transition={{ duration: 0.5, delay: 0.2 }} | ||||
| 				> | ||||
| 					<div className="text-center"> | ||||
| 						<h2 className="text-5xl font-semibold">We don't have this one...yet!</h2> | ||||
| 					</div> | ||||
| 					<IconSubmissionContent /> | ||||
| 				</div> | ||||
| 				</motion.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-8"> | ||||
| 					{filteredIcons.map(({ name, data }) => ( | ||||
| 						<Link | ||||
| 							prefetch={false} | ||||
| 					{filteredIcons.map(({ name, data }, index) => ( | ||||
| 						<motion.div | ||||
| 							key={name} | ||||
| 							href={`/icons/${name}`} | ||||
| 							className="group flex flex-col items-center p-3 sm:p-4 rounded-lg border border-border hover:border-primary hover:bg-accent transition-colors" | ||||
| 							initial={{ opacity: 0, y: 15 }} | ||||
| 							animate={{ opacity: 1, y: 0 }} | ||||
| 							transition={{ | ||||
| 								duration: 0.5, | ||||
| 								delay: index * 0.03, | ||||
| 								ease: "easeOut", | ||||
| 							}} | ||||
| 						> | ||||
| 							<div className="relative h-12 w-12 sm:h-16 sm:w-16 mb-2"> | ||||
| 								<Image | ||||
| 									src={`${BASE_URL}/${data.base}/${name}.${data.base}`} | ||||
| 									alt={`${name} icon`} | ||||
| 									fill | ||||
| 									className="object-contain p-1 group-hover:scale-110 transition-transform" | ||||
| 								/> | ||||
| 							</div> | ||||
| 							<span className="text-xs sm:text-sm text-center truncate w-full capitalize">{name.replace(/-/g, " ")}</span> | ||||
| 						</Link> | ||||
| 							<Link | ||||
| 								prefetch={false} | ||||
| 								href={`/icons/${name}`} | ||||
| 								className="group flex flex-col items-center p-3 sm:p-4 rounded-lg border border-border bg-background/95 dark:bg-background/80 hover:border-rose-500 hover:bg-rose-500/10 dark:hover:bg-rose-900/30 dark:hover:border-rose-500 transition-all duration-300 hover:shadow-lg hover:shadow-rose-500/5 relative overflow-hidden" | ||||
| 							> | ||||
| 								<div className="absolute inset-0 bg-gradient-to-br from-rose-500/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" /> | ||||
|  | ||||
| 								<div className="relative h-12 w-12 sm:h-16 sm:w-16 mb-2"> | ||||
| 									<Image | ||||
| 										src={`${BASE_URL}/${data.base}/${name}.${data.base}`} | ||||
| 										alt={`${name} icon`} | ||||
| 										fill | ||||
| 										className="object-contain p-1 group-hover:scale-110 transition-transform duration-300" | ||||
| 									/> | ||||
| 								</div> | ||||
| 								<span className="text-xs sm:text-sm text-center truncate w-full capitalize group-hover:text-rose-600 dark:group-hover:text-rose-400 transition-colors duration-200 font-medium"> | ||||
| 									{name.replace(/-/g, " ")} | ||||
| 								</span> | ||||
| 							</Link> | ||||
| 						</motion.div> | ||||
| 					))} | ||||
| 				</div> | ||||
| 			)} | ||||
|   | ||||
| @@ -52,16 +52,43 @@ export const dynamic = "force-static" | ||||
| export default async function IconsPage() { | ||||
| 	const icons = await getIconsArray() | ||||
| 	return ( | ||||
| 		<div className="py-8"> | ||||
| 			<div className="space-y-4 mb-8 mx-auto max-w-[80vw]"> | ||||
| 				<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4"> | ||||
| 					<div> | ||||
| 						<h1 className="text-3xl font-bold">Browse icons</h1> | ||||
| 						<p className="text-muted-foreground">Search through our collection of {icons.length} beautiful icons.</p> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 		<div className="relative isolate overflow-hidden"> | ||||
| 			{/* Main background glow */} | ||||
| 			<div className="absolute inset-x-0 -top-40 -z-10 transform-gpu overflow-hidden blur-3xl sm:-top-80" aria-hidden="true"> | ||||
| 				<div | ||||
| 					className="relative left-[calc(50%-11rem)] aspect-[1155/678] w-[36.125rem] -translate-x-1/2 rotate-[30deg] bg-gradient-to-tr from-rose-400/50 to-red-300/50 dark:from-red-600/60 dark:to-red-900/60 opacity-50 dark:opacity-50 sm:left-[calc(50%-30rem)] sm:w-[72.1875rem]" | ||||
| 					style={{ | ||||
| 						clipPath: | ||||
| 							"polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)", | ||||
| 					}} | ||||
| 				/> | ||||
| 			</div> | ||||
|  | ||||
| 				<IconSearch icons={icons} /> | ||||
| 			{/* Secondary glow */} | ||||
| 			<div | ||||
| 				className="absolute inset-x-0 top-[calc(100%-13rem)] -z-10 transform-gpu overflow-hidden blur-3xl sm:top-[calc(100%-30rem)]" | ||||
| 				aria-hidden="true" | ||||
| 			> | ||||
| 				<div | ||||
| 					className="relative left-[calc(50%+3rem)] aspect-[1155/678] w-[36.125rem] -translate-x-1/2 bg-gradient-to-tr from-red-300/50 to-rose-500/50 dark:from-red-700/40 dark:to-red-500/40 opacity-50 dark:opacity-40 sm:left-[calc(50%+36rem)] sm:w-[72.1875rem]" | ||||
| 					style={{ | ||||
| 						clipPath: | ||||
| 							"polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)", | ||||
| 					}} | ||||
| 				/> | ||||
| 			</div> | ||||
|  | ||||
| 			<div className="py-8"> | ||||
| 				<div className="space-y-4 mb-8 mx-auto max-w-[80vw] relative"> | ||||
| 					<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4"> | ||||
| 						<div> | ||||
| 							<h1 className="text-3xl font-bold">Browse icons</h1> | ||||
| 							<p className="text-muted-foreground">Search through our collection of {icons.length} beautiful icons.</p> | ||||
| 						</div> | ||||
| 					</div> | ||||
|  | ||||
| 					<IconSearch icons={icons} /> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	) | ||||
|   | ||||
| @@ -1,23 +1,28 @@ | ||||
| import { PostHogProvider } from "@/components/PostHogProvider"; | ||||
| import { Header } from "@/components/header"; | ||||
| import { LicenseNotice } from "@/components/license-notice"; | ||||
| 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 { PostHogProvider } from "@/components/PostHogProvider" | ||||
| import { Footer } from "@/components/footer" | ||||
| import { Header } from "@/components/header-wrapper" | ||||
| import { LicenseNotice } from "@/components/license-notice" | ||||
| import type { Metadata, Viewport } from "next" | ||||
| import { Inter } from "next/font/google" | ||||
| import { Toaster } from "sonner" | ||||
| import "./globals.css" | ||||
| import { getTotalIcons } from "@/lib/api" | ||||
| import { ThemeProvider } from "./theme-provider" | ||||
|  | ||||
| const inter = Inter({ | ||||
| 	variable: "--font-inter", | ||||
| 	subsets: ["latin"], | ||||
| }); | ||||
| }) | ||||
|  | ||||
| export const viewport: Viewport = { | ||||
| 	width: "device-width", | ||||
| 	initialScale: 1, | ||||
| 	minimumScale: 1, | ||||
| 	maximumScale: 5, | ||||
| 	userScalable: true, | ||||
| 	themeColor: "#ffffff", | ||||
| }; | ||||
| 	viewportFit: "cover", | ||||
| } | ||||
|  | ||||
| export async function generateMetadata(): Promise<Metadata> { | ||||
| 	const { totalIcons } = await getTotalIcons() | ||||
| @@ -26,14 +31,7 @@ export async function generateMetadata(): Promise<Metadata> { | ||||
| 		metadataBase: new URL("https://dashboardicons.com"), | ||||
| 		title: "Dashboard Icons - Your definitive source for dashboard icons", | ||||
| 		description: `A collection of ${totalIcons} curated icons for services, applications and tools, designed specifically for dashboards and app directories.`, | ||||
| 		keywords: [ | ||||
| 			"dashboard icons", | ||||
| 			"service icons", | ||||
| 			"application icons", | ||||
| 			"tool icons", | ||||
| 			"web dashboard", | ||||
| 			"app directory", | ||||
| 		], | ||||
| 		keywords: ["dashboard icons", "service icons", "application icons", "tool icons", "web dashboard", "app directory"], | ||||
| 		robots: { | ||||
| 			index: true, | ||||
| 			follow: true, | ||||
| @@ -84,9 +82,7 @@ export async function generateMetadata(): Promise<Metadata> { | ||||
| 				{ url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" }, | ||||
| 				{ url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" }, | ||||
| 			], | ||||
| 			apple: [ | ||||
| 				{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" }, | ||||
| 			], | ||||
| 			apple: [{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" }], | ||||
| 			other: [ | ||||
| 				{ | ||||
| 					rel: "mask-icon", | ||||
| @@ -99,26 +95,20 @@ export async function generateMetadata(): Promise<Metadata> { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| export default function RootLayout({ | ||||
| 	children, | ||||
| }: Readonly<{ children: React.ReactNode }>) { | ||||
| export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { | ||||
| 	return ( | ||||
| 		<html lang="en" suppressHydrationWarning> | ||||
| 			<body className={`${inter.variable} antialiased bg-background`}> | ||||
| 			<body className={`${inter.variable} antialiased bg-background flex flex-col min-h-screen`}> | ||||
| 				<PostHogProvider> | ||||
| 					<ThemeProvider | ||||
| 						attribute="class" | ||||
| 						defaultTheme="system" | ||||
| 						enableSystem | ||||
| 						disableTransitionOnChange | ||||
| 					> | ||||
| 					<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange> | ||||
| 						<Header /> | ||||
| 						<main>{children}</main> | ||||
| 						<main className="flex-grow">{children}</main> | ||||
| 						<Footer /> | ||||
| 						<Toaster /> | ||||
| 						<LicenseNotice /> | ||||
| 					</ThemeProvider> | ||||
| 				</PostHogProvider> | ||||
| 			</body> | ||||
| 		</html> | ||||
| 	); | ||||
| 	) | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { HeroSection } from "@/components/hero" | ||||
| import { RecentlyAddedIcons } from "@/components/recently-added-icons" | ||||
| import { BASE_URL } from "@/constants" | ||||
| import { getTotalIcons } from "@/lib/api" | ||||
| import { getRecentlyAddedIcons, getTotalIcons } from "@/lib/api" | ||||
| import type { Metadata } from "next" | ||||
|  | ||||
| export async function generateMetadata(): Promise<Metadata> { | ||||
| @@ -9,14 +10,7 @@ export async function generateMetadata(): Promise<Metadata> { | ||||
| 	return { | ||||
| 		title: "Dashboard Icons - Beautiful icons for your dashboard", | ||||
| 		description: `A collection of ${totalIcons} curated icons for services, applications and tools, designed specifically for dashboards and app directories.`, | ||||
| 		keywords: [ | ||||
| 			"dashboard icons", | ||||
| 			"service icons", | ||||
| 			"application icons", | ||||
| 			"tool icons", | ||||
| 			"web dashboard", | ||||
| 			"app directory", | ||||
| 		], | ||||
| 		keywords: ["dashboard icons", "service icons", "application icons", "tool icons", "web dashboard", "app directory"], | ||||
| 		openGraph: { | ||||
| 			title: "Dashboard Icons - Your definitive source for dashboard icons", | ||||
| 			description: `A collection of ${totalIcons} curated icons for services, applications and tools, designed specifically for dashboards and app directories.`, | ||||
| @@ -45,10 +39,12 @@ export async function generateMetadata(): Promise<Metadata> { | ||||
|  | ||||
| export default async function Home() { | ||||
| 	const { totalIcons } = await getTotalIcons() | ||||
| 	const recentIcons = await getRecentlyAddedIcons(8) | ||||
|  | ||||
| 	return ( | ||||
| 		<div className="flex flex-col min-h-screen"> | ||||
| 			<HeroSection totalIcons={totalIcons} /> | ||||
| 			<RecentlyAddedIcons icons={recentIcons} /> | ||||
| 		</div> | ||||
| 	) | ||||
| } | ||||
|   | ||||
| @@ -1,17 +1,17 @@ | ||||
| import { BASE_URL, WEB_URL } from "@/constants"; | ||||
| import { getAllIcons } from "@/lib/api"; | ||||
| import type { MetadataRoute } from "next"; | ||||
| import { BASE_URL, WEB_URL } from "@/constants" | ||||
| import { getAllIcons } from "@/lib/api" | ||||
| import type { MetadataRoute } from "next" | ||||
|  | ||||
| export const dynamic = "force-static"; | ||||
| export const dynamic = "force-static" | ||||
|  | ||||
| // Helper function to format dates as YYYY-MM-DD | ||||
| const formatDate = (date: Date): string => { | ||||
| 	// Format to YYYY-MM-DD | ||||
| 	return date.toISOString().split('T')[0]; | ||||
| }; | ||||
| 	return date.toISOString().split("T")[0] | ||||
| } | ||||
|  | ||||
| export default async function sitemap(): Promise<MetadataRoute.Sitemap> { | ||||
| 	const iconsData = await getAllIcons(); | ||||
| 	const iconsData = await getAllIcons() | ||||
| 	return [ | ||||
| 		{ | ||||
| 			url: WEB_URL, | ||||
| @@ -34,11 +34,9 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> { | ||||
| 			images: [ | ||||
| 				`${BASE_URL}/png/${iconName}.png`, | ||||
| 				// SVG is conditional if it exists | ||||
| 				iconsData[iconName].base === "svg" | ||||
| 					? `${BASE_URL}/svg/${iconName}.svg` | ||||
| 					: null, | ||||
| 				iconsData[iconName].base === "svg" ? `${BASE_URL}/svg/${iconName}.svg` : null, | ||||
| 				`${BASE_URL}/webp/${iconName}.webp`, | ||||
| 			].filter(Boolean) as string[], | ||||
| 		})), | ||||
| 	]; | ||||
| 	] | ||||
| } | ||||
|   | ||||
							
								
								
									
										161
									
								
								web/src/components/client-header.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								web/src/components/client-header.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,161 @@ | ||||
| "use client" | ||||
|  | ||||
| import { IconSubmissionForm } from "@/components/icon-submission-form" | ||||
| import { ThemeSwitcher } from "@/components/theme-switcher" | ||||
| import { REPO_PATH } from "@/constants" | ||||
| import { getAllIcons } from "@/lib/api" | ||||
| import type { Icon } from "@/types/icons" | ||||
| import { motion } from "framer-motion" | ||||
| import { Github, Menu, Search } from "lucide-react" | ||||
| import Link from "next/link" | ||||
| import { useEffect, useState } from "react" | ||||
| import { CommandMenu } from "./command-menu" | ||||
| import { HeaderNav } from "./header-nav" | ||||
| import { Button } from "./ui/button" | ||||
| import { Sheet, SheetContent, SheetTrigger } from "./ui/sheet" | ||||
| import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip" | ||||
|  | ||||
| export function ClientHeader() { | ||||
| 	const [icons, setIcons] = useState<Record<string, Icon>>({}) | ||||
| 	const [isLoaded, setIsLoaded] = useState(false) | ||||
|  | ||||
| 	useEffect(() => { | ||||
| 		async function loadIcons() { | ||||
| 			try { | ||||
| 				const iconsData = await getAllIcons() | ||||
| 				setIcons(iconsData) | ||||
| 				setIsLoaded(true) | ||||
| 			} catch (error) { | ||||
| 				console.error("Failed to load icons:", error) | ||||
| 				setIsLoaded(true) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		loadIcons() | ||||
| 	}, []) | ||||
|  | ||||
| 	return ( | ||||
| 		<motion.header | ||||
| 			className="border-b sticky top-0 z-50 bg-background/95 backdrop-blur-md border-border/50" | ||||
| 			initial={{ y: -20, opacity: 0 }} | ||||
| 			animate={{ y: 0, opacity: 1 }} | ||||
| 			transition={{ duration: 0.3, ease: "easeOut" }} | ||||
| 		> | ||||
| 			<div className="px-4 md:px-12 flex items-center justify-between h-16 md:h-18"> | ||||
| 				<div className="flex items-center gap-2 md:gap-6"> | ||||
| 					<Link href="/" className="text-lg md:text-xl font-bold group"> | ||||
| 						<span className="transition-colors duration-300 group-hover:text-rose-500">Dashboard Icons</span> | ||||
| 					</Link> | ||||
| 					<div className="hidden md:block"> | ||||
| 						<HeaderNav /> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<div className="flex items-center gap-2 md:gap-4"> | ||||
| 					{/* Desktop search button */} | ||||
| 					<div className="hidden md:block"> | ||||
| 						<TooltipProvider> | ||||
| 							<Tooltip> | ||||
| 								<TooltipTrigger asChild> | ||||
| 									<Button | ||||
| 										variant="outline" | ||||
| 										size="sm" | ||||
| 										className="gap-2 cursor-pointer hover:bg-rose-500/10 dark:hover:bg-rose-900/30 hover:border-rose-500 dark:hover:border-rose-500 transition-colors duration-200 shadow-sm" | ||||
| 										id="desktop-search-button" | ||||
| 									> | ||||
| 										<Search className="h-4 w-4" /> | ||||
| 										<span>Search</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"> | ||||
| 											<span className="text-xs">⌘</span>K | ||||
| 										</kbd> | ||||
| 									</Button> | ||||
| 								</TooltipTrigger> | ||||
| 								<TooltipContent> | ||||
| 									<p>Search icons</p> | ||||
| 								</TooltipContent> | ||||
| 							</Tooltip> | ||||
| 						</TooltipProvider> | ||||
| 					</div> | ||||
|  | ||||
| 					{/* Mobile search button */} | ||||
| 					<div className="md:hidden"> | ||||
| 						<Button | ||||
| 							variant="ghost" | ||||
| 							size="icon" | ||||
| 							className="rounded-lg cursor-pointer hover:bg-rose-500/10 dark:hover:bg-rose-900/30 transition-colors duration-200 focus:ring-2 focus:ring-rose-500/20" | ||||
| 							id="mobile-search-button" | ||||
| 						> | ||||
| 							<Search className="h-5 w-5" /> | ||||
| 							<span className="sr-only">Search icons</span> | ||||
| 						</Button> | ||||
| 					</div> | ||||
|  | ||||
| 					<div className="hidden md:flex items-center gap-2 md:gap-4"> | ||||
| 						{isLoaded && <CommandMenu icons={Object.keys(icons)} triggerButtonId="desktop-search-button" />} | ||||
| 						<IconSubmissionForm /> | ||||
| 						<TooltipProvider> | ||||
| 							<Tooltip> | ||||
| 								<TooltipTrigger asChild> | ||||
| 									<Button | ||||
| 										variant="ghost" | ||||
| 										size="icon" | ||||
| 										className="rounded-lg cursor-pointer hover:bg-rose-500/10 dark:hover:bg-rose-900/30 transition-colors duration-200 focus:ring-2 focus:ring-rose-500/20" | ||||
| 										asChild | ||||
| 									> | ||||
| 										<Link href={REPO_PATH} target="_blank" className="group"> | ||||
| 											<Github className="h-5 w-5 group-hover:text-rose-500 transition-colors duration-200" /> | ||||
| 											<span className="sr-only">GitHub</span> | ||||
| 										</Link> | ||||
| 									</Button> | ||||
| 								</TooltipTrigger> | ||||
| 								<TooltipContent> | ||||
| 									<p>GitHub</p> | ||||
| 								</TooltipContent> | ||||
| 							</Tooltip> | ||||
| 						</TooltipProvider> | ||||
| 					</div> | ||||
| 					<ThemeSwitcher /> | ||||
|  | ||||
| 					{/* Mobile menu */} | ||||
| 					<div className="md:hidden"> | ||||
| 						<Sheet> | ||||
| 							<SheetTrigger asChild> | ||||
| 								<Button | ||||
| 									variant="ghost" | ||||
| 									size="icon" | ||||
| 									className="h-10 w-10 rounded-full cursor-pointer hover:bg-rose-500/10 dark:hover:bg-rose-900/30 transition-colors duration-200 focus:ring-2 focus:ring-rose-500/20" | ||||
| 								> | ||||
| 									<Menu className="h-5 w-5" /> | ||||
| 									<span className="sr-only">Toggle menu</span> | ||||
| 								</Button> | ||||
| 							</SheetTrigger> | ||||
| 							<SheetContent side="right" className="w-[280px] sm:w-[320px] p-0"> | ||||
| 								<div className="flex flex-col h-full py-6"> | ||||
| 									<div className="px-6 mb-6"> | ||||
| 										<h2 className="text-xl font-bold text-rose-500">Dashboard Icons</h2> | ||||
| 									</div> | ||||
|  | ||||
| 									<div className="flex-1 overflow-auto px-6"> | ||||
| 										<nav className="space-y-6"> | ||||
| 											<HeaderNav /> | ||||
| 											<div className="border-t pt-6" /> | ||||
| 											{isLoaded && <CommandMenu icons={Object.keys(icons)} triggerButtonId="mobile-search-button" />} | ||||
| 											<IconSubmissionForm /> | ||||
| 											<Link | ||||
| 												href={REPO_PATH} | ||||
| 												target="_blank" | ||||
| 												className="flex items-center gap-2 text-sm font-medium text-rose-500 hover:text-rose-600 transition-colors cursor-pointer p-2 hover:bg-rose-500/5 rounded-md" | ||||
| 											> | ||||
| 												<Github className="h-4 w-4" /> | ||||
| 												GitHub Repository | ||||
| 											</Link> | ||||
| 										</nav> | ||||
| 									</div> | ||||
| 								</div> | ||||
| 							</SheetContent> | ||||
| 						</Sheet> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</motion.header> | ||||
| 	) | ||||
| } | ||||
| @@ -3,14 +3,18 @@ | ||||
| import { useRouter } from "next/navigation" | ||||
| import * as React from "react" | ||||
|  | ||||
| import { Button } from "@/components/ui/button" | ||||
| import { CommandDialog, CommandEmpty, CommandInput, CommandItem, CommandList } from "@/components/ui/command" | ||||
| import { ImageIcon, Search } from "lucide-react" | ||||
| import Link from "next/link" | ||||
|  | ||||
| interface CommandMenuProps { | ||||
| 	icons: string[] | ||||
| 	triggerButtonId?: string | ||||
| 	displayAsButton?: boolean | ||||
| } | ||||
|  | ||||
| export function CommandMenu({ icons }: CommandMenuProps) { | ||||
| export function CommandMenu({ icons, triggerButtonId, displayAsButton = false }: CommandMenuProps) { | ||||
| 	const router = useRouter() | ||||
| 	const [open, setOpen] = React.useState(false) | ||||
| 	const [mounted, setMounted] = React.useState(false) | ||||
| @@ -37,6 +41,7 @@ export function CommandMenu({ icons }: CommandMenuProps) { | ||||
| 	React.useEffect(() => { | ||||
| 		setMounted(true) | ||||
| 	}, []) | ||||
|  | ||||
| 	React.useEffect(() => { | ||||
| 		const down = (e: KeyboardEvent) => { | ||||
| 			if (e.key === "k" && (e.metaKey || e.ctrlKey)) { | ||||
| @@ -49,6 +54,21 @@ export function CommandMenu({ icons }: CommandMenuProps) { | ||||
| 		return () => document.removeEventListener("keydown", down) | ||||
| 	}, []) | ||||
|  | ||||
| 	// Effect to connect to external trigger button | ||||
| 	React.useEffect(() => { | ||||
| 		if (!triggerButtonId || !mounted) return | ||||
|  | ||||
| 		const triggerButton = document.getElementById(triggerButtonId) | ||||
| 		if (!triggerButton) return | ||||
|  | ||||
| 		const handleClick = () => { | ||||
| 			setOpen(true) | ||||
| 		} | ||||
|  | ||||
| 		triggerButton.addEventListener("click", handleClick) | ||||
| 		return () => triggerButton.removeEventListener("click", handleClick) | ||||
| 	}, [triggerButtonId, mounted]) | ||||
|  | ||||
| 	const handleInputChange = React.useCallback((value: string) => { | ||||
| 		setInputValue(value) | ||||
| 	}, []) | ||||
| @@ -60,31 +80,25 @@ export function CommandMenu({ icons }: CommandMenuProps) { | ||||
| 		}, | ||||
| 		[router], | ||||
| 	) | ||||
|  | ||||
| 	if (!mounted) return null | ||||
|  | ||||
| 	return ( | ||||
| 		<> | ||||
| 			<p className="text-sm text-muted-foreground"> | ||||
| 				Press{" "} | ||||
| 				<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100"> | ||||
| 					<span className="text-xs">⌘</span>K | ||||
| 				</kbd>{" "} | ||||
| 				to search | ||||
| 			</p> | ||||
| 			<CommandDialog open={open} onOpenChange={setOpen}> | ||||
| 				<CommandInput placeholder="Type to search icons..." value={inputValue} onValueChange={handleInputChange} /> | ||||
| 				<CommandList className="max-h-[300px]"> | ||||
| 					{filteredIcons.length === 0 && <CommandEmpty>No results found. Try a different search term.</CommandEmpty>} | ||||
| 					{filteredIcons.map((icon) => ( | ||||
| 						<CommandItem key={icon} onSelect={() => handleSelectIcon(icon)}> | ||||
| 							<Link prefetch={filteredIcons.length < 3} href={`/icons/${icon}`} className="flex items-center gap-2"> | ||||
| 								<div className="w-2 h-2 bg-primary-foreground" /> | ||||
| 								<span className="capitalize">{icon.replace(/-/g, " ")}</span> | ||||
| 							</Link> | ||||
| 						</CommandItem> | ||||
| 					))} | ||||
| 				</CommandList> | ||||
| 			</CommandDialog> | ||||
| 		</> | ||||
| 		<CommandDialog open={open} onOpenChange={setOpen}> | ||||
| 			<CommandInput placeholder="Type to search icons..." value={inputValue} onValueChange={handleInputChange} /> | ||||
| 			<CommandList className="max-h-[300px]"> | ||||
| 				{filteredIcons.length === 0 && <CommandEmpty>No results found. Try a different search term.</CommandEmpty>} | ||||
| 				{filteredIcons.map((icon) => ( | ||||
| 					<CommandItem key={icon} onSelect={() => handleSelectIcon(icon)} className="cursor-pointer"> | ||||
| 						<Link prefetch={filteredIcons.length < 3} href={`/icons/${icon}`} className="flex items-center gap-2 w-full"> | ||||
| 							<span className="text-rose-500"> | ||||
| 								<ImageIcon className="h-4 w-4" /> | ||||
| 							</span> | ||||
| 							<span className="capitalize">{icon.replace(/-/g, " ")}</span> | ||||
| 						</Link> | ||||
| 					</CommandItem> | ||||
| 				))} | ||||
| 			</CommandList> | ||||
| 		</CommandDialog> | ||||
| 	) | ||||
| } | ||||
|   | ||||
							
								
								
									
										97
									
								
								web/src/components/footer.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								web/src/components/footer.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | ||||
| "use client" | ||||
|  | ||||
| import { REPO_PATH } from "@/constants" | ||||
| import { motion } from "framer-motion" | ||||
| import { ExternalLink, Github, Heart } from "lucide-react" | ||||
| import Link from "next/link" | ||||
|  | ||||
| export function Footer() { | ||||
| 	return ( | ||||
| 		<footer className="border-t py-12 bg-background relative overflow-hidden"> | ||||
| 			<div className="absolute inset-0 bg-gradient-to-r from-rose-500/[0.03] via-transparent to-rose-500/[0.03]" /> | ||||
|  | ||||
| 			<div className="container mx-auto px-4 md:px-6 relative z-10"> | ||||
| 				<div className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-12"> | ||||
| 					<motion.div | ||||
| 						className="flex flex-col gap-3" | ||||
| 						initial={{ opacity: 0, y: 20 }} | ||||
| 						whileInView={{ opacity: 1, y: 0 }} | ||||
| 						viewport={{ once: true }} | ||||
| 						transition={{ duration: 0.5 }} | ||||
| 					> | ||||
| 						<h3 className="font-bold text-lg text-foreground/90">Dashboard Icons</h3> | ||||
| 						<p className="text-sm text-muted-foreground leading-relaxed"> | ||||
| 							A collection of curated icons for services, applications and tools, designed specifically for dashboards and app directories. | ||||
| 						</p> | ||||
| 					</motion.div> | ||||
|  | ||||
| 					<motion.div | ||||
| 						className="flex flex-col gap-3" | ||||
| 						initial={{ opacity: 0, y: 20 }} | ||||
| 						whileInView={{ opacity: 1, y: 0 }} | ||||
| 						viewport={{ once: true }} | ||||
| 						transition={{ duration: 0.5, delay: 0.1 }} | ||||
| 					> | ||||
| 						<h3 className="font-bold text-lg text-foreground/90">Links</h3> | ||||
| 						<div className="flex flex-col gap-2"> | ||||
| 							<Link | ||||
| 								href="/" | ||||
| 								className="text-sm text-muted-foreground hover:text-rose-500 transition-colors duration-200 flex items-center w-fit" | ||||
| 							> | ||||
| 								<span>Home</span> | ||||
| 							</Link> | ||||
| 							<Link | ||||
| 								href="/icons" | ||||
| 								className="text-sm text-muted-foreground hover:text-rose-500 transition-colors duration-200 flex items-center w-fit" | ||||
| 							> | ||||
| 								<span>Icons</span> | ||||
| 							</Link> | ||||
| 							<Link | ||||
| 								href={REPO_PATH} | ||||
| 								target="_blank" | ||||
| 								rel="noopener noreferrer" | ||||
| 								className="text-sm text-muted-foreground hover:text-rose-500 transition-colors duration-200 flex items-center gap-1.5 w-fit group" | ||||
| 							> | ||||
| 								<span>GitHub</span> | ||||
| 								<Github className="h-3.5 w-3.5 group-hover:text-rose-500 transition-colors duration-200 flex-shrink-0 self-center" /> | ||||
| 							</Link> | ||||
| 						</div> | ||||
| 					</motion.div> | ||||
|  | ||||
| 					<motion.div | ||||
| 						className="flex flex-col gap-3" | ||||
| 						initial={{ opacity: 0, y: 20 }} | ||||
| 						whileInView={{ opacity: 1, y: 0 }} | ||||
| 						viewport={{ once: true }} | ||||
| 						transition={{ duration: 0.5, delay: 0.2 }} | ||||
| 					> | ||||
| 						<h3 className="font-bold text-lg text-foreground/90">Community</h3> | ||||
| 						<p className="text-sm text-muted-foreground flex flex-wrap items-center gap-1.5 leading-relaxed"> | ||||
| 							Made with <Heart className="h-3.5 w-3.5 text-rose-500 flex-shrink-0 animate-pulse" /> by Homarr Labs and the open source | ||||
| 							community. | ||||
| 						</p> | ||||
| 						<Link | ||||
| 							href="https://github.com/homarr-labs" | ||||
| 							target="_blank" | ||||
| 							rel="noopener noreferrer" | ||||
| 							className="text-sm text-rose-500 hover:text-rose-600 transition-colors duration-200 flex items-center gap-1.5 w-fit mt-1 group" | ||||
| 						> | ||||
| 							<span>Contribute to this project</span> | ||||
| 							<ExternalLink className="h-3.5 w-3.5 flex-shrink-0" /> | ||||
| 						</Link> | ||||
| 					</motion.div> | ||||
| 				</div> | ||||
|  | ||||
| 				<motion.div | ||||
| 					className="mt-10 pt-6 border-t text-center text-sm text-muted-foreground/80" | ||||
| 					initial={{ opacity: 0 }} | ||||
| 					whileInView={{ opacity: 1 }} | ||||
| 					viewport={{ once: true }} | ||||
| 					transition={{ duration: 0.5, delay: 0.3 }} | ||||
| 				> | ||||
| 					<p>© {new Date().getFullYear()} Homarr Labs. All rights reserved.</p> | ||||
| 				</motion.div> | ||||
| 			</div> | ||||
| 		</footer> | ||||
| 	) | ||||
| } | ||||
| @@ -1,23 +0,0 @@ | ||||
| import { cn } from "@/lib/utils" | ||||
|  | ||||
| interface GridBackgroundProps { | ||||
| 	className?: string | ||||
| } | ||||
|  | ||||
| export function GridBackground({ className }: GridBackgroundProps) { | ||||
| 	return ( | ||||
| 		<div className={cn("absolute inset-0 overflow-hidden", className)}> | ||||
| 			{/* Grid pattern */} | ||||
| 			<div | ||||
| 				className={cn( | ||||
| 					"absolute inset-0", | ||||
| 					"[background-size:40px_40px]", | ||||
| 					"[background-image:linear-gradient(to_right,rgba(99,102,241,0.1)_1px,transparent_1px),linear-gradient(to_bottom,rgba(99,102,241,0.1)_1px,transparent_1px)]", | ||||
| 					"dark:[background-image:linear-gradient(to_right,rgba(99,102,241,0.1)_1px,transparent_1px),linear-gradient(to_bottom,rgba(99,102,241,0.1)_1px,transparent_1px)]", | ||||
| 				)} | ||||
| 			/> | ||||
|  | ||||
| 			<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-slate-900 [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)] dark:bg-slate-900" /> | ||||
| 		</div> | ||||
| 	) | ||||
| } | ||||
| @@ -9,17 +9,23 @@ export function HeaderNav() { | ||||
| 	const isIconsActive = pathname === "/icons" || pathname.startsWith("/icons/") | ||||
|  | ||||
| 	return ( | ||||
| 		<nav className="flex items-center gap-2 md:gap-6"> | ||||
| 		<nav className="flex md:flex-row flex-col md:items-center items-start gap-4 md:gap-6"> | ||||
| 			<Link | ||||
| 				href="/" | ||||
| 				className={cn("text-sm font-medium transition-colors hover:text-primary", pathname === "/" && "text-primary font-semibold")} | ||||
| 				className={cn( | ||||
| 					"text-sm font-medium transition-colors hover:text-rose-600 dark:hover:text-rose-400 cursor-pointer", | ||||
| 					pathname === "/" && "text-primary font-semibold", | ||||
| 				)} | ||||
| 			> | ||||
| 				Home | ||||
| 			</Link> | ||||
| 			<Link | ||||
| 				prefetch | ||||
| 				href="/icons" | ||||
| 				className={cn("text-sm font-medium transition-colors hover:text-primary", isIconsActive && "text-primary font-semibold")} | ||||
| 				className={cn( | ||||
| 					"text-sm font-medium transition-colors hover:text-rose-600 dark:hover:text-rose-400 cursor-pointer", | ||||
| 					isIconsActive && "text-primary font-semibold", | ||||
| 				)} | ||||
| 			> | ||||
| 				Icons | ||||
| 			</Link> | ||||
|   | ||||
							
								
								
									
										5
									
								
								web/src/components/header-wrapper.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								web/src/components/header-wrapper.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| import { ClientHeader } from "./client-header" | ||||
|  | ||||
| export function Header() { | ||||
| 	return <ClientHeader /> | ||||
| } | ||||
| @@ -1,33 +1,162 @@ | ||||
| "use client" | ||||
|  | ||||
| import { IconSubmissionForm } from "@/components/icon-submission-form" | ||||
| import { ThemeSwitcher } from "@/components/theme-switcher" | ||||
| import { REPO_PATH } from "@/constants" | ||||
| import { getAllIcons } from "@/lib/api" | ||||
| import { Github } from "lucide-react" | ||||
| import type { Icon } from "@/types/icons" | ||||
| import { motion } from "framer-motion" | ||||
| import { Github, Menu, Search } from "lucide-react" | ||||
| import Link from "next/link" | ||||
| import { useEffect, useState } from "react" | ||||
| import { CommandMenu } from "./command-menu" | ||||
| import { HeaderNav } from "./header-nav" | ||||
| import { Button } from "./ui/button" | ||||
| import { Sheet, SheetContent, SheetTrigger } from "./ui/sheet" | ||||
| import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip" | ||||
|  | ||||
| const icons = await getAllIcons() | ||||
| export function Header() { | ||||
| 	const [icons, setIcons] = useState<Record<string, Icon>>({}) | ||||
| 	const [isLoaded, setIsLoaded] = useState(false) | ||||
|  | ||||
| 	useEffect(() => { | ||||
| 		async function loadIcons() { | ||||
| 			try { | ||||
| 				const iconsData = await getAllIcons() | ||||
| 				setIcons(iconsData) | ||||
| 				setIsLoaded(true) | ||||
| 			} catch (error) { | ||||
| 				console.error("Failed to load icons:", error) | ||||
| 				setIsLoaded(true) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		loadIcons() | ||||
| 	}, []) | ||||
|  | ||||
| export async function Header() { | ||||
| 	return ( | ||||
| 		<header className="border-b"> | ||||
| 			<div className="px-4 md:px-12 flex items-center justify-between h-16"> | ||||
| 		<motion.header | ||||
| 			className="border-b sticky top-0 z-50 bg-background/95 backdrop-blur-md border-border/50" | ||||
| 			initial={{ y: -20, opacity: 0 }} | ||||
| 			animate={{ y: 0, opacity: 1 }} | ||||
| 			transition={{ duration: 0.3, ease: "easeOut" }} | ||||
| 		> | ||||
| 			<div className="px-4 md:px-12 flex items-center justify-between h-16 md:h-18"> | ||||
| 				<div className="flex items-center gap-2 md:gap-6"> | ||||
| 					<Link href="/" className="text-lg md:text-xl font-bold"> | ||||
| 						Dashboard Icons | ||||
| 					<Link href="/" className="text-lg md:text-xl font-bold group relative"> | ||||
| 						<span className="relative z-10 inline-block transition-colors duration-300 group-hover:text-rose-500">Dashboard Icons</span> | ||||
| 						<span className="absolute bottom-0 left-0 w-0 h-0.5 bg-rose-500 group-hover:w-full transition-all duration-300 ease-in-out rounded-full" /> | ||||
| 					</Link> | ||||
| 					<HeaderNav /> | ||||
| 					<div className="hidden md:block"> | ||||
| 						<HeaderNav /> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<div className="flex items-center gap-2 md:gap-4"> | ||||
| 					<CommandMenu icons={Object.keys(icons)} /> | ||||
| 					<IconSubmissionForm /> | ||||
| 					<Link href={REPO_PATH} target="_blank" className="text-sm font-medium transition-colors hover:text-primary"> | ||||
| 						<Github className="h-5 w-5" /> | ||||
| 					</Link> | ||||
| 					{/* Desktop search button */} | ||||
| 					<div className="hidden md:block"> | ||||
| 						<TooltipProvider> | ||||
| 							<Tooltip> | ||||
| 								<TooltipTrigger asChild> | ||||
| 									<Button | ||||
| 										variant="outline" | ||||
| 										size="sm" | ||||
| 										className="gap-2 cursor-pointer hover:bg-rose-500/10 dark:hover:bg-rose-900/30 hover:border-rose-500 dark:hover:border-rose-500 transition-colors duration-200 shadow-sm" | ||||
| 										id="desktop-search-button" | ||||
| 									> | ||||
| 										<Search className="h-4 w-4" /> | ||||
| 										<span>Search</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"> | ||||
| 											<span className="text-xs">⌘</span>K | ||||
| 										</kbd> | ||||
| 									</Button> | ||||
| 								</TooltipTrigger> | ||||
| 								<TooltipContent> | ||||
| 									<p>Search icons</p> | ||||
| 								</TooltipContent> | ||||
| 							</Tooltip> | ||||
| 						</TooltipProvider> | ||||
| 					</div> | ||||
|  | ||||
| 					{/* Mobile search button */} | ||||
| 					<div className="md:hidden"> | ||||
| 						<Button | ||||
| 							variant="ghost" | ||||
| 							size="icon" | ||||
| 							className="rounded-lg cursor-pointer hover:bg-rose-500/10 dark:hover:bg-rose-900/30 transition-colors duration-200 focus:ring-2 focus:ring-rose-500/20" | ||||
| 							id="mobile-search-button" | ||||
| 						> | ||||
| 							<Search className="h-5 w-5" /> | ||||
| 							<span className="sr-only">Search icons</span> | ||||
| 						</Button> | ||||
| 					</div> | ||||
|  | ||||
| 					<div className="hidden md:flex items-center gap-2 md:gap-4"> | ||||
| 						{isLoaded && <CommandMenu icons={Object.keys(icons)} triggerButtonId="desktop-search-button" />} | ||||
| 						<IconSubmissionForm /> | ||||
| 						<TooltipProvider> | ||||
| 							<Tooltip> | ||||
| 								<TooltipTrigger asChild> | ||||
| 									<Button | ||||
| 										variant="ghost" | ||||
| 										size="icon" | ||||
| 										className="rounded-lg cursor-pointer hover:bg-rose-500/10 dark:hover:bg-rose-900/30 transition-colors duration-200 focus:ring-2 focus:ring-rose-500/20" | ||||
| 										asChild | ||||
| 									> | ||||
| 										<Link href={REPO_PATH} target="_blank" className="group"> | ||||
| 											<Github className="h-5 w-5 group-hover:text-rose-500 transition-colors duration-200" /> | ||||
| 											<span className="sr-only">GitHub</span> | ||||
| 										</Link> | ||||
| 									</Button> | ||||
| 								</TooltipTrigger> | ||||
| 								<TooltipContent> | ||||
| 									<p>GitHub</p> | ||||
| 								</TooltipContent> | ||||
| 							</Tooltip> | ||||
| 						</TooltipProvider> | ||||
| 					</div> | ||||
| 					<ThemeSwitcher /> | ||||
|  | ||||
| 					{/* Mobile menu */} | ||||
| 					<div className="md:hidden"> | ||||
| 						<Sheet> | ||||
| 							<SheetTrigger asChild> | ||||
| 								<Button | ||||
| 									variant="ghost" | ||||
| 									size="icon" | ||||
| 									className="h-10 w-10 rounded-full cursor-pointer hover:bg-rose-500/10 dark:hover:bg-rose-900/30 transition-colors duration-200 focus:ring-2 focus:ring-rose-500/20" | ||||
| 								> | ||||
| 									<Menu className="h-5 w-5" /> | ||||
| 									<span className="sr-only">Toggle menu</span> | ||||
| 								</Button> | ||||
| 							</SheetTrigger> | ||||
| 							<SheetContent side="right" className="w-[280px] sm:w-[320px] p-0"> | ||||
| 								<div className="flex flex-col h-full py-6"> | ||||
| 									<div className="px-6 mb-6"> | ||||
| 										<h2 className="text-xl font-bold text-rose-500">Dashboard Icons</h2> | ||||
| 									</div> | ||||
|  | ||||
| 									<div className="flex-1 overflow-auto px-6"> | ||||
| 										<nav className="space-y-6"> | ||||
| 											<HeaderNav /> | ||||
| 											<div className="border-t pt-6" /> | ||||
| 											{isLoaded && <CommandMenu icons={Object.keys(icons)} triggerButtonId="mobile-search-button" />} | ||||
| 											<IconSubmissionForm /> | ||||
| 											<Link | ||||
| 												href={REPO_PATH} | ||||
| 												target="_blank" | ||||
| 												className="flex items-center gap-2 text-sm font-medium text-rose-500 hover:text-rose-600 transition-colors cursor-pointer p-2 hover:bg-rose-500/5 rounded-md" | ||||
| 											> | ||||
| 												<Github className="h-4 w-4" /> | ||||
| 												GitHub Repository | ||||
| 											</Link> | ||||
| 										</nav> | ||||
| 									</div> | ||||
| 								</div> | ||||
| 							</SheetContent> | ||||
| 						</Sheet> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</header> | ||||
| 		</motion.header> | ||||
| 	) | ||||
| } | ||||
|   | ||||
| @@ -4,10 +4,10 @@ import { Button } from "@/components/ui/button" | ||||
| import { Card } from "@/components/ui/card" | ||||
| import { Input } from "@/components/ui/input" | ||||
| import { cn } from "@/lib/utils" | ||||
| import { motion } from "framer-motion" | ||||
| import { Circle, Github, Search } from "lucide-react" | ||||
| import { motion, useAnimation } from "framer-motion" | ||||
| import { Circle, Github, Heart, Search, Sparkles } from "lucide-react" | ||||
| import Link from "next/link" | ||||
| import { useState } from "react" | ||||
| import { useEffect, useState } from "react" | ||||
|  | ||||
| interface IconCardProps { | ||||
| 	name: string | ||||
| @@ -31,7 +31,9 @@ function ElegantShape({ | ||||
| 	width = 400, | ||||
| 	height = 100, | ||||
| 	rotate = 0, | ||||
| 	gradient = "from-background/[0.1]", | ||||
| 	gradient = "from-rose-500/[0.5]", | ||||
| 	mobileWidth, | ||||
| 	mobileHeight, | ||||
| }: { | ||||
| 	className?: string | ||||
| 	delay?: number | ||||
| @@ -39,7 +41,21 @@ function ElegantShape({ | ||||
| 	height?: number | ||||
| 	rotate?: number | ||||
| 	gradient?: string | ||||
| 	mobileWidth?: number | ||||
| 	mobileHeight?: number | ||||
| }) { | ||||
| 	const controls = useAnimation() | ||||
| 	const [isMobile, setIsMobile] = useState(false) | ||||
|  | ||||
| 	useEffect(() => { | ||||
| 		const checkMobile = () => { | ||||
| 			setIsMobile(window.innerWidth < 768) | ||||
| 		} | ||||
| 		checkMobile() | ||||
| 		window.addEventListener("resize", checkMobile) | ||||
| 		return () => window.removeEventListener("resize", checkMobile) | ||||
| 	}, []) | ||||
|  | ||||
| 	return ( | ||||
| 		<motion.div | ||||
| 			initial={{ | ||||
| @@ -70,20 +86,20 @@ function ElegantShape({ | ||||
| 					ease: "easeInOut", | ||||
| 				}} | ||||
| 				style={{ | ||||
| 					width, | ||||
| 					height, | ||||
| 					width: isMobile && mobileWidth ? mobileWidth : width, | ||||
| 					height: isMobile && mobileHeight ? mobileHeight : height, | ||||
| 				}} | ||||
| 				className="relative" | ||||
| 			> | ||||
| 				<div | ||||
| 					className={cn( | ||||
| 						"absolute inset-0 rounded-full", | ||||
| 						"bg-gradient-to-r to-transparent", | ||||
| 						"bg-gradient-to-r from-rose-500/[0.6] via-rose-500/[0.4] to-rose-500/[0.1]", | ||||
| 						gradient, | ||||
| 						"backdrop-blur-[2px] border-2 border-white/[0.15]", | ||||
| 						"shadow-[0_8px_32px_0_rgba(255,255,255,0.1)]", | ||||
| 						"backdrop-blur-[3px]", | ||||
| 						"shadow-[0_0_40px_0_rgba(244,63,94,0.35),inset_0_0_0_1px_rgba(244,63,94,0.2)]", | ||||
| 						"after:absolute after:inset-0 after:rounded-full", | ||||
| 						"after:bg-[radial-gradient(circle_at_50%_50%,rgba(255,255,255,0.2),transparent_70%)]", | ||||
| 						"after:bg-[radial-gradient(circle_at_50%_50%,rgba(255,255,255,0.4),transparent_70%)]", | ||||
| 					)} | ||||
| 				/> | ||||
| 			</motion.div> | ||||
| @@ -108,16 +124,18 @@ export function HeroSection({ totalIcons }: { totalIcons: number }) { | ||||
| 	} | ||||
|  | ||||
| 	return ( | ||||
| 		<div className="relative pt-40 w-full flex items-center justify-center overflow-hidden bg-background"> | ||||
| 			<div className="absolute inset-0 bg-gradient-to-br from-indigo-500/[0.05] via-transparent to-rose-500/[0.05] blur-3xl" /> | ||||
| 		<div className="relative pt-20 md:pt-40 pb-10 md:pb-20 w-full flex items-center justify-center overflow-hidden bg-background"> | ||||
| 			<div className="absolute inset-0 bg-gradient-to-br from-rose-500/[0.1] via-transparent to-rose-500/[0.1] blur-3xl" /> | ||||
|  | ||||
| 			<div className="absolute inset-0 overflow-hidden"> | ||||
| 			<div className="absolute inset-0 overflow-hidden pointer-events-none"> | ||||
| 				<ElegantShape | ||||
| 					delay={0.3} | ||||
| 					width={600} | ||||
| 					height={140} | ||||
| 					mobileWidth={300} | ||||
| 					mobileHeight={80} | ||||
| 					rotate={12} | ||||
| 					gradient="from-indigo-500/[0.15]" | ||||
| 					gradient="from-rose-500/[0.6]" | ||||
| 					className="left-[-10%] md:left-[-5%] top-[15%] md:top-[20%]" | ||||
| 				/> | ||||
|  | ||||
| @@ -125,8 +143,10 @@ export function HeroSection({ totalIcons }: { totalIcons: number }) { | ||||
| 					delay={0.5} | ||||
| 					width={500} | ||||
| 					height={120} | ||||
| 					mobileWidth={250} | ||||
| 					mobileHeight={70} | ||||
| 					rotate={-15} | ||||
| 					gradient="from-rose-500/[0.15]" | ||||
| 					gradient="from-rose-500/[0.55]" | ||||
| 					className="right-[-5%] md:right-[0%] top-[70%] md:top-[75%]" | ||||
| 				/> | ||||
|  | ||||
| @@ -134,8 +154,10 @@ export function HeroSection({ totalIcons }: { totalIcons: number }) { | ||||
| 					delay={0.4} | ||||
| 					width={300} | ||||
| 					height={80} | ||||
| 					mobileWidth={150} | ||||
| 					mobileHeight={50} | ||||
| 					rotate={-8} | ||||
| 					gradient="from-violet-500/[0.15]" | ||||
| 					gradient="from-rose-500/[0.65]" | ||||
| 					className="left-[5%] md:left-[10%] bottom-[5%] md:bottom-[10%]" | ||||
| 				/> | ||||
|  | ||||
| @@ -143,8 +165,10 @@ export function HeroSection({ totalIcons }: { totalIcons: number }) { | ||||
| 					delay={0.6} | ||||
| 					width={200} | ||||
| 					height={60} | ||||
| 					mobileWidth={100} | ||||
| 					mobileHeight={40} | ||||
| 					rotate={20} | ||||
| 					gradient="from-amber-500/[0.15]" | ||||
| 					gradient="from-rose-500/[0.58]" | ||||
| 					className="right-[15%] md:right-[20%] top-[10%] md:top-[15%]" | ||||
| 				/> | ||||
|  | ||||
| @@ -152,8 +176,10 @@ export function HeroSection({ totalIcons }: { totalIcons: number }) { | ||||
| 					delay={0.7} | ||||
| 					width={150} | ||||
| 					height={40} | ||||
| 					mobileWidth={80} | ||||
| 					mobileHeight={30} | ||||
| 					rotate={-25} | ||||
| 					gradient="from-cyan-500/[0.15]" | ||||
| 					gradient="from-rose-500/[0.62]" | ||||
| 					className="left-[20%] md:left-[25%] top-[5%] md:top-[10%]" | ||||
| 				/> | ||||
| 			</div> | ||||
| @@ -161,29 +187,94 @@ export function HeroSection({ totalIcons }: { totalIcons: number }) { | ||||
| 			<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"> | ||||
| 					<Link prefetch href="https://github.com/homarr-labs" target="_blank" rel="noopener noreferrer" className="mx-auto"> | ||||
| 						<motion.div variants={fadeUpVariants} custom={0} initial="hidden" animate="visible"> | ||||
| 							<Card className="p-2 flex flex-row items-center gap-2 hover:scale-105 transition-all duration-300"> | ||||
| 								<Circle className="h-2 w-2 fill-rose-500/80" /> | ||||
| 								<span className="text-sm text-foreground/60 tracking-wide">by homarr-labs</span> | ||||
| 							</Card> | ||||
| 						<motion.div variants={fadeUpVariants} custom={0} initial="hidden" animate="visible" whileHover="hover"> | ||||
| 							<motion.div | ||||
| 								className="overflow-hidden rounded-md relative" | ||||
| 								variants={{ | ||||
| 									hover: { | ||||
| 										scale: 1.05, | ||||
| 										boxShadow: "0 10px 20px rgba(244, 63, 94, 0.15)", | ||||
| 									}, | ||||
| 								}} | ||||
| 								transition={{ | ||||
| 									type: "spring", | ||||
| 									stiffness: 400, | ||||
| 									damping: 17, | ||||
| 								}} | ||||
| 							> | ||||
| 								<motion.div | ||||
| 									className="absolute inset-0 bg-gradient-to-r from-rose-500/10 via-fuchsia-500/10 to-rose-500/10 opacity-0 z-0" | ||||
| 									variants={{ | ||||
| 										hover: { | ||||
| 											opacity: 1, | ||||
| 											backgroundPosition: ["0% 0%", "100% 100%"], | ||||
| 										}, | ||||
| 									}} | ||||
| 									transition={{ | ||||
| 										duration: 1.5, | ||||
| 										ease: "easeInOut", | ||||
| 										backgroundPosition: { | ||||
| 											repeat: Number.POSITIVE_INFINITY, | ||||
| 											duration: 3, | ||||
| 										}, | ||||
| 									}} | ||||
| 								/> | ||||
| 								<Card className="p-2 flex flex-row items-center gap-2 border-rose-200 dark:border-rose-900/30 shadow-sm bg-background z-10 relative"> | ||||
| 									<motion.div | ||||
| 										variants={{ | ||||
| 											hover: { | ||||
| 												scale: [1, 1.2, 1], | ||||
| 												rotate: [0, 5, -5, 0], | ||||
| 											}, | ||||
| 										}} | ||||
| 										transition={{ | ||||
| 											duration: 0.6, | ||||
| 											repeat: Number.POSITIVE_INFINITY, | ||||
| 											repeatType: "reverse", | ||||
| 										}} | ||||
| 									> | ||||
| 										<Heart className="h-4 w-4 text-rose-500" /> | ||||
| 									</motion.div> | ||||
| 									<span className="text-sm text-foreground/70 tracking-wide">Made with love by Homarr Labs</span> | ||||
| 								</Card> | ||||
| 							</motion.div> | ||||
| 						</motion.div> | ||||
| 					</Link> | ||||
|  | ||||
| 					<motion.div custom={1} variants={fadeUpVariants} initial="hidden" animate="visible"> | ||||
| 						<h1 className="text-4xl sm:text-6xl md:text-7xl font-bold mb-6 md:mb-8 tracking-tight"> | ||||
| 							<span className="bg-clip-text text-transparent bg-gradient-to-b from-foreground to-foreground/80"> | ||||
| 						<h1 className="text-3xl sm:text-5xl md:text-7xl font-bold mb-4 md:mb-8 tracking-tight"> | ||||
| 							<span className="text-foreground relative inline-block"> | ||||
| 								Your definitive source for | ||||
| 								<motion.span | ||||
| 									className="absolute -right-4 sm:-right-6 md:-right-8 -top-3 sm:-top-4 md:-top-6 text-rose-500" | ||||
| 									animate={{ rotate: [0, 15, 0] }} | ||||
| 									transition={{ duration: 5, repeat: Number.POSITIVE_INFINITY, ease: "easeInOut" }} | ||||
| 								> | ||||
| 									<Sparkles className="h-4 w-4 sm:h-5 sm:w-5 md:h-8 md:w-8" /> | ||||
| 								</motion.span> | ||||
| 							</span> | ||||
| 							<br /> | ||||
| 							<span className={cn("bg-clip-text text-transparent bg-gradient-to-r from-indigo-300 via-foreground/90 to-rose-300")}> | ||||
| 							<motion.span | ||||
| 								className="bg-clip-text text-transparent bg-gradient-to-r from-rose-500 via-fuchsia-500 to-rose-500 bg-[length:200%] relative inline-block" | ||||
| 								animate={{ | ||||
| 									backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"], | ||||
| 									textShadow: ["0 0 10px rgba(244,63,94,0.3)", "0 0 20px rgba(244,63,94,0.5)", "0 0 10px rgba(244,63,94,0.3)"], | ||||
| 								}} | ||||
| 								transition={{ | ||||
| 									duration: 8, | ||||
| 									repeat: Number.POSITIVE_INFINITY, | ||||
| 									ease: "easeInOut", | ||||
| 								}} | ||||
| 							> | ||||
| 								dashboard icons. | ||||
| 							</span> | ||||
| 							</motion.span> | ||||
| 						</h1> | ||||
| 					</motion.div> | ||||
|  | ||||
| 					<motion.div custom={2} variants={fadeUpVariants} initial="hidden" animate="visible"> | ||||
| 						<p className="text-base sm:text-lg md:text-xl text-muted-foreground mb-8 leading-relaxed font-light tracking-wide max-w-2xl mx-auto px-4"> | ||||
| 							A collection of {totalIcons} beautiful, clean and consistent icons for your dashboard, application, or website. | ||||
| 						<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 text-rose-500">{totalIcons}</span> curated icons for services, applications and | ||||
| 							tools, designed specifically for dashboards and app directories. | ||||
| 						</p> | ||||
| 					</motion.div> | ||||
|  | ||||
| @@ -192,29 +283,44 @@ export function HeroSection({ totalIcons }: { totalIcons: number }) { | ||||
| 						variants={fadeUpVariants} | ||||
| 						initial="hidden" | ||||
| 						animate="visible" | ||||
| 						className="flex flex-col items-center gap-6 mb-12" | ||||
| 						className="flex flex-col items-center gap-4 md:gap-6 mb-8 md:mb-12" | ||||
| 					> | ||||
| 						<form action="/icons" method="GET" className="relative w-full max-w-md"> | ||||
| 						<form action="/icons" method="GET" className="relative w-full max-w-md group"> | ||||
| 							<Input | ||||
| 								name="q" | ||||
| 								type="search" | ||||
| 								placeholder={`Search ${totalIcons} icons...`} | ||||
| 								className="pl-10 h-12 rounded-lg" | ||||
| 								className="pl-10 h-10 md:h-12 rounded-lg border-muted-foreground/20 focus:border-rose-500 focus:ring-rose-500/20 transition-all" | ||||
| 								value={searchQuery} | ||||
| 								onChange={(e) => setSearchQuery(e.target.value)} | ||||
| 							/> | ||||
| 							<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-muted-foreground" /> | ||||
| 							<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 md:h-5 w-4 md:w-5 text-muted-foreground group-focus-within:text-rose-500 transition-colors" /> | ||||
| 							<motion.span | ||||
| 								className="absolute inset-0 -z-10 rounded-lg bg-rose-500/5 opacity-0 transition-opacity group-hover:opacity-100" | ||||
| 								initial={{ scale: 0.95 }} | ||||
| 								whileHover={{ scale: 1 }} | ||||
| 								transition={{ duration: 0.2 }} | ||||
| 							/> | ||||
| 						</form> | ||||
| 						<div className="flex gap-4"> | ||||
| 							<Button variant="default" className="rounded-lg" size="lg" asChild> | ||||
| 								<Link href="/icons" className="flex items-center"> | ||||
| 						<div className="flex gap-3 md:gap-4 flex-wrap justify-center"> | ||||
| 							<Button variant="default" className="h-9 md:h-10 px-4 gap-2 bg-rose-500 hover:bg-rose-600 text-white" asChild> | ||||
| 								<Link href="/icons" className="flex items-center text-sm md:text-base"> | ||||
| 									Browse all icons | ||||
| 								</Link> | ||||
| 							</Button> | ||||
| 							<Button variant="outline" size="lg" className="gap-2" asChild> | ||||
| 								<Link href="https://github.com/homarr-labs/dashboard-icons" target="_blank" rel="noopener noreferrer"> | ||||
| 							<Button | ||||
| 								variant="outline" | ||||
| 								className="h-9 md:h-10 px-4 gap-2 border-rose-200 dark:border-rose-900/30 hover:bg-rose-50 dark:hover:bg-rose-900/20 hover:border-rose-300 dark:hover:border-rose-800" | ||||
| 								asChild | ||||
| 							> | ||||
| 								<Link | ||||
| 									href="https://github.com/homarr-labs/dashboard-icons" | ||||
| 									target="_blank" | ||||
| 									rel="noopener noreferrer" | ||||
| 									className="flex items-center text-sm md:text-base" | ||||
| 								> | ||||
| 									GitHub | ||||
| 									<Github className="h-4 w-4" /> | ||||
| 									<Github className="h-4 w-4 ml-1" /> | ||||
| 								</Link> | ||||
| 							</Button> | ||||
| 						</div> | ||||
|   | ||||
| @@ -7,7 +7,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp | ||||
| import { BASE_URL, REPO_PATH } from "@/constants" | ||||
| import type { AuthorData, Icon } from "@/types/icons" | ||||
| import { motion } from "framer-motion" | ||||
| import { Check, Copy, Download, Github } from "lucide-react" | ||||
| import { Check, Copy, Download, FileType, Github, Moon, PaletteIcon, Sun } from "lucide-react" | ||||
| import Image from "next/image" | ||||
| import Link from "next/link" | ||||
| import { useState } from "react" | ||||
| @@ -69,19 +69,19 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
|  | ||||
| 		return ( | ||||
| 			<TooltipProvider key={variantKey}> | ||||
| 				<div className="flex flex-col items-center bg-card rounded-lg p-4 border shadow-sm hover:shadow-md transition-all"> | ||||
| 				<div className="flex flex-col items-center bg-background/90 dark:bg-background/50 rounded-lg p-4 border border-rose-100/50 dark:border-rose-950/50 shadow-sm hover:shadow-md transition-all"> | ||||
| 					<Tooltip> | ||||
| 						<TooltipTrigger asChild> | ||||
| 							<motion.div | ||||
| 								className="relative w-28 h-28 mb-3 cursor-pointer rounded-md overflow-hidden group" | ||||
| 								className="relative w-28 h-28 mb-3 cursor-pointer rounded-xl overflow-hidden group" | ||||
| 								whileHover={{ scale: 1.05 }} | ||||
| 								whileTap={{ scale: 0.95 }} | ||||
| 								onClick={() => handleCopy(url, variantKey)} | ||||
| 							> | ||||
| 								<div className="absolute inset-0 border-2 border-transparent group-hover:border-primary/20 rounded-md z-10 transition-colors" /> | ||||
| 								<div className="absolute inset-0 border-2 border-transparent group-hover:border-primary/20 rounded-xl z-10 transition-colors" /> | ||||
|  | ||||
| 								<motion.div | ||||
| 									className="absolute inset-0 bg-primary/10 flex items-center justify-center z-20 rounded-md" | ||||
| 									className="absolute inset-0 bg-primary/10 flex items-center justify-center z-20 rounded-xl" | ||||
| 									initial={{ opacity: 0 }} | ||||
| 									animate={{ opacity: isCopied ? 1 : 0 }} | ||||
| 									transition={{ duration: 0.2 }} | ||||
| @@ -99,7 +99,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
| 									src={url} | ||||
| 									alt={`${iconName} in ${format} format${theme ? ` (${theme} theme)` : ""}`} | ||||
| 									fill | ||||
| 									className="object-contain p-2" | ||||
| 									className="object-contain p-4" | ||||
| 								/> | ||||
| 							</motion.div> | ||||
| 						</TooltipTrigger> | ||||
| @@ -113,7 +113,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
| 					<div className="flex gap-2 mt-3 w-full justify-center"> | ||||
| 						<Tooltip> | ||||
| 							<TooltipTrigger asChild> | ||||
| 								<Button variant="outline" size="icon" className="h-8 w-8" asChild> | ||||
| 								<Button variant="outline" size="icon" className="h-8 w-8 rounded-lg" asChild> | ||||
| 									<a href={url} download={`${iconName}.${format}`}> | ||||
| 										<Download className="w-4 h-4" /> | ||||
| 									</a> | ||||
| @@ -129,7 +129,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
| 								<Button | ||||
| 									variant="outline" | ||||
| 									size="icon" | ||||
| 									className="h-8 w-8 cursor-pointer" | ||||
| 									className="h-8 w-8 rounded-lg cursor-pointer" | ||||
| 									onClick={() => handleCopy(url, `btn-${variantKey}`)} | ||||
| 								> | ||||
| 									{copiedVariants[`btn-${variantKey}`] ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />} | ||||
| @@ -142,7 +142,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
|  | ||||
| 						<Tooltip> | ||||
| 							<TooltipTrigger asChild> | ||||
| 								<Button variant="outline" size="icon" className="h-8 w-8" asChild> | ||||
| 								<Button variant="outline" size="icon" className="h-8 w-8 rounded-lg" asChild> | ||||
| 									<Link href={githubUrl} target="_blank" rel="noopener noreferrer"> | ||||
| 										<Github className="w-4 h-4" /> | ||||
| 									</Link> | ||||
| @@ -163,10 +163,10 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
| 			<div className="grid grid-cols-1 lg:grid-cols-4 gap-6"> | ||||
| 				{/* Left Column: Icon Info and Author */} | ||||
| 				<div className="lg:col-span-1"> | ||||
| 					<Card className="h-full"> | ||||
| 					<Card className="h-full backdrop-blur-sm bg-background/95 dark:bg-background/80 border shadow-lg"> | ||||
| 						<CardHeader className="pb-4"> | ||||
| 							<div className="flex flex-col items-center"> | ||||
| 								<div className="relative w-32 h-32 bg-background rounded-xl overflow-hidden border flex items-center justify-center p-3 mb-4"> | ||||
| 								<div className="relative w-32 h-32 bg-background/90 rounded-xl overflow-hidden border flex items-center justify-center p-3 mb-4"> | ||||
| 									<Image | ||||
| 										src={`${BASE_URL}/${iconData.base}/${icon}.${iconData.base}`} | ||||
| 										width={96} | ||||
| @@ -194,14 +194,18 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
| 													<AvatarImage src={authorData.avatar_url} alt={authorName} /> | ||||
| 													<AvatarFallback>{authorName ? authorName.slice(0, 2).toUpperCase() : "??"}</AvatarFallback> | ||||
| 												</Avatar> | ||||
| 												<Link | ||||
| 													href={authorData.html_url} | ||||
| 													target="_blank" | ||||
| 													rel="noopener noreferrer" | ||||
| 													className="text-primary hover:underline text-sm" | ||||
| 												> | ||||
| 													{authorName} | ||||
| 												</Link> | ||||
| 												{authorData.html_url ? ( | ||||
| 													<Link | ||||
| 														href={authorData.html_url} | ||||
| 														target="_blank" | ||||
| 														rel="noopener noreferrer" | ||||
| 														className="text-primary hover:underline text-sm" | ||||
| 													> | ||||
| 														{authorName} | ||||
| 													</Link> | ||||
| 												) : ( | ||||
| 													<span className="text-sm">{authorName}</span> | ||||
| 												)} | ||||
| 											</div> | ||||
| 										</div> | ||||
| 									</div> | ||||
| @@ -239,7 +243,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
|  | ||||
| 				{/* Middle Column: Icon variants */} | ||||
| 				<div className="lg:col-span-2"> | ||||
| 					<Card className="h-full"> | ||||
| 					<Card className="h-full backdrop-blur-sm bg-background/95 dark:bg-background/80 border shadow-lg"> | ||||
| 						<CardHeader> | ||||
| 							<CardTitle>Icon variants</CardTitle> | ||||
| 							<CardDescription>Click on any icon to copy its URL to your clipboard</CardDescription> | ||||
| @@ -253,19 +257,19 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
| 								<div className="space-y-10"> | ||||
| 									<div> | ||||
| 										<h3 className="text-lg font-semibold mb-4 flex items-center gap-2"> | ||||
| 											<span className="inline-block w-3 h-3 rounded-full bg-primary" /> | ||||
| 											<Sun className="w-4 h-4 text-amber-500" /> | ||||
| 											Light theme | ||||
| 										</h3> | ||||
| 										<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> | ||||
| 										<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 p-3 rounded-lg bg-background/50 dark:bg-background/30 border border-rose-100 dark:border-rose-950/40"> | ||||
| 											{availableFormats.map((format) => renderVariant(format, icon, "light"))} | ||||
| 										</div> | ||||
| 									</div> | ||||
| 									<div> | ||||
| 										<h3 className="text-lg font-semibold mb-4 flex items-center gap-2"> | ||||
| 											<span className="inline-block w-3 h-3 rounded-full bg-primary" /> | ||||
| 											<Moon className="w-4 h-4 text-indigo-500" /> | ||||
| 											Dark theme | ||||
| 										</h3> | ||||
| 										<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> | ||||
| 										<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 p-3 rounded-lg bg-background/50 dark:bg-background/30 border border-rose-100 dark:border-rose-950/40"> | ||||
| 											{availableFormats.map((format) => renderVariant(format, icon, "dark"))} | ||||
| 										</div> | ||||
| 									</div> | ||||
| @@ -277,7 +281,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
|  | ||||
| 				{/* Right Column: Technical details */} | ||||
| 				<div className="lg:col-span-1"> | ||||
| 					<Card className="h-full"> | ||||
| 					<Card className="h-full backdrop-blur-sm bg-background/95 dark:bg-background/80 border shadow-lg"> | ||||
| 						<CardHeader> | ||||
| 							<CardTitle>Technical details</CardTitle> | ||||
| 						</CardHeader> | ||||
| @@ -286,8 +290,10 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
| 								<div className="space-y-3"> | ||||
| 									<h3 className="text-sm font-semibold text-muted-foreground">Base format</h3> | ||||
| 									<div className="flex items-center gap-2"> | ||||
| 										<span className="w-3 h-3 rounded-full bg-primary/80" /> | ||||
| 										<div className="px-3 py-1.5 bg-muted rounded-md text-sm font-medium">{iconData.base.toUpperCase()}</div> | ||||
| 										<FileType className="w-4 h-4 text-blue-500" /> | ||||
| 										<div className="px-3 py-1.5 bg-background/80 dark:bg-background/50 border border-border rounded-lg text-sm font-medium"> | ||||
| 											{iconData.base.toUpperCase()} | ||||
| 										</div> | ||||
| 									</div> | ||||
| 								</div> | ||||
|  | ||||
| @@ -295,7 +301,10 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
| 									<h3 className="text-sm font-semibold text-muted-foreground">Available formats</h3> | ||||
| 									<div className="flex flex-wrap gap-2"> | ||||
| 										{availableFormats.map((format) => ( | ||||
| 											<div key={format} className="px-3 py-1.5 bg-muted rounded-md text-xs font-medium"> | ||||
| 											<div | ||||
| 												key={format} | ||||
| 												className="px-3 py-1.5 bg-background/80 dark:bg-background/50 border border-border rounded-lg text-xs font-medium" | ||||
| 											> | ||||
| 												{format.toUpperCase()} | ||||
| 											</div> | ||||
| 										))} | ||||
| @@ -308,9 +317,11 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
| 										<div className="space-y-2"> | ||||
| 											{Object.entries(iconData.colors).map(([theme, variant]) => ( | ||||
| 												<div key={theme} className="flex items-center gap-2"> | ||||
| 													<span className="w-3 h-3 rounded-full bg-primary/80" /> | ||||
| 													<PaletteIcon className="w-4 h-4 text-purple-500" /> | ||||
| 													<span className="capitalize font-medium text-sm">{theme}:</span> | ||||
| 													<code className="bg-muted px-2 py-0.5 rounded text-xs">{variant}</code> | ||||
| 													<code className="bg-background/80 dark:bg-background/50 border border-border px-2 py-0.5 rounded-lg text-xs"> | ||||
| 														{variant} | ||||
| 													</code> | ||||
| 												</div> | ||||
| 											))} | ||||
| 										</div> | ||||
|   | ||||
| @@ -49,7 +49,7 @@ export function IconSubmissionContent({ onClose }: { onClose?: () => void }) { | ||||
| 						<Button | ||||
| 							key={template.id} | ||||
| 							variant="outline" | ||||
| 							className="w-full flex flex-col items-start gap-1 h-auto p-4 text-left cursor-pointer" | ||||
| 							className="w-full flex flex-col items-start gap-1 h-auto p-4 text-left cursor-pointer hover:bg-rose-500/10 dark:hover:bg-rose-900/30 hover:border-rose-500 dark:hover:border-rose-500 transition-colors duration-200" | ||||
| 						> | ||||
| 							<div className="flex w-full items-center justify-between"> | ||||
| 								<span className="font-medium">{template.name}</span> | ||||
| @@ -69,7 +69,10 @@ export function IconSubmissionForm() { | ||||
| 	return ( | ||||
| 		<Dialog open={open} onOpenChange={setOpen}> | ||||
| 			<DialogTrigger asChild> | ||||
| 				<Button variant="outline" className="hidden md:inline-flex"> | ||||
| 				<Button | ||||
| 					variant="outline" | ||||
| 					className="hidden md:inline-flex cursor-pointer hover:bg-rose-500/10 dark:hover:bg-rose-900/30 hover:border-rose-500 dark:hover:border-rose-500 transition-colors duration-200" | ||||
| 				> | ||||
| 					<PlusCircle className="h-4 w-4" /> Suggest new icon | ||||
| 				</Button> | ||||
| 			</DialogTrigger> | ||||
|   | ||||
| @@ -38,8 +38,8 @@ export function LicenseNotice() { | ||||
| 					<div className="flex items-start justify-between"> | ||||
| 						<div className="text-xs text-muted-foreground space-y-1"> | ||||
| 							<p> | ||||
| 								Unless otherwise indicated, all images and assets are the property of their respective owners and used for identification | ||||
| 								purposes only. | ||||
| 								All product names, trademarks, and registered trademarks are the property of their respective owners. Icons are used for | ||||
| 								identification purposes only and do not imply endorsement. | ||||
| 							</p> | ||||
| 							<p> | ||||
| 								Read the{" "} | ||||
|   | ||||
							
								
								
									
										114
									
								
								web/src/components/recently-added-icons.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								web/src/components/recently-added-icons.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| "use client" | ||||
|  | ||||
| import { BASE_URL } from "@/constants" | ||||
| import type { IconWithName } from "@/types/icons" | ||||
| import { format, isToday, isYesterday } from "date-fns" | ||||
| import { motion } from "framer-motion" | ||||
| import { ArrowRight, Clock, ExternalLink } from "lucide-react" | ||||
| import Image from "next/image" | ||||
| import Link from "next/link" | ||||
|  | ||||
| function formatIconDate(timestamp: string): string { | ||||
| 	const date = new Date(timestamp) | ||||
| 	if (isToday(date)) { | ||||
| 		return "Today" | ||||
| 	} | ||||
| 	if (isYesterday(date)) { | ||||
| 		return "Yesterday" | ||||
| 	} | ||||
| 	return format(date, "MMM d, yyyy") | ||||
| } | ||||
|  | ||||
| export function RecentlyAddedIcons({ icons }: { icons: IconWithName[] }) { | ||||
| 	return ( | ||||
| 		<div className="relative isolate overflow-hidden py-16 md:py-24"> | ||||
| 			{/* Background glow */} | ||||
| 			<div className="absolute inset-x-0 -top-40 -z-10 transform-gpu overflow-hidden blur-3xl sm:-top-80" aria-hidden="true"> | ||||
| 				<div | ||||
| 					className="relative left-[calc(50%-11rem)] aspect-[1155/678] w-[36.125rem] -translate-x-1/2 rotate-[30deg] bg-gradient-to-tr from-rose-400/40 to-red-300/40 dark:from-red-600/50 dark:to-red-900/50 opacity-60 dark:opacity-50 sm:left-[calc(50%-30rem)] sm:w-[72.1875rem]" | ||||
| 					style={{ | ||||
| 						clipPath: | ||||
| 							"polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)", | ||||
| 					}} | ||||
| 				/> | ||||
| 			</div> | ||||
|  | ||||
| 			<div className="mx-auto max-w-7xl px-6 lg:px-8"> | ||||
| 				<motion.div | ||||
| 					className="mx-auto max-w-2xl text-center mb-12" | ||||
| 					initial={{ opacity: 0, y: 20 }} | ||||
| 					whileInView={{ opacity: 1, y: 0 }} | ||||
| 					viewport={{ once: true }} | ||||
| 					transition={{ duration: 0.8 }} | ||||
| 				> | ||||
| 					<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"> | ||||
| 						Recently Added Icons | ||||
| 					</h2> | ||||
| 					<p className="mt-3 text-lg text-muted-foreground">Check out the latest additions to our collection.</p> | ||||
| 				</motion.div> | ||||
|  | ||||
| 				<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-8 gap-4 md:gap-5"> | ||||
| 					{icons.map(({ name, data }, index) => ( | ||||
| 						<motion.div | ||||
| 							key={name} | ||||
| 							initial={{ opacity: 0, y: 15 }} | ||||
| 							whileInView={{ opacity: 1, y: 0 }} | ||||
| 							viewport={{ once: true }} | ||||
| 							transition={{ | ||||
| 								duration: 0.5, | ||||
| 								delay: index * 0.05, | ||||
| 								ease: "easeOut", | ||||
| 							}} | ||||
| 						> | ||||
| 							<Link | ||||
| 								prefetch={false} | ||||
| 								href={`/icons/${name}`} | ||||
| 								className="group flex flex-col items-center p-3 sm:p-4 rounded-lg border border-border bg-background/95 dark:bg-background/80 hover:border-rose-500 hover:bg-rose-500/10 dark:hover:bg-rose-900/30 dark:hover:border-rose-500 transition-all duration-300 hover:shadow-lg hover:shadow-rose-500/5 relative overflow-hidden" | ||||
| 							> | ||||
| 								<div className="absolute inset-0 bg-gradient-to-br from-rose-500/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" /> | ||||
|  | ||||
| 								<div className="relative h-12 w-12 sm:h-16 sm:w-16 mb-2"> | ||||
| 									<Image | ||||
| 										src={`${BASE_URL}/${data.base}/${name}.${data.base}`} | ||||
| 										alt={`${name} icon`} | ||||
| 										fill | ||||
| 										className="object-contain p-1 group-hover:scale-110 transition-transform duration-300" | ||||
| 									/> | ||||
| 								</div> | ||||
| 								<span className="text-xs sm:text-sm text-center truncate w-full capitalize group-hover:text-rose-600 dark:group-hover:text-rose-400 transition-colors duration-200 font-medium"> | ||||
| 									{name.replace(/-/g, " ")} | ||||
| 								</span> | ||||
| 								<div className="flex items-center justify-center mt-2 w-full"> | ||||
| 									<span className="text-[10px] sm:text-xs text-muted-foreground flex items-center whitespace-nowrap group-hover:text-rose-500/70 transition-colors duration-200"> | ||||
| 										<Clock className="w-3 h-3 mr-1 shrink-0" /> | ||||
| 										{formatIconDate(data.update.timestamp)} | ||||
| 									</span> | ||||
| 								</div> | ||||
|  | ||||
| 								<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200"> | ||||
| 									<ExternalLink className="w-3 h-3 text-rose-500" /> | ||||
| 								</div> | ||||
| 							</Link> | ||||
| 						</motion.div> | ||||
| 					))} | ||||
| 				</div> | ||||
|  | ||||
| 				<motion.div | ||||
| 					className="mt-12 text-center" | ||||
| 					initial={{ opacity: 0 }} | ||||
| 					whileInView={{ opacity: 1 }} | ||||
| 					viewport={{ once: true }} | ||||
| 					transition={{ duration: 0.8, delay: 0.4 }} | ||||
| 				> | ||||
| 					<Link | ||||
| 						href="/icons" | ||||
| 						className="text-rose-500 dark:text-rose-400 hover:text-rose-600 dark:hover:text-rose-300 font-medium inline-flex items-center py-2 px-4 rounded-full border border-rose-200 dark:border-rose-800/30 hover:bg-rose-50 dark:hover:bg-rose-900/20 transition-all duration-200 group" | ||||
| 					> | ||||
| 						View all icons | ||||
| 						<ArrowRight className="w-4 h-4 ml-1 transition-transform duration-200 group-hover:translate-x-1" /> | ||||
| 					</Link> | ||||
| 				</motion.div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	) | ||||
| } | ||||
| @@ -12,16 +12,26 @@ export function ThemeSwitcher() { | ||||
| 	return ( | ||||
| 		<DropdownMenu> | ||||
| 			<DropdownMenuTrigger asChild> | ||||
| 				<Button className="hover:text-primary" variant="ghost" size="icon"> | ||||
| 				<Button | ||||
| 					className="rounded-lg cursor-pointer hover:bg-rose-500/10 dark:hover:bg-rose-900/30 transition-colors duration-200" | ||||
| 					variant="ghost" | ||||
| 					size="icon" | ||||
| 				> | ||||
| 					<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> | ||||
| 					<Moon className=" absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> | ||||
| 					<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> | ||||
| 					<span className="sr-only">Toggle theme</span> | ||||
| 				</Button> | ||||
| 			</DropdownMenuTrigger> | ||||
| 			<DropdownMenuContent align="end"> | ||||
| 				<DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem> | ||||
| 				<DropdownMenuItem onClick={() => setTheme("dark")}>Dark</DropdownMenuItem> | ||||
| 				<DropdownMenuItem onClick={() => setTheme("system")}>System</DropdownMenuItem> | ||||
| 				<DropdownMenuItem onClick={() => setTheme("light")} className="cursor-pointer"> | ||||
| 					Light | ||||
| 				</DropdownMenuItem> | ||||
| 				<DropdownMenuItem onClick={() => setTheme("dark")} className="cursor-pointer"> | ||||
| 					Dark | ||||
| 				</DropdownMenuItem> | ||||
| 				<DropdownMenuItem onClick={() => setTheme("system")} className="cursor-pointer"> | ||||
| 					System | ||||
| 				</DropdownMenuItem> | ||||
| 			</DropdownMenuContent> | ||||
| 		</DropdownMenu> | ||||
| 	) | ||||
|   | ||||
| @@ -72,3 +72,17 @@ export async function getTotalIcons() { | ||||
| 		totalIcons: Object.keys(iconsData).length, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Fetches recently added icons sorted by timestamp | ||||
|  */ | ||||
| export async function getRecentlyAddedIcons(limit = 8): Promise<IconWithName[]> { | ||||
| 	const icons = await getIconsArray() | ||||
|  | ||||
| 	return icons | ||||
| 		.sort((a, b) => { | ||||
| 			// Sort by timestamp in descending order (newest first) | ||||
| 			return new Date(b.data.update.timestamp).getTime() - new Date(a.data.update.timestamp).getTime() | ||||
| 		}) | ||||
| 		.slice(0, limit) | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user