dasdas
This commit is contained in:
@@ -3,10 +3,22 @@ import GridShape from "../../components/common/GridShape";
|
||||
import { Link } from "react-router-dom";
|
||||
import ThemeTogglerTwo from "../../components/common/ThemeTogglerTwo";
|
||||
|
||||
interface PlanPreview {
|
||||
name?: string;
|
||||
price?: number | string;
|
||||
billing_cycle?: string;
|
||||
included_credits?: number;
|
||||
max_sites?: number;
|
||||
max_users?: number;
|
||||
features?: string[];
|
||||
}
|
||||
|
||||
export default function AuthLayout({
|
||||
children,
|
||||
plan,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
plan?: PlanPreview | null;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative p-6 bg-white z-1 dark:bg-gray-900 sm:p-0">
|
||||
@@ -16,18 +28,52 @@ export default function AuthLayout({
|
||||
<div className="relative flex items-center justify-center z-1">
|
||||
{/* <!-- ===== Common Grid Shape Start ===== --> */}
|
||||
<GridShape />
|
||||
<div className="flex flex-col items-center max-w-xs">
|
||||
<Link to="/" className="block mb-4">
|
||||
<img
|
||||
width={231}
|
||||
height={48}
|
||||
src="/images/logo/auth-logo.svg"
|
||||
alt="Logo"
|
||||
/>
|
||||
</Link>
|
||||
<p className="text-center text-gray-400 dark:text-white/60">
|
||||
Free and Open-Source Tailwind CSS Admin Dashboard Template
|
||||
</p>
|
||||
<div className="flex flex-col items-center max-w-sm w-full gap-6 px-4">
|
||||
<div className="flex flex-col items-center">
|
||||
<Link to="/" className="block mb-4">
|
||||
<img
|
||||
width={231}
|
||||
height={48}
|
||||
src="/images/logo/auth-logo.svg"
|
||||
alt="Logo"
|
||||
/>
|
||||
</Link>
|
||||
<p className="text-center text-gray-400 dark:text-white/60">
|
||||
Free and Open-Source Tailwind CSS Admin Dashboard Template
|
||||
</p>
|
||||
</div>
|
||||
{plan && (
|
||||
<div className="w-full rounded-2xl border border-white/10 bg-white/5 backdrop-blur-sm p-5 text-white shadow-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-lg font-semibold">{plan.name}</div>
|
||||
{plan.price !== undefined && (
|
||||
<div className="text-sm font-medium">
|
||||
${Number(plan.price).toFixed(2)}
|
||||
{plan.billing_cycle ? `/ ${plan.billing_cycle}` : ""}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-white/80 space-y-1">
|
||||
{plan.included_credits !== undefined && (
|
||||
<div>Credits: {plan.included_credits}</div>
|
||||
)}
|
||||
{(plan.max_sites !== undefined || plan.max_users !== undefined) && (
|
||||
<div>
|
||||
{plan.max_sites !== undefined ? `Sites: ${plan.max_sites}` : ""}
|
||||
{plan.max_sites !== undefined && plan.max_users !== undefined ? " • " : ""}
|
||||
{plan.max_users !== undefined ? `Users: ${plan.max_users}` : ""}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{Array.isArray(plan.features) && plan.features.length > 0 && (
|
||||
<ul className="mt-3 space-y-1 text-xs text-white/85 list-disc list-inside">
|
||||
{plan.features.slice(0, 7).map((f) => (
|
||||
<li key={f}>{f}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,50 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import PageMeta from "../../components/common/PageMeta";
|
||||
import AuthLayout from "./AuthPageLayout";
|
||||
import SignUpForm from "../../components/auth/SignUpForm";
|
||||
|
||||
export default function SignUp() {
|
||||
const planSlug = useMemo(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get("plan") || "";
|
||||
}, []);
|
||||
|
||||
const [planDetails, setPlanDetails] = useState<any | null>(null);
|
||||
const [planLoading, setPlanLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPlans = async () => {
|
||||
if (!planSlug) return;
|
||||
setPlanLoading(true);
|
||||
try {
|
||||
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || "https://api.igny8.com/api";
|
||||
const res = await fetch(`${API_BASE_URL}/v1/auth/plans/`);
|
||||
const data = await res.json();
|
||||
const plans = data?.results || [];
|
||||
const plan = plans.find((p: any) => p.slug === planSlug);
|
||||
if (plan) {
|
||||
const features = Array.isArray(plan.features)
|
||||
? plan.features.map((f: string) => f.charAt(0).toUpperCase() + f.slice(1))
|
||||
: [];
|
||||
setPlanDetails({ ...plan, features });
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore; SignUpForm will handle lack of plan data gracefully
|
||||
} finally {
|
||||
setPlanLoading(false);
|
||||
}
|
||||
};
|
||||
fetchPlans();
|
||||
}, [planSlug]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title="React.js SignUp Dashboard | TailAdmin - Next.js Admin Dashboard Template"
|
||||
description="This is React.js SignUp Tables Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
|
||||
/>
|
||||
<AuthLayout>
|
||||
<SignUpForm />
|
||||
<AuthLayout plan={planDetails}>
|
||||
<SignUpForm planDetails={planDetails} planLoading={planLoading} />
|
||||
</AuthLayout>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useLocation, useNavigate, Link } from "react-router-dom";
|
||||
import { useAuthStore } from "../store/authStore";
|
||||
|
||||
const PLAN_COPY: Record<string, { name: string; price: string; credits: string }> = {
|
||||
starter: { name: "Starter", price: "$89/mo", credits: "1,000 credits/month" },
|
||||
@@ -10,24 +11,31 @@ const PLAN_COPY: Record<string, { name: string; price: string; credits: string }
|
||||
export default function Payment() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const [contactEmail, setContactEmail] = useState("");
|
||||
const [note, setNote] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const planSlug = useMemo(() => new URLSearchParams(location.search).get("plan") || "", [location.search]);
|
||||
const plan = planSlug ? PLAN_COPY[planSlug] : null;
|
||||
const plan = useMemo(() => {
|
||||
const slugFromAccount = user?.account?.plan?.slug;
|
||||
const slug = planSlug || slugFromAccount || "";
|
||||
return slug ? PLAN_COPY[slug] : null;
|
||||
}, [planSlug, user?.account?.plan?.slug]);
|
||||
const mailtoHref = useMemo(() => {
|
||||
if (!plan || !contactEmail.trim()) return "";
|
||||
const subject = encodeURIComponent(`Subscribe to ${plan.name}`);
|
||||
const body = encodeURIComponent(`Plan: ${plan.name}\nEmail: ${contactEmail}\nNotes: ${note || "-"}`);
|
||||
const subject = encodeURIComponent(`Payment submitted for ${plan.name}`);
|
||||
const body = encodeURIComponent(
|
||||
`Plan: ${plan.name}\nAccount: ${user?.account?.slug || user?.account?.id || "-"}\nEmail: ${contactEmail}\nNotes/Reference: ${note || "-"}`
|
||||
);
|
||||
return `mailto:sales@igny8.com?subject=${subject}&body=${body}`;
|
||||
}, [plan, contactEmail, note]);
|
||||
}, [plan, contactEmail, note, user?.account?.slug, user?.account?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!plan) {
|
||||
if (!plan || !user) {
|
||||
navigate("/pricing", { replace: true });
|
||||
}
|
||||
}, [plan, navigate]);
|
||||
}, [plan, navigate, user]);
|
||||
|
||||
const handleRequest = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
if (!plan) {
|
||||
@@ -62,7 +70,7 @@ export default function Payment() {
|
||||
<p className="text-slate-700">{plan.price}</p>
|
||||
<p className="text-sm text-slate-600">{plan.credits}</p>
|
||||
<p className="text-xs text-amber-700 mt-2">
|
||||
Payment is completed offline (bank transfer). Submit your email below and we will send payment instructions.
|
||||
Payment is completed offline (bank transfer). Submit your email and reference below; we will verify and activate your account.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user