From 78655053f55b15a4dc700647b5492efd1dcc5fbe Mon Sep 17 00:00:00 2001 From: sushi-mage Date: Wed, 18 Feb 2026 16:54:27 +0000 Subject: [PATCH] feat: add since-and-until query type - Add WBQueryInputsSinceAndUntil type with URL parsing (?since=...&until=...) - Add useSinceAndUntilForm hook and tab in WaybackQueryForm - Sort results in ascending order for since-and-until queries - Auto-swap since/until when since > until --- src/App.tsx | 15 ++++++- src/components/WaybackQueryForm.tsx | 64 +++++++++++++++++++++++++++++ src/types/WaybackQuery.ts | 53 +++++++++++++++++++++++- 3 files changed, 129 insertions(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 1f31bfa..80e3b4f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,14 +1,25 @@ import { Box, Flex, Spinner } from "@chakra-ui/react"; import { useAtomValue } from "jotai"; -import { Suspense } from "react"; +import { Suspense, useMemo } from "react"; import { Header } from "./components/Header"; import { LoginPane } from "./components/LoginPane"; import { PostTimeline } from "./components/PostTimeline"; import { WaybackQueryForm } from "./components/WaybackQueryForm"; +import type { PostOrder } from "./states/Posts"; import { myPubkeyAtom } from "./states/Profiles"; +import { waybackQueryInputsAtom } from "./states/WaybackQuery"; export const App = () => { const myPubkey = useAtomValue(myPubkeyAtom); + const queryInputs = useAtomValue(waybackQueryInputsAtom); + + const postOrder: PostOrder = useMemo(() => { + if (queryInputs?.type === "since-and-until") { + // show oldest first when since < until (natural order), newest first when reversed + return queryInputs.sinceDatetime <= queryInputs.untilDatetime ? "created-at-asc" : "created-at-desc"; + } + return "created-at-desc"; + }, [queryInputs]); return ( <> @@ -20,7 +31,7 @@ export const App = () => { {myPubkey !== "" && ( - + )} diff --git a/src/components/WaybackQueryForm.tsx b/src/components/WaybackQueryForm.tsx index e98e188..b0de2d9 100644 --- a/src/components/WaybackQueryForm.tsx +++ b/src/components/WaybackQueryForm.tsx @@ -45,10 +45,12 @@ export const WaybackQueryForm: React.FC = () => { const setQueryInputs = useSetAtom(waybackQueryInputsAtom); const sinceAndDurForm = useSinceAndDurForm(); + const sinceAndUntilForm = useSinceAndUntilForm(); const untilNowForm = useUntilNowForm(); const tabs = [ { key: "since-dur", label: "始点+期間", form: sinceAndDurForm }, + { key: "since-until", label: "始点+終点", form: sinceAndUntilForm }, { key: "until-now", label: "現在まで", form: untilNowForm }, ]; const [tabIdx, setTabIdx] = useState(0); @@ -156,6 +158,68 @@ const useSinceAndDurForm = () => { }; }; +const useSinceAndUntilForm = () => { + const now = getNow(); + const [sinceDate, setSinceDate] = useState(subHours(now, 1)); + const [sinceTime, setSinceTime] = useState(format(subHours(now, 1), "HH:mm")); + const [untilDate, setUntilDate] = useState(now); + const [untilTime, setUntilTime] = useState(format(now, "HH:mm")); + + const queryInputs: WaybackQueryInputs | undefined = useMemo(() => { + const sinceDatetime = `${format(sinceDate, "yyyy-MM-dd")}T${sinceTime}`; + const untilDatetime = `${format(untilDate, "yyyy-MM-dd")}T${untilTime}`; + return { + type: "since-and-until", + sinceDatetime, + untilDatetime, + }; + }, [sinceDate, sinceTime, untilDate, untilTime]); + + const view = ( + + + + setSinceTime(e.target.value)} w="120px" /> + から + + + + setUntilTime(e.target.value)} w="120px" /> + まで + + + ); + + return { + queryInputs, + view, + }; +}; + const agoTimeUnitLabels: Record = { minutes: "分", hours: "時間", diff --git a/src/types/WaybackQuery.ts b/src/types/WaybackQuery.ts index 325bd41..7126976 100644 --- a/src/types/WaybackQuery.ts +++ b/src/types/WaybackQuery.ts @@ -11,11 +11,17 @@ type WBQueryInputsSinceAndDur = { sinceDatetime: string; // yyyy-MM-ddTHH:mm } & DurationInputs; +type WBQueryInputsSinceAndUntil = { + type: "since-and-until"; + sinceDatetime: string; // yyyy-MM-ddTHH:mm + untilDatetime: string; // yyyy-MM-ddTHH:mm +}; + type WBQueryInputsUntilNow = { type: "until-now"; } & DurationInputs; -export type WaybackQueryInputs = WBQueryInputsSinceAndDur | WBQueryInputsUntilNow; +export type WaybackQueryInputs = WBQueryInputsSinceAndDur | WBQueryInputsSinceAndUntil | WBQueryInputsUntilNow; type WBQueryInputsTypes = WaybackQueryInputs["type"]; @@ -52,6 +58,9 @@ const stringifyDurationInputs = ({ durationValue: value, durationUnit: unit }: D }; const detectWBQueryType = (urlParams: URLSearchParams): WBQueryInputsTypes | undefined => { + if (urlParams.has("since") && urlParams.has("until")) { + return "since-and-until"; + } if (urlParams.has("since") && (urlParams.has("dur") || urlParams.has("len"))) { return "since-and-dur"; } @@ -81,6 +90,21 @@ const sinceAndDurfromURLQuery = (params: URLSearchParams): WaybackQueryInputs | }; }; +const sinceAndUntilfromURLQuery = (params: URLSearchParams): WaybackQueryInputs | undefined => { + const sinceStr = params.get("since"); + const untilStr = params.get("until"); + + if (!sinceStr || !untilStr) { + return undefined; + } + + return { + type: "since-and-until", + sinceDatetime: sinceStr, + untilDatetime: untilStr, + }; +}; + const untilNowfromURLQuery = (params: URLSearchParams): WaybackQueryInputs | undefined => { const agoStr = params.get("ago"); if (!agoStr) { @@ -108,6 +132,8 @@ export const WaybackQueryInputs = { switch (wbQueryType) { case "since-and-dur": return sinceAndDurfromURLQuery(params); + case "since-and-until": + return sinceAndUntilfromURLQuery(params); case "until-now": return untilNowfromURLQuery(params); } @@ -120,6 +146,11 @@ export const WaybackQueryInputs = { params.set("dur", stringifyDurationInputs(inputs)); break; } + case "since-and-until": { + params.set("since", inputs.sinceDatetime); + params.set("until", inputs.untilDatetime); + break; + } case "until-now": { params.set("ago", stringifyDurationInputs(inputs)); break; @@ -171,6 +202,26 @@ export const WaybackQuery = { const until = Math.min(since + durationInSecs(inputs), getUnixTime(new Date())); return { since, until }; } + case "since-and-until": { + let since: number; + let until: number; + try { + since = getUnixTime(parseISO(inputs.sinceDatetime)); + until = getUnixTime(parseISO(inputs.untilDatetime)); + } catch (err) { + console.error("fromInputs: invalid datetime:", err); + return undefined; + } + if (since === until) { + return undefined; + } + // swap if since > until (user entered times in reverse order) + if (since > until) { + [since, until] = [until, since]; + } + until = Math.min(until, getUnixTime(new Date())); + return { since, until }; + } case "until-now": { if (inputs.durationValue === 0) { return undefined;