0% found this document useful (0 votes)
35 views39 pages

Day15 Assignment

The document outlines a series of challenges focused on building a React application that interacts with a posts API using React Query. It includes tasks such as fetching posts, creating new posts with optimistic updates, handling loading and error states, and implementing a search feature. Additionally, it emphasizes the use of type-safe cache keys and stale-while-revalidate strategies for improved UI responsiveness.

Uploaded by

y22cs035
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
35 views39 pages

Day15 Assignment

The document outlines a series of challenges focused on building a React application that interacts with a posts API using React Query. It includes tasks such as fetching posts, creating new posts with optimistic updates, handling loading and error states, and implementing a search feature. Additionally, it emphasizes the use of type-safe cache keys and stale-while-revalidate strategies for improved UI responsiveness.

Uploaded by

y22cs035
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 39

Day-15 Assignment

Challenge 1:
Fetch a list of posts from https://jsonplaceholder.typicode.com/posts using useQuery.
 Add a form to create a new post using useMutation .
 Display loading and error states for both queries and mutations.
 Use DevTools to monitor active queries and mutations.
 Bonus: Implement optimistic updates for the post creation
 Form
A:

Folder Structure:
my-app/
├── public/
│ └── index.html
├── src/
│ ├── components/
│ │ └── PostForm.tsx # Optional: Extract form from App
│ │ └── PostList.tsx # Optional: Extract post list UI
│ ├── hooks/
│ │ └── usePosts.ts # useQuery logic for fetching posts
│ ├── store/
│ │ └── postStore.ts # Optional: Zustand slice if needed
│ ├── App.tsx # Main component with form + query
│ ├── main.tsx # React root + QueryClientProvider
│ └── types/
│ └── post.ts # Reusable Post interface
├── package.json
├── tsconfig.json
└── vite.config.ts # (depending on Vite )

src/main.tsx:
// Step 1: Set up QueryClient and wrap your app
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
// Create a client instance
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
{/* Wrap App with QueryClientProvider */}
<QueryClientProvider client={queryClient}>
<App />
{/* Add React Query DevTools */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</React.StrictMode>
);
src/hooks/usePosts.ts:
// useQuery to fetch posts from the API
import { useQuery } from '@tanstack/react-query';
export interface Post {
id: number;
title: string;
body: string;
}
const fetchPosts = async (): Promise<Post[]> => {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!res.ok) throw new Error('Failed to fetch posts');
return res.json();
};
export const usePosts = () =>
useQuery<Post[], Error>({
queryKey: ['posts'], // type-safe cache key
queryFn: fetchPosts,
staleTime: 1000 * 60, // 1 minute (stale-while-revalidate style)
});

src/types/post.ts:
export interface Post {
id: number;
title: string;
body: string;
}
export interface NewPost {
title: string;
body: string;
}
src/App.tsx:
import React, { useState } from 'react';
import { usePosts } from './hooks/usePosts';
import { useMutation, useQueryClient } from '@tanstack/react-query';

// ✅ IMPORT types here

import type { Post, NewPost } from './types/post'; // ✅ type-only import

// Form component inside App


function App() {
const { data: posts, isLoading, error } = usePosts();
const queryClient = useQueryClient();

const [title, setTitle] = useState('');


const [body, setBody] = useState('');

// useMutation to create a new post


const createPost = useMutation({
mutationFn: async (newPost: NewPost) => {
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost),
});
if (!res.ok) throw new Error('Failed to create post');
return res.json();
},
// Optimistic Update
onMutate: async (newPost) => {
await queryClient.cancelQueries({ queryKey: ['posts'] });

const previousPosts = queryClient.getQueryData<Post[]>(['posts']) ?? [];

const optimisticPost: Post = {


id: Date.now(), // Unique id for optimistic
...newPost,
};

queryClient.setQueryData<Post[]>(['posts'], (old) => [...(old || []), optimisticPost]);

return { previousPosts, optimisticPostId: optimisticPost.id };


}
,

// On Error: rollback
onError: (_err, _newPost, context) => {
if (context?.previousPosts) {
queryClient.setQueryData(['posts'], context.previousPosts);
}
},

onSuccess: (savedPost, _newPost, context) => {


queryClient.setQueryData<Post[]>(['posts'], (old) =>
(old || []).map(post =>
post.id === context?.optimisticPostId ? savedPost : post
)
);
}
});

// Handle form submit


