diff --git a/client/src/App.tsx b/client/src/App.tsx index c304321a2b4c9cfad810cfae12c1b2733048afb4..6a1324101f60d53d3bb271e106e06e17523b07ee 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -10,6 +10,7 @@ import classes from './App.module.css'; import Home from './components/Home/Home'; import Apps from './components/Apps/Apps'; import Settings from './components/Settings/Settings'; +import Bookmarks from './components/Bookmarks/Bookmarks'; if (localStorage.theme) { store.dispatch(setTheme(localStorage.theme)); @@ -23,6 +24,7 @@ const App = (): JSX.Element => { + diff --git a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css new file mode 100644 index 0000000000000000000000000000000000000000..81c5cc1ae6aaf615f4a619102ef513eefd306254 --- /dev/null +++ b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css @@ -0,0 +1,27 @@ +.BookmarkCard { + /* margin-top: 10px; */ + margin-bottom: 30px; +} + +.BookmarkCard h3 { + color: var(--color-accent); + margin-bottom: 10px; + font-size: 16px; + font-weight: 400; + text-transform: uppercase; +} + +.Bookmarks { + display: flex; + flex-direction: column; +} + +.Bookmarks a { + line-height: 2; + transition: all 0.25s; +} + +.BookmarkCard a:hover { + text-decoration: underline; + padding-left: 10px; +} \ No newline at end of file diff --git a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6757f5420428fe79cac0ccbcbde59062b0716b27 --- /dev/null +++ b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx @@ -0,0 +1,26 @@ +import { Bookmark, Category } from '../../../interfaces'; +import classes from './BookmarkCard.module.css'; + +interface ComponentProps { + category: Category; +} + +const BookmarkCard = (props: ComponentProps): JSX.Element => { + return ( +
+

{props.category.name}

+
+ {props.category.bookmarks.map((bookmark: Bookmark) => ( + + {bookmark.name} + + ))} +
+
+ ) +} + +export default BookmarkCard; \ No newline at end of file diff --git a/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.module.css b/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.module.css new file mode 100644 index 0000000000000000000000000000000000000000..6651f65350d7ed3d2d1bfb2d9924d1f1cc5e01dc --- /dev/null +++ b/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.module.css @@ -0,0 +1,22 @@ +.BookmarkGrid { + display: grid; + grid-template-columns: repeat(1, 1fr); +} + +@media (min-width: 430px) { + .BookmarkGrid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (min-width: 670px) { + .BookmarkGrid { + grid-template-columns: repeat(3, 1fr); + } +} + +@media (min-width: 900px) { + .BookmarkGrid { + grid-template-columns: repeat(4, 1fr); + } +} \ No newline at end of file diff --git a/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx b/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2cee06689b4dd03794899d7aa2698f070592b815 --- /dev/null +++ b/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx @@ -0,0 +1,20 @@ +import classes from './BookmarkGrid.module.css'; + +import { Bookmark, Category } from '../../../interfaces'; + +import BookmarkCard from '../BookmarkCard/BookmarkCard'; + +interface ComponentProps { + categories: Category[]; +} + +const BookmarkGrid = (props: ComponentProps): JSX.Element => { + return ( +
+ {props.categories.map((category: Category): JSX.Element => )} +
+ ) + +} + +export default BookmarkGrid; \ No newline at end of file diff --git a/client/src/components/Bookmarks/Bookmarks.module.css b/client/src/components/Bookmarks/Bookmarks.module.css new file mode 100644 index 0000000000000000000000000000000000000000..09810e1f23677e9476943773be24b3d621d48ad3 --- /dev/null +++ b/client/src/components/Bookmarks/Bookmarks.module.css @@ -0,0 +1,4 @@ +.ActionsContainer { + display: flex; + align-items: center; +} \ No newline at end of file diff --git a/client/src/components/Bookmarks/Bookmarks.tsx b/client/src/components/Bookmarks/Bookmarks.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a4e830abfa7c986db1a183e2c72dc5da0a3f8f41 --- /dev/null +++ b/client/src/components/Bookmarks/Bookmarks.tsx @@ -0,0 +1,62 @@ +import { useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { connect } from 'react-redux'; +import { getCategories } from '../../store/actions'; + +import classes from './Bookmarks.module.css'; + +import { Container } from '../UI/Layout/Layout'; +import Headline from '../UI/Headlines/Headline/Headline'; +import ActionButton from '../UI/Buttons/ActionButton/ActionButton'; + +import BookmarkGrid from './BookmarkGrid/BookmarkGrid'; +import { Category, GlobalState } from '../../interfaces'; +import Spinner from '../UI/Spinner/Spinner'; + +interface ComponentProps { + loading: boolean; + categories: Category[]; + getCategories: () => void; +} + +const Bookmarks = (props: ComponentProps): JSX.Element => { + useEffect(() => { + if (props.categories.length === 0) { + props.getCategories(); + } + }, [props.getCategories]) + + return ( + + Go back)} + /> + +
+ + +
+ + {props.loading + ? + : + } +
+ ) +} + +const mapStateToProps = (state: GlobalState) => { + return { + loading: state.bookmark.loading, + categories: state.bookmark.categories + } +} + +export default connect(mapStateToProps, { getCategories })(Bookmarks); \ No newline at end of file diff --git a/client/src/components/Home/Home.tsx b/client/src/components/Home/Home.tsx index 5021a38a456d4cc212ec8c69430756aeab925ffb..bc560fc13c1d00f490488bc6418a377672d1ebb1 100644 --- a/client/src/components/Home/Home.tsx +++ b/client/src/components/Home/Home.tsx @@ -1,8 +1,9 @@ import { useEffect } from 'react'; +import _ from 'underscore'; import { Link } from 'react-router-dom'; -import { connect } from 'react-redux'; +import { connect, useSelector, useDispatch } from 'react-redux'; import { GlobalState } from '../../interfaces/GlobalState'; -import { getApps } from '../../store/actions'; +import { getApps, getCategories } from '../../store/actions'; import Icon from '../UI/Icons/Icon/Icon'; @@ -10,21 +11,33 @@ import classes from './Home.module.css'; import { Container } from '../UI/Layout/Layout'; import SectionHeadline from '../UI/Headlines/SectionHeadline/SectionHeadline'; import AppGrid from '../Apps/AppGrid/AppGrid'; -import { App } from '../../interfaces'; +import { App, Category } from '../../interfaces'; import Spinner from '../UI/Spinner/Spinner'; import WeatherWidget from '../Widgets/WeatherWidget/WeatherWidget'; +import BookmarkGrid from '../Bookmarks/BookmarkGrid/BookmarkGrid'; interface ComponentProps { getApps: Function; - loading: boolean; + getCategories: Function; + appsLoading: boolean; apps: App[]; + categoriesLoading: boolean; + categories: Category[]; } const Home = (props: ComponentProps): JSX.Element => { useEffect(() => { - props.getApps(); + if (props.apps.length === 0) { + props.getApps(); + } }, [props.getApps]); + useEffect(() => { + if (props.categories.length === 0) { + props.getCategories(); + } + }, [props.getCategories]); + const dateAndTime = (): string => { const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; @@ -68,15 +81,19 @@ const Home = (props: ComponentProps): JSX.Element => { - {props.loading + {props.appsLoading ? : app.isPinned)} /> } + {props.categoriesLoading + ? + : category.isPinned)} /> + } - + ) @@ -84,9 +101,11 @@ const Home = (props: ComponentProps): JSX.Element => { const mapStateToProps = (state: GlobalState) => { return { - loading: state.app.loading, - apps: state.app.apps + appsLoading: state.app.loading, + apps: state.app.apps, + categoriesLoading: state.bookmark.loading, + categories: state.bookmark.categories } } -export default connect(mapStateToProps, { getApps })(Home); \ No newline at end of file +export default connect(mapStateToProps, { getApps, getCategories })(Home); \ No newline at end of file diff --git a/client/src/interfaces/App.ts b/client/src/interfaces/App.ts index 1904ea0894ef86cc5b143a739713a1dc294a2594..ed8842aab86932db2353c8faa3974e186335ef21 100644 --- a/client/src/interfaces/App.ts +++ b/client/src/interfaces/App.ts @@ -1,4 +1,4 @@ -import { Model } from './Api'; +import { Model } from '.'; export interface App extends Model { name: string; diff --git a/client/src/interfaces/Bookmark.ts b/client/src/interfaces/Bookmark.ts new file mode 100644 index 0000000000000000000000000000000000000000..2982993accd505c95d0ffebe789e7e2d47813579 --- /dev/null +++ b/client/src/interfaces/Bookmark.ts @@ -0,0 +1,7 @@ +import { Model } from '.'; + +export interface Bookmark extends Model { + name: string; + url: string; + categoryId: number; +} \ No newline at end of file diff --git a/client/src/interfaces/Category.ts b/client/src/interfaces/Category.ts new file mode 100644 index 0000000000000000000000000000000000000000..926987dadac700ff371d0184691385ff0106ab85 --- /dev/null +++ b/client/src/interfaces/Category.ts @@ -0,0 +1,11 @@ +import { Model, Bookmark } from '.'; + +export interface Category extends Model { + name: string; + isPinned: boolean; + bookmarks: Bookmark[]; +} + +export interface NewCategory { + name: string; +} \ No newline at end of file diff --git a/client/src/interfaces/GlobalState.ts b/client/src/interfaces/GlobalState.ts index 2855e3ad37d9419841c21a4d848a4d157e48dd5e..8a3c10b4ce1f964908fd538fadb925b45a3e08a6 100644 --- a/client/src/interfaces/GlobalState.ts +++ b/client/src/interfaces/GlobalState.ts @@ -1,7 +1,9 @@ import { State as AppState } from '../store/reducers/app'; import { State as ThemeState } from '../store/reducers/theme'; +import { State as BookmarkState } from '../store/reducers/bookmark'; export interface GlobalState { theme: ThemeState; app: AppState; + bookmark: BookmarkState; } \ No newline at end of file diff --git a/client/src/interfaces/Weather.ts b/client/src/interfaces/Weather.ts index ea8e0d150b877e8783bfe54138f32c4814e698a5..a99e2c7c0f446e5ea882d61181730bef6ece121f 100644 --- a/client/src/interfaces/Weather.ts +++ b/client/src/interfaces/Weather.ts @@ -1,4 +1,4 @@ -import { Model } from './Api'; +import { Model } from '.'; export interface Weather extends Model { externalLastUpdate: string; diff --git a/client/src/interfaces/index.ts b/client/src/interfaces/index.ts index 5b42fe8f2c1b591f9aec85c9f1dc284524751fcd..79078bd8889575188a53f4296cd401f7642874dc 100644 --- a/client/src/interfaces/index.ts +++ b/client/src/interfaces/index.ts @@ -2,4 +2,6 @@ export * from './App'; export * from './Theme'; export * from './GlobalState'; export * from './Api'; -export * from './Weather'; \ No newline at end of file +export * from './Weather'; +export * from './Bookmark'; +export * from './Category'; \ No newline at end of file diff --git a/client/src/store/actions/actionTypes.ts b/client/src/store/actions/actionTypes.ts index 31016e5bf33db39f096b0c6d0f5354a02674bd60..f6ee1f59eedefde8e0dcb8c375c9d0962c463158 100644 --- a/client/src/store/actions/actionTypes.ts +++ b/client/src/store/actions/actionTypes.ts @@ -4,7 +4,8 @@ import { PinAppAction, AddAppAction, DeleteAppAction, - UpdateAppAction + UpdateAppAction, + GetCategoriesAction } from './'; export enum ActionTypes { @@ -16,7 +17,10 @@ export enum ActionTypes { addApp = 'ADD_APP', addAppSuccess = 'ADD_APP_SUCCESS', deleteApp = 'DELETE_APP', - updateApp = 'UPDATE_APP' + updateApp = 'UPDATE_APP', + getCategories = 'GET_CATEGORIES', + getCategoriesSuccess = 'GET_CATEGORIES_SUCCESS', + getCategoriesError = 'GET_CATEGORIES_ERROR' } -export type Action = GetAppsAction | SetThemeAction | PinAppAction | AddAppAction | DeleteAppAction | UpdateAppAction; \ No newline at end of file +export type Action = GetAppsAction | SetThemeAction | PinAppAction | AddAppAction | DeleteAppAction | UpdateAppAction | GetCategoriesAction; \ No newline at end of file diff --git a/client/src/store/actions/bookmark.ts b/client/src/store/actions/bookmark.ts new file mode 100644 index 0000000000000000000000000000000000000000..ea074cce06cd51483996610410c8afe200019ed2 --- /dev/null +++ b/client/src/store/actions/bookmark.ts @@ -0,0 +1,27 @@ +import axios from 'axios'; +import { Dispatch } from 'redux'; +import { ActionTypes } from './actionTypes'; +import { Category, ApiResponse } from '../../interfaces'; + +export interface GetCategoriesAction { + type: ActionTypes.getCategories | ActionTypes.getCategoriesSuccess | ActionTypes.getCategoriesError; + payload: T; +} + +export const getCategories = () => async (dispatch: Dispatch) => { + dispatch>({ + type: ActionTypes.getCategories, + payload: undefined + }) + + try { + const res = await axios.get>('/api/categories'); + + dispatch>({ + type: ActionTypes.getCategoriesSuccess, + payload: res.data.data + }) + } catch (err) { + console.log(err); + } +} \ No newline at end of file diff --git a/client/src/store/actions/index.ts b/client/src/store/actions/index.ts index d9fc5a8b38c417b4d435ef42af19e36650a8f30f..15ca1e3f0e0283ff360767b110a9dfe9005efe1a 100644 --- a/client/src/store/actions/index.ts +++ b/client/src/store/actions/index.ts @@ -1,3 +1,4 @@ export * from './theme'; export * from './app'; -export * from './actionTypes'; \ No newline at end of file +export * from './actionTypes'; +export * from './bookmark'; \ No newline at end of file diff --git a/client/src/store/reducers/app.ts b/client/src/store/reducers/app.ts index f3afc578a15a29629963b70ec91aaaac1e6e979b..b445542964e8ff12258fe67c37c093cb2f7f27e0 100644 --- a/client/src/store/reducers/app.ts +++ b/client/src/store/reducers/app.ts @@ -4,7 +4,7 @@ import { App } from '../../interfaces/App'; export interface State { loading: boolean; apps: App[]; - errors: '' | undefined; + errors: string | undefined; } const initialState: State = { diff --git a/client/src/store/reducers/bookmark.ts b/client/src/store/reducers/bookmark.ts new file mode 100644 index 0000000000000000000000000000000000000000..cb8b16984afdb6341d63f3944e3bd64a72570509 --- /dev/null +++ b/client/src/store/reducers/bookmark.ts @@ -0,0 +1,40 @@ +import { ActionTypes, Action } from '../actions'; +import { Category, Bookmark } from '../../interfaces'; + +export interface State { + loading: boolean; + errors: string | undefined; + categories: Category[]; +} + +const initialState: State = { + loading: true, + errors: undefined, + categories: [] +} + +const getCategories = (state: State, action: Action): State => { + return { + ...state, + loading: true, + errors: undefined + } +} + +const getCategoriesSuccess = (state: State, action: Action): State => { + return { + ...state, + loading: false, + categories: action.payload + } +} + +const bookmarkReducer = (state = initialState, action: Action) => { + switch (action.type) { + case ActionTypes.getCategories: return getCategories(state, action); + case ActionTypes.getCategoriesSuccess: return getCategoriesSuccess(state, action); + default: return state; + } +} + +export default bookmarkReducer; \ No newline at end of file diff --git a/client/src/store/reducers/index.ts b/client/src/store/reducers/index.ts index d0119874e7a69454a1808a0ca0bcea0d05b76d57..16b9df73a0e5328a4abd1a91efb7ddccaee55bd6 100644 --- a/client/src/store/reducers/index.ts +++ b/client/src/store/reducers/index.ts @@ -4,10 +4,12 @@ import { GlobalState } from '../../interfaces/GlobalState'; import themeReducer from './theme'; import appReducer from './app'; +import bookmarkReducer from './bookmark'; const rootReducer = combineReducers({ theme: themeReducer, - app: appReducer + app: appReducer, + bookmark: bookmarkReducer }) export default rootReducer; \ No newline at end of file