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) {
|
||||
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({
|
||||
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 baseInfo = dateScreen.infoMessage ?? { text: "", icon: "ℹ️" };
|
||||
const nextInfo = { ...baseInfo, [field]: value };
|
||||
@ -99,6 +126,37 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
||||
Показывать выбранную дату под полем
|
||||
</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">
|
||||
<label className="flex flex-col gap-1 text-muted-foreground">
|
||||
Подпись выбранной даты
|
||||
|
||||
@ -11,7 +11,9 @@ import type {
|
||||
FunnelDefinition,
|
||||
FunnelAnswers,
|
||||
ListScreenDefinition,
|
||||
DateScreenDefinition,
|
||||
} from "@/lib/funnel/types";
|
||||
import { getZodiacSign } from "@/lib/funnel/zodiac";
|
||||
|
||||
// Функция для оценки длины пути пользователя на основе текущих ответов
|
||||
function estimatePathLength(funnel: FunnelDefinition, answers: FunnelAnswers): number {
|
||||
@ -152,6 +154,31 @@ export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) {
|
||||
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
|
||||
if (shouldAutoAdvance) {
|
||||
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"
|
||||
validationMessage?: string;
|
||||
selectedDateLabel?: string; // "Выбранная дата:" text
|
||||
zodiac?: {
|
||||
enabled?: boolean;
|
||||
storageKey?: string;
|
||||
};
|
||||
}
|
||||
|
||||
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