Initial commit of version 3.
This commit is contained in:
parent
509b8047f0
commit
c0121a4318
|
@ -1,8 +0,0 @@
|
||||||
.github
|
|
||||||
api
|
|
||||||
.gitignore
|
|
||||||
.dockerignore
|
|
||||||
Dockerfile*
|
|
||||||
node_modules
|
|
||||||
api/media
|
|
||||||
mongo
|
|
|
@ -1,3 +0,0 @@
|
||||||
API_PORT=5600
|
|
||||||
API_ADDRESS=192.168.2.125
|
|
||||||
CLIENT_PORT=2500
|
|
|
@ -1,7 +1,8 @@
|
||||||
# How to contribute
|
# How to contribute
|
||||||
|
|
||||||
> **For questions/help, feature requests and bug reports please create an [issue](https://github.com/Daniel31x13/link-warden/issues) (please use the right lable).**
|
> **For questions/help, feature requests and bug reports please create an [issue](https://github.com/Daniel31x13/link-warden/issues) (please use the right lable).**
|
||||||
|
|
||||||
First off, I'm really glad you're reading this and thank you for taking the time to contribute! 👍
|
First off, I'm really glad you're reading this and thank you for taking the time to contribute!
|
||||||
|
|
||||||
1. Confirm your planned implementation fit into our project [features](https://github.com/Daniel31x13/link-warden#features) and [project roadmap](https://github.com/Daniel31x13/link-warden/wiki#project-roadmap) (wether it's adding a new feature or improving our existing code).
|
1. Confirm your planned implementation fit into our project [features](https://github.com/Daniel31x13/link-warden#features) and [project roadmap](https://github.com/Daniel31x13/link-warden/wiki#project-roadmap) (wether it's adding a new feature or improving our existing code).
|
||||||
|
|
||||||
|
|
|
@ -1,34 +1,104 @@
|
||||||
node_modules
|
# Logs
|
||||||
..pnp
|
logs
|
||||||
.pnp.js
|
*.log
|
||||||
coverage
|
|
||||||
build
|
|
||||||
.DS_Store
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
mongo
|
lerna-debug.log*
|
||||||
.env
|
|
||||||
|
|
||||||
api/media
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
api/.ash_history
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
api/.config
|
|
||||||
api/.cache/
|
# Runtime data
|
||||||
api/.pki/
|
pids
|
||||||
api/node_modules
|
*.pid
|
||||||
api/..pnp
|
*.seed
|
||||||
api/.pnp.js
|
*.pid.lock
|
||||||
api/coverage
|
|
||||||
api/build
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
api/.DS_Store
|
lib-cov
|
||||||
api/.env.local
|
|
||||||
api/.env.development.local
|
# Coverage directory used by tools like istanbul
|
||||||
api/.env.test.local
|
coverage
|
||||||
api/.env.production.local
|
*.lcov
|
||||||
api/npm-debug.log*
|
|
||||||
api/yarn-debug.log*
|
# nyc test coverage
|
||||||
api/yarn-error.log*
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
.env.test
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
11
Dockerfile
11
Dockerfile
|
@ -1,11 +0,0 @@
|
||||||
# Development image for React app
|
|
||||||
FROM node:18-alpine
|
|
||||||
|
|
||||||
WORKDIR /home/node
|
|
||||||
|
|
||||||
VOLUME /home/node/node_modules
|
|
||||||
|
|
||||||
COPY package*.json ./
|
|
||||||
|
|
||||||
RUN npm i -g npm@latest \
|
|
||||||
&& npm ci --legacy-peer-deps
|
|
|
@ -1,16 +0,0 @@
|
||||||
# Production image for React app
|
|
||||||
FROM node:18-alpine AS builder
|
|
||||||
|
|
||||||
WORKDIR /home/node
|
|
||||||
|
|
||||||
VOLUME /home/node/node_modules
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
RUN npm i -g npm@latest \
|
|
||||||
&& npm ci --legacy-peer-deps \
|
|
||||||
&& npm run build
|
|
||||||
|
|
||||||
|
|
||||||
FROM nginx:alpine
|
|
||||||
COPY --from=builder /home/node/build /usr/share/nginx/html
|
|
82
README.md
82
README.md
|
@ -1,81 +1,5 @@
|
||||||
<div align="center">
|
# LinkWarden
|
||||||
<h1>
|
|
||||||
LinkWarden
|
|
||||||
|
|
||||||
<sub>A place for your useful links.</sub>
|
## A place for your useful links.
|
||||||
|
|
||||||
<img src="assets/LinkWarden.png" alt="LinkWarden.png" width="500px"/>
|
Rebuilding project from ground up...
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
![GitHub](https://img.shields.io/github/license/daniel31x13/link-warden?style=flat-square) ![GitHub top language](https://img.shields.io/github/languages/top/daniel31x13/link-warden?style=flat-square) ![GitHub last commit](https://img.shields.io/github/last-commit/daniel31x13/link-warden?style=flat-square) ![Netlify](https://img.shields.io/netlify/31890116-669c-4b1c-844e-fa427503d8bf?style=flat-square) ![GitHub Repo stars](https://img.shields.io/github/stars/daniel31x13/link-warden?style=flat-square)
|
|
||||||
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
[Demo (v1.0.0)](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
|
|
||||||
|
|
||||||
* 📷 Auto-capture a screenshot and PDF from each website.
|
|
||||||
|
|
||||||
* 🔥 Sleek, minimalist design.
|
|
||||||
|
|
||||||
* 🌤 Dark/Light mode support.
|
|
||||||
|
|
||||||
* ↔️ Responsive design.
|
|
||||||
|
|
||||||
* 🔎 Search, filter and sorting functionality.
|
|
||||||
|
|
||||||
* 🚀 Lazy loading (for better performance).
|
|
||||||
|
|
||||||
* 🏷 Set multiple tags to each link.
|
|
||||||
|
|
||||||
* 🗂 Assign each link to a collection where we can further group links.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Using Docker Compose V2 (Recommended)
|
|
||||||
|
|
||||||
1. Make sure docker is installed.
|
|
||||||
|
|
||||||
2. Clone this repository.
|
|
||||||
|
|
||||||
3. Head to the main folder and run `docker compose up -d`.
|
|
||||||
|
|
||||||
The app will be deployed on port 3000.
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
To configure the app create a `.env` file (in the main folder), here are the available variables:
|
|
||||||
```
|
|
||||||
CLIENT_PORT=2500 # Default: 3000
|
|
||||||
API_PORT=5700 # Default: 5500
|
|
||||||
API_ADDRESS=192.168.1.14 # Default: localhost
|
|
||||||
```
|
|
||||||
|
|
||||||
> If you want to use this app across the network set `API_ADDRESS` as the computer (where LinkWarden is hosted) IP address.
|
|
||||||
|
|
||||||
### Manual Setup
|
|
||||||
|
|
||||||
1. Make sure your MongoDB database and collection is up and running.
|
|
||||||
|
|
||||||
2. Edit [URI, Database name and Collection name](api/config.js) accordingly.
|
|
||||||
|
|
||||||
3. [Optional] If you want to use this app across the network change [`API_HOST`](src/config.js) address with the computer IP and API port.
|
|
||||||
|
|
||||||
4. Head to the main folder using terminal and run: `(cd api && npm install) && npm install --legacy-peer-deps` for the dependencies.
|
|
||||||
|
|
||||||
5. Run `npm start` to start the application.
|
|
||||||
|
|
||||||
## LinkWarden Development
|
|
||||||
|
|
||||||
All contributions are welcomed! But first 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,29 +0,0 @@
|
||||||
# Production image for api
|
|
||||||
# See https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#running-on-alpine
|
|
||||||
|
|
||||||
FROM node:18-alpine
|
|
||||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
|
|
||||||
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
|
||||||
|
|
||||||
WORKDIR /home/node
|
|
||||||
VOLUME /home/node/node_modules
|
|
||||||
|
|
||||||
RUN apk add --no-cache \
|
|
||||||
chromium \
|
|
||||||
nss \
|
|
||||||
freetype \
|
|
||||||
harfbuzz \
|
|
||||||
ca-certificates \
|
|
||||||
ttf-freefont
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
RUN npm ci && mkdir -p /media
|
|
||||||
|
|
||||||
# The following command fails when attempting to chown the node_modules directory.
|
|
||||||
# Running it in its own layer allows it to modify the volume.
|
|
||||||
RUN chown -R node:node /home/node /media
|
|
||||||
|
|
||||||
USER node
|
|
||||||
|
|
||||||
EXPOSE 5500
|
|
||||||
CMD node server.js
|
|
|
@ -1,7 +0,0 @@
|
||||||
module.exports.port = process.env.PORT || 5500;
|
|
||||||
module.exports.URI = process.env.MONGODB_URI || "mongodb://localhost:27017"; // URI
|
|
||||||
module.exports.database = process.env.DB_NAME || "sample_db"; // Database name
|
|
||||||
module.exports.collection = process.env.COLLECTION_NAME || "list"; // Collection name
|
|
||||||
const storageLocation = process.env.STORAGE_LOCATION || "./media";
|
|
||||||
module.exports.screenshotDirectory = storageLocation + "/screenshots";
|
|
||||||
module.exports.pdfDirectory = storageLocation + "/pdfs";
|
|
|
@ -1,54 +0,0 @@
|
||||||
const puppeteer = require("puppeteer");
|
|
||||||
const { PuppeteerBlocker } = require("@cliqz/adblocker-puppeteer");
|
|
||||||
const fetch = require("cross-fetch");
|
|
||||||
const { screenshotDirectory, pdfDirectory } = require("../config.js");
|
|
||||||
|
|
||||||
module.exports = async (link, id) => {
|
|
||||||
const browser = await puppeteer.launch({
|
|
||||||
args: ["--no-sandbox"],
|
|
||||||
timeout: 10000,
|
|
||||||
});
|
|
||||||
const page = await browser.newPage();
|
|
||||||
|
|
||||||
await PuppeteerBlocker.fromPrebuiltAdsAndTracking(fetch).then((blocker) => {
|
|
||||||
blocker.enableBlockingInPage(page);
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(link, { waitUntil: "load", timeout: 300000 });
|
|
||||||
|
|
||||||
await page.setViewport({
|
|
||||||
width: 1200,
|
|
||||||
height: 800,
|
|
||||||
});
|
|
||||||
|
|
||||||
await autoScroll(page);
|
|
||||||
|
|
||||||
await page.screenshot({
|
|
||||||
path: screenshotDirectory + "/" + id + ".png",
|
|
||||||
fullPage: true,
|
|
||||||
});
|
|
||||||
await page.pdf({ path: pdfDirectory + "/" + id + ".pdf", format: "a4" });
|
|
||||||
|
|
||||||
await browser.close();
|
|
||||||
};
|
|
||||||
|
|
||||||
async function autoScroll(page) {
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
let totalHeight = 0;
|
|
||||||
let distance = 100;
|
|
||||||
let timer = setInterval(() => {
|
|
||||||
let scrollHeight = document.body.scrollHeight;
|
|
||||||
window.scrollBy(0, distance);
|
|
||||||
totalHeight += distance;
|
|
||||||
|
|
||||||
if (totalHeight >= scrollHeight - window.innerHeight) {
|
|
||||||
clearInterval(timer);
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.scrollTo(0,0);
|
|
||||||
});
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,24 +0,0 @@
|
||||||
{
|
|
||||||
"name": "link-warden-api",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "LinkWarden backend",
|
|
||||||
"main": "server.js",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "nodemon server.js"
|
|
||||||
},
|
|
||||||
"author": "",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@cliqz/adblocker-puppeteer": "^1.23.8",
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"cross-fetch": "^3.1.5",
|
|
||||||
"express": "^4.17.3",
|
|
||||||
"mongodb": "^4.5.0",
|
|
||||||
"puppeteer": "^14.1.1",
|
|
||||||
"sanitize-filename": "^1.6.3",
|
|
||||||
"uuid": "^8.3.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"nodemon": "^2.0.15"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
<!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" />
|
|
||||||
<title>Document</title>
|
|
||||||
</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 the problem persists, looks like the file wasn't created...</h3>
|
|
||||||
<h1>¯\_(ツ)_/¯</h1>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
File diff suppressed because it is too large
Load Diff
155
api/server.js
155
api/server.js
|
@ -1,155 +0,0 @@
|
||||||
const express = require("express");
|
|
||||||
const app = express();
|
|
||||||
const { MongoClient } = require("mongodb");
|
|
||||||
const cors = require("cors");
|
|
||||||
const getData = require("./modules/getData.js");
|
|
||||||
const fs = require("fs");
|
|
||||||
const {
|
|
||||||
port,
|
|
||||||
URI,
|
|
||||||
database,
|
|
||||||
collection,
|
|
||||||
screenshotDirectory,
|
|
||||||
pdfDirectory,
|
|
||||||
} = require("./config.js");
|
|
||||||
const fetch = require("cross-fetch");
|
|
||||||
const sanitize = require("sanitize-filename");
|
|
||||||
|
|
||||||
const client = new MongoClient(URI);
|
|
||||||
const db = client.db(database);
|
|
||||||
const list = db.collection(collection);
|
|
||||||
|
|
||||||
// Create the storage directories if they do not exist
|
|
||||||
if (!fs.existsSync(screenshotDirectory)) {
|
|
||||||
fs.mkdirSync(screenshotDirectory, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync(pdfDirectory)) {
|
|
||||||
fs.mkdirSync(pdfDirectory, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
app.use(cors());
|
|
||||||
|
|
||||||
app.use(express.json());
|
|
||||||
|
|
||||||
app.get("/api", async (req, res) => {
|
|
||||||
const data = await getDoc();
|
|
||||||
res.send(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/screenshots/:id", async (req, res) => {
|
|
||||||
res.sendFile(
|
|
||||||
__dirname + "/" + screenshotDirectory + "/" + sanitize(req.params.id),
|
|
||||||
(err) => {
|
|
||||||
if (err) {
|
|
||||||
res.sendFile(__dirname + "/pages/404.html");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/pdfs/:id", async (req, res) => {
|
|
||||||
res.sendFile(
|
|
||||||
__dirname + "/" + pdfDirectory + "/" + sanitize(req.params.id),
|
|
||||||
(err) => {
|
|
||||||
if (err) {
|
|
||||||
res.sendFile(__dirname + "/pages/404.html");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post("/api", async (req, res) => {
|
|
||||||
const pageToVisit = req.body.link;
|
|
||||||
const id = req.body._id;
|
|
||||||
const getTitle = async (url) => {
|
|
||||||
let body;
|
|
||||||
await fetch(url)
|
|
||||||
.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);
|
|
||||||
await insertDoc(req.body);
|
|
||||||
|
|
||||||
res.send("DONE!");
|
|
||||||
getData(pageToVisit, req.body._id);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
insertDoc(req.body);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.put("/api", async (req, res) => {
|
|
||||||
const id = req.body._id;
|
|
||||||
|
|
||||||
await updateDoc(id, req.body);
|
|
||||||
|
|
||||||
res.send("Updated!");
|
|
||||||
});
|
|
||||||
|
|
||||||
app.delete("/api", async (req, res) => {
|
|
||||||
const id = req.body.id;
|
|
||||||
|
|
||||||
await deleteDoc(id);
|
|
||||||
|
|
||||||
res.send(`Link with _id:${id} deleted.`);
|
|
||||||
});
|
|
||||||
|
|
||||||
async function updateDoc(id, updatedListing) {
|
|
||||||
try {
|
|
||||||
await list.updateOne({ _id: id }, { $set: updatedListing });
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function insertDoc(doc) {
|
|
||||||
try {
|
|
||||||
await list.insertOne(doc);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getDoc() {
|
|
||||||
try {
|
|
||||||
const result = await list.find({}).toArray();
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteDoc(doc) {
|
|
||||||
doc = sanitize(doc);
|
|
||||||
try {
|
|
||||||
const result = await list.deleteOne({ _id: doc });
|
|
||||||
|
|
||||||
fs.unlink(screenshotDirectory + "/" + doc + ".png", (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
fs.unlink(pdfDirectory + "/" + doc + ".pdf", (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app.listen(port, () => {
|
|
||||||
console.log(`Success! running on port ${port}.`);
|
|
||||||
client.connect();
|
|
||||||
});
|
|
Binary file not shown.
Before Width: | Height: | Size: 251 KiB |
|
@ -1,40 +0,0 @@
|
||||||
version: "3"
|
|
||||||
|
|
||||||
## This compose file can be used for development
|
|
||||||
|
|
||||||
services:
|
|
||||||
mongo:
|
|
||||||
image: mongo
|
|
||||||
volumes:
|
|
||||||
- ./mongo:/data/db
|
|
||||||
ports:
|
|
||||||
- 27017
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
link-warden-api:
|
|
||||||
build: ./api
|
|
||||||
environment:
|
|
||||||
- MONGODB_URI=mongodb://mongo:27017/
|
|
||||||
- PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
|
||||||
volumes:
|
|
||||||
- ./api:/home/node
|
|
||||||
ports:
|
|
||||||
- ${API_PORT:-5500}:5500
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
- mongo
|
|
||||||
|
|
||||||
link-warden:
|
|
||||||
build: .
|
|
||||||
environment:
|
|
||||||
# - DANGEROUSLY_DISABLE_HOST_CHECK=true
|
|
||||||
- REACT_APP_API_HOST=http://${API_ADDRESS:-localhost}:${API_PORT:-5500}
|
|
||||||
command: npm run go
|
|
||||||
volumes:
|
|
||||||
- /home/node/node_modules
|
|
||||||
- .:/home/node
|
|
||||||
ports:
|
|
||||||
- ${CLIENT_PORT:-3000}:3000
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
- link-warden-api
|
|
File diff suppressed because it is too large
Load Diff
48
package.json
48
package.json
|
@ -1,48 +0,0 @@
|
||||||
{
|
|
||||||
"name": "link-warden",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "A place for all your links and resources.",
|
|
||||||
"private": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@testing-library/jest-dom": "^5.16.4",
|
|
||||||
"@testing-library/react": "^12.1.4",
|
|
||||||
"@testing-library/user-event": "^13.5.0",
|
|
||||||
"nanoid": "^3.3.4",
|
|
||||||
"react": "^18.0.0",
|
|
||||||
"react-awesome-button": "^6.5.1",
|
|
||||||
"react-dom": "^18.0.0",
|
|
||||||
"react-lazyload": "^3.2.0",
|
|
||||||
"react-loader-spinner": "^6.0.0-0",
|
|
||||||
"react-pro-sidebar": "^0.7.1",
|
|
||||||
"react-router-dom": "^6.3.0",
|
|
||||||
"react-scripts": "5.0.0",
|
|
||||||
"react-select": "^5.3.2",
|
|
||||||
"sass": "^1.53.0",
|
|
||||||
"web-vitals": "^2.1.4"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"start": " (cd api && npm run dev) & npm run go",
|
|
||||||
"go": "react-scripts start",
|
|
||||||
"build": "react-scripts build",
|
|
||||||
"test": "react-scripts test",
|
|
||||||
"eject": "react-scripts eject"
|
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"extends": [
|
|
||||||
"react-app",
|
|
||||||
"react-app/jest"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"browserslist": {
|
|
||||||
"production": [
|
|
||||||
">0.2%",
|
|
||||||
"not dead",
|
|
||||||
"not op_mini all"
|
|
||||||
],
|
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 3.8 KiB |
|
@ -1,18 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/solid.css">
|
|
||||||
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
|
||||||
<title>LinkWarden</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
Binary file not shown.
Before Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.4 KiB |
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"short_name": "LinkWarden",
|
|
||||||
"name": "LinkWarden",
|
|
||||||
"start_url": ".",
|
|
||||||
"display": "standalone",
|
|
||||||
"theme_color": "#000000",
|
|
||||||
"background_color": "#ffffff"
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
# https://www.robotstxt.org/robotstxt.html
|
|
||||||
User-agent: *
|
|
||||||
Disallow:
|
|
221
src/App.js
221
src/App.js
|
@ -1,221 +0,0 @@
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import "./styles/App.css";
|
|
||||||
import List from "./componets/List";
|
|
||||||
import AddItem from "./componets/AddItem";
|
|
||||||
import { API_HOST } from "./config";
|
|
||||||
import Filters from "./componets/Filters";
|
|
||||||
import sortList from "./modules/sortList";
|
|
||||||
import filter from "./modules/filterData";
|
|
||||||
import concatTags from "./modules/concatTags";
|
|
||||||
import concatCollections from "./modules/concatCollections";
|
|
||||||
import Loader from "./componets/Loader";
|
|
||||||
import SideBar from "./componets/SideBar";
|
|
||||||
import Tags from "./routes/Tags.js";
|
|
||||||
import Collections from "./routes/Collections.js";
|
|
||||||
import { Route, Routes } from "react-router-dom";
|
|
||||||
import { AwesomeButton } from "react-awesome-button";
|
|
||||||
import "react-awesome-button/dist/themes/theme-blue.css";
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const [data, setData] = useState([]),
|
|
||||||
[newBox, setNewBox] = useState(false),
|
|
||||||
[filterBox, setFilterBox] = useState(false),
|
|
||||||
[searchQuery, setSearchQuery] = useState(""),
|
|
||||||
[filterCheckbox, setFilterCheckbox] = useState([true, true, true]),
|
|
||||||
[sortBy, setSortBy] = useState(1),
|
|
||||||
[loader, setLoader] = useState(false),
|
|
||||||
[lightMode, setLightMode] = useState(
|
|
||||||
localStorage.getItem("light-mode") === "true"
|
|
||||||
),
|
|
||||||
[toggle, setToggle] = useState(false);
|
|
||||||
|
|
||||||
function SetLoader(x) {
|
|
||||||
setLoader(x);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleFilterCheckbox(newVal) {
|
|
||||||
setFilterCheckbox(newVal);
|
|
||||||
}
|
|
||||||
|
|
||||||
function exitAdding() {
|
|
||||||
setNewBox(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function exitFilter() {
|
|
||||||
setFilterBox(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function search(e) {
|
|
||||||
setSearchQuery(e.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSorting(e) {
|
|
||||||
setSortBy(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleToggleSidebar() {
|
|
||||||
setToggle(!toggle);
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredData = filter(data, searchQuery, filterCheckbox);
|
|
||||||
|
|
||||||
async function fetchData() {
|
|
||||||
const res = await fetch(API_HOST + "/api");
|
|
||||||
const resJSON = await res.json();
|
|
||||||
const data = resJSON.reverse();
|
|
||||||
setData(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const sortedData = sortList(data, sortBy);
|
|
||||||
setData(sortedData);
|
|
||||||
exitFilter();
|
|
||||||
// eslint-disable-next-line
|
|
||||||
}, [sortBy, filterCheckbox]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData();
|
|
||||||
// eslint-disable-next-line
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (lightMode) {
|
|
||||||
document.body.classList.add("light");
|
|
||||||
} else {
|
|
||||||
document.body.classList.remove("light");
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem("light-mode", lightMode);
|
|
||||||
}, [lightMode]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="App">
|
|
||||||
<SideBar
|
|
||||||
tags={concatTags(data)}
|
|
||||||
collections={concatCollections(data)}
|
|
||||||
handleToggleSidebar={handleToggleSidebar}
|
|
||||||
toggle={toggle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="content">
|
|
||||||
<div className="head">
|
|
||||||
<div className="sidebar-btn">
|
|
||||||
<AwesomeButton
|
|
||||||
size="icon"
|
|
||||||
type="primary"
|
|
||||||
action={handleToggleSidebar}
|
|
||||||
style={{ marginRight: "10px" }}
|
|
||||||
>
|
|
||||||

|
|
||||||
</AwesomeButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input
|
|
||||||
className="search text-field"
|
|
||||||
type="search"
|
|
||||||
placeholder=" Search"
|
|
||||||
onChange={search}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AwesomeButton
|
|
||||||
size="icon"
|
|
||||||
type="primary"
|
|
||||||
action={() => setFilterBox(true)}
|
|
||||||
style={{ marginLeft: "10px" }}
|
|
||||||
>
|
|
||||||

|
|
||||||
</AwesomeButton>
|
|
||||||
|
|
||||||
<AwesomeButton
|
|
||||||
size="icon"
|
|
||||||
type="primary"
|
|
||||||
action={() => setNewBox(true)}
|
|
||||||
style={{ marginLeft: "auto" }}
|
|
||||||
>
|
|
||||||

|
|
||||||
</AwesomeButton>
|
|
||||||
|
|
||||||
<AwesomeButton
|
|
||||||
size="icon"
|
|
||||||
type="primary"
|
|
||||||
action={() => setLightMode(!lightMode)}
|
|
||||||
style={{ marginLeft: "10px" }}
|
|
||||||
>
|
|
||||||
<div className="dark-light"></div>
|
|
||||||
</AwesomeButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{filterBox ? (
|
|
||||||
<Filters
|
|
||||||
filterCheckbox={filterCheckbox}
|
|
||||||
handleFilterCheckbox={handleFilterCheckbox}
|
|
||||||
sortBy={handleSorting}
|
|
||||||
sort={sortBy}
|
|
||||||
onExit={exitFilter}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{newBox ? (
|
|
||||||
<AddItem
|
|
||||||
SetLoader={SetLoader}
|
|
||||||
onExit={exitAdding}
|
|
||||||
reFetch={fetchData}
|
|
||||||
lightMode={lightMode}
|
|
||||||
tags={() => concatTags(data)}
|
|
||||||
collections={() => concatCollections(data)}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{loader ? <Loader lightMode={lightMode} /> : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Routes>
|
|
||||||
<Route
|
|
||||||
path="/"
|
|
||||||
element={
|
|
||||||
<div className="content">
|
|
||||||
<List
|
|
||||||
lightMode={lightMode}
|
|
||||||
SetLoader={SetLoader}
|
|
||||||
data={filteredData}
|
|
||||||
tags={concatTags(data)}
|
|
||||||
collections={concatCollections(data)}
|
|
||||||
reFetch={fetchData}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="tags/:tagId"
|
|
||||||
element={
|
|
||||||
<Tags
|
|
||||||
lightMode={lightMode}
|
|
||||||
SetLoader={SetLoader}
|
|
||||||
data={filteredData}
|
|
||||||
tags={concatTags(data)}
|
|
||||||
collections={concatCollections(data)}
|
|
||||||
reFetch={fetchData}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="collections/:collectionId"
|
|
||||||
element={
|
|
||||||
<Collections
|
|
||||||
lightMode={lightMode}
|
|
||||||
SetLoader={SetLoader}
|
|
||||||
data={filteredData}
|
|
||||||
tags={concatTags(data)}
|
|
||||||
collections={concatCollections(data)}
|
|
||||||
reFetch={fetchData}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Routes>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
|
@ -1,107 +0,0 @@
|
||||||
import { useState } from "react";
|
|
||||||
import "../styles/AddItem.css";
|
|
||||||
import TagSelection from "./TagSelection";
|
|
||||||
import addItem from "../modules/send";
|
|
||||||
import CollectionSelection from "./CollectionSelection";
|
|
||||||
import { AwesomeButton } from "react-awesome-button";
|
|
||||||
import "react-awesome-button/dist/themes/theme-blue.css";
|
|
||||||
|
|
||||||
const AddItem = ({
|
|
||||||
onExit,
|
|
||||||
reFetch,
|
|
||||||
tags,
|
|
||||||
collections,
|
|
||||||
SetLoader,
|
|
||||||
lightMode,
|
|
||||||
}) => {
|
|
||||||
const [name, setName] = useState(""),
|
|
||||||
[link, setLink] = useState(""),
|
|
||||||
[tag, setTag] = useState([]),
|
|
||||||
[collection, setCollection] = useState("Unsorted");
|
|
||||||
|
|
||||||
function newItem() {
|
|
||||||
SetLoader(true);
|
|
||||||
addItem(name, link, tag, collection, reFetch, onExit, SetLoader, "POST");
|
|
||||||
}
|
|
||||||
|
|
||||||
function SetName(e) {
|
|
||||||
setName(e.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SetLink(e) {
|
|
||||||
setLink(e.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SetTags(value) {
|
|
||||||
setTag(value.map((e) => e.value.toLowerCase()));
|
|
||||||
}
|
|
||||||
|
|
||||||
function SetCollection(value) {
|
|
||||||
setCollection(value.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function abort(e) {
|
|
||||||
if (e.target.className === "add-overlay") {
|
|
||||||
onExit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="add-overlay" onClick={abort}></div>
|
|
||||||
<div className="send-box">
|
|
||||||
<div className="box">
|
|
||||||
<h2>New Link</h2>
|
|
||||||
<div className="AddItem-content">
|
|
||||||
<h3>
|
|
||||||
<span style={{ color: "red" }}>* </span>Link:
|
|
||||||
</h3>
|
|
||||||
<input
|
|
||||||
onChange={SetLink}
|
|
||||||
className="text-field AddItem-input"
|
|
||||||
type="search"
|
|
||||||
placeholder="e.g. https://example.com/"
|
|
||||||
/>
|
|
||||||
<h3>
|
|
||||||
Name: <span className="optional">(Optional)</span>
|
|
||||||
</h3>
|
|
||||||
<input
|
|
||||||
onChange={SetName}
|
|
||||||
className="text-field AddItem-input"
|
|
||||||
type="search"
|
|
||||||
placeholder="e.g. Example Tutorial"
|
|
||||||
/>
|
|
||||||
<h3>
|
|
||||||
Tags: <span className="optional">(Optional)</span>
|
|
||||||
</h3>
|
|
||||||
<TagSelection setTags={SetTags} tags={tags} lightMode={lightMode} />
|
|
||||||
<h3>
|
|
||||||
Collections: <span className="optional">(Optional)</span>
|
|
||||||
</h3>
|
|
||||||
<CollectionSelection
|
|
||||||
setCollection={SetCollection}
|
|
||||||
collections={collections}
|
|
||||||
lightMode={lightMode}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<AwesomeButton
|
|
||||||
size="medium"
|
|
||||||
action={newItem}
|
|
||||||
style={{
|
|
||||||
marginTop: "20px",
|
|
||||||
display: "block",
|
|
||||||
marginLeft: "auto",
|
|
||||||
marginRight: "auto",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Add 
|
|
||||||
</AwesomeButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddItem;
|
|
|
@ -1,81 +0,0 @@
|
||||||
import CreatableSelect from "react-select/creatable";
|
|
||||||
|
|
||||||
export default function CollectionSelection({
|
|
||||||
setCollection,
|
|
||||||
collections,
|
|
||||||
collection = "Unsorted",
|
|
||||||
lightMode,
|
|
||||||
}) {
|
|
||||||
const customStyles = {
|
|
||||||
container: (provided) => ({
|
|
||||||
...provided,
|
|
||||||
textShadow: "none",
|
|
||||||
}),
|
|
||||||
|
|
||||||
placeholder: (provided) => ({
|
|
||||||
...provided,
|
|
||||||
color: "#a9a9a9",
|
|
||||||
}),
|
|
||||||
|
|
||||||
option: (provided) => ({
|
|
||||||
...provided,
|
|
||||||
':before': {
|
|
||||||
content: '""',
|
|
||||||
marginRight: 8,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
menu: (provided) => ({
|
|
||||||
...provided,
|
|
||||||
border: "solid",
|
|
||||||
borderWidth: "1px",
|
|
||||||
borderRadius: "0px",
|
|
||||||
borderColor: "rgb(141, 141, 141)",
|
|
||||||
opacity: "90%",
|
|
||||||
color: "gray",
|
|
||||||
background: lightMode ? "#e0e0e0" : "#273949",
|
|
||||||
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",
|
|
||||||
|
|
||||||
}),
|
|
||||||
|
|
||||||
singleValue: (provided) => ({
|
|
||||||
...provided,
|
|
||||||
':before': {
|
|
||||||
content: '""',
|
|
||||||
marginRight: 8,
|
|
||||||
},
|
|
||||||
color: lightMode ? "rgb(64, 64, 64)" : "white",
|
|
||||||
}),
|
|
||||||
|
|
||||||
control: (provided) => ({
|
|
||||||
...provided,
|
|
||||||
background: lightMode ? "#e0e0e0" : "#273949",
|
|
||||||
borderWidth: "2px",
|
|
||||||
borderColor: lightMode ? "#1e88e5": "#e7f4ff",
|
|
||||||
borderRadius: "50px",
|
|
||||||
boxShadow: lightMode ? "0px 2px 0px #354c7d, 0px 3px 1px #363636" : "0px 2px 0px #c6e4ff, 0px 3px 1px #363636",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = collections().map((e) => {
|
|
||||||
return { value: e, label: e };
|
|
||||||
});
|
|
||||||
|
|
||||||
const defaultCollection = { value: collection, label: collection };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CreatableSelect
|
|
||||||
className="select"
|
|
||||||
defaultValue={defaultCollection}
|
|
||||||
styles={customStyles}
|
|
||||||
onChange={setCollection}
|
|
||||||
options={data}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,144 +0,0 @@
|
||||||
import { useState } from "react";
|
|
||||||
import deleteEntity from "../modules/deleteEntity";
|
|
||||||
import "../styles/AddItem.css";
|
|
||||||
import TagSelection from "./TagSelection";
|
|
||||||
import editItem from "../modules/send";
|
|
||||||
import CollectionSelection from "./CollectionSelection";
|
|
||||||
import { AwesomeButton } from "react-awesome-button";
|
|
||||||
import "react-awesome-button/dist/themes/theme-blue.css";
|
|
||||||
|
|
||||||
const EditItem = ({
|
|
||||||
tags,
|
|
||||||
collections,
|
|
||||||
item,
|
|
||||||
onExit,
|
|
||||||
SetLoader,
|
|
||||||
reFetch,
|
|
||||||
lightMode,
|
|
||||||
}) => {
|
|
||||||
const [name, setName] = useState(item.name),
|
|
||||||
[tag, setTag] = useState(item.tag),
|
|
||||||
[collection, setCollection] = useState(item.collection);
|
|
||||||
|
|
||||||
function EditItem() {
|
|
||||||
SetLoader(true);
|
|
||||||
editItem(
|
|
||||||
name,
|
|
||||||
item.link,
|
|
||||||
tag,
|
|
||||||
collection,
|
|
||||||
reFetch,
|
|
||||||
onExit,
|
|
||||||
SetLoader,
|
|
||||||
"PUT",
|
|
||||||
item._id,
|
|
||||||
item.title,
|
|
||||||
item.date
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteItem() {
|
|
||||||
SetLoader(true);
|
|
||||||
deleteEntity(item._id, reFetch, onExit, SetLoader);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SetName(e) {
|
|
||||||
setName(e.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SetTags(value) {
|
|
||||||
setTag(value.map((e) => e.value.toLowerCase()));
|
|
||||||
}
|
|
||||||
|
|
||||||
function SetCollection(value) {
|
|
||||||
setCollection(value.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function abort(e) {
|
|
||||||
if (e.target.className === "add-overlay") {
|
|
||||||
onExit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = new URL(item.link);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="add-overlay" onClick={abort}></div>
|
|
||||||
<div className="send-box">
|
|
||||||
<div className="box">
|
|
||||||
<div className="title-delete-group">
|
|
||||||
<h2 className="edit-title">Edit Link</h2>
|
|
||||||
<AwesomeButton
|
|
||||||
className="delete"
|
|
||||||
size="icon"
|
|
||||||
action={deleteItem}
|
|
||||||
style={{ marginLeft: "10px" }}
|
|
||||||
>
|
|
||||||

|
|
||||||
</AwesomeButton>
|
|
||||||
</div>
|
|
||||||
<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="text-field 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}
|
|
||||||
/>
|
|
||||||
<h3>
|
|
||||||
Collection: <span className="optional">(Optional)</span>
|
|
||||||
</h3>
|
|
||||||
<CollectionSelection
|
|
||||||
setCollection={SetCollection}
|
|
||||||
collections={collections}
|
|
||||||
collection={collection}
|
|
||||||
lightMode={lightMode}
|
|
||||||
/>
|
|
||||||
<AwesomeButton
|
|
||||||
size="medium"
|
|
||||||
action={EditItem}
|
|
||||||
style={{
|
|
||||||
marginTop: "20px",
|
|
||||||
display: "block",
|
|
||||||
marginLeft: "auto",
|
|
||||||
marginRight: "auto",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Update 
|
|
||||||
</AwesomeButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EditItem;
|
|
|
@ -1,153 +0,0 @@
|
||||||
import "../styles/Filters.css";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { AwesomeButton } from "react-awesome-button";
|
|
||||||
import "react-awesome-button/dist/themes/theme-blue.css";
|
|
||||||
|
|
||||||
const Filters = ({
|
|
||||||
filterCheckbox,
|
|
||||||
handleFilterCheckbox,
|
|
||||||
sortBy,
|
|
||||||
sort,
|
|
||||||
onExit,
|
|
||||||
}) => {
|
|
||||||
const [nameChecked, setNameChecked] = useState(filterCheckbox[0]),
|
|
||||||
[titleChecked, setTitleChecked] = useState(filterCheckbox[1]),
|
|
||||||
[tagChecked, setTagChecked] = useState(filterCheckbox[2]),
|
|
||||||
[radio, setRadio] = useState(sort);
|
|
||||||
|
|
||||||
function abort(e) {
|
|
||||||
if (e.target.className === "filter-overlay") {
|
|
||||||
onExit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRadio(e) {
|
|
||||||
setRadio(e.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyChanges() {
|
|
||||||
handleFilterCheckbox([nameChecked, titleChecked, tagChecked]);
|
|
||||||
sortBy(radio);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="filter-overlay" onClick={abort}></div>
|
|
||||||
<div className="filter-box">
|
|
||||||
<div className="filter">
|
|
||||||
<h2>Filter Results</h2>
|
|
||||||
|
|
||||||
<div className="filter-groups">
|
|
||||||
<div className="section">
|
|
||||||
<h3>Sort By</h3>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
name="sort"
|
|
||||||
checked={radio.toString() === "1"}
|
|
||||||
onChange={handleRadio}
|
|
||||||
type="radio"
|
|
||||||
value={1}
|
|
||||||
/>
|
|
||||||
 Date (Newest first)
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
name="sort"
|
|
||||||
checked={radio.toString() === "2"}
|
|
||||||
onChange={handleRadio}
|
|
||||||
type="radio"
|
|
||||||
value={2}
|
|
||||||
/>
|
|
||||||
 Date (Oldest first)
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
name="sort"
|
|
||||||
checked={radio.toString() === "3"}
|
|
||||||
onChange={handleRadio}
|
|
||||||
type="radio"
|
|
||||||
value={3}
|
|
||||||
/>
|
|
||||||
 Name (A-Z)
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
name="sort"
|
|
||||||
checked={radio.toString() === "4"}
|
|
||||||
onChange={handleRadio}
|
|
||||||
type="radio"
|
|
||||||
value={4}
|
|
||||||
/>
|
|
||||||
 Name (Z-A)
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
name="sort"
|
|
||||||
checked={radio.toString() === "5"}
|
|
||||||
onChange={handleRadio}
|
|
||||||
type="radio"
|
|
||||||
value={5}
|
|
||||||
/>
|
|
||||||
 Website title (A-Z)
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
name="sort"
|
|
||||||
checked={radio.toString() === "6"}
|
|
||||||
onChange={handleRadio}
|
|
||||||
type="radio"
|
|
||||||
value={6}
|
|
||||||
/>
|
|
||||||
 Website title (Z-A)
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="section">
|
|
||||||
<h3>Include/Exclude</h3>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={nameChecked}
|
|
||||||
onChange={() => setNameChecked(!nameChecked)}
|
|
||||||
/>
|
|
||||||
Name
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={titleChecked}
|
|
||||||
onChange={() => setTitleChecked(!titleChecked)}
|
|
||||||
/>
|
|
||||||
Website title
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={tagChecked}
|
|
||||||
onChange={() => setTagChecked(!tagChecked)}
|
|
||||||
/>
|
|
||||||
Tags
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AwesomeButton
|
|
||||||
size="medium"
|
|
||||||
action={applyChanges}
|
|
||||||
style={{
|
|
||||||
marginTop: "20px",
|
|
||||||
display: "block",
|
|
||||||
marginLeft: "auto",
|
|
||||||
marginRight: "auto",
|
|
||||||
marginBottom: "15px"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Apply 
|
|
||||||
</AwesomeButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Filters;
|
|
|
@ -1,122 +0,0 @@
|
||||||
import "../styles/List.css";
|
|
||||||
import LazyLoad from "react-lazyload";
|
|
||||||
import ViewArchived from "./ViewArchived";
|
|
||||||
import EditItem from "./EditItem";
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import NoResults from "./NoResults";
|
|
||||||
import {
|
|
||||||
AwesomeButton
|
|
||||||
} from 'react-awesome-button';
|
|
||||||
import 'react-awesome-button/dist/themes/theme-blue.css';
|
|
||||||
|
|
||||||
const List = ({ data, tags, collections, reFetch, SetLoader, lightMode }) => {
|
|
||||||
const [editBox, setEditBox] = useState(false),
|
|
||||||
[editIndex, setEditIndex] = useState(0),
|
|
||||||
[numberOfResults, setNumberOfResults] = useState(0);
|
|
||||||
|
|
||||||
function edit(index) {
|
|
||||||
setEditBox(true);
|
|
||||||
setEditIndex(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
function exitEditing() {
|
|
||||||
setEditBox(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setNumberOfResults(data.length);
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
let currentPATH = new URL(window.location.href).pathname;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="list">
|
|
||||||
{numberOfResults > 0 ? (
|
|
||||||
<p>
|
|
||||||
{currentPATH === "/" ? null : <Link className="return-btn" to="/">Return to main page</Link>} {numberOfResults} {numberOfResults === 1 ? "Link " : "Links "}
|
|
||||||
found.
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{numberOfResults === 0 ? <NoResults /> : null}
|
|
||||||
|
|
||||||
{editBox ? (
|
|
||||||
<EditItem
|
|
||||||
lightMode={lightMode}
|
|
||||||
tags={() => tags}
|
|
||||||
collections={() => collections}
|
|
||||||
onExit={exitEditing}
|
|
||||||
SetLoader={SetLoader}
|
|
||||||
reFetch={reFetch}
|
|
||||||
item={data[editIndex]}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{/* eslint-disable-next-line */}
|
|
||||||
{data.map((e, i) => {
|
|
||||||
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 neumorphism">
|
|
||||||
<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="list-collection-label">
|
|
||||||
<Link to={`/collections/${e.collection}`}>
|
|
||||||
{e.collection}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="date">
|
|
||||||
{new Date(e.date).toDateString()}
|
|
||||||
</div>
|
|
||||||
<div className="tags">
|
|
||||||
{e.tag.map((e, i) => {
|
|
||||||
const tagPath = `/tags/${e}`;
|
|
||||||
return (
|
|
||||||
<Link to={tagPath} key={i}>
|
|
||||||
{e}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="etc">
|
|
||||||
<ViewArchived className="view-archived" id={e._id} />
|
|
||||||
<AwesomeButton
|
|
||||||
size="icon"
|
|
||||||
action={() => edit(i)}
|
|
||||||
style={{ margin: "20px 20px 20px 0px" }}
|
|
||||||
>
|
|
||||||

|
|
||||||
</AwesomeButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</LazyLoad>
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default List;
|
|
|
@ -1,12 +0,0 @@
|
||||||
import "../styles/Loader.css";
|
|
||||||
import { InfinitySpin } from "react-loader-spinner";
|
|
||||||
|
|
||||||
const Loader = ({ lightMode }) => {
|
|
||||||
return (
|
|
||||||
<div className="loader">
|
|
||||||
<InfinitySpin color={lightMode ? "Black" : "White"} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Loader;
|
|
|
@ -1,12 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
const NoResults = () => {
|
|
||||||
return (
|
|
||||||
<div className="no-results neumorphism">
|
|
||||||
<h1>¯\_(ツ)_/¯</h1>
|
|
||||||
<p>Nothing found.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NoResults;
|
|
|
@ -1,101 +0,0 @@
|
||||||
import {
|
|
||||||
ProSidebar,
|
|
||||||
SidebarHeader,
|
|
||||||
SidebarFooter,
|
|
||||||
SidebarContent,
|
|
||||||
Menu,
|
|
||||||
MenuItem,
|
|
||||||
SubMenu,
|
|
||||||
} from "react-pro-sidebar";
|
|
||||||
// import "react-pro-sidebar/dist/css/styles.css";
|
|
||||||
import "../styles/SideBar_S.scss";
|
|
||||||
import "../styles/SideBar.css";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
const SideBar = ({ tags, collections, 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 sortedCollections = collections
|
|
||||||
.sort((a, b) => {
|
|
||||||
const A = a.toLowerCase(),
|
|
||||||
B = b.toLowerCase();
|
|
||||||
if (A < B) return -1;
|
|
||||||
if (A > B) return 1;
|
|
||||||
return 0;
|
|
||||||
})
|
|
||||||
.filter((e) => {
|
|
||||||
return e !== "Unsorted";
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ProSidebar
|
|
||||||
toggled={toggle}
|
|
||||||
breakPoint="lg"
|
|
||||||
onToggle={handleToggleSidebar}
|
|
||||||
className="sidebar"
|
|
||||||
>
|
|
||||||
<SidebarHeader>
|
|
||||||
<h2>LinkWarden</h2>
|
|
||||||
</SidebarHeader>
|
|
||||||
<SidebarContent className="sidebar-content">
|
|
||||||
<Menu iconShape="circle">
|
|
||||||
<MenuItem icon={<h2 className="sidebar-icon"></h2>}>
|
|
||||||
<Link to="/">
|
|
||||||
<div className="menu-item">All</div>
|
|
||||||
</Link>
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem icon={<h2 className="sidebar-icon"></h2>}>
|
|
||||||
<Link to="/collections/Unsorted">
|
|
||||||
<div className="menu-item">Unsorted</div>
|
|
||||||
</Link>
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<SubMenu
|
|
||||||
icon={<h2 className="sidebar-icon"></h2>}
|
|
||||||
suffix={<span className="badge">{sortedCollections.length}</span>}
|
|
||||||
title={<div className="menu-item">Collections</div>}
|
|
||||||
>
|
|
||||||
{sortedCollections.map((e, i) => {
|
|
||||||
const path = `/collections/${e}`;
|
|
||||||
return (
|
|
||||||
<MenuItem prefix={<div className="sidebar-item-prefix"></div>} key={i}>
|
|
||||||
<Link className="sidebar-entity" to={path}>{e}</Link>
|
|
||||||
</MenuItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</SubMenu>
|
|
||||||
|
|
||||||
<SubMenu
|
|
||||||
icon={<h2 className="sidebar-icon"></h2>}
|
|
||||||
suffix={<span className="badge">{sortedTags.length}</span>}
|
|
||||||
title={<div className="menu-item">Tags</div>}
|
|
||||||
>
|
|
||||||
{sortedTags.map((e, i) => {
|
|
||||||
const path = `/tags/${e}`;
|
|
||||||
return (
|
|
||||||
<MenuItem prefix={<div className="sidebar-item-prefix">#</div>} key={i}>
|
|
||||||
<Link className="sidebar-entity" 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,81 +0,0 @@
|
||||||
import CreatableSelect from "react-select/creatable";
|
|
||||||
|
|
||||||
export default function TagSelection({ setTags, tags, tag = [], lightMode }) {
|
|
||||||
const customStyles = {
|
|
||||||
container: (provided) => ({
|
|
||||||
...provided,
|
|
||||||
textShadow: "none",
|
|
||||||
}),
|
|
||||||
|
|
||||||
placeholder: (provided) => ({
|
|
||||||
...provided,
|
|
||||||
color: "#a9a9a9",
|
|
||||||
}),
|
|
||||||
|
|
||||||
option: (provided) => ({
|
|
||||||
...provided,
|
|
||||||
':before': {
|
|
||||||
content: '"#"',
|
|
||||||
marginRight: 8,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
multiValueRemove: (provided) => ({
|
|
||||||
...provided,
|
|
||||||
color: "gray",
|
|
||||||
}),
|
|
||||||
|
|
||||||
menu: (provided) => ({
|
|
||||||
...provided,
|
|
||||||
border: "solid",
|
|
||||||
borderWidth: "1px",
|
|
||||||
borderRadius: "0px",
|
|
||||||
borderColor: "rgb(141, 141, 141)",
|
|
||||||
opacity: "90%",
|
|
||||||
color: "gray",
|
|
||||||
background: lightMode ? "#e0e0e0" : "#273949",
|
|
||||||
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",
|
|
||||||
}),
|
|
||||||
|
|
||||||
multiValueLabel: (provided) => ({
|
|
||||||
...provided,
|
|
||||||
':before': {
|
|
||||||
content: '"#"',
|
|
||||||
marginRight: 4,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
control: (provided) => ({
|
|
||||||
...provided,
|
|
||||||
background: lightMode ? "#e0e0e0" : "#273949",
|
|
||||||
borderWidth: "2px",
|
|
||||||
borderColor: lightMode ? "#1e88e5": "#e7f4ff",
|
|
||||||
borderRadius: "50px",
|
|
||||||
boxShadow: lightMode ? "0px 2px 0px #354c7d, 0px 3px 1px #363636" : "0px 2px 0px #c6e4ff, 0px 3px 1px #363636",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = tags().map((e) => {
|
|
||||||
return { value: e, label: e };
|
|
||||||
});
|
|
||||||
const defaultTags = tag.map((e) => {
|
|
||||||
return { value: e, label: e };
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CreatableSelect
|
|
||||||
className="select"
|
|
||||||
defaultValue={defaultTags}
|
|
||||||
styles={customStyles}
|
|
||||||
isMulti
|
|
||||||
onChange={setTags}
|
|
||||||
options={data}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
import "../styles/ViewArchived.css";
|
|
||||||
import { API_HOST } from "../config";
|
|
||||||
|
|
||||||
const ViewArchived = ({ id }) => {
|
|
||||||
const screenshotPath =
|
|
||||||
API_HOST + "/screenshots/" + id + ".png";
|
|
||||||
const pdfPath =
|
|
||||||
API_HOST + "/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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ViewArchived;
|
|
|
@ -1 +0,0 @@
|
||||||
export const API_HOST = process.env.REACT_APP_API_HOST || "http://localhost:5500"; // API full address
|
|
12
src/index.js
12
src/index.js
|
@ -1,12 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import ReactDOM from "react-dom/client";
|
|
||||||
import "./styles/index.css";
|
|
||||||
import App from "./App";
|
|
||||||
import { BrowserRouter } from "react-router-dom";
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById("root"));
|
|
||||||
root.render(
|
|
||||||
<BrowserRouter>
|
|
||||||
<App />
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
|
@ -1,13 +0,0 @@
|
||||||
const concatCollections = (data) => {
|
|
||||||
let collections = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
collections = collections.concat(data[i].collection);
|
|
||||||
}
|
|
||||||
|
|
||||||
collections = collections.filter((v, i, a) => a.indexOf(v) === i);
|
|
||||||
|
|
||||||
return collections;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default concatCollections;
|
|
|
@ -1,13 +0,0 @@
|
||||||
const concatTags = (data) => {
|
|
||||||
let tags = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
tags = tags.concat(data[i].tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
tags = tags.filter((v, i, a) => a.indexOf(v) === i);
|
|
||||||
|
|
||||||
return tags;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default concatTags;
|
|
|
@ -1,22 +0,0 @@
|
||||||
import { API_HOST } from "../config";
|
|
||||||
|
|
||||||
const deleteEntity = (id, reFetch, onExit, SetLoader) => {
|
|
||||||
fetch(API_HOST + "/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(() => onExit())
|
|
||||||
.then(() => reFetch())
|
|
||||||
.then(() => {
|
|
||||||
SetLoader(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default deleteEntity;
|
|
|
@ -1,29 +0,0 @@
|
||||||
const filteredData = (
|
|
||||||
data,
|
|
||||||
searchQuery,
|
|
||||||
filterCheckbox
|
|
||||||
) => {
|
|
||||||
return data.filter((e) => {
|
|
||||||
const linkName = e.name.toLowerCase().includes(searchQuery.toLowerCase());
|
|
||||||
const websiteTitle = e.title.toLowerCase().includes(searchQuery.toLowerCase());
|
|
||||||
const tags = e.tag.some((e) => e.includes(searchQuery.toLowerCase()));
|
|
||||||
|
|
||||||
if (filterCheckbox.every(e => e === true)) {
|
|
||||||
return linkName || websiteTitle || tags;
|
|
||||||
} else if (filterCheckbox[0] && filterCheckbox[2]) {
|
|
||||||
return linkName || tags;
|
|
||||||
} else if (filterCheckbox[0] && filterCheckbox[1]) {
|
|
||||||
return linkName || websiteTitle;
|
|
||||||
} else if (filterCheckbox[2] && filterCheckbox[1]) {
|
|
||||||
return tags || websiteTitle;
|
|
||||||
} else if (filterCheckbox[0]) {
|
|
||||||
return linkName;
|
|
||||||
} else if (filterCheckbox[1]) {
|
|
||||||
return websiteTitle;
|
|
||||||
} else if (filterCheckbox[2]) {
|
|
||||||
return tags;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default filteredData;
|
|
|
@ -1,62 +0,0 @@
|
||||||
import { API_HOST } from "../config";
|
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
|
|
||||||
const addItem = async (
|
|
||||||
name,
|
|
||||||
link,
|
|
||||||
tag,
|
|
||||||
collection,
|
|
||||||
reFetch,
|
|
||||||
onExit,
|
|
||||||
SetLoader,
|
|
||||||
method,
|
|
||||||
id = nanoid(),
|
|
||||||
title = "",
|
|
||||||
date = new Date().toString()
|
|
||||||
) => {
|
|
||||||
function isValidHttpUrl(string) {
|
|
||||||
let url;
|
|
||||||
|
|
||||||
try {
|
|
||||||
url = new URL(string);
|
|
||||||
} catch (_) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return url.protocol === "http:" || url.protocol === "https:";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isValidHttpUrl(link)) {
|
|
||||||
fetch(API_HOST + "/api", {
|
|
||||||
method: method,
|
|
||||||
body: JSON.stringify({
|
|
||||||
_id: id,
|
|
||||||
name: name,
|
|
||||||
title: title,
|
|
||||||
link: link,
|
|
||||||
tag: tag,
|
|
||||||
collection: collection,
|
|
||||||
date: date,
|
|
||||||
}),
|
|
||||||
headers: {
|
|
||||||
"Content-type": "application/json; charset=UTF-8",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.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 {
|
|
||||||
SetLoader(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default addItem;
|
|
|
@ -1,48 +0,0 @@
|
||||||
const sortList = (data, sortBy) => {
|
|
||||||
let sortedData = data;
|
|
||||||
if (sortBy.toString() === '1') {
|
|
||||||
sortedData.sort((a, b) => {
|
|
||||||
return new Date(b.date) - new Date(a.date);
|
|
||||||
});
|
|
||||||
} else if (sortBy.toString() === '2') {
|
|
||||||
sortedData.sort((a, b) => {
|
|
||||||
return new Date(a.date) - new Date(b.date);
|
|
||||||
});
|
|
||||||
} else if (sortBy.toString() === '3') {
|
|
||||||
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.toString() === '4') {
|
|
||||||
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.toString() === '5') {
|
|
||||||
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.toString() === '6') {
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default sortList;
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import List from "../componets/List";
|
|
||||||
|
|
||||||
const Collections = ({ data, tags, collections, SetLoader, lightMode, reFetch }) => {
|
|
||||||
const { collectionId } = useParams();
|
|
||||||
const dataWithMatchingTag = data.filter((e) => {
|
|
||||||
return e.collection.includes(collectionId);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="content">
|
|
||||||
<List
|
|
||||||
lightMode={lightMode}
|
|
||||||
data={dataWithMatchingTag}
|
|
||||||
tags={tags}
|
|
||||||
collections={collections}
|
|
||||||
SetLoader={SetLoader}
|
|
||||||
reFetch={reFetch}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Collections;
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import List from "../componets/List";
|
|
||||||
|
|
||||||
const Tags = ({ data, tags, collections, SetLoader, lightMode, reFetch }) => {
|
|
||||||
const { tagId } = useParams();
|
|
||||||
const dataWithMatchingTag = data.filter((e) => {
|
|
||||||
return e.tag.includes(tagId);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="content">
|
|
||||||
<List
|
|
||||||
lightMode={lightMode}
|
|
||||||
data={dataWithMatchingTag}
|
|
||||||
tags={tags}
|
|
||||||
collections={collections}
|
|
||||||
SetLoader={SetLoader}
|
|
||||||
reFetch={reFetch}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Tags;
|
|
|
@ -1,83 +0,0 @@
|
||||||
@media (min-width: 800px) {
|
|
||||||
.box {
|
|
||||||
left: 30%;
|
|
||||||
right: 30%;
|
|
||||||
min-width: 300px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 800px) {
|
|
||||||
.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;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.send-box {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box {
|
|
||||||
border-radius: 50px;
|
|
||||||
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;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box h2 {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box h3 {
|
|
||||||
font-weight: 300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.AddItem-content {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.AddItem-input {
|
|
||||||
font-size: 1rem;
|
|
||||||
padding: 10px;
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadein {
|
|
||||||
from {
|
|
||||||
opacity: 0%;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.optional {
|
|
||||||
color: gray;
|
|
||||||
font-size: 0.8em;
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
@media (width >= 650px) {
|
|
||||||
.search {
|
|
||||||
width: 35%;
|
|
||||||
min-width: 300px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (400px < width < 650px) {
|
|
||||||
.search {
|
|
||||||
width: 40%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (width <= 400px) {
|
|
||||||
.search {
|
|
||||||
width: 120px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 993px) {
|
|
||||||
.content {
|
|
||||||
margin-left: 270px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-btn {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.App {
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding: 0px 20px 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.head {
|
|
||||||
padding-top: 20px;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search {
|
|
||||||
padding: 10px;
|
|
||||||
font-family: "Font Awesome 5 Free";
|
|
||||||
padding-left: 10px;
|
|
||||||
font-size: 1rem;
|
|
||||||
border: solid;
|
|
||||||
border-radius: 50px;
|
|
||||||
border-width: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select {
|
|
||||||
font-family: "Font Awesome 5 Free";
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
@media (min-width: 600px) {
|
|
||||||
.filter {
|
|
||||||
left: 10%;
|
|
||||||
right: 10%;
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-groups {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.filter {
|
|
||||||
left: 10%;
|
|
||||||
right: 10%;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-box {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter {
|
|
||||||
border-radius: 50px;
|
|
||||||
animation: fadein 0.3s;
|
|
||||||
border: solid;
|
|
||||||
border-width: 1px;
|
|
||||||
font-weight: 300;
|
|
||||||
border-color: rgb(141, 141, 141);
|
|
||||||
justify-content: center;
|
|
||||||
box-shadow: rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px;
|
|
||||||
padding: 10px;
|
|
||||||
position: absolute;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter h2 {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter h3 {
|
|
||||||
font-weight: 300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section > label {
|
|
||||||
display: block;
|
|
||||||
text-align: left;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-family: "Font Awesome 5 Free";
|
|
||||||
padding: 10px;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadein {
|
|
||||||
from {
|
|
||||||
opacity: 0%;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,206 +0,0 @@
|
||||||
@media (min-width: 650px) {
|
|
||||||
.list-entity-content {
|
|
||||||
margin-left: 70px;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags {
|
|
||||||
margin: 10px 10px 10px 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.img-content-grp {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.etc {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-row {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-row:hover img {
|
|
||||||
opacity: 90%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 650px) {
|
|
||||||
.list-entity-content {
|
|
||||||
margin-top: 50px;
|
|
||||||
padding-left: 20px;
|
|
||||||
padding-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
|
||||||
display: block;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags {
|
|
||||||
margin: 10px auto 10px auto;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-row {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.etc {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.img-content-grp {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
margin-right: auto;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.list {
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
border-spacing: 10px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list img {
|
|
||||||
pointer-events: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
position: absolute;
|
|
||||||
filter: blur(5px);
|
|
||||||
opacity: 50%;
|
|
||||||
margin: 20px;
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
transition: opacity 0.3s ease-in-out;
|
|
||||||
will-change: transform, opacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-entity-content {
|
|
||||||
z-index: 0;
|
|
||||||
justify-content: space-between;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
|
||||||
white-space: nowrap;
|
|
||||||
font-family: "Font Awesome 5 Free";
|
|
||||||
pointer-events: all;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link::after {
|
|
||||||
content: " ";
|
|
||||||
opacity: 0%;
|
|
||||||
transition: opacity 0.1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link:hover::after {
|
|
||||||
opacity: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-name {
|
|
||||||
font-size: 2rem;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags {
|
|
||||||
display: flex;
|
|
||||||
border-width: 1px;
|
|
||||||
width: fit-content;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
border-radius: 5px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags a {
|
|
||||||
text-shadow: none;
|
|
||||||
margin: 5px;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags a::before {
|
|
||||||
color: rgb(0, 162, 255);
|
|
||||||
content: "# ";
|
|
||||||
}
|
|
||||||
|
|
||||||
.num {
|
|
||||||
font-size: 1rem;
|
|
||||||
margin-right: 10px;
|
|
||||||
opacity: 80%;
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
opacity: 80%;
|
|
||||||
margin-right: auto;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-results {
|
|
||||||
text-align: center;
|
|
||||||
padding-top: 5%;
|
|
||||||
padding-bottom: 5%;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-title {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-delete-group {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-collection-label {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-collection-label a::before {
|
|
||||||
font-family: "Font Awesome 5 Free";
|
|
||||||
content: " ";
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-collection-label a {
|
|
||||||
opacity: 80%;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.return-btn {
|
|
||||||
color: inherit;
|
|
||||||
opacity: 80%;
|
|
||||||
padding: 5px;
|
|
||||||
font-size: small;
|
|
||||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
.loader {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 10%;
|
|
||||||
left: 30%;
|
|
||||||
right: 30%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
.sidebar {
|
|
||||||
height: 100vh;
|
|
||||||
position: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar h2 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.credits {
|
|
||||||
text-align: center;
|
|
||||||
font-size: small;
|
|
||||||
}
|
|
||||||
|
|
||||||
.credits a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
padding: 3px 10px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
border-radius: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-icon {
|
|
||||||
font-family: "Font Awesome 5 Free";
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item {
|
|
||||||
color: rgb(255, 255, 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item:hover {
|
|
||||||
color: rgb(255, 255, 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pro-inner-item {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-entity {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-item-prefix {
|
|
||||||
font-family: "Font Awesome 5 Free";
|
|
||||||
}
|
|
||||||
|
|
||||||
.pro-sidebar-layout * {
|
|
||||||
color: white;
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
$sidebar-bg-color: #373737;
|
|
||||||
$submenu-bg-color: #373737;
|
|
||||||
|
|
||||||
@import '~react-pro-sidebar/dist/scss/styles.scss';
|
|
|
@ -1,10 +0,0 @@
|
||||||
.view-archived {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.seperator {
|
|
||||||
width: 100%;
|
|
||||||
color: #1f2c38;
|
|
||||||
}
|
|
|
@ -1,138 +0,0 @@
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
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;
|
|
||||||
background-color: #273949;
|
|
||||||
text-shadow: 0px 1px 2px #000000;
|
|
||||||
color: white;
|
|
||||||
transition: background-color 0.1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
*::selection {
|
|
||||||
background-color: black;
|
|
||||||
color: white;
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark Mode settings (Default) */
|
|
||||||
|
|
||||||
.neumorphism {
|
|
||||||
border-radius: 50px;
|
|
||||||
background: #273949;
|
|
||||||
box-shadow: 5px 5px 10px #10171d,
|
|
||||||
-5px -5px 10px #3e5b75;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-field {
|
|
||||||
background-color: #273949;
|
|
||||||
border: solid;
|
|
||||||
border-width: 2px;
|
|
||||||
color: White;
|
|
||||||
border-color: #e7f4ff;
|
|
||||||
box-shadow: 0px 2px 0px #c6e4ff, 0px 3px 1px #363636;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
-webkit-box-shadow: 0px 2px 0px #c6e4ff, 0px 3px 1px #363636;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-light::before {
|
|
||||||
content: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
.App .aws-btn {
|
|
||||||
font-family: "Font Awesome 5 Free";
|
|
||||||
--button-default-height: 51px;
|
|
||||||
--button-default-font-size: 14px;
|
|
||||||
--button-default-border-radius: 25px;
|
|
||||||
--button-horizontal-padding: 20px;
|
|
||||||
--button-raise-level: 3px;
|
|
||||||
--button-hover-pressure: 0;
|
|
||||||
--transform-speed: 0.025s;
|
|
||||||
--button-primary-color: #273949;
|
|
||||||
--button-primary-color-dark: #c6e4ff;
|
|
||||||
--button-primary-color-light: #c6e4ff;
|
|
||||||
--button-primary-color-hover: #0d4a7f;
|
|
||||||
--button-primary-color-active: #0f5ca0;
|
|
||||||
--button-primary-border: 2px solid #e7f4ff;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
|
||||||
color: rgb(194, 193, 193);
|
|
||||||
}
|
|
||||||
|
|
||||||
.search {
|
|
||||||
transition: background-color 0.1s;
|
|
||||||
background-color: #273949;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter, .box {
|
|
||||||
background-color: #273949;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Light Mode settings */
|
|
||||||
|
|
||||||
.light .dark-light::before {
|
|
||||||
content: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
.light {
|
|
||||||
text-shadow: 0px 1px 2px #ffffff;
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
color: rgb(64, 64, 64);
|
|
||||||
}
|
|
||||||
|
|
||||||
.light .neumorphism {
|
|
||||||
border-radius: 50px;
|
|
||||||
background: #e0e0e0;
|
|
||||||
box-shadow: 5px 5px 10px #5a5a5a,
|
|
||||||
-5px -5px 10px #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.light .title {
|
|
||||||
color: rgb(0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.light .text-field {
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
border: solid;
|
|
||||||
border-width: 2px;
|
|
||||||
color: black;
|
|
||||||
border-color: #1e88e5;
|
|
||||||
box-shadow: 0px 2px 0px #354c7d, 0px 3px 1px #363636;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
-webkit-box-shadow: 0px 2px 0px #354c7d, 0px 3px 1px #363636;
|
|
||||||
}
|
|
||||||
|
|
||||||
.light .box, .light .filter {
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.light .link {
|
|
||||||
color: rgb(102, 102, 102);
|
|
||||||
}
|
|
||||||
|
|
||||||
.light .aws-btn {
|
|
||||||
font-family: "Font Awesome 5 Free";
|
|
||||||
--button-default-height: 51px;
|
|
||||||
--button-default-font-size: 14px;
|
|
||||||
--button-default-border-radius: 25px;
|
|
||||||
--button-horizontal-padding: 20px;
|
|
||||||
--button-raise-level: 3px;
|
|
||||||
--button-hover-pressure: 0;
|
|
||||||
--transform-speed: 0.025s;
|
|
||||||
--button-primary-color: #e0e0e0;
|
|
||||||
--button-primary-color-dark: #354c7d;
|
|
||||||
--button-primary-color-light: #354c7d;
|
|
||||||
--button-primary-color-hover: #e1eaf1;
|
|
||||||
--button-primary-color-active: #e2e2e2;
|
|
||||||
--button-primary-border: 2px solid #1e88e5;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
Ŝarĝante…
Reference in New Issue