Compare commits

..

1 Commits

Author SHA1 Message Date
renovate[bot]
b81b1f979e chore(deps): add renovate.json 2022-06-17 09:43:38 +00:00
252 changed files with 7033 additions and 17102 deletions

View File

@@ -1,46 +0,0 @@
# 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/supabase 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 supabase
DATASOURCE_TYPE=supabase
DATASOURCE_SUPABASE_KEY=xxx
# remember: no leading slash
DATASOURCE_SUPABASE_URL=https://something.supabase.co
DATASOURCE_SUPABASE_BUCKET=zipline
UPLOADER_DEFAULT_FORMAT=RANDOM
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

View File

@@ -1,18 +1,12 @@
{
"extends": ["next", "next/core-web-vitals", "plugin:prettier/recommended"],
"extends": ["next", "next/core-web-vitals"],
"rules": {
"indent": ["error", 2],
"linebreak-style": ["error", "unix"],
"quotes": [
"error",
"single",
{
"avoidEscape": true
}
],
"quotes": ["error", "single"],
"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",
@@ -23,11 +17,9 @@
"react/no-direct-mutation-state": "warn",
"react/no-is-mounted": "warn",
"react/no-typos": "error",
"react/react-in-jsx-scope": "off",
"react/react-in-jsx-scope": "error",
"react/require-render-return": "error",
"react/style-prop-object": "warn",
"@next/next/no-img-element": "off",
"jsx-a11y/alt-text": "off",
"react/display-name": "off"
"@next/next/no-img-element": "off"
}
}
}

14
.gitattributes vendored
View File

@@ -1,14 +0,0 @@
# 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

View File

@@ -1,52 +0,0 @@
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 (ghcr.io/diced/zipline:trunk)
- latest (ghcr.io/diced/zipline:latest)
- other (provide version in additional info)
validations:
required: true
- type: dropdown
id: browsers
attributes:
label: What browser(s) are you seeing the problem on?
multiple: true
options:
- Firefox
- Chromium-based (Chrome, Edge, Brave, Opera, mobile chrome/chromium based, etc)
- Safari
- Firefox Mobile
- Safari Mobile
- type: textarea
id: zipline-logs
attributes:
label: Zipline Logs
description: Please copy and paste any relevant log output. Not seeing anything interesting? Try adding the `DEBUG=true` environment variable to see more logs, make sure to review the output and remove any sensitive information as it can be VERY verbose at times.
render: shell
- type: textarea
id: browser-logs
attributes:
label: Browser Logs
description: Please copy and paste any relevant log output.
render: shell
- type: textarea
id: additional-info
attributes:
label: Additional Info
description: Anything else that could be used to narrow down the issue, like your config.

View File

@@ -1,8 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Zipline Discord
url: https://discord.gg/EAhCRfGxCF
about: Ask for help with anything related to Zipline!
- name: Zipline Docs
url: https://zipline.diced.tech
about: Maybe take a look a the docs?

View File

