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
|
||||
|
||||
> **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,34 +1,104 @@
|
|||
node_modules
|
||||
..pnp
|
||||
.pnp.js
|
||||
coverage
|
||||
build
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
mongo
|
||||
.env
|
||||
lerna-debug.log*
|
||||
|
||||
api/media
|
||||
api/.ash_history
|
||||
api/.config
|
||||
api/.cache/
|
||||
api/.pki/
|
||||
api/node_modules
|
||||
api/..pnp
|
||||
api/.pnp.js
|
||||
api/coverage
|
||||
api/build
|
||||
api/.DS_Store
|
||||
api/.env.local
|
||||
api/.env.development.local
|
||||
api/.env.test.local
|
||||
api/.env.production.local
|
||||
api/npm-debug.log*
|
||||
api/yarn-debug.log*
|
||||
api/yarn-error.log*
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.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">
|
||||
<h1>
|
||||
LinkWarden
|
||||
# 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"/>
|
||||
|
||||
<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).**
|
||||
Rebuilding project from ground up...
|
||||
|
|
|
@ -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