deleted old files

This commit is contained in:
Daniel 2023-06-29 05:27:56 +03:30 committed by GitHub
parent a9897c4401
commit 06d8dfb435
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 0 additions and 37017 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,78 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to make participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies within all project spaces, and it also applies when
an individual is representing the project or its community in public spaces.
Examples of representing a project or community include using an official
project e-mail address, posting via an official social media account, or acting
as an appointed representative at an online or offline event. Representation of
a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

View File

@ -1,16 +0,0 @@
# 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! 👍
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).
2. Open an [issue](https://github.com/Daniel31x13/link-warden/issues/new?assignees=&labels=contribution&template=contribution.md&title=Contribution) with your planned implementation to discuss.
3. Check in with me before starting development to make sure your work wont conflict with or duplicate existing work (by doing step 2).
4. Commit, push, and submit a PR and wait for review feedback.
5. Have patience, don't abandon your PR! We love contributors but we don't always have time to respond to notifications instantly. If you want a faster response, DM me on [Twitter](https://twitter.com/daniel31x13).
Thanks again! <3

34
.gitignore vendored
View File

@ -1,34 +0,0 @@
node_modules
..pnp
.pnp.js
coverage
build
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
mongo
.env
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*

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,23 +0,0 @@
The following only applies to the "main" branch:
MIT License
Copyright (c) 2023 Daniel31x13
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,83 +0,0 @@
> Note: I'm doing a full rebuild of this project with much more features... Committing to the [dev](https://github.com/Daniel31x13/link-warden/tree/dev) branch, stay tuned!
<div align="center">
<h1>
Linkwarden
<sub>A place for your useful links.</sub>
<img src="assets/LinkWarden.png" alt="LinkWarden.png" width="500px"/>
<a href="https://twitter.com/Daniel31X13" target="_blank" rel="noopener noreferrer"><img alt="Twitter Follow" src="https://img.shields.io/twitter/follow/Daniel31X13?label=twitter&amp;style=social"></a>
![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) ![GitHub Repo stars](https://img.shields.io/github/stars/daniel31x13/link-warden?style=flat-square)
</h1>
[Demo (outdated version)](https://linkwarden.netlify.app/) | [Intro & Motivation](https://github.com/Daniel31x13/linkwarden#intro--motivation) | [Features](https://github.com/Daniel31x13/linkwarden#features) | [Setup](https://github.com/Daniel31x13/linkwarden#setup) | [Development](https://github.com/Daniel31x13/linkwarden#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,18 +0,0 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 2.x.x | :white_check_mark: |
| 1.0.0 | :x: |
## Reporting a Vulnerability
First off, really appreciate the time you spent! <3
If you found a vulnerability these are the ways you can reach me:
Email: Daniel31X13@gmail.com
Twitter: [@Daniel31X13](https://twitter.com/Daniel31X13)

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