From 9c0a6b587a4a2db782d81e456aa0a84759f1b5e9 Mon Sep 17 00:00:00 2001 From: mk Date: Tue, 24 Sep 2024 12:56:12 +0900 Subject: [PATCH 01/13] =?UTF-8?q?feat:=20location=E3=81=A8date=E3=81=AEpar?= =?UTF-8?q?am=E3=82=92=E7=AE=A1=E7=90=86=E3=81=99=E3=82=8Bhook=E3=82=92?= =?UTF-8?q?=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/weather/hooks/index.ts | 1 + .../hooks/useWeatherParams/index.test.ts | 38 +++++++++++++++++++ .../weather/hooks/useWeatherParams/index.ts | 29 ++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 src/features/weather/hooks/useWeatherParams/index.test.ts create mode 100644 src/features/weather/hooks/useWeatherParams/index.ts diff --git a/src/features/weather/hooks/index.ts b/src/features/weather/hooks/index.ts index 03abec9..a0a4bae 100644 --- a/src/features/weather/hooks/index.ts +++ b/src/features/weather/hooks/index.ts @@ -1,2 +1,3 @@ export * from "./useQueryMatchedLocation" export * from "./useQueryWeatherForecast" +export * from "./useWeatherParams" diff --git a/src/features/weather/hooks/useWeatherParams/index.test.ts b/src/features/weather/hooks/useWeatherParams/index.test.ts new file mode 100644 index 0000000..36584a2 --- /dev/null +++ b/src/features/weather/hooks/useWeatherParams/index.test.ts @@ -0,0 +1,38 @@ +import { renderHook } from "@testing-library/react" +import { useSearchParams } from "react-router-dom" +import { describe, expect, it, Mock, vitest } from "vitest" + +import { format } from "@/utils" + +import { useWeatherParams } from "./" + +vitest.mock("react-router-dom", () => ({ + useSearchParams: vitest.fn(), +})) + +describe("useWeatherParams", () => { + it("初期値を返すこと", () => { + const setSearchParams = vitest.fn() + ;(useSearchParams as Mock).mockReturnValue([new URLSearchParams(), setSearchParams]) + + const { result } = renderHook(() => useWeatherParams({ defaultLocation: "Tokyo" })) + + expect(result.current.location).toBe("Tokyo") + expect(result.current.date).toBe(format(new Date(), "yyyy-MM-dd")) + expect(result.current.setSearchParams).toBe(setSearchParams) + }) + + it("引数で渡した値が正常に処理される", () => { + const setSearchParams = vitest.fn() + const searchParams = new URLSearchParams() + searchParams.set("location", "Sapporo") + searchParams.set("date", "2024-09-30") + ;(useSearchParams as Mock).mockReturnValue([searchParams, setSearchParams]) + + const { result } = renderHook(() => useWeatherParams({ defaultLocation: "Tokyo" })) + + expect(result.current.location).toBe("Sapporo") + expect(result.current.date).toBe("2024-09-30") + expect(result.current.setSearchParams).toBe(setSearchParams) + }) +}) diff --git a/src/features/weather/hooks/useWeatherParams/index.ts b/src/features/weather/hooks/useWeatherParams/index.ts new file mode 100644 index 0000000..202f358 --- /dev/null +++ b/src/features/weather/hooks/useWeatherParams/index.ts @@ -0,0 +1,29 @@ +import { format } from "date-fns" +import { type SetURLSearchParams, useSearchParams } from "react-router-dom" + +type WeatherAPiParams = { + location: string + date: string + setSearchParams: SetURLSearchParams +} + +export function useWeatherParams({ + defaultLocation = "Tokyo", +}: { + defaultLocation?: string +}): WeatherAPiParams { + const [searchParams, setSearchParams] = useSearchParams() + + const location = searchParams.get("location") || defaultLocation + const dateParam = searchParams.get("date") + + const date = dateParam + ? format(new Date(dateParam), "yyyy-MM-dd") + : format(new Date(), "yyyy-MM-dd") + + return { + location, + date, + setSearchParams, + } +} From d6d6f23185b59095b0316a331649f07340bde3c4 Mon Sep 17 00:00:00 2001 From: mk Date: Tue, 24 Sep 2024 13:08:43 +0900 Subject: [PATCH 02/13] =?UTF-8?q?feat:=20react-router=E3=81=AELink?= =?UTF-8?q?=E3=82=B3=E3=83=B3=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=81=AEprops=E3=82=92=E7=AE=A1=E7=90=86=E3=81=99=E3=82=8Bhook?= =?UTF-8?q?=E3=82=92=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/index.ts | 1 + src/hooks/useLinkProps/index.test.ts | 75 ++++++++++++++++++++++++++++ src/hooks/useLinkProps/index.ts | 42 ++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 src/hooks/useLinkProps/index.test.ts create mode 100644 src/hooks/useLinkProps/index.ts diff --git a/src/hooks/index.ts b/src/hooks/index.ts index da31530..94ed230 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1 +1,2 @@ export * from "./useDebouncedValue" +export * from "./useLinkProps" diff --git a/src/hooks/useLinkProps/index.test.ts b/src/hooks/useLinkProps/index.test.ts new file mode 100644 index 0000000..170c0b6 --- /dev/null +++ b/src/hooks/useLinkProps/index.test.ts @@ -0,0 +1,75 @@ +import { renderHook } from "@testing-library/react" +import { useLocation } from "react-router-dom" +import { describe, expect, MockedFunction, test, vitest } from "vitest" + +import { useLinkProps } from "./" + +vitest.mock("react-router-dom", () => ({ + useLocation: vitest.fn(), +})) + +describe("useLinkProps", () => { + test("paramが追加されること", () => { + // useLocationのモックを設定 + const mockUseLocation = useLocation as MockedFunction + mockUseLocation.mockReturnValue({ search: "?existingParam=value" } as ReturnType< + typeof useLocation + >) + + const { result } = renderHook(() => useLinkProps()) + + const linkProps = result.current.generateLinkProps({ + pathname: "/test", + newParams: { newParam: "newValue" }, + }) + + expect(linkProps).toEqual({ + to: { + pathname: "/test", + search: "?existingParam=value&newParam=newValue", + }, + }) + }) + + test("指定したparamが削除されること", () => { + const mockUseLocation = useLocation as MockedFunction + mockUseLocation.mockReturnValue({ + search: "?param1=value1¶m2=value2¶m3=value3", + } as ReturnType) + + const { result } = renderHook(() => useLinkProps()) + + const linkProps = result.current.generateLinkProps({ + pathname: "/test", + removeParams: ["param2"], + }) + + expect(linkProps).toEqual({ + to: { + pathname: "/test", + search: "?param1=value1¶m3=value3", + }, + }) + }) + + test("存在するparamの場合は上書きをすること", () => { + const mockUseLocation = useLocation as MockedFunction + mockUseLocation.mockReturnValue({ search: "?existingParam=oldValue" } as ReturnType< + typeof useLocation + >) + + const { result } = renderHook(() => useLinkProps()) + + const linkProps = result.current.generateLinkProps({ + pathname: "/test", + newParams: { existingParam: "newValue" }, + }) + + expect(linkProps).toEqual({ + to: { + pathname: "/test", + search: "?existingParam=newValue", + }, + }) + }) +}) diff --git a/src/hooks/useLinkProps/index.ts b/src/hooks/useLinkProps/index.ts new file mode 100644 index 0000000..1efd049 --- /dev/null +++ b/src/hooks/useLinkProps/index.ts @@ -0,0 +1,42 @@ +import { type LinkProps as ReactRouterLinkProps, useLocation } from "react-router-dom" + +export type LinkProps = ReactRouterLinkProps + +type GenerateLinkPropsOptions = { + pathname: string + newParams?: Record + removeParams?: string[] +} + +export function useLinkProps() { + const location = useLocation() + + function generateLinkProps({ + pathname, + newParams = {}, + removeParams = [], + }: GenerateLinkPropsOptions): LinkProps { + const currentParams = new URLSearchParams(location.search) + const newParamsObj = new URLSearchParams(newParams) + + removeParams.forEach((param) => currentParams.delete(param)) + + for (const [key, value] of newParamsObj.entries()) { + if (currentParams.has(key) && currentParams.get(key) === value) { + continue + } + currentParams.set(key, value) + } + + const mergedSearch = currentParams.toString() + + return { + to: { + pathname, + search: mergedSearch ? `?${mergedSearch}` : "", + }, + } + } + + return { generateLinkProps } +} From 9b5a760264e8425ecb4a9a3f2f563e3e194efa0b Mon Sep 17 00:00:00 2001 From: mk Date: Tue, 24 Sep 2024 13:10:17 +0900 Subject: [PATCH 03/13] =?UTF-8?q?feat:=20location=E3=81=A8date=E3=82=92?= =?UTF-8?q?=E4=BF=9D=E6=8C=81=E3=81=97=E3=80=81specific=5Fday=E3=83=9A?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=81=AB=E9=81=B7=E7=A7=BB=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../weather/components/weather_forecast/container.tsx | 6 ++++++ .../components/weather_forecast/presenter.stories.tsx | 3 +++ .../weather/components/weather_forecast/presenter.tsx | 4 +++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/features/weather/components/weather_forecast/container.tsx b/src/features/weather/components/weather_forecast/container.tsx index 351275b..7b9d1f4 100644 --- a/src/features/weather/components/weather_forecast/container.tsx +++ b/src/features/weather/components/weather_forecast/container.tsx @@ -1,8 +1,10 @@ import { useMemo } from "react" import * as WeatherIcon from "@/features/weather/components/common/weather_icon" +import { useWeatherParams } from "@/features/weather/hooks" import { Forecast } from "@/features/weather/models" import { getWeatherCategory } from "@/features/weather/utils" +import { useLinkProps } from "@/hooks" import { format } from "@/utils" import { Presenter, PresenterProps } from "./presenter" @@ -12,6 +14,8 @@ export type ContainerProps = { } export const Container = ({ ...props }: ContainerProps) => { + const { generateLinkProps } = useLinkProps() + const { location } = useWeatherParams({}) const tableHead = useMemo( () => props.forecast.forecastday.filter((d) => isNotToday(d.date)).map((d) => d.date), [props.forecast.forecastday], @@ -33,6 +37,8 @@ export const Container = ({ ...props }: ContainerProps) => { const presenterProps = { tableHead, tableBody, + linkProps: (date: string) => + generateLinkProps({ pathname: "/specific_day", newParams: { location, date } }), } satisfies PresenterProps return diff --git a/src/features/weather/components/weather_forecast/presenter.stories.tsx b/src/features/weather/components/weather_forecast/presenter.stories.tsx index c220f15..cc76d53 100644 --- a/src/features/weather/components/weather_forecast/presenter.stories.tsx +++ b/src/features/weather/components/weather_forecast/presenter.stories.tsx @@ -23,6 +23,9 @@ const meta = { args: { tableHead, tableBody, + linkProps: (date: string) => ({ + to: { pathname: `/specific_date`, search: `?location=${mock.location.region}&date=${date}` }, + }), }, decorators: [ (Story) => ( diff --git a/src/features/weather/components/weather_forecast/presenter.tsx b/src/features/weather/components/weather_forecast/presenter.tsx index 5070567..7c161b0 100644 --- a/src/features/weather/components/weather_forecast/presenter.tsx +++ b/src/features/weather/components/weather_forecast/presenter.tsx @@ -1,6 +1,7 @@ import { Link } from "react-router-dom" import { Table, TBody, Td, Th, THead, Tr } from "@/components/ui/Table" +import { LinkProps } from "@/hooks" import { format } from "@/utils" import * as styles from "./presenter.css" @@ -8,6 +9,7 @@ import * as styles from "./presenter.css" export type PresenterProps = { tableHead: string[] tableBody: { code: number; icon: JSX.Element }[] + linkProps: (date: string) => LinkProps } export const Presenter = ({ ...props }: PresenterProps) => { @@ -17,7 +19,7 @@ export const Presenter = ({ ...props }: PresenterProps) => { {props.tableHead.map((v) => ( - + {format(new Date(v), "MM/dd")} From 7fc852a251580fb7b23dbf0ce446c706ff41894b Mon Sep 17 00:00:00 2001 From: mk Date: Tue, 24 Sep 2024 13:12:26 +0900 Subject: [PATCH 04/13] =?UTF-8?q?feat:=20Location=E9=81=B8=E6=8A=9E?= =?UTF-8?q?=E6=99=82=E3=81=ABparam=E3=81=AB=E3=83=87=E3=83=BC=E3=82=BF?= =?UTF-8?q?=E3=82=92=E3=82=BB=E3=83=83=E3=83=88=E3=81=99=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../weather/components/location_input/container.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/features/weather/components/location_input/container.tsx b/src/features/weather/components/location_input/container.tsx index 217a9f8..e8a1d5f 100644 --- a/src/features/weather/components/location_input/container.tsx +++ b/src/features/weather/components/location_input/container.tsx @@ -1,5 +1,6 @@ import { Suspense, useCallback, useState } from "react" +import { useWeatherParams } from "@/features/weather/hooks" import { useLocationContext } from "@/features/weather/providers/location/useLocationContext" import { useDebouncedValue } from "@/hooks" @@ -11,6 +12,8 @@ export const Container = () => { const [inputLocation, setInputLocation] = useState("") const [debouncedValue] = useDebouncedValue(inputLocation, 300) + const { setSearchParams, date } = useWeatherParams({}) + const { setLocation } = useLocationContext() const onClickHandler = useCallback( @@ -18,8 +21,9 @@ export const Container = () => { setLocation(location) setInputLocation(location) setIsFocused(false) + setSearchParams({ location, date }) }, - [setLocation], + [setLocation, setSearchParams, date], ) const handleFocus = useCallback(() => { From 2f96406ff549c2793286cb12ca8f527bd60e411a Mon Sep 17 00:00:00 2001 From: mk Date: Tue, 24 Sep 2024 13:18:05 +0900 Subject: [PATCH 05/13] =?UTF-8?q?feat:=20param=E3=81=8B=E3=82=89=E5=8F=96?= =?UTF-8?q?=E5=BE=97=E3=81=97=E3=81=9F=E6=97=A5=E3=82=92=E8=A1=A8=E7=A4=BA?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=82=B3=E3=83=B3=E3=83=9D=E3=83=BC=E3=83=8D?= =?UTF-8?q?=E3=83=B3=E3=83=88=E3=82=92=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/specific_date/container.tsx | 12 ++++++++++ .../weather/components/specific_date/index.ts | 2 ++ .../components/specific_date/presenter.css.ts | 18 +++++++++++++++ .../specific_date/presenter.stories.tsx | 23 +++++++++++++++++++ .../specific_date/presenter.test.tsx | 17 ++++++++++++++ .../components/specific_date/presenter.tsx | 13 +++++++++++ 6 files changed, 85 insertions(+) create mode 100644 src/features/weather/components/specific_date/container.tsx create mode 100644 src/features/weather/components/specific_date/index.ts create mode 100644 src/features/weather/components/specific_date/presenter.css.ts create mode 100644 src/features/weather/components/specific_date/presenter.stories.tsx create mode 100644 src/features/weather/components/specific_date/presenter.test.tsx create mode 100644 src/features/weather/components/specific_date/presenter.tsx diff --git a/src/features/weather/components/specific_date/container.tsx b/src/features/weather/components/specific_date/container.tsx new file mode 100644 index 0000000..428eafa --- /dev/null +++ b/src/features/weather/components/specific_date/container.tsx @@ -0,0 +1,12 @@ +import { useWeatherParams } from "@/features/weather/hooks" + +import { Presenter, PresenterProps } from "./presenter" + +export const Container = () => { + const { date } = useWeatherParams({}) + const presenterProps = { + date, + } satisfies PresenterProps + + return +} diff --git a/src/features/weather/components/specific_date/index.ts b/src/features/weather/components/specific_date/index.ts new file mode 100644 index 0000000..033bd48 --- /dev/null +++ b/src/features/weather/components/specific_date/index.ts @@ -0,0 +1,2 @@ +export * from "./container" +export * from "./presenter" diff --git a/src/features/weather/components/specific_date/presenter.css.ts b/src/features/weather/components/specific_date/presenter.css.ts new file mode 100644 index 0000000..c75f9e8 --- /dev/null +++ b/src/features/weather/components/specific_date/presenter.css.ts @@ -0,0 +1,18 @@ +import { style } from "@vanilla-extract/css" + +import { responsiveStyle } from "@/styles/utils" +import { vars } from "@/styles/vars.css" + +export const module = style([ + { + fontWeight: "bold", + fontSize: vars.font.size["xl"], + lineHeight: vars.font.lineHeight["xl"], + }, + responsiveStyle({ + md: { + fontSize: vars.font.size["3xl"], + lineHeight: vars.font.lineHeight["3xl"], + }, + }), +]) diff --git a/src/features/weather/components/specific_date/presenter.stories.tsx b/src/features/weather/components/specific_date/presenter.stories.tsx new file mode 100644 index 0000000..69de793 --- /dev/null +++ b/src/features/weather/components/specific_date/presenter.stories.tsx @@ -0,0 +1,23 @@ +import { Meta, StoryObj } from "@storybook/react" + +import { format } from "@/utils" + +import { Presenter } from "./presenter" + +const date = "2024-10-01" + +const meta = { + title: "Features/Weather/SpecificDate/Presenter", + tags: ["autodocs"], + component: Presenter, + args: { + date: format(new Date(date), "yyyy-MM-dd"), + }, +} satisfies Meta +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: {}, +} diff --git a/src/features/weather/components/specific_date/presenter.test.tsx b/src/features/weather/components/specific_date/presenter.test.tsx new file mode 100644 index 0000000..7be1778 --- /dev/null +++ b/src/features/weather/components/specific_date/presenter.test.tsx @@ -0,0 +1,17 @@ +import { composeStories } from "@storybook/react" +import { render, screen } from "@testing-library/react" +import { describe, expect, test } from "vitest" + +import * as stories from "./presenter.stories" + +const { Default } = composeStories(stories) + +describe("Features/Weather/SpecificDate/Presenter", () => { + test("should render Heading & time ", () => { + const date = "2024-10-01" + render() + const time = screen.getByTestId("datetime") + expect(time).toBeInTheDocument() + expect(time).toHaveAttribute("datetime", date) + }) +}) diff --git a/src/features/weather/components/specific_date/presenter.tsx b/src/features/weather/components/specific_date/presenter.tsx new file mode 100644 index 0000000..b212a55 --- /dev/null +++ b/src/features/weather/components/specific_date/presenter.tsx @@ -0,0 +1,13 @@ +import * as styles from "./presenter.css" + +export type PresenterProps = { + date: string +} + +export const Presenter = ({ ...props }: PresenterProps) => { + return ( + + ) +} From bf3b4ef3e3b1a08cf64e5559accbb09e5bff5999 Mon Sep 17 00:00:00 2001 From: mk Date: Tue, 24 Sep 2024 13:20:20 +0900 Subject: [PATCH 06/13] =?UTF-8?q?feat:=20=E7=89=B9=E5=AE=9A=E6=97=A5?= =?UTF-8?q?=E3=81=AE=E5=A4=A9=E6=B0=97=E8=A9=B3=E7=B4=B0=E3=82=92=E6=88=90?= =?UTF-8?q?=E5=BD=A2=E3=81=99=E3=82=8B=E9=96=A2=E6=95=B0=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/weather/utils/transform/index.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/features/weather/utils/transform/index.ts b/src/features/weather/utils/transform/index.ts index a411bf8..fd5c11f 100644 --- a/src/features/weather/utils/transform/index.ts +++ b/src/features/weather/utils/transform/index.ts @@ -1,3 +1,4 @@ +import { Day } from "@/features/weather/models" import { Current } from "@/features/weather/models/common" export const transformWeatherInfoForCurrent = (current: Current) => { @@ -23,3 +24,24 @@ export const transformWeatherInfoForCurrent = (current: Current) => { "Gusts (kph)": current.gust_kph, } } + +export const transformWeatherInfoForSpecificDay = (day: Day) => { + return { + "Max Temperature (°C)": day.maxtemp_c, + "Max Temperature (°F)": day.maxtemp_f, + "Min Temperature (°C)": day.mintemp_c, + "Min Temperature (°F)": day.mintemp_f, + "Average Temperature (°C)": day.avgtemp_c, + "Average Temperature (°F)": day.avgtemp_f, + "Max Wind Speed (mph)": day.maxwind_mph, + "Max Wind Speed (kph)": day.maxwind_kph, + "Total Precipitation (mm)": day.totalprecip_mm, + "Total Precipitation (in)": day.totalprecip_in, + "Average Visibility (km)": day.avgvis_km, + "Average Visibility (miles)": day.avgvis_miles, + "Average Humidity (%)": day.avghumidity, + "Chance of Rain (%)": day.daily_chance_of_rain, + "Chance of Snow (%)": day.daily_chance_of_snow, + "UV Index": day.uv, + } +} From 0a986d0e6ace0affe2813c8585d71fda46d91f51 Mon Sep 17 00:00:00 2001 From: mk Date: Tue, 24 Sep 2024 13:21:20 +0900 Subject: [PATCH 07/13] =?UTF-8?q?feat:=20=E7=89=B9=E5=AE=9A=E6=97=A5?= =?UTF-8?q?=E3=81=AE=E5=A4=A9=E6=B0=97=E8=A9=B3=E7=B4=B0=E3=82=92=E8=A1=A8?= =?UTF-8?q?=E7=A4=BA=E3=81=99=E3=82=8B=E3=82=B3=E3=83=B3=E3=83=9D=E3=83=BC?= =?UTF-8?q?=E3=83=8D=E3=83=B3=E3=83=88=E3=82=92=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../weather_specific_day/container.tsx | 37 +++++++++++++++++++ .../weather_specific_day/error.stories.tsx | 24 ++++++++++++ .../components/weather_specific_day/error.tsx | 9 +++++ .../components/weather_specific_day/index.ts | 4 ++ .../weather_specific_day/loading.css.ts | 5 +++ .../weather_specific_day/loading.stories.tsx | 14 +++++++ .../weather_specific_day/loading.tsx | 12 ++++++ .../weather_specific_day/presenter.css.ts | 5 +++ .../presenter.stories.tsx | 32 ++++++++++++++++ .../weather_specific_day/presenter.tsx | 17 +++++++++ 10 files changed, 159 insertions(+) create mode 100644 src/features/weather/components/weather_specific_day/container.tsx create mode 100644 src/features/weather/components/weather_specific_day/error.stories.tsx create mode 100644 src/features/weather/components/weather_specific_day/error.tsx create mode 100644 src/features/weather/components/weather_specific_day/index.ts create mode 100644 src/features/weather/components/weather_specific_day/loading.css.ts create mode 100644 src/features/weather/components/weather_specific_day/loading.stories.tsx create mode 100644 src/features/weather/components/weather_specific_day/loading.tsx create mode 100644 src/features/weather/components/weather_specific_day/presenter.css.ts create mode 100644 src/features/weather/components/weather_specific_day/presenter.stories.tsx create mode 100644 src/features/weather/components/weather_specific_day/presenter.tsx diff --git a/src/features/weather/components/weather_specific_day/container.tsx b/src/features/weather/components/weather_specific_day/container.tsx new file mode 100644 index 0000000..5a54ef4 --- /dev/null +++ b/src/features/weather/components/weather_specific_day/container.tsx @@ -0,0 +1,37 @@ +import { isSameDay } from "date-fns" + +import * as LocationTitle from "@/features/weather/components/location_title" +import * as WeatherInfo from "@/features/weather/components/weather_info" +import * as WeatherInfoDetail from "@/features/weather/components/weather_info_detail" +import { Day } from "@/features/weather/const/range" +import { useQueryWeatherForecast, useWeatherParams } from "@/features/weather/hooks" +import { transformWeatherInfoForSpecificDay, validateDayRange } from "@/features/weather/utils" + +import { Presenter, PresenterProps } from "./presenter" + +export const Container = () => { + const { location, date } = useWeatherParams({}) + const dateRange = validateDayRange(Day + 1) + const { data } = useQueryWeatherForecast(location, dateRange) + + const locationTitleProps = { location: data.location } satisfies LocationTitle.ContainerProps + + const weatherDayData = data.forecast.forecastday.filter((d) => + isSameDay(new Date(d.date), date), + )[0].day + + const weatherInfoDetailProps = { + weatherInfo: transformWeatherInfoForSpecificDay(weatherDayData), + } satisfies WeatherInfoDetail.ContainerProps + + const weatherInfoProps = { + condition: weatherDayData.condition, + children: , + } satisfies WeatherInfo.ContainerProps + + const presenterProps = { + locationTitleNode: , + weatherInfoNode: , + } satisfies PresenterProps + return +} diff --git a/src/features/weather/components/weather_specific_day/error.stories.tsx b/src/features/weather/components/weather_specific_day/error.stories.tsx new file mode 100644 index 0000000..b9ab56b --- /dev/null +++ b/src/features/weather/components/weather_specific_day/error.stories.tsx @@ -0,0 +1,24 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { Error } from "./error" + +const meta = { + title: "Features/Weather/WeatherSpecificDay/Error", + tags: ["autodocs"], + component: Error, + parameters: { + layout: "centered", + }, +} satisfies Meta +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + error: { + message: "Internal Server Error", + }, + resetErrorBoundary: () => {}, + }, +} diff --git a/src/features/weather/components/weather_specific_day/error.tsx b/src/features/weather/components/weather_specific_day/error.tsx new file mode 100644 index 0000000..981bb8a --- /dev/null +++ b/src/features/weather/components/weather_specific_day/error.tsx @@ -0,0 +1,9 @@ +import { Alert } from "@/components/ui/Alert" + +export const Error = () => { + return ( + +
Failed to fetch data
+
+ ) +} diff --git a/src/features/weather/components/weather_specific_day/index.ts b/src/features/weather/components/weather_specific_day/index.ts new file mode 100644 index 0000000..8c60ecf --- /dev/null +++ b/src/features/weather/components/weather_specific_day/index.ts @@ -0,0 +1,4 @@ +export * from "./container" +export * from "./error" +export * from "./loading" +export * from "./presenter" diff --git a/src/features/weather/components/weather_specific_day/loading.css.ts b/src/features/weather/components/weather_specific_day/loading.css.ts new file mode 100644 index 0000000..d222c4e --- /dev/null +++ b/src/features/weather/components/weather_specific_day/loading.css.ts @@ -0,0 +1,5 @@ +import { style } from "@vanilla-extract/css" + +import { layout } from "@/styles/utils" + +export const module = style([layout]) diff --git a/src/features/weather/components/weather_specific_day/loading.stories.tsx b/src/features/weather/components/weather_specific_day/loading.stories.tsx new file mode 100644 index 0000000..bdb044a --- /dev/null +++ b/src/features/weather/components/weather_specific_day/loading.stories.tsx @@ -0,0 +1,14 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { Loading } from "./loading" + +const meta = { + title: "Features/Weather/WeatherSpecificDay/Loading", + tags: ["autodocs"], + component: Loading, +} satisfies Meta +export default meta + +type Story = StoryObj + +export const Default: Story = {} diff --git a/src/features/weather/components/weather_specific_day/loading.tsx b/src/features/weather/components/weather_specific_day/loading.tsx new file mode 100644 index 0000000..6678f9c --- /dev/null +++ b/src/features/weather/components/weather_specific_day/loading.tsx @@ -0,0 +1,12 @@ +import * as LocationTitle from "@/features/weather/components/location_title" +import * as WeatherInfo from "@/features/weather/components/weather_info" + +import * as styles from "./loading.css" +export const Loading = () => { + return ( +
+ + +
+ ) +} diff --git a/src/features/weather/components/weather_specific_day/presenter.css.ts b/src/features/weather/components/weather_specific_day/presenter.css.ts new file mode 100644 index 0000000..aba4f0e --- /dev/null +++ b/src/features/weather/components/weather_specific_day/presenter.css.ts @@ -0,0 +1,5 @@ +import { style } from "@vanilla-extract/css" + +import { layout } from "@/styles/utils/layout.css" + +export const module = style([layout]) diff --git a/src/features/weather/components/weather_specific_day/presenter.stories.tsx b/src/features/weather/components/weather_specific_day/presenter.stories.tsx new file mode 100644 index 0000000..92fefe4 --- /dev/null +++ b/src/features/weather/components/weather_specific_day/presenter.stories.tsx @@ -0,0 +1,32 @@ +import { Meta, StoryObj } from "@storybook/react" + +import * as LocationTitle from "@/features/weather/components/location_title" +import * as WeatherInfo from "@/features/weather/components/weather_info" +import * as WeatherInfoDetail from "@/features/weather/components/weather_info_detail" +import { mock } from "@/features/weather/services/forecast/mock" +import { transformWeatherInfoForSpecificDay } from "@/features/weather/utils" + +import { Presenter } from "./presenter" + +const meta = { + title: "Features/Weather/WeatherSpecificDay/Presenter", + component: Presenter, + args: { + locationTitleNode: , + weatherInfoNode: ( + + } + /> + ), + }, +} satisfies Meta +export default meta + +type Story = StoryObj + +export const Default: Story = {} diff --git a/src/features/weather/components/weather_specific_day/presenter.tsx b/src/features/weather/components/weather_specific_day/presenter.tsx new file mode 100644 index 0000000..af156db --- /dev/null +++ b/src/features/weather/components/weather_specific_day/presenter.tsx @@ -0,0 +1,17 @@ +import { ReactNode } from "react" + +import * as styles from "./presenter.css" + +export type PresenterProps = { + locationTitleNode: ReactNode + weatherInfoNode: ReactNode +} + +export const Presenter = ({ ...props }: PresenterProps) => { + return ( +
+ {props.locationTitleNode} + {props.weatherInfoNode} +
+ ) +} From afc8161815544a3475b5f4cd8a3d1fa01dfc990c Mon Sep 17 00:00:00 2001 From: mk Date: Tue, 24 Sep 2024 13:28:53 +0900 Subject: [PATCH 08/13] =?UTF-8?q?feat:=20param=E3=81=AB=E3=83=87=E3=83=BC?= =?UTF-8?q?=E3=82=BF=E3=81=8C=E4=BF=9D=E6=8C=81=E3=81=95=E3=82=8C=E3=81=A6?= =?UTF-8?q?=E3=81=84=E3=82=8B=E5=A0=B4=E5=90=88=E3=81=AF=E5=84=AA=E5=85=88?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../weather/components/weather_overview/container.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/features/weather/components/weather_overview/container.tsx b/src/features/weather/components/weather_overview/container.tsx index 1a67739..5675511 100644 --- a/src/features/weather/components/weather_overview/container.tsx +++ b/src/features/weather/components/weather_overview/container.tsx @@ -3,7 +3,7 @@ import * as WeatherForecast from "@/features/weather/components/weather_forecast import * as WeatherInfo from "@/features/weather/components/weather_info" import * as WeatherInfoDetail from "@/features/weather/components/weather_info_detail" import { Day } from "@/features/weather/const" -import { useQueryWeatherForecast } from "@/features/weather/hooks" +import { useQueryWeatherForecast, useWeatherParams } from "@/features/weather/hooks" import { useLocationContext } from "@/features/weather/providers/location/useLocationContext" import { validateDayRange } from "@/features/weather/utils" import { transformWeatherInfoForCurrent } from "@/features/weather/utils/transform" @@ -12,9 +12,10 @@ import { Presenter, PresenterProps } from "./presenter" export const Container = () => { const { location } = useLocationContext() + const { location: param } = useWeatherParams({}) const dateRange = validateDayRange(Day + 1) - const { data } = useQueryWeatherForecast(location, dateRange) + const { data } = useQueryWeatherForecast(param ?? location, dateRange) const locationTitleProps = { location: data.location } satisfies LocationTitle.ContainerProps From a4af4e0b32098fb3e626b66bd3aee85ba4a06401 Mon Sep 17 00:00:00 2001 From: mk Date: Tue, 24 Sep 2024 13:31:44 +0900 Subject: [PATCH 09/13] =?UTF-8?q?feat:=20Button=E3=82=B3=E3=83=B3=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=83=8D=E3=83=B3=E3=83=88=E3=82=92=E4=BD=9C=E6=88=90?= =?UTF-8?q?,button=E3=81=A7=E4=BD=BF=E3=81=86=E5=A4=89=E6=95=B0=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/Button/index.css.ts | 36 ++++++++++++++ src/components/ui/Button/index.stories.tsx | 56 ++++++++++++++++++++++ src/components/ui/Button/index.tsx | 14 ++++++ src/styles/vars.css.ts | 4 ++ 4 files changed, 110 insertions(+) create mode 100644 src/components/ui/Button/index.css.ts create mode 100644 src/components/ui/Button/index.stories.tsx create mode 100644 src/components/ui/Button/index.tsx diff --git a/src/components/ui/Button/index.css.ts b/src/components/ui/Button/index.css.ts new file mode 100644 index 0000000..db2911c --- /dev/null +++ b/src/components/ui/Button/index.css.ts @@ -0,0 +1,36 @@ +import { recipe, RecipeVariants } from "@vanilla-extract/recipes" + +import { vars } from "@/styles/vars.css" + +export const button = recipe({ + base: { + display: "inline-flex", + justifyContent: "center", + alignItems: "center", + borderRadius: vars.rounded.md, + fontSize: "0.875rem", + lineHeight: "1.25rem", + fontWeight: 500, + whiteSpace: "nowrap", + color: vars.colors.button.text, + cursor: "pointer", + ":hover": { + opacity: ".7", + }, + }, + variants: { + color: { + primary: { background: vars.colors.button.primary }, + }, + size: { + sm: { padding: "0 0.75rem", height: "2.25rem" }, + md: { padding: "0 1rem", height: "2.5rem" }, + }, + }, + defaultVariants: { + color: "primary", + size: "md", + }, +}) + +export type ButtonVariants = RecipeVariants diff --git a/src/components/ui/Button/index.stories.tsx b/src/components/ui/Button/index.stories.tsx new file mode 100644 index 0000000..af76067 --- /dev/null +++ b/src/components/ui/Button/index.stories.tsx @@ -0,0 +1,56 @@ +import type { Meta, StoryObj } from "@storybook/react" +import { expect, fn, userEvent, within } from "@storybook/test" + +import { Button } from "." + +const meta = { + title: "UI/Button", + component: Button, + tags: ["autodocs"], + parameters: { + layout: "centered", + }, + args: { children: "button", onClick: fn() }, +} satisfies Meta + +export default meta +type Story = StoryObj + +const testButton: Story["play"] = async ({ args, canvasElement, step }) => { + const canvas = within(canvasElement) + const button = await canvas.findByRole("button") + + await step("ボタンをクリックできること", async () => { + await userEvent.click(button) + await expect(args.onClick).toHaveBeenCalled() + }) +} + +const testDisabledButton: Story["play"] = async ({ args, canvasElement, step }) => { + const canvas = within(canvasElement) + const button = await canvas.findByRole("button") + + await step("ボタンがクリックできないこと", async () => { + await userEvent.click(button, { pointerEventsCheck: 0 }) + await expect(args.onClick).not.toHaveBeenCalled() + }) +} + +export const Default: Story = { + play: testButton, +} + +export const Disabled: Story = { + args: { + disabled: true, + }, + play: testDisabledButton, +} + +export const Small: Story = { + args: { size: "sm" }, +} + +export const Medium: Story = { + args: { size: "md" }, +} diff --git a/src/components/ui/Button/index.tsx b/src/components/ui/Button/index.tsx new file mode 100644 index 0000000..e884cd0 --- /dev/null +++ b/src/components/ui/Button/index.tsx @@ -0,0 +1,14 @@ +import clsx from "clsx" +import type { ComponentPropsWithoutRef } from "react" +import { forwardRef } from "react" + +import * as styles from "./index.css" + +type ButtonProps = ComponentPropsWithoutRef<"button"> & styles.ButtonVariants + +export const Button = forwardRef(function ButtonBase( + { color, size, className, ...props }, + ref, +) { + return + + ), + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = {} diff --git a/src/components/ui/HeadGroup/index.tsx b/src/components/ui/HeadGroup/index.tsx new file mode 100644 index 0000000..0866080 --- /dev/null +++ b/src/components/ui/HeadGroup/index.tsx @@ -0,0 +1,13 @@ +import clsx from "clsx" +import React from "react" + +import * as styles from "./index.css" + +type Props = { + children: React.ReactNode + className?: string +} + +export const HeadGroup = ({ children, className }: Props) => { + return
{children}
+} From 1e254e147674c973b1a134e8c8f6defbc09bdae9 Mon Sep 17 00:00:00 2001 From: mk Date: Tue, 24 Sep 2024 13:50:06 +0900 Subject: [PATCH 12/13] =?UTF-8?q?feat:=20=E7=89=B9=E5=AE=9A=E6=97=A5?= =?UTF-8?q?=E3=81=AE=E5=A4=A9=E6=B0=97=E8=A9=B3=E7=B4=B0=E3=82=92=E8=A1=A8?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/pages/specific_day.tsx | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/app/pages/specific_day.tsx b/src/app/pages/specific_day.tsx index 8a57a2d..8be9c1a 100644 --- a/src/app/pages/specific_day.tsx +++ b/src/app/pages/specific_day.tsx @@ -1,12 +1,30 @@ +import { Suspense } from "react" +import { ErrorBoundary } from "react-error-boundary" + +import { HeadGroup } from "@/components/ui/HeadGroup" import { Section } from "@/components/ui/Section" +import * as BackButton from "@/features/weather/components/back_button" import * as LocationInput from "@/features/weather/components/location_input" +import * as SpecificDate from "@/features/weather/components/specific_date" +import * as WeatherSpecificDay from "@/features/weather/components/weather_specific_day" import { LocationContextProvider } from "@/features/weather/providers/location/provider" export default function SpecificDay() { return (
- + + + + + + + + + }> + + +
) From 823eb2a08446d0bcb5b22ca5d5c3217fd7246a09 Mon Sep 17 00:00:00 2001 From: mk Date: Tue, 24 Sep 2024 13:50:27 +0900 Subject: [PATCH 13/13] =?UTF-8?q?feat:=20Story=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../weather_info/presenter.stories.tsx | 24 ++++++++++++++++++- .../weather_info_detail/presenter.stories.tsx | 13 ++++++++-- .../weather_info_detail/presenter.test.tsx | 4 ++-- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/features/weather/components/weather_info/presenter.stories.tsx b/src/features/weather/components/weather_info/presenter.stories.tsx index 048fa5e..74d9899 100644 --- a/src/features/weather/components/weather_info/presenter.stories.tsx +++ b/src/features/weather/components/weather_info/presenter.stories.tsx @@ -3,7 +3,11 @@ import { Meta, StoryObj } from "@storybook/react" import * as WeatherIcon from "@/features/weather/components/common/weather_icon" import * as WeatherInfoDetail from "@/features/weather/components/weather_info_detail" import { mock } from "@/features/weather/services/forecast/mock" -import { getWeatherCategory, transformWeatherInfoForCurrent } from "@/features/weather/utils" +import { + getWeatherCategory, + transformWeatherInfoForCurrent, + transformWeatherInfoForSpecificDay, +} from "@/features/weather/utils" import { Presenter } from "./presenter" @@ -22,6 +26,24 @@ export default meta type Story = StoryObj +export const Current: Story = { + args: { + weatherInfoDetailNode: ( + + ), + }, +} + +export const SpecificDay: Story = { + args: { + weatherInfoDetailNode: ( + + ), + }, +} + export const Sunny: Story = { args: { weatherCategory: "sunny", diff --git a/src/features/weather/components/weather_info_detail/presenter.stories.tsx b/src/features/weather/components/weather_info_detail/presenter.stories.tsx index d542aa0..142028a 100644 --- a/src/features/weather/components/weather_info_detail/presenter.stories.tsx +++ b/src/features/weather/components/weather_info_detail/presenter.stories.tsx @@ -1,7 +1,10 @@ import { Meta, StoryObj } from "@storybook/react" import { mock } from "@/features/weather/services/forecast/mock" -import { transformWeatherInfoForCurrent } from "@/features/weather/utils" +import { + transformWeatherInfoForCurrent, + transformWeatherInfoForSpecificDay, +} from "@/features/weather/utils" import { Presenter } from "./presenter" import * as styles from "./story.css" @@ -22,8 +25,14 @@ export default meta type Story = StoryObj -export const Default: Story = { +export const Current: Story = { args: { weatherInfo: transformWeatherInfoForCurrent(mock.current), }, } + +export const SpecificDay: Story = { + args: { + weatherInfo: transformWeatherInfoForSpecificDay(mock.forecast.forecastday[0].day), + }, +} diff --git a/src/features/weather/components/weather_info_detail/presenter.test.tsx b/src/features/weather/components/weather_info_detail/presenter.test.tsx index 5f8404d..7c7ebb9 100644 --- a/src/features/weather/components/weather_info_detail/presenter.test.tsx +++ b/src/features/weather/components/weather_info_detail/presenter.test.tsx @@ -4,11 +4,11 @@ import { describe, expect, test } from "vitest" import * as stories from "./presenter.stories" -const { Default } = composeStories(stories) +const { Current } = composeStories(stories) describe("Features/Weather/WeatherInfoDetail/Presenter", () => { test("天気詳細情報のリストが表示される", () => { - render() + render() expect(screen.getByRole("list")).toBeInTheDocument() }) })