const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
createPost.mutate({ title, body });
setTitle('');
setBody('');
};
return (
<div style={{ padding: '1rem' }}>
<h1>Post Feed</h1>
{/* Form to create post */}
<form onSubmit={handleSubmit}>
<input
placeholder="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
<br />
<textarea
placeholder="Body"
value={body}
onChange={(e) => setBody(e.target.value)}
required
/>
<br />
<button type="submit" disabled={createPost.isPending}>
{createPost.isPending ? 'Posting...' : 'Post'}
</button>
</form>
{/* Loading and error states */}
{isLoading && <p>Loading posts...</p>}
{error && <p style={{ color: 'red' }}>{error.message}</p>}

{/* Display posts */}


{posts?.map((post) => (
<div key={post.id} style={{ margin: '1rem 0', border: '1px solid #ccc', padding: '0.5rem'
}}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</div>
))}
</div>
);
}
export default App;

Output:
Challenge 2:
1. Set Up Your Project
 Create a new React app.
 Install React Query (or SWR) and Axios (optional, you can use fetch).
 Set up the QueryClient and wrap your app with QueryClientProvider .
2. Fetch and Display Data
 Use the useQuery hook to fetch a list of posts from
 https://jsonplaceholder.typicode.com/posts .
 Display the posts in a simple list.
 Show a loading state while the data is being fetched.
 Show an error message if the fetch fails.
3. Add a Form to Create a New Post
 Create a form with fields for title and body .
 Use the useMutation hook to post the new data to
https://jsonplaceholder.typicode.com/posts .
 After submitting the form, display a success message and update the list of posts.
4. Implement Optimistic Updates
 When you submit the form, immediately add the new post to the list before the
 server responds.
 If the server request fails, remove the optimistically added post.
 Show a loading indicator while the mutation is in progress.
5. Invalidate the Cache
 After a successful mutation, invalidate the posts query so the list is refreshed and
 up-to-date.
 Alternatively, update the cache manually with the response from the server.
6. Bonus: Use DevTools
 Add React Query DevTools to your app to monitor and debug your queries and
 mutations.
A:
src/main.tsx:

// ✅ Task 1: Setup Project with QueryClientProvider and DevTools


import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import './index.css';

const queryClient = new QueryClient();

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />

{/* ✅ Task 6: Add DevTools */}


<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</React.StrictMode>
);

src/hooks/usePosts.ts:

// ✅ Task 2: Fetch and Display Data using useQuery


import { useQuery } from '@tanstack/react-query';
import type { Post } from '../types/post';

const fetchPosts = async (): Promise<Post[]> => {


const res = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!res.ok) throw new Error('Failed to fetch posts');
return res.json();
};

export const usePosts = () =>


useQuery<Post[], Error>({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: 60_000, // 1 min SWR-style
});

src/types/post.ts:
export interface Post {
id: number;
title: string;
body: string;
}
export interface NewPost {
title: string;
body: string;
}

src/App.tsx:
import React, { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { usePosts } from './hooks/usePosts';
import type { Post, NewPost } from './types/post';

function App() {
const { data: posts, isLoading, error } = usePosts();
const queryClient = useQueryClient();

const [title, setTitle] = useState('');


const [body, setBody] = useState('');
const [successMessage, setSuccessMessage] = useState('');
const [searchQuery, setSearchQuery] = useState('');

const createPost = useMutation({


mutationFn: async (newPost: NewPost) => {
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost),
});
if (!res.ok) throw new Error('Failed to create post');
return res.json();
},

onMutate: async (newPost) => {


await queryClient.cancelQueries({ queryKey: ['posts'] });
const previousPosts = queryClient.getQueryData<Post[]>(['posts']) ?? [];

const optimisticPost: Post = {


id: Date.now(),
...newPost,
};

queryClient.setQueryData<Post[]>(['posts'], (old) => [


...(old || []),
optimisticPost,
]);

return { previousPosts, optimisticPostId: optimisticPost.id };


},

onError: (_err, _newPost, context) => {


if (context?.previousPosts) {
queryClient.setQueryData(['posts'], context.previousPosts);
}
},

onSuccess: (savedPost, _newPost, context) => {


queryClient.setQueryData<Post[]>(['posts'], (old) =>
(old || []).map((post) =>
post.id === context?.optimisticPostId ? savedPost : post
)
);
setSuccessMessage('Post created successfully!');
setTimeout(() => setSuccessMessage(''), 3000);
},
});

const handleSubmit = (e: React.FormEvent) => {


e.preventDefault();
createPost.mutate({ title, body });
setTitle('');
setBody('');
};

// Filter posts by search query (case insensitive)


const filteredPosts = posts?.filter((post) =>
post.title.toLowerCase().includes(searchQuery.toLowerCase())
);

