diff --git a/web/public/site.webmanifest b/web/public/site.webmanifest
index d8091890..3c963dc8 100644
--- a/web/public/site.webmanifest
+++ b/web/public/site.webmanifest
@@ -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"}
\ No newline at end of file
+{
+ "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"]
+}
diff --git a/web/src/app/icons/[icon]/page.tsx b/web/src/app/icons/[icon]/page.tsx
index e1e5021b..bfb11e16 100644
--- a/web/src/app/icons/[icon]/page.tsx
+++ b/web/src/app/icons/[icon]/page.tsx
@@ -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
+
+ return (
+
+ {/* Background glow effect */}
+
+
+ {/* Secondary glow for additional effect */}
+
+
+ {/* Additional central glow */}
+
+
+ {/* Title and Description Section */}
+
+
{formattedIconName} Icon
+
+ Part of a collection of {totalIcons} curated icons for services, applications and tools, designed specifically for dashboards and
+ app directories.
+
+
+
+ {/* Existing Icon Details */}
+
+
+ )
}
diff --git a/web/src/app/icons/components/icon-search.tsx b/web/src/app/icons/components/icon-search.tsx
index 35b83185..8250b99b 100644
--- a/web/src/app/icons/components/icon-search.tsx
+++ b/web/src/app/icons/components/icon-search.tsx
@@ -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 (
<>
-
+
handleSearch(e.target.value)}
/>
-
+
{filteredIcons.length === 0 ? (
-
+
We don't have this one...yet!
-
+
) : (
- {filteredIcons.map(({ name, data }) => (
-
(
+
-
-
-
- {name.replace(/-/g, " ")}
-
+
+
+
+
+
+
+
+ {name.replace(/-/g, " ")}
+
+
+
))}
)}
diff --git a/web/src/app/icons/page.tsx b/web/src/app/icons/page.tsx
index 9acaa359..533ae76e 100644
--- a/web/src/app/icons/page.tsx
+++ b/web/src/app/icons/page.tsx
@@ -52,16 +52,43 @@ export const dynamic = "force-static"
export default async function IconsPage() {
const icons = await getIconsArray()
return (
-
-
-
-
-
Browse icons
-
Search through our collection of {icons.length} beautiful icons.
-
-
+
+ {/* Main background glow */}
+
-
+ {/* Secondary glow */}
+
+
+
+
+
+
+
Browse icons
+
Search through our collection of {icons.length} beautiful icons.
+
+
+
+
+
)
diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx
index bb631a4a..bf2f67d0 100644
--- a/web/src/app/layout.tsx
+++ b/web/src/app/layout.tsx
@@ -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
{
const { totalIcons } = await getTotalIcons()
@@ -26,14 +31,7 @@ export async function generateMetadata(): Promise {
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 {
{ 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 {
}
}
-export default function RootLayout({
- children,
-}: Readonly<{ children: React.ReactNode }>) {
+export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
-
+
-
+
- {children}
+ {children}
+
- );
+ )
}
diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx
index 0a1d82ff..926917de 100644
--- a/web/src/app/page.tsx
+++ b/web/src/app/page.tsx
@@ -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 {
@@ -9,14 +10,7 @@ export async function generateMetadata(): Promise {
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 {
export default async function Home() {
const { totalIcons } = await getTotalIcons()
+ const recentIcons = await getRecentlyAddedIcons(8)
return (
+
)
}
diff --git a/web/src/app/sitemap.ts b/web/src/app/sitemap.ts
index 2a17a1f8..1b7114c3 100644
--- a/web/src/app/sitemap.ts
+++ b/web/src/app/sitemap.ts
@@ -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 {
- const iconsData = await getAllIcons();
+ const iconsData = await getAllIcons()
return [
{
url: WEB_URL,
@@ -34,11 +34,9 @@ export default async function sitemap(): Promise {
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[],
})),
- ];
+ ]
}
diff --git a/web/src/components/client-header.tsx b/web/src/components/client-header.tsx
new file mode 100644
index 00000000..7831983d
--- /dev/null
+++ b/web/src/components/client-header.tsx
@@ -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>({})
+ 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 (
+
+
+
+
+
Dashboard Icons
+
+
+
+
+
+
+ {/* Desktop search button */}
+
+
+
+
+
+
+ Search
+
+ ⌘ K
+
+
+
+
+ Search icons
+
+
+
+
+
+ {/* Mobile search button */}
+
+
+
+ Search icons
+
+
+
+
+ {isLoaded &&
}
+
+
+
+
+
+
+
+ GitHub
+
+
+
+
+ GitHub
+
+
+
+
+
+
+ {/* Mobile menu */}
+
+
+
+
+
+ Toggle menu
+
+
+
+
+
+
Dashboard Icons
+
+
+
+
+
+
+ {isLoaded && }
+
+
+
+ GitHub Repository
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/web/src/components/command-menu.tsx b/web/src/components/command-menu.tsx
index ea698527..f34f8cad 100644
--- a/web/src/components/command-menu.tsx
+++ b/web/src/components/command-menu.tsx
@@ -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 (
- <>
-
- Press{" "}
-
- ⌘ K
- {" "}
- to search
-
-
-
-
- {filteredIcons.length === 0 && No results found. Try a different search term. }
- {filteredIcons.map((icon) => (
- handleSelectIcon(icon)}>
-
-
- {icon.replace(/-/g, " ")}
-
-
- ))}
-
-
- >
+
+
+
+ {filteredIcons.length === 0 && No results found. Try a different search term. }
+ {filteredIcons.map((icon) => (
+ handleSelectIcon(icon)} className="cursor-pointer">
+
+
+
+
+ {icon.replace(/-/g, " ")}
+
+
+ ))}
+
+
)
}
diff --git a/web/src/components/footer.tsx b/web/src/components/footer.tsx
new file mode 100644
index 00000000..2802fad7
--- /dev/null
+++ b/web/src/components/footer.tsx
@@ -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 (
+
+
+
+
+
+
+ Dashboard Icons
+
+ A collection of curated icons for services, applications and tools, designed specifically for dashboards and app directories.
+
+
+
+
+ Links
+
+
+ Home
+
+
+ Icons
+
+
+ GitHub
+
+
+
+
+
+
+ Community
+
+ Made with by Homarr Labs and the open source
+ community.
+
+
+ Contribute to this project
+
+
+
+
+
+
+ © {new Date().getFullYear()} Homarr Labs. All rights reserved.
+
+
+
+ )
+}
diff --git a/web/src/components/grid-background.tsx b/web/src/components/grid-background.tsx
deleted file mode 100644
index 8ee5e5ea..00000000
--- a/web/src/components/grid-background.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { cn } from "@/lib/utils"
-
-interface GridBackgroundProps {
- className?: string
-}
-
-export function GridBackground({ className }: GridBackgroundProps) {
- return (
-
- {/* Grid pattern */}
-
-
-
-
- )
-}
diff --git a/web/src/components/header-nav.tsx b/web/src/components/header-nav.tsx
index 3402e324..5b44da2b 100644
--- a/web/src/components/header-nav.tsx
+++ b/web/src/components/header-nav.tsx
@@ -9,17 +9,23 @@ export function HeaderNav() {
const isIconsActive = pathname === "/icons" || pathname.startsWith("/icons/")
return (
-
+
Home
Icons
diff --git a/web/src/components/header-wrapper.tsx b/web/src/components/header-wrapper.tsx
new file mode 100644
index 00000000..df873bf8
--- /dev/null
+++ b/web/src/components/header-wrapper.tsx
@@ -0,0 +1,5 @@
+import { ClientHeader } from "./client-header"
+
+export function Header() {
+ return
+}
diff --git a/web/src/components/header.tsx b/web/src/components/header.tsx
index c06f6ddb..9391f633 100644
--- a/web/src/components/header.tsx
+++ b/web/src/components/header.tsx
@@ -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>({})
+ 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 (
-
-
+
+
-
- Dashboard Icons
+
+
Dashboard Icons
+
-
+
+
+
-
-
-
-
-
+ {/* Desktop search button */}
+
+
+
+
+
+
+ Search
+
+ ⌘ K
+
+
+
+
+ Search icons
+
+
+
+
+
+ {/* Mobile search button */}
+
+
+
+ Search icons
+
+
+
+
+ {isLoaded &&
}
+
+
+
+
+
+
+
+ GitHub
+
+
+
+
+ GitHub
+
+
+
+
+
+ {/* Mobile menu */}
+
+
+
+
+
+ Toggle menu
+
+
+
+
+
+
Dashboard Icons
+
+
+
+
+
+
+ {isLoaded && }
+
+
+
+ GitHub Repository
+
+
+
+
+
+
+
-
+
)
}
diff --git a/web/src/components/hero.tsx b/web/src/components/hero.tsx
index ae9606d2..6e4794fc 100644
--- a/web/src/components/hero.tsx
+++ b/web/src/components/hero.tsx
@@ -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 (
@@ -108,16 +124,18 @@ export function HeroSection({ totalIcons }: { totalIcons: number }) {
}
return (
-
-
+
+
-
+
@@ -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%]"
/>
@@ -161,29 +187,94 @@ export function HeroSection({ totalIcons }: { totalIcons: number }) {
-
-
-
- by homarr-labs
-
+
+
+
+
+
+
+
+ Made with love by Homarr Labs
+
+
-
-
+
+
Your definitive source for
+
+
+
-
+
dashboard icons.
-
+
-
- A collection of {totalIcons} beautiful, clean and consistent icons for your dashboard, application, or website.
+
+ A collection of {totalIcons} curated icons for services, applications and
+ tools, designed specifically for dashboards and app directories.
@@ -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"
>
-
-
-
-
+
+
+
Browse all icons
-
-
+
+
GitHub
-
+
diff --git a/web/src/components/icon-details.tsx b/web/src/components/icon-details.tsx
index 294b98b4..7da12cb3 100644
--- a/web/src/components/icon-details.tsx
+++ b/web/src/components/icon-details.tsx
@@ -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 (
-
+
handleCopy(url, variantKey)}
>
-
+
@@ -113,7 +113,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
-
+
@@ -129,7 +129,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
handleCopy(url, `btn-${variantKey}`)}
>
{copiedVariants[`btn-${variantKey}`] ? : }
@@ -142,7 +142,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
-
+
@@ -163,10 +163,10 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
{/* Left Column: Icon Info and Author */}
-
+
-
+
{authorName ? authorName.slice(0, 2).toUpperCase() : "??"}
-
- {authorName}
-
+ {authorData.html_url ? (
+
+ {authorName}
+
+ ) : (
+
{authorName}
+ )}
@@ -239,7 +243,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
{/* Middle Column: Icon variants */}
-
+
Icon variants
Click on any icon to copy its URL to your clipboard
@@ -253,19 +257,19 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
-
+
Light theme
-
+
{availableFormats.map((format) => renderVariant(format, icon, "light"))}
-
+
Dark theme
-
+
{availableFormats.map((format) => renderVariant(format, icon, "dark"))}
@@ -277,7 +281,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
{/* Right Column: Technical details */}
-
+
Technical details
@@ -286,8 +290,10 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
Base format
-
-
{iconData.base.toUpperCase()}
+
+
+ {iconData.base.toUpperCase()}
+
@@ -295,7 +301,10 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
Available formats
{availableFormats.map((format) => (
-
+
{format.toUpperCase()}
))}
@@ -308,9 +317,11 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
{Object.entries(iconData.colors).map(([theme, variant]) => (
-
+
{theme}:
-
{variant}
+
+ {variant}
+
))}
diff --git a/web/src/components/icon-submission-form.tsx b/web/src/components/icon-submission-form.tsx
index 1e2a77b9..14a4a544 100644
--- a/web/src/components/icon-submission-form.tsx
+++ b/web/src/components/icon-submission-form.tsx
@@ -49,7 +49,7 @@ export function IconSubmissionContent({ onClose }: { onClose?: () => void }) {
{template.name}
@@ -69,7 +69,10 @@ export function IconSubmissionForm() {
return (
-
+
Suggest new icon
diff --git a/web/src/components/license-notice.tsx b/web/src/components/license-notice.tsx
index e333460a..ad669d64 100644
--- a/web/src/components/license-notice.tsx
+++ b/web/src/components/license-notice.tsx
@@ -38,8 +38,8 @@ export function LicenseNotice() {
- 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.
Read the{" "}
diff --git a/web/src/components/recently-added-icons.tsx b/web/src/components/recently-added-icons.tsx
new file mode 100644
index 00000000..7fd95d1c
--- /dev/null
+++ b/web/src/components/recently-added-icons.tsx
@@ -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 (
+
+ {/* Background glow */}
+
+
+
+
+
+ Recently Added Icons
+
+ Check out the latest additions to our collection.
+
+
+
+ {icons.map(({ name, data }, index) => (
+
+
+
+
+
+
+
+
+ {name.replace(/-/g, " ")}
+
+
+
+
+ {formatIconDate(data.update.timestamp)}
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+ View all icons
+
+
+
+
+
+ )
+}
diff --git a/web/src/components/theme-switcher.tsx b/web/src/components/theme-switcher.tsx
index 293f630c..1ed793e6 100644
--- a/web/src/components/theme-switcher.tsx
+++ b/web/src/components/theme-switcher.tsx
@@ -12,16 +12,26 @@ export function ThemeSwitcher() {
return (
-
+
-
+
Toggle theme
- setTheme("light")}>Light
- setTheme("dark")}>Dark
- setTheme("system")}>System
+ setTheme("light")} className="cursor-pointer">
+ Light
+
+ setTheme("dark")} className="cursor-pointer">
+ Dark
+
+ setTheme("system")} className="cursor-pointer">
+ System
+
)
diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts
index d15bfe97..8b70159f 100644
--- a/web/src/lib/api.ts
+++ b/web/src/lib/api.ts
@@ -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
{
+ 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)
+}