Skip to content

Commit d499f02

Browse files
committed
記事のアーカイブ / 削除をできるように
1 parent 7b269b2 commit d499f02

File tree

5 files changed

+501
-304
lines changed

5 files changed

+501
-304
lines changed

apps/web/src/client/Home/_components/Article/ArticleList.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { AnimatedList } from '@/client/Home/_components/Article/AnimatedList';
2+
import type { ArticleListItemProps } from '@/client/Home/_components/Article/ArticleListItem';
23
import type {
34
AdditionalProps,
45
MutatorKey,
@@ -18,16 +19,15 @@ export interface FetchArticleResult<T> {
1819
finished: boolean;
1920
}
2021

21-
export interface ArticleListProps<T extends MutatorKey> {
22+
export type ArticleListProps<T extends MutatorKey> = {
2223
stateKey: T;
2324
keyConstructor: (
2425
size: number,
2526
prev?: FetchArticleResult<AdditionalProps[T]>,
2627
) => string | null;
2728
fetcher: (url: string) => Promise<FetchArticleResult<AdditionalProps[T]>>;
28-
renderActions?: (article: Article & AdditionalProps[T]) => ReactNode;
2929
noContentComponent: ReactNode;
30-
}
30+
} & Omit<ArticleListItemProps<AdditionalProps[T]>, 'article'>;
3131

3232
const useLoaderIntersection = (loadNext: () => void, isLoading: boolean) => {
3333
const containerRef = useRef<HTMLDivElement>(null);
@@ -51,8 +51,8 @@ export const ArticleList = <T extends MutatorKey>({
5151
stateKey,
5252
keyConstructor,
5353
fetcher,
54-
renderActions,
5554
noContentComponent,
55+
...itemProps
5656
}: ArticleListProps<T>): ReactNode => {
5757
const { setMutator, removeMutator } = useSetMutator();
5858
const { data, setSize, isLoading, mutate } = useSWRInfinite(
@@ -107,7 +107,7 @@ export const ArticleList = <T extends MutatorKey>({
107107
<AnimatedList
108108
articles={articles}
109109
duration={2000}
110-
itemProps={{ renderActions }}
110+
itemProps={itemProps}
111111
/>
112112
</div>
113113

apps/web/src/client/Home/_components/Article/ArticleListItem.tsx

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@ import type { ComponentProps, ReactNode } from 'react';
77
export interface ArticleListItemProps<T> {
88
article: Article & T;
99
renderActions?: (article: Article & T) => ReactNode;
10+
onDelete?: (article: Article & T) => void;
11+
onArchive?: (article: Article & T) => void;
1012
}
1113

1214
export const ArticleListItem = <T,>({
1315
article,
1416
renderActions,
17+
onDelete,
18+
onArchive,
1519
...props
1620
}: ArticleListItemProps<T> & ComponentProps<'div'>) => {
1721
const theme = useMantineTheme();
@@ -82,12 +86,24 @@ export const ArticleListItem = <T,>({
8286
gap: 0.1rem;
8387
`}
8488
>
85-
<ActionIcon onClick={() => void 0}>
86-
<IconArchive />
87-
</ActionIcon>
88-
<ActionIcon onClick={() => void 0}>
89-
<IconTrash />
90-
</ActionIcon>
89+
{onArchive ? (
90+
<ActionIcon
91+
onClick={() => {
92+
onArchive(article);
93+
}}
94+
>
95+
<IconArchive />
96+
</ActionIcon>
97+
) : null}
98+
{onDelete ? (
99+
<ActionIcon
100+
onClick={() => {
101+
onDelete(article);
102+
}}
103+
>
104+
<IconTrash />
105+
</ActionIcon>
106+
) : null}
91107
</div>
92108
{article.ogImageUrl ? (
93109
<img

apps/web/src/client/Home/_components/ArticleListModel/InboxItemList.tsx

Lines changed: 172 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { ArticleList } from '@/client/Home/_components/Article/ArticleList';
33
import { ArticleListLayout, keyConstructorGenerator } from './common';
44
import { fetcher } from '@/features/swr/fetcher';
55
import { Stack, Button, Text } from '@mantine/core';
6-
import type { InboxItem, InboxItemWithArticle } from '@read-stack/openapi';
6+
import type { Article, InboxItem } from '@read-stack/openapi';
77
import {
8+
archiveInboxItemResponseSchema,
89
getInboxItemsResponseSchema,
910
moveInboxItemToClipResponseSchema,
1011
} from '@read-stack/openapi';
@@ -14,12 +15,15 @@ import Image from 'next/image';
1415
import { toast } from 'react-toastify';
1516
import { IconChevronRight } from '@tabler/icons-react';
1617
import { useMutators } from './useMutators';
17-
import type { UnreadClipAdditionalProps } from './UnreadClipList';
18+
19+
import type { ReadClipAdditionalProps } from './ReadClipList';
1820
// eslint-disable-next-line import/no-cycle -- しゃーなし
21+
import { readClipsFetcher, readClipsKeyConstructor } from './ReadClipList';
1922
import {
2023
unreadClipsFetcher,
2124
unreadClipsKeyConstructor,
2225
} from './UnreadClipList';
26+
import type { UnreadClipAdditionalProps } from './UnreadClipList';
2327

2428
export interface InboxItemAdditionalProps {
2529
item: InboxItem;
@@ -47,90 +51,187 @@ const NoContentComponent = (
4751
</Stack>
4852
);
4953

50-
interface ActionSectionProps {
51-
item: InboxItemWithArticle;
52-
}
53-
54-
const ActionSection: FC<ActionSectionProps> = ({ item }) => {
54+
const useReducers = () => {
5555
const mutators = useMutators();
5656

57-
const moveToClip = useCallback(async () => {
58-
try {
59-
const mutating = fetch(
60-
`/api/v1/users/me/inboxes/${item.id}/move-to-clip`,
61-
{ method: 'POST' },
62-
)
63-
.then((res) => res.json())
64-
.then((json) => moveInboxItemToClipResponseSchema.parse(json).clip);
65-
66-
void mutators.inboxItem?.(
67-
async () => {
68-
await mutating;
69-
const result = await inboxFetcher(inboxKeyConstructor(1));
70-
71-
return [result];
72-
},
73-
{
74-
optimisticData: (prev) => {
75-
if (prev === undefined) return [];
76-
77-
const result = prev.map((r) => ({
78-
...r,
79-
articles: r.articles.filter((a) => a.id !== item.articleId),
80-
}));
81-
82-
return result;
57+
const moveToClip = useCallback(
58+
async (article: Article & InboxItemAdditionalProps) => {
59+
try {
60+
const mutating = fetch(
61+
`/api/v1/users/me/inboxes/${article.item.id}/move-to-clip`,
62+
{ method: 'POST' },
63+
)
64+
.then((res) => res.json())
65+
.then((json) => moveInboxItemToClipResponseSchema.parse(json).clip);
66+
67+
void mutators.inboxItem?.(
68+
async () => {
69+
await mutating;
70+
const result = await inboxFetcher(inboxKeyConstructor(1));
71+
72+
return [result];
8373
},
84-
},
85-
);
86-
87-
const clip = await mutating;
88-
void mutators.unreadClip?.(
89-
async () => {
90-
await mutating;
91-
const result = await unreadClipsFetcher(unreadClipsKeyConstructor(1));
92-
return [result];
93-
},
94-
{
95-
optimisticData: (prev) => {
96-
if (prev === undefined) return [];
97-
98-
const newResult: FetchArticleResult<UnreadClipAdditionalProps> = {
99-
articles: [{ ...item.article, clip }],
100-
finished: false,
101-
};
102-
103-
return [newResult, ...prev];
74+
{
75+
optimisticData: (prev) => {
76+
if (prev === undefined) return [];
77+
78+
const result = prev.map((r) => ({
79+
...r,
80+
articles: r.articles.filter((a) => a.id !== article.id),
81+
}));
82+
83+
return result;
84+
},
10485
},
105-
},
106-
);
107-
} catch (err) {
108-
console.error(err);
109-
toast('記事の移動に失敗しました', { type: 'error' });
110-
}
111-
}, [item.article, item.articleId, item.id, mutators]);
112-
return (
113-
<Button
114-
fullWidth
115-
onClick={moveToClip}
116-
rightIcon={<IconChevronRight />}
117-
type="button"
118-
variant="light"
119-
>
120-
スタックに積む
121-
</Button>
86+
);
87+
88+
const clip = await mutating;
89+
void mutators.unreadClip?.(
90+
async () => {
91+
await mutating;
92+
const result = await unreadClipsFetcher(
93+
unreadClipsKeyConstructor(1),
94+
);
95+
return [result];
96+
},
97+
{
98+
optimisticData: (prev) => {
99+
if (prev === undefined) return [];
100+
101+
const newResult: FetchArticleResult<UnreadClipAdditionalProps> = {
102+
articles: [{ ...article, clip }],
103+
finished: false,
104+
};
105+
106+
return [newResult, ...prev];
107+
},
108+
},
109+
);
110+
} catch (err) {
111+
console.error(err);
112+
toast('記事の移動に失敗しました', { type: 'error' });
113+
}
114+
},
115+
[mutators],
116+
);
117+
118+
const archive = useCallback(
119+
async (article: Article & InboxItemAdditionalProps) => {
120+
try {
121+
const mutating = fetch(
122+
`/api/v1/users/me/inboxes/${article.item.id}/archive`,
123+
{ method: 'POST' },
124+
)
125+
.then((res) => res.json())
126+
.then((json) => archiveInboxItemResponseSchema.parse(json).clip);
127+
128+
void mutators.inboxItem?.(
129+
async () => {
130+
await mutating;
131+
const result = await inboxFetcher(inboxKeyConstructor(1));
132+
133+
return [result];
134+
},
135+
{
136+
optimisticData: (prev) => {
137+
if (prev === undefined) return [];
138+
139+
const result = prev.map((r) => ({
140+
...r,
141+
articles: r.articles.filter((a) => a.id !== article.id),
142+
}));
143+
144+
return result;
145+
},
146+
},
147+
);
148+
149+
const clip = await mutating;
150+
console.log('clip', clip);
151+
152+
void mutators.readClip?.(
153+
async () => {
154+
const result = await readClipsFetcher(readClipsKeyConstructor(1));
155+
return [result];
156+
},
157+
{
158+
optimisticData: (prev) => {
159+
if (prev === undefined) return [];
160+
161+
const newResult: FetchArticleResult<ReadClipAdditionalProps> = {
162+
articles: [{ ...article, clip }],
163+
finished: false,
164+
};
165+
166+
return [newResult, ...prev];
167+
},
168+
},
169+
);
170+
} catch (err) {
171+
console.error(err);
172+
toast('記事の移動に失敗しました', { type: 'error' });
173+
}
174+
},
175+
[mutators],
122176
);
177+
178+
const deleteItem = useCallback(
179+
async (article: Article & InboxItemAdditionalProps) => {
180+
try {
181+
await fetch(`/api/v1/users/me/inboxes/${article.item.id}`, {
182+
method: 'DELETE',
183+
});
184+
185+
void mutators.inboxItem?.(
186+
async () => {
187+
const result = await inboxFetcher(inboxKeyConstructor(1));
188+
189+
return [result];
190+
},
191+
{
192+
optimisticData: (prev) => {
193+
if (prev === undefined) return [];
194+
195+
const result = prev.map((r) => ({
196+
...r,
197+
articles: r.articles.filter((a) => a.id !== article.id),
198+
}));
199+
200+
return result;
201+
},
202+
},
203+
);
204+
} catch (err) {
205+
console.error(err);
206+
toast('記事の削除に失敗しました', { type: 'error' });
207+
}
208+
},
209+
[mutators],
210+
);
211+
212+
return { moveToClip, archive, deleteItem };
123213
};
124214

125215
export const InboxItemList: FC = () => {
216+
const { moveToClip, archive, deleteItem } = useReducers();
126217
return (
127218
<ArticleListLayout label="受信箱">
128219
<ArticleList
129220
fetcher={inboxFetcher}
130221
keyConstructor={inboxKeyConstructor}
131222
noContentComponent={NoContentComponent}
223+
onArchive={(article) => archive(article)}
224+
onDelete={(article) => deleteItem(article)}
132225
renderActions={(article) => (
133-
<ActionSection item={{ ...article.item, article }} />
226+
<Button
227+
fullWidth
228+
onClick={() => moveToClip(article)}
229+
rightIcon={<IconChevronRight />}
230+
type="button"
231+
variant="light"
232+
>
233+
スタックに積む
234+
</Button>
134235
)}
135236
stateKey="inboxItem"
136237
/>

0 commit comments

Comments
 (0)