diff --git a/client/src/App.tsx b/client/src/App.tsx index c304321..6a13241 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 0000000..81c5cc1 --- /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 0000000..6757f54 --- /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 0000000..6651f65 --- /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 0000000..2cee066 --- /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 0000000..09810e1 --- /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 0000000..a4e830a --- /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 5021a38..bc560fc 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 1904ea0..ed8842a 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 0000000..2982993 --- /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 0000000..926987d --- /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 2855e3a..8a3c10b 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 ea8e0d1..a99e2c7 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 5b42fe8..79078bd 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 31016e5..f6ee1f5 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 0000000..ea074cc --- /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 d9fc5a8..15ca1e3 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 f3afc57..b445542 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 0000000..cb8b169 --- /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 d011987..16b9df7 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