@@ -1,4 +1,4 @@
name: 'Build'
name: 'CI: Build'
on:
push:
@@ -11,28 +11,23 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '18.x'
node-version: '16.x'
- name: 'Restore dependency cache'
id: cache-restore
uses: actions/cache@v3
uses: actions/cache@v2
with:
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') }}-
path: node_modules
key: ${{ 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
env:
ZIPLINE_DOCKER_BUILD: true
run: yarn build

41
.github/workflows/docker-arm.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: 'CD: Push ARM64 Docker Images'
on:
push:
branches: [ trunk ]
paths:
- 'src/**'
- 'server/**'
- 'prisma/**'
- '.github/**'
workflow_dispatch:
jobs:
push_to_ghcr:
name: Push Image to GitHub Packages
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v2
- name: Setup QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Login to Github Packages
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker Image
uses: docker/build-push-action@v2
with:
file: ./Dockerfile-arm
platforms: linux/arm64
push: true
tags: ghcr.io/diced/zipline/arm64:trunk

View File

@@ -1,51 +0,0 @@
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

View File

@@ -1,4 +1,4 @@
name: 'Push Docker Images'
name: 'CD: Push Docker Images'
on:
push:
@@ -8,7 +8,6 @@ on:
- 'server/**'
- 'prisma/**'
- '.github/**'
- 'Dockerfile'
workflow_dispatch:
jobs:
@@ -17,34 +16,33 @@ jobs:
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
uses: actions/checkout@v2
- name: Setup QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v1
- name: Login to Github Packages
uses: docker/login-action@v2
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build Docker Image
uses: docker/build-push-action@v3
uses: docker/build-push-action@v2
with:
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
ghcr.io/diced/zipline/zipline:trunk
ghcr.io/diced/zipline/amd64:trunk
diced/zipline:trunk

3
.gitignore vendored
View File

@@ -42,4 +42,5 @@ yarn-error.log*
# zipline
config.toml
uploads/
dist/
dist/
docker-compose.local.yml

1
.nvmrc
View File

@@ -1 +0,0 @@
18.12.0

View File

@@ -1,5 +0,0 @@
{
"singleQuote": true,
"jsxSingleQuote": true,
"printWidth": 110
}

View File

@@ -1,5 +0,0 @@
{
"editor.tabSize": 2,
"files.eol": "\n",
"typescript.tsdk": "node_modules/typescript/lib"
}

786
.yarn/releases/yarn-3.2.1.cjs vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,9 +1,7 @@
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
yarnPath: .yarn/releases/yarn-3.2.1.cjs

View File

@@ -1,23 +0,0 @@
# 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.

View File

@@ -1,65 +1,47 @@
FROM ghcr.io/diced/prisma-binaries:4.5.x as prisma
FROM node:alpine3.16 AS deps
RUN mkdir -p /prisma-engines
FROM node:16-alpine AS deps
WORKDIR /build
COPY .yarn .yarn
COPY package.json yarn.lock .yarnrc.yml ./
RUN apk add --no-cache libc6-compat
RUN yarn install --immutable
FROM node:alpine3.16 AS builder
FROM node:16-alpine 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 scripts ./scripts
COPY prisma ./prisma
COPY .yarn .yarn
COPY package.json yarn.lock .yarnrc.yml esbuild.config.js next.config.js next-env.d.ts zip-env.d.ts tsconfig.json mimes.json ./
COPY package.json yarn.lock .yarnrc.yml esbuild.config.js next.config.js next-env.d.ts zip-env.d.ts tsconfig.json ./
ENV ZIPLINE_DOCKER_BUILD 1
ENV NEXT_TELEMETRY_DISABLED 1
RUN yarn build
FROM node:alpine3.16 AS runner
FROM node:16-alpine 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
RUN apk add --no-cache perl procps
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
COPY --from=builder /build/.next ./.next
COPY --from=builder /build/node_modules ./node_modules
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/dist ./dist
COPY --from=builder --chown=zipline:zipline /build/node_modules ./node_modules
COPY --from=builder /build/next.config.js ./next.config.js
COPY --from=builder /build/esbuild.config.js ./esbuild.config.js
COPY --from=builder /build/src ./src
COPY --from=builder /build/dist ./dist
COPY --from=builder /build/scripts ./scripts
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
CMD ["node", "--enable-source-maps", "dist/server"]
USER zipline
CMD ["node", "dist/server"]

46
Dockerfile-arm Normal file
View File

@@ -0,0 +1,46 @@
FROM node:16 AS deps
WORKDIR /build
COPY .yarn .yarn
COPY package.json yarn.lock .yarnrc.yml ./
RUN yarn install --immutable
FROM node:16 AS builder
WORKDIR /build
COPY --from=deps /build/node_modules ./node_modules
COPY src ./src
COPY scripts ./scripts
COPY prisma ./prisma
COPY .yarn .yarn
COPY package.json yarn.lock .yarnrc.yml esbuild.config.js next.config.js next-env.d.ts zip-env.d.ts tsconfig.json ./
ENV ZIPLINE_DOCKER_BUILD 1
ENV NEXT_TELEMETRY_DISABLED 1
RUN yarn build
FROM node:16 AS runner
WORKDIR /zipline
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/dist ./dist
COPY --from=builder --chown=zipline:zipline /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/scripts ./scripts
COPY --from=builder /build/prisma ./prisma
COPY --from=builder /build/tsconfig.json ./tsconfig.json
COPY --from=builder /build/package.json ./package.json
USER zipline
CMD ["node", "dist/server"]

154
README.md
View File

@@ -1,159 +1,35 @@
<div align="center">
<img src="https://raw.githubusercontent.com/diced/zipline/trunk/public/zipline_small.png"/>
A ShareX/file upload server that is easy to use, packed with features, and with an easy setup!
![Stars](https://img.shields.io/github/stars/diced/zipline?logo=github&style=flat)
![Version](https://img.shields.io/github/package-json/v/diced/zipline?logo=git&logoColor=white&style=flat)
![GitHub last commit (branch)](https://img.shields.io/github/last-commit/diced/zipline/trunk?logo=git&logoColor=white&style=flat)
[![Discord](https://img.shields.io/discord/729771078196527176?color=%23777ed3&label=discord&logo=discord&logoColor=white&style=flat)](https://discord.gg/EAhCRfGxCF)
![Build](https://img.shields.io/github/workflow/status/diced/zipline/Build?logo=github&style=flat)
[![Docker Image (trunk)](https://img.shields.io/github/workflow/status/diced/zipline/Push%20Docker%20Images?label=Docker%20%28trunk%29&logo=github&style=flat)](https://github.com/diced/zipline/pkgs/container/zipline/?tag=trunk)
[![Docker Image (release)](https://img.shields.io/github/workflow/status/diced/zipline/Push%20Release%20Docker%20Images?label=Docker%20%28release%29&logo=github&style=flat)](https://github.com/diced/zipline/pkgs/container/zipline/?tag=latest)
Zipline is a ShareX/file upload server that is easy to use, packed with features and can be setup in one command!
![Build](https://img.shields.io/github/workflow/status/diced/zipline/CD:%20Push%20Docker%20Images?logo=github&style=flat-square)
![Stars](https://img.shields.io/github/stars/diced/zipline?logo=github&style=flat-square)
![Version](https://img.shields.io/github/package-json/v/diced/zipline?logo=git&logoColor=white&style=flat-square)
![GitHub last commit (branch)](https://img.shields.io/github/last-commit/diced/zipline/trunk?logo=git&logoColor=white&style=flat-square)
[![Discord](https://img.shields.io/discord/729771078196527176?color=%23777ed3&label=discord&logo=discord&logoColor=white&style=flat-square)](https://discord.gg/EAhCRfGxCF)
</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
- Code highlighting
- Fully customizable Discord webhook notifications
- OAuth2 registration (Discord and GitHub)
- Two-Factor authentication with Google Authenticator, Authy, etc (totp services).
- 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`)
<details>
<summary><h2>Screenshots (click)</h2></summary>
View full album at [imgur](https://imgur.com/a/GzyowZ7)
![Login Page](https://i.imgur.com/14Er7qt.png)
![Dashboard](https://i.imgur.com/3JK5bp6.png)
![Files Page](https://i.imgur.com/grIaDs8.png)
</details>
## Installing
[See how to install here](https://zipl.vercel.app/docs/get-started)
# Usage
## Configuration
[See how to configure here](https://zipl.vercel.app/docs/config/overview)
## 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).
<details>
<summary>Wayland instructions</summary>
If using wayland you will need to have [wl-clipboard](https://github.com/bugaevc/wl-clipboard) installed, for the `wl-copy` command.
If you are not using GNOME/KDE/Qtile/Sway, and are using something like a wlroots-based compositor (ex. [Hyprland](https://github.com/hyprwm/Hyprland/), [River](https://github.com/riverwm/river), etc), you will need to set the `XDG_CURRENT_DESKTOP` environment variable to `sway`, which will just override it for this script. Adding `export XDG_CURRENT_DESKTOP=sway` to the start of the script will work.
After this, replace the `xsel -ib` with `wl-copy` in the script.
</details>
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 and use the template, 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:
- Brief explanation of the feature in the title (very brief 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.
## Theming
[See how to theme here](https://zipl.vercel.app/docs/themes/reference)

View File

@@ -4,10 +4,9 @@
| Version | Supported |
| ------- | ------------------ |
| 3.4.8 | :white_check_mark: |
| 3.4.4 | :white_check_mark: |
| < 3 | :x: |
| < 2 | :x: |
## Reporting a Vulnerability
Report a Vulnerability by issuing a bug report, with exact details with how the vulnerability happened, what "exploits" can happen, and possible fixes (optional). Vulnerability reports are treated with high priority and will be resolved most of the time quickly.

19
config.example.toml Normal file
View File

@@ -0,0 +1,19 @@
[core]
secure = true
secret = 'changethis'
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']

46
docker-compose.arm.yml Normal file
View File

@@ -0,0 +1,46 @@
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:
image: ghcr.io/diced/zipline/arm64:trunk
ports:
- '3000:3000'
restart: unless-stopped
environment:
- SECURE=false
- SECRET=changethis
- HOST=0.0.0.0
- PORT=3000
- DATASOURCE_TYPE=local
- DATASOURCE_DIRECTORY=./uploads
- DATABASE_URL=postgresql://postgres:postgres@postgres/postgres/
- UPLOADER_ROUTE=/u
- UPLOADER_EMBED_ROUTE=/a
- UPLOADER_LENGTH=6
- UPLOADER_ADMIN_LIMIT=104900000
- UPLOADER_USER_LIMIT=104900000
- UPLOADER_DISABLED_EXTS=
- URLS_ROUTE=/go
- URLS_LENGTH=6
volumes:
- '$PWD/uploads:/zipline/uploads'
- '$PWD/public:/zipline/public'
depends_on:
- 'postgres'
volumes:
pg_data:

View File

@@ -22,8 +22,22 @@ services:
ports:
- '3000:3000'
restart: unless-stopped
env_file:
- .env.local
environment:
- SECURE=false
- SECRET=changethis
- HOST=0.0.0.0
- PORT=3000
- DATASOURCE_TYPE=local
- DATASOURCE_DIRECTORY=./uploads
- DATABASE_URL=postgresql://postgres:postgres@postgres/postgres/
- UPLOADER_ROUTE=/u
- UPLOADER_EMBED_ROUTE=/a
- UPLOADER_LENGTH=6
- UPLOADER_ADMIN_LIMIT=104900000
- UPLOADER_USER_LIMIT=104900000
- UPLOADER_DISABLED_EXTS=
- URLS_ROUTE=/go
- URLS_LENGTH=6
volumes:
- '$PWD/uploads:/zipline/uploads'
- '$PWD/public:/zipline/public'
@@ -31,4 +45,4 @@ services:
- 'postgres'
volumes:
pg_data:
pg_data:

View File

@@ -16,22 +16,31 @@ services:
retries: 5
zipline:
image: ghcr.io/diced/zipline
image: ghcr.io/diced/zipline/zipline:trunk
ports:
- '3000:3000'
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
- SECURE=false
- SECRET=changethis
- HOST=0.0.0.0
- PORT=3000
- DATASOURCE_TYPE=local
- DATASOURCE_DIRECTORY=./uploads
- DATABASE_URL=postgresql://postgres:postgres@postgres/postgres/
- UPLOADER_ROUTE=/u
- UPLOADER_EMBED_ROUTE=/a
- UPLOADER_LENGTH=6
- UPLOADER_ADMIN_LIMIT=104900000
- UPLOADER_USER_LIMIT=104900000
- UPLOADER_DISABLED_EXTS=
- URLS_ROUTE=/go
- URLS_LENGTH=6
volumes:
- './uploads:/zipline/uploads'
- '$PWD/uploads:/zipline/uploads'
- '$PWD/public:/zipline/public'
depends_on:
- 'postgres'
volumes:
pg_data:
pg_data:

View File

@@ -1,23 +1,40 @@
const esbuild = require('esbuild');
const { existsSync } = require('fs');
const { rm } = require('fs/promises');
const { recursiveReadDir } = require('next/dist/lib/recursive-readdir');
(async () => {
const watch = process.argv[2] === '--watch';
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',
bundle: false,
platform: 'node',
entryPoints,
treeShaking: true,
entryPoints: [
'src/server/index.ts',
'src/server/server.ts',
'src/server/util.ts',
'src/server/validateConfig.ts',
'src/lib/logger.ts',
'src/lib/readConfig.ts',
'src/lib/datasource/datasource.ts',
'src/lib/datasource/index.ts',
'src/lib/datasource/Local.ts',
'src/lib/datasource/S3.ts',
'src/lib/ds.ts',
'src/lib/config.ts',
],
format: 'cjs',
resolveExtensions: ['.ts', '.js'],
write: true,
sourcemap: true,
watch,
incremental: watch,
sourcemap: false,
minify: process.env.NODE_ENV === 'production',
});
})();
})();

1384
mimes.json

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,3 @@
/**
* @type {import('next').NextConfig}
**/
module.exports = {
async redirects() {
return [
@@ -11,16 +8,4 @@ module.exports = {
},
];
},
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,
};
};

View File

@@ -1,92 +1,69 @@
{
"name": "zipline",
"version": "3.6.3",
"version": "3.4.4",
"license": "MIT",
"scripts": {
"dev": "npm-run-all build:server dev:run",
"dev:run": "cross-env DEBUG=true REACT_EDITOR=code NODE_ENV=development RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED=false node --enable-source-maps dist/server",
"dev": "node esbuild.config.js && REACT_EDITOR=code-insiders NODE_ENV=development node 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 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": "node dist/scripts/read-config",
"scripts:import-dir": "node dist/scripts/import-dir",
"scripts:list-users": "node dist/scripts/list-users",
"scripts:set-user": "node dist/scripts/set-user"
"docker:build-dev": "docker-compose --file docker-compose.dev.yml up --build"
},
"dependencies": {
"@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",
"@iarna/toml": "2.2.5",
"@mantine/core": "^4.2.9",
"@mantine/dropzone": "^4.2.9",
"@mantine/hooks": "^4.2.9",
"@mantine/modals": "^4.2.9",
"@mantine/next": "^4.2.9",
"@mantine/notifications": "^4.2.9",
"@mantine/prism": "^4.2.9",
"@modulz/radix-icons": "^4.0.0",
"@prisma/client": "^3.15.2",
"@prisma/migrate": "^3.15.2",
"@prisma/sdk": "^3.15.2",
"@reduxjs/toolkit": "^1.8.2",
"argon2": "^0.28.5",
"aws-sdk": "^2.1156.0",
"colorette": "^2.0.19",
"cookie": "^0.5.0",
"dayjs": "^1.11.6",
"dotenv": "^16.0.3",
"dotenv-expand": "^9.0.0",
"exiftool-vendored": "^18.6.0",
"fflate": "^0.7.4",
"find-my-way": "^7.3.1",
"minio": "^7.0.32",
"ms": "canary",
"fecha": "^4.2.3",
"fflate": "^0.7.3",
"find-my-way": "^6.3.0",
"multer": "^1.4.5-lts.1",
"next": "^13.0.0",
"otplib": "^12.0.1",
"prisma": "^4.5.0",
"qrcode": "^1.5.1",
"next": "^12.1.6",
"prisma": "^3.15.2",
"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"
"react-redux": "^8.0.2",
"react-table": "^7.8.0",
"redux": "^4.2.0",
"redux-thunk": "^2.4.1",
"uuid": "^8.3.2",
"yup": "^0.32.11"
},
"devDependencies": {
"@types/cookie": "^0.5.1",
"@types/minio": "^7.0.14",
"@types/multer": "^1.4.7",
"@types/node": "^18.11.7",
"@types/qrcode": "^1.5.0",
"@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",
"@types/node": "^15.12.2",
"babel-plugin-import": "^1.13.5",
"esbuild": "^0.14.44",
"eslint": "^7.32.0",
"eslint-config-next": "12.1.6",
"npm-run-all": "^4.1.5",
"prettier": "^2.7.1",
"typescript": "^4.8.4"
"ts-node": "^10.8.1",
"typescript": "^4.7.3"
},
"repository": {
"type": "git",
"url": "https://github.com/diced/zipline.git"
},
"packageManager": "yarn@3.2.4"
"packageManager": "yarn@3.2.1"
}

View File

@@ -1,9 +0,0 @@
/*
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);

View File

@@ -1,17 +0,0 @@
-- 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;

View File

@@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE "Invite" ALTER COLUMN "expires_at" DROP NOT NULL,
ALTER COLUMN "expires_at" DROP DEFAULT;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Image" ADD COLUMN "expires_at" TIMESTAMP(3);

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "avatar" TEXT;

View File

@@ -1,4 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "oauth" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "oauthProvider" TEXT,
ALTER COLUMN "password" DROP NOT NULL;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "oauthAccessToken" TEXT;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "superAdmin" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Image" ADD COLUMN "maxViews" INTEGER;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Url" ADD COLUMN "maxViews" INTEGER;

View File

@@ -1,31 +0,0 @@
/*
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;

View File

@@ -1,5 +0,0 @@
-- DropIndex
DROP INDEX "OAuth_provider_key";
-- AlterTable
ALTER TABLE "OAuth" ADD COLUMN "refresh" TEXT;

View File

@@ -1,8 +0,0 @@
-- 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;

View File

@@ -1,2 +0,0 @@
-- AlterEnum
ALTER TYPE "OauthProviders" ADD VALUE 'GOOGLE';

View File

@@ -1,8 +0,0 @@
/*
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;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "totpSecret" TEXT;

View File

@@ -1,26 +0,0 @@
-- DropForeignKey
ALTER TABLE "InvisibleImage" DROP CONSTRAINT "InvisibleImage_imageId_fkey";
-- DropForeignKey
ALTER TABLE "InvisibleUrl" DROP CONSTRAINT "InvisibleUrl_urlId_fkey";
-- DropForeignKey
ALTER TABLE "Invite" DROP CONSTRAINT "Invite_createdById_fkey";
-- DropForeignKey
ALTER TABLE "Url" DROP CONSTRAINT "Url_userId_fkey";
-- AlterTable
ALTER TABLE "Url" ALTER COLUMN "userId" DROP NOT NULL;
-- AddForeignKey
ALTER TABLE "InvisibleImage" ADD CONSTRAINT "InvisibleImage_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Url" ADD CONSTRAINT "Url_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "InvisibleUrl" ADD CONSTRAINT "InvisibleUrl_urlId_fkey" FOREIGN KEY ("urlId") REFERENCES "Url"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Invite" ADD CONSTRAINT "Invite_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -10,22 +10,17 @@ generator client {
model User {
id Int @id @default(autoincrement())
username String
password String?
avatar String?
password String
token String
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}")
ratelimit DateTime?
totpSecret String?
ratelimited Boolean @default(false)
domains String[]
oauth OAuth[]
images Image[]
urls Url[]
Invite Invite[]
}
enum ImageFormat {
@@ -40,23 +35,21 @@ 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], onDelete: SetNull)
userId Int?
user User @relation(fields: [userId], references: [id])
userId Int
}
model InvisibleImage {
id Int @id @default(autoincrement())
invis String @unique
imageId Int @unique
image Image @relation(fields: [imageId], references: [id], onDelete: Cascade)
image Image @relation(fields: [imageId], references: [id])
}
model Url {
@@ -64,18 +57,17 @@ 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], onDelete: SetNull)
userId Int?
user User @relation(fields: [userId], references: [id])
userId Int
}
model InvisibleUrl {
id Int @id @default(autoincrement())
invis String @unique
urlId String @unique
url Url @relation(fields: [urlId], references: [id], onDelete: Cascade)
url Url @relation(fields: [urlId], references: [id])
}
model Stats {
@@ -83,29 +75,3 @@ model Stats {
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], onDelete: Cascade)
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
}

31
prisma/seed.ts Normal file
View File

@@ -0,0 +1,31 @@
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();
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

5
renovate.json Normal file
View File

@@ -0,0 +1,5 @@
{
"extends": [
"config:base"
]
}

38
scripts/exts.js Normal file
View File

@@ -0,0 +1,38 @@
// https://github.com/toptal/haste-server/blob/master/static/application.js#L167-L174
// Popular extension map
module.exports = {
rb: 'ruby',
py: 'python',
pl: 'perl',
php: 'php',
scala: 'scala',
go: 'go',
xml: 'xml',
html: 'xml',
htm: 'xml',
css: 'css',
js: 'javascript',
json: 'json',
vbs: 'vbscript',
lua: 'lua',
pas: 'delphi',
java: 'java',
cpp: 'cpp',
cc: 'cpp',
m: 'objectivec',
vala: 'vala',
sql: 'sql',
sm: 'smalltalk',
lisp: 'lisp',
ini: 'ini',
diff: 'diff',
bash: 'bash',
sh: 'bash',
tex: 'tex',
erl: 'erlang',
hs: 'haskell',
md: 'markdown',
txt: '',
coffee: 'coffee',
swift: 'swift',
};

35
scripts/migrate-v2-v3.js Normal file
View File

@@ -0,0 +1,35 @@
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();
})();

78
scripts/mimes.js Normal file
View File

@@ -0,0 +1,78 @@
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',
};

View File

@@ -0,0 +1,8 @@
import React from 'react';
import { LoadingOverlay } from '@mantine/core';
export default function Backdrop({ open }) {
return (
<LoadingOverlay visible={open} />
);
}

View File

@@ -1,10 +1,16 @@
import { Card as MCard, Title } from '@mantine/core';
import React from 'react';
import {
Card as MCard,
Title,
} from '@mantine/core';
export default function Card(props) {
const { name, children, ...other } = props;
export default function Card({ name, children, ...other }) {
return (
<MCard p='md' shadow='sm' {...other}>
{name && <Title order={2}>{name}</Title>}
<MCard padding='md' shadow='sm' {...other}>
<Title order={2}>{name}</Title>
{children}
</MCard>
);
}
}

View File

@@ -1,15 +0,0 @@
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} />;
}

View File

@@ -1,200 +0,0 @@
import { Button, Card, Group, LoadingOverlay, Modal, Stack, Text, Title, Tooltip } from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { showNotification } from '@mantine/notifications';
import { useFileDelete, useFileFavorite } from 'lib/queries/files';
import { relativeTime } from 'lib/utils/client';
import { useState } from 'react';
import {
CalendarIcon,
ClockIcon,
CopyIcon,
CrossIcon,
DeleteIcon,
ExternalLinkIcon,
EyeIcon,
FileIcon,
HashIcon,
ImageIcon,
StarIcon,
} from './icons';
import Link from './Link';
import MutedText from './MutedText';
import Type from './Type';
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, disableMediaPreview, exifEnabled }) {
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'>
{exifEnabled && (
<Link href={`/dashboard/metadata/${image.id}`} target='_blank' rel='noopener noreferrer'>
<Button leftIcon={<ExternalLinkIcon />}>View Metadata</Button>
</Link>
)}
<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>
</>
);
}

94
src/components/Image.tsx Normal file
View File

@@ -0,0 +1,94 @@
import React, { useState } from 'react';
import useFetch from 'hooks/useFetch';
import { Button, Card, Group, Image as MImage, Modal, Title } from '@mantine/core';
import { useNotifications } from '@mantine/notifications';
import { CopyIcon, Cross1Icon, StarIcon, TrashIcon } from '@modulz/radix-icons';
import { useClipboard } from '@mantine/hooks';
export default function Image({ image, updateImages }) {
const [open, setOpen] = useState(false);
const [t] = useState(image.mimetype.split('/')[0]);
const notif = useNotifications();
const clipboard = useClipboard();
const handleDelete = async () => {
const res = await useFetch('/api/user/files', 'DELETE', { id: image.id });
if (!res.error) {
updateImages(true);
notif.showNotification({
title: 'Image Deleted',
message: '',
color: 'green',
icon: <TrashIcon />,
});
} else {
notif.showNotification({
title: 'Failed to delete image',
message: res.error,
color: 'red',
icon: <Cross1Icon />,
});
}
setOpen(false);
};
const handleCopy = () => {
clipboard.copy(`${window.location.protocol}//${window.location.host}${image.url}`);
setOpen(false);
notif.showNotification({
title: 'Copied to clipboard',
message: '',
icon: <CopyIcon />,
});
};
const handleFavorite = async () => {
const data = await useFetch('/api/user/files', 'PATCH', { id: image.id, favorite: !image.favorite });
if (!data.error) updateImages(true);
notif.showNotification({
title: 'Image is now ' + (!image.favorite ? 'favorited' : 'unfavorited'),
message: '',
icon: <StarIcon />,
});
};
const Type = (props) => {
return {
'video': <video controls {...props} />,
'image': <MImage {...props} />,
'audio': <audio controls {...props} />,
}[t];
};
return (
<>
<Modal
opened={open}
onClose={() => setOpen(false)}
title={<Title>{image.file}</Title>}
>
<Type
src={image.url}
alt={image.file}
/>
<Group position='right' mt={22}>
<Button onClick={handleCopy}>Copy</Button>
<Button onClick={handleDelete}>Delete</Button>
<Button onClick={handleFavorite}>{image.favorite ? 'Unfavorite' : 'Favorite'}</Button>
</Group>
</Modal>
<Card sx={{ maxWidth: '100%', height: '100%' }} shadow='md'>
<Card.Section>
<Type
sx={{ maxHeight: 320, fontSize: 70, width: '100%', cursor: 'pointer' }}
style={{ maxHeight: 320, fontSize: 70, width: '100%', cursor: 'pointer' }}
src={image.url}
alt={image.file}
onClick={() => setOpen(true)}
/>
</Card.Section>
</Card>
</>
);
}

View File

@@ -0,0 +1,158 @@
/* eslint-disable react/jsx-key */
/* eslint-disable react/display-name */
// Code taken from https://codesandbox.io/s/eojw8 and is modified a bit (the entire src/components/table directory)
import React from 'react';
import {
usePagination,
useTable,
} from 'react-table';
import {
ActionIcon,
createStyles,
Divider,
Group,
Pagination,
Select,
Table,
Text,
useMantineTheme,
} from '@mantine/core';
import {
CopyIcon,
EnterIcon,
TrashIcon,
} from '@modulz/radix-icons';
const pageSizeOptions = ['10', '25', '50'];
const useStyles = createStyles((t) => ({
root: { height: '100%', display: 'block', marginTop: 10 },
tableContainer: {
display: 'block',
overflow: 'auto',
'& > table': {
'& > thead': { backgroundColor: t.colorScheme === 'dark' ? t.colors.dark[6] : t.colors.gray[0], zIndex: 1 },
'& > thead > tr > th': { padding: t.spacing.md },
'& > tbody > tr > td': { padding: t.spacing.md },
},
borderRadius: 6,
},
stickHeader: { top: 0, position: 'sticky' },
disableSortIcon: { color: t.colors.gray[5] },
sortDirectionIcon: { transition: 'transform 200ms ease' },
}));
export default function ImagesTable({
columns,
data = [],
serverSideDataSource = false,
initialPageSize = 10,
initialPageIndex = 0,
pageCount = 0,
total = 0,
deleteImage, copyImage, viewImage,
}) {
const { classes } = useStyles();
const theme = useMantineTheme();
const tableOptions = useTable(
{
data,
columns,
pageCount,
initialState: { pageSize: initialPageSize, pageIndex: initialPageIndex },
},
usePagination
);
const {
getTableProps, getTableBodyProps, headerGroups, rows, prepareRow, page, gotoPage, setPageSize, state: { pageIndex, pageSize },
} = tableOptions;
const getPageRecordInfo = () => {
const firstRowNum = pageIndex * pageSize + 1;
const totalRows = serverSideDataSource ? total : rows.length;
const currLastRowNum = (pageIndex + 1) * pageSize;
let lastRowNum = currLastRowNum < totalRows ? currLastRowNum : totalRows;
return `${firstRowNum} - ${lastRowNum} of ${totalRows}`;
};
const getPageCount = () => {
const totalRows = serverSideDataSource ? total : rows.length;
return Math.ceil(totalRows / pageSize);
};
const handlePageChange = (pageNum) => gotoPage(pageNum - 1);
const renderHeader = () => headerGroups.map(hg => (
<tr {...hg.getHeaderGroupProps()}>
{hg.headers.map(column => (
<th {...column.getHeaderProps()}>
<Group noWrap position={column.align || 'apart'}>
<div>{column.render('Header')}</div>
</Group>
</th>
))}
<th>Actions</th>
</tr>
));
const renderRow = rows => rows.map(row => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => (
<td align={cell.column.align || 'left'} {...cell.getCellProps()}>
{cell.render('Cell')}
</td>
))}
<td align='right'>
<Group noWrap>
<ActionIcon color='red' variant='outline' onClick={() => deleteImage(row)}><TrashIcon /></ActionIcon>
<ActionIcon color='primary' variant='outline' onClick={() => copyImage(row)}><CopyIcon /></ActionIcon>
<ActionIcon color='green' variant='outline' onClick={() => viewImage(row)}><EnterIcon /></ActionIcon>
</Group>
</td>
</tr>
);
});
return (
<div className={classes.root}>
<div
className={classes.tableContainer}
style={{ height: 'calc(100% - 44px)' }}
>
<Table {...getTableProps()}>
<thead style={{ backgroundColor: theme.other.hover }}>
{renderHeader()}
</thead>
<tbody {...getTableBodyProps()}>
{renderRow(page)}
</tbody>
</Table>
</div>
<Divider mb='md' variant='dotted' />
<Group position='left'>
<Text size='sm'>Rows per page: </Text>
<Select
style={{ width: '72px' }}
variant='filled'
data={pageSizeOptions}
value={pageSize + ''}
onChange={pageSize => setPageSize(Number(pageSize))} />
<Divider orientation='vertical' />
<Text size='sm'>{getPageRecordInfo()}</Text>
<Divider orientation='vertical' />
<Pagination
page={pageIndex + 1}
total={getPageCount()}
onChange={handlePageChange} />
</Group>
</div>
);
}

View File

@@ -1,64 +1,20 @@
import {
AppShell,
Badge,
Box,
Burger,
Button,
Group,
Header,
Image,
MediaQuery,
Menu,
Navbar,
NavLink,
Paper,
Popover,
ScrollArea,
Select,
Stack,
Text,
Title,
Tooltip,
UnstyledButton,
useMantineTheme,
} 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 React, { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { useRecoilState } from 'recoil';
import {
ActivityIcon,
CheckIcon,
CopyIcon,
CrossIcon,
DeleteIcon,
DiscordIcon,
ExternalLinkIcon,
FileIcon,
GitHubIcon,
GoogleIcon,
HomeIcon,
LinkIcon,
LogoutIcon,
PencilIcon,
SettingsIcon,
TagIcon,
TypeIcon,
UploadIcon,
UserIcon,
} from './icons';
import { useStoreDispatch } from 'lib/redux/store';
import { updateUser } from 'lib/redux/reducers/user';
import useFetch from 'hooks/useFetch';
import { CheckIcon, CopyIcon, Cross1Icon, FileIcon, GearIcon, HomeIcon, Link1Icon, ResetIcon, UploadIcon, PinRightIcon, PersonIcon, Pencil1Icon, MixerHorizontalIcon } from '@modulz/radix-icons';
import { AppShell, Burger, Divider, Group, Header, MediaQuery, Navbar, Paper, Popover, ScrollArea, Select, Text, ThemeIcon, Title, UnstyledButton, useMantineTheme, Box } from '@mantine/core';
import { useModals } from '@mantine/modals';
import { useNotifications } from '@mantine/notifications';
import { useClipboard } from '@mantine/hooks';
import { friendlyThemeName, themes } from './Theming';
function MenuItemLink(props) {
return (
<Link href={props.href} passHref legacyBehavior>
<Link href={props.href} passHref>
<MenuItem {...props} />
</Link>
);
@@ -67,7 +23,7 @@ function MenuItemLink(props) {
function MenuItem(props) {
return (
<UnstyledButton
sx={(theme) => ({
sx={theme => ({
display: 'block',
width: '100%',
padding: 5,
@@ -75,34 +31,30 @@ function MenuItem(props) {
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,
? theme.colors.dark[0]
: theme.black,
'&:hover': {
backgroundColor: props.color
? theme.fn.rgba(
theme.fn.themeColor(props.color, theme.colorScheme === 'dark' ? 9 : 0),
theme.colorScheme === 'dark' ? 0.2 : 1
)
: theme.colorScheme === 'dark'
? theme.fn.rgba(theme.colors.dark[3], 0.35)
: theme.colors.gray[0],
},
})}
{...props}
>
<Group noWrap>
<Box
sx={(theme) => ({
marginRight: theme.spacing.xs / 4,
paddingLeft: theme.spacing.xs / 2,
<Box sx={theme => ({
marginRight: theme.spacing.xs / 4,
paddingLeft: theme.spacing.xs / 2,
'& *': {
display: 'block',
},
})}
>
'& *': {
display: 'block',
},
})}>
{props.icon}
</Box>
<Text size='sm'>{props.children}</Text>
@@ -113,236 +65,189 @@ function MenuItem(props) {
const items = [
{
icon: <HomeIcon size={18} />,
icon: <HomeIcon />,
text: 'Home',
link: '/dashboard',
},
{
icon: <FileIcon size={18} />,
icon: <FileIcon />,
text: 'Files',
link: '/dashboard/files',
},
{
icon: <ActivityIcon size={18} />,
icon: <MixerHorizontalIcon />,
text: 'Stats',
link: '/dashboard/stats',
},
{
icon: <LinkIcon size={18} />,
icon: <Link1Icon />,
text: 'URLs',
link: '/dashboard/urls',
},
{
icon: <UploadIcon size={18} />,
icon: <UploadIcon />,
text: 'Upload',
link: '/dashboard/upload/file',
},
{
icon: <TypeIcon size={18} />,
text: 'Upload Text',
link: '/dashboard/upload/text',
link: '/dashboard/upload',
},
];
const admin_items = [
{
icon: <UserIcon size={18} />,
text: 'Users',
link: '/dashboard/users',
if: () => true,
},
{
icon: <TagIcon size={18} />,
text: 'Invites',
link: '/dashboard/invites',
if: (props) => props.invites,
},
];
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,
};
for (const provider of oauth_providers) {
provider.Icon = icons[provider.name];
}
const external_links = JSON.parse(props.external_links ?? '[]');
export default function Layout({ children, user }) {
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 notif = useNotifications();
const clipboard = useClipboard();
const handleUpdateTheme = async (value) => {
const handleUpdateTheme = async value => {
const newUser = await useFetch('/api/user', 'PATCH', {
systemTheme: value || 'dark_blue',
});
setSystemTheme(newUser.systemTheme);
setUser(newUser);
dispatch(updateUser(newUser));
router.replace(router.pathname);
showNotification({
notif.showNotification({
title: `Theme changed to ${friendlyThemeName[value]}`,
message: '',
color: 'green',
icon: <PencilIcon />,
icon: <Pencil1Icon />,
});
};
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 />,
});
}
modals.closeAll();
},
});
const openCopyToken = () =>
modals.openConfirmModal({
title: <Title>Copy Token</Title>,
children: (
<Text size='sm'>
Make sure you don&apos;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.',
const openResetToken = () => modals.openConfirmModal({
title: 'Reset Token',
centered: true,
children: (
<Text size='sm'>
Once you reset your token, you will have to update any uploaders to use this new token.
</Text>
),
labels: { confirm: 'Reset', cancel: 'Cancel' },
onConfirm: async () => {
const a = await useFetch('/api/user/token', 'PATCH');
if (!a.success) {
setToken(a.success);
notif.showNotification({
title: 'Token Reset Failed',
message: a.error,
color: 'red',
icon: <Cross1Icon />,
});
} else {
notif.showNotification({
title: 'Token Reset',
message: 'Your token has been reset. You will need to update any uploaders to use this new token.',
color: 'green',
icon: <CheckIcon />,
});
}
modals.closeAll();
},
});
modals.closeAll();
},
});
const openCopyToken = () => modals.openConfirmModal({
title: 'Copy Token',
centered: true,
children: (
<Text size='sm'>
Make sure you don&apos;t share this token with anyone as they will be able to upload files on your behalf.
</Text>
),
labels: { confirm: 'Copy', cancel: 'Cancel' },
onConfirm: async () => {
clipboard.copy(token);
notif.showNotification({
title: 'Token Copied',
message: 'Your token has been copied to your clipboard.',
color: 'green',
icon: <CheckIcon />,
});
modals.closeAll();
},
});
return (
<AppShell
navbarOffsetBreakpoint='sm'
fixed
navbar={
<Navbar pt='sm' hiddenBreakpoint='sm' hidden={!opened} width={{ sm: 200, lg: 230 }}>
<Navbar.Section grow component={ScrollArea}>
<Navbar
padding='md'
hiddenBreakpoint='sm'
hidden={!opened}
width={{ sm: 200, lg: 230 }}
>
<Navbar.Section
grow
component={ScrollArea}
ml={-10}
mr={-10}
sx={{ paddingLeft: 10, paddingRight: 10 }}
>
{items.map(({ icon, text, link }) => (
<Link href={link} key={text} passHref legacyBehavior>
<NavLink
component='a'
label={text}
icon={icon}
active={router.pathname === link}
variant='light'
/>
<Link href={link} key={text} passHref>
<UnstyledButton
sx={{
display: 'block',
width: '100%',
padding: theme.spacing.xs,
borderRadius: theme.radius.sm,
color: theme.colorScheme === 'dark' ? theme.colors.dark[0] : theme.black,
'&:hover': {
backgroundColor: theme.other.hover,
},
}}
>
<Group>
<ThemeIcon color='primary' variant='filled'>
{icon}
</ThemeIcon>
<Text size='lg'>{text}</Text>
</Group>
</UnstyledButton>
</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>
<Link href='/dashboard/users' passHref>
<UnstyledButton
sx={{
display: 'block',
width: '100%',
padding: theme.spacing.xs,
borderRadius: theme.radius.sm,
color: theme.colorScheme === 'dark' ? theme.colors.dark[0] : theme.black,
'&:hover': {
backgroundColor: theme.other.hover,
},
}}
>
<Group>
<ThemeIcon color='primary' variant='filled'>
<PersonIcon />
</ThemeIcon>
<Text size='lg'>Users</Text>
</Group>
</UnstyledButton>
</Link>
)}
</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'>
<Header height={70} padding='md'>
<div style={{ display: 'flex', alignItems: 'center', height: '100%' }}>
<MediaQuery largerThan='sm' styles={{ display: 'none' }}>
<Burger
@@ -352,116 +257,75 @@ export default function Layout({ children, props }) {
color={theme.colors.gray[6]}
/>
</MediaQuery>
<Title ml='sm'>{title}</Title>
<Title sx={{ marginLeft: 12 }}>Zipline</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',
<Popover
position='top'
placement='end'
spacing={4}
opened={open}
onClose={() => setOpen(false)}
target={
<UnstyledButton
onClick={() => setOpen(!open)}
sx={{
display: 'block',
width: '100%',
padding: theme.spacing.xs,
borderRadius: theme.radius.sm,
color: theme.other.color,
'&:hover': {
backgroundColor: t.other.hover,
backgroundColor: theme.other.hover,
},
color: t.colorScheme === 'dark' ? 'white' : 'black',
})}
size='xl'
p='sm'
}}
>
{user.username}
</Button>
</Popover.Target>
<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>
<Group>
<ThemeIcon color='primary' variant='filled'>
<GearIcon />
</ThemeIcon>
<Text>{user.username}</Text>
</Group>
</UnstyledButton>
}
>
<Group direction='column' spacing={2}>
<Text sx={{
color: theme.colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6],
fontWeight: 500,
fontSize: theme.fontSizes.xs,
padding: `${theme.spacing.xs / 2}px ${theme.spacing.sm}px`,
cursor: 'default',
}}>User: {user.username}</Text>
<MenuItemLink icon={<GearIcon />} href='/dashboard/manage'>Manage Account</MenuItemLink>
<MenuItem icon={<CopyIcon />} onClick={() => {setOpen(false);openCopyToken();}}>Copy Token</MenuItem>
<MenuItem icon={<ResetIcon />} onClick={() => {setOpen(false);openResetToken();}} color='red'>Reset Token</MenuItem>
<MenuItemLink icon={<PinRightIcon />} href='/auth/logout' color='red'>Logout</MenuItemLink>
<Divider
variant='solid'
my={theme.spacing.xs / 2}
sx={theme => ({
width: '110%',
borderTopColor: theme.colorScheme === 'dark' ? theme.colors.dark[7] : theme.colors.gray[2],
margin: `${theme.spacing.xs / 2}px -4px`,
})}
/>
<MenuItem icon={<Pencil1Icon />}>
<Select
size='xs'
data={Object.keys(themes).map(t => ({ value: t, label: friendlyThemeName[t] }))}
value={systemTheme}
onChange={handleUpdateTheme}
/>
</MenuItem>
</Group>
</Popover>
</Box>
</div>
</Header>
}
>
<Paper
withBorder
p='md'
shadow='xs'
sx={(t) => ({
borderColor: t.colorScheme === 'dark' ? t.colors.dark[5] : t.colors.dark[0],
})}
>
{children}
</Paper>
<Paper withBorder padding='md' shadow='xs'>{children}</Paper>
</AppShell>
);
}
}

View File

@@ -1,5 +1,76 @@
import { NextLink } from '@mantine/next';
/// 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 { Text } from '@mantine/core';
export default function Link(props) {
return <NextLink legacyBehavior {...props} />;
}
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 <Text variant='link' component='a' className={className} href={href} ref={ref} {...other} />;
}
return <Text component='a' variant='link' href={href} ref={ref} {...other} />;
}
if (noLinkStyle) {
return <NextLinkComposed className={className} ref={ref} to={href} {...other} />;
}
return (
<Text
component={NextLinkComposed}
variant='link'
linkAs={linkAs}
className={className}
ref={ref}
to={href}
{...other}
/>
);
});
export default Link;

View File

@@ -1,9 +0,0 @@
import { Text } from '@mantine/core';
export default function MutedText({ children, ...props }) {
return (
<Text color='dimmed' size='xl' {...props}>
{children}
</Text>
);
}

View File

@@ -1,75 +0,0 @@
// https://mantine.dev/core/password-input/
import { Box, PasswordInput, Popover, Progress, Text } from '@mantine/core';
import { useState } from 'react';
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>
);
}

View File

@@ -1,27 +0,0 @@
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>
);
}

View File

@@ -1,71 +0,0 @@
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>
);
}

View File

@@ -0,0 +1,6 @@
import React from 'react';
import { Text } from '@mantine/core';
export default function StatText({ children }) {
return <Text color='gray' size='xl'>{children}</Text>;
}

View File

@@ -1,26 +1,25 @@
import { useEffect } from 'react';
import React, { useEffect } from 'react';
// themes
import ayu_dark from 'lib/themes/ayu_dark';
import ayu_light from 'lib/themes/ayu_light';
import ayu_mirage from 'lib/themes/ayu_mirage';
import dark from 'lib/themes/dark';
import dark_blue from 'lib/themes/dark_blue';
import dracula from 'lib/themes/dracula';
import light_blue from 'lib/themes/light_blue';
import matcha_dark_azul from 'lib/themes/matcha_dark_azul';
import 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 dracula from 'lib/themes/dracula';
import matcha_dark_azul from 'lib/themes/matcha_dark_azul';
import qogir_dark from 'lib/themes/qogir_dark';
import { createEmotionCache, MantineProvider, MantineThemeOverride } from '@mantine/core';
import { useColorScheme } from '@mantine/hooks';
import { useStoreSelector } from 'lib/redux/store';
import { MantineProvider, MantineThemeOverride } from '@mantine/core';
import { ModalsProvider } from '@mantine/modals';
import { NotificationsProvider } from '@mantine/notifications';
import { userSelector } from 'lib/recoil/user';
import { useRecoilValue } from 'recoil';
import { useColorScheme } from '@mantine/hooks';
export const themes = {
system: (colorScheme: 'dark' | 'light') => (colorScheme === 'dark' ? dark_blue : light_blue),
system: (colorScheme: 'dark' | 'light') => colorScheme === 'dark' ? dark_blue : light_blue,
dark_blue,
light_blue,
dark,
@@ -34,23 +33,21 @@ export const themes = {
};
export const friendlyThemeName = {
system: 'System Theme',
dark_blue: 'Dark Blue',
light_blue: 'Light Blue',
dark: 'Very Dark',
ayu_dark: 'Ayu Dark',
ayu_mirage: 'Ayu Mirage',
ayu_light: 'Ayu Light',
nord: 'Nord',
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',
};
const cache = createEmotionCache({ key: 'zipline' });
export default function ZiplineTheming({ Component, pageProps, ...props }) {
const user = useRecoilValue(userSelector);
const user = useStoreSelector(state => state.user);
const colorScheme = useColorScheme();
let theme: MantineThemeOverride;
@@ -67,66 +64,24 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
<MantineProvider
withGlobalStyles
withNormalizeCSS
emotionCache={cache}
theme={{
...theme,
fontFamily: 'Ubuntu, sans-serif',
fontFamilyMonospace: 'Ubuntu Mono, monospace',
headings: {
fontFamily: 'Ubuntu, sans-serif',
theme={theme}
styles={{
AppShell: t => ({
root: {
backgroundColor: t.other.AppShell_backgroundColor,
},
}),
Popover: {
inner: {
width: 200,
},
},
components: {
AppShell: {
styles: (t) => ({
root: {
backgroundColor: t.other.AppShell_backgroundColor,
},
}),
Accordion: {
itemTitle: {
border: 0,
},
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',
shadow: 'lg',
},
},
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],
},
}),
itemOpened: {
border: 0,
},
},
}}
@@ -138,4 +93,4 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
</ModalsProvider>
</MantineProvider>
);
}
}

View File

@@ -1,87 +0,0 @@
import { Box, Center, Group, Image, Text } from '@mantine/core';
import { Prism } from '@mantine/prism';
import { useEffect, useState } from 'react';
import { AudioIcon, FileIcon, ImageIcon, PlayIcon } from './icons';
function PlaceholderContent({ text, Icon }) {
return (
<Group sx={(t) => ({ color: t.colors.dark[2] })}>
<Icon size={48} />
<Text size='md'>{text}</Text>
</Group>
);
}
function Placeholder({ text, Icon, ...props }) {
if (props.disableResolve) props.src = null;
return (
<Box sx={{ height: 200 }} {...props}>
<Center sx={{ height: 200 }}>
<PlaceholderContent text={text} Icon={Icon} />
</Center>
</Box>
);
}
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
placeholder={<PlaceholderContent Icon={FileIcon} text={'Image failed to load...'} />}
{...props}
/>
),
audio: <audio autoPlay controls {...props} style={{ width: '100%' }} />,
text: (
<Prism withLineNumbers language={name.split('.').pop()} {...props} style={{}} sx={{}}>
{text}
</Prism>
),
}[type]
) : (
<Text>Can&apos;t preview {file.type || file.mimetype}</Text>
)
) : media ? (
{
video: <Placeholder Icon={PlayIcon} text={`Click to view video (${name})`} {...props} />,
image: (
<Image
placeholder={<PlaceholderContent Icon={ImageIcon} text={'Image failed to load...'} />}
{...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} />
);
}

View File

@@ -1,21 +0,0 @@
import { Group, Text, useMantineTheme } from '@mantine/core';
import { Dropzone as MantineDropzone } from '@mantine/dropzone';
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>
);
}

