Compare commits

...

208 Commits

Author SHA1 Message Date
diced 0ac81c887e feat: CHUNKS_ENABLED config 2023-04-03 22:36:32 -07:00
diced 5178f762eb fix: use temp_directory instead of tmpdir() 2023-03-31 22:33:37 -07:00
dicedtomato bcc2d673dd Merge branch 'trunk' into feature/offload-uploads 2023-03-31 22:32:15 -07:00
Jayvin Hernandez bf40fa9cd2 feat: many things (#351)
* remove source from final image

* move check state to ClearStorage

* use inspect for fancy colors

* newlines are now possible! yay!

* Catch user's leave if uploading

* feat?: Temp directory can be specified by the user.
Default is /tmp/zipline (or os equivalent)

* fix: ignore onDash config, use only ?compress query

---------

Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
2023-03-31 22:25:00 -07:00
diced bc58c1b56e fix: milestone again again again again again again 2023-03-31 22:12:13 -07:00
diced c57a6e1700 fix: milestone again again again again again 2023-03-31 22:07:17 -07:00
diced 8649a489d8 fix: milestone again again again again 2023-03-31 22:05:59 -07:00
diced 40f29907c7 fix: milestone again again again 2023-03-31 21:55:26 -07:00
diced 34005ece43 fix: milestone again again 2023-03-31 21:53:30 -07:00
diced 8e6fc1e8a3 fix: milestone again 2023-03-31 21:49:50 -07:00
diced 065f44b145 fix: milestone 2023-03-31 21:41:19 -07:00
diced e707685da3 feat: offloaded chunked uploads 2023-03-31 21:32:38 -07:00
diced e5a07f568d fix: update milestone action 2023-03-27 16:48:56 -07:00
dicedtomato a728d71da1 Merge branch '3.x' into trunk 2023-03-26 20:46:00 -07:00
diced 91e468791e feat(3.7.0): version 2023-03-26 20:45:01 -07:00
dicedtomato 169a2ea562 Revert "Release 3.7.0 (#328)" (#347)
This reverts commit f9060f8ae7.
2023-03-26 20:40:41 -07:00
dicedtomato f9060f8ae7 Release 3.7.0 (#328)
* fix: oauthId optional

* fix: remove optional

* hotfix: make oauthid optional (#249)

* hotfix: fallback oauth find (#250)

* fix: add a forgotten ? to schema

* fix: catch null at other places (#252)

* fix: forgor (#253)

* Fix root url & uploader stuff (#254)

* fix: uploader route as root won't be broken

* fix: fix broken url route for when on root

* fix: catch hopefully the most of the edge cases (#251)

* fix: catch hopefully the most of the edge cases

* fix: invite only, fools

* feat: tsup, small fixes

* fix: #264

* fix: urls handle empty strings

* fix: remove esbuild

* fix: do not mutate res #266

* feat: new embed method

* fix: overwrite tmp ss flameshot

* fix: overrides for uploading

* refactor: chart.js -> recharts

* feat: download query on /r/

* fix: better icons on file vie2

* feat: ability to generate url shorten config

* fix: sxcu name

* fix: react hooks error

* feat: ability to insert tabs on Tab keypress

* feat: overhaul image upload

* fix: clean

* fix: group icons vertically

* fix: embeds not showing up

* fix: docker stuff

* feat(3.7.0-rc1): version

* fix: cors for files (#257)

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

* refactor: many columns/tables in prisma

* feat: keep original name #247

* fix: ability to gen with original-name

* fix: type error

* fix: no name on dashboard

* feat(v3.7.0-rc2): version

* fix: sharex DestinationType

* fix: ensureDatabaseExists args

* fix: sharex config

* fix: #277 #272

* fix: 🐛 Add Menu component as parent

* refactor: popover -> menu

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

* fix: add a "skip" for fresh db's (#274)

* fix: add a "skip" for fresh db's

* fix: trimming

* fix: elevate logging!

* fix: allow more variables on view

* fix: optimize docker image

* fix:  root url for upload and shorten (#255)

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

* fix: /app -> /zipline

* feat(v3.7.0-rc3): folders for files

* fix: use `name` instead of `file` (#281)

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

* feat: search+create for folder select (#283)

* feat?: Search for the folder to add.
Also you can create a folder right from the file, rather than being redirected.

* woops wrong import

* fix: return null for no string in parser (#285)

* feat: use ENTRYPOINT in docker (#286)

* :3

* Update Dockerfile

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

* Update Dockerfile

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

* Update Dockerfile

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

* test

---------

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

* fix: set password to actual text value

* fix: url encode password query

* fix: entrypoint executable (#289)

* feat: override domain header

* fix: random domains

* feat: better version checking

* feat: public folders

* fix: dates #278

* feat: devcontainers for codespaces, etc.

* experiment with devcontainer.json

* introduce a docker-compose for devcontainer

* Devcontainers!

* version pop

* Port labeling and a complimentary env variable

* see it to believe it

* Update .devcontainer/devcontainer.json

* Update .devcontainer/devcontainer.json

* Update .devcontainer/docker-compose.yml

---------

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

* fix: spaces and route fixes (#294)

* fix: spaces and route fixes

* fix: shorten url response

* feat: better version checking

* fix: use special characters should work

If it doesn't, better call saul

* save that extra byte

* fix: returning protocol again in domain

unrelated to this pr but whatever

* fix: above ^

* Rename shorten.tsv to shorten.ts

---------

Co-authored-by: diced <pranaco2@gmail.com>
Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>

* fix: #296

* fix: show files per user (#299)

* feat: clearing orphaned files (#303)

* fix: default public folder (docker)

* feat: seperate discord webhooks (shorten/upload) (#260)

* fix: title for folders

* fix: clipboard & 2fa improvements

A workaround that shows the content that would have been copied if `navigator.clipboard` is unavailable for whatever reason.

2FA input autofocuses & submits on enter.

* fix: revamp uploaded file modal

* fix: revamp mobile ui

* feat: more functionality within files table

* feat: clear zero byte files script

* feat: logger improvements
- Timestamp is gray
- removed colorette dependency
- introduction of LOGGER_FILTERS

* chore: update deps

* feat(v3.7.0-rc4): version

* fix: show warning when password protect

* fix: fix (#310)

* Muted audio by default!

* Code renderin'

* not but still decently standard files being viewable

* reserved routes

* Update validateConfig.ts

---------

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

* feat: file size (#308)

* feat: baseline support for file sizes

* feat: script to add file sizes

* fix: #311

* chore: update to mantine@6

* refactor: remove old File.tsx

* feat: initial move to mantine v6

* feat: use api option

* remove: useless size modifier

* fix: user button

* feat: use pininput for 2fa

* fix: breaking changes in migrating mantine v6

---------

Co-authored-by: TacticalCoderJay <gogojayvin923@gmail.com>

* feat: add size to datatable

* fix: null on non originalName

* fix: allow download query on non raw

* fix: undef file

* fix: spacingg between count_by_user

* feat: new ui for shortened urls

* fix: spacing within appshell/paper

* feat: new login page

* feat: reorganize menu

* feat: keyboard spotlight

* feat: tabler icons

* fix: remove feather import

* fix: update 2fa enabled appropriately & delete files (#315)

* fix: update 2fa enabled appropriately

* fix: a proper delete

---------

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

* feat(v3.7.0-rc5): version

* feat: add feature request "contact_link"

* feat: multiple stuffs

* feat: gfycat url #322

* feat: gfycat attribution

* feat(3.7.0-rc6): version

* fix: type cast

* feat: list view for urls, invites, users: #302

* refactor: docker-compose -> docker compose

* fix: open folder in new tab

* fix: save list-view setting to localStorage

* fix: Bug: URLs list view #330

* fix: #332

* fix: #331

* fix: #333

* feat: link to view gallery (icon)

* fix: clean up Anchors

* refactor: new eslint changes

* fix: fine tune devcontainer (#329)

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

* fix: FileModal scrollbars

* fix: dynamically import katex

* fix: remove rogue console.log

* fix: open folder onRowClick

* fix: filter on usePaginatedFiles

* fix: icon sizes

* fix: paste listener

* feat(actions): auto-assign milestone

* feat(v3.7.0-rc7): version

* fix: #339

* fix: resetting avatars

* feat: new icons / oauth icons changed

* feat: UPLOADER_ASSUME_MIMETYPES (#337)

* fix: any instance of #342

* fix: any instance of #345

* fix: make tables take entire vh

* chore: update deps

* fix: add bigger sizeLimit

* fix: token exposed on view/[id]

---------

Co-authored-by: Jayvin Hernandez <gogojayvin923@gmail.com>
Co-authored-by: IceToast <54889359+IceToast@users.noreply.github.com>
Co-authored-by: IThundxr <harshdhaliwal9767@gmail.com>
Co-authored-by: IThundxr <contact@ithundxr.dev>
2023-03-26 20:40:01 -07:00
diced d379bf8b1c fix: token exposed on view/[id] 2023-03-26 12:14:41 -07:00
diced 67b71ceffe fix: add bigger sizeLimit 2023-03-26 00:17:59 -07:00
diced eb6929b889 chore: update deps 2023-03-26 00:03:10 -07:00
diced d7299f8220 fix: make tables take entire vh 2023-03-25 23:39:16 -07:00
diced 1ed267ad94 fix: any instance of #345 2023-03-25 23:36:16 -07:00
diced 40a0cce3e8 fix: any instance of #342 2023-03-25 23:33:37 -07:00
diced 556aafaad3 feat: UPLOADER_ASSUME_MIMETYPES (#337) 2023-03-24 17:57:56 -07:00
diced fdc7901eff feat: new icons / oauth icons changed 2023-03-24 17:56:53 -07:00
diced 9632399f5d fix: resetting avatars 2023-03-24 17:24:15 -07:00
diced cc8a5411ab fix: #339 2023-03-24 17:20:33 -07:00
diced 12bb804e6a feat(v3.7.0-rc7): version 2023-03-22 20:27:52 -07:00
diced 3a27f31a03 feat(actions): auto-assign milestone 2023-03-22 19:41:06 -07:00
diced 37e7ad840c fix: paste listener 2023-03-22 19:36:26 -07:00
diced c57a1ea326 fix: icon sizes 2023-03-21 20:06:46 -07:00
diced 12d5d5f08f fix: filter on usePaginatedFiles 2023-03-21 19:57:16 -07:00
diced e7cf44e8e9 fix: open folder onRowClick 2023-03-21 19:53:59 -07:00
diced a81f797266 fix: remove rogue console.log 2023-03-21 19:51:02 -07:00
diced 6ada79017a fix: dynamically import katex 2023-03-21 19:46:13 -07:00
diced bdf34bbbbf fix: FileModal scrollbars 2023-03-21 19:31:45 -07:00
Jayvin Hernandez c0d1b3d887 fix: fine tune devcontainer (#329)
Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
2023-03-21 18:01:02 -07:00
diced 1b505d463c refactor: new eslint changes 2023-03-21 17:53:22 -07:00
diced 25606a80ec fix: clean up Anchors 2023-03-21 17:09:50 -07:00
diced 8b540bff62 feat: link to view gallery (icon) 2023-03-21 16:36:47 -07:00
diced 8a2064e09d fix: #333 2023-03-21 16:30:01 -07:00
diced 1f0fb32b9b fix: #331 2023-03-20 22:50:28 -07:00
diced 3cbc345c00 fix: #332 2023-03-20 22:49:24 -07:00
diced 3c66c18c77 fix: Bug: URLs list view #330 2023-03-20 22:37:30 -07:00
diced bcc816ea55 fix: save list-view setting to localStorage 2023-03-20 22:36:10 -07:00
diced eb2713bc23 fix: open folder in new tab 2023-03-20 22:25:56 -07:00
diced bcd68ae98b refactor: docker-compose -> docker compose 2023-03-19 16:48:08 -07:00
diced d1a486ac1f feat: list view for urls, invites, users: #302 2023-03-19 16:44:04 -07:00
diced 0d36f5f091 fix: type cast 2023-03-18 21:57:41 -07:00
diced 3d5cdf50e6 feat(3.7.0-rc6): version 2023-03-18 21:54:20 -07:00
diced 1e81822c11 feat: gfycat attribution 2023-03-18 21:52:14 -07:00
diced f8cd847588 feat: gfycat url #322 2023-03-18 21:48:19 -07:00
Jayvin Hernandez 5b9b454330 feat: multiple stuffs 2023-03-18 20:52:04 -07:00
dicedtomato 9c5b3f60d5 feat: add feature request "contact_link" 2023-03-09 20:51:07 -08:00
diced d83c255382 feat(v3.7.0-rc5): version 2023-03-04 22:08:09 -08:00
Jayvin Hernandez 656b900256 fix: update 2fa enabled appropriately & delete files (#315)
* fix: update 2fa enabled appropriately

* fix: a proper delete

---------

Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
2023-03-04 22:07:29 -08:00
diced a16b516163 fix: remove feather import 2023-03-04 21:56:47 -08:00
diced 6d8e66478c feat: tabler icons 2023-03-04 21:51:58 -08:00
diced 4428555762 feat: keyboard spotlight 2023-03-04 19:57:09 -08:00
diced 463e91c3bd feat: reorganize menu 2023-03-04 18:48:05 -08:00
diced 1e37f06ab6 feat: new login page 2023-03-04 18:33:30 -08:00
diced 3af3ba69f5 fix: spacing within appshell/paper 2023-03-04 18:11:11 -08:00
diced 0adc07ac38 feat: new ui for shortened urls 2023-03-04 18:04:22 -08:00
diced 4fe4faa202 fix: spacingg between count_by_user 2023-03-04 17:58:35 -08:00
diced 4912a872e0 fix: undef file 2023-03-04 17:51:40 -08:00
diced ac05d82e3a fix: allow download query on non raw 2023-03-04 17:50:14 -08:00
diced 6583f1114c fix: null on non originalName 2023-03-04 17:47:40 -08:00
diced e2673fa9e1 feat: add size to datatable 2023-03-04 17:20:49 -08:00
dicedtomato bc4b528ac6 chore: update to mantine@6
* refactor: remove old File.tsx

* feat: initial move to mantine v6

* feat: use api option

* remove: useless size modifier

* fix: user button

* feat: use pininput for 2fa

* fix: breaking changes in migrating mantine v6

---------

Co-authored-by: TacticalCoderJay <gogojayvin923@gmail.com>
2023-03-04 17:08:43 -08:00
dicedtomato 986858345e fix: #311 2023-03-04 04:52:00 +00:00
dicedtomato 912e439645 feat: file size (#308)
* feat: baseline support for file sizes

* feat: script to add file sizes
2023-03-03 20:40:28 -08:00
Jayvin Hernandez 8e44b71614 fix: fix (#310)
* Muted audio by default!

* Code renderin'

* not but still decently standard files being viewable

* reserved routes

* Update validateConfig.ts

---------

Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
2023-03-03 20:19:19 -08:00
dicedtomato 11bca28ef5 fix: show warning when password protect 2023-03-04 04:15:24 +00:00
dicedtomato 4ef0c6021a feat(v3.7.0-rc4): version 2023-02-27 03:46:50 +00:00
dicedtomato 4fbbd58ae9 chore: update deps 2023-02-27 03:46:27 +00:00
dicedtomato 81dea6cf90 feat: logger improvements
- Timestamp is gray
- removed colorette dependency
- introduction of LOGGER_FILTERS
2023-02-27 01:54:37 +00:00
dicedtomato 9b57fb280b feat: clear zero byte files script 2023-02-27 01:52:39 +00:00
dicedtomato e804d0b31e feat: more functionality within files table 2023-02-27 01:52:22 +00:00
dicedtomato 76845fc7e4 fix: revamp mobile ui 2023-02-26 20:45:20 +00:00
dicedtomato decd7f7918 fix: revamp uploaded file modal 2023-02-26 20:20:36 +00:00
Jayvin Hernandez 8c5ff4f230 fix: clipboard & 2fa improvements
A workaround that shows the content that would have been copied if `navigator.clipboard` is unavailable for whatever reason.

2FA input autofocuses & submits on enter.
2023-02-26 11:33:57 -08:00
dicedtomato 0848702f65 fix: title for folders 2023-02-26 04:57:44 +00:00
Jayvin Hernandez 5379374135 feat: seperate discord webhooks (shorten/upload) (#260) 2023-02-25 20:47:14 -08:00
Jayvin Hernandez b7772128d7 fix: default public folder (docker) 2023-02-25 20:36:05 -08:00
Jayvin Hernandez 95a1c7f92c feat: clearing orphaned files (#303) 2023-02-25 20:35:08 -08:00
Jayvin Hernandez 2d69cd580a fix: show files per user (#299) 2023-02-23 21:33:12 -08:00
dicedtomato 34552926d1 fix: #296 2023-02-24 00:18:03 +00:00
Jayvin Hernandez 739f584921 fix: spaces and route fixes (#294)
* fix: spaces and route fixes

* fix: shorten url response

* feat: better version checking

* fix: use special characters should work

If it doesn't, better call saul

* save that extra byte

* fix: returning protocol again in domain

unrelated to this pr but whatever

* fix: above ^

* Rename shorten.tsv to shorten.ts

---------

Co-authored-by: diced <pranaco2@gmail.com>
Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
2023-02-23 16:16:11 -08:00
Jayvin Hernandez 04d8b6421a feat: devcontainers for codespaces, etc.
* experiment with devcontainer.json

* introduce a docker-compose for devcontainer

* Devcontainers!

* version pop

* Port labeling and a complimentary env variable

* see it to believe it

* Update .devcontainer/devcontainer.json

* Update .devcontainer/devcontainer.json

* Update .devcontainer/docker-compose.yml

---------

Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
2023-02-23 16:00:59 -08:00
diced fdcd1f3d28 fix: dates #278 2023-02-23 14:42:39 -08:00
diced fc02dc02e8 feat: public folders 2023-02-23 14:37:22 -08:00
diced 6955d83b0c feat: better version checking 2023-02-20 23:17:04 -08:00
diced 1b3d3a867b fix: random domains 2023-02-18 14:08:49 -08:00
diced 83718d7b31 feat: override domain header 2023-02-18 11:19:50 -08:00
IThundxr e80627a3c3 fix: entrypoint executable (#289) 2023-02-18 10:02:40 -08:00
diced e1003d4bb6 fix: url encode password query 2023-02-17 19:45:04 -08:00
diced 2ef4a52be0 fix: set password to actual text value 2023-02-17 19:44:51 -08:00
IThundxr 93a63d3714 feat: use ENTRYPOINT in docker (#286)
* :3

* Update Dockerfile

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

* Update Dockerfile

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

* Update Dockerfile

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

* test

---------

Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
2023-02-17 19:09:50 -08:00
Jayvin Hernandez a8d9d98cf2 fix: return null for no string in parser (#285) 2023-02-16 16:47:07 -08:00
Jayvin Hernandez d70ddd1f53 feat: search+create for folder select (#283)
* feat?: Search for the folder to add.
Also you can create a folder right from the file, rather than being redirected.

* woops wrong import
2023-02-13 19:37:47 -08:00
Jayvin Hernandez 283c7c5a26 fix: use name instead of file (#281)
Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
2023-02-10 22:47:06 -08:00
diced fb5f50d5bd feat(v3.7.0-rc3): folders for files 2023-02-10 22:32:57 -08:00
diced 06e84b41aa fix: /app -> /zipline 2023-01-28 10:28:29 -08:00
Jayvin Hernandez e3f262322a fix: root url for upload and shorten (#255)
Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
2023-01-26 20:06:30 -08:00
diced 70c2fa8ef4 fix: optimize docker image 2023-01-26 15:58:22 -08:00
diced 9f534e18c8 fix: allow more variables on view 2023-01-26 15:58:06 -08:00
Jayvin Hernandez 55bd72aef8 fix: add a "skip" for fresh db's (#274)
* fix: add a "skip" for fresh db's

* fix: trimming

* fix: elevate logging!
2023-01-26 12:46:10 -08:00
IceToast c1a23faf1f fix: #277 #272
* fix: 🐛 Add Menu component as parent

* refactor: popover -> menu

Co-authored-by: IceToast <>
Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
2023-01-25 17:43:16 -08:00
dicedtomato 3588c297f8 fix: sharex config 2023-01-19 04:44:37 +00:00
diced 04d03cbc8f fix: ensureDatabaseExists args 2023-01-18 09:25:54 -08:00
diced 4e27efb6a1 fix: sharex DestinationType 2023-01-18 09:05:12 -08:00
diced 59b3e5bb24 feat(v3.7.0-rc2): version 2023-01-15 21:09:51 -08:00
diced d8eee3d81a fix: no name on dashboard 2023-01-15 21:07:11 -08:00
diced c8926682b2 fix: type error 2023-01-15 16:58:26 -08:00
diced 9117a9d779 fix: ability to gen with original-name 2023-01-15 14:05:37 -08:00
diced 4ea1775f2c feat: keep original name #247 2023-01-15 13:57:28 -08:00
diced a8020ecebe refactor: many columns/tables in prisma 2023-01-15 13:39:07 -08:00
Jayvin Hernandez 2ace076fce fix: cors for files (#257)
Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
2023-01-15 11:26:30 -08:00
diced 45e897d475 feat(3.7.0-rc1): version 2023-01-14 15:27:21 -08:00
diced 98676f0573 fix: docker stuff 2023-01-14 15:25:36 -08:00
diced c966ab9a52 fix: embeds not showing up 2023-01-14 13:50:56 -08:00
diced ebaf11ad10 fix: group icons vertically 2023-01-14 13:27:23 -08:00
diced 19c7ba03c6 fix: clean 2023-01-14 13:20:04 -08:00
diced 894b5c5c6c feat: overhaul image upload 2023-01-14 12:04:30 -08:00
diced 516e93cee2 feat: ability to insert tabs on Tab keypress 2023-01-14 11:27:59 -08:00
diced cc0ffc6e60 fix: react hooks error 2023-01-14 11:21:56 -08:00
diced a97ace6e73 fix: sxcu name 2023-01-14 10:30:24 -08:00
diced 6d49463dad feat: ability to generate url shorten config 2023-01-14 10:25:54 -08:00
diced 81e6e4e5f2 fix: better icons on file vie2 2023-01-13 19:40:15 -08:00
diced 2adb355183 feat: download query on /r/ 2023-01-13 17:28:38 -08:00
diced 5e6c53432b refactor: chart.js -> recharts 2023-01-13 17:20:40 -08:00
diced 873f77bc43 fix: overrides for uploading 2023-01-12 20:17:25 -08:00
diced 9bf098a93a fix: overwrite tmp ss flameshot 2023-01-11 21:44:24 -08:00
diced 388713a3c6 feat: new embed method 2023-01-11 21:33:01 -08:00
diced e94dd58542 fix: do not mutate res #266 2023-01-10 21:49:59 -08:00
diced d985a1c588 fix: remove esbuild 2023-01-06 15:08:18 -08:00
diced dbac6e8918 fix: urls handle empty strings 2023-01-06 15:06:34 -08:00
diced a481c0ee5e fix: #264 2023-01-06 15:04:43 -08:00
diced eef6fdaeb3 feat: tsup, small fixes 2023-01-06 14:45:48 -08:00
Jayvin Hernandez b8b1a5bba6 fix: catch hopefully the most of the edge cases (#251)
* fix: catch hopefully the most of the edge cases

* fix: invite only, fools
2022-12-29 20:39:32 -08:00
Jayvin Hernandez f06f52fce7 Fix root url & uploader stuff (#254)
* fix: uploader route as root won't be broken

* fix: fix broken url route for when on root
2022-12-18 17:29:50 -08:00
Jayvin Hernandez 4a332bb77b fix: forgor (#253) 2022-12-18 16:07:38 -08:00
Jayvin Hernandez eb1b202566 fix: catch null at other places (#252) 2022-12-18 15:05:54 -08:00
diced 658f3a1a09 fix: add a forgotten ? to schema 2022-12-17 14:12:44 -08:00
Jayvin Hernandez 55eba480ac hotfix: fallback oauth find (#250) 2022-12-17 13:18:15 -08:00
Jayvin Hernandez bbeea5b0ec hotfix: make oauthid optional (#249) 2022-12-17 09:37:29 -08:00
diced ad454a94ef fix: remove optional 2022-12-17 09:33:07 -08:00
diced 268215ff5f fix: oauthId optional 2022-12-17 09:06:00 -08:00
diced 4e70daa4d8 feat(v3.6.4): version 2022-12-15 20:13:45 -08:00
diced bb28f49cf5 feat: default avatar stuffs 2022-12-15 20:09:02 -08:00
diced d85211a145 feat: better nav bar stuff 2022-12-15 20:09:02 -08:00
diced a7291d374d fix: favorite pagination num 2022-12-15 20:09:02 -08:00
Jayvin Hernandez 5c9b558ac2 chore: update the readme (#246)
feature requests are now discussion threads instead of issues

Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
2022-12-15 18:09:36 -08:00
diced 36ede22d45 fix: readme sheilds.io images 2022-12-15 18:02:10 -08:00
diced 6528ec4056 fix: add default exit transition to modals 2022-12-14 22:46:41 -08:00
diced 56ee494c7d fix: add wayland instructions in flameshot builder 2022-12-14 22:45:10 -08:00
diced b21995a0b9 fix: cleanup old methods from /api/user/files 2022-12-14 22:18:50 -08:00
diced 3c00575ecd feat: new system for paged files 2022-12-13 23:32:57 -08:00
diced 27ccbcb54a chore: pg latest -> pg 15 2022-12-13 22:26:50 -08:00
diced fecbf394c1 fix: remove comments 2022-12-11 15:37:59 -08:00
diced 91341e2d21 feat: new variables parser 2022-12-11 15:30:19 -08:00
TacticalCoderJay 6349503b00 feat: clear storage (#244) (#239)
* - fix: use oauth's user id instead of username
 - feat: add login only config for oauth

* Addresses tomato's concerns

* fix: catch same account on different user

* Add storage cleaning

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

* Update src/components/pages/Manage/index.tsx

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

* Update src/components/pages/Manage/index.tsx

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

Co-authored-by: dicedtomato <diced@users.noreply.github.com>
Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
2022-12-10 15:00:39 -08:00
diced 58e8c103b7 fix: change CORE_HTTPS To CORE_RETURN_HTTPS in compose file 2022-12-10 14:24:34 -08:00
diced 5d115afa71 feat: update deps & fix stuff 2022-12-10 14:19:53 -08:00
diced d8b308a18c fix: bind 2022-12-09 19:37:40 -08:00
diced 76267c00d5 fix: attempt to fix #243 2022-12-09 19:31:35 -08:00
diced 9648856052 chore: update prisma-binaries 2022-12-09 18:09:58 -08:00
diced d87e465a8e fix: undefined 2022-12-08 18:53:35 -08:00
diced 2c07d6719e feat: save showNonMedia checkbox value 2022-12-08 18:50:00 -08:00
diced 4c633eb60d fix: clean up 2022-12-07 23:11:19 -08:00
diced ba6580e4ef fix: route not found for nextjs api routes 2022-12-07 23:10:43 -08:00
diced c21d8f837e feat: built-in ssl support
- CORE_HTTPS is now CORE_RETURN_HTTPS
- SSL_(KEY/CERT/ALLOW_HTTP1)
2022-12-07 19:40:54 -08:00
diced eadfa09570 refactor: migrate to fastify
- (maybe) faster http server
- easy to develop on
2022-12-07 19:21:26 -08:00
TacticalCoderJay ea1a0b7fc8 fix: new oauth stuff (#240)
* - fix: use oauth's user id instead of username
 - feat: add login only config for oauth

* Addresses tomato's concerns

* fix: catch same account on different user
2022-12-06 21:40:13 -08:00
diced 9f797613d2 fix: render in files & fix typos 2022-12-06 17:19:02 -08:00
diced b728ff33ec fix: delete URLs after serving 2022-12-06 16:35:38 -08:00
diced 7dc036c6e4 fix: order urls by desc 2022-12-06 16:30:47 -08:00
diced 78135aac02 fix: dynamically import prism languages 2022-12-05 19:57:14 -08:00
diced 950018673f feat: render tex (katex) and markdown 2022-12-05 18:16:31 -08:00
diced cfdcf05135 feat: ability to use / for URLS_ROUTE 2022-12-03 14:31:13 -08:00
diced ace474eb2c feat(v3.6.3): version 2022-12-03 08:37:37 -08:00
diced 285ed8d56e fix: actually fix recursive thing 2022-12-03 08:02:16 -08:00
diced 738e25feda fix: exiftool not working on docker 2022-12-02 17:35:47 -08:00
diced 6d2d071293 fix: max call stack error 2022-12-02 08:37:13 -08:00
diced 725ce50608 feat(V3.6.2): version 2022-12-01 09:47:09 -08:00
diced 78e884e97e feat: default expiration/ttl (#237) 2022-12-01 09:42:16 -08:00
diced cb123cb575 fix: formatting 2022-12-01 09:31:29 -08:00
diced 6f3081cb8e feat: headless mode 2022-12-01 09:27:14 -08:00
dicedtomato 231f734fd5 Update README.md 2022-11-28 20:47:24 -08:00
TacticalCoderJay fce7325a24 fix: add onDelete to all relations (#236) 2022-11-28 20:39:20 -08:00
diced 2bec45411f feat: overhaul upload frontend 2022-11-28 19:58:21 -08:00
diced 577195b578 fix: serve favicon.ico always 2022-11-27 20:06:22 -08:00
diced a402227c4f fix: custom placeholder 2022-11-27 19:55:14 -08:00
TacticalCoderJay a75b790654 fix: allow root route & remove swift refs (#235) (#214)
* fix:
 - readd root route for uploads only
 - catch 1 edge case for root route (/dashboard)

* fix: spelt dashboard right

* fix: include the dot for the extension

* fix: remove any possible references of swift

* fix: missed a spot

* Update .env.local.example

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

* Update .env.local.example

* Update validateConfig.ts

* format

Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
Co-authored-by: Jonathan <axis@axis.moe>
2022-11-27 18:20:29 -08:00
TacticalCoderJay f07cbeac52 fix: small bug & no file ext (#234)
* fix: allow empty file extensions

* fix: Add a button to show non-media files

* fix: Looks better

* Update FilePagation.tsx

Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
2022-11-26 16:15:09 -08:00
diced dcfcce7803 update readme with stuff 2022-11-26 14:33:42 -08:00
diced 659868181d feat: supabase datasource & remove swift 2022-11-25 14:30:18 -08:00
diced d76e6444e0 fix: dumb cors headers 2022-11-24 14:34:24 -08:00
diced 0dbbf4840c fix: CORS not working because of auth headers 2022-11-24 14:17:46 -08:00
diced 1b6af9fc08 fix: OPTIONS being blocked 2022-11-24 10:35:06 -08:00
karlmanait 8e1541ea56 feat: add configuration for default upload format (#232)
* feat: add configuration for default upload format

* fix: change default back to original
2022-11-22 20:36:49 -08:00
diced fd9908833a fix: link on files 2022-11-20 00:01:31 -08:00
diced 24f8300b2c fix: delete invites that expired or are used 2022-11-19 17:16:46 -08:00
diced 8d510f5d90 fix: typo 2022-11-19 17:05:15 -08:00
diced 6457680065 feat: exif metadata & remove gps 2022-11-19 13:58:14 -08:00
diced 3175911105 feat: totp 2022-11-17 22:13:23 -08:00
dicedtomato 00f26bdc75 fix: delete suggestion issues, use discussions 2022-11-17 16:04:08 -08:00
Winter 9db95bb772 fix: README spelling errors (#224) 2022-11-17 15:30:49 -08:00
231 changed files with 17605 additions and 5249 deletions
+10
View File
@@ -0,0 +1,10 @@
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-18
RUN usermod -l zipline node \
&& groupmod -n zipline node \
&& usermod -d /home/zipline zipline \
&& echo "zipline ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers.d/zipline \
&& chmod 0440 /etc/sudoers.d/zipline \
&& sudo apt-get update && apt-get install gnupg2 -y
USER zipline
+56
View File
@@ -0,0 +1,56 @@
{
"name": "Zipline Codespace",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/zipline",
"features": {
"ghcr.io/devcontainers/features/common-utils:2": {
"username": "zipline"
},
"ghcr.io/devcontainers/features/docker-in-docker:1": {
"dockerDashComposeVersion": "v2",
"installDockerBuildx": true
}
},
"customizations": {
"vscode": {
"settings": {
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"files.autoSave": "afterDelay",
"terminal.integrated.persistentSessionReviveProcess": "never",
"terminal.integrated.defaultProfile.linux": "zsh",
"terminal.integrated.profiles.linux": {
"zsh": {
"path": "/bin/zsh",
"env": {
"ZSH_THEME": "devcontainers"
}
}
}
},
"extensions": ["prisma.prisma", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
}
},
"remoteUser": "zipline",
"updateRemoteUserUID": true,
"remoteEnv": {
"CORE_DATABASE_URL": "postgres://postgres:postgres@localhost/zip10"
},
"portsAttributes": {
"3000": {
"label": "Zipline",
"onAutoForward": "openBrowser"
},
"5432": {
"label": "Postgres"
}
},
"postCreateCommand": "sudo chown -R zipline:zipline /zipline && yarn install"
}
+25
View File
@@ -0,0 +1,25 @@
version: '3.8'
services:
app:
build:
context: ./
dockerfile: Dockerfile
volumes:
- ../:/zipline:cached
- uploads:/zipline/uploads
- node_modules:/zipline/node_modules
command: sleep infinity
db:
image: postgres:latest
restart: unless-stopped
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DATABASE=postgres
volumes:
- pg_data:/var/lib/postgresql/data
volumes:
pg_data:
uploads:
node_modules:
+9 -10
View File
@@ -1,7 +1,7 @@
# 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
# if using s3/supabase make sure to comment out the other datasources
CORE_HTTPS=true
CORE_SECRET="changethis"
@@ -25,15 +25,14 @@ 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
# or supabase
DATASOURCE_TYPE=supabase
DATASOURCE_SUPABASE_KEY=xxx
# remember: no leading slash
DATASOURCE_SUPABASE_URL=https://something.supabase.co
DATASOURCE_SUPABASE_BUCKET=zipline
UPLOADER_DEFAULT_FORMAT=RANDOM
UPLOADER_ROUTE=/u
UPLOADER_LENGTH=6
UPLOADER_ADMIN_LIMIT=104900000
@@ -44,4 +43,4 @@ URLS_ROUTE=/go
URLS_LENGTH=6
RATELIMIT_USER = 5
RATELIMIT_ADMIN = 3
RATELIMIT_ADMIN = 3
+7
View File
@@ -0,0 +1,7 @@
node_modules
dist
.yarn
.devcontainer
.github
.next
.vscode
+18 -2
View File
@@ -1,5 +1,13 @@
{
"extends": ["next", "next/core-web-vitals", "plugin:prettier/recommended"],
"root": true,
"extends": [
"next",
"next/core-web-vitals",
"plugin:prettier/recommended",
"plugin:@typescript-eslint/recommended"
],
"plugins": ["unused-imports", "@typescript-eslint"],
"parser": "@typescript-eslint/parser",
"rules": {
"linebreak-style": ["error", "unix"],
"quotes": [
@@ -28,6 +36,14 @@
"react/style-prop-object": "warn",
"@next/next/no-img-element": "off",
"jsx-a11y/alt-text": "off",
"react/display-name": "off"
"react/display-name": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"error",
{ "vars": "all", "varsIgnorePattern": "^_", "args": "after-used", "argsIgnorePattern": "^_" }
],
"@typescript-eslint/ban-ts-comment": "off"
}
}
+3
View File
@@ -1,5 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Feature Request
url: https://github.com/diced/zipline/discussions/new?category=ideas&title=Your%20breif%20description%20here&labels=feature
about: Ask for a new feature
- name: Zipline Discord
url: https://discord.gg/EAhCRfGxCF
about: Ask for help with anything related to Zipline!
-12
View File
@@ -1,12 +0,0 @@
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
+31
View File
@@ -0,0 +1,31 @@
name: 'Issue/PR Milestones'
on:
pull_request_target:
types: [opened, reopened]
issues:
types: [opened, reopened]
permissions:
issues: write
checks: write
contents: read
pull-requests: write
jobs:
set:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/github-script@v6
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const milestone = 3
github.rest.issues.update({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
milestone
})
+1 -1
View File
@@ -41,5 +41,5 @@ yarn-error.log*
# zipline
config.toml
uploads/
uploads*/
dist/
+1
View File
@@ -0,0 +1 @@
package-lock=false
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -5,5 +5,7 @@ nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: "@yarnpkg/plugin-workspace-tools"
yarnPath: .yarn/releases/yarn-3.2.4.cjs
yarnPath: .yarn/releases/yarn-3.3.1.cjs
+1 -1
View File
@@ -12,7 +12,7 @@ Create an issue on GitHub, please include the following (if one of them is not a
## Feature requests
Create an issue on GitHub, please include the following:
Create an discussion on GitHub, please include the following:
- Breif explanation of the feature in the title (very breif please)
- How it would work (detailed, but optional)
+61 -49
View File
@@ -1,64 +1,76 @@
FROM ghcr.io/diced/prisma-binaries:4.5.x as prisma
# Use the Prisma binaries image as the first stage
FROM ghcr.io/diced/prisma-binaries:4.10.x as prisma
FROM node:alpine3.16 AS deps
RUN mkdir -p /prisma-engines
WORKDIR /build
# Use Alpine Linux as the second stage
FROM node:18-alpine3.16 as base
COPY .yarn .yarn
COPY package.json yarn.lock .yarnrc.yml ./
RUN yarn install --immutable
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 prisma ./prisma
COPY .yarn .yarn
COPY package.json yarn.lock .yarnrc.yml esbuild.config.js next.config.js next-env.d.ts zip-env.d.ts tsconfig.json mimes.json ./
ENV ZIPLINE_DOCKER_BUILD 1
ENV NEXT_TELEMETRY_DISABLED 1
RUN yarn build
FROM node:alpine3.16 AS runner
# Set the working directory
WORKDIR /zipline
# Copy the necessary files from the project
COPY prisma ./prisma
COPY src ./src
COPY next.config.js ./next.config.js
COPY tsup.config.ts ./tsup.config.ts
COPY tsconfig.json ./tsconfig.json
COPY mimes.json ./mimes.json
COPY public ./public
FROM base as builder
COPY .yarn ./.yarn
COPY package*.json ./
COPY yarn*.lock ./
COPY .yarnrc.yml ./
# Copy the prisma binaries from prisma stage
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
PRISMA_CLIENT_ENGINE_TYPE=binary \
ZIPLINE_DOCKER_BUILD=true \
NEXT_TELEMETRY_DISABLED=1
RUN apk add --no-cache openssl openssl-dev
# Install production dependencies then temporarily save
RUN yarn workspaces focus --production --all
RUN cp -RL node_modules /tmp/node_modules
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
# Install the dependencies
RUN yarn install --immutable
COPY --from=builder /build/.next ./.next
COPY --from=builder /build/node_modules ./node_modules
# Run the build
RUN yarn build
COPY --from=builder /build/next.config.js ./next.config.js
COPY --from=builder /build/esbuild.config.js ./esbuild.config.js
COPY --from=builder /build/src ./src
COPY --from=builder /build/dist ./dist
COPY --from=builder /build/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
# Use Alpine Linux as the final image
FROM base
# Install the necessary packages
RUN apk add --no-cache perl procps tini
CMD ["node", "--enable-source-maps", "dist/server"]
COPY --from=builder /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 \
NEXT_TELEMETRY_DISABLED=1
# Copy only the necessary files from the previous stage
COPY --from=builder /zipline/dist ./dist
COPY --from=builder /zipline/.next ./.next
COPY --from=builder /zipline/package.json ./package.json
COPY --from=builder /zipline/node_modules ./node_modules
COPY --from=builder /zipline/node_modules/.prisma/client ./node_modules/.prisma/client
COPY --from=builder /zipline/node_modules/@prisma/client ./node_modules/@prisma/client
# Copy Startup Script
COPY docker-entrypoint.sh /zipline
# Make Startup Script Executable
RUN chmod a+x /zipline/docker-entrypoint.sh && rm -rf /zipline/src
# Set the entrypoint to the startup script
ENTRYPOINT ["tini", "--", "/zipline/docker-entrypoint.sh"]
+32 -11
View File
@@ -8,9 +8,9 @@ A ShareX/file upload server that is easy to use, packed with features, and with
![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)
![Build](https://img.shields.io/github/actions/workflow/status/diced/zipline/build.yml?logo=github&style=flat&branch=trunk)
[![Docker Image (trunk)](https://img.shields.io/github/actions/workflow/status/diced/zipline/docker.yml?label=Docker%20%28trunk%29&logo=github&style=flat&branch=trunk)](https://github.com/diced/zipline/pkgs/container/zipline/?tag=trunk)
[![Docker Image (release)](https://img.shields.io/github/actions/workflow/status/diced/zipline/docker-release.yml?label=Docker%20%28release%29&logo=github&style=flat&branch=trunk)](https://github.com/diced/zipline/pkgs/container/zipline/?tag=latest)
</div>
@@ -25,16 +25,26 @@ A ShareX/file upload server that is easy to use, packed with features, and with
- Password Protected Uploads
- URL shortening
- Text uploading
- URL Formats (uuid, dates, random alphanumeric, original name, zws)
- URL Formats (uuid, dates, random alphanumeric, original name, zws, gfycat -> [animals](https://assets.gfycat.com/animals) [adjectives](https://assets.gfycat.com/adjectives))
- Discord embeds (OG metadata)
- Gallery viewer, and multiple file format support
- Code highlighting
- Fully customizable Discord webhook notifications
- OAuth2 registration (Discord and GitHub)
- Two-Factor authentication with Google Authenticator, Authy, etc (totp services).
- User invites
- File Chunking (for large files)
- File deletion once it reaches a certain amount of views
- Easy setup instructions on [docs](https://zipl.vercel.app/) (One command install `docker-compose up -d`)
- Easy setup instructions on [docs](https://zipl.vercel.app/) (One command install `docker compose up -d`)
<details>
<summary>View upstream documentation</summary>
The website below provides documentation for more up-to-date features with the upstream branch. The normal documentation is for the latest release and is not updated unless a new release is made.
[https://trunk.zipline.diced.tech/](https://trunk.zipline.diced.tech/)
</details>
<details>
<summary><h2>Screenshots (click)</h2></summary>
@@ -50,13 +60,13 @@ A ShareX/file upload server that is easy to use, packed with features, and with
## Install & run with Docker
This section requires [Docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/).
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
docker compose up -d
```
### After installing
@@ -113,6 +123,17 @@ After navigating to Zipline, click on the top right corner where it says your us
This section requires [Flameshot](https://www.flameshot.org/), [jq](https://stedolan.github.io/jq/), and [xsel](https://github.com/kfish/xsel).
<details>
<summary>Wayland instructions</summary>
If using wayland you will need to have [wl-clipboard](https://github.com/bugaevc/wl-clipboard) installed, for the `wl-copy` command.
If you are not using GNOME/KDE/Qtile/Sway, and are using something like a wlroots-based compositor (ex. [Hyprland](https://github.com/hyprwm/Hyprland/), [River](https://github.com/riverwm/river), etc), you will need to set the `XDG_CURRENT_DESKTOP` environment variable to `sway`, which will just override it for this script. Adding `export XDG_CURRENT_DESKTOP=sway` to the start of the script will work.
After this, replace the `xsel -ib` with `wl-copy` in the script.
</details>
You can either use the script below, or generate one directly from Zipline (just like how you can generate a ShareX config).
To upload files using flameshot we will use a script. Replace $TOKEN and $HOST with your own values, you probably know how to do this if you use linux.
@@ -127,7 +148,7 @@ curl -H "Content-Type: multipart/form-data" -H "authorization: $TOKEN" -F file=@
## 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):
Create an issue on GitHub and use the template, please include the following (if one of them is not applicable to the issue then it's not needed):
- The steps to reproduce the bug
- Logs of Zipline
@@ -137,10 +158,10 @@ Create an issue on GitHub, please include the following (if one of them is not a
## Feature requests
Create an issue on GitHub, please include the following:
Create a discussion on GitHub, please include the following:
- Breif explanation of the feature in the title (very breif please)
- How it would work (detailed, but optional)
- Brief explanation of the feature in the title (very brief please)
- How it would work (Be detailed!)
## Pull Requests (contributions to the codebase)
+1 -1
View File
@@ -4,7 +4,7 @@
| Version | Supported |
| ------- | ------------------ |
| 3.4.8 | :white_check_mark: |
| 3.6.x | :white_check_mark: |
| < 3 | :x: |
| < 2 | :x: |
+1 -3
View File
@@ -1,8 +1,7 @@
version: '3'
services:
postgres:
image: postgres
restart: always
image: postgres:15
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
@@ -21,7 +20,6 @@ services:
dockerfile: Dockerfile
ports:
- '3000:3000'
restart: unless-stopped
env_file:
- .env.local
volumes:
+4 -4
View File
@@ -1,8 +1,8 @@
version: '3'
services:
postgres:
image: postgres
restart: always
image: postgres:15
restart: unless-stopped
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
@@ -19,9 +19,9 @@ services:
image: ghcr.io/diced/zipline
ports:
- '3000:3000'
restart: always
restart: unless-stopped
environment:
- CORE_HTTPS=false
- CORE_RETURN_HTTPS=false
- CORE_SECRET=changethis
- CORE_HOST=0.0.0.0
- CORE_PORT=3000
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
set -e
node --enable-source-maps dist/index.js
-23
View File
@@ -1,23 +0,0 @@
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,
});
})();
-9
View File
@@ -2,15 +2,6 @@
* @type {import('next').NextConfig}
**/
module.exports = {
async redirects() {
return [
{
source: '/',
destination: '/dashboard',
permanent: true,
},
];
},
images: {
domains: [
// For sharex icon in manage user
+67 -53
View File
@@ -1,88 +1,102 @@
{
"name": "zipline",
"version": "3.6.1",
"version": "3.7.0",
"license": "MIT",
"scripts": {
"dev": "npm-run-all build:server dev:run",
"dev:run": "cross-env DEBUG=true REACT_EDITOR=code NODE_ENV=development RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED=false node --enable-source-maps dist/server",
"dev:run": "cross-env DEBUG=true REACT_EDITOR=code NODE_ENV=development RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED=false node --enable-source-maps dist",
"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:server": "tsup",
"build:next": "next build",
"build:schema": "prisma generate --schema=prisma/schema.prisma",
"format": "prettier --write ./src/**/*.{ts,tsx} ./*.{md,js,json,yml}",
"migrate:dev": "prisma migrate dev --create-only",
"start": "node dist/server",
"start": "node dist",
"lint": "next lint",
"docker:run": "docker-compose up -d",
"docker:down": "docker-compose down",
"docker:build-dev": "docker-compose --file docker-compose.dev.yml up --build",
"scripts:read-config": "node dist/scripts/read-config",
"scripts:import-dir": "node dist/scripts/import-dir",
"scripts:list-users": "node dist/scripts/list-users",
"scripts:set-user": "node dist/scripts/set-user"
"compose:up": "docker compose up",
"compose:down": "docker compose down",
"compose:build-dev": "docker compose --file docker-compose.dev.yml up --build",
"compose:up-dev": "docker compose --file docker-compose.dev.yml up",
"compose:down-dev": "docker compose --file docker-compose.dev.yml down",
"scripts:read-config": "node --enable-source-maps dist/scripts/read-config",
"scripts:import-dir": "node --enable-source-maps dist/scripts/import-dir",
"scripts:list-users": "node --enable-source-maps dist/scripts/list-users",
"scripts:set-user": "node --enable-source-maps dist/scripts/set-user",
"scripts:clear-zero-byte": "node --enable-source-maps dist/scripts/clear-zero-byte",
"scripts:query-size": "node --enable-source-maps dist/scripts/query-size"
},
"dependencies": {
"@dicedtomato/mantine-data-grid": "0.0.23",
"@emotion/react": "^11.10.5",
"@emotion/react": "^11.10.6",
"@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",
"@mantine/core": "^6.0.4",
"@mantine/dropzone": "^6.0.4",
"@mantine/form": "^6.0.4",
"@mantine/hooks": "^6.0.4",
"@mantine/modals": "^6.0.4",
"@mantine/next": "^6.0.4",
"@mantine/notifications": "^6.0.4",
"@mantine/prism": "^6.0.4",
"@mantine/spotlight": "^6.0.4",
"@prisma/client": "^4.10.1",
"@prisma/internals": "^4.10.1",
"@prisma/migrate": "^4.10.1",
"@sapphire/shapeshift": "^3.8.1",
"@tabler/icons-react": "^2.11.0",
"@tanstack/react-query": "^4.28.0",
"argon2": "^0.30.3",
"cookie": "^0.5.0",
"dayjs": "^1.11.6",
"dayjs": "^1.11.7",
"dotenv": "^16.0.3",
"dotenv-expand": "^9.0.0",
"dotenv-expand": "^10.0.0",
"exiftool-vendored": "^21.2.0",
"fastify": "^4.15.0",
"fastify-plugin": "^4.5.0",
"fflate": "^0.7.4",
"find-my-way": "^7.3.1",
"minio": "^7.0.32",
"find-my-way": "^7.6.0",
"katex": "^0.16.4",
"mantine-datatable": "^2.2.6",
"minio": "^7.0.33",
"ms": "canary",
"multer": "^1.4.5-lts.1",
"next": "^13.0.0",
"prisma": "^4.5.0",
"next": "^13.2.4",
"otplib": "^12.0.1",
"prisma": "^4.10.1",
"prismjs": "^1.29.0",
"qrcode": "^1.5.1",
"react": "^18.2.0",
"react-chartjs-2": "^4.3.1",
"react-dom": "^18.2.0",
"react-feather": "^2.0.10",
"recoil": "^0.7.6",
"sharp": "^0.31.1"
"react-markdown": "^8.0.6",
"recharts": "^2.5.0",
"recoil": "^0.7.7",
"remark-gfm": "^3.0.1",
"sharp": "^0.32.0"
},
"devDependencies": {
"@types/cookie": "^0.5.1",
"@types/minio": "^7.0.14",
"@types/katex": "^0.16.0",
"@types/minio": "^7.0.17",
"@types/multer": "^1.4.7",
"@types/node": "^18.11.7",
"@types/react": "^18.0.24",
"@types/sharp": "^0.31.0",
"@types/node": "^18.15.10",
"@types/qrcode": "^1.5.0",
"@types/react": "^18.0.29",
"@types/sharp": "^0.31.1",
"@typescript-eslint/eslint-plugin": "^5.56.0",
"@typescript-eslint/parser": "^5.56.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": "^8.36.0",
"eslint-config-next": "^13.2.4",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-unused-imports": "^2.0.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.7.1",
"typescript": "^4.8.4"
"prettier": "^2.8.7",
"tsup": "^6.7.0",
"typescript": "^5.0.2"
},
"repository": {
"type": "git",
"url": "https://github.com/diced/zipline.git"
},
"packageManager": "yarn@3.2.4"
"packageManager": "yarn@3.3.1"
}
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "totpSecret" TEXT;
@@ -0,0 +1,26 @@
-- DropForeignKey
ALTER TABLE "InvisibleImage" DROP CONSTRAINT "InvisibleImage_imageId_fkey";
-- DropForeignKey
ALTER TABLE "InvisibleUrl" DROP CONSTRAINT "InvisibleUrl_urlId_fkey";
-- DropForeignKey
ALTER TABLE "Invite" DROP CONSTRAINT "Invite_createdById_fkey";
-- DropForeignKey
ALTER TABLE "Url" DROP CONSTRAINT "Url_userId_fkey";
-- AlterTable
ALTER TABLE "Url" ALTER COLUMN "userId" DROP NOT NULL;
-- AddForeignKey
ALTER TABLE "InvisibleImage" ADD CONSTRAINT "InvisibleImage_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Url" ADD CONSTRAINT "Url_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "InvisibleUrl" ADD CONSTRAINT "InvisibleUrl_urlId_fkey" FOREIGN KEY ("urlId") REFERENCES "Url"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Invite" ADD CONSTRAINT "Invite_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,12 @@
/*
Warnings:
- A unique constraint covering the columns `[provider,oauthId]` on the table `OAuth` will be added. If there are existing duplicate values, this will fail.
- Added the required column `oauthId` to the `OAuth` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "OAuth" ADD COLUMN "oauthId" TEXT;
-- CreateIndex
CREATE UNIQUE INDEX "OAuth_provider_oauthId_key" ON "OAuth"("provider", "oauthId");
@@ -0,0 +1,13 @@
/*
Warnings:
- You are about to drop the column `embedColor` on the `User` table. All the data in the column will be lost.
- You are about to drop the column `embedSiteName` on the `User` table. All the data in the column will be lost.
- You are about to drop the column `embedTitle` on the `User` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "User" DROP COLUMN "embedColor",
DROP COLUMN "embedSiteName",
DROP COLUMN "embedTitle",
ADD COLUMN "embed" JSONB NOT NULL DEFAULT '{}';
@@ -0,0 +1,8 @@
-- AlterTable
ALTER TABLE "Image" RENAME COLUMN "created_at" TO "createdAt";
-- AlterTable
ALTER TABLE "Image" RENAME COLUMN "expires_at" TO "expiresAt";
-- AlterTable
ALTER TABLE "Image" RENAME COLUMN "file" TO "name";
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Url" RENAME COLUMN "created_at" TO "createdAt";
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Stats" RENAME COLUMN "created_at" TO "createdAt";
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "Invite" RENAME COLUMN "created_at" TO "createdAt";
-- AlterTable
ALTER TABLE "Invite" RENAME COLUMN "expires_at" TO "expiresAt";
@@ -0,0 +1,19 @@
-- AlterEnum
ALTER TYPE "ImageFormat" RENAME TO "FileNameFormat";
-- AlterTable
ALTER TABLE "Image" RENAME TO "File";
-- AlterTable
ALTER TABLE "InvisibleImage" RENAME TO "InvisibleFile";
-- AlterTable
ALTER TABLE "InvisibleFile" RENAME COLUMN "imageId" TO "fileId";
-- AlterForeignKey
ALTER TABLE "InvisibleFile" RENAME CONSTRAINT "InvisibleImage_imageId_fkey" TO "InvisibleFile_fileId_fkey";
ALTER INDEX "InvisibleImage_imageId_key" RENAME TO "InvisibleFile_fileId_key";
-- AlterForeignKey
ALTER TABLE "InvisibleFile" RENAME CONSTRAINT "InvisibleImage_pkey" TO "InvisibleFile_pkey";
ALTER TABLE "File" RENAME CONSTRAINT "Image_pkey" TO "File_pkey";
@@ -0,0 +1,8 @@
-- AlterTable
ALTER TABLE "File" ADD COLUMN "originalName" TEXT;
-- RenameForeignKey
ALTER TABLE "File" RENAME CONSTRAINT "Image_userId_fkey" TO "File_userId_fkey";
-- RenameIndex
ALTER INDEX "InvisibleImage_invis_key" RENAME TO "InvisibleFile_invis_key";
@@ -0,0 +1,19 @@
-- AlterTable
ALTER TABLE "File" ADD COLUMN "folderId" INTEGER;
-- CreateTable
CREATE TABLE "Folder" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"userId" INTEGER NOT NULL,
CONSTRAINT "Folder_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Folder" ADD CONSTRAINT "Folder_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "File" ADD CONSTRAINT "File_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "Folder"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Folder" ADD COLUMN "public" BOOLEAN NOT NULL DEFAULT false;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "File" ADD COLUMN "size" INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,11 @@
/*
Warnings:
- You are about to drop the column `format` on the `File` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "File" DROP COLUMN "format";
-- DropEnum
DROP TYPE "FileNameFormat";
@@ -0,0 +1,18 @@
-- CreateEnum
CREATE TYPE "ProcessingStatus" AS ENUM ('PENDING', 'PROCESSING', 'COMPLETE');
-- CreateTable
CREATE TABLE "IncompleteFile" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"status" "ProcessingStatus" NOT NULL,
"chunks" INTEGER NOT NULL,
"chunksComplete" INTEGER NOT NULL,
"userId" INTEGER NOT NULL,
"data" JSONB NOT NULL,
CONSTRAINT "IncompleteFile_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "IncompleteFile" ADD CONSTRAINT "IncompleteFile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+94 -55
View File
@@ -8,88 +8,104 @@ generator client {
}
model User {
id Int @id @default(autoincrement())
username String
password String?
avatar String?
token String
administrator Boolean @default(false)
superAdmin Boolean @default(false)
systemTheme String @default("system")
embedTitle String?
embedColor String @default("#2f3136")
embedSiteName String? @default("{image.file} • {user.name}")
ratelimit DateTime?
domains String[]
oauth OAuth[]
images Image[]
urls Url[]
Invite Invite[]
id Int @id @default(autoincrement())
username String
password String?
avatar String?
token String
administrator Boolean @default(false)
superAdmin Boolean @default(false)
systemTheme String @default("system")
embed Json @default("{}")
ratelimit DateTime?
totpSecret String?
domains String[]
oauth OAuth[]
files File[]
urls Url[]
Invite Invite[]
Folder Folder[]
IncompleteFile IncompleteFile[]
}
enum ImageFormat {
UUID
DATE
RANDOM
NAME
model Folder {
id Int @id @default(autoincrement())
name String
public Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
files File[]
}
model Image {
id Int @id @default(autoincrement())
file String
mimetype String @default("image/png")
created_at DateTime @default(now())
expires_at DateTime?
maxViews Int?
views Int @default(0)
favorite Boolean @default(false)
embed Boolean @default(false)
password String?
invisible InvisibleImage?
format ImageFormat @default(RANDOM)
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
userId Int?
model File {
id Int @id @default(autoincrement())
name String
originalName String?
mimetype String @default("image/png")
createdAt DateTime @default(now())
size Int @default(0)
expiresAt DateTime?
maxViews Int?
views Int @default(0)
favorite Boolean @default(false)
embed Boolean @default(false)
password String?
invisible InvisibleFile?
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
userId Int?
folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
folderId Int?
}
model InvisibleImage {
id Int @id @default(autoincrement())
invis String @unique
imageId Int @unique
image Image @relation(fields: [imageId], references: [id])
model InvisibleFile {
id Int @id @default(autoincrement())
invis String @unique
fileId Int @unique
file File @relation(fields: [fileId], references: [id], onDelete: Cascade)
}
model Url {
id String @id @unique
destination String
vanity String?
created_at DateTime @default(now())
createdAt DateTime @default(now())
maxViews Int?
views Int @default(0)
invisible InvisibleUrl?
user User @relation(fields: [userId], references: [id])
userId Int
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
userId Int?
}
model InvisibleUrl {
id Int @id @default(autoincrement())
invis String @unique
urlId String @unique
url Url @relation(fields: [urlId], references: [id])
url Url @relation(fields: [urlId], references: [id], onDelete: Cascade)
}
model Stats {
id Int @id @default(autoincrement())
created_at DateTime @default(now())
data Json
id Int @id @default(autoincrement())
createdAt 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])
id Int @id @default(autoincrement())
code String @unique
createdAt DateTime @default(now())
expiresAt DateTime?
used Boolean @default(false)
createdBy User @relation(fields: [createdById], references: [id], onDelete: Cascade)
createdById Int
}
@@ -99,8 +115,11 @@ model OAuth {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
username String
oauthId String?
token String
refresh String?
@@unique([provider, oauthId])
}
enum OauthProviders {
@@ -108,3 +127,23 @@ enum OauthProviders {
GITHUB
GOOGLE
}
model IncompleteFile {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
status ProcessingStatus
chunks Int
chunksComplete Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
data Json
}
enum ProcessingStatus {
PENDING
PROCESSING
COMPLETE
}
File diff suppressed because it is too large Load Diff
+1750
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
import { Anchor } from '@mantine/core';
import Link from 'next/link';
export default function AnchorNext({ href, ...others }) {
return <Anchor component={Link} href={href} {...others} />;
}
+26 -5
View File
@@ -1,15 +1,36 @@
import { createStyles, MantineSize, Textarea } from '@mantine/core';
import { createStyles, Textarea } from '@mantine/core';
import { useEffect } from 'react';
const useStyles = createStyles((theme, { size }: { size: MantineSize }) => ({
const useStyles = createStyles(() => ({
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' });
const { classes } = useStyles(null, { name: 'CodeInput' });
return <Textarea classNames={{ input: classes.input }} autoComplete='nope' {...props} />;
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Tab') {
if (document.activeElement?.tagName !== 'TEXTAREA') return;
e.preventDefault();
const target = e.target as HTMLTextAreaElement;
const start = target.selectionStart;
const end = target.selectionEnd;
target.value = `${target.value.substring(0, start)} ${target.value.substring(end)}`;
target.selectionStart = target.selectionEnd = start + 2;
target.focus();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, []);
return <Textarea classNames={{ input: classes.input }} {...props} />;
}
-195
View File
@@ -1,195 +0,0 @@
import { Button, Card, Group, LoadingOverlay, Modal, Stack, Text, Title, Tooltip } from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { showNotification } from '@mantine/notifications';
import { useFileDelete, useFileFavorite } from 'lib/queries/files';
import { relativeTime } from 'lib/utils/client';
import { useState } from 'react';
import {
CalendarIcon,
ClockIcon,
CopyIcon,
CrossIcon,
DeleteIcon,
ExternalLinkIcon,
EyeIcon,
FileIcon,
HashIcon,
ImageIcon,
StarIcon,
} from './icons';
import Link from './Link';
import MutedText from './MutedText';
import Type from './Type';
export function FileMeta({ Icon, title, subtitle, ...other }) {
return other.tooltip ? (
<Group>
<Icon size={24} />
<Tooltip label={other.tooltip}>
<Stack spacing={1}>
<Text>{title}</Text>
<MutedText size='md'>{subtitle}</MutedText>
</Stack>
</Tooltip>
</Group>
) : (
<Group>
<Icon size={24} />
<Stack spacing={1}>
<Text>{title}</Text>
<MutedText size='md'>{subtitle}</MutedText>
</Stack>
</Group>
);
}
export default function File({ image, 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>
</>
);
}
+358
View File
@@ -0,0 +1,358 @@
import {
ActionIcon,
Group,
LoadingOverlay,
Modal,
Select,
SimpleGrid,
Stack,
Title,
Tooltip,
} from '@mantine/core';
import { useClipboard, useMediaQuery } from '@mantine/hooks';
import { showNotification } from '@mantine/notifications';
import {
IconAlarm,
IconCalendarPlus,
IconClipboardCopy,
IconDeviceSdCard,
IconExternalLink,
IconEye,
IconEyeglass,
IconFile,
IconFileDownload,
IconFolderCancel,
IconFolderMinus,
IconFolderPlus,
IconHash,
IconInfoCircle,
IconPhoto,
IconPhotoCancel,
IconPhotoMinus,
IconPhotoStar,
} from '@tabler/icons-react';
import useFetch, { ApiError } from 'hooks/useFetch';
import { useFileDelete, useFileFavorite, UserFilesResponse } from 'lib/queries/files';
import { useFolders } from 'lib/queries/folders';
import { bytesToHuman } from 'lib/utils/bytes';
import { relativeTime } from 'lib/utils/client';
import { useState } from 'react';
import { FileMeta } from '.';
import Type from '../Type';
export default function FileModal({
open,
setOpen,
file,
loading,
refresh,
reducedActions = false,
exifEnabled,
compress,
}: {
open: boolean;
setOpen: (open: boolean) => void;
file: UserFilesResponse;
loading: boolean;
refresh: () => void;
reducedActions?: boolean;
exifEnabled?: boolean;
compress: boolean;
}) {
const deleteFile = useFileDelete();
const favoriteFile = useFileFavorite();
const folders = useFolders();
const [overrideRender, setOverrideRender] = useState(false);
const clipboard = useClipboard();
const handleDelete = async () => {
deleteFile.mutate(file.id, {
onSuccess: () => {
showNotification({
title: 'File Deleted',
message: '',
color: 'green',
icon: <IconPhotoMinus size='1rem' />,
});
},
onError: (res: ApiError) => {
showNotification({
title: 'Failed to delete file',
message: res.error,
color: 'red',
icon: <IconPhotoCancel size='1rem' />,
});
},
onSettled: () => {
setOpen(false);
},
});
};
const handleCopy = () => {
clipboard.copy(`${window.location.protocol}//${window.location.host}${file.url}`);
setOpen(false);
if (!navigator.clipboard)
showNotification({
title: 'Unable to copy to clipboard',
message: 'Zipline is unable to copy to clipboard due to security reasons.',
color: 'red',
});
else
showNotification({
title: 'Copied to clipboard',
message: '',
icon: <IconClipboardCopy size='1rem' />,
});
};
const handleFavorite = async () => {
favoriteFile.mutate(
{ id: file.id, favorite: !file.favorite },
{
onSuccess: () => {
showNotification({
title: 'The file is now ' + (!file.favorite ? 'favorited' : 'unfavorited'),
message: '',
icon: <IconPhotoStar size='1rem' />,
});
},
onError: (res: { error: string }) => {
showNotification({
title: 'Failed to favorite file',
message: res.error,
color: 'red',
icon: <IconPhotoCancel size='1rem' />,
});
},
}
);
};
const inFolder = file.folderId;
const removeFromFolder = async () => {
const res = await useFetch('/api/user/folders/' + file.folderId, 'DELETE', {
file: Number(file.id),
});
refresh();
if (!res.error) {
showNotification({
title: 'Removed from folder',
message: res.name,
color: 'green',
icon: <IconFolderMinus size='1rem' />,
});
} else {
showNotification({
title: 'Failed to remove from folder',
message: res.error,
color: 'red',
icon: <IconFolderCancel size='1rem' />,
});
}
};
const addToFolder = async (t) => {
const res = await useFetch('/api/user/folders/' + t, 'POST', {
file: Number(file.id),
});
refresh();
if (!res.error) {
showNotification({
title: 'Added to folder',
message: res.name,
color: 'green',
icon: <IconFolderPlus size='1rem' />,
});
} else {
showNotification({
title: 'Failed to add to folder',
message: res.error,
color: 'red',
icon: <IconFolderCancel size='1rem' />,
});
}
};
const createFolder = (t) => {
useFetch('/api/user/folders', 'POST', {
name: t,
add: [Number(file.id)],
}).then((res) => {
refresh();
if (!res.error) {
showNotification({
title: 'Created & added to folder',
message: res.name,
color: 'green',
icon: <IconFolderPlus size='1rem' />,
});
} else {
showNotification({
title: 'Failed to create folder',
message: res.error,
color: 'red',
icon: <IconFolderCancel size='1rem' />,
});
}
});
return { value: t, label: t };
};
return (
<Modal
opened={open}
onClose={() => setOpen(false)}
title={<Title>{file.name}</Title>}
size='auto'
fullScreen={useMediaQuery('(max-width: 600px)')}
>
<LoadingOverlay visible={loading} />
<Stack>
<Type
file={file}
src={`/r/${encodeURI(file.name)}?compress=${compress}`}
alt={file.name}
popup
sx={{ minHeight: 200 }}
style={{ minHeight: 200 }}
disableMediaPreview={false}
overrideRender={overrideRender}
setOverrideRender={setOverrideRender}
/>
<SimpleGrid
my='md'
cols={3}
breakpoints={[
{ maxWidth: 600, cols: 1 },
{ maxWidth: 900, cols: 2 },
{ maxWidth: 1200, cols: 3 },
]}
>
<FileMeta Icon={IconFile} title='Name' subtitle={file.name} />
<FileMeta Icon={IconPhoto} title='Type' subtitle={file.mimetype} />
<FileMeta Icon={IconDeviceSdCard} title='Size' subtitle={bytesToHuman(file.size || 0)} />
<FileMeta Icon={IconEye} title='Views' subtitle={file?.views?.toLocaleString()} />
{file.maxViews && (
<FileMeta
Icon={IconEyeglass}
title='Max views'
subtitle={file?.maxViews?.toLocaleString()}
tooltip={`This file will be deleted after being viewed ${file?.maxViews?.toLocaleString()} times.`}
/>
)}
<FileMeta
Icon={IconCalendarPlus}
title='Uploaded'
subtitle={relativeTime(new Date(file.createdAt))}
tooltip={new Date(file?.createdAt).toLocaleString()}
/>
{file.expiresAt && !reducedActions && (
<FileMeta
Icon={IconAlarm}
title='Expires'
subtitle={relativeTime(new Date(file.expiresAt))}
tooltip={new Date(file.expiresAt).toLocaleString()}
/>
)}
<FileMeta Icon={IconHash} title='ID' subtitle={file.id} />
</SimpleGrid>
</Stack>
<Group position='apart' my='md'>
<Group position='left'>
{exifEnabled && !reducedActions && (
<Tooltip label='View Metadata'>
<ActionIcon
color='blue'
variant='filled'
onClick={() => window.open(`/dashboard/metadata/${file.id}`, '_blank')}
>
<IconInfoCircle size='1rem' />
</ActionIcon>
</Tooltip>
)}
{reducedActions ? null : inFolder && !folders.isLoading ? (
<Tooltip
label={`Remove from folder "${folders.data.find((f) => f.id === file.folderId)?.name ?? ''}"`}
>
<ActionIcon color='red' variant='filled' onClick={removeFromFolder} loading={folders.isLoading}>
<IconFolderMinus size='1rem' />
</ActionIcon>
</Tooltip>
) : (
<Tooltip label='Add to folder'>
<Select
onChange={addToFolder}
placeholder='Add to folder'
data={[
...(folders.data ? folders.data : []).map((folder) => ({
value: String(folder.id),
label: `${folder.id}: ${folder.name}`,
})),
]}
searchable
creatable
getCreateLabel={(query) => `Create folder "${query}"`}
onCreate={createFolder}
/>
</Tooltip>
)}
</Group>
<Group position='right'>
{reducedActions ? null : (
<>
<Tooltip label='Delete file'>
<ActionIcon color='red' variant='filled' onClick={handleDelete}>
<IconPhotoMinus size='1rem' />
</ActionIcon>
</Tooltip>
<Tooltip label={file.favorite ? 'Unfavorite' : 'Favorite'}>
<ActionIcon
color={file.favorite ? 'yellow' : 'gray'}
variant='filled'
onClick={handleFavorite}
>
<IconPhotoStar size='1rem' />
</ActionIcon>
</Tooltip>
</>
)}
<Tooltip label='Open in new tab'>
<ActionIcon color='blue' variant='filled' onClick={() => window.open(file.url, '_blank')}>
<IconExternalLink size='1rem' />
</ActionIcon>
</Tooltip>
<Tooltip label='Copy URL'>
<ActionIcon color='blue' variant='filled' onClick={handleCopy}>
<IconClipboardCopy size='1rem' />
</ActionIcon>
</Tooltip>
<Tooltip label='Download'>
<ActionIcon
color='blue'
variant='filled'
onClick={() => window.open(`/r/${encodeURI(file.name)}?download=true`, '_blank')}
>
<IconFileDownload size='1rem' />
</ActionIcon>
</Tooltip>
</Group>
</Group>
</Modal>
);
}
+91
View File
@@ -0,0 +1,91 @@
import { Card, Group, LoadingOverlay, Stack, Text, Tooltip } from '@mantine/core';
import { useFileDelete, useFileFavorite } from 'lib/queries/files';
import { useFolders } from 'lib/queries/folders';
import { useState } from 'react';
import MutedText from '../MutedText';
import Type from '../Type';
import FileModal from './FileModal';
export function FileMeta({ Icon, title, subtitle, ...other }) {
return other.tooltip ? (
<Group>
<Icon size={24} />
<Tooltip label={other.tooltip}>
<Stack spacing={1}>
<Text>{title}</Text>
<MutedText size='md'>{subtitle}</MutedText>
</Stack>
</Tooltip>
</Group>
) : (
<Group>
<Icon size={24} />
<Stack spacing={1}>
<Text>{title}</Text>
<MutedText size='md'>{subtitle}</MutedText>
</Stack>
</Group>
);
}
export default function File({
image,
disableMediaPreview,
exifEnabled,
refreshImages,
reducedActions = false,
onDash,
}) {
const [open, setOpen] = useState(false);
const deleteFile = useFileDelete();
const favoriteFile = useFileFavorite();
const loading = deleteFile.isLoading || favoriteFile.isLoading;
const folders = useFolders();
const refresh = () => {
refreshImages();
folders.refetch();
};
return (
<>
<FileModal
open={open}
setOpen={setOpen}
file={image}
loading={loading}
refresh={refresh}
reducedActions={reducedActions}
exifEnabled={exifEnabled}
compress={onDash}
/>
<Card sx={{ maxWidth: '100%', height: '100%' }} shadow='md' onClick={() => setOpen(true)}>
<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/${encodeURI(image.name)}?compress=${onDash}`}
alt={image.name}
disableMediaPreview={disableMediaPreview}
/>
</Card.Section>
</Card>
</>
);
}
+267 -251
View File
@@ -7,155 +7,126 @@ import {
Group,
Header,
Image,
Input,
MediaQuery,
Menu,
Navbar,
NavLink,
Paper,
Popover,
rem,
ScrollArea,
Select,
Stack,
Text,
Title,
Tooltip,
UnstyledButton,
useMantineTheme,
} from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { useClipboard, useMediaQuery } from '@mantine/hooks';
import { useModals } from '@mantine/modals';
import { showNotification } from '@mantine/notifications';
import {
IconBackspace,
IconBrandDiscordFilled,
IconBrandGithubFilled,
IconBrandGoogle,
IconBrush,
IconClipboardCopy,
IconExternalLink,
IconFiles,
IconFileText,
IconFileUpload,
IconFolders,
IconGraph,
IconHome,
IconLink,
IconLogout,
IconReload,
IconSettings,
IconTag,
IconUpload,
IconUser,
IconUserCog,
IconUsers,
} from '@tabler/icons-react';
import useFetch from 'hooks/useFetch';
import { useVersion } from 'lib/queries/version';
import { userSelector } from 'lib/recoil/user';
import { capitalize } from 'lib/utils/client';
import { UserExtended } from 'middleware/withZipline';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { useRecoilState } from 'recoil';
import {
ActivityIcon,
CheckIcon,
CopyIcon,
CrossIcon,
DeleteIcon,
DiscordIcon,
ExternalLinkIcon,
FileIcon,
GitHubIcon,
GoogleIcon,
HomeIcon,
LinkIcon,
LogoutIcon,
PencilIcon,
SettingsIcon,
TagIcon,
TypeIcon,
UploadIcon,
UserIcon,
} from './icons';
import { friendlyThemeName, themes } from './Theming';
function MenuItemLink(props) {
return (
<Link href={props.href} passHref legacyBehavior>
<MenuItem {...props} />
</Link>
);
}
export type NavbarItems = {
icon: React.ReactNode;
text: string;
link?: string;
children?: NavbarItems[];
if?: (user: UserExtended, props: unknown) => boolean;
};
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 = [
const items: NavbarItems[] = [
{
icon: <HomeIcon size={18} />,
icon: <IconHome size={18} />,
text: 'Home',
link: '/dashboard',
},
{
icon: <FileIcon size={18} />,
icon: <IconFiles size={18} />,
text: 'Files',
link: '/dashboard/files',
},
{
icon: <ActivityIcon size={18} />,
icon: <IconFolders size={18} />,
text: 'Folders',
link: '/dashboard/folders',
},
{
icon: <IconGraph size={18} />,
text: 'Stats',
link: '/dashboard/stats',
},
{
icon: <LinkIcon size={18} />,
icon: <IconLink size={18} />,
text: 'URLs',
link: '/dashboard/urls',
},
{
icon: <UploadIcon size={18} />,
icon: <IconUpload size={18} />,
text: 'Upload',
link: '/dashboard/upload/file',
children: [
{
icon: <IconFileUpload size={18} />,
text: 'File',
link: '/dashboard/upload/file',
},
{
icon: <IconFileText size={18} />,
text: 'Text',
link: '/dashboard/upload/text',
},
],
},
{
icon: <TypeIcon size={18} />,
text: 'Upload Text',
link: '/dashboard/upload/text',
},
];
const admin_items = [
{
icon: <UserIcon size={18} />,
text: 'Users',
link: '/dashboard/users',
if: () => true,
},
{
icon: <TagIcon size={18} />,
text: 'Invites',
link: '/dashboard/invites',
if: (props) => props.invites,
icon: <IconUser size={18} />,
text: 'Administration',
if: (user, _) => user.administrator as boolean,
children: [
{
icon: <IconUsers size={18} />,
text: 'Users',
link: '/dashboard/users',
if: () => true,
},
{
icon: <IconTag size={18} />,
text: 'Invites',
link: '/dashboard/invites',
if: (_, props: { invites: boolean }) => props.invites,
},
],
},
];
@@ -165,9 +136,9 @@ export default function Layout({ children, props }) {
const { title, oauth_providers: unparsed } = props;
const oauth_providers = JSON.parse(unparsed);
const icons = {
GitHub: GitHubIcon,
Discord: DiscordIcon,
Google: GoogleIcon,
GitHub: IconBrandGithubFilled,
Discord: IconBrandDiscordFilled,
Google: IconBrandGoogle,
};
for (const provider of oauth_providers) {
@@ -180,7 +151,6 @@ export default function Layout({ children, props }) {
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();
@@ -201,7 +171,7 @@ export default function Layout({ children, props }) {
title: `Theme changed to ${friendlyThemeName[value]}`,
message: '',
color: 'green',
icon: <PencilIcon />,
icon: <IconBrush size='1rem' />,
});
};
@@ -222,7 +192,7 @@ export default function Layout({ children, props }) {
title: 'Token Reset Failed',
message: a.error,
color: 'red',
icon: <CrossIcon />,
icon: <IconReload size='1rem' />,
});
} else {
showNotification({
@@ -230,7 +200,7 @@ export default function Layout({ children, props }) {
message:
'Your token has been reset. You will need to update any uploaders to use this new token.',
color: 'green',
icon: <CheckIcon />,
icon: <IconReload size='1rem' />,
});
}
@@ -250,13 +220,29 @@ export default function Layout({ children, props }) {
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 />,
});
if (!navigator.clipboard)
showNotification({
title: 'Unable to copy to clipboard',
message: (
<Text size='sm'>
Zipline is unable to copy to clipboard due to security reasons. However, you can still copy
the token manually.
<br />
<Group position='left' spacing='sm'>
<Text>Your token is:</Text>
<Input size='sm' onFocus={(e) => e.target.select()} type='text' value={token} />
</Group>
</Text>
),
color: 'red',
});
else
showNotification({
title: 'Token Copied',
message: 'Your token has been copied to your clipboard.',
color: 'green',
icon: <IconClipboardCopy size='1rem' />,
});
modals.closeAll();
},
@@ -269,52 +255,55 @@ export default function Layout({ children, props }) {
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>
)}
{items
.filter((x) => (x.if ? x.if(user, props) : true))
.map(({ icon, text, link, children }) =>
children ? (
<NavLink
key={text}
label={text}
icon={icon}
defaultOpened={children.map((x) => x.link).includes(router.pathname)}
>
{children
.filter((x) => (x.if ? x.if(user, props) : true))
.map(({ icon, text, link }) => (
<NavLink
key={text}
label={text}
icon={icon}
active={router.pathname === link}
variant='light'
component={Link}
href={link}
/>
))}
</NavLink>
) : (
<NavLink
key={text}
label={text}
icon={icon}
active={router.pathname === link}
variant='light'
component={Link}
href={link}
/>
)
)}
</Navbar.Section>
<Navbar.Section>
{external_links.length
? external_links.map(({ label, link }, i) => (
<Link href={link} passHref key={i} legacyBehavior>
<NavLink
label={label}
component='a'
target='_blank'
variant='light'
icon={<ExternalLinkIcon />}
/>
</Link>
? external_links.map(({ label, link }, i: number) => (
<NavLink
key={i}
label={label}
target='_blank'
variant='light'
icon={<IconExternalLink size={18} />}
component={Link}
href={link}
/>
))
: null}
</Navbar.Section>
@@ -322,9 +311,11 @@ export default function Layout({ children, props }) {
<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'
version.data.update
? `There is a new ${version.data.updateToType} version: ${
version.data.versions[version.data.updateToType]
}`
: `You are running the latest ${version.data.isUpstream ? 'upstream' : 'stable'} version`
}
>
<Badge
@@ -332,9 +323,9 @@ export default function Layout({ children, props }) {
radius='md'
size='lg'
variant='dot'
color={version.data.local !== version.data.upstream ? 'red' : 'primary'}
color={version.data.update ? 'red' : 'primary'}
>
{version.data.local}
{version.data.versions.current}
</Badge>
</Tooltip>
</Navbar.Section>
@@ -354,99 +345,118 @@ export default function Layout({ children, props }) {
</MediaQuery>
<Title ml='sm'>{title}</Title>
<Box sx={{ marginLeft: 'auto', marginRight: 0 }}>
<Popover position='bottom-end' opened={open} onClose={() => setOpen(false)}>
<Popover.Target>
<Menu
styles={{
item: {
'@media (max-width: 768px)': {
padding: '1rem',
width: '80vw',
},
},
}}
>
<Menu.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',
})}
leftIcon={
avatar ? <Image src={avatar} height={32} radius='md' /> : <IconUserCog size='1rem' />
}
variant='subtle'
color='gray'
compact
size='xl'
p='sm'
>
{user.username}
</Button>
</Popover.Target>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>
{user.username} ({user.id}){' '}
{user.administrator && user.username !== 'administrator' ? '(Administrator)' : ''}
</Menu.Label>
<Menu.Item component={Link} icon={<IconFiles size='1rem' />} href='/dashboard/files'>
Files
</Menu.Item>
<Menu.Item
component={Link}
icon={<IconFileUpload size='1rem' />}
href='/dashboard/upload/file'
>
Upload File
</Menu.Item>
<Menu.Item component={Link} icon={<IconLink size='1rem' />} href='/dashboard/urls'>
Shorten URL
</Menu.Item>
<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) =>
<Menu.Label>Settings</Menu.Label>
<Menu.Item component={Link} icon={<IconSettings size='1rem' />} href='/dashboard/manage'>
Manage Account
</Menu.Item>
<Menu.Item
icon={<IconClipboardCopy size='1rem' />}
onClick={() => {
openCopyToken();
}}
>
Copy Token
</Menu.Item>
<Menu.Item icon={<IconLogout size='1rem' />} component={Link} href='/auth/logout'>
Logout
</Menu.Item>
<Menu.Label>Danger</Menu.Label>
<Menu.Item
icon={<IconBackspace size='1rem' />}
onClick={() => {
openResetToken();
}}
color='red'
>
Reset Token
</Menu.Item>
<Menu.Divider />
<>
{oauth_providers.filter((x) =>
user.oauth?.map(({ provider }) => provider.toLowerCase()).includes(x.name.toLowerCase())
).length ? (
<Menu.Label>Connected Accounts</Menu.Label>
) : null}
{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>
)
.map(({ name, Icon }, i) => (
<>
<Menu.Item
closeMenuOnClick={false}
key={i}
icon={<Icon size={18} colorScheme={theme.colorScheme} />}
>
Logged in with {capitalize(name)}
</Menu.Item>
</>
))}
{oauth_providers.filter((x) =>
user.oauth?.map(({ provider }) => provider.toLowerCase()).includes(x.name.toLowerCase())
).length ? (
<Menu.Divider />
) : null}
</>
<Menu.Item closeMenuOnClick={false} icon={<IconBrush size='1rem' />}>
<Select
size={useMediaQuery('(max-width: 768px)') ? 'md' : 'xs'}
data={Object.keys(themes).map((t) => ({
value: t,
label: friendlyThemeName[t],
}))}
value={systemTheme}
onChange={handleUpdateTheme}
/>
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Box>
</div>
</Header>
@@ -455,9 +465,15 @@ export default function Layout({ children, props }) {
<Paper
withBorder
p='md'
mr='md'
mb='md'
shadow='xs'
sx={(t) => ({
borderColor: t.colorScheme === 'dark' ? t.colors.dark[5] : t.colors.dark[0],
sx={(theme) => ({
'&[data-with-border]': {
border: `${rem(1)} solid ${
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[0]
}`,
},
})}
>
{children}
-5
View File
@@ -1,5 +0,0 @@
import { NextLink } from '@mantine/next';
export default function Link(props) {
return <NextLink legacyBehavior {...props} />;
}
+2 -2
View File
@@ -1,13 +1,13 @@
// https://mantine.dev/core/password-input/
import { Box, PasswordInput, Popover, Progress, Text } from '@mantine/core';
import { IconCheck, IconCross } from '@tabler/icons-react';
import { useState } from 'react';
import { CheckIcon, CrossIcon } from './icons';
function PasswordRequirement({ meets, label }: { meets: boolean; label: string }) {
return (
<Text color={meets ? 'teal' : 'red'} sx={{ display: 'flex', alignItems: 'center' }} mt='sm' size='sm'>
{meets ? <CheckIcon /> : <CrossIcon />} <Box ml='md'>{label}</Box>
{meets ? <IconCheck size='1rem' /> : <IconCross size='1rem' />} <Box ml='md'>{label}</Box>
</Text>
);
}
+3 -3
View File
@@ -1,9 +1,9 @@
import { Card, createStyles, Group, Text } from '@mantine/core';
import { ArrowDownRight, ArrowUpRight } from 'react-feather';
import { IconArrowDownRight, IconArrowUpRight } from '@tabler/icons-react';
const useStyles = createStyles((theme) => ({
root: {
padding: theme.spacing.xl * 1.5,
padding: `calc(${theme.spacing.xl} * 1.5)`,
},
value: {
@@ -57,7 +57,7 @@ export default function StatCard({ stat }: StatsGridProps) {
<>
<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} />}
{stat.diff >= 0 ? <IconArrowUpRight size={16} /> : <IconArrowDownRight size={16} />}
</Text>
</>
)}
+24 -7
View File
@@ -15,10 +15,15 @@ import qogir_dark from 'lib/themes/qogir_dark';
import { createEmotionCache, MantineProvider, MantineThemeOverride } from '@mantine/core';
import { useColorScheme } from '@mantine/hooks';
import { ModalsProvider } from '@mantine/modals';
import { NotificationsProvider } from '@mantine/notifications';
import { Notifications } from '@mantine/notifications';
import { SpotlightProvider } from '@mantine/spotlight';
import { userSelector } from 'lib/recoil/user';
import { useRecoilValue } from 'recoil';
import { createSpotlightActions } from 'lib/spotlight';
import { useRouter } from 'next/router';
import { IconSearch } from '@tabler/icons-react';
export const themes = {
system: (colorScheme: 'dark' | 'light') => (colorScheme === 'dark' ? dark_blue : light_blue),
dark_blue,
@@ -52,6 +57,7 @@ const cache = createEmotionCache({ key: 'zipline' });
export default function ZiplineTheming({ Component, pageProps, ...props }) {
const user = useRecoilValue(userSelector);
const colorScheme = useColorScheme();
const router = useRouter();
let theme: MantineThemeOverride;
@@ -78,7 +84,7 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
components: {
AppShell: {
styles: (t) => ({
root: {
main: {
backgroundColor: t.other.AppShell_backgroundColor,
},
}),
@@ -92,9 +98,15 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
},
Modal: {
defaultProps: {
closeButtonProps: { size: 'lg' },
centered: true,
overlayBlur: 3,
overlayColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
transitionProps: {
exitDuration: 100,
},
overlayProps: {
blur: 6,
color: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
},
},
},
Popover: {
@@ -105,8 +117,8 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
},
LoadingOverlay: {
defaultProps: {
overlayBlur: 3,
overlayColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
overlayOpacity: 0.3,
},
},
Loader: {
@@ -132,9 +144,14 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
}}
>
<ModalsProvider>
<NotificationsProvider>
<SpotlightProvider
searchIcon={<IconSearch size='1rem' />}
shortcut={['mod + k', '/']}
actions={createSpotlightActions(router)}
>
<Notifications position='top-center' style={{ marginTop: -10 }} />
{props.children ? props.children : <Component {...pageProps} />}
</NotificationsProvider>
</SpotlightProvider>
</ModalsProvider>
</MantineProvider>
);
+135 -32
View File
@@ -1,61 +1,157 @@
import { Group, Image, Text } from '@mantine/core';
import { Prism } from '@mantine/prism';
import {
Alert,
Box,
Button,
Card,
Center,
Group,
Image,
LoadingOverlay,
Text,
UnstyledButton,
} from '@mantine/core';
import {
IconFile,
IconFileAlert,
IconFileText,
IconFileUnknown,
IconHeadphones,
IconPhotoCancel,
IconPlayerPlay,
} from '@tabler/icons-react';
import exts from 'lib/exts';
import { useEffect, useState } from 'react';
import { AudioIcon, FileIcon, PlayIcon } from './icons';
import KaTeX from './render/KaTeX';
import Markdown from './render/Markdown';
import PrismCode from './render/PrismCode';
function PlaceholderContent({ text, Icon }) {
return (
<Group sx={(t) => ({ color: t.colors.dark[2] })}>
<Icon size={48} />
<Text size='md'>{text}</Text>
</Group>
);
}
function Placeholder({ text, Icon, ...props }) {
if (props.disableResolve) props.src = null;
if (props.onClick)
return (
<UnstyledButton sx={{ height: 200 }} {...props}>
<Center sx={{ height: 200 }}>
<PlaceholderContent text={text} Icon={Icon} />
</Center>
</UnstyledButton>
);
return (
<Image
height={200}
withPlaceholder
placeholder={
<Group>
<Icon size={48} />
<Text size='md'>{text}</Text>
</Group>
}
{...props}
/>
<Box sx={{ height: 320 }} {...props}>
<Center sx={{ height: 320 }}>
<PlaceholderContent text={text} Icon={Icon} />
</Center>
</Box>
);
}
export default function Type({ file, popup = false, disableMediaPreview, ...props }) {
const type = (file.type || file.mimetype).split('/')[0];
const name = file.name || file.file;
const type =
(file.type ?? file.mimetype) === ''
? file.name.split('.').pop()
: (file.type ?? file.mimetype).split('/')[0];
const media = /^(video|audio|image|text)/.test(type);
const [text, setText] = useState('');
const shouldRenderMarkdown = file.name.endsWith('.md');
const shouldRenderTex = file.name.endsWith('.tex');
const shouldRenderCode: boolean = Object.keys(exts).includes(file.name.split('.').pop());
if (type === 'text') {
const [loading, setLoading] = useState(type === 'text' && popup);
if ((type === 'text' || shouldRenderMarkdown || shouldRenderTex || shouldRenderCode) && popup) {
useEffect(() => {
(async () => {
const res = await fetch('/r/' + name);
const res = await fetch('/r/' + file.name);
const text = await res.text();
setText(text);
setLoading(false);
})();
}, []);
}
if (media && disableMediaPreview) {
const renderAlert = () => {
return (
<Placeholder Icon={FileIcon} text={`Click to view file (${name})`} disableResolve={true} {...props} />
<Alert color='blue' variant='outline' sx={{ width: '100%' }}>
You are{props.overrideRender ? ' not ' : ' '}viewing a rendered version of the file
<Button
mx='md'
onClick={() => props.setOverrideRender(!props.overrideRender)}
compact
variant='light'
>
View {props.overrideRender ? 'rendered' : 'raw'}
</Button>
</Alert>
);
};
if ((shouldRenderMarkdown || shouldRenderTex || shouldRenderCode) && !props.overrideRender && popup)
return (
<>
{renderAlert()}
<Card p='md' my='sm'>
{shouldRenderMarkdown && <Markdown code={text} />}
{shouldRenderTex && <KaTeX code={text} />}
{shouldRenderCode && !(shouldRenderTex || shouldRenderMarkdown) && (
<PrismCode code={text} ext={type} />
)}
</Card>
</>
);
if (media && disableMediaPreview) {
return <Placeholder Icon={IconFile} text={`Click to view file (${file.name})`} {...props} />;
}
if (file.password) {
return (
<Placeholder
Icon={IconFileAlert}
text={`This file is password protected. Click to view file (${file.name})`}
onClick={() => window.open(file.url)}
{...props}
/>
);
}
return popup ? (
media ? (
{
video: <video width='100%' autoPlay controls {...props} />,
image: <Image {...props} />,
audio: <audio autoPlay controls {...props} style={{ width: '100%' }} />,
video: <video width='100%' autoPlay muted controls {...props} />,
image: (
<Image
styles={{
imageWrapper: {
position: 'inherit',
},
}}
placeholder={<PlaceholderContent Icon={IconPhotoCancel} text={'Image failed to load...'} />}
{...props}
/>
),
audio: <audio autoPlay muted controls {...props} style={{ width: '100%' }} />,
text: (
<Prism withLineNumbers language={name.split('.').pop()} {...props} style={{}} sx={{}}>
{text}
</Prism>
<>
{loading ? (
<LoadingOverlay visible={loading} />
) : (
<>
{(shouldRenderMarkdown || shouldRenderTex) && renderAlert()}
<PrismCode code={text} ext={file.name.split('.').pop()} {...props} />
</>
)}
</>
),
}[type]
) : (
@@ -63,12 +159,19 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop
)
) : 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} />,
video: <Placeholder Icon={IconPlayerPlay} text={`Click to view video (${file.name})`} {...props} />,
image: (
<Image
placeholder={<PlaceholderContent Icon={IconPhotoCancel} text={'Image failed to load...'} />}
height={320}
fit='contain'
{...props}
/>
),
audio: <Placeholder Icon={IconHeadphones} text={`Click to view audio (${file.name})`} {...props} />,
text: <Placeholder Icon={IconFileText} text={`Click to view text file (${file.name})`} {...props} />,
}[type]
) : (
<Placeholder Icon={FileIcon} text={`Click to view file (${name})`} {...props} />
<Placeholder Icon={IconFileUnknown} text={`Click to view file (${file.name})`} {...props} />
);
}
+19 -14
View File
@@ -1,21 +1,26 @@
import { Group, Text, useMantineTheme } from '@mantine/core';
import { Box, Group, SimpleGrid, Text } from '@mantine/core';
import { Dropzone as MantineDropzone } from '@mantine/dropzone';
import { ImageIcon } from 'components/icons';
import { IconPhoto } from '@tabler/icons-react';
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} />
<SimpleGrid
cols={2}
breakpoints={[
{ maxWidth: 'md', cols: 1 },
{ maxWidth: 'xs', cols: 1 },
]}
>
<MantineDropzone loading={loading} onDrop={onDrop} styles={{ inner: { pointerEvents: 'none' } }}>
<Group position='center' spacing='xl' style={{ minHeight: 440 }}>
<IconPhoto size={80} />
<Text size='xl' inline>
Drag files here or click to select files
</Text>
</Group>
<div style={{ pointerEvents: 'all' }}>{children}</div>
</MantineDropzone>
<Text size='xl' inline>
Drag files here or click to select files
</Text>
</Group>
</MantineDropzone>
<Box>{children}</Box>
</SimpleGrid>
);
}
+23 -3
View File
@@ -1,4 +1,5 @@
import { Badge, Group, HoverCard, Table, useMantineTheme } from '@mantine/core';
import { ActionIcon, Box, Card, Group, HoverCard, Table, useMantineTheme } from '@mantine/core';
import { IconX } from '@tabler/icons-react';
import Type from 'components/Type';
export function FilePreview({ file }: { file: File }) {
@@ -16,15 +17,34 @@ export function FilePreview({ file }: { file: File }) {
);
}
export default function FileDropzone({ file }: { file: File }) {
export default function FileDropzone({ file, onRemove }: { file: File; onRemove: () => void }) {
const theme = useMantineTheme();
return (
<HoverCard shadow='md'>
<HoverCard.Target>
<Badge size='lg'>{file.name}</Badge>
<Card shadow='sm' radius='sm' p='sm'>
<Group position='center' spacing='xl'>
{file.name}
</Group>
</Card>
</HoverCard.Target>
<HoverCard.Dropdown>
<Box
sx={{
position: 'absolute',
top: 0,
right: 0,
zIndex: 1,
color: theme.colorScheme === 'dark' ? 'white' : 'white',
}}
m='xs'
>
<ActionIcon onClick={onRemove} size='sm' color='red' variant='filled'>
<IconX />
</ActionIcon>
</Box>
<Group grow>
<FilePreview file={file} />
-5
View File
@@ -1,5 +0,0 @@
import { Activity } from 'react-feather';
export default function ActivityIcon({ ...props }) {
return <Activity size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { Disc } from 'react-feather';
export default function AudioIcon({ ...props }) {
return <Disc size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { Calendar } from 'react-feather';
export default function CalendarIcon({ ...props }) {
return <Calendar size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { Check } from 'react-feather';
export default function CheckIcon({ ...props }) {
return <Check size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { Clock } from 'react-feather';
export default function ClockIcon({ ...props }) {
return <Clock size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { Copy } from 'react-feather';
export default function CopyIcon({ ...props }) {
return <Copy size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { X } from 'react-feather';
export default function CrossIcon({ ...props }) {
return <X size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { Delete } from 'react-feather';
export default function DeleteIcon({ ...props }) {
return <Delete size={15} {...props} />;
}
-21
View File
@@ -1,21 +0,0 @@
// https://discord.com/branding
import Image from 'next/image';
export default function DiscordIcon({ ...props }) {
return (
<svg width='24' height='24' viewBox='0 0 71 55' xmlns='http://www.w3.org/2000/svg'>
<g clipPath='url(#clip0)'>
<path
fill={props.colorScheme === 'manage' ? '#ffffff' : '#5865F2'}
d='M60.1045 4.8978C55.5792 2.8214 50.7265 1.2916 45.6527 0.41542C45.5603 0.39851 45.468 0.440769 45.4204 0.525289C44.7963 1.6353 44.105 3.0834 43.6209 4.2216C38.1637 3.4046 32.7345 3.4046 27.3892 4.2216C26.905 3.0581 26.1886 1.6353 25.5617 0.525289C25.5141 0.443589 25.4218 0.40133 25.3294 0.41542C20.2584 1.2888 15.4057 2.8186 10.8776 4.8978C10.8384 4.9147 10.8048 4.9429 10.7825 4.9795C1.57795 18.7309 -0.943561 32.1443 0.293408 45.3914C0.299005 45.4562 0.335386 45.5182 0.385761 45.5576C6.45866 50.0174 12.3413 52.7249 18.1147 54.5195C18.2071 54.5477 18.305 54.5139 18.3638 54.4378C19.7295 52.5728 20.9469 50.6063 21.9907 48.5383C22.0523 48.4172 21.9935 48.2735 21.8676 48.2256C19.9366 47.4931 18.0979 46.6 16.3292 45.5858C16.1893 45.5041 16.1781 45.304 16.3068 45.2082C16.679 44.9293 17.0513 44.6391 17.4067 44.3461C17.471 44.2926 17.5606 44.2813 17.6362 44.3151C29.2558 49.6202 41.8354 49.6202 53.3179 44.3151C53.3935 44.2785 53.4831 44.2898 53.5502 44.3433C53.9057 44.6363 54.2779 44.9293 54.6529 45.2082C54.7816 45.304 54.7732 45.5041 54.6333 45.5858C52.8646 46.6197 51.0259 47.4931 49.0921 48.2228C48.9662 48.2707 48.9102 48.4172 48.9718 48.5383C50.038 50.6034 51.2554 52.5699 52.5959 54.435C52.6519 54.5139 52.7526 54.5477 52.845 54.5195C58.6464 52.7249 64.529 50.0174 70.6019 45.5576C70.6551 45.5182 70.6887 45.459 70.6943 45.3942C72.1747 30.0791 68.2147 16.7757 60.1968 4.9823C60.1772 4.9429 60.1437 4.9147 60.1045 4.8978ZM23.7259 37.3253C20.2276 37.3253 17.3451 34.1136 17.3451 30.1693C17.3451 26.225 20.1717 23.0133 23.7259 23.0133C27.308 23.0133 30.1626 26.2532 30.1066 30.1693C30.1066 34.1136 27.28 37.3253 23.7259 37.3253ZM47.3178 37.3253C43.8196 37.3253 40.9371 34.1136 40.9371 30.1693C40.9371 26.225 43.7636 23.0133 47.3178 23.0133C50.9 23.0133 53.7545 26.2532 53.6986 30.1693C53.6986 34.1136 50.9 37.3253 47.3178 37.3253Z'
/>
</g>
<defs>
<clipPath id='clip0'>
<rect width='71' height='55' fill='white' />
</clipPath>
</defs>
</svg>
);
}
-5
View File
@@ -1,5 +0,0 @@
import { Download } from 'react-feather';
export default function DownloadIcon({ ...props }) {
return <Download size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { LogIn } from 'react-feather';
export default function EnterIcon({ ...props }) {
return <LogIn size={15} {...props} />;
}
@@ -1,5 +0,0 @@
import { ExternalLink } from 'react-feather';
export default function ExternalLinkIcon({ ...props }) {
return <ExternalLink size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { Eye } from 'react-feather';
export default function EyeIcon({ ...props }) {
return <Eye size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { File } from 'react-feather';
export default function FileIcon({ ...props }) {
return <File size={15} {...props} />;
}
-17
View File
@@ -1,17 +0,0 @@
import { GitHub } from 'react-feather';
import Image from 'next/image';
// https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg
export default function GitHubIcon({ colorScheme, ...props }) {
return (
<svg width={24} height={24} viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg' {...props}>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z'
transform='scale(64)'
fill={colorScheme === 'dark' ? '#FFFFFF' : '#1B1F23'}
/>
</svg>
);
}
-15
View File
@@ -1,15 +0,0 @@
// https://developers.google.com/identity/branding-guidelines
import Image from 'next/image';
export default function GoogleIcon({ ...props }) {
return (
<Image
alt='google'
src='https://madeby.google.com/static/images/google_g_logo.svg'
width={24}
height={24}
{...props}
/>
);
}
-5
View File
@@ -1,5 +0,0 @@
import { Hash } from 'react-feather';
export default function HashIcon({ ...props }) {
return <Hash size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { Home } from 'react-feather';
export default function HomeIcon({ ...props }) {
return <Home size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { Image as FeatherImage } from 'react-feather';
export default function ImageIcon({ ...props }) {
return <FeatherImage size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { Link } from 'react-feather';
export default function LinkIcon({ ...props }) {
return <Link size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { LogOut } from 'react-feather';
export default function LogoutIcon({ ...props }) {
return <LogOut size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { Edit2 } from 'react-feather';
export default function PencilIcon({ ...props }) {
return <Edit2 size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { Play } from 'react-feather';
export default function PlayIcon({ ...props }) {
return <Play size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { Plus } from 'react-feather';
export default function PlusIcon({ ...props }) {
return <Plus size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { RefreshCw } from 'react-feather';
export default function RefreshIcon({ ...props }) {
return <RefreshCw size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { Settings } from 'react-feather';
export default function SettingsIcon({ ...props }) {
return <Settings size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { Star } from 'react-feather';
export default function StarIcon({ ...props }) {
return <Star size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { Tag } from 'react-feather';
export default function TagIcon({ ...props }) {
return <Tag size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { Trash2 } from 'react-feather';
export default function TrashIcon({ ...props }) {
return <Trash2 size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { Type } from 'react-feather';
export default function TypeIcon({ ...props }) {
return <Type size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { Upload } from 'react-feather';
export default function UploadIcon({ ...props }) {
return <Upload size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { User } from 'react-feather';
export default function UserIcon({ ...props }) {
return <User size={15} {...props} />;
}
-5
View File
@@ -1,5 +0,0 @@
import { Video } from 'react-feather';
export default function VideoIcon({ ...props }) {
return <Video size={15} {...props} />;
}
+1 -68
View File
@@ -1,71 +1,4 @@
import ActivityIcon from './ActivityIcon';
import CheckIcon from './CheckIcon';
import CopyIcon from './CopyIcon';
import CrossIcon from './CrossIcon';
import DeleteIcon from './DeleteIcon';
import FileIcon from './FileIcon';
import HomeIcon from './HomeIcon';
import LinkIcon from './LinkIcon';
import LogoutIcon from './LogoutIcon';
import PencilIcon from './PencilIcon';
import SettingsIcon from './SettingsIcon';
import TypeIcon from './TypeIcon';
import UploadIcon from './UploadIcon';
import UserIcon from './UserIcon';
import EnterIcon from './EnterIcon';
import PlusIcon from './PlusIcon';
import ImageIcon from './ImageIcon';
import StarIcon from './StarIcon';
import AudioIcon from './AudioIcon';
import VideoIcon from './VideoIcon';
import PlayIcon from './PlayIcon';
import CalendarIcon from './CalendarIcon';
import HashIcon from './HashIcon';
import TagIcon from './TagIcon';
import ClockIcon from './ClockIcon';
import ExternalLinkIcon from './ExternalLinkIcon';
import ShareXIcon from './ShareXIcon';
import DownloadIcon from './DownloadIcon';
import FlameshotIcon from './FlameshotIcon';
import GitHubIcon from './GitHubIcon';
import DiscordIcon from './DiscordIcon';
import GoogleIcon from './GoogleIcon';
import EyeIcon from './EyeIcon';
import RefreshIcon from './RefreshIcon';
export {
ActivityIcon,
CheckIcon,
CopyIcon,
CrossIcon,
DeleteIcon,
FileIcon,
HomeIcon,
LinkIcon,
LogoutIcon,
PencilIcon,
SettingsIcon,
TypeIcon,
UploadIcon,
UserIcon,
EnterIcon,
PlusIcon,
ImageIcon,
StarIcon,
AudioIcon,
VideoIcon,
PlayIcon,
CalendarIcon,
HashIcon,
TagIcon,
ClockIcon,
ExternalLinkIcon,
ShareXIcon,
DownloadIcon,
FlameshotIcon,
GitHubIcon,
DiscordIcon,
GoogleIcon,
EyeIcon,
RefreshIcon,
};
export { ShareXIcon, FlameshotIcon };
@@ -1,11 +1,11 @@
import { Card as MantineCard, Center, Group, SimpleGrid, Skeleton, Title } from '@mantine/core';
import { randomId } from '@mantine/hooks';
import { IconCloudUpload } from '@tabler/icons-react';
import File from 'components/File';
import MutedText from 'components/MutedText';
import { invalidateFiles, useRecent } from 'lib/queries/files';
import { UploadCloud } from 'react-feather';
import { useRecent } from 'lib/queries/files';
export default function RecentFiles({ disableMediaPreview }) {
export default function RecentFiles({ disableMediaPreview, exifEnabled, compress }) {
const recent = useRecent('media');
return (
@@ -22,8 +22,10 @@ export default function RecentFiles({ disableMediaPreview }) {
<File
key={randomId()}
image={image}
updateImages={invalidateFiles}
disableMediaPreview={disableMediaPreview}
exifEnabled={exifEnabled}
refreshImages={recent.refetch}
onDash={compress}
/>
))
) : (
@@ -31,7 +33,7 @@ export default function RecentFiles({ disableMediaPreview }) {
<Center>
<Group>
<div>
<UploadCloud size={48} />
<IconCloudUpload size={48} />
</div>
<div>
<Title>Nothing here</Title>
+9 -10
View File
@@ -1,9 +1,8 @@
import { SimpleGrid } from '@mantine/core';
import { FileIcon } from 'components/icons';
import { IconDatabase, IconEye, IconFile, IconUsers } from '@tabler/icons-react';
import StatCard from 'components/StatCard';
import { useStats } from 'lib/queries/stats';
import { percentChange } from 'lib/utils/client';
import { Database, Eye, Users } from 'react-feather';
export function StatCards() {
const stats = useStats();
@@ -21,10 +20,10 @@ export function StatCards() {
>
<StatCard
stat={{
title: 'UPLOADED FILES',
title: 'FILES',
value: stats.isSuccess ? latest.data.count.toLocaleString() : '...',
desc: 'files have been uploaded',
icon: <FileIcon />,
icon: <IconFile />,
diff:
stats.isSuccess && before?.data ? percentChange(before.data.count, latest.data.count) : undefined,
}}
@@ -34,8 +33,8 @@ export function StatCards() {
stat={{
title: 'STORAGE',
value: stats.isSuccess ? latest.data.size : '...',
desc: 'of storage used',
icon: <Database size={15} />,
desc: 'used',
icon: <IconDatabase />,
diff:
stats.isSuccess && before?.data
? percentChange(before.data.size_num, latest.data.size_num)
@@ -47,8 +46,8 @@ export function StatCards() {
stat={{
title: 'VIEWS',
value: stats.isSuccess ? latest.data.views_count.toLocaleString() : '...',
desc: 'total page views',
icon: <Eye size={15} />,
desc: 'total file views',
icon: <IconEye />,
diff:
stats.isSuccess && before?.data
? percentChange(before.data.views_count, latest.data.views_count)
@@ -60,8 +59,8 @@ export function StatCards() {
stat={{
title: 'USERS',
value: stats.isSuccess ? latest.data.count_users.toLocaleString() : '...',
desc: 'total registered users',
icon: <Users size={15} />,
desc: 'users',
icon: <IconUsers />,
}}
/>
</SimpleGrid>
+198 -94
View File
@@ -1,151 +1,255 @@
import { DataGrid, dateFilterFn, stringFilterFn } from '@dicedtomato/mantine-data-grid';
import { Title, useMantineTheme, Box } from '@mantine/core';
import { ActionIcon, Box, Group, Title, Tooltip } from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { showNotification } from '@mantine/notifications';
import { CopyIcon, CrossIcon, DeleteIcon, EnterIcon } from 'components/icons';
import Link from 'components/Link';
import {
IconClipboardCopy,
IconExternalLink,
IconGridDots,
IconPhotoCancel,
IconPhotoMinus,
IconPhotoUp,
} from '@tabler/icons-react';
import FileModal from 'components/File/FileModal';
import MutedText from 'components/MutedText';
import useFetch from 'lib/hooks/useFetch';
import { useFiles, useRecent } from 'lib/queries/files';
import { usePaginatedFiles, useRecent } from 'lib/queries/files';
import { useStats } from 'lib/queries/stats';
import { userSelector } from 'lib/recoil/user';
import { bytesToHuman } from 'lib/utils/bytes';
import { DataTable, DataTableSortStatus } from 'mantine-datatable';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
import RecentFiles from './RecentFiles';
import { StatCards } from './StatCards';
export default function Dashboard({ disableMediaPreview }) {
export default function Dashboard({ disableMediaPreview, exifEnabled, compress }) {
const user = useRecoilValue(userSelector);
const theme = useMantineTheme();
const images = useFiles();
const recent = useRecent('media');
const stats = useStats();
const clipboard = useClipboard();
const updateImages = () => {
images.refetch();
// pagination
const [, setNumPages] = useState(0);
const [page, setPage] = useState(1);
const [numFiles, setNumFiles] = useState(0);
useEffect(() => {
(async () => {
const { count } = await useFetch('/api/user/paged?count=true');
setNumPages(count);
const { count: filesCount } = await useFetch('/api/user/files?count=true');
setNumFiles(filesCount);
})();
}, [page]);
const files = usePaginatedFiles(page, 'none');
// sorting
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({
columnAccessor: 'date',
direction: 'asc',
});
const [records, setRecords] = useState(files.data);
useEffect(() => {
setRecords(files.data);
}, [files.data]);
useEffect(() => {
if (!records || records.length === 0) return;
const sortedRecords = [...records].sort((a, b) => {
if (sortStatus.direction === 'asc') {
return a[sortStatus.columnAccessor] > b[sortStatus.columnAccessor] ? 1 : -1;
}
return a[sortStatus.columnAccessor] < b[sortStatus.columnAccessor] ? 1 : -1;
});
setRecords(sortedRecords);
}, [sortStatus]);
// file modal on click
const [open, setOpen] = useState(false);
const [selectedFile, setSelectedFile] = useState(null);
const updateFiles = () => {
files.refetch();
recent.refetch();
stats.refetch();
};
const deleteImage = async ({ original }) => {
const deleteFile = async (file) => {
const res = await useFetch('/api/user/files', 'DELETE', {
id: original.id,
id: file.id,
});
if (!res.error) {
updateImages();
updateFiles();
showNotification({
title: 'Image Deleted',
message: '',
title: 'File Deleted',
message: `${file.name}`,
color: 'green',
icon: <DeleteIcon />,
icon: <IconPhotoMinus size='1rem' />,
});
} else {
showNotification({
title: 'Failed to delete image',
title: 'Failed to Delete File',
message: res.error,
color: 'red',
icon: <CrossIcon />,
icon: <IconPhotoCancel size='1rem' />,
});
}
};
const copyImage = async ({ original }) => {
clipboard.copy(`${window.location.protocol}//${window.location.host}${original.url}`);
showNotification({
title: 'Copied to clipboard',
message: '',
icon: <CopyIcon />,
});
const copyFile = async (file) => {
clipboard.copy(`${window.location.protocol}//${window.location.host}${file.url}`);
if (!navigator.clipboard)
showNotification({
title: 'Unable to copy to clipboard',
message: 'Zipline is unable to copy to clipboard due to security reasons.',
color: 'red',
});
else
showNotification({
title: 'Copied to clipboard',
message: (
<a
href={`${window.location.protocol}//${window.location.host}${file.url}`}
>{`${window.location.protocol}//${window.location.host}${file.url}`}</a>
),
icon: <IconClipboardCopy size='1rem' />,
});
};
const viewImage = async ({ original }) => {
window.open(`${window.location.protocol}//${window.location.host}${original.url}`);
const viewFile = async (file) => {
window.open(`${window.location.protocol}//${window.location.host}${file.url}`);
};
return (
<div>
{selectedFile && (
<FileModal
open={open}
setOpen={setOpen}
file={selectedFile}
loading={files.isLoading}
refresh={() => files.refetch()}
reducedActions={false}
exifEnabled={exifEnabled}
compress={compress}
/>
)}
<Title>Welcome back, {user?.username}</Title>
<MutedText size='md'>
You have <b>{images.isSuccess ? images.data.length : '...'}</b> files
You have <b>{numFiles === 0 ? '...' : numFiles}</b> files
</MutedText>
<StatCards />
<RecentFiles disableMediaPreview={disableMediaPreview} />
<RecentFiles disableMediaPreview={disableMediaPreview} exifEnabled={exifEnabled} compress={compress} />
<Box my='sm'>
<Title>Files</Title>
<MutedText size='md'>
View your gallery <Link href='/dashboard/files'>here</Link>.
</MutedText>
<DataGrid
data={images.data ?? []}
loading={images.isLoading}
withPagination={true}
withColumnResizing={false}
withColumnFilters={true}
noEllipsis={true}
withSorting={true}
highlightOnHover={true}
CopyIcon={CopyIcon}
DeleteIcon={DeleteIcon}
EnterIcon={EnterIcon}
deleteImage={deleteImage}
copyImage={copyImage}
viewImage={viewImage}
styles={{
dataCell: {
width: '100%',
},
td: {
':nth-child(1)': {
minWidth: 170,
},
':nth-child(2)': {
minWidth: 100,
},
},
th: {
':nth-child(1)': {
minWidth: 170,
padding: theme.spacing.lg,
borderTopLeftRadius: theme.radius.sm,
},
':nth-child(2)': {
minWidth: 100,
padding: theme.spacing.lg,
},
':nth-child(3)': {
padding: theme.spacing.lg,
},
':nth-child(4)': {
padding: theme.spacing.lg,
borderTopRightRadius: theme.radius.sm,
},
},
thead: {
backgroundColor: theme.colors.dark[6],
},
}}
empty={<></>}
<Group mb='md'>
<Title>Files</Title>
<Tooltip label='View Gallery'>
<ActionIcon variant='filled' color='primary' component={Link} href='/dashboard/files'>
<IconGridDots size='1rem' />
</ActionIcon>
</Tooltip>
</Group>
<DataTable
withBorder
borderRadius='md'
highlightOnHover
verticalSpacing='sm'
columns={[
{ accessor: 'name', sortable: true },
{ accessor: 'mimetype', sortable: true },
{ accessor: 'size', sortable: true, render: (file) => bytesToHuman(file.size) },
{
accessorKey: 'file',
header: 'Name',
filterFn: stringFilterFn,
accessor: 'createdAt',
sortable: true,
render: (file) => new Date(file.createdAt).toLocaleString(),
},
{
accessorKey: 'mimetype',
header: 'Type',
filterFn: stringFilterFn,
},
{
accessorKey: 'created_at',
header: 'Date',
filterFn: dateFilterFn,
accessor: 'actions',
textAlignment: 'right',
render: (file) => (
<Group spacing={4} position='right' noWrap>
<Tooltip label='More details'>
<ActionIcon
onClick={() => {
setSelectedFile(file);
setOpen(true);
}}
color='blue'
>
<IconPhotoUp size='1rem' />
</ActionIcon>
</Tooltip>
<Tooltip label='Open file in new tab'>
<ActionIcon onClick={() => viewFile(file)} color='blue'>
<IconExternalLink size='1rem' />
</ActionIcon>
</Tooltip>
<ActionIcon onClick={() => copyFile(file)} color='green'>
<IconClipboardCopy size='1rem' />
</ActionIcon>
<ActionIcon onClick={() => deleteFile(file)} color='red'>
<IconPhotoMinus size='1rem' />
</ActionIcon>
</Group>
),
},
]}
records={records ?? []}
fetching={files.isLoading}
loaderBackgroundBlur={5}
loaderVariant='dots'
minHeight={620}
page={page}
onPageChange={setPage}
recordsPerPage={16}
totalRecords={numFiles}
sortStatus={sortStatus}
onSortStatusChange={setSortStatus}
rowContextMenu={{
shadow: 'xl',
borderRadius: 'md',
items: (file) => [
{
key: 'view',
icon: <IconExternalLink size='1rem' />,
title: `View ${file.name}`,
onClick: () => viewFile(file),
},
{
key: 'copy',
icon: <IconClipboardCopy size='1rem' />,
title: `Copy ${file.name}`,
onClick: () => copyFile(file),
},
{
key: 'delete',
icon: <IconPhotoMinus size='1rem' />,
title: `Delete ${file.name}`,
onClick: () => deleteFile(file),
},
],
}}
onCellClick={({ column, record: file }) => {
if (column.accessor === 'actions') return;
setSelectedFile(file);
setOpen(true);
}}
/>
</Box>
</div>
+58 -18
View File
@@ -1,28 +1,64 @@
import { Box, Center, Checkbox, Group, Pagination, SimpleGrid, Skeleton, Title } from '@mantine/core';
import { Box, Button, Center, Checkbox, Group, Pagination, SimpleGrid, Skeleton, Title } from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks';
import { IconFile } from '@tabler/icons-react';
import File from 'components/File';
import { FileIcon } from 'components/icons';
import MutedText from 'components/MutedText';
import useFetch from 'hooks/useFetch';
import { usePaginatedFiles } from 'lib/queries/files';
import { useState } from 'react';
import { showNonMediaSelector } from 'lib/recoil/settings';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { useRecoilState } from 'recoil';
export default function FilePagation({ disableMediaPreview }) {
const [checked, setChecked] = useState(false);
export default function FilePagation({ disableMediaPreview, exifEnabled, queryPage, compress }) {
const [checked, setChecked] = useRecoilState(showNonMediaSelector);
const [numPages, setNumPages] = useState(Number(queryPage)); // just set it to the queryPage, since the req may have not loaded yet
const [page, setPage] = useState(Number(queryPage));
const pages = usePaginatedFiles(!checked ? { filter: 'media' } : {});
const [page, setPage] = useState(1);
const isMobile = useMediaQuery('(max-width: 768px)');
const router = useRouter();
useEffect(() => {
(async () => {
router.replace(
{
query: {
...router.query,
page: page,
},
},
undefined,
{ shallow: true }
);
const { count } = await useFetch(`/api/user/paged?count=true${!checked ? '&filter=media' : ''}`);
setNumPages(count);
})();
}, [page]);
const pages = usePaginatedFiles(page, !checked ? 'media' : null);
if (pages.isSuccess && pages.data.length === 0) {
return (
<Center>
<Center sx={{ flexDirection: 'column' }}>
<Group>
<div>
<FileIcon size={48} />
<IconFile size={48} />
</div>
<div>
<Title>Nothing here</Title>
<MutedText size='md'>Upload some files and they will show up here.</MutedText>
</div>
</Group>
<Box my='sm' hidden={checked}>
<MutedText size='md'>
There might be some non-media files, would you like to show them?
<Button mx='sm' compact type='button' onClick={() => setChecked(true)}>
Show
</Button>
</MutedText>
</Box>
</Center>
);
}
@@ -32,12 +68,14 @@ export default function FilePagation({ disableMediaPreview }) {
<SimpleGrid cols={3} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
{pages.isSuccess
? pages.data.length
? pages.data[page - 1 ?? 0].map((image) => (
? pages.data.map((image) => (
<div key={image.id}>
<File
image={image}
updateImages={() => pages.refetch()}
disableMediaPreview={disableMediaPreview}
exifEnabled={exifEnabled}
refreshImages={pages.refetch}
onDash={compress}
/>
</div>
))
@@ -58,13 +96,15 @@ export default function FilePagation({ disableMediaPreview }) {
paddingBottom: 3,
}}
>
<div></div>
<Pagination total={pages.data?.length ?? 0} page={page} onChange={setPage} />
<Checkbox
label='Show non-media files'
checked={checked}
onChange={(event) => setChecked(event.currentTarget.checked)}
/>
{!isMobile && <div></div>}
<Pagination total={numPages} value={page} onChange={setPage} withEdges />
{!isMobile && (
<Checkbox
label='Show non-media files'
checked={checked}
onChange={(event) => setChecked(event.currentTarget.checked)}
/>
)}
</Box>
) : null}
</>
@@ -0,0 +1,118 @@
import { Button, Modal, Title, Tooltip } from '@mantine/core';
import { IconTrash } from '@tabler/icons-react';
import AnchorNext from 'components/AnchorNext';
import MutedText from 'components/MutedText';
import useFetch from 'hooks/useFetch';
import { DataTable } from 'mantine-datatable';
import { useEffect, useState } from 'react';
export type PendingFiles = {
id: number;
createdAt: string;
status: string;
chunks: number;
chunksComplete: number;
userId: number;
data: {
file: {
filename: string;
mimetype: string;
lastchunk: boolean;
identifier: string;
totalBytes: number;
};
code?: number;
message?: string;
};
};
export default function PendingFilesModal({ open, onClose }) {
const [incFiles, setIncFiles] = useState<PendingFiles[]>([]);
const [loading, setLoading] = useState(true);
const [selectedFiles, setSelectedFiles] = useState<PendingFiles[]>([]);
async function updateIncFiles() {
setLoading(true);
const files = await useFetch('/api/user/pending');
setIncFiles(files);
setLoading(false);
}
async function deleteIncFiles() {
await useFetch('/api/user/pending', 'DELETE', {
id: selectedFiles.map((file) => file.id),
});
updateIncFiles();
setSelectedFiles([]);
}
useEffect(() => {
updateIncFiles();
}, []);
useEffect(() => {
const interval = setInterval(() => {
if (open) updateIncFiles();
}, 5000);
return () => clearInterval(interval);
}, [open]);
return (
<Modal title={<Title>Pending Files</Title>} size='auto' opened={open} onClose={onClose}>
<MutedText size='xs'>Refreshing every 5 seconds...</MutedText>
<DataTable
withBorder
borderRadius='md'
highlightOnHover
verticalSpacing='sm'
minHeight={200}
records={incFiles ?? []}
columns={[
{ accessor: 'id', title: 'ID' },
{ accessor: 'createdAt', render: (file) => new Date(file.createdAt).toLocaleString() },
{ accessor: 'status', render: (file) => file.status.toLowerCase() },
{
accessor: 'progress',
title: 'Progress',
render: (file) => `${file.chunksComplete}/${file.chunks} chunks`,
},
{
accessor: 'message',
render: (file) =>
file.data.code === 200 ? (
<AnchorNext href={file.data.message} target='_blank'>
view file
</AnchorNext>
) : (
file.data.message
),
},
]}
fetching={loading}
loaderBackgroundBlur={5}
loaderVariant='dots'
onSelectedRecordsChange={setSelectedFiles}
selectedRecords={selectedFiles}
/>
{selectedFiles.length ? (
<Tooltip label='Clearing pending files will still leave the final file on the server.'>
<Button
variant='filled'
my='md'
color='red'
onClick={deleteIncFiles}
leftIcon={<IconTrash size='1rem' />}
fullWidth
>
Clear {selectedFiles.length} pending file{selectedFiles.length > 1 ? 's' : ''}
</Button>
</Tooltip>
) : null}
</Modal>
);
}
+36 -24
View File
@@ -1,33 +1,42 @@
import { Accordion, ActionIcon, Box, Group, Pagination, SimpleGrid, Title } from '@mantine/core';
import { Accordion, ActionIcon, Box, Group, Pagination, SimpleGrid, Title, Tooltip } from '@mantine/core';
import { IconFileUpload, IconPhotoUp } from '@tabler/icons-react';
import File from 'components/File';
import { PlusIcon } from 'components/icons';
import useFetch from 'hooks/useFetch';
import { usePaginatedFiles } from 'lib/queries/files';
import Link from 'next/link';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import FilePagation from './FilePagation';
import PendingFilesModal from './PendingFilesModal';
export default function Files({ disableMediaPreview }) {
const pages = usePaginatedFiles({ filter: 'media' });
const favoritePages = usePaginatedFiles({ favorite: 'media' });
export default function Files({ disableMediaPreview, exifEnabled, queryPage, compress }) {
const [favoritePage, setFavoritePage] = useState(1);
const [favoriteNumPages, setFavoriteNumPages] = useState(0);
const favoritePages = usePaginatedFiles(favoritePage, 'media', true);
const updatePages = async (favorite) => {
pages.refetch();
const [open, setOpen] = useState(false);
if (favorite) {
favoritePages.refetch();
}
};
useEffect(() => {
(async () => {
const { count } = await useFetch('/api/user/paged?count=true&filter=media&favorite=true');
setFavoriteNumPages(count);
})();
});
return (
<>
<PendingFilesModal open={open} onClose={() => setOpen(false)} />
<Group mb='md'>
<Title>Files</Title>
<Link href='/dashboard/upload' passHref legacyBehavior>
<ActionIcon component='a' variant='filled' color='primary'>
<PlusIcon />
<ActionIcon component={Link} href='/dashboard/upload/file' variant='filled' color='primary'>
<IconFileUpload size='1rem' />
</ActionIcon>
<Tooltip label='View pending uploads'>
<ActionIcon onClick={() => setOpen(true)} variant='filled' color='primary'>
<IconPhotoUp size='1rem' />
</ActionIcon>
</Link>
</Tooltip>
</Group>
{favoritePages.isSuccess && favoritePages.data.length ? (
<Accordion
@@ -43,12 +52,14 @@ export default function Files({ disableMediaPreview }) {
<Accordion.Panel>
<SimpleGrid cols={3} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
{favoritePages.isSuccess && favoritePages.data.length
? favoritePages.data[favoritePage - 1 ?? 0].map((image) => (
? favoritePages.data.map((image) => (
<div key={image.id}>
<File
image={image}
updateImages={() => updatePages(true)}
disableMediaPreview={disableMediaPreview}
exifEnabled={exifEnabled}
refreshImages={favoritePages.refetch}
onDash={compress}
/>
</div>
))
@@ -63,18 +74,19 @@ export default function Files({ disableMediaPreview }) {
paddingBottom: 3,
}}
>
<Pagination
total={favoritePages.data.length}
page={favoritePage}
onChange={setFavoritePage}
/>
<Pagination total={favoriteNumPages} value={favoritePage} onChange={setFavoritePage} />
</Box>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
) : null}
<FilePagation disableMediaPreview={disableMediaPreview} />
<FilePagation
disableMediaPreview={disableMediaPreview}
exifEnabled={exifEnabled}
queryPage={queryPage}
compress={compress}
/>
</>
);
}
@@ -0,0 +1,67 @@
import { Button, Group, Modal, TextInput, Title } from '@mantine/core';
import { useForm } from '@mantine/form';
import { showNotification } from '@mantine/notifications';
import { IconFolderPlus, IconFolderX } from '@tabler/icons-react';
import MutedText from 'components/MutedText';
import useFetch from 'hooks/useFetch';
import { useRouter } from 'next/router';
export default function CreateFolderModal({ open, setOpen, updateFolders, createWithFile }) {
const router = useRouter();
const form = useForm({
initialValues: {
name: '',
},
});
const onSubmit = async (values) => {
const res = await useFetch('/api/user/folders', 'POST', {
name: values.name,
add: createWithFile ? [createWithFile] : undefined,
});
if (res.error) {
showNotification({
title: 'Failed to create folder',
message: res.error,
icon: <IconFolderX size='1rem' />,
color: 'red',
});
} else {
showNotification({
title: 'Created folder ' + res.name,
message: createWithFile ? 'Added file to folder' : undefined,
icon: <IconFolderPlus size='1rem' />,
color: 'green',
});
if (createWithFile) {
router.push('/dashboard/folders');
}
}
setOpen(false);
updateFolders();
form.setValues({ name: '' });
};
return (
<Modal opened={open} onClose={() => setOpen(false)} title={<Title>Create Folder</Title>}>
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
<TextInput label='Folder Name' placeholder='Folder Name' {...form.getInputProps('name')} />
{createWithFile && (
<MutedText size='sm'>
Creating this folder will add file with an ID of <b>{createWithFile}</b> to it automatically.
</MutedText>
)}
<Group position='right' mt='md'>
<Button onClick={() => setOpen(false)}>Cancel</Button>
<Button type='submit'>Create</Button>
</Group>
</form>
</Modal>
);
}
@@ -0,0 +1,54 @@
import { Center, Modal, SimpleGrid, Stack, Text, Title } from '@mantine/core';
import File from 'components/File';
import MutedText from 'components/MutedText';
import { useFolder } from 'lib/queries/folders';
export default function ViewFolderFilesModal({
open,
setOpen,
folderId,
disableMediaPreview,
exifEnabled,
compress,
}) {
if (!folderId) return null;
const folder = useFolder(folderId, true);
return (
<Modal
opened={open}
onClose={() => setOpen(false)}
title={<Title>View {folder.data?.name}&apos;s files</Title>}
size='xl'
>
{folder.isSuccess ? (
<>
{folder.data.files.length ? (
<SimpleGrid cols={3} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
{folder.data.files.map((file) => (
<File
disableMediaPreview={disableMediaPreview}
key={file.id}
image={file}
exifEnabled={exifEnabled}
refreshImages={folder.refetch}
onDash={compress}
/>
))}
</SimpleGrid>
) : (
<Center>
<Stack>
<Text align='center'>No files in this folder</Text>
<MutedText size='sm'>
Add files to {folder.data.name} by clicking a file in the Files tab and selecting a folder
</MutedText>
</Stack>
</Center>
)}
</>
) : null}
</Modal>
);
}
+410
View File
@@ -0,0 +1,410 @@
import { ActionIcon, Avatar, Card, Group, SimpleGrid, Skeleton, Stack, Title, Tooltip } from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { useModals } from '@mantine/modals';
import { showNotification } from '@mantine/notifications';
import {
IconClipboardCheck,
IconClipboardCopy,
IconExternalLink,
IconFiles,
IconFolderCancel,
IconFolderMinus,
IconFolderPlus,
IconFolderShare,
IconFolderX,
IconGridDots,
IconList,
IconLock,
IconLockCancel,
IconLockOpen,
} from '@tabler/icons-react';
import AnchorNext from 'components/AnchorNext';
import MutedText from 'components/MutedText';
import useFetch from 'hooks/useFetch';
import { useFolders } from 'lib/queries/folders';
import { listViewFoldersSelector } from 'lib/recoil/settings';
import { relativeTime } from 'lib/utils/client';
import { DataTable, DataTableSortStatus } from 'mantine-datatable';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { useRecoilState } from 'recoil';
import CreateFolderModal from './CreateFolderModal';
import ViewFolderFilesModal from './ViewFolderFilesModal';
export default function Folders({ disableMediaPreview, exifEnabled, compress }) {
const folders = useFolders();
const [createOpen, setCreateOpen] = useState(false);
const [createWithFile, setCreateWithFile] = useState(null);
const [viewOpen, setViewOpen] = useState(false);
const [activeFolderId, setActiveFolderId] = useState(null);
const modals = useModals();
const clipboard = useClipboard();
const router = useRouter();
const [listView, setListView] = useRecoilState(listViewFoldersSelector);
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({
columnAccessor: 'updatedAt',
direction: 'desc',
});
const [records, setRecords] = useState(folders.data);
useEffect(() => {
setRecords(folders.data);
}, [folders.data]);
useEffect(() => {
if (!records || records.length === 0) return;
const sortedRecords = [...records].sort((a, b) => {
if (sortStatus.direction === 'asc') {
return a[sortStatus.columnAccessor] > b[sortStatus.columnAccessor] ? 1 : -1;
}
return a[sortStatus.columnAccessor] < b[sortStatus.columnAccessor] ? 1 : -1;
});
setRecords(sortedRecords);
}, [sortStatus]);
useEffect(() => {
if (router.query.create) {
setCreateOpen(true);
setCreateWithFile(router.query.create);
}
}, []);
const deleteFolder = (folder) => {
modals.openConfirmModal({
title: <Title>Delete folder {folder.name}?</Title>,
children:
'Are you sure you want to delete this folder? All files within the folder will still exist, but will no longer be in a folder.',
labels: {
confirm: 'Delete',
cancel: 'Cancel',
},
onConfirm: async () => {
const res = await useFetch(`/api/user/folders/${folder.id}`, 'DELETE', {
deleteFolder: true,
});
if (!res.error) {
showNotification({
title: 'Deleted folder',
message: `Deleted folder ${folder.name}`,
color: 'green',
icon: <IconFolderMinus size='1rem' />,
});
folders.refetch();
} else {
showNotification({
title: 'Failed to delete folder',
message: res.error,
color: 'red',
icon: <IconFolderCancel size='1rem' />,
});
folders.refetch();
}
},
});
};
const makePublic = async (folder) => {
const res = await useFetch(`/api/user/folders/${folder.id}`, 'PATCH', {
public: folder.public ? false : true,
});
if (!res.error) {
showNotification({
title: 'Made folder public',
message: `Made folder ${folder.name} ${folder.public ? 'private' : 'public'}`,
color: 'green',
icon: <IconLockOpen size='1rem' />,
});
folders.refetch();
} else {
showNotification({
title: 'Failed to make folder public/private',
message: res.error,
color: 'red',
icon: <IconLockCancel size='1rem' />,
});
folders.refetch();
}
};
return (
<>
<CreateFolderModal
open={createOpen}
setOpen={setCreateOpen}
createWithFile={createWithFile}
updateFolders={folders.refetch}
/>
<ViewFolderFilesModal
open={viewOpen}
setOpen={setViewOpen}
folderId={activeFolderId}
disableMediaPreview={disableMediaPreview}
exifEnabled={exifEnabled}
compress={compress}
/>
<Group mb='md'>
<Title>Folders</Title>
<ActionIcon onClick={() => setCreateOpen(!createOpen)} component='a' variant='filled' color='primary'>
<IconFolderPlus size='1rem' />
</ActionIcon>
<Tooltip label={listView ? 'Switch to grid view' : 'Switch to list view'}>
<ActionIcon variant='filled' color='primary' onClick={() => setListView(!listView)}>
{listView ? <IconList size='1rem' /> : <IconGridDots size='1rem' />}
</ActionIcon>
</Tooltip>
</Group>
{listView ? (
<DataTable
withBorder
borderRadius='md'
highlightOnHover
verticalSpacing='sm'
columns={[
{ accessor: 'id', title: 'ID', sortable: true },
{ accessor: 'name', sortable: true },
{
accessor: 'public',
sortable: true,
render: (folder) => (folder.public ? 'Public' : 'Private'),
},
{
accessor: 'createdAt',
title: 'Created',
sortable: true,
render: (folder) => new Date(folder.createdAt).toLocaleString(),
},
{
accessor: 'updatedAt',
title: 'Last updated',
sortable: true,
render: (folder) => new Date(folder.updatedAt).toLocaleString(),
},
{
accessor: 'actions',
textAlignment: 'right',
render: (folder) => (
<Group spacing={4} position='right' noWrap>
<Tooltip label='View files in folder'>
<ActionIcon
onClick={() => {
setViewOpen(true);
setActiveFolderId(folder.id);
}}
variant='subtle'
color='primary'
>
<IconFiles size='1rem' />
</ActionIcon>
</Tooltip>
<Tooltip label={folder.public ? 'Make folder private' : 'Make folder public'}>
<ActionIcon onClick={() => makePublic(folder)} variant='subtle' color='primary'>
{folder.public ? <IconLockOpen size='1rem' /> : <IconLock size='1rem' />}
</ActionIcon>
</Tooltip>
<Tooltip label='Open folder in new tab'>
<ActionIcon
onClick={() => window.open(`/folder/${folder.id}`, '_blank')}
variant='subtle'
color='primary'
>
<IconFolderShare size='1rem' />
</ActionIcon>
</Tooltip>
<Tooltip label='Copy folder link'>
<ActionIcon
onClick={() => {
clipboard.copy(`${window.location.origin}/folder/${folder.id}`);
showNotification({
title: 'Copied folder link',
message: 'Copied folder link to clipboard',
color: 'green',
icon: <IconClipboardCheck size='1rem' />,
});
}}
variant='subtle'
color='primary'
>
<IconClipboardCopy size='1rem' />
</ActionIcon>
</Tooltip>
<Tooltip label='Delete folder'>
<ActionIcon onClick={() => deleteFolder(folder)} variant='subtle' color='red'>
<IconFolderX size='1rem' />
</ActionIcon>
</Tooltip>
</Group>
),
},
]}
sortStatus={sortStatus}
onSortStatusChange={setSortStatus}
records={records ?? []}
fetching={folders.isLoading}
loaderBackgroundBlur={5}
minHeight='calc(100vh - 200px)'
loaderVariant='dots'
rowContextMenu={{
shadow: 'xl',
borderRadius: 'md',
items: (folder) => [
{
key: 'viewFiles',
title: 'View files in folder',
icon: <IconFiles size='1rem' />,
onClick: () => {
setViewOpen(true);
setActiveFolderId(folder.id);
},
},
{
key: 'makePublic',
title: folder.public ? 'Make folder private' : 'Make folder public',
icon: folder.public ? <IconLockOpen size='1rem' /> : <IconLock size='1rem' />,
onClick: () => makePublic(folder),
},
{
key: 'openFolder',
title: 'Open folder in a new tab',
icon: <IconExternalLink size='1rem' />,
onClick: () => window.open(`/folder/${folder.id}`, '_blank'),
},
{
key: 'copyLink',
title: 'Copy folder link to clipboard',
icon: <IconClipboardCopy size='1rem' />,
onClick: () => {
clipboard.copy(`${window.location.origin}/folder/${folder.id}`);
},
},
{
key: 'deleteFolder',
title: 'Delete folder',
icon: <IconFolderX size='1rem' />,
onClick: () => deleteFolder(folder),
},
],
}}
onCellClick={({ column, record: folder }) => {
if (column.accessor === 'actions') return;
setViewOpen(true);
setActiveFolderId(folder.id);
}}
/>
) : (
<SimpleGrid cols={3} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
{folders.isSuccess
? folders.data.length
? folders.data.map((folder) => (
<Card key={folder.id}>
<Group position='apart'>
<Group position='left'>
<Avatar size='lg' color='primary'>
{folder.id}
</Avatar>
<Stack spacing={0}>
<Title>{folder.name}</Title>
<MutedText size='sm'>ID: {folder.id}</MutedText>
<MutedText size='sm'>Public: {folder.public ? 'Yes' : 'No'}</MutedText>
<Tooltip label={new Date(folder.createdAt).toLocaleString()}>
<div>
<MutedText size='sm'>
Created {relativeTime(new Date(folder.createdAt))}
</MutedText>
</div>
</Tooltip>
<Tooltip label={new Date(folder.updatedAt).toLocaleString()}>
<div>
<MutedText size='sm'>
Last updated {relativeTime(new Date(folder.updatedAt))}
</MutedText>
</div>
</Tooltip>
</Stack>
</Group>
<Group>
<Stack>
<Tooltip label={folder.public ? 'Make folder private' : 'Make folder public'}>
<ActionIcon
aria-label={folder.public ? 'make private' : 'make public'}
onClick={() => makePublic(folder)}
>
{folder.public ? <IconLock size='1rem' /> : <IconLockOpen size='1rem' />}
</ActionIcon>
</Tooltip>
<Tooltip label='Delete folder'>
<ActionIcon aria-label='delete' onClick={() => deleteFolder(folder)}>
<IconFolderMinus size='1rem' />
</ActionIcon>
</Tooltip>
</Stack>
<Stack>
<ActionIcon
aria-label='view files'
onClick={() => {
setViewOpen(!viewOpen);
setActiveFolderId(folder.id);
}}
>
<IconFiles size='1rem' />
</ActionIcon>
<ActionIcon
aria-label='copy link'
onClick={() => {
clipboard.copy(`${window.location.origin}/folder/${folder.id}`);
if (!navigator.clipboard)
showNotification({
title: 'Unable to copy to clipboard',
message: 'Zipline is unable to copy to clipboard due to security reasons.',
color: 'red',
});
else
showNotification({
title: 'Copied folder link',
message: (
<>
Copied{' '}
<AnchorNext href={`/folder/${folder.id}`}>folder link</AnchorNext> to
clipboard
</>
),
color: 'green',
icon: <IconClipboardCopy size='1rem' />,
});
}}
>
<IconClipboardCopy size='1rem' />
</ActionIcon>
<ActionIcon
aria-label='open in new tab'
onClick={() => window.open(`/folder/${folder.id}`)}
>
<IconFolderShare size='1rem' />
</ActionIcon>
</Stack>
</Group>
</Group>
</Card>
))
: null
: [1, 2, 3, 4].map((x) => (
<div key={x}>
<Skeleton width='100%' height={220} sx={{ borderRadius: 1 }} />
</div>
))}
</SimpleGrid>
)}
</>
);
}
+187 -54
View File
@@ -17,12 +17,24 @@ import { useForm } from '@mantine/form';
import { useClipboard } from '@mantine/hooks';
import { useModals } from '@mantine/modals';
import { showNotification } from '@mantine/notifications';
import { CopyIcon, CrossIcon, DeleteIcon, PlusIcon, TagIcon } from 'components/icons';
import type { Invite } from '@prisma/client';
import {
IconClipboardCopy,
IconGridDots,
IconList,
IconPlus,
IconTag,
IconTagOff,
IconTrash,
} from '@tabler/icons-react';
import MutedText from 'components/MutedText';
import useFetch from 'hooks/useFetch';
import { listViewInvitesSelector } from 'lib/recoil/settings';
import { expireText, relativeTime } from 'lib/utils/client';
import { DataTable, DataTableSortStatus } from 'mantine-datatable';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { useRecoilState } from 'recoil';
const expires = ['30m', '1h', '6h', '12h', '1d', '3d', '5d', '7d', 'never'];
@@ -38,7 +50,7 @@ function CreateInviteModal({ open, setOpen, updateInvites }) {
if (!expires.includes(values.expires)) return form.setFieldError('expires', 'Invalid expiration');
if (values.count < 1 || values.count > 100)
return form.setFieldError('count', 'Must be between 1 and 100');
const expires_at =
const expiresAt =
values.expires === 'never'
? null
: new Date(
@@ -57,7 +69,7 @@ function CreateInviteModal({ open, setOpen, updateInvites }) {
setOpen(false);
const res = await useFetch('/api/auth/invite', 'POST', {
expires_at,
expiresAt,
count: values.count,
});
@@ -65,14 +77,14 @@ function CreateInviteModal({ open, setOpen, updateInvites }) {
showNotification({
title: 'Failed to create invite',
message: res.error,
icon: <CrossIcon />,
icon: <IconTagOff size='1rem' />,
color: 'red',
});
} else {
showNotification({
title: 'Created invite',
message: '',
icon: <TagIcon />,
icon: <IconTag size='1rem' />,
color: 'green',
});
}
@@ -108,7 +120,7 @@ function CreateInviteModal({ open, setOpen, updateInvites }) {
min={1}
stepHoldDelay={200}
stepHoldInterval={100}
parser={(v: string) => Number(v.replace(/[^\d]/g, ''))}
parser={(v: string) => v.replace(/[^\d]/g, '')}
/>
<Group position='right' mt='md'>
@@ -125,29 +137,56 @@ export default function Invites() {
const modals = useModals();
const clipboard = useClipboard();
const [invites, setInvites] = useState([]);
const [invites, setInvites] = useState<Invite[]>([]);
const [open, setOpen] = useState(false);
const [ok, setOk] = useState(false);
const [listView, setListView] = useRecoilState(listViewInvitesSelector);
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({
columnAccessor: 'createdAt',
direction: 'asc',
});
const [records, setRecords] = useState(invites);
useEffect(() => {
setRecords(invites);
}, [invites]);
useEffect(() => {
if (!records || records.length === 0) return;
const sortedRecords = [...records].sort((a, b) => {
if (sortStatus.direction === 'asc') {
return a[sortStatus.columnAccessor] > b[sortStatus.columnAccessor] ? 1 : -1;
}
return a[sortStatus.columnAccessor] < b[sortStatus.columnAccessor] ? 1 : -1;
});
setRecords(sortedRecords);
}, [sortStatus]);
const openDeleteModal = (invite) =>
modals.openConfirmModal({
title: `Delete ${invite.code}?`,
centered: true,
overlayBlur: 3,
overlayProps: { blur: 3 },
labels: { confirm: 'Yes', cancel: 'No' },
onConfirm: async () => {
const res = await useFetch(`/api/auth/invite?code=${invite.code}`, 'DELETE');
if (res.error) {
showNotification({
title: 'Failed to delete invite ${invite.code}',
title: `Failed to delete invite ${invite.code}`,
message: res.error,
icon: <CrossIcon />,
icon: <IconTagOff size='1rem' />,
color: 'red',
});
} else {
showNotification({
title: `Deleted invite ${invite.code}`,
message: '',
icon: <DeleteIcon />,
icon: <IconTag size='1rem' />,
color: 'green',
});
}
@@ -158,17 +197,25 @@ export default function Invites() {
const handleCopy = async (invite) => {
clipboard.copy(`${window.location.protocol}//${window.location.host}/auth/register?code=${invite.code}`);
showNotification({
title: 'Copied to clipboard',
message: '',
icon: <CopyIcon />,
});
if (!navigator.clipboard)
showNotification({
title: 'Unable to copy to clipboard',
message: 'Zipline is unable to copy to clipboard due to security reasons.',
color: 'red',
});
else
showNotification({
title: 'Copied to clipboard',
message: '',
icon: <IconClipboardCopy size='1rem' />,
});
};
const updateInvites = async () => {
const us = await useFetch('/api/auth/invite');
if (!us.error) {
setInvites(us);
setOk(true);
} else {
router.push('/dashboard');
}
@@ -184,48 +231,134 @@ export default function Invites() {
<Group mb='md'>
<Title>Invites</Title>
<ActionIcon variant='filled' color='primary' onClick={() => setOpen(true)}>
<PlusIcon />
<IconPlus size='1rem' />
</ActionIcon>
<Tooltip label={listView ? 'Switch to grid view' : 'Switch to list view'}>
<ActionIcon variant='filled' color='primary' onClick={() => setListView(!listView)}>
{listView ? <IconList size='1rem' /> : <IconGridDots size='1rem' />}
</ActionIcon>
</Tooltip>
</Group>
<SimpleGrid cols={3} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
{invites.length
? invites.map((invite) => (
<Card key={invite.id} sx={{ maxWidth: '100%' }}>
<Group position='apart'>
<Group position='left'>
<Avatar size='lg' color={invite.used ? 'dark' : 'primary'}>
{invite.id}
</Avatar>
<Stack spacing={0}>
<Title>
{invite.code}
{invite.used && <> (Used)</>}
</Title>
<Tooltip label={new Date(invite.created_at).toLocaleString()}>
<div>
<MutedText size='sm'>Created {relativeTime(new Date(invite.created_at))}</MutedText>
</div>
</Tooltip>
<Tooltip label={new Date(invite.expires_at).toLocaleString()}>
<div>
<MutedText size='sm'>{expireText(invite.expires_at)}</MutedText>
</div>
</Tooltip>
{listView ? (
<DataTable
withBorder
borderRadius='md'
highlightOnHover
verticalSpacing='sm'
columns={[
{ accessor: 'id', sortable: true },
{ accessor: 'code', sortable: true },
{
accessor: 'createdAt',
title: 'Created At',
sortable: true,
render: (invite) => new Date(invite.createdAt).toLocaleString(),
},
{
accessor: 'expiresAt',
title: 'Expires At',
sortable: true,
render: (invite) => new Date(invite.expiresAt).toLocaleString(),
},
{
accessor: 'used',
sortable: true,
render: (invite) => (invite.used ? 'Yes' : 'No'),
},
{
accessor: 'actions',
textAlignment: 'right',
render: (invite) => (
<Group spacing={4} position='right' noWrap>
<Tooltip label='Copy invite link'>
<ActionIcon variant='subtle' color='primary' onClick={() => handleCopy(invite)}>
<IconClipboardCopy size='1rem' />
</ActionIcon>
</Tooltip>
<Tooltip label='Delete invite'>
<ActionIcon variant='subtle' color='red' onClick={() => openDeleteModal(invite)}>
<IconTrash size='1rem' />
</ActionIcon>
</Tooltip>
</Group>
),
},
]}
sortStatus={sortStatus}
onSortStatusChange={setSortStatus}
records={records ?? []}
fetching={!ok}
minHeight='calc(100vh - 200px)'
loaderBackgroundBlur={5}
loaderVariant='dots'
rowContextMenu={{
shadow: 'xl',
borderRadius: 'md',
items: (invite) => [
{
key: 'copy',
icon: <IconClipboardCopy size='1rem' />,
title: `Copy invite code: "${invite.code}"`,
onClick: () => clipboard.copy(invite.code),
},
{
key: 'copyLink',
icon: <IconClipboardCopy size='1rem' />,
title: 'Copy invite link',
onClick: () => handleCopy(invite),
},
{
key: 'delete',
icon: <IconTrash size='1rem' />,
title: `Delete invite ${invite.code}`,
onClick: () => openDeleteModal(invite),
},
],
}}
/>
) : (
<SimpleGrid cols={3} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
{invites.length
? invites.map((invite) => (
<Card key={invite.id} sx={{ maxWidth: '100%' }}>
<Group position='apart'>
<Group position='left'>
<Avatar size='lg' color={invite.used ? 'dark' : 'primary'}>
{invite.id}
</Avatar>
<Stack spacing={0}>
<Title>
{invite.code}
{invite.used && <> (Used)</>}
</Title>
<Tooltip label={new Date(invite.createdAt).toLocaleString()}>
<div>
<MutedText size='sm'>
Created {relativeTime(new Date(invite.createdAt))}
</MutedText>
</div>
</Tooltip>
<Tooltip label={new Date(invite.expiresAt).toLocaleString()}>
<div>
<MutedText size='sm'>{expireText(invite.expiresAt.toString())}</MutedText>
</div>
</Tooltip>
</Stack>
</Group>
<Stack>
<ActionIcon aria-label='copy' onClick={() => handleCopy(invite)}>
<IconClipboardCopy size='1rem' />
</ActionIcon>
<ActionIcon aria-label='delete' onClick={() => openDeleteModal(invite)}>
<IconTrash size='1rem' />
</ActionIcon>
</Stack>
</Group>
<Group position='right'>
<ActionIcon aria-label='copy' onClick={() => handleCopy(invite)}>
<CopyIcon />
</ActionIcon>
<ActionIcon aria-label='delete' onClick={() => openDeleteModal(invite)}>
<DeleteIcon />
</ActionIcon>
</Group>
</Group>
</Card>
))
: [1, 2, 3].map((x) => <Skeleton key={x} width='100%' height={100} radius='sm' />)}
</SimpleGrid>
</Card>
))
: [1, 2, 3].map((x) => <Skeleton key={x} width='100%' height={100} radius='sm' />)}
</SimpleGrid>
)}
</>
);
}
@@ -0,0 +1,93 @@
import { Button, Checkbox, Group, Modal, Text, Title } from '@mantine/core';
import { closeAllModals, openConfirmModal } from '@mantine/modals';
import { showNotification, updateNotification } from '@mantine/notifications';
import { IconFiles, IconFilesOff } from '@tabler/icons-react';
import useFetch from 'hooks/useFetch';
import { useState } from 'react';
export default function ClearStorage({ open, setOpen }) {
const [check, setCheck] = useState(false);
const handleDelete = async (datasource: boolean, orphaned?: boolean) => {
showNotification({
id: 'clear-uploads',
title: 'Clearing...',
message: '',
loading: true,
autoClose: false,
});
const res = await useFetch('/api/admin/clear', 'POST', { datasource, orphaned });
if (res.error) {
updateNotification({
id: 'clear-uploads',
title: 'Error while clearing uploads',
message: res.error,
color: 'red',
icon: <IconFilesOff size='1rem' />,
});
} else {
updateNotification({
id: 'clear-uploads',
title: 'Successfully cleared uploads',
message: '',
color: 'green',
icon: <IconFiles size='1rem' />,
});
}
};
return (
<Modal
opened={open}
onClose={() => {
setOpen(false);
setCheck(() => false);
}}
title={<Title size='sm'>Are you sure you want to clear all uploads in the database?</Title>}
>
<Checkbox
id='orphanedFiles'
label='Clear only orphaned files?'
description='Orphaned files are not owned by anyone. They can&#39;t be seen the dashboard by anyone.'
checked={check}
onChange={(e) => setCheck(e.currentTarget.checked)}
/>
<Group position='right' mt='md'>
<Button
onClick={() => {
setOpen(() => false);
}}
>
No
</Button>
<Button
onClick={() => {
setOpen(false);
openConfirmModal({
title: 'Do you want to clear storage too?',
labels: { confirm: 'Yes', cancel: check ? 'Ok' : 'No' },
children: check && (
<Text size='sm' color='gray'>
Due to clearing orphaned files, storage clearing will be unavailable.
</Text>
),
confirmProps: { disabled: check },
onConfirm: () => {
closeAllModals();
handleDelete(true);
},
onCancel: () => {
closeAllModals();
handleDelete(false, check);
},
onClose: () => setCheck(false),
});
}}
>
Yes
</Button>
</Group>
</Modal>
);
}

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