Explorar el Código

Bookmarks view with grid + redux actions

unknown hace 4 años
padre
commit
e22e5afcb9

+ 2 - 0
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<any>(setTheme(localStorage.theme));
@@ -23,6 +24,7 @@ const App = (): JSX.Element => {
           <Route exact path='/' component={Home} />
           <Route path='/settings' component={Settings} />
           <Route path='/applications' component={Apps} />
+          <Route path='/bookmarks' component={Bookmarks} />
         </Switch>
       </BrowserRouter>
     </Provider>

+ 27 - 0
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;
+}

+ 26 - 0
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 (
+    <div className={classes.BookmarkCard}>
+      <h3>{props.category.name}</h3>
+      <div className={classes.Bookmarks}>
+        {props.category.bookmarks.map((bookmark: Bookmark) => (
+          <a
+            href={`http://${bookmark.url}`}
+            target='blank'
+            key={bookmark.id}>
+            {bookmark.name}
+          </a>
+        ))}
+      </div>
+    </div>
+  )
+}
+
+export default BookmarkCard;

+ 22 - 0
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);
+  }
+}

+ 20 - 0
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 (
+    <div className={classes.BookmarkGrid}>
+      {props.categories.map((category: Category): JSX.Element => <BookmarkCard category={category} key={category.id} />)}
+    </div>
+  )
+  
+}
+
+export default BookmarkGrid;

+ 4 - 0
client/src/components/Bookmarks/Bookmarks.module.css

@@ -0,0 +1,4 @@
+.ActionsContainer {
+  display: flex;
+  align-items: center;
+}

+ 62 - 0
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 (
+    <Container>
+      <Headline
+        title='All Bookmarks'
+        subtitle={(<Link to='/'>Go back</Link>)}
+      />
+      
+      <div className={classes.ActionsContainer}>
+        <ActionButton
+          name='Add'
+          icon='mdiPlusBox'
+        />
+        <ActionButton
+          name='Edit'
+          icon='mdiPencil'
+        />
+      </div>
+
+      {props.loading
+        ? <Spinner />
+        : <BookmarkGrid categories={props.categories} />
+      }
+    </Container>
+  )
+}
+
+const mapStateToProps = (state: GlobalState) => {
+  return {
+    loading: state.bookmark.loading,
+    categories: state.bookmark.categories
+  }
+}
+
+export default connect(mapStateToProps, { getCategories })(Bookmarks);

+ 29 - 10
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 => {
       </header>
       
       <SectionHeadline title='Applications' link='/applications' />
-      {props.loading
+      {props.appsLoading
         ? <Spinner />
         : <AppGrid apps={props.apps.filter((app: App) => app.isPinned)} />
       }
 
       <SectionHeadline title='Bookmarks' link='/bookmarks' />
+      {props.categoriesLoading
+        ? <Spinner />
+        : <BookmarkGrid categories={props.categories.filter((category: Category) => category.isPinned)} />
+      }
 
       <Link to='/settings' className={classes.SettingsButton}>
-        <Icon icon='mdiCog' />
+        <Icon icon='mdiCog' color='var(--color-background)' />
       </Link>
     </Container>
   )
@@ -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);
+export default connect(mapStateToProps, { getApps, getCategories })(Home);

+ 1 - 1
client/src/interfaces/App.ts

@@ -1,4 +1,4 @@
-import { Model } from './Api';
+import { Model } from '.';
 
 export interface App extends Model {
   name: string;

+ 7 - 0
client/src/interfaces/Bookmark.ts

@@ -0,0 +1,7 @@
+import { Model } from '.';
+
+export interface Bookmark extends Model {
+  name: string;
+  url: string;
+  categoryId: number;
+}

+ 11 - 0
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;
+}

+ 2 - 0
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;
 }

+ 1 - 1
client/src/interfaces/Weather.ts

@@ -1,4 +1,4 @@
-import { Model } from './Api';
+import { Model } from '.';
 
 export interface Weather extends Model {
   externalLastUpdate: string;

+ 3 - 1
client/src/interfaces/index.ts

@@ -2,4 +2,6 @@ export * from './App';
 export * from './Theme';
 export * from './GlobalState';
 export * from './Api';
-export * from './Weather';
+export * from './Weather';
+export * from './Bookmark';
+export * from './Category';

+ 7 - 3
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<any> | SetThemeAction | PinAppAction | AddAppAction | DeleteAppAction | UpdateAppAction;
+export type Action = GetAppsAction<any> | SetThemeAction | PinAppAction | AddAppAction | DeleteAppAction | UpdateAppAction | GetCategoriesAction<any>;

+ 27 - 0
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<T> {
+  type: ActionTypes.getCategories | ActionTypes.getCategoriesSuccess | ActionTypes.getCategoriesError;
+  payload: T;
+}
+
+export const getCategories = () => async (dispatch: Dispatch) => {
+  dispatch<GetCategoriesAction<undefined>>({
+    type: ActionTypes.getCategories,
+    payload: undefined
+  })
+
+  try {
+    const res = await axios.get<ApiResponse<Category[]>>('/api/categories');
+
+    dispatch<GetCategoriesAction<Category[]>>({
+      type: ActionTypes.getCategoriesSuccess,
+      payload: res.data.data
+    })
+  } catch (err) {
+    console.log(err);
+  }
+}

+ 2 - 1
client/src/store/actions/index.ts

@@ -1,3 +1,4 @@
 export * from './theme';
 export * from './app';
-export * from './actionTypes';
+export * from './actionTypes';
+export * from './bookmark';

+ 1 - 1
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 = {

+ 40 - 0
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;

+ 3 - 1
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<GlobalState>({
   theme: themeReducer,
-  app: appReducer
+  app: appReducer,
+  bookmark: bookmarkReducer
 })
 
 export default rootReducer;