Skip to content

Commit

Permalink
記事のアーカイブ / 削除をできるように
Browse files Browse the repository at this point in the history
  • Loading branch information
cp-20 committed Feb 16, 2024
1 parent 7b269b2 commit d499f02
Show file tree
Hide file tree
Showing 5 changed files with 501 additions and 304 deletions.
10 changes: 5 additions & 5 deletions apps/web/src/client/Home/_components/Article/ArticleList.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -18,16 +19,15 @@ export interface FetchArticleResult<T> {
finished: boolean;
}

export interface ArticleListProps<T extends MutatorKey> {
export type ArticleListProps<T extends MutatorKey> = {
stateKey: T;
keyConstructor: (
size: number,
prev?: FetchArticleResult<AdditionalProps[T]>,
) => string | null;
fetcher: (url: string) => Promise<FetchArticleResult<AdditionalProps[T]>>;
renderActions?: (article: Article & AdditionalProps[T]) => ReactNode;
noContentComponent: ReactNode;
}
} & Omit<ArticleListItemProps<AdditionalProps[T]>, 'article'>;

const useLoaderIntersection = (loadNext: () => void, isLoading: boolean) => {
const containerRef = useRef<HTMLDivElement>(null);
Expand All @@ -51,8 +51,8 @@ export const ArticleList = <T extends MutatorKey>({
stateKey,
keyConstructor,
fetcher,
renderActions,
noContentComponent,
...itemProps
}: ArticleListProps<T>): ReactNode => {
const { setMutator, removeMutator } = useSetMutator();
const { data, setSize, isLoading, mutate } = useSWRInfinite(
Expand Down Expand Up @@ -107,7 +107,7 @@ export const ArticleList = <T extends MutatorKey>({
<AnimatedList
articles={articles}
duration={2000}
itemProps={{ renderActions }}
itemProps={itemProps}
/>
</div>

Expand Down
28 changes: 22 additions & 6 deletions apps/web/src/client/Home/_components/Article/ArticleListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ import type { ComponentProps, ReactNode } from 'react';
export interface ArticleListItemProps<T> {
article: Article & T;
renderActions?: (article: Article & T) => ReactNode;
onDelete?: (article: Article & T) => void;
onArchive?: (article: Article & T) => void;
}

export const ArticleListItem = <T,>({
article,
renderActions,
onDelete,
onArchive,
...props
}: ArticleListItemProps<T> & ComponentProps<'div'>) => {
const theme = useMantineTheme();
Expand Down Expand Up @@ -82,12 +86,24 @@ export const ArticleListItem = <T,>({
gap: 0.1rem;
`}
>
<ActionIcon onClick={() => void 0}>
<IconArchive />
</ActionIcon>
<ActionIcon onClick={() => void 0}>
<IconTrash />
</ActionIcon>
{onArchive ? (
<ActionIcon
onClick={() => {
onArchive(article);
}}
>
<IconArchive />
</ActionIcon>
) : null}
{onDelete ? (
<ActionIcon
onClick={() => {
onDelete(article);
}}
>
<IconTrash />
</ActionIcon>
) : null}
</div>
{article.ogImageUrl ? (
<img
Expand Down
243 changes: 172 additions & 71 deletions apps/web/src/client/Home/_components/ArticleListModel/InboxItemList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ 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 { InboxItem, InboxItemWithArticle } from '@read-stack/openapi';
import type { Article, InboxItem } from '@read-stack/openapi';
import {
archiveInboxItemResponseSchema,
getInboxItemsResponseSchema,
moveInboxItemToClipResponseSchema,
} from '@read-stack/openapi';
Expand All @@ -14,12 +15,15 @@ import Image from 'next/image';
import { toast } from 'react-toastify';
import { IconChevronRight } from '@tabler/icons-react';
import { useMutators } from './useMutators';
import type { UnreadClipAdditionalProps } from './UnreadClipList';

import type { ReadClipAdditionalProps } from './ReadClipList';
// eslint-disable-next-line import/no-cycle -- しゃーなし
import { readClipsFetcher, readClipsKeyConstructor } from './ReadClipList';
import {
unreadClipsFetcher,
unreadClipsKeyConstructor,
} from './UnreadClipList';
import type { UnreadClipAdditionalProps } from './UnreadClipList';

export interface InboxItemAdditionalProps {
item: InboxItem;
Expand Down Expand Up @@ -47,90 +51,187 @@ const NoContentComponent = (
</Stack>
);

interface ActionSectionProps {
item: InboxItemWithArticle;
}

const ActionSection: FC<ActionSectionProps> = ({ 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<UnreadClipAdditionalProps> = {
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 (
<Button
fullWidth
onClick={moveToClip}
rightIcon={<IconChevronRight />}
type="button"
variant="light"
>
スタックに積む
</Button>
);

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<UnreadClipAdditionalProps> = {
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<ReadClipAdditionalProps> = {
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 (
<ArticleListLayout label="受信箱">
<ArticleList
fetcher={inboxFetcher}
keyConstructor={inboxKeyConstructor}
noContentComponent={NoContentComponent}
onArchive={(article) => archive(article)}
onDelete={(article) => deleteItem(article)}
renderActions={(article) => (
<ActionSection item={{ ...article.item, article }} />
<Button
fullWidth
onClick={() => moveToClip(article)}
rightIcon={<IconChevronRight />}
type="button"
variant="light"
>
スタックに積む
</Button>
)}
stateKey="inboxItem"
/>
Expand Down
Loading

0 comments on commit d499f02

Please sign in to comment.