mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-16 09:56:36 +00:00
Merge branch 'dev' into feature/newt-podman-install
This commit is contained in:
@@ -53,13 +53,11 @@ const createSiteFormSchema = z
|
||||
.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(2, {
|
||||
message: "Name must be at least 2 characters."
|
||||
})
|
||||
.min(2, { message: "Name must be at least 2 characters." })
|
||||
.max(30, {
|
||||
message: "Name must not be longer than 30 characters."
|
||||
}),
|
||||
method: z.string(),
|
||||
method: z.enum(["newt", "wireguard", "local"]),
|
||||
copied: z.boolean()
|
||||
})
|
||||
.refine(
|
||||
@@ -77,6 +75,15 @@ const createSiteFormSchema = z
|
||||
|
||||
type CreateSiteFormValues = z.infer<typeof createSiteFormSchema>;
|
||||
|
||||
type SiteType = "newt" | "wireguard" | "local";
|
||||
|
||||
interface TunnelTypeOption {
|
||||
id: SiteType;
|
||||
title: string;
|
||||
description: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
type Commands = {
|
||||
mac: Record<string, string[]>;
|
||||
linux: Record<string, string[]>;
|
||||
@@ -102,7 +109,9 @@ export default function Page() {
|
||||
const { orgId } = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
const [tunnelTypes, setTunnelTypes] = useState<any>([
|
||||
const [tunnelTypes, setTunnelTypes] = useState<
|
||||
ReadonlyArray<TunnelTypeOption>
|
||||
>([
|
||||
{
|
||||
id: "newt",
|
||||
title: "Newt Tunnel (Recommended)",
|
||||
@@ -342,22 +351,15 @@ WantedBy=default.target`
|
||||
}
|
||||
};
|
||||
|
||||
const form = useForm({
|
||||
const form = useForm<CreateSiteFormValues>({
|
||||
resolver: zodResolver(createSiteFormSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
copied: false,
|
||||
method: "newt"
|
||||
}
|
||||
defaultValues: { name: "", copied: false, method: "newt" }
|
||||
});
|
||||
|
||||
async function onSubmit(data: CreateSiteFormValues) {
|
||||
setCreateLoading(true);
|
||||
|
||||
let payload: CreateSiteBody = {
|
||||
name: data.name,
|
||||
type: data.method
|
||||
};
|
||||
let payload: CreateSiteBody = { name: data.name, type: data.method };
|
||||
|
||||
if (data.method == "wireguard") {
|
||||
if (!siteDefaults || !wgConfig) {
|
||||
@@ -486,10 +488,7 @@ WantedBy=default.target`
|
||||
|
||||
setTunnelTypes((prev: any) => {
|
||||
return prev.map((item: any) => {
|
||||
return {
|
||||
...item,
|
||||
disabled: false
|
||||
};
|
||||
return { ...item, disabled: false };
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -564,9 +563,8 @@ WantedBy=default.target`
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
This is the
|
||||
display name for the
|
||||
site.
|
||||
This is the display
|
||||
name for the site.
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -590,12 +588,10 @@ WantedBy=default.target`
|
||||
<SettingsSectionBody>
|
||||
<StrategySelect
|
||||
options={tunnelTypes}
|
||||
defaultValue={
|
||||
form.getValues("method") as string
|
||||
}
|
||||
onChange={(value) =>
|
||||
form.setValue("method", value)
|
||||
}
|
||||
defaultValue={form.getValues("method")}
|
||||
onChange={(value) => {
|
||||
form.setValue("method", value);
|
||||
}}
|
||||
cols={3}
|
||||
/>
|
||||
</SettingsSectionBody>
|
||||
|
||||
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 text-primary text-sm rounded-md shadow-lg px-4 py-2 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
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/"
|
||||
|
||||
@@ -4,38 +4,39 @@ import { cn } from "@app/lib/cn";
|
||||
import { RadioGroup, RadioGroupItem } from "./ui/radio-group";
|
||||
import { useState } from "react";
|
||||
|
||||
interface StrategyOption {
|
||||
id: string;
|
||||
interface StrategyOption<TValue extends string> {
|
||||
id: TValue;
|
||||
title: string;
|
||||
description: string;
|
||||
disabled?: boolean; // New optional property
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface StrategySelectProps {
|
||||
options: StrategyOption[];
|
||||
defaultValue?: string;
|
||||
onChange?: (value: string) => void;
|
||||
interface StrategySelectProps<TValue extends string> {
|
||||
options: ReadonlyArray<StrategyOption<TValue>>;
|
||||
defaultValue?: TValue;
|
||||
onChange?: (value: TValue) => void;
|
||||
cols?: number;
|
||||
}
|
||||
|
||||
export function StrategySelect({
|
||||
export function StrategySelect<TValue extends string>({
|
||||
options,
|
||||
defaultValue,
|
||||
onChange,
|
||||
cols
|
||||
}: StrategySelectProps) {
|
||||
const [selected, setSelected] = useState(defaultValue);
|
||||
}: StrategySelectProps<TValue>) {
|
||||
const [selected, setSelected] = useState<TValue | undefined>(defaultValue);
|
||||
|
||||
return (
|
||||
<RadioGroup
|
||||
defaultValue={defaultValue}
|
||||
onValueChange={(value) => {
|
||||
setSelected(value);
|
||||
onChange?.(value);
|
||||
onValueChange={(value: string) => {
|
||||
const typedValue = value as TValue;
|
||||
setSelected(typedValue);
|
||||
onChange?.(typedValue);
|
||||
}}
|
||||
className={`grid md:grid-cols-${cols ? cols : 1} gap-4`}
|
||||
>
|
||||
{options.map((option) => (
|
||||
{options.map((option: StrategyOption<TValue>) => (
|
||||
<label
|
||||
key={option.id}
|
||||
htmlFor={option.id}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
Reference in New Issue
Block a user