diff --git a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx
index d306655..78ff585 100644
--- a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx
+++ b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx
@@ -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 = (
+
+

+
+ );
+ }
+ else if (isImage(icon)) {
const source = isUrl(icon) ? icon : `/uploads/${icon}`;
iconEl = (
diff --git a/client/src/components/Bookmarks/Bookmarks.tsx b/client/src/components/Bookmarks/Bookmarks.tsx
index 0905711..7311e17 100644
--- a/client/src/components/Bookmarks/Bookmarks.tsx
+++ b/client/src/components/Bookmarks/Bookmarks.tsx
@@ -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}
/>
)}
+ await importFromFile(f)}
+ />
)}
diff --git a/client/src/components/UI/Buttons/FileButton/FileButton.module.css b/client/src/components/UI/Buttons/FileButton/FileButton.module.css
new file mode 100644
index 0000000..7c7dbf6
--- /dev/null
+++ b/client/src/components/UI/Buttons/FileButton/FileButton.module.css
@@ -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;
+}
\ No newline at end of file
diff --git a/client/src/components/UI/Buttons/FileButton/FileButton.tsx b/client/src/components/UI/Buttons/FileButton/FileButton.tsx
new file mode 100644
index 0000000..d4fe6a4
--- /dev/null
+++ b/client/src/components/UI/Buttons/FileButton/FileButton.tsx
@@ -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(null);
+
+ const handleChange = (e: ChangeEvent) => {
+ 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 = (
+
+
+
+
+ {props.name}
+
+ );
+
+ if (props.link) {
+ return (
+
+ {body}
+
+ );
+ } else if (props.handler) {
+ return (
+
+
+ {body}
+
+ );
+ } else {
+ return {body}
;
+ }
+};
diff --git a/client/src/components/UI/index.ts b/client/src/components/UI/index.ts
index 23d5f73..a79f1a0 100644
--- a/client/src/components/UI/index.ts
+++ b/client/src/components/UI/index.ts
@@ -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';
diff --git a/client/src/interfaces/Bookmark.ts b/client/src/interfaces/Bookmark.ts
index 858101c..9781028 100644
--- a/client/src/interfaces/Bookmark.ts
+++ b/client/src/interfaces/Bookmark.ts
@@ -11,3 +11,7 @@ export interface NewBookmark {
export interface Bookmark extends Model, NewBookmark {
orderId: number;
}
+
+export interface BookmarkImport {
+ file: File
+}
diff --git a/client/src/store/action-creators/bookmark.ts b/client/src/store/action-creators/bookmark.ts
index 5f077a6..15acc79 100644
--- a/client/src/store/action-creators/bookmark.ts
+++ b/client/src/store/action-creators/bookmark.ts
@@ -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) => {
+ try {
+ let formd = new FormData();
+ formd.append("file", formData.file);
+
+ const res = await axios.post>(
+ '/api/bookmarks/import',
+ formd,
+ {
+ headers: {
+ ...applyAuth(),
+ 'Content-Type': 'multipart/form-data',
+ },
+ }
+ );
+
+ dispatch({
+ type: ActionType.createNotification,
+ payload: {
+ title: 'Success',
+ message: `Bookmark file uploaded.`,
+ },
+ });
+
+ dispatch({
+ type: ActionType.importBookmark,
+ payload: res.data.data,
+ });
+ dispatch(sortBookmarks(res.data.data.categoryId));
+ } catch (err) {
+ console.log(err);
+ }
+ };
+
export const pinCategory =
(category: Category) => async (dispatch: Dispatch) => {
try {
diff --git a/client/src/store/action-types/index.ts b/client/src/store/action-types/index.ts
index 4be159f..7c30712 100644
--- a/client/src/store/action-types/index.ts
+++ b/client/src/store/action-types/index.ts
@@ -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',
diff --git a/client/src/store/actions/bookmark.ts b/client/src/store/actions/bookmark.ts
index 7c9e1f2..2d103fe 100644
--- a/client/src/store/actions/bookmark.ts
+++ b/client/src/store/actions/bookmark.ts
@@ -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;
diff --git a/client/src/utility/validators.ts b/client/src/utility/validators.ts
index df4de72..271bbb4 100644
--- a/client/src/utility/validators.ts
+++ b/client/src/utility/validators.ts
@@ -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;
diff --git a/controllers/bookmarks/importBookmark.js b/controllers/bookmarks/importBookmark.js
new file mode 100644
index 0000000..1d8a82e
--- /dev/null
+++ b/controllers/bookmarks/importBookmark.js
@@ -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;
diff --git a/controllers/bookmarks/index.js b/controllers/bookmarks/index.js
index 5c1bd86..c9ca352 100644
--- a/controllers/bookmarks/index.js
+++ b/controllers/bookmarks/index.js
@@ -3,6 +3,7 @@ module.exports = {
getAllBookmarks: require('./getAllBookmarks'),
getSingleBookmark: require('./getSingleBookmark'),
updateBookmark: require('./updateBookmark'),
+ importBookmark: require('./importBookmark'),
deleteBookmark: require('./deleteBookmark'),
reorderBookmarks: require('./reorderBookmarks'),
};
diff --git a/middleware/multer.js b/middleware/multer.js
index cf5a384..e6bdd99 100644
--- a/middleware/multer.js
+++ b/middleware/multer.js
@@ -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'),
+};
diff --git a/package.json b/package.json
index 1b3c92b..4b9810f 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/routes/apps.js b/routes/apps.js
index 3fd8a6c..0ddccb3 100644
--- a/routes/apps.js
+++ b/routes/apps.js
@@ -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);
diff --git a/routes/bookmark.js b/routes/bookmark.js
index 6bc96ad..23fc5bb 100644
--- a/routes/bookmark.js
+++ b/routes/bookmark.js
@@ -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;
diff --git a/utils/importBookmark.js b/utils/importBookmark.js
new file mode 100644
index 0000000..b3c3d01
--- /dev/null
+++ b/utils/importBookmark.js
@@ -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();
+};