return (
<div style={styles.container}>

<h1 style={styles.header}>🌟 React Query Post Feed</h1>


<form onSubmit={handleSubmit} style={styles.form}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Title"
required
style={styles.input}
/>
<textarea
value={body}
onChange={(e) => setBody(e.target.value)}
placeholder="Body"
required
style={styles.textarea}
/>
<button type="submit" disabled={createPost.isPending} style={styles.button}>
{createPost.isPending ? 'Posting...' : 'Post'}
</button>
</form>

{successMessage && <p style={styles.success}>{successMessage}</p>}


{isLoading && <p style={styles.loading}>Loading posts...</p>}
{error && <p style={styles.error}>{error.message}</p>}

{/* ✅ Search Bar */}


<input
type="text"

placeholder="🔍 Search posts by title..."


value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
style={styles.search}
/>

{/* ✅ Post List */}


{filteredPosts?.map((post) => (
<div key={post.id} style={styles.post}>
<h3>{post.title}</h3>
<p
style={{
fontStyle: post.id > 100 ? 'italic' : 'normal',
color: post.id > 100 ? 'green' : 'black',
}}
>
{post.body}
{post.id > 100 && <strong style={{ color: 'green' }}> Optimistic</strong>}
</p>
</div>
))}

{/* ✅ No Results Fallback */}


{filteredPosts?.length === 0 && (
<p style={{ color: '#999', fontStyle: 'italic', marginTop: '1rem' }}>
No posts found matching your search.
</p>
)}

</div>
);
}
export default App;

// ✅ Inline Styles
const styles: {[key: string]: React.CSSProperties } = {
container: {
width: '100%',
minHeight: '100vh',
padding: '2rem',
fontFamily: 'Segoe UI, sans-serif',
background: '#fdfdfd',
boxSizing: 'border-box',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
header: {
textAlign: 'center',
color: '#333',
},
form: {
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
marginBottom: '1rem',
},
input: {
padding: '0.5rem',
fontSize: '1rem',
borderRadius: '5px',
border: '1px solid #aaa',
},
textarea: {
padding: '0.5rem',
fontSize: '1rem',
borderRadius: '5px',
height: '80px',
border: '1px solid #aaa',
},
button: {
backgroundColor: '#4caf50',
color: 'white',
border: 'none',
padding: '0.5rem 1rem',
fontSize: '1rem',
borderRadius: '5px',
cursor: 'pointer',
},
search: {
width: '100%',
padding: '0.5rem',
marginBottom: '1rem',
borderRadius: '5px',
border: '1px solid #aaa',
fontSize: '1rem',
},
post: {
border: '1px solid #ccc',
padding: '1rem',
marginBottom: '1rem',
backgroundColor: '#fafafa',
borderRadius: '6px',
},
success: {
color: 'green',
fontWeight: 'bold',
},
error: {
color: 'red',
},
loading: {
fontStyle: 'italic',
},

};

Output:
Challenge 3:
 Implement a data-fetching hook that uses a type-safe cache key and supports a stale-
while-revalidate strategy.
 Use useMemo to cache expensive computations in a component.
 Experiment with React Query or SWR to see how cache keys and revalidation affect
UI responsiveness.
 Try invalidang the cache and observe how fresh data is fetched and displayed
A:

src/hooks/usePosts.ts:
import { useQuery } from '@tanstack/react-query';
import type { Post } from '../types/post';

const fetchPosts = async (): Promise<Post[]> => {


const res = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!res.ok) throw new Error('Failed to fetch posts');
return res.json();
};

// Type-safe key
const POST_QUERY_KEY = ['posts'] as const;

export const usePosts = () =>


useQuery<Post[], Error>({
queryKey: POST_QUERY_KEY,
queryFn: fetchPosts,
staleTime: 60_000, // Stale-while-revalidate (1 min)
refetchOnWindowFocus: true,
retry: 1,
});

export { POST_QUERY_KEY };
src/components/PostList.tsx:
import React, { useMemo } from 'react';
import { usePosts } from '../hooks/usePosts';

const PostList: React.FC<{ searchQuery: string }> = ({ searchQuery }) => {


const { data: posts, isLoading, error } = usePosts();
const filteredPosts = useMemo(() => {
if (!posts) return [];
return posts.filter(post =>
post.title.toLowerCase().includes(searchQuery.toLowerCase())
);
}, [posts, searchQuery]);

if (isLoading) return <p>Loading posts...</p>;


if (error) return <p>Error: {error.message}</p>;
if (filteredPosts.length === 0) return <p style={{ textAlign: 'center' }}>No posts
found.</p>;
return (
<>
{filteredPosts.map(post => (
<div key={post.id} style={{ borderBottom: '1px solid #ccc', padding: '1rem 0' }}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</div>
))}
</>
);
};

