mirror of
https://github.com/diced/zipline.git
synced 2025-12-10 06:40:44 -08:00
Compare commits
144 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12baadd563 | ||
|
|
f5ae36d4e7 | ||
|
|
04ca738fb1 | ||
|
|
95e09e51e1 | ||
|
|
2f0af385c7 | ||
|
|
786e6d5799 | ||
|
|
61c5df750a | ||
|
|
eb30afcb83 | ||
|
|
cdf0f6e96c | ||
|
|
54158c5dbe | ||
|
|
56ff86db44 | ||
|
|
b7560c80aa | ||
|
|
03379943de | ||
|
|
2376fd8968 | ||
|
|
2f90193d7e | ||
|
|
964199f8a9 | ||
|
|
678ea20004 | ||
|
|
ea27fd8a45 | ||
|
|
38eef3f0ad | ||
|
|
22615e9ce9 | ||
|
|
a999abfbf8 | ||
|
|
20c1d3ef08 | ||
|
|
b06c8e4918 | ||
|
|
6edfdcefcc | ||
|
|
10b145b006 | ||
|
|
0ba9a9659d | ||
|
|
2dfa1b6b14 | ||
|
|
7a3f9f1fa4 | ||
|
|
f276fdc6a0 | ||
|
|
7963bdd1e4 | ||
|
|
195c57edc3 | ||
|
|
4442c85dc1 | ||
|
|
5bcac2a2b0 | ||
|
|
5303b67d11 | ||
|
|
af59e9abb8 | ||
|
|
fb098c9147 | ||
|
|
739974bef4 | ||
|
|
d21e48a1a3 | ||
|
|
8fea0cbe77 | ||
|
|
1e2b8efb13 | ||
|
|
8495963094 | ||
|
|
06d1c0bc3b | ||
|
|
5965c2e237 | ||
|
|
fb34dfadb0 | ||
|
|
13b0ac737b | ||
|
|
300430b3ec | ||
|
|
cf6f154e6e | ||
|
|
2ddf8c0cdb | ||
|
|
2a402f77b5 | ||
|
|
7b2c31658a | ||
|
|
7a91a60af9 | ||
|
|
bfa6c70bf3 | ||
|
|
73eff05180 | ||
|
|
74f3b3f13d | ||
|
|
181833d768 | ||
|
|
be9523304a | ||
|
|
b26fef3ad4 | ||
|
|
9f86674bbe | ||
|
|
095e57a037 | ||
|
|
66a8e3bb79 | ||
|
|
473137abdf | ||
|
|
740f1605e7 | ||
|
|
0922ec020e | ||
|
|
dbe8291f55 | ||
|
|
9dcc16277e | ||
|
|
aa611fa6ba | ||
|
|
083040e300 | ||
|
|
99e92e4594 | ||
|
|
870f6e88b1 | ||
|
|
16d2014bfb | ||
|
|
4d9a22e82c | ||
|
|
42d77e445b | ||
|
|
6506846207 | ||
|
|
2b9af0e0de | ||
|
|
762d2927f7 | ||
|
|
d9561f3b12 | ||
|
|
dde24848d4 | ||
|
|
e786482902 | ||
|
|
4e64922b70 | ||
|
|
15042b16d1 | ||
|
|
5e4c4fc6c9 | ||
|
|
7194c53891 | ||
|
|
7eff77ccc4 | ||
|
|
1b78ffaa91 | ||
|
|
8e8bfd68d1 | ||
|
|
b029505cdd | ||
|
|
c5c862bee3 | ||
|
|
3c38d008f1 | ||
|
|
dc52b00a00 | ||
|
|
b5d2e7040e | ||
|
|
5818440721 | ||
|
|
f1c46da47d | ||
|
|
212c69d303 | ||
|
|
9e4152e298 | ||
|
|
307f023e47 | ||
|
|
3451bd8762 | ||
|
|
a9d0be8aae | ||
|
|
d83f720631 | ||
|
|
1f3d396296 | ||
|
|
48f771f344 | ||
|
|
555bc6aa26 | ||
|
|
8bd0eaac1e | ||
|
|
3280c77002 | ||
|
|
b39743a53a | ||
|
|
9a73da56e9 | ||
|
|
c9b0d2664f | ||
|
|
6063c9efac | ||
|
|
dd6f192d4a | ||
|
|
d956f4ed3d | ||
|
|
4728258750 | ||
|
|
ece3e16459 | ||
|
|
9208dbe2f3 | ||
|
|
636de18642 | ||
|
|
ee48456291 | ||
|
|
a06d5ffaed | ||
|
|
606821a2c0 | ||
|
|
5c980c21e5 | ||
|
|
771cc380df | ||
|
|
38217870fe | ||
|
|
5b82c96a43 | ||
|
|
6f5f9869ad | ||
|
|
b29bfeb8b1 | ||
|
|
cb40559e49 | ||
|
|
90c72f7ffe | ||
|
|
002bd2e6f7 | ||
|
|
7b44f17a64 | ||
|
|
b5c83f92e3 | ||
|
|
51b4d64a93 | ||
|
|
62c9e0a22f | ||
|
|
3daac34d3e | ||
|
|
d80d5d1632 | ||
|
|
912f716362 | ||
|
|
16ecdf41af | ||
|
|
f0bb6b08fa | ||
|
|
efb4e2ce9a | ||
|
|
03238d10bf | ||
|
|
e71590b9fb | ||
|
|
4728f1cc46 | ||
|
|
794778dee2 | ||
|
|
b5e882f07e | ||
|
|
e7c58a4847 | ||
|
|
bdb44db25e | ||
|
|
e8b82ffe62 | ||
|
|
53c53c009e |
20
.babelrc
20
.babelrc
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
"next/babel"
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"babel-plugin-transform-imports",
|
||||
{
|
||||
"@material-ui/core": {
|
||||
"transform": "@material-ui/core/${member}",
|
||||
"preventFullImport": true
|
||||
},
|
||||
"@material-ui/icons": {
|
||||
"transform": "@material-ui/icons/${member}",
|
||||
"preventFullImport": true
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
.next/
|
||||
uploads/
|
||||
.git/
|
||||
.yarn/*
|
||||
!.yarn/releases
|
||||
!.yarn/plugins
|
||||
46
.env.local.example
Normal file
46
.env.local.example
Normal file
@@ -0,0 +1,46 @@
|
||||
# every field in here is optional except, CORE_SECRET and CORE_DATABASE_URL.
|
||||
# if CORE_SECRET is still "changethis" then zipline will exit and tell you to change it.
|
||||
|
||||
# if using s3/swift make sure to comment out the other datasources
|
||||
|
||||
CORE_HTTPS=true
|
||||
CORE_SECRET="changethis"
|
||||
CORE_HOST=0.0.0.0
|
||||
CORE_PORT=3000
|
||||
CORE_DATABASE_URL="postgres://postgres:postgres@localhost/zip10"
|
||||
CORE_LOGGER=false
|
||||
CORE_STATS_INTERVAL=1800
|
||||
|
||||
# default
|
||||
DATASOURCE_TYPE=local
|
||||
DATASOURCE_LOCAL_DIRECTORY=./uploads
|
||||
|
||||
# or you can choose to use s3
|
||||
DATASOURCE_TYPE=s3
|
||||
DATASOURCE_S3_ACCESS_KEY_ID=key
|
||||
DATASOURCE_S3_SECRET_ACCESS_KEY=secret
|
||||
DATASOURCE_S3_BUCKET=bucket
|
||||
DATASOURCE_S3_ENDPOINT=s3.amazonaws.com
|
||||
DATASOURCE_S3_REGION=us-west-2
|
||||
DATASOURCE_S3_FORCE_S3_PATH=false
|
||||
|
||||
# or you can use swift
|
||||
DATASOURCE_TYPE=swift
|
||||
DATASOURCE_SWIFT_CONTAINER=container
|
||||
DATASOURCE_SWIFT_AUTH_ENDPOINT="https://something/v3"
|
||||
DATASOURCE_SWIFT_USERNAME=username
|
||||
DATASOURCE_SWIFT_PASSWORD=password
|
||||
DATASOURCE_SWIFT_PROJECT_ID=project_id
|
||||
DATASOURCE_SWIFT_DOMAIN_ID=domain_id
|
||||
|
||||
UPLOADER_ROUTE=/u
|
||||
UPLOADER_LENGTH=6
|
||||
UPLOADER_ADMIN_LIMIT=104900000
|
||||
UPLOADER_USER_LIMIT=104900000
|
||||
UPLOADER_DISABLED_EXTENSIONS=someext
|
||||
|
||||
URLS_ROUTE=/go
|
||||
URLS_LENGTH=6
|
||||
|
||||
RATELIMIT_USER = 5
|
||||
RATELIMIT_ADMIN = 3
|
||||
24
.eslintrc.js
24
.eslintrc.js
@@ -1,24 +0,0 @@
|
||||
module.exports = {
|
||||
'extends': ['next', 'next/core-web-vitals'],
|
||||
'rules': {
|
||||
'indent': ['error', 2],
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
'quotes': ['error', 'single'],
|
||||
'semi': ['error', 'always'],
|
||||
'jsx-quotes': ['error', 'prefer-single'],
|
||||
'react/prop-types': 'off',
|
||||
'react-hooks/rules-of-hooks': 'off',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'react/jsx-uses-react': 'warn',
|
||||
'react/jsx-uses-vars': 'warn',
|
||||
'react/no-danger-with-children': 'warn',
|
||||
'react/no-deprecated': 'warn',
|
||||
'react/no-direct-mutation-state': 'warn',
|
||||
'react/no-is-mounted': 'warn',
|
||||
'react/no-typos': 'error',
|
||||
'react/react-in-jsx-scope': 'error',
|
||||
'react/require-render-return': 'error',
|
||||
'react/style-prop-object': 'warn',
|
||||
'@next/next/no-img-element': 'off'
|
||||
}
|
||||
};
|
||||
51
.eslintrc.json
Normal file
51
.eslintrc.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"extends": [
|
||||
"next",
|
||||
"next/core-web-vitals"
|
||||
],
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
2,
|
||||
{
|
||||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
"always-multiline"
|
||||
],
|
||||
"jsx-quotes": [
|
||||
"error",
|
||||
"prefer-single"
|
||||
],
|
||||
"react/prop-types": "off",
|
||||
"react-hooks/rules-of-hooks": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"react/jsx-uses-react": "warn",
|
||||
"react/jsx-uses-vars": "warn",
|
||||
"react/no-danger-with-children": "warn",
|
||||
"react/no-deprecated": "warn",
|
||||
"react/no-direct-mutation-state": "warn",
|
||||
"react/no-is-mounted": "warn",
|
||||
"react/no-typos": "error",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/require-render-return": "error",
|
||||
"react/style-prop-object": "warn",
|
||||
"@next/next/no-img-element": "off",
|
||||
"jsx-a11y/alt-text": "off",
|
||||
"react/display-name": "off"
|
||||
}
|
||||
}
|
||||
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: 'CI: Build'
|
||||
name: 'Build'
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -22,13 +22,11 @@ jobs:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Create mock config
|
||||
run: echo -e "[uploader]\nroute = '/u'\nembed_route = '/a'\nlength = 6\ndirectory = './uploads'" > config.toml
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.cache-restore.outputs.cache-hit != 'true'
|
||||
run: yarn install
|
||||
|
||||
|
||||
- name: Build
|
||||
run: yarn build
|
||||
|
||||
env:
|
||||
ZIPLINE_DOCKER_BUILD: true
|
||||
50
.github/workflows/docker-release.yml
vendored
Normal file
50
.github/workflows/docker-release.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
name: 'Push Release Docker Images'
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'server/**'
|
||||
- 'prisma/**'
|
||||
- '.github/**'
|
||||
- 'Dockerfile'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
push_to_ghcr:
|
||||
name: Push Release Image to GitHub Packages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get version
|
||||
uses: sergeysova/jq-action@v2
|
||||
id: version
|
||||
with:
|
||||
cmd: 'jq .version package.json -r'
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Login to Github Packages
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
ghcr.io/diced/zipline:latest
|
||||
ghcr.io/diced/zipline:${{ steps.version.outputs.value }}
|
||||
40
.github/workflows/docker.yml
vendored
40
.github/workflows/docker.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: 'CD: Push Docker Images'
|
||||
name: 'Push Docker Images'
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -7,6 +7,8 @@ on:
|
||||
- 'src/**'
|
||||
- 'server/**'
|
||||
- 'prisma/**'
|
||||
- '.github/**'
|
||||
- 'Dockerfile'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -17,28 +19,24 @@ jobs:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Push to GitHub Packages
|
||||
uses: docker/build-push-action@v1
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Login to Github Packages
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
registry: docker.pkg.github.com
|
||||
repository: diced/zipline/zipline
|
||||
dockerfile: Dockerfile
|
||||
tag_with_ref: true
|
||||
|
||||
push_to_dockerhub:
|
||||
name: Push Image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Push to Docker Hub
|
||||
uses: docker/build-push-action@v1
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
repository: diced/zipline
|
||||
dockerfile: Dockerfile
|
||||
tag_with_ref: true
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
ghcr.io/diced/zipline:trunk
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -5,6 +5,11 @@
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# yarn
|
||||
.yarn/*
|
||||
!.yarn/releases
|
||||
!.yarn/plugins
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
@@ -18,6 +23,7 @@
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
.idea
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
@@ -36,4 +42,4 @@ yarn-error.log*
|
||||
# zipline
|
||||
config.toml
|
||||
uploads/
|
||||
data.db*
|
||||
dist/
|
||||
1
.husky/.gitignore
vendored
1
.husky/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
_
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn commitlint --edit $1
|
||||
546
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
Normal file
546
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
786
.yarn/releases/yarn-3.2.1.cjs
vendored
Normal file
786
.yarn/releases/yarn-3.2.1.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
7
.yarnrc.yml
Normal file
7
.yarnrc.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
nodeLinker: node-modules
|
||||
|
||||
plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||
spec: "@yarnpkg/plugin-interactive-tools"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.2.1.cjs
|
||||
61
Dockerfile
61
Dockerfile
@@ -1,31 +1,64 @@
|
||||
FROM node:16-alpine3.11 AS builder
|
||||
FROM ghcr.io/diced/prisma-binaries:3.15.x as prisma
|
||||
|
||||
FROM alpine:3.16 AS deps
|
||||
RUN mkdir -p /prisma-engines
|
||||
WORKDIR /build
|
||||
|
||||
COPY .yarn .yarn
|
||||
COPY package.json yarn.lock .yarnrc.yml ./
|
||||
|
||||
RUN apk add --no-cache nodejs yarn
|
||||
RUN yarn install --immutable
|
||||
|
||||
FROM alpine:3.16 AS builder
|
||||
WORKDIR /build
|
||||
|
||||
COPY --from=prisma /prisma-engines /prisma-engines
|
||||
ENV PRISMA_QUERY_ENGINE_BINARY=/prisma-engines/query-engine \
|
||||
PRISMA_MIGRATION_ENGINE_BINARY=/prisma-engines/migration-engine \
|
||||
PRISMA_INTROSPECTION_ENGINE_BINARY=/prisma-engines/introspection-engine \
|
||||
PRISMA_FMT_BINARY=/prisma-engines/prisma-fmt \
|
||||
PRISMA_CLI_QUERY_ENGINE_TYPE=binary \
|
||||
PRISMA_CLIENT_ENGINE_TYPE=binary
|
||||
|
||||
RUN apk add --no-cache nodejs yarn openssl openssl-dev
|
||||
|
||||
COPY --from=deps /build/node_modules ./node_modules
|
||||
COPY src ./src
|
||||
COPY server ./server
|
||||
COPY scripts ./scripts
|
||||
COPY prisma ./prisma
|
||||
COPY .yarn .yarn
|
||||
COPY package.json yarn.lock .yarnrc.yml esbuild.config.js next.config.js next-env.d.ts zip-env.d.ts tsconfig.json mimes.json ./
|
||||
|
||||
COPY package.json yarn.lock next.config.js next-env.d.ts zip-env.d.ts tsconfig.json ./
|
||||
|
||||
RUN yarn install
|
||||
|
||||
# create a mock config.toml to spoof next build!
|
||||
RUN echo -e "[uploader]\nroute = '/u'\nembed_route = '/a'\nlength = 6\ndirectory = './uploads'" > config.toml
|
||||
ENV ZIPLINE_DOCKER_BUILD 1
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN yarn build
|
||||
|
||||
FROM node:16-alpine3.11 AS runner
|
||||
FROM alpine:3.16 AS runner
|
||||
WORKDIR /zipline
|
||||
|
||||
COPY --from=prisma /prisma-engines /prisma-engines
|
||||
ENV PRISMA_QUERY_ENGINE_BINARY=/prisma-engines/query-engine \
|
||||
PRISMA_MIGRATION_ENGINE_BINARY=/prisma-engines/migration-engine \
|
||||
PRISMA_INTROSPECTION_ENGINE_BINARY=/prisma-engines/introspection-engine \
|
||||
PRISMA_FMT_BINARY=/prisma-engines/prisma-fmt \
|
||||
PRISMA_CLI_QUERY_ENGINE_TYPE=binary \
|
||||
PRISMA_CLIENT_ENGINE_TYPE=binary
|
||||
|
||||
RUN apk add --no-cache nodejs yarn openssl openssl-dev
|
||||
|
||||
ENV NODE_ENV production
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
COPY --from=builder /build/.next ./.next
|
||||
COPY --from=builder /build/dist ./dist
|
||||
COPY --from=builder /build/node_modules ./node_modules
|
||||
|
||||
COPY --from=builder /build/next.config.js ./next.config.js
|
||||
COPY --from=builder /build/src ./src
|
||||
COPY --from=builder /build/server ./server
|
||||
COPY --from=builder /build/scripts ./scripts
|
||||
COPY --from=builder /build/prisma ./prisma
|
||||
COPY --from=builder /build/.next ./.next
|
||||
COPY --from=builder /build/tsconfig.json ./tsconfig.json
|
||||
COPY --from=builder /build/package.json ./package.json
|
||||
COPY --from=builder /build/mimes.json ./mimes.json
|
||||
|
||||
CMD ["node", "server"]
|
||||
CMD ["node", "dist/server"]
|
||||
@@ -1,3 +0,0 @@
|
||||
prisma/migrations
|
||||
node_modules
|
||||
.next
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 dicedtomato
|
||||
|
||||
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.
|
||||
118
README.md
118
README.md
@@ -1,25 +1,113 @@
|
||||
<p align="center"><img src="https://raw.githubusercontent.com/diced/zipline/trunk/public/zipline_small.png"/></p>
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/diced/zipline/trunk/public/zipline_small.png"/>
|
||||
|
||||

|
||||

|
||||
[](https://discord.gg/EAhCRfGxCF)
|
||||

|
||||

|
||||

|
||||
<br>
|
||||
Zipline is a ShareX/file upload server that is easy to use, packed with features and can be setup in one command!
|
||||
|
||||
# Zipline
|
||||

|
||||

|
||||

|
||||

|
||||
[](https://discord.gg/EAhCRfGxCF)
|
||||
|
||||
Fast & lightweight file uploading.
|
||||
|
||||
# Features
|
||||
</div>
|
||||
|
||||
## Features
|
||||
- Configurable
|
||||
- Fast
|
||||
- Built with Next.js & React
|
||||
- Token protected uploading
|
||||
- Easy setup instructions on [docs](https://zipline.diced.me) (One command install `docker-compose up`)
|
||||
- Image uploading
|
||||
- Password Protected Uploads
|
||||
- URL shortening
|
||||
- Text uploading
|
||||
- URL Formats (uuid, dates, random alphanumeric, original name, zws)
|
||||
- Discord embeds (OG metadata)
|
||||
- Gallery viewer, and multiple file format support
|
||||
- Code highlighting
|
||||
- Easy setup instructions on [docs](https://zipl.vercel.app/) (One command install `docker-compose up -d`)
|
||||
|
||||
# Installing
|
||||
# Usage
|
||||
|
||||
[See how to install here](https://zipline.diced.me/docs/getting-started)
|
||||
## Install & run with Docker
|
||||
This section requires [Docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/).
|
||||
|
||||
```shell
|
||||
git clone https://github.com/diced/zipline
|
||||
cd zipline
|
||||
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### After installing
|
||||
After installing, please edit the `docker-compose.yml` file and find the line that says `SECRET=changethis` and replace `changethis` with a random string.
|
||||
Ways you could generate the string could be from a password managers generator, or you could just slam your keyboard and hope for the best.
|
||||
|
||||
## Building & running from source
|
||||
This section requires [nodejs](https://nodejs.org), [yarn](https://yarnpkg.com/) or [npm](https://npmjs.com).
|
||||
```shell
|
||||
git clone https://github.com/diced/zipline
|
||||
cd zipline
|
||||
|
||||
# npm install
|
||||
yarn install
|
||||
# npm run build
|
||||
yarn build
|
||||
# npm start
|
||||
yarn start
|
||||
```
|
||||
|
||||
# NGINX Proxy
|
||||
This section requires [NGINX](https://nginx.org/).
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80 default_server;
|
||||
client_max_body_size 100M;
|
||||
server_name <your domain (optional)>;
|
||||
location / {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Website
|
||||
The default port is `3000`, once you have accessed it you can see a login screen. The default credentials are "administrator" and "password". Once you login please immediately change the details to something more secure. You can do this by clicking on the top right corner where it says "administrator" with a gear icon and clicking Manage Account.
|
||||
|
||||
# ShareX (Windows)
|
||||
This section requires [ShareX](https://www.getsharex.com/).
|
||||
|
||||
After navigating to Zipline, click on the top right corner where it says your username and click Manage Account. Scroll down to see "ShareX Config", select the one you would prefer using. After this you can import the .sxcu into sharex. [More information here](https://zipl.vercel.app/docs/uploaders/sharex)
|
||||
|
||||
# Flameshot (Linux)
|
||||
This section requires [Flameshot](https://www.flameshot.org/), [jq](https://stedolan.github.io/jq/), and [xsel](https://github.com/kfish/xsel).
|
||||
|
||||
To upload files using flameshot we will use a script. Replace $TOKEN and $HOST with your own values, you probably know how to do this if you use linux.
|
||||
|
||||
```shell
|
||||
DATE=$(date '+%h_%Y_%d_%I_%m_%S.png');
|
||||
flameshot gui -r > ~/Pictures/$DATE;
|
||||
|
||||
curl -H "Content-Type: multipart/form-data" -H "authorization: $TOKEN" -F file=@$1 $HOST/api/upload | jq -r 'files[0].url' | xsel -ib
|
||||
```
|
||||
|
||||
# Contributing
|
||||
|
||||
## Bug reports
|
||||
Create an issue on GitHub, please include the following (if one of them is not applicable to the issue then it's not needed):
|
||||
* The steps to reproduce the bug
|
||||
* Logs of Zipline
|
||||
* The version of Zipline
|
||||
* Your OS & Browser including server OS
|
||||
* What you were expecting to see
|
||||
|
||||
## Feature requests
|
||||
Create an issue on GitHub, please include the following:
|
||||
* Breif explanation of the feature in the title (very breif please)
|
||||
* How it would work (detailed, but optional)
|
||||
|
||||
## Pull Requests (contributions to the codebase)
|
||||
Create a pull request on GitHub. If your PR does not pass the action checks, then please fix the errors. If your PR was submitted before a release, and I have pushed a new release, please make sure to update your PR to reflect any changes, usually this is handled by GitHub.
|
||||
12
SECURITY.md
Normal file
12
SECURITY.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 3.4.4 | :white_check_mark: |
|
||||
| < 3 | :x: |
|
||||
| < 2 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
Report a Vulnerability by issuing a bug report, with exact details with how the vulnerability happened, what "exploits" can happen, and possible fixes (optional). Vulnerability reports are treated with high priority and will be resolved most of the time quickly.
|
||||
@@ -1,53 +0,0 @@
|
||||
module.exports = {
|
||||
parserPreset: 'conventional-changelog-conventionalcommits',
|
||||
rules: {
|
||||
'body-leading-blank': [1, 'always'],
|
||||
'body-max-line-length': [2, 'always', 100],
|
||||
'footer-leading-blank': [1, 'always'],
|
||||
'footer-max-line-length': [2, 'always', 100],
|
||||
'header-max-length': [2, 'always', 100],
|
||||
'subject-case': [
|
||||
2,
|
||||
'never',
|
||||
['sentence-case', 'start-case', 'pascal-case', 'upper-case'],
|
||||
],
|
||||
'subject-empty': [2, 'never'],
|
||||
'subject-full-stop': [2, 'never', '.'],
|
||||
'type-case': [2, 'always', 'lower-case'],
|
||||
'type-empty': [2, 'never'],
|
||||
'type-enum': [
|
||||
2,
|
||||
'always',
|
||||
[
|
||||
'build',
|
||||
'chore',
|
||||
'ci',
|
||||
'docs',
|
||||
'feat',
|
||||
'fix',
|
||||
'perf',
|
||||
'refactor',
|
||||
'revert',
|
||||
'style',
|
||||
'test',
|
||||
],
|
||||
],
|
||||
'scope-enum': [
|
||||
1,
|
||||
'always',
|
||||
[
|
||||
'prisma',
|
||||
'scripts',
|
||||
'server',
|
||||
'pages',
|
||||
'api',
|
||||
'hooks',
|
||||
'components',
|
||||
'middleware',
|
||||
'redux',
|
||||
'themes',
|
||||
'lib'
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
[core]
|
||||
secure = true
|
||||
secret = 'some secret'
|
||||
host = '0.0.0.0'
|
||||
port = 3000
|
||||
|
||||
[database]
|
||||
type = 'psql'
|
||||
url = 'postgres://postgres:postgres@postgres/postgres'
|
||||
|
||||
[uploader]
|
||||
route = '/u'
|
||||
embed_route = '/a'
|
||||
length = 6
|
||||
directory = './uploads'
|
||||
39
docker-compose.dev.yml
Normal file
39
docker-compose.dev.yml
Normal file
@@ -0,0 +1,39 @@
|
||||
version: '3'
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DATABASE=postgres
|
||||
volumes:
|
||||
- pg_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U postgres']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
zipline:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- '3000:3000'
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- CORE_HTTPS=false
|
||||
- CORE_SECRET=changethislol
|
||||
- CORE_HOST=0.0.0.0
|
||||
- CORE_PORT=3000
|
||||
- CORE_DATABASE_URL=postgres://postgres:postgres@postgres/postgres
|
||||
- CORE_LOGGER=true
|
||||
volumes:
|
||||
- '$PWD/uploads:/zipline/uploads'
|
||||
- '$PWD/public:/zipline/public'
|
||||
depends_on:
|
||||
- 'postgres'
|
||||
|
||||
volumes:
|
||||
pg_data:
|
||||
@@ -2,11 +2,12 @@ version: '3'
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
environment:
|
||||
restart: always
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DATABASE=postgres
|
||||
volumes:
|
||||
volumes:
|
||||
- pg_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U postgres']
|
||||
@@ -15,24 +16,19 @@ services:
|
||||
retries: 5
|
||||
|
||||
zipline:
|
||||
image: diced/zipline:trunk
|
||||
image: ghcr.io/diced/zipline
|
||||
ports:
|
||||
- '3000:3000'
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- SECURE=false
|
||||
- SECRET=changethis
|
||||
- HOST=0.0.0.0
|
||||
- PORT=3000
|
||||
- DATABASE_TYPE=psql
|
||||
- DATABASE_URL=postgresql://postgres:postgres@postgres/postgres/
|
||||
- UPLOADER_ROUTE=/u
|
||||
- UPLOADER_EMBED_ROUTE=/a
|
||||
- UPLOADER_LENGTH=6
|
||||
- UPLOADER_DIRECTORY=./uploads
|
||||
restart: always
|
||||
environment:
|
||||
- CORE_HTTPS=false
|
||||
- CORE_SECRET=changethis
|
||||
- CORE_HOST=0.0.0.0
|
||||
- CORE_PORT=3000
|
||||
- CORE_DATABASE_URL=postgres://postgres:postgres@postgres/postgres
|
||||
- CORE_LOGGER=true
|
||||
volumes:
|
||||
- '$PWD/uploads:/zipline/uploads'
|
||||
- '$PWD/prisma:/zipline/prisma'
|
||||
- './uploads:/zipline/uploads'
|
||||
- '$PWD/public:/zipline/public'
|
||||
depends_on:
|
||||
- 'postgres'
|
||||
|
||||
43
esbuild.config.js
Normal file
43
esbuild.config.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const esbuild = require('esbuild');
|
||||
const { existsSync } = require('fs');
|
||||
const { rm } = require('fs/promises');
|
||||
|
||||
(async () => {
|
||||
const watch = process.argv[2] === '--watch';
|
||||
|
||||
if (existsSync('./dist')) {
|
||||
await rm('./dist', { recursive: true });
|
||||
}
|
||||
|
||||
await esbuild.build({
|
||||
tsconfig: 'tsconfig.json',
|
||||
outdir: 'dist',
|
||||
bundle: false,
|
||||
platform: 'node',
|
||||
treeShaking: true,
|
||||
entryPoints: [
|
||||
'src/server/index.ts',
|
||||
'src/server/util.ts',
|
||||
'src/lib/logger.ts',
|
||||
'src/lib/config.ts',
|
||||
'src/lib/mimes.ts',
|
||||
'src/lib/exts.ts',
|
||||
'src/lib/config/Config.ts',
|
||||
'src/lib/config/readConfig.ts',
|
||||
'src/lib/config/validateConfig.ts',
|
||||
'src/lib/datasources/Datasource.ts',
|
||||
'src/lib/datasources/index.ts',
|
||||
'src/lib/datasources/Local.ts',
|
||||
'src/lib/datasources/S3.ts',
|
||||
'src/lib/datasources/Swift.ts',
|
||||
'src/lib/datasource.ts',
|
||||
],
|
||||
format: 'cjs',
|
||||
resolveExtensions: ['.ts', '.js'],
|
||||
write: true,
|
||||
watch,
|
||||
incremental: watch,
|
||||
sourcemap: false,
|
||||
minify: false,
|
||||
});
|
||||
})();
|
||||
8298
mimes.json
Normal file
8298
mimes.json
Normal file
File diff suppressed because it is too large
Load Diff
1
next-env.d.ts
vendored
1
next-env.d.ts
vendored
@@ -1,5 +1,4 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
module.exports = {
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: '/',
|
||||
destination: '/dashboard',
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
api: {
|
||||
responseLimit: false,
|
||||
},
|
||||
poweredByHeader: false,
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
};
|
||||
99
package.json
99
package.json
@@ -1,62 +1,69 @@
|
||||
{
|
||||
"name": "zip3",
|
||||
"version": "3.2.0",
|
||||
"name": "zipline",
|
||||
"version": "3.4.8",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"prepare": "husky install",
|
||||
"dev": "NODE_ENV=development node server",
|
||||
"build": "npm-run-all build:schema build:next",
|
||||
"dev": "node esbuild.config.js && REACT_EDITOR=code NODE_ENV=development node dist/server",
|
||||
"build": "npm-run-all build:server build:schema build:next",
|
||||
"build:server": "node esbuild.config.js",
|
||||
"build:next": "next build",
|
||||
"build:schema": "prisma generate --schema=prisma/schema.prisma",
|
||||
"start": "node server",
|
||||
"migrate:dev": "prisma migrate dev --create-only",
|
||||
"start": "node dist/server",
|
||||
"lint": "next lint",
|
||||
"ts-node": "ts-node --compiler-options \"{\\\"module\\\":\\\"commonjs\\\"}\" --transpile-only",
|
||||
"create-all-migrations": "node scripts/create-migrations",
|
||||
"semantic-release": "semantic-release"
|
||||
"docker:run": "docker-compose up -d",
|
||||
"docker:down": "docker-compose down",
|
||||
"docker:build-dev": "docker-compose --file docker-compose.dev.yml up --build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.4.0",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@iarna/toml": "2.2.5",
|
||||
"@material-ui/core": "^5.0.0-alpha.37",
|
||||
"@material-ui/data-grid": "^4.0.0-alpha.32",
|
||||
"@material-ui/icons": "^5.0.0-alpha.37",
|
||||
"@material-ui/styles": "^5.0.0-alpha.35",
|
||||
"@prisma/client": "^2.30.0",
|
||||
"@reduxjs/toolkit": "^1.6.0",
|
||||
"argon2": "^0.28.2",
|
||||
"colorette": "^1.2.2",
|
||||
"cookie": "^0.4.1",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"fecha": "^4.2.1",
|
||||
"formik": "^2.2.9",
|
||||
"multer": "^1.4.2",
|
||||
"next": "11.1.0",
|
||||
"prisma": "^2.30.0",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-dropzone": "^11.3.2",
|
||||
"react-redux": "^7.2.4",
|
||||
"redux": "^4.1.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"yup": "^0.32.9"
|
||||
"@mantine/core": "^4.2.9",
|
||||
"@mantine/dropzone": "^4.2.9",
|
||||
"@mantine/hooks": "^4.2.9",
|
||||
"@mantine/modals": "^4.2.9",
|
||||
"@mantine/next": "^4.2.9",
|
||||
"@mantine/notifications": "^4.2.9",
|
||||
"@mantine/prism": "^4.2.9",
|
||||
"@prisma/client": "^3.15.2",
|
||||
"@prisma/migrate": "^3.15.2",
|
||||
"@prisma/sdk": "^3.15.2",
|
||||
"@reduxjs/toolkit": "^1.8.2",
|
||||
"argon2": "^0.28.5",
|
||||
"colorette": "^2.0.19",
|
||||
"cookie": "^0.5.0",
|
||||
"dotenv": "^16.0.1",
|
||||
"dotenv-expand": "^8.0.3",
|
||||
"fecha": "^4.2.3",
|
||||
"fflate": "^0.7.3",
|
||||
"find-my-way": "^6.3.0",
|
||||
"minio": "^7.0.28",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"next": "^12.1.6",
|
||||
"prisma": "^3.15.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-feather": "^2.0.10",
|
||||
"react-redux": "^8.0.2",
|
||||
"react-table": "^7.8.0",
|
||||
"redux": "^4.2.0",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^12.1.4",
|
||||
"@commitlint/config-conventional": "^12.1.4",
|
||||
"@types/cookie": "^0.4.0",
|
||||
"@types/multer": "^1.4.6",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/minio": "^7.0.13",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^15.12.2",
|
||||
"babel-plugin-transform-imports": "^2.0.0",
|
||||
"eslint": "7.28.0",
|
||||
"eslint-config-next": "11.0.0",
|
||||
"husky": "^6.0.0",
|
||||
"babel-plugin-import": "^1.13.5",
|
||||
"esbuild": "^0.14.44",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-next": "12.1.6",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"release": "^6.3.0",
|
||||
"ts-node": "^10.0.0",
|
||||
"typescript": "^4.3.2"
|
||||
"ts-node": "^10.8.1",
|
||||
"typescript": "^4.7.3"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/diced/workflow-testing.git"
|
||||
}
|
||||
"url": "https://github.com/diced/zipline.git"
|
||||
},
|
||||
"packageManager": "yarn@3.2.1"
|
||||
}
|
||||
|
||||
2
prisma/migrations/20210827202147_favorite/migration.sql
Normal file
2
prisma/migrations/20210827202147_favorite/migration.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Image" ADD COLUMN "favorite" BOOLEAN NOT NULL DEFAULT false;
|
||||
25
prisma/migrations/20210830210159_zws/migration.sql
Normal file
25
prisma/migrations/20210830210159_zws/migration.sql
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[imageId]` on the table `InvisibleImage` will be added. If there are existing duplicate values, this will fail.
|
||||
- Added the required column `imageId` to the `InvisibleImage` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "InvisibleImage" DROP CONSTRAINT "InvisibleImage_id_fkey";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "InvisibleImage_id_unique";
|
||||
|
||||
-- AlterTable
|
||||
CREATE SEQUENCE "invisibleimage_id_seq";
|
||||
ALTER TABLE "InvisibleImage" ADD COLUMN "imageId" INTEGER NOT NULL,
|
||||
ALTER COLUMN "id" SET DEFAULT nextval('invisibleimage_id_seq'),
|
||||
ADD PRIMARY KEY ("id");
|
||||
ALTER SEQUENCE "invisibleimage_id_seq" OWNED BY "InvisibleImage"."id";
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "InvisibleImage_imageId_unique" ON "InvisibleImage"("imageId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InvisibleImage" ADD FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Image" ADD COLUMN "embed" BOOLEAN NOT NULL DEFAULT false;
|
||||
39
prisma/migrations/20210924045900_delete_url/migration.sql
Normal file
39
prisma/migrations/20210924045900_delete_url/migration.sql
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `InvisibleUrl` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `Url` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Image" DROP CONSTRAINT "Image_userId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "InvisibleImage" DROP CONSTRAINT "InvisibleImage_imageId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "InvisibleUrl" DROP CONSTRAINT "InvisibleUrl_id_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Theme" DROP CONSTRAINT "Theme_userId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Url" DROP CONSTRAINT "Url_userId_fkey";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "InvisibleUrl";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "Url";
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Theme" ADD CONSTRAINT "Theme_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Image" ADD CONSTRAINT "Image_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InvisibleImage" ADD CONSTRAINT "InvisibleImage_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "InvisibleImage.invis_unique" RENAME TO "InvisibleImage_invis_key";
|
||||
34
prisma/migrations/20210924050753_new_url/migration.sql
Normal file
34
prisma/migrations/20210924050753_new_url/migration.sql
Normal file
@@ -0,0 +1,34 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Url" (
|
||||
"id" TEXT NOT NULL,
|
||||
"destination" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"views" INTEGER NOT NULL DEFAULT 0,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "Url_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "InvisibleUrl" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"invis" TEXT NOT NULL,
|
||||
"urlId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "InvisibleUrl_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Url_id_key" ON "Url"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "InvisibleUrl_invis_key" ON "InvisibleUrl"("invis");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "InvisibleUrl_urlId_unique" ON "InvisibleUrl"("urlId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Url" ADD CONSTRAINT "Url_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InvisibleUrl" ADD CONSTRAINT "InvisibleUrl_urlId_fkey" FOREIGN KEY ("urlId") REFERENCES "Url"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Url" ADD COLUMN "vanity" TEXT;
|
||||
2
prisma/migrations/20211003022626_site_name/migration.sql
Normal file
2
prisma/migrations/20211003022626_site_name/migration.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "embedSiteName" TEXT DEFAULT E'{image.file} • {user.name}';
|
||||
11
prisma/migrations/20211128031800_ratelimit/migration.sql
Normal file
11
prisma/migrations/20211128031800_ratelimit/migration.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "ratelimited" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "InvisibleImage_imageId_unique" RENAME TO "InvisibleImage_imageId_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "InvisibleUrl_urlId_unique" RENAME TO "InvisibleUrl_urlId_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "Theme_userId_unique" RENAME TO "Theme_userId_key";
|
||||
8
prisma/migrations/20220103232702_stats/migration.sql
Normal file
8
prisma/migrations/20220103232702_stats/migration.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Stats" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"data" JSONB NOT NULL,
|
||||
|
||||
CONSTRAINT "Stats_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
5
prisma/migrations/20220221053815_format/migration.sql
Normal file
5
prisma/migrations/20220221053815_format/migration.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ImageFormat" AS ENUM ('UUID', 'DATE', 'RANDOM');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Image" ADD COLUMN "format" "ImageFormat" NOT NULL DEFAULT E'RANDOM';
|
||||
2
prisma/migrations/20220221055817_name/migration.sql
Normal file
2
prisma/migrations/20220221055817_name/migration.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "ImageFormat" ADD VALUE 'NAME';
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
Warnings:
|
||||
- You are about to drop the `Theme` table. If the table is not empty, all the data it contains will be lost.
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Theme" DROP CONSTRAINT "Theme_userId_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ALTER COLUMN "systemTheme" SET DEFAULT E'system';
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "Theme";
|
||||
2
prisma/migrations/20220304004623_domains/migration.sql
Normal file
2
prisma/migrations/20220304004623_domains/migration.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "domains" TEXT[];
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Image" ADD COLUMN "password" TEXT;
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `ratelimited` on the `User` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" DROP COLUMN "ratelimited",
|
||||
ADD COLUMN "ratelimit" TIMESTAMP(3);
|
||||
17
prisma/migrations/20220713164531_invites/migration.sql
Normal file
17
prisma/migrations/20220713164531_invites/migration.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Invite" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"expires_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"used" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdById" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "Invite_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Invite_code_key" ON "Invite"("code");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Invite" ADD CONSTRAINT "Invite_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Invite" ALTER COLUMN "expires_at" DROP NOT NULL,
|
||||
ALTER COLUMN "expires_at" DROP DEFAULT;
|
||||
@@ -8,32 +8,27 @@ generator client {
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
id Int @id @default(autoincrement())
|
||||
username String
|
||||
password String
|
||||
token String
|
||||
administrator Boolean @default(false)
|
||||
systemTheme String @default("dark_blue")
|
||||
customTheme Theme?
|
||||
administrator Boolean @default(false)
|
||||
systemTheme String @default("system")
|
||||
embedTitle String?
|
||||
embedColor String @default("#2f3136")
|
||||
embedColor String @default("#2f3136")
|
||||
embedSiteName String? @default("{image.file} • {user.name}")
|
||||
ratelimit DateTime?
|
||||
domains String[]
|
||||
images Image[]
|
||||
urls Url[]
|
||||
Invite Invite[]
|
||||
}
|
||||
|
||||
model Theme {
|
||||
id Int @id @default(autoincrement())
|
||||
type String
|
||||
primary String
|
||||
secondary String
|
||||
error String
|
||||
warning String
|
||||
info String
|
||||
border String
|
||||
mainBackground String
|
||||
paperBackground String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
enum ImageFormat {
|
||||
UUID
|
||||
DATE
|
||||
RANDOM
|
||||
NAME
|
||||
}
|
||||
|
||||
model Image {
|
||||
@@ -42,31 +37,53 @@ model Image {
|
||||
mimetype String @default("image/png")
|
||||
created_at DateTime @default(now())
|
||||
views Int @default(0)
|
||||
favorite Boolean @default(false)
|
||||
embed Boolean @default(false)
|
||||
password String?
|
||||
invisible InvisibleImage?
|
||||
format ImageFormat @default(RANDOM)
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
}
|
||||
|
||||
model InvisibleImage {
|
||||
id Int
|
||||
image Image @relation(fields: [id], references: [id])
|
||||
|
||||
invis String @unique
|
||||
id Int @id @default(autoincrement())
|
||||
invis String @unique
|
||||
imageId Int @unique
|
||||
image Image @relation(fields: [imageId], references: [id])
|
||||
}
|
||||
|
||||
model Url {
|
||||
id Int @id @default(autoincrement())
|
||||
to String
|
||||
created_at DateTime @default(now())
|
||||
views Int @default(0)
|
||||
invisible InvisibleUrl?
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
id String @id @unique
|
||||
destination String
|
||||
vanity String?
|
||||
created_at DateTime @default(now())
|
||||
views Int @default(0)
|
||||
invisible InvisibleUrl?
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
}
|
||||
|
||||
model InvisibleUrl {
|
||||
id Int
|
||||
url Url @relation(fields: [id], references: [id])
|
||||
|
||||
id Int @id @default(autoincrement())
|
||||
invis String @unique
|
||||
urlId String @unique
|
||||
url Url @relation(fields: [urlId], references: [id])
|
||||
}
|
||||
|
||||
model Stats {
|
||||
id Int @id @default(autoincrement())
|
||||
created_at DateTime @default(now())
|
||||
data Json
|
||||
}
|
||||
|
||||
model Invite {
|
||||
id Int @id @default(autoincrement())
|
||||
code String @unique
|
||||
created_at DateTime @default(now())
|
||||
expires_at DateTime?
|
||||
used Boolean @default(false)
|
||||
|
||||
createdBy User @relation(fields: [createdById], references: [id])
|
||||
createdById Int
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ async function main() {
|
||||
username: 'administrator',
|
||||
password: await hashPassword('password'),
|
||||
token: createToken(),
|
||||
administrator: true
|
||||
}
|
||||
administrator: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 279 KiB After Width: | Height: | Size: 279 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 4.8 KiB |
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
branches: ['trunk'],
|
||||
plugins: [
|
||||
'@semantic-release/commit-analyzer',
|
||||
'@semantic-release/github',
|
||||
'@semantic-release/changelog'
|
||||
]
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
const Logger = require('../src/lib/logger');
|
||||
const prismaRun = require('./prisma-run');
|
||||
|
||||
module.exports = async (config) => {
|
||||
try {
|
||||
await prismaRun(config.database.url, ['migrate', 'deploy']);
|
||||
await prismaRun(config.database.url, ['generate']);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
Logger.get('db').error('there was an error.. exiting..');
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
const { readdir } = require('fs/promises');
|
||||
const { extname } = require('path');
|
||||
const validateConfig = require('../server/validateConfig');
|
||||
const Logger = require('../src/lib/logger');
|
||||
const readConfig = require('../src/lib/readConfig');
|
||||
const mimes = require('./mimes');
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
|
||||
(async () => {
|
||||
const config = readConfig();
|
||||
|
||||
await validateConfig(config);
|
||||
|
||||
process.env.DATABASE_URL = config.database.url;
|
||||
|
||||
const files = await readdir(process.argv[2]);
|
||||
const data = files.map(x => {
|
||||
const mime = mimes[extname(x)] ?? 'application/octet-stream';
|
||||
|
||||
return {
|
||||
file: x,
|
||||
mimetype: mime,
|
||||
userId: 1
|
||||
};
|
||||
});
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
Logger.get('migrator').info('starting migrations...');
|
||||
await prisma.image.createMany({
|
||||
data
|
||||
});
|
||||
Logger.get('migrator').info('finished migrations! It is recomended to move your old uploads folder (' + process.argv[2] + ') to the current one which is ' + config.uploader.directory);
|
||||
process.exit();
|
||||
})();
|
||||
@@ -1,78 +0,0 @@
|
||||
module.exports = {
|
||||
'.aac': 'audio/aac',
|
||||
'.abw': 'application/x-abiword',
|
||||
'.arc': 'application/x-freearc',
|
||||
'.avi': 'video/x-msvideo',
|
||||
'.azw': 'application/vnd.amazon.ebook',
|
||||
'.bin': 'application/octet-stream',
|
||||
'.bmp': 'image/bmp',
|
||||
'.bz': 'application/x-bzip',
|
||||
'.bz2': 'application/x-bzip2',
|
||||
'.cda': 'application/x-cdf',
|
||||
'.csh': 'application/x-csh',
|
||||
'.css': 'text/css',
|
||||
'.csv': 'text/csv',
|
||||
'.doc': 'application/msword',
|
||||
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'.eot': 'application/vnd.ms-fontobject',
|
||||
'.epub': 'application/epub+zip',
|
||||
'.gz': 'application/gzip',
|
||||
'.gif': 'image/gif',
|
||||
'.htm': 'text/html',
|
||||
'.html': 'text/html',
|
||||
'.ico': 'image/vnd.microsoft.icon',
|
||||
'.ics': 'text/calendar',
|
||||
'.jar': 'application/java-archive',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.js': 'text/javascript',
|
||||
'.json': 'application/json',
|
||||
'.jsonld': 'application/ld+json',
|
||||
'.mid': 'audio/midi',
|
||||
'.midi': 'audio/midi',
|
||||
'.mjs': 'text/javascript',
|
||||
'.mp3': 'audio/mpeg',
|
||||
'.mp4': 'video/mp4',
|
||||
'.mpeg': 'video/mpeg',
|
||||
'.mpkg': 'application/vnd.apple.installer+xml',
|
||||
'.odp': 'application/vnd.oasis.opendocument.presentation',
|
||||
'.ods': 'application/vnd.oasis.opendocument.spreadsheet',
|
||||
'.odt': 'application/vnd.oasis.opendocument.text',
|
||||
'.oga': 'audio/ogg',
|
||||
'.ogv': 'video/ogg',
|
||||
'.ogx': 'application/ogg',
|
||||
'.opus': 'audio/opus',
|
||||
'.otf': 'font/otf',
|
||||
'.png': 'image/png',
|
||||
'.pdf': 'application/pdf',
|
||||
'.php': 'application/x-httpd-php',
|
||||
'.ppt': 'application/vnd.ms-powerpoint',
|
||||
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'.rar': 'application/vnd.rar',
|
||||
'.rtf': 'application/rtf',
|
||||
'.sh': 'application/x-sh',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.swf': 'application/x-shockwave-flash',
|
||||
'.tar': 'application/x-tar',
|
||||
'.tif': 'image/tiff',
|
||||
'.tiff': 'image/tiff',
|
||||
'.ts': 'video/mp2t',
|
||||
'.ttf': 'font/ttf',
|
||||
'.txt': 'text/plain',
|
||||
'.vsd': 'application/vnd.visio',
|
||||
'.wav': 'audio/wav',
|
||||
'.weba': 'audio/webm',
|
||||
'.webm': 'video/webm',
|
||||
'.webp': 'image/webp',
|
||||
'.woff': 'font/woff',
|
||||
'.woff2': 'font/woff2',
|
||||
'.xhtml': 'application/xhtml+xml',
|
||||
'.xls': 'application/vnd.ms-excel',
|
||||
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'.xml': 'application/xml',
|
||||
'.xul': 'application/vnd.mozilla.xul+xml',
|
||||
'.zip': 'application/zip',
|
||||
'.3gp': 'video/3gpp',
|
||||
'.3g2': 'video/3gpp2',
|
||||
'.7z': 'application/x-7z-compressed'
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
const { spawn } = require('child_process');
|
||||
const { join } = require('path');
|
||||
|
||||
module.exports = (url, args) => {
|
||||
return new Promise((res, rej) => {
|
||||
const proc = spawn(join(process.cwd(), 'node_modules', '.bin', 'prisma'), args, {
|
||||
env: {
|
||||
DATABASE_URL: url,
|
||||
...process.env
|
||||
},
|
||||
});
|
||||
|
||||
let a = '';
|
||||
|
||||
proc.stdout.on('data', d => {
|
||||
console.log(d.toString());
|
||||
a += d.toString();
|
||||
});
|
||||
proc.stderr.on('data', d => {
|
||||
console.log(d.toString());
|
||||
rej(d.toString());
|
||||
});
|
||||
proc.stdout.on('end', () => res(a));
|
||||
proc.stdout.on('close', () => res(a));
|
||||
});
|
||||
};
|
||||
129
server/index.js
129
server/index.js
@@ -1,129 +0,0 @@
|
||||
const next = require('next');
|
||||
const { createServer } = require('http');
|
||||
const { stat, mkdir } = require('fs/promises');
|
||||
const { execSync } = require('child_process');
|
||||
const { extname } = require('path');
|
||||
const { red, green, bold } = require('colorette');
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const validateConfig = require('./validateConfig');
|
||||
const Logger = require('../src/lib/logger');
|
||||
const getFile = require('./static');
|
||||
const prismaRun = require('../scripts/prisma-run');
|
||||
const readConfig = require('../src/lib/readConfig');
|
||||
const mimes = require('../scripts/mimes');
|
||||
const deployDb = require('../scripts/deploy-db');
|
||||
|
||||
|
||||
Logger.get('server').info('starting zipline server');
|
||||
|
||||
const dev = process.env.NODE_ENV === 'development';
|
||||
|
||||
function log(url, status) {
|
||||
if (url.startsWith('/_next') || url.startsWith('/__nextjs')) return;
|
||||
return Logger.get('url').info(`${status === 200 ? bold(green(status)) : bold(red(status))}: ${url}`);
|
||||
}
|
||||
|
||||
function shouldUseYarn() {
|
||||
try {
|
||||
execSync('yarnpkg --version', { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const config = readConfig();
|
||||
await validateConfig(config);
|
||||
|
||||
const data = await prismaRun(config.database.url, ['migrate', 'status']);
|
||||
if (data.includes('Following migration have not yet been applied:')) {
|
||||
Logger.get('database').info('some migrations are not applied, applying them now...');
|
||||
await deployDb(config);
|
||||
Logger.get('database').info('finished applying migrations');
|
||||
}
|
||||
process.env.DATABASE_URL = config.database.url;
|
||||
|
||||
await stat('./.next');
|
||||
await mkdir(config.uploader.directory, { recursive: true });
|
||||
|
||||
const app = next({
|
||||
dir: '.',
|
||||
dev,
|
||||
quiet: dev
|
||||
}, config.core.port, config.core.host);
|
||||
|
||||
await app.prepare();
|
||||
|
||||
const handle = app.getRequestHandler();
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const srv = createServer(async (req, res) => {
|
||||
if (req.url.startsWith(config.uploader.route)) {
|
||||
const parts = req.url.split('/');
|
||||
if (!parts[2] || parts[2] === '') return;
|
||||
|
||||
const data = await getFile(config.uploader.directory, parts[2]);
|
||||
if (!data) {
|
||||
app.render404(req, res);
|
||||
} else {
|
||||
let image = await prisma.image.findFirst({
|
||||
where: {
|
||||
OR: {
|
||||
file: parts[2],
|
||||
},
|
||||
OR: {
|
||||
invisible: {
|
||||
invis: decodeURI(parts[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (image) {
|
||||
await prisma.image.update({
|
||||
where: {
|
||||
id: image.id,
|
||||
},
|
||||
data: {
|
||||
views: {
|
||||
increment: 1
|
||||
}
|
||||
}
|
||||
});
|
||||
res.setHeader('Content-Type', image.mimetype);
|
||||
} else {
|
||||
const mimetype = mimes[extname(parts[2])] ?? 'application/octet-stream';
|
||||
res.setHeader('Content-Type', mimetype);
|
||||
}
|
||||
|
||||
res.end(data);
|
||||
}
|
||||
} else {
|
||||
handle(req, res);
|
||||
}
|
||||
|
||||
log(req.url, res.statusCode);
|
||||
});
|
||||
|
||||
srv.on('error', (e) => {
|
||||
Logger.get('server').error(e);
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
srv.on('listening', () => {
|
||||
Logger.get('server').info(`listening on ${config.core.host}:${config.core.port}`);
|
||||
});
|
||||
|
||||
srv.listen(config.core.port, config.core.host ?? '0.0.0.0');
|
||||
} catch (e) {
|
||||
if (e.message && e.message.startsWith('Could not find a production')) {
|
||||
Logger.get('web').error(`there is no production build - run \`${shouldUseYarn() ? 'yarn build' : 'npm build'}\``);
|
||||
} else if (e.code && e.code === 'ENOENT') {
|
||||
if (e.path === './.next') Logger.get('web').error(`there is no production build - run \`${shouldUseYarn() ? 'yarn build' : 'npm build'}\``);
|
||||
} else {
|
||||
Logger.get('server').error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
})();
|
||||
@@ -1,11 +0,0 @@
|
||||
const { readFile } = require('fs/promises');
|
||||
const { join } = require('path');
|
||||
|
||||
module.exports = async (dir, file) => {
|
||||
try {
|
||||
const data = await readFile(join(process.cwd(), dir, file));
|
||||
return data;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
const Logger = require('../src/lib/logger');
|
||||
|
||||
function dot(str, obj) {
|
||||
return str.split('.').reduce((a,b) => a[b], obj);
|
||||
}
|
||||
|
||||
const path = (path, type) => ({ path, type });
|
||||
|
||||
module.exports = async config => {
|
||||
const paths = [
|
||||
path('core.secure', 'boolean'),
|
||||
path('core.secret', 'string'),
|
||||
path('core.host', 'string'),
|
||||
path('core.port', 'number'),
|
||||
path('database.type', 'string'),
|
||||
path('database.url', 'string'),
|
||||
path('uploader.route', 'string'),
|
||||
path('uploader.embed_route', 'string'),
|
||||
path('uploader.length', 'number'),
|
||||
path('uploader.directory', 'string')
|
||||
];
|
||||
|
||||
let errors = 0;
|
||||
|
||||
for (let i = 0, L = paths.length; i !== L; ++i) {
|
||||
const path = paths[i];
|
||||
const value = dot(path.path, config);
|
||||
if (value === undefined) {
|
||||
Logger.get('config').error(`there was no ${path.path} in config`);
|
||||
++errors;
|
||||
}
|
||||
const type = typeof value;
|
||||
|
||||
if (value !== undefined && type !== path.type) {
|
||||
Logger.get('config').error(`expected ${path.type} on ${path.path}, but got ${type}`);
|
||||
++errors;
|
||||
}
|
||||
}
|
||||
|
||||
if (errors !== 0) {
|
||||
Logger.get('config').error(`exiting due to ${errors} errors`);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Snackbar, Alert as MuiAlert } from '@material-ui/core';
|
||||
|
||||
export default function Alert({ open, setOpen, severity, message }) {
|
||||
return (
|
||||
<Snackbar open={open} autoHideDuration={6000} anchorOrigin={{ vertical: 'top', horizontal: 'center' }} onClose={() => setOpen(false)}>
|
||||
<MuiAlert severity={severity} sx={{ width: '100%' }}>
|
||||
{message}
|
||||
</MuiAlert>
|
||||
</Snackbar>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Backdrop as MuiBackdrop,
|
||||
CircularProgress
|
||||
} from '@material-ui/core';
|
||||
|
||||
export default function Backdrop({ open }) {
|
||||
return (
|
||||
<MuiBackdrop
|
||||
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||
open={open}
|
||||
>
|
||||
<CircularProgress color='inherit' />
|
||||
</MuiBackdrop>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,11 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Card as MuiCard,
|
||||
CardContent,
|
||||
Typography
|
||||
} from '@material-ui/core';
|
||||
import { Card as MCard, Title } from '@mantine/core';
|
||||
|
||||
export default function Card(props) {
|
||||
const { name, children, ...other } = props;
|
||||
export default function Card({ name, children, ...other }) {
|
||||
|
||||
return (
|
||||
<MuiCard sx={{ minWidth: 100 }} {...other}>
|
||||
<CardContent>
|
||||
<Typography variant='h3'>{name}</Typography>
|
||||
{children}
|
||||
</CardContent>
|
||||
</MuiCard>
|
||||
<MCard p='md' shadow='sm' {...other}>
|
||||
<Title order={2}>{name}</Title>
|
||||
{children}
|
||||
</MCard>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@material-ui/core';
|
||||
|
||||
export default function CenteredBox({ children, ...other }) {
|
||||
return (
|
||||
<Box
|
||||
justifyContent='center'
|
||||
display='flex'
|
||||
alignItems='center'
|
||||
{...other}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
21
src/components/CodeInput.tsx
Normal file
21
src/components/CodeInput.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { createStyles, MantineSize, Textarea } from '@mantine/core';
|
||||
|
||||
const useStyles = createStyles((theme, { size }: { size: MantineSize }) => ({
|
||||
input: {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: theme.fn.size({ size, sizes: theme.fontSizes }) - 2,
|
||||
height: '100vh',
|
||||
},
|
||||
}));
|
||||
|
||||
export default function CodeInput({ ...props }) {
|
||||
const { classes } = useStyles({ size: 'md' }, { name: 'CodeInput' });
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
classNames={{ input: classes.input }}
|
||||
autoComplete='nope'
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
118
src/components/File.tsx
Normal file
118
src/components/File.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { Button, Card, Grid, Group, Image as MImage, Modal, Stack, Text, Title, useMantineTheme } from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { useNotifications } from '@mantine/notifications';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { useState } from 'react';
|
||||
import Type from './Type';
|
||||
import { CalendarIcon, CopyIcon, CrossIcon, DeleteIcon, FileIcon, HashIcon, ImageIcon, StarIcon } from './icons';
|
||||
import MutedText from './MutedText';
|
||||
|
||||
export function FileMeta({ Icon, title, subtitle }) {
|
||||
return (
|
||||
<Group>
|
||||
<Icon size={24} />
|
||||
<Stack spacing={1}>
|
||||
<Text>{title}</Text>
|
||||
<MutedText size='md'>{subtitle}</MutedText>
|
||||
</Stack>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
export default function File({ image, updateImages }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const notif = useNotifications();
|
||||
const clipboard = useClipboard();
|
||||
const theme = useMantineTheme();
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await useFetch('/api/user/files', 'DELETE', { id: image.id });
|
||||
if (!res.error) {
|
||||
updateImages(true);
|
||||
notif.showNotification({
|
||||
title: 'File Deleted',
|
||||
message: '',
|
||||
color: 'green',
|
||||
icon: <DeleteIcon />,
|
||||
});
|
||||
} else {
|
||||
notif.showNotification({
|
||||
title: 'Failed to delete file',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
clipboard.copy(`${window.location.protocol}//${window.location.host}${image.url}`);
|
||||
setOpen(false);
|
||||
notif.showNotification({
|
||||
title: 'Copied to clipboard',
|
||||
message: '',
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
};
|
||||
|
||||
const handleFavorite = async () => {
|
||||
const data = await useFetch('/api/user/files', 'PATCH', { id: image.id, favorite: !image.favorite });
|
||||
if (!data.error) updateImages(true);
|
||||
notif.showNotification({
|
||||
title: 'Image is now ' + (!image.favorite ? 'favorited' : 'unfavorited'),
|
||||
message: '',
|
||||
icon: <StarIcon />,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
opened={open}
|
||||
onClose={() => setOpen(false)}
|
||||
title={<Title>{image.file}</Title>}
|
||||
size='xl'
|
||||
overlayBlur={3}
|
||||
overlayColor={theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white'}
|
||||
>
|
||||
<Stack>
|
||||
<Type
|
||||
file={image}
|
||||
src={image.url}
|
||||
alt={image.file}
|
||||
popup
|
||||
sx={{ minHeight: 200 }}
|
||||
style={{ minHeight: 200 }}
|
||||
/>
|
||||
<Stack>
|
||||
<FileMeta Icon={FileIcon} title='Name' subtitle={image.file} />
|
||||
<FileMeta Icon={ImageIcon} title='Type' subtitle={image.mimetype} />
|
||||
<FileMeta Icon={CalendarIcon} title='Uploaded at' subtitle={new Date(image.created_at).toLocaleString()} />
|
||||
<FileMeta Icon={HashIcon} title='ID' subtitle={image.id} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Group position='right' mt={22}>
|
||||
<Button onClick={handleCopy}>Copy</Button>
|
||||
<Button onClick={handleDelete}>Delete</Button>
|
||||
<Button onClick={handleFavorite}>{image.favorite ? 'Unfavorite' : 'Favorite'}</Button>
|
||||
</Group>
|
||||
</Modal>
|
||||
<Card sx={{ maxWidth: '100%', height: '100%' }} shadow='md'>
|
||||
<Card.Section>
|
||||
<Type
|
||||
file={image}
|
||||
sx={{ minHeight: 200, maxHeight: 320, fontSize: 70, width: '100%', cursor: 'pointer' }}
|
||||
style={{ minHeight: 200, maxHeight: 320, fontSize: 70, width: '100%', cursor: 'pointer' }}
|
||||
src={image.url}
|
||||
alt={image.file}
|
||||
onClick={() => setOpen(true)}
|
||||
/>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardMedia,
|
||||
CardActionArea,
|
||||
Popover,
|
||||
Button,
|
||||
ButtonGroup
|
||||
} from '@material-ui/core';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import useFetch from '../lib/hooks/useFetch';
|
||||
|
||||
export default function Image({ image, updateImages }) {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await useFetch('/api/user/images', 'DELETE', { id: image.id });
|
||||
if (!res.error) updateImages();
|
||||
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
copy(`${window.location.protocol}//${window.location.host}${image.url}`);
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card sx={{ maxWidth: '100%' }}>
|
||||
<CardActionArea>
|
||||
<CardMedia
|
||||
sx={{ height: 320 }}
|
||||
image={image.url}
|
||||
title={image.file}
|
||||
onClick={e => setAnchorEl(e.currentTarget)}
|
||||
/>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
<Popover
|
||||
open={Boolean(anchorEl)}
|
||||
anchorEl={anchorEl}
|
||||
onClose={() => setAnchorEl(null)}
|
||||
anchorOrigin={{
|
||||
vertical: 'center',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'center',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
>
|
||||
<ButtonGroup variant='contained'>
|
||||
<Button onClick={handleDelete} color='primary'>Delete</Button>
|
||||
<Button onClick={handleCopy} color='primary'>Copy URL</Button>
|
||||
</ButtonGroup>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
}
|
||||
168
src/components/ImagesTable.tsx
Normal file
168
src/components/ImagesTable.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
/* eslint-disable react/jsx-key */
|
||||
// Code taken from https://codesandbox.io/s/eojw8 and is modified a bit (the entire src/components/table directory)
|
||||
import {
|
||||
ActionIcon,
|
||||
createStyles,
|
||||
Divider,
|
||||
Group, Image, Pagination,
|
||||
Select,
|
||||
Table,
|
||||
Text,
|
||||
useMantineTheme,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
usePagination,
|
||||
useTable,
|
||||
} from 'react-table';
|
||||
import { CopyIcon, DeleteIcon, EnterIcon } from './icons';
|
||||
|
||||
const pageSizeOptions = ['10', '25', '50'];
|
||||
|
||||
const useStyles = createStyles((t) => ({
|
||||
root: { height: '100%', display: 'block', marginTop: 10 },
|
||||
tableContainer: {
|
||||
display: 'block',
|
||||
overflow: 'auto',
|
||||
'& > table': {
|
||||
'& > thead': { backgroundColor: t.colorScheme === 'dark' ? t.colors.dark[6] : t.colors.gray[0], zIndex: 1 },
|
||||
'& > thead > tr > th': { padding: t.spacing.md },
|
||||
'& > tbody > tr > td': { padding: t.spacing.md },
|
||||
},
|
||||
borderRadius: 6,
|
||||
},
|
||||
stickHeader: { top: 0, position: 'sticky' },
|
||||
disableSortIcon: { color: t.colors.gray[5] },
|
||||
sortDirectionIcon: { transition: 'transform 200ms ease' },
|
||||
}));
|
||||
|
||||
export function FilePreview({ url, type }) {
|
||||
const Type = props => {
|
||||
return {
|
||||
'video': <video autoPlay controls {...props} />,
|
||||
'image': <Image {...props} />,
|
||||
'audio': <audio autoPlay controls {...props} />,
|
||||
}[type.split('/')[0]];
|
||||
};
|
||||
|
||||
return (
|
||||
<Type
|
||||
sx={{ maxWidth: '10vw', maxHeight: '100vh' }}
|
||||
style={{ maxWidth: '10vw', maxHeight: '100vh' }}
|
||||
mr='sm'
|
||||
src={url}
|
||||
alt={'Unable to preview file'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ImagesTable({
|
||||
columns,
|
||||
data = [],
|
||||
serverSideDataSource = false,
|
||||
initialPageSize = 10,
|
||||
initialPageIndex = 0,
|
||||
pageCount = 0,
|
||||
total = 0,
|
||||
deleteImage, copyImage, viewImage,
|
||||
}) {
|
||||
const { classes } = useStyles();
|
||||
const theme = useMantineTheme();
|
||||
|
||||
const tableOptions = useTable(
|
||||
{
|
||||
data,
|
||||
columns,
|
||||
pageCount,
|
||||
initialState: { pageSize: initialPageSize, pageIndex: initialPageIndex },
|
||||
},
|
||||
usePagination
|
||||
);
|
||||
|
||||
const {
|
||||
getTableProps, getTableBodyProps, headerGroups, rows, prepareRow, page, gotoPage, setPageSize, state: { pageIndex, pageSize },
|
||||
} = tableOptions;
|
||||
|
||||
const getPageRecordInfo = () => {
|
||||
const firstRowNum = pageIndex * pageSize + 1;
|
||||
const totalRows = rows.length;
|
||||
|
||||
const currLastRowNum = (pageIndex + 1) * pageSize;
|
||||
let lastRowNum = currLastRowNum < totalRows ? currLastRowNum : totalRows;
|
||||
return `${firstRowNum} - ${lastRowNum} of ${totalRows}`;
|
||||
};
|
||||
|
||||
const getPageCount = () => Math.ceil(rows.length / pageSize);
|
||||
|
||||
const handlePageChange = (pageNum) => gotoPage(pageNum - 1);
|
||||
|
||||
const renderHeader = () => headerGroups.map(hg => (
|
||||
<tr {...hg.getHeaderGroupProps()}>
|
||||
{hg.headers.map(column => (
|
||||
<th {...column.getHeaderProps()}>
|
||||
<Group noWrap position={column.align || 'apart'}>
|
||||
<div>{column.render('Header')}</div>
|
||||
</Group>
|
||||
</th>
|
||||
))}
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
));
|
||||
|
||||
const renderRow = rows => rows.map(row => {
|
||||
prepareRow(row);
|
||||
return (
|
||||
<tr {...row.getRowProps()}>
|
||||
{row.cells.map(cell => (
|
||||
<td align={cell.column.align || 'left'} {...cell.getCellProps()}>
|
||||
{cell.render('Cell')}
|
||||
</td>
|
||||
))}
|
||||
<td align='right'>
|
||||
<Group noWrap>
|
||||
<ActionIcon color='red' variant='outline' onClick={() => deleteImage(row)}><DeleteIcon /></ActionIcon>
|
||||
<ActionIcon color='primary' variant='outline' onClick={() => copyImage(row)}><CopyIcon /></ActionIcon>
|
||||
<ActionIcon color='green' variant='outline' onClick={() => viewImage(row)}><EnterIcon /></ActionIcon>
|
||||
</Group>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<div
|
||||
className={classes.tableContainer}
|
||||
style={{ height: 'calc(100% - 44px)' }}
|
||||
>
|
||||
<Table {...getTableProps()}>
|
||||
<thead style={{ backgroundColor: theme.other.hover }}>
|
||||
{renderHeader()}
|
||||
</thead>
|
||||
|
||||
<tbody {...getTableBodyProps()}>
|
||||
{renderRow(page)}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
<Divider mb='md' variant='dotted' />
|
||||
<Group position='left'>
|
||||
<Text size='sm'>Rows per page: </Text>
|
||||
<Select
|
||||
style={{ width: '72px' }}
|
||||
variant='filled'
|
||||
data={pageSizeOptions}
|
||||
value={pageSize + ''}
|
||||
onChange={pageSize => setPageSize(Number(pageSize))} />
|
||||
<Divider orientation='vertical' />
|
||||
|
||||
<Text size='sm'>{getPageRecordInfo()}</Text>
|
||||
<Divider orientation='vertical' />
|
||||
|
||||
<Pagination
|
||||
page={pageIndex + 1}
|
||||
total={getPageCount()}
|
||||
onChange={handlePageChange} />
|
||||
</Group>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,417 +1,365 @@
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import useFetch from '../lib/hooks/useFetch';
|
||||
|
||||
import {
|
||||
AppBar,
|
||||
Box,
|
||||
Divider,
|
||||
Drawer,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Toolbar,
|
||||
Typography,
|
||||
Button,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
Select,
|
||||
} from '@material-ui/core';
|
||||
import {
|
||||
Menu as MenuIcon,
|
||||
Home as HomeIcon,
|
||||
AccountCircle as AccountIcon,
|
||||
Image as ImageIcon,
|
||||
Upload as UploadIcon,
|
||||
ContentCopy as CopyIcon,
|
||||
Autorenew as ResetIcon,
|
||||
Logout as LogoutIcon,
|
||||
PeopleAlt as UsersIcon,
|
||||
Brush as BrushIcon
|
||||
} from '@material-ui/icons';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import Backdrop from './Backdrop';
|
||||
import { friendlyThemeName, themes } from './Theming';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useStoreDispatch } from 'lib/redux/store';
|
||||
import { AppShell, Box, Burger, Divider, Group, Header, MediaQuery, Navbar, Paper, Popover, ScrollArea, Select, Text, ThemeIcon, Title, UnstyledButton, useMantineTheme } from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useNotifications } from '@mantine/notifications';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { updateUser } from 'lib/redux/reducers/user';
|
||||
import { useStoreDispatch } from 'lib/redux/store';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { ActivityIcon, CheckIcon, CopyIcon, CrossIcon, DeleteIcon, FileIcon, HomeIcon, LinkIcon, LogoutIcon, PencilIcon, SettingsIcon, TagIcon, TypeIcon, UploadIcon, UserIcon } from './icons';
|
||||
import { friendlyThemeName, themes } from './Theming';
|
||||
|
||||
function MenuItemLink(props) {
|
||||
return (
|
||||
<Link href={props.href} passHref>
|
||||
<MenuItem {...props} />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function MenuItem(props) {
|
||||
return (
|
||||
<UnstyledButton
|
||||
sx={theme => ({
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
padding: 5,
|
||||
borderRadius: theme.radius.sm,
|
||||
color: props.color
|
||||
? theme.fn.themeColor(props.color, theme.colorScheme === 'dark' ? 5 : 7)
|
||||
: theme.colorScheme === 'dark'
|
||||
? theme.colors.dark[0]
|
||||
: theme.black,
|
||||
'&:hover': {
|
||||
backgroundColor: props.color
|
||||
? theme.fn.rgba(
|
||||
theme.fn.themeColor(props.color, theme.colorScheme === 'dark' ? 9 : 0),
|
||||
theme.colorScheme === 'dark' ? 0.2 : 1
|
||||
)
|
||||
: theme.colorScheme === 'dark'
|
||||
? theme.fn.rgba(theme.colors.dark[3], 0.35)
|
||||
: theme.colors.gray[0],
|
||||
},
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
<Group noWrap>
|
||||
<Box sx={theme => ({
|
||||
marginRight: theme.spacing.xs / 4,
|
||||
paddingLeft: theme.spacing.xs / 2,
|
||||
|
||||
'& *': {
|
||||
display: 'block',
|
||||
},
|
||||
})}>
|
||||
{props.icon}
|
||||
</Box>
|
||||
<Text size='sm'>{props.children}</Text>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
);
|
||||
}
|
||||
|
||||
const items = [
|
||||
{
|
||||
icon: <HomeIcon />,
|
||||
text: 'Home',
|
||||
link: '/dashboard'
|
||||
link: '/dashboard',
|
||||
},
|
||||
{
|
||||
icon: <ImageIcon />,
|
||||
text: 'Images',
|
||||
link: '/dashboard/images'
|
||||
icon: <FileIcon />,
|
||||
text: 'Files',
|
||||
link: '/dashboard/files',
|
||||
},
|
||||
{
|
||||
icon: <ActivityIcon />,
|
||||
text: 'Stats',
|
||||
link: '/dashboard/stats',
|
||||
},
|
||||
{
|
||||
icon: <LinkIcon />,
|
||||
text: 'URLs',
|
||||
link: '/dashboard/urls',
|
||||
},
|
||||
{
|
||||
icon: <UploadIcon />,
|
||||
text: 'Upload',
|
||||
link: '/dashboard/upload'
|
||||
}
|
||||
link: '/dashboard/upload',
|
||||
},
|
||||
{
|
||||
icon: <TypeIcon />,
|
||||
text: 'Upload Text',
|
||||
link: '/dashboard/text',
|
||||
},
|
||||
];
|
||||
|
||||
const drawerWidth = 240;
|
||||
|
||||
function CopyTokenDialog({ open, setOpen, token }) {
|
||||
const handleCopyToken = () => {
|
||||
copy(token);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<DialogTitle id='copy-dialog-title'>
|
||||
Copy Token
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id='copy-dialog-description'>
|
||||
Make sure you don't share this token with anyone as they will be able to upload images on your behalf.
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setOpen(false)} color='inherit' autoFocus>Cancel</Button>
|
||||
<Button onClick={handleCopyToken} color='inherit'>
|
||||
Copy
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ResetTokenDialog({ open, setOpen, setToken }) {
|
||||
const handleResetToken = async () => {
|
||||
const a = await useFetch('/api/user/token', 'PATCH');
|
||||
if (a.success) setToken(a.success);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<DialogTitle id='reset-dialog-title'>
|
||||
Reset Token
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id='reset-dialog-description'>
|
||||
Once you reset your token, you will have to update any uploaders to use this new token.
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setOpen(false)} color='inherit' autoFocus>Cancel</Button>
|
||||
<Button onClick={handleResetToken} color='inherit'>
|
||||
Reset
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Layout({ children, user, loading, noPaper }) {
|
||||
const [systemTheme, setSystemTheme] = useState(user.systemTheme || 'dark_blue');
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [copyOpen, setCopyOpen] = useState(false);
|
||||
const [resetOpen, setResetOpen] = useState(false);
|
||||
export default function Layout({ children, user, title }) {
|
||||
const [token, setToken] = useState(user?.token);
|
||||
const [systemTheme, setSystemTheme] = useState(user.systemTheme ?? 'system');
|
||||
const [opened, setOpened] = useState(false); // navigation open
|
||||
const [open, setOpen] = useState(false); // manage acc dropdown
|
||||
const router = useRouter();
|
||||
const dispatch = useStoreDispatch();
|
||||
const theme = useMantineTheme();
|
||||
const modals = useModals();
|
||||
const notif = useNotifications();
|
||||
const clipboard = useClipboard();
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
const handleClick = e => setAnchorEl(e.currentTarget);
|
||||
const handleClose = (cmd: 'copy' | 'reset') => () => {
|
||||
switch (cmd) {
|
||||
case 'copy':
|
||||
setCopyOpen(true);
|
||||
break;
|
||||
case 'reset':
|
||||
setResetOpen(true);
|
||||
break;
|
||||
}
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleUpdateTheme = async (event: React.ChangeEvent<{ value: string }>) => {
|
||||
const handleUpdateTheme = async value => {
|
||||
const newUser = await useFetch('/api/user', 'PATCH', {
|
||||
systemTheme: event.target.value || 'dark_blue'
|
||||
systemTheme: value || 'dark_blue',
|
||||
});
|
||||
|
||||
setSystemTheme(newUser.systemTheme);
|
||||
dispatch(updateUser(newUser));
|
||||
|
||||
router.replace(router.pathname);
|
||||
|
||||
notif.showNotification({
|
||||
title: `Theme changed to ${friendlyThemeName[value]}`,
|
||||
message: '',
|
||||
color: 'green',
|
||||
icon: <PencilIcon />,
|
||||
});
|
||||
};
|
||||
|
||||
const drawer = (
|
||||
<div>
|
||||
<CopyTokenDialog open={copyOpen} setOpen={setCopyOpen} token={token} />
|
||||
<ResetTokenDialog open={resetOpen} setOpen={setResetOpen} setToken={setToken} />
|
||||
<Toolbar
|
||||
sx={{
|
||||
width: { xs: drawerWidth }
|
||||
}}
|
||||
>
|
||||
<AppBar
|
||||
position='fixed'
|
||||
elevation={0}
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderBottomColor: t => t.palette.divider,
|
||||
display: { xs: 'none', sm: 'block' }
|
||||
}}
|
||||
>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
color='inherit'
|
||||
aria-label='open drawer'
|
||||
edge='start'
|
||||
onClick={() => setMobileOpen(true)}
|
||||
sx={{ mr: 2, display: { sm: 'none' } }}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Typography
|
||||
variant='h5'
|
||||
noWrap
|
||||
component='div'
|
||||
>
|
||||
Zipline
|
||||
</Typography>
|
||||
{user && (
|
||||
<Box sx={{ marginLeft: 'auto' }}>
|
||||
<Button
|
||||
color='inherit'
|
||||
aria-expanded={open ? 'true' : undefined}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<AccountIcon />
|
||||
</Button>
|
||||
<Menu
|
||||
id='zipline-user-menu'
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose(null)}
|
||||
MenuListProps={{
|
||||
'aria-labelledby': 'basic-button',
|
||||
}}
|
||||
>
|
||||
<MenuItem disableRipple>
|
||||
<Typography variant='h5'>
|
||||
<b>{user.username}</b>
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<Link href='/dashboard/manage'>
|
||||
<a style={{ color: 'white', textDecoration: 'none' }}>
|
||||
<MenuItem onClick={handleClose(null)}>
|
||||
<AccountIcon sx={{ mr: 2 }} /> Manage Account
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<MenuItem onClick={handleClose('copy')}>
|
||||
<CopyIcon sx={{ mr: 2 }} /> Copy Token
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleClose('reset')}>
|
||||
<ResetIcon sx={{ mr: 2 }} /> Reset Token
|
||||
</MenuItem>
|
||||
<Link href='/auth/logout'>
|
||||
<a style={{ color: 'white', textDecoration: 'none' }}>
|
||||
<MenuItem onClick={handleClose(null)}>
|
||||
<LogoutIcon sx={{ mr: 2 }} /> Logout
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<MenuItem>
|
||||
<BrushIcon sx={{ mr: 2 }} />
|
||||
<Select
|
||||
variant='standard'
|
||||
label='Theme'
|
||||
value={systemTheme}
|
||||
onChange={handleUpdateTheme}
|
||||
fullWidth
|
||||
>
|
||||
{Object.keys(themes).map(t => (
|
||||
<MenuItem value={t} key={t}>
|
||||
{friendlyThemeName[t]}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
)}
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</Toolbar>
|
||||
<Divider />
|
||||
<List>
|
||||
{items.map((item, i) => (
|
||||
<Link key={i} href={item.link}>
|
||||
<a href={item.link} style={{ color: 'white', textDecoration: 'none' }}>
|
||||
<ListItem button>
|
||||
<ListItemIcon>{item.icon}</ListItemIcon>
|
||||
<ListItemText primary={item.text} />
|
||||
</ListItem>
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
{user && user.administrator && (
|
||||
<Link href='/dashboard/users' passHref>
|
||||
<a style={{ color: 'white', textDecoration: 'none' }}>
|
||||
<ListItem button>
|
||||
<ListItemIcon><UsersIcon /></ListItemIcon>
|
||||
<ListItemText primary='Users' />
|
||||
</ListItem>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
</List>
|
||||
|
||||
</div>
|
||||
);
|
||||
const openResetToken = () => modals.openConfirmModal({
|
||||
title: 'Reset Token',
|
||||
centered: true,
|
||||
overlayBlur: 3,
|
||||
children: (
|
||||
<Text size='sm'>
|
||||
Once you reset your token, you will have to update any uploaders to use this new token.
|
||||
</Text>
|
||||
),
|
||||
labels: { confirm: 'Reset', cancel: 'Cancel' },
|
||||
onConfirm: async () => {
|
||||
const a = await useFetch('/api/user/token', 'PATCH');
|
||||
if (!a.success) {
|
||||
setToken(a.success);
|
||||
notif.showNotification({
|
||||
title: 'Token Reset Failed',
|
||||
message: a.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
} else {
|
||||
notif.showNotification({
|
||||
title: 'Token Reset',
|
||||
message: 'Your token has been reset. You will need to update any uploaders to use this new token.',
|
||||
color: 'green',
|
||||
icon: <CheckIcon />,
|
||||
});
|
||||
}
|
||||
|
||||
const container = typeof window !== 'undefined' ? window.document.body : undefined;
|
||||
modals.closeAll();
|
||||
},
|
||||
});
|
||||
|
||||
const openCopyToken = () => modals.openConfirmModal({
|
||||
title: 'Copy Token',
|
||||
centered: true,
|
||||
overlayBlur: 3,
|
||||
children: (
|
||||
<Text size='sm'>
|
||||
Make sure you don't share this token with anyone as they will be able to upload files on your behalf.
|
||||
</Text>
|
||||
),
|
||||
labels: { confirm: 'Copy', cancel: 'Cancel' },
|
||||
onConfirm: async () => {
|
||||
clipboard.copy(token);
|
||||
|
||||
notif.showNotification({
|
||||
title: 'Token Copied',
|
||||
message: 'Your token has been copied to your clipboard.',
|
||||
color: 'green',
|
||||
icon: <CheckIcon />,
|
||||
});
|
||||
|
||||
modals.closeAll();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Backdrop open={loading} />
|
||||
<AppShell
|
||||
navbarOffsetBreakpoint='sm'
|
||||
fixed
|
||||
navbar={
|
||||
<Navbar
|
||||
p='md'
|
||||
hiddenBreakpoint='sm'
|
||||
hidden={!opened}
|
||||
width={{ sm: 200, lg: 230 }}
|
||||
>
|
||||
<Navbar.Section
|
||||
grow
|
||||
component={ScrollArea}
|
||||
ml={-10}
|
||||
mr={-10}
|
||||
sx={{ paddingLeft: 10, paddingRight: 10 }}
|
||||
>
|
||||
{items.map(({ icon, text, link }) => (
|
||||
<Link href={link} key={text} passHref>
|
||||
<UnstyledButton
|
||||
sx={{
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
padding: theme.spacing.xs,
|
||||
borderRadius: theme.radius.sm,
|
||||
color: theme.colorScheme === 'dark' ? theme.colors.dark[0] : theme.black,
|
||||
|
||||
'&:hover': {
|
||||
backgroundColor: theme.other.hover,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Group>
|
||||
<ThemeIcon color='primary' variant='filled'>
|
||||
{icon}
|
||||
</ThemeIcon>
|
||||
|
||||
<AppBar
|
||||
position='fixed'
|
||||
elevation={0}
|
||||
sx={{
|
||||
width: { sm: `calc(100% - ${drawerWidth}px)` },
|
||||
ml: { sm: `${drawerWidth}px` }
|
||||
}}
|
||||
>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
color='inherit'
|
||||
aria-label='open drawer'
|
||||
edge='start'
|
||||
onClick={() => setMobileOpen(true)}
|
||||
sx={{ mr: 2, display: { sm: 'none' } }}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Typography
|
||||
variant='h5'
|
||||
noWrap
|
||||
component='div'
|
||||
sx={{ display: { sm: 'none' } }}
|
||||
>
|
||||
Zipline
|
||||
</Typography>
|
||||
{user && (
|
||||
<Box sx={{ marginLeft: 'auto' }}>
|
||||
<Button
|
||||
color='inherit'
|
||||
aria-expanded={open ? 'true' : undefined}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<AccountIcon />
|
||||
</Button>
|
||||
<Menu
|
||||
id='zipline-user-menu'
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose(null)}
|
||||
MenuListProps={{
|
||||
'aria-labelledby': 'basic-button',
|
||||
}}
|
||||
>
|
||||
<MenuItem disableRipple>
|
||||
<Typography variant='h5'>
|
||||
<b>{user.username}</b>
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<Link href='/dash/manage'>
|
||||
<a style={{ color: 'white', textDecoration: 'none' }}>
|
||||
<MenuItem onClick={handleClose(null)}>
|
||||
<AccountIcon sx={{ mr: 2 }} /> Manage Account
|
||||
</MenuItem>
|
||||
</a>
|
||||
<Text size='lg'>{text}</Text>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</Link>
|
||||
))}
|
||||
{user.administrator && (
|
||||
<>
|
||||
<Link href='/dashboard/users' passHref>
|
||||
<UnstyledButton
|
||||
sx={{
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
padding: theme.spacing.xs,
|
||||
borderRadius: theme.radius.sm,
|
||||
color: theme.colorScheme === 'dark' ? theme.colors.dark[0] : theme.black,
|
||||
|
||||
'&:hover': {
|
||||
backgroundColor: theme.other.hover,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Group>
|
||||
<ThemeIcon color='primary' variant='filled'>
|
||||
<UserIcon />
|
||||
</ThemeIcon>
|
||||
|
||||
<Text size='lg'>Users</Text>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</Link>
|
||||
<MenuItem onClick={handleClose('copy')}>
|
||||
<CopyIcon sx={{ mr: 2 }} /> Copy Token
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleClose('reset')}>
|
||||
<ResetIcon sx={{ mr: 2 }} /> Reset Token
|
||||
</MenuItem>
|
||||
<Link href='/auth/logout'>
|
||||
<a style={{ color: 'white', textDecoration: 'none' }}>
|
||||
<MenuItem onClick={handleClose(null)}>
|
||||
<LogoutIcon sx={{ mr: 2 }} /> Logout
|
||||
</MenuItem>
|
||||
</a>
|
||||
<Link href='/dashboard/invites' passHref>
|
||||
<UnstyledButton
|
||||
sx={{
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
padding: theme.spacing.xs,
|
||||
borderRadius: theme.radius.sm,
|
||||
color: theme.colorScheme === 'dark' ? theme.colors.dark[0] : theme.black,
|
||||
|
||||
'&:hover': {
|
||||
backgroundColor: theme.other.hover,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Group>
|
||||
<ThemeIcon color='primary' variant='filled'>
|
||||
<TagIcon />
|
||||
</ThemeIcon>
|
||||
|
||||
<Text size='lg'>Invites</Text>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</Link>
|
||||
</Menu>
|
||||
</>
|
||||
)}
|
||||
</Navbar.Section>
|
||||
</Navbar>
|
||||
}
|
||||
header={
|
||||
<Header height={70} p='md'>
|
||||
<div style={{ display: 'flex', alignItems: 'center', height: '100%' }}>
|
||||
<MediaQuery largerThan='sm' styles={{ display: 'none' }}>
|
||||
<Burger
|
||||
opened={opened}
|
||||
onClick={() => setOpened((o) => !o)}
|
||||
size='sm'
|
||||
color={theme.colors.gray[6]}
|
||||
/>
|
||||
</MediaQuery>
|
||||
<Title ml='md'>{title}</Title>
|
||||
<Box sx={{ marginLeft: 'auto', marginRight: 0 }}>
|
||||
<Popover
|
||||
position='top'
|
||||
placement='end'
|
||||
spacing={4}
|
||||
opened={open}
|
||||
onClose={() => setOpen(false)}
|
||||
target={
|
||||
<UnstyledButton
|
||||
onClick={() => setOpen(!open)}
|
||||
sx={{
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
padding: theme.spacing.xs,
|
||||
borderRadius: theme.radius.sm,
|
||||
color: theme.other.color,
|
||||
|
||||
'&:hover': {
|
||||
backgroundColor: theme.other.hover,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Group>
|
||||
<ThemeIcon color='primary' variant='filled'>
|
||||
<SettingsIcon />
|
||||
</ThemeIcon>
|
||||
<Text>{user.username}</Text>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
}
|
||||
>
|
||||
<Group direction='column' spacing={2}>
|
||||
<Text sx={{
|
||||
color: theme.colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6],
|
||||
fontWeight: 500,
|
||||
fontSize: theme.fontSizes.sm,
|
||||
padding: `${theme.spacing.xs / 2}px ${theme.spacing.sm}px`,
|
||||
cursor: 'default',
|
||||
}}
|
||||
>
|
||||
{user.username}
|
||||
</Text>
|
||||
<MenuItemLink icon={<SettingsIcon />} href='/dashboard/manage'>Manage Account</MenuItemLink>
|
||||
<MenuItem icon={<CopyIcon />} onClick={() => {setOpen(false);openCopyToken();}}>Copy Token</MenuItem>
|
||||
<MenuItem icon={<DeleteIcon />} onClick={() => {setOpen(false);openResetToken();}} color='red'>Reset Token</MenuItem>
|
||||
<MenuItemLink icon={<LogoutIcon />} href='/auth/logout' color='red'>Logout</MenuItemLink>
|
||||
<Divider
|
||||
variant='solid'
|
||||
my={theme.spacing.xs / 2}
|
||||
sx={theme => ({
|
||||
width: '110%',
|
||||
borderTopColor: theme.colorScheme === 'dark' ? theme.colors.dark[7] : theme.colors.gray[2],
|
||||
margin: `${theme.spacing.xs / 2}px -4px`,
|
||||
})}
|
||||
/>
|
||||
<MenuItem icon={<PencilIcon />}>
|
||||
<Select
|
||||
size='xs'
|
||||
data={Object.keys(themes).map(t => ({ value: t, label: friendlyThemeName[t] }))}
|
||||
value={systemTheme}
|
||||
onChange={handleUpdateTheme}
|
||||
/>
|
||||
</MenuItem>
|
||||
</Group>
|
||||
</Popover>
|
||||
</Box>
|
||||
)}
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Box
|
||||
component='nav'
|
||||
sx={{
|
||||
width: { sm: drawerWidth },
|
||||
flexShrink: { sm: 0 }
|
||||
}}
|
||||
>
|
||||
<Drawer
|
||||
container={container}
|
||||
variant='temporary'
|
||||
onClose={() => setMobileOpen(false)}
|
||||
open={mobileOpen}
|
||||
elevation={0}
|
||||
ModalProps={{
|
||||
keepMounted: true
|
||||
}}
|
||||
sx={{
|
||||
display: { xs: 'block', sm: 'none' },
|
||||
'* .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth }
|
||||
}}
|
||||
>
|
||||
{drawer}
|
||||
</Drawer>
|
||||
<Drawer
|
||||
variant='permanent'
|
||||
sx={{
|
||||
display: { xs: 'none', sm: 'block' },
|
||||
'* .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth }
|
||||
}}
|
||||
open
|
||||
>
|
||||
{drawer}
|
||||
</Drawer>
|
||||
</Box>
|
||||
<Box component='main' sx={{ flexGrow: 1, p: 3, mt: 8 }}>
|
||||
{user && noPaper ? children : (
|
||||
<Paper elevation={0} sx={{ p: 2 }} variant='outlined'>
|
||||
{children}
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</div>
|
||||
</Header>
|
||||
}
|
||||
>
|
||||
<Paper withBorder p='md' shadow='xs'>{children}</Paper>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,75 +1,3 @@
|
||||
/// https://github.com/mui-org/material-ui/blob/next/examples/nextjs/src/Link.js
|
||||
/* eslint-disable jsx-a11y/anchor-has-content */
|
||||
import React, { forwardRef } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useRouter } from 'next/router';
|
||||
import NextLink from 'next/link';
|
||||
import MuiLink from '@material-ui/core/Link';
|
||||
|
||||
export const NextLinkComposed = forwardRef(function NextLinkComposed(props: any, ref) {
|
||||
const { to, linkAs, href, replace, scroll, passHref, shallow, prefetch, locale, ...other } =
|
||||
props;
|
||||
|
||||
return (
|
||||
<NextLink
|
||||
href={to}
|
||||
prefetch={prefetch}
|
||||
as={linkAs}
|
||||
replace={replace}
|
||||
scroll={scroll}
|
||||
shallow={shallow}
|
||||
passHref={passHref}
|
||||
locale={locale}
|
||||
>
|
||||
<a ref={ref} {...other} />
|
||||
</NextLink>
|
||||
);
|
||||
});
|
||||
|
||||
// A styled version of the Next.js Link component:
|
||||
// https://nextjs.org/docs/#with-link
|
||||
const Link = forwardRef(function Link(props: any, ref) {
|
||||
const {
|
||||
activeClassName = 'active',
|
||||
as: linkAs,
|
||||
className: classNameProps,
|
||||
href,
|
||||
noLinkStyle,
|
||||
role, // Link don't have roles.
|
||||
...other
|
||||
} = props;
|
||||
|
||||
const router = useRouter();
|
||||
const pathname = typeof href === 'string' ? href : href.pathname;
|
||||
const className = clsx(classNameProps, {
|
||||
[activeClassName]: router.pathname === pathname && activeClassName,
|
||||
});
|
||||
|
||||
const isExternal =
|
||||
typeof href === 'string' && (href.indexOf('http') === 0 || href.indexOf('mailto:') === 0);
|
||||
|
||||
if (isExternal) {
|
||||
if (noLinkStyle) {
|
||||
return <a className={className} href={href} ref={ref} {...other} />;
|
||||
}
|
||||
|
||||
return <MuiLink className={className} href={href} ref={ref} {...other} />;
|
||||
}
|
||||
|
||||
if (noLinkStyle) {
|
||||
return <NextLinkComposed className={className} ref={ref} to={href} {...other} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<MuiLink
|
||||
component={NextLinkComposed}
|
||||
linkAs={linkAs}
|
||||
className={className}
|
||||
ref={ref}
|
||||
to={href}
|
||||
{...other}
|
||||
/>
|
||||
);
|
||||
});
|
||||
import { NextLink as Link } from '@mantine/next';
|
||||
|
||||
export default Link;
|
||||
5
src/components/MutedText.tsx
Normal file
5
src/components/MutedText.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Text } from '@mantine/core';
|
||||
|
||||
export default function MutedText({ children, ...props }) {
|
||||
return <Text color='gray' size='xl' {...props}>{children}</Text>;
|
||||
}
|
||||
75
src/components/PasswordStrength.tsx
Normal file
75
src/components/PasswordStrength.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
// https://mantine.dev/core/password-input/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { PasswordInput, Progress, Text, Popover, Box } from '@mantine/core';
|
||||
import { CheckIcon, CrossIcon } from './icons';
|
||||
|
||||
function PasswordRequirement({ meets, label }: { meets: boolean; label: string }) {
|
||||
return (
|
||||
<Text
|
||||
color={meets ? 'teal' : 'red'}
|
||||
sx={{ display: 'flex', alignItems: 'center' }}
|
||||
mt='sm'
|
||||
size='sm'
|
||||
>
|
||||
{meets ? <CheckIcon /> : <CrossIcon />} <Box ml='md'>{label}</Box>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const requirements = [
|
||||
{ re: /[0-9]/, label: 'Includes number' },
|
||||
{ re: /[a-z]/, label: 'Includes lowercase letter' },
|
||||
{ re: /[A-Z]/, label: 'Includes uppercase letter' },
|
||||
{ re: /[$&+,:;=?@#|'<>.^*()%!-]/, label: 'Includes special symbol' },
|
||||
];
|
||||
|
||||
function getStrength(password: string) {
|
||||
let multiplier = password.length > 7 ? 0 : 1;
|
||||
|
||||
requirements.forEach((requirement) => {
|
||||
if (!requirement.re.test(password)) {
|
||||
multiplier += 1;
|
||||
}
|
||||
});
|
||||
|
||||
return Math.max(100 - (100 / (requirements.length + 1)) * multiplier, 10);
|
||||
}
|
||||
|
||||
export default function PasswordStrength({ value, setValue, setStrength, ...props }) {
|
||||
const [popoverOpened, setPopoverOpened] = useState(false);
|
||||
const checks = requirements.map((requirement, index) => (
|
||||
<PasswordRequirement key={index} label={requirement.label} meets={requirement.re.test(value)} />
|
||||
));
|
||||
|
||||
const strength = getStrength(value);
|
||||
setStrength(strength);
|
||||
const color = strength === 100 ? 'teal' : strength > 50 ? 'yellow' : 'red';
|
||||
|
||||
return (
|
||||
<Popover
|
||||
opened={popoverOpened}
|
||||
position='bottom'
|
||||
placement='start'
|
||||
withArrow
|
||||
trapFocus={false}
|
||||
transition='pop-top-left'
|
||||
onFocusCapture={() => setPopoverOpened(true)}
|
||||
onBlurCapture={() => setPopoverOpened(false)}
|
||||
styles={{ root: { width: '100%' } }}
|
||||
target={
|
||||
<PasswordInput
|
||||
label='Password'
|
||||
description='Strong password should include letters in lower and uppercase, at least 1 number, at least 1 special symbol'
|
||||
value={value}
|
||||
onChange={(event) => setValue(event.currentTarget.value)}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Progress color={color} value={strength} size={7} mb='md' />
|
||||
<PasswordRequirement label='Includes at least 8 characters' meets={value.length > 7} />
|
||||
{checks}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
29
src/components/SmallTable.tsx
Normal file
29
src/components/SmallTable.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Box, Table } from '@mantine/core';
|
||||
import { randomId } from '@mantine/hooks';
|
||||
|
||||
export function SmallTable({ rows, columns }) {
|
||||
return (
|
||||
<Box sx={{ pt: 1 }}>
|
||||
<Table highlightOnHover>
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(col => (
|
||||
<th key={randomId()}>{col.name}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map(row => (
|
||||
<tr key={randomId()}>
|
||||
{columns.map(col => (
|
||||
<td key={randomId()}>
|
||||
{col.format ? col.format(row[col.id]) : row[col.id]}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,65 +1,96 @@
|
||||
import React from 'react';
|
||||
import { ThemeProvider } from '@emotion/react';
|
||||
import { CssBaseline } from '@material-ui/core';
|
||||
import dark_blue from 'lib/themes/dark_blue';
|
||||
import dark from 'lib/themes/dark';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
// themes
|
||||
import ayu_dark from 'lib/themes/ayu_dark';
|
||||
import ayu_mirage from 'lib/themes/ayu_mirage';
|
||||
import ayu_light from 'lib/themes/ayu_light';
|
||||
import ayu_mirage from 'lib/themes/ayu_mirage';
|
||||
import dark from 'lib/themes/dark';
|
||||
import dark_blue from 'lib/themes/dark_blue';
|
||||
import dracula from 'lib/themes/dracula';
|
||||
import light_blue from 'lib/themes/light_blue';
|
||||
import matcha_dark_azul from 'lib/themes/matcha_dark_azul';
|
||||
import nord from 'lib/themes/nord';
|
||||
import polar from 'lib/themes/polar';
|
||||
import qogir_dark from 'lib/themes/qogir_dark';
|
||||
|
||||
import { MantineProvider, MantineThemeOverride } from '@mantine/core';
|
||||
import { useColorScheme } from '@mantine/hooks';
|
||||
import { ModalsProvider } from '@mantine/modals';
|
||||
import { NotificationsProvider } from '@mantine/notifications';
|
||||
import { useStoreSelector } from 'lib/redux/store';
|
||||
import createTheme from 'lib/themes';
|
||||
|
||||
export const themes = {
|
||||
'dark_blue': dark_blue,
|
||||
'dark': dark,
|
||||
'ayu_dark': ayu_dark,
|
||||
'ayu_mirage': ayu_mirage,
|
||||
'ayu_light': ayu_light,
|
||||
'nord': nord,
|
||||
'polar': polar
|
||||
system: (colorScheme: 'dark' | 'light') => colorScheme === 'dark' ? dark_blue : light_blue,
|
||||
dark_blue,
|
||||
light_blue,
|
||||
dark,
|
||||
ayu_dark,
|
||||
ayu_mirage,
|
||||
ayu_light,
|
||||
nord,
|
||||
dracula,
|
||||
matcha_dark_azul,
|
||||
qogir_dark,
|
||||
};
|
||||
|
||||
export const friendlyThemeName = {
|
||||
'system': 'System Theme',
|
||||
'dark_blue': 'Dark Blue',
|
||||
'light_blue': 'Light Blue',
|
||||
'dark': 'Very Dark',
|
||||
'ayu_dark': 'Ayu Dark',
|
||||
'ayu_mirage': 'Ayu Mirage',
|
||||
'ayu_light': 'Ayu Light',
|
||||
'nord': 'Nord',
|
||||
'polar': 'Polar'
|
||||
'dracula': 'Dracula',
|
||||
'matcha_dark_azul': 'Matcha Dark Azul',
|
||||
'qogir_dark': 'Qogir Dark',
|
||||
};
|
||||
|
||||
export default function ZiplineTheming({ Component, pageProps }) {
|
||||
let t;
|
||||
|
||||
export default function ZiplineTheming({ Component, pageProps, ...props }) {
|
||||
const user = useStoreSelector(state => state.user);
|
||||
if (!user) t = themes.dark_blue;
|
||||
else {
|
||||
if (user.customTheme) {
|
||||
t = createTheme({
|
||||
type: 'dark',
|
||||
primary: user.customTheme.primary,
|
||||
secondary: user.customTheme.secondary,
|
||||
error: user.customTheme.error,
|
||||
warning: user.customTheme.warning,
|
||||
info: user.customTheme.info,
|
||||
border: user.customTheme.border,
|
||||
background: {
|
||||
main: user.customTheme.mainBackground,
|
||||
paper: user.customTheme.paperBackground
|
||||
}
|
||||
});
|
||||
} else {
|
||||
t = themes[user.systemTheme] ?? themes.dark_blue;
|
||||
}
|
||||
}
|
||||
const colorScheme = useColorScheme();
|
||||
|
||||
let theme: MantineThemeOverride;
|
||||
|
||||
if (!user) theme = themes.system(colorScheme);
|
||||
else if (user.systemTheme === 'system') theme = themes.system(colorScheme);
|
||||
else theme = themes[user.systemTheme] ?? themes.system(colorScheme);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.style.setProperty('color-scheme', theme.colorScheme);
|
||||
}, [user, theme]);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={t}>
|
||||
<CssBaseline />
|
||||
<Component {...pageProps} />
|
||||
</ThemeProvider>
|
||||
<MantineProvider
|
||||
withGlobalStyles
|
||||
withNormalizeCSS
|
||||
theme={theme}
|
||||
styles={{
|
||||
AppShell: t => ({
|
||||
root: {
|
||||
backgroundColor: t.other.AppShell_backgroundColor,
|
||||
},
|
||||
}),
|
||||
Popover: {
|
||||
inner: {
|
||||
width: 200,
|
||||
},
|
||||
},
|
||||
Accordion: {
|
||||
itemTitle: {
|
||||
border: 0,
|
||||
},
|
||||
itemOpened: {
|
||||
border: 0,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ModalsProvider>
|
||||
<NotificationsProvider>
|
||||
{props.children ? props.children : <Component {...pageProps} />}
|
||||
</NotificationsProvider>
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
);
|
||||
}
|
||||
46
src/components/Type.tsx
Normal file
46
src/components/Type.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Group, Image, Stack, Text } from '@mantine/core';
|
||||
import { Prism } from '@mantine/prism';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AudioIcon, FileIcon, PlayIcon, TypeIcon, VideoIcon } from './icons';
|
||||
import MutedText from './MutedText';
|
||||
|
||||
function Placeholder({ text, Icon, ...props }) {
|
||||
return (
|
||||
<Image height={200} withPlaceholder placeholder={
|
||||
<Group>
|
||||
<Icon size={48} />
|
||||
<Text>{text}</Text>
|
||||
</Group>
|
||||
} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export default function Type({ file, popup = false, ...props }){
|
||||
const type = (file.type || file.mimetype).split('/')[0];
|
||||
const name = (file.name || file.file);
|
||||
|
||||
const [text, setText] = useState('');
|
||||
|
||||
if (type === 'text') {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const res = await fetch('/r/' + name);
|
||||
const text = await res.text();
|
||||
|
||||
setText(text);
|
||||
})();
|
||||
}, []);
|
||||
}
|
||||
|
||||
return popup ? {
|
||||
'video': <video width='100%' autoPlay controls {...props} />,
|
||||
'image': <Image {...props} />,
|
||||
'audio': <audio autoPlay controls {...props} style={{ width: '100%' }}/>,
|
||||
'text': <Prism withLineNumbers language={name.split('.').pop()} {...props} style={{}} sx={{}}>{text}</Prism>,
|
||||
}[type] : {
|
||||
'video': <Placeholder Icon={PlayIcon} text={`Click to view video (${name})`} {...props} />,
|
||||
'image': <Image {...props} />,
|
||||
'audio': <Placeholder Icon={AudioIcon} text={`Click to view audio (${name})`} {...props}/>,
|
||||
'text': <Placeholder Icon={FileIcon} text={`Click to view text file (${name})`} {...props}/>,
|
||||
}[type];
|
||||
};
|
||||
52
src/components/dropzone/Dropzone.tsx
Normal file
52
src/components/dropzone/Dropzone.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import { Dropzone as MantineDropzone } from '@mantine/dropzone';
|
||||
import { Group, Text, useMantineTheme } from '@mantine/core';
|
||||
import { CrossIcon, UploadIcon, ImageIcon } from 'components/icons';
|
||||
|
||||
function ImageUploadIcon({ status, ...props }) {
|
||||
if (status.accepted) {
|
||||
return <UploadIcon {...props} />;
|
||||
}
|
||||
|
||||
if (status.rejected) {
|
||||
return <CrossIcon {...props} />;
|
||||
}
|
||||
|
||||
return <ImageIcon {...props} />;
|
||||
}
|
||||
|
||||
function getIconColor(status, theme) {
|
||||
return status.accepted
|
||||
? theme.colors[theme.primaryColor][6]
|
||||
: status.rejected
|
||||
? theme.colors.red[6]
|
||||
: theme.colorScheme === 'dark'
|
||||
? theme.colors.dark[0]
|
||||
: theme.black;
|
||||
}
|
||||
|
||||
|
||||
export default function Dropzone({ loading, onDrop, children }) {
|
||||
const theme = useMantineTheme();
|
||||
|
||||
return (
|
||||
<MantineDropzone loading={loading} onDrop={onDrop}>
|
||||
{status => (
|
||||
<>
|
||||
<Group position='center' spacing='xl' style={{ minHeight: 440, pointerEvents: 'none' }}>
|
||||
<ImageUploadIcon
|
||||
status={status}
|
||||
style={{ width: 80, height: 80, color: getIconColor(status, theme) }}
|
||||
/>
|
||||
|
||||
<Text size='xl' inline>
|
||||
Drag images here or click to select files
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
{children}
|
||||
</>
|
||||
)}
|
||||
</MantineDropzone>
|
||||
);
|
||||
}
|
||||
54
src/components/dropzone/DropzoneFile.tsx
Normal file
54
src/components/dropzone/DropzoneFile.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { Table, Tooltip, Badge, useMantineTheme } from '@mantine/core';
|
||||
import Type from 'components/Type';
|
||||
|
||||
export function FilePreview({ file }: { file: File }) {
|
||||
return (
|
||||
<Type
|
||||
file={file}
|
||||
autoPlay
|
||||
sx={{ maxWidth: '10vw', maxHeight: '100vh' }}
|
||||
style={{ maxWidth: '10vw', maxHeight: '100vh' }}
|
||||
src={URL.createObjectURL(file)}
|
||||
alt={file.name}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FileDropzone({ file }: { file: File }) {
|
||||
const theme = useMantineTheme();
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
position='top'
|
||||
placement='center'
|
||||
allowPointerEvents
|
||||
label={
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<FilePreview file={file} />
|
||||
|
||||
<Table sx={{ color: theme.colorScheme === 'dark' ? 'black' : 'white' }} ml='md'>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{file.name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{file.type}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Last Modified</td>
|
||||
<td>{new Date(file.lastModified).toLocaleString()}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Badge size='lg'>
|
||||
{file.name}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
5
src/components/icons/ActivityIcon.tsx
Normal file
5
src/components/icons/ActivityIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Activity } from 'react-feather';
|
||||
|
||||
export default function ActivityIcon({ ...props }) {
|
||||
return <Activity size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/AudioIcon.tsx
Normal file
5
src/components/icons/AudioIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Disc } from 'react-feather';
|
||||
|
||||
export default function AudioIcon({ ...props }) {
|
||||
return <Disc size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/CalendarIcon.tsx
Normal file
5
src/components/icons/CalendarIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Calendar } from 'react-feather';
|
||||
|
||||
export default function CalendarIcon({ ...props }) {
|
||||
return <Calendar size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/CheckIcon.tsx
Normal file
5
src/components/icons/CheckIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Check } from 'react-feather';
|
||||
|
||||
export default function CheckIcon({ ...props }) {
|
||||
return <Check size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/CopyIcon.tsx
Normal file
5
src/components/icons/CopyIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Copy } from 'react-feather';
|
||||
|
||||
export default function CopyIcon({ ...props }) {
|
||||
return <Copy size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/CrossIcon.tsx
Normal file
5
src/components/icons/CrossIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { X } from 'react-feather';
|
||||
|
||||
export default function CrossIcon({ ...props }) {
|
||||
return <X size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/DeleteIcon.tsx
Normal file
5
src/components/icons/DeleteIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Delete } from 'react-feather';
|
||||
|
||||
export default function DeleteIcon({ ...props }) {
|
||||
return <Delete size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/DownloadIcon.tsx
Normal file
5
src/components/icons/DownloadIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Download } from 'react-feather';
|
||||
|
||||
export default function DownloadIcon({ ...props }) {
|
||||
return <Download size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/EnterIcon.tsx
Normal file
5
src/components/icons/EnterIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { LogIn } from 'react-feather';
|
||||
|
||||
export default function EnterIcon({ ...props }) {
|
||||
return <LogIn size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/FileIcon.tsx
Normal file
5
src/components/icons/FileIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { File } from 'react-feather';
|
||||
|
||||
export default function FileIcon({ ...props }) {
|
||||
return <File size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/HashIcon.tsx
Normal file
5
src/components/icons/HashIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Hash } from 'react-feather';
|
||||
|
||||
export default function HashIcon({ ...props }) {
|
||||
return <Hash size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/HomeIcon.tsx
Normal file
5
src/components/icons/HomeIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Home } from 'react-feather';
|
||||
|
||||
export default function HomeIcon({ ...props }) {
|
||||
return <Home size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/ImageIcon.tsx
Normal file
5
src/components/icons/ImageIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Image as FeatherImage } from 'react-feather';
|
||||
|
||||
export default function ImageIcon({ ...props }) {
|
||||
return <FeatherImage size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/LinkIcon.tsx
Normal file
5
src/components/icons/LinkIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Link } from 'react-feather';
|
||||
|
||||
export default function LinkIcon({ ...props }) {
|
||||
return <Link size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/LogoutIcon.tsx
Normal file
5
src/components/icons/LogoutIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { LogOut } from 'react-feather';
|
||||
|
||||
export default function LogoutIcon({ ...props }) {
|
||||
return <LogOut size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/PencilIcon.tsx
Normal file
5
src/components/icons/PencilIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Edit2 } from 'react-feather';
|
||||
|
||||
export default function PencilIcon({ ...props }) {
|
||||
return <Edit2 size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/PlayIcon.tsx
Normal file
5
src/components/icons/PlayIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Play } from 'react-feather';
|
||||
|
||||
export default function PlayIcon({ ...props }) {
|
||||
return <Play size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/PlusIcon.tsx
Normal file
5
src/components/icons/PlusIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Plus } from 'react-feather';
|
||||
|
||||
export default function PlusIcon({ ...props }) {
|
||||
return <Plus size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/SettingsIcon.tsx
Normal file
5
src/components/icons/SettingsIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Settings } from 'react-feather';
|
||||
|
||||
export default function SettingsIcon({ ...props }) {
|
||||
return <Settings size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/StarIcon.tsx
Normal file
5
src/components/icons/StarIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Star } from 'react-feather';
|
||||
|
||||
export default function StarIcon({ ...props }) {
|
||||
return <Star size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/TagIcon.tsx
Normal file
5
src/components/icons/TagIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Tag } from 'react-feather';
|
||||
|
||||
export default function TagIcon({ ...props }) {
|
||||
return <Tag size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/TypeIcon.tsx
Normal file
5
src/components/icons/TypeIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Type } from 'react-feather';
|
||||
|
||||
export default function TypeIcon({ ...props }) {
|
||||
return <Type size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/UploadIcon.tsx
Normal file
5
src/components/icons/UploadIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Upload } from 'react-feather';
|
||||
|
||||
export default function UploadIcon({ ...props }) {
|
||||
return <Upload size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/UserIcon.tsx
Normal file
5
src/components/icons/UserIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { User } from 'react-feather';
|
||||
|
||||
export default function UserIcon({ ...props }) {
|
||||
return <User size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/VideoIcon.tsx
Normal file
5
src/components/icons/VideoIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Video } from 'react-feather';
|
||||
|
||||
export default function VideoIcon({ ...props }) {
|
||||
return <Video size={15} {...props} />;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user