Bookmark import.

This commit is contained in:
Jackson D 2022-02-06 08:21:01 -05:00
parent 1fff824957
commit 1aedb87bf4
17 changed files with 327 additions and 8 deletions

View file

@ -12,7 +12,7 @@ import { Bookmark, Category } from '../../../interfaces';
// Other
import classes from './BookmarkCard.module.css';
import { Icon } from '../../UI';
import { iconParser, isImage, isSvg, isUrl, urlParser } from '../../../utility';
import { iconParser, isImage, isBase64Image, isSvg, isUrl, urlParser } from '../../../utility';
interface Props {
category: Category;
@ -54,7 +54,18 @@ export const BookmarkCard = (props: Props): JSX.Element => {
if (bookmark.icon) {
const { icon, name } = bookmark;
if (isImage(icon)) {
if(isBase64Image(icon)){
iconEl = (
<div className={classes.BookmarkIcon}>
<img
src={icon}
alt={`${name} icon`}
className={classes.CustomIcon}
/>
</div>
);
}
else if (isImage(icon)) {
const source = isUrl(icon) ? icon : `/uploads/${icon}`;
iconEl = (

View file

@ -7,6 +7,8 @@ import { State } from '../../store/reducers';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../store';
import { importBookmark } from '../../store/action-creators';
// Typescript
import { Category, Bookmark } from '../../interfaces';
@ -21,6 +23,7 @@ import {
Spinner,
Modal,
Message,
FileButton,
} from '../UI';
// Components
@ -49,6 +52,11 @@ export const Bookmarks = (props: Props): JSX.Element => {
const { getCategories, setEditCategory, setEditBookmark } =
bindActionCreators(actionCreators, dispatch);
const { importBookmark } = bindActionCreators(
actionCreators,
dispatch
);
// Load categories if array is empty
useEffect(() => {
if (!categories.length) {
@ -128,6 +136,10 @@ export const Bookmarks = (props: Props): JSX.Element => {
}
};
const importFromFile = async (file: File) => {
await importBookmark({file});
};
const finishEditing = () => {
setShowTable(false);
setEditCategory(null);
@ -169,6 +181,11 @@ export const Bookmarks = (props: Props): JSX.Element => {
handler={finishEditing}
/>
)}
<FileButton
name="Import From File"
icon="mdiPlusBox"
handler={async (f) => await importFromFile(f)}
/>
</div>
)}

View file

@ -0,0 +1,31 @@
.ActionButton {
/* background-color: var(--color-accent); */
border: 1.5px solid var(--color-accent);
border-radius: 4px;
color: var(--color-primary);
padding: 5px 15px;
transition: all 0.3s;
display: flex;
justify-content: center;
align-items: center;
max-width: 250px;
margin-right: 10px;
margin-bottom: 20px;
}
.ActionButton:hover {
background-color: var(--color-accent);
color: var(--color-background);
cursor: pointer;
}
.ActionButtonIcon {
--size: 20px;
width: var(--size);
height: var(--size);
/* display: flex; */
}
.ActionButtonName {
display: flex;
}

View file

@ -0,0 +1,64 @@
import { ChangeEvent, Fragment, useRef } from 'react';
import { Link } from 'react-router-dom';
import classes from './FileButton.module.css';
import { Icon } from '../..';
interface Props {
name: string;
icon: string;
link?: string;
handler?: (file: File) => void;
}
export const FileButton = (props: Props): JSX.Element => {
const imageRef = useRef<HTMLInputElement>(null);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
let target = e.target as HTMLInputElement;
if (target?.files && target?.files[0]) {
if(props.handler) props.handler(target?.files[0]);
}
};
const open = () => {
imageRef?.current?.click();
};
const body = (
<Fragment>
<div className={classes.ActionButtonIcon}>
<Icon icon={props.icon} />
</div>
<div className={classes.ActionButtonName}>{props.name}</div>
</Fragment>
);
if (props.link) {
return (
<Link to={props.link} tabIndex={0}>
{body}
</Link>
);
} else if (props.handler) {
return (
<div
className={classes.ActionButton}
onClick={open}
tabIndex={0}
>
<input
ref={imageRef}
type="file"
style={{ display: 'none' }}
accept="image/*"
onChange={handleChange}
/>
{body}
</div>
);
} else {
return <div className={classes.ActionButton}>{body}</div>;
}
};

View file

@ -12,4 +12,5 @@ export * from './Forms/InputGroup/InputGroup';
export * from './Forms/ModalForm/ModalForm';
export * from './Buttons/ActionButton/ActionButton';
export * from './Buttons/Button/Button';
export * from './Buttons/FileButton/FileButton';
export * from './Text/Message/Message';

View file

@ -11,3 +11,7 @@ export interface NewBookmark {
export interface Bookmark extends Model, NewBookmark {
orderId: number;
}
export interface BookmarkImport {
file: File
}

View file

@ -6,6 +6,7 @@ import { ActionType } from '../action-types';
import {
ApiResponse,
Bookmark,
BookmarkImport,
Category,
Config,
NewBookmark,
@ -18,6 +19,7 @@ import {
DeleteBookmarkAction,
DeleteCategoryAction,
GetCategoriesAction,
ImportBookmarkAction,
PinCategoryAction,
ReorderBookmarksAction,
ReorderCategoriesAction,
@ -108,6 +110,42 @@ export const addBookmark =
}
};
export const importBookmark =
(formData: BookmarkImport) =>
async (dispatch: Dispatch<ImportBookmarkAction>) => {
try {
let formd = new FormData();
formd.append("file", formData.file);
const res = await axios.post<ApiResponse<Bookmark>>(
'/api/bookmarks/import',
formd,
{
headers: {
...applyAuth(),
'Content-Type': 'multipart/form-data',
},
}
);
dispatch<any>({
type: ActionType.createNotification,
payload: {
title: 'Success',
message: `Bookmark file uploaded.`,
},
});
dispatch({
type: ActionType.importBookmark,
payload: res.data.data,
});
dispatch<any>(sortBookmarks(res.data.data.categoryId));
} catch (err) {
console.log(err);
}
};
export const pinCategory =
(category: Category) => async (dispatch: Dispatch<PinCategoryAction>) => {
try {

View file

@ -42,6 +42,7 @@ export enum ActionType {
setEditBookmark = 'SET_EDIT_BOOKMARK',
reorderBookmarks = 'REORDER_BOOKMARKS',
sortBookmarks = 'SORT_BOOKMARKS',
importBookmark = 'IMPORT_BOOKMARK',
// AUTH
login = 'LOGIN',
logout = 'LOGOUT',

View file

@ -19,6 +19,11 @@ export interface AddBookmarkAction {
payload: Bookmark;
}
export interface ImportBookmarkAction {
type: ActionType.importBookmark;
payload: Bookmark;
}
export interface PinCategoryAction {
type: ActionType.pinCategory;
payload: Category;

View file

@ -18,6 +18,10 @@ export const isImage = (data: string): boolean => {
return regex.test(data);
};
export const isBase64Image = (data: string): boolean => {
return data.startsWith("data:image/")
};
export const isSvg = (data: string): boolean => {
const regex = /.(svg)$/i;

View file

@ -0,0 +1,22 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const ErrorResponse = require('../../utils/ErrorResponse');
const importBookmarkData = require('../../utils/importBookmark');
const Bookmark = require('../../models/Bookmark');
// @desc Import bookmarks from file.
// @route POST /api/bookmarks/import
// @access Public
const importBookmark = asyncWrapper(async (req, res, next) => {
importBookmarkData(req.file.path);
if(next){
next();
} else {
res.status(200).json({
success: true,
data: bookmark,
});
}
});
module.exports = importBookmark;

View file

@ -3,6 +3,7 @@ module.exports = {
getAllBookmarks: require('./getAllBookmarks'),
getSingleBookmark: require('./getSingleBookmark'),
updateBookmark: require('./updateBookmark'),
importBookmark: require('./importBookmark'),
deleteBookmark: require('./deleteBookmark'),
reorderBookmarks: require('./reorderBookmarks'),
};

View file

@ -14,7 +14,15 @@ const storage = multer.diskStorage({
},
});
const supportedTypes = ['jpg', 'jpeg', 'png', 'svg', 'svg+xml', 'x-icon'];
const supportedTypes = [
'jpg',
'jpeg',
'png',
'svg',
'svg+xml',
'x-icon',
'html',
];
const fileFilter = (req, file, cb) => {
if (supportedTypes.includes(file.mimetype.split('/')[1])) {
@ -26,4 +34,7 @@ const fileFilter = (req, file, cb) => {
const upload = multer({ storage, fileFilter });
module.exports = upload.single('icon');
module.exports = {
icon: upload.single('icon'),
bookmark: upload.single('file'),
};

View file

@ -26,6 +26,7 @@
"express": "^4.17.1",
"jsonwebtoken": "^8.5.1",
"multer": "^1.4.3",
"node-bookmarks-parser": "^2.0.0",
"node-schedule": "^2.0.0",
"sequelize": "^6.9.0",
"sqlite3": "^5.0.2",

View file

@ -15,13 +15,13 @@ const {
router
.route('/')
.post(auth, requireAuth, upload, createApp)
.post(auth, requireAuth, upload.icon, createApp)
.get(auth, getAllApps);
router
.route('/:id')
.get(auth, getSingleApp)
.put(auth, requireAuth, upload, updateApp)
.put(auth, requireAuth, upload.icon, updateApp)
.delete(auth, requireAuth, deleteApp);
router.route('/0/reorder').put(auth, requireAuth, reorderApps);

View file

@ -11,19 +11,24 @@ const {
updateBookmark,
deleteBookmark,
reorderBookmarks,
importBookmark,
} = require('../controllers/bookmarks');
router
.route('/')
.post(auth, requireAuth, upload, createBookmark)
.post(auth, requireAuth, upload.icon, createBookmark)
.get(auth, getAllBookmarks);
router
.route('/:id')
.get(auth, getSingleBookmark)
.put(auth, requireAuth, upload, updateBookmark)
.put(auth, requireAuth, upload.icon, updateBookmark)
.delete(auth, requireAuth, deleteBookmark);
router
.route('/import')
.post(auth, requireAuth, upload.bookmark, importBookmark, getAllBookmarks)
router.route('/0/reorder').put(auth, requireAuth, reorderBookmarks);
module.exports = router;

103
utils/importBookmark.js Normal file
View file

@ -0,0 +1,103 @@
const parse = require('node-bookmarks-parser');
var sqlite3 = require('sqlite3').verbose();
const File = require('./File');
const databaseFilePath = 'data/db.sqlite';
let bookmarks = [];
function saveBookmarks() {
const db = new sqlite3.Database(databaseFilePath);
const importDate = new Date().toISOString();
db.serialize(() => {
db.all(`SELECT id, name FROM categories`, (err, data) => {
if (err) {
}
for (const bookmark of bookmarks) {
// Found in db.
let stmt = db.prepare(
'INSERT INTO bookmarks (name, url, categoryId, icon, createdAt, updatedAt) VALUES(?,?,?,?,?,?)'
);
stmt.run(
bookmark.title,
bookmark.url,
data.find(
(r) => r.name.toLowerCase() === bookmark.category.toLowerCase()
)?.id,
bookmark.icon,
importDate,
importDate,
(err) => {
if (err) {
console.error(err);
}
}
);
stmt.finalize();
}
});
});
}
function saveCategories() {
let db = new sqlite3.Database(databaseFilePath);
let uniqueCats = [...new Set(bookmarks.map((r) => r.category))];
const importDate = new Date().toString();
db.serialize(() => {
db.all(`SELECT * FROM categories`, (err, data) => {
if (err) {
console.error(err);
}
for (const newCaterory of uniqueCats) {
for (const category of data) {
if (category.name.toLowerCase() === newCaterory.toLowerCase()) {
continue;
}
}
let stmt = db.prepare(
'INSERT INTO categories (name, createdAt, updatedAt) VALUES(?,?,?)'
);
stmt.run(newCaterory, importDate, importDate, (err) => {
if (err) {
console.error(err);
}
});
stmt.finalize();
}
saveBookmarks();
});
});
}
function crawlBookmarks(bookmark, category) {
if (bookmark.type === 'bookmark') {
bookmarks.push({
title: bookmark.title,
url: bookmark.url,
icon: bookmark.icon,
category: category,
});
}
if (bookmark.type === 'folder') {
for (const child of bookmark.children) {
crawlBookmarks(child, bookmark.title);
}
}
}
module.exports = function importBookmark(path) {
const fileContent = new File(path).read();
const bookmarkData = parse(fileContent);
for (const bookmark of bookmarkData) {
crawlBookmarks(bookmark);
}
saveCategories();
};