diff --git a/client/src/components/Apps/AppTable/AppTable.module.css b/client/src/components/Apps/AppTable/AppTable.module.css index 2ccabb7..fc79b68 100644 --- a/client/src/components/Apps/AppTable/AppTable.module.css +++ b/client/src/components/Apps/AppTable/AppTable.module.css @@ -1,58 +1,8 @@ -.TableContainer { - width: 100%; -} - -.Table { - border-collapse: collapse; - width: 100%; - text-align: left; - font-size: 16px; - color: var(--color-primary); -} - -.Table th, -.Table td { - /* border: 1px solid orange; */ - padding: 10px; -} - -/* Head */ - -.Table th { - --header-radius: 4px; - background-color: var(--color-primary); - color: var(--color-background); -} - -.Table th:first-child { - border-top-left-radius: var(--header-radius); - border-bottom-left-radius: var(--header-radius); -} - -.Table th:last-child { - border-top-right-radius: var(--header-radius); - border-bottom-right-radius: var(--header-radius); -} - -/* Body */ - -.Table td { - /* opacity: 0.5; */ - transition: all 0.2s; -} - -/* .Table td:hover { - opacity: 1; -} */ - -/* Actions */ - .TableActions { display: flex; align-items: center; } - .TableAction { width: 22px; } diff --git a/client/src/components/Apps/AppTable/AppTable.tsx b/client/src/components/Apps/AppTable/AppTable.tsx index ec0a11c..728e079 100644 --- a/client/src/components/Apps/AppTable/AppTable.tsx +++ b/client/src/components/Apps/AppTable/AppTable.tsx @@ -5,6 +5,7 @@ import { pinApp, deleteApp } from '../../../store/actions'; import classes from './AppTable.module.css'; import Icon from '../../UI/Icons/Icon/Icon'; +import Table from '../../UI/Table/Table'; interface ComponentProps { apps: App[]; @@ -29,55 +30,48 @@ const AppTable = (props: ComponentProps): JSX.Element => { } return ( -
- - - - - - - +
NameUrlIconActions
+ {props.apps.map((app: App): JSX.Element => { + return ( + + + + + - - - {props.apps.map((app: App): JSX.Element => { - return ( - - - - - - - ) - })} - -
{app.name}{app.url}{app.icon} +
deleteAppHandler(app)} + onKeyDown={(e) => keyboardActionHandler(e, app, deleteAppHandler)} + tabIndex={0}> + +
+
props.updateAppHandler(app)} + onKeyDown={(e) => keyboardActionHandler(e, app, props.updateAppHandler)} + tabIndex={0}> + +
+
props.pinApp(app)} + onKeyDown={(e) => keyboardActionHandler(e, app, props.pinApp)} + tabIndex={0}> + {app.isPinned + ? + : + } +
+
{app.name}{app.url}{app.icon} -
deleteAppHandler(app)} - onKeyDown={(e) => keyboardActionHandler(e, app, deleteAppHandler)} - tabIndex={0}> - -
-
props.updateAppHandler(app)} - onKeyDown={(e) => keyboardActionHandler(e, app, props.updateAppHandler)} - tabIndex={0}> - -
-
props.pinApp(app)} - onKeyDown={(e) => keyboardActionHandler(e, app, props.pinApp)} - tabIndex={0}> - {app.isPinned - ? - : - } -
-
-
+ ) + })} + ) } diff --git a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css index 81c5cc1..e3a672f 100644 --- a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css +++ b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css @@ -21,7 +21,8 @@ transition: all 0.25s; } -.BookmarkCard a:hover { +.BookmarkCard a:hover, +.BookmarkCard a:focus { text-decoration: underline; padding-left: 10px; } \ No newline at end of file diff --git a/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx b/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx index 728baaa..0d6223e 100644 --- a/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx +++ b/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx @@ -4,12 +4,12 @@ import { connect } from 'react-redux'; import ModalForm from '../../UI/Forms/ModalForm/ModalForm'; import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; import { Category, GlobalState, NewBookmark, NewCategory } from '../../../interfaces'; -import { FormContentType } from '../Bookmarks'; +import { ContentType } from '../Bookmarks'; import { getCategories, addCategory, addBookmark } from '../../../store/actions'; interface ComponentProps { modalHandler: () => void; - contentType: FormContentType; + contentType: ContentType; categories: Category[]; addCategory: (formData: NewCategory) => void; addBookmark: (formData: NewBookmark) => void; @@ -29,10 +29,10 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { const formSubmitHandler = (e: SyntheticEvent): void => { e.preventDefault(); - if (props.contentType === FormContentType.category) { + if (props.contentType === ContentType.category) { props.addCategory(categoryName); setCategoryName({ name: '' }); - } else if (props.contentType === FormContentType.bookmark) { + } else if (props.contentType === ContentType.bookmark) { if (formData.categoryId === -1) { alert('select category'); return; @@ -66,7 +66,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { modalHandler={props.modalHandler} formHandler={formSubmitHandler} > - {props.contentType === FormContentType.category + {props.contentType === ContentType.category ? ( diff --git a/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.module.css b/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.module.css new file mode 100644 index 0000000..fc79b68 --- /dev/null +++ b/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.module.css @@ -0,0 +1,12 @@ +.TableActions { + display: flex; + align-items: center; +} + +.TableAction { + width: 22px; +} + +.TableAction:hover { + cursor: pointer; +} \ No newline at end of file diff --git a/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx b/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx new file mode 100644 index 0000000..388b4d8 --- /dev/null +++ b/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx @@ -0,0 +1,103 @@ +import { ContentType } from '../Bookmarks'; +import classes from './BookmarkTable.module.css'; + +import Table from '../../UI/Table/Table'; +import { Bookmark, Category } from '../../../interfaces'; +import Icon from '../../UI/Icons/Icon/Icon'; + +interface ComponentProps { + contentType: ContentType; + categories: Category[]; +} + +const BookmarkTable = (props: ComponentProps): JSX.Element => { + if (props.contentType === ContentType.category) { + return ( + + {props.categories.map((category: Category) => { + return ( + + + + + ) + })} +
{category.name} +
deleteAppHandler(app)} + // onKeyDown={(e) => keyboardActionHandler(e, app, deleteAppHandler)} + tabIndex={0}> + +
+
props.updateAppHandler(app)} + // onKeyDown={(e) => keyboardActionHandler(e, app, props.updateAppHandler)} + tabIndex={0}> + +
+
props.pinApp(app)} + // onKeyDown={(e) => keyboardActionHandler(e, app, props.pinApp)} + tabIndex={0}> + {category.isPinned + ? + : + } +
+
+ ) + } else { + const bookmarks: {bookmark: Bookmark, categoryName: string}[] = []; + props.categories.forEach((category: Category) => { + category.bookmarks.forEach((bookmark: Bookmark) => { + bookmarks.push({ + bookmark, + categoryName: category.name + }); + }) + }) + + return ( + + {bookmarks.map((bookmark: {bookmark: Bookmark, categoryName: string}) => { + return ( + + + + + + + ) + })} +
{bookmark.bookmark.name}{bookmark.bookmark.url}{bookmark.categoryName} +
deleteAppHandler(app)} + // onKeyDown={(e) => keyboardActionHandler(e, app, deleteAppHandler)} + tabIndex={0}> + +
+
props.updateAppHandler(app)} + // onKeyDown={(e) => keyboardActionHandler(e, app, props.updateAppHandler)} + tabIndex={0}> + +
+
+ ) + } +} + +export default BookmarkTable; \ No newline at end of file diff --git a/client/src/components/Bookmarks/Bookmarks.tsx b/client/src/components/Bookmarks/Bookmarks.tsx index 3c4aadd..ba14b5e 100644 --- a/client/src/components/Bookmarks/Bookmarks.tsx +++ b/client/src/components/Bookmarks/Bookmarks.tsx @@ -14,6 +14,7 @@ import { Category, GlobalState } from '../../interfaces'; import Spinner from '../UI/Spinner/Spinner'; import Modal from '../UI/Modal/Modal'; import BookmarkForm from './BookmarkForm/BookmarkForm'; +import BookmarkTable from './BookmarkTable/BookmarkTable'; interface ComponentProps { loading: boolean; @@ -21,14 +22,16 @@ interface ComponentProps { getCategories: () => void; } -export enum FormContentType { +export enum ContentType { category, bookmark } const Bookmarks = (props: ComponentProps): JSX.Element => { const [modalIsOpen, setModalIsOpen] = useState(false); - const [formContentType, setFormContentType] = useState(FormContentType.category); + const [formContentType, setFormContentType] = useState(ContentType.category); + const [isInEdit, setIsInEdit] = useState(false); + const [tableContentType, setTableContentType] = useState(ContentType.category); useEffect(() => { if (props.categories.length === 0) { @@ -40,24 +43,29 @@ const Bookmarks = (props: ComponentProps): JSX.Element => { setModalIsOpen(!modalIsOpen); } - const addActionHandler = (contentType: FormContentType) => { + const addActionHandler = (contentType: ContentType) => { setFormContentType(contentType); toggleModal(); } + const toggleEdit = (): void => { + setIsInEdit(!isInEdit); + } + + const editActionHandler = (contentType: ContentType) => { + // We're in the edit mode and the same button was clicked - go back to list + if (isInEdit && contentType === tableContentType) { + setIsInEdit(false); + } else { + setIsInEdit(true); + setTableContentType(contentType); + } + } + return ( - {formContentType === FormContentType.category - ? - : - } + { addActionHandler(FormContentType.category)} + handler={() => addActionHandler(ContentType.category)} /> addActionHandler(FormContentType.bookmark)} + handler={() => addActionHandler(ContentType.bookmark)} /> editActionHandler(ContentType.category)} /> editActionHandler(ContentType.bookmark)} /> {props.loading ? - : + : (!isInEdit + ? + : ) } ) diff --git a/client/src/components/UI/Layout/Layout.tsx b/client/src/components/UI/Layout/Layout.tsx index 03a20fc..b7fe50f 100644 --- a/client/src/components/UI/Layout/Layout.tsx +++ b/client/src/components/UI/Layout/Layout.tsx @@ -1,6 +1,10 @@ import classes from './Layout.module.css'; -export const Container = (props: any): JSX.Element => { +interface ComponentProps { + children: JSX.Element | JSX.Element[]; +} + +export const Container = (props: ComponentProps): JSX.Element => { return (
{props.children} diff --git a/client/src/components/UI/Table/Table.module.css b/client/src/components/UI/Table/Table.module.css new file mode 100644 index 0000000..33b712b --- /dev/null +++ b/client/src/components/UI/Table/Table.module.css @@ -0,0 +1,41 @@ +.TableContainer { + width: 100%; +} + +.Table { + border-collapse: collapse; + width: 100%; + text-align: left; + font-size: 16px; + color: var(--color-primary); +} + +.Table th, +.Table td { + padding: 10px; +} + +/* Head */ + +.Table th { + --header-radius: 4px; + background-color: var(--color-primary); + color: var(--color-background); +} + +.Table th:first-child { + border-top-left-radius: var(--header-radius); + border-bottom-left-radius: var(--header-radius); +} + +.Table th:last-child { + border-top-right-radius: var(--header-radius); + border-bottom-right-radius: var(--header-radius); +} + +/* Body */ + +.Table td { + /* opacity: 0.5; */ + transition: all 0.2s; +} \ No newline at end of file diff --git a/client/src/components/UI/Table/Table.tsx b/client/src/components/UI/Table/Table.tsx new file mode 100644 index 0000000..882ebfb --- /dev/null +++ b/client/src/components/UI/Table/Table.tsx @@ -0,0 +1,25 @@ +import classes from './Table.module.css'; + +interface ComponentProps { + children: JSX.Element | JSX.Element[]; + headers: string[]; +} + +const Table = (props: ComponentProps): JSX.Element => { + return ( +
+ + + + {props.headers.map((header: string, index: number): JSX.Element => ())} + + + + {props.children} + +
{header}
+
+ ) +} + +export default Table; \ No newline at end of file diff --git a/client/src/index.css b/client/src/index.css index a9c6013..4f64f2a 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -2,7 +2,6 @@ margin: 0; padding: 0; box-sizing: border-box; - /* transition: all 0.3s; */ user-select: none; } @@ -14,13 +13,6 @@ body { background-color: var(--color-background); transition: background-color 0.3s; - /* font weights - light 300 - regular 400 - semi-bold 600 - bold 700 - extra-bold 800 - */ font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, Roboto, sans-serif; font-size: 14px; } @@ -28,11 +20,4 @@ body { a { color: var(--color-primary); text-decoration: none; - /* opacity: 0.75; */ -} - -/* 320px — 480px: Mobile devices. -481px — 768px: iPads, Tablets. -769px — 1024px: Small screens, laptops. -1025px — 1200px: Desktops, large screens. -1201px and more — Extra large screens, TV. */ \ No newline at end of file +} \ No newline at end of file diff --git a/client/src/store/actions/bookmark.ts b/client/src/store/actions/bookmark.ts index b1bd6f1..7c121dc 100644 --- a/client/src/store/actions/bookmark.ts +++ b/client/src/store/actions/bookmark.ts @@ -2,6 +2,7 @@ import axios from 'axios'; import { Dispatch } from 'redux'; import { ActionTypes } from './actionTypes'; import { Category, ApiResponse, NewCategory, Bookmark, NewBookmark } from '../../interfaces'; +import { CreateNotificationAction } from './notification'; export interface GetCategoriesAction { type: ActionTypes.getCategories | ActionTypes.getCategoriesSuccess | ActionTypes.getCategoriesError; @@ -35,6 +36,14 @@ export const addCategory = (formData: NewCategory) => async (dispatch: Dispatch) try { const res = await axios.post>('/api/categories', formData); + dispatch({ + type: ActionTypes.createNotification, + payload: { + title: 'Success', + message: `Category ${formData.name} created` + } + }) + dispatch({ type: ActionTypes.addCategory, payload: res.data.data @@ -53,6 +62,14 @@ export const addBookmark = (formData: NewBookmark) => async (dispatch: Dispatch) try { const res = await axios.post>('/api/bookmarks', formData); + dispatch({ + type: ActionTypes.createNotification, + payload: { + title: 'Success', + message: `Bookmark ${formData.name} created` + } + }) + dispatch({ type: ActionTypes.addBookmark, payload: res.data.data