Merge pull request #477 from grokdesigns/feature-add-sponsor-message

Add supporter message feature
This commit is contained in:
Owen Schwartz 2025-04-08 11:23:41 -04:00 committed by GitHub
commit 787a172a7c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 222 additions and 93 deletions

11
package-lock.json generated
View file

@ -34,6 +34,7 @@
"@tanstack/react-table": "8.20.6", "@tanstack/react-table": "8.20.6",
"axios": "1.7.9", "axios": "1.7.9",
"better-sqlite3": "11.7.0", "better-sqlite3": "11.7.0",
"canvas-confetti": "^1.9.3",
"class-variance-authority": "0.7.1", "class-variance-authority": "0.7.1",
"clsx": "2.1.1", "clsx": "2.1.1",
"cmdk": "1.0.4", "cmdk": "1.0.4",
@ -5417,6 +5418,16 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/canvas-confetti": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.3.tgz",
"integrity": "sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==",
"license": "ISC",
"funding": {
"type": "donate",
"url": "https://www.paypal.me/kirilvatev"
}
},
"node_modules/chalk": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",

View file

@ -45,6 +45,7 @@
"@tanstack/react-table": "8.20.6", "@tanstack/react-table": "8.20.6",
"axios": "1.7.9", "axios": "1.7.9",
"better-sqlite3": "11.7.0", "better-sqlite3": "11.7.0",
"canvas-confetti": "^1.9.3",
"class-variance-authority": "0.7.1", "class-variance-authority": "0.7.1",
"clsx": "2.1.1", "clsx": "2.1.1",
"cmdk": "1.0.4", "cmdk": "1.0.4",

View file

@ -0,0 +1,46 @@
"use client";
import React from "react";
import confetti from "canvas-confetti";
export default function SupporterMessage({ tier }: { tier: string }) {
return (
<div className="relative flex items-center space-x-2 whitespace-nowrap group">
<span
className="cursor-pointer"
onClick={(e) => {
// Get the bounding box of the element
const rect = (
e.target as HTMLElement
).getBoundingClientRect();
// Trigger confetti centered on the word "Pangolin"
confetti({
particleCount: 100,
spread: 70,
origin: {
x: (rect.left + rect.width / 2) / window.innerWidth,
y: rect.top / window.innerHeight
},
colors: ["#FFA500", "#FF4500", "#FFD700"]
});
}}
>
Pangolin
</span>
{/* SVG Star */}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 24 24"
className="w-4 h-4 text-primary"
>
<path d="M12 .587l3.668 7.431 8.2 1.192-5.934 5.782 1.4 8.168L12 18.896l-7.334 3.864 1.4-8.168L.132 9.21l8.2-1.192z" />
</svg>
{/* Popover */}
<div className="absolute left-1/2 transform -translate-x-1/2 -top-10 hidden group-hover:block bg-white/10 backdrop-blur-md text-primary text-sm rounded-md shadow-lg px-4 py-2">
Thank you for supporting Pangolin as a {tier}!
</div>
</div>
);
}

View file

