Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 特定日の天気詳細を表示する #31

Merged
merged 13 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion src/app/pages/specific_day.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<LocationContextProvider>
<Section>
<LocationInput.Container />
<HeadGroup>
<BackButton.Container />
<SpecificDate.Container />
</HeadGroup>
<ErrorBoundary fallbackRender={LocationInput.Error}>
<LocationInput.Container />
</ErrorBoundary>
<ErrorBoundary fallbackRender={WeatherSpecificDay.Error}>
<Suspense fallback={<WeatherSpecificDay.Loading />}>
<WeatherSpecificDay.Container />
</Suspense>
</ErrorBoundary>
</Section>
</LocationContextProvider>
)
Expand Down
36 changes: 36 additions & 0 deletions src/components/ui/Button/index.css.ts
Original file line number Diff line number Diff line change
@@ -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<typeof button>
56 changes: 56 additions & 0 deletions src/components/ui/Button/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof Button>

export default meta
type Story = StoryObj<typeof meta>

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" },
}
14 changes: 14 additions & 0 deletions src/components/ui/Button/index.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLButtonElement, ButtonProps>(function ButtonBase(
{ color, size, className, ...props },
ref,
) {
return <button className={clsx(styles.button({ color, size }), className)} ref={ref} {...props} />
})
7 changes: 7 additions & 0 deletions src/components/ui/HeadGroup/index.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { style } from "@vanilla-extract/css"

export const module = style({
display: "flex",
alignItems: "center",
justifyContent: "space-between",
})
26 changes: 26 additions & 0 deletions src/components/ui/HeadGroup/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Meta, StoryObj } from "@storybook/react"

import { Button } from "../Button"
import { Heading } from "../Heading"
import { HeadGroup } from "."

const meta = {
title: "UI/HeadGroup",
component: HeadGroup,
tags: ["autodocs"],
args: {
children: (
<>
<Heading className="grow" level={"h1"}>
見出し見出し
</Heading>
<Button size="sm">詳細へ</Button>
</>
),
},
} satisfies Meta<typeof HeadGroup>

export default meta
type Story = StoryObj<typeof meta>

export const Default: Story = {}
13 changes: 13 additions & 0 deletions src/components/ui/HeadGroup/index.tsx
Original file line number Diff line number Diff line change
@@ -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 <header className={clsx(styles.module, className)}>{children}</header>
}
23 changes: 23 additions & 0 deletions src/features/weather/components/back_button/container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ArrowLeft } from "lucide-react"
import { useNavigate } from "react-router-dom"

import { useLinkProps } from "@/hooks"

import { Presenter, PresenterProps } from "./presenter"

export const Container = () => {
const navigate = useNavigate()
const { generateLinkProps } = useLinkProps()

const presenterProps = {
children: (
<>
<ArrowLeft />
戻る
</>
),
onClick: () => navigate(generateLinkProps({ pathname: "/", removeParams: ["date"] }).to),
} satisfies PresenterProps

return <Presenter {...presenterProps} />
}
2 changes: 2 additions & 0 deletions src/features/weather/components/back_button/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./container"
export * from "./presenter"
6 changes: 6 additions & 0 deletions src/features/weather/components/back_button/presenter.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { style } from "@vanilla-extract/css"

export const module = style({
display: "flex",
gap: "0.5rem",
})
19 changes: 19 additions & 0 deletions src/features/weather/components/back_button/presenter.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Meta, StoryObj } from "@storybook/react"

import { Presenter } from "./presenter"

const meta = {
title: "Features/Weather/BackButton/Presenter",
tags: ["autodocs"],
component: Presenter,
args: {
children: "戻る",
},
} satisfies Meta<typeof Presenter>
export default meta

type Story = StoryObj<typeof meta>

export const Default: Story = {
args: {},
}
11 changes: 11 additions & 0 deletions src/features/weather/components/back_button/presenter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ComponentProps } from "react"

import { Button } from "@/components/ui/Button"

import * as styles from "./presenter.css"

export type PresenterProps = ComponentProps<typeof Button>

export const Presenter = ({ ...props }: PresenterProps) => {
return <Button className={styles.module} {...props} />
}
6 changes: 5 additions & 1 deletion src/features/weather/components/location_input/container.tsx
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -11,15 +12,18 @@ export const Container = () => {
const [inputLocation, setInputLocation] = useState<string>("")
const [debouncedValue] = useDebouncedValue(inputLocation, 300)

const { setSearchParams, date } = useWeatherParams({})

const { setLocation } = useLocationContext()

const onClickHandler = useCallback(
(location: string) => {
setLocation(location)
setInputLocation(location)
setIsFocused(false)
setSearchParams({ location, date })
},
[setLocation],
[setLocation, setSearchParams, date],
)

const handleFocus = useCallback(() => {
Expand Down
12 changes: 12 additions & 0 deletions src/features/weather/components/specific_date/container.tsx
Original file line number Diff line number Diff line change
@@ -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 <Presenter {...presenterProps} />
}
2 changes: 2 additions & 0 deletions src/features/weather/components/specific_date/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./container"
export * from "./presenter"
18 changes: 18 additions & 0 deletions src/features/weather/components/specific_date/presenter.css.ts
Original file line number Diff line number Diff line change
@@ -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"],
},
}),
])
Original file line number Diff line number Diff line change
@@ -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<typeof Presenter>
export default meta

type Story = StoryObj<typeof meta>

export const Default: Story = {
args: {},
}
17 changes: 17 additions & 0 deletions src/features/weather/components/specific_date/presenter.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<Default />)
const time = screen.getByTestId("datetime")
expect(time).toBeInTheDocument()
expect(time).toHaveAttribute("datetime", date)
})
})
13 changes: 13 additions & 0 deletions src/features/weather/components/specific_date/presenter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as styles from "./presenter.css"

export type PresenterProps = {
date: string
}

export const Presenter = ({ ...props }: PresenterProps) => {
return (
<time data-testid="datetime" dateTime={props.date} className={styles.module}>
{props.date}
</time>
)
}
Loading