Cleaner code with prettier.

This commit is contained in:
Daniel 2022-06-16 13:43:44 +04:30
parent e2db7e71ac
commit 10d3a05c1d
32 changed files with 1021 additions and 754 deletions

View File

@ -4,7 +4,6 @@ LinkWarden
<sub>A place for your useful links.</sub>
<img src="assets/LinkWarden.png" alt="LinkWarden.png" width="500px"/>
<a href="https://twitter.com/Daniel31X13" target="_blank" rel="noopener noreferrer"><img alt="Twitter Follow" src="https://img.shields.io/twitter/follow/Daniel31X13?label=twitter&amp;style=social"></a>
@ -13,16 +12,18 @@ LinkWarden
</h1>
[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)
</div>
## 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).**

View File

@ -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 });
}
@ -23,10 +24,13 @@ module.exports = async (link, id) => {
blocker.enableBlockingInPage(page);
});
await page.goto(link, { waitUntil: 'load', timeout: 0 });
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.screenshot({
path: screenshotDirectory + id + ".png",
fullPage: true,
});
await page.pdf({ path: pdfDirectory + id + ".pdf", format: "a4" });
await browser.close();
}
};

View File

@ -1,15 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body style="text-align: center; background-color: rgb(0, 51, 78); color:white;">
</head>
<body
style="text-align: center; background-color: rgb(0, 51, 78); color: white"
>
<h1>404: NOT FOUND</h1>
<h3>If you are trying to access a recently added Screenshot/PDF, just wait a bit more for it to be uploaded.</h3>
<h3>
If you are trying to access a recently added Screenshot/PDF, just wait a
bit more for it to be uploaded.
</h3>
<h3>If the problem persists, looks like the file wasn't created...</h3>
<h1>¯\_(ツ)_/¯</h1>
</body>
</body>
</html>

View File

@ -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) => {
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');
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) => {
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');
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 <title> 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);
@ -86,9 +92,7 @@ app.delete('/api', async (req, res) => {
async function updateDoc(id, updatedListing) {
try {
await list.updateOne({ _id: id }, { $set: updatedListing });
}
catch(err) {
} 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) => {
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) => {
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);
}
}

View File

@ -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,35 +112,66 @@ 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}>&#xf0c9;</button>
<input className="search" type="search" placeholder="&#xf002; Search" onChange={search}/>
<button className="add-btn btn" onClick={() => setNewBox(true)}>&#xf067;</button>
<button className="dark-light-btn btn" onClick={() => setLightMode(!lightMode)}></button>
<button
className="sidebar-btn btn"
style={{ marginRight: "10px" }}
onClick={handleToggleSidebar}
>
&#xf0c9;
</button>
<input
className="search"
type="search"
placeholder="&#xf002; Search"
onChange={search}
/>
<button className="add-btn btn" onClick={() => setNewBox(true)}>
&#xf067;
</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)}>&#xf0b0;</button>
<button className='btn' style={{marginLeft: '10px'}} onClick={() => setSortBox(true)}>&#xf0dc;</button>
<button
className="btn"
style={{ marginTop: "10px" }}
onClick={() => setFilterBox(true)}
>
&#xf0b0;
</button>
<button
className="btn"
style={{ marginLeft: "10px" }}
onClick={() => setSortBox(true)}
>
&#xf0dc;
</button>
{numberOfResults === 0 ? <NoResults /> : null}
{sortBox ? <Sort
sortBy={sortByFunc}
onExit={exitSorting}
/> : null}
{sortBox ? <Sort sortBy={sortByFunc} onExit={exitSorting} /> : null}
{filterBox ? <Filters
{filterBox ? (
<Filters
nameChecked={nameChecked}
handleNameCheckbox={handleNameCheckbox}
descriptionChecked={descriptionChecked}
@ -140,21 +179,27 @@ function App() {
tagsChecked={tagsChecked}
handleTagsCheckbox={handleTagsCheckbox}
onExit={exitFilter}
/> : null}
/>
) : null}
{newBox ? <AddItem
{newBox ? (
<AddItem
SetLoader={SetLoader}
onExit={exitAdding}
reFetch={fetchData}
lightMode={lightMode}
tags={() => tags}
/> : null}
/>
) : null}
{loader ? <Loader lightMode={lightMode} /> : null}
</div>
<Routes>
<Route path="/" element={<div className='content'>
<Route
path="/"
element={
<div className="content">
<List
lightMode={lightMode}
SetLoader={SetLoader}
@ -162,15 +207,22 @@ function App() {
tags={tags}
reFetch={fetchData}
/>
</div>} />
</div>
}
/>
<Route path="tags/:tagId" element={<Tags
<Route
path="tags/:tagId"
element={
<Tags
lightMode={lightMode}
SetLoader={SetLoader}
data={filteredData}
tags={tags}
reFetch={fetchData}
/>} />
/>
}
/>
</Routes>
</div>
);

View File

@ -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 &#xf067;</button>
<button onClick={newItem} className="send-btn">
Add &#xf067;
</button>
</div>
</fieldset>
</div>
</>
)
}
);
};
export default AddItem;