export default PostList;


src/components/PostForm.tsx:
import React, { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { POST_QUERY_KEY } from '../hooks/usePosts';
import type { Post, NewPost } from '../types/post';

const PostForm = () => {


const [title, setTitle] = useState('');
const [body, setBody] = useState('');
const [success, setSuccess] = useState('');
const queryClient = useQueryClient();

const mutation = useMutation({


mutationFn: async (newPost: NewPost) => {
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost),
});
if (!res.ok) throw new Error('Failed to create post');
return res.json();
},

onMutate: async newPost => {


await queryClient.cancelQueries({ queryKey: POST_QUERY_KEY });
const previousPosts = queryClient.getQueryData<Post[]>(POST_QUERY_KEY) ?? [];

const optimisticPost: Post = {


id: Date.now(),
...newPost,
};

queryClient.setQueryData<Post[]>(POST_QUERY_KEY, old => [


...(old || []),
optimisticPost,
]);

return { previousPosts };
},

onError: (_err, _newPost, context) => {


if (context?.previousPosts) {
queryClient.setQueryData(POST_QUERY_KEY, context.previousPosts);
}
},

onSuccess: () => {
setSuccess('Post created successfully!');
setTimeout(() => setSuccess(''), 3000);
},

onSettled: () => {
queryClient.invalidateQueries({ queryKey: POST_QUERY_KEY });
},
});

const handleSubmit = (e: React.FormEvent) => {


e.preventDefault();
mutation.mutate({ title, body });
setTitle('');
setBody('');
};

return (
<form onSubmit={handleSubmit} style={{ marginBottom: '1rem' }}>
<input value={title} onChange={e => setTitle(e.target.value)} placeholder="Title"
required />
<textarea value={body} onChange={e => setBody(e.target.value)} placeholder="Body"
required />
<button type="submit">{mutation.isPending ? 'Posting...' : 'Post'}</button>
{success && <p style={{ color: 'green' }}>{success}</p>}
</form>
);
};

export default PostForm;

App.tsx:
import React, { useState, useMemo, useEffect } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { usePosts } from './hooks/usePosts';
import type { Post, NewPost } from './types/post';

function App() {
const { data: posts, isLoading, error } = usePosts();
const queryClient = useQueryClient();

const [title, setTitle] = useState('');


const [body, setBody] = useState('');
const [successMessage, setSuccessMessage] = useState('');
const [searchQuery, setSearchQuery] = useState('');
const createPost = useMutation({
mutationFn: async (newPost: NewPost) => {
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost),
});
if (!res.ok) throw new Error('Failed to create post');
return res.json();
},
onMutate: async (newPost) => {
await queryClient.cancelQueries({ queryKey: ['posts'] });
const previousPosts = queryClient.getQueryData<Post[]>(['posts']) ?? [];

const optimisticPost: Post = {


id: Date.now(),
...newPost,
};

queryClient.setQueryData<Post[]>(['posts'], (old) => [


...(old || []),
optimisticPost,
]);

return { previousPosts, optimisticPostId: optimisticPost.id };


},
onError: (_err, _newPost, context) => {
if (context?.previousPosts) {
queryClient.setQueryData(['posts'], context.previousPosts);
}
},
onSuccess: (savedPost, _newPost, context) => {
queryClient.setQueryData<Post[]>(['posts'], (old) =>
(old || []).map((post) =>
post.id === context?.optimisticPostId ? savedPost : post
)
);
setSuccessMessage('Post created successfully!');
},
});

// Auto clear success message after 3 seconds with fade effect


useEffect(() => {
if (successMessage) {
const timeout = setTimeout(() => setSuccessMessage(''), 3000);
return () => clearTimeout(timeout);
}
}, [successMessage]);

const handleSubmit = (e: React.FormEvent) => {


e.preventDefault();
createPost.mutate({ title, body });
setTitle('');
setBody('');
};

const filteredPosts = useMemo(() => {


return posts?.filter((post) =>
post.title.toLowerCase().includes(searchQuery.toLowerCase())
);
}, [posts, searchQuery]);

