Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,9 @@ NODE_ENV=development
ANALYZE=false

# Telemetry (set to 1 to disable)
NEXT_TELEMETRY_DISABLED=1
NEXT_TELEMETRY_DISABLED=1

# Get embed ID from: peerlist.io → your project → Share → Embed badge → copy the hash from the URL
NEXT_PUBLIC_PEERLIST_EMBED_ID=
# Get post ID from: producthunt.com → your product page → Share → Embed → copy post_id from the embed code
NEXT_PUBLIC_PRODUCT_HUNT_POST_ID=990971
2 changes: 2 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ const nextConfig: NextConfig = {
hostname: "nurui.vercel.app",
pathname: "/**",
},
{ hostname: "peerlist.io" },
{ hostname: "api.producthunt.com" },
],
formats: ["image/avif", "image/webp"],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
Expand Down
32 changes: 32 additions & 0 deletions src/components/common/BadgeLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { IBadgeConfig, TBadgeTheme } from "@/types/launchpad.type";
import Image from "next/image";

interface BadgeLinkProps {
badge: IBadgeConfig;
theme: TBadgeTheme;
}

const BadgeLink = ({ badge, theme }: BadgeLinkProps) => {
const { href, alt, height, width, getSrc } = badge;

return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={alt}
className="transition-opacity duration-200 hover:opacity-80"
>
<Image
src={getSrc(theme)}
alt={alt}
height={height}
width={width ?? 160}
style={{ height, width: width ?? "auto" }}
unoptimized // external SVG URLs — Next.js optimizer doesn't support them
/>
</a>
);
};

export default BadgeLink;
30 changes: 30 additions & 0 deletions src/components/common/BadgeSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { LAUNCHPAD_BADGES } from "@/config/launchpad.config";
import { cn } from "@/lib/utils";

const SKELETON_WIDTHS: Record<string, number> = {
peerlist: 160,
"product-hunt": 250,
};

interface BadgeSkeletonProps {
className?: string;
}

const BadgeSkeleton = ({ className }: BadgeSkeletonProps) => (
<div
className={cn(
"flex flex-wrap items-center justify-center gap-4",
className,
)}
>
{LAUNCHPAD_BADGES.map(({ id, height }) => (
<div
key={id}
className="rounded-lg bg-[var(--border-color)] animate-pulse"
style={{ height, width: SKELETON_WIDTHS[id] ?? 200 }}
/>
))}
</div>
);

export default BadgeSkeleton;
37 changes: 37 additions & 0 deletions src/components/common/LaunchpadBadges.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use client";

import BadgeLink from "@/components/common/BadgeLink";
import BadgeSkeleton from "@/components/common/BadgeSkeleton";
import { LAUNCHPAD_BADGES } from "@/config/launchpad.config";
import { useMounted } from "@/hooks/useMounted";
import { cn } from "@/lib/utils";
import type { TBadgeTheme } from "@/types/launchpad.type";
import { useTheme } from "next-themes";

interface LaunchpadBadgesProps {
className?: string;
}

const LaunchpadBadges = ({ className }: LaunchpadBadgesProps) => {
const mounted = useMounted();
const { resolvedTheme } = useTheme();

if (!mounted) return <BadgeSkeleton className={cn("mt-4", className)} />;

const theme: TBadgeTheme = resolvedTheme === "dark" ? "light" : "light";

return (
<div
className={cn(
"flex flex-wrap items-center justify-center gap-4 mt-8",
className,
)}
>
{LAUNCHPAD_BADGES.map((badge) => (
<BadgeLink key={badge.id} badge={badge} theme={theme} />
))}
</div>
);
};

export default LaunchpadBadges;
5 changes: 4 additions & 1 deletion src/components/layout/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { cn } from "@/lib/utils";
import { navigationActive } from "@/utils/navigationActive";
import { usePathname } from "next/navigation";
import "../../styles/footer.css";
import LaunchpadBadges from "../common/LaunchpadBadges";
import AnimatedInput from "../nurui/animated-input";