View File

@@ -1,51 +0,0 @@
import { Badge, Group, HoverCard, Table, useMantineTheme } from '@mantine/core';
import Type from 'components/Type';
export function FilePreview({ file }: { file: File }) {
return (
<Type
file={file}
autoPlay
sx={{ maxWidth: '10vw', maxHeight: '100vh' }}
style={{ maxWidth: '10vw', maxHeight: '100vh' }}
src={URL.createObjectURL(file)}
alt={file.name}
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>
);
}

View File

@@ -1,5 +0,0 @@
import { Activity } from 'react-feather';
export default function ActivityIcon({ ...props }) {
return <Activity size={15} {...props} />;
}

View File

@@ -1,5 +0,0 @@
import { Disc } from 'react-feather';
export default function AudioIcon({ ...props }) {
return <Disc size={15} {...props} />;
}

View File

@@ -1,5 +0,0 @@
import { Calendar } from 'react-feather';
export default function CalendarIcon({ ...props }) {
return <Calendar size={15} {...props} />;
}

View File

@@ -1,5 +0,0 @@
import { Check } from 'react-feather';
export default function CheckIcon({ ...props }) {
return <Check size={15} {...props} />;
}

View File

@@ -1,5 +0,0 @@
import { Clock } from 'react-feather';
export default function ClockIcon({ ...props }) {
return <Clock size={15} {...props} />;
}

