diff --git a/app/components/Announce.stories.tsx b/app/components/Announce.stories.tsx new file mode 100644 index 0000000..60bdca6 --- /dev/null +++ b/app/components/Announce.stories.tsx @@ -0,0 +1,20 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { Announce } from "./Announce"; + +const meta = { + component: Announce +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + announceType: "warn", + title: "定期メンテナンスのお知らせ", + body: "以下の日時に定期メンテナンスを行います", + updatedAt: new Date("2023-09-10T00:00:00Z") + } +}; diff --git a/app/components/Announce.tsx b/app/components/Announce.tsx new file mode 100644 index 0000000..d3b99e2 --- /dev/null +++ b/app/components/Announce.tsx @@ -0,0 +1,56 @@ +import { IconButton, Text, ThickCheckIcon, Tooltip } from "@radix-ui/themes"; +import styles from "./announce.module.css"; +import { t } from "i18next"; +import { Time } from "~/components/Time"; +import { + ExclamationTriangleIcon, + InfoCircledIcon +} from "@radix-ui/react-icons"; + +export interface AnnounceProps { + announceType: "warn" | "info"; + title: string; + body: string; + updatedAt: Date; +} + +export const Announce = ({ + announceType, + title, + body, + updatedAt +}: AnnounceProps) => { + return ( +
+
+
+ + {announceType === "warn" ? ( + + ) : ( + + )} + + + {title} + +
+ + + + + +
+ +

{body}

+ + + +
+ ); +}; diff --git a/app/components/Time.stories.tsx b/app/components/Time.stories.tsx new file mode 100644 index 0000000..ae8084f --- /dev/null +++ b/app/components/Time.stories.tsx @@ -0,0 +1,17 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { Time } from "./Time"; + +const meta = { + component: Time +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + date: new Date() + } +}; diff --git a/app/components/Time.tsx b/app/components/Time.tsx new file mode 100644 index 0000000..88ea7d7 --- /dev/null +++ b/app/components/Time.tsx @@ -0,0 +1,54 @@ +export interface TimeProps { + date: Date; +} + +const timeFormatter = new Intl.DateTimeFormat(); +const relativeTimeFormatter = new Intl.RelativeTimeFormat(undefined, { + style: "short" +}); + +const Millisecond = 1 as const; +const SECOND = Millisecond * 1000; +const MINUTE = SECOND * 60; +const HOUR = MINUTE * 60; +const DAY = HOUR * 24; + +const getRelativeTimeDiff = (date: Date, now = new Date()) => { + const diffMilliseconds = now.getTime() - date.getTime(); + const absDiff = Math.abs(diffMilliseconds); + + if (absDiff < MINUTE) { + const diffSeconds = Math.floor(diffMilliseconds / SECOND); + return relativeTimeFormatter.format(-diffSeconds, "second"); + } else if (absDiff < HOUR) { + const diffMinutes = Math.floor(diffMilliseconds / MINUTE); + return relativeTimeFormatter.format(-diffMinutes, "minute"); + } else if (absDiff < DAY) { + const diffHours = Math.floor(diffMilliseconds / HOUR); + return relativeTimeFormatter.format(-diffHours, "hour"); + } + + const diffDays = Math.floor(diffMilliseconds / DAY); + if (Math.abs(diffDays) < 30) { + return relativeTimeFormatter.format(-diffDays, "day"); + } + + const diffMonths = Math.floor(diffDays / 30); + if (Math.abs(diffMonths) < 12) { + return relativeTimeFormatter.format(-diffMonths, "month"); + } + + const diffYears = Math.floor(diffMonths / 12); + return relativeTimeFormatter.format(-diffYears, "year"); +}; + +export const Time = ({ date }: TimeProps) => { + const formattedDate = timeFormatter.format(date); + const formattedRelativeDate = getRelativeTimeDiff(date); + + return ( +

+ {formattedDate} ({formattedRelativeDate}) +

+ ); +}; diff --git a/app/components/announce.module.css b/app/components/announce.module.css new file mode 100644 index 0000000..69611dd --- /dev/null +++ b/app/components/announce.module.css @@ -0,0 +1,31 @@ +.announceTitleContainer { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + + .announceIcon { + width: 1.5rem; + height: 1.5rem; + margin-right: .5rem; + } + + .readButtonIcon { + width: .8rem; + height: .8rem; + } + + .announceTitle { + display: flex; + flex-direction: row; + align-items: center; + } + + margin-bottom: .5em; + + p { + margin: 0; + padding: 0; + } + +} diff --git a/i18n/locales/en_US.json b/i18n/locales/en_US.json index 26678ca..17e5949 100644 --- a/i18n/locales/en_US.json +++ b/i18n/locales/en_US.json @@ -15,5 +15,8 @@ "followers": "followers", "editProfile": "edit profile", "blocking": "blocking", - "followBack": "followback" + "followBack": "followback", + "notification.read": "Mark as read", + "announce.warn": "Warning", + "announce.info": "Information" } diff --git a/i18n/locales/ja_JP.json b/i18n/locales/ja_JP.json index ebf6fee..2f6c72c 100644 --- a/i18n/locales/ja_JP.json +++ b/i18n/locales/ja_JP.json @@ -15,5 +15,8 @@ "followers": "フォロワー", "editProfile": "プロフィールを編集", "blocking": "ブロック中", - "followBack": "フォローバック" + "followBack": "フォローバック", + "notification.read": "既読にする", + "announce.warn": "警告", + "announce.info": "情報" } diff --git a/package.json b/package.json index 0f35b15..5bfe26d 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "build-storybook": "storybook build" }, "dependencies": { + "@radix-ui/react-icons": "^1.3.0", "@radix-ui/themes": "^3.1.1", "@remix-run/node": "^2.10.3", "@remix-run/react": "^2.10.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1fa1cae..91fd8c0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@radix-ui/react-icons': + specifier: ^1.3.0 + version: 1.3.0(react@18.3.1) '@radix-ui/themes': specifier: ^3.1.1 version: 3.1.3(@types/react-dom@18.3.0)(@types/react@18.3.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1084,6 +1087,11 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-icons@1.3.0': + resolution: {integrity: sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==} + peerDependencies: + react: ^16.x || ^17.x || ^18.x + '@radix-ui/react-id@1.1.0': resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} peerDependencies: @@ -5412,6 +5420,10 @@ snapshots: '@types/react': 18.3.8 '@types/react-dom': 18.3.0 + '@radix-ui/react-icons@1.3.0(react@18.3.1)': + dependencies: + react: 18.3.1 + '@radix-ui/react-id@1.1.0(@types/react@18.3.8)(react@18.3.1)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.8)(react@18.3.1)