Bookmark import.
This commit is contained in:
parent
1fff824957
commit
1aedb87bf4
17 changed files with 327 additions and 8 deletions
|
@ -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 = (
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
64
client/src/components/UI/Buttons/FileButton/FileButton.tsx
Normal file
64
client/src/components/UI/Buttons/FileButton/FileButton.tsx
Normal 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>;
|
||||
}
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -11,3 +11,7 @@ export interface NewBookmark {
|
|||
export interface Bookmark extends Model, NewBookmark {
|
||||
orderId: number;
|
||||
}
|
||||
|
||||
export interface BookmarkImport {
|
||||
file: 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 {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
22
controllers/bookmarks/importBookmark.js
Normal file
22
controllers/bookmarks/importBookmark.js
Normal 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;
|
|
@ -3,6 +3,7 @@ module.exports = {
|
|||
getAllBookmarks: require('./getAllBookmarks'),
|
||||
getSingleBookmark: require('./getSingleBookmark'),
|
||||
updateBookmark: require('./updateBookmark'),
|
||||
importBookmark: require('./importBookmark'),
|
||||
deleteBookmark: require('./deleteBookmark'),
|
||||
reorderBookmarks: require('./reorderBookmarks'),
|
||||
};
|
||||
|
|
|
@ -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'),
|
||||
};
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
103
utils/importBookmark.js
Normal 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();
|
||||
};
|
Loading…
Add table
Reference in a new issue