mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-04 01:55:10 +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",
|
||||
"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",
|
||||
|
@ -5417,6 +5418,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",
|
||||
|
|
|
@ -45,6 +45,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",
|
||||
|
|
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 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,13 +12,14 @@ 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`,
|
||||
description: ""
|
||||
};
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
// const font = Figtree({ subsets: ["latin"] });
|
||||
const font = Inter({ subsets: ["latin"] });
|
||||
|
@ -34,9 +35,9 @@ export default async function RootLayout({
|
|||
visible: true
|
||||
} as any;
|
||||
|
||||
const res = await priv.get<
|
||||
AxiosResponse<IsSupporterKeyVisibleResponse>
|
||||
>("supporter-key/visible");
|
||||
const res = await priv.get<AxiosResponse<IsSupporterKeyVisibleResponse>>(
|
||||
"supporter-key/visible"
|
||||
);
|
||||
supporterData.visible = res.data.data.visible;
|
||||
supporterData.tier = res.data.data.tier;
|
||||
|
||||
|
@ -61,9 +62,15 @@ export default async function RootLayout({
|
|||
{/* Footer */}
|
||||
<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="flex items-center space-x-2 whitespace-nowrap">
|
||||
<span>Pangolin</span>
|
||||
</div>
|
||||
{supporterData?.tier ? (
|
||||
<SupporterMessage
|
||||
tier={supporterData.tier}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<span>Pangolin</span>
|
||||
</div>
|
||||
)}
|
||||
<Separator orientation="vertical" />
|
||||
<a
|
||||
href="https://fossorial.io/"
|
||||
|
|
|
@ -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() {
|
|||
</p>
|
||||
|
||||
<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">
|
||||
<Card>
|
||||
<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: [
|
||||
"./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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue