615 lines
24 KiB
TypeScript
615 lines
24 KiB
TypeScript
"use client";
|
||
|
||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||
import Link from "next/link";
|
||
import { useRouter, useSearchParams } from "next/navigation";
|
||
import { Badge, Button, Container } from "@/components/ui";
|
||
import { useAuth } from "@/hooks/useAuth";
|
||
import {
|
||
loadAssessmentSchema,
|
||
loadCommercialBundles,
|
||
loadCommercialPricing,
|
||
loadCommercialProducts,
|
||
requestRecommendations,
|
||
} from "../adapters";
|
||
import type {
|
||
AdapterResult,
|
||
AssessmentQuestion,
|
||
AssessmentSchema,
|
||
CommercialBundle,
|
||
CommercialPricingItem,
|
||
CommercialProduct,
|
||
RecommendationResult,
|
||
} from "../types";
|
||
import {
|
||
BundleCard,
|
||
CommercialEmptyState,
|
||
ProductCard,
|
||
} from "../components";
|
||
import {
|
||
loadAssessmentSession,
|
||
saveAssessmentSession,
|
||
type AssessmentSession,
|
||
} from "../session";
|
||
|
||
type WizardStep =
|
||
| "assessment"
|
||
| "recommendations"
|
||
| "bundles"
|
||
| "pricing"
|
||
| "trial_summary"
|
||
| "create_workspace";
|
||
|
||
function QuestionField({
|
||
question,
|
||
value,
|
||
onChange,
|
||
}: {
|
||
question: AssessmentQuestion;
|
||
value: unknown;
|
||
onChange: (v: unknown) => void;
|
||
}) {
|
||
const inputClass =
|
||
"mt-1 w-full rounded-xl border border-gray-200 bg-white px-4 py-2.5 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary dark:border-gray-700 dark:bg-gray-900";
|
||
|
||
if (question.input_type === "boolean") {
|
||
return (
|
||
<label className="flex items-center gap-2 text-sm">
|
||
<input
|
||
type="checkbox"
|
||
checked={Boolean(value)}
|
||
onChange={(e) => onChange(e.target.checked)}
|
||
/>
|
||
{question.prompt}
|
||
</label>
|
||
);
|
||
}
|
||
|
||
if (question.input_type === "number") {
|
||
return (
|
||
<label className="block text-sm font-medium text-secondary dark:text-gray-100">
|
||
{question.prompt}
|
||
<input
|
||
type="number"
|
||
className={inputClass}
|
||
value={typeof value === "number" || typeof value === "string" ? value : ""}
|
||
onChange={(e) => onChange(e.target.value === "" ? null : Number(e.target.value))}
|
||
/>
|
||
</label>
|
||
);
|
||
}
|
||
|
||
if ((question.input_type === "multi" || question.input_type === "single") && question.options?.length) {
|
||
const multi = question.input_type === "multi";
|
||
const selected = multi ? (Array.isArray(value) ? (value as string[]) : []) : null;
|
||
return (
|
||
<fieldset>
|
||
<legend className="text-sm font-medium text-secondary dark:text-gray-100">
|
||
{question.prompt}
|
||
</legend>
|
||
<div className="mt-3 grid gap-2 sm:grid-cols-2">
|
||
{question.options.map((opt) => {
|
||
const on = multi ? selected!.includes(opt.value) : value === opt.value;
|
||
return (
|
||
<button
|
||
key={opt.value}
|
||
type="button"
|
||
className={`rounded-2xl border p-4 text-right text-sm transition ${
|
||
on
|
||
? "border-primary bg-[var(--color-primary-soft)] shadow-sm"
|
||
: "border-gray-200 bg-white hover:border-gray-300 dark:border-gray-700 dark:bg-gray-900"
|
||
}`}
|
||
onClick={() => {
|
||
if (multi) {
|
||
onChange(
|
||
on ? selected!.filter((v) => v !== opt.value) : [...selected!, opt.value]
|
||
);
|
||
} else onChange(opt.value);
|
||
}}
|
||
>
|
||
{opt.label}
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</fieldset>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<label className="block text-sm font-medium text-secondary dark:text-gray-100">
|
||
{question.prompt}
|
||
<input
|
||
className={inputClass}
|
||
value={typeof value === "string" ? value : ""}
|
||
onChange={(e) => onChange(e.target.value)}
|
||
/>
|
||
</label>
|
||
);
|
||
}
|
||
|
||
const STEPS: { id: WizardStep; label: string }[] = [
|
||
{ id: "assessment", label: "کشف" },
|
||
{ id: "recommendations", label: "پیشنهاد" },
|
||
{ id: "bundles", label: "باندل" },
|
||
{ id: "pricing", label: "قیمت" },
|
||
{ id: "trial_summary", label: "Trial" },
|
||
];
|
||
|
||
export function CommercialDiscoverExperience() {
|
||
const { isAuthenticated, login, loading: authLoading } = useAuth();
|
||
const router = useRouter();
|
||
const searchParams = useSearchParams();
|
||
const typeParam = searchParams.get("type");
|
||
|
||
const [step, setStep] = useState<WizardStep>("assessment");
|
||
const [schemaRes, setSchemaRes] = useState<AdapterResult<AssessmentSchema> | null>(null);
|
||
const [productsRes, setProductsRes] = useState<AdapterResult<CommercialProduct[]> | null>(null);
|
||
const [bundlesRes, setBundlesRes] = useState<AdapterResult<CommercialBundle[]> | null>(null);
|
||
const [pricingRes, setPricingRes] = useState<AdapterResult<CommercialPricingItem[]> | null>(null);
|
||
const [recoRes, setRecoRes] = useState<AdapterResult<RecommendationResult> | null>(null);
|
||
const [answers, setAnswers] = useState<Record<string, unknown>>({});
|
||
const [selectedBundle, setSelectedBundle] = useState<string | null>(null);
|
||
const [qIndex, setQIndex] = useState(0);
|
||
const [busy, setBusy] = useState(false);
|
||
const [booted, setBooted] = useState(false);
|
||
|
||
useEffect(() => {
|
||
const session = loadAssessmentSession();
|
||
const nextAnswers = { ...session.answers };
|
||
if (typeParam && !nextAnswers.business_type_code) {
|
||
nextAnswers.business_type_code = typeParam;
|
||
}
|
||
setAnswers(nextAnswers);
|
||
if (session.selected_bundle_code) setSelectedBundle(session.selected_bundle_code);
|
||
void (async () => {
|
||
setBusy(true);
|
||
const [schema, products, bundles, pricing] = await Promise.all([
|
||
loadAssessmentSchema(),
|
||
loadCommercialProducts(),
|
||
loadCommercialBundles(),
|
||
loadCommercialPricing(),
|
||
]);
|
||
setSchemaRes(schema);
|
||
setProductsRes(products);
|
||
setBundlesRes(bundles);
|
||
setPricingRes(pricing);
|
||
setBusy(false);
|
||
setBooted(true);
|
||
})();
|
||
}, [typeParam]);
|
||
|
||
const persist = useCallback((next: AssessmentSession) => {
|
||
saveAssessmentSession(next);
|
||
}, []);
|
||
|
||
const questions = schemaRes?.data.questions ?? [];
|
||
const currentQ = questions[qIndex];
|
||
|
||
const runRecommendations = async () => {
|
||
setBusy(true);
|
||
persist({ answers, selected_bundle_code: selectedBundle });
|
||
const reco = await requestRecommendations(answers);
|
||
setRecoRes(reco);
|
||
const top = reco.data.recommended_bundles[0]?.code;
|
||
if (top && !selectedBundle) {
|
||
setSelectedBundle(top);
|
||
persist({ answers, selected_bundle_code: top });
|
||
}
|
||
setStep("recommendations");
|
||
setBusy(false);
|
||
};
|
||
|
||
const productByCode = useMemo(() => {
|
||
const map = new Map<string, CommercialProduct>();
|
||
(productsRes?.data ?? []).forEach((p) => map.set(p.product_code, p));
|
||
return map;
|
||
}, [productsRes]);
|
||
|
||
const bundleByCode = useMemo(() => {
|
||
const map = new Map<string, CommercialBundle>();
|
||
(bundlesRes?.data ?? []).forEach((b) => map.set(b.bundle_code, b));
|
||
return map;
|
||
}, [bundlesRes]);
|
||
|
||
const recommendedProducts = (recoRes?.data.recommended_products ?? [])
|
||
.map((r) => ({ ...r, product: productByCode.get(r.code) }))
|
||
.filter((r) => r.product);
|
||
|
||
const optionalProducts = (productsRes?.data ?? []).filter(
|
||
(p) => !recommendedProducts.some((r) => r.code === p.product_code) && p.visibility !== "hidden"
|
||
);
|
||
|
||
const recommendedBundles = (recoRes?.data.recommended_bundles ?? [])
|
||
.map((r) => ({ ...r, bundle: bundleByCode.get(r.code) }))
|
||
.filter((r) => r.bundle);
|
||
|
||
const displayBundles =
|
||
recommendedBundles.length > 0
|
||
? recommendedBundles.map((r) => r.bundle!)
|
||
: bundlesRes?.data ?? [];
|
||
|
||
const selectedBundleObj = selectedBundle ? bundleByCode.get(selectedBundle) : null;
|
||
const pricingForBundle = (pricingRes?.data ?? []).filter((p) =>
|
||
selectedBundleObj?.pricing_refs?.includes(p.pricing_item_code)
|
||
);
|
||
|
||
const goWorkspace = () => {
|
||
persist({
|
||
answers,
|
||
selected_bundle_code: selectedBundle,
|
||
selected_product_codes: recoRes?.data.recommended_products.map((p) => p.code),
|
||
activate_trial: true,
|
||
});
|
||
if (isAuthenticated) {
|
||
const intent =
|
||
typeof answers.business_type_code === "string" ? answers.business_type_code : "";
|
||
router.push(intent ? `/onboarding?intent=${encodeURIComponent(intent)}` : "/onboarding");
|
||
} else {
|
||
setStep("create_workspace");
|
||
}
|
||
};
|
||
|
||
const stepIndex = STEPS.findIndex((s) => s.id === step);
|
||
|
||
if (!booted) {
|
||
return (
|
||
<Container className="py-24 text-center">
|
||
<div className="mx-auto h-10 w-10 animate-pulse rounded-full bg-[var(--color-primary-muted)]" />
|
||
<p className="mt-4 text-sm text-gray-500">آمادهسازی مسیر کشف…</p>
|
||
</Container>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-[70vh] bg-[var(--canvas)] pb-16 dark:bg-gray-950">
|
||
<div className="border-b border-gray-100 bg-white dark:border-gray-800 dark:bg-gray-950">
|
||
<Container className="py-6">
|
||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||
<div>
|
||
<Link href="/" className="text-xs text-primary">
|
||
← بازگشت به خانه
|
||
</Link>
|
||
<h1 className="mt-2 text-2xl font-bold text-secondary dark:text-gray-100">
|
||
کشف و راهاندازی
|
||
</h1>
|
||
</div>
|
||
<ol className="flex flex-wrap gap-2">
|
||
{STEPS.map((s, i) => (
|
||
<li
|
||
key={s.id}
|
||
className={`rounded-full px-3 py-1 text-xs font-medium ${
|
||
i <= stepIndex
|
||
? "bg-primary text-white"
|
||
: "bg-gray-100 text-gray-500 dark:bg-gray-800"
|
||
}`}
|
||
>
|
||
{s.label}
|
||
</li>
|
||
))}
|
||
</ol>
|
||
</div>
|
||
</Container>
|
||
</div>
|
||
|
||
{step === "assessment" ? (
|
||
<Container className="py-10">
|
||
<div className="mx-auto max-w-xl">
|
||
<h2 className="text-xl font-bold text-secondary dark:text-gray-100">ارزیابی نیازها</h2>
|
||
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||
سؤالات از assessment schema — صنایع و نیازها بدون لیست ثابت frontend.
|
||
</p>
|
||
|
||
{schemaRes?.availability === "unavailable" ||
|
||
schemaRes?.availability === "empty" ||
|
||
!questions.length ? (
|
||
<div className="mt-6 space-y-4">
|
||
<CommercialEmptyState
|
||
title="اسکیمای ارزیابی در دسترس نیست"
|
||
description={schemaRes?.message}
|
||
/>
|
||
<div className="flex flex-wrap gap-2">
|
||
<Button
|
||
loading={busy}
|
||
onClick={() => {
|
||
setRecoRes({
|
||
availability: "unavailable",
|
||
data: {
|
||
recommended_products: [],
|
||
recommended_bundles: [],
|
||
recommended_capabilities: [],
|
||
recommended_pricing: [],
|
||
},
|
||
message: schemaRes?.message,
|
||
source: "none",
|
||
});
|
||
setStep("recommendations");
|
||
}}
|
||
>
|
||
ادامه با کاتالوگ رجیستری
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
) : currentQ ? (
|
||
<div className="mt-6 space-y-4 rounded-3xl border border-gray-100 bg-white p-6 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||
<div className="h-1.5 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-800">
|
||
<div
|
||
className="h-full rounded-full bg-primary transition-all"
|
||
style={{ width: `${((qIndex + 1) / questions.length) * 100}%` }}
|
||
/>
|
||
</div>
|
||
<p className="text-xs text-gray-400">
|
||
سؤال {qIndex + 1} از {questions.length}
|
||
</p>
|
||
<QuestionField
|
||
question={currentQ}
|
||
value={answers[currentQ.question_id]}
|
||
onChange={(v) => {
|
||
const next = { ...answers, [currentQ.question_id]: v };
|
||
setAnswers(next);
|
||
persist({ answers: next, selected_bundle_code: selectedBundle });
|
||
}}
|
||
/>
|
||
<div className="flex flex-wrap gap-2 pt-2">
|
||
<Button
|
||
variant="outline"
|
||
disabled={qIndex === 0}
|
||
onClick={() => setQIndex((i) => i - 1)}
|
||
>
|
||
قبلی
|
||
</Button>
|
||
{qIndex < questions.length - 1 ? (
|
||
<Button
|
||
onClick={() => setQIndex((i) => i + 1)}
|
||
disabled={currentQ.required && answers[currentQ.question_id] == null}
|
||
>
|
||
بعدی
|
||
</Button>
|
||
) : (
|
||
<Button loading={busy} onClick={() => void runRecommendations()}>
|
||
دریافت پیشنهاد
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
</Container>
|
||
) : null}
|
||
|
||
{step === "recommendations" ? (
|
||
<Container className="py-10">
|
||
<div className="mx-auto max-w-4xl space-y-10">
|
||
<div>
|
||
<h2 className="text-2xl font-bold text-secondary dark:text-gray-100">
|
||
پیشنهاد شخصیسازیشده
|
||
</h2>
|
||
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||
محصولات الزامی، اختیاری و مسیر رشد از recommendation + registry.
|
||
</p>
|
||
{recoRes?.message ? (
|
||
<p className="mt-3 rounded-xl bg-amber-50 px-3 py-2 text-xs text-amber-900 dark:bg-amber-950/40 dark:text-amber-200">
|
||
{recoRes.message}
|
||
</p>
|
||
) : null}
|
||
{recoRes?.data.rule_trace?.length ? (
|
||
<details className="mt-4 rounded-xl border border-gray-100 bg-white p-4 dark:border-gray-800 dark:bg-gray-950">
|
||
<summary className="cursor-pointer text-sm font-semibold text-secondary dark:text-gray-100">
|
||
چرا این پیشنهاد؟
|
||
</summary>
|
||
<ol className="mt-3 list-decimal space-y-1 ps-5 text-xs text-gray-600 dark:text-gray-400">
|
||
{recoRes.data.rule_trace.map((step, i) => (
|
||
<li key={`${i}-${step}`}>{step}</li>
|
||
))}
|
||
</ol>
|
||
</details>
|
||
) : null}
|
||
</div>
|
||
|
||
<section>
|
||
<h3 className="mb-3 text-sm font-semibold text-secondary dark:text-gray-100">
|
||
محصولات پیشنهادی / الزامی
|
||
</h3>
|
||
{recommendedProducts.length ? (
|
||
<div className="grid gap-3 sm:grid-cols-2">
|
||
{recommendedProducts.map((r) => (
|
||
<div key={r.code} className="space-y-1">
|
||
<ProductCard
|
||
product={r.product!}
|
||
recommendationScore={r.score}
|
||
/>
|
||
{r.reason ? (
|
||
<p className="px-1 text-xs text-gray-500">{r.reason}</p>
|
||
) : null}
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<CommercialEmptyState
|
||
title="پیشنهاد محصول خالی است"
|
||
description={productsRes?.message || recoRes?.message}
|
||
hint="پاسخهای ارزیابی را بررسی کنید یا بعداً وقتی رجیستری پر شد دوباره امتحان کنید."
|
||
actionHref="/discover"
|
||
actionLabel="تکرار ارزیابی"
|
||
secondaryHref="/pricing"
|
||
secondaryLabel="مشاهده قیمتها"
|
||
/>
|
||
)}
|
||
</section>
|
||
|
||
{optionalProducts.length ? (
|
||
<section>
|
||
<h3 className="mb-3 text-sm font-semibold text-secondary dark:text-gray-100">
|
||
محصولات اختیاری / قابل گسترش
|
||
</h3>
|
||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||
{optionalProducts.slice(0, 6).map((p) => (
|
||
<ProductCard key={p.product_code} product={p} />
|
||
))}
|
||
</div>
|
||
</section>
|
||
) : null}
|
||
|
||
{selectedBundleObj ? (
|
||
<div className="rounded-2xl border border-primary/30 bg-[var(--color-primary-soft)] p-4 text-sm">
|
||
<p className="font-semibold text-secondary">باندل پیشنهادی فعلی</p>
|
||
<p className="mt-1">{selectedBundleObj.display_name}</p>
|
||
{selectedBundleObj.upgrade_path_code ? (
|
||
<p className="mt-1 text-xs text-primary">
|
||
مسیر رشد: {selectedBundleObj.upgrade_path_code}
|
||
</p>
|
||
) : null}
|
||
</div>
|
||
) : null}
|
||
|
||
<div className="flex flex-wrap gap-2">
|
||
<Button variant="outline" onClick={() => setStep("assessment")}>
|
||
بازگشت
|
||
</Button>
|
||
<Button onClick={() => setStep("bundles")}>مقایسه باندل</Button>
|
||
</div>
|
||
</div>
|
||
</Container>
|
||
) : null}
|
||
|
||
{step === "bundles" ? (
|
||
<Container className="py-10">
|
||
<div className="mx-auto max-w-4xl space-y-6">
|
||
<h2 className="text-2xl font-bold text-secondary dark:text-gray-100">مقایسه باندل</h2>
|
||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||
Starter / Professional / Enterprise از metadata باندل — بدون پلن ثابت.
|
||
</p>
|
||
{displayBundles.length ? (
|
||
<div className="grid gap-4 lg:grid-cols-2">
|
||
{displayBundles.map((b) => (
|
||
<BundleCard
|
||
key={b.bundle_code}
|
||
bundle={b}
|
||
recommendationScore={
|
||
recoRes?.data.recommended_bundles.find((r) => r.code === b.bundle_code)?.score
|
||
}
|
||
selected={selectedBundle === b.bundle_code}
|
||
onSelect={() => {
|
||
setSelectedBundle(b.bundle_code);
|
||
persist({ answers, selected_bundle_code: b.bundle_code });
|
||
}}
|
||
/>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<CommercialEmptyState title="باندلی نیست" description={bundlesRes?.message} />
|
||
)}
|
||
<div className="flex flex-wrap gap-2">
|
||
<Button variant="outline" onClick={() => setStep("recommendations")}>
|
||
بازگشت
|
||
</Button>
|
||
<Button onClick={() => setStep("pricing")}>مقایسه قیمت</Button>
|
||
</div>
|
||
</div>
|
||
</Container>
|
||
) : null}
|
||
|
||
{step === "pricing" ? (
|
||
<Container className="py-10">
|
||
<div className="mx-auto max-w-3xl space-y-6">
|
||
<h2 className="text-2xl font-bold text-secondary dark:text-gray-100">مقایسه قیمت</h2>
|
||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||
آیتمهای قیمت از رجیستری؛ تخفیف و پروموشن از discount_refs / metadata.
|
||
</p>
|
||
{(pricingForBundle.length ? pricingForBundle : pricingRes?.data ?? []).length ? (
|
||
<ul className="grid gap-3 sm:grid-cols-2">
|
||
{(pricingForBundle.length ? pricingForBundle : pricingRes!.data).map((item) => (
|
||
<li
|
||
key={item.pricing_item_code}
|
||
className="rounded-2xl border border-gray-100 bg-white p-4 dark:border-gray-800 dark:bg-gray-900"
|
||
>
|
||
<p className="font-semibold text-secondary dark:text-gray-100">
|
||
{item.display_name}
|
||
</p>
|
||
<p className="mt-1 text-xs text-gray-500">
|
||
{item.pricing_model || "—"} ·{" "}
|
||
{item.amount_minor != null
|
||
? `${item.amount_minor} ${item.currency || ""}`
|
||
: "مبلغ از بکاند"}
|
||
</p>
|
||
{item.trial_days != null ? (
|
||
<Badge variant="default" className="mt-2">
|
||
Trial {item.trial_days} روز
|
||
</Badge>
|
||
) : null}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
) : (
|
||
<CommercialEmptyState title="قیمتگذاری خالی است" description={pricingRes?.message} />
|
||
)}
|
||
<div className="flex flex-wrap gap-2">
|
||
<Button variant="outline" onClick={() => setStep("bundles")}>
|
||
بازگشت
|
||
</Button>
|
||
<Button onClick={() => setStep("trial_summary")}>خلاصه Trial</Button>
|
||
</div>
|
||
</div>
|
||
</Container>
|
||
) : null}
|
||
|
||
{step === "trial_summary" ? (
|
||
<Container className="py-10">
|
||
<div className="mx-auto max-w-xl space-y-4 rounded-3xl border border-gray-100 bg-white p-8 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||
<h2 className="text-xl font-bold text-secondary dark:text-gray-100">خلاصه Trial</h2>
|
||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||
پس از ثبتنام و ساخت workspace، trial از Billing / commercial subscriptions فعال
|
||
میشود — بدون اعداد ساختگی در این صفحه.
|
||
</p>
|
||
{selectedBundle ? (
|
||
<p className="text-sm">
|
||
باندل: <span className="font-semibold">{selectedBundle}</span>
|
||
</p>
|
||
) : (
|
||
<p className="text-xs text-gray-500">باندل انتخاب نشده — میتوانید بعداً در Billing انتخاب کنید.</p>
|
||
)}
|
||
<ul className="space-y-1 text-xs text-gray-500">
|
||
<li>✓ ساخت tenant و زیردامنه</li>
|
||
<li>✓ داشبورد تجاری و لانچر اپها</li>
|
||
<li>✓ دامنه اختصاصی پس از entitlement</li>
|
||
</ul>
|
||
<Button className="w-full" size="lg" onClick={goWorkspace}>
|
||
ساخت Workspace
|
||
</Button>
|
||
</div>
|
||
</Container>
|
||
) : null}
|
||
|
||
{step === "create_workspace" ? (
|
||
<Container className="py-10">
|
||
<div className="mx-auto max-w-md space-y-4 rounded-3xl border border-gray-100 bg-white p-8 text-center shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||
<h2 className="text-xl font-bold text-secondary dark:text-gray-100">ثبتنام / ورود</h2>
|
||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||
انتخابهای ارزیابی در session میماند و پس از ورود به onboarding منتقل میشود.
|
||
</p>
|
||
<Button
|
||
className="w-full"
|
||
loading={authLoading}
|
||
onClick={() => {
|
||
persist({
|
||
answers,
|
||
selected_bundle_code: selectedBundle,
|
||
activate_trial: true,
|
||
});
|
||
login();
|
||
}}
|
||
>
|
||
ورود
|
||
</Button>
|
||
<Link href="/register?redirect=/onboarding" className="block">
|
||
<Button variant="outline" className="w-full">
|
||
ثبتنام
|
||
</Button>
|
||
</Link>
|
||
</div>
|
||
</Container>
|
||
) : null}
|
||
</div>
|
||
);
|
||
}
|