Initial commit of version 3.

This commit is contained in:
Daniel 2022-12-18 19:00:37 +03:30
parent 509b8047f0
commit c0121a4318
56 changed files with 105 additions and 36875 deletions

View File

@ -1,8 +0,0 @@
.github
api
.gitignore
.dockerignore
Dockerfile*
node_modules
api/media
mongo

3
.env
View File

@ -1,3 +0,0 @@
API_PORT=5600
API_ADDRESS=192.168.2.125
CLIENT_PORT=2500

View File

@ -1,3 +0,0 @@
API_PORT=5600
API_ADDRESS=192.168.2.125
CLIENT_PORT=2500

View File

@ -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).

130
.gitignore vendored
View File

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

View File

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

View File

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

View File

@ -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&amp;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).**

View File

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

View File

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

View File

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

4694
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

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

View File

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

View File

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

28140
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -1,8 +0,0 @@
{
"short_name": "LinkWarden",
"name": "LinkWarden",
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -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" }}
>
&#xf0c9;
</AwesomeButton>
</div>
<input
className="search text-field"
type="search"
placeholder=" Search"
onChange={search}
/>
<AwesomeButton
size="icon"
type="primary"
action={() => setFilterBox(true)}
style={{ marginLeft: "10px" }}
>
&#xf160;
</AwesomeButton>
<AwesomeButton
size="icon"
type="primary"
action={() => setNewBox(true)}
style={{ marginLeft: "auto" }}
>
&#xf067;
</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;

View File

@ -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 &#xf067;
</AwesomeButton>
</div>
</div>
</div>
</div>
</>
);
};
export default AddItem;

View File

@ -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}
/>
);
}

View File

@ -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" }}
>
&#xf2ed;
</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 &#xf303;
</AwesomeButton>
</div>
</div>
</div>
</>
);
};
export default EditItem;

View File

@ -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}
/>
&#xf271; Date (Newest first)
</label>
<label>
<input
name="sort"
checked={radio.toString() === "2"}
onChange={handleRadio}
type="radio"
value={2}
/>
&#xf272; Date (Oldest first)
</label>
<label>
<input
name="sort"
checked={radio.toString() === "3"}
onChange={handleRadio}
type="radio"
value={3}
/>
&#xf15d; Name (A-Z)
</label>
<label>
<input
name="sort"
checked={radio.toString() === "4"}
onChange={handleRadio}
type="radio"
value={4}
/>
&#xf15e; Name (Z-A)
</label>
<label>
<input
name="sort"
checked={radio.toString() === "5"}
onChange={handleRadio}
type="radio"
value={5}
/>
&#xf15d; Website title (A-Z)
</label>
<label>
<input
name="sort"
checked={radio.toString() === "6"}
onChange={handleRadio}
type="radio"
value={6}
/>
&#xf15e; 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 &#xf00c;
</AwesomeButton>
</div>
</div>
</>
);
};
export default Filters;

View File

@ -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" }}
>
&#xf303;
</AwesomeButton>
</div>
</div>
</LazyLoad>
);
} catch (e) {
console.log(e);
}
})}
</div>
);
};
export default List;

View File

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

View File

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

View File

@ -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">&#xf49e;</h2>}>
<Link to="/">
<div className="menu-item">All</div>
</Link>
</MenuItem>
<MenuItem icon={<h2 className="sidebar-icon">&#xf01c;</h2>}>
<Link to="/collections/Unsorted">
<div className="menu-item">Unsorted</div>
</Link>
</MenuItem>
<SubMenu
icon={<h2 className="sidebar-icon">&#xf5fd;</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">&#xf07b;</div>} key={i}>
<Link className="sidebar-entity" to={path}>{e}</Link>
</MenuItem>
);
})}
</SubMenu>
<SubMenu
icon={<h2 className="sidebar-icon">&#xf02c;</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">&#x23;</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;

View File

@ -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}
/>
);
}

View File

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

View File

@ -1 +0,0 @@
export const API_HOST = process.env.REACT_APP_API_HOST || "http://localhost:5500"; // API full address

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
.loader {
position: fixed;
bottom: 10%;
left: 30%;
right: 30%;
text-align: center;
}

View File

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

View File

@ -1,4 +0,0 @@
$sidebar-bg-color: #373737;
$submenu-bg-color: #373737;
@import '~react-pro-sidebar/dist/scss/styles.scss';

View File

@ -1,10 +0,0 @@
.view-archived {
display: flex;
flex-direction: column;
text-align: left;
}
.seperator {
width: 100%;
color: #1f2c38;
}

View File

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