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
parent
15f841cb09
commit
86b89f5518
@@ -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[],
|
||||
})),
|
||||
];
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user