View File

@@ -1,5 +0,0 @@
import { Copy } from 'react-feather';
export default function CopyIcon({ ...props }) {
return <Copy size={15} {...props} />;
}

View File

@@ -1,5 +0,0 @@
import { X } from 'react-feather';
export default function CrossIcon({ ...props }) {
return <X size={15} {...props} />;
}

View File

@@ -1,5 +0,0 @@
import { Delete } from 'react-feather';
export default function DeleteIcon({ ...props }) {
return <Delete size={15} {...props} />;
}

View File

@@ -1,21 +0,0 @@
// 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>
);
}

View File

@@ -1,5 +0,0 @@
import { Download } from 'react-feather';
export default function DownloadIcon({ ...props }) {
return <Download size={15} {...props} />;
}

View File

@@ -1,5 +0,0 @@
import { LogIn } from 'react-feather';
export default function EnterIcon({ ...props }) {
return <LogIn size={15} {...props} />;
}

View File

@@ -1,5 +0,0 @@
import { ExternalLink } from 'react-feather';
export default function ExternalLinkIcon({ ...props }) {
return <ExternalLink size={15} {...props} />;
}

View File

@@ -1,5 +0,0 @@
import { Eye } from 'react-feather';
export default function EyeIcon({ ...props }) {
return <Eye size={15} {...props} />;
}