@ -2,20 +2,19 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 0 0% 100%;
--foreground: 20 0.0% 10.0%; --foreground: 20 0% 10%;
--card: 0 0% 100%; --card: 0 0% 100%;
--card-foreground: 20 0.0% 10.0%; --card-foreground: 20 0% 10%;
--popover: 0 0% 100%; --popover: 0 0% 100%;
--popover-foreground: 20 0.0% 10.0%; --popover-foreground: 20 0% 10%;
--primary: 24.6 95% 53.1%; --primary: 24.6 95% 53.1%;
--primary-foreground: 60 9.1% 97.8%; --primary-foreground: 60 9.1% 97.8%;
--secondary: 60 4.8% 95.9%; --secondary: 60 4.8% 95.9%;
--secondary-foreground: 24 9.8% 10%; --secondary-foreground: 24 9.8% 10%;
--muted: 60 4.8% 85.0%; --muted: 60 4.8% 85%;
--muted-foreground: 25 5.3% 44.7%; --muted-foreground: 25 5.3% 44.7%;
--accent: 60 4.8% 90%; --accent: 60 4.8% 90%;
--accent-foreground: 24 9.8% 10%; --accent-foreground: 24 9.8% 10%;
@ -33,24 +32,24 @@
} }
.dark { .dark {
--background: 20 0.0% 10.0%; --background: 20 0% 10%;
--foreground: 60 9.1% 97.8%; --foreground: 60 9.1% 97.8%;
--card: 20 0.0% 10.0%; --card: 20 0% 10%;
--card-foreground: 60 9.1% 97.8%; --card-foreground: 60 9.1% 97.8%;
--popover: 20 0.0% 10.0%; --popover: 20 0% 10%;
--popover-foreground: 60 9.1% 97.8%; --popover-foreground: 60 9.1% 97.8%;
--primary: 20.5 90.2% 48.2%; --primary: 20.5 90.2% 48.2%;
--primary-foreground: 60 9.1% 97.8%; --primary-foreground: 60 9.1% 97.8%;
--secondary: 12 6.5% 15.0%; --secondary: 12 6.5% 15%;
--secondary-foreground: 60 9.1% 97.8%; --secondary-foreground: 60 9.1% 97.8%;
--muted: 12 6.5% 25.0%; --muted: 12 6.5% 25%;
--muted-foreground: 24 5.4% 63.9%; --muted-foreground: 24 5.4% 63.9%;
--accent: 12 2.5% 15.0%; --accent: 12 2.5% 15%;
--accent-foreground: 60 9.1% 97.8%; --accent-foreground: 60 9.1% 97.8%;
--destructive: 0 72.2% 50.6%; --destructive: 0 72.2% 50.6%;
--destructive-foreground: 60 9.1% 97.8%; --destructive-foreground: 60 9.1% 97.8%;
--border: 12 6.5% 30.0%; --border: 12 6.5% 30%;
--input: 12 6.5% 35.0%; --input: 12 6.5% 35%;
--ring: 20.5 90.2% 48.2%; --ring: 20.5 90.2% 48.2%;
--chart-1: 220 70% 50%; --chart-1: 220 70% 50%;
--chart-2: 160 60% 45%; --chart-2: 160 60% 45%;
@ -60,7 +59,6 @@
} }
} }
@layer base { @layer base {
* { * {
@apply border-border; @apply border-border;
@ -70,4 +68,3 @@
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }

View file

@ -12,13 +12,14 @@ import SupportStatusProvider from "@app/providers/SupporterStatusProvider";
import { createApiClient, internal, priv } from "@app/lib/api"; import { createApiClient, internal, priv } from "@app/lib/api";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { IsSupporterKeyVisibleResponse } from "@server/routers/supporterKey"; import { IsSupporterKeyVisibleResponse } from "@server/routers/supporterKey";
import SupporterMessage from "./components/SupporterMessage";
export const metadata: Metadata = { export const metadata: Metadata = {
title: `Dashboard - Pangolin`, title: `Dashboard - Pangolin`,
description: "" description: ""
}; };
export const dynamic = 'force-dynamic'; export const dynamic = "force-dynamic";
// const font = Figtree({ subsets: ["latin"] }); // const font = Figtree({ subsets: ["latin"] });
const font = Inter({ subsets: ["latin"] }); const font = Inter({ subsets: ["latin"] });
@ -34,9 +35,9 @@ export default async function RootLayout({
visible: true visible: true
} as any; } as any;
const res = await priv.get< const res = await priv.get<AxiosResponse<IsSupporterKeyVisibleResponse>>(
AxiosResponse<IsSupporterKeyVisibleResponse> "supporter-key/visible"
>("supporter-key/visible"); );
supporterData.visible = res.data.data.visible; supporterData.visible = res.data.data.visible;
supporterData.tier = res.data.data.tier; supporterData.tier = res.data.data.tier;
@ -61,9 +62,15 @@ export default async function RootLayout({
{/* Footer */} {/* Footer */}
<footer className="hidden md:block w-full mt-12 py-3 mb-6 px-4"> <footer className="hidden md:block w-full mt-12 py-3 mb-6 px-4">
<div className="container mx-auto flex flex-wrap justify-center items-center h-3 space-x-4 text-sm text-neutral-400 dark:text-neutral-600"> <div className="container mx-auto flex flex-wrap justify-center items-center h-3 space-x-4 text-sm text-neutral-400 dark:text-neutral-600">
{supporterData?.tier ? (
<SupporterMessage
tier={supporterData.tier}
/>
) : (
<div className="flex items-center space-x-2 whitespace-nowrap"> <div className="flex items-center space-x-2 whitespace-nowrap">
<span>Pangolin</span> <span>Pangolin</span>
</div> </div>
)}
<Separator orientation="vertical" /> <Separator orientation="vertical" />
<a <a
href="https://fossorial.io/" href="https://fossorial.io/"

View file

@ -47,6 +47,7 @@ import {
CardTitle CardTitle
} from "./ui/card"; } from "./ui/card";
import { Check, ExternalLink } from "lucide-react"; import { Check, ExternalLink } from "lucide-react";
import confetti from "canvas-confetti";
const formSchema = z.object({ const formSchema = z.object({
githubUsername: z githubUsername: z
@ -100,6 +101,7 @@ export default function SupporterStatus() {
return; return;
} }
// Trigger the toast
toast({ toast({
variant: "default", variant: "default",
title: "Valid Key", title: "Valid Key",
@ -107,6 +109,50 @@ export default function SupporterStatus() {
"Your supporter key has been validated. Thank you for your support!" "Your supporter key has been validated. Thank you for your support!"
}); });
// Fireworks-style confetti
const duration = 5 * 1000; // 5 seconds
const animationEnd = Date.now() + duration;
const defaults = {
startVelocity: 30,
spread: 360,
ticks: 60,
zIndex: 0,
colors: ["#FFA500", "#FF4500", "#FFD700"] // Orange hues
};
function randomInRange(min: number, max: number) {
return Math.random() * (max - min) + min;
}
const interval = setInterval(() => {
const timeLeft = animationEnd - Date.now();
if (timeLeft <= 0) {
clearInterval(interval);
return;
}
const particleCount = 50 * (timeLeft / duration);
// Launch confetti from two random horizontal positions
confetti({
...defaults,
particleCount,
origin: {
x: randomInRange(0.1, 0.3),
y: Math.random() - 0.2
}
});
confetti({
...defaults,
particleCount,
origin: {
x: randomInRange(0.7, 0.9),
y: Math.random() - 0.2
}
});
}, 250);
setPurchaseOptionsOpen(false); setPurchaseOptionsOpen(false);
setKeyOpen(false); setKeyOpen(false);
@ -177,7 +223,9 @@ export default function SupporterStatus() {
</p> </p>
<div className="py-6"> <div className="py-6">
<p className="mb-3 text-center">Please select the option that best suits you.</p> <p className="mb-3 text-center">
Please select the option that best suits you.
</p>
<div className="grid md:grid-cols-2 grid-cols-1 gap-8"> <div className="grid md:grid-cols-2 grid-cols-1 gap-8">
<Card> <Card>
<CardHeader> <CardHeader>

19
src/types/canvas-confetti.d.ts vendored Normal file
View file

@ -0,0 +1,19 @@
declare module "canvas-confetti" {
export interface ConfettiOptions {
particleCount?: number;
angle?: number;
spread?: number;
startVelocity?: number;
decay?: number;
gravity?: number;
drift?: number;
ticks?: number;
origin?: { x?: number; y?: number };
colors?: string[];
shapes?: string[];
scalar?: number;
zIndex?: number;
}
export default function confetti(options?: ConfettiOptions): Promise<null>;
}

View file

@ -5,59 +5,59 @@ const config: Config = {
content: [ content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}", "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}", "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}", "./src/app/**/*.{js,ts,jsx,tsx,mdx}"
], ],
theme: { theme: {
extend: { extend: {
colors: { colors: {
background: 'hsl(var(--background))', background: "hsl(var(--background))",
foreground: 'hsl(var(--foreground))', foreground: "hsl(var(--foreground))",
card: { card: {
DEFAULT: 'hsl(var(--card))', DEFAULT: "hsl(var(--card))",
foreground: 'hsl(var(--card-foreground))' foreground: "hsl(var(--card-foreground))"
}, },
popover: { popover: {
DEFAULT: 'hsl(var(--popover))', DEFAULT: "hsl(var(--popover))",
foreground: 'hsl(var(--popover-foreground))' foreground: "hsl(var(--popover-foreground))"
}, },
primary: { primary: {
DEFAULT: 'hsl(var(--primary))', DEFAULT: "hsl(var(--primary))",
foreground: 'hsl(var(--primary-foreground))' foreground: "hsl(var(--primary-foreground))"
}, },
secondary: { secondary: {
DEFAULT: 'hsl(var(--secondary))', DEFAULT: "hsl(var(--secondary))",
foreground: 'hsl(var(--secondary-foreground))' foreground: "hsl(var(--secondary-foreground))"
}, },
muted: { muted: {
DEFAULT: 'hsl(var(--muted))', DEFAULT: "hsl(var(--muted))",
foreground: 'hsl(var(--muted-foreground))' foreground: "hsl(var(--muted-foreground))"
}, },
accent: { accent: {
DEFAULT: 'hsl(var(--accent))', DEFAULT: "hsl(var(--accent))",
foreground: 'hsl(var(--accent-foreground))' foreground: "hsl(var(--accent-foreground))"
}, },
destructive: { destructive: {
DEFAULT: 'hsl(var(--destructive))', DEFAULT: "hsl(var(--destructive))",
foreground: 'hsl(var(--destructive-foreground))' foreground: "hsl(var(--destructive-foreground))"
}, },
border: 'hsl(var(--border))', border: "hsl(var(--border))",
input: 'hsl(var(--input))', input: "hsl(var(--input))",
ring: 'hsl(var(--ring))', ring: "hsl(var(--ring))",
chart: { chart: {
'1': 'hsl(var(--chart-1))', "1": "hsl(var(--chart-1))",
'2': 'hsl(var(--chart-2))', "2": "hsl(var(--chart-2))",
'3': 'hsl(var(--chart-3))', "3": "hsl(var(--chart-3))",
'4': 'hsl(var(--chart-4))', "4": "hsl(var(--chart-4))",
'5': 'hsl(var(--chart-5))' "5": "hsl(var(--chart-5))"
} }
}, },
borderRadius: { borderRadius: {
lg: 'var(--radius)', lg: "var(--radius)",
md: 'calc(var(--radius) - 2px)', md: "calc(var(--radius) - 2px)",
sm: 'calc(var(--radius) - 4px)' sm: "calc(var(--radius) - 4px)"
} }
} }
}, },
plugins: [require("tailwindcss-animate")], plugins: [require("tailwindcss-animate")]
}; };
export default config; export default config;