commit
c539dbbfe4
@ -259,6 +259,7 @@ export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) {
|
|||||||
const canGoBack = historyWithCurrent.lastIndexOf(currentScreen.id) > 0;
|
const canGoBack = historyWithCurrent.lastIndexOf(currentScreen.id) > 0;
|
||||||
|
|
||||||
return renderScreen({
|
return renderScreen({
|
||||||
|
funnel,
|
||||||
screen: currentScreen,
|
screen: currentScreen,
|
||||||
selectedOptionIds,
|
selectedOptionIds,
|
||||||
onSelectionChange: handleSelectionChange,
|
onSelectionChange: handleSelectionChange,
|
||||||
|
|||||||
@ -4,20 +4,27 @@ import { useState, useEffect } from "react";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import PrivacySecurityBanner from "@/components/widgets/PrivacySecurityBanner/PrivacySecurityBanner";
|
import PrivacySecurityBanner from "@/components/widgets/PrivacySecurityBanner/PrivacySecurityBanner";
|
||||||
import { TextInput } from "@/components/ui/TextInput/TextInput";
|
import { TextInput } from "@/components/ui/TextInput/TextInput";
|
||||||
import type { EmailScreenDefinition, DefaultTexts } from "@/lib/funnel/types";
|
import type {
|
||||||
|
EmailScreenDefinition,
|
||||||
|
DefaultTexts,
|
||||||
|
FunnelDefinition,
|
||||||
|
} from "@/lib/funnel/types";
|
||||||
import { TemplateLayout } from "../layouts/TemplateLayout";
|
import { TemplateLayout } from "../layouts/TemplateLayout";
|
||||||
import { createTemplateLayoutProps } from "@/lib/funnel/templateHelpers";
|
import { createTemplateLayoutProps } from "@/lib/funnel/templateHelpers";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
import { useAuth } from "@/hooks/auth/useAuth";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
email: z.string().email({
|
email: z.email({
|
||||||
message: "Please enter a valid email address",
|
message: "Please enter a valid email address",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
interface EmailTemplateProps {
|
interface EmailTemplateProps {
|
||||||
|
funnel: FunnelDefinition;
|
||||||
screen: EmailScreenDefinition;
|
screen: EmailScreenDefinition;
|
||||||
selectedEmail: string;
|
selectedEmail: string;
|
||||||
onEmailChange: (email: string) => void;
|
onEmailChange: (email: string) => void;
|
||||||
@ -29,6 +36,7 @@ interface EmailTemplateProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function EmailTemplate({
|
export function EmailTemplate({
|
||||||
|
funnel,
|
||||||
screen,
|
screen,
|
||||||
selectedEmail,
|
selectedEmail,
|
||||||
onEmailChange,
|
onEmailChange,
|
||||||
@ -38,8 +46,12 @@ export function EmailTemplate({
|
|||||||
screenProgress,
|
screenProgress,
|
||||||
defaultTexts,
|
defaultTexts,
|
||||||
}: EmailTemplateProps) {
|
}: EmailTemplateProps) {
|
||||||
|
const { authorization, isLoading, error } = useAuth({
|
||||||
|
funnelId: funnel.meta.id,
|
||||||
|
});
|
||||||
|
|
||||||
const [isTouched, setIsTouched] = useState(false);
|
const [isTouched, setIsTouched] = useState(false);
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@ -58,6 +70,21 @@ export function EmailTemplate({
|
|||||||
onEmailChange(value);
|
onEmailChange(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleContinue = async () => {
|
||||||
|
const email = form.getValues("email");
|
||||||
|
|
||||||
|
if (!email || !form.formState.isValid || isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await authorization(email);
|
||||||
|
onContinue();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Authorization failed:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const isFormValid = form.formState.isValid && form.getValues("email");
|
const isFormValid = form.formState.isValid && form.getValues("email");
|
||||||
|
|
||||||
const layoutProps = createTemplateLayoutProps(
|
const layoutProps = createTemplateLayoutProps(
|
||||||
@ -67,9 +94,10 @@ export function EmailTemplate({
|
|||||||
{
|
{
|
||||||
preset: "center",
|
preset: "center",
|
||||||
actionButton: {
|
actionButton: {
|
||||||
|
children: isLoading ? <Spinner className="size-6" /> : undefined,
|
||||||
defaultText: defaultTexts?.nextButton || "Continue",
|
defaultText: defaultTexts?.nextButton || "Continue",
|
||||||
disabled: !isFormValid,
|
disabled: !isFormValid,
|
||||||
onClick: onContinue,
|
onClick: handleContinue,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -87,9 +115,10 @@ export function EmailTemplate({
|
|||||||
setIsTouched(true);
|
setIsTouched(true);
|
||||||
form.trigger("email");
|
form.trigger("email");
|
||||||
}}
|
}}
|
||||||
aria-invalid={isTouched && !!form.formState.errors.email}
|
aria-invalid={(isTouched && !!form.formState.errors.email) || !!error}
|
||||||
aria-errormessage={
|
aria-errormessage={
|
||||||
isTouched ? form.formState.errors.email?.message : undefined
|
(isTouched ? form.formState.errors.email?.message : undefined) ||
|
||||||
|
(error ? "Something went wrong" : undefined)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -97,16 +126,18 @@ export function EmailTemplate({
|
|||||||
<Image
|
<Image
|
||||||
src={screen.image.src}
|
src={screen.image.src}
|
||||||
alt="portrait"
|
alt="portrait"
|
||||||
width={164}
|
width={164}
|
||||||
height={245}
|
height={245}
|
||||||
className="mt-3.5 rounded-[50px] blur-sm"
|
className="mt-3.5 rounded-[50px] blur-sm"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<PrivacySecurityBanner
|
<PrivacySecurityBanner
|
||||||
className="mt-[26px]"
|
className="mt-[26px]"
|
||||||
text={{
|
text={{
|
||||||
children: defaultTexts?.privacyBanner || "Мы не передаем личную информацию, она остаётся в безопасности и под вашим контролем."
|
children:
|
||||||
|
defaultTexts?.privacyBanner ||
|
||||||
|
"Мы не передаем личную информацию, она остаётся в безопасности и под вашим контролем.",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
16
src/components/ui/spinner.tsx
Normal file
16
src/components/ui/spinner.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Loader2Icon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
||||||
|
return (
|
||||||
|
<Loader2Icon
|
||||||
|
role="status"
|
||||||
|
aria-label="Loading"
|
||||||
|
className={cn("size-4 animate-spin", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Spinner }
|
||||||
@ -1,7 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import React, { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
|
import React, {
|
||||||
|
forwardRef,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
import { GradientBlur } from "../GradientBlur/GradientBlur";
|
import { GradientBlur } from "../GradientBlur/GradientBlur";
|
||||||
import { ActionButton } from "@/components/ui/ActionButton/ActionButton";
|
import { ActionButton } from "@/components/ui/ActionButton/ActionButton";
|
||||||
@ -86,4 +91,4 @@ const BottomActionButton = forwardRef<HTMLDivElement, BottomActionButtonProps>(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export { BottomActionButton };
|
export { BottomActionButton };
|
||||||
|
|||||||
17
src/entities/session/serverActions.ts
Normal file
17
src/entities/session/serverActions.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
|
||||||
|
export const setSessionIdToCookie = async (
|
||||||
|
key: string,
|
||||||
|
value: string
|
||||||
|
): Promise<void> => {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
cookieStore.set(key, value, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
path: "/",
|
||||||
|
sameSite: "lax",
|
||||||
|
maxAge: 60 * 60 * 24 * 365,
|
||||||
|
});
|
||||||
|
};
|
||||||
21
src/entities/user/actions.ts
Normal file
21
src/entities/user/actions.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { http } from "@/shared/api/httpClient";
|
||||||
|
import {
|
||||||
|
CreateAuthorizeResponseSchema,
|
||||||
|
ICreateAuthorizeRequest,
|
||||||
|
ICreateAuthorizeResponse,
|
||||||
|
} from "./types";
|
||||||
|
import { API_ROUTES } from "@/shared/constants/api-routes";
|
||||||
|
|
||||||
|
export const createAuthorization = async (
|
||||||
|
payload: ICreateAuthorizeRequest
|
||||||
|
): Promise<ICreateAuthorizeResponse> => {
|
||||||
|
return http.post<ICreateAuthorizeResponse>(
|
||||||
|
API_ROUTES.authorization(),
|
||||||
|
payload,
|
||||||
|
{
|
||||||
|
tags: ["authorization", "create"],
|
||||||
|
schema: CreateAuthorizeResponseSchema,
|
||||||
|
revalidate: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
14
src/entities/user/serverActions.ts
Normal file
14
src/entities/user/serverActions.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
|
||||||
|
export const setAuthTokenToCookie = async (token: string): Promise<void> => {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
cookieStore.set("accessToken", token, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
path: "/",
|
||||||
|
sameSite: "lax",
|
||||||
|
maxAge: 60 * 60 * 24 * 365,
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -20,3 +20,35 @@ export const CreateAuthorizeUserSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
relationship_status: RelationshipStatusSchema,
|
relationship_status: RelationshipStatusSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const CreateAuthorizeRequestSchema = z.object({
|
||||||
|
email: z.string(),
|
||||||
|
locale: z.string(),
|
||||||
|
timezone: z.string(),
|
||||||
|
source: z.string(),
|
||||||
|
profile: CreateAuthorizeUserSchema.optional(),
|
||||||
|
partner: CreateAuthorizeUserSchema.omit({
|
||||||
|
relationship_status: true,
|
||||||
|
}).optional(),
|
||||||
|
sign: z.boolean(),
|
||||||
|
signDate: z.string().optional(),
|
||||||
|
feature: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateAuthorizeResponseSchema = z.object({
|
||||||
|
token: z.string(),
|
||||||
|
userId: z.string().optional(),
|
||||||
|
generatingVideo: z.boolean().optional(),
|
||||||
|
videoId: z.string().optional(),
|
||||||
|
authCode: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ICreateAuthorizeUser = z.infer<typeof CreateAuthorizeUserSchema>;
|
||||||
|
|
||||||
|
export type ICreateAuthorizeRequest = z.infer<
|
||||||
|
typeof CreateAuthorizeRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type ICreateAuthorizeResponse = z.infer<
|
||||||
|
typeof CreateAuthorizeResponseSchema
|
||||||
|
>;
|
||||||
|
|||||||
153
src/hooks/auth/useAuth.ts
Normal file
153
src/hooks/auth/useAuth.ts
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import { useSession } from "../session/useSession";
|
||||||
|
import { getClientTimezone } from "@/shared/utils/locales";
|
||||||
|
import { ICreateAuthorizeRequest } from "@/entities/user/types";
|
||||||
|
import { filterNullKeysOfObject } from "@/shared/utils/filter-object";
|
||||||
|
import { createAuthorization } from "@/entities/user/actions";
|
||||||
|
import { setAuthTokenToCookie } from "@/entities/user/serverActions";
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
const locale = "en";
|
||||||
|
|
||||||
|
interface IUseAuthProps {
|
||||||
|
funnelId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuth = ({ funnelId }: IUseAuthProps) => {
|
||||||
|
const { updateSession } = useSession({ funnelId });
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const getAllCookies = useCallback(() => {
|
||||||
|
const cookies: Record<string, string> = {};
|
||||||
|
document.cookie.split(";").forEach((cookie) => {
|
||||||
|
const [name, value] = cookie.trim().split("=");
|
||||||
|
if (name && value) {
|
||||||
|
cookies[name] = decodeURIComponent(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return cookies;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getAuthorizationPayload = useCallback(
|
||||||
|
(email: string): ICreateAuthorizeRequest => {
|
||||||
|
const timezone = getClientTimezone();
|
||||||
|
return filterNullKeysOfObject<ICreateAuthorizeRequest>({
|
||||||
|
timezone,
|
||||||
|
locale,
|
||||||
|
email,
|
||||||
|
// source: funnelId,
|
||||||
|
source: "aura.compatibility.v2",
|
||||||
|
// profile: {
|
||||||
|
// name: username || "",
|
||||||
|
// gender: EGender[gender as keyof typeof EGender] || null,
|
||||||
|
// birthdate: formatDate(`${birthdate} ${birthtime}`),
|
||||||
|
// birthplace: {
|
||||||
|
// address: birthPlace,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// partner: {
|
||||||
|
// name: partnerName,
|
||||||
|
// gender: EGender[partnerGender as keyof typeof EGender] || null,
|
||||||
|
// birthdate: formatDate(partnerBirthdate),
|
||||||
|
// birthplace: {
|
||||||
|
// address: partnerBirthPlace,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
sign: true,
|
||||||
|
signDate: new Date().toISOString(),
|
||||||
|
// feature: feature.includes("black") ? "ios" : feature,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[
|
||||||
|
// birthPlace,
|
||||||
|
// birthdate,
|
||||||
|
// gender,
|
||||||
|
// locale,
|
||||||
|
// partnerBirthPlace,
|
||||||
|
// partnerBirthdate,
|
||||||
|
// partnerGender,
|
||||||
|
// partnerName,
|
||||||
|
// username,
|
||||||
|
// birthtime,
|
||||||
|
// checked,
|
||||||
|
// dateOfCheck,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const authorization = useCallback(
|
||||||
|
async (email: string) => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// Обновляем сессию с куки перед авторизацией
|
||||||
|
try {
|
||||||
|
const cookies = getAllCookies();
|
||||||
|
await updateSession({ cookies });
|
||||||
|
console.log(
|
||||||
|
"Session updated with cookies before authorization:",
|
||||||
|
cookies
|
||||||
|
);
|
||||||
|
} catch (sessionError) {
|
||||||
|
console.warn("Failed to update session with cookies:", sessionError);
|
||||||
|
// Продолжаем авторизацию даже если обновление сессии не удалось
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = getAuthorizationPayload(email);
|
||||||
|
const {
|
||||||
|
token,
|
||||||
|
// userId: userIdFromApi,
|
||||||
|
// generatingVideo,
|
||||||
|
// videoId,
|
||||||
|
// authCode,
|
||||||
|
} = await createAuthorization(payload);
|
||||||
|
await setAuthTokenToCookie(token);
|
||||||
|
// const { user: userMe } = await api.getMe({ token });
|
||||||
|
// const userId = userIdFromApi || userMe?._id;
|
||||||
|
// if (userId?.length) {
|
||||||
|
// dispatch(actions.userId.update({ userId }));
|
||||||
|
// metricService.userParams({
|
||||||
|
// hasPersonalVideo: generatingVideo || false,
|
||||||
|
// email: user?.email,
|
||||||
|
// UserID: userId,
|
||||||
|
// });
|
||||||
|
// metricService.setUserID(userId);
|
||||||
|
// }
|
||||||
|
// signUp(token, userMe, isAnonymous);
|
||||||
|
// setToken(token);
|
||||||
|
// dispatch(actions.userConfig.setAuthCode(authCode || ""));
|
||||||
|
// dispatch(
|
||||||
|
// actions.personalVideo.updateStatus({
|
||||||
|
// generatingVideo: generatingVideo || false,
|
||||||
|
// videoId: videoId || "",
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// if (generatingVideo) {
|
||||||
|
// metricService.reachGoal(EGoals.ROSE_VIDEO_CREATION_START, [
|
||||||
|
// EMetrics.YANDEX,
|
||||||
|
// EMetrics.KLAVIYO,
|
||||||
|
// ]);
|
||||||
|
// }
|
||||||
|
// dispatch(actions.status.update("registred"));
|
||||||
|
} catch (error) {
|
||||||
|
setError((error as Error).message);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[getAllCookies, getAuthorizationPayload, updateSession]
|
||||||
|
);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
authorization,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
}),
|
||||||
|
[authorization, isLoading, error]
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -9,9 +9,10 @@ import {
|
|||||||
import { getClientTimezone } from "@/shared/utils/locales";
|
import { getClientTimezone } from "@/shared/utils/locales";
|
||||||
import { parseQueryParams } from "@/shared/utils/url";
|
import { parseQueryParams } from "@/shared/utils/url";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import { setSessionIdToCookie } from "@/entities/session/serverActions";
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
const language = "en";
|
const locale = "en";
|
||||||
|
|
||||||
interface IUseSessionProps {
|
interface IUseSessionProps {
|
||||||
funnelId: string;
|
funnelId: string;
|
||||||
@ -26,6 +27,15 @@ export const useSession = ({ funnelId }: IUseSessionProps) => {
|
|||||||
|
|
||||||
const [isError, setIsError] = useState(false);
|
const [isError, setIsError] = useState(false);
|
||||||
|
|
||||||
|
const setSessionId = useCallback(
|
||||||
|
async (sessionId: string) => {
|
||||||
|
localStorage.setItem(localStorageKey, sessionId);
|
||||||
|
localStorage.setItem("activeSessionId", sessionId);
|
||||||
|
await setSessionIdToCookie("activeSessionId", sessionId);
|
||||||
|
},
|
||||||
|
[localStorageKey]
|
||||||
|
);
|
||||||
|
|
||||||
const createSession =
|
const createSession =
|
||||||
useCallback(async (): Promise<ICreateSessionResponse> => {
|
useCallback(async (): Promise<ICreateSessionResponse> => {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
@ -35,6 +45,7 @@ export const useSession = ({ funnelId }: IUseSessionProps) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (sessionId?.length) {
|
if (sessionId?.length) {
|
||||||
|
setSessionId(sessionId);
|
||||||
return {
|
return {
|
||||||
sessionId,
|
sessionId,
|
||||||
status: "old",
|
status: "old",
|
||||||
@ -43,7 +54,7 @@ export const useSession = ({ funnelId }: IUseSessionProps) => {
|
|||||||
try {
|
try {
|
||||||
const utm = parseQueryParams();
|
const utm = parseQueryParams();
|
||||||
const sessionParams = {
|
const sessionParams = {
|
||||||
locale: language,
|
locale,
|
||||||
timezone,
|
timezone,
|
||||||
// source: funnelId,
|
// source: funnelId,
|
||||||
source: "aura.compatibility.v2",
|
source: "aura.compatibility.v2",
|
||||||
@ -58,7 +69,7 @@ export const useSession = ({ funnelId }: IUseSessionProps) => {
|
|||||||
sessionFromServer?.sessionId?.length &&
|
sessionFromServer?.sessionId?.length &&
|
||||||
sessionFromServer?.status === "success"
|
sessionFromServer?.status === "success"
|
||||||
) {
|
) {
|
||||||
localStorage.setItem(localStorageKey, sessionFromServer.sessionId);
|
await setSessionId(sessionFromServer.sessionId);
|
||||||
return sessionFromServer;
|
return sessionFromServer;
|
||||||
}
|
}
|
||||||
console.error(
|
console.error(
|
||||||
@ -78,7 +89,7 @@ export const useSession = ({ funnelId }: IUseSessionProps) => {
|
|||||||
sessionId: "",
|
sessionId: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [localStorageKey, timezone, sessionId]);
|
}, [sessionId, timezone, setSessionId]);
|
||||||
// localStorageKey, sessionId, timezone, utm
|
// localStorageKey, sessionId, timezone, utm
|
||||||
|
|
||||||
const updateSession = useCallback(
|
const updateSession = useCallback(
|
||||||
|
|||||||
@ -15,7 +15,16 @@ import type { LayoutQuestionProps } from "@/components/layout/LayoutQuestion/Lay
|
|||||||
import type { ActionButtonProps } from "@/components/ui/ActionButton/ActionButton";
|
import type { ActionButtonProps } from "@/components/ui/ActionButton/ActionButton";
|
||||||
import type { BottomActionButtonProps } from "@/components/widgets/BottomActionButton/BottomActionButton";
|
import type { BottomActionButtonProps } from "@/components/widgets/BottomActionButton/BottomActionButton";
|
||||||
|
|
||||||
type TypographyAs = "span" | "p" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "div";
|
type TypographyAs =
|
||||||
|
| "span"
|
||||||
|
| "p"
|
||||||
|
| "h1"
|
||||||
|
| "h2"
|
||||||
|
| "h3"
|
||||||
|
| "h4"
|
||||||
|
| "h5"
|
||||||
|
| "h6"
|
||||||
|
| "div";
|
||||||
|
|
||||||
interface TypographyDefaults {
|
interface TypographyDefaults {
|
||||||
font?: TypographyVariant["font"];
|
font?: TypographyVariant["font"];
|
||||||
@ -39,7 +48,7 @@ export function buildTypographyProps<T extends TypographyAs>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем поле show - если false, не показываем
|
// Проверяем поле show - если false, не показываем
|
||||||
if ('show' in variant && variant.show === false) {
|
if ("show" in variant && variant.show === false) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,7 +68,7 @@ export function buildTypographyProps<T extends TypographyAs>(
|
|||||||
align: variant.align ?? defaults?.align,
|
align: variant.align ?? defaults?.align,
|
||||||
color: variant.color ?? defaults?.color,
|
color: variant.color ?? defaults?.color,
|
||||||
className: variant.className,
|
className: variant.className,
|
||||||
enableMarkup: hasTextMarkup(variant.text || ''), // 🎨 АВТОМАТИЧЕСКИ включаем разметку если обнаружена
|
enableMarkup: hasTextMarkup(variant.text || ""), // 🎨 АВТОМАТИЧЕСКИ включаем разметку если обнаружена
|
||||||
} as TypographyProps<T>;
|
} as TypographyProps<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,7 +80,8 @@ export function buildHeaderProgress(progress?: HeaderProgressDefinition) {
|
|||||||
const { current, total, value, label, className } = progress;
|
const { current, total, value, label, className } = progress;
|
||||||
|
|
||||||
const computedValue =
|
const computedValue =
|
||||||
value ?? (current !== undefined && total ? (current / total) * 100 : undefined);
|
value ??
|
||||||
|
(current !== undefined && total ? (current / total) * 100 : undefined);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: computedValue,
|
value: computedValue,
|
||||||
@ -90,14 +100,14 @@ export function buildAutoHeaderProgress(
|
|||||||
if (explicitProgress) {
|
if (explicitProgress) {
|
||||||
return buildHeaderProgress(explicitProgress);
|
return buildHeaderProgress(explicitProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, auto-calculate
|
// Otherwise, auto-calculate
|
||||||
const autoProgress: HeaderProgressDefinition = {
|
const autoProgress: HeaderProgressDefinition = {
|
||||||
current: currentPosition,
|
current: currentPosition,
|
||||||
total: totalScreens,
|
total: totalScreens,
|
||||||
label: `${currentPosition} of ${totalScreens}`,
|
label: `${currentPosition} of ${totalScreens}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
return buildHeaderProgress(autoProgress);
|
return buildHeaderProgress(autoProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,7 +123,10 @@ export function mapListOptionsToButtons(
|
|||||||
disabled: option.disabled,
|
disabled: option.disabled,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
export function shouldShowBackButton(header?: HeaderDefinition, canGoBack?: boolean) {
|
export function shouldShowBackButton(
|
||||||
|
header?: HeaderDefinition,
|
||||||
|
canGoBack?: boolean
|
||||||
|
) {
|
||||||
if (header?.showBackButton === false) {
|
if (header?.showBackButton === false) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -129,6 +142,7 @@ export function shouldShowHeader(header?: HeaderDefinition) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface BuildActionButtonOptions {
|
interface BuildActionButtonOptions {
|
||||||
|
children?: React.ReactNode;
|
||||||
defaultText?: string;
|
defaultText?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
@ -139,9 +153,11 @@ export function buildActionButtonProps(
|
|||||||
buttonDef?: BottomActionButtonDefinition
|
buttonDef?: BottomActionButtonDefinition
|
||||||
): ActionButtonProps {
|
): ActionButtonProps {
|
||||||
const { defaultText = "Continue", disabled = false, onClick } = options;
|
const { defaultText = "Continue", disabled = false, onClick } = options;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
children: buttonDef?.text ?? defaultText,
|
children: options.children
|
||||||
|
? options.children
|
||||||
|
: buttonDef?.text ?? defaultText,
|
||||||
cornerRadius: buttonDef?.cornerRadius,
|
cornerRadius: buttonDef?.cornerRadius,
|
||||||
disabled: disabled, // disabled управляется только логикой экрана, не админкой
|
disabled: disabled, // disabled управляется только логикой экрана, не админкой
|
||||||
onClick: disabled ? undefined : onClick,
|
onClick: disabled ? undefined : onClick,
|
||||||
@ -156,10 +172,10 @@ export function buildBottomActionButtonProps(
|
|||||||
if (buttonDef?.show === false) {
|
if (buttonDef?.show === false) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// В остальных случаях показать кнопку с градиентом
|
// В остальных случаях показать кнопку с градиентом
|
||||||
const actionButtonProps = buildActionButtonProps(options, buttonDef);
|
const actionButtonProps = buildActionButtonProps(options, buttonDef);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
actionButtonProps,
|
actionButtonProps,
|
||||||
showGradientBlur: true, // Градиент всегда включен (как требовалось)
|
showGradientBlur: true, // Градиент всегда включен (как требовалось)
|
||||||
@ -178,13 +194,18 @@ interface BuildLayoutQuestionOptions {
|
|||||||
export function buildLayoutQuestionProps(
|
export function buildLayoutQuestionProps(
|
||||||
options: BuildLayoutQuestionOptions
|
options: BuildLayoutQuestionOptions
|
||||||
): Omit<LayoutQuestionProps, "children"> {
|
): Omit<LayoutQuestionProps, "children"> {
|
||||||
const {
|
const {
|
||||||
screen,
|
screen,
|
||||||
titleDefaults = { font: "manrope", weight: "bold", align: "left" },
|
titleDefaults = { font: "manrope", weight: "bold", align: "left" },
|
||||||
subtitleDefaults = { font: "inter", weight: "medium", color: "muted", align: "left" },
|
subtitleDefaults = {
|
||||||
canGoBack,
|
font: "inter",
|
||||||
onBack,
|
weight: "medium",
|
||||||
screenProgress
|
color: "muted",
|
||||||
|
align: "left",
|
||||||
|
},
|
||||||
|
canGoBack,
|
||||||
|
onBack,
|
||||||
|
screenProgress,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const showBackButton = shouldShowBackButton(screen.header, canGoBack);
|
const showBackButton = shouldShowBackButton(screen.header, canGoBack);
|
||||||
@ -192,25 +213,34 @@ export function buildLayoutQuestionProps(
|
|||||||
const showProgress = shouldShowProgress(screen.header);
|
const showProgress = shouldShowProgress(screen.header);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
headerProps: showHeader ? {
|
headerProps: showHeader
|
||||||
progressProps: showProgress ? (
|
? {
|
||||||
screenProgress ? buildHeaderProgress({
|
progressProps: showProgress
|
||||||
current: screenProgress.current,
|
? screenProgress
|
||||||
total: screenProgress.total,
|
? buildHeaderProgress({
|
||||||
label: `${screenProgress.current} of ${screenProgress.total}`
|
current: screenProgress.current,
|
||||||
}) : buildHeaderProgress(screen.header?.progress)
|
total: screenProgress.total,
|
||||||
) : undefined,
|
label: `${screenProgress.current} of ${screenProgress.total}`,
|
||||||
onBack: showBackButton ? onBack : undefined,
|
})
|
||||||
showBackButton,
|
: buildHeaderProgress(screen.header?.progress)
|
||||||
} : undefined,
|
: undefined,
|
||||||
title: screen.title ? buildTypographyProps(screen.title, {
|
onBack: showBackButton ? onBack : undefined,
|
||||||
as: "h2",
|
showBackButton,
|
||||||
defaults: titleDefaults,
|
}
|
||||||
}) : undefined,
|
: undefined,
|
||||||
subtitle: 'subtitle' in screen ? buildTypographyProps(screen.subtitle, {
|
title: screen.title
|
||||||
as: "p",
|
? buildTypographyProps(screen.title, {
|
||||||
defaults: subtitleDefaults,
|
as: "h2",
|
||||||
}) : undefined,
|
defaults: titleDefaults,
|
||||||
|
})
|
||||||
|
: undefined,
|
||||||
|
subtitle:
|
||||||
|
"subtitle" in screen
|
||||||
|
? buildTypographyProps(screen.subtitle, {
|
||||||
|
as: "p",
|
||||||
|
defaults: subtitleDefaults,
|
||||||
|
})
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,11 +255,10 @@ export function buildTemplateBottomActionButtonProps(options: {
|
|||||||
// Принудительно включаем кнопку независимо от screen.bottomActionButton.show
|
// Принудительно включаем кнопку независимо от screen.bottomActionButton.show
|
||||||
return buildBottomActionButtonProps(
|
return buildBottomActionButtonProps(
|
||||||
actionButtonOptions,
|
actionButtonOptions,
|
||||||
'bottomActionButton' in screen
|
"bottomActionButton" in screen
|
||||||
? (screen.bottomActionButton?.show === false
|
? screen.bottomActionButton?.show === false
|
||||||
? { ...screen.bottomActionButton, show: true }
|
? { ...screen.bottomActionButton, show: true }
|
||||||
: screen.bottomActionButton)
|
: screen.bottomActionButton
|
||||||
: undefined
|
: undefined
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,9 +23,11 @@ import type {
|
|||||||
SoulmatePortraitScreenDefinition,
|
SoulmatePortraitScreenDefinition,
|
||||||
ScreenDefinition,
|
ScreenDefinition,
|
||||||
DefaultTexts,
|
DefaultTexts,
|
||||||
|
FunnelDefinition,
|
||||||
} from "@/lib/funnel/types";
|
} from "@/lib/funnel/types";
|
||||||
|
|
||||||
export interface ScreenRenderProps {
|
export interface ScreenRenderProps {
|
||||||
|
funnel: FunnelDefinition;
|
||||||
screen: ScreenDefinition;
|
screen: ScreenDefinition;
|
||||||
selectedOptionIds: string[];
|
selectedOptionIds: string[];
|
||||||
onSelectionChange: (ids: string[]) => void;
|
onSelectionChange: (ids: string[]) => void;
|
||||||
@ -38,8 +40,18 @@ export interface ScreenRenderProps {
|
|||||||
|
|
||||||
export type TemplateRenderer = (props: ScreenRenderProps) => JSX.Element;
|
export type TemplateRenderer = (props: ScreenRenderProps) => JSX.Element;
|
||||||
|
|
||||||
const TEMPLATE_REGISTRY: Record<ScreenDefinition["template"], TemplateRenderer> = {
|
const TEMPLATE_REGISTRY: Record<
|
||||||
info: ({ screen, onContinue, canGoBack, onBack, screenProgress, defaultTexts }) => {
|
ScreenDefinition["template"],
|
||||||
|
TemplateRenderer
|
||||||
|
> = {
|
||||||
|
info: ({
|
||||||
|
screen,
|
||||||
|
onContinue,
|
||||||
|
canGoBack,
|
||||||
|
onBack,
|
||||||
|
screenProgress,
|
||||||
|
defaultTexts,
|
||||||
|
}) => {
|
||||||
const infoScreen = screen as InfoScreenDefinition;
|
const infoScreen = screen as InfoScreenDefinition;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -53,9 +65,18 @@ const TEMPLATE_REGISTRY: Record<ScreenDefinition["template"], TemplateRenderer>
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
date: ({ screen, selectedOptionIds, onSelectionChange, onContinue, canGoBack, onBack, screenProgress, defaultTexts }) => {
|
date: ({
|
||||||
|
screen,
|
||||||
|
selectedOptionIds,
|
||||||
|
onSelectionChange,
|
||||||
|
onContinue,
|
||||||
|
canGoBack,
|
||||||
|
onBack,
|
||||||
|
screenProgress,
|
||||||
|
defaultTexts,
|
||||||
|
}) => {
|
||||||
const dateScreen = screen as DateScreenDefinition;
|
const dateScreen = screen as DateScreenDefinition;
|
||||||
|
|
||||||
// For date screens, we store date components as array: [month, day, year]
|
// For date screens, we store date components as array: [month, day, year]
|
||||||
const currentDateArray = selectedOptionIds;
|
const currentDateArray = selectedOptionIds;
|
||||||
const selectedDate = {
|
const selectedDate = {
|
||||||
@ -64,7 +85,11 @@ const TEMPLATE_REGISTRY: Record<ScreenDefinition["template"], TemplateRenderer>
|
|||||||
year: currentDateArray[2] || "",
|
year: currentDateArray[2] || "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDateChange = (date: { month?: string; day?: string; year?: string }) => {
|
const handleDateChange = (date: {
|
||||||
|
month?: string;
|
||||||
|
day?: string;
|
||||||
|
year?: string;
|
||||||
|
}) => {
|
||||||
const dateArray = [date.month || "", date.day || "", date.year || ""];
|
const dateArray = [date.month || "", date.day || "", date.year || ""];
|
||||||
onSelectionChange(dateArray);
|
onSelectionChange(dateArray);
|
||||||
};
|
};
|
||||||
@ -82,13 +107,22 @@ const TEMPLATE_REGISTRY: Record<ScreenDefinition["template"], TemplateRenderer>
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
form: ({ screen, selectedOptionIds, onSelectionChange, onContinue, canGoBack, onBack, screenProgress, defaultTexts }) => {
|
form: ({
|
||||||
|
screen,
|
||||||
|
selectedOptionIds,
|
||||||
|
onSelectionChange,
|
||||||
|
onContinue,
|
||||||
|
canGoBack,
|
||||||
|
onBack,
|
||||||
|
screenProgress,
|
||||||
|
defaultTexts,
|
||||||
|
}) => {
|
||||||
const formScreen = screen as FormScreenDefinition;
|
const formScreen = screen as FormScreenDefinition;
|
||||||
|
|
||||||
// For form screens, we store form data as JSON string in the first element
|
// For form screens, we store form data as JSON string in the first element
|
||||||
const formDataJson = selectedOptionIds[0] || "{}";
|
const formDataJson = selectedOptionIds[0] || "{}";
|
||||||
let formData: Record<string, string> = {};
|
let formData: Record<string, string> = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
formData = JSON.parse(formDataJson);
|
formData = JSON.parse(formDataJson);
|
||||||
} catch {
|
} catch {
|
||||||
@ -113,7 +147,14 @@ const TEMPLATE_REGISTRY: Record<ScreenDefinition["template"], TemplateRenderer>
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
coupon: ({ screen, onContinue, canGoBack, onBack, screenProgress, defaultTexts }) => {
|
coupon: ({
|
||||||
|
screen,
|
||||||
|
onContinue,
|
||||||
|
canGoBack,
|
||||||
|
onBack,
|
||||||
|
screenProgress,
|
||||||
|
defaultTexts,
|
||||||
|
}) => {
|
||||||
const couponScreen = screen as CouponScreenDefinition;
|
const couponScreen = screen as CouponScreenDefinition;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -143,16 +184,17 @@ const TEMPLATE_REGISTRY: Record<ScreenDefinition["template"], TemplateRenderer>
|
|||||||
// Используем только общую кнопку экрана
|
// Используем только общую кнопку экрана
|
||||||
const bottomActionButton = listScreen.bottomActionButton;
|
const bottomActionButton = listScreen.bottomActionButton;
|
||||||
const isButtonDisabled = bottomActionButton?.show === false;
|
const isButtonDisabled = bottomActionButton?.show === false;
|
||||||
|
|
||||||
// Простая логика: кнопка есть если не отключена (show: false)
|
// Простая логика: кнопка есть если не отключена (show: false)
|
||||||
const hasActionButton = !isButtonDisabled;
|
const hasActionButton = !isButtonDisabled;
|
||||||
|
|
||||||
// Правильная логика приоритетов для текста кнопки:
|
// Правильная логика приоритетов для текста кнопки:
|
||||||
// 1. bottomActionButton.text (настройка экрана)
|
// 1. bottomActionButton.text (настройка экрана)
|
||||||
// 2. defaultTexts.nextButton (глобальная настройка воронки)
|
// 2. defaultTexts.nextButton (глобальная настройка воронки)
|
||||||
// 3. "Next" (хардкод fallback)
|
// 3. "Next" (хардкод fallback)
|
||||||
const buttonText = bottomActionButton?.text || defaultTexts?.nextButton || "Next";
|
const buttonText =
|
||||||
|
bottomActionButton?.text || defaultTexts?.nextButton || "Next";
|
||||||
|
|
||||||
const actionDisabled = hasActionButton && isSelectionEmpty;
|
const actionDisabled = hasActionButton && isSelectionEmpty;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -160,22 +202,34 @@ const TEMPLATE_REGISTRY: Record<ScreenDefinition["template"], TemplateRenderer>
|
|||||||
screen={listScreen}
|
screen={listScreen}
|
||||||
selectedOptionIds={selectedOptionIds}
|
selectedOptionIds={selectedOptionIds}
|
||||||
onSelectionChange={onSelectionChange}
|
onSelectionChange={onSelectionChange}
|
||||||
actionButtonProps={hasActionButton
|
actionButtonProps={
|
||||||
? {
|
hasActionButton
|
||||||
children: buttonText,
|
? {
|
||||||
disabled: actionDisabled,
|
children: buttonText,
|
||||||
onClick: actionDisabled ? undefined : onContinue,
|
disabled: actionDisabled,
|
||||||
}
|
onClick: actionDisabled ? undefined : onContinue,
|
||||||
: undefined}
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
canGoBack={canGoBack}
|
canGoBack={canGoBack}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
screenProgress={screenProgress}
|
screenProgress={screenProgress}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
email: ({ screen, selectedOptionIds, onSelectionChange, onContinue, canGoBack, onBack, screenProgress, defaultTexts }) => {
|
email: ({
|
||||||
|
screen,
|
||||||
|
selectedOptionIds,
|
||||||
|
onSelectionChange,
|
||||||
|
onContinue,
|
||||||
|
canGoBack,
|
||||||
|
onBack,
|
||||||
|
screenProgress,
|
||||||
|
defaultTexts,
|
||||||
|
funnel,
|
||||||
|
}) => {
|
||||||
const emailScreen = screen as EmailScreenDefinition;
|
const emailScreen = screen as EmailScreenDefinition;
|
||||||
|
|
||||||
// For email screens, we store email as single string in first element
|
// For email screens, we store email as single string in first element
|
||||||
const selectedEmail = selectedOptionIds[0] || "";
|
const selectedEmail = selectedOptionIds[0] || "";
|
||||||
|
|
||||||
@ -193,10 +247,18 @@ const TEMPLATE_REGISTRY: Record<ScreenDefinition["template"], TemplateRenderer>
|
|||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
screenProgress={screenProgress}
|
screenProgress={screenProgress}
|
||||||
defaultTexts={defaultTexts}
|
defaultTexts={defaultTexts}
|
||||||
|
funnel={funnel}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loaders: ({ screen, onContinue, canGoBack, onBack, screenProgress, defaultTexts }) => {
|
loaders: ({
|
||||||
|
screen,
|
||||||
|
onContinue,
|
||||||
|
canGoBack,
|
||||||
|
onBack,
|
||||||
|
screenProgress,
|
||||||
|
defaultTexts,
|
||||||
|
}) => {
|
||||||
const loadersScreen = screen as LoadersScreenDefinition;
|
const loadersScreen = screen as LoadersScreenDefinition;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -210,7 +272,14 @@ const TEMPLATE_REGISTRY: Record<ScreenDefinition["template"], TemplateRenderer>
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
soulmate: ({ screen, onContinue, canGoBack, onBack, screenProgress, defaultTexts }) => {
|
soulmate: ({
|
||||||
|
screen,
|
||||||
|
onContinue,
|
||||||
|
canGoBack,
|
||||||
|
onBack,
|
||||||
|
screenProgress,
|
||||||
|
defaultTexts,
|
||||||
|
}) => {
|
||||||
const soulmateScreen = screen as SoulmatePortraitScreenDefinition;
|
const soulmateScreen = screen as SoulmatePortraitScreenDefinition;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -234,7 +303,9 @@ export function renderScreen(props: ScreenRenderProps): JSX.Element {
|
|||||||
return renderer(props);
|
return renderer(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTemplateRenderer(screen: ScreenDefinition): TemplateRenderer {
|
export function getTemplateRenderer(
|
||||||
|
screen: ScreenDefinition
|
||||||
|
): TemplateRenderer {
|
||||||
const renderer = TEMPLATE_REGISTRY[screen.template];
|
const renderer = TEMPLATE_REGISTRY[screen.template];
|
||||||
if (!renderer) {
|
if (!renderer) {
|
||||||
throw new Error(`Unsupported template: ${screen.template}`);
|
throw new Error(`Unsupported template: ${screen.template}`);
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
/**
|
/**
|
||||||
* Helper функции для упрощения работы с темплейтами воронки
|
* Helper функции для упрощения работы с темплейтами воронки
|
||||||
*
|
*
|
||||||
* Эти функции помогают избежать дублирования кода при создании props
|
* Эти функции помогают избежать дублирования кода при создании props
|
||||||
* для TemplateLayout компонента
|
* для TemplateLayout компонента
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ScreenDefinition, TypographyVariant } from "./types";
|
import type { ScreenDefinition, TypographyVariant } from "./types";
|
||||||
import { TEMPLATE_DEFAULTS, TEMPLATE_DEFAULTS_CENTERED } from "@/components/funnel/templates/constants";
|
import {
|
||||||
|
TEMPLATE_DEFAULTS,
|
||||||
|
TEMPLATE_DEFAULTS_CENTERED,
|
||||||
|
} from "@/components/funnel/templates/constants";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Тип preset для быстрого выбора стиля темплейта
|
* Тип preset для быстрого выбора стиля темплейта
|
||||||
@ -17,6 +20,7 @@ export type TemplatePreset = "left" | "center";
|
|||||||
* Конфигурация action кнопки для темплейта
|
* Конфигурация action кнопки для темплейта
|
||||||
*/
|
*/
|
||||||
export interface ActionButtonConfig {
|
export interface ActionButtonConfig {
|
||||||
|
children?: React.ReactNode;
|
||||||
defaultText: string;
|
defaultText: string;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
@ -72,11 +76,11 @@ export interface TemplateNavigation {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper функция для создания props для TemplateLayout компонента
|
* Helper функция для создания props для TemplateLayout компонента
|
||||||
*
|
*
|
||||||
* Упрощает создание темплейтов, предоставляя единообразный способ
|
* Упрощает создание темплейтов, предоставляя единообразный способ
|
||||||
* настройки всех параметров с использованием preset-ов и опциональных
|
* настройки всех параметров с использованием preset-ов и опциональных
|
||||||
* переопределений
|
* переопределений
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* const layoutProps = createTemplateLayoutProps(
|
* const layoutProps = createTemplateLayoutProps(
|
||||||
@ -92,7 +96,7 @@ export interface TemplateNavigation {
|
|||||||
* },
|
* },
|
||||||
* }
|
* }
|
||||||
* );
|
* );
|
||||||
*
|
*
|
||||||
* return <TemplateLayout {...layoutProps}>{children}</TemplateLayout>;
|
* return <TemplateLayout {...layoutProps}>{children}</TemplateLayout>;
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@ -103,9 +107,10 @@ export function createTemplateLayoutProps(
|
|||||||
options?: CreateTemplateLayoutOptions
|
options?: CreateTemplateLayoutOptions
|
||||||
) {
|
) {
|
||||||
// Выбираем preset на основе опций
|
// Выбираем preset на основе опций
|
||||||
const defaults = options?.preset === "center"
|
const defaults =
|
||||||
? TEMPLATE_DEFAULTS_CENTERED
|
options?.preset === "center"
|
||||||
: TEMPLATE_DEFAULTS;
|
? TEMPLATE_DEFAULTS_CENTERED
|
||||||
|
: TEMPLATE_DEFAULTS;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
screen,
|
screen,
|
||||||
|
|||||||
@ -79,6 +79,21 @@ class HttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
|
|
||||||
|
let sessionId: string | null = null;
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
const { getServerSessionId } = await import("../session/sessionId");
|
||||||
|
sessionId = (await getServerSessionId()) ?? null;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const { getClientSessionId } = await import("../session/sessionId");
|
||||||
|
sessionId = getClientSessionId();
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sessionId) headers.set("session-id", sessionId);
|
||||||
|
|
||||||
let accessToken: string | undefined;
|
let accessToken: string | undefined;
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
const { getServerAccessToken } = await import("../auth/token");
|
const { getServerAccessToken } = await import("../auth/token");
|
||||||
|
|||||||
@ -1,24 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the access token from client-side cookies
|
|
||||||
* @returns The access token or undefined if not found
|
|
||||||
*/
|
|
||||||
export function getClientAccessToken(): string | undefined {
|
|
||||||
if (typeof document === "undefined") {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cookies = document.cookie.split(";");
|
|
||||||
const accessTokenCookie = cookies.find(cookie =>
|
|
||||||
cookie.trim().startsWith("accessToken=")
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!accessTokenCookie) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return decodeURIComponent(
|
|
||||||
accessTokenCookie.trim().substring("accessToken=".length)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -10,4 +10,5 @@ const createRoute = (
|
|||||||
|
|
||||||
export const API_ROUTES = {
|
export const API_ROUTES = {
|
||||||
session: (id?: string) => createRoute(["session", id], ROOT_ROUTE_V2),
|
session: (id?: string) => createRoute(["session", id], ROOT_ROUTE_V2),
|
||||||
|
authorization: () => createRoute(["users", "auth"]),
|
||||||
};
|
};
|
||||||
|
|||||||
12
src/shared/session/sessionId.ts
Normal file
12
src/shared/session/sessionId.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// Server-side token functions (only for Server Components)
|
||||||
|
export async function getServerSessionId() {
|
||||||
|
const { cookies } = await import("next/headers");
|
||||||
|
return (await cookies()).get("activeSessionId")?.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client-side token functions
|
||||||
|
export function getClientSessionId(): string | null {
|
||||||
|
if (typeof window === "undefined") return null;
|
||||||
|
|
||||||
|
return localStorage.getItem("activeSessionId");
|
||||||
|
}
|
||||||
31
src/shared/utils/filter-object/index.ts
Normal file
31
src/shared/utils/filter-object/index.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
export function filterNullKeysOfObject<T extends object>(object: T): T {
|
||||||
|
if (typeof object !== "object") {
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
return Object.keys(object)
|
||||||
|
.filter((key) => {
|
||||||
|
if (
|
||||||
|
typeof object[key as keyof T] === "object" &&
|
||||||
|
object[key as keyof T] !== null
|
||||||
|
) {
|
||||||
|
return Object.keys(object[key as keyof T] as object).length;
|
||||||
|
}
|
||||||
|
if (typeof object[key as keyof T] === "string") {
|
||||||
|
return !!(object[key as keyof T] as string).length;
|
||||||
|
}
|
||||||
|
return object[key as keyof T] !== null;
|
||||||
|
})
|
||||||
|
.reduce(
|
||||||
|
(acc, key) => {
|
||||||
|
return Object.assign(acc, {
|
||||||
|
[key]:
|
||||||
|
typeof object[key as keyof T] === "object"
|
||||||
|
? filterNullKeysOfObject<typeof object>(
|
||||||
|
object[key as keyof object]
|
||||||
|
)
|
||||||
|
: object[key as keyof T],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
Array.isArray(object) ? [] : {}
|
||||||
|
) as T;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user