From aa45150c518dd922dcb2a77a5c7f223671f1fde3 Mon Sep 17 00:00:00 2001 From: grokdesigns Date: Mon, 7 Apr 2025 10:34:32 -0700 Subject: [PATCH 1/4] Add supporter message feature --- src/app/layout.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index bc322572..b80b0790 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -113,6 +113,15 @@ export default async function RootLayout({ )} + {supporterData?.tier && ( +
+
+

+ Thank you for supporting Pangolin as a {supporterData.tier}! +

+
+
+ )} From 9ea37789d63313f463700f0bd71ba9e2e80e61c2 Mon Sep 17 00:00:00 2001 From: grokdesigns Date: Mon, 7 Apr 2025 13:32:07 -0700 Subject: [PATCH 2/4] Applied Prettier formatting --- src/app/layout.tsx | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b80b0790..ce75dc27 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -18,7 +18,7 @@ export const metadata: Metadata = { description: "" }; -export const dynamic = 'force-dynamic'; +export const dynamic = "force-dynamic"; // const font = Figtree({ subsets: ["latin"] }); const font = Inter({ subsets: ["latin"] }); @@ -34,9 +34,9 @@ export default async function RootLayout({ visible: true } as any; - const res = await priv.get< - AxiosResponse - >("supporter-key/visible"); + const res = await priv.get>( + "supporter-key/visible" + ); supporterData.visible = res.data.data.visible; supporterData.tier = res.data.data.tier; @@ -114,14 +114,16 @@ export default async function RootLayout({ )} {supporterData?.tier && ( -
-
-

- Thank you for supporting Pangolin as a {supporterData.tier}! -

+
+
+

+ Thank you for supporting + Pangolin as a{" "} + {supporterData.tier}! +

