diff --git a/README.md b/README.md index 20db62b..c2039e1 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ LinkWarden A place for your useful links. - LinkWarden.png Twitter Follow @@ -13,16 +12,18 @@ LinkWarden -[Demo](https://linkwarden.netlify.app/) | [Intro & Motivation](https://github.com/Daniel31x13/link-warden#intro--motivation) | [Features](https://github.com/Daniel31x13/link-warden#features) | [Setup](https://github.com/Daniel31x13/link-warden#setup) | [Development](https://github.com/Daniel31x13/link-warden#linkwarden-development) +[Demo](https://linkwarden.netlify.app/) | [Intro & Motivation](https://github.com/Daniel31x13/link-warden#intro--motivation) | [Features](https://github.com/Daniel31x13/link-warden#features) | [Roadmap](https://github.com/Daniel31x13/link-warden/wiki#project-roadmap) | [Setup](https://github.com/Daniel31x13/link-warden#setup) | [Development](https://github.com/Daniel31x13/link-warden#linkwarden-development) ## Intro & Motivation + **LinkWarden is a self-hosted, open-source bookmark + archive manager to collect, and save websites for offline use.** The objective is to have a self-hosted place to keep useful links in one place, and since useful links can go away (see the inevitability of [Link Rot](https://www.howtogeek.com/786227/what-is-link-rot-and-how-does-it-threaten-the-web/)), LinkWarden also saves a copy of the link as screenshot and PDF. ## Features + - [x] Sleek, minimalist design. - [x] Save a copy of each link as screenshot and PDF. - [x] Dark/Light mode support. @@ -34,7 +35,9 @@ The objective is to have a self-hosted place to keep useful links in one place, **Also take a look at our planned features in the [project roadmap section](https://github.com/Daniel31x13/link-warden/wiki#project-roadmap).** ## Setup + ### Linux/MacOS + 1. Make sure your MongoDB database and collection is up and running. 2. Edit [/src/config.js](src/config.js) accordingly. @@ -44,6 +47,7 @@ The objective is to have a self-hosted place to keep useful links in one place, 4. Run `npm start` to start the application. ## LinkWarden Development + All contributions are welcomed! Please take a look at [how to contribute](.github/CONTRIBUTING.md). > **For questions/help, feature requests and bug reports please create an [issue](https://github.com/Daniel31x13/link-warden/issues) (please use the right lable).** diff --git a/api/modules/getData.js b/api/modules/getData.js index dc0476e..3626753 100644 --- a/api/modules/getData.js +++ b/api/modules/getData.js @@ -1,17 +1,18 @@ -const puppeteer = require('puppeteer'); -const { PuppeteerBlocker } = require('@cliqz/adblocker-puppeteer'); -const fetch = require('cross-fetch'); -const config = require('../../src/config.js'); -const fs = require('fs'); +const puppeteer = require("puppeteer"); +const { PuppeteerBlocker } = require("@cliqz/adblocker-puppeteer"); +const fetch = require("cross-fetch"); +const config = require("../../src/config.js"); +const fs = require("fs"); -const screenshotDirectory = config.API.STORAGE_LOCATION + '/LinkWarden/screenshot\'s/'; -const pdfDirectory = config.API.STORAGE_LOCATION + '/LinkWarden/pdf\'s/'; +const screenshotDirectory = + config.API.STORAGE_LOCATION + "/LinkWarden/screenshot's/"; +const pdfDirectory = config.API.STORAGE_LOCATION + "/LinkWarden/pdf's/"; -if (!fs.existsSync(screenshotDirectory)){ +if (!fs.existsSync(screenshotDirectory)) { fs.mkdirSync(screenshotDirectory, { recursive: true }); } -if (!fs.existsSync(pdfDirectory)){ +if (!fs.existsSync(pdfDirectory)) { fs.mkdirSync(pdfDirectory, { recursive: true }); } @@ -22,11 +23,14 @@ module.exports = async (link, id) => { await PuppeteerBlocker.fromPrebuiltAdsAndTracking(fetch).then((blocker) => { blocker.enableBlockingInPage(page); }); - - await page.goto(link, { waitUntil: 'load', timeout: 0 }); - await page.screenshot({ path: screenshotDirectory + id + '.png', fullPage: true}); - await page.pdf({ path: pdfDirectory + id + '.pdf', format: 'a4' }); + await page.goto(link, { waitUntil: "load", timeout: 0 }); + + await page.screenshot({ + path: screenshotDirectory + id + ".png", + fullPage: true, + }); + await page.pdf({ path: pdfDirectory + id + ".pdf", format: "a4" }); await browser.close(); -} +}; diff --git a/api/pages/404.html b/api/pages/404.html index 7cd5206..5348452 100644 --- a/api/pages/404.html +++ b/api/pages/404.html @@ -1,15 +1,20 @@ - - - - + + + + Document - - + +

404: NOT FOUND

-

If you are trying to access a recently added Screenshot/PDF, just wait a bit more for it to be uploaded.

+

+ If you are trying to access a recently added Screenshot/PDF, just wait a + bit more for it to be uploaded. +

If the problem persists, looks like the file wasn't created...

¯\_(ツ)_/¯

- - \ No newline at end of file + + diff --git a/api/server.js b/api/server.js index dd1630e..b234173 100644 --- a/api/server.js +++ b/api/server.js @@ -1,11 +1,11 @@ -const express = require('express'); +const express = require("express"); const app = express(); -const { MongoClient } = require('mongodb'); -const cors = require('cors'); -const config = require('../src/config.js'); -const getData = require('./modules/getData.js'); -const fs = require('fs'); -const fetch = require('cross-fetch'); +const { MongoClient } = require("mongodb"); +const cors = require("cors"); +const config = require("../src/config.js"); +const getData = require("./modules/getData.js"); +const fs = require("fs"); +const fetch = require("cross-fetch"); const port = config.API.PORT; @@ -21,39 +21,45 @@ app.use(cors()); app.use(express.json()); -app.get('/api', async (req, res) => { +app.get("/api", async (req, res) => { const data = await getDoc(); res.send(data); }); -app.get('/screenshots/:id', async (req, res) => { - res.sendFile(config.API.STORAGE_LOCATION + '/LinkWarden/screenshot\'s/' + req.params.id, (err) => { - if (err) { - res.sendFile(__dirname +'/pages/404.html'); +app.get("/screenshots/:id", async (req, res) => { + res.sendFile( + config.API.STORAGE_LOCATION + "/LinkWarden/screenshot's/" + req.params.id, + (err) => { + if (err) { + res.sendFile(__dirname + "/pages/404.html"); + } } - }); + ); }); -app.get('/pdfs/:id', async (req, res) => { - res.sendFile(config.API.STORAGE_LOCATION + '/LinkWarden/pdf\'s/' + req.params.id, (err) => { - if (err) { - res.sendFile(__dirname +'/pages/404.html'); +app.get("/pdfs/:id", async (req, res) => { + res.sendFile( + config.API.STORAGE_LOCATION + "/LinkWarden/pdf's/" + req.params.id, + (err) => { + if (err) { + res.sendFile(__dirname + "/pages/404.html"); + } } - }); + ); }); -app.post('/api', async (req, res) => { +app.post("/api", async (req, res) => { const pageToVisit = req.body.link; const id = req.body._id; - const getTitle = async(url) => { + const getTitle = async (url) => { let body; await fetch(url) - .then(res => res.text()) - .then(text => body = text) + .then((res) => res.text()) + .then((text) => (body = text)); // regular expression to parse contents of the tag let match = body.match(/<title>([^<]*)<\/title>/); return match[1]; - } + }; try { req.body.title = await getTitle(req.body.link); @@ -67,15 +73,15 @@ app.post('/api', async (req, res) => { } }); -app.put('/api', async (req, res) => { +app.put("/api", async (req, res) => { const id = req.body._id; await updateDoc(id, req.body); - res.send('Updated!'); + res.send("Updated!"); }); -app.delete('/api', async (req, res) => { +app.delete("/api", async (req, res) => { const id = req.body.id; await deleteDoc(id); @@ -85,10 +91,8 @@ app.delete('/api', async (req, res) => { async function updateDoc(id, updatedListing) { try { - await list.updateOne({ _id: id }, { $set: updatedListing }); - } - - catch(err) { + await list.updateOne({ _id: id }, { $set: updatedListing }); + } catch (err) { console.log(err); } } @@ -96,9 +100,7 @@ async function updateDoc(id, updatedListing) { async function insertDoc(doc) { try { await list.insertOne(doc); - } - - catch(err) { + } catch (err) { console.log(err); } } @@ -108,34 +110,35 @@ async function getDoc() { const result = await list.find({}).toArray(); return result; - } - - catch(err) { + } catch (err) { console.log(err); } } async function deleteDoc(doc) { try { - 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) => { - if (err) { - console.log(err); - } - }); - - fs.unlink(config.API.STORAGE_LOCATION + '/LinkWarden/pdf\'s/' + doc + '.pdf', (err) => { - if (err) { + fs.unlink( + config.API.STORAGE_LOCATION + "/LinkWarden/screenshot's/" + doc + ".png", + (err) => { + if (err) { console.log(err); + } } - }); + ); + fs.unlink( + config.API.STORAGE_LOCATION + "/LinkWarden/pdf's/" + doc + ".pdf", + (err) => { + if (err) { + console.log(err); + } + } + ); return result; - } - - catch(err) { + } catch (err) { console.log(err); } } diff --git a/src/App.js b/src/App.js index c0cd014..9de48a7 100644 --- a/src/App.js +++ b/src/App.js @@ -1,17 +1,17 @@ -import { useEffect, useState } from 'react'; -import './styles/App.css'; -import List from './componets/List'; -import AddItem from './componets/AddItem'; -import config from './config'; -import Filters from './componets/Filters'; -import Sort from './componets/Sort'; -import sortList from './modules/sortList'; -import filter from './modules/filterData'; -import concatTags from './modules/concatTags'; -import NoResults from './componets/NoResults'; -import Loader from './componets/Loader'; -import SideBar from './componets/SideBar'; -import Tags from './routes/Tags.js'; +import { useEffect, useState } from "react"; +import "./styles/App.css"; +import List from "./componets/List"; +import AddItem from "./componets/AddItem"; +import config from "./config"; +import Filters from "./componets/Filters"; +import Sort from "./componets/Sort"; +import sortList from "./modules/sortList"; +import filter from "./modules/filterData"; +import concatTags from "./modules/concatTags"; +import NoResults from "./componets/NoResults"; +import Loader from "./componets/Loader"; +import SideBar from "./componets/SideBar"; +import Tags from "./routes/Tags.js"; import { Route, Routes } from "react-router-dom"; function App() { @@ -19,14 +19,16 @@ function App() { [newBox, setNewBox] = useState(false), [filterBox, setFilterBox] = useState(false), [sortBox, setSortBox] = useState(false), - [searchQuery, setSearchQuery] = useState(''), + [searchQuery, setSearchQuery] = useState(""), [numberOfResults, setNumberOfResults] = useState(0), [nameChecked, setNameChecked] = useState(true), [descriptionChecked, setDescriptionChecked] = useState(true), [tagsChecked, setTagsChecked] = useState(true), - [sortBy, setSortBy] = useState('Default'), + [sortBy, setSortBy] = useState("Default"), [loader, setLoader] = useState(false), - [lightMode, setLightMode] = useState(localStorage.getItem('light-mode') === 'true'), + [lightMode, setLightMode] = useState( + localStorage.getItem("light-mode") === "true" + ), [toggle, setToggle] = useState(false); function SetLoader(x) { @@ -66,16 +68,22 @@ function App() { } function handleToggleSidebar() { - setToggle(!toggle) + setToggle(!toggle); } - const filteredData = filter(data, searchQuery, nameChecked, tagsChecked, descriptionChecked); + const filteredData = filter( + data, + searchQuery, + nameChecked, + tagsChecked, + descriptionChecked + ); const tags = concatTags(data); async function fetchData() { const ADDRESS = config.API.ADDRESS + ":" + config.API.PORT; - const res = await fetch(ADDRESS + '/api'); + const res = await fetch(ADDRESS + "/api"); const resJSON = await res.json(); const data = resJSON.reverse(); setData(data); @@ -104,73 +112,117 @@ function App() { document.body.classList.remove("light"); } - localStorage.setItem('light-mode', lightMode); + localStorage.setItem("light-mode", lightMode); }, [lightMode]); - return ( <div className="App"> - <SideBar tags={tags} handleToggleSidebar={handleToggleSidebar} toggle={toggle} /> + <SideBar + tags={tags} + handleToggleSidebar={handleToggleSidebar} + toggle={toggle} + /> - <div className='content'> + <div className="content"> <div className="head"> - <button className='sidebar-btn btn' style={{marginRight: '10px'}} onClick={handleToggleSidebar}></button> - <input className="search" type="search" placeholder=" Search" onChange={search}/> - <button className="add-btn btn" onClick={() => setNewBox(true)}></button> - <button className="dark-light-btn btn" onClick={() => setLightMode(!lightMode)}></button> + <button + className="sidebar-btn btn" + style={{ marginRight: "10px" }} + onClick={handleToggleSidebar} + > +  + </button> + <input + className="search" + type="search" + placeholder=" Search" + onChange={search} + /> + <button className="add-btn btn" onClick={() => setNewBox(true)}> +  + </button> + <button + className="dark-light-btn btn" + onClick={() => setLightMode(!lightMode)} + ></button> </div> - {numberOfResults > 0 ? <p className="results">{numberOfResults} Bookmarks found</p> : null} + {numberOfResults > 0 ? ( + <p className="results">{numberOfResults} Bookmarks found</p> + ) : null} + + <button + className="btn" + style={{ marginTop: "10px" }} + onClick={() => setFilterBox(true)} + > +  + </button> + <button + className="btn" + style={{ marginLeft: "10px" }} + onClick={() => setSortBox(true)} + > +  + </button> - <button className='btn' style={{marginTop: '10px'}} onClick={() => setFilterBox(true)}></button> - <button className='btn' style={{marginLeft: '10px'}} onClick={() => setSortBox(true)}></button> - {numberOfResults === 0 ? <NoResults /> : null} - {sortBox ? <Sort - sortBy={sortByFunc} - onExit={exitSorting} - /> : null} + {sortBox ? <Sort sortBy={sortByFunc} onExit={exitSorting} /> : null} - {filterBox ? <Filters - nameChecked={nameChecked} - handleNameCheckbox={handleNameCheckbox} - descriptionChecked={descriptionChecked} - handleDescriptionCheckbox={handleDescriptionCheckbox} - tagsChecked={tagsChecked} - handleTagsCheckbox={handleTagsCheckbox} - onExit={exitFilter} - /> : null} + {filterBox ? ( + <Filters + nameChecked={nameChecked} + handleNameCheckbox={handleNameCheckbox} + descriptionChecked={descriptionChecked} + handleDescriptionCheckbox={handleDescriptionCheckbox} + tagsChecked={tagsChecked} + handleTagsCheckbox={handleTagsCheckbox} + onExit={exitFilter} + /> + ) : null} - {newBox ? <AddItem - SetLoader={SetLoader} - onExit={exitAdding} - reFetch={fetchData} - lightMode={lightMode} - tags={() => tags} - /> : null} + {newBox ? ( + <AddItem + SetLoader={SetLoader} + onExit={exitAdding} + reFetch={fetchData} + lightMode={lightMode} + tags={() => tags} + /> + ) : null} {loader ? <Loader lightMode={lightMode} /> : null} </div> <Routes> - <Route path="/" element={<div className='content'> - <List - lightMode={lightMode} - SetLoader={SetLoader} - data={filteredData} - tags={tags} - reFetch={fetchData} - /> - </div>} /> + <Route + path="/" + element={ + <div className="content"> + <List + lightMode={lightMode} + SetLoader={SetLoader} + data={filteredData} + tags={tags} + reFetch={fetchData} + /> + </div> + } + /> - <Route path="tags/:tagId" element={<Tags - lightMode={lightMode} - SetLoader={SetLoader} - data={filteredData} - tags={tags} - reFetch={fetchData} - />} /> + <Route + path="tags/:tagId" + element={ + <Tags + lightMode={lightMode} + SetLoader={SetLoader} + data={filteredData} + tags={tags} + reFetch={fetchData} + /> + } + /> </Routes> </div> ); diff --git a/src/componets/AddItem.js b/src/componets/AddItem.js index 56136d4..6ec4e22 100644 --- a/src/componets/AddItem.js +++ b/src/componets/AddItem.js @@ -1,15 +1,15 @@ -import { useState } from 'react'; -import '../styles/SendItem.css'; -import TagSelection from './TagSelection'; -import addItem from '../modules/send'; +import { useState } from "react"; +import "../styles/SendItem.css"; +import TagSelection from "./TagSelection"; +import addItem from "../modules/send"; -const AddItem = ({onExit, reFetch, tags, SetLoader, lightMode}) => { - const [name, setName] = useState(''); - const [link, setLink] = useState(''); +const AddItem = ({ onExit, reFetch, tags, SetLoader, lightMode }) => { + const [name, setName] = useState(""); + const [link, setLink] = useState(""); const [tag, setTag] = useState([]); - + function newItem() { - SetLoader(true) + SetLoader(true); addItem(name, link, tag, reFetch, onExit, SetLoader, "POST"); } @@ -23,7 +23,7 @@ const AddItem = ({onExit, reFetch, tags, SetLoader, lightMode}) => { function SetTags(value) { setTag(value); - setTag(value.map(e => e.value.toLowerCase())); + setTag(value.map((e) => e.value.toLowerCase())); } function abort(e) { @@ -34,23 +34,41 @@ const AddItem = ({onExit, reFetch, tags, SetLoader, lightMode}) => { return ( <> - <div className='add-overlay' onClick={abort}></div> - <div className='send-box'> - <fieldset className='box'> + <div className="add-overlay" onClick={abort}></div> + <div className="send-box"> + <fieldset className="box"> <legend>New bookmark</legend> - <div className='AddItem-content'> - <h3><span style={{color:"red"}}>* </span>Link:</h3> - <input onChange={SetLink} className="AddItem-input" type="search" placeholder="e.g. https://example.com/"/> - <h3>Name: <span className='optional'>(Optional)</span></h3> - <input onChange={SetName} className="AddItem-input" type="search" placeholder="e.g. Example Tutorial"/> - <h3>Tags: <span className='optional'>(Optional)</span></h3> + <div className="AddItem-content"> + <h3> + <span style={{ color: "red" }}>* </span>Link: + </h3> + <input + onChange={SetLink} + className="AddItem-input" + type="search" + placeholder="e.g. https://example.com/" + /> + <h3> + Name: <span className="optional">(Optional)</span> + </h3> + <input + onChange={SetName} + className="AddItem-input" + type="search" + placeholder="e.g. Example Tutorial" + /> + <h3> + Tags: <span className="optional">(Optional)</span> + </h3> <TagSelection setTags={SetTags} tags={tags} lightMode={lightMode} /> - <button onClick={newItem} className="send-btn">Add </button> + <button onClick={newItem} className="send-btn"> + Add  + </button> </div> </fieldset> </div> </> - ) -} + ); +}; export default AddItem; diff --git a/src/componets/EditItem.js b/src/componets/EditItem.js index 5878296..38c35da 100644 --- a/src/componets/EditItem.js +++ b/src/componets/EditItem.js @@ -1,16 +1,26 @@ -import { useState } from 'react'; -import deleteEntity from '../modules/deleteEntity'; -import '../styles/SendItem.css'; -import TagSelection from './TagSelection'; -import editItem from '../modules/send'; +import { useState } from "react"; +import deleteEntity from "../modules/deleteEntity"; +import "../styles/SendItem.css"; +import TagSelection from "./TagSelection"; +import editItem from "../modules/send"; -const EditItem = ({tags, item, onExit, SetLoader, reFetch, lightMode }) => { +const EditItem = ({ tags, item, onExit, SetLoader, reFetch, lightMode }) => { 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, item.title); + editItem( + name, + item.link, + tag, + reFetch, + onExit, + SetLoader, + "PUT", + item._id, + item.title + ); } function deleteItem() { @@ -24,7 +34,7 @@ const EditItem = ({tags, item, onExit, SetLoader, reFetch, lightMode }) => { function SetTags(value) { setTag(value); - setTag(value.map(e => e.value.toLowerCase())); + setTag(value.map((e) => e.value.toLowerCase())); } function abort(e) { @@ -37,25 +47,56 @@ const EditItem = ({tags, item, onExit, SetLoader, reFetch, lightMode }) => { return ( <> - <div className='add-overlay' onClick={abort}></div> - <div className='send-box'> - <fieldset className='box'> - <legend >Edit bookmark</legend> - <button className="delete" onClick={deleteItem}></button> - <div className='AddItem-content'> - <h3>Link: <a className='link' target="_blank" rel="noreferrer" href={item.link}>{url.hostname}</a></h3> - <h3 className='title'><b>{item.title}</b></h3> - - <h3>Name: <span className='optional'>(Optional)</span></h3> - <input onChange={SetName} className="AddItem-input" type="search" value={name} placeholder={"e.g. Example Tutorial"} /> - <h3>Tags: <span className='optional'>(Optional)</span></h3> - <TagSelection setTags={SetTags} tags={tags} tag={tag} lightMode={lightMode} /> - <button onClick={EditItem} className="send-btn">Update </button> + <div className="add-overlay" onClick={abort}></div> + <div className="send-box"> + <fieldset className="box"> + <legend>Edit bookmark</legend> + <button className="delete" onClick={deleteItem}> +  + </button> + <div className="AddItem-content"> + <h3> + Link:{" "} + <a + className="link" + target="_blank" + rel="noreferrer" + href={item.link} + > + {url.hostname} + </a> + </h3> + <h3 className="title"> + <b>{item.title}</b> + </h3> + + <h3> + Name: <span className="optional">(Optional)</span> + </h3> + <input + onChange={SetName} + className="AddItem-input" + type="search" + value={name} + placeholder={"e.g. Example Tutorial"} + /> + <h3> + Tags: <span className="optional">(Optional)</span> + </h3> + <TagSelection + setTags={SetTags} + tags={tags} + tag={tag} + lightMode={lightMode} + /> + <button onClick={EditItem} className="send-btn"> + Update  + </button> </div> </fieldset> </div> </> - ) -} + ); +}; -export default EditItem \ No newline at end of file +export default EditItem; diff --git a/src/componets/Filters.js b/src/componets/Filters.js index 1c496b1..ffbc093 100644 --- a/src/componets/Filters.js +++ b/src/componets/Filters.js @@ -1,25 +1,54 @@ -import '../styles/Filters.css' +import "../styles/Filters.css"; -const Filters = ({nameChecked, handleNameCheckbox, descriptionChecked, handleDescriptionCheckbox, tagsChecked, handleTagsCheckbox, onExit}) => { - function abort(e) { - if (e.target.className === "filter-overlay") { - onExit(); - } +const Filters = ({ + nameChecked, + handleNameCheckbox, + descriptionChecked, + handleDescriptionCheckbox, + tagsChecked, + handleTagsCheckbox, + onExit, +}) => { + function abort(e) { + if (e.target.className === "filter-overlay") { + onExit(); } + } return ( - <> - <div className='filter-overlay' onClick={abort}></div> - <div className='filter-box'> - <fieldset className='filter'> - <legend >Filter by</legend> - <label><input type="checkbox" checked={nameChecked} onChange={handleNameCheckbox} />Name</label> - <label><input type="checkbox" checked={descriptionChecked} onChange={handleDescriptionCheckbox} />Website title</label> - <label><input type="checkbox" checked={tagsChecked} onChange={handleTagsCheckbox} />Tags</label> - </fieldset> - </div> + <> + <div className="filter-overlay" onClick={abort}></div> + <div className="filter-box"> + <fieldset className="filter"> + <legend>Filter by</legend> + <label> + <input + type="checkbox" + checked={nameChecked} + onChange={handleNameCheckbox} + /> + Name + </label> + <label> + <input + type="checkbox" + checked={descriptionChecked} + onChange={handleDescriptionCheckbox} + /> + Website title + </label> + <label> + <input + type="checkbox" + checked={tagsChecked} + onChange={handleTagsCheckbox} + /> + Tags + </label> + </fieldset> + </div> </> - ) -} + ); +}; -export default Filters \ No newline at end of file +export default Filters; diff --git a/src/componets/List.js b/src/componets/List.js index 88a933b..ee8d87c 100644 --- a/src/componets/List.js +++ b/src/componets/List.js @@ -1,77 +1,90 @@ -import '../styles/List.css'; -import LazyLoad from 'react-lazyload'; -import ViewArchived from './ViewArchived'; -import EditItem from './EditItem'; -import { useState } from 'react' +import "../styles/List.css"; +import LazyLoad from "react-lazyload"; +import ViewArchived from "./ViewArchived"; +import EditItem from "./EditItem"; +import { useState } from "react"; import { Link } from "react-router-dom"; -const List = ({data, tags, reFetch, SetLoader, lightMode}) => { - const [editBox, setEditBox] = useState(false) - const [editIndex, setEditIndex] = useState(0) +const List = ({ data, tags, reFetch, SetLoader, lightMode }) => { + const [editBox, setEditBox] = useState(false); + const [editIndex, setEditIndex] = useState(0); - function edit(index) { - setEditBox(true); - setEditIndex(index); - } + function edit(index) { + setEditBox(true); + setEditIndex(index); + } - function exitEditing() { - setEditBox(false); - } + function exitEditing() { + setEditBox(false); + } - return ( - <div className="list"> - {editBox ? <EditItem - lightMode={lightMode} - tags={() => tags} - onExit={exitEditing} - SetLoader={SetLoader} - reFetch={reFetch} - item={data[editIndex]} - /> : null} - {/* eslint-disable-next-line */} - {data.map((e, i, array) => { - try { - const url = new URL(e.link); - const favicon = 'https://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 - className='link' - target="_blank" - rel="noreferrer" - href={e.link} - > - ({url.hostname}) - </a> - </div> - <div className='title'>{e.title}</div> - <div className="tags"> - {e.tag.map((e, i) => { - const tagPath = `/tags/${e}`; - return (<Link to={tagPath} key={i}>{e}</Link>) - })} - </div> - <div className='date'>{new Date(e.date).toDateString()}</div> - </div> - </div> - <div className='etc'> - <ViewArchived className='view-archived' id={e._id} /> - <button className="btn edit-btn" onClick={() => edit(i)}></button> - </div> - </div> - </LazyLoad>) - } catch (e) { - console.log(e); - } - })} - </div> - ) -} + return ( + <div className="list"> + {editBox ? ( + <EditItem + lightMode={lightMode} + tags={() => tags} + onExit={exitEditing} + SetLoader={SetLoader} + reFetch={reFetch} + item={data[editIndex]} + /> + ) : null} + {/* eslint-disable-next-line */} + {data.map((e, i, array) => { + try { + const url = new URL(e.link); + const favicon = + "https://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 + className="link" + target="_blank" + rel="noreferrer" + href={e.link} + > + ({url.hostname}) + </a> + </div> + <div className="title">{e.title}</div> + <div className="tags"> + {e.tag.map((e, i) => { + const tagPath = `/tags/${e}`; + return ( + <Link to={tagPath} key={i}> + {e} + </Link> + ); + })} + </div> + <div className="date"> + {new Date(e.date).toDateString()} + </div> + </div> + </div> + <div className="etc"> + <ViewArchived className="view-archived" id={e._id} /> + <button className="btn edit-btn" onClick={() => edit(i)}> +  + </button> + </div> + </div> + </LazyLoad> + ); + } catch (e) { + console.log(e); + } + })} + </div> + ); +}; -export default List \ No newline at end of file +export default List; diff --git a/src/componets/Loader.js b/src/componets/Loader.js index 944b066..f60b308 100644 --- a/src/componets/Loader.js +++ b/src/componets/Loader.js @@ -1,13 +1,12 @@ -import '../styles/Loader.css'; -import { InfinitySpin } from 'react-loader-spinner' - +import "../styles/Loader.css"; +import { InfinitySpin } from "react-loader-spinner"; const Loader = ({ lightMode }) => { return ( - <div className='loader'> + <div className="loader"> <InfinitySpin color={lightMode ? "Black" : "White"} /> </div> - ) -} + ); +}; -export default Loader \ No newline at end of file +export default Loader; diff --git a/src/componets/NoResults.js b/src/componets/NoResults.js index 450e5f6..6db3f49 100644 --- a/src/componets/NoResults.js +++ b/src/componets/NoResults.js @@ -1,12 +1,12 @@ -import React from 'react' +import React from "react"; const NoResults = () => { return ( - <div className='no-results'> - <h1>¯\_(ツ)_/¯</h1> - <p>Nothing found.</p> + <div className="no-results"> + <h1>¯\_(ツ)_/¯</h1> + <p>Nothing found.</p> </div> - ) -} + ); +}; -export default NoResults; \ No newline at end of file +export default NoResults; diff --git a/src/componets/SideBar.js b/src/componets/SideBar.js index 012e1ca..d563143 100644 --- a/src/componets/SideBar.js +++ b/src/componets/SideBar.js @@ -1,47 +1,62 @@ -import { ProSidebar, SidebarHeader, SidebarFooter, SidebarContent, Menu, MenuItem, SubMenu } from 'react-pro-sidebar'; -import 'react-pro-sidebar/dist/css/styles.css'; -import '../styles/SideBar.css'; +import { + ProSidebar, + SidebarHeader, + SidebarFooter, + SidebarContent, + Menu, + MenuItem, + SubMenu, +} from "react-pro-sidebar"; +import "react-pro-sidebar/dist/css/styles.css"; +import "../styles/SideBar.css"; import { Link } from "react-router-dom"; const SideBar = ({ tags, handleToggleSidebar, toggle }) => { - const sortedTags = tags.sort((a, b) => { - const A = a.toLowerCase(), B = b.toLowerCase(); - if (A < B) - return -1; - if (A > B) - return 1; - return 0; - }); + const sortedTags = tags.sort((a, b) => { + const A = a.toLowerCase(), + B = b.toLowerCase(); + if (A < B) return -1; + if (A > B) return 1; + return 0; + }); return ( - <ProSidebar - toggled={toggle} - breakPoint="lg" - onToggle={handleToggleSidebar} - className='sidebar'> - <SidebarHeader> + <ProSidebar + toggled={toggle} + breakPoint="lg" + onToggle={handleToggleSidebar} + className="sidebar" + > + <SidebarHeader> <h1>LinkWarden</h1> - </SidebarHeader> - <SidebarContent className='sidebar-content'> + </SidebarHeader> + <SidebarContent className="sidebar-content"> <Menu iconShape="circle"> - - <MenuItem><Link to="/"><h3>Show Everything</h3></Link></MenuItem> - - <SubMenu icon='#' defaultOpen={true} title='Tags'> - {sortedTags.map((e, i) => { - const path = `/tags/${e}`; - return <MenuItem key={i}><Link to={path}>{e}</Link></MenuItem> - })} - </SubMenu> - - - - </Menu> - </SidebarContent> - <SidebarFooter> - <p className='credits'>©{new Date().getFullYear()} Made with 💙 by <a href='https://github.com/Daniel31x13'>Daniel 31X13</a></p> - </SidebarFooter> - </ProSidebar> - ) -} + <MenuItem> + <Link to="/"> + <h3>Show Everything</h3> + </Link> + </MenuItem> -export default SideBar \ No newline at end of file + <SubMenu icon="#" defaultOpen={true} title="Tags"> + {sortedTags.map((e, i) => { + const path = `/tags/${e}`; + return ( + <MenuItem key={i}> + <Link to={path}>{e}</Link> + </MenuItem> + ); + })} + </SubMenu> + </Menu> + </SidebarContent> + <SidebarFooter> + <p className="credits"> + ©{new Date().getFullYear()} Made with 💙 by{" "} + <a href="https://github.com/Daniel31x13">Daniel 31X13</a> + </p> + </SidebarFooter> + </ProSidebar> + ); +}; + +export default SideBar; diff --git a/src/componets/Sort.js b/src/componets/Sort.js index f8e828e..9ce7468 100644 --- a/src/componets/Sort.js +++ b/src/componets/Sort.js @@ -1,32 +1,44 @@ -import '../styles/Sort.css' +import "../styles/Sort.css"; const Sort = ({ sortBy, onExit }) => { - function abort(e) { - if (e.target.className === "sort-overlay") { - onExit(); - } + function abort(e) { + if (e.target.className === "sort-overlay") { + onExit(); } + } - function sort(e) { - sortBy(e.target.value); - } + function sort(e) { + sortBy(e.target.value); + } return ( <> - <div className='sort-overlay' onClick={abort}></div> - <div className='sort-box'> - <fieldset className='sort' onClick={sort}> - <legend>Sort by</legend> - <button className='sort-by-btn' value='Default'> Date (Newest first)</button> - <button className='sort-by-btn' value='Date (Oldest first)'> Date (Oldest first)</button> - <button className='sort-by-btn' value='Name (A-Z)'> Name (A-Z)</button> - <button className='sort-by-btn' value='Name (Z-A)'> Name (Z-A)</button> - <button className='sort-by-btn' value='Title (A-Z)'> Website title (A-Z)</button> - <button className='sort-by-btn' value='Title (Z-A)'> Website title (Z-A)</button> - </fieldset> - </div> + <div className="sort-overlay" onClick={abort}></div> + <div className="sort-box"> + <fieldset className="sort" onClick={sort}> + <legend>Sort by</legend> + <button className="sort-by-btn" value="Default"> +  Date (Newest first) + </button> + <button className="sort-by-btn" value="Date (Oldest first)"> +  Date (Oldest first) + </button> + <button className="sort-by-btn" value="Name (A-Z)"> +  Name (A-Z) + </button> + <button className="sort-by-btn" value="Name (Z-A)"> +  Name (Z-A) + </button> + <button className="sort-by-btn" value="Title (A-Z)"> +  Website title (A-Z) + </button> + <button className="sort-by-btn" value="Title (Z-A)"> +  Website title (Z-A) + </button> + </fieldset> + </div> </> - ) -} + ); +}; -export default Sort \ No newline at end of file +export default Sort; diff --git a/src/componets/TagSelection.js b/src/componets/TagSelection.js index bdc6c80..3f36e76 100644 --- a/src/componets/TagSelection.js +++ b/src/componets/TagSelection.js @@ -1,68 +1,71 @@ import CreatableSelect from "react-select/creatable"; // lightMode ? "Black" : "White" -export default function TagSelection({setTags, tags, tag=[], lightMode}) { +export default function TagSelection({ setTags, tags, tag = [], lightMode }) { const customStyles = { container: (provided) => ({ ...provided, - textShadow: 'none', + textShadow: "none", }), - + placeholder: (provided) => ({ ...provided, - color: '#a9a9a9', + color: "#a9a9a9", }), - + multiValueRemove: (provided) => ({ ...provided, - color: 'gray', + color: "gray", }), - + indicatorSeparator: (provided) => ({ ...provided, - display: 'none', + display: "none", }), - + menu: (provided) => ({ ...provided, - border: 'solid', - borderWidth: '1px', - borderRadius: '0px', - borderColor: 'rgb(141, 141, 141)', - opacity: '90%', - color: 'gray', + border: "solid", + borderWidth: "1px", + borderRadius: "0px", + borderColor: "rgb(141, 141, 141)", + opacity: "90%", + color: "gray", background: lightMode ? "lightyellow" : "#273949", - boxShadow: 'rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px', + boxShadow: + "rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px", }), - + input: (provided) => ({ ...provided, color: lightMode ? "rgb(64, 64, 64)" : "white", - }), - + }), + control: (provided, state) => ({ ...provided, background: lightMode ? "lightyellow" : "#273949", - border: 'none', - borderRadius: '0px', - boxShadow: state.isFocused ? 'rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px' : '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', + border: "none", + borderRadius: "0px", + boxShadow: state.isFocused + ? "rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px" + : "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", }), - } + }; 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 { value: e, label: e }; + }); return ( <CreatableSelect - defaultValue={defaultTags} - styles={customStyles} - isMulti - onChange={setTags} - options={data} + defaultValue={defaultTags} + styles={customStyles} + isMulti + onChange={setTags} + options={data} /> ); } diff --git a/src/componets/ViewArchived.js b/src/componets/ViewArchived.js index c64ab80..dcb9b0e 100644 --- a/src/componets/ViewArchived.js +++ b/src/componets/ViewArchived.js @@ -1,17 +1,28 @@ -import '../styles/ViewArchived.css'; -import config from '../config'; +import "../styles/ViewArchived.css"; +import config from "../config"; const ViewArchived = ({ id }) => { - const screenshotPath = config.API.ADDRESS + ":" + config.API.PORT + '/screenshots/' + id + '.png'; - const pdfPath = config.API.ADDRESS + ":" + config.API.PORT + '/pdfs/' + id + '.pdf'; + const screenshotPath = + config.API.ADDRESS + ":" + config.API.PORT + "/screenshots/" + id + ".png"; + const pdfPath = + config.API.ADDRESS + ":" + config.API.PORT + "/pdfs/" + id + ".pdf"; return ( - <div className='view-archived'> - <a className='link' href={screenshotPath} target='_blank' rel="noreferrer">Screenshot</a> - <hr className='seperator' /> - <a className='link' href={pdfPath} target='_blank' rel="noreferrer">PDF</a> + <div className="view-archived"> + <a + className="link" + href={screenshotPath} + target="_blank" + rel="noreferrer" + > + Screenshot + </a> + <hr className="seperator" /> + <a className="link" href={pdfPath} target="_blank" rel="noreferrer"> + PDF + </a> </div> - ) -} + ); +}; -export default ViewArchived; \ No newline at end of file +export default ViewArchived; diff --git a/src/config.js b/src/config.js index 1d88df8..7dc4bca 100644 --- a/src/config.js +++ b/src/config.js @@ -1,12 +1,12 @@ // Note: the formatting are really sensitive so for example DO NOT end // the "STORAGE_LOCATION" path with an extra slash "/" (i.e. "/home/") module.exports = { - "API": { - "ADDRESS": "http://192.168.1.7", // IP address of the computer which LinkWarden is running - "PORT": 5000, // The api port - "MONGODB_URI": "mongodb://localhost:27017", // MongoDB link - "DB_NAME": "sample_db", // MongoDB database name - "COLLECTION_NAME": "list", // MongoDB collection name - "STORAGE_LOCATION": "/home/danny/Documents" // The path to store the archived data - } -} \ No newline at end of file + API: { + ADDRESS: "http://192.168.1.7", // IP address of the computer which LinkWarden is running + PORT: 5000, // The api port + MONGODB_URI: "mongodb://localhost:27017", // MongoDB link + DB_NAME: "sample_db", // MongoDB database name + COLLECTION_NAME: "list", // MongoDB collection name + STORAGE_LOCATION: "/home/danny/Documents", // The path to store the archived data + }, +}; diff --git a/src/index.js b/src/index.js index 55f7c67..bd6f7fb 100644 --- a/src/index.js +++ b/src/index.js @@ -11,4 +11,4 @@ root.render( <App /> </BrowserRouter> </React.StrictMode> -); \ No newline at end of file +); diff --git a/src/modules/concatTags.js b/src/modules/concatTags.js index 2c9ad4f..0d99303 100644 --- a/src/modules/concatTags.js +++ b/src/modules/concatTags.js @@ -1,13 +1,13 @@ const concatTags = (data) => { - let tags = []; + let tags = []; - for (let i = 0; i < data.length; i++) { - tags = tags.concat(data[i].tag) - } + for (let i = 0; i < data.length; i++) { + tags = tags.concat(data[i].tag); + } - tags = tags.filter((v, i, a) => a.indexOf(v) === i); + tags = tags.filter((v, i, a) => a.indexOf(v) === i); - return tags; -} + return tags; +}; -export default concatTags; \ No newline at end of file +export default concatTags; diff --git a/src/modules/deleteEntity.js b/src/modules/deleteEntity.js index 0b573a5..7083dee 100644 --- a/src/modules/deleteEntity.js +++ b/src/modules/deleteEntity.js @@ -1,19 +1,23 @@ -import config from '../config'; +import config from "../config"; const deleteEntity = (id, reFetch, onExit, SetLoader) => { - const ADDRESS = config.API.ADDRESS + ":" + config.API.PORT; - fetch(ADDRESS + "/api", { - method: "DELETE", - body: JSON.stringify({id}), - headers: { - "Content-type": "application/json; charset=UTF-8", - } + const ADDRESS = config.API.ADDRESS + ":" + config.API.PORT; + fetch(ADDRESS + "/api", { + method: "DELETE", + body: JSON.stringify({ id }), + headers: { + "Content-type": "application/json; charset=UTF-8", + }, + }) + .then((res) => res.text()) + .then((message) => { + console.log(message); }) - .then(res => res.text()) - .then(message => {console.log(message)}) .then(() => onExit()) .then(() => reFetch()) - .then(() => {SetLoader(false)}); - } + .then(() => { + SetLoader(false); + }); +}; - export default deleteEntity; \ No newline at end of file +export default deleteEntity; diff --git a/src/modules/filterData.js b/src/modules/filterData.js index 0795503..3240871 100644 --- a/src/modules/filterData.js +++ b/src/modules/filterData.js @@ -1,22 +1,31 @@ -const filteredData = (data, searchQuery, nameChecked, tagsChecked, descriptionChecked) => { - return data.filter((e) => { - const name = e.name.toLowerCase().includes(searchQuery.toLowerCase()); - const title = e.title.toLowerCase().includes(searchQuery.toLowerCase()); - const tags = e.tag.some((e) => e.includes(searchQuery.toLowerCase())); - - if((nameChecked && tagsChecked && descriptionChecked)) { - return (name || title || tags); - } else if(nameChecked && tagsChecked) { - return (name || tags); - } else if(nameChecked && descriptionChecked) { - return (name || title); - } else if(tagsChecked && descriptionChecked) { - return (tags || title); - } - else if(nameChecked) { return name } - else if(tagsChecked) { return tags } - else if(descriptionChecked) { return title } - }); -} +const filteredData = ( + data, + searchQuery, + nameChecked, + tagsChecked, + descriptionChecked +) => { + return data.filter((e) => { + const name = e.name.toLowerCase().includes(searchQuery.toLowerCase()); + const title = e.title.toLowerCase().includes(searchQuery.toLowerCase()); + const tags = e.tag.some((e) => e.includes(searchQuery.toLowerCase())); -export default filteredData; \ No newline at end of file + if (nameChecked && tagsChecked && descriptionChecked) { + return name || title || tags; + } else if (nameChecked && tagsChecked) { + return name || tags; + } else if (nameChecked && descriptionChecked) { + return name || title; + } else if (tagsChecked && descriptionChecked) { + return tags || title; + } else if (nameChecked) { + return name; + } else if (tagsChecked) { + return tags; + } else if (descriptionChecked) { + return title; + } + }); +}; + +export default filteredData; diff --git a/src/modules/send.js b/src/modules/send.js index bb3c070..3002db2 100644 --- a/src/modules/send.js +++ b/src/modules/send.js @@ -1,48 +1,63 @@ -import config from '../config'; -import { nanoid } from 'nanoid'; +import config from "../config"; +import { nanoid } from "nanoid"; -const addItem = async (name, link, tag, reFetch, onExit, SetLoader, method, id=nanoid(), title='', date=new Date()) => { +const addItem = async ( + name, + link, + tag, + reFetch, + onExit, + SetLoader, + method, + id = nanoid(), + title = "", + date = new Date() +) => { const dateCreated = date.toString(); function isValidHttpUrl(string) { let url; - + try { url = new URL(string); } catch (_) { - return false; + return false; } - + return url.protocol === "http:" || url.protocol === "https:"; } - if(isValidHttpUrl(link)) { + if (isValidHttpUrl(link)) { const ADDRESS = config.API.ADDRESS + ":" + config.API.PORT; fetch(ADDRESS + "/api", { method: method, body: JSON.stringify({ - _id: id, - name: name, - title: title, - link: link, - tag: tag, - date: dateCreated + _id: id, + name: name, + title: title, + link: link, + tag: tag, + date: dateCreated, }), headers: { - "Content-type": "application/json; charset=UTF-8" - } + "Content-type": "application/json; charset=UTF-8", + }, }) - .then(res => res.text()) - .then(() => reFetch()) - .then(() => {SetLoader(false)}); + .then((res) => res.text()) + .then(() => reFetch()) + .then(() => { + SetLoader(false); + }); onExit(); - } else if(!isValidHttpUrl(link) && link !== '') { - SetLoader(false) - alert('Please make sure the link is valid.\n\n(i.e. starts with "http"/"https")'); + } else if (!isValidHttpUrl(link) && link !== "") { + SetLoader(false); + alert( + 'Please make sure the link is valid.\n\n(i.e. starts with "http"/"https")' + ); } else { - SetLoader(false) + SetLoader(false); } -} +}; -export default addItem; \ No newline at end of file +export default addItem; diff --git a/src/modules/sortList.js b/src/modules/sortList.js index a44a616..3211259 100644 --- a/src/modules/sortList.js +++ b/src/modules/sortList.js @@ -1,52 +1,48 @@ const sortList = (data, sortBy) => { - let sortedData = data; - if(sortBy === 'Default') { - sortedData.sort((a, b) => { - return new Date(b.date) - new Date(a.date); - }); - } else if(sortBy === 'Date (Oldest first)') { - sortedData.sort((a,b) => { - return new Date(a.date) - new Date(b.date); - }); - } else if(sortBy === 'Name (A-Z)') { - sortedData.sort((a, b) => { - const A = a.name.toLowerCase(), B = b.name.toLowerCase(); - if (A < B) - return -1; - if (A > B) - return 1; - return 0; - }); - } else if(sortBy === 'Name (Z-A)') { - sortedData.sort((a, b) => { - const A = a.name.toLowerCase(), B = b.name.toLowerCase(); - if (A > B) - return -1; - if (A < B) - return 1; - return 0; - }); - } else if(sortBy === 'Title (A-Z)') { - sortedData.sort((a, b) => { - const A = a.title.toLowerCase(), B = b.title.toLowerCase(); - if (A < B) - return -1; - if (A > B) - return 1; - return 0; - }); - } else if(sortBy === 'Title (Z-A)') { - sortedData.sort((a, b) => { - const A = a.title.toLowerCase(), B = b.title.toLowerCase(); - if (A > B) - return -1; - if (A < B) - return 1; - return 0; - }); - } + let sortedData = data; + if (sortBy === "Default") { + sortedData.sort((a, b) => { + return new Date(b.date) - new Date(a.date); + }); + } else if (sortBy === "Date (Oldest first)") { + sortedData.sort((a, b) => { + return new Date(a.date) - new Date(b.date); + }); + } else if (sortBy === "Name (A-Z)") { + sortedData.sort((a, b) => { + const A = a.name.toLowerCase(), + B = b.name.toLowerCase(); + if (A < B) return -1; + if (A > B) return 1; + return 0; + }); + } else if (sortBy === "Name (Z-A)") { + sortedData.sort((a, b) => { + const A = a.name.toLowerCase(), + B = b.name.toLowerCase(); + if (A > B) return -1; + if (A < B) return 1; + return 0; + }); + } else if (sortBy === "Title (A-Z)") { + sortedData.sort((a, b) => { + const A = a.title.toLowerCase(), + B = b.title.toLowerCase(); + if (A < B) return -1; + if (A > B) return 1; + return 0; + }); + } else if (sortBy === "Title (Z-A)") { + sortedData.sort((a, b) => { + const A = a.title.toLowerCase(), + B = b.title.toLowerCase(); + if (A > B) return -1; + if (A < B) return 1; + return 0; + }); + } - return sortedData; -} + return sortedData; +}; -export default sortList; \ No newline at end of file +export default sortList; diff --git a/src/routes/Tags.js b/src/routes/Tags.js index 6fa643d..d2f8fee 100644 --- a/src/routes/Tags.js +++ b/src/routes/Tags.js @@ -1,23 +1,23 @@ -import { useParams } from 'react-router-dom'; -import List from '../componets/List'; +import { useParams } from "react-router-dom"; +import List from "../componets/List"; const Tags = ({ data, tags, SetLoader, lightMode, reFetch }) => { const { tagId } = useParams(); const dataWithMatchingTag = data.filter((e) => { - return e.tag.includes(tagId) + return e.tag.includes(tagId); }); return ( <div className="content"> <List lightMode={lightMode} - data={dataWithMatchingTag} + data={dataWithMatchingTag} tags={tags} SetLoader={SetLoader} reFetch={reFetch} /> </div> - ) -} + ); +}; -export default Tags \ No newline at end of file +export default Tags; diff --git a/src/styles/App.css b/src/styles/App.css index 51050c6..bda507c 100644 --- a/src/styles/App.css +++ b/src/styles/App.css @@ -35,14 +35,16 @@ .search { padding: 10px; - font-family: 'Font Awesome 5 Free'; + font-family: "Font Awesome 5 Free"; font-size: 1.2rem; padding-left: 10px; border: none; border-radius: 0; -webkit-appearance: none; - -webkit-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; - 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; + -webkit-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; + 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; } .search:focus { @@ -52,28 +54,32 @@ .btn { position: relative; border-radius: 100%; - font-family: 'Font Awesome 5 Free'; + 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; + 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; border: none; transition: background-color 0.1s; } -.btn:active { box-shadow: 0px 0px 10px rgb(83, 143, 255); } +.btn:active { + box-shadow: 0px 0px 10px rgb(83, 143, 255); +} .add-btn { margin-left: auto; } -textarea:focus, input:focus{ +textarea:focus, +input:focus { outline: none; } -.results { +.results { margin: 20px 20px 0px 5px; display: inline-block; } @@ -82,11 +88,12 @@ textarea:focus, input:focus{ text-align: center; padding-top: 5%; padding-bottom: 5%; - 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; + 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; margin-top: 10px; } .dark-light-btn { margin-left: 10px; font-size: 1.2em; -} \ No newline at end of file +} diff --git a/src/styles/Filters.css b/src/styles/Filters.css index 290b3f8..c4de1ce 100644 --- a/src/styles/Filters.css +++ b/src/styles/Filters.css @@ -1,73 +1,77 @@ @media (min-width: 800px) { - .filter { - left: 35%; - right: 35%; - min-width: 200px; - } + .filter { + left: 35%; + right: 35%; + min-width: 200px; + } } @media (max-width: 800px) { - .filter { - left: 20%; - right: 20%; - min-width: 100px; - } + .filter { + left: 20%; + right: 20%; + min-width: 100px; + } } .filter-overlay { - animation: fadein 0.2s; - background-color: black; - opacity: 10%; - position: fixed; - top: 0; - left: 0; - bottom: 0; - width: 100vw; - z-index: 1; + animation: fadein 0.2s; + background-color: black; + opacity: 10%; + position: fixed; + top: 0; + left: 0; + bottom: 0; + width: 100vw; + z-index: 1; } .filter-box { - position: relative; + position: relative; } .filter { - animation: fadein 0.3s; - border: solid; - border-width: 1px; - font-weight: 300; - border-color: rgb(141, 141, 141); - display: flex; - flex-direction: column; - box-shadow: rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px; - padding: 5px; - position: absolute; - -moz-user-select: none; - -webkit-user-select: none; - user-select: none; - z-index: 2; + animation: fadein 0.3s; + border: solid; + border-width: 1px; + font-weight: 300; + border-color: rgb(141, 141, 141); + display: flex; + flex-direction: column; + box-shadow: rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px; + padding: 5px; + position: absolute; + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; + z-index: 2; } .filter legend { - font-weight: 600; + font-weight: 600; } .filter > label { - margin: 5px; - text-align: left; - margin-bottom: 5px; - font-family: 'Font Awesome 5 Free'; - 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; - border: none; + margin: 5px; + text-align: left; + margin-bottom: 5px; + font-family: "Font Awesome 5 Free"; + 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; + border: none; } .filter label:active { - box-shadow: 0px 0px 10px rgb(83, 143, 255); + box-shadow: 0px 0px 10px rgb(83, 143, 255); } @keyframes fadein { - from { opacity: 0%; } - to { } -} \ No newline at end of file + from { + opacity: 0%; + } + to { + } +} diff --git a/src/styles/List.css b/src/styles/List.css index f95cd38..928cdb4 100644 --- a/src/styles/List.css +++ b/src/styles/List.css @@ -23,7 +23,9 @@ display: flex; justify-content: space-between; align-items: center; - 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; + 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; } .list a { @@ -39,7 +41,7 @@ .tags { margin: 10px auto 10px auto; } - + .list-row { margin-top: 20px; display: flex; @@ -47,7 +49,9 @@ justify-content: space-between; align-items: center; text-align: center; - 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; + 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; } .etc { display: flex; @@ -108,7 +112,7 @@ .link { white-space: nowrap; - font-family: 'Font Awesome 5 Free'; + font-family: "Font Awesome 5 Free"; pointer-events: all; font-size: 1rem; } @@ -123,7 +127,6 @@ opacity: 100%; } - .edit-btn { margin: 20px 20px 20px 0px; width: 50px; @@ -169,21 +172,22 @@ .delete { margin: 20px 20px 20px 0px; - background-color:#273949; + background-color: #273949; float: right; font-size: 1.1rem; width: 40px; height: 40px; position: relative; border-radius: 100%; - font-family: 'Font Awesome 5 Free'; + 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; - background-color:#273949; + 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; + background-color: #273949; border: none; transition: background-color 0.1s; } diff --git a/src/styles/Loader.css b/src/styles/Loader.css index 29a84a1..eadbf3b 100644 --- a/src/styles/Loader.css +++ b/src/styles/Loader.css @@ -1,7 +1,7 @@ .loader { - position: fixed; - bottom: 10%; - left: 30%; - right: 30%; - text-align: center; -} \ No newline at end of file + position: fixed; + bottom: 10%; + left: 30%; + right: 30%; + text-align: center; +} diff --git a/src/styles/SendItem.css b/src/styles/SendItem.css index ac13811..bcc6902 100644 --- a/src/styles/SendItem.css +++ b/src/styles/SendItem.css @@ -1,103 +1,111 @@ @media (min-width: 800px) { - .box { - left: 30%; - right: 30%; - min-width: 300px; - } + .box { + left: 30%; + right: 30%; + min-width: 300px; + } } @media (max-width: 800px) { - .box { - left: 15%; - right: 15%; - min-width: 200px; - } + .box { + left: 15%; + right: 15%; + min-width: 200px; + } } .add-overlay { - animation: fadein 0.2s; - background-color: black; - opacity: 10%; - position: fixed; - top: 0; - left: 0; - bottom: 0; - width: 100vw; + animation: fadein 0.2s; + background-color: black; + opacity: 10%; + position: fixed; + top: 0; + left: 0; + bottom: 0; + width: 100vw; } .send-box { - position: relative; + position: relative; } .box { - animation: fadein 0.3s; - border: solid; - border-width: 1px; - border-color: rgb(141, 141, 141); - box-shadow: rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px; - position: absolute; - z-index: 2; - background-color: #1f2c38; - overflow-x: hidden; - overflow-y: auto; + animation: fadein 0.3s; + border: solid; + border-width: 1px; + border-color: rgb(141, 141, 141); + box-shadow: rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px; + position: absolute; + z-index: 2; + background-color: #1f2c38; + overflow-x: hidden; + overflow-y: auto; } .box legend { - font-weight: 600; + font-weight: 600; } .box h3 { - font-weight: 300; + font-weight: 300; } .AddItem-content { - padding: 20px; + padding: 20px; } .AddItem-input { - font-size: 1rem; - padding: 10px; - border: none; - width: 100%; - color: white; - background-color:#273949; - border-radius: 0; - -webkit-appearance: none; - -webkit-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; - 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; + font-size: 1rem; + padding: 10px; + border: none; + width: 100%; + color: white; + background-color: #273949; + border-radius: 0; + -webkit-appearance: none; + -webkit-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; + 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; } .AddItem-input:focus { - 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; } .send-btn { - font-family: 'Font Awesome 5 Free'; - font-size: 1.2rem; - padding: 10px; - 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; - border: none; - margin-top: 20px; - display: block; - margin-left: auto; - margin-right: auto; - transition: background-color 0.1s; + font-family: "Font Awesome 5 Free"; + font-size: 1.2rem; + padding: 10px; + 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; + border: none; + margin-top: 20px; + display: block; + margin-left: auto; + margin-right: auto; + transition: background-color 0.1s; } -.send-btn:active { box-shadow: 0px 0px 10px rgb(83, 143, 255); } +.send-btn:active { + box-shadow: 0px 0px 10px rgb(83, 143, 255); +} @keyframes fadein { - from { opacity: 0%; } - to { } + from { + opacity: 0%; + } + to { + } } .optional { - color: gray; - font-size: 0.8em; - float: right; + color: gray; + font-size: 0.8em; + float: right; } .title { - font-size: 0.9em; + font-size: 0.9em; } diff --git a/src/styles/SideBar.css b/src/styles/SideBar.css index 6a43231..8b022fb 100644 --- a/src/styles/SideBar.css +++ b/src/styles/SideBar.css @@ -1,27 +1,27 @@ .sidebar { - height: 100vh; - position: fixed; - border-right: solid; - border-width: 1px; - border-color: gray; + height: 100vh; + position: fixed; + border-right: solid; + border-width: 1px; + border-color: gray; } .sidebar h1 { - text-align: center; + text-align: center; } .credits { - text-align: center; - font-size: small; + text-align: center; + font-size: small; } .credits a { - color: inherit; - text-decoration: underline; + color: inherit; + text-decoration: underline; } .pro-sidebar-layout { - background: #384952; - text-shadow: none; - color: white; -} \ No newline at end of file + background: #384952; + text-shadow: none; + color: white; +} diff --git a/src/styles/Sort.css b/src/styles/Sort.css index a4a57d6..5927d34 100644 --- a/src/styles/Sort.css +++ b/src/styles/Sort.css @@ -1,73 +1,77 @@ @media (min-width: 800px) { - .sort { - left: 30%; - right: 30%; - min-width: 200px; - } + .sort { + left: 30%; + right: 30%; + min-width: 200px; + } } @media (max-width: 800px) { - .sort { - left: 20%; - right: 20%; - min-width: 100px; - } + .sort { + left: 20%; + right: 20%; + min-width: 100px; + } } .sort-overlay { - animation: fadein 0.2s; - background-color: black; - opacity: 10%; - position: fixed; - top: 0; - left: 0; - bottom: 0; - width: 100vw; - z-index: 1; + animation: fadein 0.2s; + background-color: black; + opacity: 10%; + position: fixed; + top: 0; + left: 0; + bottom: 0; + width: 100vw; + z-index: 1; } .sort-box { - position: relative; + position: relative; } .sort { - animation: fadein 0.3s; - border: solid; - border-width: 1px; - font-weight: 300; - border-color: rgb(141, 141, 141); - display: flex; - flex-direction: column; - box-shadow: rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px; - padding: 5px; - position: absolute; - -moz-user-select: none; - -webkit-user-select: none; - user-select: none; - z-index: 2; + animation: fadein 0.3s; + border: solid; + border-width: 1px; + font-weight: 300; + border-color: rgb(141, 141, 141); + display: flex; + flex-direction: column; + box-shadow: rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px; + padding: 5px; + position: absolute; + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; + z-index: 2; } .sort legend { - font-weight: 600; + font-weight: 600; } .sort-by-btn { - margin: 5px; - text-align: left; - font-family: 'Font Awesome 5 Free'; - 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; - border: none; - transition: background-color 0.1s; + margin: 5px; + text-align: left; + font-family: "Font Awesome 5 Free"; + 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; + border: none; + transition: background-color 0.1s; } .sort-by-btn:active { - box-shadow: 0px 0px 10px rgb(83, 143, 255); + box-shadow: 0px 0px 10px rgb(83, 143, 255); } @keyframes fadein { - from { opacity: 0%; } - to { } -} \ No newline at end of file + from { + opacity: 0%; + } + to { + } +} diff --git a/src/styles/ViewArchived.css b/src/styles/ViewArchived.css index 05c2f7a..cf94677 100644 --- a/src/styles/ViewArchived.css +++ b/src/styles/ViewArchived.css @@ -1,10 +1,10 @@ .view-archived { - display: flex; - flex-direction: column; - text-align: left; + display: flex; + flex-direction: column; + text-align: left; } .seperator { - width: 100%; - color: #1f2c38; -} \ No newline at end of file + width: 100%; + color: #1f2c38; +} diff --git a/src/styles/index.css b/src/styles/index.css index e6d1519..df67303 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -1,7 +1,7 @@ body { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; @@ -20,7 +20,7 @@ body { /* Dark Mode settings (Default) */ .dark-light-btn::before { - content: ''; + content: ""; } .delete { @@ -30,10 +30,10 @@ body { .no-results { background-color: #1f2c38; -} +} .send-btn { - background-color:#273949; + background-color: #273949; color: #ffffffb6; } @@ -42,11 +42,13 @@ body { } .sort-by-btn { - background-color:#273949; - color: #ffffffb6; + background-color: #273949; + color: #ffffffb6; } -.btn:hover, .sort-by-btn:hover, .send-btn:hover { +.btn:hover, +.sort-by-btn:hover, +.send-btn:hover { background-color: rgb(76, 117, 170); } @@ -64,21 +66,22 @@ body { .btn { color: #ffffffb6; - background-color:#273949; + background-color: #273949; } .edit-btn { background-color: #1f2c38; } -.no-results, .list-row { +.no-results, +.list-row { transition: background-color 0.1s; - background-color:#273949; + background-color: #273949; } .search { transition: background-color 0.1s; - background-color:#273949; + background-color: #273949; color: white; } @@ -87,19 +90,19 @@ body { } .filter > label { - background-color:#273949; + background-color: #273949; color: #ffffffb6; } /* Light Mode settings */ .light .dark-light-btn::before { - content: ''; + content: ""; } .light { text-shadow: 0px 1px 2px #ffffff; - background-color:rgb(233, 220, 179); + background-color: rgb(233, 220, 179); color: rgb(64, 64, 64); } @@ -122,8 +125,9 @@ body { color: black; } -.light .box, .light .edit-btn { - background-color:rgb(233, 220, 179); +.light .box, +.light .edit-btn { + background-color: rgb(233, 220, 179); } .light .title { @@ -138,7 +142,8 @@ body { color: rgb(9, 139, 214); } -.light .filter, .light .sort { +.light .filter, +.light .sort { background-color: rgb(233, 220, 179); } @@ -148,7 +153,7 @@ body { } .light .sort-by-btn { - background-color:lightyellow; + background-color: lightyellow; color: #4b4b4bb6; } @@ -157,11 +162,13 @@ body { color: #717171b6; } -.light .sort-by-btn:hover, .light .btn:hover, .light .send-btn:hover { +.light .sort-by-btn:hover, +.light .btn:hover, +.light .send-btn:hover { background-color: rgb(55, 131, 237); color: #d8d8d8; } .light .no-results { background-color: lightyellow; -} \ No newline at end of file +}