Fresh Support

Fresh Support

While Fresh Support is a clone of Freshdesk, I did not follow any tutorials to build it. I created everything from scratch, adhering to best practices, with most components built entirely on my own. In this project, custom hooks proved to be incredibly useful. React context enhanced the page’s responsiveness to user interaction, and algorithms played a fundamental role in creating components. The backend is straightforward, yet beautifully designed.


Request custom hook

This hook allows us to send requests to the backend regardless of the type of method or parameters. We can make a request using two different approaches. One way is by using axios directly. While this works fine, it lacks some of the benefits that React can offer.


The other method involves using a custom hook, which still utilizes axios under the hood, but in this case we can retrieve information such as errors or loading status. We can pass the method, URL, and data if necessary. The implementation looks like this:

Using custom hook
const { data, error, loading, finished, sendRequest, setError } = useRequest();
const response = await sendRequest({
method: "POST",
url: "/api/tickets",
data: {
title: "PHONES ARE DOWN",
description:
"Phones are down and the office cannot help its customers. Please help ASAP",
time: Date.now(),
},
});

While getting error information or loading status is really cool, what really felt awesome was the ability to log out the user across all open windows simultaneously. For instance, if the user had two windows open and logged out in one, I wanted to ensure that the other window would also log out and redirect to the sign-in page.


This feature was possible thanks to the custom hook. Here is the code:

const sendRequest = async ({ url, method, data, params }) => {
setError(null);
setLoading(true);
try {
const response = await axios({
url: url,
method: method,
data: data,
params: params,
});
setData(response.data);
setLoading(false);
setFinished(true);
return response;
} catch (err) {
console.log(err);
if (err.response.status === 401) {
dispatch({ type: "LOGOUT" });
}
setError(err.response.data.error);
setLoading(false);
setFinished(true);
}
};

Inside the catch block, if the response status is 401 (Unauthorized), the dispatch function logs out the agent. If there is no agent in the context, it redirects the user to the sign-in page.


Components

Single Select & Multi Select

Queries

Queries can be performed in both single select and multi select components. Using React state and a simple algorithm the options can be displayed for selection.

Query search (Single Select)
function makeQuery(e) {
setQueriedOptions(() => {
const newArray = options.filter((element) => {
const rx = new RegExp(e.target.value, "i");
const variable = element.name.search(rx) >= 0;
return variable;
});
return newArray;
});
}

A new array (newArray) is generated by filtering the options array. Using the target’s value (e.target.value) we create a regular expression with the flag “i” in the RegExp constructor that makes the search case-insensitive. The element.name.search(rx) >= 0 conditions checks if the search query matches partially or completely the name property of each option. Once we have the newArray array we return so it can be assigned to the state.

Selecting options

Selecting an options is fairly basic in the single select.

Selecting an option (Single Select)
function selectOption(e) {
const { id, textContent } = e.target;
setOptionSelected({ name: textContent, _id: id });
edit.current.value = e.target.textContent;
}

We just return the id and text content of the target.


For the multi select options it gets a little bit more complicated.

Selecting an option (Multi Select)
function selectOption(e) {
const { id, textContent } = e.target;
edit.current.textContent = "";
setQueriedOptions([...options]);
setOptionsSelected((prevoptions) => {
if (prevoptions.some((option) => option._id === id)) {
return [...prevoptions];
} else {
return [...prevoptions, { _id: id, name: textContent }];
}
});
}
  1. Editable div is cleared: edit.current.textContent = "";
  2. Queried options state is reset: setQueriedOptions([…options]);
  3. Inside the setState for the options selected, .some() is used to check if if at least one of the options id coincides with the selected option’s id.
  4. If the condition is true, all the previous options are returned, since the selected option is already included.
  5. If the condition is false, the previous options are returned, plus the selected option.

To-Do

To-dos are ordered cronologically, and they are reorganized after being checked. Regardless of the reordering, they are maintained in chronological order. Unchecked tasks are listed at the top, followed by checked ones.

Reordering algorithm
function reOrderTodos(todos) {
const checkedTodos = [];
const uncheckedTodos = [];
todos.slice().forEach((element) => {
if (element.checked === false) {
uncheckedTodos.splice(0, 0, element);
} else if (element.checked === true) {
checkedTodos.splice(0, 0, element);
}
});
setTodos(uncheckedTodos.concat(checkedTodos));
}

The function reOrderTodos takes an array of todos as input. It initializes two empty arrays, checkedTodos and uncheckedTodos, which will be used to store checked and unchecked todos, respectively. To avoid modifying the original array, a shallow copy is created using the slice method.


For each element in the copied array, the function checks whether it is marked as checked or unchecked. Depending on the result, the element is inserted at the beginning of the corresponding array (checkedTodos or uncheckedTodos) using the splice method.


After processing all todos, the function concatenates uncheckedTodos with checkedTodos, in a way that unchecked tasks appear first. Finally, the state is updated using the setTodos function.


React Context

As mentioned earlier, React Context played a crucial role in dinamically updating the information presented to the user. In the software, I utilized a total of three React Contexts: one for authentication, another for tickets, and a third for filters.


The authentication context stores user related information such as names, assigned tickets, and to-dos. This information is across the application, and it is important in the creation of tickets and activities.


The ticket context is used only in the single ticket page. I used a context because there are a few components that needed to access or update the ticket information. Using this context not only allowed to access and update the info with ease, but also allowed the page to feel more alive. This dynamism is evident as all components are displayed and updated promptly in response to user interactions, enhancing the overall user experience.


The Filters context is used to pass all the options to single and multi selects that are used in the single ticket page and in all tickets page.


Here is the use effect for the filters context.

Use Effect hook (FiltersContext.js)
useEffect(() => {
const fetchFilters = async () => {
const response = await sendRequest({
url: "api/filters/get-filters",
method: "GET",
});
dispatch({ type: "UPDATE", payload: response.data });
setLoaded(true);
};
const filters = JSON.parse(localStorage.getItem("filters"));
if (filters) {
dispatch({ type: "UPDATE-SELECTED", payload: filters });
}
try {
fetchFilters();
} catch (error) {
console.log(error);
}
}, []);

Initially, this context fetches all the filters from the backend and then updates its own state. Additionally, selected filters are stored in local storage. In the event that any filters have already been stored, the context is updated with them.


Code and Demo

If you are interested, you can check the demo or the code