mirror of
https://github.com/JOYCEQL/magic-resume.git
synced 2026-07-03 14:07:11 +02:00
fix: update date format
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { DateInput } from "@heroui/date-input";
|
||||
import { HeroUIProvider } from "@heroui/react";
|
||||
import { CalendarDate, parseDate } from "@internationalized/date";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
|
||||
interface UnifiedDateInputProps {
|
||||
value: string;
|
||||
@@ -18,52 +18,53 @@ export function UnifiedDateInput({
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
placeholder,
|
||||
isRequired,
|
||||
className,
|
||||
}: UnifiedDateInputProps) {
|
||||
const t = useTranslations("common");
|
||||
|
||||
// Parse string "YYYY-MM" to CalendarDate
|
||||
// We use useMemo to calculate the initial value only once or when key changes
|
||||
// actually, we want to respect value updates if they are completely different (e.g. item switch)
|
||||
// but ignore them if they are just caused by our own null update.
|
||||
// Easiest robust way for now: Uncontrolled with defaultValue.
|
||||
// Assuming the component is remounted when switching items (verified by AnimatePresence in parents).
|
||||
const initialDate = useMemo(() => {
|
||||
if (!value) return null;
|
||||
const parseValue = (input: string): CalendarDate | null => {
|
||||
if (!input) return null;
|
||||
try {
|
||||
const dateStr = value.length === 7 ? `${value}-01` : value;
|
||||
return parseDate(dateStr);
|
||||
} catch (e) {
|
||||
let normalized = input.replace(/[./]/g, "-");
|
||||
if (normalized.length === 7) normalized = `${normalized}-01`;
|
||||
return parseDate(normalized);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}, []); // Empty dependency array to treat as mount-time config
|
||||
};
|
||||
|
||||
const [selectedDate, setSelectedDate] = useState<CalendarDate | null>(() =>
|
||||
parseValue(value)
|
||||
);
|
||||
|
||||
const handleDateChange = (date: CalendarDate | null) => {
|
||||
setSelectedDate(date);
|
||||
if (!date) {
|
||||
onChange("");
|
||||
return;
|
||||
}
|
||||
const month = date.month.toString().padStart(2, "0");
|
||||
onChange(`${date.year}-${month}`);
|
||||
onChange(`${date.year}/${month}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<DateInput
|
||||
label={label}
|
||||
defaultValue={initialDate}
|
||||
onChange={handleDateChange}
|
||||
isRequired={isRequired}
|
||||
granularity="month"
|
||||
variant="bordered"
|
||||
labelPlacement="outside"
|
||||
classNames={{
|
||||
label: "text-sm font-medium text-foreground",
|
||||
inputWrapper: "shadow-sm hover:border-primary/50 focus-within:ring-2 focus-within:ring-primary focus-within:border-primary bg-background",
|
||||
}}
|
||||
/>
|
||||
<HeroUIProvider locale="ja-JP">
|
||||
<DateInput
|
||||
label={label}
|
||||
value={selectedDate}
|
||||
onChange={handleDateChange}
|
||||
isRequired={isRequired}
|
||||
granularity="month"
|
||||
variant="bordered"
|
||||
labelPlacement="outside"
|
||||
shouldForceLeadingZeros
|
||||
classNames={{
|
||||
label: "text-sm font-medium text-foreground",
|
||||
inputWrapper:
|
||||
"shadow-sm hover:border-primary/50 focus-within:ring-2 focus-within:ring-primary focus-within:border-primary bg-background",
|
||||
}}
|
||||
/>
|
||||
</HeroUIProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { DateInput } from "@heroui/date-input";
|
||||
import { HeroUIProvider } from "@heroui/react";
|
||||
import { CalendarDate, parseDate } from "@internationalized/date";
|
||||
import { useMemo, useState, useEffect } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface UnifiedDateRangeInputProps {
|
||||
@@ -14,120 +14,128 @@ interface UnifiedDateRangeInputProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SEPARATOR = " - ";
|
||||
const PRESENT_VALUES = new Set(["至今", "Present", "Now"]);
|
||||
|
||||
const parsePart = (part: string): CalendarDate | null => {
|
||||
if (!part) return null;
|
||||
const cleanPart = part.trim();
|
||||
if (PRESENT_VALUES.has(cleanPart)) return null;
|
||||
|
||||
try {
|
||||
let isoStr = cleanPart.replace(/[./]/g, "-");
|
||||
if (isoStr.length === 7) isoStr += "-01";
|
||||
if (isoStr.length === 4) isoStr += "-01-01";
|
||||
return parseDate(isoStr);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const parseRange = (rangeValue: string) => {
|
||||
if (!rangeValue) return { start: null, end: null };
|
||||
|
||||
let startStr = "";
|
||||
let endStr = "";
|
||||
|
||||
if (rangeValue.includes(SEPARATOR)) {
|
||||
[startStr, endStr] = rangeValue.split(SEPARATOR);
|
||||
} else {
|
||||
const match = rangeValue.match(/^([^\s]+)\s*(?:-|–|—)\s*([^\s]+)$/);
|
||||
if (match) {
|
||||
startStr = match[1];
|
||||
endStr = match[2];
|
||||
} else {
|
||||
startStr = rangeValue;
|
||||
}
|
||||
}
|
||||
|
||||
return { start: parsePart(startStr), end: parsePart(endStr) };
|
||||
};
|
||||
|
||||
export function UnifiedDateRangeInput({
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
className,
|
||||
}: UnifiedDateRangeInputProps) {
|
||||
// Parsing logic
|
||||
// Expected formats: "YYYY.MM - YYYY.MM"
|
||||
const t = useTranslations("common");
|
||||
|
||||
// Helper to normalize separator
|
||||
const SEPARATOR = " - ";
|
||||
|
||||
// Helper to parse a single date string part
|
||||
const parsePart = (part: string): CalendarDate | null => {
|
||||
if (!part) return null;
|
||||
const cleanPart = part.trim();
|
||||
// Legacy support: if value has "Present", treat as null/empty for end date input?
|
||||
// Or just let it fail parse and show empty.
|
||||
if (["至今", "Present", "Now"].includes(cleanPart)) return null;
|
||||
|
||||
try {
|
||||
// Replace dots with dashes for standard parsing
|
||||
let isoStr = cleanPart.replace(/\./g, "-");
|
||||
if (isoStr.length === 7) isoStr += "-01"; // YYYY-MM -> YYYY-MM-DD
|
||||
if (isoStr.length === 4) isoStr += "-01-01"; // YYYY -> YYYY-MM-DD
|
||||
return parseDate(isoStr);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Deconstruct value into state
|
||||
const { start, end } = useMemo(() => {
|
||||
if (!value) return { start: null, end: null };
|
||||
|
||||
// ... logic ...
|
||||
const parts = value.split("-").map(s => s.trim());
|
||||
let startStr = "", endStr = "";
|
||||
|
||||
if (value.includes(" - ")) {
|
||||
[startStr, endStr] = value.split(" - ");
|
||||
} else {
|
||||
const match = value.match(/^([^\s]+)\s*(?:-|–|—)\s*([^\s]+)$/);
|
||||
if (match) {
|
||||
startStr = match[1];
|
||||
endStr = match[2];
|
||||
} else {
|
||||
startStr = value;
|
||||
}
|
||||
}
|
||||
|
||||
const start = parsePart(startStr);
|
||||
// If endStr was "Present", parsePart returns null, which is what we want (empty input)
|
||||
const end = parsePart(endStr);
|
||||
|
||||
return { start, end };
|
||||
}, []); // Empty dependency array for uncontrolled initialization
|
||||
const [range, setRange] = useState<{ start: CalendarDate | null; end: CalendarDate | null }>(
|
||||
() => parseRange(value)
|
||||
);
|
||||
|
||||
const updateValue = (newStart: CalendarDate | null, newEnd: CalendarDate | null) => {
|
||||
// Format: YYYY.MM
|
||||
const format = (d: CalendarDate) => `${d.year}.${d.month.toString().padStart(2, "0")}`;
|
||||
|
||||
const format = (d: CalendarDate) =>
|
||||
`${d.year}/${d.month.toString().padStart(2, "0")}`;
|
||||
|
||||
const startStr = newStart ? format(newStart) : "";
|
||||
const endStr = newEnd ? format(newEnd) : "";
|
||||
|
||||
if (!startStr && !endStr) {
|
||||
onChange("");
|
||||
return;
|
||||
onChange("");
|
||||
return;
|
||||
}
|
||||
|
||||
if (startStr && !endStr) {
|
||||
// If end is empty, maybe keep separator? "YYYY.MM - "
|
||||
// Or just "YYYY.MM"? Context usually implies " - Present" if missing?
|
||||
// But user removed "Present" option.
|
||||
// Let's keep separator " - " to denote range started.
|
||||
onChange(`${startStr}${SEPARATOR}`);
|
||||
return;
|
||||
onChange(`${startStr}${SEPARATOR}`);
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(`${startStr}${SEPARATOR}${endStr}`);
|
||||
};
|
||||
|
||||
const handleStartChange = (newStart: CalendarDate | null) => {
|
||||
setRange((prev) => {
|
||||
const next = { start: newStart, end: prev.end };
|
||||
updateValue(next.start, next.end);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleEndChange = (newEnd: CalendarDate | null) => {
|
||||
setRange((prev) => {
|
||||
const next = { start: prev.start, end: newEnd };
|
||||
updateValue(next.start, next.end);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("w-full", className)}>
|
||||
<HeroUIProvider locale="ja-JP">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1">
|
||||
<DateInput
|
||||
value={start}
|
||||
onChange={(d) => updateValue(d, end)}
|
||||
variant="bordered"
|
||||
granularity="month"
|
||||
aria-label="Start Date"
|
||||
classNames={{
|
||||
inputWrapper: "bg-background hover:bg-muted/20 h-9 min-h-0 py-0 px-3 shadow-sm ring-1 ring-inset ring-input border-0",
|
||||
innerWrapper: "pb-0",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-muted-foreground">-</span>
|
||||
<div className="flex-1 relative">
|
||||
<DateInput
|
||||
value={end}
|
||||
onChange={(d) => updateValue(start, d)}
|
||||
variant="bordered"
|
||||
granularity="month"
|
||||
aria-label="End Date"
|
||||
classNames={{
|
||||
inputWrapper: "bg-background hover:bg-muted/20 h-9 min-h-0 py-0 px-3 shadow-sm ring-1 ring-inset ring-input border-0",
|
||||
innerWrapper: "pb-0",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<DateInput
|
||||
value={range.start}
|
||||
onChange={handleStartChange}
|
||||
variant="bordered"
|
||||
granularity="month"
|
||||
shouldForceLeadingZeros
|
||||
aria-label="Start Date"
|
||||
classNames={{
|
||||
inputWrapper:
|
||||
"bg-background hover:bg-muted/20 h-9 min-h-0 py-0 px-3 shadow-sm ring-1 ring-inset ring-input border-0",
|
||||
innerWrapper: "pb-0",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-muted-foreground">-</span>
|
||||
<div className="flex-1 relative">
|
||||
<DateInput
|
||||
value={range.end}
|
||||
onChange={handleEndChange}
|
||||
variant="bordered"
|
||||
granularity="month"
|
||||
shouldForceLeadingZeros
|
||||
aria-label="End Date"
|
||||
classNames={{
|
||||
inputWrapper:
|
||||
"bg-background hover:bg-muted/20 h-9 min-h-0 py-0 px-3 shadow-sm ring-1 ring-inset ring-input border-0",
|
||||
innerWrapper: "pb-0",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</HeroUIProvider>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ export function formatDateString(dateStr: string | undefined, locale: string = "
|
||||
|
||||
try {
|
||||
if (locale === "zh" || locale === "zh-CN") {
|
||||
return `${date.getUTCFullYear()}.${String(date.getUTCMonth() + 1).padStart(2, "0")}`;
|
||||
return `${date.getUTCFullYear()}/${String(date.getUTCMonth() + 1).padStart(2, "0")}`;
|
||||
}
|
||||
const formatter = new Intl.DateTimeFormat(locale, {
|
||||
year: 'numeric',
|
||||
|
||||
Reference in New Issue
Block a user