Files
YABA/yaba-web/src/views/bookmarksListView.jsx
Carl Tibule 067b32826d Redirected Home page to Bookmarks list page when authenticated and login page when not
Also modified to have 200 returned when only tags are changed and not the bookmark itself
2023-03-05 18:57:46 -06:00

349 lines
17 KiB
JavaScript

import React, { useEffect, useReducer, useState } from "react";
import { Alert, Col, Container, Row, Button, Modal } from "../components/external";
import { Bookmark } from "../components";
import { useAuth0 } from "@auth0/auth0-react";
import { deleteBookmarks, getAllBookmarks, hideBookmarks } from "../api";
import { SplashScreen, SearchForm, Tag } from "../components";
import { getTagGroups, isSubset, containsSubstring, flattenTagArrays } from "../utils";
export function BookmarksListView(props) {
const { getAccessTokenSilently } = useAuth0();
const [searchString, setSearchString] = useState("");
const initialSplashScreenState = { show: false, message: null };
const splashScreenReducer = (state = initialSplashScreenState, action) => {
switch(action.type) {
case "SHOW_SPLASH_SCREEN":
return Object.assign({}, state, {
show: true,
message: action.payload.message
});
case "HIDE_SPLASH_SCREEN":
default:
return Object.assign({}, state, {
show: false,
message: null
});
}
};
const [splashScreenState, dispatchSplashScreenState] = useReducer(splashScreenReducer, initialSplashScreenState);
const alertMessageInitialState = { show: false, type: "primary", message: null};
const alertReducer = (state = alertMessageInitialState, action) => {
switch(action.type) {
case "SHOW_ALERT":
return Object.assign({}, state, {
show: action.payload.show,
type: action.payload.alertType,
message: action.payload.message
});
case "HIDE_ALERT":
default:
return Object.assign({}, state, {
show: false
});
}
};
const [alertMessageState, dispatchAlertMessageState] = useReducer(alertReducer, alertMessageInitialState);
const tagsInitialState = [];
const tagsReducer = (state = tagsInitialState, action) => {
switch(action.type) {
case "SET":
return action.payload.tags.map(x => ({...x, isSelected: false, isDisplayed: true }));
case "ADD_SELECTED":
case "REMOVE_SELECTED":
const modifiedTags = [...state];
const selectedTagIndex = modifiedTags.findIndex((x) => x.id === action.payload.selectedTagId);
modifiedTags[selectedTagIndex].isSelected = action.type === "ADD_SELECTED";
return modifiedTags;
default:
return state;
}
}
const [tagsState, dispatchTagsState] = useReducer(tagsReducer, tagsInitialState);
const getSelectedTags = () => tagsState.filter((x) => x.isSelected);
const getNotSelectedTags = () => tagsState.filter((x) => !x.isSelected);
const bookmarksInitialState = [];
const bookmarksReducer = (state = bookmarksInitialState, action) => {
switch(action.type) {
case "SET":
return action.payload.bookmarks.map(x => ({...x, isSelected: false, isDisplayed: true}));
case "ADD_SELECTED":
case "REMOVE_SELECTED":
const modifiedBookmarks = [...state];
const modifiedBookmarkIndex = modifiedBookmarks.findIndex((x) => x.id === action.payload.selectedBookmarkId);
modifiedBookmarks[modifiedBookmarkIndex].isSelected = action.type === "ADD_SELECTED";
return modifiedBookmarks;
case "DELETE_SELECTED":
case "HIDE_SELECTED":
return state.filter((x) => !action.payload.selectedBookmarkIds.includes(x.id));
case "SELECT_ALL":
return state.map(x => ({...x, isSelected: x.isDisplayed}));
case "UNSELECT_ALL":
return state.map(x => ({...x, isSelected: false}));
case "DISPLAY_ALL":
return state.map(x => ({...x, isDisplayed: true}));
case "SEARCH":
return state.map(x => ({...x, isDisplayed: (getSelectedTags().length <= 0 || isSubset(x.tags.map(x => x.id), getSelectedTags().map(x => x.id)))
&& (!searchString || containsSubstring(x, searchString))}));
default:
return state;
}
};
const [bookmarksState, dispatchBookmarksState] = useReducer(bookmarksReducer, []);
const onBookmarkSelected = (isBookmarkSelected, bookmark) => dispatchBookmarksState({type: isBookmarkSelected ? "ADD_SELECTED" : "REMOVE_SELECTED", payload: {selectedBookmarkId: bookmark.id}});
const getDisplayedBookmarksCount = () => bookmarksState.filter(x => x.isDisplayed).length;
const getSelectedBookmarksCount = () => bookmarksState.filter(x => x.isSelected && x.isDisplayed).length;
const getSelectedBookmarks = () => bookmarksState.filter((x) => x.isSelected && x.isDisplayed);
const getAreAllBookmarksSelected = () => bookmarksState.filter(x => x.isDisplayed).every(x => x.isSelected);
const getFilteredBookmarks = () => bookmarksState.filter(x => x.isDisplayed);
const onDeleteSelectedBookmarks = async (ids) => {
if(ids.length <= 0) {
dispatchAlertMessageState({type: "SHOW_ALERT", payload: {show: true, alertType: "warning", message: "No bookmark(s) to delete"}});
} else {
dispatchSplashScreenState({type: "SHOW_SPLASH_SCREEN", payload: {message: "Deleting bookmark(s)"}});
const accessToken = await getAccessTokenSilently();
const { data, error } = await deleteBookmarks(accessToken, ids);
if(error) {
dispatchAlertMessageState({type: "SHOW_ALERT", payload: {show: true, alertType: "danger", message: `An error has occurred while deleting bookmark(s): ${error.message}`}})
} else {
dispatchBookmarksState({type: "DELETE_SELECTED", payload: {selectedBookmarkIds: ids}});
}
dispatchSplashScreenState({type: "HIDE_SPLASH_SCREEN", payload: {message: null }});
}
};
const initialConfirmModalState = {show: false, title: null, message: null, selectedBookmarkId: null};
const confirmModalReducer = (state = initialConfirmModalState, action) => {
switch(action.type) {
case "SHOW_FOR_MULTIPLE_BOOKMARK_DELETE":
return {
...state,
show: action.payload.show,
title: action.payload.title,
message: action.payload.message
};
case "SHOW_FOR_BOOKMARK_DELETE":
return {
...state,
show: action.payload.show,
title: action.payload.title,
message: action.payload.message,
selectedBookmarkId: action.payload.selectedBookmarkId
};
case "HIDE":
default:
return initialConfirmModalState;
}
};
const [confirmModalState, dispatchConfirmModalState] = useReducer(confirmModalReducer, initialConfirmModalState);
const handleDeleteMultipleBookmarks = () => dispatchConfirmModalState({"type": "SHOW_FOR_MULTIPLE_BOOKMARK_DELETE", payload: {show: true, title: "Delete selected bookmarks", "message": "Are you sure you want to delete selected bookmarks?"}});
const handleDeleteBookmark = (bookmarkId) => dispatchConfirmModalState({type: "SHOW_FOR_BOOKMARK_DELETE", payload: { show: true, title: "Delete bookmark", message: "Are you sure you want to delete this bookmark?", selectedBookmarkId: bookmarkId}});
const onHandleCloseConfirmModal = async () => {
const bookmarkIdsToDelete = confirmModalState.selectedBookmarkId ? [confirmModalState.selectedBookmarkId] : getSelectedBookmarks().map((x) => x.id);
dispatchConfirmModalState({type: "HIDE"});
await onDeleteSelectedBookmarks(bookmarkIdsToDelete);
};
const handleHideBookmarks = async(ids) => {
if(ids.length <= 0) {
dispatchAlertMessageState({type: "SHOW_ALERT", payload: {show: true, alertType: "warning", message: "No bookmark(s) to hide"}});
} else {
dispatchSplashScreenState({type: "SHOW_SPLASH_SCREEN", payload: {message: "Hiding bookmark(s)"}});
const accessToken = await getAccessTokenSilently();
const { data, error } = await hideBookmarks(accessToken, ids);
if(error) {
dispatchAlertMessageState({type: "SHOW_ALERT", payload: {show: true, alertType: "danger", message: `An error has occurred while hiding bookmark(s): ${error.message}`}})
} else {
dispatchBookmarksState({type: "HIDE_SELECTED", payload: {selectedBookmarkIds: ids}});
}
dispatchSplashScreenState({type: "HIDE_SPLASH_SCREEN", payload: {message: null }});
}
};
const onTagSelected = (isTagSelected, tag) => {
dispatchTagsState({type: isTagSelected ? "ADD_SELECTED" : "REMOVE_SELECTED", payload: {selectedTagId: tag.id}});
dispatchBookmarksState({type: "SEARCH"});
};
const handleSearch = (e) => {
e.preventDefault();
setSearchString(e.target[0].value);
dispatchBookmarksState({type: "SEARCH"});
};
const handleSearchStringChange = (e) => {
setSearchString(e.target.value);
dispatchBookmarksState({type: "SEARCH"});
};
useEffect(() => {
dispatchSplashScreenState({type: "SHOW_SPLASH_SCREEN", payload: {message: "Retrieving Bookmarks..."}});
const fetchBookmarks = async() => {
const accessToken = await getAccessTokenSilently();
const { data, error } = await getAllBookmarks(accessToken, props.showHidden);
if(error) {
dispatchAlertMessageState({type: "SHOW_ALERT", payload: {show: true, alertType: "danger", "message": `Error fetching bookmarks: ${error.message}`}});
} else {
dispatchBookmarksState({type: "SET", payload: {bookmarks: data}});
dispatchTagsState({type: "SET", payload: {tags: flattenTagArrays(data.map(x => x.tags))}});
}
dispatchSplashScreenState({type: "HIDE_SPLASH_SCREEN", payload: {message: null}});
}
fetchBookmarks();
}, []);
return(
<React.Fragment>
{splashScreenState.show && (
<SplashScreen
message={splashScreenState.message}
/>
)}
<div style={{flexGrow: 1}}>
<Container>
{alertMessageState.show && (
<Alert
variant={alertMessageState.type}
onClose={() => dispatchAlertMessageState({type: "CLOSE"})}
dismissible
>
{alertMessageState.message}
</Alert>
)}
<Modal
show={confirmModalState.show}
onHide={() => {dispatchConfirmModalState({type: "HIDE"})}}
keyboard={false}
>
<Modal.Header closeButton>
<Modal.Title>{confirmModalState.title}</Modal.Title>
</Modal.Header>
<Modal.Body>{confirmModalState.message}</Modal.Body>
<Modal.Footer>
<Button
variant="secondary"
onClick={() => {dispatchConfirmModalState({type: "HIDE"})}}
>
Cancel
</Button>
<Button
variant="danger"
onClick={onHandleCloseConfirmModal}
>
Delete
</Button>
</Modal.Footer>
</Modal>
<Row>
<Col xs="9">
<Row>
<Col xs="7">
<span className="fs-4">{props.showHidden ? "Hidden Bookmarks" : "Bookmarks"}</span>
</Col>
<Col xs="5" className="d-flex justify-content-end">
<SearchForm
onHandleSearch={handleSearch}
onSearchStringChange={handleSearchStringChange}
searchString={searchString}
/>
</Col>
</Row>
<hr className="mt-1" />
</Col>
<Col xs="3">
<span className="fs-4">Tags</span>
<hr className="mt-1" />
</Col>
</Row>
{
(bookmarksState.length > 0 || getSelectedBookmarksCount() > 0) && <Row>
<Col xs="2" className="mb-3">
{
bookmarksState.length > 0 &&
<Button variant="primary" onClick={() => dispatchBookmarksState({type: getAreAllBookmarksSelected() ? "UNSELECT_ALL" : "SELECT_ALL"})}>{getAreAllBookmarksSelected() ? "Unselect All" : "Select All" }</Button>
}
</Col>
<Col xs="7" className="mb-3">
{
getSelectedBookmarksCount() > 0 &&
<div className="d-flex justify-content-end align-items-center">
<span className="fs-5 me-2"> {getSelectedBookmarksCount()} selected</span>
<Button variant="primary" className="me-2" onClick={() => handleHideBookmarks(getSelectedBookmarks().map(x => x.id))}>{props.showHidden ? "Unhide" : "Hide"}</Button>
<Button
variant="danger"
onClick={handleDeleteMultipleBookmarks}
>
Delete
</Button>
</div>
}
</Col>
<Col xs="3" className="mb-3">
{
getSelectedTags().length > 0 && (
getSelectedTags().map((tag) => {
return <Button key={tag.id} variant="primary" style={{textDecoration: "none", cursor: "pointer" }} className="badge rounded-pill text-bg-primary me-2" onClick={() => onTagSelected(false, tag)}>#{tag.name} | x</Button>
})
)
}
</Col>
</Row>
}
<Row>
<Col xs="9">
{ getFilteredBookmarks().length <= 0 && <div className="fs-3">No Bookmarks found</div> }
{
getFilteredBookmarks().map((bookmark) => {
return <Bookmark
key={bookmark.id}
bookmark={bookmark}
onBookmarkSelected={(selected) => onBookmarkSelected(selected, bookmark)}
onDeleteClicked={() => handleDeleteBookmark(bookmark.id)}
onHideClicked={() => handleHideBookmarks([bookmark.id])}
searchString={searchString}
/>
})
}
</Col>
<Col xs="3">
{ getTagGroups(getNotSelectedTags()).length <= 0 && <div className="fs-3">No Tags found</div> }
{
getTagGroups(getNotSelectedTags()).map((group) => {
return <div key={group.name} className="mb-3">
<span key={group.name} className="text-primary fw-bold">{group.name}</span>
<br />
{
group.tags.map((tag) => <Tag key={tag.id} tag={tag} onClick={() => onTagSelected(true, tag)} />)
}
</div>
})
}
</Col>
</Row>
</Container>
</div>
</React.Fragment>
);
}