Added update/edit support.
This commit is contained in:
parent
a28417beeb
commit
e57365fd0c
|
@ -13,6 +13,8 @@ const database = config.API.DB_NAME;
|
||||||
const collection = config.API.COLLECTION_NAME;
|
const collection = config.API.COLLECTION_NAME;
|
||||||
|
|
||||||
const client = new MongoClient(URI);
|
const client = new MongoClient(URI);
|
||||||
|
const db = client.db(database);
|
||||||
|
const list = db.collection(collection);
|
||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
|
|
||||||
|
@ -42,23 +44,39 @@ app.post('/api', async (req, res) => {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
insertDoc(req.body);
|
insertDoc(req.body);
|
||||||
} finally {
|
} finally {
|
||||||
res.send('DONE!');
|
res.send('Posted!');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.put('/api', async (req, res) => {
|
||||||
|
const id = req.body._id;
|
||||||
|
|
||||||
|
await updateDoc(id, req.body);
|
||||||
|
|
||||||
|
res.send('Updated!');
|
||||||
|
});
|
||||||
|
|
||||||
app.delete('/api', async (req, res) => {
|
app.delete('/api', async (req, res) => {
|
||||||
const id = req.body.id.toString();
|
const id = req.body.id;
|
||||||
|
|
||||||
await deleteDoc(id);
|
await deleteDoc(id);
|
||||||
|
|
||||||
res.send(`Bookmark with _id:${id} deleted.`);
|
res.send(`Bookmark with _id:${id} deleted.`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function updateDoc(id, updatedListing) {
|
||||||
|
try {
|
||||||
|
await list.updateOne({ _id: id }, { $set: updatedListing });
|
||||||
|
}
|
||||||
|
|
||||||
|
catch(err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function insertDoc(doc) {
|
async function insertDoc(doc) {
|
||||||
try {
|
try {
|
||||||
const db = client.db(database);
|
await list.insertOne(doc);
|
||||||
const list = db.collection(collection);
|
|
||||||
const result = await list.insertOne(doc);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
catch(err) {
|
catch(err) {
|
||||||
|
@ -68,8 +86,6 @@ async function insertDoc(doc) {
|
||||||
|
|
||||||
async function getDoc() {
|
async function getDoc() {
|
||||||
try {
|
try {
|
||||||
const db = client.db(database);
|
|
||||||
const list = db.collection(collection);
|
|
||||||
const result = await list.find({}).toArray();
|
const result = await list.find({}).toArray();
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -82,8 +98,6 @@ async function getDoc() {
|
||||||
|
|
||||||
async function deleteDoc(doc) {
|
async function deleteDoc(doc) {
|
||||||
try {
|
try {
|
||||||
const db = client.db(database);
|
|
||||||
const list = db.collection(collection);
|
|
||||||
const result = await list.deleteOne({"_id": doc});
|
const result = await list.deleteOne({"_id": doc});
|
||||||
|
|
||||||
fs.unlink(config.API.STORAGE_LOCATION + '/LinkWarden/screenshot\'s/' + doc + '.png', (err) => {
|
fs.unlink(config.API.STORAGE_LOCATION + '/LinkWarden/screenshot\'s/' + doc + '.png', (err) => {
|
||||||
|
|
13
src/App.js
13
src/App.js
|
@ -41,15 +41,15 @@ function App() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function exitAdding() {
|
function exitAdding() {
|
||||||
setNewBox(!newBox);
|
setNewBox(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function exitFilter() {
|
function exitFilter() {
|
||||||
setFilterBox(!filterBox);
|
setFilterBox(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function exitSorting() {
|
function exitSorting() {
|
||||||
setSortBox(!sortBox);
|
setSortBox(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function search(e) {
|
function search(e) {
|
||||||
|
@ -59,8 +59,9 @@ function App() {
|
||||||
function sortByFunc(e) {
|
function sortByFunc(e) {
|
||||||
setSortBy(e)
|
setSortBy(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredData = filter(data, searchQuery, nameChecked, tagsChecked, descriptionChecked);
|
const filteredData = filter(data, searchQuery, nameChecked, tagsChecked, descriptionChecked);
|
||||||
|
const tags = concatTags(data);
|
||||||
|
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
const ADDRESS = config.API.ADDRESS + ":" + config.API.PORT;
|
const ADDRESS = config.API.ADDRESS + ":" + config.API.PORT;
|
||||||
|
@ -91,7 +92,7 @@ function App() {
|
||||||
|
|
||||||
<button className='btn' onClick={() => setFilterBox(true)}></button>
|
<button className='btn' onClick={() => setFilterBox(true)}></button>
|
||||||
<button className='btn' onClick={() => setSortBox(true)}></button>
|
<button className='btn' onClick={() => setSortBox(true)}></button>
|
||||||
<List data={filteredData} reFetch={fetchData} />
|
<List SetLoader={SetLoader} data={filteredData} tags={tags} reFetch={fetchData} />
|
||||||
|
|
||||||
{numberOfResults === 0 ? <NoResults /> : null}
|
{numberOfResults === 0 ? <NoResults /> : null}
|
||||||
|
|
||||||
|
@ -114,7 +115,7 @@ function App() {
|
||||||
SetLoader={SetLoader}
|
SetLoader={SetLoader}
|
||||||
onExit={exitAdding}
|
onExit={exitAdding}
|
||||||
reFetch={fetchData}
|
reFetch={fetchData}
|
||||||
tags={() => concatTags(data)}
|
tags={() => tags}
|
||||||
/> : null}
|
/> : null}
|
||||||
|
|
||||||
{loader ? <Loader /> : null}
|
{loader ? <Loader /> : null}
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import '../styles/AddItem.css';
|
import '../styles/AddItem.css';
|
||||||
import TagSelection from './TagSelection';
|
import TagSelection from './TagSelection';
|
||||||
import addItem from '../modules/addItem';
|
import addItem from '../modules/send';
|
||||||
|
|
||||||
const AddItem = ({onExit, reFetch, tags, SetLoader}) => {
|
const AddItem = ({onExit, reFetch, tags, SetLoader}) => {
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [link, setLink] = useState('');
|
const [link, setLink] = useState('');
|
||||||
const [tag, setTag] = useState([]);
|
const [tag, setTag] = useState([]);
|
||||||
|
|
||||||
function newItem() {
|
function newItem() {
|
||||||
SetLoader(true)
|
SetLoader(true)
|
||||||
addItem(name, link, tag, reFetch, onExit, SetLoader);
|
addItem(name, link, tag, reFetch, onExit, SetLoader, "POST");
|
||||||
}
|
}
|
||||||
|
|
||||||
function SetName(e) {
|
function SetName(e) {
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import deleteEntity from '../modules/deleteEntity';
|
||||||
|
import '../styles/AddItem.css';
|
||||||
|
import TagSelection from './TagSelection';
|
||||||
|
import editItem from '../modules/send';
|
||||||
|
|
||||||
|
// deleteEntity(e._id, reFetch)
|
||||||
|
|
||||||
|
const EditItem = ({tags, item, onExit, SetLoader, reFetch}) => {
|
||||||
|
const [name, setName] = useState(item.name);
|
||||||
|
const [tag, setTag] = useState(item.tag);
|
||||||
|
|
||||||
|
function EditItem() {
|
||||||
|
SetLoader(true)
|
||||||
|
editItem(name, item.link, tag, reFetch, onExit, SetLoader, "PUT", item._id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteItem() {
|
||||||
|
SetLoader(true)
|
||||||
|
deleteEntity(item._id, reFetch, onExit, SetLoader)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SetName(e) {
|
||||||
|
setName(e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SetTags(value) {
|
||||||
|
setTag(value);
|
||||||
|
setTag(value.map(e => e.value.toLowerCase()));
|
||||||
|
}
|
||||||
|
|
||||||
|
function abort(e) {
|
||||||
|
if (e.target.className === "add-overlay") {
|
||||||
|
onExit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(item.link);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='add-overlay' onClick={abort}></div>
|
||||||
|
<fieldset className='box'>
|
||||||
|
<legend >Edit bookmark</legend>
|
||||||
|
<button className="edit-btn delete" onClick={deleteItem}></button>
|
||||||
|
<div className='AddItem-content'>
|
||||||
|
<h3>Link: <a target="_blank" rel="noreferrer" href={item.link}>{url.hostname}</a></h3>
|
||||||
|
<h3>{item.title}</h3>
|
||||||
|
|
||||||
|
<h3>Name:</h3>
|
||||||
|
<input onChange={SetName} className="AddItem-input" type="search" value={name} placeholder={"e.g. Example Tutorial"} />
|
||||||
|
<h3>Tags:</h3>
|
||||||
|
<TagSelection setTags={SetTags} tags={tags} tag={tag} />
|
||||||
|
<button onClick={EditItem} className="upload-btn">Update </button>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditItem
|
|
@ -1,44 +1,58 @@
|
||||||
import '../styles/List.css';
|
import '../styles/List.css';
|
||||||
import LazyLoad from 'react-lazyload';
|
import LazyLoad from 'react-lazyload';
|
||||||
import ViewArchived from './ViewArchived';
|
import ViewArchived from './ViewArchived';
|
||||||
import deleteEntity from '../modules/deleteEntity';
|
import EditItem from './EditItem';
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
const List = ({data, reFetch}) => {
|
const List = ({data, tags, reFetch, SetLoader}) => {
|
||||||
return (
|
const [editBox, setEditBox] = useState(false)
|
||||||
<div className="list">
|
const [editIndex, setEditIndex] = useState(0)
|
||||||
{/* eslint-disable-next-line */}
|
|
||||||
{data.map((e, i) => {
|
function edit(index) {
|
||||||
try {
|
setEditBox(true);
|
||||||
const url = new URL(e.link);
|
setEditIndex(index);
|
||||||
const favicon = 'http://www.google.com/s2/favicons?domain=' + url.hostname;
|
}
|
||||||
return (<LazyLoad key={i} height={200} offset={200}>
|
|
||||||
<div className="list-row">
|
function exitEditing() {
|
||||||
<div className="img-content-grp">
|
setEditBox(false);
|
||||||
<img alt='' src={favicon} />
|
}
|
||||||
<div className="list-entity-content">
|
|
||||||
<div className='row-name'>
|
return (
|
||||||
<span className="num">{i + 1}.</span> {e.name} <a target="_blank" rel="noreferrer" href={e.link}>({url.hostname})</a>
|
<div className="list">
|
||||||
</div>
|
{/* eslint-disable-next-line */}
|
||||||
<div>{e.title}</div>
|
{data.map((e, i, array) => {
|
||||||
<div className="tags">
|
try {
|
||||||
{e.tag.map((e, i) => {
|
const url = new URL(e.link);
|
||||||
return (<div key={i}>{e}</div>)
|
const favicon = 'http://www.google.com/s2/favicons?domain=' + url.hostname;
|
||||||
})}
|
return (<LazyLoad key={i} height={200} offset={200}>
|
||||||
|
<div className="list-row">
|
||||||
|
<div className="img-content-grp">
|
||||||
|
<img alt='' src={favicon} />
|
||||||
|
<div className="list-entity-content">
|
||||||
|
<div className='row-name'>
|
||||||
|
<span className="num">{i + 1}.</span> {e.name} <a target="_blank" rel="noreferrer" href={e.link}>({url.hostname})</a>
|
||||||
|
</div>
|
||||||
|
<div>{e.title}</div>
|
||||||
|
<div className="tags">
|
||||||
|
{e.tag.map((e, i) => {
|
||||||
|
return (<div key={i}>{e}</div>)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className='etc'>
|
||||||
|
<ViewArchived className='view-archived' id={e._id} />
|
||||||
|
<button className="edit-btn" onClick={() => edit(i)}></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='options'>
|
</LazyLoad>)
|
||||||
<ViewArchived className='view-archived' id={e._id} />
|
} catch (e) {
|
||||||
<div className="delete" onClick={() => deleteEntity(e._id, reFetch)}></div>
|
console.log(e);
|
||||||
</div>
|
}
|
||||||
</div>
|
})}
|
||||||
</LazyLoad>)
|
{editBox ? <EditItem tags={() => tags} onExit={exitEditing} SetLoader={SetLoader} reFetch={reFetch} item={data[editIndex]} /> : null}
|
||||||
} catch (e) {
|
</div>
|
||||||
console.log(e);
|
)
|
||||||
}
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default List
|
export default List
|
|
@ -42,13 +42,17 @@ const customStyles = {
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TagSelection({setTags, tags}) {
|
export default function TagSelection({setTags, tags, tag=[]}) {
|
||||||
const data = tags().map((e) => {
|
const data = tags().map((e) => {
|
||||||
return { value: e, label: e }
|
return { value: e, label: e }
|
||||||
})
|
})
|
||||||
|
const defaultTags = tag.map((e) => {
|
||||||
|
return { value: e, label: e }
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CreatableSelect
|
<CreatableSelect
|
||||||
|
defaultValue={defaultTags}
|
||||||
styles={customStyles}
|
styles={customStyles}
|
||||||
isMulti
|
isMulti
|
||||||
onChange={setTags}
|
onChange={setTags}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
|
||||||
const deleteEntity = (id, reFetch) => {
|
const deleteEntity = (id, reFetch, onExit, SetLoader) => {
|
||||||
const ADDRESS = config.API.ADDRESS + ":" + config.API.PORT;
|
const ADDRESS = config.API.ADDRESS + ":" + config.API.PORT;
|
||||||
fetch(ADDRESS + "/api", {
|
fetch(ADDRESS + "/api", {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
|
@ -11,7 +11,9 @@ const deleteEntity = (id, reFetch) => {
|
||||||
})
|
})
|
||||||
.then(res => res.text())
|
.then(res => res.text())
|
||||||
.then(message => {console.log(message)})
|
.then(message => {console.log(message)})
|
||||||
|
.then(() => onExit())
|
||||||
.then(() => reFetch())
|
.then(() => reFetch())
|
||||||
|
.then(() => {SetLoader(false)});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default deleteEntity;
|
export default deleteEntity;
|
|
@ -1,7 +1,7 @@
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
const addItem = async (name, link, tag, reFetch, onExit, SetLoader) => {
|
const addItem = async (name, link, tag, reFetch, onExit, SetLoader, method, id=nanoid()) => {
|
||||||
function isValidHttpUrl(string) {
|
function isValidHttpUrl(string) {
|
||||||
let url;
|
let url;
|
||||||
|
|
||||||
|
@ -14,12 +14,12 @@ const addItem = async (name, link, tag, reFetch, onExit, SetLoader) => {
|
||||||
return url.protocol === "http:" || url.protocol === "https:";
|
return url.protocol === "http:" || url.protocol === "https:";
|
||||||
}
|
}
|
||||||
|
|
||||||
if(name !== '' && isValidHttpUrl(link) && tag !== '') {
|
if(isValidHttpUrl(link)) {
|
||||||
const ADDRESS = config.API.ADDRESS + ":" + config.API.PORT;
|
const ADDRESS = config.API.ADDRESS + ":" + config.API.PORT;
|
||||||
fetch(ADDRESS + "/api", {
|
fetch(ADDRESS + "/api", {
|
||||||
method: "POST",
|
method: method,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
_id: nanoid(),
|
_id: id,
|
||||||
name: name,
|
name: name,
|
||||||
title: '',
|
title: '',
|
||||||
link: link,
|
link: link,
|
||||||
|
@ -30,17 +30,14 @@ const addItem = async (name, link, tag, reFetch, onExit, SetLoader) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(res => res.text())
|
.then(res => res.text())
|
||||||
.then(message => {SetLoader(false)})
|
.then(() => reFetch())
|
||||||
.then(() => reFetch());
|
.then(() => {SetLoader(false)});
|
||||||
|
|
||||||
onExit();
|
onExit();
|
||||||
} else if(name !== '' && link !== '' && tag !== '') {
|
} else {
|
||||||
|
SetLoader(false)
|
||||||
alert('Please make sure the link is valid.\n\n(i.e. starts with "http"/"https")');
|
alert('Please make sure the link is valid.\n\n(i.e. starts with "http"/"https")');
|
||||||
}
|
}
|
||||||
|
|
||||||
else {
|
|
||||||
alert('Please fill all fields and make sure the link is valid.\n\n(i.e. starts with "http"/"https")');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default addItem;
|
export default addItem;
|
|
@ -67,20 +67,28 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.delete {
|
.edit-btn {
|
||||||
color: white;
|
position: relative;
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.1s;
|
|
||||||
font-family: 'Font Awesome 5 Free';
|
|
||||||
padding: 10px;
|
|
||||||
width: fit-content;
|
|
||||||
height: fit-content;
|
|
||||||
margin: 10px;
|
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
|
margin: 20px 20px 20px 0px;
|
||||||
|
font-family: 'Font Awesome 5 Free';
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.4) 0px 2px 4px, rgba(0, 0, 0, 0.3) 0px 7px 13px -3px, rgba(0, 0, 0, 0.2) 0px -3px 0px inset;
|
||||||
|
color: #ffffffb6;
|
||||||
|
background-color: #1f2c38;
|
||||||
|
border: none;
|
||||||
|
transition: background-color 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete:hover {
|
.edit-btn:hover {
|
||||||
background-color: rgb(255, 123, 123);
|
background-color: rgb(76, 117, 170);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn:active {
|
||||||
box-shadow: rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px;
|
box-shadow: rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,7 +119,15 @@
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.options {
|
.etc {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.delete {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete:hover {
|
||||||
|
background-color: rgba(255, 65, 65, 0.8);
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
.loader {
|
.loader {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
bottom: 100px;
|
bottom: 10%;
|
||||||
left: 30%;
|
left: 30%;
|
||||||
right: 30%;
|
right: 30%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
Ŝarĝante…
Reference in New Issue