mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-17 10:26:39 +00:00
Merge branch 'dev' into refactor/paginated-tables
This commit is contained in:
24
src/components/ApplyInternalRedirect.tsx
Normal file
24
src/components/ApplyInternalRedirect.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { consumeInternalRedirectPath } from "@app/lib/internalRedirect";
|
||||
|
||||
type ApplyInternalRedirectProps = {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export default function ApplyInternalRedirect({
|
||||
orgId
|
||||
}: ApplyInternalRedirectProps) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const path = consumeInternalRedirectPath();
|
||||
if (path) {
|
||||
router.replace(`/${orgId}${path}`);
|
||||
}
|
||||
}, [orgId, router]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
InfoSections,
|
||||
InfoSectionTitle
|
||||
} from "@app/components/InfoSection";
|
||||
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type ClientInfoCardProps = {};
|
||||
@@ -16,6 +17,12 @@ export default function SiteInfoCard({}: ClientInfoCardProps) {
|
||||
const { client, updateClient } = useClientContext();
|
||||
const t = useTranslations();
|
||||
|
||||
const userDisplayName = getUserDisplayName({
|
||||
email: client.userEmail,
|
||||
name: client.userName,
|
||||
username: client.userUsername
|
||||
});
|
||||
|
||||
return (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
@@ -25,8 +32,12 @@ export default function SiteInfoCard({}: ClientInfoCardProps) {
|
||||
<InfoSectionContent>{client.name}</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>{t("identifier")}</InfoSectionTitle>
|
||||
<InfoSectionContent>{client.niceId}</InfoSectionContent>
|
||||
<InfoSectionTitle>
|
||||
{userDisplayName ? t("user") : t("identifier")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{userDisplayName || client.niceId}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>{t("status")}</InfoSectionTitle>
|
||||
|
||||
@@ -29,6 +29,7 @@ type DashboardLoginFormProps = {
|
||||
searchParams?: {
|
||||
[key: string]: string | string[] | undefined;
|
||||
};
|
||||
defaultUser?: string;
|
||||
};
|
||||
|
||||
export default function DashboardLoginForm({
|
||||
@@ -36,7 +37,8 @@ export default function DashboardLoginForm({
|
||||
idps,
|
||||
forceLogin,
|
||||
showOrgLogin,
|
||||
searchParams
|
||||
searchParams,
|
||||
defaultUser
|
||||
}: DashboardLoginFormProps) {
|
||||
const router = useRouter();
|
||||
const { env } = useEnvContext();
|
||||
@@ -75,6 +77,7 @@ export default function DashboardLoginForm({
|
||||
redirect={redirect}
|
||||
idps={idps}
|
||||
forceLogin={forceLogin}
|
||||
defaultEmail={defaultUser}
|
||||
onLogin={(redirectUrl) => {
|
||||
if (redirectUrl) {
|
||||
const safe = cleanRedirect(redirectUrl);
|
||||
|
||||
@@ -55,12 +55,14 @@ type DeviceLoginFormProps = {
|
||||
userEmail: string;
|
||||
userName?: string;
|
||||
initialCode?: string;
|
||||
userQueryParam?: string;
|
||||
};
|
||||
|
||||
export default function DeviceLoginForm({
|
||||
userEmail,
|
||||
userName,
|
||||
initialCode = ""
|
||||
initialCode = "",
|
||||
userQueryParam
|
||||
}: DeviceLoginFormProps) {
|
||||
const router = useRouter();
|
||||
const { env } = useEnvContext();
|
||||
@@ -219,9 +221,12 @@ export default function DeviceLoginForm({
|
||||
const currentSearch =
|
||||
typeof window !== "undefined" ? window.location.search : "";
|
||||
const redirectTarget = `/auth/login/device${currentSearch || ""}`;
|
||||
router.push(
|
||||
`/auth/login?forceLogin=true&redirect=${encodeURIComponent(redirectTarget)}`
|
||||
);
|
||||
const loginUrl = new URL("/auth/login", "http://x");
|
||||
loginUrl.searchParams.set("forceLogin", "true");
|
||||
loginUrl.searchParams.set("redirect", redirectTarget);
|
||||
if (userQueryParam)
|
||||
loginUrl.searchParams.set("user", userQueryParam);
|
||||
router.push(loginUrl.pathname + loginUrl.search);
|
||||
router.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,20 +250,41 @@ export default function GenerateLicenseKeyForm({
|
||||
const submitLicenseRequest = async (payload: any) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await api.put<
|
||||
AxiosResponse<GenerateNewLicenseResponse>
|
||||
>(`/org/${orgId}/license`, payload);
|
||||
// Check if this is a business/enterprise license request
|
||||
if (payload.useCaseType === "business") {
|
||||
const response = await api.put<
|
||||
AxiosResponse<string>
|
||||
>(`/org/${orgId}/license/enterprise`, { ...payload, tier: "big_license" } );
|
||||
|
||||
if (response.data.data?.licenseKey?.licenseKey) {
|
||||
setGeneratedKey(response.data.data.licenseKey.licenseKey);
|
||||
onGenerated?.();
|
||||
toast({
|
||||
title: t("generateLicenseKeyForm.toasts.success.title"),
|
||||
description: t(
|
||||
"generateLicenseKeyForm.toasts.success.description"
|
||||
),
|
||||
variant: "default"
|
||||
});
|
||||
console.log("Checkout session response:", response.data);
|
||||
const checkoutUrl = response.data.data;
|
||||
if (checkoutUrl) {
|
||||
window.location.href = checkoutUrl;
|
||||
} else {
|
||||
toast({
|
||||
title: "Failed to get checkout URL",
|
||||
description: "Please try again later",
|
||||
variant: "destructive"
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
} else {
|
||||
// Personal license flow
|
||||
const response = await api.put<
|
||||
AxiosResponse<GenerateNewLicenseResponse>
|
||||
>(`/org/${orgId}/license`, payload);
|
||||
|
||||
if (response.data.data?.licenseKey?.licenseKey) {
|
||||
setGeneratedKey(response.data.data.licenseKey.licenseKey);
|
||||
onGenerated?.();
|
||||
toast({
|
||||
title: t("generateLicenseKeyForm.toasts.success.title"),
|
||||
description: t(
|
||||
"generateLicenseKeyForm.toasts.success.description"
|
||||
),
|
||||
variant: "default"
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -1066,16 +1087,16 @@ export default function GenerateLicenseKeyForm({
|
||||
)}
|
||||
|
||||
{!generatedKey && useCaseType === "business" && (
|
||||
<Button
|
||||
type="submit"
|
||||
form="generate-license-business-form"
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
>
|
||||
{t(
|
||||
"generateLicenseKeyForm.buttons.generateLicenseKey"
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
form="generate-license-business-form"
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
>
|
||||
{t(
|
||||
"generateLicenseKeyForm.buttons.generateLicenseKey"
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
|
||||
@@ -10,12 +10,12 @@ import { Badge } from "./ui/badge";
|
||||
import moment from "moment";
|
||||
import { DataTable } from "./ui/data-table";
|
||||
import { GeneratedLicenseKey } from "@server/routers/generatedLicense/types";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import GenerateLicenseKeyForm from "./GenerateLicenseKeyForm";
|
||||
import NewPricingLicenseForm from "./NewPricingLicenseForm";
|
||||
|
||||
type GnerateLicenseKeysTableProps = {
|
||||
licenseKeys: GeneratedLicenseKey[];
|
||||
@@ -29,12 +29,15 @@ function obfuscateLicenseKey(key: string): string {
|
||||
return `${firstPart}••••••••••••••••••••${lastPart}`;
|
||||
}
|
||||
|
||||
const GENERATE_QUERY = "generate";
|
||||
|
||||
export default function GenerateLicenseKeysTable({
|
||||
licenseKeys,
|
||||
orgId
|
||||
}: GnerateLicenseKeysTableProps) {
|
||||
const t = useTranslations();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const { env } = useEnvContext();
|
||||
const api = createApiClient({ env });
|
||||
@@ -42,6 +45,19 @@ export default function GenerateLicenseKeysTable({
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [showGenerateForm, setShowGenerateForm] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchParams.get(GENERATE_QUERY) !== null) {
|
||||
setShowGenerateForm(true);
|
||||
const next = new URLSearchParams(searchParams);
|
||||
next.delete(GENERATE_QUERY);
|
||||
const qs = next.toString();
|
||||
const url = qs
|
||||
? `${window.location.pathname}?${qs}`
|
||||
: window.location.pathname;
|
||||
window.history.replaceState(null, "", url);
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
const handleLicenseGenerated = () => {
|
||||
// Refresh the data after license is generated
|
||||
refreshData();
|
||||
@@ -158,6 +174,48 @@ export default function GenerateLicenseKeysTable({
|
||||
: t("licenseTierPersonal");
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "users",
|
||||
friendlyName: t("users"),
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("users")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const users = row.original.users;
|
||||
return users === -1 ? "∞" : users;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "sites",
|
||||
friendlyName: t("sites"),
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("sites")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const sites = row.original.sites;
|
||||
return sites === -1 ? "∞" : sites;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "terminateAt",
|
||||
friendlyName: t("licenseTableValidUntil"),
|
||||
@@ -198,7 +256,7 @@ export default function GenerateLicenseKeysTable({
|
||||
}}
|
||||
/>
|
||||
|
||||
<GenerateLicenseKeyForm
|
||||
<NewPricingLicenseForm
|
||||
open={showGenerateForm}
|
||||
setOpen={setShowGenerateForm}
|
||||
orgId={orgId}
|
||||
|
||||
@@ -54,6 +54,7 @@ type LoginFormProps = {
|
||||
idps?: LoginFormIDP[];
|
||||
orgId?: string;
|
||||
forceLogin?: boolean;
|
||||
defaultEmail?: string;
|
||||
};
|
||||
|
||||
export default function LoginForm({
|
||||
@@ -61,7 +62,8 @@ export default function LoginForm({
|
||||
onLogin,
|
||||
idps,
|
||||
orgId,
|
||||
forceLogin
|
||||
forceLogin,
|
||||
defaultEmail
|
||||
}: LoginFormProps) {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -116,7 +118,7 @@ export default function LoginForm({
|
||||
const form = useForm({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
email: defaultEmail ?? "",
|
||||
password: ""
|
||||
}
|
||||
});
|
||||
|
||||
913
src/components/NewPricingLicenseForm.tsx
Normal file
913
src/components/NewPricingLicenseForm.tsx
Normal file
@@ -0,0 +1,913 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { Checkbox } from "@app/components/ui/checkbox";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { useState } from "react";
|
||||
import { useForm, type Resolver } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import CopyTextBox from "@app/components/CopyTextBox";
|
||||
import {
|
||||
Credenza,
|
||||
CredenzaBody,
|
||||
CredenzaClose,
|
||||
CredenzaContent,
|
||||
CredenzaDescription,
|
||||
CredenzaFooter,
|
||||
CredenzaHeader,
|
||||
CredenzaTitle
|
||||
} from "@app/components/Credenza";
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { GenerateNewLicenseResponse } from "@server/routers/generatedLicense/types";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React from "react";
|
||||
import { StrategySelect, StrategyOption } from "./StrategySelect";
|
||||
import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { useUserContext } from "@app/hooks/useUserContext";
|
||||
|
||||
const TIER_TO_LICENSE_ID = {
|
||||
starter: "small_license",
|
||||
scale: "big_license"
|
||||
} as const;
|
||||
|
||||
type FormProps = {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
orgId: string;
|
||||
onGenerated?: () => void;
|
||||
};
|
||||
|
||||
export default function NewPricingLicenseForm({
|
||||
open,
|
||||
setOpen,
|
||||
orgId,
|
||||
onGenerated
|
||||
}: FormProps) {
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
const api = createApiClient({ env });
|
||||
const { user } = useUserContext();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [generatedKey, setGeneratedKey] = useState<string | null>(null);
|
||||
const [personalUseOnly, setPersonalUseOnly] = useState(false);
|
||||
const [selectedTier, setSelectedTier] = useState<"starter" | "scale">(
|
||||
"starter"
|
||||
);
|
||||
|
||||
const personalFormSchema = z.object({
|
||||
email: z.email(),
|
||||
firstName: z.string().min(1),
|
||||
lastName: z.string().min(1),
|
||||
primaryUse: z.string().min(1),
|
||||
country: z.string().min(1),
|
||||
phoneNumber: z.string().optional(),
|
||||
agreedToTerms: z.boolean().refine((val) => val === true),
|
||||
complianceConfirmed: z.boolean().refine((val) => val === true)
|
||||
});
|
||||
|
||||
const businessFormSchema = z.object({
|
||||
email: z.email(),
|
||||
firstName: z.string().min(1),
|
||||
lastName: z.string().min(1),
|
||||
primaryUse: z.string().min(1),
|
||||
industry: z.string().min(1),
|
||||
companyName: z.string().min(1),
|
||||
companyWebsite: z.string().optional(),
|
||||
companyPhoneNumber: z.string().optional(),
|
||||
agreedToTerms: z.boolean().refine((val) => val === true),
|
||||
complianceConfirmed: z.boolean().refine((val) => val === true)
|
||||
});
|
||||
|
||||
type PersonalFormData = z.infer<typeof personalFormSchema>;
|
||||
type BusinessFormData = z.infer<typeof businessFormSchema>;
|
||||
|
||||
const personalForm = useForm<PersonalFormData>({
|
||||
resolver: zodResolver(personalFormSchema) as Resolver<PersonalFormData>,
|
||||
defaultValues: {
|
||||
email: user?.email || "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
primaryUse: "",
|
||||
country: "",
|
||||
phoneNumber: "",
|
||||
agreedToTerms: false,
|
||||
complianceConfirmed: false
|
||||
}
|
||||
});
|
||||
|
||||
const businessForm = useForm<BusinessFormData>({
|
||||
resolver: zodResolver(businessFormSchema) as Resolver<BusinessFormData>,
|
||||
defaultValues: {
|
||||
email: user?.email || "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
primaryUse: "",
|
||||
industry: "",
|
||||
companyName: "",
|
||||
companyWebsite: "",
|
||||
companyPhoneNumber: "",
|
||||
agreedToTerms: false,
|
||||
complianceConfirmed: false
|
||||
}
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (open) {
|
||||
resetForm();
|
||||
setGeneratedKey(null);
|
||||
setPersonalUseOnly(false);
|
||||
setSelectedTier("starter");
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
function resetForm() {
|
||||
personalForm.reset({
|
||||
email: user?.email || "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
primaryUse: "",
|
||||
country: "",
|
||||
phoneNumber: "",
|
||||
agreedToTerms: false,
|
||||
complianceConfirmed: false
|
||||
});
|
||||
businessForm.reset({
|
||||
email: user?.email || "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
primaryUse: "",
|
||||
industry: "",
|
||||
companyName: "",
|
||||
companyWebsite: "",
|
||||
companyPhoneNumber: "",
|
||||
agreedToTerms: false,
|
||||
complianceConfirmed: false
|
||||
});
|
||||
}
|
||||
|
||||
const tierOptions: StrategyOption<"starter" | "scale">[] = [
|
||||
{
|
||||
id: "starter",
|
||||
title: t("newPricingLicenseForm.tiers.starter.title"),
|
||||
description: t("newPricingLicenseForm.tiers.starter.description")
|
||||
},
|
||||
{
|
||||
id: "scale",
|
||||
title: t("newPricingLicenseForm.tiers.scale.title"),
|
||||
description: t("newPricingLicenseForm.tiers.scale.description")
|
||||
}
|
||||
];
|
||||
|
||||
const submitLicenseRequest = async (
|
||||
payload: Record<string, unknown>
|
||||
): Promise<void> => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// Check if this is a business/enterprise license request
|
||||
if (!personalUseOnly) {
|
||||
const response = await api.put<AxiosResponse<string>>(
|
||||
`/org/${orgId}/license/enterprise`,
|
||||
{ ...payload, tier: TIER_TO_LICENSE_ID[selectedTier] }
|
||||
);
|
||||
|
||||
console.log("Checkout session response:", response.data);
|
||||
const checkoutUrl = response.data.data;
|
||||
if (checkoutUrl) {
|
||||
window.location.href = checkoutUrl;
|
||||
} else {
|
||||
toast({
|
||||
title: "Failed to get checkout URL",
|
||||
description: "Please try again later",
|
||||
variant: "destructive"
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
} else {
|
||||
// Personal license flow
|
||||
const response = await api.put<
|
||||
AxiosResponse<GenerateNewLicenseResponse>
|
||||
>(`/org/${orgId}/license`, payload);
|
||||
|
||||
if (response.data.data?.licenseKey?.licenseKey) {
|
||||
setGeneratedKey(response.data.data.licenseKey.licenseKey);
|
||||
onGenerated?.();
|
||||
toast({
|
||||
title: t("generateLicenseKeyForm.toasts.success.title"),
|
||||
description: t(
|
||||
"generateLicenseKeyForm.toasts.success.description"
|
||||
),
|
||||
variant: "default"
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast({
|
||||
title: t("generateLicenseKeyForm.toasts.error.title"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t("generateLicenseKeyForm.toasts.error.description")
|
||||
),
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const onSubmitPersonal = async (values: PersonalFormData) => {
|
||||
await submitLicenseRequest({
|
||||
email: values.email,
|
||||
useCaseType: "personal",
|
||||
personal: {
|
||||
firstName: values.firstName,
|
||||
lastName: values.lastName,
|
||||
aboutYou: { primaryUse: values.primaryUse },
|
||||
personalInfo: {
|
||||
country: values.country,
|
||||
phoneNumber: values.phoneNumber || ""
|
||||
}
|
||||
},
|
||||
business: undefined,
|
||||
consent: {
|
||||
agreedToTerms: values.agreedToTerms,
|
||||
acknowledgedPrivacyPolicy: values.agreedToTerms,
|
||||
complianceConfirmed: values.complianceConfirmed
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmitBusiness = async (values: BusinessFormData) => {
|
||||
const payload = {
|
||||
email: values.email,
|
||||
useCaseType: "business",
|
||||
personal: undefined,
|
||||
business: {
|
||||
firstName: values.firstName,
|
||||
lastName: values.lastName,
|
||||
jobTitle: "N/A",
|
||||
aboutYou: {
|
||||
primaryUse: values.primaryUse,
|
||||
industry: values.industry,
|
||||
prospectiveUsers: 100,
|
||||
prospectiveSites: 100
|
||||
},
|
||||
companyInfo: {
|
||||
companyName: values.companyName,
|
||||
countryOfResidence: "N/A",
|
||||
stateProvinceRegion: "N/A",
|
||||
postalZipCode: "N/A",
|
||||
companyWebsite: values.companyWebsite || "",
|
||||
companyPhoneNumber: values.companyPhoneNumber || ""
|
||||
}
|
||||
},
|
||||
consent: {
|
||||
agreedToTerms: values.agreedToTerms,
|
||||
acknowledgedPrivacyPolicy: values.agreedToTerms,
|
||||
complianceConfirmed: values.complianceConfirmed
|
||||
}
|
||||
};
|
||||
|
||||
await submitLicenseRequest(payload);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
setGeneratedKey(null);
|
||||
resetForm();
|
||||
};
|
||||
|
||||
return (
|
||||
<Credenza open={open} onOpenChange={handleClose}>
|
||||
<CredenzaContent className="max-w-4xl">
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>
|
||||
{t("newPricingLicenseForm.title")}
|
||||
</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
{t("newPricingLicenseForm.description")}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<div className="space-y-6">
|
||||
{generatedKey ? (
|
||||
<div className="space-y-4">
|
||||
<CopyTextBox
|
||||
text={generatedKey}
|
||||
wrapText={false}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Tier selection - required when not personal use */}
|
||||
{!personalUseOnly && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">
|
||||
{t(
|
||||
"newPricingLicenseForm.chooseTier"
|
||||
)}
|
||||
</label>
|
||||
<StrategySelect
|
||||
options={tierOptions}
|
||||
defaultValue={selectedTier}
|
||||
onChange={(value) =>
|
||||
setSelectedTier(value)
|
||||
}
|
||||
cols={2}
|
||||
/>
|
||||
<a
|
||||
href="https://pangolin.net/pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-primary hover:underline"
|
||||
>
|
||||
{t(
|
||||
"newPricingLicenseForm.viewPricingLink"
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Personal use only checkbox at the bottom of options */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="personal-use-only"
|
||||
checked={personalUseOnly}
|
||||
onCheckedChange={(checked) => {
|
||||
setPersonalUseOnly(
|
||||
checked === true
|
||||
);
|
||||
if (checked) {
|
||||
businessForm.reset();
|
||||
} else {
|
||||
personalForm.reset();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
htmlFor="personal-use-only"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
{t(
|
||||
"newPricingLicenseForm.personalUseOnly"
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* License disclosure - only when personal use */}
|
||||
{personalUseOnly && (
|
||||
<Alert variant="neutral">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle>
|
||||
{t(
|
||||
"generateLicenseKeyForm.alerts.commercialUseDisclosure.title"
|
||||
)}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t(
|
||||
"generateLicenseKeyForm.alerts.commercialUseDisclosure.description"
|
||||
)
|
||||
.split(
|
||||
"Fossorial Commercial License Terms"
|
||||
)
|
||||
.map((part, index) => (
|
||||
<span key={index}>
|
||||
{part}
|
||||
{index === 0 && (
|
||||
<a
|
||||
href="https://pangolin.net/fcl.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Fossorial
|
||||
Commercial
|
||||
License Terms
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Personal form: only when personal use only is checked */}
|
||||
{personalUseOnly && (
|
||||
<Form {...personalForm}>
|
||||
<form
|
||||
onSubmit={personalForm.handleSubmit(
|
||||
onSubmitPersonal
|
||||
)}
|
||||
className="space-y-4"
|
||||
id="new-pricing-license-personal-form"
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={
|
||||
personalForm.control
|
||||
}
|
||||
name="firstName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"generateLicenseKeyForm.form.firstName"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={
|
||||
personalForm.control
|
||||
}
|
||||
name="lastName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"generateLicenseKeyForm.form.lastName"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={personalForm.control}
|
||||
name="primaryUse"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"generateLicenseKeyForm.form.primaryUseQuestion"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={
|
||||
personalForm.control
|
||||
}
|
||||
name="country"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"generateLicenseKeyForm.form.country"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={
|
||||
personalForm.control
|
||||
}
|
||||
name="phoneNumber"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"generateLicenseKeyForm.form.phoneNumberOptional"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 pt-4">
|
||||
<FormField
|
||||
control={
|
||||
personalForm.control
|
||||
}
|
||||
name="agreedToTerms"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={
|
||||
field.value
|
||||
}
|
||||
onCheckedChange={
|
||||
field.onChange
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel className="text-sm font-normal">
|
||||
<div>
|
||||
{t(
|
||||
"signUpTerms.IAgreeToThe"
|
||||
)}{" "}
|
||||
<a
|
||||
href="https://pangolin.net/terms-of-service.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
{t(
|
||||
"signUpTerms.termsOfService"
|
||||
)}{" "}
|
||||
</a>
|
||||
{t(
|
||||
"signUpTerms.and"
|
||||
)}{" "}
|
||||
<a
|
||||
href="https://pangolin.net/privacy-policy.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
{t(
|
||||
"signUpTerms.privacyPolicy"
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
</FormLabel>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={
|
||||
personalForm.control
|
||||
}
|
||||
name="complianceConfirmed"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={
|
||||
field.value
|
||||
}
|
||||
onCheckedChange={
|
||||
field.onChange
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel className="text-sm font-normal">
|
||||
<div>
|
||||
{t(
|
||||
"generateLicenseKeyForm.form.complianceConfirmation"
|
||||
)}{" "}
|
||||
<a
|
||||
href="https://pangolin.net/fcl.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
https://pangolin.net/fcl.html
|
||||
</a>
|
||||
</div>
|
||||
</FormLabel>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
|
||||
{/* Business form: when not personal use - enter business info then continue to checkout */}
|
||||
{!personalUseOnly && (
|
||||
<Form {...businessForm}>
|
||||
<form
|
||||
onSubmit={businessForm.handleSubmit(
|
||||
onSubmitBusiness
|
||||
)}
|
||||
className="space-y-4"
|
||||
id="new-pricing-license-business-form"
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={
|
||||
businessForm.control
|
||||
}
|
||||
name="firstName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"generateLicenseKeyForm.form.firstName"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={
|
||||
businessForm.control
|
||||
}
|
||||
name="lastName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"generateLicenseKeyForm.form.lastName"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={businessForm.control}
|
||||
name="primaryUse"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"generateLicenseKeyForm.form.primaryUseQuestion"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={businessForm.control}
|
||||
name="industry"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"generateLicenseKeyForm.form.industryQuestion"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={businessForm.control}
|
||||
name="companyName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"generateLicenseKeyForm.form.companyName"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={
|
||||
businessForm.control
|
||||
}
|
||||
name="companyWebsite"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"generateLicenseKeyForm.form.companyWebsite"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={
|
||||
businessForm.control
|
||||
}
|
||||
name="companyPhoneNumber"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"generateLicenseKeyForm.form.companyPhoneNumber"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 pt-4">
|
||||
<FormField
|
||||
control={
|
||||
businessForm.control
|
||||
}
|
||||
name="agreedToTerms"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={
|
||||
field.value
|
||||
}
|
||||
onCheckedChange={
|
||||
field.onChange
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel className="text-sm font-normal">
|
||||
<div>
|
||||
{t(
|
||||
"signUpTerms.IAgreeToThe"
|
||||
)}{" "}
|
||||
<a
|
||||
href="https://pangolin.net/terms-of-service.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
{t(
|
||||
"signUpTerms.termsOfService"
|
||||
)}{" "}
|
||||
</a>
|
||||
{t(
|
||||
"signUpTerms.and"
|
||||
)}{" "}
|
||||
<a
|
||||
href="https://pangolin.net/privacy-policy.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
{t(
|
||||
"signUpTerms.privacyPolicy"
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
</FormLabel>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={
|
||||
businessForm.control
|
||||
}
|
||||
name="complianceConfirmed"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={
|
||||
field.value
|
||||
}
|
||||
onCheckedChange={
|
||||
field.onChange
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel className="text-sm font-normal">
|
||||
<div>
|
||||
{t(
|
||||
"generateLicenseKeyForm.form.complianceConfirmation"
|
||||
)}{" "}
|
||||
<a
|
||||
href="https://pangolin.net/fcl.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
https://pangolin.net/fcl.html
|
||||
</a>
|
||||
</div>
|
||||
</FormLabel>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">
|
||||
{t("generateLicenseKeyForm.buttons.close")}
|
||||
</Button>
|
||||
</CredenzaClose>
|
||||
|
||||
{!generatedKey && personalUseOnly && (
|
||||
<Button
|
||||
type="submit"
|
||||
form="new-pricing-license-personal-form"
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
>
|
||||
{t(
|
||||
"generateLicenseKeyForm.buttons.generateLicenseKey"
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{!generatedKey && !personalUseOnly && (
|
||||
<Button
|
||||
type="submit"
|
||||
form="new-pricing-license-business-form"
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
>
|
||||
{t(
|
||||
"newPricingLicenseForm.buttons.continueToCheckout"
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
);
|
||||
}
|
||||
24
src/components/RedirectToOrg.tsx
Normal file
24
src/components/RedirectToOrg.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { getInternalRedirectTarget } from "@app/lib/internalRedirect";
|
||||
|
||||
type RedirectToOrgProps = {
|
||||
targetOrgId: string;
|
||||
};
|
||||
|
||||
export default function RedirectToOrg({ targetOrgId }: RedirectToOrgProps) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const target = getInternalRedirectTarget(targetOrgId);
|
||||
router.replace(target);
|
||||
} catch {
|
||||
router.replace(`/${targetOrgId}`);
|
||||
}
|
||||
}, [targetOrgId, router]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import * as z from "zod";
|
||||
@@ -42,6 +42,7 @@ const isValidEmail = (str: string): boolean => {
|
||||
type SmartLoginFormProps = {
|
||||
redirect?: string;
|
||||
forceLogin?: boolean;
|
||||
defaultUser?: string;
|
||||
};
|
||||
|
||||
type ViewState =
|
||||
@@ -59,7 +60,8 @@ type ViewState =
|
||||
|
||||
export default function SmartLoginForm({
|
||||
redirect,
|
||||
forceLogin
|
||||
forceLogin,
|
||||
defaultUser
|
||||
}: SmartLoginFormProps) {
|
||||
const router = useRouter();
|
||||
const { lookup, loading, error } = useUserLookup();
|
||||
@@ -72,10 +74,18 @@ export default function SmartLoginForm({
|
||||
const form = useForm<z.infer<typeof identifierSchema>>({
|
||||
resolver: zodResolver(identifierSchema),
|
||||
defaultValues: {
|
||||
identifier: ""
|
||||
identifier: defaultUser ?? ""
|
||||
}
|
||||
});
|
||||
|
||||
const hasAutoLookedUp = useRef(false);
|
||||
useEffect(() => {
|
||||
if (defaultUser?.trim() && !hasAutoLookedUp.current) {
|
||||
hasAutoLookedUp.current = true;
|
||||
void handleLookup({ identifier: defaultUser.trim() });
|
||||
}
|
||||
}, [defaultUser]);
|
||||
|
||||
const handleLookup = async (values: z.infer<typeof identifierSchema>) => {
|
||||
const identifier = values.identifier.trim();
|
||||
const isEmail = isValidEmail(identifier);
|
||||
|
||||
27
src/components/StoreInternalRedirect.tsx
Normal file
27
src/components/StoreInternalRedirect.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { INTERNAL_REDIRECT_KEY } from "@app/lib/internalRedirect";
|
||||
|
||||
const TTL_MS = 10 * 60 * 1000; // 10 minutes
|
||||
|
||||
export default function StoreInternalRedirect() {
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const value = params.get("internal_redirect");
|
||||
if (value != null && value !== "") {
|
||||
try {
|
||||
const payload = JSON.stringify({
|
||||
path: value,
|
||||
expiresAt: Date.now() + TTL_MS
|
||||
});
|
||||
window.localStorage.setItem(INTERNAL_REDIRECT_KEY, payload);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -226,6 +226,21 @@ export default function SupporterStatus({
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<div className="my-4 p-4 border border-blue-500/50 bg-blue-500/10 rounded-lg">
|
||||
<p className="text-sm">
|
||||
<strong>Business & Enterprise Users:</strong> For larger organizations or teams requiring advanced features, consider our self-serve enterprise license and Enterprise Edition.{" "}
|
||||
<Link
|
||||
href="https://pangolin.net/pricing?hosting=self-host"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline inline-flex items-center gap-1"
|
||||
>
|
||||
Learn more
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="py-6">
|
||||
<p className="mb-3 text-center">
|
||||
{t("supportKeyOptions")}
|
||||
|
||||
@@ -1,41 +1,13 @@
|
||||
"use client";
|
||||
import * as React from "react";
|
||||
import * as NProgress from "nprogress";
|
||||
|
||||
import NextTopLoader from "nextjs-toploader";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
export function TopLoader() {
|
||||
return (
|
||||
<>
|
||||
<NextTopLoader showSpinner={false} color="var(--color-primary)" />
|
||||
<FinishingLoader />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function FinishingLoader() {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
React.useEffect(() => {
|
||||
NProgress.done();
|
||||
}, [pathname, router, searchParams]);
|
||||
React.useEffect(() => {
|
||||
const linkClickListener = (ev: MouseEvent) => {
|
||||
const element = ev.target as HTMLElement;
|
||||
const closestlink = element.closest("a");
|
||||
const isOpenToNewTabClick =
|
||||
ev.ctrlKey ||
|
||||
ev.shiftKey ||
|
||||
ev.metaKey || // apple
|
||||
(ev.button && ev.button == 1); // middle click, >IE9 + everyone else
|
||||
|
||||
if (closestlink && isOpenToNewTabClick) {
|
||||
NProgress.done();
|
||||
}
|
||||
};
|
||||
window.addEventListener("click", linkClickListener);
|
||||
return () => window.removeEventListener("click", linkClickListener);
|
||||
}, []);
|
||||
return null;
|
||||
return (
|
||||
<NextTopLoader
|
||||
color="var(--color-primary)"
|
||||
showSpinner={false}
|
||||
height={2}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ export function NewtSiteInstallCommands({
|
||||
- NEWT_SECRET=${secret}${acceptClientsEnv}`
|
||||
],
|
||||
"Docker Run": [
|
||||
`docker run -dit fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
`docker run -dit --network host fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
]
|
||||
},
|
||||
kubernetes: {
|
||||
|
||||
Reference in New Issue
Block a user