From 2bf38f2778fd904290eb3efd5fc5101483e7b7de Mon Sep 17 00:00:00 2001 From: cp-20 <47262658+mario-hsp@users.noreply.github.com> Date: Sat, 3 Feb 2024 22:13:08 +0900 Subject: [PATCH 1/6] =?UTF-8?q?=E3=81=A8=E3=82=8A=E3=81=82=E3=81=88?= =?UTF-8?q?=E3=81=9A=E3=82=A2=E3=83=BC=E3=82=AB=E3=82=A4=E3=83=96=E3=83=9C?= =?UTF-8?q?=E3=82=BF=E3=83=B3=E3=81=A8=E5=89=8A=E9=99=A4=E3=83=9C=E3=82=BF?= =?UTF-8?q?=E3=83=B3=E3=82=92=E3=81=A4=E3=81=91=E3=81=9F=20(=E4=B8=AD?= =?UTF-8?q?=E8=BA=AB=E3=81=AF=E3=81=AA=E3=81=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/Article/ArticleListItem.tsx | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/apps/web/src/client/Home/_components/Article/ArticleListItem.tsx b/apps/web/src/client/Home/_components/Article/ArticleListItem.tsx index 56694bc..f504915 100644 --- a/apps/web/src/client/Home/_components/Article/ArticleListItem.tsx +++ b/apps/web/src/client/Home/_components/Article/ArticleListItem.tsx @@ -1,6 +1,7 @@ import { css } from '@emotion/react'; -import { Text, useMantineTheme } from '@mantine/core'; +import { ActionIcon, Text, useMantineTheme } from '@mantine/core'; import type { Article } from '@read-stack/openapi'; +import { IconArchive, IconTrash } from '@tabler/icons-react'; import type { ComponentProps, FC, ReactNode } from 'react'; export interface ArticleListItemProps { @@ -67,6 +68,24 @@ export const ArticleListItem: FC> =
+
+ void 0}> + + + void 0}> + + +
{article.ogImageUrl ? ( Date: Fri, 9 Feb 2024 18:46:24 +0900 Subject: [PATCH 2/6] format --- .../_components/Article/AnimatedListItem.tsx | 6 +- packages/database/drizzle.config.ts | 4 +- .../database/drizzle/meta/0008_snapshot.json | 132 +++++------------- packages/database/drizzle/meta/_journal.json | 2 +- 4 files changed, 38 insertions(+), 106 deletions(-) diff --git a/apps/web/src/client/Home/_components/Article/AnimatedListItem.tsx b/apps/web/src/client/Home/_components/Article/AnimatedListItem.tsx index 0739837..88e6a1c 100644 --- a/apps/web/src/client/Home/_components/Article/AnimatedListItem.tsx +++ b/apps/web/src/client/Home/_components/Article/AnimatedListItem.tsx @@ -16,9 +16,9 @@ const UnmemorizedAnimatedListItem: FC = ({ css={css` min-height: 0; max-height: ${isRemoved ? '0' : '300px'}; - animation: ${isRemoved - ? 'disappear 0.2s forwards' - : 'appear 0.2s forwards'}; + animation: ${ + isRemoved ? 'disappear 0.2s forwards' : 'appear 0.2s forwards' + }; opacity: ${isRemoved ? 0 : 1}; transform: ${isRemoved ? 'scale(0.9)' : 'none'}; transition: diff --git a/packages/database/drizzle.config.ts b/packages/database/drizzle.config.ts index 087cafc..77f4d93 100644 --- a/packages/database/drizzle.config.ts +++ b/packages/database/drizzle.config.ts @@ -7,7 +7,7 @@ if (connectionString === undefined) { } // eslint-disable-next-line import/no-default-export -- for drizzle -export default { +export default ({ schema: './src/models/index.ts', out: './drizzle', driver: 'pg', @@ -16,4 +16,4 @@ export default { }, verbose: true, strict: true, -} satisfies Config; +} satisfies Config); diff --git a/packages/database/drizzle/meta/0008_snapshot.json b/packages/database/drizzle/meta/0008_snapshot.json index 7e3ac05..2c5d689 100644 --- a/packages/database/drizzle/meta/0008_snapshot.json +++ b/packages/database/drizzle/meta/0008_snapshot.json @@ -40,12 +40,8 @@ "name": "api_keys_user_id_users_id_fk", "tableFrom": "api_keys", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -55,9 +51,7 @@ "user_id_in_api_keys": { "name": "user_id_in_api_keys", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } } }, @@ -97,12 +91,8 @@ "name": "article_refs_refer_from_articles_id_fk", "tableFrom": "article_refs", "tableTo": "articles", - "columnsFrom": [ - "refer_from" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["refer_from"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -110,12 +100,8 @@ "name": "article_refs_refer_to_articles_id_fk", "tableFrom": "article_refs", "tableTo": "articles", - "columnsFrom": [ - "refer_to" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["refer_to"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -123,10 +109,7 @@ "compositePrimaryKeys": { "article_refs_refer_from_refer_to_pk": { "name": "article_refs_refer_from_refer_to_pk", - "columns": [ - "refer_from", - "refer_to" - ] + "columns": ["refer_from", "refer_to"] } }, "uniqueConstraints": {} @@ -155,9 +138,7 @@ "article_tags_name_unique": { "name": "article_tags_name_unique", "nullsNotDistinct": false, - "columns": [ - "name" - ] + "columns": ["name"] } } }, @@ -197,12 +178,8 @@ "name": "article_tags_on_articles_article_id_articles_id_fk", "tableFrom": "article_tags_on_articles", "tableTo": "articles", - "columnsFrom": [ - "article_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["article_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -210,12 +187,8 @@ "name": "article_tags_on_articles_tag_id_article_tags_id_fk", "tableFrom": "article_tags_on_articles", "tableTo": "article_tags", - "columnsFrom": [ - "tag_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["tag_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -223,10 +196,7 @@ "compositePrimaryKeys": { "article_tags_on_articles_article_id_tag_id_pk": { "name": "article_tags_on_articles_article_id_tag_id_pk", - "columns": [ - "article_id", - "tag_id" - ] + "columns": ["article_id", "tag_id"] } }, "uniqueConstraints": {} @@ -292,9 +262,7 @@ "articles_url_unique": { "name": "articles_url_unique", "nullsNotDistinct": false, - "columns": [ - "url" - ] + "columns": ["url"] } } }, @@ -358,12 +326,8 @@ "name": "clips_article_id_articles_id_fk", "tableFrom": "clips", "tableTo": "articles", - "columnsFrom": [ - "article_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["article_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -371,12 +335,8 @@ "name": "clips_user_id_users_id_fk", "tableFrom": "clips", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -386,10 +346,7 @@ "article_and_user_in_clip": { "name": "article_and_user_in_clip", "nullsNotDistinct": false, - "columns": [ - "article_id", - "user_id" - ] + "columns": ["article_id", "user_id"] } } }, @@ -435,12 +392,8 @@ "name": "inboxes_user_id_users_id_fk", "tableFrom": "inboxes", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -448,12 +401,8 @@ "name": "inboxes_article_id_articles_id_fk", "tableFrom": "inboxes", "tableTo": "articles", - "columnsFrom": [ - "article_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["article_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -463,10 +412,7 @@ "article_and_user_in_inbox": { "name": "article_and_user_in_inbox", "nullsNotDistinct": false, - "columns": [ - "article_id", - "user_id" - ] + "columns": ["article_id", "user_id"] } } }, @@ -513,10 +459,7 @@ "article_and_rss_url_in_rss_contents": { "name": "article_and_rss_url_in_rss_contents", "nullsNotDistinct": false, - "columns": [ - "rss_url", - "article_url" - ] + "columns": ["rss_url", "article_url"] } } }, @@ -568,12 +511,8 @@ "name": "rss_items_user_id_users_id_fk", "tableFrom": "rss_items", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -583,10 +522,7 @@ "user_id_and_url_in_rss_items": { "name": "user_id_and_url_in_rss_items", "nullsNotDistinct": false, - "columns": [ - "user_id", - "url" - ] + "columns": ["user_id", "url"] } } }, @@ -641,9 +577,7 @@ "indexes": { "idx_email_on_users": { "name": "idx_email_on_users", - "columns": [ - "email" - ], + "columns": ["email"], "isUnique": false } }, @@ -653,9 +587,7 @@ "email_on_users": { "name": "email_on_users", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] } } } @@ -667,4 +599,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/packages/database/drizzle/meta/_journal.json b/packages/database/drizzle/meta/_journal.json index 23e881e..32c279a 100644 --- a/packages/database/drizzle/meta/_journal.json +++ b/packages/database/drizzle/meta/_journal.json @@ -66,4 +66,4 @@ "breakpoints": true } ] -} \ No newline at end of file +} From d8e7f6ade486ddd99668b837fc9d51d7d198f6fe Mon Sep 17 00:00:00 2001 From: cp-20 <47262658+mario-hsp@users.noreply.github.com> Date: Sat, 10 Feb 2024 01:22:15 +0900 Subject: [PATCH 3/6] =?UTF-8?q?=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF?= =?UTF-8?q?=E3=82=BF=E3=83=AA=E3=83=B3=E3=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/Article/AddNewClipForm.tsx | 3 +- .../Home/_components/Article/AnimatedList.tsx | 16 +-- .../_components/Article/AnimatedListItem.tsx | 12 +- .../Home/_components/Article/ArticleList.tsx | 18 +-- .../_components/Article/ArticleListItem.tsx | 135 +++++++++--------- .../ArticleListModel/InboxItemList.tsx | 22 +-- .../ArticleListModel/ReadClipList.tsx | 18 +-- .../ArticleListModel/UnreadClipList.tsx | 28 ++-- 8 files changed, 108 insertions(+), 144 deletions(-) diff --git a/apps/web/src/client/Home/_components/Article/AddNewClipForm.tsx b/apps/web/src/client/Home/_components/Article/AddNewClipForm.tsx index bf32b53..61c080f 100644 --- a/apps/web/src/client/Home/_components/Article/AddNewClipForm.tsx +++ b/apps/web/src/client/Home/_components/Article/AddNewClipForm.tsx @@ -53,8 +53,7 @@ export const AddNewClipForm: FC = () => { if (prev === undefined) return []; const newResult: FetchArticleResult = { - articles: [clip.article], - clips: [clip], + articles: [{ ...clip.article, clip }], finished: false, }; diff --git a/apps/web/src/client/Home/_components/Article/AnimatedList.tsx b/apps/web/src/client/Home/_components/Article/AnimatedList.tsx index d2fca95..aa407b7 100644 --- a/apps/web/src/client/Home/_components/Article/AnimatedList.tsx +++ b/apps/web/src/client/Home/_components/Article/AnimatedList.tsx @@ -1,7 +1,7 @@ import type { AnimatedListItemProps } from '@/client/Home/_components/Article/AnimatedListItem'; import { AnimatedListItem } from '@/client/Home/_components/Article/AnimatedListItem'; import type { Article } from '@read-stack/openapi'; -import { type FC, useState, useEffect, Fragment } from 'react'; +import { useState, useEffect, Fragment } from 'react'; // FIXME: 入れ替えとかで壊れるかも // 和集合を順序をなるべく考慮して取る @@ -51,21 +51,21 @@ const mergeArray = (prev: T[], next: T[]) => { return result; }; -interface AnimatedListProps { - articles: Article[]; +interface AnimatedListProps { + articles: (Article & T)[]; duration: number; - itemProps?: Omit; + itemProps?: Omit, 'article' | 'isRemoved'>; } const isSubset = (a: T[], b: T[]) => a.every((v) => b.includes(v)); -export const AnimatedList: FC = ({ +export const AnimatedList = ({ articles, duration, itemProps, -}) => { +}: AnimatedListProps) => { const [displayedArticles, setDisplayedArticles] = - useState(articles); + useState<(Article & T)[]>(articles); const [removingArticleIds, setRemovingArticleIds] = useState([]); const articleIds = articles.map((a) => a.id); @@ -110,7 +110,7 @@ export const AnimatedList: FC = ({ return ( <> {displayedArticles.map((a) => ( - article={a} isRemoved={removingArticleIds.includes(a.id)} key={a.id} diff --git a/apps/web/src/client/Home/_components/Article/AnimatedListItem.tsx b/apps/web/src/client/Home/_components/Article/AnimatedListItem.tsx index 88e6a1c..3fc1f0c 100644 --- a/apps/web/src/client/Home/_components/Article/AnimatedListItem.tsx +++ b/apps/web/src/client/Home/_components/Article/AnimatedListItem.tsx @@ -1,16 +1,16 @@ import type { ArticleListItemProps } from '@/client/Home/_components/Article/ArticleListItem'; import { ArticleListItem } from '@/client/Home/_components/Article/ArticleListItem'; import { css } from '@emotion/react'; -import { memo, type FC } from 'react'; +import { memo } from 'react'; -export type AnimatedListItemProps = { +export type AnimatedListItemProps = { isRemoved: boolean; -} & ArticleListItemProps; +} & ArticleListItemProps; -const UnmemorizedAnimatedListItem: FC = ({ +const UnmemorizedAnimatedListItem = ({ isRemoved, ...props -}) => { +}: AnimatedListItemProps) => { return ( = { - articles: Article[]; +export interface FetchArticleResult { + articles: (Article & T)[]; finished: boolean; -} & T; +} export interface ArticleListProps { stateKey: T; @@ -25,10 +25,7 @@ export interface ArticleListProps { prev?: FetchArticleResult, ) => string | null; fetcher: (url: string) => Promise>; - renderActions?: ( - article: Article, - results: FetchArticleResult[], - ) => ReactNode; + renderActions?: (article: Article & AdditionalProps[T]) => ReactNode; noContentComponent: ReactNode; } @@ -110,12 +107,7 @@ export const ArticleList = ({ { - if (!data) return; - return renderActions?.(a, data); - }, - }} + itemProps={{ renderActions }} />
diff --git a/apps/web/src/client/Home/_components/Article/ArticleListItem.tsx b/apps/web/src/client/Home/_components/Article/ArticleListItem.tsx index f504915..ce8850e 100644 --- a/apps/web/src/client/Home/_components/Article/ArticleListItem.tsx +++ b/apps/web/src/client/Home/_components/Article/ArticleListItem.tsx @@ -2,43 +2,46 @@ import { css } from '@emotion/react'; import { ActionIcon, Text, useMantineTheme } from '@mantine/core'; import type { Article } from '@read-stack/openapi'; import { IconArchive, IconTrash } from '@tabler/icons-react'; -import type { ComponentProps, FC, ReactNode } from 'react'; +import type { ComponentProps, ReactNode } from 'react'; -export interface ArticleListItemProps { - article: Article; - renderActions?: (article: Article) => ReactNode; +export interface ArticleListItemProps { + article: Article & T; + renderActions?: (article: Article & T) => ReactNode; } -export const ArticleListItem: FC> = - ({ article, renderActions, ...props }) => { - const theme = useMantineTheme(); - return ( -
-
({ + article, + renderActions, + ...props +}: ArticleListItemProps & ComponentProps<'div'>) => { + const theme = useMantineTheme(); + return ( +
+
-
+
-
+
- - + + > = text-decoration: underline; } `} - href={article.url} - rel="noopener noreferrer" - target="_blank" - > - {article.title} - - - - {article.body} - - + {article.title} + + + + {article.body} + + - {new URL(article.url).host} - -
-
-
+ {new URL(article.url).host} + +
+
+
> = color: ${theme.white}; gap: 0.1rem; `} - > - void 0}> - - - void 0}> - - -
- {article.ogImageUrl ? ( - + void 0}> + + + void 0}> + + +
+ {article.ogImageUrl ? ( + - ) : null} -
+ src={article.ogImageUrl} + width="100%" + /> + ) : null}
-
+
- {renderActions ? renderActions(article) : null} -
+ > + {renderActions ? renderActions(article) : null}
- ); - }; +
+ ); +}; diff --git a/apps/web/src/client/Home/_components/ArticleListModel/InboxItemList.tsx b/apps/web/src/client/Home/_components/ArticleListModel/InboxItemList.tsx index c79c070..f6a317f 100644 --- a/apps/web/src/client/Home/_components/ArticleListModel/InboxItemList.tsx +++ b/apps/web/src/client/Home/_components/ArticleListModel/InboxItemList.tsx @@ -3,7 +3,7 @@ import { ArticleList } from '@/client/Home/_components/Article/ArticleList'; import { ArticleListLayout, keyConstructorGenerator } from './common'; import { fetcher } from '@/features/swr/fetcher'; import { Stack, Button, Text } from '@mantine/core'; -import type { InboxItemWithArticle } from '@read-stack/openapi'; +import type { InboxItem, InboxItemWithArticle } from '@read-stack/openapi'; import { getInboxItemsResponseSchema, moveInboxItemToClipResponseSchema, @@ -22,16 +22,15 @@ import { } from './UnreadClipList'; export interface InboxItemAdditionalProps { - items: InboxItemWithArticle[]; + item: InboxItem; } export const inboxFetcher = async (url: string) => { const res = await fetcher(url); const body = getInboxItemsResponseSchema.parse(res); const result: FetchArticleResult = { - articles: body.items.map((item) => item.article), + articles: body.items.map((item) => ({ ...item.article, item })), finished: body.finished, - items: body.items, }; return result; }; @@ -78,7 +77,6 @@ const ActionSection: FC = ({ item }) => { const result = prev.map((r) => ({ ...r, articles: r.articles.filter((a) => a.id !== item.articleId), - items: r.items.filter((i) => i.id !== item.id), })); return result; @@ -98,8 +96,7 @@ const ActionSection: FC = ({ item }) => { if (prev === undefined) return []; const newResult: FetchArticleResult = { - articles: [item.article], - clips: [{ ...clip, article: item.article }], + articles: [{ ...item.article, clip }], finished: false, }; @@ -132,14 +129,9 @@ export const InboxItemList: FC = () => { fetcher={inboxFetcher} keyConstructor={inboxKeyConstructor} noContentComponent={NoContentComponent} - renderActions={(article, results) => { - const item = results - .flatMap((r) => r.items) - .find((i) => i.articleId === article.id); - if (item === undefined) return null; - - return ; - }} + renderActions={(article) => ( + + )} stateKey="inboxItem" /> diff --git a/apps/web/src/client/Home/_components/ArticleListModel/ReadClipList.tsx b/apps/web/src/client/Home/_components/ArticleListModel/ReadClipList.tsx index d5e6333..9025626 100644 --- a/apps/web/src/client/Home/_components/ArticleListModel/ReadClipList.tsx +++ b/apps/web/src/client/Home/_components/ArticleListModel/ReadClipList.tsx @@ -21,16 +21,15 @@ import { import { useMutators } from './useMutators'; export interface ReadClipAdditionalProps { - clips: ClipWithArticle[]; + clip: ClipWithArticle; } export const readClipsFetcher = async (url: string) => { const res = await fetcher(url); const body = getClipsResponseSchema.parse(res); const result: FetchArticleResult = { - articles: body.clips.map((clip) => clip.article), + articles: body.clips.map((clip) => ({ ...clip.article, clip })), finished: body.finished, - clips: body.clips, }; return result; }; @@ -78,7 +77,6 @@ const ActionSection: FC = ({ clip }) => { const result = prev.map((r) => ({ ...r, articles: r.articles.filter((a) => a.id !== clip.articleId), - clips: r.clips.filter((c) => c.id !== clip.id), })); return result; @@ -97,8 +95,7 @@ const ActionSection: FC = ({ clip }) => { if (prev === undefined) return []; const newResult: FetchArticleResult = { - articles: [clip.article], - clips: [{ ...clip, status: 2 }], + articles: [{ ...clip.article, clip: { ...clip, status: 2 } }], finished: false, }; @@ -131,14 +128,7 @@ export const ReadClipList: FC = () => { fetcher={readClipsFetcher} keyConstructor={readClipsKeyConstructor} noContentComponent={NoContentComponent} - renderActions={(article, results) => { - const clip = results - .flatMap((r) => r.clips) - .find((c) => c.articleId === article.id); - if (clip === undefined) return null; - - return ; - }} + renderActions={(article) => } stateKey="readClip" /> diff --git a/apps/web/src/client/Home/_components/ArticleListModel/UnreadClipList.tsx b/apps/web/src/client/Home/_components/ArticleListModel/UnreadClipList.tsx index db668f4..e8a3e58 100644 --- a/apps/web/src/client/Home/_components/ArticleListModel/UnreadClipList.tsx +++ b/apps/web/src/client/Home/_components/ArticleListModel/UnreadClipList.tsx @@ -3,7 +3,7 @@ import { ArticleList } from '@/client/Home/_components/Article/ArticleList'; import { ArticleListLayout, keyConstructorGenerator } from './common'; import { fetcher } from '@/features/swr/fetcher'; import { Stack, Button, Text } from '@mantine/core'; -import type { ClipWithArticle } from '@read-stack/openapi'; +import type { Clip, ClipWithArticle } from '@read-stack/openapi'; import { getClipsResponseSchema, moveUserClipToInboxResponseSchema, @@ -27,16 +27,15 @@ import { readClipsFetcher, readClipsKeyConstructor } from './ReadClipList'; import { AddNewClipForm } from '@/client/Home/_components/Article/AddNewClipForm'; export interface UnreadClipAdditionalProps { - clips: ClipWithArticle[]; + clip: Clip; } export const unreadClipsFetcher = async (url: string) => { const res = await fetcher(url); const body = getClipsResponseSchema.parse(res); const result: FetchArticleResult = { - articles: body.clips.map((clip) => clip.article), + articles: body.clips.map((clip) => ({ ...clip.article, clip })), finished: body.finished, - clips: body.clips, }; return result; }; @@ -115,7 +114,6 @@ const ActionSection: FC = ({ clip }) => { const result = prev.map((r) => ({ ...r, articles: r.articles.filter((a) => a.id !== clip.articleId), - clips: r.clips.filter((c) => c.id !== clip.id), })); return result; @@ -135,8 +133,7 @@ const ActionSection: FC = ({ clip }) => { if (prev === undefined) return []; const newResult: FetchArticleResult = { - articles: [clip.article], - items: [{ ...item, article: clip.article }], + articles: [{ ...clip.article, item }], finished: false, }; @@ -175,7 +172,6 @@ const ActionSection: FC = ({ clip }) => { const result = prev.map((r) => ({ ...r, articles: r.articles.filter((a) => a.id !== clip.articleId), - clips: r.clips.filter((c) => c.id !== clip.id), })); return result; @@ -194,8 +190,7 @@ const ActionSection: FC = ({ clip }) => { if (prev === undefined) return []; const newResult: FetchArticleResult = { - articles: [clip.article], - clips: [{ ...clip, status: 2 }], + articles: [{ ...clip.article, clip: { ...clip, status: 2 } }], finished: false, }; @@ -225,16 +220,9 @@ export const UnreadClipList: FC = () => { fetcher={unreadClipsFetcher} keyConstructor={unreadClipsKeyConstructor} noContentComponent={NoContentComponent} - renderActions={(article, results) => { - const clip = results - .flatMap((r) => r.clips) - .find((c) => c.articleId === article.id); - if (clip === undefined) { - return ; - } - - return ; - }} + renderActions={(article) => ( + + )} stateKey="unreadClip" /> From 7b269b26f6aef2f029d405f48072533796561b14 Mon Sep 17 00:00:00 2001 From: cp-20 <47262658+mario-hsp@users.noreply.github.com> Date: Fri, 16 Feb 2024 23:55:27 +0900 Subject: [PATCH 4/6] =?UTF-8?q?inbox=E3=81=AEitem=E3=82=92=E6=97=A2?= =?UTF-8?q?=E8=AA=AD=E3=81=AB=E3=81=99=E3=82=8B=20(=E3=82=A2=E3=83=BC?= =?UTF-8?q?=E3=82=AB=E3=82=A4=E3=83=96=E3=81=99=E3=82=8B)=20API=E3=82=92?= =?UTF-8?q?=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/server/src/handlers/users.ts | 28 ++++++++++ packages/openapi/src/index.ts | 1 + .../src/routes/users/archiveUserInboxItem.ts | 54 +++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 packages/openapi/src/routes/users/archiveUserInboxItem.ts diff --git a/apps/server/src/handlers/users.ts b/apps/server/src/handlers/users.ts index a5878fd..110f97f 100644 --- a/apps/server/src/handlers/users.ts +++ b/apps/server/src/handlers/users.ts @@ -20,6 +20,7 @@ import { } from '@read-stack/database'; import { fetchArticle, parseIntWithDefaultValue } from '@read-stack/lib'; import { + archiveMyInboxItemRoute, deleteMyApiKeyRoute, deleteMyClipRoute, deleteMyInboxItemRoute, @@ -478,4 +479,31 @@ export const registerUsersHandlers = (app: WithSupabaseClient) => { return c.json({ item }, 200); }); + + app.openapi(archiveMyInboxItemRoute, async (c) => { + const user = await getUser(c); + if (user === null) return c.json({}, 401); + + const itemIdStr = c.req.param('itemId'); + const itemId = parseIntWithDefaultValue(itemIdStr, null); + + if (itemId === null) { + return c.json({ error: 'itemId is not configured and valid' }, 400); + } + + const item = await findInboxItemById(itemId); + if (item === undefined || item.userId !== user.id) { + return c.json({ error: 'item not found' }, 404); + } + + await deleteInboxItemByIdAndUserId(itemId, user.id); + const { clip } = await saveClip(item.articleId, user.id, () => ({ + articleId: item.articleId, + userId: user.id, + progress: 100, + status: 2, + })); + + return c.json({ clip }, 200); + }); }; diff --git a/packages/openapi/src/index.ts b/packages/openapi/src/index.ts index 281afb8..33c3eb1 100644 --- a/packages/openapi/src/index.ts +++ b/packages/openapi/src/index.ts @@ -19,5 +19,6 @@ export * from '@/routes/users/postUserInboxItem'; export * from '@/routes/users/deleteUserInboxItem'; export * from '@/routes/users/moveUserInboxItemToClip'; export * from '@/routes/users/moveUserClipToInbox'; +export * from '@/routes/users/archiveUserInboxItem'; export * from '@/routes/articles/getArticle'; export * from '@/routes/articles/postArticle'; diff --git a/packages/openapi/src/routes/users/archiveUserInboxItem.ts b/packages/openapi/src/routes/users/archiveUserInboxItem.ts new file mode 100644 index 0000000..dde0605 --- /dev/null +++ b/packages/openapi/src/routes/users/archiveUserInboxItem.ts @@ -0,0 +1,54 @@ +import type { RouteConfig } from '@asteasolutions/zod-to-openapi'; +import { createRoute } from '@hono/zod-openapi'; +import { z } from 'zod'; + +import { + badRequestResponse, + internalServerErrorResponse, + notFoundResponse, + okJsonResponse, + unauthorizedResponse, +} from '@/routes/helpers/response'; +import { userIdPathRouteHelper } from '@/routes/users/common'; +import { ClipSchema } from '@/schema'; + +export const archiveInboxItemRequestParamsSchema = z.object({ + itemId: z.string().openapi({ + param: { + name: 'itemId', + in: 'path', + required: true, + description: 'アイテムのID', + }, + example: '1', + }), +}); + +export const archiveInboxItemResponseSchema = z.object({ + clip: ClipSchema, +}); + +const archiveInboxItemRouteBase = { + method: 'post', + path: '/users/me/inboxes/{itemId}/archive' as const, + operationId: 'archiveMyInboxItem', + description: '受信箱のアイテムをアーカイブに移動します', + request: { + params: archiveInboxItemRequestParamsSchema, + }, + responses: { + ...okJsonResponse({ + schema: archiveInboxItemResponseSchema, + }), + ...badRequestResponse(), + ...unauthorizedResponse(), + ...notFoundResponse(), + ...internalServerErrorResponse(), + }, +} satisfies RouteConfig; + +export const archiveMyInboxItemRoute = createRoute(archiveInboxItemRouteBase); + +export const archiveUserInboxItemRoute = createRoute( + userIdPathRouteHelper(archiveInboxItemRouteBase), +); From d499f02a2af25fcb25eb4c1ffab9a5c9cfd18402 Mon Sep 17 00:00:00 2001 From: cp-20 <47262658+mario-hsp@users.noreply.github.com> Date: Fri, 16 Feb 2024 23:55:39 +0900 Subject: [PATCH 5/6] =?UTF-8?q?=E8=A8=98=E4=BA=8B=E3=81=AE=E3=82=A2?= =?UTF-8?q?=E3=83=BC=E3=82=AB=E3=82=A4=E3=83=96=20/=20=E5=89=8A=E9=99=A4?= =?UTF-8?q?=E3=82=92=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/_components/Article/ArticleList.tsx | 10 +- .../_components/Article/ArticleListItem.tsx | 28 +- .../ArticleListModel/InboxItemList.tsx | 243 +++++++++---- .../ArticleListModel/ReadClipList.tsx | 186 ++++++---- .../ArticleListModel/UnreadClipList.tsx | 338 ++++++++++-------- 5 files changed, 501 insertions(+), 304 deletions(-) diff --git a/apps/web/src/client/Home/_components/Article/ArticleList.tsx b/apps/web/src/client/Home/_components/Article/ArticleList.tsx index a5a3567..96f0dc1 100644 --- a/apps/web/src/client/Home/_components/Article/ArticleList.tsx +++ b/apps/web/src/client/Home/_components/Article/ArticleList.tsx @@ -1,4 +1,5 @@ import { AnimatedList } from '@/client/Home/_components/Article/AnimatedList'; +import type { ArticleListItemProps } from '@/client/Home/_components/Article/ArticleListItem'; import type { AdditionalProps, MutatorKey, @@ -18,16 +19,15 @@ export interface FetchArticleResult { finished: boolean; } -export interface ArticleListProps { +export type ArticleListProps = { stateKey: T; keyConstructor: ( size: number, prev?: FetchArticleResult, ) => string | null; fetcher: (url: string) => Promise>; - renderActions?: (article: Article & AdditionalProps[T]) => ReactNode; noContentComponent: ReactNode; -} +} & Omit, 'article'>; const useLoaderIntersection = (loadNext: () => void, isLoading: boolean) => { const containerRef = useRef(null); @@ -51,8 +51,8 @@ export const ArticleList = ({ stateKey, keyConstructor, fetcher, - renderActions, noContentComponent, + ...itemProps }: ArticleListProps): ReactNode => { const { setMutator, removeMutator } = useSetMutator(); const { data, setSize, isLoading, mutate } = useSWRInfinite( @@ -107,7 +107,7 @@ export const ArticleList = ({
diff --git a/apps/web/src/client/Home/_components/Article/ArticleListItem.tsx b/apps/web/src/client/Home/_components/Article/ArticleListItem.tsx index ce8850e..6630dea 100644 --- a/apps/web/src/client/Home/_components/Article/ArticleListItem.tsx +++ b/apps/web/src/client/Home/_components/Article/ArticleListItem.tsx @@ -7,11 +7,15 @@ import type { ComponentProps, ReactNode } from 'react'; export interface ArticleListItemProps { article: Article & T; renderActions?: (article: Article & T) => ReactNode; + onDelete?: (article: Article & T) => void; + onArchive?: (article: Article & T) => void; } export const ArticleListItem = ({ article, renderActions, + onDelete, + onArchive, ...props }: ArticleListItemProps & ComponentProps<'div'>) => { const theme = useMantineTheme(); @@ -82,12 +86,24 @@ export const ArticleListItem = ({ gap: 0.1rem; `} > - void 0}> - - - void 0}> - - + {onArchive ? ( + { + onArchive(article); + }} + > + + + ) : null} + {onDelete ? ( + { + onDelete(article); + }} + > + + + ) : null}
{article.ogImageUrl ? ( ); -interface ActionSectionProps { - item: InboxItemWithArticle; -} - -const ActionSection: FC = ({ item }) => { +const useReducers = () => { const mutators = useMutators(); - const moveToClip = useCallback(async () => { - try { - const mutating = fetch( - `/api/v1/users/me/inboxes/${item.id}/move-to-clip`, - { method: 'POST' }, - ) - .then((res) => res.json()) - .then((json) => moveInboxItemToClipResponseSchema.parse(json).clip); - - void mutators.inboxItem?.( - async () => { - await mutating; - const result = await inboxFetcher(inboxKeyConstructor(1)); - - return [result]; - }, - { - optimisticData: (prev) => { - if (prev === undefined) return []; - - const result = prev.map((r) => ({ - ...r, - articles: r.articles.filter((a) => a.id !== item.articleId), - })); - - return result; + const moveToClip = useCallback( + async (article: Article & InboxItemAdditionalProps) => { + try { + const mutating = fetch( + `/api/v1/users/me/inboxes/${article.item.id}/move-to-clip`, + { method: 'POST' }, + ) + .then((res) => res.json()) + .then((json) => moveInboxItemToClipResponseSchema.parse(json).clip); + + void mutators.inboxItem?.( + async () => { + await mutating; + const result = await inboxFetcher(inboxKeyConstructor(1)); + + return [result]; }, - }, - ); - - const clip = await mutating; - void mutators.unreadClip?.( - async () => { - await mutating; - const result = await unreadClipsFetcher(unreadClipsKeyConstructor(1)); - return [result]; - }, - { - optimisticData: (prev) => { - if (prev === undefined) return []; - - const newResult: FetchArticleResult = { - articles: [{ ...item.article, clip }], - finished: false, - }; - - return [newResult, ...prev]; + { + optimisticData: (prev) => { + if (prev === undefined) return []; + + const result = prev.map((r) => ({ + ...r, + articles: r.articles.filter((a) => a.id !== article.id), + })); + + return result; + }, }, - }, - ); - } catch (err) { - console.error(err); - toast('記事の移動に失敗しました', { type: 'error' }); - } - }, [item.article, item.articleId, item.id, mutators]); - return ( - + ); + + const clip = await mutating; + void mutators.unreadClip?.( + async () => { + await mutating; + const result = await unreadClipsFetcher( + unreadClipsKeyConstructor(1), + ); + return [result]; + }, + { + optimisticData: (prev) => { + if (prev === undefined) return []; + + const newResult: FetchArticleResult = { + articles: [{ ...article, clip }], + finished: false, + }; + + return [newResult, ...prev]; + }, + }, + ); + } catch (err) { + console.error(err); + toast('記事の移動に失敗しました', { type: 'error' }); + } + }, + [mutators], + ); + + const archive = useCallback( + async (article: Article & InboxItemAdditionalProps) => { + try { + const mutating = fetch( + `/api/v1/users/me/inboxes/${article.item.id}/archive`, + { method: 'POST' }, + ) + .then((res) => res.json()) + .then((json) => archiveInboxItemResponseSchema.parse(json).clip); + + void mutators.inboxItem?.( + async () => { + await mutating; + const result = await inboxFetcher(inboxKeyConstructor(1)); + + return [result]; + }, + { + optimisticData: (prev) => { + if (prev === undefined) return []; + + const result = prev.map((r) => ({ + ...r, + articles: r.articles.filter((a) => a.id !== article.id), + })); + + return result; + }, + }, + ); + + const clip = await mutating; + console.log('clip', clip); + + void mutators.readClip?.( + async () => { + const result = await readClipsFetcher(readClipsKeyConstructor(1)); + return [result]; + }, + { + optimisticData: (prev) => { + if (prev === undefined) return []; + + const newResult: FetchArticleResult = { + articles: [{ ...article, clip }], + finished: false, + }; + + return [newResult, ...prev]; + }, + }, + ); + } catch (err) { + console.error(err); + toast('記事の移動に失敗しました', { type: 'error' }); + } + }, + [mutators], ); + + const deleteItem = useCallback( + async (article: Article & InboxItemAdditionalProps) => { + try { + await fetch(`/api/v1/users/me/inboxes/${article.item.id}`, { + method: 'DELETE', + }); + + void mutators.inboxItem?.( + async () => { + const result = await inboxFetcher(inboxKeyConstructor(1)); + + return [result]; + }, + { + optimisticData: (prev) => { + if (prev === undefined) return []; + + const result = prev.map((r) => ({ + ...r, + articles: r.articles.filter((a) => a.id !== article.id), + })); + + return result; + }, + }, + ); + } catch (err) { + console.error(err); + toast('記事の削除に失敗しました', { type: 'error' }); + } + }, + [mutators], + ); + + return { moveToClip, archive, deleteItem }; }; export const InboxItemList: FC = () => { + const { moveToClip, archive, deleteItem } = useReducers(); return ( archive(article)} + onDelete={(article) => deleteItem(article)} renderActions={(article) => ( - + )} stateKey="inboxItem" /> diff --git a/apps/web/src/client/Home/_components/ArticleListModel/ReadClipList.tsx b/apps/web/src/client/Home/_components/ArticleListModel/ReadClipList.tsx index 9025626..6684f0e 100644 --- a/apps/web/src/client/Home/_components/ArticleListModel/ReadClipList.tsx +++ b/apps/web/src/client/Home/_components/ArticleListModel/ReadClipList.tsx @@ -3,7 +3,7 @@ import { ArticleList } from '@/client/Home/_components/Article/ArticleList'; import { ArticleListLayout, keyConstructorGenerator } from './common'; import { fetcher } from '@/features/swr/fetcher'; import { Stack, Button, Text } from '@mantine/core'; -import type { ClipWithArticle } from '@read-stack/openapi'; +import type { Article, Clip } from '@read-stack/openapi'; import { getClipsResponseSchema, patchClipResponseSchema, @@ -21,7 +21,7 @@ import { import { useMutators } from './useMutators'; export interface ReadClipAdditionalProps { - clip: ClipWithArticle; + clip: Clip; } export const readClipsFetcher = async (url: string) => { @@ -46,89 +46,133 @@ const NoContentComponent = ( ); -interface ActionSectionProps { - clip: ClipWithArticle; -} - -const ActionSection: FC = ({ clip }) => { +const useReducers = () => { const mutators = useMutators(); - const markAsUnread = useCallback(() => { - try { - const body = { clip: { status: 0 } }; - const mutating = fetch(`/api/v1/users/me/clips/${clip.id}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }) - .then((res) => res.json()) - .then((json) => patchClipResponseSchema.parse(json).clip); - - void mutators.readClip?.( - async () => { - await mutating; - const result = await readClipsFetcher(readClipsKeyConstructor(1)); - - return [result]; - }, - { - optimisticData: (prev) => { - if (prev === undefined) return []; - - const result = prev.map((r) => ({ - ...r, - articles: r.articles.filter((a) => a.id !== clip.articleId), - })); - - return result; + const markAsUnread = useCallback( + (article: Article & ReadClipAdditionalProps) => { + try { + const body = { clip: { status: 0 } }; + const mutating = fetch(`/api/v1/users/me/clips/${article.clip.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + .then((res) => res.json()) + .then((json) => patchClipResponseSchema.parse(json).clip); + + void mutators.readClip?.( + async () => { + await mutating; + const result = await readClipsFetcher(readClipsKeyConstructor(1)); + + return [result]; }, - }, - ); - - void mutators.unreadClip?.( - async () => { - await mutating; - const result = await unreadClipsFetcher(unreadClipsKeyConstructor(1)); - return [result]; - }, - { - optimisticData: (prev) => { - if (prev === undefined) return []; - - const newResult: FetchArticleResult = { - articles: [{ ...clip.article, clip: { ...clip, status: 2 } }], - finished: false, - }; - - return [newResult, ...prev]; + { + optimisticData: (prev) => { + if (prev === undefined) return []; + + const result = prev.map((r) => ({ + ...r, + articles: r.articles.filter((a) => a.id !== article.id), + })); + + return result; + }, }, - }, - ); - } catch (err) { - console.error(err); - toast('記事を未読にすることに失敗しました', { type: 'error' }); - } - }, [clip, mutators]); + ); - return ( - + void mutators.unreadClip?.( + async () => { + await mutating; + const result = await unreadClipsFetcher( + unreadClipsKeyConstructor(1), + ); + return [result]; + }, + { + optimisticData: (prev) => { + if (prev === undefined) return []; + + const newResult: FetchArticleResult = { + articles: [ + { ...article, clip: { ...article.clip, status: 2 } }, + ], + finished: false, + }; + + return [newResult, ...prev]; + }, + }, + ); + } catch (err) { + console.error(err); + toast('記事を未読にすることに失敗しました', { type: 'error' }); + } + }, + [mutators], ); + + const deleteClip = useCallback( + (article: Article & ReadClipAdditionalProps) => { + try { + void fetch(`/api/v1/users/me/clips/${article.clip.id}`, { + method: 'DELETE', + }); + + void mutators.readClip?.( + async () => { + const result = await readClipsFetcher(readClipsKeyConstructor(1)); + + return [result]; + }, + { + optimisticData: (prev) => { + if (prev === undefined) return []; + + const result = prev.map((r) => ({ + ...r, + articles: r.articles.filter((a) => a.id !== article.id), + })); + + return result; + }, + }, + ); + } catch (err) { + console.error(err); + toast('記事の削除に失敗しました', { type: 'error' }); + } + }, + [mutators], + ); + + return { markAsUnread, deleteClip }; }; export const ReadClipList: FC = () => { + const { markAsUnread, deleteClip } = useReducers(); + return ( } + onDelete={(article) => { + deleteClip(article); + }} + renderActions={(article) => ( + + )} stateKey="readClip" /> diff --git a/apps/web/src/client/Home/_components/ArticleListModel/UnreadClipList.tsx b/apps/web/src/client/Home/_components/ArticleListModel/UnreadClipList.tsx index e8a3e58..dcfeedf 100644 --- a/apps/web/src/client/Home/_components/ArticleListModel/UnreadClipList.tsx +++ b/apps/web/src/client/Home/_components/ArticleListModel/UnreadClipList.tsx @@ -3,7 +3,7 @@ import { ArticleList } from '@/client/Home/_components/Article/ArticleList'; import { ArticleListLayout, keyConstructorGenerator } from './common'; import { fetcher } from '@/features/swr/fetcher'; import { Stack, Button, Text } from '@mantine/core'; -import type { Clip, ClipWithArticle } from '@read-stack/openapi'; +import type { Article, Clip } from '@read-stack/openapi'; import { getClipsResponseSchema, moveUserClipToInboxResponseSchema, @@ -52,167 +52,177 @@ const NoContentComponent = ( ); -interface PresentationActionSectionProps { - moveToInbox?: () => void; - markAsRead?: () => void; -} +const useReducers = () => { + const mutators = useMutators(); + const moveToInbox = useCallback( + async (article: Article & UnreadClipAdditionalProps) => { + try { + const mutating = fetch( + `/api/v1/users/me/clips/${article.clip.id}/move-to-inbox`, + { method: 'POST' }, + ) + .then((res) => res.json()) + .then((json) => moveUserClipToInboxResponseSchema.parse(json).item); -const PresentationActionSection: FC = ({ - moveToInbox, - markAsRead, -}) => ( -
- - -
-); + void mutators.unreadClip?.( + async () => { + await mutating; + const result = await unreadClipsFetcher( + unreadClipsKeyConstructor(1), + ); -interface ActionSectionProps { - clip: ClipWithArticle; -} + return [result]; + }, + { + optimisticData: (prev) => { + if (prev === undefined) return []; -const ActionSection: FC = ({ clip }) => { - const mutators = useMutators(); - const moveToInbox = useCallback(async () => { - try { - const mutating = fetch( - `/api/v1/users/me/clips/${clip.id}/move-to-inbox`, - { method: 'POST' }, - ) - .then((res) => res.json()) - .then((json) => moveUserClipToInboxResponseSchema.parse(json).item); - - void mutators.unreadClip?.( - async () => { - await mutating; - const result = await unreadClipsFetcher(unreadClipsKeyConstructor(1)); - - return [result]; - }, - { - optimisticData: (prev) => { - if (prev === undefined) return []; - - const result = prev.map((r) => ({ - ...r, - articles: r.articles.filter((a) => a.id !== clip.articleId), - })); - - return result; + const result = prev.map((r) => ({ + ...r, + articles: r.articles.filter((a) => a.id !== article.id), + })); + + return result; + }, }, - }, - ); - - const item = await mutating; - void mutators.inboxItem?.( - async () => { - await mutating; - const result = await inboxFetcher(inboxKeyConstructor(1)); - return [result]; - }, - { - optimisticData: (prev) => { - if (prev === undefined) return []; - - const newResult: FetchArticleResult = { - articles: [{ ...clip.article, item }], - finished: false, - }; - - return [newResult, ...prev]; + ); + + const item = await mutating; + void mutators.inboxItem?.( + async () => { + await mutating; + const result = await inboxFetcher(inboxKeyConstructor(1)); + return [result]; }, - }, - ); - } catch (err) { - console.error(err); - toast('記事の移動に失敗しました', { type: 'error' }); - } - }, [clip.article, clip.articleId, clip.id, mutators]); - - const markAsRead = useCallback(() => { - try { - const body = { clip: { status: 2 } }; - const mutating = fetch(`/api/v1/users/me/clips/${clip.id}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }) - .then((res) => res.json()) - .then((json) => patchClipResponseSchema.parse(json).clip); - - void mutators.unreadClip?.( - async () => { - await mutating; - const result = await unreadClipsFetcher(unreadClipsKeyConstructor(1)); - - return [result]; - }, - { - optimisticData: (prev) => { - if (prev === undefined) return []; - - const result = prev.map((r) => ({ - ...r, - articles: r.articles.filter((a) => a.id !== clip.articleId), - })); - - return result; + { + optimisticData: (prev) => { + if (prev === undefined) return []; + + const newResult: FetchArticleResult = { + articles: [{ ...article, item }], + finished: false, + }; + + return [newResult, ...prev]; + }, }, - }, - ); - - void mutators.readClip?.( - async () => { - await mutating; - const result = await readClipsFetcher(readClipsKeyConstructor(1)); - return [result]; - }, - { - optimisticData: (prev) => { - if (prev === undefined) return []; - - const newResult: FetchArticleResult = { - articles: [{ ...clip.article, clip: { ...clip, status: 2 } }], - finished: false, - }; - - return [newResult, ...prev]; + ); + } catch (err) { + console.error(err); + toast('記事の移動に失敗しました', { type: 'error' }); + } + }, + [mutators], + ); + + const markAsRead = useCallback( + (article: Article & UnreadClipAdditionalProps) => { + try { + const body = { clip: { status: 2 } }; + const mutating = fetch(`/api/v1/users/me/clips/${article.clip.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + .then((res) => res.json()) + .then((json) => patchClipResponseSchema.parse(json).clip); + + void mutators.unreadClip?.( + async () => { + await mutating; + const result = await unreadClipsFetcher( + unreadClipsKeyConstructor(1), + ); + + return [result]; }, - }, - ); - } catch (err) { - console.error(err); - toast('記事を既読にすることに失敗しました', { type: 'error' }); - } - }, [clip, mutators]); + { + optimisticData: (prev) => { + if (prev === undefined) return []; - return ( - + const result = prev.map((r) => ({ + ...r, + articles: r.articles.filter((a) => a.id !== article.id), + })); + + return result; + }, + }, + ); + + void mutators.readClip?.( + async () => { + await mutating; + const result = await readClipsFetcher(readClipsKeyConstructor(1)); + return [result]; + }, + { + optimisticData: (prev) => { + if (prev === undefined) return []; + + const newResult: FetchArticleResult = { + articles: [ + { ...article, clip: { ...article.clip, status: 2 } }, + ], + finished: false, + }; + + return [newResult, ...prev]; + }, + }, + ); + } catch (err) { + console.error(err); + toast('記事を既読にすることに失敗しました', { type: 'error' }); + } + }, + [mutators], ); + + const deleteClip = useCallback( + (article: Article & UnreadClipAdditionalProps) => { + try { + const mutating = fetch(`/api/v1/users/me/clips/${article.clip.id}`, { + method: 'DELETE', + }) + .then((res) => res.json()) + .then((json) => patchClipResponseSchema.parse(json).clip); + + void mutators.unreadClip?.( + async () => { + await mutating; + const result = await unreadClipsFetcher( + unreadClipsKeyConstructor(1), + ); + return [result]; + }, + { + optimisticData: (prev) => { + if (prev === undefined) return []; + + const result = prev.map((r) => ({ + ...r, + articles: r.articles.filter((a) => a.id !== article.id), + })); + + return result; + }, + }, + ); + } catch (err) { + console.error(err); + toast('記事の削除に失敗しました', { type: 'error' }); + } + }, + [mutators], + ); + + return { markAsRead, moveToInbox, deleteClip }; }; export const UnreadClipList: FC = () => { + const { markAsRead, moveToInbox, deleteClip } = useReducers(); + return ( @@ -220,8 +230,34 @@ export const UnreadClipList: FC = () => { fetcher={unreadClipsFetcher} keyConstructor={unreadClipsKeyConstructor} noContentComponent={NoContentComponent} + onDelete={(article) => { + deleteClip(article); + }} renderActions={(article) => ( - +
+ + +
)} stateKey="unreadClip" /> From 7119966ce88ff47cac933740deb88e1abfb8f6fd Mon Sep 17 00:00:00 2001 From: cp-20 <47262658+mario-hsp@users.noreply.github.com> Date: Fri, 16 Feb 2024 23:58:24 +0900 Subject: [PATCH 6/6] remove console.log --- .../client/Home/_components/ArticleListModel/InboxItemList.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/web/src/client/Home/_components/ArticleListModel/InboxItemList.tsx b/apps/web/src/client/Home/_components/ArticleListModel/InboxItemList.tsx index 018affd..3ec4702 100644 --- a/apps/web/src/client/Home/_components/ArticleListModel/InboxItemList.tsx +++ b/apps/web/src/client/Home/_components/ArticleListModel/InboxItemList.tsx @@ -147,8 +147,6 @@ const useReducers = () => { ); const clip = await mutating; - console.log('clip', clip); - void mutators.readClip?.( async () => { const result = await readClipsFetcher(readClipsKeyConstructor(1));