View File

@ -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}>&#xf2ed;</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>
<div className="add-overlay" onClick={abort}></div>
<div className="send-box">
<fieldset className="box">
<legend>Edit bookmark</legend>
<button className="delete" onClick={deleteItem}>
&#xf2ed;
</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 &#xf303;</button>
<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 &#xf303;
</button>
</div>
</fieldset>
</div>
</>
)
}
);
};
export default EditItem
export default EditItem;

View File

@ -1,6 +1,14 @@
import '../styles/Filters.css'
import "../styles/Filters.css";
const Filters = ({nameChecked, handleNameCheckbox, descriptionChecked, handleDescriptionCheckbox, tagsChecked, handleTagsCheckbox, onExit}) => {
const Filters = ({
nameChecked,
handleNameCheckbox,
descriptionChecked,
handleDescriptionCheckbox,
tagsChecked,
handleTagsCheckbox,
onExit,
}) => {
function abort(e) {
if (e.target.className === "filter-overlay") {
onExit();
@ -9,17 +17,38 @@ const Filters = ({nameChecked, handleNameCheckbox, descriptionChecked, handleDes
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>
<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
export default Filters;

View File

@ -1,13 +1,13 @@
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);
@ -20,29 +20,33 @@ const List = ({data, tags, reFetch, SetLoader, lightMode}) => {
return (
<div className="list">
{editBox ? <EditItem
{editBox ? (
<EditItem
lightMode={lightMode}
tags={() => tags}
onExit={exitEditing}
SetLoader={SetLoader}
reFetch={reFetch}
item={data[editIndex]}
/> : null}
/>
) : 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}>
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} />
<img alt="" src={favicon} />
<div className="list-entity-content">
<div className='row-name'>
<div className="row-name">
<span className="num">{i + 1}.</span>
{e.name}
<a
className='link'
className="link"
target="_blank"
rel="noreferrer"
href={e.link}
@ -50,28 +54,37 @@ const List = ({data, tags, reFetch, SetLoader, lightMode}) => {
({url.hostname})
</a>
</div>
<div className='title'>{e.title}</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>)
return (
<Link to={tagPath} key={i}>
{e}
</Link>
);
})}
</div>
<div className='date'>{new Date(e.date).toDateString()}</div>
<div className="date">
{new Date(e.date).toDateString()}
</div>
</div>
<div className='etc'>
<ViewArchived className='view-archived' id={e._id} />
<button className="btn edit-btn" onClick={() => edit(i)}>&#xf303;</button>
</div>
<div className="etc">
<ViewArchived className="view-archived" id={e._id} />
<button className="btn edit-btn" onClick={() => edit(i)}>
&#xf303;
</button>
</div>
</div>
</LazyLoad>)
</LazyLoad>
);
} catch (e) {
console.log(e);
}
})}
</div>
)
}
);
};
export default List
export default List;

View File

@ -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
export default Loader;

View File

@ -1,12 +1,12 @@
import React from 'react'
import React from "react";
const NoResults = () => {
return (
<div className='no-results'>
<div className="no-results">
<h1>¯\_()_/¯</h1>
<p>Nothing found.</p>
</div>
)
}
);
};
export default NoResults;

View File

