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:
Bjorn Lammers
2025-04-17 02:43:14 +02:00
committed by Thomas Camlong
parent 15f841cb09
commit 86b89f5518
21 changed files with 997 additions and 230 deletions

View File

@@ -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>
)
}

View File

@@ -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>
)}

View File

@@ -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>
)

View File

@@ -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>
);
)
}

View File

@@ -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>
)
}

View File

@@ -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[],
})),
];
]
}