Add zodiac calculation support for date screens

This commit is contained in:
pennyteenycat 2025-09-27 23:05:47 +02:00
parent 0fc1dc756e
commit da3c53f211
4 changed files with 193 additions and 1 deletions

View File

@ -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">
Подпись выбранной даты

View File

@ -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);

View File

@ -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
View 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;
}