Cleaner code with prettier.
This commit is contained in:
parent
e2db7e71ac
commit
10d3a05c1d
|
@ -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&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).**
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
</html>
|
||||
</body>
|
||||
</html>
|
||||
|
|
101
api/server.js
101
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 <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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
188
src/App.js
188
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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
export default EditItem;
|
||||
|
|
|
@ -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
|
||||
export default Filters;
|
||||
|
|
|
@ -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
|
||||
export default List;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
export default NoResults;
|
||||
|
|
|
@ -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
|
||||
<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;
|
||||
|
|
|
@ -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
|
||||
export default Sort;
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
export default ViewArchived;
|
||||
|
|
|
@ -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
|
||||
},
|
||||
};
|
||||
|
|
|
@ -11,4 +11,4 @@ root.render(
|
|||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
);
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
export default concatTags;
|
||||
|
|
|
@ -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;
|
||||
export default deleteEntity;
|
||||
|
|
|
@ -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;
|
||||
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;
|
||||
|
|
|
@ -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;
|
||||
export default addItem;
|
||||
|
|
|
@ -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;
|
||||
export default sortList;
|
||||
|
|
|
@ -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
|
||||
export default Tags;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 { }
|
||||
}
|
||||
from {
|
||||
opacity: 0%;
|
||||
}
|
||||
to {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.loader {
|
||||
position: fixed;
|
||||
bottom: 10%;
|
||||
left: 30%;
|
||||
right: 30%;
|
||||
text-align: center;
|
||||
}
|
||||
position: fixed;
|
||||
bottom: 10%;
|
||||
left: 30%;
|
||||
right: 30%;
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
background: #384952;
|
||||
text-shadow: none;
|
||||
color: white;
|
||||
}
|
||||
|
|
|
@ -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 { }
|
||||
}
|
||||
from {
|
||||
opacity: 0%;
|
||||
}
|
||||
to {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
width: 100%;
|
||||
color: #1f2c38;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Ŝarĝante…
Reference in New Issue