Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tags page with React Query fetching implementation #439

Merged
merged 50 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
9a6ae3a
Implement Add Tag functionality
petark7 Jul 15, 2024
73ed7fb
Update page.tsx
petark7 Jul 15, 2024
240e1a4
Implement delete functionality
petark7 Jul 16, 2024
571e788
Merge branch 'staging' into 366-fe-implement-delete-tag-functionality
petark7 Jul 17, 2024
7b466d9
Translation + lift state to parent
petark7 Aug 3, 2024
2f9a5ac
Implement Add Tag functionality
petark7 Jul 15, 2024
89f7d4b
Implement Add Tag functionality
petark7 Aug 3, 2024
54fd679
Implement Add Tag functionality
petark7 Jul 15, 2024
7de8099
Implement delete functionality
petark7 Jul 16, 2024
c83b098
Implement delete functionality
petark7 Aug 3, 2024
1178528
Merge branch '366-fe-implement-delete-tag-functionality' of https://g…
petark7 Aug 3, 2024
e91ed9a
Remove unrelated functionality from branch
petark7 Aug 3, 2024
afa1ab4
Implement Add Tag functionality
petark7 Jul 15, 2024
720dd1a
Implement delete functionality
petark7 Jul 16, 2024
3e8ae58
Remove unrelated functionality from branch
petark7 Aug 3, 2024
369d821
Add temporary tag component
petark7 Aug 4, 2024
dd7af05
Merge branch '366-fe-implement-delete-tag-functionality' of https://g…
petark7 Aug 4, 2024
fd91a9b
Implement edit functionality
petark7 Aug 4, 2024
ffa1201
Fix conflicts with ReusableTable
petark7 Aug 4, 2024
ee0324a
Merge pull request #425 from learnhubmk/366-fe-implement-delete-tag-f…
petark7 Aug 5, 2024
14895f0
Merge branch '359-fe-implement-add-tag-functionality' into feature/ta…
petark7 Aug 6, 2024
055f9a6
Merge edit functionality
petark7 Aug 6, 2024
097dc23
Implement search functionality
petark7 Aug 6, 2024
2202175
Implement debouncing
petark7 Aug 6, 2024
2ae1288
Fix issue with reusable table component row height
petark7 Aug 9, 2024
652129c
Ignore whitespace in search results
petark7 Aug 13, 2024
344e872
Merge pull request #433 from learnhubmk/tags-search
edichoska Aug 15, 2024
d5b1ee1
Styling updates
petark7 Aug 17, 2024
dc2b7c9
Merge branch 'staging' into feature/tags-with-query
petark7 Aug 17, 2024
ed05325
Fetch tags with React Query
petark7 Aug 17, 2024
2a03c8e
Implement add tags functionality
petark7 Aug 18, 2024
a2a884d
Implement delete tags funcitonality
petark7 Aug 18, 2024
5738a86
Implement edit functionality + clarity changes
petark7 Aug 18, 2024
65abcd1
Fix typescript errors
petark7 Aug 18, 2024
a8183f3
Update useEditTag.ts
petark7 Aug 18, 2024
a482db6
Implement search functionality
petark7 Aug 18, 2024
16b544e
Update page.tsx
petark7 Aug 18, 2024
ea443ea
Remove hardcoded token
petark7 Aug 21, 2024
5c84b14
Update axiosInstance.ts
petark7 Aug 21, 2024
4ca558b
Merge branch 'staging' into feature/tags-with-query
petark7 Aug 21, 2024
eeccc63
Merge branch 'feature/tags-with-query' of https://github.com/learnhub…
petark7 Aug 21, 2024
8036c34
Implement refetching on CRUD operations
petark7 Aug 21, 2024
31b1981
Update page.tsx
petark7 Nov 11, 2024
b2a7695
Simplify error handling
petark7 Nov 11, 2024
dcc638e
Fix type errors
petark7 Nov 11, 2024
b35d394
Update queryKeys to use defined constants
petark7 Nov 11, 2024
327e31f
Update ReusableTable and TableRowComponents to solve conflicts
petark7 Nov 11, 2024
c75f947
Merge branch 'staging' into feature/tags-with-query
petark7 Nov 11, 2024
6ee47dc
Update eslint-config-next version to newest + modify gitleaks configu…
petark7 Nov 12, 2024
dacfa08
Rename /api to /apis
petark7 Nov 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
title = "Custom Gitleaks Config"
allowlist = { regexes = ["https://registry\\.npmjs\\.org"] }

