diff --git a/components/admin/users/AdminUsersSplitSearch.tsx b/components/admin/users/AdminUsersSplitSearch.tsx new file mode 100644 index 000000000..7e1c05fda --- /dev/null +++ b/components/admin/users/AdminUsersSplitSearch.tsx @@ -0,0 +1,53 @@ +import React from 'react' + +/* + convert string to array, separated by and including searchTerm + Ex: + Inputs: searchTerm='bon', str = 'bonJourbon' + Output: [bon, Jour, bon] +*/ +const splitWithSearchTerm = (str: string, searchTerm: string) => { + const lowerCaseSearchTerm = searchTerm.toLowerCase() + const splitArr = str.toLowerCase().split(lowerCaseSearchTerm) + // tracker is used for `str.substr` to extract characters from the original string at the correct places + let tracker = 0 + + const res = splitArr.reduce( + (acc: string[], word: string, splitArrIndex: number) => { + // converts words back into their original capitalization + const originalWord = str.substr(tracker, word.length) + acc.push(originalWord) + tracker += word.length + + // used to prevent over-adding of searchTerm back into array + if (splitArrIndex === splitArr.length - 1) return acc + acc.push(searchTerm) + tracker += searchTerm.length + return acc + }, + [] + ) + + return res +} + +export const AdminUsersSplitSearch = (str: string, searchTerm: string) => { + // make all lowercase now to ensure both lower and uppercase characters can be searched for + const lowerCaseSearchTerm = searchTerm.toLowerCase() + + // convert string to array with searchTerm included + const splitArr = splitWithSearchTerm(str, searchTerm) + + // highlight search Term + const res = splitArr.map((word: string, key: number) => { + const bgColor = + word.toLowerCase() === lowerCaseSearchTerm ? 'rgb(84, 64, 216, .25)' : '' + return ( + + {word} + + ) + }) + + return res +} diff --git a/components/admin/users/AdminUsersTable.tsx b/components/admin/users/AdminUsersTable.tsx index 85a1d8776..5f6fe38e9 100644 --- a/components/admin/users/AdminUsersTable.tsx +++ b/components/admin/users/AdminUsersTable.tsx @@ -1,12 +1,14 @@ import React from 'react' import { useMutation } from '@apollo/react-hooks' -import _ from 'lodash' import { Button } from '../../theme/Button' import changeAdminRights from '../../../graphql/queries/changeAdminRights' import { User } from '../../../graphql' +import { AdminUsersSplitSearch } from './AdminUsersSplitSearch' +import { filter } from '../../../pages/admin/users' type UsersListProps = { users: User[] + searchOption: filter setUsers: React.Dispatch> } @@ -14,91 +16,91 @@ type RowDataProps = { user: any users: User[] setUsers: React.Dispatch> - index: number + usersIndex: number + option: string + searchTerm: string } type AdminOptionProps = { isAdmin: boolean users: User[] setUsers: React.Dispatch> - index: number + usersIndex: number id: string | null | undefined } type UsersTableProps = { users: User[] + searchOption: filter setUsers: React.Dispatch> } -export const headerValues = ['ID', 'Username', 'Name', 'Email', 'Admin'] +export const headerTitles = ['ID', 'Username', 'Name', 'Email', 'Admin'] export const userProperties = ['id', 'username', 'name', 'email', 'isAdmin'] -const TableHeaders: React.FC = () => { - const head = headerValues.map((property, key) => ( - - {property} - - )) - - return ( - - {head} - - ) -} - const AdminOption: React.FC = ({ isAdmin, setUsers, - index, + usersIndex, users, id }) => { const [changeRights] = useMutation(changeAdminRights) + const newAdminRights = isAdmin ? 'false' : 'true' + const mutationVariable = { variables: { id: parseInt(id + ''), - status: isAdmin ? 'false' : 'true' + status: newAdminRights } } const changeButton = async () => { await changeRights(mutationVariable) - const newUsers = users && [...users] - if (newUsers) { - newUsers[index].isAdmin = isAdmin ? 'false' : 'true' - } + const newUsers = [...users] + newUsers[usersIndex].isAdmin = newAdminRights setUsers(newUsers) } return ( ) } -const RowData: React.FC = ({ user, users, setUsers, index }) => { +const RowData: React.FC = ({ + user, + users, + setUsers, + usersIndex, + searchTerm, + option +}) => { + option = option.toLowerCase() + const data = userProperties.map((property: string, key: number) => { let value = user[property] - const displayOption = - property !== 'isAdmin' ? ( - value - ) : ( + if (searchTerm && property === option) { + value = AdminUsersSplitSearch(value, searchTerm) + } + + if (property === 'isAdmin') + value = ( ) return ( - - {displayOption} + + {value} ) }) @@ -106,30 +108,65 @@ const RowData: React.FC = ({ user, users, setUsers, index }) => { return <>{data} } -const UsersList: React.FC = ({ users, setUsers }) => { - const list = - users && - users.reduce((acc: any[], user: any, usersIndex: number) => { - acc.push( - - - - ) +const UsersList: React.FC = ({ + users, + setUsers, + searchOption +}) => { + const { searchTerm, admin } = searchOption + let { option } = searchOption + option = option.toLowerCase() + + // usersIndex is needed for the RowData component to function properly + const usersListIndex: any = [] - return acc - }, []) + // remove all users from list that are not going to be rendered + const list: User[] = users.filter((user: any, usersIndex: number) => { + let bool = true - return {list} + if (searchTerm) bool = (user[option] || '').includes(searchTerm) + if (bool && admin === 'Non-Admins') bool = user.isAdmin === 'false' + if (bool && admin === 'Admins') bool = user.isAdmin === 'true' + + bool && usersListIndex.push(usersIndex) + return bool + }) + + const usersList = list.map((user: User, key: number) => { + return ( + + + + ) + }) + + return {usersList} } -export const UsersTable: React.FC = ({ users, setUsers }) => ( - - - -
-) +export const UsersTable: React.FC = ({ + users, + setUsers, + searchOption +}) => { + const head = headerTitles.map((title, key) => {title}) + + return ( + + + {head} + + +
+ ) +} diff --git a/pages/admin/users.tsx b/pages/admin/users.tsx index a27159973..1454a921d 100644 --- a/pages/admin/users.tsx +++ b/pages/admin/users.tsx @@ -1,30 +1,93 @@ import React, { useState } from 'react' import allUsers from '../../graphql/queries/allUsers' -import { UsersTable } from '../../components/admin/users/AdminUsersTable' +import { + UsersTable, + headerTitles +} from '../../components/admin/users/AdminUsersTable' import { User, withGetApp, GetAppProps } from '../../graphql/index' import { AdminLayout } from '../../components/admin/AdminLayout' import withQueryLoader, { QueryDataProps } from '../../containers/withQueryLoader' - -const titleStyle: React.CSSProperties | undefined = { - fontSize: '6rem', - textAlign: 'center', - fontWeight: 'bold' -} +import { FilterButtons } from '../../components/FilterButtons' +import _ from 'lodash' type AllUsersData = { allUsers: User[] } +export type filter = { + option: string + admin: string + searchTerm: string +} + +const initialSearchOptions: filter = { + option: 'Username', + admin: 'None', + searchTerm: '' +} + +const adminFilters = ['Admins', 'Non-Admins', 'None'] + +const searchHeaders = [...headerTitles] + +searchHeaders.length = 4 + const AdminUsers: React.FC> = ({ queryData }) => { + const [searchOption, setSearchOption] = useState(initialSearchOptions) const [users, setUsers] = useState(queryData.allUsers) + /* + The reason debounce is used here is to prevent page rerenders on every keystroke. + If there is a rerender on every keystroke, the CPU consumption is high and + creates a perception of the page being slow and sluggish. + Therefore, we only rerender when the user stops typing. + */ + const run = _.debounce(setSearchOption, 500) + + const handleChange = (e: React.ChangeEvent) => { + const newSearchOption = { + ...searchOption, + searchTerm: e.target.value + } + run(newSearchOption) + } + + const changeFilter = (str: string, type: string) => { + const newSearchOption: any = { ...searchOption } + newSearchOption[type] = str + setSearchOption(newSearchOption) + } + return (
- +

Users - - +

+
+ changeFilter(value, 'option')} + currentOption={searchOption.option} + > + Search By: + +
+ +
+ changeFilter(value, 'admin')} + currentOption={searchOption.admin} + > + Filter By: + +
+
) }