mirror of
https://github.com/diced/zipline.git
synced 2025-12-05 20:40:12 -08:00
Compare commits
177 Commits
feature/ma
...
v3.6.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1f281d8b4 | ||
|
|
d2f3999cf1 | ||
|
|
87fc9f2fb9 | ||
|
|
8c9064fd93 | ||
|
|
561849ae5b | ||
|
|
0847802ce4 | ||
|
|
d5a8b3f1fb | ||
|
|
e6cebd8c46 | ||
|
|
f2be036bac | ||
|
|
f14448d40d | ||
|
|
bf719808f2 | ||
|
|
9dd82c91d7 | ||
|
|
535f84064a | ||
|
|
0c0a55d766 | ||
|
|
6e3ee29eb4 | ||
|
|
6a7a5dc7a3 | ||
|
|
e78d2d79d0 | ||
|
|
451027eaf3 | ||
|
|
e4491610fb | ||
|
|
f30e10f235 | ||
|
|
f9249b1380 | ||
|
|
3df94526b0 | ||
|
|
b30b7b1bd3 | ||
|
|
a9defd67d6 | ||
|
|
68d346e69d | ||
|
|
e2fd27cbba | ||
|
|
4c0532006c | ||
|
|
7ac574b230 | ||
|
|
7eb855de8f | ||
|
|
d5984f4141 | ||
|
|
b7c0c85639 | ||
|
|
84ba166aea | ||
|
|
bd79858681 | ||
|
|
0f10fa3991 | ||
|
|
74b1799d21 | ||
|
|
4552643ff8 | ||
|
|
d432b388f6 | ||
|
|
a8475602c7 | ||
|
|
f58d33af9e | ||
|
|
0150ea5e70 | ||
|
|
3bf43f1606 | ||
|
|
b8729a6ec7 | ||
|
|
1f44aa7e85 | ||
|
|
2bd5352fc5 | ||
|
|
a90130e8bf | ||
|
|
642e8796f0 | ||
|
|
615cbddc89 | ||
|
|
4ef82bdff4 | ||
|
|
dafde04c2c | ||
|
|
1be61b8d89 | ||
|
|
c3215c7425 | ||
|
|
af0cd26ea0 | ||
|
|
cb7dacd089 | ||
|
|
8c04971094 | ||
|
|
3a4802f09a | ||
|
|
d78db306c5 | ||
|
|
3f8790ece1 | ||
|
|
f9e6158144 | ||
|
|
05de3fed15 | ||
|
|
38cba9cb39 | ||
|
|
a4af980e11 | ||
|
|
940b844857 | ||
|
|
41b766216e | ||
|
|
402987baba | ||
|
|
3cb08c73d3 | ||
|
|
4cb92a7257 | ||
|
|
a095768eae | ||
|
|
1a5925d7e8 | ||
|
|
9147847710 | ||
|
|
05fe8bcaca | ||
|
|
b0c3c6f45a | ||
|
|
0f641aa852 | ||
|
|
2651bbe50c | ||
|
|
d31371eb6c | ||
|
|
ec0e7e5ec7 | ||
|
|
feb75a8a42 | ||
|
|
d4369d2503 | ||
|
|
d236589644 | ||
|
|
8044b7f623 | ||
|
|
9f0697dd34 | ||
|
|
78a6f3122d | ||
|
|
b460da74dd | ||
|
|
75a8bb7962 | ||
|
|
9ac876e30a | ||
|
|
26cb4ea034 | ||
|
|
0d65ee1a32 | ||
|
|
4a753376b7 | ||
|
|
dc926e9f5a | ||
|
|
722372c7f6 | ||
|
|
4589c6ee0a | ||
|
|
67ff93e640 | ||
|
|
bd055d704b | ||
|
|
2e8bee931c | ||
|
|
a454a4f4a8 | ||
|
|
45541a3cdd | ||
|
|
1d42d922bd | ||
|
|
4f631fbd0e | ||
|
|
e911db4c1a | ||
|
|
9b60147e11 | ||
|
|
acd0cabdff | ||
|
|
d41f6058f7 | ||
|
|
8f835eec4e | ||
|
|
ecab525ffd | ||
|
|
7c887e8ec1 | ||
|
|
f3a23a528b | ||
|
|
cdcb31130b | ||
|
|
3ea24ddf0c | ||
|
|
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 |
@@ -2,3 +2,6 @@ node_modules/
|
||||
.next/
|
||||
uploads/
|
||||
.git/
|
||||
.yarn/*
|
||||
!.yarn/releases
|
||||
!.yarn/plugins
|
||||
|
||||
47
.env.local.example
Normal file
47
.env.local.example
Normal file
@@ -0,0 +1,47 @@
|
||||
# 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
|
||||
DATASOURCE_S3_USE_SSL=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
|
||||
@@ -1,12 +1,18 @@
|
||||
{
|
||||
"extends": ["next", "next/core-web-vitals"],
|
||||
"extends": ["next", "next/core-web-vitals", "plugin:prettier/recommended"],
|
||||
"rules": {
|
||||
"indent": ["error", 2],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": ["error", "single"],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single",
|
||||
{
|
||||
"avoidEscape": true
|
||||
}
|
||||
],
|
||||
"semi": ["error", "always"],
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"jsx-quotes": ["error", "prefer-single"],
|
||||
"indent": "off",
|
||||
"react/prop-types": "off",
|
||||
"react-hooks/rules-of-hooks": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
@@ -17,9 +23,11 @@
|
||||
"react/no-direct-mutation-state": "warn",
|
||||
"react/no-is-mounted": "warn",
|
||||
"react/no-typos": "error",
|
||||
"react/react-in-jsx-scope": "error",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/require-render-return": "error",
|
||||
"react/style-prop-object": "warn",
|
||||
"@next/next/no-img-element": "off"
|
||||
"@next/next/no-img-element": "off",
|
||||
"jsx-a11y/alt-text": "off",
|
||||
"react/display-name": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
.gitattributes
vendored
Normal file
14
.gitattributes
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Set the default behavior, in case people don't have core.autocrlf set.
|
||||
* text eol=lf
|
||||
|
||||
# Explicitly declare text files you want to always be normalized and converted
|
||||
# to native line endings on checkout.
|
||||
*.c text
|
||||
*.h text
|
||||
|
||||
# Declare files that will always have CRLF line endings on checkout.
|
||||
*.sln text eol=crlf
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
45
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
45
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: Bug
|
||||
description: File a bug report
|
||||
title: 'Bug: '
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Provide steps to reproduce the bug, and some context.
|
||||
value: 'A bug happened!'
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of Zipline are you using?
|
||||
options:
|
||||
- upstream
|
||||
- latest
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: What browser(s) are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Firefox
|
||||
- Chrome
|
||||
- Safari
|
||||
- Microsoft Edge
|
||||
- type: textarea
|
||||
id: zipline-logs
|
||||
attributes:
|
||||
label: Zipline Logs
|
||||
description: Please copy and paste any relevant log output.
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: browser-logs
|
||||
attributes:
|
||||
label: Browser Logs
|
||||
description: Please copy and paste any relevant log output.
|
||||
render: shell
|
||||
12
.github/ISSUE_TEMPLATE/suggest.yml
vendored
Normal file
12
.github/ISSUE_TEMPLATE/suggest.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
name: Suggestion
|
||||
description: Suggest a feature to be added
|
||||
title: 'Suggestion: '
|
||||
labels: ['suggestion']
|
||||
body:
|
||||
- type: textarea
|
||||
id: suggest
|
||||
attributes:
|
||||
label: Suggestion
|
||||
description: Be as descriptive as possible!
|
||||
placeholder: What do you want in Zipline?
|
||||
value: A suggestion
|
||||
25
.github/workflows/build.yml
vendored
25
.github/workflows/build.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: 'CI: Build'
|
||||
name: 'Build'
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -11,23 +11,28 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.x'
|
||||
node-version: '18.x'
|
||||
|
||||
- name: 'Restore dependency cache'
|
||||
id: cache-restore
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
|
||||
path: |
|
||||
node_modules
|
||||
${{ github.workspace }}/.next/cache
|
||||
key: ${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}-
|
||||
|
||||
- name: Create mock config
|
||||
run: echo -e "[core]\nsecret = '12345678'\ndatabase_url = 'postgres://postgres:postgres@postgres/postgres'\n[uploader]\nroute = '/u'\ndirectory = './uploads'\n[urls]\nroute = '/go'" > config.toml
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.cache-restore.outputs.cache-hit != 'true'
|
||||
run: yarn install
|
||||
|
||||
- name: Build
|
||||
run: yarn build
|
||||
run: yarn build
|
||||
env:
|
||||
ZIPLINE_DOCKER_BUILD: true
|
||||
51
.github/workflows/docker-release.yml
vendored
Normal file
51
.github/workflows/docker-release.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
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@v3
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: |
|
||||
echo "zipline_version=$(jq .version package.json -r)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to Github Packages
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
ghcr.io/diced/zipline:latest
|
||||
ghcr.io/diced/zipline:${{ steps.version.outputs.zipline_version }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
49
.github/workflows/docker.yml
vendored
49
.github/workflows/docker.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: 'CD: Push Docker Images'
|
||||
name: 'Push Docker Images'
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
- 'server/**'
|
||||
- 'prisma/**'
|
||||
- '.github/**'
|
||||
- 'Dockerfile'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -16,30 +17,34 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Push to GitHub Packages
|
||||
uses: docker/build-push-action@v1
|
||||
- name: Get version
|
||||
id: version
|
||||
run: |
|
||||
echo "zipline_version=$(jq .version package.json -r)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to Github Packages
|
||||
uses: docker/login-action@v2
|
||||
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@v3
|
||||
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
|
||||
ghcr.io/diced/zipline:trunk-${{ steps.version.outputs.zipline_version }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -5,6 +5,11 @@
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# yarn
|
||||
.yarn/*
|
||||
!.yarn/releases
|
||||
!.yarn/plugins
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
@@ -36,4 +41,5 @@ yarn-error.log*
|
||||
|
||||
# zipline
|
||||
config.toml
|
||||
uploads/
|
||||
uploads/
|
||||
dist/
|
||||
5
.prettierrc.json
Normal file
5
.prettierrc.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 110
|
||||
}
|
||||
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"editor.tabSize": 2,
|
||||
"files.eol": "\n",
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
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
801
.yarn/releases/yarn-3.2.4.cjs
vendored
Executable file
801
.yarn/releases/yarn-3.2.4.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
9
.yarnrc.yml
Normal file
9
.yarnrc.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
checksumBehavior: update
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||
spec: "@yarnpkg/plugin-interactive-tools"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.2.4.cjs
|
||||
23
CONTRIBUTING.md
Normal file
23
CONTRIBUTING.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# 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.
|
||||
Please make sure your code also reflects the style of the rest of the code.
|
||||
55
Dockerfile
55
Dockerfile
@@ -1,46 +1,63 @@
|
||||
FROM node:16-alpine AS deps
|
||||
FROM ghcr.io/diced/prisma-binaries:4.5.x as prisma
|
||||
|
||||
FROM node:alpine3.16 AS deps
|
||||
RUN mkdir -p /prisma-engines
|
||||
WORKDIR /build
|
||||
|
||||
COPY package.json yarn.lock ./
|
||||
COPY .yarn .yarn
|
||||
COPY package.json yarn.lock .yarnrc.yml ./
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN yarn install --frozen-lockfile
|
||||
RUN yarn install --immutable
|
||||
|
||||
FROM node:16-alpine AS builder
|
||||
FROM node:alpine3.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 openssl openssl-dev
|
||||
|
||||
COPY --from=deps /build/node_modules ./node_modules
|
||||
COPY src ./src
|
||||
COPY server ./server
|
||||
COPY scripts ./scripts
|
||||
COPY prisma ./prisma
|
||||
COPY package.json yarn.lock next.config.js next-env.d.ts zip-env.d.ts tsconfig.json ./
|
||||
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 ./
|
||||
|
||||
ENV ZIPLINE_DOCKER_BUILD 1
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN yarn build
|
||||
|
||||
FROM node:16-alpine AS runner
|
||||
FROM node:alpine3.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 openssl openssl-dev
|
||||
|
||||
ENV NODE_ENV production
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN addgroup --system --gid 1001 zipline
|
||||
RUN adduser --system --uid 1001 zipline
|
||||
|
||||
COPY --from=builder --chown=zipline:zipline /build/.next ./.next
|
||||
COPY --from=builder --chown=zipline:zipline /build/node_modules ./node_modules
|
||||
COPY --from=builder /build/.next ./.next
|
||||
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/dist ./dist
|
||||
COPY --from=builder /build/prisma ./prisma
|
||||
COPY --from=builder /build/tsconfig.json ./tsconfig.json
|
||||
COPY --from=builder /build/package.json ./package.json
|
||||
COPY --from=builder /build/mimes.json ./mimes.json
|
||||
|
||||
USER zipline
|
||||
|
||||
CMD ["node", "server"]
|
||||
CMD ["node", "--enable-source-maps", "dist/server"]
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 dicedtomato
|
||||
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
|
||||
|
||||
145
README.md
145
README.md
@@ -1,34 +1,147 @@
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/diced/zipline/trunk/public/zipline_small.png"/>
|
||||
|
||||
Zipline is a ShareX/file upload server that is easy to use, packed with features and can be setup in one command!
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
[](https://discord.gg/EAhCRfGxCF)
|
||||
|
||||
|
||||
A ShareX/file upload server that is easy to use, packed with features, and with an easy setup!
|
||||
|
||||

|
||||

|
||||

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

|
||||
[](https://github.com/diced/zipline/pkgs/container/zipline/?tag=trunk)
|
||||
[](https://github.com/diced/zipline/pkgs/container/zipline/?tag=latest)
|
||||
|
||||
</div>
|
||||
|
||||
## Features
|
||||
|
||||
- Configurable
|
||||
- Fast
|
||||
- Built with Next.js & React
|
||||
- Token protected uploading
|
||||
- Image uploading
|
||||
- Image compression
|
||||
- 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
|
||||
- Easy setup instructions on [docs](https://zipline.diced.tech/) (One command install `docker-compose up -d`)
|
||||
- Code highlighting
|
||||
- Fully customizable Discord webhook notifications
|
||||
- OAuth2 registration (Discord and GitHub)
|
||||
- User invites
|
||||
- File Chunking (for large files)
|
||||
- File deletion once it reaches a certain amount of views
|
||||
- Easy setup instructions on [docs](https://zipl.vercel.app/) (One command install `docker-compose up -d`)
|
||||
|
||||
## Installing
|
||||
[See how to install here](https://zipline.diced.tech/docs/get-started)
|
||||
<details>
|
||||
<summary><h2>Screenshots (click)</h2></summary>
|
||||
|
||||
View full album at [imgur](https://imgur.com/a/GzyowZ7)
|
||||
|
||||

|
||||

|
||||

|
||||
</details>
|
||||
|
||||
## Configuration
|
||||
[See how to configure here](https://zipline.diced.tech/docs/config/overview)
|
||||
# Usage
|
||||
|
||||
## Theming
|
||||
[See how to theme here](https://zipline.diced.tech/docs/themes/reference)
|
||||
## 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/guides/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).
|
||||
|
||||
You can either use the script below, or generate one directly from Zipline (just like how you can generate a ShareX config).
|
||||
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.
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 3.2.x | :white_check_mark: |
|
||||
| 3.4.8 | :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,19 +0,0 @@
|
||||
[core]
|
||||
secure = true
|
||||
secret = 'some secret'
|
||||
host = '0.0.0.0'
|
||||
port = 3000
|
||||
database_url = 'postgres://postgres:postgres@postgres/postgres'
|
||||
|
||||
[urls]
|
||||
route = '/go'
|
||||
length = 6
|
||||
|
||||
[uploader]
|
||||
route = '/u'
|
||||
embed_route = '/a'
|
||||
length = 6
|
||||
directory = './uploads'
|
||||
user_limit = 104900000 # 100mb
|
||||
admin_limit = 104900000 # 100mb
|
||||
disabled_extentions = ['jpg']
|
||||
34
docker-compose.dev.yml
Normal file
34
docker-compose.dev.yml
Normal file
@@ -0,0 +1,34 @@
|
||||
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
|
||||
env_file:
|
||||
- .env.local
|
||||
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,30 +16,22 @@ services:
|
||||
retries: 5
|
||||
|
||||
zipline:
|
||||
image: ghcr.io/diced/zipline/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_URL=postgresql://postgres:postgres@postgres/postgres/
|
||||
- UPLOADER_ROUTE=/u
|
||||
- UPLOADER_EMBED_ROUTE=/a
|
||||
- UPLOADER_LENGTH=6
|
||||
- UPLOADER_DIRECTORY=./uploads
|
||||
- UPLOADER_ADMIN_LIMIT=104900000
|
||||
- UPLOADER_USER_LIMIT=104900000
|
||||
- UPLOADER_DISABLED_EXTS=
|
||||
- URLS_ROUTE=/go
|
||||
- URLS_LENGTH=6
|
||||
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'
|
||||
- './uploads:/zipline/uploads'
|
||||
- '$PWD/public:/zipline/public'
|
||||
depends_on:
|
||||
- 'postgres'
|
||||
|
||||
volumes:
|
||||
pg_data:
|
||||
pg_data:
|
||||
|
||||
23
esbuild.config.js
Normal file
23
esbuild.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const esbuild = require('esbuild');
|
||||
const { existsSync } = require('fs');
|
||||
const { rm } = require('fs/promises');
|
||||
const { recursiveReadDir } = require('next/dist/lib/recursive-readdir');
|
||||
|
||||
(async () => {
|
||||
if (existsSync('./dist')) {
|
||||
await rm('./dist', { recursive: true });
|
||||
}
|
||||
|
||||
const entryPoints = await recursiveReadDir('./src', /.*\.(ts)$/, /(themes|queries|pages)/);
|
||||
|
||||
await esbuild.build({
|
||||
tsconfig: 'tsconfig.json',
|
||||
outdir: 'dist',
|
||||
platform: 'node',
|
||||
entryPoints,
|
||||
format: 'cjs',
|
||||
resolveExtensions: ['.ts', '.js'],
|
||||
write: true,
|
||||
sourcemap: true,
|
||||
});
|
||||
})();
|
||||
1384
mimes.json
Normal file
1384
mimes.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,26 @@
|
||||
/**
|
||||
* @type {import('next').NextConfig}
|
||||
**/
|
||||
module.exports = {
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: '/',
|
||||
destination: '/dashboard',
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
images: {
|
||||
domains: [
|
||||
// For sharex icon in manage user
|
||||
'getsharex.com',
|
||||
// For flameshot icon, and maybe in the future other stuff from github
|
||||
'raw.githubusercontent.com',
|
||||
// Google Icon
|
||||
'madeby.google.com',
|
||||
],
|
||||
},
|
||||
poweredByHeader: false,
|
||||
reactStrictMode: true,
|
||||
};
|
||||
};
|
||||
|
||||
111
package.json
111
package.json
@@ -1,59 +1,86 @@
|
||||
{
|
||||
"name": "zip3",
|
||||
"version": "3.3.2",
|
||||
"name": "zipline",
|
||||
"version": "3.6.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "NODE_ENV=development node server",
|
||||
"build": "npm-run-all build:schema build:next",
|
||||
"dev": "npm-run-all build:server dev:run",
|
||||
"dev:run": "cross-env REACT_EDITOR=code NODE_ENV=development RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED=false node --enable-source-maps dist/server",
|
||||
"build": "npm-run-all build:server build:schema build:next",
|
||||
"build-ci": "cross-env ZIPLINE_DOCKER_BUILD=1 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",
|
||||
"format": "prettier --write ./src/**/*.{ts,tsx} ./*.{md,js,json,yml}",
|
||||
"migrate:dev": "prisma migrate dev --create-only",
|
||||
"start": "node server",
|
||||
"start": "node dist/server",
|
||||
"lint": "next lint",
|
||||
"seed": "ts-node --compiler-options \"{\\\"module\\\":\\\"commonjs\\\"}\" --transpile-only prisma/seed.ts"
|
||||
"docker:run": "docker-compose up -d",
|
||||
"docker:down": "docker-compose down",
|
||||
"docker:build-dev": "docker-compose --file docker-compose.dev.yml up --build",
|
||||
"scripts:read-config": "npm-run-all build:server && node dist/scripts/read-config",
|
||||
"scripts:import-dir": "npm-run-all build:server && node dist/scripts/import-dir"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.4.0",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@iarna/toml": "2.2.5",
|
||||
"@mui/icons-material": "^5.0.0",
|
||||
"@mui/material": "^5.0.2",
|
||||
"@mui/styles": "^5.0.1",
|
||||
"@prisma/client": "^3.9.2",
|
||||
"@prisma/migrate": "^3.9.2",
|
||||
"@prisma/sdk": "^3.9.2",
|
||||
"@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": "^12.1.0",
|
||||
"prisma": "^3.9.2",
|
||||
"react": "17.0.2",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "17.0.2",
|
||||
"react-dropzone": "^11.3.2",
|
||||
"react-redux": "^7.2.4",
|
||||
"redux": "^4.1.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"uuid": "^8.3.2",
|
||||
"yup": "^0.32.9"
|
||||
"@dicedtomato/mantine-data-grid": "0.0.23",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/server": "^11.10.0",
|
||||
"@mantine/core": "^5.6.3",
|
||||
"@mantine/dropzone": "^5.6.3",
|
||||
"@mantine/form": "^5.6.3",
|
||||
"@mantine/hooks": "^5.6.3",
|
||||
"@mantine/modals": "^5.6.3",
|
||||
"@mantine/next": "^5.6.3",
|
||||
"@mantine/notifications": "^5.6.3",
|
||||
"@mantine/nprogress": "^5.6.3",
|
||||
"@mantine/prism": "^5.6.3",
|
||||
"@prisma/client": "^4.5.0",
|
||||
"@prisma/internals": "^4.5.0",
|
||||
"@prisma/migrate": "^4.5.0",
|
||||
"@sapphire/shapeshift": "^3.7.0",
|
||||
"@tanstack/react-query": "^4.13.0",
|
||||
"argon2": "^0.30.1",
|
||||
"chart.js": "^3.9.1",
|
||||
"chartjs-plugin-datalabels": "^2.1.0",
|
||||
"color-hash": "^2.0.1",
|
||||
"colorette": "^2.0.19",
|
||||
"cookie": "^0.5.0",
|
||||
"dayjs": "^1.11.6",
|
||||
"dotenv": "^16.0.3",
|
||||
"dotenv-expand": "^9.0.0",
|
||||
"fflate": "^0.7.4",
|
||||
"find-my-way": "^7.3.1",
|
||||
"minio": "^7.0.32",
|
||||
"ms": "canary",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"next": "^13.0.0",
|
||||
"prisma": "^4.5.0",
|
||||
"react": "^18.2.0",
|
||||
"react-chartjs-2": "^4.3.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-feather": "^2.0.10",
|
||||
"recoil": "^0.7.6",
|
||||
"sharp": "^0.31.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cookie": "^0.4.0",
|
||||
"@types/multer": "^1.4.6",
|
||||
"@types/node": "^15.12.2",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-next": "11.0.0",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/minio": "^7.0.14",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^18.11.7",
|
||||
"@types/react": "^18.0.24",
|
||||
"@types/sharp": "^0.31.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild": "^0.15.12",
|
||||
"eslint": "^8.26.0",
|
||||
"eslint-config-next": "^13.0.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"ts-node": "^10.0.0",
|
||||
"typescript": "^4.3.2"
|
||||
"prettier": "^2.7.1",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/diced/zipline.git"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@3.2.4"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Image" ADD COLUMN "expires_at" TIMESTAMP(3);
|
||||
2
prisma/migrations/20220816212407_avatar/migration.sql
Normal file
2
prisma/migrations/20220816212407_avatar/migration.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "avatar" TEXT;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "oauth" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "oauthProvider" TEXT,
|
||||
ALTER COLUMN "password" DROP NOT NULL;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "oauthAccessToken" TEXT;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "superAdmin" BOOLEAN NOT NULL DEFAULT false;
|
||||
2
prisma/migrations/20221028005627_max_views/migration.sql
Normal file
2
prisma/migrations/20221028005627_max_views/migration.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Image" ADD COLUMN "maxViews" INTEGER;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Url" ADD COLUMN "maxViews" INTEGER;
|
||||
31
prisma/migrations/20221030222208_oauth_reform/migration.sql
Normal file
31
prisma/migrations/20221030222208_oauth_reform/migration.sql
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `oauth` on the `User` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `oauthAccessToken` on the `User` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `oauthProvider` on the `User` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- CreateEnum
|
||||
CREATE TYPE "OauthProviders" AS ENUM ('DISCORD', 'GITHUB');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" DROP COLUMN "oauth",
|
||||
DROP COLUMN "oauthAccessToken",
|
||||
DROP COLUMN "oauthProvider";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "OAuth" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"provider" "OauthProviders" NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "OAuth_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "OAuth_provider_key" ON "OAuth"("provider");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OAuth" ADD CONSTRAINT "OAuth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- DropIndex
|
||||
DROP INDEX "OAuth_provider_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "OAuth" ADD COLUMN "refresh" TEXT;
|
||||
@@ -0,0 +1,8 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Image" DROP CONSTRAINT "Image_userId_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Image" ALTER COLUMN "userId" DROP NOT NULL;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Image" ADD CONSTRAINT "Image_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "OauthProviders" ADD VALUE 'GOOGLE';
|
||||
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `username` to the `OAuth` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "OAuth" ADD COLUMN "username" TEXT NOT NULL;
|
||||
@@ -8,34 +8,23 @@ generator client {
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
id Int @id @default(autoincrement())
|
||||
username String
|
||||
password String
|
||||
password String?
|
||||
avatar String?
|
||||
token String
|
||||
administrator Boolean @default(false)
|
||||
systemTheme String @default("dark_blue")
|
||||
customTheme Theme?
|
||||
administrator Boolean @default(false)
|
||||
superAdmin Boolean @default(false)
|
||||
systemTheme String @default("system")
|
||||
embedTitle String?
|
||||
embedColor String @default("#2f3136")
|
||||
embedSiteName String? @default("{image.file} • {user.name}")
|
||||
ratelimited Boolean @default(false)
|
||||
embedColor String @default("#2f3136")
|
||||
embedSiteName String? @default("{image.file} • {user.name}")
|
||||
ratelimit DateTime?
|
||||
domains String[]
|
||||
oauth OAuth[]
|
||||
images Image[]
|
||||
urls Url[]
|
||||
}
|
||||
|
||||
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
|
||||
Invite Invite[]
|
||||
}
|
||||
|
||||
enum ImageFormat {
|
||||
@@ -50,19 +39,22 @@ model Image {
|
||||
file String
|
||||
mimetype String @default("image/png")
|
||||
created_at DateTime @default(now())
|
||||
expires_at DateTime?
|
||||
maxViews Int?
|
||||
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
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||
userId Int?
|
||||
}
|
||||
|
||||
model InvisibleImage {
|
||||
id Int @id @default(autoincrement())
|
||||
invis String @unique
|
||||
imageId Int
|
||||
imageId Int @unique
|
||||
image Image @relation(fields: [imageId], references: [id])
|
||||
}
|
||||
|
||||
@@ -71,6 +63,7 @@ model Url {
|
||||
destination String
|
||||
vanity String?
|
||||
created_at DateTime @default(now())
|
||||
maxViews Int?
|
||||
views Int @default(0)
|
||||
invisible InvisibleUrl?
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
@@ -80,12 +73,38 @@ model Url {
|
||||
model InvisibleUrl {
|
||||
id Int @id @default(autoincrement())
|
||||
invis String @unique
|
||||
urlId String
|
||||
urlId String @unique
|
||||
url Url @relation(fields: [urlId], references: [id])
|
||||
}
|
||||
|
||||
model Stats {
|
||||
id Int @id @default(autoincrement())
|
||||
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
|
||||
}
|
||||
|
||||
model OAuth {
|
||||
id Int @id @default(autoincrement())
|
||||
provider OauthProviders
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int
|
||||
username String
|
||||
token String
|
||||
refresh String?
|
||||
}
|
||||
|
||||
enum OauthProviders {
|
||||
DISCORD
|
||||
GITHUB
|
||||
GOOGLE
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { hashPassword, createToken } from '../src/lib/util';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
username: 'administrator',
|
||||
password: await hashPassword('password'),
|
||||
token: createToken(),
|
||||
administrator: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`
|
||||
When logging into Zipline for the first time, use these credentials:
|
||||
|
||||
Username: "${user.username}"
|
||||
Password: "password"
|
||||
`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
@@ -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.core.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',
|
||||
};
|
||||
161
server/index.js
161
server/index.js
@@ -1,161 +0,0 @@
|
||||
const next = require('next').default;
|
||||
const defaultConfig = require('next/dist/server/config-shared').defaultConfig;
|
||||
const { createServer } = require('http');
|
||||
const { stat, mkdir } = require('fs/promises');
|
||||
const { extname } = require('path');
|
||||
const validateConfig = require('./validateConfig');
|
||||
const Logger = require('../src/lib/logger');
|
||||
const readConfig = require('../src/lib/readConfig');
|
||||
const mimes = require('../scripts/mimes');
|
||||
const { log, getStats, shouldUseYarn, getFile, migrations } = require('./util');
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const { version } = require('../package.json');
|
||||
const nextConfig = require('../next.config');
|
||||
const serverLog = Logger.get('server');
|
||||
const webLog = Logger.get('web');
|
||||
|
||||
serverLog.info(`starting zipline@${version} server`);
|
||||
|
||||
const dev = process.env.NODE_ENV === 'development';
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await run();
|
||||
} catch (e) {
|
||||
serverLog.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
|
||||
async function run() {
|
||||
const a = readConfig();
|
||||
const config = validateConfig(a);
|
||||
|
||||
process.env.DATABASE_URL = config.core.database_url;
|
||||
await migrations();
|
||||
|
||||
await mkdir(config.uploader.directory, { recursive: true });
|
||||
|
||||
const app = next({
|
||||
dir: '.',
|
||||
dev,
|
||||
quiet: !dev,
|
||||
hostname: config.core.host,
|
||||
port: config.core.port,
|
||||
conf: Object.assign(defaultConfig, nextConfig),
|
||||
});
|
||||
|
||||
await app.prepare();
|
||||
|
||||
const handle = app.getRequestHandler();
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const srv = createServer(async (req, res) => {
|
||||
const parts = req.url.split('/');
|
||||
if (!parts[2] || parts[2] === '') return;
|
||||
|
||||
if (req.url.startsWith('/r')) {
|
||||
let image = await prisma.image.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ file: parts[2] },
|
||||
{ invisible: { invis: decodeURI(parts[2]) } },
|
||||
],
|
||||
},
|
||||
select: {
|
||||
mimetype: true,
|
||||
id: true,
|
||||
file: true,
|
||||
invisible: true,
|
||||
},
|
||||
});
|
||||
|
||||
image && await prisma.image.update({
|
||||
where: { id: image.id },
|
||||
data: { views: { increment: 1 } },
|
||||
});
|
||||
|
||||
const data = await getFile(config.uploader.directory, parts[2]);
|
||||
if (!data) return app.render404(req, res);
|
||||
|
||||
if (!image) { // raw image
|
||||
const mimetype = mimes[extname(parts[2])] ?? 'application/octet-stream';
|
||||
res.setHeader('Content-Type', mimetype);
|
||||
res.end(data);
|
||||
} else { // raw image & update db
|
||||
await prisma.image.update({
|
||||
where: { id: image.id },
|
||||
data: { views: { increment: 1 } },
|
||||
});
|
||||
res.setHeader('Content-Type', image.mimetype);
|
||||
res.end(data);
|
||||
}
|
||||
} else if (req.url.startsWith(config.uploader.route)) {
|
||||
const data = await getFile(config.uploader.directory, parts[2]);
|
||||
if (!data) return app.render404(req, res);
|
||||
|
||||
let image = await prisma.image.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ file: parts[2] },
|
||||
{ invisible: { invis: decodeURI(parts[2]) } },
|
||||
],
|
||||
},
|
||||
select: {
|
||||
mimetype: true,
|
||||
id: true,
|
||||
file: true,
|
||||
invisible: true,
|
||||
embed: true,
|
||||
},
|
||||
});
|
||||
|
||||
image && await prisma.image.update({
|
||||
where: { id: image.id },
|
||||
data: { views: { increment: 1 } },
|
||||
});
|
||||
|
||||
if (!image) { // raw image
|
||||
const mimetype = mimes[extname(parts[2])] ?? 'application/octet-stream';
|
||||
res.setHeader('Content-Type', mimetype);
|
||||
res.end(data);
|
||||
} else if (image.embed) { // embed image
|
||||
handle(req, res);
|
||||
} else { // raw image fallback
|
||||
res.setHeader('Content-Type', image.mimetype);
|
||||
res.end(data);
|
||||
}
|
||||
} else {
|
||||
handle(req, res);
|
||||
}
|
||||
|
||||
if (config.core.logger) log(req.url, res.statusCode);
|
||||
});
|
||||
|
||||
srv.on('error', (e) => {
|
||||
serverLog.error(e);
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
srv.on('listening', () => {
|
||||
serverLog.info(`listening on ${config.core.host}:${config.core.port}`);
|
||||
});
|
||||
|
||||
srv.listen(config.core.port, config.core.host ?? '0.0.0.0');
|
||||
|
||||
const stats = await getStats(prisma, config);
|
||||
await prisma.stats.create({
|
||||
data: {
|
||||
data: stats,
|
||||
},
|
||||
});
|
||||
setInterval(async () => {
|
||||
const stats = await getStats(prisma, config);
|
||||
await prisma.stats.create({
|
||||
data: {
|
||||
data: stats,
|
||||
},
|
||||
});
|
||||
if (config.core.logger) serverLog.info('stats updated');
|
||||
}, config.core.stats_interval * 1000);
|
||||
}
|
||||
130
server/util.js
130
server/util.js
@@ -1,130 +0,0 @@
|
||||
const { readFile, readdir, stat } = require('fs/promises');
|
||||
const { join } = require('path');
|
||||
const { Migrate } = require('@prisma/migrate/dist/Migrate.js');
|
||||
const Logger = require('../src/lib/logger.js');
|
||||
|
||||
async function migrations() {
|
||||
const migrate = new Migrate('./prisma/schema.prisma');
|
||||
const diagnose = await migrate.diagnoseMigrationHistory({
|
||||
optInToShadowDatabase: false,
|
||||
});
|
||||
|
||||
if (diagnose.history?.diagnostic === 'databaseIsBehind') {
|
||||
Logger.get('database').info('migrating database');
|
||||
await migrate.applyMigrations();
|
||||
Logger.get('database').info('finished migrating database');
|
||||
}
|
||||
|
||||
migrate.stop();
|
||||
}
|
||||
|
||||
function log(url) {
|
||||
if (url.startsWith('/_next') || url.startsWith('/__nextjs')) return;
|
||||
return Logger.get('url').info(url);
|
||||
}
|
||||
|
||||
function shouldUseYarn() {
|
||||
try {
|
||||
execSync('yarnpkg --version', { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function getFile(dir, file) {
|
||||
try {
|
||||
const data = await readFile(join(process.cwd(), dir, file));
|
||||
return data;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function sizeOfDir(directory) {
|
||||
const files = await readdir(directory);
|
||||
|
||||
let size = 0;
|
||||
for (let i = 0, L = files.length; i !== L; ++i) {
|
||||
const sta = await stat(join(directory, files[i]));
|
||||
size += sta.size;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
function bytesToRead(bytes) {
|
||||
const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'];
|
||||
let num = 0;
|
||||
|
||||
while (bytes > 1024) {
|
||||
bytes /= 1024;
|
||||
++num;
|
||||
}
|
||||
|
||||
return `${bytes.toFixed(1)} ${units[num]}`;
|
||||
}
|
||||
|
||||
|
||||
async function getStats(prisma, config) {
|
||||
const size = await sizeOfDir(join(process.cwd(), config.uploader.directory));
|
||||
const byUser = await prisma.image.groupBy({
|
||||
by: ['userId'],
|
||||
_count: {
|
||||
_all: true,
|
||||
},
|
||||
});
|
||||
const count_users = await prisma.user.count();
|
||||
|
||||
const count_by_user = [];
|
||||
for (let i = 0, L = byUser.length; i !== L; ++i) {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: byUser[i].userId,
|
||||
},
|
||||
});
|
||||
|
||||
count_by_user.push({
|
||||
username: user.username,
|
||||
count: byUser[i]._count._all,
|
||||
});
|
||||
}
|
||||
|
||||
const count = await prisma.image.count();
|
||||
const viewsCount = await prisma.image.groupBy({
|
||||
by: ['views'],
|
||||
_sum: {
|
||||
views: true,
|
||||
},
|
||||
});
|
||||
|
||||
const typesCount = await prisma.image.groupBy({
|
||||
by: ['mimetype'],
|
||||
_count: {
|
||||
mimetype: true,
|
||||
},
|
||||
});
|
||||
const types_count = [];
|
||||
for (let i = 0, L = typesCount.length; i !== L; ++i) types_count.push({ mimetype: typesCount[i].mimetype, count: typesCount[i]._count.mimetype });
|
||||
|
||||
return {
|
||||
size: bytesToRead(size),
|
||||
size_num: size,
|
||||
count,
|
||||
count_by_user: count_by_user.sort((a,b) => b.count-a.count),
|
||||
count_users,
|
||||
views_count: (viewsCount[0]?._sum?.views ?? 0),
|
||||
types_count: types_count.sort((a,b) => b.count-a.count),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
migrations,
|
||||
bytesToRead,
|
||||
getFile,
|
||||
getStats,
|
||||
log,
|
||||
sizeOfDir,
|
||||
shouldUseYarn,
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
const { object, bool, string, number, boolean, array } = require('yup');
|
||||
|
||||
const validator = object({
|
||||
core: object({
|
||||
secure: bool().default(false),
|
||||
secret: string().min(8).required(),
|
||||
host: string().default('0.0.0.0'),
|
||||
port: number().default(3000),
|
||||
database_url: string().required(),
|
||||
logger: boolean().default(true),
|
||||
stats_interval: number().default(1800),
|
||||
}).required(),
|
||||
uploader: object({
|
||||
route: string().default('/u'),
|
||||
embed_route: string().default('/a'),
|
||||
length: number().default(6),
|
||||
directory: string().default('./uploads'),
|
||||
admin_limit: number().default(104900000),
|
||||
user_limit: number().default(104900000),
|
||||
disabled_extensions: array().default([]),
|
||||
}).required(),
|
||||
urls: object({
|
||||
route: string().default('/go'),
|
||||
length: number().default(6),
|
||||
}).required(),
|
||||
ratelimit: object({
|
||||
user: number().default(0),
|
||||
admin: number().default(0),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
module.exports = function validate(config) {
|
||||
try {
|
||||
return validator.validateSync(config, { abortEarly: false });
|
||||
} catch (e) {
|
||||
if (process.env.ZIPLINE_DOCKER_BUILD) return {};
|
||||
throw `${e.errors.length} errors occured\n${e.errors.map(x => '\t' + x).join('\n')}`;
|
||||
}
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Snackbar, Alert as MuiAlert } from '@mui/material';
|
||||
|
||||
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 '@mui/material';
|
||||
|
||||
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,10 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Card as MuiCard,
|
||||
CardContent,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
|
||||
export default function Card(props) {
|
||||
const { name, children, ...other } = props;
|
||||
import { Card as MCard, Title } from '@mantine/core';
|
||||
|
||||
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}>
|
||||
{name && <Title order={2}>{name}</Title>}
|
||||
{children}
|
||||
</MCard>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
|
||||
export default function CenteredBox({ children, ...other }) {
|
||||
return (
|
||||
<Box
|
||||
justifyContent='center'
|
||||
display='flex'
|
||||
alignItems='center'
|
||||
{...other}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
15
src/components/CodeInput.tsx
Normal file
15
src/components/CodeInput.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
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: '80vh',
|
||||
},
|
||||
}));
|
||||
|
||||
export default function CodeInput({ ...props }) {
|
||||
const { classes } = useStyles({ size: 'md' }, { name: 'CodeInput' });
|
||||
|
||||
return <Textarea classNames={{ input: classes.input }} autoComplete='nope' {...props} />;
|
||||
}
|
||||
195
src/components/File.tsx
Normal file
195
src/components/File.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import { Button, Card, Group, LoadingOverlay, Modal, Stack, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { relativeTime } from 'lib/utils/client';
|
||||
import { useFileDelete, useFileFavorite } from 'lib/queries/files';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
CalendarIcon,
|
||||
ClockIcon,
|
||||
CopyIcon,
|
||||
CrossIcon,
|
||||
DeleteIcon,
|
||||
ExternalLinkIcon,
|
||||
FileIcon,
|
||||
HashIcon,
|
||||
ImageIcon,
|
||||
StarIcon,
|
||||
EyeIcon,
|
||||
} from './icons';
|
||||
import MutedText from './MutedText';
|
||||
import Type from './Type';
|
||||
import Link from './Link';
|
||||
|
||||
export function FileMeta({ Icon, title, subtitle, ...other }) {
|
||||
return other.tooltip ? (
|
||||
<Group>
|
||||
<Icon size={24} />
|
||||
<Tooltip label={other.tooltip}>
|
||||
<Stack spacing={1}>
|
||||
<Text>{title}</Text>
|
||||
<MutedText size='md'>{subtitle}</MutedText>
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
) : (
|
||||
<Group>
|
||||
<Icon size={24} />
|
||||
<Stack spacing={1}>
|
||||
<Text>{title}</Text>
|
||||
<MutedText size='md'>{subtitle}</MutedText>
|
||||
</Stack>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
export default function File({ image, updateImages, disableMediaPreview }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const deleteFile = useFileDelete();
|
||||
const favoriteFile = useFileFavorite();
|
||||
const clipboard = useClipboard();
|
||||
|
||||
const loading = deleteFile.isLoading || favoriteFile.isLoading;
|
||||
|
||||
const handleDelete = async () => {
|
||||
deleteFile.mutate(image.id, {
|
||||
onSuccess: () => {
|
||||
showNotification({
|
||||
title: 'File Deleted',
|
||||
message: '',
|
||||
color: 'green',
|
||||
icon: <DeleteIcon />,
|
||||
});
|
||||
},
|
||||
|
||||
onError: (res: any) => {
|
||||
showNotification({
|
||||
title: 'Failed to delete file',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
},
|
||||
|
||||
onSettled: () => {
|
||||
setOpen(false);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
clipboard.copy(`${window.location.protocol}//${window.location.host}${image.url}`);
|
||||
setOpen(false);
|
||||
showNotification({
|
||||
title: 'Copied to clipboard',
|
||||
message: '',
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
};
|
||||
|
||||
const handleFavorite = async () => {
|
||||
favoriteFile.mutate(
|
||||
{ id: image.id, favorite: !image.favorite },
|
||||
{
|
||||
onSuccess: () => {
|
||||
showNotification({
|
||||
title: 'Image is now ' + (!image.favorite ? 'favorited' : 'unfavorited'),
|
||||
message: '',
|
||||
icon: <StarIcon />,
|
||||
});
|
||||
},
|
||||
|
||||
onError: (res: any) => {
|
||||
showNotification({
|
||||
title: 'Failed to favorite file',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal opened={open} onClose={() => setOpen(false)} title={<Title>{image.file}</Title>} size='xl'>
|
||||
<LoadingOverlay visible={loading} />
|
||||
<Stack>
|
||||
<Type
|
||||
file={image}
|
||||
src={`/r/${image.file}`}
|
||||
alt={image.file}
|
||||
popup
|
||||
sx={{ minHeight: 200 }}
|
||||
style={{ minHeight: 200 }}
|
||||
disableMediaPreview={false}
|
||||
/>
|
||||
<Stack>
|
||||
<FileMeta Icon={FileIcon} title='Name' subtitle={image.file} />
|
||||
<FileMeta Icon={ImageIcon} title='Type' subtitle={image.mimetype} />
|
||||
<FileMeta Icon={EyeIcon} title='Views' subtitle={image?.views?.toLocaleString()} />
|
||||
{image.maxViews && (
|
||||
<FileMeta
|
||||
Icon={EyeIcon}
|
||||
title='Max views'
|
||||
subtitle={image?.maxViews?.toLocaleString()}
|
||||
tooltip={`This file will be deleted after being viewed ${image?.maxViews?.toLocaleString()} times.`}
|
||||
/>
|
||||
)}
|
||||
<FileMeta
|
||||
Icon={CalendarIcon}
|
||||
title='Uploaded'
|
||||
subtitle={relativeTime(new Date(image.created_at))}
|
||||
tooltip={new Date(image?.created_at).toLocaleString()}
|
||||
/>
|
||||
{image.expires_at && (
|
||||
<FileMeta
|
||||
Icon={ClockIcon}
|
||||
title='Expires'
|
||||
subtitle={relativeTime(new Date(image.expires_at))}
|
||||
tooltip={new Date(image.expires_at).toLocaleString()}
|
||||
/>
|
||||
)}
|
||||
<FileMeta Icon={HashIcon} title='ID' subtitle={image.id} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Group position='right' mt='md'>
|
||||
<Button onClick={handleCopy}>Copy URL</Button>
|
||||
<Button onClick={handleDelete}>Delete</Button>
|
||||
<Button onClick={handleFavorite}>{image.favorite ? 'Unfavorite' : 'Favorite'}</Button>
|
||||
<Link href={image.url} target='_blank'>
|
||||
<Button rightIcon={<ExternalLinkIcon />}>Open</Button>
|
||||
</Link>
|
||||
</Group>
|
||||
</Modal>
|
||||
<Card sx={{ maxWidth: '100%', height: '100%' }} shadow='md'>
|
||||
<Card.Section>
|
||||
<LoadingOverlay visible={loading} />
|
||||
<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={`/r/${image.file}`}
|
||||
alt={image.file}
|
||||
onClick={() => setOpen(true)}
|
||||
disableMediaPreview={disableMediaPreview}
|
||||
/>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardMedia,
|
||||
CardActionArea,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
} from '@mui/material';
|
||||
import AudioIcon from '@mui/icons-material/Audiotrack';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
|
||||
export default function Image({ image, updateImages }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [t] = useState(image.mimetype.split('/')[0]);
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await useFetch('/api/user/files', 'DELETE', { id: image.id });
|
||||
if (!res.error) updateImages(true);
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
copy(`${window.location.protocol}//${window.location.host}${image.url}`);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleFavorite = async () => {
|
||||
const data = await useFetch('/api/user/files', 'PATCH', { id: image.id, favorite: !image.favorite });
|
||||
if (!data.error) updateImages(true);
|
||||
};
|
||||
|
||||
const Type = (props) => {
|
||||
return {
|
||||
'video': <video controls {...props} />,
|
||||
// eslint-disable-next-line jsx-a11y/alt-text
|
||||
'image': <img {...props} />,
|
||||
'audio': <audio controls {...props} />,
|
||||
}[t];
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card sx={{ maxWidth: '100%' }}>
|
||||
<CardActionArea sx={t === 'audio' ? { justifyContent: 'center', display: 'flex', alignItems: 'center' } : {}}>
|
||||
<CardMedia
|
||||
sx={{ height: 320, fontSize: 70, width: '100%' }}
|
||||
image={image.url}
|
||||
title={image.file}
|
||||
component={t === 'audio' ? AudioIcon : t} // this is done because audio without controls is hidden
|
||||
onClick={() => setOpen(true)}
|
||||
/>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<DialogTitle id='alert-dialog-title'>
|
||||
{image.file}
|
||||
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Type
|
||||
style={{ width: '100%' }}
|
||||
src={image.url}
|
||||
alt={image.url}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleDelete} color='inherit'>Delete</Button>
|
||||
<Button onClick={handleCopy} color='inherit'>Copy URL</Button>
|
||||
<Button onClick={handleFavorite} color='inherit'>{image.favorite ? 'Unfavorite' : 'Favorite'}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,411 +1,468 @@
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import {
|
||||
AppBar,
|
||||
AppShell,
|
||||
Box,
|
||||
Divider,
|
||||
Drawer,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Toolbar,
|
||||
Typography,
|
||||
Burger,
|
||||
Button,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Divider,
|
||||
Header,
|
||||
MediaQuery,
|
||||
Navbar,
|
||||
NavLink,
|
||||
Paper,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Menu as MenuIcon,
|
||||
Home as HomeIcon,
|
||||
AccountCircle as AccountIcon,
|
||||
Folder as FolderIcon,
|
||||
Upload as UploadIcon,
|
||||
ContentCopy as CopyIcon,
|
||||
Autorenew as ResetIcon,
|
||||
Logout as LogoutIcon,
|
||||
PeopleAlt as UsersIcon,
|
||||
Brush as BrushIcon,
|
||||
Link as URLIcon,
|
||||
} from '@mui/icons-material';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import Backdrop from './Backdrop';
|
||||
import { friendlyThemeName, themes } from 'components/Theming';
|
||||
import Select from 'components/input/Select';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useStoreDispatch } from 'lib/redux/store';
|
||||
import { updateUser } from 'lib/redux/reducers/user';
|
||||
Popover,
|
||||
ScrollArea,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
UnstyledButton,
|
||||
useMantineTheme,
|
||||
Group,
|
||||
Image,
|
||||
Tooltip,
|
||||
Badge,
|
||||
Menu,
|
||||
} from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { useVersion } from 'lib/queries/version';
|
||||
import { userSelector } from 'lib/recoil/user';
|
||||
import { capitalize } from 'lib/utils/client';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
ExternalLinkIcon,
|
||||
ActivityIcon,
|
||||
CheckIcon,
|
||||
CopyIcon,
|
||||
CrossIcon,
|
||||
DeleteIcon,
|
||||
FileIcon,
|
||||
HomeIcon,
|
||||
LinkIcon,
|
||||
LogoutIcon,
|
||||
PencilIcon,
|
||||
SettingsIcon,
|
||||
TagIcon,
|
||||
TypeIcon,
|
||||
UploadIcon,
|
||||
UserIcon,
|
||||
DiscordIcon,
|
||||
GitHubIcon,
|
||||
GoogleIcon,
|
||||
} from './icons';
|
||||
import { friendlyThemeName, themes } from './Theming';
|
||||
|
||||
function MenuItemLink(props) {
|
||||
return (
|
||||
<Link href={props.href} passHref legacyBehavior>
|
||||
<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': !props.noClick
|
||||
? {
|
||||
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],
|
||||
}
|
||||
: null,
|
||||
})}
|
||||
{...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 />,
|
||||
icon: <HomeIcon size={18} />,
|
||||
text: 'Home',
|
||||
link: '/dashboard',
|
||||
},
|
||||
{
|
||||
icon: <FolderIcon />,
|
||||
icon: <FileIcon size={18} />,
|
||||
text: 'Files',
|
||||
link: '/dashboard/files',
|
||||
},
|
||||
{
|
||||
icon: <URLIcon />,
|
||||
icon: <ActivityIcon size={18} />,
|
||||
text: 'Stats',
|
||||
link: '/dashboard/stats',
|
||||
},
|
||||
{
|
||||
icon: <LinkIcon size={18} />,
|
||||
text: 'URLs',
|
||||
link: '/dashboard/urls',
|
||||
},
|
||||
{
|
||||
icon: <UploadIcon />,
|
||||
icon: <UploadIcon size={18} />,
|
||||
text: 'Upload',
|
||||
link: '/dashboard/upload',
|
||||
},
|
||||
{
|
||||
icon: <TypeIcon size={18} />,
|
||||
text: 'Upload Text',
|
||||
link: '/dashboard/text',
|
||||
},
|
||||
];
|
||||
|
||||
const drawerWidth = 240;
|
||||
const admin_items = [
|
||||
{
|
||||
icon: <UserIcon size={18} />,
|
||||
text: 'Users',
|
||||
link: '/dashboard/users',
|
||||
if: (props) => true,
|
||||
},
|
||||
{
|
||||
icon: <TagIcon size={18} />,
|
||||
text: 'Invites',
|
||||
link: '/dashboard/invites',
|
||||
if: (props) => props.invites,
|
||||
},
|
||||
];
|
||||
|
||||
function CopyTokenDialog({ open, setOpen, token }) {
|
||||
const handleCopyToken = () => {
|
||||
copy(token);
|
||||
setOpen(false);
|
||||
export default function Layout({ children, props }) {
|
||||
const [user, setUser] = useRecoilState(userSelector);
|
||||
|
||||
const { title, oauth_providers: unparsed } = props;
|
||||
const oauth_providers = JSON.parse(unparsed);
|
||||
const icons = {
|
||||
GitHub: GitHubIcon,
|
||||
Discord: DiscordIcon,
|
||||
Google: GoogleIcon,
|
||||
};
|
||||
|
||||
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 files 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>
|
||||
);
|
||||
}
|
||||
for (const provider of oauth_providers) {
|
||||
provider.Icon = icons[provider.name];
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
const external_links = JSON.parse(props.external_links ?? '[]');
|
||||
|
||||
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);
|
||||
const [token, setToken] = useState(user?.token);
|
||||
const [systemTheme, setSystemTheme] = useState(user.systemTheme ?? 'system');
|
||||
const version = useVersion();
|
||||
const [opened, setOpened] = useState(false); // navigation open
|
||||
const [open, setOpen] = useState(false); // manage acc dropdown
|
||||
|
||||
const avatar = user?.avatar ?? null;
|
||||
const router = useRouter();
|
||||
const dispatch = useStoreDispatch();
|
||||
const theme = useMantineTheme();
|
||||
const modals = useModals();
|
||||
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 => {
|
||||
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));
|
||||
|
||||
setUser(newUser);
|
||||
router.replace(router.pathname);
|
||||
|
||||
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' passHref>
|
||||
<MenuItem onClick={handleClose(null)}>
|
||||
<AccountIcon sx={{ mr: 2 }} /> Manage Account
|
||||
</MenuItem>
|
||||
</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' passHref>
|
||||
<MenuItem onClick={handleClose(null)}>
|
||||
<LogoutIcon sx={{ mr: 2 }} /> Logout
|
||||
</MenuItem>
|
||||
</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} passHref>
|
||||
<ListItem button>
|
||||
<ListItemIcon>{item.icon}</ListItemIcon>
|
||||
<ListItemText primary={item.text} />
|
||||
</ListItem>
|
||||
</Link>
|
||||
))}
|
||||
{user && user.administrator && (
|
||||
<Link href='/dashboard/users' passHref>
|
||||
<ListItem button>
|
||||
<ListItemIcon><UsersIcon /></ListItemIcon>
|
||||
<ListItemText primary='Users' />
|
||||
</ListItem>
|
||||
</Link>
|
||||
)}
|
||||
</List>
|
||||
|
||||
</div>
|
||||
);
|
||||
const openResetToken = () =>
|
||||
modals.openConfirmModal({
|
||||
title: <Title>Reset Token?</Title>,
|
||||
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);
|
||||
showNotification({
|
||||
title: 'Token Reset Failed',
|
||||
message: a.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
} else {
|
||||
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: <Title>Copy Token</Title>,
|
||||
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);
|
||||
|
||||
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 pt='sm' hiddenBreakpoint='sm' hidden={!opened} width={{ sm: 200, lg: 230 }}>
|
||||
<Navbar.Section grow component={ScrollArea}>
|
||||
{items.map(({ icon, text, link }) => (
|
||||
<Link href={link} key={text} passHref legacyBehavior>
|
||||
<NavLink
|
||||
component='a'
|
||||
label={text}
|
||||
icon={icon}
|
||||
active={router.pathname === link}
|
||||
variant='light'
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
{user.administrator && (
|
||||
<NavLink
|
||||
label='Administration'
|
||||
icon={<SettingsIcon />}
|
||||
childrenOffset={28}
|
||||
defaultOpened={admin_items.map((x) => x.link).includes(router.pathname)}
|
||||
>
|
||||
{admin_items
|
||||
.filter((x) => x.if(props))
|
||||
.map(({ icon, text, link }) => (
|
||||
<Link href={link} key={text} passHref legacyBehavior>
|
||||
<NavLink
|
||||
component='a'
|
||||
label={text}
|
||||
icon={icon}
|
||||
active={router.pathname === link}
|
||||
variant='light'
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
</NavLink>
|
||||
)}
|
||||
</Navbar.Section>
|
||||
<Navbar.Section>
|
||||
{external_links.length
|
||||
? external_links.map(({ label, link }, i) => (
|
||||
<Link href={link} passHref key={i} legacyBehavior>
|
||||
<NavLink
|
||||
label={label}
|
||||
component='a'
|
||||
target='_blank'
|
||||
variant='light'
|
||||
icon={<ExternalLinkIcon />}
|
||||
/>
|
||||
</Link>
|
||||
))
|
||||
: null}
|
||||
</Navbar.Section>
|
||||
{version.isSuccess ? (
|
||||
<Navbar.Section>
|
||||
<Tooltip
|
||||
label={
|
||||
version.data.local !== version.data.upstream
|
||||
? `You are running an outdated version of Zipline, refer to the docs on how to update to ${version.data.upstream}`
|
||||
: 'You are running the latest version of Zipline'
|
||||
}
|
||||
>
|
||||
<Badge
|
||||
m='md'
|
||||
radius='md'
|
||||
size='lg'
|
||||
variant='dot'
|
||||
color={version.data.local !== version.data.upstream ? 'red' : 'primary'}
|
||||
>
|
||||
{version.data.local}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</Navbar.Section>
|
||||
) : null}
|
||||
</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='sm'>{title}</Title>
|
||||
<Box sx={{ marginLeft: 'auto', marginRight: 0 }}>
|
||||
<Popover position='bottom-end' opened={open} onClose={() => setOpen(false)}>
|
||||
<Popover.Target>
|
||||
<Button
|
||||
leftIcon={avatar ? <Image src={avatar} height={32} radius='md' /> : <SettingsIcon />}
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
sx={(t) => ({
|
||||
backgroundColor: 'inherit',
|
||||
'&:hover': {
|
||||
backgroundColor: t.other.hover,
|
||||
},
|
||||
color: t.colorScheme === 'dark' ? 'white' : 'black',
|
||||
})}
|
||||
size='xl'
|
||||
p='sm'
|
||||
>
|
||||
{user.username}
|
||||
</Button>
|
||||
</Popover.Target>
|
||||
|
||||
<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' passHref>
|
||||
<MenuItem onClick={handleClose(null)}>
|
||||
<AccountIcon sx={{ mr: 2 }} /> Manage Account
|
||||
</MenuItem>
|
||||
</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' passHref>
|
||||
<MenuItem onClick={handleClose(null)}>
|
||||
<LogoutIcon sx={{ mr: 2 }} /> Logout
|
||||
</MenuItem>
|
||||
</Link>
|
||||
</Menu>
|
||||
<Popover.Dropdown p={4} mr='md' sx={{ minWidth: '200px' }}>
|
||||
<Stack spacing={2}>
|
||||
<Menu.Label>
|
||||
{user.username} ({user.id}){' '}
|
||||
{user.administrator && user.username !== 'administrator' ? '(Administrator)' : ''}
|
||||
</Menu.Label>
|
||||
<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>
|
||||
<Menu.Divider />
|
||||
<>
|
||||
{oauth_providers
|
||||
.filter((x) =>
|
||||
user.oauth
|
||||
?.map(({ provider }) => provider.toLowerCase())
|
||||
.includes(x.name.toLowerCase())
|
||||
)
|
||||
.map(({ name, Icon }, i) => (
|
||||
<>
|
||||
<MenuItem
|
||||
sx={{ '&:hover': { backgroundColor: 'inherit' } }}
|
||||
key={i}
|
||||
py={5}
|
||||
px={4}
|
||||
icon={<Icon size={18} colorScheme={theme.colorScheme} />}
|
||||
>
|
||||
Logged in with {capitalize(name)}
|
||||
</MenuItem>
|
||||
</>
|
||||
))}
|
||||
{oauth_providers.filter((x) =>
|
||||
user.oauth
|
||||
?.map(({ provider }) => provider.toLowerCase())
|
||||
.includes(x.name.toLowerCase())
|
||||
).length ? (
|
||||
<Menu.Divider />
|
||||
) : null}
|
||||
</>
|
||||
<MenuItem icon={<PencilIcon />}>
|
||||
<Select
|
||||
size='xs'
|
||||
data={Object.keys(themes).map((t) => ({
|
||||
value: t,
|
||||
label: friendlyThemeName[t],
|
||||
}))}
|
||||
value={systemTheme}
|
||||
onChange={handleUpdateTheme}
|
||||
/>
|
||||
</MenuItem>
|
||||
</Stack>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
</Box>
|
||||
)}
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Box
|
||||
component='nav'
|
||||
sx={{
|
||||
width: { sm: drawerWidth },
|
||||
flexShrink: { sm: 0 },
|
||||
}}
|
||||
</div>
|
||||
</Header>
|
||||
}
|
||||
>
|
||||
<Paper
|
||||
withBorder
|
||||
p='md'
|
||||
shadow='xs'
|
||||
sx={(t) => ({
|
||||
borderColor: t.colorScheme === 'dark' ? t.colors.dark[5] : t.colors.dark[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>
|
||||
{children}
|
||||
</Paper>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,75 +1,5 @@
|
||||
/// 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 '@mui/material/Link';
|
||||
import { NextLink } from '@mantine/next';
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default Link;
|
||||
export default function Link(props) {
|
||||
return <NextLink legacyBehavior {...props} />;
|
||||
}
|
||||
|
||||
9
src/components/MutedText.tsx
Normal file
9
src/components/MutedText.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Text } from '@mantine/core';
|
||||
|
||||
export default function MutedText({ children, ...props }) {
|
||||
return (
|
||||
<Text color='dimmed' 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'
|
||||
width='target'
|
||||
withArrow
|
||||
trapFocus={false}
|
||||
styles={{
|
||||
dropdown: {
|
||||
zIndex: 999999,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Popover.Target>
|
||||
<div onFocusCapture={() => setPopoverOpened(true)} onBlurCapture={() => setPopoverOpened(false)}>
|
||||
<PasswordInput
|
||||
label='Password'
|
||||
description='A 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}
|
||||
/>
|
||||
</div>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<Progress color={color} value={strength} size={7} mb='md' />
|
||||
<PasswordRequirement label='Includes at least 8 characters' meets={value.length > 7} />
|
||||
{checks}
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
27
src/components/SmallTable.tsx
Normal file
27
src/components/SmallTable.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
71
src/components/StatCard.tsx
Normal file
71
src/components/StatCard.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Card, createStyles, Group, Text } from '@mantine/core';
|
||||
import { ArrowDownRight, ArrowUpRight } from 'react-feather';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
root: {
|
||||
padding: theme.spacing.xl * 1.5,
|
||||
},
|
||||
|
||||
value: {
|
||||
fontSize: 24,
|
||||
fontWeight: 700,
|
||||
lineHeight: 1,
|
||||
},
|
||||
|
||||
diff: {
|
||||
lineHeight: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
icon: {
|
||||
color: theme.colorScheme === 'dark' ? theme.colors.dark[3] : theme.colors.gray[4],
|
||||
},
|
||||
|
||||
title: {
|
||||
fontWeight: 700,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
}));
|
||||
|
||||
interface StatsGridProps {
|
||||
stat: {
|
||||
title: string;
|
||||
icon: React.ReactNode;
|
||||
value: string;
|
||||
desc: string;
|
||||
diff?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export default function StatCard({ stat }: StatsGridProps) {
|
||||
const { classes } = useStyles();
|
||||
if (stat.diff) stat.diff = Math.round(stat.diff);
|
||||
|
||||
return (
|
||||
<Card p='md' radius='md' key={stat.title}>
|
||||
<Group position='apart'>
|
||||
<Text size='xs' color='dimmed' className={classes.title}>
|
||||
{stat.title}
|
||||
</Text>
|
||||
{stat.icon}
|
||||
</Group>
|
||||
|
||||
<Group align='flex-end' spacing='xs' mt='md'>
|
||||
<Text className={classes.value}>{stat.value}</Text>
|
||||
{typeof stat.diff == 'number' && (
|
||||
<>
|
||||
<Text color={stat.diff >= 0 ? 'teal' : 'red'} size='sm' weight={500} className={classes.diff}>
|
||||
<span>{stat.diff === Infinity ? '∞' : stat.diff}%</span>
|
||||
{stat.diff >= 0 ? <ArrowUpRight size={16} /> : <ArrowDownRight size={16} />}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Text size='xs' color='dimmed' mt='sm'>
|
||||
{stat.desc}
|
||||
</Text>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,77 +1,132 @@
|
||||
import React from 'react';
|
||||
import { ThemeProvider } from '@emotion/react';
|
||||
import { CssBaseline } from '@mui/material';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
// themes
|
||||
import dark_blue from 'lib/themes/dark_blue';
|
||||
import dark from 'lib/themes/dark';
|
||||
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 nord from 'lib/themes/nord';
|
||||
import polar from 'lib/themes/polar';
|
||||
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 qogir_dark from 'lib/themes/qogir_dark';
|
||||
|
||||
import { useStoreSelector } from 'lib/redux/store';
|
||||
import createTheme from 'lib/themes';
|
||||
import { MantineProvider, MantineThemeOverride } from '@mantine/core';
|
||||
import { useColorScheme } from '@mantine/hooks';
|
||||
import { ModalsProvider } from '@mantine/modals';
|
||||
import { NotificationsProvider } from '@mantine/notifications';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { userSelector } from 'lib/recoil/user';
|
||||
|
||||
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,
|
||||
'dracula': dracula,
|
||||
'matcha_dark_azul': matcha_dark_azul,
|
||||
'qogir_dark': qogir_dark,
|
||||
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 = {
|
||||
'dark_blue': 'Dark 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',
|
||||
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',
|
||||
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 = useRecoilValue(userSelector);
|
||||
const colorScheme = useColorScheme();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
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,
|
||||
components: {
|
||||
AppShell: {
|
||||
styles: (t) => ({
|
||||
root: {
|
||||
backgroundColor: t.other.AppShell_backgroundColor,
|
||||
},
|
||||
}),
|
||||
},
|
||||
NavLink: {
|
||||
styles: (t) => ({
|
||||
icon: {
|
||||
paddingLeft: t.spacing.sm,
|
||||
},
|
||||
}),
|
||||
},
|
||||
Modal: {
|
||||
defaultProps: {
|
||||
centered: true,
|
||||
overlayBlur: 3,
|
||||
overlayColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
|
||||
},
|
||||
},
|
||||
Popover: {
|
||||
defaultProps: {
|
||||
transition: 'pop',
|
||||
},
|
||||
},
|
||||
LoadingOverlay: {
|
||||
defaultProps: {
|
||||
overlayBlur: 3,
|
||||
overlayColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
|
||||
},
|
||||
},
|
||||
Loader: {
|
||||
defaultProps: {
|
||||
variant: 'dots',
|
||||
},
|
||||
},
|
||||
Card: {
|
||||
styles: (t) => ({
|
||||
root: {
|
||||
backgroundColor: t.colorScheme === 'dark' ? t.colors.dark[6] : t.colors.gray[0],
|
||||
},
|
||||
}),
|
||||
},
|
||||
Image: {
|
||||
styles: (t) => ({
|
||||
placeholder: {
|
||||
backgroundColor: t.colorScheme === 'dark' ? t.colors.dark[6] : t.colors.gray[0],
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ModalsProvider>
|
||||
<NotificationsProvider>
|
||||
{props.children ? props.children : <Component {...pageProps} />}
|
||||
</NotificationsProvider>
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
74
src/components/Type.tsx
Normal file
74
src/components/Type.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Group, Image, Text } from '@mantine/core';
|
||||
import { Prism } from '@mantine/prism';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AudioIcon, FileIcon, PlayIcon } from './icons';
|
||||
|
||||
function Placeholder({ text, Icon, ...props }) {
|
||||
if (props.disableResolve) props.src = null;
|
||||
|
||||
return (
|
||||
<Image
|
||||
height={200}
|
||||
withPlaceholder
|
||||
placeholder={
|
||||
<Group>
|
||||
<Icon size={48} />
|
||||
<Text size='md'>{text}</Text>
|
||||
</Group>
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Type({ file, popup = false, disableMediaPreview, ...props }) {
|
||||
const type = (file.type || file.mimetype).split('/')[0];
|
||||
const name = file.name || file.file;
|
||||
|
||||
const media = /^(video|audio|image|text)/.test(type);
|
||||
|
||||
const [text, setText] = useState('');
|
||||
|
||||
if (type === 'text') {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const res = await fetch('/r/' + name);
|
||||
const text = await res.text();
|
||||
|
||||
setText(text);
|
||||
})();
|
||||
}, []);
|
||||
}
|
||||
|
||||
if (media && disableMediaPreview) {
|
||||
return (
|
||||
<Placeholder Icon={FileIcon} text={`Click to view file (${name})`} disableResolve={true} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
return popup ? (
|
||||
media ? (
|
||||
{
|
||||
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]
|
||||
) : (
|
||||
<Text>Can't preview {file.type || file.mimetype}</Text>
|
||||
)
|
||||
) : media ? (
|
||||
{
|
||||
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]
|
||||
) : (
|
||||
<Placeholder Icon={FileIcon} text={`Click to view file (${name})`} {...props} />
|
||||
);
|
||||
}
|
||||
22
src/components/dropzone/Dropzone.tsx
Normal file
22
src/components/dropzone/Dropzone.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { Dropzone as MantineDropzone } from '@mantine/dropzone';
|
||||
import { Group, Text, useMantineTheme } from '@mantine/core';
|
||||
import { ImageIcon } from 'components/icons';
|
||||
|
||||
export default function Dropzone({ loading, onDrop, children }) {
|
||||
const theme = useMantineTheme();
|
||||
|
||||
return (
|
||||
<MantineDropzone onDrop={onDrop}>
|
||||
<Group position='center' spacing='xl' style={{ minHeight: 440 }}>
|
||||
<ImageIcon size={80} />
|
||||
|
||||
<Text size='xl' inline>
|
||||
Drag files here or click to select files
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
<div style={{ pointerEvents: 'all' }}>{children}</div>
|
||||
</MantineDropzone>
|
||||
);
|
||||
}
|
||||
52
src/components/dropzone/DropzoneFile.tsx
Normal file
52
src/components/dropzone/DropzoneFile.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import { Table, Tooltip, Badge, HoverCard, Text, useMantineTheme, Group } 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}
|
||||
disableMediaPreview={false}
|
||||
popup
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FileDropzone({ file }: { file: File }) {
|
||||
const theme = useMantineTheme();
|
||||
|
||||
return (
|
||||
<HoverCard shadow='md'>
|
||||
<HoverCard.Target>
|
||||
<Badge size='lg'>{file.name}</Badge>
|
||||
</HoverCard.Target>
|
||||
<HoverCard.Dropdown>
|
||||
<Group grow>
|
||||
<FilePreview file={file} />
|
||||
|
||||
<Table sx={{ color: theme.colorScheme === 'dark' ? 'white' : '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>
|
||||
</Group>
|
||||
</HoverCard.Dropdown>
|
||||
</HoverCard>
|
||||
);
|
||||
}
|
||||
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/ClockIcon.tsx
Normal file
5
src/components/icons/ClockIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Clock } from 'react-feather';
|
||||
|
||||
export default function ClockIcon({ ...props }) {
|
||||
return <Clock 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} />;
|
||||
}
|
||||
21
src/components/icons/DiscordIcon.tsx
Normal file
21
src/components/icons/DiscordIcon.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
// https://discord.com/branding
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function DiscordIcon({ ...props }) {
|
||||
return (
|
||||
<svg width='24' height='24' viewBox='0 0 71 55' xmlns='http://www.w3.org/2000/svg'>
|
||||
<g clipPath='url(#clip0)'>
|
||||
<path
|
||||
fill={props.colorScheme === 'manage' ? '#ffffff' : '#5865F2'}
|
||||
d='M60.1045 4.8978C55.5792 2.8214 50.7265 1.2916 45.6527 0.41542C45.5603 0.39851 45.468 0.440769 45.4204 0.525289C44.7963 1.6353 44.105 3.0834 43.6209 4.2216C38.1637 3.4046 32.7345 3.4046 27.3892 4.2216C26.905 3.0581 26.1886 1.6353 25.5617 0.525289C25.5141 0.443589 25.4218 0.40133 25.3294 0.41542C20.2584 1.2888 15.4057 2.8186 10.8776 4.8978C10.8384 4.9147 10.8048 4.9429 10.7825 4.9795C1.57795 18.7309 -0.943561 32.1443 0.293408 45.3914C0.299005 45.4562 0.335386 45.5182 0.385761 45.5576C6.45866 50.0174 12.3413 52.7249 18.1147 54.5195C18.2071 54.5477 18.305 54.5139 18.3638 54.4378C19.7295 52.5728 20.9469 50.6063 21.9907 48.5383C22.0523 48.4172 21.9935 48.2735 21.8676 48.2256C19.9366 47.4931 18.0979 46.6 16.3292 45.5858C16.1893 45.5041 16.1781 45.304 16.3068 45.2082C16.679 44.9293 17.0513 44.6391 17.4067 44.3461C17.471 44.2926 17.5606 44.2813 17.6362 44.3151C29.2558 49.6202 41.8354 49.6202 53.3179 44.3151C53.3935 44.2785 53.4831 44.2898 53.5502 44.3433C53.9057 44.6363 54.2779 44.9293 54.6529 45.2082C54.7816 45.304 54.7732 45.5041 54.6333 45.5858C52.8646 46.6197 51.0259 47.4931 49.0921 48.2228C48.9662 48.2707 48.9102 48.4172 48.9718 48.5383C50.038 50.6034 51.2554 52.5699 52.5959 54.435C52.6519 54.5139 52.7526 54.5477 52.845 54.5195C58.6464 52.7249 64.529 50.0174 70.6019 45.5576C70.6551 45.5182 70.6887 45.459 70.6943 45.3942C72.1747 30.0791 68.2147 16.7757 60.1968 4.9823C60.1772 4.9429 60.1437 4.9147 60.1045 4.8978ZM23.7259 37.3253C20.2276 37.3253 17.3451 34.1136 17.3451 30.1693C17.3451 26.225 20.1717 23.0133 23.7259 23.0133C27.308 23.0133 30.1626 26.2532 30.1066 30.1693C30.1066 34.1136 27.28 37.3253 23.7259 37.3253ZM47.3178 37.3253C43.8196 37.3253 40.9371 34.1136 40.9371 30.1693C40.9371 26.225 43.7636 23.0133 47.3178 23.0133C50.9 23.0133 53.7545 26.2532 53.6986 30.1693C53.6986 34.1136 50.9 37.3253 47.3178 37.3253Z'
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id='clip0'>
|
||||
<rect width='71' height='55' fill='white' />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
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/ExternalLinkIcon.tsx
Normal file
5
src/components/icons/ExternalLinkIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ExternalLink } from 'react-feather';
|
||||
|
||||
export default function ExternalLinkIcon({ ...props }) {
|
||||
return <ExternalLink size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/EyeIcon.tsx
Normal file
5
src/components/icons/EyeIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Eye } from 'react-feather';
|
||||
|
||||
export default function EyeIcon({ ...props }) {
|
||||
return <Eye 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} />;
|
||||
}
|
||||
15
src/components/icons/FlameshotIcon.tsx
Normal file
15
src/components/icons/FlameshotIcon.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
// https://github.com/flameshot-org/flameshot/blob/master/data/img/app/flameshot.svg
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function FlameshotIcon({ ...props }) {
|
||||
return (
|
||||
<Image
|
||||
alt='flameshot'
|
||||
src='https://raw.githubusercontent.com/flameshot-org/flameshot/master/data/img/app/flameshot.svg'
|
||||
width={24}
|
||||
height={24}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
17
src/components/icons/GitHubIcon.tsx
Normal file
17
src/components/icons/GitHubIcon.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { GitHub } from 'react-feather';
|
||||
import Image from 'next/image';
|
||||
|
||||
// https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg
|
||||
export default function GitHubIcon({ colorScheme, ...props }) {
|
||||
return (
|
||||
<svg width={24} height={24} viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg' {...props}>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z'
|
||||
transform='scale(64)'
|
||||
fill={colorScheme === 'dark' ? '#FFFFFF' : '#1B1F23'}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
15
src/components/icons/GoogleIcon.tsx
Normal file
15
src/components/icons/GoogleIcon.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
// https://developers.google.com/identity/branding-guidelines
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function GoogleIcon({ ...props }) {
|
||||
return (
|
||||
<Image
|
||||
alt='google'
|
||||
src='https://madeby.google.com/static/images/google_g_logo.svg'
|
||||
width={24}
|
||||
height={24}
|
||||
{...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/RefreshIcon.tsx
Normal file
5
src/components/icons/RefreshIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { RefreshCw } from 'react-feather';
|
||||
|
||||
export default function RefreshIcon({ ...props }) {
|
||||
return <RefreshCw 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} />;
|
||||
}
|
||||
9
src/components/icons/ShareXIcon.tsx
Normal file
9
src/components/icons/ShareXIcon.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
// https://getsharex.com/brand-assets/
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function ShareXIcon({ ...props }) {
|
||||
return (
|
||||
<Image alt='sharex' src='https://getsharex.com/img/ShareX_Logo.svg' width={24} height={24} {...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} />;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user