Split categories and bookmarks forms into separate files. Added visibility functionality to categories and bookmarks

This commit is contained in:
Paweł Malak 2021-11-09 23:40:58 +01:00
parent f127a354ef
commit d83e3056c6
16 changed files with 484 additions and 369 deletions

View file

@ -14,7 +14,7 @@ interface Props {
app?: App;
}
export const AppForm = (props: Props): JSX.Element => {
export const AppForm = ({ app, modalHandler }: Props): JSX.Element => {
const dispatch = useDispatch();
const { addApp, updateApp } = bindActionCreators(actionCreators, dispatch);
@ -23,14 +23,14 @@ export const AppForm = (props: Props): JSX.Element => {
const [formData, setFormData] = useState<NewApp>(newAppTemplate);
useEffect(() => {
if (props.app) {
if (app) {
setFormData({
...props.app,
...app,
});
} else {
setFormData(newAppTemplate);
}
}, [props.app]);
}, [app]);
const inputChangeHandler = (
e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
@ -61,11 +61,12 @@ export const AppForm = (props: Props): JSX.Element => {
}
data.append('name', formData.name);
data.append('url', formData.url);
data.append('isPublic', `${formData.isPublic}`);
return data;
};
if (!props.app) {
if (!app) {
if (customIcon) {
const data = createFormData();
addApp(data);
@ -75,10 +76,10 @@ export const AppForm = (props: Props): JSX.Element => {
} else {
if (customIcon) {
const data = createFormData();
updateApp(props.app.id, data);
updateApp(app.id, data);
} else {
updateApp(props.app.id, formData);
props.modalHandler();
updateApp(app.id, formData);
modalHandler();
}
}
@ -86,10 +87,7 @@ export const AppForm = (props: Props): JSX.Element => {
};
return (
<ModalForm
modalHandler={props.modalHandler}
formHandler={formSubmitHandler}
>
<ModalForm modalHandler={modalHandler} formHandler={formSubmitHandler}>
{/* NAME */}
<InputGroup>
<label htmlFor="name">App Name</label>
@ -184,7 +182,7 @@ export const AppForm = (props: Props): JSX.Element => {
</select>
</InputGroup>
{!props.app ? (
{!app ? (
<Button>Add new application</Button>
) : (
<Button>Update application</Button>

View file

@ -1,318 +0,0 @@
// React
import {
useState,
SyntheticEvent,
Fragment,
ChangeEvent,
useEffect,
} from 'react';
// Redux
import { useDispatch, useSelector } from 'react-redux';
// Typescript
import {
Bookmark,
Category,
NewBookmark,
NewCategory,
} from '../../../interfaces';
import { ContentType } from '../Bookmarks';
// UI
import { ModalForm, InputGroup, Button } from '../../UI';
// CSS
import classes from './BookmarkForm.module.css';
import { newBookmarkTemplate, newCategoryTemplate } from '../../../utility';
import { State } from '../../../store/reducers';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../../store';
interface Props {
modalHandler: () => void;
contentType: ContentType;
category?: Category;
bookmark?: Bookmark;
}
export const BookmarkForm = (props: Props): JSX.Element => {
const { categories } = useSelector((state: State) => state.bookmarks);
const dispatch = useDispatch();
const {
addCategory,
addBookmark,
updateCategory,
updateBookmark,
createNotification,
} = bindActionCreators(actionCreators, dispatch);
const [useCustomIcon, toggleUseCustomIcon] = useState<boolean>(false);
const [customIcon, setCustomIcon] = useState<File | null>(null);
const [categoryName, setCategoryName] =
useState<NewCategory>(newCategoryTemplate);
const [formData, setFormData] = useState<NewBookmark>(newBookmarkTemplate);
// Load category data if provided for editing
useEffect(() => {
if (props.category) {
setCategoryName({ ...props.category });
} else {
setCategoryName(newCategoryTemplate);
}
}, [props.category]);
// Load bookmark data if provided for editing
useEffect(() => {
if (props.bookmark) {
setFormData({ ...props.bookmark });
} else {
setFormData(newBookmarkTemplate);
}
}, [props.bookmark]);
const formSubmitHandler = (e: SyntheticEvent<HTMLFormElement>): void => {
e.preventDefault();
const createFormData = (): FormData => {
const data = new FormData();
if (customIcon) {
data.append('icon', customIcon);
}
data.append('name', formData.name);
data.append('url', formData.url);
data.append('categoryId', `${formData.categoryId}`);
return data;
};
if (!props.category && !props.bookmark) {
// Add new
if (props.contentType === ContentType.category) {
// Add category
addCategory(categoryName);
setCategoryName(newCategoryTemplate);
} else if (props.contentType === ContentType.bookmark) {
// Add bookmark
if (formData.categoryId === -1) {
createNotification({
title: 'Error',
message: 'Please select category',
});
return;
}
if (customIcon) {
const data = createFormData();
addBookmark(data);
} else {
addBookmark(formData);
}
setFormData({
...newBookmarkTemplate,
categoryId: formData.categoryId,
});
// setCustomIcon(null);
}
} else {
// Update
if (props.contentType === ContentType.category && props.category) {
// Update category
updateCategory(props.category.id, categoryName);
setCategoryName(newCategoryTemplate);
} else if (props.contentType === ContentType.bookmark && props.bookmark) {
// Update bookmark
if (customIcon) {
const data = createFormData();
updateBookmark(props.bookmark.id, data, {
prev: props.bookmark.categoryId,
curr: formData.categoryId,
});
} else {
updateBookmark(props.bookmark.id, formData, {
prev: props.bookmark.categoryId,
curr: formData.categoryId,
});
}
setFormData(newBookmarkTemplate);
setCustomIcon(null);
}
props.modalHandler();
}
};
const inputChangeHandler = (e: ChangeEvent<HTMLInputElement>): void => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
const selectChangeHandler = (e: ChangeEvent<HTMLSelectElement>): void => {
setFormData({
...formData,
categoryId: parseInt(e.target.value),
});
};
const fileChangeHandler = (e: ChangeEvent<HTMLInputElement>): void => {
if (e.target.files) {
setCustomIcon(e.target.files[0]);
}
};
let button = <Button>Submit</Button>;
if (!props.category && !props.bookmark) {
if (props.contentType === ContentType.category) {
button = <Button>Add new category</Button>;
} else {
button = <Button>Add new bookmark</Button>;
}
} else if (props.category) {
button = <Button>Update category</Button>;
} else if (props.bookmark) {
button = <Button>Update bookmark</Button>;
}
return (
<ModalForm
modalHandler={props.modalHandler}
formHandler={formSubmitHandler}
>
{props.contentType === ContentType.category ? (
<Fragment>
<InputGroup>
<label htmlFor="categoryName">Category Name</label>
<input
type="text"
name="categoryName"
id="categoryName"
placeholder="Social Media"
required
value={categoryName.name}
onChange={(e) =>
setCategoryName({ name: e.target.value, isPublic: !!!!!false })
}
/>
</InputGroup>
</Fragment>
) : (
<Fragment>
<InputGroup>
<label htmlFor="name">Bookmark Name</label>
<input
type="text"
name="name"
id="name"
placeholder="Reddit"
required
value={formData.name}
onChange={(e) => inputChangeHandler(e)}
/>
</InputGroup>
<InputGroup>
<label htmlFor="url">Bookmark URL</label>
<input
type="text"
name="url"
id="url"
placeholder="reddit.com"
required
value={formData.url}
onChange={(e) => inputChangeHandler(e)}
/>
<span>
<a
href="https://github.com/pawelmalak/flame#supported-url-formats-for-applications-and-bookmarks"
target="_blank"
rel="noreferrer"
>
{' '}
Check supported URL formats
</a>
</span>
</InputGroup>
<InputGroup>
<label htmlFor="categoryId">Bookmark Category</label>
<select
name="categoryId"
id="categoryId"
required
onChange={(e) => selectChangeHandler(e)}
value={formData.categoryId}
>
<option value={-1}>Select category</option>
{categories.map((category: Category): JSX.Element => {
return (
<option key={category.id} value={category.id}>
{category.name}
</option>
);
})}
</select>
</InputGroup>
{!useCustomIcon ? (
// mdi
<InputGroup>
<label htmlFor="icon">Bookmark Icon (optional)</label>
<input
type="text"
name="icon"
id="icon"
placeholder="book-open-outline"
value={formData.icon}
onChange={(e) => inputChangeHandler(e)}
/>
<span>
Use icon name from MDI.
<a href="https://materialdesignicons.com/" target="blank">
{' '}
Click here for reference
</a>
</span>
<span
onClick={() => toggleUseCustomIcon(!useCustomIcon)}
className={classes.Switch}
>
Switch to custom icon upload
</span>
</InputGroup>
) : (
// custom
<InputGroup>
<label htmlFor="icon">Bookmark Icon (optional)</label>
<input
type="file"
name="icon"
id="icon"
onChange={(e) => fileChangeHandler(e)}
accept=".jpg,.jpeg,.png,.svg"
/>
<span
onClick={() => {
setCustomIcon(null);
toggleUseCustomIcon(!useCustomIcon);
}}
className={classes.Switch}
>
Switch to MDI
</span>
</InputGroup>
)}
</Fragment>
)}
{button}
</ModalForm>
);
};

View file

@ -125,7 +125,10 @@ export const BookmarkTable = (props: Props): JSX.Element => {
<DragDropContext onDragEnd={dragEndHanlder}>
<Droppable droppableId="categories">
{(provided) => (
<Table headers={['Name', 'Actions']} innerRef={provided.innerRef}>
<Table
headers={['Name', 'Visibility', 'Actions']}
innerRef={provided.innerRef}
>
{localCategories.map(
(category: Category, index): JSX.Element => {
return (
@ -150,7 +153,12 @@ export const BookmarkTable = (props: Props): JSX.Element => {
ref={provided.innerRef}
style={style}
>
<td>{category.name}</td>
<td style={{ width: '300px' }}>
{category.name}
</td>
<td style={{ width: '300px' }}>
{category.isPublic ? 'Visible' : 'Hidden'}
</td>
{!snapshot.isDragging && (
<td className={classes.TableActions}>
<div
@ -226,7 +234,9 @@ export const BookmarkTable = (props: Props): JSX.Element => {
});
return (
<Table headers={['Name', 'URL', 'Icon', 'Category', 'Actions']}>
<Table
headers={['Name', 'URL', 'Icon', 'Visibility', 'Category', 'Actions']}
>
{bookmarks.map(
(bookmark: { bookmark: Bookmark; categoryName: string }) => {
return (
@ -234,6 +244,7 @@ export const BookmarkTable = (props: Props): JSX.Element => {
<td>{bookmark.bookmark.name}</td>
<td>{bookmark.bookmark.url}</td>
<td>{bookmark.bookmark.icon}</td>
<td>{bookmark.bookmark.isPublic ? 'Visible' : 'Hidden'}</td>
<td>{bookmark.categoryName}</td>
<td className={classes.TableActions}>
<div

View file

@ -18,8 +18,8 @@ import { Container, Headline, ActionButton, Spinner, Modal } from '../UI';
// Components
import { BookmarkGrid } from './BookmarkGrid/BookmarkGrid';
import { BookmarkForm } from './BookmarkForm/BookmarkForm';
import { BookmarkTable } from './BookmarkTable/BookmarkTable';
import { Form } from './Form/Form';
// Utils
import { bookmarkTemplate, categoryTemplate } from '../../utility';
@ -98,24 +98,13 @@ export const Bookmarks = (props: Props): JSX.Element => {
return (
<Container>
<Modal isOpen={modalIsOpen} setIsOpen={toggleModal}>
{!isInUpdate ? (
<BookmarkForm
modalHandler={toggleModal}
contentType={formContentType}
/>
) : formContentType === ContentType.category ? (
<BookmarkForm
modalHandler={toggleModal}
contentType={formContentType}
category={categoryInUpdate}
/>
) : (
<BookmarkForm
modalHandler={toggleModal}
contentType={formContentType}
bookmark={bookmarkInUpdate}
/>
)}
<Form
modalHandler={toggleModal}
contentType={formContentType}
inUpdate={isInUpdate}
category={categoryInUpdate}
bookmark={bookmarkInUpdate}
/>
</Modal>
<Headline title="All Bookmarks" subtitle={<Link to="/">Go back</Link>} />

View file

@ -0,0 +1,260 @@
import { useState, ChangeEvent, useEffect, FormEvent } from 'react';
// Redux
import { useDispatch, useSelector } from 'react-redux';
import { State } from '../../../store/reducers';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../../store';
// Typescript
import { Bookmark, Category, NewBookmark } from '../../../interfaces';
// UI
import { ModalForm, InputGroup, Button } from '../../UI';
// CSS
import classes from './Form.module.css';
// Utils
import { inputHandler, newBookmarkTemplate } from '../../../utility';
interface Props {
modalHandler: () => void;
bookmark?: Bookmark;
}
export const BookmarksForm = ({
bookmark,
modalHandler,
}: Props): JSX.Element => {
const { categories } = useSelector((state: State) => state.bookmarks);
const dispatch = useDispatch();
const { addBookmark, updateBookmark, createNotification } =
bindActionCreators(actionCreators, dispatch);
const [useCustomIcon, toggleUseCustomIcon] = useState<boolean>(false);
const [customIcon, setCustomIcon] = useState<File | null>(null);
const [formData, setFormData] = useState<NewBookmark>(newBookmarkTemplate);
// Load bookmark data if provided for editing
useEffect(() => {
if (bookmark) {
setFormData({ ...bookmark });
} else {
setFormData(newBookmarkTemplate);
}
}, [bookmark]);
const inputChangeHandler = (
e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
options?: { isNumber?: boolean; isBool?: boolean }
) => {
inputHandler<NewBookmark>({
e,
options,
setStateHandler: setFormData,
state: formData,
});
};
const fileChangeHandler = (e: ChangeEvent<HTMLInputElement>): void => {
if (e.target.files) {
setCustomIcon(e.target.files[0]);
}
};
// Bookmarks form handler
const formSubmitHandler = (e: FormEvent): void => {
e.preventDefault();
const createFormData = (): FormData => {
const data = new FormData();
if (customIcon) {
data.append('icon', customIcon);
}
data.append('name', formData.name);
data.append('url', formData.url);
data.append('categoryId', `${formData.categoryId}`);
data.append('isPublic', `${formData.isPublic}`);
return data;
};
const checkCategory = (): boolean => {
if (formData.categoryId < 0) {
createNotification({
title: 'Error',
message: 'Please select category',
});
return false;
}
return true;
};
if (!bookmark) {
// add new bookmark
if (!checkCategory()) return;
if (formData.categoryId < 0) {
createNotification({
title: 'Error',
message: 'Please select category',
});
return;
}
if (customIcon) {
const data = createFormData();
addBookmark(data);
} else {
addBookmark(formData);
}
setFormData({
...newBookmarkTemplate,
categoryId: formData.categoryId,
isPublic: formData.isPublic,
});
} else {
// update
if (!checkCategory()) return;
if (customIcon) {
const data = createFormData();
updateBookmark(bookmark.id, data, {
prev: bookmark.categoryId,
curr: formData.categoryId,
});
} else {
updateBookmark(bookmark.id, formData, {
prev: bookmark.categoryId,
curr: formData.categoryId,
});
}
modalHandler();
setFormData(newBookmarkTemplate);
setCustomIcon(null);
}
};
return (
<ModalForm modalHandler={modalHandler} formHandler={formSubmitHandler}>
<InputGroup>
<label htmlFor="name">Bookmark Name</label>
<input
type="text"
name="name"
id="name"
placeholder="Reddit"
required
value={formData.name}
onChange={(e) => inputChangeHandler(e)}
/>
</InputGroup>
<InputGroup>
<label htmlFor="url">Bookmark URL</label>
<input
type="text"
name="url"
id="url"
placeholder="reddit.com"
required
value={formData.url}
onChange={(e) => inputChangeHandler(e)}
/>
</InputGroup>
<InputGroup>
<label htmlFor="categoryId">Bookmark Category</label>
<select
name="categoryId"
id="categoryId"
required
onChange={(e) => inputChangeHandler(e, { isNumber: true })}
value={formData.categoryId}
>
<option value={-1}>Select category</option>
{categories.map((category: Category): JSX.Element => {
return (
<option key={category.id} value={category.id}>
{category.name}
</option>
);
})}
</select>
</InputGroup>
{!useCustomIcon ? (
// mdi
<InputGroup>
<label htmlFor="icon">Bookmark Icon (optional)</label>
<input
type="text"
name="icon"
id="icon"
placeholder="book-open-outline"
value={formData.icon}
onChange={(e) => inputChangeHandler(e)}
/>
<span>
Use icon name from MDI.
<a href="https://materialdesignicons.com/" target="blank">
{' '}
Click here for reference
</a>
</span>
<span
onClick={() => toggleUseCustomIcon(!useCustomIcon)}
className={classes.Switch}
>
Switch to custom icon upload
</span>
</InputGroup>
) : (
// custom
<InputGroup>
<label htmlFor="icon">Bookmark Icon (optional)</label>
<input
type="file"
name="icon"
id="icon"
onChange={(e) => fileChangeHandler(e)}
accept=".jpg,.jpeg,.png,.svg"
/>
<span
onClick={() => {
setCustomIcon(null);
toggleUseCustomIcon(!useCustomIcon);
}}
className={classes.Switch}
>
Switch to MDI
</span>
</InputGroup>
)}
<InputGroup>
<label htmlFor="isPublic">Bookmark visibility</label>
<select
id="isPublic"
name="isPublic"
value={formData.isPublic ? 1 : 0}
onChange={(e) => inputChangeHandler(e, { isBool: true })}
>
<option value={1}>Visible (anyone can access it)</option>
<option value={0}>Hidden (authentication required)</option>
</select>
</InputGroup>
<Button>{bookmark ? 'Update bookmark' : 'Add new bookmark'}</Button>
</ModalForm>
);
};

View file

@ -0,0 +1,100 @@
import { ChangeEvent, FormEvent, useEffect, useState } from 'react';
// Redux
import { useDispatch } from 'react-redux';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../../store';
// Typescript
import { Category, NewCategory } from '../../../interfaces';
// UI
import { ModalForm, InputGroup, Button } from '../../UI';
// Utils
import { inputHandler, newCategoryTemplate } from '../../../utility';
interface Props {
modalHandler: () => void;
category?: Category;
}
export const CategoryForm = ({
category,
modalHandler,
}: Props): JSX.Element => {
const dispatch = useDispatch();
const { addCategory, updateCategory } = bindActionCreators(
actionCreators,
dispatch
);
const [formData, setFormData] = useState<NewCategory>(newCategoryTemplate);
// Load category data if provided for editing
useEffect(() => {
if (category) {
setFormData({ ...category });
} else {
setFormData(newCategoryTemplate);
}
}, [category]);
const inputChangeHandler = (
e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
options?: { isNumber?: boolean; isBool?: boolean }
) => {
inputHandler<NewCategory>({
e,
options,
setStateHandler: setFormData,
state: formData,
});
};
// Category form handler
const formSubmitHandler = (e: FormEvent): void => {
e.preventDefault();
if (!category) {
addCategory(formData);
} else {
updateCategory(category.id, formData);
setFormData(newCategoryTemplate);
modalHandler();
}
};
return (
<ModalForm modalHandler={modalHandler} formHandler={formSubmitHandler}>
<InputGroup>
<label htmlFor="name">Category Name</label>
<input
type="text"
name="name"
id="name"
placeholder="Social Media"
required
value={formData.name}
onChange={(e) => inputChangeHandler(e)}
/>
</InputGroup>
<InputGroup>
<label htmlFor="isPublic">Category visibility</label>
<select
id="isPublic"
name="isPublic"
value={formData.isPublic ? 1 : 0}
onChange={(e) => inputChangeHandler(e, { isBool: true })}
>
<option value={1}>Visible (anyone can access it)</option>
<option value={0}>Hidden (authentication required)</option>
</select>
</InputGroup>
<Button>{category ? 'Update category' : 'Add new category'}</Button>
</ModalForm>
);
};

View file

@ -0,0 +1,44 @@
// Typescript
import { Bookmark, Category } from '../../../interfaces';
import { ContentType } from '../Bookmarks';
// Utils
import { CategoryForm } from './CategoryForm';
import { BookmarksForm } from './BookmarksForm';
import { Fragment } from 'react';
interface Props {
modalHandler: () => void;
contentType: ContentType;
inUpdate?: boolean;
category?: Category;
bookmark?: Bookmark;
}
export const Form = (props: Props): JSX.Element => {
const { modalHandler, contentType, inUpdate, category, bookmark } = props;
return (
<Fragment>
{!inUpdate ? (
// form: add new
<Fragment>
{contentType === ContentType.category ? (
<CategoryForm modalHandler={modalHandler} />
) : (
<BookmarksForm modalHandler={modalHandler} />
)}
</Fragment>
) : (
// form: update
<Fragment>
{contentType === ContentType.category ? (
<CategoryForm modalHandler={modalHandler} category={category} />
) : (
<BookmarksForm modalHandler={modalHandler} bookmark={bookmark} />
)}
</Fragment>
)}
</Fragment>
);
};

View file

@ -1,7 +1,8 @@
import { ReactNode } from 'react';
import classes from './Button.module.css';
interface Props {
children: string;
children: ReactNode;
click?: any;
}

View file

@ -1,15 +1,11 @@
import { Model } from '.';
export interface Bookmark extends Model {
name: string;
url: string;
categoryId: number;
icon: string;
}
export interface NewBookmark {
name: string;
url: string;
categoryId: number;
icon: string;
}
isPublic: boolean;
}
export interface Bookmark extends Model, NewBookmark {}

View file

@ -1,12 +1,12 @@
import { Model, Bookmark } from '.';
export interface Category extends Model {
export interface NewCategory {
name: string;
isPublic: boolean;
}
export interface Category extends Model, NewCategory {
isPinned: boolean;
orderId: number;
bookmarks: Bookmark[];
}
export interface NewCategory {
name: string;
}

View file

@ -1,6 +1,6 @@
import axios from 'axios';
import { store } from '../store/store';
import { createNotification } from '../store/actions';
import { createNotification } from '../store/action-creators';
export const checkVersion = async (isForced: boolean = false) => {
try {

View file

@ -11,7 +11,7 @@ export const appTemplate: App = {
...newAppTemplate,
isPinned: false,
orderId: 0,
id: 0,
id: -1,
createdAt: new Date(),
updatedAt: new Date(),
};

View file

@ -0,0 +1,16 @@
import { Bookmark, NewBookmark } from '../../interfaces';
export const newBookmarkTemplate: NewBookmark = {
name: '',
url: '',
categoryId: -1,
icon: '',
isPublic: true,
};
export const bookmarkTemplate: Bookmark = {
...newBookmarkTemplate,
id: -1,
createdAt: new Date(),
updatedAt: new Date(),
};

View file

@ -0,0 +1,16 @@
import { Category, NewCategory } from '../../interfaces';
export const newCategoryTemplate: NewCategory = {
name: '',
isPublic: true,
};
export const categoryTemplate: Category = {
...newCategoryTemplate,
id: -1,
isPinned: false,
orderId: 0,
bookmarks: [],
createdAt: new Date(),
updatedAt: new Date(),
};

View file

@ -1,3 +1,5 @@
export * from './configTemplate';
export * from './settingsTemplate';
export * from './appTemplate';
export * from './categoryTemplate';
export * from './bookmarkTemplate';