Improve error handling, automatically renew access token
This commit is contained in:
@@ -10,7 +10,7 @@ import type { Post } from "~/types/post";
|
||||
import Modal from "~/components/Modal";
|
||||
import type { Comment } from "~/types/comment";
|
||||
import Loading from "~/components/Loading";
|
||||
import { useAuth } from "~/context/AuthContext";
|
||||
import { getApiErrorMessage, useAuth } from "~/context/AuthContext";
|
||||
import { NotFound } from "~/components/NotFound";
|
||||
import type { PaginatedResponse } from "~/types/PaginatedResponse";
|
||||
|
||||
@@ -52,6 +52,7 @@ export default function Post() {
|
||||
}
|
||||
|
||||
const [post, setPost] = useState<Post|null>(null);
|
||||
const [notFound, setNotFound] = useState(false);
|
||||
const [isEditPostModalOpen, setIsEditPostModalOpen] = useState(false);
|
||||
|
||||
const [paginatedComments, setPaginatedComments] = useState<PaginatedResponse<Comment> | null>(null);
|
||||
@@ -68,8 +69,19 @@ export default function Post() {
|
||||
console.log(`Fetching post with id: ${id}`);
|
||||
|
||||
async function fetchData() {
|
||||
const post = await API.fetchPostById(idNum);
|
||||
setPost(post);
|
||||
try {
|
||||
const postData = await API.fetchPostById(idNum);
|
||||
setPost(postData);
|
||||
} catch (error: any) {
|
||||
if (error.status === 404) {
|
||||
setNotFound(true);
|
||||
} else {
|
||||
console.error("Failed to fetch post:", error);
|
||||
// Optionally, you could navigate to a generic error page
|
||||
// or show a toast notification.
|
||||
alert(getApiErrorMessage(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
fetchData();
|
||||
}, [id]);
|
||||
@@ -84,6 +96,12 @@ export default function Post() {
|
||||
|
||||
const { user } = useAuth();
|
||||
|
||||
if (notFound) {
|
||||
return <RootLayout>
|
||||
<NotFound />
|
||||
</RootLayout>;
|
||||
}
|
||||
|
||||
if (!post) {
|
||||
return <RootLayout>
|
||||
<Loading />
|
||||
@@ -151,14 +169,18 @@ export default function Post() {
|
||||
const enteredTags = tags.split(' ').filter(t => t);
|
||||
|
||||
if (title && description && tags) {
|
||||
const res = await API.updatePost(post.id, title, description, enteredTags, user);
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
setError(data.detail || "Failed to update post.");
|
||||
return;
|
||||
try {
|
||||
const res = await API.updatePost(post.id, title, description, enteredTags, user);
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
setError(getApiErrorMessage(data));
|
||||
return;
|
||||
}
|
||||
setPost(data);
|
||||
setIsEditPostModalOpen(false);
|
||||
} catch (err: any) {
|
||||
setError(getApiErrorMessage(err));
|
||||
}
|
||||
setPost(data);
|
||||
setIsEditPostModalOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -203,51 +225,61 @@ export default function Post() {
|
||||
*/
|
||||
const onPostDelete = async () => {
|
||||
if (!user) return;
|
||||
const res = await API.deletePost(post.id, user)
|
||||
if (!res.ok) {
|
||||
alert("Failed to delete post.");
|
||||
return;
|
||||
try {
|
||||
const res = await API.deletePost(post.id, user)
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
alert(`Failed to delete post: ${getApiErrorMessage(err)}`);
|
||||
return;
|
||||
}
|
||||
await navigate('/')
|
||||
} catch (err: any) {
|
||||
alert(`Failed to delete post: ${getApiErrorMessage(err)}`);
|
||||
}
|
||||
await navigate('/')
|
||||
};
|
||||
|
||||
|
||||
const onAddComment = async (postId: number, commentText: string) => {
|
||||
if (!user) return;
|
||||
const res = await API.postComment(postId, commentText, user);
|
||||
if (!res.ok) {
|
||||
alert("Failed to add comment");
|
||||
return;
|
||||
}
|
||||
const newComment: Comment = await res.json();
|
||||
try {
|
||||
const res = await API.postComment(postId, commentText, user);
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
alert(`Failed to add comment: ${getApiErrorMessage(err)}`);
|
||||
return;
|
||||
}
|
||||
const newComment: Comment = await res.json();
|
||||
|
||||
// Refetch comments if this is the first one.
|
||||
if (!paginatedComments || paginatedComments.totalCount === 0) {
|
||||
const comments = await API.fetchCommentsByPostId(idNum, 1);
|
||||
setPaginatedComments(comments);
|
||||
setCurrentCommentPage(1);
|
||||
return;
|
||||
}
|
||||
// Refetch comments if this is the first one.
|
||||
if (!paginatedComments || paginatedComments.totalCount === 0) {
|
||||
const comments = await API.fetchCommentsByPostId(idNum, 1);
|
||||
setPaginatedComments(comments);
|
||||
setCurrentCommentPage(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const totalPages = Math.ceil((paginatedComments.totalCount + 1) / paginatedComments.pageSize);
|
||||
if (currentCommentPage !== totalPages) {
|
||||
setCurrentCommentPage(totalPages);
|
||||
} else {
|
||||
setPaginatedComments(prev => {
|
||||
if (!prev) return null;
|
||||
// If current page is not full, just add the comment
|
||||
if (prev.items.length < prev.pageSize) {
|
||||
return {
|
||||
...prev,
|
||||
items: [...prev.items, newComment],
|
||||
totalCount: prev.totalCount + 1
|
||||
};
|
||||
} else {
|
||||
// This case is unlikely if totalPages is calculated correctly, but as a fallback, we refetch.
|
||||
setCurrentCommentPage(totalPages);
|
||||
return prev;
|
||||
}
|
||||
});
|
||||
const totalPages = Math.ceil((paginatedComments.totalCount + 1) / paginatedComments.pageSize);
|
||||
if (currentCommentPage !== totalPages) {
|
||||
setCurrentCommentPage(totalPages);
|
||||
} else {
|
||||
setPaginatedComments(prev => {
|
||||
if (!prev) return null;
|
||||
// If current page is not full, just add the comment
|
||||
if (prev.items.length < prev.pageSize) {
|
||||
return {
|
||||
...prev,
|
||||
items: [...prev.items, newComment],
|
||||
totalCount: prev.totalCount + 1
|
||||
};
|
||||
} else {
|
||||
// This case is unlikely if totalPages is calculated correctly, but as a fallback, we refetch.
|
||||
setCurrentCommentPage(totalPages);
|
||||
return prev;
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
alert(`Failed to add comment: ${getApiErrorMessage(err)}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -258,23 +290,28 @@ export default function Post() {
|
||||
|
||||
const onDeleteComment = async (postId: number, commentId: number) => {
|
||||
if (!user || !paginatedComments) return;
|
||||
const res = await API.deleteComment(postId, commentId, user);
|
||||
if (!res.ok) {
|
||||
alert("Failed to delete comment.");
|
||||
return;
|
||||
}
|
||||
|
||||
setPaginatedComments(prev => {
|
||||
if (!prev) return null;
|
||||
const newItems = prev.items.filter(c => c.id !== commentId);
|
||||
const newTotalCount = prev.totalCount - 1;
|
||||
|
||||
if (newItems.length === 0 && currentCommentPage > 1) {
|
||||
setCurrentCommentPage(currentCommentPage - 1);
|
||||
return { ...prev, items: newItems, totalCount: newTotalCount };
|
||||
try {
|
||||
const res = await API.deleteComment(postId, commentId, user);
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
alert(`Failed to delete comment: ${getApiErrorMessage(err)}`);
|
||||
return;
|
||||
}
|
||||
return { ...prev, items: newItems, totalCount: newTotalCount };
|
||||
});
|
||||
|
||||
setPaginatedComments(prev => {
|
||||
if (!prev) return null;
|
||||
const newItems = prev.items.filter(c => c.id !== commentId);
|
||||
const newTotalCount = prev.totalCount - 1;
|
||||
|
||||
if (newItems.length === 0 && currentCommentPage > 1) {
|
||||
setCurrentCommentPage(currentCommentPage - 1);
|
||||
return { ...prev, items: newItems, totalCount: newTotalCount };
|
||||
}
|
||||
return { ...prev, items: newItems, totalCount: newTotalCount };
|
||||
});
|
||||
} catch (err: any) {
|
||||
alert(`Failed to delete comment: ${getApiErrorMessage(err)}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -311,13 +348,17 @@ export default function Post() {
|
||||
const handleSubmit: FormEventHandler = async (e) => {
|
||||
e.preventDefault();
|
||||
if (newText && commentToEdit && newText !== commentToEdit.text && user) {
|
||||
// TODO
|
||||
const res = await API.updateComment(post.id, commentToEdit.id, newText, user);
|
||||
if (!res.ok) {
|
||||
alert("Failed to update comment.");
|
||||
return;
|
||||
try {
|
||||
const res = await API.updateComment(post.id, commentToEdit.id, newText, user);
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
alert(`Failed to update comment: ${getApiErrorMessage(err)}`);
|
||||
return;
|
||||
}
|
||||
setPaginatedComments(prev => prev ? { ...prev, items: prev.items.map(c => c.id === commentToEdit.id ? {...c, text: newText} : c) } : null);
|
||||
} catch (err: any) {
|
||||
alert(`Failed to update comment: ${getApiErrorMessage(err)}`);
|
||||
}
|
||||
setPaginatedComments(prev => prev ? { ...prev, items: prev.items.map(c => c.id === commentToEdit.id ? {...c, text: newText} : c) } : null);
|
||||
}
|
||||
setIsEditCommentModalOpen(false)
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import type { Route } from "../+types/root";
|
||||
import { useState, useEffect, type FormEventHandler } from "react";
|
||||
|
||||
@@ -12,6 +11,7 @@ import { API } from "../api/api";
|
||||
import { useNavigate } from "react-router";
|
||||
import type { PaginatedResponse } from "~/types/PaginatedResponse";
|
||||
import Loading from "~/components/Loading";
|
||||
import { getApiErrorMessage } from "~/context/AuthContext";
|
||||
|
||||
interface PostCardProps {
|
||||
post: Post;
|
||||
@@ -113,8 +113,12 @@ export default function Posts() {
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const data = await API.fetchPosts(currentPage, searchQuery);
|
||||
setPaginatedData(data);
|
||||
try {
|
||||
const data = await API.fetchPosts(currentPage, searchQuery);
|
||||
setPaginatedData(data);
|
||||
} catch (err: any) {
|
||||
alert(`Failed to fetch posts: ${getApiErrorMessage(err)}`);
|
||||
}
|
||||
}
|
||||
fetchData();
|
||||
}, [currentPage, searchQuery]);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useEffect, useState, type FormEventHandler } from "react";
|
||||
import { API } from "~/api/api";
|
||||
import Icon, { ICONS } from "~/components/Icon";
|
||||
import Loading from "~/components/Loading";
|
||||
import { useAuth } from "~/context/AuthContext";
|
||||
import { getApiErrorMessage, useAuth } from "~/context/AuthContext";
|
||||
import Modal from "~/components/Modal";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
@@ -28,8 +28,12 @@ export default function Tags() {
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
const tags = await API.fetchTags(currentPage);
|
||||
setTags(tags);
|
||||
try {
|
||||
const tags = await API.fetchTags(currentPage);
|
||||
setTags(tags);
|
||||
} catch (err: any) {
|
||||
alert(`Failed to fetch tags: ${getApiErrorMessage(err)}`);
|
||||
}
|
||||
}
|
||||
fetchData();
|
||||
}, [currentPage]);
|
||||
@@ -158,49 +162,64 @@ export default function Tags() {
|
||||
setIsEditTagModalOpen(false);
|
||||
return;
|
||||
};
|
||||
const res = await API.updateTag(tag.name, newType, user);
|
||||
if (!res.ok) {
|
||||
alert(`Failed to update tag: ${await res.text()}`);
|
||||
return;
|
||||
try {
|
||||
const res = await API.updateTag(tag.name, newType, user);
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
alert(`Failed to update tag: ${getApiErrorMessage(err)}`);
|
||||
return;
|
||||
}
|
||||
const updatedTag = await res.json();
|
||||
setTags(prev => prev ? { ...prev, items: prev.items.map(t => t.name === updatedTag.name ? updatedTag : t) } : null);
|
||||
setIsEditTagModalOpen(false);
|
||||
} catch (err: any) {
|
||||
alert(`Failed to update tag: ${getApiErrorMessage(err)}`);
|
||||
}
|
||||
const updatedTag = await res.json();
|
||||
setTags(prev => prev ? { ...prev, items: prev.items.map(t => t.name === updatedTag.name ? updatedTag : t) } : null);
|
||||
setIsEditTagModalOpen(false);
|
||||
}
|
||||
|
||||
async function onCreateTag(name: string, type: TagType) {
|
||||
if (!user) return;
|
||||
const res = await API.createTag(name, type, user);
|
||||
if (!res.ok) {
|
||||
alert(`Failed to create tag: ${await res.text()}`);
|
||||
return;
|
||||
try {
|
||||
const res = await API.createTag(name, type, user);
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
alert(`Failed to create tag: ${getApiErrorMessage(err)}`);
|
||||
return;
|
||||
}
|
||||
const newTag = await res.json();
|
||||
// Reset to first page to see the new tag
|
||||
if (currentPage !== 1) {
|
||||
setCurrentPage(1);
|
||||
} else {
|
||||
setTags(prev => prev ? { ...prev, items: [newTag, ...prev.items] } : { items: [newTag], totalCount: 1, currentPage: 1, pageSize: 20 });
|
||||
}
|
||||
setIsCreateTagModalOpen(false);
|
||||
} catch (err: any) {
|
||||
alert(`Failed to create tag: ${getApiErrorMessage(err)}`);
|
||||
}
|
||||
const newTag = await res.json();
|
||||
// Reset to first page to see the new tag
|
||||
if (currentPage !== 1) {
|
||||
setCurrentPage(1);
|
||||
} else {
|
||||
setTags(prev => prev ? { ...prev, items: [newTag, ...prev.items] } : { items: [newTag], totalCount: 1, currentPage: 1, pageSize: 20 });
|
||||
}
|
||||
setIsCreateTagModalOpen(false);
|
||||
}
|
||||
|
||||
async function onDeleteTag(tag: Tag) {
|
||||
if (!user) return
|
||||
const res = await API.deleteTag(tag.name, user);
|
||||
if (!res.ok) {
|
||||
alert("Failed to delete tag");
|
||||
return;
|
||||
}
|
||||
setTags(prev => {
|
||||
if (!prev) return null;
|
||||
const newItems = prev.items.filter(t => t.name !== tag.name);
|
||||
// If the page is now empty and it's not the first page, go to the previous page
|
||||
if (newItems.length === 0 && currentPage > 1) {
|
||||
setCurrentPage(currentPage - 1);
|
||||
try {
|
||||
const res = await API.deleteTag(tag.name, user);
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
alert(`Failed to delete tag: ${getApiErrorMessage(err)}`);
|
||||
return;
|
||||
}
|
||||
return { ...prev, items: newItems, total: prev.totalCount - 1 };
|
||||
});
|
||||
setTags(prev => {
|
||||
if (!prev) return null;
|
||||
const newItems = prev.items.filter(t => t.name !== tag.name);
|
||||
// If the page is now empty and it's not the first page, go to the previous page
|
||||
if (newItems.length === 0 && currentPage > 1) {
|
||||
setCurrentPage(currentPage - 1);
|
||||
}
|
||||
return { ...prev, items: newItems, totalCount: prev.totalCount - 1 };
|
||||
});
|
||||
} catch (err: any) {
|
||||
alert(`Failed to delete tag: ${getApiErrorMessage(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
|
||||
|
||||
Reference in New Issue
Block a user