const navigation = [
Expand Down Expand Up @@ -52,7 +53,7 @@ const Footer = () => {
>
<RocketScrollToTop className="bg-[var(--background-color)] max-w-24 mx-auto rounded-full -mt-16 hidden md:block" />
<div className="container">
<div className="flex flex-col items-center text-center pb-7 pt-7 xl:pb-16 xl:pt-8 gap-2">
<div className="flex flex-col items-center text-center pb-7 pt-7 xl:pb-14 xl:pt-8 gap-2">
<Nurui
textSize="text-2xl lg:text-3xl -mb-0"
logoNameClassName="flex"
Expand All @@ -70,6 +71,8 @@ const Footer = () => {
buttonTitle={newsLetter?.subscribe_button.label}
buttonClassName="bg-[var(--primary-color-4)] border border-[var(--primary-color)] hover:text-[var(--primary-color)] text-[var(--primary-color)] hover:bg-[var(--primary-color-3)]"
/>

<LaunchpadBadges />
</div>

<div className="border-t border-[var(--border-color)] border-opacity-20 p-5 flex items-center justify-center lg:justify-between">
Expand Down
33 changes: 33 additions & 0 deletions src/config/launchpad.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { getPeerlistBadgeSrc, getProductHuntBadgeSrc } from "@/lib/launchpad";
import { IBadgeConfig } from "@/types/launchpad.type";

const PEERLIST_EMBED_ID = process.env.NEXT_PUBLIC_PEERLIST_EMBED_ID ?? "";
const PRODUCT_HUNT_POST_ID = process.env.NEXT_PUBLIC_PRODUCT_HUNT_POST_ID ?? "";

export const LAUNCHPAD_BADGES: IBadgeConfig[] = [
{
id: "swb",
href: "https://sellwithboost.com",
alt: "Listed on Sell With Boost",
height: 40,
getSrc: () => "https://sellwithboost.com/badge/listing.svg",
},
{
id: "product-hunt",
href: "https://www.producthunt.com/posts/nurui",
alt: "Nur UI — Find us on Product Hunt",
height: 54,
width: 250,
getSrc: (theme) => getProductHuntBadgeSrc(PRODUCT_HUNT_POST_ID, theme),
},
{
id: "peerlist",
href: "https://peerlist.io/scroll/post/ACTH7B8DOGRRGMGRA1NKGJ8GL7GMOJ",
alt: "Nur UI — Listed on Peerlist Launchpad",
height: 56,
getSrc: (theme) =>
PEERLIST_EMBED_ID
? getPeerlistBadgeSrc(PEERLIST_EMBED_ID, theme)
: `https://peerlist.io/api/v1/projects/embed/placeholder?showUpvote=true&theme=${theme}&count=0`,
},
];
35 changes: 35 additions & 0 deletions src/hooks/useBadgeSvg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useEffect, useState } from "react";

interface UseBadgeSvgReturn {
svg: string | null;
isLoading: boolean;
}

export function useBadgeSvg(src: string): UseBadgeSvgReturn {
const [svg, setSvg] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
let cancelled = false;

async function load() {
try {
const res = await fetch(src);
if (!res.ok) throw new Error("Failed to fetch badge SVG");
const text = await res.text();
if (!cancelled) setSvg(text);
} catch {
if (!cancelled) setSvg(null);
} finally {
if (!cancelled) setIsLoading(false);
}
}

load();
return () => {
cancelled = true;
};
}, [src]);

return { svg, isLoading };
}
7 changes: 7 additions & 0 deletions src/hooks/useMounted.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { useEffect, useState } from "react";

export function useMounted(): boolean {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
return mounted;
}
7 changes: 7 additions & 0 deletions src/lib/launchpad.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { TBadgeTheme } from "@/types/launchpad.type";

export const getPeerlistBadgeSrc = (embedId: string, theme: TBadgeTheme) =>
`https://peerlist.io/api/v1/projects/embed/${embedId}?showUpvote=true&theme=${theme}`;

export const getProductHuntBadgeSrc = (postId: string, theme: TBadgeTheme) =>
`https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=${postId}&theme=${theme}`;
10 changes: 10 additions & 0 deletions src/types/launchpad.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type TBadgeTheme = "light" | "dark";

export interface IBadgeConfig {
id: string;
href: string;
alt: string;
height: number;
width?: number;
getSrc: (theme: TBadgeTheme) => string;
}
Loading