Added update/edit support.

This commit is contained in:
Daniel 2022-06-04 14:37:35 +04:30
parent a28417beeb
commit e57365fd0c
11 changed files with 188 additions and 79 deletions

View File

@ -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) => {

View File

@ -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)}>&#xf0b0;</button> <button className='btn' onClick={() => setFilterBox(true)}>&#xf0b0;</button>
<button className='btn' onClick={() => setSortBox(true)}>&#xf0dc;</button> <button className='btn' onClick={() => setSortBox(true)}>&#xf0dc;</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}

View File

@ -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) {

61
src/componets/EditItem.js Normal file
View File

@ -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}>&#xf2ed;</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 &#xf093;</button>
</div>
</fieldset>
</>
)
}
export default EditItem

View File

@ -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)}>&#xf044;</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)}>&#xf2ed;</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

View File

@ -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}

View File

@ -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;

View File

@ -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;

0
src/styles/EditItem.css Normal file
View File

View File

@ -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);
}

View File

@ -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;