+
-
- )} + )} From b2faeb3c17a3914f24f8a5d91d277d4d47a35089 Mon Sep 17 00:00:00 2001 From: grokdesigns Date: Mon, 7 Apr 2025 14:38:49 -0700 Subject: [PATCH 3/4] Added some more pizazz to the thank you message --- package-lock.json | 11 ++++++++ package.json | 1 + src/app/components/SupporterMessage.tsx | 34 +++++++++++++++++++++++++ src/app/layout.tsx | 13 +++------- src/types/canvas-confetti.d.ts | 19 ++++++++++++++ 5 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 src/app/components/SupporterMessage.tsx create mode 100644 src/types/canvas-confetti.d.ts diff --git a/package-lock.json b/package-lock.json index c9e0a334..443d478d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "@tanstack/react-table": "8.20.6", "axios": "1.7.9", "better-sqlite3": "11.7.0", + "canvas-confetti": "^1.9.3", "class-variance-authority": "0.7.1", "clsx": "2.1.1", "cmdk": "1.0.4", @@ -5384,6 +5385,16 @@ ], "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": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", diff --git a/package.json b/package.json index 08cb73aa..c853d004 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@tanstack/react-table": "8.20.6", "axios": "1.7.9", "better-sqlite3": "11.7.0", + "canvas-confetti": "^1.9.3", "class-variance-authority": "0.7.1", "clsx": "2.1.1", "cmdk": "1.0.4", diff --git a/src/app/components/SupporterMessage.tsx b/src/app/components/SupporterMessage.tsx new file mode 100644 index 00000000..678b1bdd --- /dev/null +++ b/src/app/components/SupporterMessage.tsx @@ -0,0 +1,34 @@ +"use client"; + +import React from "react"; +import confetti from "canvas-confetti"; + +export default function SupporterMessage({ tier }: { tier: string }) { + return ( +
+
{ + // Get the bounding box of the element + const rect = ( + e.target as HTMLElement + ).getBoundingClientRect(); + + // Calculate the origin based on the top center of the box + confetti({ + particleCount: 100, + spread: 70, + origin: { + x: (rect.left + rect.width / 2) / window.innerWidth, // Horizontal center of the box + y: rect.top / window.innerHeight // Top of the box + } + }); + }} + > +

+ Thank you for supporting Pangolin as a {tier}! +

+
+
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index ce75dc27..8d472673 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -12,6 +12,7 @@ import SupportStatusProvider from "@app/providers/SupporterStatusProvider"; import { createApiClient, internal, priv } from "@app/lib/api"; import { AxiosResponse } from "axios"; import { IsSupporterKeyVisibleResponse } from "@server/routers/supporterKey"; +import SupporterMessage from "./components/SupporterMessage"; export const metadata: Metadata = { title: `Dashboard - Pangolin`, @@ -114,15 +115,9 @@ export default async function RootLayout({ )}
{supporterData?.tier && ( -
-
-

- Thank you for supporting - Pangolin as a{" "} - {supporterData.tier}! -

-
-
+ )} diff --git a/src/types/canvas-confetti.d.ts b/src/types/canvas-confetti.d.ts new file mode 100644 index 00000000..c88a6443 --- /dev/null +++ b/src/types/canvas-confetti.d.ts @@ -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; +} \ No newline at end of file From 23a68fbc10a1eba8f2a3f0eaca1de4f8d1ff4e60 Mon Sep 17 00:00:00 2001 From: grokdesigns Date: Mon, 7 Apr 2025 21:15:24 -0700 Subject: [PATCH 4/4] Add confetti on valid key and make thank you less intrusive --- src/app/components/SupporterMessage.tsx | 32 ++++--- src/app/globals.css | 109 ++++++++++++------------ src/app/layout.tsx | 17 ++-- src/components/SupporterStatus.tsx | 50 ++++++++++- tailwind.config.ts | 58 ++++++------- 5 files changed, 162 insertions(+), 104 deletions(-) diff --git a/src/app/components/SupporterMessage.tsx b/src/app/components/SupporterMessage.tsx index 678b1bdd..7706e190 100644 --- a/src/app/components/SupporterMessage.tsx +++ b/src/app/components/SupporterMessage.tsx @@ -5,29 +5,41 @@ import confetti from "canvas-confetti"; export default function SupporterMessage({ tier }: { tier: string }) { return ( -
-
+ { // Get the bounding box of the element const rect = ( e.target as HTMLElement ).getBoundingClientRect(); - // Calculate the origin based on the top center of the box + // Trigger confetti centered on the word "Pangolin" confetti({ particleCount: 100, spread: 70, origin: { - x: (rect.left + rect.width / 2) / window.innerWidth, // Horizontal center of the box - y: rect.top / window.innerHeight // Top of the box - } + x: (rect.left + rect.width / 2) / window.innerWidth, + y: rect.top / window.innerHeight + }, + colors: ["#FFA500", "#FF4500", "#FFD700"] }); }} > -

- Thank you for supporting Pangolin as a {tier}! -

+ Pangolin +
+ {/* SVG Star */} + + + + {/* Popover */} +
+ Thank you for supporting Pangolin as a {tier}!
); diff --git a/src/app/globals.css b/src/app/globals.css index cb32e061..db1d8feb 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,65 +2,63 @@ @tailwind components; @tailwind utilities; - @layer base { - :root { - --background: 0 0% 100%; - --foreground: 20 0.0% 10.0%; - --card: 0 0% 100%; - --card-foreground: 20 0.0% 10.0%; - --popover: 0 0% 100%; - --popover-foreground: 20 0.0% 10.0%; - --primary: 24.6 95% 53.1%; - --primary-foreground: 60 9.1% 97.8%; - --secondary: 60 4.8% 95.9%; - --secondary-foreground: 24 9.8% 10%; - --muted: 60 4.8% 85.0%; - --muted-foreground: 25 5.3% 44.7%; - --accent: 60 4.8% 90%; - --accent-foreground: 24 9.8% 10%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 60 9.1% 97.8%; - --border: 20 5.9% 80%; - --input: 20 5.9% 75%; - --ring: 24.6 95% 53.1%; - --radius: 0.75rem; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - } + :root { + --background: 0 0% 100%; + --foreground: 20 0% 10%; + --card: 0 0% 100%; + --card-foreground: 20 0% 10%; + --popover: 0 0% 100%; + --popover-foreground: 20 0% 10%; + --primary: 24.6 95% 53.1%; + --primary-foreground: 60 9.1% 97.8%; + --secondary: 60 4.8% 95.9%; + --secondary-foreground: 24 9.8% 10%; + --muted: 60 4.8% 85%; + --muted-foreground: 25 5.3% 44.7%; + --accent: 60 4.8% 90%; + --accent-foreground: 24 9.8% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 60 9.1% 97.8%; + --border: 20 5.9% 80%; + --input: 20 5.9% 75%; + --ring: 24.6 95% 53.1%; + --radius: 0.75rem; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + } - .dark { - --background: 20 0.0% 10.0%; - --foreground: 60 9.1% 97.8%; - --card: 20 0.0% 10.0%; - --card-foreground: 60 9.1% 97.8%; - --popover: 20 0.0% 10.0%; - --popover-foreground: 60 9.1% 97.8%; - --primary: 20.5 90.2% 48.2%; - --primary-foreground: 60 9.1% 97.8%; - --secondary: 12 6.5% 15.0%; - --secondary-foreground: 60 9.1% 97.8%; - --muted: 12 6.5% 25.0%; - --muted-foreground: 24 5.4% 63.9%; - --accent: 12 2.5% 15.0%; - --accent-foreground: 60 9.1% 97.8%; - --destructive: 0 72.2% 50.6%; - --destructive-foreground: 60 9.1% 97.8%; - --border: 12 6.5% 30.0%; - --input: 12 6.5% 35.0%; - --ring: 20.5 90.2% 48.2%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - } + .dark { + --background: 20 0% 10%; + --foreground: 60 9.1% 97.8%; + --card: 20 0% 10%; + --card-foreground: 60 9.1% 97.8%; + --popover: 20 0% 10%; + --popover-foreground: 60 9.1% 97.8%; + --primary: 20.5 90.2% 48.2%; + --primary-foreground: 60 9.1% 97.8%; + --secondary: 12 6.5% 15%; + --secondary-foreground: 60 9.1% 97.8%; + --muted: 12 6.5% 25%; + --muted-foreground: 24 5.4% 63.9%; + --accent: 12 2.5% 15%; + --accent-foreground: 60 9.1% 97.8%; + --destructive: 0 72.2% 50.6%; + --destructive-foreground: 60 9.1% 97.8%; + --border: 12 6.5% 30%; + --input: 12 6.5% 35%; + --ring: 20.5 90.2% 48.2%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } } - @layer base { * { @apply border-border; @@ -70,4 +68,3 @@ @apply bg-background text-foreground; } } - diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 8d472673..9a6f7cb9 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -62,9 +62,15 @@ export default async function RootLayout({ {/* Footer */} diff --git a/src/components/SupporterStatus.tsx b/src/components/SupporterStatus.tsx index b7b45c40..a08fdf77 100644 --- a/src/components/SupporterStatus.tsx +++ b/src/components/SupporterStatus.tsx @@ -47,6 +47,7 @@ import { CardTitle } from "./ui/card"; import { Check, ExternalLink } from "lucide-react"; +import confetti from "canvas-confetti"; const formSchema = z.object({ githubUsername: z @@ -100,6 +101,7 @@ export default function SupporterStatus() { return; } + // Trigger the toast toast({ variant: "default", title: "Valid Key", @@ -107,6 +109,50 @@ export default function SupporterStatus() { "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); setKeyOpen(false); @@ -177,7 +223,9 @@ export default function SupporterStatus() {

-

Please select the option that best suits you.

+

+ Please select the option that best suits you. +

diff --git a/tailwind.config.ts b/tailwind.config.ts index 1978d23a..67762da1 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -5,59 +5,59 @@ const config: Config = { content: [ "./src/pages/**/*.{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: { extend: { colors: { - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))' + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))" }, popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))' + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))" }, primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))' + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))" }, secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))' + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))" }, muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))' + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))" }, accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))' + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))" }, destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))' + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))" }, - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", chart: { - '1': 'hsl(var(--chart-1))', - '2': 'hsl(var(--chart-2))', - '3': 'hsl(var(--chart-3))', - '4': 'hsl(var(--chart-4))', - '5': 'hsl(var(--chart-5))' + "1": "hsl(var(--chart-1))", + "2": "hsl(var(--chart-2))", + "3": "hsl(var(--chart-3))", + "4": "hsl(var(--chart-4))", + "5": "hsl(var(--chart-5))" } }, borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)' + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)" } } }, - plugins: [require("tailwindcss-animate")], + plugins: [require("tailwindcss-animate")] }; export default config;