View File

@@ -1,5 +0,0 @@
import { File } from 'react-feather';
export default function FileIcon({ ...props }) {
return <File size={15} {...props} />;
}

View File

@@ -1,15 +0,0 @@
// 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}
/>
);
}

View File

@@ -1,17 +0,0 @@
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>
);
}

View File

@@ -1,15 +0,0 @@
// 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}
/>
);
}

View File

@@ -1,5 +0,0 @@
import { Hash } from 'react-feather';
export default function HashIcon({ ...props }) {
return <Hash size={15} {...props} />;
}

View File

@@ -1,5 +0,0 @@
import { Home } from 'react-feather';
export default function HomeIcon({ ...props }) {
return <Home size={15} {...props} />;
}

View File

@@ -1,5 +0,0 @@
import { Image as FeatherImage } from 'react-feather';
export default function ImageIcon({ ...props }) {
return <FeatherImage size={15} {...props} />;
}

View File

@@ -1,5 +0,0 @@
import { Key } from 'react-feather';
export default function KeyIcon({ ...props }) {
return <Key size={15} {...props} />;
}

View File

@@ -1,5 +0,0 @@
import { Link } from 'react-feather';
export default function LinkIcon({ ...props }) {
return <Link size={15} {...props} />;
}

View File

@@ -1,5 +0,0 @@
import { LogOut } from 'react-feather';
export default function LogoutIcon({ ...props }) {
return <LogOut size={15} {...props} />;
}

View File

@@ -1,5 +0,0 @@
import { Edit2 } from 'react-feather';
export default function PencilIcon({ ...props }) {
return <Edit2 size={15} {...props} />;
}

View File

@@ -1,5 +0,0 @@
import { Play } from 'react-feather';
export default function PlayIcon({ ...props }) {
return <Play size={15} {...props} />;
}

View File

@@ -1,5 +0,0 @@
import { Plus } from 'react-feather';
export default function PlusIcon({ ...props }) {
return <Plus size={15} {...props} />;
}

View File

@@ -1,5 +0,0 @@
import { RefreshCw } from 'react-feather';
export default function RefreshIcon({ ...props }) {
return <RefreshCw size={15} {...props} />;
}

View File

@@ -1,5 +0,0 @@
import { Settings } from 'react-feather';
export default function SettingsIcon({ ...props }) {
return <Settings size={15} {...props} />;
}

Some files were not shown because too many files have changed in this diff Show More