Compare commits

...

144 Commits

Author SHA1 Message Date
dicedtomato
12baadd563 fix: revert version 2022-07-15 21:29:44 +00:00
dicedtomato
f5ae36d4e7 feat: version 3.4.9 2022-07-15 21:28:27 +00:00
dicedtomato
04ca738fb1 feat: add more mimetypes! 2022-07-15 21:27:01 +00:00
dicedtomato
95e09e51e1 fix: add title to Layout 2022-07-15 17:34:32 +00:00
dicedtomato
2f0af385c7 feat: add content-length headers 2022-07-15 17:20:24 +00:00
dicedtomato
786e6d5799 feat: new configuration options 2022-07-14 03:05:16 +00:00
dicedtomato
61c5df750a feat: invitations to create accounts 2022-07-14 02:31:23 +00:00
dicedtomato
eb30afcb83 remove: meta config (will be added another day) 2022-07-13 16:32:56 +00:00
dicedtomato
cdf0f6e96c feat: versioned docker images 2022-07-13 04:32:52 +00:00
dicedtomato
54158c5dbe refactor: remove config file in favor for env variables 2022-07-13 02:50:25 +00:00
dicedtomato
56ff86db44 feat: revamp file gallery 2022-07-12 22:09:57 +00:00
dicedtomato
b7560c80aa fix: make dropzone larger 2022-07-10 05:43:19 +00:00
dicedtomato
03379943de fix: clean up upload components 2022-07-10 05:38:53 +00:00
dicedtomato
2376fd8968 feat: switch from radix-icons to feathericons 2022-07-10 00:46:15 +00:00
dicedtomato
2f90193d7e feat: add text uplading 2022-07-09 23:54:55 +00:00
dicedtomato
964199f8a9 fix(config): make s3/swift optional and not error out 2022-07-09 20:35:21 +00:00
dicedtomato
678ea20004 fix(docker): build 2022-07-08 02:57:31 +00:00
dicedtomato
ea27fd8a45 feat(3.4.8): fix bug where you can crash zipline 2022-07-08 02:52:19 +00:00
dicedtomato
38eef3f0ad feat(v3.4.7): version 2022-07-06 17:03:44 +00:00
dicedtomato
22615e9ce9 fix: build 2022-07-06 17:01:12 +00:00
dicedtomato
a999abfbf8 feat: a bunch of changes 2022-07-06 16:57:39 +00:00
dicedtomato
20c1d3ef08 Merge branch 'trunk' of https://github.com/diced/zipline into trunk 2022-06-26 17:58:14 +00:00
Adil Mohiuddin
b06c8e4918 fix: add link to xsel (#157) 2022-06-25 21:41:42 -07:00
dicedtomato
6edfdcefcc fix(s3): use smaller libraries 2022-06-25 17:07:19 +00:00
dicedtomato
10b145b006 fix(docker): use prebuilt binaries 2022-06-25 00:01:23 +00:00
dicedtomato
0ba9a9659d fix(docker): build prisma so it works on alpine arm64 2022-06-24 18:27:35 +00:00
cstef
2dfa1b6b14 feat: add Openstack Swift support (#154)
* Add Openstack support

* Fix datasource getting

* Restore example config + remove useless vscode dir

* Restore example config + remove useless vscode dir

* Add config.ts to the entryPoints list

* Fix indenting problems with switch-case

* Replace openstack-swift-client

* Add openstack to the config validator

* Rename Openstack to Swift

* Better error handling for swift

* More error handling

* Update Swift.ts

Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
2022-06-21 17:54:05 -07:00
dicedtomato
7a3f9f1fa4 fix: maybe fix action idk 2022-06-20 22:43:21 -07:00
diced
f276fdc6a0 feat(docker): remove arm docker stuff in favor of cross arch dockerfile 2022-06-20 22:29:33 -07:00
diced
7963bdd1e4 feat(v3.4.6): version & fixes 2022-06-20 17:40:36 -07:00
diced
195c57edc3 refactor(server): clean up server code 2022-06-20 17:09:52 -07:00
diced
4442c85dc1 fix(server): prisma migrations dont exit (high cpu) 2022-06-20 17:08:43 -07:00
diced
5bcac2a2b0 refactor(readme): more info 2022-06-20 15:07:15 -07:00
diced
5303b67d11 fix(pages): add blur to password modal 2022-06-20 14:35:26 -07:00
diced
af59e9abb8 fix(pages): videos supported on embeded uploads 2022-06-20 14:33:10 -07:00
diced
fb098c9147 feat(page): add preview on hover & progress bar 2022-06-20 14:12:57 -07:00
diced
739974bef4 refactor(config): move into directory 2022-06-20 10:55:43 -07:00
diced
d21e48a1a3 fix(api): improve ratelimits 2022-06-20 10:49:24 -07:00
diced
8fea0cbe77 refactor: clean up datasource stuff 2022-06-20 10:25:22 -07:00
diced
1e2b8efb13 feat(v3.4.5): version 2022-06-19 17:46:45 -07:00
diced
8495963094 feat(v3.4.5): exporting images and more stuff 2022-06-19 17:46:20 -07:00
diced
06d1c0bc3b fix(api): make delete user with images actually delete their images from the datasource 2022-06-19 17:26:52 -07:00
diced
5965c2e237 fix(config): extention -> extension 2022-06-19 16:44:55 -07:00
diced
fb34dfadb0 fix(config): make endpoint nullable 2022-06-18 13:47:59 -07:00
diced
13b0ac737b feat(datasource): s3 path styles 2022-06-18 13:39:12 -07:00
diced
300430b3ec Merge branch 'trunk' of github.com:diced/zipline into trunk 2022-06-18 12:38:42 -07:00
NebulaBC
cf6f154e6e fix: add env vars for s3 endpoint (#153) 2022-06-17 19:05:20 -07:00
diced
2ddf8c0cdb fix(api): password protected images wont show up on root 2022-06-17 15:35:29 -07:00
NebulaBC
2a402f77b5 feat(api): S3 endpoint support (#152)
* S3 endpoint support

Adding endpoint support to S3 allows for other S3-compatible uploaders to be used

* Fix formatting error
2022-06-17 14:29:34 -07:00
Han Cen
7b2c31658a feat(api): root uploader route (#150) 2022-06-17 14:20:21 -07:00
Han Cen
7a91a60af9 fix: fix build (#149) 2022-06-17 08:35:53 -07:00
diced
bfa6c70bf3 chore(deps): update stuff 2022-06-16 14:22:26 -07:00
Jonathan
73eff05180 feat: use yarn v3 (#136)
* feat: use yarn v3

* chore: bump yarn to 3.2.1
2022-06-06 16:38:15 -07:00
relaxtakenotes
74f3b3f13d fix: image width not being set properly (#143)
* Fix image width not being set properly

Sometimes it got set to 0 because the original image wasn't loaded yet.

* fix: eslint

Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
2022-06-04 22:18:07 -07:00
diced
181833d768 Merge branch 'trunk' of github.com:diced/zipline into trunk 2022-06-04 22:05:26 -07:00
diced
be9523304a feat(v3.4.4): fix many bugs and password protected uploads 2022-06-04 22:05:08 -07:00
dicedtomato
b26fef3ad4 fix(docker): add restart policy for postgres 2022-03-26 20:37:02 +00:00
diced
9f86674bbe fix: update security policy 2022-03-14 20:31:18 -07:00
diced
095e57a037 fix(actions): arm -> arm64 2022-03-14 20:22:39 -07:00
diced
66a8e3bb79 feat: arm docker-compose file 2022-03-13 20:05:41 -07:00
diced
473137abdf fix(actions): fix arm action path 2022-03-13 19:36:28 -07:00
diced
740f1605e7 fix(actions): maybe fix actions 2022-03-13 19:30:37 -07:00
diced
0922ec020e fix: revert to node 16 on actions 2022-03-13 19:27:32 -07:00
diced
dbe8291f55 fix(actions): maybe fix arm action 2022-03-13 19:26:33 -07:00
diced
9dcc16277e refactor(actions): update to v2 of build-push-action & push arm image 2022-03-13 19:25:11 -07:00
diced
aa611fa6ba feat(v3.4.3): cleanup, fix memory leak, arm support 2022-03-13 19:13:18 -07:00
diced
083040e300 feat(v3.4.2): random domain selection #129 2022-03-03 17:52:34 -08:00
diced
99e92e4594 feat(v3.4.1): datasource api, for S3 functionality 2022-03-02 22:04:56 -08:00
diced
870f6e88b1 fix(prisma): add removal of custom theme migration 2022-02-26 17:27:37 -08:00
dicedtomato
16d2014bfb feat(v3.4.0): switch from Material-UI to Mantine! (#127) 2022-02-26 17:19:02 -08:00
diced
4d9a22e82c fix(api): data not defined 2022-02-21 09:28:32 -08:00
diced
42d77e445b feat(v3.3.2): image formats 2022-02-21 09:27:10 -08:00
diced
6506846207 fix: cleanup 2022-02-21 09:26:26 -08:00
diced
2b9af0e0de feat(api): formats for uploaded images 2022-02-20 22:01:31 -08:00
diced
762d2927f7 Merge branch 'trunk' of github.com:diced/zipline into trunk 2022-02-19 20:17:53 -08:00
diced
d9561f3b12 feat(v3.3.1): bug fixes and new features 2022-02-19 20:17:02 -08:00
dicedtomato
dde24848d4 fix: domains in readme 2022-02-17 16:31:12 -08:00
diced
e786482902 fix: multiple issues & new features 2022-02-12 20:35:36 -08:00
diced
4e64922b70 feat(v3.3): release 3.3 2022-01-03 19:00:51 -08:00
diced
15042b16d1 feat(v3.3): ctrl+v to upload image 2022-01-03 19:00:20 -08:00
diced
5e4c4fc6c9 feat(v3.3): faster stats 2022-01-03 15:56:33 -08:00
diced
7194c53891 feat(v3.3): ratelimit 2022-01-03 15:17:47 -08:00
Nguyen Thanh Quang
7eff77ccc4 refactor(api): cors duplication (#109)
* refactor(api): cors duplication

* refactor(middleware): moved content-type setter to top
2021-11-27 15:00:18 -08:00
Nguyen Thanh Quang
1b78ffaa91 fix(prisma): make sure migrations are migrated in the first run (#105)
* fix(prisma): make sure migrations are migrated in the first run

* chore: removed redundant parentheses
2021-11-27 14:39:57 -08:00
dicedtomato
8e8bfd68d1 Update README.md 2021-11-23 18:31:26 -08:00
diced
b029505cdd feat(api): add cors 2021-11-04 17:09:18 -07:00
Kyle
c5c862bee3 fix: readme links (#104) 2021-10-03 11:42:27 -07:00
Nguyen Thanh Quang
3c38d008f1 fix(config): updated example config file (#103) 2021-10-03 11:41:50 -07:00
diced
dc52b00a00 feat(v3.2.5): update mui & embed vars 2021-10-02 20:16:23 -07:00
diced
b5d2e7040e fix: multi 1000 to expires 2021-09-25 18:03:06 -07:00
diced
5818440721 feat(pages): add create url 2021-09-25 18:00:00 -07:00
diced
f1c46da47d feat(pages): add urls page 2021-09-25 17:30:23 -07:00
diced
212c69d303 fix: add comma dangles 2021-09-25 09:39:51 -07:00
diced
9e4152e298 fix: github actions build 2021-09-24 20:41:26 -07:00
diced
307f023e47 fix: github actions build 2021-09-24 20:39:43 -07:00
diced
3451bd8762 feat(v3.2.4): url shortenning 2021-09-24 20:31:45 -07:00
diced
a9d0be8aae fix: revert arm stuff 2021-09-18 21:34:02 -07:00
diced
d83f720631 fix(actions): add custom prisma engines 2021-09-18 21:28:24 -07:00
diced
1f3d396296 fix(actions): make action use v2 2021-09-18 20:59:03 -07:00
diced
48f771f344 fix(actions): make action use v2 2021-09-18 20:42:47 -07:00
diced
555bc6aa26 fix(docker): make action target linux/arm64 2021-09-18 20:33:25 -07:00
diced
8bd0eaac1e fix(docker): make action target linux/arm64 2021-09-18 20:19:50 -07:00
diced
3280c77002 fix(docker): make action target linux/arm64 2021-09-18 20:18:04 -07:00
diced
b39743a53a fix(docker): make action target linux/arm64 2021-09-18 20:16:33 -07:00
diced
9a73da56e9 feat(docker): add arm64 compatible images 2021-09-18 20:10:22 -07:00
diced
c9b0d2664f feat(v3.2.3): new config validation 2021-09-17 21:38:24 -07:00
dicedtomato
6063c9efac Update README.md 2021-09-17 20:40:38 -07:00
diced
dd6f192d4a fix: many things 2021-09-17 20:39:20 -07:00
diced
d956f4ed3d fix(api): fix recent images showing other users images 2021-09-12 21:31:43 -07:00
diced
4728258750 feat: v3.2.2 new file management & viewing 2021-09-12 21:17:27 -07:00
diced
ece3e16459 fix(api): fixed being able to override user (#98) 2021-09-08 20:56:47 -07:00
Nguyen Thanh Quang
9208dbe2f3 fix(api): fixed being able to override user (#98)
* fix(api): fixed being able to override user

* Update index.ts
2021-09-08 20:56:10 -07:00
diced
636de18642 feat(pages): add recent images to dashboard 2021-09-06 15:58:01 -07:00
diced
ee48456291 fix(api): change cookie max-age from 10m seconds to 1 week 2021-09-06 15:32:39 -07:00
diced
a06d5ffaed fix(api): sort images count and types count 2021-09-06 15:22:36 -07:00
diced
606821a2c0 fix(components): add skeleton placeholders to dashboard 2021-09-06 14:54:25 -07:00
diced
5c980c21e5 feat(api): allow bulk uploading (#97) 2021-09-05 11:23:52 -07:00
diced
771cc380df fix(api): maybe fix default user not being created 2021-09-05 09:14:19 -07:00
diced
38217870fe fix(api): maybe fix default user not being created 2021-09-05 09:13:13 -07:00
dicedtomato
5b82c96a43 Update README.md 2021-09-04 15:01:08 -07:00
diced
6f5f9869ad refactor(assets): update assets to v3 2021-09-04 14:42:38 -07:00
diced
b29bfeb8b1 fix(api): new way to handle non-embedded images to overwrite nextjs & supports discord now 2021-09-03 17:20:43 -07:00
diced
cb40559e49 fix(server): some changes 2021-09-03 17:03:47 -07:00
diced
90c72f7ffe fix(api): small images height strech fix 2021-09-03 16:50:14 -07:00
diced
002bd2e6f7 refactor(api): redirect non-embedded images to /r 2021-09-03 16:40:50 -07:00
diced
7b44f17a64 refactor(api): /raw -> /r 2021-09-03 16:26:04 -07:00
diced
b5c83f92e3 fix(api): fix images not showing 2021-09-02 21:46:10 -07:00
diced
51b4d64a93 fix(build): fix some docker stuff 2021-09-02 21:27:12 -07:00
diced
62c9e0a22f feat(api): add support for invisible images 2021-09-02 21:00:16 -07:00
diced
3daac34d3e feat(api): add support for invisible images 2021-09-02 20:59:30 -07:00
dependabot[bot]
d80d5d1632 build(deps): bump next from 11.1.0 to 11.1.1 (#95)
Bumps [next](https://github.com/vercel/next.js) from 11.1.0 to 11.1.1.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v11.1.0...v11.1.1)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-02 06:51:13 -07:00
diced
912f716362 feat(api): add support for zws images 2021-08-30 20:56:34 -07:00
diced
16ecdf41af feat(components): added sharex config generator (#93) 2021-08-30 13:58:06 -07:00
Nguyen Thanh Quang
f0bb6b08fa feat(components): added sharex config generator (#93) 2021-08-30 07:59:08 -07:00
Nguyen Thanh Quang
efb4e2ce9a feat(sharex): added sharex config generator 2021-08-30 20:19:12 +07:00
diced
03238d10bf fix(config): notify which vars are missing 2021-08-28 21:41:04 -07:00
diced
e71590b9fb fix(config): new opts: admin_limit, user_limit, disabled_extensions (#68) 2021-08-28 21:02:04 -07:00
diced
4728f1cc46 fix(api): accidently sending images as a part of the user object 2021-08-28 19:37:38 -07:00
diced
794778dee2 feat(config): database section removed 2021-08-28 11:32:09 -07:00
Nguyen Thanh Quang
b5e882f07e feat(themes): added dracula theme (#92)
* added dracula theme

* change border color from white

Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
2021-08-27 15:33:32 -07:00
dicedtomato
e7c58a4847 licence 2021-08-27 15:24:37 -07:00
dicedtomato
bdb44db25e security policy 2021-08-27 15:23:28 -07:00
diced
e8b82ffe62 feat(api): image favoriting (#67) 2021-08-27 13:48:22 -07:00
diced
53c53c009e fix(components): white text on links when light theme 2021-08-26 21:41:30 -07:00
189 changed files with 24080 additions and 8720 deletions

View File

@@ -1,20 +0,0 @@
{
"presets": [
"next/babel"
],
"plugins": [
[
"babel-plugin-transform-imports",
{
"@material-ui/core": {
"transform": "@material-ui/core/${member}",
"preventFullImport": true
},
"@material-ui/icons": {
"transform": "@material-ui/icons/${member}",
"preventFullImport": true
}
}
]
]
}

7
.dockerignore Normal file
View File

@@ -0,0 +1,7 @@
node_modules/
.next/
uploads/
.git/
.yarn/*
!.yarn/releases
!.yarn/plugins

46
.env.local.example Normal file
View File

@@ -0,0 +1,46 @@
# every field in here is optional except, CORE_SECRET and CORE_DATABASE_URL.
# if CORE_SECRET is still "changethis" then zipline will exit and tell you to change it.
# if using s3/swift make sure to comment out the other datasources
CORE_HTTPS=true
CORE_SECRET="changethis"
CORE_HOST=0.0.0.0
CORE_PORT=3000
CORE_DATABASE_URL="postgres://postgres:postgres@localhost/zip10"
CORE_LOGGER=false
CORE_STATS_INTERVAL=1800
# default
DATASOURCE_TYPE=local
DATASOURCE_LOCAL_DIRECTORY=./uploads
# or you can choose to use s3
DATASOURCE_TYPE=s3
DATASOURCE_S3_ACCESS_KEY_ID=key
DATASOURCE_S3_SECRET_ACCESS_KEY=secret
DATASOURCE_S3_BUCKET=bucket
DATASOURCE_S3_ENDPOINT=s3.amazonaws.com
DATASOURCE_S3_REGION=us-west-2
DATASOURCE_S3_FORCE_S3_PATH=false
# or you can use swift
DATASOURCE_TYPE=swift
DATASOURCE_SWIFT_CONTAINER=container
DATASOURCE_SWIFT_AUTH_ENDPOINT="https://something/v3"
DATASOURCE_SWIFT_USERNAME=username
DATASOURCE_SWIFT_PASSWORD=password
DATASOURCE_SWIFT_PROJECT_ID=project_id
DATASOURCE_SWIFT_DOMAIN_ID=domain_id
UPLOADER_ROUTE=/u
UPLOADER_LENGTH=6
UPLOADER_ADMIN_LIMIT=104900000
UPLOADER_USER_LIMIT=104900000
UPLOADER_DISABLED_EXTENSIONS=someext
URLS_ROUTE=/go
URLS_LENGTH=6
RATELIMIT_USER = 5
RATELIMIT_ADMIN = 3

View File

@@ -1,24 +0,0 @@
module.exports = {
'extends': ['next', 'next/core-web-vitals'],
'rules': {
'indent': ['error', 2],
'linebreak-style': ['error', 'unix'],
'quotes': ['error', 'single'],
'semi': ['error', 'always'],
'jsx-quotes': ['error', 'prefer-single'],
'react/prop-types': 'off',
'react-hooks/rules-of-hooks': 'off',
'react-hooks/exhaustive-deps': 'off',
'react/jsx-uses-react': 'warn',
'react/jsx-uses-vars': 'warn',
'react/no-danger-with-children': 'warn',
'react/no-deprecated': 'warn',
'react/no-direct-mutation-state': 'warn',
'react/no-is-mounted': 'warn',
'react/no-typos': 'error',
'react/react-in-jsx-scope': 'error',
'react/require-render-return': 'error',
'react/style-prop-object': 'warn',
'@next/next/no-img-element': 'off'
}
};

51
.eslintrc.json Normal file
View File

@@ -0,0 +1,51 @@
{
"extends": [
"next",
"next/core-web-vitals"
],
"rules": {
"indent": [
"error",
2,
{
"SwitchCase": 1
}
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
],
"comma-dangle": [
"error",
"always-multiline"
],
"jsx-quotes": [
"error",
"prefer-single"
],
"react/prop-types": "off",
"react-hooks/rules-of-hooks": "off",
"react-hooks/exhaustive-deps": "off",
"react/jsx-uses-react": "warn",
"react/jsx-uses-vars": "warn",
"react/no-danger-with-children": "warn",
"react/no-deprecated": "warn",
"react/no-direct-mutation-state": "warn",
"react/no-is-mounted": "warn",
"react/no-typos": "error",
"react/react-in-jsx-scope": "off",
"react/require-render-return": "error",
"react/style-prop-object": "warn",
"@next/next/no-img-element": "off",
"jsx-a11y/alt-text": "off",
"react/display-name": "off"
}
}

View File

@@ -1,4 +1,4 @@
name: 'CI: Build'
name: 'Build'
on:
push:
@@ -22,13 +22,11 @@ jobs:
path: node_modules
key: ${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
- name: Create mock config
run: echo -e "[uploader]\nroute = '/u'\nembed_route = '/a'\nlength = 6\ndirectory = './uploads'" > config.toml
- name: Install dependencies
if: steps.cache-restore.outputs.cache-hit != 'true'
run: yarn install
- name: Build
run: yarn build
env:
ZIPLINE_DOCKER_BUILD: true

50
.github/workflows/docker-release.yml vendored Normal file
View File

@@ -0,0 +1,50 @@
name: 'Push Release Docker Images'
on:
push:
tags:
- 'v*.*.*'
paths:
- 'src/**'
- 'server/**'
- 'prisma/**'
- '.github/**'
- 'Dockerfile'
workflow_dispatch:
jobs:
push_to_ghcr:
name: Push Release Image to GitHub Packages
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v2
- name: Get version
uses: sergeysova/jq-action@v2
id: version
with:
cmd: 'jq .version package.json -r'
- name: Setup QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Login to Github Packages
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker Image
uses: docker/build-push-action@v2
with:
push: true
platforms: linux/amd64,linux/arm64
tags: |
ghcr.io/diced/zipline:latest
ghcr.io/diced/zipline:${{ steps.version.outputs.value }}

View File

@@ -1,4 +1,4 @@
name: 'CD: Push Docker Images'
name: 'Push Docker Images'
on:
push:
@@ -7,6 +7,8 @@ on:
- 'src/**'
- 'server/**'
- 'prisma/**'
- '.github/**'
- 'Dockerfile'
workflow_dispatch:
jobs:
@@ -17,28 +19,24 @@ jobs:
- name: Check out the repo
uses: actions/checkout@v2
- name: Push to GitHub Packages
uses: docker/build-push-action@v1
- name: Setup QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Login to Github Packages
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
registry: docker.pkg.github.com
repository: diced/zipline/zipline
dockerfile: Dockerfile
tag_with_ref: true
push_to_dockerhub:
name: Push Image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v2
- name: Push to Docker Hub
uses: docker/build-push-action@v1
- name: Build Docker Image
uses: docker/build-push-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: diced/zipline
dockerfile: Dockerfile
tag_with_ref: true
push: true
platforms: linux/amd64,linux/arm64
tags: |
ghcr.io/diced/zipline:trunk

8
.gitignore vendored
View File

@@ -5,6 +5,11 @@
/.pnp
.pnp.js
# yarn
.yarn/*
!.yarn/releases
!.yarn/plugins
# testing
/coverage
@@ -18,6 +23,7 @@
# misc
.DS_Store
*.pem
.idea
# debug
npm-debug.log*
@@ -36,4 +42,4 @@ yarn-error.log*
# zipline
config.toml
uploads/
data.db*
dist/

1
.husky/.gitignore vendored
View File

@@ -1 +0,0 @@
_

View File

@@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn commitlint --edit $1

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

7
.yarnrc.yml Normal file
View File

@@ -0,0 +1,7 @@
nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-3.2.1.cjs

View File

@@ -1,31 +1,64 @@
FROM node:16-alpine3.11 AS builder
FROM ghcr.io/diced/prisma-binaries:3.15.x as prisma
FROM alpine:3.16 AS deps
RUN mkdir -p /prisma-engines
WORKDIR /build
COPY .yarn .yarn
COPY package.json yarn.lock .yarnrc.yml ./
RUN apk add --no-cache nodejs yarn
RUN yarn install --immutable
FROM alpine:3.16 AS builder
WORKDIR /build
COPY --from=prisma /prisma-engines /prisma-engines
ENV PRISMA_QUERY_ENGINE_BINARY=/prisma-engines/query-engine \
PRISMA_MIGRATION_ENGINE_BINARY=/prisma-engines/migration-engine \
PRISMA_INTROSPECTION_ENGINE_BINARY=/prisma-engines/introspection-engine \
PRISMA_FMT_BINARY=/prisma-engines/prisma-fmt \
PRISMA_CLI_QUERY_ENGINE_TYPE=binary \
PRISMA_CLIENT_ENGINE_TYPE=binary
RUN apk add --no-cache nodejs yarn openssl openssl-dev
COPY --from=deps /build/node_modules ./node_modules
COPY src ./src
COPY server ./server
COPY scripts ./scripts
COPY prisma ./prisma
COPY .yarn .yarn
COPY package.json yarn.lock .yarnrc.yml esbuild.config.js next.config.js next-env.d.ts zip-env.d.ts tsconfig.json mimes.json ./
COPY package.json yarn.lock next.config.js next-env.d.ts zip-env.d.ts tsconfig.json ./
RUN yarn install
# create a mock config.toml to spoof next build!
RUN echo -e "[uploader]\nroute = '/u'\nembed_route = '/a'\nlength = 6\ndirectory = './uploads'" > config.toml
ENV ZIPLINE_DOCKER_BUILD 1
ENV NEXT_TELEMETRY_DISABLED 1
RUN yarn build
FROM node:16-alpine3.11 AS runner
FROM alpine:3.16 AS runner
WORKDIR /zipline
COPY --from=prisma /prisma-engines /prisma-engines
ENV PRISMA_QUERY_ENGINE_BINARY=/prisma-engines/query-engine \
PRISMA_MIGRATION_ENGINE_BINARY=/prisma-engines/migration-engine \
PRISMA_INTROSPECTION_ENGINE_BINARY=/prisma-engines/introspection-engine \
PRISMA_FMT_BINARY=/prisma-engines/prisma-fmt \
PRISMA_CLI_QUERY_ENGINE_TYPE=binary \
PRISMA_CLIENT_ENGINE_TYPE=binary
RUN apk add --no-cache nodejs yarn openssl openssl-dev
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
COPY --from=builder /build/.next ./.next
COPY --from=builder /build/dist ./dist
COPY --from=builder /build/node_modules ./node_modules
COPY --from=builder /build/next.config.js ./next.config.js
COPY --from=builder /build/src ./src
COPY --from=builder /build/server ./server
COPY --from=builder /build/scripts ./scripts
COPY --from=builder /build/prisma ./prisma
COPY --from=builder /build/.next ./.next
COPY --from=builder /build/tsconfig.json ./tsconfig.json
COPY --from=builder /build/package.json ./package.json
COPY --from=builder /build/mimes.json ./mimes.json
CMD ["node", "server"]
CMD ["node", "dist/server"]

View File

@@ -1,3 +0,0 @@
prisma/migrations
node_modules
.next

21
LICENSE Normal file
View File

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

118
README.md
View File

@@ -1,25 +1,113 @@
<p align="center"><img src="https://raw.githubusercontent.com/diced/zipline/trunk/public/zipline_small.png"/></p>
<div align="center">
<img src="https://raw.githubusercontent.com/diced/zipline/trunk/public/zipline_small.png"/>
![Version](https://img.shields.io/github/package-json/v/diced/zipline)
![LICENCE](https://img.shields.io/github/license/diced/zipline)
[![Discord](https://img.shields.io/discord/729771078196527176)](https://discord.gg/EAhCRfGxCF)
![Stars](https://img.shields.io/github/stars/diced/zipline)
![GitHub repo size](https://img.shields.io/github/repo-size/diced/zipline)
![GitHub last commit (branch)](https://img.shields.io/github/last-commit/diced/zipline/trunk)
<br>
Zipline is a ShareX/file upload server that is easy to use, packed with features and can be setup in one command!
# Zipline
![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)
Fast & lightweight file uploading.
# Features
</div>
## Features
- Configurable
- Fast
- Built with Next.js & React
- Token protected uploading
- Easy setup instructions on [docs](https://zipline.diced.me) (One command install `docker-compose up`)
- Image uploading
- Password Protected Uploads
- URL shortening
- Text uploading
- URL Formats (uuid, dates, random alphanumeric, original name, zws)
- Discord embeds (OG metadata)
- Gallery viewer, and multiple file format support
- Code highlighting
- Easy setup instructions on [docs](https://zipl.vercel.app/) (One command install `docker-compose up -d`)
# Installing
# Usage
[See how to install here](https://zipline.diced.me/docs/getting-started)
## Install & run with Docker
This section requires [Docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/).
```shell
git clone https://github.com/diced/zipline
cd zipline
docker-compose up -d
```
### After installing
After installing, please edit the `docker-compose.yml` file and find the line that says `SECRET=changethis` and replace `changethis` with a random string.
Ways you could generate the string could be from a password managers generator, or you could just slam your keyboard and hope for the best.
## Building & running from source
This section requires [nodejs](https://nodejs.org), [yarn](https://yarnpkg.com/) or [npm](https://npmjs.com).
```shell
git clone https://github.com/diced/zipline
cd zipline
# npm install
yarn install
# npm run build
yarn build
# npm start
yarn start
```
# NGINX Proxy
This section requires [NGINX](https://nginx.org/).
```nginx
server {
listen 80 default_server;
client_max_body_size 100M;
server_name <your domain (optional)>;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
# Website
The default port is `3000`, once you have accessed it you can see a login screen. The default credentials are "administrator" and "password". Once you login please immediately change the details to something more secure. You can do this by clicking on the top right corner where it says "administrator" with a gear icon and clicking Manage Account.
# ShareX (Windows)
This section requires [ShareX](https://www.getsharex.com/).
After navigating to Zipline, click on the top right corner where it says your username and click Manage Account. Scroll down to see "ShareX Config", select the one you would prefer using. After this you can import the .sxcu into sharex. [More information here](https://zipl.vercel.app/docs/uploaders/sharex)
# Flameshot (Linux)
This section requires [Flameshot](https://www.flameshot.org/), [jq](https://stedolan.github.io/jq/), and [xsel](https://github.com/kfish/xsel).
To upload files using flameshot we will use a script. Replace $TOKEN and $HOST with your own values, you probably know how to do this if you use linux.
```shell
DATE=$(date '+%h_%Y_%d_%I_%m_%S.png');
flameshot gui -r > ~/Pictures/$DATE;
curl -H "Content-Type: multipart/form-data" -H "authorization: $TOKEN" -F file=@$1 $HOST/api/upload | jq -r 'files[0].url' | xsel -ib
```
# Contributing
## Bug reports
Create an issue on GitHub, please include the following (if one of them is not applicable to the issue then it's not needed):
* The steps to reproduce the bug
* Logs of Zipline
* The version of Zipline
* Your OS & Browser including server OS
* What you were expecting to see
## Feature requests
Create an issue on GitHub, please include the following:
* Breif explanation of the feature in the title (very breif please)
* How it would work (detailed, but optional)
## Pull Requests (contributions to the codebase)
Create a pull request on GitHub. If your PR does not pass the action checks, then please fix the errors. If your PR was submitted before a release, and I have pushed a new release, please make sure to update your PR to reflect any changes, usually this is handled by GitHub.

12
SECURITY.md Normal file
View File

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

View File

@@ -1,53 +0,0 @@
module.exports = {
parserPreset: 'conventional-changelog-conventionalcommits',
rules: {
'body-leading-blank': [1, 'always'],
'body-max-line-length': [2, 'always', 100],
'footer-leading-blank': [1, 'always'],
'footer-max-line-length': [2, 'always', 100],
'header-max-length': [2, 'always', 100],
'subject-case': [
2,
'never',
['sentence-case', 'start-case', 'pascal-case', 'upper-case'],
],
'subject-empty': [2, 'never'],
'subject-full-stop': [2, 'never', '.'],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'type-enum': [
2,
'always',
[
'build',
'chore',
'ci',
'docs',
'feat',
'fix',
'perf',
'refactor',
'revert',
'style',
'test',
],
],
'scope-enum': [
1,
'always',
[
'prisma',
'scripts',
'server',
'pages',
'api',
'hooks',
'components',
'middleware',
'redux',
'themes',
'lib'
],
],
},
};

View File

@@ -1,15 +0,0 @@
[core]
secure = true
secret = 'some secret'
host = '0.0.0.0'
port = 3000
[database]
type = 'psql'
url = 'postgres://postgres:postgres@postgres/postgres'
[uploader]
route = '/u'
embed_route = '/a'
length = 6
directory = './uploads'

39
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,39 @@
version: '3'
services:
postgres:
image: postgres
restart: always
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DATABASE=postgres
volumes:
- pg_data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 10s
timeout: 5s
retries: 5
zipline:
build:
context: .
dockerfile: Dockerfile
ports:
- '3000:3000'
restart: unless-stopped
environment:
- CORE_HTTPS=false
- CORE_SECRET=changethislol
- CORE_HOST=0.0.0.0
- CORE_PORT=3000
- CORE_DATABASE_URL=postgres://postgres:postgres@postgres/postgres
- CORE_LOGGER=true
volumes:
- '$PWD/uploads:/zipline/uploads'
- '$PWD/public:/zipline/public'
depends_on:
- 'postgres'
volumes:
pg_data:

View File

@@ -2,11 +2,12 @@ version: '3'
services:
postgres:
image: postgres
environment:
restart: always
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DATABASE=postgres
volumes:
volumes:
- pg_data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
@@ -15,24 +16,19 @@ services:
retries: 5
zipline:
image: diced/zipline:trunk
image: ghcr.io/diced/zipline
ports:
- '3000:3000'
restart: unless-stopped
environment:
- SECURE=false
- SECRET=changethis
- HOST=0.0.0.0
- PORT=3000
- DATABASE_TYPE=psql
- DATABASE_URL=postgresql://postgres:postgres@postgres/postgres/
- UPLOADER_ROUTE=/u
- UPLOADER_EMBED_ROUTE=/a
- UPLOADER_LENGTH=6
- UPLOADER_DIRECTORY=./uploads
restart: always
environment:
- CORE_HTTPS=false
- CORE_SECRET=changethis
- CORE_HOST=0.0.0.0
- CORE_PORT=3000
- CORE_DATABASE_URL=postgres://postgres:postgres@postgres/postgres
- CORE_LOGGER=true
volumes:
- '$PWD/uploads:/zipline/uploads'
- '$PWD/prisma:/zipline/prisma'
- './uploads:/zipline/uploads'
- '$PWD/public:/zipline/public'
depends_on:
- 'postgres'

43
esbuild.config.js Normal file
View File

@@ -0,0 +1,43 @@
const esbuild = require('esbuild');
const { existsSync } = require('fs');
const { rm } = require('fs/promises');
(async () => {
const watch = process.argv[2] === '--watch';
if (existsSync('./dist')) {
await rm('./dist', { recursive: true });
}
await esbuild.build({
tsconfig: 'tsconfig.json',
outdir: 'dist',
bundle: false,
platform: 'node',
treeShaking: true,
entryPoints: [
'src/server/index.ts',
'src/server/util.ts',
'src/lib/logger.ts',
'src/lib/config.ts',
'src/lib/mimes.ts',
'src/lib/exts.ts',
'src/lib/config/Config.ts',
'src/lib/config/readConfig.ts',
'src/lib/config/validateConfig.ts',
'src/lib/datasources/Datasource.ts',
'src/lib/datasources/index.ts',
'src/lib/datasources/Local.ts',
'src/lib/datasources/S3.ts',
'src/lib/datasources/Swift.ts',
'src/lib/datasource.ts',
],
format: 'cjs',
resolveExtensions: ['.ts', '.js'],
write: true,
watch,
incremental: watch,
sourcemap: false,
minify: false,
});
})();

8298
mimes.json Normal file

File diff suppressed because it is too large Load Diff

1
next-env.d.ts vendored
View File

@@ -1,5 +1,4 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited

View File

@@ -1,4 +1,16 @@
module.exports = {
async redirects() {
return [
{
source: '/',
destination: '/dashboard',
permanent: true,
},
];
},
api: {
responseLimit: false,
},
poweredByHeader: false,
reactStrictMode: true,
};
};

View File

@@ -1,62 +1,69 @@
{
"name": "zip3",
"version": "3.2.0",
"name": "zipline",
"version": "3.4.8",
"license": "MIT",
"scripts": {
"prepare": "husky install",
"dev": "NODE_ENV=development node server",
"build": "npm-run-all build:schema build:next",
"dev": "node esbuild.config.js && REACT_EDITOR=code NODE_ENV=development node dist/server",
"build": "npm-run-all build:server build:schema build:next",
"build:server": "node esbuild.config.js",
"build:next": "next build",
"build:schema": "prisma generate --schema=prisma/schema.prisma",
"start": "node server",
"migrate:dev": "prisma migrate dev --create-only",
"start": "node dist/server",
"lint": "next lint",
"ts-node": "ts-node --compiler-options \"{\\\"module\\\":\\\"commonjs\\\"}\" --transpile-only",
"create-all-migrations": "node scripts/create-migrations",
"semantic-release": "semantic-release"
"docker:run": "docker-compose up -d",
"docker:down": "docker-compose down",
"docker:build-dev": "docker-compose --file docker-compose.dev.yml up --build"
},
"dependencies": {
"@emotion/react": "^11.4.0",
"@emotion/styled": "^11.3.0",
"@iarna/toml": "2.2.5",
"@material-ui/core": "^5.0.0-alpha.37",
"@material-ui/data-grid": "^4.0.0-alpha.32",
"@material-ui/icons": "^5.0.0-alpha.37",
"@material-ui/styles": "^5.0.0-alpha.35",
"@prisma/client": "^2.30.0",
"@reduxjs/toolkit": "^1.6.0",
"argon2": "^0.28.2",
"colorette": "^1.2.2",
"cookie": "^0.4.1",
"copy-to-clipboard": "^3.3.1",
"fecha": "^4.2.1",
"formik": "^2.2.9",
"multer": "^1.4.2",
"next": "11.1.0",
"prisma": "^2.30.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-dropzone": "^11.3.2",
"react-redux": "^7.2.4",
"redux": "^4.1.0",
"redux-thunk": "^2.3.0",
"yup": "^0.32.9"
"@mantine/core": "^4.2.9",
"@mantine/dropzone": "^4.2.9",
"@mantine/hooks": "^4.2.9",
"@mantine/modals": "^4.2.9",
"@mantine/next": "^4.2.9",
"@mantine/notifications": "^4.2.9",
"@mantine/prism": "^4.2.9",
"@prisma/client": "^3.15.2",
"@prisma/migrate": "^3.15.2",
"@prisma/sdk": "^3.15.2",
"@reduxjs/toolkit": "^1.8.2",
"argon2": "^0.28.5",
"colorette": "^2.0.19",
"cookie": "^0.5.0",
"dotenv": "^16.0.1",
"dotenv-expand": "^8.0.3",
"fecha": "^4.2.3",
"fflate": "^0.7.3",
"find-my-way": "^6.3.0",
"minio": "^7.0.28",
"multer": "^1.4.5-lts.1",
"next": "^12.1.6",
"prisma": "^3.15.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-feather": "^2.0.10",
"react-redux": "^8.0.2",
"react-table": "^7.8.0",
"redux": "^4.2.0",
"yup": "^0.32.11"
},
"devDependencies": {
"@commitlint/cli": "^12.1.4",
"@commitlint/config-conventional": "^12.1.4",
"@types/cookie": "^0.4.0",
"@types/multer": "^1.4.6",
"@types/cookie": "^0.5.1",
"@types/minio": "^7.0.13",
"@types/multer": "^1.4.7",
"@types/node": "^15.12.2",
"babel-plugin-transform-imports": "^2.0.0",
"eslint": "7.28.0",
"eslint-config-next": "11.0.0",
"husky": "^6.0.0",
"babel-plugin-import": "^1.13.5",
"esbuild": "^0.14.44",
"eslint": "^7.32.0",
"eslint-config-next": "12.1.6",
"npm-run-all": "^4.1.5",
"release": "^6.3.0",
"ts-node": "^10.0.0",
"typescript": "^4.3.2"
"ts-node": "^10.8.1",
"typescript": "^4.7.3"
},
"repository": {
"type": "git",
"url": "https://github.com/diced/workflow-testing.git"
}
"url": "https://github.com/diced/zipline.git"
},
"packageManager": "yarn@3.2.1"
}

View File

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

View File

@@ -0,0 +1,25 @@
/*
Warnings:
- A unique constraint covering the columns `[imageId]` on the table `InvisibleImage` will be added. If there are existing duplicate values, this will fail.
- Added the required column `imageId` to the `InvisibleImage` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "InvisibleImage" DROP CONSTRAINT "InvisibleImage_id_fkey";
-- DropIndex
DROP INDEX "InvisibleImage_id_unique";
-- AlterTable
CREATE SEQUENCE "invisibleimage_id_seq";
ALTER TABLE "InvisibleImage" ADD COLUMN "imageId" INTEGER NOT NULL,
ALTER COLUMN "id" SET DEFAULT nextval('invisibleimage_id_seq'),
ADD PRIMARY KEY ("id");
ALTER SEQUENCE "invisibleimage_id_seq" OWNED BY "InvisibleImage"."id";
-- CreateIndex
CREATE UNIQUE INDEX "InvisibleImage_imageId_unique" ON "InvisibleImage"("imageId");
-- AddForeignKey
ALTER TABLE "InvisibleImage" ADD FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

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

View File

@@ -0,0 +1,39 @@
/*
Warnings:
- You are about to drop the `InvisibleUrl` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Url` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "Image" DROP CONSTRAINT "Image_userId_fkey";
-- DropForeignKey
ALTER TABLE "InvisibleImage" DROP CONSTRAINT "InvisibleImage_imageId_fkey";
-- DropForeignKey
ALTER TABLE "InvisibleUrl" DROP CONSTRAINT "InvisibleUrl_id_fkey";
-- DropForeignKey
ALTER TABLE "Theme" DROP CONSTRAINT "Theme_userId_fkey";
-- DropForeignKey
ALTER TABLE "Url" DROP CONSTRAINT "Url_userId_fkey";
-- DropTable
DROP TABLE "InvisibleUrl";
-- DropTable
DROP TABLE "Url";
-- AddForeignKey
ALTER TABLE "Theme" ADD CONSTRAINT "Theme_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Image" ADD CONSTRAINT "Image_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "InvisibleImage" ADD CONSTRAINT "InvisibleImage_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- RenameIndex
ALTER INDEX "InvisibleImage.invis_unique" RENAME TO "InvisibleImage_invis_key";

View File

@@ -0,0 +1,34 @@
-- CreateTable
CREATE TABLE "Url" (
"id" TEXT NOT NULL,
"destination" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"views" INTEGER NOT NULL DEFAULT 0,
"userId" INTEGER NOT NULL,
CONSTRAINT "Url_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "InvisibleUrl" (
"id" SERIAL NOT NULL,
"invis" TEXT NOT NULL,
"urlId" TEXT NOT NULL,
CONSTRAINT "InvisibleUrl_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Url_id_key" ON "Url"("id");
-- CreateIndex
CREATE UNIQUE INDEX "InvisibleUrl_invis_key" ON "InvisibleUrl"("invis");
-- CreateIndex
CREATE UNIQUE INDEX "InvisibleUrl_urlId_unique" ON "InvisibleUrl"("urlId");
-- AddForeignKey
ALTER TABLE "Url" ADD CONSTRAINT "Url_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "InvisibleUrl" ADD CONSTRAINT "InvisibleUrl_urlId_fkey" FOREIGN KEY ("urlId") REFERENCES "Url"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Url" ADD COLUMN "vanity" TEXT;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "embedSiteName" TEXT DEFAULT E'{image.file} • {user.name}';

View File

@@ -0,0 +1,11 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "ratelimited" BOOLEAN NOT NULL DEFAULT false;
-- RenameIndex
ALTER INDEX "InvisibleImage_imageId_unique" RENAME TO "InvisibleImage_imageId_key";
-- RenameIndex
ALTER INDEX "InvisibleUrl_urlId_unique" RENAME TO "InvisibleUrl_urlId_key";
-- RenameIndex
ALTER INDEX "Theme_userId_unique" RENAME TO "Theme_userId_key";

View File

@@ -0,0 +1,8 @@
-- CreateTable
CREATE TABLE "Stats" (
"id" SERIAL NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"data" JSONB NOT NULL,
CONSTRAINT "Stats_pkey" PRIMARY KEY ("id")
);

View File

@@ -0,0 +1,5 @@
-- CreateEnum
CREATE TYPE "ImageFormat" AS ENUM ('UUID', 'DATE', 'RANDOM');
-- AlterTable
ALTER TABLE "Image" ADD COLUMN "format" "ImageFormat" NOT NULL DEFAULT E'RANDOM';

View File

@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "ImageFormat" ADD VALUE 'NAME';

View File

@@ -0,0 +1,12 @@
/*
Warnings:
- You are about to drop the `Theme` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "Theme" DROP CONSTRAINT "Theme_userId_fkey";
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "systemTheme" SET DEFAULT E'system';
-- DropTable
DROP TABLE "Theme";

View File

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

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Image" ADD COLUMN "password" TEXT;

View File

@@ -0,0 +1,9 @@
/*
Warnings:
- You are about to drop the column `ratelimited` on the `User` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "User" DROP COLUMN "ratelimited",
ADD COLUMN "ratelimit" TIMESTAMP(3);

View File

@@ -0,0 +1,17 @@
-- CreateTable
CREATE TABLE "Invite" (
"id" SERIAL NOT NULL,
"code" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expires_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"used" BOOLEAN NOT NULL DEFAULT false,
"createdById" INTEGER NOT NULL,
CONSTRAINT "Invite_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Invite_code_key" ON "Invite"("code");
-- AddForeignKey
ALTER TABLE "Invite" ADD CONSTRAINT "Invite_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

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

View File

@@ -8,32 +8,27 @@ generator client {
}
model User {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
username String
password String
token String
administrator Boolean @default(false)
systemTheme String @default("dark_blue")
customTheme Theme?
administrator Boolean @default(false)
systemTheme String @default("system")
embedTitle String?
embedColor String @default("#2f3136")
embedColor String @default("#2f3136")
embedSiteName String? @default("{image.file} • {user.name}")
ratelimit DateTime?
domains String[]
images Image[]
urls Url[]
Invite Invite[]
}
model Theme {
id Int @id @default(autoincrement())
type String
primary String
secondary String
error String
warning String
info String
border String
mainBackground String
paperBackground String
user User @relation(fields: [userId], references: [id])
userId Int
enum ImageFormat {
UUID
DATE
RANDOM
NAME
}
model Image {
@@ -42,31 +37,53 @@ model Image {
mimetype String @default("image/png")
created_at DateTime @default(now())
views Int @default(0)
favorite Boolean @default(false)
embed Boolean @default(false)
password String?
invisible InvisibleImage?
format ImageFormat @default(RANDOM)
user User @relation(fields: [userId], references: [id])
userId Int
}
model InvisibleImage {
id Int
image Image @relation(fields: [id], references: [id])
invis String @unique
id Int @id @default(autoincrement())
invis String @unique
imageId Int @unique
image Image @relation(fields: [imageId], references: [id])
}
model Url {
id Int @id @default(autoincrement())
to String
created_at DateTime @default(now())
views Int @default(0)
invisible InvisibleUrl?
user User @relation(fields: [userId], references: [id])
userId Int
id String @id @unique
destination String
vanity String?
created_at DateTime @default(now())
views Int @default(0)
invisible InvisibleUrl?
user User @relation(fields: [userId], references: [id])
userId Int
}
model InvisibleUrl {
id Int
url Url @relation(fields: [id], references: [id])
id Int @id @default(autoincrement())
invis String @unique
urlId String @unique
url Url @relation(fields: [urlId], references: [id])
}
model Stats {
id Int @id @default(autoincrement())
created_at DateTime @default(now())
data Json
}
model Invite {
id Int @id @default(autoincrement())
code String @unique
created_at DateTime @default(now())
expires_at DateTime?
used Boolean @default(false)
createdBy User @relation(fields: [createdById], references: [id])
createdById Int
}

View File

@@ -9,8 +9,8 @@ async function main() {
username: 'administrator',
password: await hashPassword('password'),
token: createToken(),
administrator: true
}
administrator: true,
},
});
console.log(`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -1,8 +0,0 @@
module.exports = {
branches: ['trunk'],
plugins: [
'@semantic-release/commit-analyzer',
'@semantic-release/github',
'@semantic-release/changelog'
]
};

View File

@@ -1,13 +0,0 @@
const Logger = require('../src/lib/logger');
const prismaRun = require('./prisma-run');
module.exports = async (config) => {
try {
await prismaRun(config.database.url, ['migrate', 'deploy']);
await prismaRun(config.database.url, ['generate']);
} catch (e) {
console.log(e);
Logger.get('db').error('there was an error.. exiting..');
process.exit(1);
}
};

View File

@@ -1,35 +0,0 @@
const { readdir } = require('fs/promises');
const { extname } = require('path');
const validateConfig = require('../server/validateConfig');
const Logger = require('../src/lib/logger');
const readConfig = require('../src/lib/readConfig');
const mimes = require('./mimes');
const { PrismaClient } = require('@prisma/client');
(async () => {
const config = readConfig();
await validateConfig(config);
process.env.DATABASE_URL = config.database.url;
const files = await readdir(process.argv[2]);
const data = files.map(x => {
const mime = mimes[extname(x)] ?? 'application/octet-stream';
return {
file: x,
mimetype: mime,
userId: 1
};
});
const prisma = new PrismaClient();
Logger.get('migrator').info('starting migrations...');
await prisma.image.createMany({
data
});
Logger.get('migrator').info('finished migrations! It is recomended to move your old uploads folder (' + process.argv[2] + ') to the current one which is ' + config.uploader.directory);
process.exit();
})();

View File

@@ -1,78 +0,0 @@
module.exports = {
'.aac': 'audio/aac',
'.abw': 'application/x-abiword',
'.arc': 'application/x-freearc',
'.avi': 'video/x-msvideo',
'.azw': 'application/vnd.amazon.ebook',
'.bin': 'application/octet-stream',
'.bmp': 'image/bmp',
'.bz': 'application/x-bzip',
'.bz2': 'application/x-bzip2',
'.cda': 'application/x-cdf',
'.csh': 'application/x-csh',
'.css': 'text/css',
'.csv': 'text/csv',
'.doc': 'application/msword',
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'.eot': 'application/vnd.ms-fontobject',
'.epub': 'application/epub+zip',
'.gz': 'application/gzip',
'.gif': 'image/gif',
'.htm': 'text/html',
'.html': 'text/html',
'.ico': 'image/vnd.microsoft.icon',
'.ics': 'text/calendar',
'.jar': 'application/java-archive',
'.jpeg': 'image/jpeg',
'.jpg': 'image/jpeg',
'.js': 'text/javascript',
'.json': 'application/json',
'.jsonld': 'application/ld+json',
'.mid': 'audio/midi',
'.midi': 'audio/midi',
'.mjs': 'text/javascript',
'.mp3': 'audio/mpeg',
'.mp4': 'video/mp4',
'.mpeg': 'video/mpeg',
'.mpkg': 'application/vnd.apple.installer+xml',
'.odp': 'application/vnd.oasis.opendocument.presentation',
'.ods': 'application/vnd.oasis.opendocument.spreadsheet',
'.odt': 'application/vnd.oasis.opendocument.text',
'.oga': 'audio/ogg',
'.ogv': 'video/ogg',
'.ogx': 'application/ogg',
'.opus': 'audio/opus',
'.otf': 'font/otf',
'.png': 'image/png',
'.pdf': 'application/pdf',
'.php': 'application/x-httpd-php',
'.ppt': 'application/vnd.ms-powerpoint',
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'.rar': 'application/vnd.rar',
'.rtf': 'application/rtf',
'.sh': 'application/x-sh',
'.svg': 'image/svg+xml',
'.swf': 'application/x-shockwave-flash',
'.tar': 'application/x-tar',
'.tif': 'image/tiff',
'.tiff': 'image/tiff',
'.ts': 'video/mp2t',
'.ttf': 'font/ttf',
'.txt': 'text/plain',
'.vsd': 'application/vnd.visio',
'.wav': 'audio/wav',
'.weba': 'audio/webm',
'.webm': 'video/webm',
'.webp': 'image/webp',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.xhtml': 'application/xhtml+xml',
'.xls': 'application/vnd.ms-excel',
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'.xml': 'application/xml',
'.xul': 'application/vnd.mozilla.xul+xml',
'.zip': 'application/zip',
'.3gp': 'video/3gpp',
'.3g2': 'video/3gpp2',
'.7z': 'application/x-7z-compressed'
};

View File

@@ -1,26 +0,0 @@
const { spawn } = require('child_process');
const { join } = require('path');
module.exports = (url, args) => {
return new Promise((res, rej) => {
const proc = spawn(join(process.cwd(), 'node_modules', '.bin', 'prisma'), args, {
env: {
DATABASE_URL: url,
...process.env
},
});
let a = '';
proc.stdout.on('data', d => {
console.log(d.toString());
a += d.toString();
});
proc.stderr.on('data', d => {
console.log(d.toString());
rej(d.toString());
});
proc.stdout.on('end', () => res(a));
proc.stdout.on('close', () => res(a));
});
};

View File

@@ -1,129 +0,0 @@
const next = require('next');
const { createServer } = require('http');
const { stat, mkdir } = require('fs/promises');
const { execSync } = require('child_process');
const { extname } = require('path');
const { red, green, bold } = require('colorette');
const { PrismaClient } = require('@prisma/client');
const validateConfig = require('./validateConfig');
const Logger = require('../src/lib/logger');
const getFile = require('./static');
const prismaRun = require('../scripts/prisma-run');
const readConfig = require('../src/lib/readConfig');
const mimes = require('../scripts/mimes');
const deployDb = require('../scripts/deploy-db');
Logger.get('server').info('starting zipline server');
const dev = process.env.NODE_ENV === 'development';
function log(url, status) {
if (url.startsWith('/_next') || url.startsWith('/__nextjs')) return;
return Logger.get('url').info(`${status === 200 ? bold(green(status)) : bold(red(status))}: ${url}`);
}
function shouldUseYarn() {
try {
execSync('yarnpkg --version', { stdio: 'ignore' });
return true;
} catch (e) {
return false;
}
}
(async () => {
try {
const config = readConfig();
await validateConfig(config);
const data = await prismaRun(config.database.url, ['migrate', 'status']);
if (data.includes('Following migration have not yet been applied:')) {
Logger.get('database').info('some migrations are not applied, applying them now...');
await deployDb(config);
Logger.get('database').info('finished applying migrations');
}
process.env.DATABASE_URL = config.database.url;
await stat('./.next');
await mkdir(config.uploader.directory, { recursive: true });
const app = next({
dir: '.',
dev,
quiet: dev
}, config.core.port, config.core.host);
await app.prepare();
const handle = app.getRequestHandler();
const prisma = new PrismaClient();
const srv = createServer(async (req, res) => {
if (req.url.startsWith(config.uploader.route)) {
const parts = req.url.split('/');
if (!parts[2] || parts[2] === '') return;
const data = await getFile(config.uploader.directory, parts[2]);
if (!data) {
app.render404(req, res);
} else {
let image = await prisma.image.findFirst({
where: {
OR: {
file: parts[2],
},
OR: {
invisible: {
invis: decodeURI(parts[2])
}
}
}
});
if (image) {
await prisma.image.update({
where: {
id: image.id,
},
data: {
views: {
increment: 1
}
}
});
res.setHeader('Content-Type', image.mimetype);
} else {
const mimetype = mimes[extname(parts[2])] ?? 'application/octet-stream';
res.setHeader('Content-Type', mimetype);
}
res.end(data);
}
} else {
handle(req, res);
}
log(req.url, res.statusCode);
});
srv.on('error', (e) => {
Logger.get('server').error(e);
process.exit(1);
});
srv.on('listening', () => {
Logger.get('server').info(`listening on ${config.core.host}:${config.core.port}`);
});
srv.listen(config.core.port, config.core.host ?? '0.0.0.0');
} catch (e) {
if (e.message && e.message.startsWith('Could not find a production')) {
Logger.get('web').error(`there is no production build - run \`${shouldUseYarn() ? 'yarn build' : 'npm build'}\``);
} else if (e.code && e.code === 'ENOENT') {
if (e.path === './.next') Logger.get('web').error(`there is no production build - run \`${shouldUseYarn() ? 'yarn build' : 'npm build'}\``);
} else {
Logger.get('server').error(e);
process.exit(1);
}
}
})();

View File

@@ -1,11 +0,0 @@
const { readFile } = require('fs/promises');
const { join } = require('path');
module.exports = async (dir, file) => {
try {
const data = await readFile(join(process.cwd(), dir, file));
return data;
} catch (e) {
return null;
}
};

View File

@@ -1,44 +0,0 @@
const Logger = require('../src/lib/logger');
function dot(str, obj) {
return str.split('.').reduce((a,b) => a[b], obj);
}
const path = (path, type) => ({ path, type });
module.exports = async config => {
const paths = [
path('core.secure', 'boolean'),
path('core.secret', 'string'),
path('core.host', 'string'),
path('core.port', 'number'),
path('database.type', 'string'),
path('database.url', 'string'),
path('uploader.route', 'string'),
path('uploader.embed_route', 'string'),
path('uploader.length', 'number'),
path('uploader.directory', 'string')
];
let errors = 0;
for (let i = 0, L = paths.length; i !== L; ++i) {
const path = paths[i];
const value = dot(path.path, config);
if (value === undefined) {
Logger.get('config').error(`there was no ${path.path} in config`);
++errors;
}
const type = typeof value;
if (value !== undefined && type !== path.type) {
Logger.get('config').error(`expected ${path.type} on ${path.path}, but got ${type}`);
++errors;
}
}
if (errors !== 0) {
Logger.get('config').error(`exiting due to ${errors} errors`);
process.exit(1);
}
};

View File

@@ -1,12 +0,0 @@
import React from 'react';
import { Snackbar, Alert as MuiAlert } from '@material-ui/core';
export default function Alert({ open, setOpen, severity, message }) {
return (
<Snackbar open={open} autoHideDuration={6000} anchorOrigin={{ vertical: 'top', horizontal: 'center' }} onClose={() => setOpen(false)}>
<MuiAlert severity={severity} sx={{ width: '100%' }}>
{message}
</MuiAlert>
</Snackbar>
);
}

View File

@@ -1,16 +0,0 @@
import React from 'react';
import {
Backdrop as MuiBackdrop,
CircularProgress
} from '@material-ui/core';
export default function Backdrop({ open }) {
return (
<MuiBackdrop
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
open={open}
>
<CircularProgress color='inherit' />
</MuiBackdrop>
);
}

View File

@@ -1,19 +1,11 @@
import React from 'react';
import {
Card as MuiCard,
CardContent,
Typography
} from '@material-ui/core';
import { Card as MCard, Title } from '@mantine/core';
export default function Card(props) {
const { name, children, ...other } = props;
export default function Card({ name, children, ...other }) {
return (
<MuiCard sx={{ minWidth: 100 }} {...other}>
<CardContent>
<Typography variant='h3'>{name}</Typography>
{children}
</CardContent>
</MuiCard>
<MCard p='md' shadow='sm' {...other}>
<Title order={2}>{name}</Title>
{children}
</MCard>
);
}

View File

@@ -1,15 +0,0 @@
import React from 'react';
import { Box } from '@material-ui/core';
export default function CenteredBox({ children, ...other }) {
return (
<Box
justifyContent='center'
display='flex'
alignItems='center'
{...other}
>
{children}
</Box>
);
}

View File

@@ -0,0 +1,21 @@
import { createStyles, MantineSize, Textarea } from '@mantine/core';
const useStyles = createStyles((theme, { size }: { size: MantineSize }) => ({
input: {
fontFamily: 'monospace',
fontSize: theme.fn.size({ size, sizes: theme.fontSizes }) - 2,
height: '100vh',
},
}));
export default function CodeInput({ ...props }) {
const { classes } = useStyles({ size: 'md' }, { name: 'CodeInput' });
return (
<Textarea
classNames={{ input: classes.input }}
autoComplete='nope'
{...props}
/>
);
}

118
src/components/File.tsx Normal file
View File

@@ -0,0 +1,118 @@
import { Button, Card, Grid, Group, Image as MImage, Modal, Stack, Text, Title, useMantineTheme } from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { useNotifications } from '@mantine/notifications';
import useFetch from 'hooks/useFetch';
import { useState } from 'react';
import Type from './Type';
import { CalendarIcon, CopyIcon, CrossIcon, DeleteIcon, FileIcon, HashIcon, ImageIcon, StarIcon } from './icons';
import MutedText from './MutedText';
export function FileMeta({ Icon, title, subtitle }) {
return (
<Group>
<Icon size={24} />
<Stack spacing={1}>
<Text>{title}</Text>
<MutedText size='md'>{subtitle}</MutedText>
</Stack>
</Group>
);
}
export default function File({ image, updateImages }) {
const [open, setOpen] = useState(false);
const notif = useNotifications();
const clipboard = useClipboard();
const theme = useMantineTheme();
const handleDelete = async () => {
const res = await useFetch('/api/user/files', 'DELETE', { id: image.id });
if (!res.error) {
updateImages(true);
notif.showNotification({
title: 'File Deleted',
message: '',
color: 'green',
icon: <DeleteIcon />,
});
} else {
notif.showNotification({
title: 'Failed to delete file',
message: res.error,
color: 'red',
icon: <CrossIcon />,
});
}
setOpen(false);
};
const handleCopy = () => {
clipboard.copy(`${window.location.protocol}//${window.location.host}${image.url}`);
setOpen(false);
notif.showNotification({
title: 'Copied to clipboard',
message: '',
icon: <CopyIcon />,
});
};
const handleFavorite = async () => {
const data = await useFetch('/api/user/files', 'PATCH', { id: image.id, favorite: !image.favorite });
if (!data.error) updateImages(true);
notif.showNotification({
title: 'Image is now ' + (!image.favorite ? 'favorited' : 'unfavorited'),
message: '',
icon: <StarIcon />,
});
};
return (
<>
<Modal
opened={open}
onClose={() => setOpen(false)}
title={<Title>{image.file}</Title>}
size='xl'
overlayBlur={3}
overlayColor={theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white'}
>
<Stack>
<Type
file={image}
src={image.url}
alt={image.file}
popup
sx={{ minHeight: 200 }}
style={{ minHeight: 200 }}
/>
<Stack>
<FileMeta Icon={FileIcon} title='Name' subtitle={image.file} />
<FileMeta Icon={ImageIcon} title='Type' subtitle={image.mimetype} />
<FileMeta Icon={CalendarIcon} title='Uploaded at' subtitle={new Date(image.created_at).toLocaleString()} />
<FileMeta Icon={HashIcon} title='ID' subtitle={image.id} />
</Stack>
</Stack>
<Group position='right' mt={22}>
<Button onClick={handleCopy}>Copy</Button>
<Button onClick={handleDelete}>Delete</Button>
<Button onClick={handleFavorite}>{image.favorite ? 'Unfavorite' : 'Favorite'}</Button>
</Group>
</Modal>
<Card sx={{ maxWidth: '100%', height: '100%' }} shadow='md'>
<Card.Section>
<Type
file={image}
sx={{ minHeight: 200, maxHeight: 320, fontSize: 70, width: '100%', cursor: 'pointer' }}
style={{ minHeight: 200, maxHeight: 320, fontSize: 70, width: '100%', cursor: 'pointer' }}
src={image.url}
alt={image.file}
onClick={() => setOpen(true)}
/>
</Card.Section>
</Card>
</>
);
}

View File

@@ -1,61 +0,0 @@
import React, { useState } from 'react';
import {
Card,
CardMedia,
CardActionArea,
Popover,
Button,
ButtonGroup
} from '@material-ui/core';
import copy from 'copy-to-clipboard';
import useFetch from '../lib/hooks/useFetch';
export default function Image({ image, updateImages }) {
const [anchorEl, setAnchorEl] = useState(null);
const handleDelete = async () => {
const res = await useFetch('/api/user/images', 'DELETE', { id: image.id });
if (!res.error) updateImages();
setAnchorEl(null);
};
const handleCopy = () => {
copy(`${window.location.protocol}//${window.location.host}${image.url}`);
setAnchorEl(null);
};
return (
<>
<Card sx={{ maxWidth: '100%' }}>
<CardActionArea>
<CardMedia
sx={{ height: 320 }}
image={image.url}
title={image.file}
onClick={e => setAnchorEl(e.currentTarget)}
/>
</CardActionArea>
</Card>
<Popover
open={Boolean(anchorEl)}
anchorEl={anchorEl}
onClose={() => setAnchorEl(null)}
anchorOrigin={{
vertical: 'center',
horizontal: 'center',
}}
transformOrigin={{
vertical: 'center',
horizontal: 'center',
}}
>
<ButtonGroup variant='contained'>
<Button onClick={handleDelete} color='primary'>Delete</Button>
<Button onClick={handleCopy} color='primary'>Copy URL</Button>
</ButtonGroup>
</Popover>
</>
);
}

View File

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

View File

@@ -1,417 +1,365 @@
import React, { useState } from 'react';
import Link from 'next/link';
import useFetch from '../lib/hooks/useFetch';
import {
AppBar,
Box,
Divider,
Drawer,
IconButton,
List,
ListItem,
ListItemIcon,
ListItemText,
Toolbar,
Typography,
Button,
Menu,
MenuItem,
Paper,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Select,
} from '@material-ui/core';
import {
Menu as MenuIcon,
Home as HomeIcon,
AccountCircle as AccountIcon,
Image as ImageIcon,
Upload as UploadIcon,
ContentCopy as CopyIcon,
Autorenew as ResetIcon,
Logout as LogoutIcon,
PeopleAlt as UsersIcon,
Brush as BrushIcon
} from '@material-ui/icons';
import copy from 'copy-to-clipboard';
import Backdrop from './Backdrop';
import { friendlyThemeName, themes } from './Theming';
import { useRouter } from 'next/router';
import { useStoreDispatch } from 'lib/redux/store';
import { AppShell, Box, Burger, Divider, Group, Header, MediaQuery, Navbar, Paper, Popover, ScrollArea, Select, Text, ThemeIcon, Title, UnstyledButton, useMantineTheme } from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { useModals } from '@mantine/modals';
import { useNotifications } from '@mantine/notifications';
import useFetch from 'hooks/useFetch';
import { updateUser } from 'lib/redux/reducers/user';
import { useStoreDispatch } from 'lib/redux/store';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { ActivityIcon, CheckIcon, CopyIcon, CrossIcon, DeleteIcon, FileIcon, HomeIcon, LinkIcon, LogoutIcon, PencilIcon, SettingsIcon, TagIcon, TypeIcon, UploadIcon, UserIcon } from './icons';
import { friendlyThemeName, themes } from './Theming';
function MenuItemLink(props) {
return (
<Link href={props.href} passHref>
<MenuItem {...props} />
</Link>
);
}
function MenuItem(props) {
return (
<UnstyledButton
sx={theme => ({
display: 'block',
width: '100%',
padding: 5,
borderRadius: theme.radius.sm,
color: props.color
? theme.fn.themeColor(props.color, theme.colorScheme === 'dark' ? 5 : 7)
: theme.colorScheme === 'dark'
? theme.colors.dark[0]
: theme.black,
'&:hover': {
backgroundColor: props.color
? theme.fn.rgba(
theme.fn.themeColor(props.color, theme.colorScheme === 'dark' ? 9 : 0),
theme.colorScheme === 'dark' ? 0.2 : 1
)
: theme.colorScheme === 'dark'
? theme.fn.rgba(theme.colors.dark[3], 0.35)
: theme.colors.gray[0],
},
})}
{...props}
>
<Group noWrap>
<Box sx={theme => ({
marginRight: theme.spacing.xs / 4,
paddingLeft: theme.spacing.xs / 2,
'& *': {
display: 'block',
},
})}>
{props.icon}
</Box>
<Text size='sm'>{props.children}</Text>
</Group>
</UnstyledButton>
);
}
const items = [
{
icon: <HomeIcon />,
text: 'Home',
link: '/dashboard'
link: '/dashboard',
},
{
icon: <ImageIcon />,
text: 'Images',
link: '/dashboard/images'
icon: <FileIcon />,
text: 'Files',
link: '/dashboard/files',
},
{
icon: <ActivityIcon />,
text: 'Stats',
link: '/dashboard/stats',
},
{
icon: <LinkIcon />,
text: 'URLs',
link: '/dashboard/urls',
},
{
icon: <UploadIcon />,
text: 'Upload',
link: '/dashboard/upload'
}
link: '/dashboard/upload',
},
{
icon: <TypeIcon />,
text: 'Upload Text',
link: '/dashboard/text',
},
];
const drawerWidth = 240;
function CopyTokenDialog({ open, setOpen, token }) {
const handleCopyToken = () => {
copy(token);
setOpen(false);
};
return (
<div>
<Dialog
open={open}
onClose={() => setOpen(false)}
>
<DialogTitle id='copy-dialog-title'>
Copy Token
</DialogTitle>
<DialogContent>
<DialogContentText id='copy-dialog-description'>
Make sure you don&apos;t share this token with anyone as they will be able to upload images on your behalf.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpen(false)} color='inherit' autoFocus>Cancel</Button>
<Button onClick={handleCopyToken} color='inherit'>
Copy
</Button>
</DialogActions>
</Dialog>
</div>
);
}
function ResetTokenDialog({ open, setOpen, setToken }) {
const handleResetToken = async () => {
const a = await useFetch('/api/user/token', 'PATCH');
if (a.success) setToken(a.success);
setOpen(false);
};
return (
<div>
<Dialog
open={open}
onClose={() => setOpen(false)}
>
<DialogTitle id='reset-dialog-title'>
Reset Token
</DialogTitle>
<DialogContent>
<DialogContentText id='reset-dialog-description'>
Once you reset your token, you will have to update any uploaders to use this new token.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpen(false)} color='inherit' autoFocus>Cancel</Button>
<Button onClick={handleResetToken} color='inherit'>
Reset
</Button>
</DialogActions>
</Dialog>
</div>
);
}
export default function Layout({ children, user, loading, noPaper }) {
const [systemTheme, setSystemTheme] = useState(user.systemTheme || 'dark_blue');
const [mobileOpen, setMobileOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);
const [copyOpen, setCopyOpen] = useState(false);
const [resetOpen, setResetOpen] = useState(false);
export default function Layout({ children, user, title }) {
const [token, setToken] = useState(user?.token);
const [systemTheme, setSystemTheme] = useState(user.systemTheme ?? 'system');
const [opened, setOpened] = useState(false); // navigation open
const [open, setOpen] = useState(false); // manage acc dropdown
const router = useRouter();
const dispatch = useStoreDispatch();
const theme = useMantineTheme();
const modals = useModals();
const notif = useNotifications();
const clipboard = useClipboard();
const open = Boolean(anchorEl);
const handleClick = e => setAnchorEl(e.currentTarget);
const handleClose = (cmd: 'copy' | 'reset') => () => {
switch (cmd) {
case 'copy':
setCopyOpen(true);
break;
case 'reset':
setResetOpen(true);
break;
}
setAnchorEl(null);
};
const handleUpdateTheme = async (event: React.ChangeEvent<{ value: string }>) => {
const handleUpdateTheme = async value => {
const newUser = await useFetch('/api/user', 'PATCH', {
systemTheme: event.target.value || 'dark_blue'
systemTheme: value || 'dark_blue',
});
setSystemTheme(newUser.systemTheme);
dispatch(updateUser(newUser));
router.replace(router.pathname);
notif.showNotification({
title: `Theme changed to ${friendlyThemeName[value]}`,
message: '',
color: 'green',
icon: <PencilIcon />,
});
};
const drawer = (
<div>
<CopyTokenDialog open={copyOpen} setOpen={setCopyOpen} token={token} />
<ResetTokenDialog open={resetOpen} setOpen={setResetOpen} setToken={setToken} />
<Toolbar
sx={{
width: { xs: drawerWidth }
}}
>
<AppBar
position='fixed'
elevation={0}
sx={{
borderBottom: 1,
borderBottomColor: t => t.palette.divider,
display: { xs: 'none', sm: 'block' }
}}
>
<Toolbar>
<IconButton
color='inherit'
aria-label='open drawer'
edge='start'
onClick={() => setMobileOpen(true)}
sx={{ mr: 2, display: { sm: 'none' } }}
>
<MenuIcon />
</IconButton>
<Typography
variant='h5'
noWrap
component='div'
>
Zipline
</Typography>
{user && (
<Box sx={{ marginLeft: 'auto' }}>
<Button
color='inherit'
aria-expanded={open ? 'true' : undefined}
onClick={handleClick}
>
<AccountIcon />
</Button>
<Menu
id='zipline-user-menu'
anchorEl={anchorEl}
open={open}
onClose={handleClose(null)}
MenuListProps={{
'aria-labelledby': 'basic-button',
}}
>
<MenuItem disableRipple>
<Typography variant='h5'>
<b>{user.username}</b>
</Typography>
</MenuItem>
<Divider />
<Link href='/dashboard/manage'>
<a style={{ color: 'white', textDecoration: 'none' }}>
<MenuItem onClick={handleClose(null)}>
<AccountIcon sx={{ mr: 2 }} /> Manage Account
</MenuItem>
</a>
</Link>
<MenuItem onClick={handleClose('copy')}>
<CopyIcon sx={{ mr: 2 }} /> Copy Token
</MenuItem>
<MenuItem onClick={handleClose('reset')}>
<ResetIcon sx={{ mr: 2 }} /> Reset Token
</MenuItem>
<Link href='/auth/logout'>
<a style={{ color: 'white', textDecoration: 'none' }}>
<MenuItem onClick={handleClose(null)}>
<LogoutIcon sx={{ mr: 2 }} /> Logout
</MenuItem>
</a>
</Link>
<MenuItem>
<BrushIcon sx={{ mr: 2 }} />
<Select
variant='standard'
label='Theme'
value={systemTheme}
onChange={handleUpdateTheme}
fullWidth
>
{Object.keys(themes).map(t => (
<MenuItem value={t} key={t}>
{friendlyThemeName[t]}
</MenuItem>
))}
</Select>
</MenuItem>
</Menu>
</Box>
)}
</Toolbar>
</AppBar>
</Toolbar>
<Divider />
<List>
{items.map((item, i) => (
<Link key={i} href={item.link}>
<a href={item.link} style={{ color: 'white', textDecoration: 'none' }}>
<ListItem button>
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={item.text} />
</ListItem>
</a>
</Link>
))}
{user && user.administrator && (
<Link href='/dashboard/users' passHref>
<a style={{ color: 'white', textDecoration: 'none' }}>
<ListItem button>
<ListItemIcon><UsersIcon /></ListItemIcon>
<ListItemText primary='Users' />
</ListItem>
</a>
</Link>
)}
</List>
</div>
);
const openResetToken = () => modals.openConfirmModal({
title: 'Reset Token',
centered: true,
overlayBlur: 3,
children: (
<Text size='sm'>
Once you reset your token, you will have to update any uploaders to use this new token.
</Text>
),
labels: { confirm: 'Reset', cancel: 'Cancel' },
onConfirm: async () => {
const a = await useFetch('/api/user/token', 'PATCH');
if (!a.success) {
setToken(a.success);
notif.showNotification({
title: 'Token Reset Failed',
message: a.error,
color: 'red',
icon: <CrossIcon />,
});
} else {
notif.showNotification({
title: 'Token Reset',
message: 'Your token has been reset. You will need to update any uploaders to use this new token.',
color: 'green',
icon: <CheckIcon />,
});
}
const container = typeof window !== 'undefined' ? window.document.body : undefined;
modals.closeAll();
},
});
const openCopyToken = () => modals.openConfirmModal({
title: 'Copy Token',
centered: true,
overlayBlur: 3,
children: (
<Text size='sm'>
Make sure you don&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 (
<Box sx={{ display: 'flex' }}>
<Backdrop open={loading} />
<AppShell
navbarOffsetBreakpoint='sm'
fixed
navbar={
<Navbar
p='md'
hiddenBreakpoint='sm'
hidden={!opened}
width={{ sm: 200, lg: 230 }}
>
<Navbar.Section
grow
component={ScrollArea}
ml={-10}
mr={-10}
sx={{ paddingLeft: 10, paddingRight: 10 }}
>
{items.map(({ icon, text, link }) => (
<Link href={link} key={text} passHref>
<UnstyledButton
sx={{
display: 'block',
width: '100%',
padding: theme.spacing.xs,
borderRadius: theme.radius.sm,
color: theme.colorScheme === 'dark' ? theme.colors.dark[0] : theme.black,
'&:hover': {
backgroundColor: theme.other.hover,
},
}}
>
<Group>
<ThemeIcon color='primary' variant='filled'>
{icon}
</ThemeIcon>
<AppBar
position='fixed'
elevation={0}
sx={{
width: { sm: `calc(100% - ${drawerWidth}px)` },
ml: { sm: `${drawerWidth}px` }
}}
>
<Toolbar>
<IconButton
color='inherit'
aria-label='open drawer'
edge='start'
onClick={() => setMobileOpen(true)}
sx={{ mr: 2, display: { sm: 'none' } }}
>
<MenuIcon />
</IconButton>
<Typography
variant='h5'
noWrap
component='div'
sx={{ display: { sm: 'none' } }}
>
Zipline
</Typography>
{user && (
<Box sx={{ marginLeft: 'auto' }}>
<Button
color='inherit'
aria-expanded={open ? 'true' : undefined}
onClick={handleClick}
>
<AccountIcon />
</Button>
<Menu
id='zipline-user-menu'
anchorEl={anchorEl}
open={open}
onClose={handleClose(null)}
MenuListProps={{
'aria-labelledby': 'basic-button',
}}
>
<MenuItem disableRipple>
<Typography variant='h5'>
<b>{user.username}</b>
</Typography>
</MenuItem>
<Divider />
<Link href='/dash/manage'>
<a style={{ color: 'white', textDecoration: 'none' }}>
<MenuItem onClick={handleClose(null)}>
<AccountIcon sx={{ mr: 2 }} /> Manage Account
</MenuItem>
</a>
<Text size='lg'>{text}</Text>
</Group>
</UnstyledButton>
</Link>
))}
{user.administrator && (
<>
<Link href='/dashboard/users' passHref>
<UnstyledButton
sx={{
display: 'block',
width: '100%',
padding: theme.spacing.xs,
borderRadius: theme.radius.sm,
color: theme.colorScheme === 'dark' ? theme.colors.dark[0] : theme.black,
'&:hover': {
backgroundColor: theme.other.hover,
},
}}
>
<Group>
<ThemeIcon color='primary' variant='filled'>
<UserIcon />
</ThemeIcon>
<Text size='lg'>Users</Text>
</Group>
</UnstyledButton>
</Link>
<MenuItem onClick={handleClose('copy')}>
<CopyIcon sx={{ mr: 2 }} /> Copy Token
</MenuItem>
<MenuItem onClick={handleClose('reset')}>
<ResetIcon sx={{ mr: 2 }} /> Reset Token
</MenuItem>
<Link href='/auth/logout'>
<a style={{ color: 'white', textDecoration: 'none' }}>
<MenuItem onClick={handleClose(null)}>
<LogoutIcon sx={{ mr: 2 }} /> Logout
</MenuItem>
</a>
<Link href='/dashboard/invites' passHref>
<UnstyledButton
sx={{
display: 'block',
width: '100%',
padding: theme.spacing.xs,
borderRadius: theme.radius.sm,
color: theme.colorScheme === 'dark' ? theme.colors.dark[0] : theme.black,
'&:hover': {
backgroundColor: theme.other.hover,
},
}}
>
<Group>
<ThemeIcon color='primary' variant='filled'>
<TagIcon />
</ThemeIcon>
<Text size='lg'>Invites</Text>
</Group>
</UnstyledButton>
</Link>
</Menu>
</>
)}
</Navbar.Section>
</Navbar>
}
header={
<Header height={70} p='md'>
<div style={{ display: 'flex', alignItems: 'center', height: '100%' }}>
<MediaQuery largerThan='sm' styles={{ display: 'none' }}>
<Burger
opened={opened}
onClick={() => setOpened((o) => !o)}
size='sm'
color={theme.colors.gray[6]}
/>
</MediaQuery>
<Title ml='md'>{title}</Title>
<Box sx={{ marginLeft: 'auto', marginRight: 0 }}>
<Popover
position='top'
placement='end'
spacing={4}
opened={open}
onClose={() => setOpen(false)}
target={
<UnstyledButton
onClick={() => setOpen(!open)}
sx={{
display: 'block',
width: '100%',
padding: theme.spacing.xs,
borderRadius: theme.radius.sm,
color: theme.other.color,
'&:hover': {
backgroundColor: theme.other.hover,
},
}}
>
<Group>
<ThemeIcon color='primary' variant='filled'>
<SettingsIcon />
</ThemeIcon>
<Text>{user.username}</Text>
</Group>
</UnstyledButton>
}
>
<Group direction='column' spacing={2}>
<Text sx={{
color: theme.colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6],
fontWeight: 500,
fontSize: theme.fontSizes.sm,
padding: `${theme.spacing.xs / 2}px ${theme.spacing.sm}px`,
cursor: 'default',
}}
>
{user.username}
</Text>
<MenuItemLink icon={<SettingsIcon />} href='/dashboard/manage'>Manage Account</MenuItemLink>
<MenuItem icon={<CopyIcon />} onClick={() => {setOpen(false);openCopyToken();}}>Copy Token</MenuItem>
<MenuItem icon={<DeleteIcon />} onClick={() => {setOpen(false);openResetToken();}} color='red'>Reset Token</MenuItem>
<MenuItemLink icon={<LogoutIcon />} href='/auth/logout' color='red'>Logout</MenuItemLink>
<Divider
variant='solid'
my={theme.spacing.xs / 2}
sx={theme => ({
width: '110%',
borderTopColor: theme.colorScheme === 'dark' ? theme.colors.dark[7] : theme.colors.gray[2],
margin: `${theme.spacing.xs / 2}px -4px`,
})}
/>
<MenuItem icon={<PencilIcon />}>
<Select
size='xs'
data={Object.keys(themes).map(t => ({ value: t, label: friendlyThemeName[t] }))}
value={systemTheme}
onChange={handleUpdateTheme}
/>
</MenuItem>
</Group>
</Popover>
</Box>
)}
</Toolbar>
</AppBar>
<Box
component='nav'
sx={{
width: { sm: drawerWidth },
flexShrink: { sm: 0 }
}}
>
<Drawer
container={container}
variant='temporary'
onClose={() => setMobileOpen(false)}
open={mobileOpen}
elevation={0}
ModalProps={{
keepMounted: true
}}
sx={{
display: { xs: 'block', sm: 'none' },
'* .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth }
}}
>
{drawer}
</Drawer>
<Drawer
variant='permanent'
sx={{
display: { xs: 'none', sm: 'block' },
'* .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth }
}}
open
>
{drawer}
</Drawer>
</Box>
<Box component='main' sx={{ flexGrow: 1, p: 3, mt: 8 }}>
{user && noPaper ? children : (
<Paper elevation={0} sx={{ p: 2 }} variant='outlined'>
{children}
</Paper>
)}
</Box>
</Box>
</div>
</Header>
}
>
<Paper withBorder p='md' shadow='xs'>{children}</Paper>
</AppShell>
);
}
}

View File

@@ -1,75 +1,3 @@
/// https://github.com/mui-org/material-ui/blob/next/examples/nextjs/src/Link.js
/* eslint-disable jsx-a11y/anchor-has-content */
import React, { forwardRef } from 'react';
import clsx from 'clsx';
import { useRouter } from 'next/router';
import NextLink from 'next/link';
import MuiLink from '@material-ui/core/Link';
export const NextLinkComposed = forwardRef(function NextLinkComposed(props: any, ref) {
const { to, linkAs, href, replace, scroll, passHref, shallow, prefetch, locale, ...other } =
props;
return (
<NextLink
href={to}
prefetch={prefetch}
as={linkAs}
replace={replace}
scroll={scroll}
shallow={shallow}
passHref={passHref}
locale={locale}
>
<a ref={ref} {...other} />
</NextLink>
);
});
// A styled version of the Next.js Link component:
// https://nextjs.org/docs/#with-link
const Link = forwardRef(function Link(props: any, ref) {
const {
activeClassName = 'active',
as: linkAs,
className: classNameProps,
href,
noLinkStyle,
role, // Link don't have roles.
...other
} = props;
const router = useRouter();
const pathname = typeof href === 'string' ? href : href.pathname;
const className = clsx(classNameProps, {
[activeClassName]: router.pathname === pathname && activeClassName,
});
const isExternal =
typeof href === 'string' && (href.indexOf('http') === 0 || href.indexOf('mailto:') === 0);
if (isExternal) {
if (noLinkStyle) {
return <a className={className} href={href} ref={ref} {...other} />;
}
return <MuiLink className={className} href={href} ref={ref} {...other} />;
}
if (noLinkStyle) {
return <NextLinkComposed className={className} ref={ref} to={href} {...other} />;
}
return (
<MuiLink
component={NextLinkComposed}
linkAs={linkAs}
className={className}
ref={ref}
to={href}
{...other}
/>
);
});
import { NextLink as Link } from '@mantine/next';
export default Link;

View File

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

View File

@@ -0,0 +1,75 @@
// https://mantine.dev/core/password-input/
import { useState } from 'react';
import { PasswordInput, Progress, Text, Popover, Box } from '@mantine/core';
import { CheckIcon, CrossIcon } from './icons';
function PasswordRequirement({ meets, label }: { meets: boolean; label: string }) {
return (
<Text
color={meets ? 'teal' : 'red'}
sx={{ display: 'flex', alignItems: 'center' }}
mt='sm'
size='sm'
>
{meets ? <CheckIcon /> : <CrossIcon />} <Box ml='md'>{label}</Box>
</Text>
);
}
const requirements = [
{ re: /[0-9]/, label: 'Includes number' },
{ re: /[a-z]/, label: 'Includes lowercase letter' },
{ re: /[A-Z]/, label: 'Includes uppercase letter' },
{ re: /[$&+,:;=?@#|'<>.^*()%!-]/, label: 'Includes special symbol' },
];
function getStrength(password: string) {
let multiplier = password.length > 7 ? 0 : 1;
requirements.forEach((requirement) => {
if (!requirement.re.test(password)) {
multiplier += 1;
}
});
return Math.max(100 - (100 / (requirements.length + 1)) * multiplier, 10);
}
export default function PasswordStrength({ value, setValue, setStrength, ...props }) {
const [popoverOpened, setPopoverOpened] = useState(false);
const checks = requirements.map((requirement, index) => (
<PasswordRequirement key={index} label={requirement.label} meets={requirement.re.test(value)} />
));
const strength = getStrength(value);
setStrength(strength);
const color = strength === 100 ? 'teal' : strength > 50 ? 'yellow' : 'red';
return (
<Popover
opened={popoverOpened}
position='bottom'
placement='start'
withArrow
trapFocus={false}
transition='pop-top-left'
onFocusCapture={() => setPopoverOpened(true)}
onBlurCapture={() => setPopoverOpened(false)}
styles={{ root: { width: '100%' } }}
target={
<PasswordInput
label='Password'
description='Strong password should include letters in lower and uppercase, at least 1 number, at least 1 special symbol'
value={value}
onChange={(event) => setValue(event.currentTarget.value)}
{...props}
/>
}
>
<Progress color={color} value={strength} size={7} mb='md' />
<PasswordRequirement label='Includes at least 8 characters' meets={value.length > 7} />
{checks}
</Popover>
);
}

View File

@@ -0,0 +1,29 @@
import { Box, Table } from '@mantine/core';
import { randomId } from '@mantine/hooks';
export function SmallTable({ rows, columns }) {
return (
<Box sx={{ pt: 1 }}>
<Table highlightOnHover>
<thead>
<tr>
{columns.map(col => (
<th key={randomId()}>{col.name}</th>
))}
</tr>
</thead>
<tbody>
{rows.map(row => (
<tr key={randomId()}>
{columns.map(col => (
<td key={randomId()}>
{col.format ? col.format(row[col.id]) : row[col.id]}
</td>
))}
</tr>
))}
</tbody>
</Table>
</Box>
);
}

View File

@@ -1,65 +1,96 @@
import React from 'react';
import { ThemeProvider } from '@emotion/react';
import { CssBaseline } from '@material-ui/core';
import dark_blue from 'lib/themes/dark_blue';
import dark from 'lib/themes/dark';
import { useEffect } from 'react';
// themes
import ayu_dark from 'lib/themes/ayu_dark';
import ayu_mirage from 'lib/themes/ayu_mirage';
import ayu_light from 'lib/themes/ayu_light';
import ayu_mirage from 'lib/themes/ayu_mirage';
import dark from 'lib/themes/dark';
import dark_blue from 'lib/themes/dark_blue';
import dracula from 'lib/themes/dracula';
import light_blue from 'lib/themes/light_blue';
import matcha_dark_azul from 'lib/themes/matcha_dark_azul';
import nord from 'lib/themes/nord';
import polar from 'lib/themes/polar';
import qogir_dark from 'lib/themes/qogir_dark';
import { MantineProvider, MantineThemeOverride } from '@mantine/core';
import { useColorScheme } from '@mantine/hooks';
import { ModalsProvider } from '@mantine/modals';
import { NotificationsProvider } from '@mantine/notifications';
import { useStoreSelector } from 'lib/redux/store';
import createTheme from 'lib/themes';
export const themes = {
'dark_blue': dark_blue,
'dark': dark,
'ayu_dark': ayu_dark,
'ayu_mirage': ayu_mirage,
'ayu_light': ayu_light,
'nord': nord,
'polar': polar
system: (colorScheme: 'dark' | 'light') => colorScheme === 'dark' ? dark_blue : light_blue,
dark_blue,
light_blue,
dark,
ayu_dark,
ayu_mirage,
ayu_light,
nord,
dracula,
matcha_dark_azul,
qogir_dark,
};
export const friendlyThemeName = {
'system': 'System Theme',
'dark_blue': 'Dark Blue',
'light_blue': 'Light Blue',
'dark': 'Very Dark',
'ayu_dark': 'Ayu Dark',
'ayu_mirage': 'Ayu Mirage',
'ayu_light': 'Ayu Light',
'nord': 'Nord',
'polar': 'Polar'
'dracula': 'Dracula',
'matcha_dark_azul': 'Matcha Dark Azul',
'qogir_dark': 'Qogir Dark',
};
export default function ZiplineTheming({ Component, pageProps }) {
let t;
export default function ZiplineTheming({ Component, pageProps, ...props }) {
const user = useStoreSelector(state => state.user);
if (!user) t = themes.dark_blue;
else {
if (user.customTheme) {
t = createTheme({
type: 'dark',
primary: user.customTheme.primary,
secondary: user.customTheme.secondary,
error: user.customTheme.error,
warning: user.customTheme.warning,
info: user.customTheme.info,
border: user.customTheme.border,
background: {
main: user.customTheme.mainBackground,
paper: user.customTheme.paperBackground
}
});
} else {
t = themes[user.systemTheme] ?? themes.dark_blue;
}
}
const colorScheme = useColorScheme();
let theme: MantineThemeOverride;
if (!user) theme = themes.system(colorScheme);
else if (user.systemTheme === 'system') theme = themes.system(colorScheme);
else theme = themes[user.systemTheme] ?? themes.system(colorScheme);
useEffect(() => {
document.documentElement.style.setProperty('color-scheme', theme.colorScheme);
}, [user, theme]);
return (
<ThemeProvider theme={t}>
<CssBaseline />
<Component {...pageProps} />
</ThemeProvider>
<MantineProvider
withGlobalStyles
withNormalizeCSS
theme={theme}
styles={{
AppShell: t => ({
root: {
backgroundColor: t.other.AppShell_backgroundColor,
},
}),
Popover: {
inner: {
width: 200,
},
},
Accordion: {
itemTitle: {
border: 0,
},
itemOpened: {
border: 0,
},
},
}}
>
<ModalsProvider>
<NotificationsProvider>
{props.children ? props.children : <Component {...pageProps} />}
</NotificationsProvider>
</ModalsProvider>
</MantineProvider>
);
}

46
src/components/Type.tsx Normal file
View File

@@ -0,0 +1,46 @@
import { Group, Image, Stack, Text } from '@mantine/core';
import { Prism } from '@mantine/prism';
import { useEffect, useState } from 'react';
import { AudioIcon, FileIcon, PlayIcon, TypeIcon, VideoIcon } from './icons';
import MutedText from './MutedText';
function Placeholder({ text, Icon, ...props }) {
return (
<Image height={200} withPlaceholder placeholder={
<Group>
<Icon size={48} />
<Text>{text}</Text>
</Group>
} {...props} />
);
}
export default function Type({ file, popup = false, ...props }){
const type = (file.type || file.mimetype).split('/')[0];
const name = (file.name || file.file);
const [text, setText] = useState('');
if (type === 'text') {
useEffect(() => {
(async () => {
const res = await fetch('/r/' + name);
const text = await res.text();
setText(text);
})();
}, []);
}
return popup ? {
'video': <video width='100%' autoPlay controls {...props} />,
'image': <Image {...props} />,
'audio': <audio autoPlay controls {...props} style={{ width: '100%' }}/>,
'text': <Prism withLineNumbers language={name.split('.').pop()} {...props} style={{}} sx={{}}>{text}</Prism>,
}[type] : {
'video': <Placeholder Icon={PlayIcon} text={`Click to view video (${name})`} {...props} />,
'image': <Image {...props} />,
'audio': <Placeholder Icon={AudioIcon} text={`Click to view audio (${name})`} {...props}/>,
'text': <Placeholder Icon={FileIcon} text={`Click to view text file (${name})`} {...props}/>,
}[type];
};

View File

@@ -0,0 +1,52 @@
import React from 'react';
import { Dropzone as MantineDropzone } from '@mantine/dropzone';
import { Group, Text, useMantineTheme } from '@mantine/core';
import { CrossIcon, UploadIcon, ImageIcon } from 'components/icons';
function ImageUploadIcon({ status, ...props }) {
if (status.accepted) {
return <UploadIcon {...props} />;
}
if (status.rejected) {
return <CrossIcon {...props} />;
}
return <ImageIcon {...props} />;
}
function getIconColor(status, theme) {
return status.accepted
? theme.colors[theme.primaryColor][6]
: status.rejected
? theme.colors.red[6]
: theme.colorScheme === 'dark'
? theme.colors.dark[0]
: theme.black;
}
export default function Dropzone({ loading, onDrop, children }) {
const theme = useMantineTheme();
return (
<MantineDropzone loading={loading} onDrop={onDrop}>
{status => (
<>
<Group position='center' spacing='xl' style={{ minHeight: 440, pointerEvents: 'none' }}>
<ImageUploadIcon
status={status}
style={{ width: 80, height: 80, color: getIconColor(status, theme) }}
/>
<Text size='xl' inline>
Drag images here or click to select files
</Text>
</Group>
{children}
</>
)}
</MantineDropzone>
);
}

View File

@@ -0,0 +1,54 @@
import React from 'react';
import { Table, Tooltip, Badge, useMantineTheme } from '@mantine/core';
import Type from 'components/Type';
export function FilePreview({ file }: { file: File }) {
return (
<Type
file={file}
autoPlay
sx={{ maxWidth: '10vw', maxHeight: '100vh' }}
style={{ maxWidth: '10vw', maxHeight: '100vh' }}
src={URL.createObjectURL(file)}
alt={file.name}
/>
);
}
export default function FileDropzone({ file }: { file: File }) {
const theme = useMantineTheme();
return (
<Tooltip
position='top'
placement='center'
allowPointerEvents
label={
<div style={{ display: 'flex', alignItems: 'center' }}>
<FilePreview file={file} />
<Table sx={{ color: theme.colorScheme === 'dark' ? 'black' : 'white' }} ml='md'>
<tbody>
<tr>
<td>Name</td>
<td>{file.name}</td>
</tr>
<tr>
<td>Type</td>
<td>{file.type}</td>
</tr>
<tr>
<td>Last Modified</td>
<td>{new Date(file.lastModified).toLocaleString()}</td>
</tr>
</tbody>
</Table>
</div>
}
>
<Badge size='lg'>
{file.name}
</Badge>
</Tooltip>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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