[allowlist]
files = ["package-lock.json"]
regexes = ["https://registry\\.npmjs\\.org"]
paths = ['''package-lock\.json''']

# Rule to detect API keys
[[rules]]
Expand Down
10 changes: 0 additions & 10 deletions api/axiosInstance.ts

This file was deleted.

File renamed without changes.
26 changes: 26 additions & 0 deletions apis/axiosInstance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import axios from 'axios';

const axiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});

// TODO: REPLACE BEARER LOGIC AS SOON AS AUTHENTICATION IS IMPLEMENTED. THIS IS FOR TESTING PURPOSES
axiosInstance.interceptors.request.use(
(config) => {
if (process.env.NEXT_PUBLIC_BEARER) {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${process.env.NEXT_PUBLIC_BEARER}`;
} else {
throw new Error('No bearer token found');
}
return config;
},
(error) => {
return Promise.reject(error);
}
);

export default axiosInstance;
6 changes: 6 additions & 0 deletions api/endpoints.ts → apis/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ const ENDPOINTS = {
NEWSLETTER: {
SUBSCRIBE: `${API_BASE_URL}/newsletter-subscribers`,
},
TAGS: {
GET_ALL: `${API_BASE_URL}/content/blog-post-tags`,
CREATE: `${API_BASE_URL}/content/blog-post-tags`,
DELETE: `${API_BASE_URL}/content/blog-post-tags`,
EDIT: `${API_BASE_URL}/content/blog-post-tags`,
},
};

export default ENDPOINTS;
34 changes: 34 additions & 0 deletions apis/mutations/tags/useAddNewTag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client';

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'react-toastify';
import { AxiosError } from 'axios';
import { useAxios } from '../../AxiosProvider';
import ENDPOINTS from '../../endpoints';
import QUERY_KEYS from '../../queryKeys';

const useAddNewTag = () => {
const queryClient = useQueryClient();
const axios = useAxios();

interface ErrorResponse {
message?: string;
}

return useMutation({
mutationFn: async ({ tagName }: { tagName: string }) => {
const response = await axios.post(ENDPOINTS.TAGS.CREATE, { name: tagName });
return response.data;
},
onError: (error: AxiosError<ErrorResponse>) => {
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.TAGS.ALL });
toast.error(error?.response?.data?.message || 'Настана грешка при додавање на тагот');
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.TAGS.ALL });
toast.success('Тагот беше успешно додаден.');
},
});
};

export default useAddNewTag;
34 changes: 34 additions & 0 deletions apis/mutations/tags/useDeleteTag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client';

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'react-toastify';
import { AxiosError } from 'axios';
import { useAxios } from '../../AxiosProvider';
import ENDPOINTS from '../../endpoints';
import QUERY_KEYS from '../../queryKeys';

const useDeleteTag = () => {
const queryClient = useQueryClient();
const axios = useAxios();

interface ErrorResponse {
message?: string;
}

return useMutation({
mutationFn: async (tagId: string) => {
const response = await axios.delete(`${ENDPOINTS.TAGS.DELETE}/${tagId}`);
return response.data;
},
onError: (error: AxiosError<ErrorResponse>) => {
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.TAGS.ALL });
toast.error(error?.response?.data?.message || 'Настана грешка при бришење на тагот');
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.TAGS.ALL });
toast.success('Тагот беше успешно избришан.');
},
});
};

export default useDeleteTag;
34 changes: 34 additions & 0 deletions apis/mutations/tags/useEditTag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client';

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'react-toastify';
import { AxiosError } from 'axios';
import { useAxios } from '../../AxiosProvider';
import ENDPOINTS from '../../endpoints';
import QUERY_KEYS from '../../queryKeys';

const useEditTag = () => {
const queryClient = useQueryClient();
const axios = useAxios();

interface ErrorResponse {
message?: string;
}

return useMutation({
mutationFn: async ({ tagId, newName }: { tagId: string; newName: string }) => {
const response = await axios.patch(`${ENDPOINTS.TAGS.EDIT}/${tagId}`, { name: newName });
return response.data;
},
onError: (error: AxiosError<ErrorResponse>) => {
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.TAGS.ALL });
toast.error(error?.response?.data?.message || 'Настана грешка при изменување на тагот');
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.TAGS.ALL });
toast.success('Тагот беше успешно изменет');
},
});
};

export default useEditTag;
File renamed without changes.
22 changes: 22 additions & 0 deletions apis/queries/tags/getTags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useQuery } from '@tanstack/react-query';
import { useAxios } from '../../AxiosProvider';
import ENDPOINTS from '../../endpoints';
import QUERY_KEYS from '../../queryKeys';

const useGetTags = (search?: string) => {
const axios = useAxios();

return useQuery({
queryKey: [...QUERY_KEYS.TAGS.ALL, search],
queryFn: async () => {
const url = search
? `${ENDPOINTS.TAGS.GET_ALL}?search=${encodeURIComponent(search)}`
: ENDPOINTS.TAGS.GET_ALL;

const { data } = await axios.get(url);
return data;
},
});
};

export default useGetTags;
File renamed without changes.
3 changes: 3 additions & 0 deletions api/queryKeys.ts → apis/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ const QUERY_KEYS = {
USERS: {
ALL: ['users'],
},
TAGS: {
ALL: ['blogPostTags'],
},
} as const;

export default QUERY_KEYS;
2 changes: 1 addition & 1 deletion app/action.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use server';

import axios from 'axios';
import axiosInstance from '../api/axiosInstance';
import axiosInstance from '../apis/axiosInstance';
import { BlogCardProps } from '../components/reusable-components/blog-card/BlogCard';

const fetchBlogPosts = async (
Expand Down
2 changes: 1 addition & 1 deletion app/content-panel/blogs/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import styles from './BlogDetailsPage.module.scss';
import BlogDetailsCard from '../../../../components/reusable-components/blogDetails-card/BlogDetailsCard';
import useGetBlogDetails from '../../../../api/queries/blogs/getBlogDetails';
import useGetBlogDetails from '../../../../apis/queries/blogs/getBlogDetails';
import { BlogDetailsData } from '../../../../components/reusable-components/_Types';

const BlogDetailsPage = ({ params }: { params: { id: string } }) => {
Expand Down
126 changes: 122 additions & 4 deletions app/content-panel/tags/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,133 @@
'use client';

import React from 'react';
import React, { useEffect, useState } from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';

import styles from '../../../components/module-components/tags/Tags.module.scss';
import AddTags from '../../../components/module-components/tags/AddTags';
import TagTable from '../../../components/module-components/tags/TagTable';
import TextInput from '../../../components/reusable-components/text-input/TextInput';
import AddTag from '../../../components/module-components/tags/AddTag';
import TagManagementControls from '../../../components/module-components/tags/TagManagementControls';
import useDebounce from '../../../utils/hooks/useDebounce';

import useGetTags from '../../../apis/queries/tags/getTags';
import useAddNewTag from '../../../apis/mutations/tags/useAddNewTag';
import useDeleteTag from '../../../apis/mutations/tags/useDeleteTag';
import useEditTag from '../../../apis/mutations/tags/useEditTag';

interface Tag {
id: string;
name: string;
}

const Tags = () => {
// MUTATIONS
const addNewTagMutation = useAddNewTag();
const deleteTagMutation = useDeleteTag();
const editTagMutation = useEditTag();

// STATE
const [showAddTag, setShowAddTag] = useState(false);
const [tags, setTags] = useState<Tag[]>([]);
const [editingTagId, setEditingTagId] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState('');

const debouncedSearchTerm = useDebounce(searchTerm, 300);
const { data, isLoading } = useGetTags(debouncedSearchTerm);

const validationSchema = Yup.object().shape({
tagName: Yup.string()
.required('Името за тагот е задолжително')
.test('unique', 'Тагот веќе постои', (value) => {
return !tags.some((tag) => tag.name.toLowerCase() === value?.toLowerCase().trim());
}),
});

const handleDelete = async (id: string) => {
await deleteTagMutation.mutateAsync(id);
};

const addTag = async (newTag: string) => {
const trimmedNewTag = newTag.trim();

if (tags.some((tag) => tag.name.toLowerCase() === trimmedNewTag.toLowerCase())) {
return { success: false, error: 'Тагот веќе постои.' };
}

await addNewTagMutation.mutateAsync({ tagName: trimmedNewTag });
return { success: true }; // this is for formik validation purposes
};

const handleSaveChanges = async (tagId: string, newName: string) => {
await editTagMutation.mutateAsync({ tagId, newName: newName.trim() });
setEditingTagId(null);
};

const formik = useFormik({
initialValues: {
tagName: '',
},
validationSchema,
onSubmit: async (values, { resetForm }) => {
await handleSaveChanges(editingTagId!, values.tagName);
resetForm();
},
});

const triggerEdit = (tagId: string) => {
const tagToEdit = tags.find((tag) => tag.id === tagId);

if (tagToEdit) {
setEditingTagId(tagId);
formik.setFieldValue('tagName', tagToEdit.name);
}
};

const handleCancelEdit = () => {
setEditingTagId(null);
formik.resetForm();
};

useEffect(() => {
if (data?.data) {
setTags(data.data);
}
}, [data]);

return (
<div className={styles.container}>
<AddTags />
<TagTable />
<TagManagementControls
onAddClick={() => setShowAddTag(true)}
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
/>

{showAddTag && <AddTag onCancel={() => setShowAddTag(false)} onAdd={addTag} />}

<TagTable
isLoading={isLoading}
tags={tags}
editingTagId={editingTagId}
onEdit={triggerEdit}
onDelete={handleDelete}
onSave={formik.handleSubmit}
onCancel={handleCancelEdit}
renderEditInput={() => (
<div style={{ height: '40px' }}>
<TextInput
placeholder="Внеси име за тагот"
name="tagName"
type="text"
field="tagName"
formik={formik}
isRequired
label=""
inputClass={['tagTableInput']}
/>
</div>
)}
/>
</div>
);
};
Expand Down
2 changes: 1 addition & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +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';
import { AxiosProvider } from '../apis/AxiosProvider';
import 'bootstrap-icons/font/bootstrap-icons.css';
import { EditorProvider } from './context/EditorContext';

Expand Down
2 changes: 1 addition & 1 deletion components/module-components/blog/addNewPost.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import useAddNewPost from '../../../api/mutations/blogs/useAddNewPost';
import useAddNewPost from '../../../apis/mutations/blogs/useAddNewPost';

const AddNewPost = () => {
const addNewPostMutation = useAddNewPost();
Expand Down
4 changes: 2 additions & 2 deletions components/module-components/contact/ContactForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { fullNameRegexValidation, emailRegexValidation } from './regexValidation
import {
useSubmitContactForm,
ContactFormData,
} from '../../../api/mutations/contact/useSubmitContactform';
} from '../../../apis/mutations/contact/useSubmitContactform';

const ContactForm = () => {
const [turnstileToken, setTurnstileToken] = useState<string | null>(null);
Expand Down Expand Up @@ -54,7 +54,7 @@ const ContactForm = () => {
toast.success('Пораката беше успешно испратена!');
resetForm();
setTurnstileToken(null);
} catch (error) {
} catch {
toast.error('Настана грешка при испраќањето на пораката. Пробајте повторно.');
}
},
Expand Down
Loading