@greatapps/common 1.1.677 → 1.1.679
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/account/sections/PreferencesSection.mjs +8 -5
- package/dist/components/account/sections/PreferencesSection.mjs.map +1 -1
- package/dist/components/layouts/AppMobileNavBar.mjs +148 -2
- package/dist/components/layouts/AppMobileNavBar.mjs.map +1 -1
- package/dist/components/modals/BuyCreditsModal.mjs +1 -1
- package/dist/components/modals/BuyCreditsModal.mjs.map +1 -1
- package/dist/components/ui/feedback/CircularProgress.mjs.map +1 -1
- package/dist/i18n/messages/en-us.mjs +2 -1
- package/dist/i18n/messages/en-us.mjs.map +1 -1
- package/dist/i18n/messages/es-es.mjs +2 -1
- package/dist/i18n/messages/es-es.mjs.map +1 -1
- package/dist/i18n/messages/pt-br.mjs +2 -1
- package/dist/i18n/messages/pt-br.mjs.map +1 -1
- package/dist/modules/plans/handlers/list-plans.handler.mjs +2 -1
- package/dist/modules/plans/handlers/list-plans.handler.mjs.map +1 -1
- package/dist/modules/plans/services/plans.service.mjs +2 -2
- package/dist/modules/plans/services/plans.service.mjs.map +1 -1
- package/dist/modules/plans/types/plan.type.mjs +7 -4
- package/dist/modules/plans/types/plan.type.mjs.map +1 -1
- package/dist/modules/plans/utils/map-api-plan-to-ui.mjs +10 -4
- package/dist/modules/plans/utils/map-api-plan-to-ui.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/account/sections/PreferencesSection.tsx +8 -5
- package/src/components/layouts/AppMobileNavBar.tsx +154 -1
- package/src/components/modals/BuyCreditsModal.tsx +1 -1
- package/src/components/ui/feedback/CircularProgress.tsx +1 -1
- package/src/i18n/messages/en-us.ts +1 -0
- package/src/i18n/messages/es-es.ts +1 -0
- package/src/i18n/messages/pt-br.ts +1 -0
- package/src/modules/plans/handlers/list-plans.handler.ts +3 -1
- package/src/modules/plans/services/plans.service.ts +2 -2
- package/src/modules/plans/types/plan.type.ts +8 -4
- package/src/modules/plans/utils/map-api-plan-to-ui.ts +13 -6
|
@@ -44,7 +44,10 @@ function buildMainFeaturesFromItems(items, translate) {
|
|
|
44
44
|
return { pages, domains, users };
|
|
45
45
|
}
|
|
46
46
|
function computePricingByPeriod(apiPlan, currency) {
|
|
47
|
-
const monthly =
|
|
47
|
+
const monthly = apiPlan.value;
|
|
48
|
+
if (monthly == null) {
|
|
49
|
+
return { monthly: 0, semiannual: 0, annual: 0 };
|
|
50
|
+
}
|
|
48
51
|
const round2 = (n) => Math.round(n * 100) / 100;
|
|
49
52
|
return {
|
|
50
53
|
monthly,
|
|
@@ -68,8 +71,10 @@ function buildUiPlan(plan, currency, translate) {
|
|
|
68
71
|
];
|
|
69
72
|
const currencyAwareItems = plan.items.map((item) => ({
|
|
70
73
|
...item,
|
|
71
|
-
value:
|
|
74
|
+
value: item.value ?? 0
|
|
72
75
|
}));
|
|
76
|
+
const priceUnavailable = plan.value == null;
|
|
77
|
+
const unavailableLabel = translate ? translate("common.plans.usdNotConfigured") : "USD n\xE3o configurado";
|
|
73
78
|
return {
|
|
74
79
|
planId: plan.id,
|
|
75
80
|
name: plan.name,
|
|
@@ -77,9 +82,10 @@ function buildUiPlan(plan, currency, translate) {
|
|
|
77
82
|
buttonVariant,
|
|
78
83
|
mainFeatures: buildMainFeaturesFromItems(plan.items, translate),
|
|
79
84
|
features,
|
|
80
|
-
originalPrice: formatCurrencyNumber(monthlyFinal, 2, currency),
|
|
81
|
-
price: formatCurrencyNumber(monthlyFinal, 2, currency),
|
|
85
|
+
originalPrice: priceUnavailable ? unavailableLabel : formatCurrencyNumber(monthlyFinal, 2, currency),
|
|
86
|
+
price: priceUnavailable ? unavailableLabel : formatCurrencyNumber(monthlyFinal, 2, currency),
|
|
82
87
|
pricingByPeriod,
|
|
88
|
+
priceUnavailable,
|
|
83
89
|
items: currencyAwareItems
|
|
84
90
|
};
|
|
85
91
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/plans/utils/map-api-plan-to-ui.ts"],"sourcesContent":["import type {\n Plan,\n PlanFeature,\n PlanMainFeatures,\n PlanPricingByPeriod,\n UiPlan,\n} from \"../types/plan.type\";\nimport { ADDON_IDS } from \"../../subscriptions/constants/addons.constants\";\nimport {\n formatCurrencyNumber,\n type CurrencyCode,\n} from \"../../../utils/format/currency\";\n\nexport type PlanBillingPeriod = \"monthly\" | \"semiannual\" | \"annual\";\n\ntype TranslateFn = (key: any, values?: Record<string, string | number>) => string;\n\nconst BASE_FEATURES_KEYS = [\n { icon: \"check\", key: \"plans.features.unlimitedVisits\", fallback: \"Visitas ilimitadas\" },\n { icon: \"check\", key: \"plans.features.unlimitedLeads\", fallback: \"Leads ilimitadas\" },\n { icon: \"check\", key: \"plans.features.sharePages\", fallback: \"Compartilhar páginas\", tooltip: \"sharePages\" },\n { icon: \"check\", key: \"plans.features.hosting\", fallback: \"Hospedagem inclusa\" },\n // SSL (HTTPS) + CDN — brand/acronym: never translated\n { icon: \"check\", key: null, fallback: \"SSL (HTTPS) + CDN\" },\n { icon: \"check\", key: \"plans.features.freeTemplates\", fallback: \"Templates gratuitos\" },\n] as const;\n\nfunction buildBaseFeatures(translate?: TranslateFn): PlanFeature[] {\n return BASE_FEATURES_KEYS.map(({ icon, key, fallback, ...rest }) => {\n const text = key && translate ? translate(`common.${key}`) : fallback;\n const feature: PlanFeature = { icon, text };\n if (\"tooltip\" in rest && rest.tooltip) feature.tooltip = rest.tooltip;\n return feature;\n });\n}\n\nfunction buildMainFeaturesFromItems(\n items: Plan[\"items\"],\n translate?: TranslateFn\n): PlanMainFeatures {\n const pagesItem = items.find((i) => i.id_addon === ADDON_IDS.PAGES);\n const domainsItem = items.find((i) => i.id_addon === ADDON_IDS.DOMAINS);\n\n const pagesQty = pagesItem?.quantity ?? 0;\n const domainsQty = domainsItem?.quantity ?? 1;\n\n let pages: string;\n if (pagesQty === 0) {\n pages = translate\n ? translate(\"common.plans.mainFeatures.pagesUnlimited\")\n : \"Páginas ilimitadas\";\n } else if (pagesQty === 1) {\n pages = translate\n ? translate(\"common.plans.mainFeatures.pageSingular\")\n : \"1 página\";\n } else {\n pages = translate\n ? translate(\"common.plans.mainFeatures.pagesPlural\", { count: pagesQty })\n : `${pagesQty} páginas`;\n }\n\n let domains: string;\n if (domainsQty >= 999) {\n domains = translate\n ? translate(\"common.plans.mainFeatures.domainsUnlimited\")\n : \"Domínios ilimitados\";\n } else if (domainsQty === 1) {\n domains = translate\n ? translate(\"common.plans.mainFeatures.domainSingular\")\n : \"1 domínio externo\";\n } else {\n domains = translate\n ? translate(\"common.plans.mainFeatures.domainsPlural\", { count: domainsQty })\n : `${domainsQty} domínios externos`;\n }\n\n const users = translate\n ? translate(\"common.plans.mainFeatures.usersUnlimited\")\n : \"Usuários ilimitados\";\n\n return { pages, domains, users };\n}\n\nfunction computePricingByPeriod(apiPlan: Plan, currency: CurrencyCode): PlanPricingByPeriod {\n //
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/plans/utils/map-api-plan-to-ui.ts"],"sourcesContent":["import type {\n Plan,\n PlanFeature,\n PlanMainFeatures,\n PlanPricingByPeriod,\n UiPlan,\n} from \"../types/plan.type\";\nimport { ADDON_IDS } from \"../../subscriptions/constants/addons.constants\";\nimport {\n formatCurrencyNumber,\n type CurrencyCode,\n} from \"../../../utils/format/currency\";\n\nexport type PlanBillingPeriod = \"monthly\" | \"semiannual\" | \"annual\";\n\ntype TranslateFn = (key: any, values?: Record<string, string | number>) => string;\n\nconst BASE_FEATURES_KEYS = [\n { icon: \"check\", key: \"plans.features.unlimitedVisits\", fallback: \"Visitas ilimitadas\" },\n { icon: \"check\", key: \"plans.features.unlimitedLeads\", fallback: \"Leads ilimitadas\" },\n { icon: \"check\", key: \"plans.features.sharePages\", fallback: \"Compartilhar páginas\", tooltip: \"sharePages\" },\n { icon: \"check\", key: \"plans.features.hosting\", fallback: \"Hospedagem inclusa\" },\n // SSL (HTTPS) + CDN — brand/acronym: never translated\n { icon: \"check\", key: null, fallback: \"SSL (HTTPS) + CDN\" },\n { icon: \"check\", key: \"plans.features.freeTemplates\", fallback: \"Templates gratuitos\" },\n] as const;\n\nfunction buildBaseFeatures(translate?: TranslateFn): PlanFeature[] {\n return BASE_FEATURES_KEYS.map(({ icon, key, fallback, ...rest }) => {\n const text = key && translate ? translate(`common.${key}`) : fallback;\n const feature: PlanFeature = { icon, text };\n if (\"tooltip\" in rest && rest.tooltip) feature.tooltip = rest.tooltip;\n return feature;\n });\n}\n\nfunction buildMainFeaturesFromItems(\n items: Plan[\"items\"],\n translate?: TranslateFn\n): PlanMainFeatures {\n const pagesItem = items.find((i) => i.id_addon === ADDON_IDS.PAGES);\n const domainsItem = items.find((i) => i.id_addon === ADDON_IDS.DOMAINS);\n\n const pagesQty = pagesItem?.quantity ?? 0;\n const domainsQty = domainsItem?.quantity ?? 1;\n\n let pages: string;\n if (pagesQty === 0) {\n pages = translate\n ? translate(\"common.plans.mainFeatures.pagesUnlimited\")\n : \"Páginas ilimitadas\";\n } else if (pagesQty === 1) {\n pages = translate\n ? translate(\"common.plans.mainFeatures.pageSingular\")\n : \"1 página\";\n } else {\n pages = translate\n ? translate(\"common.plans.mainFeatures.pagesPlural\", { count: pagesQty })\n : `${pagesQty} páginas`;\n }\n\n let domains: string;\n if (domainsQty >= 999) {\n domains = translate\n ? translate(\"common.plans.mainFeatures.domainsUnlimited\")\n : \"Domínios ilimitados\";\n } else if (domainsQty === 1) {\n domains = translate\n ? translate(\"common.plans.mainFeatures.domainSingular\")\n : \"1 domínio externo\";\n } else {\n domains = translate\n ? translate(\"common.plans.mainFeatures.domainsPlural\", { count: domainsQty })\n : `${domainsQty} domínios externos`;\n }\n\n const users = translate\n ? translate(\"common.plans.mainFeatures.usersUnlimited\")\n : \"Usuários ilimitados\";\n\n return { pages, domains, users };\n}\n\nfunction computePricingByPeriod(apiPlan: Plan, currency: CurrencyCode): PlanPricingByPeriod {\n // Backend agora resolve value por moeda da conta; mapper apenas lê.\n // value pode ser null (ex.: conta USD, preço USD não cadastrado).\n const monthly = apiPlan.value;\n if (monthly == null) {\n return { monthly: 0, semiannual: 0, annual: 0 };\n }\n const round2 = (n: number) => Math.round(n * 100) / 100;\n return {\n monthly,\n semiannual: round2(monthly * (1 - apiPlan.discount_semester / 100)),\n annual: round2(monthly * (1 - apiPlan.discount_annual / 100)),\n };\n}\n\nfunction buildUiPlan(plan: Plan, currency: CurrencyCode, translate?: TranslateFn): UiPlan {\n const popular = plan.id === 2 || plan.id_plan === 2;\n const buttonVariant = popular ? \"brand\" : \"default\";\n\n const pricingByPeriod = computePricingByPeriod(plan, currency);\n const monthlyFinal = pricingByPeriod.monthly;\n\n const lowerName = plan.name.toLowerCase();\n const hasProjectAndOnboarding =\n lowerName.includes(\"agência\") || lowerName.includes(\"agencia\");\n\n const onboardingText = translate\n ? translate(\"common.plans.features.onboarding\")\n : \"Reunião de Onboarding\";\n const projectMgmtText = translate\n ? translate(\"common.plans.features.projectManagement\")\n : \"Gestão de projetos\";\n\n const features: PlanFeature[] = [\n ...buildBaseFeatures(translate),\n hasProjectAndOnboarding\n ? { icon: \"check\", text: onboardingText }\n : { icon: \"x\", text: onboardingText, disabled: true },\n hasProjectAndOnboarding\n ? { icon: \"check\", text: projectMgmtText, tooltip: \"projectManagement\" }\n : { icon: \"x\", text: projectMgmtText, disabled: true, tooltip: \"projectManagement\" },\n ];\n\n const currencyAwareItems = plan.items.map((item) => ({\n ...item,\n value: item.value ?? 0,\n }));\n\n const priceUnavailable = plan.value == null;\n const unavailableLabel = translate ? translate(\"common.plans.usdNotConfigured\") : \"USD não configurado\";\n\n return {\n planId: plan.id,\n name: plan.name,\n isPopular: popular,\n buttonVariant,\n mainFeatures: buildMainFeaturesFromItems(plan.items, translate),\n features,\n originalPrice: priceUnavailable ? unavailableLabel : formatCurrencyNumber(monthlyFinal, 2, currency),\n price: priceUnavailable ? unavailableLabel : formatCurrencyNumber(monthlyFinal, 2, currency),\n pricingByPeriod,\n priceUnavailable,\n items: currencyAwareItems,\n };\n}\n\n/**\n * Mapeia um plano da API para UiPlan em BRL.\n * Assinatura de 1 argumento — seguro em `plans.map(mapApiPlanToUiPlan)`.\n * Passa `translate` opcional para i18n; sem ele cai nos literais pt-br (back-compat).\n */\nexport function mapApiPlanToUiPlan(plan: Plan, translate?: TranslateFn): UiPlan {\n return buildUiPlan(plan, \"brl\", translate);\n}\n\n/**\n * Variante currency-aware: a moeda vem do gateway da conta (getCurrencyForGateway).\n * Fábrica para uso ergonômico em `.map`: `plans.map(mapApiPlanToUiPlanForCurrency(currency, translate))`.\n * Mantida separada de mapApiPlanToUiPlan para não quebrar callers que passam a fn direto pro `.map`.\n */\nexport const mapApiPlanToUiPlanForCurrency =\n (currency: CurrencyCode, translate?: TranslateFn) =>\n (plan: Plan): UiPlan =>\n buildUiPlan(plan, currency, translate);\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from '../constants';
|
|
31
31
|
import { useTimezones } from '../../../modules/accounts/hooks/useTimezones';
|
|
32
32
|
import { useCurrencies } from '../../../modules/accounts/hooks/useCurrencies';
|
|
33
|
+
import { getCurrencyForGateway } from '../../../utils/format/currency';
|
|
33
34
|
|
|
34
35
|
interface PreferencesFormValues {
|
|
35
36
|
companyName: string;
|
|
@@ -98,7 +99,9 @@ export function PreferencesSection({ onClose }: PreferencesSectionProps) {
|
|
|
98
99
|
language: LANGUAGE_OPTIONS.find(
|
|
99
100
|
(o) => o.apiValue === user.language?.toLowerCase()
|
|
100
101
|
)?.value ?? 'pt-br',
|
|
101
|
-
|
|
102
|
+
// A moeda é derivada do gateway da conta (read-only para o usuário) —
|
|
103
|
+
// espelha o que useCurrencyFormatter usa no display de preços.
|
|
104
|
+
currency: getCurrencyForGateway(account.gateway).toUpperCase(),
|
|
102
105
|
timezone: account.timezone,
|
|
103
106
|
timeFormat: '24h',
|
|
104
107
|
receive_sms: user.receive_sms ?? false,
|
|
@@ -145,7 +148,7 @@ export function PreferencesSection({ onClose }: PreferencesSectionProps) {
|
|
|
145
148
|
const onSubmit = (data: PreferencesFormValues) => {
|
|
146
149
|
const accountFields: UpdateAccountRequest = {
|
|
147
150
|
...(isOwner && dirtyFields.companyName && { name: data.companyName }),
|
|
148
|
-
|
|
151
|
+
// currency é read-only (derivada do gateway) — não enviada no update.
|
|
149
152
|
...(canEditPreferences && dirtyFields.timezone && { timezone: data.timezone }),
|
|
150
153
|
};
|
|
151
154
|
const globalFieldChanged = Object.keys(accountFields).length > 0;
|
|
@@ -205,9 +208,9 @@ export function PreferencesSection({ onClose }: PreferencesSectionProps) {
|
|
|
205
208
|
containerClassName="w-full"
|
|
206
209
|
options={currencies}
|
|
207
210
|
value={watch('currency')}
|
|
208
|
-
onChange={(
|
|
209
|
-
icon={
|
|
210
|
-
disabled
|
|
211
|
+
onChange={() => undefined}
|
|
212
|
+
icon={<IconLock className="size-4 text-gray-400" />}
|
|
213
|
+
disabled
|
|
211
214
|
/>
|
|
212
215
|
</div>
|
|
213
216
|
|
|
@@ -4,13 +4,20 @@ import { ReactNode } from "react";
|
|
|
4
4
|
import Image from "next/image";
|
|
5
5
|
import { IconMenu2, IconX } from "@tabler/icons-react";
|
|
6
6
|
import { ProfilePopover } from "./ProfilePopover";
|
|
7
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/overlay/Tooltip";
|
|
7
8
|
import { useMobileNavbarSheet } from "../../store/useMobileNavbarSheet";
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
useWhitelabel,
|
|
11
|
+
useIsDefaultWhitelabel,
|
|
12
|
+
} from "../../providers/whitelabel.provider";
|
|
13
|
+
import { cn } from "../../infra/utils/clsx";
|
|
9
14
|
import type { ProfileMenuItem } from "./ProfilePopover";
|
|
10
15
|
|
|
11
16
|
export interface AppMobileNavBarProps {
|
|
12
17
|
/** Which app is currently active — controls which icon gets the indicator */
|
|
13
18
|
activeApp?: "gapps" | "gpages";
|
|
19
|
+
/** Called when the GreatApps logo is clicked */
|
|
20
|
+
onLogoClick?: () => void;
|
|
14
21
|
/** Called when the app icon (e.g. GreatPages) is clicked */
|
|
15
22
|
onAppIconClick?: () => void;
|
|
16
23
|
/** Extra slot rendered after the app icon (e.g. project selector) */
|
|
@@ -21,19 +28,165 @@ export interface AppMobileNavBarProps {
|
|
|
21
28
|
|
|
22
29
|
export function AppMobileNavBar({
|
|
23
30
|
activeApp,
|
|
31
|
+
onLogoClick,
|
|
24
32
|
onAppIconClick,
|
|
25
33
|
extraLeftSlot,
|
|
26
34
|
menuItems = [],
|
|
27
35
|
showMenuToggle = true,
|
|
28
36
|
}: AppMobileNavBarProps) {
|
|
29
37
|
const { isOpen, toggle } = useMobileNavbarSheet();
|
|
38
|
+
const { whitelabel } = useWhitelabel();
|
|
30
39
|
const isDefaultWl = useIsDefaultWhitelabel();
|
|
40
|
+
const primaryColor = whitelabel?.general?.primary_color;
|
|
41
|
+
const wlName = whitelabel?.name || "Great";
|
|
31
42
|
|
|
43
|
+
const isGappsActive = activeApp === "gapps";
|
|
32
44
|
const isGpagesActive = activeApp === "gpages";
|
|
33
45
|
|
|
46
|
+
const wlIcon = whitelabel?.secondary_logo ? (
|
|
47
|
+
<Image
|
|
48
|
+
className="size-7 object-contain"
|
|
49
|
+
src={`${process.env.NEXT_PUBLIC_STORAGE_URL}/${whitelabel.secondary_logo}`}
|
|
50
|
+
alt={`${wlName} Logo`}
|
|
51
|
+
width={28}
|
|
52
|
+
height={28}
|
|
53
|
+
/>
|
|
54
|
+
) : (
|
|
55
|
+
<div
|
|
56
|
+
className={cn(
|
|
57
|
+
"size-10 rounded-lg flex items-center justify-center transition-opacity duration-300",
|
|
58
|
+
!isGappsActive && "opacity-20",
|
|
59
|
+
)}
|
|
60
|
+
style={{ backgroundColor: primaryColor }}
|
|
61
|
+
>
|
|
62
|
+
<svg
|
|
63
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
64
|
+
width="24"
|
|
65
|
+
height="24"
|
|
66
|
+
viewBox="0 0 24 24"
|
|
67
|
+
fill="none"
|
|
68
|
+
stroke="white"
|
|
69
|
+
strokeWidth="2"
|
|
70
|
+
strokeLinecap="round"
|
|
71
|
+
strokeLinejoin="round"
|
|
72
|
+
>
|
|
73
|
+
<path d="M4 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
|
|
74
|
+
<path d="M14 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
|
|
75
|
+
<path d="M4 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
|
|
76
|
+
<path d="M14 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
|
|
77
|
+
</svg>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
|
|
34
81
|
return (
|
|
35
82
|
<div className="fixed top-0 left-0 right-0 flex md:hidden items-center justify-between p-4 bg-white z-50 border-b border-gray-200">
|
|
36
83
|
<div className="flex items-center gap-1.5">
|
|
84
|
+
{/* GreatApps logo */}
|
|
85
|
+
<div className="relative">
|
|
86
|
+
<Tooltip>
|
|
87
|
+
<TooltipTrigger asChild>
|
|
88
|
+
<div
|
|
89
|
+
className={cn(
|
|
90
|
+
"flex items-center justify-center",
|
|
91
|
+
onLogoClick ? "cursor-pointer" : "",
|
|
92
|
+
)}
|
|
93
|
+
onClick={onLogoClick}
|
|
94
|
+
>
|
|
95
|
+
{isDefaultWl ? (
|
|
96
|
+
<div className="size-10 flex items-center justify-center rounded-lg">
|
|
97
|
+
<Image
|
|
98
|
+
src="/icons/great-icon.svg"
|
|
99
|
+
alt="Great Icon"
|
|
100
|
+
width={28}
|
|
101
|
+
height={28}
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
) : (
|
|
105
|
+
wlIcon
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
</TooltipTrigger>
|
|
109
|
+
<TooltipContent side="bottom">{wlName}</TooltipContent>
|
|
110
|
+
</Tooltip>
|
|
111
|
+
<div
|
|
112
|
+
className={cn(
|
|
113
|
+
"absolute -bottom-[17px] left-1/2 -translate-x-1/2 w-5 h-[3px] rounded-t-xl transition-opacity duration-300",
|
|
114
|
+
isDefaultWl ? "bg-gray-950" : "",
|
|
115
|
+
isGappsActive ? "opacity-100" : "opacity-0",
|
|
116
|
+
)}
|
|
117
|
+
style={!isDefaultWl ? { backgroundColor: primaryColor } : undefined}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* GreatPages icon */}
|
|
122
|
+
<div className="relative">
|
|
123
|
+
<Tooltip>
|
|
124
|
+
<TooltipTrigger asChild>
|
|
125
|
+
<div
|
|
126
|
+
className={cn(
|
|
127
|
+
"size-10 rounded-lg flex items-center justify-center border-2 ring-2 transition-colors duration-300",
|
|
128
|
+
isGpagesActive
|
|
129
|
+
? isDefaultWl
|
|
130
|
+
? "bg-cyan-100 border-white ring-cyan-300"
|
|
131
|
+
: "border-white ring-transparent"
|
|
132
|
+
: isDefaultWl
|
|
133
|
+
? "hover:bg-gray-100 border-transparent ring-transparent"
|
|
134
|
+
: "border-transparent ring-transparent",
|
|
135
|
+
onAppIconClick ? "cursor-pointer" : "",
|
|
136
|
+
)}
|
|
137
|
+
style={
|
|
138
|
+
!isDefaultWl
|
|
139
|
+
? {
|
|
140
|
+
backgroundColor: primaryColor,
|
|
141
|
+
boxShadow: isGpagesActive
|
|
142
|
+
? `0 0 0 2px ${primaryColor}40`
|
|
143
|
+
: undefined,
|
|
144
|
+
opacity: !isGpagesActive ? 0.2 : undefined,
|
|
145
|
+
}
|
|
146
|
+
: undefined
|
|
147
|
+
}
|
|
148
|
+
onClick={onAppIconClick}
|
|
149
|
+
>
|
|
150
|
+
{isDefaultWl ? (
|
|
151
|
+
<Image
|
|
152
|
+
src="/icons/icon-navbar.svg"
|
|
153
|
+
alt="GreatPages"
|
|
154
|
+
width={24}
|
|
155
|
+
height={24}
|
|
156
|
+
/>
|
|
157
|
+
) : (
|
|
158
|
+
<svg
|
|
159
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
160
|
+
width="24"
|
|
161
|
+
height="24"
|
|
162
|
+
viewBox="0 0 24 24"
|
|
163
|
+
fill="none"
|
|
164
|
+
stroke="white"
|
|
165
|
+
strokeWidth="2"
|
|
166
|
+
strokeLinecap="round"
|
|
167
|
+
strokeLinejoin="round"
|
|
168
|
+
>
|
|
169
|
+
<path d="M4 8h16" />
|
|
170
|
+
<path d="M4 6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2l0 -12" />
|
|
171
|
+
<path d="M8 4v4" />
|
|
172
|
+
</svg>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
</TooltipTrigger>
|
|
176
|
+
<TooltipContent side="bottom">
|
|
177
|
+
{isDefaultWl ? "GreatPages" : "Páginas"}
|
|
178
|
+
</TooltipContent>
|
|
179
|
+
</Tooltip>
|
|
180
|
+
<div
|
|
181
|
+
className={cn(
|
|
182
|
+
"absolute -bottom-[17px] left-1/2 -translate-x-1/2 w-5 h-[3px] rounded-t-xl transition-opacity duration-300",
|
|
183
|
+
isDefaultWl ? "bg-cyan-300" : "",
|
|
184
|
+
isGpagesActive ? "opacity-100" : "opacity-0",
|
|
185
|
+
)}
|
|
186
|
+
style={!isDefaultWl ? { backgroundColor: primaryColor } : undefined}
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
37
190
|
{extraLeftSlot}
|
|
38
191
|
</div>
|
|
39
192
|
<div className="flex items-center gap-1.5">
|
|
@@ -195,7 +195,7 @@ export default function BuyCreditsModal() {
|
|
|
195
195
|
),
|
|
196
196
|
displayValue: translate('common.credits.buy.creditsOptionDisplayValue', { credits: opt.credits.toLocaleString('pt-BR') }),
|
|
197
197
|
}));
|
|
198
|
-
}, []);
|
|
198
|
+
}, [translate]);
|
|
199
199
|
|
|
200
200
|
const onSubmit = useCallback(
|
|
201
201
|
async (data: BuyCreditFormData) => {
|
|
@@ -36,7 +36,7 @@ export default function CircularProgress({
|
|
|
36
36
|
aria-valuemax={total}
|
|
37
37
|
// i18n-TODO: aria-label hardcoded — CircularProgress has no 'use client' directive and
|
|
38
38
|
// receives no translate prop, so useTranslations() cannot be called here without
|
|
39
|
-
// converting it to a client component. Key to use: common.forms.
|
|
39
|
+
// converting it to a client component. Key to use: common.forms.circularProgress.ariaLabel ('Progresso').
|
|
40
40
|
// Either add 'use client' + useTranslations(), or accept a translate prop from the caller.
|
|
41
41
|
aria-label="Progresso"
|
|
42
42
|
>
|
|
@@ -14,7 +14,9 @@ export function createListPlansHandler() {
|
|
|
14
14
|
|
|
15
15
|
if (!apiPlans?.length) return [];
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
// null/undefined (USD price não configurado) ordena por último — evita aparecer como "o mais barato"
|
|
18
|
+
const sortValue = (v: number | null | undefined) => (v == null ? Number.POSITIVE_INFINITY : v);
|
|
19
|
+
return [...apiPlans].sort((a, b) => sortValue(a.value) - sortValue(b.value));
|
|
18
20
|
});
|
|
19
21
|
};
|
|
20
22
|
}
|
|
@@ -32,7 +32,7 @@ class PlansService {
|
|
|
32
32
|
const sort = params?.sort ?? "id:ASC";
|
|
33
33
|
const active = params?.active ?? "";
|
|
34
34
|
const search = params?.search ?? "";
|
|
35
|
-
return `${key}-${idWl}-${idAccount}-${sort}-${active}-${search}-
|
|
35
|
+
return `${key}-${idWl}-${idAccount}-${sort}-${active}-${search}-v4`;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/**
|
|
@@ -91,7 +91,7 @@ class PlansService {
|
|
|
91
91
|
|
|
92
92
|
async findById(idPlan: number | string): Promise<SuccessResult<Plan>> {
|
|
93
93
|
const { id_wl, id_account } = await getUserContext();
|
|
94
|
-
const cacheKey = this.buildCacheKey(`plan-${idPlan}`, {
|
|
94
|
+
const cacheKey = this.buildCacheKey(`plan-${idPlan}-v2`, {
|
|
95
95
|
id_wl,
|
|
96
96
|
id_account: String(id_account),
|
|
97
97
|
});
|
|
@@ -5,7 +5,7 @@ export const PlanItemSchema = z.object({
|
|
|
5
5
|
id_addon: z.number(),
|
|
6
6
|
quantity: z.number(),
|
|
7
7
|
value: z.number(),
|
|
8
|
-
value_usd: z.
|
|
8
|
+
value_usd: z.number().nullish(),
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
export type PlanItem = z.infer<typeof PlanItemSchema>;
|
|
@@ -14,10 +14,13 @@ export const PlanSchema = z.object({
|
|
|
14
14
|
id: z.number(),
|
|
15
15
|
id_plan: z.number().optional(),
|
|
16
16
|
name: z.string(),
|
|
17
|
-
value: z.number().default(0),
|
|
17
|
+
value: z.number().nullable().default(0),
|
|
18
18
|
// Preço em USD (conta stripe_usd). Operador cadastra no backend; ausente em planos
|
|
19
|
-
// sem suporte internacional.
|
|
20
|
-
value_usd
|
|
19
|
+
// sem suporte internacional. Backend agora resolve por moeda da conta (customer),
|
|
20
|
+
// omitindo value_usd para customer requests e adicionando currency. Admin paths
|
|
21
|
+
// ainda recebem value_usd como número (sem .coerce — backend joga agora como Number).
|
|
22
|
+
value_usd: z.number().nullish(),
|
|
23
|
+
currency: z.enum(['brl', 'usd']).optional(),
|
|
21
24
|
type: z.string().optional(),
|
|
22
25
|
discount_semester: z.number().default(0),
|
|
23
26
|
discount_annual: z.number().default(0),
|
|
@@ -60,6 +63,7 @@ export type UiPlan = {
|
|
|
60
63
|
price: string;
|
|
61
64
|
pricingByPeriod?: PlanPricingByPeriod;
|
|
62
65
|
buttonVariant?: 'brand' | 'default';
|
|
66
|
+
priceUnavailable?: boolean;
|
|
63
67
|
items: PlanItem[];
|
|
64
68
|
};
|
|
65
69
|
|
|
@@ -82,9 +82,12 @@ function buildMainFeaturesFromItems(
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
function computePricingByPeriod(apiPlan: Plan, currency: CurrencyCode): PlanPricingByPeriod {
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
const monthly =
|
|
85
|
+
// Backend agora resolve value por moeda da conta; mapper apenas lê.
|
|
86
|
+
// value pode ser null (ex.: conta USD, preço USD não cadastrado).
|
|
87
|
+
const monthly = apiPlan.value;
|
|
88
|
+
if (monthly == null) {
|
|
89
|
+
return { monthly: 0, semiannual: 0, annual: 0 };
|
|
90
|
+
}
|
|
88
91
|
const round2 = (n: number) => Math.round(n * 100) / 100;
|
|
89
92
|
return {
|
|
90
93
|
monthly,
|
|
@@ -123,9 +126,12 @@ function buildUiPlan(plan: Plan, currency: CurrencyCode, translate?: TranslateFn
|
|
|
123
126
|
|
|
124
127
|
const currencyAwareItems = plan.items.map((item) => ({
|
|
125
128
|
...item,
|
|
126
|
-
value:
|
|
129
|
+
value: item.value ?? 0,
|
|
127
130
|
}));
|
|
128
131
|
|
|
132
|
+
const priceUnavailable = plan.value == null;
|
|
133
|
+
const unavailableLabel = translate ? translate("common.plans.usdNotConfigured") : "USD não configurado";
|
|
134
|
+
|
|
129
135
|
return {
|
|
130
136
|
planId: plan.id,
|
|
131
137
|
name: plan.name,
|
|
@@ -133,9 +139,10 @@ function buildUiPlan(plan: Plan, currency: CurrencyCode, translate?: TranslateFn
|
|
|
133
139
|
buttonVariant,
|
|
134
140
|
mainFeatures: buildMainFeaturesFromItems(plan.items, translate),
|
|
135
141
|
features,
|
|
136
|
-
originalPrice: formatCurrencyNumber(monthlyFinal, 2, currency),
|
|
137
|
-
price: formatCurrencyNumber(monthlyFinal, 2, currency),
|
|
142
|
+
originalPrice: priceUnavailable ? unavailableLabel : formatCurrencyNumber(monthlyFinal, 2, currency),
|
|
143
|
+
price: priceUnavailable ? unavailableLabel : formatCurrencyNumber(monthlyFinal, 2, currency),
|
|
138
144
|
pricingByPeriod,
|
|
145
|
+
priceUnavailable,
|
|
139
146
|
items: currencyAwareItems,
|
|
140
147
|
};
|
|
141
148
|
}
|