diff --git a/apps/demo/app/article/[id]/page.tsx b/apps/demo/app/article/[id]/page.tsx
index 0f3b42878..b1d87b3f3 100644
--- a/apps/demo/app/article/[id]/page.tsx
+++ b/apps/demo/app/article/[id]/page.tsx
@@ -1,21 +1,22 @@
+'use client';
+
import CommentList from '../components/comment-list';
import Link from 'next/link';
import ArticleActions from '../components/article-actions';
import { marked } from 'marked';
+import { useArticle } from '../hooks/article.hook';
+import { dateFormatter } from '../../utils/date.utils';
-export default async function Page({ params }: { params: { id: string } }) {
- const article = await fetch(`https://api.realworld.io/api/articles/${params.id}`)
- .then(res => res.json())
- .then(res => res.article);
+export default function Page({ params }: { params: { id: string } }) {
+ const { article } = useArticle(params.id);
if (!article) {
return
Loading...
;
}
if (article) {
- console.log(article.body);
const markup = {
- __html: marked(article.body.replace('\\n', '\n'), {
+ __html: marked(article.body, {
sanitize: true,
breaks: true,
gfm: true,
@@ -36,7 +37,7 @@ export default async function Page({ params }: { params: { id: string } }) {
{article.author.username}
- {new Date(article.createdAt).toDateString()}
+ {dateFormatter(article.createdAt)}
@@ -59,7 +60,7 @@ export default async function Page({ params }: { params: { id: string } }) {
-
+
);
diff --git a/apps/demo/app/article/components/comment-list.tsx b/apps/demo/app/article/components/comment-list.tsx
index f9d97f81e..27c88f28d 100644
--- a/apps/demo/app/article/components/comment-list.tsx
+++ b/apps/demo/app/article/components/comment-list.tsx
@@ -1,11 +1,33 @@
'use client';
-import { useContext } from 'react';
+import React, { useContext } from 'react';
import { AuthContext } from '../../auth/auth.context';
import Link from 'next/link';
+import { useComments } from '../hooks/comments.hook';
+import { Comment } from '../../models/comment.model';
+import { createComment, deleteComment } from '../services/comment.service';
+import { dateFormatter } from '../../utils/date.utils';
-export default function CommentList() {
+interface CommentListProps {
+ slug: string;
+}
+
+export default function CommentList({ slug }: CommentListProps) {
const { user } = useContext(AuthContext);
+ const { comments, mutate } = useComments(slug);
+
+ async function create(event: React.FormEvent): Promise {
+ event.preventDefault();
+
+ const comment = (event.target as HTMLFormElement).comment.value;
+ const createdComment = await createComment(slug, comment);
+ mutate([...comments, createdComment], { populateCache: false });
+ }
+
+ async function deleteC(id: number): Promise {
+ await deleteComment(slug, id);
+ mutate([...comments.filter((comment: Comment) => comment.id !== id)], { populateCache: false });
+ }
if (!user) {
return (
@@ -25,55 +47,48 @@ export default function CommentList() {
return (
-
-
-
-
- With supporting text below as a natural lead-in to additional content.
-
-
-
-
-
-
-
-
- With supporting text below as a natural lead-in to additional content.
-
-
-
-
+ {comments &&
+ comments.map((comment: Comment, key: number) => (
+
+
+
+
+

+
+
+
+ {comment.author.username}
+
+
{dateFormatter(comment.createdAt)}
+
+ deleteC(comment.id)}>
+
+
+
+ ))}
);
diff --git a/apps/demo/app/article/components/delete-comment-button.tsx b/apps/demo/app/article/components/delete-comment-button.tsx
new file mode 100644
index 000000000..096bf2a96
--- /dev/null
+++ b/apps/demo/app/article/components/delete-comment-button.tsx
@@ -0,0 +1,27 @@
+'use client';
+
+interface DeleteCommentButtonProps {
+ slug: string;
+ commentId: number;
+ onDelete: () => void;
+}
+export default function DeleteCommentButton({
+ slug,
+ commentId,
+ onDelete,
+}: DeleteCommentButtonProps) {
+ async function deleteComment(): Promise {
+ const response = await fetch(
+ `https://api.realworld.io/api/articles/${slug}/comments/${commentId}`,
+ {
+ method: 'DELETE',
+ },
+ );
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/apps/demo/app/article/hooks/article.hook.ts b/apps/demo/app/article/hooks/article.hook.ts
new file mode 100644
index 000000000..8f1363224
--- /dev/null
+++ b/apps/demo/app/article/hooks/article.hook.ts
@@ -0,0 +1,19 @@
+import useSWR from 'swr';
+
+export function useArticle(slug: string) {
+ const token = localStorage.getItem('token') as string;
+ const { data, error, isLoading } = useSWR(`/api/articles/${slug}`, url =>
+ fetch('https://api.realworld.io' + url, {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(token ? { Authorization: `Token ${token}` } : {}),
+ },
+ }).then(res => res.json()),
+ );
+
+ return {
+ article: data?.article,
+ isLoading,
+ isError: error,
+ };
+}
diff --git a/apps/demo/app/article/hooks/comments.hook.ts b/apps/demo/app/article/hooks/comments.hook.ts
new file mode 100644
index 000000000..2c49c65a6
--- /dev/null
+++ b/apps/demo/app/article/hooks/comments.hook.ts
@@ -0,0 +1,19 @@
+import useSWR from 'swr';
+
+export function useComments(slug: string) {
+ const { data, error, isLoading, mutate } = useSWR(`/api/articles/${slug}/comments`, url =>
+ fetch('https://api.realworld.io' + url, {
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Token ${localStorage.getItem('token')}`,
+ },
+ }).then(res => res.json()),
+ );
+ console.log({ data });
+ return {
+ comments: data?.comments,
+ isLoading,
+ isError: error,
+ mutate,
+ };
+}
diff --git a/apps/demo/app/article/services/comment.service.ts b/apps/demo/app/article/services/comment.service.ts
new file mode 100644
index 000000000..49c8c157c
--- /dev/null
+++ b/apps/demo/app/article/services/comment.service.ts
@@ -0,0 +1,24 @@
+export async function createComment(slug: string, body: string): Promise {
+ const token = localStorage.getItem('token') as string;
+ return fetch(`https://api.realworld.io/api/articles/${slug}/comments`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Token ${token}`,
+ },
+ body: JSON.stringify({ comment: { body } }),
+ })
+ .then(res => res.json())
+ .then(data => data.comment);
+}
+
+export async function deleteComment(slug: string, id: number): Promise {
+ const token = localStorage.getItem('token') as string;
+ return fetch(`https://api.realworld.io/api/articles/${slug}/comments/${id}`, {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Token ${token}`,
+ },
+ });
+}
diff --git a/apps/demo/app/auth/auth.context.tsx b/apps/demo/app/auth/auth.context.tsx
index e5159cd64..ad862a312 100644
--- a/apps/demo/app/auth/auth.context.tsx
+++ b/apps/demo/app/auth/auth.context.tsx
@@ -5,6 +5,7 @@ import { PropsWithChildren } from 'react';
import { AuthContextType } from './auth.context.model';
import { User } from '../models/user.model';
import { getCurrentUser } from '../services/auth.service';
+import { mutate } from 'swr';
export const AuthContext = createContext(null as unknown as AuthContextType);
@@ -14,12 +15,14 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
useEffect(() => {
const token = localStorage.getItem('token');
- console.log(token);
-
const setCurrentUser = async () => {
const response = await getCurrentUser();
const data = await response.json();
setUser(data.user);
+
+ mutate(key => typeof key === 'string' && key.startsWith('/api/'), undefined, {
+ revalidate: true,
+ });
};
if (token) {
diff --git a/apps/demo/app/components/article-list.tsx b/apps/demo/app/components/article-list.tsx
index 31b9beb78..2bbd10cb6 100644
--- a/apps/demo/app/components/article-list.tsx
+++ b/apps/demo/app/components/article-list.tsx
@@ -5,11 +5,15 @@ import ArticlePreview from './article-preview';
import { useArticles } from '../services/article.service';
import { Pagination } from './pagination';
-export default function ArticleList() {
- const { articles, articlesCount, isLoading, isError } = useArticles();
+interface ArticleListProps {
+ limit?: number;
+}
+
+export default function ArticleList({ limit = 10 }: ArticleListProps = {}) {
+ const { articles, articlesCount, isLoading, isError } = useArticles({ limit });
return (
<>
- {isLoading && Loading...
}
+ {isLoading && Loading articles...
}
{isError && Error
}
{articles &&
articles.map((article: Article, index: number) => (
diff --git a/apps/demo/app/components/article-preview.tsx b/apps/demo/app/components/article-preview.tsx
index 6716fa8b0..c15af517a 100644
--- a/apps/demo/app/components/article-preview.tsx
+++ b/apps/demo/app/components/article-preview.tsx
@@ -1,6 +1,7 @@
import Link from 'next/link';
import FavoriteButton from './favorite-button';
import { Article } from '../models/article.model';
+import { dateFormatter } from '../utils/date.utils';
interface ArticlePreviewProps {
article: Article;
@@ -17,7 +18,7 @@ export default function ArticlePreview({ article }: ArticlePreviewProps) {
{article.author.username}
- {new Date(article.createdAt).toDateString()}
+ {dateFormatter(article.createdAt)}
diff --git a/apps/demo/app/components/favorite-button.tsx b/apps/demo/app/components/favorite-button.tsx
index 884eae022..bf6204afc 100644
--- a/apps/demo/app/components/favorite-button.tsx
+++ b/apps/demo/app/components/favorite-button.tsx
@@ -4,6 +4,10 @@ import { AuthContext } from '../auth/auth.context';
import { useRouter } from 'next/navigation';
import { favoriteArticle, unfavoriteArticle } from '../services/article.service';
import { Article } from '../models/article.model';
+import { mutate } from 'swr';
+
+const FAVORITED_CLASS = 'btn btn-sm btn-primary';
+const NOT_FAVORITED_CLASS = 'btn btn-sm btn-outline-primary';
interface FavoriteButtonProps {
article: Article;
@@ -11,6 +15,7 @@ interface FavoriteButtonProps {
}
export default function FavoriteButton({ article, isExtended }: FavoriteButtonProps) {
+ console.log('article', article.favorited);
const { user } = useContext(AuthContext);
const router = useRouter();
const [count, setCount] = useState(article.favoritesCount);
@@ -28,13 +33,21 @@ export default function FavoriteButton({ article, isExtended }: FavoriteButtonPr
if (article.favorited) {
setCount(count - 1);
setIsFavorited(false);
- await favoriteArticle(article.slug);
- // TODO: update article
+ mutate(`/api/articles/${article.slug}`, {
+ ...article,
+ favorited: false,
+ favoritesCount: count - 1,
+ });
+ await unfavoriteArticle(article.slug);
} else {
setCount(count + 1);
setIsFavorited(true);
- await unfavoriteArticle(article.slug);
- // TODO: update article
+ mutate(`/api/articles/${article.slug}`, {
+ ...article,
+ favorited: true,
+ favoritesCount: count + 1,
+ });
+ await favoriteArticle(article.slug);
}
setIsLoading(false);
@@ -42,14 +55,22 @@ export default function FavoriteButton({ article, isExtended }: FavoriteButtonPr
if (isExtended) {
return (
-