diff --git a/api/AxiosProvider.tsx b/api/AxiosProvider.tsx new file mode 100644 index 00000000..c1f5611b --- /dev/null +++ b/api/AxiosProvider.tsx @@ -0,0 +1,23 @@ +'use client'; + +import React, { createContext, useContext } from 'react'; +import { AxiosInstance } from 'axios'; +import axiosInstanceDefault from './axiosInstance'; + +const AxiosContext = createContext(null); + +export const useAxios = () => { + const axiosInstance = useContext(AxiosContext); + if (!axiosInstance) { + throw new Error('useAxios must be used within an AxiosProvider'); + } + return axiosInstance; +}; + +interface AxiosProviderProps { + children: React.ReactNode; +} + +export const AxiosProvider = ({ children }: AxiosProviderProps) => { + return {children}; +}; diff --git a/api/axiosInstance.ts b/api/axiosInstance.ts new file mode 100644 index 00000000..1d08f3ca --- /dev/null +++ b/api/axiosInstance.ts @@ -0,0 +1,10 @@ +import axios from 'axios'; + +const axiosInstance = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, + headers: { + 'Content-Type': 'application/json', + }, +}); + +export default axiosInstance; diff --git a/api/endpoints.ts b/api/endpoints.ts new file mode 100644 index 00000000..1edf1e4d --- /dev/null +++ b/api/endpoints.ts @@ -0,0 +1,23 @@ +const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL; + +if (!API_BASE_URL) { + throw new Error('NEXT_PUBLIC_API_BASE_URL is not defined in the environment variables'); +} + +const ENDPOINTS = { + BLOGS: { + GET_ALL: (limit: number, skip: number) => `${API_BASE_URL}/posts?limit=${limit}&skip=${skip}`, + CREATE: `${API_BASE_URL}/posts`, + }, + USERS: { + GET_ALL: `${API_BASE_URL}/users`, + }, + CONTACT: { + SUBMIT: `${API_BASE_URL}/contact`, + }, + NEWSLETTER: { + SUBSCRIBE: `${API_BASE_URL}/newsletter-subscribers`, + }, +}; + +export default ENDPOINTS; diff --git a/api/mutations/blogs/useAddNewPost.ts b/api/mutations/blogs/useAddNewPost.ts new file mode 100644 index 00000000..c98c8e52 --- /dev/null +++ b/api/mutations/blogs/useAddNewPost.ts @@ -0,0 +1,24 @@ +'use client'; + +import { useMutation } from '@tanstack/react-query'; +import { useAxios } from '../../AxiosProvider'; +import ENDPOINTS from '../../endpoints'; + +type NewPost = { + title: string; + body: string; + userId: number; +}; + +const useAddNewPost = () => { + const axios = useAxios(); + + return useMutation({ + mutationFn: async (newPost: NewPost) => { + const response = await axios.post(ENDPOINTS.BLOGS.CREATE, newPost); + return response.data; + }, + }); +}; + +export default useAddNewPost; diff --git a/api/mutations/contact/useSubmitContactform.ts b/api/mutations/contact/useSubmitContactform.ts new file mode 100644 index 00000000..f4c23555 --- /dev/null +++ b/api/mutations/contact/useSubmitContactform.ts @@ -0,0 +1,21 @@ +import { useMutation } from '@tanstack/react-query'; +import ENDPOINTS from '../../endpoints'; +import { useAxios } from '../../AxiosProvider'; + +export interface ContactFormData { + name: string; + email: string; + message: string; + cfTurnstileResponse: string; +} + +export const useSubmitContactForm = () => { + const axios = useAxios(); + + return useMutation({ + mutationFn: async (formData: ContactFormData) => { + const response = await axios.post(ENDPOINTS.CONTACT.SUBMIT, formData); + return response.data.message; + }, + }); +}; diff --git a/api/mutations/newsletter/useSubmitNewsletterForm.ts b/api/mutations/newsletter/useSubmitNewsletterForm.ts new file mode 100644 index 00000000..8bfc83ae --- /dev/null +++ b/api/mutations/newsletter/useSubmitNewsletterForm.ts @@ -0,0 +1,20 @@ +import { useMutation } from '@tanstack/react-query'; +import ENDPOINTS from '../../endpoints'; +import { useAxios } from '../../AxiosProvider'; + +export interface NewsletterFormData { + first_name: string; + email: string; + 'cf-turnstile-response': string; +} + +export const useSubmitNewsletterForm = () => { + const axios = useAxios(); + + return useMutation({ + mutationFn: async (formData: NewsletterFormData) => { + const response = await axios.post(ENDPOINTS.NEWSLETTER.SUBSCRIBE, formData); + return response.data.message; + }, + }); +}; diff --git a/api/queries/blogs/getBlogs.ts b/api/queries/blogs/getBlogs.ts new file mode 100644 index 00000000..07749b55 --- /dev/null +++ b/api/queries/blogs/getBlogs.ts @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query'; +import fetchBlogPosts from '../../../app/action'; +import QUERY_KEYS from '../../queryKeys'; + +interface GetBlogsParams { + pageTitle: string; + blogCardsNumber: number; +} + +const useGetBlogs = ({ pageTitle, blogCardsNumber }: GetBlogsParams) => { + return useQuery({ + queryKey: [...QUERY_KEYS.BLOGS.ALL, pageTitle, blogCardsNumber], + queryFn: () => fetchBlogPosts(0, blogCardsNumber), + }); +}; + +export default useGetBlogs; diff --git a/api/queries/blogs/getInfiniteBlogs.ts b/api/queries/blogs/getInfiniteBlogs.ts new file mode 100644 index 00000000..b9e03632 --- /dev/null +++ b/api/queries/blogs/getInfiniteBlogs.ts @@ -0,0 +1,32 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; +import QUERY_KEYS from '../../queryKeys'; +import fetchBlogPosts from '../../../app/action'; +import { BlogCardProps } from '../../../components/reusable-components/blog-card/BlogCard'; + +interface GetInfiniteBlogsParams { + pageTitle: string; + blogCardsNumber: number; +} +const useGetInfiniteBlogs = ({ pageTitle, blogCardsNumber }: GetInfiniteBlogsParams) => { + return useInfiniteQuery< + BlogCardProps[], + Error, + BlogCardProps[], + [string, string, string, string], + number + >({ + queryKey: [ + QUERY_KEYS.BLOGS.INFINITE[0], + QUERY_KEYS.BLOGS.INFINITE[1], + pageTitle, + blogCardsNumber.toString(), + ], + queryFn: ({ pageParam }) => fetchBlogPosts(pageParam, blogCardsNumber), + initialPageParam: 0, + getNextPageParam: (lastPage, allPages) => { + return lastPage.length === blogCardsNumber ? allPages.length * blogCardsNumber : undefined; + }, + }); +}; + +export default useGetInfiniteBlogs; diff --git a/api/queries/users/getUsers.ts b/api/queries/users/getUsers.ts new file mode 100644 index 00000000..17ace93c --- /dev/null +++ b/api/queries/users/getUsers.ts @@ -0,0 +1,17 @@ +import { useAxios } from '../../AxiosProvider'; +import ENDPOINTS from '../../endpoints'; +import QUERY_KEYS from '../../queryKeys'; + +const useGetUsers = () => { + const axios = useAxios(); + + return { + queryKey: QUERY_KEYS.USERS.ALL, + queryFn: async () => { + const { data } = await axios.get(ENDPOINTS.USERS.GET_ALL); + return data; + }, + }; +}; + +export default useGetUsers; diff --git a/api/queryKeys.ts b/api/queryKeys.ts new file mode 100644 index 00000000..7622ab1f --- /dev/null +++ b/api/queryKeys.ts @@ -0,0 +1,11 @@ +const QUERY_KEYS = { + BLOGS: { + ALL: ['blogPosts'], + INFINITE: ['infiniteBlogPosts', 'infinite'] as const, + }, + USERS: { + ALL: ['users'], + }, +} as const; + +export default QUERY_KEYS; diff --git a/app/action.tsx b/app/action.tsx index f04ea989..b78626c8 100644 --- a/app/action.tsx +++ b/app/action.tsx @@ -1,28 +1,28 @@ 'use server'; -import BlogCard, { BlogCardProps } from '../components/reusable-components/blog-card/BlogCard'; +import axios from 'axios'; +import axiosInstance from '../api/axiosInstance'; +import { BlogCardProps } from '../components/reusable-components/blog-card/BlogCard'; -const fetchBlogPosts = async (nextPosts: number, pageTitle: string, blogCardsNumber: number) => { +const fetchBlogPosts = async ( + nextPosts: number, + blogCardsNumber: number +): Promise => { try { - const response = await fetch( - `https://dummyjson.com/posts?limit=${blogCardsNumber}&skip=${nextPosts}` - ); - - const data = await response.json(); - - return data?.posts.map((post: BlogCardProps) => { - return ( - - ); + const { data } = await axiosInstance.get('/posts', { + params: { + limit: blogCardsNumber, + skip: nextPosts, + }, }); + return data.posts; } catch (error: any) { - throw new Error(error); + if (axios.isAxiosError(error)) { + throw new Error( + error.response?.data?.message || 'An error occurred while fetching blog posts' + ); + } + throw new Error('An unexpected error occurred'); } }; diff --git a/app/addNewPost.tsx b/app/addNewPost.tsx deleted file mode 100644 index d302c2bc..00000000 --- a/app/addNewPost.tsx +++ /dev/null @@ -1,52 +0,0 @@ -'use client'; - -import { useMutation } from '@tanstack/react-query'; - -type User = { - title: string; - body: string; - userId: number; -}; - -const AddNewPost = () => { - const mutation = useMutation({ - mutationFn: async (newUser: User) => { - const response = await fetch('https://jsonplaceholder.typicode.com/posts', { - method: 'POST', - body: JSON.stringify({ - newUser, - }), - headers: { - 'Content-type': 'application/json; charset=UTF-8', - }, - }); - const data = await response.json(); - return data; - }, - }); - - return ( -
- {mutation.isPending ? ( - 'Adding post...' - ) : ( - <> - {mutation.isError ?
An error occurred: {mutation.error.message}
: null} - - {mutation.isSuccess ?
Added new post
: null} - - - - )} -
- ); -}; - -export default AddNewPost; diff --git a/app/blog/page.tsx b/app/blog/page.tsx index 0218d10f..445285c3 100644 --- a/app/blog/page.tsx +++ b/app/blog/page.tsx @@ -1,6 +1,6 @@ import BlogList from '../../components/module-components/blog-list/BlogList'; import Tab from '../../components/reusable-components/tab/Tab'; -// import AddNewPost from '../addNewPost'; +// import AddNewPost from '../../components/module-components/blog/addNewPost'; const Blog = () => { return ( diff --git a/app/getUsers.tsx b/app/getUsers.tsx deleted file mode 100644 index 244864d9..00000000 --- a/app/getUsers.tsx +++ /dev/null @@ -1,13 +0,0 @@ -async function getData() { - try { - const response = await fetch('https://jsonplaceholder.typicode.com/users'); - return response.json(); - } catch (error: any) { - throw new Error(error); - } -} - -export default async function getUsers() { - const data = await getData(); - return data; -} diff --git a/app/layout.tsx b/app/layout.tsx index 0ccd8fbb..70b71c51 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -5,6 +5,7 @@ import React, { Suspense } from 'react'; import Head from 'next/head'; import Script from 'next/script'; import './styles/main.scss'; +import { ToastContainer } from 'react-toastify'; import Loading from './loading'; import Footer from '../components/reusable-components/footer/Footer'; import Navigation from '../components/reusable-components/navigation/Navigation'; @@ -12,6 +13,7 @@ import ReactQueryProvider from '../utils/providers/ReactQueryProvider'; import { ThemeProvider } from './context/themeContext'; import styles from './page.module.scss'; import { AuthProvider } from './context/authContext'; +import { AxiosProvider } from '../api/AxiosProvider'; const montserrat = Montserrat({ subsets: ['latin'], weight: ['400', '500', '700'] }); @@ -50,17 +52,20 @@ const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => { /> )} + - - - -
- }>{children} - -
-