mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-28 21:58:15 +02:00
Merge pull request #477 from grokdesigns/feature-add-sponsor-message
Add supporter message feature
This commit is contained in:
commit
787a172a7c
8 changed files with 222 additions and 93 deletions
11
package-lock.json
generated
11
package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
46
src/app/components/SupporterMessage.tsx
Normal file
46
src/app/components/SupporterMessage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -2,65 +2,63 @@
|
||||||
@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%;
|
||||||
--destructive: 0 84.2% 60.2%;
|
--destructive: 0 84.2% 60.2%;
|
||||||
--destructive-foreground: 60 9.1% 97.8%;
|
--destructive-foreground: 60 9.1% 97.8%;
|
||||||
--border: 20 5.9% 80%;
|
--border: 20 5.9% 80%;
|
||||||
--input: 20 5.9% 75%;
|
--input: 20 5.9% 75%;
|
||||||
--ring: 24.6 95% 53.1%;
|
--ring: 24.6 95% 53.1%;
|
||||||
--radius: 0.75rem;
|
--radius: 0.75rem;
|
||||||
--chart-1: 12 76% 61%;
|
--chart-1: 12 76% 61%;
|
||||||
--chart-2: 173 58% 39%;
|
--chart-2: 173 58% 39%;
|
||||||
--chart-3: 197 37% 24%;
|
--chart-3: 197 37% 24%;
|
||||||
--chart-4: 43 74% 66%;
|
--chart-4: 43 74% 66%;
|
||||||
--chart-5: 27 87% 67%;
|
--chart-5: 27 87% 67%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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%;
|
||||||
--chart-3: 30 80% 55%;
|
--chart-3: 30 80% 55%;
|
||||||
--chart-4: 280 65% 60%;
|
--chart-4: 280 65% 60%;
|
||||||
--chart-5: 340 75% 55%;
|
--chart-5: 340 75% 55%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
|
@ -70,4 +68,3 @@
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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">
|
||||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
{supporterData?.tier ? (
|
||||||
<span>Pangolin</span>
|
<SupporterMessage
|
||||||
</div>
|
tier={supporterData.tier}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||||
|
<span>Pangolin</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Separator orientation="vertical" />
|
<Separator orientation="vertical" />
|
||||||
<a
|
<a
|
||||||
href="https://fossorial.io/"
|
href="https://fossorial.io/"
|
||||||
|
|
|
@ -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
19
src/types/canvas-confetti.d.ts
vendored
Normal 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>;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue