Add zodiac calculation support for date screens
This commit is contained in:
parent
0fc1dc756e
commit
da3c53f211
@ -12,7 +12,10 @@ interface DateScreenConfigProps {
|
|||||||
export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
||||||
const dateScreen = screen as DateScreenDefinition & { position: { x: number; y: number } };
|
const dateScreen = screen as DateScreenDefinition & { position: { x: number; y: number } };
|
||||||
|
|
||||||
const handleDateInputChange = <T extends keyof DateScreenDefinition["dateInput"]>(field: T, value: string | boolean) => {
|
const handleDateInputChange = <T extends keyof DateScreenDefinition["dateInput"]>(
|
||||||
|
field: T,
|
||||||
|
value: DateScreenDefinition["dateInput"][T]
|
||||||
|
) => {
|
||||||
onUpdate({
|
onUpdate({
|
||||||
dateInput: {
|
dateInput: {
|
||||||
...dateScreen.dateInput,
|
...dateScreen.dateInput,
|
||||||
@ -21,6 +24,30 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleZodiacSettingsChange = (
|
||||||
|
updates: Partial<NonNullable<DateScreenDefinition["dateInput"]["zodiac"]>>
|
||||||
|
) => {
|
||||||
|
const currentZodiac = dateScreen.dateInput?.zodiac ?? {
|
||||||
|
enabled: false,
|
||||||
|
storageKey: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextZodiac = {
|
||||||
|
...currentZodiac,
|
||||||
|
...updates,
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldRemove =
|
||||||
|
(nextZodiac.enabled ?? false) === false && (nextZodiac.storageKey ?? "") === "";
|
||||||
|
|
||||||
|
onUpdate({
|
||||||
|
dateInput: {
|
||||||
|
...dateScreen.dateInput,
|
||||||
|
zodiac: shouldRemove ? undefined : nextZodiac,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleInfoMessageChange = (field: "text" | "icon", value: string) => {
|
const handleInfoMessageChange = (field: "text" | "icon", value: string) => {
|
||||||
const baseInfo = dateScreen.infoMessage ?? { text: "", icon: "ℹ️" };
|
const baseInfo = dateScreen.infoMessage ?? { text: "", icon: "ℹ️" };
|
||||||
const nextInfo = { ...baseInfo, [field]: value };
|
const nextInfo = { ...baseInfo, [field]: value };
|
||||||
@ -99,6 +126,37 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
|||||||
Показывать выбранную дату под полем
|
Показывать выбранную дату под полем
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<div className="rounded-xl border border-border/60 p-4">
|
||||||
|
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={dateScreen.dateInput?.zodiac?.enabled === true}
|
||||||
|
onChange={(event) =>
|
||||||
|
handleZodiacSettingsChange({ enabled: event.target.checked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Автоматически определять знак зодиака
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mt-2 text-xs text-muted-foreground">
|
||||||
|
Если включено, система вычислит знак зодиака по выбранной дате и сохранит его по
|
||||||
|
указанному ключу. Значение можно использовать в правилах навигации и вариативности.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{dateScreen.dateInput?.zodiac?.enabled && (
|
||||||
|
<label className="mt-3 flex flex-col gap-1 text-xs text-muted-foreground">
|
||||||
|
Ключ для сохранения знака зодиака
|
||||||
|
<TextInput
|
||||||
|
placeholder="Например, userZodiac"
|
||||||
|
value={dateScreen.dateInput?.zodiac?.storageKey ?? ""}
|
||||||
|
onChange={(event) =>
|
||||||
|
handleZodiacSettingsChange({ storageKey: event.target.value.trim() })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3 text-xs">
|
<div className="grid grid-cols-2 gap-3 text-xs">
|
||||||
<label className="flex flex-col gap-1 text-muted-foreground">
|
<label className="flex flex-col gap-1 text-muted-foreground">
|
||||||
Подпись выбранной даты
|
Подпись выбранной даты
|
||||||
|
|||||||
@ -11,7 +11,9 @@ import type {
|
|||||||
FunnelDefinition,
|
FunnelDefinition,
|
||||||
FunnelAnswers,
|
FunnelAnswers,
|
||||||
ListScreenDefinition,
|
ListScreenDefinition,
|
||||||
|
DateScreenDefinition,
|
||||||
} from "@/lib/funnel/types";
|
} from "@/lib/funnel/types";
|
||||||
|
import { getZodiacSign } from "@/lib/funnel/zodiac";
|
||||||
|
|
||||||
// Функция для оценки длины пути пользователя на основе текущих ответов
|
// Функция для оценки длины пути пользователя на основе текущих ответов
|
||||||
function estimatePathLength(funnel: FunnelDefinition, answers: FunnelAnswers): number {
|
function estimatePathLength(funnel: FunnelDefinition, answers: FunnelAnswers): number {
|
||||||
@ -152,6 +154,31 @@ export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) {
|
|||||||
setAnswers(currentScreen.id, ids);
|
setAnswers(currentScreen.id, ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentScreen.template === "date") {
|
||||||
|
const dateScreen = currentScreen as DateScreenDefinition;
|
||||||
|
const zodiacSettings = dateScreen.dateInput?.zodiac;
|
||||||
|
const storageKey = zodiacSettings?.storageKey?.trim();
|
||||||
|
|
||||||
|
if (storageKey) {
|
||||||
|
if (zodiacSettings?.enabled) {
|
||||||
|
const [monthValue, dayValue] = ids;
|
||||||
|
const month = parseInt(monthValue ?? "", 10);
|
||||||
|
const day = parseInt(dayValue ?? "", 10);
|
||||||
|
const zodiac = Number.isNaN(month) || Number.isNaN(day)
|
||||||
|
? null
|
||||||
|
: getZodiacSign(month, day);
|
||||||
|
|
||||||
|
if (zodiac) {
|
||||||
|
setAnswers(storageKey, [zodiac]);
|
||||||
|
} else {
|
||||||
|
setAnswers(storageKey, []);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setAnswers(storageKey, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-advance for single selection without action button
|
// Auto-advance for single selection without action button
|
||||||
if (shouldAutoAdvance) {
|
if (shouldAutoAdvance) {
|
||||||
const nextScreenId = resolveNextScreenId(currentScreen, nextAnswers, funnel.screens);
|
const nextScreenId = resolveNextScreenId(currentScreen, nextAnswers, funnel.screens);
|
||||||
|
|||||||
@ -116,6 +116,10 @@ export interface DateInputDefinition {
|
|||||||
selectedDateFormat?: string; // e.g., "MMMM d, yyyy" for "April 8, 1987"
|
selectedDateFormat?: string; // e.g., "MMMM d, yyyy" for "April 8, 1987"
|
||||||
validationMessage?: string;
|
validationMessage?: string;
|
||||||
selectedDateLabel?: string; // "Выбранная дата:" text
|
selectedDateLabel?: string; // "Выбранная дата:" text
|
||||||
|
zodiac?: {
|
||||||
|
enabled?: boolean;
|
||||||
|
storageKey?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DateScreenDefinition {
|
export interface DateScreenDefinition {
|
||||||
|
|||||||
103
src/lib/funnel/zodiac.ts
Normal file
103
src/lib/funnel/zodiac.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
export type ZodiacSign =
|
||||||
|
| "capricorn"
|
||||||
|
| "aquarius"
|
||||||
|
| "pisces"
|
||||||
|
| "aries"
|
||||||
|
| "taurus"
|
||||||
|
| "gemini"
|
||||||
|
| "cancer"
|
||||||
|
| "leo"
|
||||||
|
| "virgo"
|
||||||
|
| "libra"
|
||||||
|
| "scorpio"
|
||||||
|
| "sagittarius";
|
||||||
|
|
||||||
|
interface ZodiacBoundary {
|
||||||
|
sign: ZodiacSign;
|
||||||
|
month: number;
|
||||||
|
day: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DAYS_BEFORE_MONTH = [
|
||||||
|
0, // January
|
||||||
|
31, // February
|
||||||
|
59, // March
|
||||||
|
90, // April
|
||||||
|
120, // May
|
||||||
|
151, // June
|
||||||
|
181, // July
|
||||||
|
212, // August
|
||||||
|
243, // September
|
||||||
|
273, // October
|
||||||
|
304, // November
|
||||||
|
334, // December
|
||||||
|
];
|
||||||
|
|
||||||
|
const ZODIAC_BOUNDARIES: ZodiacBoundary[] = [
|
||||||
|
{ sign: "capricorn", month: 1, day: 1 },
|
||||||
|
{ sign: "aquarius", month: 1, day: 20 },
|
||||||
|
{ sign: "pisces", month: 2, day: 19 },
|
||||||
|
{ sign: "aries", month: 3, day: 21 },
|
||||||
|
{ sign: "taurus", month: 4, day: 20 },
|
||||||
|
{ sign: "gemini", month: 5, day: 21 },
|
||||||
|
{ sign: "cancer", month: 6, day: 21 },
|
||||||
|
{ sign: "leo", month: 7, day: 23 },
|
||||||
|
{ sign: "virgo", month: 8, day: 23 },
|
||||||
|
{ sign: "libra", month: 9, day: 23 },
|
||||||
|
{ sign: "scorpio", month: 10, day: 23 },
|
||||||
|
{ sign: "sagittarius", month: 11, day: 22 },
|
||||||
|
{ sign: "capricorn", month: 12, day: 22 },
|
||||||
|
];
|
||||||
|
|
||||||
|
function isValidMonth(month: number): boolean {
|
||||||
|
return Number.isInteger(month) && month >= 1 && month <= 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDaysInMonth(month: number): number {
|
||||||
|
return new Date(2024, month, 0).getDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDayOfYear(month: number, day: number): number | null {
|
||||||
|
if (!isValidMonth(month)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxDay = getDaysInMonth(month);
|
||||||
|
if (!Number.isInteger(day) || day < 1 || day > maxDay) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DAYS_BEFORE_MONTH[month - 1] + day;
|
||||||
|
}
|
||||||
|
|
||||||
|
function boundaryToDayOfYear(boundary: ZodiacBoundary): number {
|
||||||
|
const dayOfYear = toDayOfYear(boundary.month, boundary.day);
|
||||||
|
if (dayOfYear === null) {
|
||||||
|
throw new Error(`Invalid zodiac boundary: ${boundary.sign}`);
|
||||||
|
}
|
||||||
|
return dayOfYear;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ZODIAC_BOUNDARIES_WITH_DAY = ZODIAC_BOUNDARIES.map((boundary) => ({
|
||||||
|
...boundary,
|
||||||
|
dayOfYear: boundaryToDayOfYear(boundary),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export function getZodiacSign(month: number, day: number): ZodiacSign | null {
|
||||||
|
const dayOfYear = toDayOfYear(month, day);
|
||||||
|
if (dayOfYear === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentSign: ZodiacSign = "capricorn";
|
||||||
|
|
||||||
|
for (const boundary of ZODIAC_BOUNDARIES_WITH_DAY) {
|
||||||
|
if (dayOfYear >= boundary.dayOfYear) {
|
||||||
|
currentSign = boundary.sign;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentSign;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user