return (
<div style={styles.container}>

<h1 style={styles.header}>🌟 React Query Post Feed</h1>

<form onSubmit={handleSubmit} style={styles.form} noValidate>


<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Post Title"
required
style={styles.input}
maxLength={100}
spellCheck={false}
autoComplete="off"
/>
<textarea
value={body}
onChange={(e) => setBody(e.target.value)}
placeholder="Write your post here..."
required
style={styles.textarea}
rows={4}
maxLength={300}
spellCheck={false}
autoComplete="off"
/>
<button type="submit" disabled={createPost.isPending} style={styles.button}>
{createPost.isPending ? 'Posting...' : 'Post'}
</button>
</form>

{successMessage && <p style={styles.success}>{successMessage}</p>}


{isLoading && <p style={styles.loading}>Loading posts...</p>}
{error && <p style={styles.error}>{error.message}</p>}

<input
type="text"

placeholder="🔍 Search posts by title..."


value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
style={styles.search}
spellCheck={false}
autoComplete="off"
/>

{filteredPosts && filteredPosts.length > 0 ? (


filteredPosts.map((post) => (
<div key={post.id} style={styles.post}>
<h3 style={styles.postTitle}>{post.title}</h3>
<p
style={{
...styles.postBody,
fontStyle: post.id > 100 ? 'italic' : 'normal',
color: post.id > 100 ? '#22863a' : '#333',
}}
>
{post.body}
{post.id > 100 && ' (Optimistic)'}
</p>
</div>
))
):(
<p style={styles.noPosts}>No posts found.</p>
)}
</div>
);
}

export default App;

const styles: { [key: string]: React.CSSProperties } = {


container: {
maxWidth: '720px',
margin: '2rem auto',
padding: '2rem 2.5rem',
fontFamily: "'Inter', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif",
background: '#fff',
borderRadius: '12px',
boxShadow:
'0 4px 12px rgba(0, 0, 0, 0.05), 0 2px 6px rgba(0, 0, 0, 0.07)',
},
header: {
textAlign: 'center',
color: '#24292e',
fontWeight: 700,
fontSize: '2.25rem',
marginBottom: '1.5rem',
userSelect: 'none',
},
form: {
display: 'flex',
flexDirection: 'column',
gap: '0.75rem',
marginBottom: '1.5rem',
},
input: {
padding: '0.75rem 1rem',
fontSize: '1.125rem',
borderRadius: '8px',
border: '1.5px solid #d1d5da',
outlineOffset: '2px',
outlineColor: 'transparent',
transition: 'border-color 0.3s ease, box-shadow 0.3s ease',
fontWeight: 500,
},
textarea: {
padding: '0.75rem 1rem',
fontSize: '1.125rem',
borderRadius: '8px',
border: '1.5px solid #d1d5da',
outlineOffset: '2px',
outlineColor: 'transparent',
transition: 'border-color 0.3s ease, box-shadow 0.3s ease',
fontWeight: 500,
resize: 'vertical',
fontFamily: "'Inter', sans-serif",
},
button: {
marginTop: '0.5rem',
backgroundColor: '#2ea44f',
color: '#fff',
border: 'none',
padding: '0.75rem 1.25rem',
fontSize: '1.125rem',
fontWeight: 600,
borderRadius: '8px',
cursor: 'pointer',
boxShadow: '0 4px 8px rgb(46 164 79 / 0.35)',
transition: 'background-color 0.3s ease, box-shadow 0.3s ease',
userSelect: 'none',
},
search: {
width: '100%',
padding: '0.6rem 1rem',
marginBottom: '1.75rem',
borderRadius: '8px',
border: '1.5px solid #d1d5da',
fontSize: '1rem',
fontWeight: 500,
outlineOffset: '2px',
outlineColor: 'transparent',
transition: 'border-color 0.3s ease, box-shadow 0.3s ease',
},
post: {
border: '1px solid #e1e4e8',
padding: '1.25rem 1.5rem',
marginBottom: '1.25rem',
backgroundColor: '#f6f8fa',
borderRadius: '10px',
boxShadow: 'inset 0 1px 2px rgb(27 31 35 / 0.05)',
},
postTitle: {
margin: '0 0 0.4rem 0',
fontWeight: 700,
fontSize: '1.3rem',
color: '#0366d6',
userSelect: 'text',
},
postBody: {
margin: 0,
fontSize: '1rem',
lineHeight: 1.5,
color: '#24292e',
},
success: {
color: '#2ea44f',
fontWeight: 600,
fontSize: '1rem',
textAlign: 'center',
marginBottom: '1rem',
opacity: 1,
transition: 'opacity 0.5s ease-in-out',
userSelect: 'none',
},
error: {
color: '#d73a49',
fontWeight: 600,
textAlign: 'center',
marginBottom: '1rem',
userSelect: 'none',
},
loading: {
fontStyle: 'italic',
textAlign: 'center',
color: '#6a737d',
marginBottom: '1rem',
userSelect: 'none',
},
noPosts: {
textAlign: 'center',
fontStyle: 'italic',
color: '#6a737d',
userSelect: 'none',
},
};

Output:

You might also like