Compare commits

...

177 Commits

Author SHA1 Message Date
diced
a1f281d8b4 feat(v3.6.0): version 2022-10-31 20:02:55 -07:00
diced
d2f3999cf1 fix: expires/expired text change 2022-10-31 16:43:12 -07:00
TacticalCoderJay
87fc9f2fb9 feat: v3.6.0-rc4 (#207)
* feat: oauth reform for potential improvements

* fix: update avatars with new pfp

* fix: remove redundant include

* feat: v3.6.0-rc4

Co-authored-by: dicedtomato <diced@users.noreply.github.com>

* fix: remove console log

Co-authored-by: dicedtomato <diced@users.noreply.github.com>
2022-10-30 21:42:39 -07:00
diced
8c9064fd93 fix: rework image serving 2022-10-29 22:43:42 -07:00
diced
561849ae5b feat: ability to link existing accounts to oauth 2022-10-29 20:02:54 -07:00
diced
0847802ce4 fix: remove console.log 2022-10-29 14:59:26 -07:00
diced
d5a8b3f1fb chore: update workflows node@18 2022-10-29 10:53:21 -07:00
diced
e6cebd8c46 fix: update node@18, fix views aggregation, force update stats 2022-10-29 10:52:35 -07:00
TacticalCoderJay
f2be036bac feat: issue template (#202)
* feat: new issue templates

* fix: unique ids

* fix: typo

* fix: tabbing

* Update bug.yml

* Update suggest.yml

Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
2022-10-29 09:56:39 -07:00
diced
f14448d40d fix: add id to version 2022-10-28 21:14:34 -07:00
diced
bf719808f2 fix: merge changes to release 2022-10-28 21:09:41 -07:00
diced
9dd82c91d7 fix: actions outputs maybe 2022-10-28 21:08:40 -07:00
diced
535f84064a fix: actions outputs 2022-10-28 21:05:44 -07:00
diced
0c0a55d766 fix: update actions/cache 2022-10-28 21:01:49 -07:00
diced
6e3ee29eb4 fix: upgrade checkout action 2022-10-28 21:01:02 -07:00
diced
6a7a5dc7a3 fix: migrate from save-state to env 2022-10-28 20:57:01 -07:00
diced
e78d2d79d0 feat: upgrade docker actions 2022-10-28 20:53:13 -07:00
diced
451027eaf3 feat: add cache to releawes 2022-10-28 20:44:43 -07:00
diced
e4491610fb feat: add trunk-(version) to tag & test cache 2022-10-28 20:41:39 -07:00
diced
f30e10f235 feat: try adding docker caching 2022-10-28 19:37:09 -07:00
diced
f9249b1380 feat: even more for URLs 2022-10-28 17:41:46 -07:00
diced
3df94526b0 feat: add more info to cards & relative time 2022-10-28 17:28:15 -07:00
diced
b30b7b1bd3 fix: #204 2022-10-28 16:47:16 -07:00
dicedtomato
a9defd67d6 feat: add screenshots to readme 2022-10-27 21:53:12 -07:00
diced
68d346e69d fix: stuff 2022-10-27 21:26:54 -07:00
diced
e2fd27cbba feat(3.6.0-rc3): version & bump deps 2022-10-27 21:09:14 -07:00
diced
4c0532006c fix: resolve multiple el in titles 2022-10-27 21:07:44 -07:00
diced
7ac574b230 fix: support .env file 2022-10-27 19:38:54 -07:00
diced
7eb855de8f feat: new file serving method & max views for files 2022-10-27 19:34:20 -07:00
diced
d5984f4141 chore: make things work on next 13 2022-10-26 18:20:56 -07:00
diced
b7c0c85639 chore: bump nextjs 2022-10-25 17:52:04 -07:00
diced
84ba166aea feat: file chunking for large uploads 2022-10-24 18:31:49 -07:00
diced
bd79858681 feat(3.6.0-rc2): version 2022-10-24 11:39:42 -07:00
diced
0f10fa3991 fix: uniform margins 2022-10-24 11:38:38 -07:00
diced
74b1799d21 feat: user registration without oauth 2022-10-24 11:28:06 -07:00
diced
4552643ff8 feat: more options for text uploads & password protect them 2022-10-24 11:10:32 -07:00
diced
d432b388f6 feat: preview text uploads 2022-10-24 10:57:13 -07:00
diced
a8475602c7 feat: add port config opt for s3 2022-10-23 12:33:04 -07:00
diced
f58d33af9e fix: recurse for entryPoints 2022-10-23 12:16:59 -07:00
diced
0150ea5e70 fix: spacing between main dashboard elements 2022-10-23 11:57:24 -07:00
diced
3bf43f1606 fix: maybe fix exports 2022-10-23 10:36:38 -07:00
diced
b8729a6ec7 fix: save imported files to datasource 2022-10-23 10:14:29 -07:00
diced
1f44aa7e85 fix: await on prisma.image.createMany 2022-10-23 10:09:48 -07:00
diced
2bd5352fc5 feat: import-dir script 2022-10-23 10:08:21 -07:00
diced
a90130e8bf feat(v3.6.0-rc1): small fixes 2022-10-22 23:42:52 -07:00
diced
642e8796f0 feat: oauth info in user dropdown 2022-10-22 16:10:31 -07:00
diced
615cbddc89 feat: ability to edit/delete users with master administrator 2022-10-22 14:50:53 -07:00
diced
4ef82bdff4 fix: prettier 2022-10-22 14:30:50 -07:00
diced
dafde04c2c fix: config validation for discord is null 2022-10-22 14:26:34 -07:00
diced
1be61b8d89 chore: update deps & tsx -> esbuild 2022-10-22 14:23:23 -07:00
diced
c3215c7425 fix: add oauth to readme 2022-10-19 19:49:25 -07:00
Jonathan
af0cd26ea0 feat: prettier run (#200)
* feat: prettier run

* fix: whatever that was

* chore: format more files

* chore: make format command better
2022-10-19 19:43:01 -07:00
Jonathan
cb7dacd089 perf: config validation improvements (#192)
* perf: improve config validation

* chore: remove extra space in package.json

* fix: actually update file

* fix: `datasource.local` not providing a default value

* fix: small oversight in readConfig & better error

Co-authored-by: diced <pranaco2@gmail.com>
2022-10-18 22:15:15 -07:00
TacticalCoderJay
8c04971094 fix: create -> save (edit user) (#199) 2022-10-18 20:19:40 -07:00
diced
3a4802f09a fix: checks for export & refresh button 2022-10-17 17:20:54 -07:00
TacticalCoderJay
d78db306c5 fix: use os.tmpdir instead of hardcoded /tmp (#198) 2022-10-17 17:05:38 -07:00
TacticalCoderJay
3f8790ece1 fix: only display exports that are your own (#197) 2022-10-17 16:48:01 -07:00
TacticalCoderJay
f9e6158144 hotfix: change text to adjusted limit (#196) 2022-10-17 07:59:33 -07:00
TacticalCoderJay
05de3fed15 feat: ability to create many invites (#194) (#183)
* feat: Create many invites added.

* Update src/pages/api/auth/invite.ts

Co-authored-by: Jonathan <axis@axis.moe>

* fix: Lowered limit.

Co-authored-by: Jonathan <axis@axis.moe>
2022-10-16 20:18:21 -07:00
TacticalCoderJay
38cba9cb39 fix: Follow proper linebreak style. (#191) 2022-10-16 20:10:52 -07:00
diced
a4af980e11 fix: password strength not showing up on invites (#186) 2022-10-16 14:32:27 -07:00
diced
940b844857 feat: admins can't edit/delete other admins 2022-10-16 14:12:44 -07:00
diced
41b766216e feat: naming & views on files #187 #181 2022-10-16 14:06:32 -07:00
Jonathan
402987baba fix(dashboard): error when fetching stats (#193)
Handles an edge case where stats.data is a 1 length array since there's no before data
2022-10-16 13:44:46 -07:00
Winter
3cb08c73d3 feat: add S3 SSL as an env variable (#188)
Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
2022-10-15 12:33:37 -07:00
Derock
4cb92a7257 fix: NaN on stat card (#179)
* fix: NaN % change

* ref: format

Co-authored-by: Jonathan <axis@axis.moe>

Co-authored-by: Jonathan <axis@axis.moe>
Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
2022-10-15 12:22:57 -07:00
diced
a095768eae ref: change look of oauth 2022-10-13 21:47:56 -07:00
diced
1a5925d7e8 fix: add minWidth to user dropdown 2022-10-08 21:28:36 -07:00
diced
9147847710 fix: make line graph tooltips work everywhere 2022-10-08 21:24:05 -07:00
diced
05fe8bcaca fix: remove debug logs 2022-10-08 21:07:55 -07:00
diced
b0c3c6f45a feat: oauth sign up 2022-10-08 17:58:56 -07:00
diced
0f641aa852 fix: make last icon flat 2022-10-02 16:01:33 -07:00
diced
2651bbe50c fix: change readme 2022-10-02 16:01:05 -07:00
diced
d31371eb6c feat: configurable invites & disable_media_preview config 2022-10-02 15:39:59 -07:00
diced
ec0e7e5ec7 feat: edit users (admin-only) 2022-10-01 14:04:18 -07:00
diced
feb75a8a42 fix(config): add env for UPLOADER_DATE_FORMAT 2022-10-01 13:34:41 -07:00
diced
d4369d2503 refactor: redux -> recoil 2022-10-01 11:09:07 -07:00
diced
d236589644 fix: tidy up stuff 2022-10-01 11:09:07 -07:00
diced
8044b7f623 refactor: move clientUtils into utils/ 2022-10-01 11:09:07 -07:00
dicedtomato
9f0697dd34 feat: add contributing.md 2022-09-30 16:16:08 -07:00
diced
78a6f3122d fix: escape . in regexes 2022-09-28 20:45:50 -07:00
diced
b460da74dd refactor: change bool() -> boolean() 2022-09-28 20:30:24 -07:00
Wolfy0615
75a8bb7962 fix: change outdated url (#172)
* Fix outdated url

Updated to resolve merge conflicts

* Update index.tsx

Fix #2
2022-09-28 19:50:01 -07:00
diced
9ac876e30a feat: ability to view non-media views 2022-09-24 14:25:34 -07:00
diced
26cb4ea034 fix: placeholder and red buttons 2022-09-24 10:43:23 -07:00
diced
0d65ee1a32 feat: flameshot generator 2022-09-24 10:41:03 -07:00
Erik Bender
4a753376b7 feat: add password field in file upload ui (#169) 2022-09-24 10:14:36 -07:00
Derock
dc926e9f5a feat: overhaul a lot of stuff (#171)
* feat: ssr for /code/[id], fix: syntax highlighting

* feat: cache responses

* ref: eslint

* wip

* Create .gitattributes

* wip again

* redesign dashboard

* ref: use react-query for url
ref: break into components
feat: loading animation for delete
feat: no url image

* feat: use react-query mutation for files

* ref: update sizing on code input

* chore(deps): update mantine

* feat: overhaul stats page

* fix: incorrectly calculating stat % change

* fix: use latest data in stats per day

* feat: add validation on stats amount query string

* refactor: clean up imports & code

* fix: remove prettier (fixes eslint)

* ref: run eslint autofix

* ref: more eslint

* ref: replace undraw on homepage with react-feather

* refactor: remove tailwind & add responsiveness to stuff

* fix: colors on file placeholder

* fix: make actions work

* feat: new sharex configuration generator

Co-authored-by: diced <pranaco2@gmail.com>
2022-09-23 18:19:27 -07:00
diced
722372c7f6 fix: #173 2022-09-11 12:08:38 -07:00
diced
4589c6ee0a fix: #166 2022-09-05 15:06:07 -07:00
diced
67ff93e640 fix: remove useless webpack cofnig 2022-08-26 20:38:31 -07:00
diced
bd055d704b fix: domain duplication 2022-08-26 20:35:25 -07:00
diced
2e8bee931c feat: add a debug read-config script 2022-08-26 20:06:57 -07:00
diced
a454a4f4a8 feat: external links & bug fixes 2022-08-26 20:04:25 -07:00
diced
45541a3cdd feat: add version to appshell 2022-08-24 20:37:57 -07:00
diced
1d42d922bd feat: discord webhook notifs 2022-08-23 09:38:29 -07:00
diced
4f631fbd0e feat: more ways to expire 2022-08-21 22:24:56 -07:00
diced
e911db4c1a fix: image table on dashboard 2022-08-17 15:01:23 -07:00
diced
9b60147e11 feat(v3.5.0): version 2022-08-16 15:25:44 -07:00
diced
acd0cabdff feat: update prisma binaries 2022-08-16 15:24:09 -07:00
diced
d41f6058f7 feat: user avatars 2022-08-16 14:50:59 -07:00
diced
8f835eec4e feat: add image compression 2022-08-16 14:04:11 -07:00
diced
ecab525ffd fix: text & video embed 2022-08-15 20:55:46 -07:00
diced
7c887e8ec1 fix: vanities can be overwritten 2022-07-28 14:03:29 -07:00
diced
f3a23a528b feat: expiring images 2022-07-28 13:53:46 -07:00
diced
cdcb31130b feat: switch to tsx (typescript execute) 2022-07-28 11:30:14 -07:00
diced
3ea24ddf0c feat: switch to mantine v5 2022-07-28 11:03:22 -07:00
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
233 changed files with 21310 additions and 10165 deletions

View File

@@ -2,3 +2,6 @@ node_modules/
.next/
uploads/
.git/
.yarn/*
!.yarn/releases
!.yarn/plugins

47
.env.local.example Normal file
View File

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

View File

@@ -1,12 +1,18 @@
{
"extends": ["next", "next/core-web-vitals"],
"extends": ["next", "next/core-web-vitals", "plugin:prettier/recommended"],
"rules": {
"indent": ["error", 2],
"linebreak-style": ["error", "unix"],
"quotes": ["error", "single"],
"quotes": [
"error",
"single",
{
"avoidEscape": true
}
],
"semi": ["error", "always"],
"comma-dangle": ["error", "always-multiline"],
"jsx-quotes": ["error", "prefer-single"],
"indent": "off",
"react/prop-types": "off",
"react-hooks/rules-of-hooks": "off",
"react-hooks/exhaustive-deps": "off",
@@ -17,9 +23,11 @@
"react/no-direct-mutation-state": "warn",
"react/no-is-mounted": "warn",
"react/no-typos": "error",
"react/react-in-jsx-scope": "error",
"react/react-in-jsx-scope": "off",
"react/require-render-return": "error",
"react/style-prop-object": "warn",
"@next/next/no-img-element": "off"
"@next/next/no-img-element": "off",
"jsx-a11y/alt-text": "off",
"react/display-name": "off"
}
}
}

14
.gitattributes vendored Normal file
View File

@@ -0,0 +1,14 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text eol=lf
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
*.c text
*.h text
# Declare files that will always have CRLF line endings on checkout.
*.sln text eol=crlf
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary

45
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: Bug
description: File a bug report
title: 'Bug: '
labels: ['bug']
body:
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Provide steps to reproduce the bug, and some context.
value: 'A bug happened!'
validations:
required: true
- type: dropdown
id: version
attributes:
label: Version
description: What version of Zipline are you using?
options:
- upstream
- latest
validations:
required: true
- type: dropdown
id: browsers
attributes:
label: What browser(s) are you seeing the problem on?
multiple: true
options:
- Firefox
- Chrome
- Safari
- Microsoft Edge
- type: textarea
id: zipline-logs
attributes:
label: Zipline Logs
description: Please copy and paste any relevant log output.
render: shell
- type: textarea
id: browser-logs
attributes:
label: Browser Logs
description: Please copy and paste any relevant log output.
render: shell

12
.github/ISSUE_TEMPLATE/suggest.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
name: Suggestion
description: Suggest a feature to be added
title: 'Suggestion: '
labels: ['suggestion']
body:
- type: textarea
id: suggest
attributes:
label: Suggestion
description: Be as descriptive as possible!
placeholder: What do you want in Zipline?
value: A suggestion

View File

@@ -1,4 +1,4 @@
name: 'CI: Build'
name: 'Build'
on:
push:
@@ -11,23 +11,28 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16.x'
node-version: '18.x'
- name: 'Restore dependency cache'
id: cache-restore
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
path: |
node_modules
${{ github.workspace }}/.next/cache
key: ${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
restore-keys: |
${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}-
- name: Create mock config
run: echo -e "[core]\nsecret = '12345678'\ndatabase_url = 'postgres://postgres:postgres@postgres/postgres'\n[uploader]\nroute = '/u'\ndirectory = './uploads'\n[urls]\nroute = '/go'" > config.toml
- name: Install dependencies
if: steps.cache-restore.outputs.cache-hit != 'true'
run: yarn install
- name: Build
run: yarn build
run: yarn build
env:
ZIPLINE_DOCKER_BUILD: true

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

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

View File

@@ -1,4 +1,4 @@
name: 'CD: Push Docker Images'
name: 'Push Docker Images'
on:
push:
@@ -8,6 +8,7 @@ on:
- 'server/**'
- 'prisma/**'
- '.github/**'
- 'Dockerfile'
workflow_dispatch:
jobs:
@@ -16,30 +17,34 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Push to GitHub Packages
uses: docker/build-push-action@v1
- name: Get version
id: version
run: |
echo "zipline_version=$(jq .version package.json -r)" >> $GITHUB_OUTPUT
- name: Setup QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
- name: Login to Github Packages
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
registry: docker.pkg.github.com
repository: diced/zipline/zipline
dockerfile: Dockerfile
tag_with_ref: true
push_to_dockerhub:
name: Push Image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v2
- name: Push to Docker Hub
uses: docker/build-push-action@v1
- name: Build Docker Image
uses: docker/build-push-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: diced/zipline
dockerfile: Dockerfile
tag_with_ref: true
push: true
platforms: linux/amd64,linux/arm64
tags: |
ghcr.io/diced/zipline:trunk
ghcr.io/diced/zipline:trunk-${{ steps.version.outputs.zipline_version }}
cache-from: type=gha
cache-to: type=gha,mode=max

8
.gitignore vendored
View File

@@ -5,6 +5,11 @@
/.pnp
.pnp.js
# yarn
.yarn/*
!.yarn/releases
!.yarn/plugins
# testing
/coverage
@@ -36,4 +41,5 @@ yarn-error.log*
# zipline
config.toml
uploads/
uploads/
dist/

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
18.12.0

5
.prettierrc.json Normal file
View File

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

5
.vscode/settings.json vendored Normal file
View File

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

File diff suppressed because one or more lines are too long

801
.yarn/releases/yarn-3.2.4.cjs vendored Executable file

File diff suppressed because one or more lines are too long

9
.yarnrc.yml Normal file
View File

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

23
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,23 @@
# Contributing
## Bug reports
Create an issue on GitHub, please include the following (if one of them is not applicable to the issue then it's not needed):
- The steps to reproduce the bug
- Logs of Zipline
- The version of Zipline
- Your OS & Browser including server OS
- What you were expecting to see
## Feature requests
Create an issue on GitHub, please include the following:
- Breif explanation of the feature in the title (very breif please)
- How it would work (detailed, but optional)
## Pull Requests (contributions to the codebase)
Create a pull request on GitHub. If your PR does not pass the action checks, then please fix the errors. If your PR was submitted before a release, and I have pushed a new release, please make sure to update your PR to reflect any changes, usually this is handled by GitHub.
Please make sure your code also reflects the style of the rest of the code.

View File

@@ -1,46 +1,63 @@
FROM node:16-alpine AS deps
FROM ghcr.io/diced/prisma-binaries:4.5.x as prisma
FROM node:alpine3.16 AS deps
RUN mkdir -p /prisma-engines
WORKDIR /build
COPY package.json yarn.lock ./
COPY .yarn .yarn
COPY package.json yarn.lock .yarnrc.yml ./
RUN apk add --no-cache libc6-compat
RUN yarn install --frozen-lockfile
RUN yarn install --immutable
FROM node:16-alpine AS builder
FROM node:alpine3.16 AS builder
WORKDIR /build
COPY --from=prisma /prisma-engines /prisma-engines
ENV PRISMA_QUERY_ENGINE_BINARY=/prisma-engines/query-engine \
PRISMA_MIGRATION_ENGINE_BINARY=/prisma-engines/migration-engine \
PRISMA_INTROSPECTION_ENGINE_BINARY=/prisma-engines/introspection-engine \
PRISMA_FMT_BINARY=/prisma-engines/prisma-fmt \
PRISMA_CLI_QUERY_ENGINE_TYPE=binary \
PRISMA_CLIENT_ENGINE_TYPE=binary
RUN apk add --no-cache openssl openssl-dev
COPY --from=deps /build/node_modules ./node_modules
COPY src ./src
COPY server ./server
COPY scripts ./scripts
COPY prisma ./prisma
COPY package.json yarn.lock next.config.js next-env.d.ts zip-env.d.ts tsconfig.json ./
COPY .yarn .yarn
COPY package.json yarn.lock .yarnrc.yml esbuild.config.js next.config.js next-env.d.ts zip-env.d.ts tsconfig.json mimes.json ./
ENV ZIPLINE_DOCKER_BUILD 1
ENV NEXT_TELEMETRY_DISABLED 1
RUN yarn build
FROM node:16-alpine AS runner
FROM node:alpine3.16 AS runner
WORKDIR /zipline
COPY --from=prisma /prisma-engines /prisma-engines
ENV PRISMA_QUERY_ENGINE_BINARY=/prisma-engines/query-engine \
PRISMA_MIGRATION_ENGINE_BINARY=/prisma-engines/migration-engine \
PRISMA_INTROSPECTION_ENGINE_BINARY=/prisma-engines/introspection-engine \
PRISMA_FMT_BINARY=/prisma-engines/prisma-fmt \
PRISMA_CLI_QUERY_ENGINE_TYPE=binary \
PRISMA_CLIENT_ENGINE_TYPE=binary
RUN apk add --no-cache openssl openssl-dev
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 zipline
RUN adduser --system --uid 1001 zipline
COPY --from=builder --chown=zipline:zipline /build/.next ./.next
COPY --from=builder --chown=zipline:zipline /build/node_modules ./node_modules
COPY --from=builder /build/.next ./.next
COPY --from=builder /build/node_modules ./node_modules
COPY --from=builder /build/next.config.js ./next.config.js
COPY --from=builder /build/src ./src
COPY --from=builder /build/server ./server
COPY --from=builder /build/scripts ./scripts
COPY --from=builder /build/dist ./dist
COPY --from=builder /build/prisma ./prisma
COPY --from=builder /build/tsconfig.json ./tsconfig.json
COPY --from=builder /build/package.json ./package.json
COPY --from=builder /build/mimes.json ./mimes.json
USER zipline
CMD ["node", "server"]
CMD ["node", "--enable-source-maps", "dist/server"]

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021 dicedtomato
Copyright (c) 2022 dicedtomato
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

145
README.md
View File

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

View File

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

View File

@@ -1,19 +0,0 @@
[core]
secure = true
secret = 'some secret'
host = '0.0.0.0'
port = 3000
database_url = 'postgres://postgres:postgres@postgres/postgres'
[urls]
route = '/go'
length = 6
[uploader]
route = '/u'
embed_route = '/a'
length = 6
directory = './uploads'
user_limit = 104900000 # 100mb
admin_limit = 104900000 # 100mb
disabled_extentions = ['jpg']

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

@@ -0,0 +1,34 @@
version: '3'
services:
postgres:
image: postgres
restart: always
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DATABASE=postgres
volumes:
- pg_data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 10s
timeout: 5s
retries: 5
zipline:
build:
context: .
dockerfile: Dockerfile
ports:
- '3000:3000'
restart: unless-stopped
env_file:
- .env.local
volumes:
- '$PWD/uploads:/zipline/uploads'
- '$PWD/public:/zipline/public'
depends_on:
- 'postgres'
volumes:
pg_data:

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,30 +16,22 @@ services:
retries: 5
zipline:
image: ghcr.io/diced/zipline/zipline:trunk
image: ghcr.io/diced/zipline
ports:
- '3000:3000'
restart: unless-stopped
environment:
- SECURE=false
- SECRET=changethis
- HOST=0.0.0.0
- PORT=3000
- DATABASE_URL=postgresql://postgres:postgres@postgres/postgres/
- UPLOADER_ROUTE=/u
- UPLOADER_EMBED_ROUTE=/a
- UPLOADER_LENGTH=6
- UPLOADER_DIRECTORY=./uploads
- UPLOADER_ADMIN_LIMIT=104900000
- UPLOADER_USER_LIMIT=104900000
- UPLOADER_DISABLED_EXTS=
- URLS_ROUTE=/go
- URLS_LENGTH=6
restart: always
environment:
- CORE_HTTPS=false
- CORE_SECRET=changethis
- CORE_HOST=0.0.0.0
- CORE_PORT=3000
- CORE_DATABASE_URL=postgres://postgres:postgres@postgres/postgres
- CORE_LOGGER=true
volumes:
- '$PWD/uploads:/zipline/uploads'
- './uploads:/zipline/uploads'
- '$PWD/public:/zipline/public'
depends_on:
- 'postgres'
volumes:
pg_data:
pg_data:

23
esbuild.config.js Normal file
View File

@@ -0,0 +1,23 @@
const esbuild = require('esbuild');
const { existsSync } = require('fs');
const { rm } = require('fs/promises');
const { recursiveReadDir } = require('next/dist/lib/recursive-readdir');
(async () => {
if (existsSync('./dist')) {
await rm('./dist', { recursive: true });
}
const entryPoints = await recursiveReadDir('./src', /.*\.(ts)$/, /(themes|queries|pages)/);
await esbuild.build({
tsconfig: 'tsconfig.json',
outdir: 'dist',
platform: 'node',
entryPoints,
format: 'cjs',
resolveExtensions: ['.ts', '.js'],
write: true,
sourcemap: true,
});
})();

1384
mimes.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,26 @@
/**
* @type {import('next').NextConfig}
**/
module.exports = {
async redirects() {
return [
{
source: '/',
destination: '/dashboard',
permanent: true,
},
];
},
images: {
domains: [
// For sharex icon in manage user
'getsharex.com',
// For flameshot icon, and maybe in the future other stuff from github
'raw.githubusercontent.com',
// Google Icon
'madeby.google.com',
],
},
poweredByHeader: false,
reactStrictMode: true,
};
};

View File

@@ -1,59 +1,86 @@
{
"name": "zip3",
"version": "3.3.2",
"name": "zipline",
"version": "3.6.0",
"license": "MIT",
"scripts": {
"dev": "NODE_ENV=development node server",
"build": "npm-run-all build:schema build:next",
"dev": "npm-run-all build:server dev:run",
"dev:run": "cross-env REACT_EDITOR=code NODE_ENV=development RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED=false node --enable-source-maps dist/server",
"build": "npm-run-all build:server build:schema build:next",
"build-ci": "cross-env ZIPLINE_DOCKER_BUILD=1 npm-run-all build:server build:schema build:next",
"build:server": "node esbuild.config.js",
"build:next": "next build",
"build:schema": "prisma generate --schema=prisma/schema.prisma",
"format": "prettier --write ./src/**/*.{ts,tsx} ./*.{md,js,json,yml}",
"migrate:dev": "prisma migrate dev --create-only",
"start": "node server",
"start": "node dist/server",
"lint": "next lint",
"seed": "ts-node --compiler-options \"{\\\"module\\\":\\\"commonjs\\\"}\" --transpile-only prisma/seed.ts"
"docker:run": "docker-compose up -d",
"docker:down": "docker-compose down",
"docker:build-dev": "docker-compose --file docker-compose.dev.yml up --build",
"scripts:read-config": "npm-run-all build:server && node dist/scripts/read-config",
"scripts:import-dir": "npm-run-all build:server && node dist/scripts/import-dir"
},
"dependencies": {
"@emotion/react": "^11.4.0",
"@emotion/styled": "^11.3.0",
"@iarna/toml": "2.2.5",
"@mui/icons-material": "^5.0.0",
"@mui/material": "^5.0.2",
"@mui/styles": "^5.0.1",
"@prisma/client": "^3.9.2",
"@prisma/migrate": "^3.9.2",
"@prisma/sdk": "^3.9.2",
"@reduxjs/toolkit": "^1.6.0",
"argon2": "^0.28.2",
"colorette": "^1.2.2",
"cookie": "^0.4.1",
"copy-to-clipboard": "^3.3.1",
"fecha": "^4.2.1",
"formik": "^2.2.9",
"multer": "^1.4.2",
"next": "^12.1.0",
"prisma": "^3.9.2",
"react": "17.0.2",
"react-color": "^2.19.3",
"react-dom": "17.0.2",
"react-dropzone": "^11.3.2",
"react-redux": "^7.2.4",
"redux": "^4.1.0",
"redux-thunk": "^2.3.0",
"uuid": "^8.3.2",
"yup": "^0.32.9"
"@dicedtomato/mantine-data-grid": "0.0.23",
"@emotion/react": "^11.10.5",
"@emotion/server": "^11.10.0",
"@mantine/core": "^5.6.3",
"@mantine/dropzone": "^5.6.3",
"@mantine/form": "^5.6.3",
"@mantine/hooks": "^5.6.3",
"@mantine/modals": "^5.6.3",
"@mantine/next": "^5.6.3",
"@mantine/notifications": "^5.6.3",
"@mantine/nprogress": "^5.6.3",
"@mantine/prism": "^5.6.3",
"@prisma/client": "^4.5.0",
"@prisma/internals": "^4.5.0",
"@prisma/migrate": "^4.5.0",
"@sapphire/shapeshift": "^3.7.0",
"@tanstack/react-query": "^4.13.0",
"argon2": "^0.30.1",
"chart.js": "^3.9.1",
"chartjs-plugin-datalabels": "^2.1.0",
"color-hash": "^2.0.1",
"colorette": "^2.0.19",
"cookie": "^0.5.0",
"dayjs": "^1.11.6",
"dotenv": "^16.0.3",
"dotenv-expand": "^9.0.0",
"fflate": "^0.7.4",
"find-my-way": "^7.3.1",
"minio": "^7.0.32",
"ms": "canary",
"multer": "^1.4.5-lts.1",
"next": "^13.0.0",
"prisma": "^4.5.0",
"react": "^18.2.0",
"react-chartjs-2": "^4.3.1",
"react-dom": "^18.2.0",
"react-feather": "^2.0.10",
"recoil": "^0.7.6",
"sharp": "^0.31.1"
},
"devDependencies": {
"@types/cookie": "^0.4.0",
"@types/multer": "^1.4.6",
"@types/node": "^15.12.2",
"eslint": "^7.32.0",
"eslint-config-next": "11.0.0",
"@types/cookie": "^0.5.1",
"@types/minio": "^7.0.14",
"@types/multer": "^1.4.7",
"@types/node": "^18.11.7",
"@types/react": "^18.0.24",
"@types/sharp": "^0.31.0",
"cross-env": "^7.0.3",
"esbuild": "^0.15.12",
"eslint": "^8.26.0",
"eslint-config-next": "^13.0.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"npm-run-all": "^4.1.5",
"ts-node": "^10.0.0",
"typescript": "^4.3.2"
"prettier": "^2.7.1",
"typescript": "^4.8.4"
},
"repository": {
"type": "git",
"url": "https://github.com/diced/zipline.git"
}
},
"packageManager": "yarn@3.2.4"
}

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,31 @@
/*
Warnings:
- You are about to drop the column `oauth` on the `User` table. All the data in the column will be lost.
- You are about to drop the column `oauthAccessToken` on the `User` table. All the data in the column will be lost.
- You are about to drop the column `oauthProvider` on the `User` table. All the data in the column will be lost.
*/
-- CreateEnum
CREATE TYPE "OauthProviders" AS ENUM ('DISCORD', 'GITHUB');
-- AlterTable
ALTER TABLE "User" DROP COLUMN "oauth",
DROP COLUMN "oauthAccessToken",
DROP COLUMN "oauthProvider";
-- CreateTable
CREATE TABLE "OAuth" (
"id" SERIAL NOT NULL,
"provider" "OauthProviders" NOT NULL,
"userId" INTEGER NOT NULL,
"token" TEXT NOT NULL,
CONSTRAINT "OAuth_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "OAuth_provider_key" ON "OAuth"("provider");
-- AddForeignKey
ALTER TABLE "OAuth" ADD CONSTRAINT "OAuth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

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

View File

@@ -0,0 +1,8 @@
-- DropForeignKey
ALTER TABLE "Image" DROP CONSTRAINT "Image_userId_fkey";
-- AlterTable
ALTER TABLE "Image" ALTER COLUMN "userId" DROP NOT NULL;
-- AddForeignKey
ALTER TABLE "Image" ADD CONSTRAINT "Image_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

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

View File

@@ -0,0 +1,8 @@
/*
Warnings:
- Added the required column `username` to the `OAuth` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "OAuth" ADD COLUMN "username" TEXT NOT NULL;

View File

@@ -8,34 +8,23 @@ generator client {
}
model User {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
username String
password String
password String?
avatar String?
token String
administrator Boolean @default(false)
systemTheme String @default("dark_blue")
customTheme Theme?
administrator Boolean @default(false)
superAdmin Boolean @default(false)
systemTheme String @default("system")
embedTitle String?
embedColor String @default("#2f3136")
embedSiteName String? @default("{image.file} • {user.name}")
ratelimited Boolean @default(false)
embedColor String @default("#2f3136")
embedSiteName String? @default("{image.file} • {user.name}")
ratelimit DateTime?
domains String[]
oauth OAuth[]
images Image[]
urls Url[]
}
model Theme {
id Int @id @default(autoincrement())
type String
primary String
secondary String
error String
warning String
info String
border String
mainBackground String
paperBackground String
user User @relation(fields: [userId], references: [id])
userId Int
Invite Invite[]
}
enum ImageFormat {
@@ -50,19 +39,22 @@ model Image {
file String
mimetype String @default("image/png")
created_at DateTime @default(now())
expires_at DateTime?
maxViews Int?
views Int @default(0)
favorite Boolean @default(false)
embed Boolean @default(false)
password String?
invisible InvisibleImage?
format ImageFormat @default(RANDOM)
user User @relation(fields: [userId], references: [id])
userId Int
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
userId Int?
}
model InvisibleImage {
id Int @id @default(autoincrement())
invis String @unique
imageId Int
imageId Int @unique
image Image @relation(fields: [imageId], references: [id])
}
@@ -71,6 +63,7 @@ model Url {
destination String
vanity String?
created_at DateTime @default(now())
maxViews Int?
views Int @default(0)
invisible InvisibleUrl?
user User @relation(fields: [userId], references: [id])
@@ -80,12 +73,38 @@ model Url {
model InvisibleUrl {
id Int @id @default(autoincrement())
invis String @unique
urlId String
urlId String @unique
url Url @relation(fields: [urlId], references: [id])
}
model Stats {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
created_at DateTime @default(now())
data Json
}
}
model Invite {
id Int @id @default(autoincrement())
code String @unique
created_at DateTime @default(now())
expires_at DateTime?
used Boolean @default(false)
createdBy User @relation(fields: [createdById], references: [id])
createdById Int
}
model OAuth {
id Int @id @default(autoincrement())
provider OauthProviders
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
username String
token String
refresh String?
}
enum OauthProviders {
DISCORD
GITHUB
GOOGLE
}

View File

@@ -1,31 +0,0 @@
import { PrismaClient } from '@prisma/client';
import { hashPassword, createToken } from '../src/lib/util';
const prisma = new PrismaClient();
async function main() {
const user = await prisma.user.create({
data: {
username: 'administrator',
password: await hashPassword('password'),
token: createToken(),
administrator: true,
},
});
console.log(`
When logging into Zipline for the first time, use these credentials:
Username: "${user.username}"
Password: "password"
`);
}
main()
.catch(e => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

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.core.database_url;
const files = await readdir(process.argv[2]);
const data = files.map(x => {
const mime = mimes[extname(x)] ?? 'application/octet-stream';
return {
file: x,
mimetype: mime,
userId: 1,
};
});
const prisma = new PrismaClient();
Logger.get('migrator').info('starting migrations...');
await prisma.image.createMany({
data,
});
Logger.get('migrator').info('finished migrations! It is recomended to move your old uploads folder (' + process.argv[2] + ') to the current one which is ' + config.uploader.directory);
process.exit();
})();

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,161 +0,0 @@
const next = require('next').default;
const defaultConfig = require('next/dist/server/config-shared').defaultConfig;
const { createServer } = require('http');
const { stat, mkdir } = require('fs/promises');
const { extname } = require('path');
const validateConfig = require('./validateConfig');
const Logger = require('../src/lib/logger');
const readConfig = require('../src/lib/readConfig');
const mimes = require('../scripts/mimes');
const { log, getStats, shouldUseYarn, getFile, migrations } = require('./util');
const { PrismaClient } = require('@prisma/client');
const { version } = require('../package.json');
const nextConfig = require('../next.config');
const serverLog = Logger.get('server');
const webLog = Logger.get('web');
serverLog.info(`starting zipline@${version} server`);
const dev = process.env.NODE_ENV === 'development';
(async () => {
try {
await run();
} catch (e) {
serverLog.error(e);
process.exit(1);
}
})();
async function run() {
const a = readConfig();
const config = validateConfig(a);
process.env.DATABASE_URL = config.core.database_url;
await migrations();
await mkdir(config.uploader.directory, { recursive: true });
const app = next({
dir: '.',
dev,
quiet: !dev,
hostname: config.core.host,
port: config.core.port,
conf: Object.assign(defaultConfig, nextConfig),
});
await app.prepare();
const handle = app.getRequestHandler();
const prisma = new PrismaClient();
const srv = createServer(async (req, res) => {
const parts = req.url.split('/');
if (!parts[2] || parts[2] === '') return;
if (req.url.startsWith('/r')) {
let image = await prisma.image.findFirst({
where: {
OR: [
{ file: parts[2] },
{ invisible: { invis: decodeURI(parts[2]) } },
],
},
select: {
mimetype: true,
id: true,
file: true,
invisible: true,
},
});
image && await prisma.image.update({
where: { id: image.id },
data: { views: { increment: 1 } },
});
const data = await getFile(config.uploader.directory, parts[2]);
if (!data) return app.render404(req, res);
if (!image) { // raw image
const mimetype = mimes[extname(parts[2])] ?? 'application/octet-stream';
res.setHeader('Content-Type', mimetype);
res.end(data);
} else { // raw image & update db
await prisma.image.update({
where: { id: image.id },
data: { views: { increment: 1 } },
});
res.setHeader('Content-Type', image.mimetype);
res.end(data);
}
} else if (req.url.startsWith(config.uploader.route)) {
const data = await getFile(config.uploader.directory, parts[2]);
if (!data) return app.render404(req, res);
let image = await prisma.image.findFirst({
where: {
OR: [
{ file: parts[2] },
{ invisible: { invis: decodeURI(parts[2]) } },
],
},
select: {
mimetype: true,
id: true,
file: true,
invisible: true,
embed: true,
},
});
image && await prisma.image.update({
where: { id: image.id },
data: { views: { increment: 1 } },
});
if (!image) { // raw image
const mimetype = mimes[extname(parts[2])] ?? 'application/octet-stream';
res.setHeader('Content-Type', mimetype);
res.end(data);
} else if (image.embed) { // embed image
handle(req, res);
} else { // raw image fallback
res.setHeader('Content-Type', image.mimetype);
res.end(data);
}
} else {
handle(req, res);
}
if (config.core.logger) log(req.url, res.statusCode);
});
srv.on('error', (e) => {
serverLog.error(e);
process.exit(1);
});
srv.on('listening', () => {
serverLog.info(`listening on ${config.core.host}:${config.core.port}`);
});
srv.listen(config.core.port, config.core.host ?? '0.0.0.0');
const stats = await getStats(prisma, config);
await prisma.stats.create({
data: {
data: stats,
},
});
setInterval(async () => {
const stats = await getStats(prisma, config);
await prisma.stats.create({
data: {
data: stats,
},
});
if (config.core.logger) serverLog.info('stats updated');
}, config.core.stats_interval * 1000);
}

View File

@@ -1,130 +0,0 @@
const { readFile, readdir, stat } = require('fs/promises');
const { join } = require('path');
const { Migrate } = require('@prisma/migrate/dist/Migrate.js');
const Logger = require('../src/lib/logger.js');
async function migrations() {
const migrate = new Migrate('./prisma/schema.prisma');
const diagnose = await migrate.diagnoseMigrationHistory({
optInToShadowDatabase: false,
});
if (diagnose.history?.diagnostic === 'databaseIsBehind') {
Logger.get('database').info('migrating database');
await migrate.applyMigrations();
Logger.get('database').info('finished migrating database');
}
migrate.stop();
}
function log(url) {
if (url.startsWith('/_next') || url.startsWith('/__nextjs')) return;
return Logger.get('url').info(url);
}
function shouldUseYarn() {
try {
execSync('yarnpkg --version', { stdio: 'ignore' });
return true;
} catch (e) {
return false;
}
}
async function getFile(dir, file) {
try {
const data = await readFile(join(process.cwd(), dir, file));
return data;
} catch (e) {
return null;
}
}
async function sizeOfDir(directory) {
const files = await readdir(directory);
let size = 0;
for (let i = 0, L = files.length; i !== L; ++i) {
const sta = await stat(join(directory, files[i]));
size += sta.size;
}
return size;
}
function bytesToRead(bytes) {
const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'];
let num = 0;
while (bytes > 1024) {
bytes /= 1024;
++num;
}
return `${bytes.toFixed(1)} ${units[num]}`;
}
async function getStats(prisma, config) {
const size = await sizeOfDir(join(process.cwd(), config.uploader.directory));
const byUser = await prisma.image.groupBy({
by: ['userId'],
_count: {
_all: true,
},
});
const count_users = await prisma.user.count();
const count_by_user = [];
for (let i = 0, L = byUser.length; i !== L; ++i) {
const user = await prisma.user.findFirst({
where: {
id: byUser[i].userId,
},
});
count_by_user.push({
username: user.username,
count: byUser[i]._count._all,
});
}
const count = await prisma.image.count();
const viewsCount = await prisma.image.groupBy({
by: ['views'],
_sum: {
views: true,
},
});
const typesCount = await prisma.image.groupBy({
by: ['mimetype'],
_count: {
mimetype: true,
},
});
const types_count = [];
for (let i = 0, L = typesCount.length; i !== L; ++i) types_count.push({ mimetype: typesCount[i].mimetype, count: typesCount[i]._count.mimetype });
return {
size: bytesToRead(size),
size_num: size,
count,
count_by_user: count_by_user.sort((a,b) => b.count-a.count),
count_users,
views_count: (viewsCount[0]?._sum?.views ?? 0),
types_count: types_count.sort((a,b) => b.count-a.count),
};
}
module.exports = {
migrations,
bytesToRead,
getFile,
getStats,
log,
sizeOfDir,
shouldUseYarn,
};

View File

@@ -1,40 +0,0 @@
const { object, bool, string, number, boolean, array } = require('yup');
const validator = object({
core: object({
secure: bool().default(false),
secret: string().min(8).required(),
host: string().default('0.0.0.0'),
port: number().default(3000),
database_url: string().required(),
logger: boolean().default(true),
stats_interval: number().default(1800),
}).required(),
uploader: object({
route: string().default('/u'),
embed_route: string().default('/a'),
length: number().default(6),
directory: string().default('./uploads'),
admin_limit: number().default(104900000),
user_limit: number().default(104900000),
disabled_extensions: array().default([]),
}).required(),
urls: object({
route: string().default('/go'),
length: number().default(6),
}).required(),
ratelimit: object({
user: number().default(0),
admin: number().default(0),
}),
});
module.exports = function validate(config) {
try {
return validator.validateSync(config, { abortEarly: false });
} catch (e) {
if (process.env.ZIPLINE_DOCKER_BUILD) return {};
throw `${e.errors.length} errors occured\n${e.errors.map(x => '\t' + x).join('\n')}`;
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

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

@@ -0,0 +1,195 @@
import { Button, Card, Group, LoadingOverlay, Modal, Stack, Text, Title, Tooltip } from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { showNotification } from '@mantine/notifications';
import { relativeTime } from 'lib/utils/client';
import { useFileDelete, useFileFavorite } from 'lib/queries/files';
import { useState } from 'react';
import {
CalendarIcon,
ClockIcon,
CopyIcon,
CrossIcon,
DeleteIcon,
ExternalLinkIcon,
FileIcon,
HashIcon,
ImageIcon,
StarIcon,
EyeIcon,
} from './icons';
import MutedText from './MutedText';
import Type from './Type';
import Link from './Link';
export function FileMeta({ Icon, title, subtitle, ...other }) {
return other.tooltip ? (
<Group>
<Icon size={24} />
<Tooltip label={other.tooltip}>
<Stack spacing={1}>
<Text>{title}</Text>
<MutedText size='md'>{subtitle}</MutedText>
</Stack>
</Tooltip>
</Group>
) : (
<Group>
<Icon size={24} />
<Stack spacing={1}>
<Text>{title}</Text>
<MutedText size='md'>{subtitle}</MutedText>
</Stack>
</Group>
);
}
export default function File({ image, updateImages, disableMediaPreview }) {
const [open, setOpen] = useState(false);
const deleteFile = useFileDelete();
const favoriteFile = useFileFavorite();
const clipboard = useClipboard();
const loading = deleteFile.isLoading || favoriteFile.isLoading;
const handleDelete = async () => {
deleteFile.mutate(image.id, {
onSuccess: () => {
showNotification({
title: 'File Deleted',
message: '',
color: 'green',
icon: <DeleteIcon />,
});
},
onError: (res: any) => {
showNotification({
title: 'Failed to delete file',
message: res.error,
color: 'red',
icon: <CrossIcon />,
});
},
onSettled: () => {
setOpen(false);
},
});
};
const handleCopy = () => {
clipboard.copy(`${window.location.protocol}//${window.location.host}${image.url}`);
setOpen(false);
showNotification({
title: 'Copied to clipboard',
message: '',
icon: <CopyIcon />,
});
};
const handleFavorite = async () => {
favoriteFile.mutate(
{ id: image.id, favorite: !image.favorite },
{
onSuccess: () => {
showNotification({
title: 'Image is now ' + (!image.favorite ? 'favorited' : 'unfavorited'),
message: '',
icon: <StarIcon />,
});
},
onError: (res: any) => {
showNotification({
title: 'Failed to favorite file',
message: res.error,
color: 'red',
icon: <CrossIcon />,
});
},
}
);
};
return (
<>
<Modal opened={open} onClose={() => setOpen(false)} title={<Title>{image.file}</Title>} size='xl'>
<LoadingOverlay visible={loading} />
<Stack>
<Type
file={image}
src={`/r/${image.file}`}
alt={image.file}
popup
sx={{ minHeight: 200 }}
style={{ minHeight: 200 }}
disableMediaPreview={false}
/>
<Stack>
<FileMeta Icon={FileIcon} title='Name' subtitle={image.file} />
<FileMeta Icon={ImageIcon} title='Type' subtitle={image.mimetype} />
<FileMeta Icon={EyeIcon} title='Views' subtitle={image?.views?.toLocaleString()} />
{image.maxViews && (
<FileMeta
Icon={EyeIcon}
title='Max views'
subtitle={image?.maxViews?.toLocaleString()}
tooltip={`This file will be deleted after being viewed ${image?.maxViews?.toLocaleString()} times.`}
/>
)}
<FileMeta
Icon={CalendarIcon}
title='Uploaded'
subtitle={relativeTime(new Date(image.created_at))}
tooltip={new Date(image?.created_at).toLocaleString()}
/>
{image.expires_at && (
<FileMeta
Icon={ClockIcon}
title='Expires'
subtitle={relativeTime(new Date(image.expires_at))}
tooltip={new Date(image.expires_at).toLocaleString()}
/>
)}
<FileMeta Icon={HashIcon} title='ID' subtitle={image.id} />
</Stack>
</Stack>
<Group position='right' mt='md'>
<Button onClick={handleCopy}>Copy URL</Button>
<Button onClick={handleDelete}>Delete</Button>
<Button onClick={handleFavorite}>{image.favorite ? 'Unfavorite' : 'Favorite'}</Button>
<Link href={image.url} target='_blank'>
<Button rightIcon={<ExternalLinkIcon />}>Open</Button>
</Link>
</Group>
</Modal>
<Card sx={{ maxWidth: '100%', height: '100%' }} shadow='md'>
<Card.Section>
<LoadingOverlay visible={loading} />
<Type
file={image}
sx={{
minHeight: 200,
maxHeight: 320,
fontSize: 70,
width: '100%',
cursor: 'pointer',
}}
style={{
minHeight: 200,
maxHeight: 320,
fontSize: 70,
width: '100%',
cursor: 'pointer',
}}
src={`/r/${image.file}`}
alt={image.file}
onClick={() => setOpen(true)}
disableMediaPreview={disableMediaPreview}
/>
</Card.Section>
</Card>
</>
);
}

View File

@@ -1,83 +0,0 @@
import React, { useState } from 'react';
import {
Card,
CardMedia,
CardActionArea,
Button,
Dialog,
DialogTitle,
DialogActions,
DialogContent,
} from '@mui/material';
import AudioIcon from '@mui/icons-material/Audiotrack';
import copy from 'copy-to-clipboard';
import useFetch from 'hooks/useFetch';
export default function Image({ image, updateImages }) {
const [open, setOpen] = useState(false);
const [t] = useState(image.mimetype.split('/')[0]);
const handleDelete = async () => {
const res = await useFetch('/api/user/files', 'DELETE', { id: image.id });
if (!res.error) updateImages(true);
setOpen(false);
};
const handleCopy = () => {
copy(`${window.location.protocol}//${window.location.host}${image.url}`);
setOpen(false);
};
const handleFavorite = async () => {
const data = await useFetch('/api/user/files', 'PATCH', { id: image.id, favorite: !image.favorite });
if (!data.error) updateImages(true);
};
const Type = (props) => {
return {
'video': <video controls {...props} />,
// eslint-disable-next-line jsx-a11y/alt-text
'image': <img {...props} />,
'audio': <audio controls {...props} />,
}[t];
};
return (
<>
<Card sx={{ maxWidth: '100%' }}>
<CardActionArea sx={t === 'audio' ? { justifyContent: 'center', display: 'flex', alignItems: 'center' } : {}}>
<CardMedia
sx={{ height: 320, fontSize: 70, width: '100%' }}
image={image.url}
title={image.file}
component={t === 'audio' ? AudioIcon : t} // this is done because audio without controls is hidden
onClick={() => setOpen(true)}
/>
</CardActionArea>
</Card>
<Dialog
open={open}
onClose={() => setOpen(false)}
>
<DialogTitle id='alert-dialog-title'>
{image.file}
</DialogTitle>
<DialogContent>
<Type
style={{ width: '100%' }}
src={image.url}
alt={image.url}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleDelete} color='inherit'>Delete</Button>
<Button onClick={handleCopy} color='inherit'>Copy URL</Button>
<Button onClick={handleFavorite} color='inherit'>{image.favorite ? 'Unfavorite' : 'Favorite'}</Button>
</DialogActions>
</Dialog>
</>
);
}

View File

@@ -1,411 +1,468 @@
import React, { useState } from 'react';
import Link from 'next/link';
import {
AppBar,
AppShell,
Box,
Divider,
Drawer,
IconButton,
List,
ListItem,
ListItemIcon,
ListItemText,
Toolbar,
Typography,
Burger,
Button,
Menu,
MenuItem,
Divider,
Header,
MediaQuery,
Navbar,
NavLink,
Paper,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
} from '@mui/material';
import {
Menu as MenuIcon,
Home as HomeIcon,
AccountCircle as AccountIcon,
Folder as FolderIcon,
Upload as UploadIcon,
ContentCopy as CopyIcon,
Autorenew as ResetIcon,
Logout as LogoutIcon,
PeopleAlt as UsersIcon,
Brush as BrushIcon,
Link as URLIcon,
} from '@mui/icons-material';
import copy from 'copy-to-clipboard';
import Backdrop from './Backdrop';
import { friendlyThemeName, themes } from 'components/Theming';
import Select from 'components/input/Select';
import { useRouter } from 'next/router';
import { useStoreDispatch } from 'lib/redux/store';
import { updateUser } from 'lib/redux/reducers/user';
Popover,
ScrollArea,
Select,
Stack,
Text,
Title,
UnstyledButton,
useMantineTheme,
Group,
Image,
Tooltip,
Badge,
Menu,
} from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { useModals } from '@mantine/modals';
import { showNotification } from '@mantine/notifications';
import useFetch from 'hooks/useFetch';
import { useVersion } from 'lib/queries/version';
import { userSelector } from 'lib/recoil/user';
import { capitalize } from 'lib/utils/client';
import { useRecoilState } from 'recoil';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useState } from 'react';
import {
ExternalLinkIcon,
ActivityIcon,
CheckIcon,
CopyIcon,
CrossIcon,
DeleteIcon,
FileIcon,
HomeIcon,
LinkIcon,
LogoutIcon,
PencilIcon,
SettingsIcon,
TagIcon,
TypeIcon,
UploadIcon,
UserIcon,
DiscordIcon,
GitHubIcon,
GoogleIcon,
} from './icons';
import { friendlyThemeName, themes } from './Theming';
function MenuItemLink(props) {
return (
<Link href={props.href} passHref legacyBehavior>
<MenuItem {...props} />
</Link>
);
}
function MenuItem(props) {
return (
<UnstyledButton
sx={(theme) => ({
display: 'block',
width: '100%',
padding: 5,
borderRadius: theme.radius.sm,
color: props.color
? theme.fn.themeColor(props.color, theme.colorScheme === 'dark' ? 5 : 7)
: theme.colorScheme === 'dark'
? theme.colors.dark[0]
: theme.black,
'&:hover': !props.noClick
? {
backgroundColor: props.color
? theme.fn.rgba(
theme.fn.themeColor(props.color, theme.colorScheme === 'dark' ? 9 : 0),
theme.colorScheme === 'dark' ? 0.2 : 1
)
: theme.colorScheme === 'dark'
? theme.fn.rgba(theme.colors.dark[3], 0.35)
: theme.colors.gray[0],
}
: null,
})}
{...props}
>
<Group noWrap>
<Box
sx={(theme) => ({
marginRight: theme.spacing.xs / 4,
paddingLeft: theme.spacing.xs / 2,
'& *': {
display: 'block',
},
})}
>
{props.icon}
</Box>
<Text size='sm'>{props.children}</Text>
</Group>
</UnstyledButton>
);
}
const items = [
{
icon: <HomeIcon />,
icon: <HomeIcon size={18} />,
text: 'Home',
link: '/dashboard',
},
{
icon: <FolderIcon />,
icon: <FileIcon size={18} />,
text: 'Files',
link: '/dashboard/files',
},
{
icon: <URLIcon />,
icon: <ActivityIcon size={18} />,
text: 'Stats',
link: '/dashboard/stats',
},
{
icon: <LinkIcon size={18} />,
text: 'URLs',
link: '/dashboard/urls',
},
{
icon: <UploadIcon />,
icon: <UploadIcon size={18} />,
text: 'Upload',
link: '/dashboard/upload',
},
{
icon: <TypeIcon size={18} />,
text: 'Upload Text',
link: '/dashboard/text',
},
];
const drawerWidth = 240;
const admin_items = [
{
icon: <UserIcon size={18} />,
text: 'Users',
link: '/dashboard/users',
if: (props) => true,
},
{
icon: <TagIcon size={18} />,
text: 'Invites',
link: '/dashboard/invites',
if: (props) => props.invites,
},
];
function CopyTokenDialog({ open, setOpen, token }) {
const handleCopyToken = () => {
copy(token);
setOpen(false);
export default function Layout({ children, props }) {
const [user, setUser] = useRecoilState(userSelector);
const { title, oauth_providers: unparsed } = props;
const oauth_providers = JSON.parse(unparsed);
const icons = {
GitHub: GitHubIcon,
Discord: DiscordIcon,
Google: GoogleIcon,
};
return (
<div>
<Dialog
open={open}
onClose={() => setOpen(false)}
>
<DialogTitle id='copy-dialog-title'>
Copy Token
</DialogTitle>
<DialogContent>
<DialogContentText id='copy-dialog-description'>
Make sure you don&apos;t share this token with anyone as they will be able to upload files on your behalf.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpen(false)} color='inherit' autoFocus>Cancel</Button>
<Button onClick={handleCopyToken} color='inherit'>
Copy
</Button>
</DialogActions>
</Dialog>
</div>
);
}
for (const provider of oauth_providers) {
provider.Icon = icons[provider.name];
}
function ResetTokenDialog({ open, setOpen, setToken }) {
const handleResetToken = async () => {
const a = await useFetch('/api/user/token', 'PATCH');
if (a.success) setToken(a.success);
setOpen(false);
};
return (
<div>
<Dialog
open={open}
onClose={() => setOpen(false)}
>
<DialogTitle id='reset-dialog-title'>
Reset Token
</DialogTitle>
<DialogContent>
<DialogContentText id='reset-dialog-description'>
Once you reset your token, you will have to update any uploaders to use this new token.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpen(false)} color='inherit' autoFocus>Cancel</Button>
<Button onClick={handleResetToken} color='inherit'>
Reset
</Button>
</DialogActions>
</Dialog>
</div>
);
}
const external_links = JSON.parse(props.external_links ?? '[]');
export default function Layout({ children, user, loading, noPaper }) {
const [systemTheme, setSystemTheme] = useState(user.systemTheme || 'dark_blue');
const [mobileOpen, setMobileOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);
const [copyOpen, setCopyOpen] = useState(false);
const [resetOpen, setResetOpen] = useState(false);
const [token, setToken] = useState(user?.token);
const [systemTheme, setSystemTheme] = useState(user.systemTheme ?? 'system');
const version = useVersion();
const [opened, setOpened] = useState(false); // navigation open
const [open, setOpen] = useState(false); // manage acc dropdown
const avatar = user?.avatar ?? null;
const router = useRouter();
const dispatch = useStoreDispatch();
const theme = useMantineTheme();
const modals = useModals();
const clipboard = useClipboard();
const open = Boolean(anchorEl);
const handleClick = e => setAnchorEl(e.currentTarget);
const handleClose = (cmd: 'copy' | 'reset') => () => {
switch (cmd) {
case 'copy':
setCopyOpen(true);
break;
case 'reset':
setResetOpen(true);
break;
}
setAnchorEl(null);
};
const handleUpdateTheme = async event => {
const handleUpdateTheme = async (value) => {
const newUser = await useFetch('/api/user', 'PATCH', {
systemTheme: event.target.value || 'dark_blue',
systemTheme: value || 'dark_blue',
});
setSystemTheme(newUser.systemTheme);
dispatch(updateUser(newUser));
setUser(newUser);
router.replace(router.pathname);
showNotification({
title: `Theme changed to ${friendlyThemeName[value]}`,
message: '',
color: 'green',
icon: <PencilIcon />,
});
};
const drawer = (
<div>
<CopyTokenDialog open={copyOpen} setOpen={setCopyOpen} token={token} />
<ResetTokenDialog open={resetOpen} setOpen={setResetOpen} setToken={setToken} />
<Toolbar
sx={{
width: { xs: drawerWidth },
}}
>
<AppBar
position='fixed'
elevation={0}
sx={{
borderBottom: 1,
borderBottomColor: t => t.palette.divider,
display: { xs: 'none', sm: 'block' },
}}
>
<Toolbar>
<IconButton
color='inherit'
aria-label='open drawer'
edge='start'
onClick={() => setMobileOpen(true)}
sx={{ mr: 2, display: { sm: 'none' } }}
>
<MenuIcon />
</IconButton>
<Typography
variant='h5'
noWrap
component='div'
>
Zipline
</Typography>
{user && (
<Box sx={{ marginLeft: 'auto' }}>
<Button
color='inherit'
aria-expanded={open ? 'true' : undefined}
onClick={handleClick}
>
<AccountIcon />
</Button>
<Menu
id='zipline-user-menu'
anchorEl={anchorEl}
open={open}
onClose={handleClose(null)}
MenuListProps={{
'aria-labelledby': 'basic-button',
}}
>
<MenuItem disableRipple>
<Typography variant='h5'>
<b>{user.username}</b>
</Typography>
</MenuItem>
<Divider />
<Link href='/dashboard/manage' passHref>
<MenuItem onClick={handleClose(null)}>
<AccountIcon sx={{ mr: 2 }} /> Manage Account
</MenuItem>
</Link>
<MenuItem onClick={handleClose('copy')}>
<CopyIcon sx={{ mr: 2 }} /> Copy Token
</MenuItem>
<MenuItem onClick={handleClose('reset')}>
<ResetIcon sx={{ mr: 2 }} /> Reset Token
</MenuItem>
<Link href='/auth/logout' passHref>
<MenuItem onClick={handleClose(null)}>
<LogoutIcon sx={{ mr: 2 }} /> Logout
</MenuItem>
</Link>
<MenuItem>
<BrushIcon sx={{ mr: 2 }} />
<Select
variant='standard'
label='Theme'
value={systemTheme}
onChange={handleUpdateTheme}
fullWidth
>
{Object.keys(themes).map(t => (
<MenuItem value={t} key={t}>
{friendlyThemeName[t]}
</MenuItem>
))}
</Select>
</MenuItem>
</Menu>
</Box>
)}
</Toolbar>
</AppBar>
</Toolbar>
<Divider />
<List>
{items.map((item, i) => (
<Link key={i} href={item.link} passHref>
<ListItem button>
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={item.text} />
</ListItem>
</Link>
))}
{user && user.administrator && (
<Link href='/dashboard/users' passHref>
<ListItem button>
<ListItemIcon><UsersIcon /></ListItemIcon>
<ListItemText primary='Users' />
</ListItem>
</Link>
)}
</List>
</div>
);
const openResetToken = () =>
modals.openConfirmModal({
title: <Title>Reset Token?</Title>,
children: (
<Text size='sm'>
Once you reset your token, you will have to update any uploaders to use this new token.
</Text>
),
labels: { confirm: 'Reset', cancel: 'Cancel' },
onConfirm: async () => {
const a = await useFetch('/api/user/token', 'PATCH');
if (!a.success) {
setToken(a.success);
showNotification({
title: 'Token Reset Failed',
message: a.error,
color: 'red',
icon: <CrossIcon />,
});
} else {
showNotification({
title: 'Token Reset',
message:
'Your token has been reset. You will need to update any uploaders to use this new token.',
color: 'green',
icon: <CheckIcon />,
});
}
const container = typeof window !== 'undefined' ? window.document.body : undefined;
modals.closeAll();
},
});
const openCopyToken = () =>
modals.openConfirmModal({
title: <Title>Copy Token</Title>,
children: (
<Text size='sm'>
Make sure you don&apos;t share this token with anyone as they will be able to upload files on your
behalf.
</Text>
),
labels: { confirm: 'Copy', cancel: 'Cancel' },
onConfirm: async () => {
clipboard.copy(token);
showNotification({
title: 'Token Copied',
message: 'Your token has been copied to your clipboard.',
color: 'green',
icon: <CheckIcon />,
});
modals.closeAll();
},
});
return (
<Box sx={{ display: 'flex' }}>
<Backdrop open={loading} />
<AppShell
navbarOffsetBreakpoint='sm'
fixed
navbar={
<Navbar pt='sm' hiddenBreakpoint='sm' hidden={!opened} width={{ sm: 200, lg: 230 }}>
<Navbar.Section grow component={ScrollArea}>
{items.map(({ icon, text, link }) => (
<Link href={link} key={text} passHref legacyBehavior>
<NavLink
component='a'
label={text}
icon={icon}
active={router.pathname === link}
variant='light'
/>
</Link>
))}
{user.administrator && (
<NavLink
label='Administration'
icon={<SettingsIcon />}
childrenOffset={28}
defaultOpened={admin_items.map((x) => x.link).includes(router.pathname)}
>
{admin_items
.filter((x) => x.if(props))
.map(({ icon, text, link }) => (
<Link href={link} key={text} passHref legacyBehavior>
<NavLink
component='a'
label={text}
icon={icon}
active={router.pathname === link}
variant='light'
/>
</Link>
))}
</NavLink>
)}
</Navbar.Section>
<Navbar.Section>
{external_links.length
? external_links.map(({ label, link }, i) => (
<Link href={link} passHref key={i} legacyBehavior>
<NavLink
label={label}
component='a'
target='_blank'
variant='light'
icon={<ExternalLinkIcon />}
/>
</Link>
))
: null}
</Navbar.Section>
{version.isSuccess ? (
<Navbar.Section>
<Tooltip
label={
version.data.local !== version.data.upstream
? `You are running an outdated version of Zipline, refer to the docs on how to update to ${version.data.upstream}`
: 'You are running the latest version of Zipline'
}
>
<Badge
m='md'
radius='md'
size='lg'
variant='dot'
color={version.data.local !== version.data.upstream ? 'red' : 'primary'}
>
{version.data.local}
</Badge>
</Tooltip>
</Navbar.Section>
) : null}
</Navbar>
}
header={
<Header height={70} p='md'>
<div style={{ display: 'flex', alignItems: 'center', height: '100%' }}>
<MediaQuery largerThan='sm' styles={{ display: 'none' }}>
<Burger
opened={opened}
onClick={() => setOpened((o) => !o)}
size='sm'
color={theme.colors.gray[6]}
/>
</MediaQuery>
<Title ml='sm'>{title}</Title>
<Box sx={{ marginLeft: 'auto', marginRight: 0 }}>
<Popover position='bottom-end' opened={open} onClose={() => setOpen(false)}>
<Popover.Target>
<Button
leftIcon={avatar ? <Image src={avatar} height={32} radius='md' /> : <SettingsIcon />}
onClick={() => setOpen((o) => !o)}
sx={(t) => ({
backgroundColor: 'inherit',
'&:hover': {
backgroundColor: t.other.hover,
},
color: t.colorScheme === 'dark' ? 'white' : 'black',
})}
size='xl'
p='sm'
>
{user.username}
</Button>
</Popover.Target>
<AppBar
position='fixed'
elevation={0}
sx={{
width: { sm: `calc(100% - ${drawerWidth}px)` },
ml: { sm: `${drawerWidth}px` },
}}
>
<Toolbar>
<IconButton
color='inherit'
aria-label='open drawer'
edge='start'
onClick={() => setMobileOpen(true)}
sx={{ mr: 2, display: { sm: 'none' } }}
>
<MenuIcon />
</IconButton>
<Typography
variant='h5'
noWrap
component='div'
sx={{ display: { sm: 'none' } }}
>
Zipline
</Typography>
{user && (
<Box sx={{ marginLeft: 'auto' }}>
<Button
color='inherit'
aria-expanded={open ? 'true' : undefined}
onClick={handleClick}
>
<AccountIcon />
</Button>
<Menu
id='zipline-user-menu'
anchorEl={anchorEl}
open={open}
onClose={handleClose(null)}
MenuListProps={{
'aria-labelledby': 'basic-button',
}}
>
<MenuItem disableRipple>
<Typography variant='h5'>
<b>{user.username}</b>
</Typography>
</MenuItem>
<Divider />
<Link href='/dash/manage' passHref>
<MenuItem onClick={handleClose(null)}>
<AccountIcon sx={{ mr: 2 }} /> Manage Account
</MenuItem>
</Link>
<MenuItem onClick={handleClose('copy')}>
<CopyIcon sx={{ mr: 2 }} /> Copy Token
</MenuItem>
<MenuItem onClick={handleClose('reset')}>
<ResetIcon sx={{ mr: 2 }} /> Reset Token
</MenuItem>
<Link href='/auth/logout' passHref>
<MenuItem onClick={handleClose(null)}>
<LogoutIcon sx={{ mr: 2 }} /> Logout
</MenuItem>
</Link>
</Menu>
<Popover.Dropdown p={4} mr='md' sx={{ minWidth: '200px' }}>
<Stack spacing={2}>
<Menu.Label>
{user.username} ({user.id}){' '}
{user.administrator && user.username !== 'administrator' ? '(Administrator)' : ''}
</Menu.Label>
<MenuItemLink icon={<SettingsIcon />} href='/dashboard/manage'>
Manage Account
</MenuItemLink>
<MenuItem
icon={<CopyIcon />}
onClick={() => {
setOpen(false);
openCopyToken();
}}
>
Copy Token
</MenuItem>
<MenuItem
icon={<DeleteIcon />}
onClick={() => {
setOpen(false);
openResetToken();
}}
color='red'
>
Reset Token
</MenuItem>
<MenuItemLink icon={<LogoutIcon />} href='/auth/logout' color='red'>
Logout
</MenuItemLink>
<Menu.Divider />
<>
{oauth_providers
.filter((x) =>
user.oauth
?.map(({ provider }) => provider.toLowerCase())
.includes(x.name.toLowerCase())
)
.map(({ name, Icon }, i) => (
<>
<MenuItem
sx={{ '&:hover': { backgroundColor: 'inherit' } }}
key={i}
py={5}
px={4}
icon={<Icon size={18} colorScheme={theme.colorScheme} />}
>
Logged in with {capitalize(name)}
</MenuItem>
</>
))}
{oauth_providers.filter((x) =>
user.oauth
?.map(({ provider }) => provider.toLowerCase())
.includes(x.name.toLowerCase())
).length ? (
<Menu.Divider />
) : null}
</>
<MenuItem icon={<PencilIcon />}>
<Select
size='xs'
data={Object.keys(themes).map((t) => ({
value: t,
label: friendlyThemeName[t],
}))}
value={systemTheme}
onChange={handleUpdateTheme}
/>
</MenuItem>
</Stack>
</Popover.Dropdown>
</Popover>
</Box>
)}
</Toolbar>
</AppBar>
<Box
component='nav'
sx={{
width: { sm: drawerWidth },
flexShrink: { sm: 0 },
}}
</div>
</Header>
}
>
<Paper
withBorder
p='md'
shadow='xs'
sx={(t) => ({
borderColor: t.colorScheme === 'dark' ? t.colors.dark[5] : t.colors.dark[0],
})}
>
<Drawer
container={container}
variant='temporary'
onClose={() => setMobileOpen(false)}
open={mobileOpen}
elevation={0}
ModalProps={{
keepMounted: true,
}}
sx={{
display: { xs: 'block', sm: 'none' },
'* .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
>
{drawer}
</Drawer>
<Drawer
variant='permanent'
sx={{
display: { xs: 'none', sm: 'block' },
'* .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
open
>
{drawer}
</Drawer>
</Box>
<Box component='main' sx={{ flexGrow: 1, p: 3, mt: 8 }}>
{user && noPaper ? children : (
<Paper elevation={0} sx={{ p: 2 }} variant='outlined'>
{children}
</Paper>
)}
</Box>
</Box>
{children}
</Paper>
</AppShell>
);
}

View File

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

View File

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

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'
width='target'
withArrow
trapFocus={false}
styles={{
dropdown: {
zIndex: 999999,
},
}}
>
<Popover.Target>
<div onFocusCapture={() => setPopoverOpened(true)} onBlurCapture={() => setPopoverOpened(false)}>
<PasswordInput
label='Password'
description='A strong password should include letters in lower and uppercase, at least 1 number, at least 1 special symbol'
value={value}
onChange={(event) => setValue(event.currentTarget.value)}
{...props}
/>
</div>
</Popover.Target>
<Popover.Dropdown>
<Progress color={color} value={strength} size={7} mb='md' />
<PasswordRequirement label='Includes at least 8 characters' meets={value.length > 7} />
{checks}
</Popover.Dropdown>
</Popover>
);
}

View File

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

View File

@@ -0,0 +1,71 @@
import { Card, createStyles, Group, Text } from '@mantine/core';
import { ArrowDownRight, ArrowUpRight } from 'react-feather';
const useStyles = createStyles((theme) => ({
root: {
padding: theme.spacing.xl * 1.5,
},
value: {
fontSize: 24,
fontWeight: 700,
lineHeight: 1,
},
diff: {
lineHeight: 1,
display: 'flex',
alignItems: 'center',
},
icon: {
color: theme.colorScheme === 'dark' ? theme.colors.dark[3] : theme.colors.gray[4],
},
title: {
fontWeight: 700,
textTransform: 'uppercase',
},
}));
interface StatsGridProps {
stat: {
title: string;
icon: React.ReactNode;
value: string;
desc: string;
diff?: number;
};
}
export default function StatCard({ stat }: StatsGridProps) {
const { classes } = useStyles();
if (stat.diff) stat.diff = Math.round(stat.diff);
return (
<Card p='md' radius='md' key={stat.title}>
<Group position='apart'>
<Text size='xs' color='dimmed' className={classes.title}>
{stat.title}
</Text>
{stat.icon}
</Group>
<Group align='flex-end' spacing='xs' mt='md'>
<Text className={classes.value}>{stat.value}</Text>
{typeof stat.diff == 'number' && (
<>
<Text color={stat.diff >= 0 ? 'teal' : 'red'} size='sm' weight={500} className={classes.diff}>
<span>{stat.diff === Infinity ? '∞' : stat.diff}%</span>
{stat.diff >= 0 ? <ArrowUpRight size={16} /> : <ArrowDownRight size={16} />}
</Text>
</>
)}
</Group>
<Text size='xs' color='dimmed' mt='sm'>
{stat.desc}
</Text>
</Card>
);
}

View File

@@ -1,77 +1,132 @@
import React from 'react';
import { ThemeProvider } from '@emotion/react';
import { CssBaseline } from '@mui/material';
import { useEffect } from 'react';
// themes
import dark_blue from 'lib/themes/dark_blue';
import dark from 'lib/themes/dark';
import ayu_dark from 'lib/themes/ayu_dark';
import ayu_mirage from 'lib/themes/ayu_mirage';
import ayu_light from 'lib/themes/ayu_light';
import nord from 'lib/themes/nord';
import polar from 'lib/themes/polar';
import ayu_mirage from 'lib/themes/ayu_mirage';
import dark from 'lib/themes/dark';
import dark_blue from 'lib/themes/dark_blue';
import dracula from 'lib/themes/dracula';
import light_blue from 'lib/themes/light_blue';
import matcha_dark_azul from 'lib/themes/matcha_dark_azul';
import nord from 'lib/themes/nord';
import qogir_dark from 'lib/themes/qogir_dark';
import { useStoreSelector } from 'lib/redux/store';
import createTheme from 'lib/themes';
import { MantineProvider, MantineThemeOverride } from '@mantine/core';
import { useColorScheme } from '@mantine/hooks';
import { ModalsProvider } from '@mantine/modals';
import { NotificationsProvider } from '@mantine/notifications';
import { useRecoilValue } from 'recoil';
import { userSelector } from 'lib/recoil/user';
export const themes = {
'dark_blue': dark_blue,
'dark': dark,
'ayu_dark': ayu_dark,
'ayu_mirage': ayu_mirage,
'ayu_light': ayu_light,
'nord': nord,
'polar': polar,
'dracula': dracula,
'matcha_dark_azul': matcha_dark_azul,
'qogir_dark': qogir_dark,
system: (colorScheme: 'dark' | 'light') => (colorScheme === 'dark' ? dark_blue : light_blue),
dark_blue,
light_blue,
dark,
ayu_dark,
ayu_mirage,
ayu_light,
nord,
dracula,
matcha_dark_azul,
qogir_dark,
};
export const friendlyThemeName = {
'dark_blue': 'Dark Blue',
'dark': 'Very Dark',
'ayu_dark': 'Ayu Dark',
'ayu_mirage': 'Ayu Mirage',
'ayu_light': 'Ayu Light',
'nord': 'Nord',
'polar': 'Polar',
'dracula': 'Dracula',
'matcha_dark_azul': 'Matcha Dark Azul',
'qogir_dark': 'Qogir Dark',
system: 'System Theme',
dark_blue: 'Dark Blue',
light_blue: 'Light Blue',
dark: 'Very Dark',
ayu_dark: 'Ayu Dark',
ayu_mirage: 'Ayu Mirage',
ayu_light: 'Ayu Light',
nord: 'Nord',
dracula: 'Dracula',
matcha_dark_azul: 'Matcha Dark Azul',
qogir_dark: 'Qogir Dark',
};
export default function ZiplineTheming({ Component, pageProps }) {
let t;
export default function ZiplineTheming({ Component, pageProps, ...props }) {
const user = useRecoilValue(userSelector);
const colorScheme = useColorScheme();
const user = useStoreSelector(state => state.user);
if (!user) t = themes.dark_blue;
else {
if (user.customTheme) {
t = createTheme({
type: 'dark',
primary: user.customTheme.primary,
secondary: user.customTheme.secondary,
error: user.customTheme.error,
warning: user.customTheme.warning,
info: user.customTheme.info,
border: user.customTheme.border,
background: {
main: user.customTheme.mainBackground,
paper: user.customTheme.paperBackground,
},
});
} else {
t = themes[user.systemTheme] ?? themes.dark_blue;
}
}
let theme: MantineThemeOverride;
if (!user) theme = themes.system(colorScheme);
else if (user.systemTheme === 'system') theme = themes.system(colorScheme);
else theme = themes[user.systemTheme] ?? themes.system(colorScheme);
useEffect(() => {
document.documentElement.style.setProperty('color-scheme', theme.colorScheme);
}, [user, theme]);
return (
<ThemeProvider theme={t}>
<CssBaseline />
<Component {...pageProps} />
</ThemeProvider>
<MantineProvider
withGlobalStyles
withNormalizeCSS
theme={{
...theme,
components: {
AppShell: {
styles: (t) => ({
root: {
backgroundColor: t.other.AppShell_backgroundColor,
},
}),
},
NavLink: {
styles: (t) => ({
icon: {
paddingLeft: t.spacing.sm,
},
}),
},
Modal: {
defaultProps: {
centered: true,
overlayBlur: 3,
overlayColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
},
},
Popover: {
defaultProps: {
transition: 'pop',
},
},
LoadingOverlay: {
defaultProps: {
overlayBlur: 3,
overlayColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
},
},
Loader: {
defaultProps: {
variant: 'dots',
},
},
Card: {
styles: (t) => ({
root: {
backgroundColor: t.colorScheme === 'dark' ? t.colors.dark[6] : t.colors.gray[0],
},
}),
},
Image: {
styles: (t) => ({
placeholder: {
backgroundColor: t.colorScheme === 'dark' ? t.colors.dark[6] : t.colors.gray[0],
},
}),
},
},
}}
>
<ModalsProvider>
<NotificationsProvider>
{props.children ? props.children : <Component {...pageProps} />}
</NotificationsProvider>
</ModalsProvider>
</MantineProvider>
);
}
}

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

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

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { Dropzone as MantineDropzone } from '@mantine/dropzone';
import { Group, Text, useMantineTheme } from '@mantine/core';
import { ImageIcon } from 'components/icons';
export default function Dropzone({ loading, onDrop, children }) {
const theme = useMantineTheme();
return (
<MantineDropzone onDrop={onDrop}>
<Group position='center' spacing='xl' style={{ minHeight: 440 }}>
<ImageIcon size={80} />
<Text size='xl' inline>
Drag files here or click to select files
</Text>
</Group>
<div style={{ pointerEvents: 'all' }}>{children}</div>
</MantineDropzone>
);
}

View File

@@ -0,0 +1,52 @@
import React from 'react';
import { Table, Tooltip, Badge, HoverCard, Text, useMantineTheme, Group } from '@mantine/core';
import Type from 'components/Type';
export function FilePreview({ file }: { file: File }) {
return (
<Type
file={file}
autoPlay
sx={{ maxWidth: '10vw', maxHeight: '100vh' }}
style={{ maxWidth: '10vw', maxHeight: '100vh' }}
src={URL.createObjectURL(file)}
alt={file.name}
disableMediaPreview={false}
popup
/>
);
}
export default function FileDropzone({ file }: { file: File }) {
const theme = useMantineTheme();
return (
<HoverCard shadow='md'>
<HoverCard.Target>
<Badge size='lg'>{file.name}</Badge>
</HoverCard.Target>
<HoverCard.Dropdown>
<Group grow>
<FilePreview file={file} />
<Table sx={{ color: theme.colorScheme === 'dark' ? 'white' : 'white' }} ml='md'>
<tbody>
<tr>
<td>Name</td>
<td>{file.name}</td>
</tr>
<tr>
<td>Type</td>
<td>{file.type}</td>
</tr>
<tr>
<td>Last Modified</td>
<td>{new Date(file.lastModified).toLocaleString()}</td>
</tr>
</tbody>
</Table>
</Group>
</HoverCard.Dropdown>
</HoverCard>
);
}

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 { Clock } from 'react-feather';
export default function ClockIcon({ ...props }) {
return <Clock 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,21 @@
// https://discord.com/branding
import Image from 'next/image';
export default function DiscordIcon({ ...props }) {
return (
<svg width='24' height='24' viewBox='0 0 71 55' xmlns='http://www.w3.org/2000/svg'>
<g clipPath='url(#clip0)'>
<path
fill={props.colorScheme === 'manage' ? '#ffffff' : '#5865F2'}
d='M60.1045 4.8978C55.5792 2.8214 50.7265 1.2916 45.6527 0.41542C45.5603 0.39851 45.468 0.440769 45.4204 0.525289C44.7963 1.6353 44.105 3.0834 43.6209 4.2216C38.1637 3.4046 32.7345 3.4046 27.3892 4.2216C26.905 3.0581 26.1886 1.6353 25.5617 0.525289C25.5141 0.443589 25.4218 0.40133 25.3294 0.41542C20.2584 1.2888 15.4057 2.8186 10.8776 4.8978C10.8384 4.9147 10.8048 4.9429 10.7825 4.9795C1.57795 18.7309 -0.943561 32.1443 0.293408 45.3914C0.299005 45.4562 0.335386 45.5182 0.385761 45.5576C6.45866 50.0174 12.3413 52.7249 18.1147 54.5195C18.2071 54.5477 18.305 54.5139 18.3638 54.4378C19.7295 52.5728 20.9469 50.6063 21.9907 48.5383C22.0523 48.4172 21.9935 48.2735 21.8676 48.2256C19.9366 47.4931 18.0979 46.6 16.3292 45.5858C16.1893 45.5041 16.1781 45.304 16.3068 45.2082C16.679 44.9293 17.0513 44.6391 17.4067 44.3461C17.471 44.2926 17.5606 44.2813 17.6362 44.3151C29.2558 49.6202 41.8354 49.6202 53.3179 44.3151C53.3935 44.2785 53.4831 44.2898 53.5502 44.3433C53.9057 44.6363 54.2779 44.9293 54.6529 45.2082C54.7816 45.304 54.7732 45.5041 54.6333 45.5858C52.8646 46.6197 51.0259 47.4931 49.0921 48.2228C48.9662 48.2707 48.9102 48.4172 48.9718 48.5383C50.038 50.6034 51.2554 52.5699 52.5959 54.435C52.6519 54.5139 52.7526 54.5477 52.845 54.5195C58.6464 52.7249 64.529 50.0174 70.6019 45.5576C70.6551 45.5182 70.6887 45.459 70.6943 45.3942C72.1747 30.0791 68.2147 16.7757 60.1968 4.9823C60.1772 4.9429 60.1437 4.9147 60.1045 4.8978ZM23.7259 37.3253C20.2276 37.3253 17.3451 34.1136 17.3451 30.1693C17.3451 26.225 20.1717 23.0133 23.7259 23.0133C27.308 23.0133 30.1626 26.2532 30.1066 30.1693C30.1066 34.1136 27.28 37.3253 23.7259 37.3253ZM47.3178 37.3253C43.8196 37.3253 40.9371 34.1136 40.9371 30.1693C40.9371 26.225 43.7636 23.0133 47.3178 23.0133C50.9 23.0133 53.7545 26.2532 53.6986 30.1693C53.6986 34.1136 50.9 37.3253 47.3178 37.3253Z'
/>
</g>
<defs>
<clipPath id='clip0'>
<rect width='71' height='55' fill='white' />
</clipPath>
</defs>
</svg>
);
}

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 { ExternalLink } from 'react-feather';
export default function ExternalLinkIcon({ ...props }) {
return <ExternalLink size={15} {...props} />;
}

View File

@@ -0,0 +1,5 @@
import { Eye } from 'react-feather';
export default function EyeIcon({ ...props }) {
return <Eye 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,15 @@
// https://github.com/flameshot-org/flameshot/blob/master/data/img/app/flameshot.svg
import Image from 'next/image';
export default function FlameshotIcon({ ...props }) {
return (
<Image
alt='flameshot'
src='https://raw.githubusercontent.com/flameshot-org/flameshot/master/data/img/app/flameshot.svg'
width={24}
height={24}
{...props}
/>
);
}

View File

@@ -0,0 +1,17 @@
import { GitHub } from 'react-feather';
import Image from 'next/image';
// https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg
export default function GitHubIcon({ colorScheme, ...props }) {
return (
<svg width={24} height={24} viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg' {...props}>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z'
transform='scale(64)'
fill={colorScheme === 'dark' ? '#FFFFFF' : '#1B1F23'}
/>
</svg>
);
}

View File

@@ -0,0 +1,15 @@
// https://developers.google.com/identity/branding-guidelines
import Image from 'next/image';
export default function GoogleIcon({ ...props }) {
return (
<Image
alt='google'
src='https://madeby.google.com/static/images/google_g_logo.svg'
width={24}
height={24}
{...props}
/>
);
}

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 { RefreshCw } from 'react-feather';
export default function RefreshIcon({ ...props }) {
return <RefreshCw 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,9 @@
// https://getsharex.com/brand-assets/
import Image from 'next/image';
export default function ShareXIcon({ ...props }) {
return (
<Image alt='sharex' src='https://getsharex.com/img/ShareX_Logo.svg' width={24} height={24} {...props} />
);
}

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

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