@ -1,15 +1,22 @@
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;
const A = a.toLowerCase(),
B = b.toLowerCase();
if (A < B) return -1;
if (A > B) return 1;
return 0;
});
return (
@ -17,31 +24,39 @@ const SideBar = ({ tags, handleToggleSidebar, toggle }) => {
toggled={toggle}
breakPoint="lg"
onToggle={handleToggleSidebar}
className='sidebar'>
className="sidebar"
>
<SidebarHeader>
<h1>LinkWarden</h1>
</SidebarHeader>
<SidebarContent className='sidebar-content'>
<SidebarContent className="sidebar-content">
<Menu iconShape="circle">
<MenuItem>
<Link to="/">
<h3>Show Everything</h3>
</Link>
</MenuItem>
<MenuItem><Link to="/"><h3>Show Everything</h3></Link></MenuItem>
<SubMenu icon='#' defaultOpen={true} title='Tags'>
<SubMenu icon="#" defaultOpen={true} title="Tags">
{sortedTags.map((e, i) => {
const path = `/tags/${e}`;
return <MenuItem key={i}><Link to={path}>{e}</Link></MenuItem>
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>
<p className="credits">
©{new Date().getFullYear()} Made with 💙 by{" "}
<a href="https://github.com/Daniel31x13">Daniel 31X13</a>
</p>
</SidebarFooter>
</ProSidebar>
)
}
);
};
export default SideBar
export default SideBar;

View File

@ -1,4 +1,4 @@
import '../styles/Sort.css'
import "../styles/Sort.css";
const Sort = ({ sortBy, onExit }) => {
function abort(e) {
@ -13,20 +13,32 @@ const Sort = ({ sortBy, onExit }) => {
return (
<>
<div className='sort-overlay' onClick={abort}></div>
<div className='sort-box'>
<fieldset className='sort' onClick={sort}>
<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'>&#xf271; Date (Newest first)</button>
<button className='sort-by-btn' value='Date (Oldest first)'>&#xf272; Date (Oldest first)</button>
<button className='sort-by-btn' value='Name (A-Z)'>&#xf15d; Name (A-Z)</button>
<button className='sort-by-btn' value='Name (Z-A)'>&#xf15e; Name (Z-A)</button>
<button className='sort-by-btn' value='Title (A-Z)'>&#xf15d; Website title (A-Z)</button>
<button className='sort-by-btn' value='Title (Z-A)'>&#xf15e; Website title (Z-A)</button>
<button className="sort-by-btn" value="Default">
&#xf271; Date (Newest first)
</button>
<button className="sort-by-btn" value="Date (Oldest first)">
&#xf272; Date (Oldest first)
</button>
<button className="sort-by-btn" value="Name (A-Z)">
&#xf15d; Name (A-Z)
</button>
<button className="sort-by-btn" value="Name (Z-A)">
&#xf15e; Name (Z-A)
</button>
<button className="sort-by-btn" value="Title (A-Z)">
&#xf15d; Website title (A-Z)
</button>
<button className="sort-by-btn" value="Title (Z-A)">
&#xf15e; Website title (Z-A)
</button>
</fieldset>
</div>
</>
)
}
);
};
export default Sort
export default Sort;

View File

@ -1,38 +1,39 @@
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) => ({
@ -43,18 +44,20 @@ export default function TagSelection({setTags, tags, tag=[], lightMode}) {
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

View File

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

View File

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

View File

@ -2,12 +2,12 @@ const concatTags = (data) => {
let tags = [];
for (let i = 0; i < data.length; i++) {
tags = tags.concat(data[i].tag)
tags = tags.concat(data[i].tag);
}
tags = tags.filter((v, i, a) => a.indexOf(v) === i);
return tags;
}
};
export default concatTags;

View File

@ -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}),
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;
export default deleteEntity;

View File

@ -1,22 +1,31 @@
const filteredData = (data, searchQuery, nameChecked, tagsChecked, descriptionChecked) => {
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);
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;
}
else if(nameChecked) { return name }
else if(tagsChecked) { return tags }
else if(descriptionChecked) { return title }
});
}
};
export default filteredData;

View File

@ -1,7 +1,18 @@
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) {
@ -16,7 +27,7 @@ const addItem = async (name, link, tag, reFetch, onExit, SetLoader, method, id=n
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,
@ -26,23 +37,27 @@ const addItem = async (name, link, tag, reFetch, onExit, SetLoader, method, id=n
title: title,
link: link,
tag: tag,
date: dateCreated
date: dateCreated,
}),
headers: {
"Content-type": "application/json; charset=UTF-8"
}
"Content-type": "application/json; charset=UTF-8",
},
})
.then(res => res.text())
.then((res) => res.text())
.then(() => reFetch())
.then(() => {SetLoader(false)});
.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;

View File

@ -1,52 +1,48 @@
const sortList = (data, sortBy) => {
let sortedData = data;
if(sortBy === 'Default') {
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) => {
} 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)') {
} 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;
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)') {
} 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;
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)') {
} 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;
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)') {
} 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;
const A = a.title.toLowerCase(),
B = b.title.toLowerCase();
if (A > B) return -1;
if (A < B) return 1;
return 0;
});
}
return sortedData;
}
};
export default sortList;

View File

@ -1,10 +1,10 @@
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 (
@ -17,7 +17,7 @@ const Tags = ({ data, tags, SetLoader, lightMode, reFetch }) => {
reFetch={reFetch}
/>
</div>
)
}
);
};
export default Tags
export default Tags;

View File

@ -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,24 +54,28 @@
.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;
}
@ -82,7 +88,8 @@ 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;
}

View File

@ -55,11 +55,12 @@
margin: 5px;
text-align: left;
margin-bottom: 5px;
font-family: 'Font Awesome 5 Free';
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;
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;
}
@ -68,6 +69,9 @@
}
@keyframes fadein {
from { opacity: 0%; }
to { }
from {
opacity: 0%;
}
to {
}
}

View File

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

View File

@ -60,11 +60,13 @@
border: none;
width: 100%;
color: white;
background-color:#273949;
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;
-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 {
@ -72,11 +74,12 @@
}
.send-btn {
font-family: 'Font Awesome 5 Free';
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;
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;
@ -85,11 +88,16 @@
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 {

View File

@ -54,11 +54,12 @@
.sort-by-btn {
margin: 5px;
text-align: left;
font-family: 'Font Awesome 5 Free';
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;
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;
}
@ -68,6 +69,9 @@
}
@keyframes fadein {
from { opacity: 0%; }
to { }
from {
opacity: 0%;
}
to {
}
}

View File

@ -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 {
@ -33,7 +33,7 @@ body {
}
.send-btn {
background-color:#273949;
background-color: #273949;
color: #ffffffb6;
}
@ -42,11 +42,13 @@ body {
}
.sort-by-btn {
background-color:#273949;
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,7 +162,9 @@ 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;
}