Compare commits

...

243 Commits

Author SHA1 Message Date
Derrick Hammer d7f0154fb8
fix: add a queued tab to the pinning accordion 2024-03-29 13:21:03 -04:00
Derrick Hammer eea567d89e
fix: disable local storage by using NoopUrlStorage 2024-03-29 12:05:46 -04:00
Derrick Hammer cd49a5977e
fix: set chunk size to 1 mb 2024-03-29 00:19:04 -04:00
Derrick Hammer e5f8aef83f
fix: change limits 2024-03-28 23:39:46 -04:00
Derrick Hammer 80f9fec610
fix: temporarily disable parallelUploads 2024-03-28 22:52:44 -04:00
Derrick Hammer ff9a14a8b5
fix: bad hash progress calc 2024-03-28 20:38:31 -04:00
Derrick Hammer 5176de0113
dep: update portal-sdk 2024-03-28 20:33:45 -04:00
Derrick Hammer 4e5fde24ae
fix: preprocess-progress needs to just be passed the preprocess object 2024-03-28 20:22:30 -04:00
Derrick Hammer 4b5fe20df3
fix: add preprocess-progress event as uploading state 2024-03-28 20:13:20 -04:00
Derrick Hammer 64b1a31a8f
fix: pre-progress bar fixes 2024-03-28 20:04:53 -04:00
Juan Di Toro 377976b8b2 fix: jiji forgot to add the progress bar on pre process 2024-03-29 00:42:27 +01:00
Juan Di Toro 8235f516e9 feat: add verification bannner 2024-03-29 00:38:11 +01:00
Juan Di Toro 1118ba2a71 fix: fixing the bar 2024-03-29 00:28:44 +01:00
Juan Di Toro a58914a69a fix: progress bar not being rendered due to progress not being tracked 2024-03-29 00:14:35 +01:00
Juan Di Toro 6b097e0860 fix: a tiny bit better upload ui 2024-03-29 00:09:14 +01:00
Juan Di Toro 0d2a73b508 Merge branch 'develop' of git.lumeweb.com:LumeWeb/portal-dashboard into develop 2024-03-28 23:59:19 +01:00
Juan Di Toro 3c767f8e05 fix: pinning progress bar is now fully working 2024-03-28 23:59:11 +01:00
Derrick Hammer 558cb3a54e
dep: force override tus-js-client version 2024-03-28 17:18:19 -04:00
Derrick Hammer 34de535f75
dep: update deps and patches 2024-03-28 16:56:21 -04:00
Derrick Hammer 3e2ec0a93a
fix: update tus patch to patch lib as well 2024-03-28 16:31:28 -04:00
Derrick Hammer 2a7fa75329
fix: update tus patch to patch lib.es5 as well 2024-03-28 16:19:39 -04:00
Derrick Hammer 0818b8b87d
fix: add patch for tus on parallel uploads to ensure it sends the meta 2024-03-28 15:44:29 -04:00
Juan Di Toro 2cb1ed558a Merge branch 'develop' of git.lumeweb.com:LumeWeb/portal-dashboard into develop 2024-03-28 10:04:42 +01:00
Juan Di Toro 10610db63e fix: cannot upload if there are no files 2024-03-28 10:04:31 +01:00
Derrick Hammer 38593ae032
dep: update portal-sdk 2024-03-28 00:01:34 -04:00
Derrick Hammer 44b9f625cc
feat: implement download file 2024-03-27 06:05:48 -04:00
Juan Di Toro 810948242f fix: add extra invalidate after pinning and unpinning request 2024-03-27 07:53:51 +01:00
Juan Di Toro af9acd1bf7 fix: properly invalidate file resource when uploading and pinning finishes 2024-03-27 07:46:12 +01:00
Juan Di Toro 4cadd0072d fix: filesize lib was buggy. removed selection options as we dont use thos 2024-03-27 07:34:52 +01:00
Juan Di Toro ffef784f50 feat: closes #21 2024-03-27 07:22:58 +01:00
Juan Di Toro 7fec1f0543 fix: remove unusued type 2024-03-27 07:02:53 +01:00
Juan Di Toro 4afa8f58b9 fix: adds proper filesize formatting 2024-03-27 07:02:21 +01:00
Derrick Hammer c699672737
fix: only run when we have both the email and token 2024-03-26 23:52:01 -04:00
Derrick Hammer 41bff5acdd
dep: update portal-sdk 2024-03-26 23:51:22 -04:00
Derrick Hammer f0dc27e49b
dep: update portal-sdk 2024-03-26 16:46:40 -04:00
Derrick Hammer c61e5550b8
refactor: wire up verify email, change route, and make it authenticated 2024-03-26 16:41:57 -04:00
Derrick Hammer be574451c6
dep: update portal-sdk 2024-03-26 15:20:06 -04:00
Derrick Hammer c5e22f52e1
refactor: we need to split root out for readability into a SdkWrapper component to reflect changes in init'ing the sdk down the component tree, and sdk should be a react var 2024-03-26 13:01:46 -04:00
Derrick Hammer 5bdd888f63
fix: don't cache/store the providers, we only use in 1 place currently and we can memo it 2024-03-26 12:58:16 -04:00
Derrick Hammer 849b723e8c
dep: update portal-sdk 2024-03-26 11:48:56 -04:00
Derrick Hammer 740d276071
feat: if dev mode is enabled store jwt to local storage and fetch it as needed 2024-03-26 11:38:28 -04:00
Juan Di Toro e6d296d6af chore: debugging 2024-03-26 15:54:51 +01:00
Juan Di Toro b2a822bf08 fix: error state for file upload 2024-03-26 14:42:18 +01:00
Juan Di Toro 2bca9ce939 feat: closes #7. Adds a verify screen 2024-03-26 13:22:18 +01:00
Derrick Hammer 4dcac43e73
fix: wrong property 2024-03-26 01:18:12 -04:00
Derrick Hammer 056c1de31f
fix: prefix with https 2024-03-26 00:59:51 -04:00
Derrick Hammer 9378a984dd
fix: if no portalUrl, return a loading tag 2024-03-26 00:59:19 -04:00
Derrick Hammer d5f63490a5
feat: access api endpoint relatively to get the portal domain if VITE_PORTAL_URL is not set 2024-03-26 00:41:43 -04:00
Juan Di Toro ab425c6f2c fix: every time in progress query is fetched, we check the results for atleast one complete query and this invalidates the query results for the files data provider on the list query 2024-03-25 09:12:31 +01:00
Juan Di Toro 1cb7afda8f fix: only completed items can be unpinned. Maybe cross iscon is not the best 2024-03-25 09:02:09 +01:00
Juan Di Toro 4290904143 fix: pinned items in progress cannot be cancelled 2024-03-25 09:00:41 +01:00
Juan Di Toro 4d271ec421 fix: textarea for pinning modal. small refactor to the api exposed by the hook 2024-03-25 08:59:35 +01:00
Juan Di Toro 415d9d14a6 fix: notifications now are not transparent. Modals for my account now close whe nfinished 2024-03-25 08:34:22 +01:00
Derrick Hammer 7ca98d0cfb
refactor: add CID validation 2024-03-22 19:10:20 -04:00
Derrick Hammer b8999efcd7
refactor: update how we poll for the pin status and if a 404 assume completed and delete 2024-03-22 18:59:19 -04:00
Derrick Hammer 4c731e1120
dep: update portal-sdk 2024-03-22 18:38:19 -04:00
Derrick Hammer c1b4e668e2
feat: initial wiring of the pinning system 2024-03-22 18:30:58 -04:00
Derrick Hammer 9938beddbc
dep: update portal-sdk 2024-03-22 18:17:17 -04:00
Juan Di Toro 8d4fcfba5c feat: connecting the pinning thingy 2024-03-22 19:33:30 +01:00
Juan Di Toro c9de506c56 fix: small fixes 2024-03-22 18:53:39 +01:00
Juan Di Toro e1ca45f1f1 Merge branch 'develop' into riobuenoDevelops/pinning-network 2024-03-22 17:13:10 +01:00
Tania Gutierrez 7b31d561fe
fix: Moved useQuery to PinningProvider and moved mutations into usePinning 2024-03-22 09:54:46 -04:00
Juan Di Toro c9851d8dee fix: closes #10 2024-03-22 14:27:40 +01:00
Juan Di Toro 2ccf55f75f fix: closes #9 2024-03-22 14:14:45 +01:00
Juan Di Toro a61fe195ee fix: closes #8 2024-03-22 14:04:22 +01:00
Juan Di Toro e401889b04 fix: UX changes regarding showing dates on table and actions pannel 2024-03-22 13:47:50 +01:00
Juan Di Toro 9d9aa4e9c9 feat: add pinning modal 2024-03-22 12:31:31 +01:00
Juan Di Toro db91cb9590 Merge branch 'develop' of git.lumeweb.com:LumeWeb/portal-dashboard into develop 2024-03-22 11:43:16 +01:00
Tania Gutierrez 8b8fa967d0
fix: Removed PinningProvider and used react-query directly 2024-03-21 23:15:56 -04:00
Derrick Hammer 364f2b048b
fix: wrong accessorKey 2024-03-21 17:00:30 -04:00
Derrick Hammer 4619f9709c
fix: size is missing 2024-03-21 16:49:14 -04:00
Derrick Hammer 6252979d28
refactor: update columns 2024-03-21 16:46:02 -04:00
Derrick Hammer 23cc02b26e
dep: add date-fns 2024-03-21 16:45:20 -04:00
Derrick Hammer ea395df494
refactor: add pinned property for date 2024-03-21 16:45:06 -04:00
Derrick Hammer 7ea4af1346
dep: update portal-sdk 2024-03-21 16:37:25 -04:00
Derrick Hammer 20d8905a67
dep: update portal-sdk 2024-03-21 16:32:23 -04:00
Derrick Hammer 0c304b20db
refactor: use new CID with the multihash 2024-03-21 16:20:37 -04:00
Derrick Hammer f7a760051f
refactor: use our own abort controller 2024-03-21 16:15:04 -04:00
Derrick Hammer 6cdf8e40b9
dep: update portal-sdk 2024-03-21 16:13:35 -04:00
Derrick Hammer 3a5cd0ae32
refactor: split app to App and Root and store the sdk context in the root for app to use 2024-03-21 15:32:39 -04:00
Derrick Hammer b42a04ebc3
dep: update portal-sdk 2024-03-21 15:17:45 -04:00
Derrick Hammer 61b70ffa2e
fix: use Authenticated component 2024-03-21 15:04:19 -04:00
Derrick Hammer a7ef0b4773
fix: update resource name 2024-03-21 14:34:55 -04:00
Derrick Hammer 3162d4dcd6
fix: pass all providers 2024-03-21 14:30:11 -04:00
Derrick Hammer d724359622
feat: initial very basic file listing 2024-03-21 13:24:13 -04:00
Derrick Hammer 39ac3467fc
dep: update portal-sdk 2024-03-21 13:22:19 -04:00
Derrick Hammer b9652ab261
dep: update portal-sdk 2024-03-21 13:16:42 -04:00
Derrick Hammer 184b3d9a0e
dep: update portal-sdk 2024-03-21 13:10:36 -04:00
Derrick Hammer 335e982ab0
dep: update portal-sdk 2024-03-21 12:56:25 -04:00
Derrick Hammer be31f0db04
dep: update portal-sdk 2024-03-21 12:41:05 -04:00
Derrick Hammer 7bd2ecdc82
dep: update portal-sdk 2024-03-21 12:21:52 -04:00
Derrick Hammer d326881f9b
dep: update portal-sdk 2024-03-21 12:15:55 -04:00
Derrick Hammer d4ad64bf88
dep: update portal-sdk 2024-03-21 12:13:02 -04:00
Derrick Hammer 51e8cfef76
dep: update portal-sdk 2024-03-21 11:42:26 -04:00
Derrick Hammer 583a95e68a
dep: update portal-sdk 2024-03-21 11:39:40 -04:00
Derrick Hammer adf306b074
dep: update portal-sdk 2024-03-21 11:03:55 -04:00
Derrick Hammer 02b712793e
dep: update portal-sdk 2024-03-21 10:58:18 -04:00
Tania Gutierrez 39a8789f95
fix: Added resurce and dataProvider props to data-table, fixed UI 2024-03-21 10:56:08 -04:00
Derrick Hammer 8fecd31e43
dep: update portal-sdk 2024-03-21 10:50:44 -04:00
Derrick Hammer 214eb14583
dep: update portal-sdk 2024-03-21 10:28:38 -04:00
Derrick Hammer 82b4f9f4fc
fix: need to pass currentPassword 2024-03-21 08:49:29 -04:00
Derrick Hammer 2d0609c95c
dep: update portal-sdk 2024-03-21 08:49:05 -04:00
Tania Gutierrez b646fc4887
feat: Created Pinning banner to show inprogress and completed PinningProcess 2024-03-21 01:17:00 -04:00
Tania Gutierrez 140a9d222f
feat: Created PinningContext and PinningProvider to abstract logic from frontend components 2024-03-21 01:16:27 -04:00
Tania Gutierrez 52fc50d480
feat: Mocked PinningProvider 2024-03-21 01:15:12 -04:00
Derrick Hammer bba7ce59d3
refactor: nop on onError 2024-03-20 16:22:06 -04:00
Derrick Hammer 9332598627
refactor: pass ret as a HttpError 2024-03-20 16:21:56 -04:00
Derrick Hammer 6ce3b02dc6
dep: update portal-sdk 2024-03-20 16:21:37 -04:00
Derrick Hammer 81d52d2524
refactor: ret should now satisfy HttpError 2024-03-20 16:08:40 -04:00
Derrick Hammer 64e2216a36
dep: update portal-sdk 2024-03-20 15:44:12 -04:00
Derrick Hammer bec75e63ee
refactor: fix handling of check by creating a handleCheckResponse wrapper to translate success to authenticated 2024-03-20 15:26:24 -04:00
Juan Di Toro 43a0ab0b29 Merge branch 'develop' of git.lumeweb.com:LumeWeb/portal-dashboard into develop 2024-03-20 20:05:13 +01:00
Derrick Hammer b3f0044723
refactor: use handleResponse on login 2024-03-20 13:43:51 -04:00
Derrick Hammer 729414c45a
refactor: add success notification on register, remove success from route form and auto login call 2024-03-20 13:42:21 -04:00
Derrick Hammer 6fbbe4975c
refactor: switch to using Authenticated and Navigate components 2024-03-20 13:26:41 -04:00
Derrick Hammer 33decc2e2d
Revert "fix: route redirection issue"
This reverts commit 9a8a7ee978.
2024-03-20 13:19:53 -04:00
Derrick Hammer 081df029e0
refactor: de-duplicate code into handleResponse helper and add support for success notifications 2024-03-20 13:10:57 -04:00
Derrick Hammer 6bffca0524
dep: update portal-sdk 2024-03-20 13:10:21 -04:00
Derrick Hammer 5aa62f7d82 Merge pull request 'Refine Integration' (#13) from riobuenoDevelops/refine-integration into develop
Reviewed-on: #13
2024-03-19 23:50:54 +00:00
Juan Di Toro 65bf663b67 chore: dont break the app by throwing error just yet 2024-03-19 23:47:33 +01:00
Juan Di Toro 9a8a7ee978 fix: route redirection issue 2024-03-19 23:41:45 +01:00
Tania Gutierrez dec1fd29f5
Merge branch 'develop' into riobuenoDevelops/refine-integration 2024-03-19 14:17:59 -04:00
Tania Gutierrez d59130930f
fix: Fixed Dialog not reseting state 2024-03-19 14:13:37 -04:00
Tania Gutierrez 34e9009e96
fix: Added cancelMutation to toast 2024-03-19 11:38:42 -04:00
Derrick Hammer 8770e24639
fix: wrap page in Authenticated component 2024-03-19 11:10:02 -04:00
Juan Di Toro c291434881 fix: wrong login schema 2024-03-19 15:40:12 +01:00
Derrick Hammer 847f7537a3
fix: don't validate as email 2024-03-19 10:27:55 -04:00
Derrick Hammer 02cd490700
fix: update password needs the correct data sent to the mutation 2024-03-19 10:25:44 -04:00
Derrick Hammer 1dae0ba771
refactor: have UpdatePasswordFormRequest extend UpdatePasswordFormTypes 2024-03-19 10:23:03 -04:00
Derrick Hammer 03b1d761f0
fix: use ChangePasswordSchema 2024-03-19 10:16:22 -04:00
Derrick Hammer b65bd12c8a
feat: implement updatePassword 2024-03-19 10:12:13 -04:00
Derrick Hammer 8ca46c6f18
dep: update portal-sdk 2024-03-19 10:07:38 -04:00
Derrick Hammer 2dccaca132
fix: missing await 2024-03-19 09:12:21 -04:00
Derrick Hammer 3502732e76
fix: pass password to updateEmail 2024-03-19 08:59:10 -04:00
Derrick Hammer ddf9a0ddea
feat: initial update email support 2024-03-19 08:54:10 -04:00
Derrick Hammer 9fed7a2643
dep: update portal-sdk 2024-03-19 08:53:49 -04:00
Derrick Hammer ff5e5dec14
refactor: major refactor of the providers and resources to be more dynamic and flexible 2024-03-19 07:20:57 -04:00
Derrick Hammer 2e918e7802
refactor: we don't need to pass the endpoint, the sdk handles it 2024-03-19 07:18:52 -04:00
Derrick Hammer 482fd966cc
chore: remove unused property 2024-03-19 07:17:15 -04:00
Tania Gutierrez 37ad1d1dc9
feat: Added Dialog to Edit Avatar Card on Account View 2024-03-18 21:42:32 -04:00
Tania Gutierrez add532aa51
feat: Added Notification provided and toast to verify email and recovery password 2024-03-18 21:41:53 -04:00
Derrick Hammer 1ff5f205b2
feat: add callback and emit to track hashing progress 2024-03-18 18:51:41 -04:00
Derrick Hammer 0a51f3e76d
dep: update portal-sdk 2024-03-18 18:42:53 -04:00
Derrick Hammer c12c980b17
dep: update portal-sdk 2024-03-18 18:32:12 -04:00
Derrick Hammer 48f29a1338
refactor: set parallelUploads to 10 2024-03-18 18:20:24 -04:00
Derrick Hammer 9a9357bfc0
dep: update portal-sdk 2024-03-18 18:06:49 -04:00
Derrick Hammer 7293cb0b5a
refactor: switch to pulling the cookie from the account service after ping as ping now returns it in the response 2024-03-18 17:25:49 -04:00
Derrick Hammer ba374a851c
dep: update portal-sdk 2024-03-18 17:23:38 -04:00
Derrick Hammer d1e059fd71
fix: need to merge tus metadata to file meta 2024-03-18 16:48:29 -04:00
Derrick Hammer 90f14b63e5
build: add patch-package to package.json 2024-03-18 16:36:17 -04:00
Derrick Hammer 04c38429fd
fix: disable refine telemetry 2024-03-18 15:57:17 -04:00
Tania Gutierrez 43ac8560cb
Merge branch 'develop' into riobuenoDevelops/refine-integration 2024-03-18 15:52:14 -04:00
Tania Gutierrez 85615f53f5
fix: Added Row Skeletons with loading table data 2024-03-18 15:51:02 -04:00
Derrick Hammer dc4f6f7e23
refactor: need to add an uppy pre-processor to configure the tus options per file, including the hashing 2024-03-18 15:32:47 -04:00
Derrick Hammer 043c9b8375
dep: update portal-sdk 2024-03-18 14:32:33 -04:00
Derrick Hammer 7f8310ea9c
debug: disable minify 2024-03-18 13:02:48 -04:00
Derrick Hammer 6794bb2a4a
dep: update portal-sdk 2024-03-18 13:02:22 -04:00
Derrick Hammer 7779f794e4
fix: bind handleUpload to current instance 2024-03-18 12:50:44 -04:00
Derrick Hammer 2402d3e076
fix: set auth token globally on sdk after login 2024-03-18 12:50:26 -04:00
Derrick Hammer 2140a63add
fix: put plugin logic inside setInputProps onChange 2024-03-18 12:37:22 -04:00
Derrick Hammer 756a505590
fix: uppy requires a type 2024-03-18 12:36:58 -04:00
Derrick Hammer 7e78f17937
ci: disable protocolImports 2024-03-18 11:57:38 -04:00
Derrick Hammer ed410051e2
ci: add vite-plugin-node-polyfills plugin 2024-03-18 11:37:44 -04:00
Derrick Hammer 42de97519b
dep: update portal-sdk 2024-03-18 11:37:17 -04:00
Derrick Hammer d11bf0861c
dep: update portal-sdk 2024-03-18 11:18:29 -04:00
Derrick Hammer a3d958065c
dep: update portal-sdk 2024-03-18 11:13:06 -04:00
Juan Di Toro 26b0246429 Merge branch 'develop' of git.lumeweb.com:LumeWeb/portal-dashboard into develop 2024-03-18 16:07:24 +01:00
Juan Di Toro c9d956e1b6 fix: remove the backup key card from my account 2024-03-18 16:07:11 +01:00
Derrick Hammer ec9509ef6c
dep: update portal-sdk 2024-03-18 11:06:45 -04:00
Derrick Hammer 8a3181177b
dep: update portal-sdk 2024-03-18 11:01:13 -04:00
Derrick Hammer 84f7585a66
dep: update portal-sdk 2024-03-18 10:56:20 -04:00
Derrick Hammer 3a4b40ef27
refactor: remove uploader from uppy hook and check by the upload size limit if we use tus plugin or just file post. additionally add a onBeforeUpload filter to set the custom uploader property. 2024-03-18 10:12:27 -04:00
Derrick Hammer 988780b25f
feat: add initial plugin for a post file upload via s5 sdk 2024-03-18 10:11:07 -04:00
Derrick Hammer dca77ba71a
fix: add patch for uppy tus to filter on files with a custom uploader property set to tus 2024-03-18 10:10:33 -04:00
Derrick Hammer 7f26bc1060
feat: add sdk context and useSdk 2024-03-18 10:09:37 -04:00
Derrick Hammer 8643363736
refactor: export RequiredAuthProvider 2024-03-18 10:09:08 -04:00
Derrick Hammer 7c332d0f43
refactor: add sdk getter 2024-03-18 10:08:57 -04:00
Derrick Hammer d3e847baf8
dep: update portal-sdk 2024-03-18 10:08:15 -04:00
Derrick Hammer 06db71bd8a
dep: update portal-sdk 2024-03-17 11:26:23 -04:00
Derrick Hammer 928deb89b6
refactor: update logout text name 2024-03-17 09:48:21 -04:00
Derrick Hammer 245467155d
refactor: don't try to delete the auth cookie 2024-03-17 09:41:52 -04:00
Derrick Hammer fd7fec8580
dep: just use universal-cookie 2024-03-17 09:41:12 -04:00
Derrick Hammer 0e083c7a58
dep: update portal-sdk 2024-03-17 09:37:53 -04:00
Derrick Hammer 9fd55bf994
refactor: update auth token name 2024-03-17 08:18:33 -04:00
Derrick Hammer 20533913bd
refactor: switch to reading cookie only and let server handle it for security, add maybeSetupAuth helper. 2024-03-16 15:41:31 -04:00
Tania Gutierrez 6506917ddb
feat: Integrated Refine to Account modals and User DropDownMenu 2024-03-15 16:00:57 -04:00
Tania Gutierrez 67b579d43b
feat: Integrated Refine into Data Table 2024-03-15 16:00:25 -04:00
Tania Gutierrez e559e4d709
fix: Adapted File-provider to be default dataProvider 2024-03-15 15:59:02 -04:00
Tania Gutierrez f67ebbd98a
Merge branch 'develop' of git.lumeweb.com:LumeWeb/portal-dashboard into develop 2024-03-14 13:53:01 -04:00
Tania Gutierrez ad033614f9
fix: Applied Composition pattern to ManagementCard 2024-03-14 13:45:21 -04:00
Juan Di Toro 4432c4e7f6 fix: icons and reove package.json 2024-03-14 17:17:41 +01:00
ditorodev 83ddd0613b Merge pull request 'My Account Route' (#3) from riobuenoDevelops/my-account into develop
Reviewed-on: #3
2024-03-14 15:43:53 +00:00
Juan Di Toro 1732c18059 Merge branch 'develop' of git.lumeweb.com:LumeWeb/portal-dashboard into riobuenoDevelops/my-account 2024-03-14 16:43:28 +01:00
Juan Di Toro a1e73b8ed8 fix: formatting 2024-03-14 16:41:48 +01:00
Derrick Hammer 5f97e8c7d8
feat: implement identity api 2024-03-14 07:10:17 -04:00
Derrick Hammer 0b5e066aab
dep: update portal-sdk 2024-03-14 07:10:00 -04:00
Tania Gutierrez 260b41b29b
style: Formatted code 2024-03-13 21:55:44 -04:00
Tania Gutierrez c7d7a129b9
chore: Moved Icon definitions to icon.tsx 2024-03-13 21:54:05 -04:00
Tania Gutierrez bdc6de39b4
feat: Added My Account route 2024-03-13 21:52:31 -04:00
Tania Gutierrez ff74a98e24
feat: Allow usageCard button to be replaced for another one 2024-03-13 21:50:32 -04:00
Tania Gutierrez 988dab24d1
fix: Styled input and field components acording to design 2024-03-13 21:50:01 -04:00
Tania Gutierrez 6f10d173c0
feat: Added user dropdown and active navbar item state 2024-03-13 21:49:04 -04:00
Derrick Hammer 48bb9d1121
refactor: handle redirects from ?to after login, or when already logged in 2024-03-13 19:23:29 -04:00
Derrick Hammer 4c972d289d
chore: update package-lock.json 2024-03-13 19:08:43 -04:00
Derrick Hammer 70a4139b93
feat: add cookie support for auth 2024-03-13 19:08:14 -04:00
Derrick Hammer 0d7215ff66
feat: implement register 2024-03-13 19:07:46 -04:00
Derrick Hammer 7b645666f6
dep: update sdk, add cookie lib 2024-03-13 18:49:19 -04:00
Juan Di Toro d07dbff31f feat: add first and last name to register form 2024-03-13 19:06:43 +01:00
Derrick Hammer 99ac9533f0
fix: update submit button name 2024-03-13 13:35:42 -04:00
Derrick Hammer 3f77d16f0b
style: prettier 2024-03-13 13:33:03 -04:00
Derrick Hammer 273b41d243
refactor: change register name from "sign up" to "register" 2024-03-13 13:32:07 -04:00
Derrick Hammer 4259f38566
chore: remove un-used import 2024-03-13 13:19:41 -04:00
Derrick Hammer c06ca5afe9
chore: update package-lock.json 2024-03-13 13:18:51 -04:00
Derrick Hammer 51618a2fda
feat: setup auth provider and refine with login, check and logout methods 2024-03-13 13:18:18 -04:00
Juan Di Toro 7032892686 chore: cleanup to imports 2024-03-13 18:07:03 +01:00
ditorodev d824c8c678 Merge pull request 'File Manager - Dashboard Routes' (#2) from riobuenoDevelops/fileManager-dashboard into develop
Reviewed-on: #2
2024-03-13 17:05:34 +00:00
Juan Di Toro 292b8b2f21 Merge branch 'develop' of git.lumeweb.com:LumeWeb/portal-dashboard into riobuenoDevelops/fileManager-dashboard 2024-03-13 18:04:25 +01:00
Juan Di Toro 467c29c82c fix: normalize icons. fix some spacing and UI issues 2024-03-13 18:01:23 +01:00
Juan Di Toro f432cb6b3d chore: remove package lock and ignore it 2024-03-13 17:06:42 +01:00
Derrick Hammer 069fdb49bd
chore: add prettier config 2024-03-13 09:40:45 -04:00
Derrick Hammer 427bdbb407
refactor: integrate auth provider 2024-03-13 09:40:25 -04:00
Derrick Hammer 552bf84247
fix: update import 2024-03-13 09:38:22 -04:00
Derrick Hammer 2f53b37775
refactor: add in refine 2024-03-13 09:38:01 -04:00
Tania Gutierrez 1dc0a8abf5
chore: Removed UsageList component 2024-03-13 09:22:29 -04:00
Tania Gutierrez 68d8a3030b
Merge branch 'riobuenoDevelops/file-manager' into riobuenoDevelops/fileManager-dashboard 2024-03-13 09:18:54 -04:00
Tania Gutierrez 6bdbc644e9
fix: prevent nav scrolling when main wiew height surpass viewport 2024-03-12 16:37:29 -04:00
Tania Gutierrez ddc7628023
Merge branch 'develop' into riobuenoDevelops/dashboard 2024-03-12 16:28:49 -04:00
Tania Gutierrez 32768712c8
feat: completed Dashboard View 2024-03-12 16:22:54 -04:00
Tania Gutierrez c30d230fc5
Merge branch 'develop' into riobuenoDevelops/file-manager 2024-03-12 14:26:20 -04:00
Juan Di Toro 0ba128b135 fix: added cleanup when component unmount 2024-03-12 15:36:44 +01:00
Juan Di Toro 94406b7f13 fix: state and rendering 2024-03-12 15:32:00 +01:00
Juan Di Toro b0809c65f3 feat: (wip) added support for TUS file uploaded. Still missing support for resetting the uppy state plus handling uppy errors 2024-03-12 13:42:17 +01:00
Tania Gutierrez 139cc8737d
Added fullWidth and leftIcon properties to input and finished FileManager design 2024-03-11 23:43:26 -04:00
Tania Gutierrez 41151f74de
Created File Manager table related components 2024-03-11 23:41:05 -04:00
Tania Gutierrez 1e486ab3dd
Created FileCard component 2024-03-11 23:40:23 -04:00
Tania Gutierrez 619963688b
Changed DashboardLayout name to GeneralLayout 2024-03-11 23:38:01 -04:00
Tania Gutierrez 9b09f6854a
Made main layout and footer not be in column direction 2024-03-11 22:40:05 -04:00
Juan Di Toro 92b414afb1 feat: add dashboard layout 2024-03-07 18:26:11 +01:00
Juan Di Toro ec402fc38c feat: wiring of the authentication pages 2024-03-07 15:04:59 +01:00
Juan Di Toro 4b9ea7f472 feat: sign-up route 2024-03-05 20:25:40 +01:00
Juan Di Toro 51f212c7b4 Merge branch 'master' into develop 2024-03-05 20:16:18 +01:00
Juan Di Toro 393fda9710 fix 2024-03-05 20:16:05 +01:00
Derrick Hammer eb82f68bd8
chore: update LICENSE 2024-02-14 05:03:41 -05:00
67 changed files with 6064 additions and 11513 deletions

5
.gitignore vendored
View File

@ -1,4 +1,7 @@
node_modules /node_modules
package-lock.json
yarn.lock
/.cache /.cache
/build /build

8
.prettierrc.json Normal file
View File

@ -0,0 +1,8 @@
{
"bracketSpacing": true,
"bracketSameLine": true,
"quoteProps": "consistent",
"semi": true,
"singleQuote": false,
"trailingComma": "all"
}

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2024 LumeWeb Copyright (c) 2024 Hammer Technologies LLC
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View File

@ -0,0 +1,101 @@
import { useMemo} from "react";
import type { BaseRecord } from "@refinedev/core";
import { useTable } from "@refinedev/react-table";
import {
type ColumnDef,
flexRender,
} from "@tanstack/react-table";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "./ui/table"
import { Skeleton } from "./ui/skeleton";
import { DataTablePagination } from "./table-pagination"
interface DataTableProps<TData extends BaseRecord = BaseRecord, TValue = unknown> {
columns: ColumnDef<TData, TValue>[],
resource: string;
dataProviderName?: string;
}
export function DataTable<TData extends BaseRecord, TValue>({
columns,
resource,
dataProviderName
}: DataTableProps<TData, TValue>) {
const table = useTable({
columns,
refineCoreProps: {
resource,
dataProviderName: dataProviderName || "default"
}
})
const loadingRows = useMemo(() => Array(4).fill({}).map(() => ({
getIsSelected: () => false,
getVisibleCells: () => columns.map(() => ({
column: {
columnDef: {
cell: <Skeleton className="h-4 w-full bg-primary-1/30" />,
}
},
getContext: () => null
})),
})), [])
const rows = table.refineCore.tableQueryResult.isLoading ? loadingRows : table.getRowModel().rows
return (
<>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header, index) => {
return (
<TableHead key={`FileDataTableHeader_${index}`} style={{ width: header.getSize() }}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{rows.length ? (
rows.map((row, index) => (
<TableRow
key={`FileDataTableRow_${index}`}
data-state={row.getIsSelected() && "selected"}
className="group"
>
{row.getVisibleCells().map((cell, index) => (
<TableCell key={`FileDataTableCell_${index}`}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<DataTablePagination table={table} />
</>
)
}

View File

@ -0,0 +1,37 @@
import { FolderIcon, MoreIcon, RecentIcon } from "./icons";
export enum FileTypes {
Folder = "FOLDER",
Document = "DOCUMENT",
Image = "IMAGE",
}
interface FileCardProps {
type: FileTypes;
fileName: string;
createdAt: string;
size: string;
}
export const FileCardList = ({ children }: React.PropsWithChildren<{}>) => {
return <div className="flex flex-row gap-x-8">{children}</div>;
};
export const FileCard = ({ type, fileName, createdAt, size }: FileCardProps) => {
return (
<div className="border-1 rounded-lg p-4 w-[calc((100%/4))]">
<div className="flex justify-end">
<MoreIcon className="text-ring/50" />
</div>
<FolderIcon className="text-ring" />
<span className="block font-semibold my-4">{fileName}</span>
<div className="flex justify-between items-center">
<span className="text-primary-2 font-semibold text-sm">{size}</span>
<div className="flex items-center space-x-2 text-primary-2">
<RecentIcon />
<span className="font-semibold text-sm">{createdAt}</span>
</div>
</div>
</div>
);
};

View File

@ -1,9 +1,10 @@
import { Label } from "@radix-ui/react-label" import { Label } from "@radix-ui/react-label"
import { Input } from "./ui/input" import { Input } from "./ui/input"
import { FieldName, useInputControl } from "@conform-to/react" import { type FieldName, useInputControl } from "@conform-to/react"
import { useId } from "react" import { useId } from "react"
import { cn } from "~/utils" import { cn } from "~/utils"
import { Checkbox } from "~/components/ui/checkbox" import { Checkbox } from "~/components/ui/checkbox"
import { Textarea } from "./ui/textarea"
export const Field = ({ export const Field = ({
inputProps, inputProps,
@ -23,9 +24,10 @@ export const Field = ({
const errorId = errors?.length ? `${id}-error` : undefined const errorId = errors?.length ? `${id}-error` : undefined
return ( return (
<div className={className}> <div className={className}>
<Label {...labelProps} htmlFor={id} /> <Label {...labelProps} htmlFor={id} className="font-semibold text-sm" />
<Input <Input
{...inputProps} {...inputProps}
className="mt-4"
id={id} id={id}
aria-invalid={errorId ? true : undefined} aria-invalid={errorId ? true : undefined}
aria-describedby={errorId} aria-describedby={errorId}
@ -57,44 +59,76 @@ export const FieldCheckbox = ({
const input = useInputControl({ const input = useInputControl({
key, key,
name: inputProps.name, name: inputProps.name,
formId: inputProps.form, formId: inputProps.form,
initialValue: defaultChecked ? checkedValue : undefined initialValue: defaultChecked ? checkedValue : undefined
}) })
const fallbackId = useId() const fallbackId = useId()
const id = inputProps.id ?? fallbackId const id = inputProps.id ?? fallbackId
const errorId = errors?.length ? `${id}-error` : undefined const errorId = errors?.length ? `${id}-error` : undefined
return ( return (
<div <>
className={cn("space-x-2 flex items-center text-primary-2", className)} <div
> className={cn("space-x-2 flex items-center text-primary-2", className)}
<Checkbox >
{...checkboxProps} <Checkbox
id={id} {...checkboxProps}
aria-invalid={errorId ? true : undefined} id={id}
aria-describedby={errorId} aria-invalid={errorId ? true : undefined}
checked={input.value === checkedValue} aria-describedby={errorId}
onCheckedChange={(state) => { checked={input.value === checkedValue}
input.change(state.valueOf() ? checkedValue : "") onCheckedChange={(state) => {
inputProps.onCheckedChange?.(state) input.change(state.valueOf() ? checkedValue : "")
}} inputProps.onCheckedChange?.(state)
onFocus={(event) => { }}
input.focus() onFocus={(event) => {
inputProps.onFocus?.(event) input.focus()
}} inputProps.onFocus?.(event)
onBlur={(event) => { }}
input.blur() onBlur={(event) => {
inputProps.onBlur?.(event) input.blur()
}} inputProps.onBlur?.(event)
type="button" }}
/> type="button"
<Label {...labelProps} htmlFor={id} /> />
<Label {...labelProps} htmlFor={id} />
</div>
<div className="min-h-[32px] px-4 pb-3 pt-1"> <div className="min-h-[32px] px-4 pb-3 pt-1">
{errorId ? <ErrorList id={errorId} errors={errors} /> : null} {errorId ? <ErrorList id={errorId} errors={errors} /> : null}
</div> </div>
</div> </>
) )
} }
export function TextareaField({
labelProps,
textareaProps,
errors,
className,
}: {
labelProps: React.LabelHTMLAttributes<HTMLLabelElement>
textareaProps: React.TextareaHTMLAttributes<HTMLTextAreaElement>
errors?: ListOfErrors
className?: string
}) {
const fallbackId = useId()
const id = textareaProps.id ?? textareaProps.name ?? fallbackId
const errorId = errors?.length ? `${id}-error` : undefined
return (
<div className={className}>
<Label htmlFor={id} {...labelProps} />
<Textarea
id={id}
aria-invalid={errorId ? true : undefined}
aria-describedby={errorId}
{...textareaProps}
/>
<div className="min-h-[32px] pb-1 pt-1">
{errorId ? <ErrorList id={errorId} errors={errors} /> : null}
</div>
</div>
)
}
export type ListOfErrors = Array<string | null | undefined> | null | undefined export type ListOfErrors = Array<string | null | undefined> | null | undefined
export function ErrorList({ export function ErrorList({
id, id,
@ -108,7 +142,7 @@ export function ErrorList({
return ( return (
<ul id={id} className="flex flex-col gap-1"> <ul id={id} className="flex flex-col gap-1">
{errorsToRender.map((e) => ( {errorsToRender.map((e) => (
<li key={e} className="text-[10px] text-foreground-destructive"> <li key={e} className="text-[12px] text-destructive-foreground">
{e} {e}
</li> </li>
))} ))}

View File

@ -0,0 +1,384 @@
import { Button } from "~/components/ui/button";
import logoPng from "~/images/lume-logo.png?url";
import lumeColorLogoPng from "~/images/lume-color-logo.png?url";
import discordLogoPng from "~/images/discord-logo.png?url";
import { Link, useLocation } from "@remix-run/react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "~/components/ui/dialog";
import { useUppy } from "./lib/uppy";
import type { FailedUppyFile, UppyFile } from "@uppy/core";
import { Progress } from "~/components/ui/progress";
import { DialogClose } from "@radix-ui/react-dialog";
import { ChevronDownIcon, ExitIcon, TrashIcon } from "@radix-ui/react-icons";
import {
ClockIcon,
DriveIcon,
CircleLockIcon,
CloudUploadIcon,
CloudCheckIcon,
BoxCheckedIcon,
PageIcon,
ThemeIcon,
ExclamationCircleIcon,
} from "./icons";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
} from "./ui/dropdown-menu";
import { Avatar } from "@radix-ui/react-avatar";
import { cn } from "~/utils";
import { useGetIdentity, useLogout } from "@refinedev/core";
import { PinningNetworkBanner } from "./pinning-network-banner";
import { PinningProvider } from "~/providers/PinningProvider";
import type { Identity } from "~/data/auth-provider";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
TooltipProvider,
} from "./ui/tooltip";
import filesize from "./lib/filesize";
import { ErrorList } from "./forms";
export const GeneralLayout = ({ children }: React.PropsWithChildren) => {
const location = useLocation();
const { data: identity } = useGetIdentity<Identity>();
const { mutate: logout } = useLogout();
return (
<PinningProvider>
{!identity?.verified ? (
<div className="bg-primary-1 text-primary-1-foreground p-4">
We have sent you a verification email. Please click on the link in the
email to start using the platform.
</div>
) : null}
<div className={"h-full flex flex-row"}>
<header className="p-10 pr-0 flex flex-col w-[240px] h-full scroll-m-0 overflow-hidden">
<img src={logoPng} alt="Lume logo" className="h-10 w-32" />
<nav className="my-10 flex-1">
<ul>
<li>
<Link to="/dashboard">
<NavigationButton
active={location.pathname.includes("dashboard")}>
<ClockIcon className="w-5 h-5 mr-2" />
Dashboard
</NavigationButton>
</Link>
</li>
<li>
<Link to="/file-manager">
<NavigationButton
active={location.pathname.includes("file-manager")}>
<DriveIcon className="w-5 h-5 mr-2" />
File Manager
</NavigationButton>
</Link>
</li>
<li>
<Link to="/account">
<NavigationButton
active={location.pathname.includes("account")}>
<CircleLockIcon className="w-5 h-5 mr-2" />
Account
</NavigationButton>
</Link>
</li>
</ul>
</nav>
<span className="text-primary-2 mb-3 -space-y-1 opacity-40">
<p>Freedom</p>
<p>Privacy</p>
<p>Ownership</p>
</span>
<Dialog>
<DialogTrigger asChild>
<Button size={"lg"} className="w-[calc(100%-3rem)] font-semibold">
<CloudUploadIcon className="w-5 h-5 mr-2" />
Upload Files
</Button>
</DialogTrigger>
<DialogContent className="border rounded-lg p-8">
<UploadFileForm />
</DialogContent>
</Dialog>
</header>
<div className="flex-1 overflow-y-auto p-10">
<div className="flex items-center gap-x-4 justify-end">
<Button variant="ghost" className="rounded-full w-fit">
<ThemeIcon className="text-ring" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="border rounded-full h-auto p-2 gap-x-2 text-ring font-semibold">
<Avatar className="bg-ring h-7 w-7 rounded-full" />
{`${identity?.firstName} ${identity?.lastName}`}
<ChevronDownIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => logout()}>
<ExitIcon className="mr-2" />
Logout
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
{children}
<footer className="mt-5">
<ul className="flex flex-row">
<li>
<Link to="https://discord.lumeweb.com">
<Button
variant={"link"}
className="flex flex-row gap-x-2 text-input-placeholder">
<img
className="h-5"
src={discordLogoPng}
alt="Discord Logo"
/>
Connect with us
</Button>
</Link>
</li>
<li>
<Link to="https://lumeweb.com">
<Button
variant={"link"}
className="flex flex-row gap-x-2 text-input-placeholder">
<img
className="h-5"
src={lumeColorLogoPng}
alt="Lume Logo"
/>
Connect with us
</Button>
</Link>
</li>
</ul>
</footer>
</div>
</div>
<PinningNetworkBanner />
</PinningProvider>
);
};
const UploadFileForm = () => {
const {
getRootProps,
getInputProps,
getFiles,
state,
removeFile,
cancelAll,
failedFiles,
upload,
} = useUppy();
const inputProps = getInputProps();
const isUploading = state === "uploading";
const isCompleted = state === "completed";
const hasErrored = state === "error";
const hasStarted = state !== "idle" && state !== "initializing";
const isValid = getFiles().length > 0;
const getFailedState = (id: string) =>
failedFiles.find((file) => file.id === id);
return (
<>
<DialogHeader className="mb-6">
<DialogTitle>Upload Files</DialogTitle>
</DialogHeader>
{!hasStarted ? (
<div
{...getRootProps()}
className="border border-border rounded text-primary-2 bg-primary-dark h-48 flex flex-col items-center justify-center">
<input
hidden
aria-hidden
key={new Date().toISOString()}
multiple
name="uppyFiles[]"
{...inputProps}
/>
<CloudUploadIcon className="w-24 h-24 stroke stroke-primary-dark" />
<p>Drag & Drop Files or Browse</p>
</div>
) : null}
<div className="w-full space-y-3 max-h-48 overflow-y-auto">
{getFiles().map((file) => (
<UploadFileItem
key={file.id}
file={file}
onRemove={(id) => {
removeFile(id);
}}
failedState={getFailedState(file.id)}
/>
))}
</div>
<ErrorList errors={[...(hasErrored ? ["An error occurred"] : [])]} />
{hasStarted && !hasErrored ? (
<div className="flex flex-col items-center gap-y-2 w-full text-primary-1">
<CloudCheckIcon className="w-32 h-32" />
{isCompleted
? "Upload completed"
: `${getFiles().length} files being uploaded`}
</div>
) : null}
{isUploading ? (
<DialogClose asChild onClick={cancelAll}>
<Button type="button" size={"lg"} className="mt-6">
Cancel
</Button>
</DialogClose>
) : null}
{isCompleted ? (
<DialogClose asChild>
<Button type="button" size={"lg"} className="mt-6">
Close
</Button>
</DialogClose>
) : null}
{!hasStarted && !isCompleted && !isUploading ? (
<Button
type="submit"
size={"lg"}
onClick={isValid ? upload : () => {}}
className="mt-6"
disabled={!isValid}>
Upload
</Button>
) : null}
</>
);
};
const UploadFileItem = ({
file,
failedState,
onRemove,
}: {
file: UppyFile;
failedState?: FailedUppyFile<Record<string, any>, Record<string, any>>;
onRemove: (id: string) => void;
}) => {
console.log({ file: file.progress });
return (
<div className="flex flex-col w-full py-4 px-2 bg-primary-dark">
<div
className={`flex items-center justify-between ${
failedState ? "text-red-500" : "text-primary-1"
}`}>
<div className="flex items-center">
<div className="p-2">
{file.progress?.uploadComplete ? (
<BoxCheckedIcon className="w-4 h-4" />
) : failedState?.error ? (
<ExclamationCircleIcon className="w-4 h-4" />
) : (
<PageIcon className="w-4 h-4" />
)}
</div>
<TooltipProvider>
<Tooltip delayDuration={500}>
<TooltipTrigger>
<p className="w-full flex justify-between items-center">
<span className="truncate text-ellipsis max-w-[20ch]">
{file.name}
</span>{" "}
<span>({filesize(file.size, 2)})</span>
</p>
</TooltipTrigger>
<TooltipContent>
<p>
{file.name} ({filesize(file.size, 2)})
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Button
size={"icon"}
variant={"ghost"}
className="!text-inherit"
onClick={() => onRemove(file.id)}>
<TrashIcon className="w-4 h-4" />
</Button>
</div>
{failedState ? (
<div className="mt-2 text-red-500 text-sm">
<p>Error uploading: {failedState.error}</p>
<div className="flex gap-2">
<Button
size={"sm"}
onClick={() => {
/* Retry upload function here */
}}>
Retry
</Button>
<Button
size={"sm"}
variant={"outline"}
onClick={() => onRemove(file.id)}>
Remove
</Button>
</div>
</div>
) : null}
{file.progress?.preprocess ? (
<div>
<p className="text-sm text-primary-2 ml-2">{file.progress?.preprocess?.message ?? "Processing..."}</p>
<Progress max={100} value={
file.progress?.preprocess?.value ?? 0} className="mt-2" />
</div>
) : null}
{file.progress?.uploadStarted && !file.progress.uploadComplete ? (
<div>
<p className="text-sm text-primary-2 ml-2">Uploading...</p>
<Progress max={100} value={file.progress.percentage} className="mt-2" />
</div>
) : null}
</div>
);
};
const NavigationButton = ({
children,
active,
}: React.PropsWithChildren<{ active?: boolean }>) => {
return (
<Button
variant="ghost"
className={cn(
"justify-start h-14 w-full font-semibold",
active && "bg-secondary-1 text-secondary-1-foreground",
)}>
{children}
</Button>
);
};

610
app/components/icons.tsx Normal file
View File

@ -0,0 +1,610 @@
export const InfoIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="23"
height="24"
viewBox="0 0 23 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<g clipPath="url(#clip0_295_884)">
<path
d="M11.5067 2C6.21422 2 1.92755 6.475 1.92755 12C1.92755 17.525 6.21422 22 11.5067 22C16.7992 22 21.0859 17.525 21.0859 12C21.0859 6.475 16.7992 2 11.5067 2ZM12.4646 17H10.5488V11H12.4646V17ZM12.4646 9H10.5488V7H12.4646V9Z"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_295_884">
<rect
width="22.99"
height="24"
fill="currentColor"
transform="translate(0.0117188)"
/>
</clipPath>
</defs>
</svg>
);
};
export const AddIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<g clipPath="url(#clip0_323_1258)">
<path
d="M9 1.5C4.85625 1.5 1.5 4.85625 1.5 9C1.5 13.1438 4.85625 16.5 9 16.5C13.1438 16.5 16.5 13.1438 16.5 9C16.5 4.85625 13.1438 1.5 9 1.5ZM12.75 9.75H9.75V12.75H8.25V9.75H5.25V8.25H8.25V5.25H9.75V8.25H12.75V9.75Z"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_323_1258">
<rect width="18" height="18" fill="currentColor" />
</clipPath>
</defs>
</svg>
);
};
export const CloudIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="23"
height="21"
viewBox="0 0 23 21"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
d="M19.5194 13.3041C19.5166 13.3041 19.5124 13.3056 19.5095 13.3056C19.7518 12.1637 19.7036 10.917 19.0902 9.59094C18.2217 7.7138 16.4438 6.31126 14.3938 6.05058C11.5434 5.6879 9.02304 7.31287 8.01859 9.72836C7.68 9.59802 7.31165 9.52577 6.92631 9.52577C5.188 9.52577 3.77838 10.9354 3.77838 12.6737C3.77838 12.923 3.81379 13.1611 3.86905 13.3934C3.11394 13.2163 2.28658 13.2361 1.28355 14.0465C0.53836 14.65 -0.00990665 15.5284 1.03265e-05 16.4875C0.0198443 18.2102 1.42097 19.6 3.14794 19.6H19.3679C20.8398 19.6 22.2282 18.6636 22.5725 17.2327C23.0726 15.1558 21.5128 13.3041 19.5194 13.3041Z"
fill="currentColor"
/>
</svg>
);
};
export const CloudDownloadIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="24"
height="24"
viewBox="0 0 21 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M16.4554 5.625C15.8616 2.4375 13.317 0 10.1786 0C7.71875 0 5.59821 1.5 4.58036 3.75C1.95089 4.125 0 6.46875 0 9.375C0 12.4688 2.29018 15 5.08928 15H16.1161C18.4911 15 20.3571 12.9375 20.3571 10.3125C20.3571 7.875 18.5759 5.8125 16.4554 5.625ZM14.4196 8.4375L10.1786 13.125L5.9375 8.4375H8.48214V4.6875H11.875V8.4375H14.4196Z"
fill="currentColor"
/>
</svg>
);
};
export const CloudUploadIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="21"
height="17"
viewBox="-0.5 0 21 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
d="M16.6551 4.54509C16.0058 3.1411 14.8851 1.97617 13.4647 1.22882C12.0444 0.481467 10.4029 0.192923 8.79158 0.4074C7.18031 0.621877 5.68822 1.32754 4.54396 2.41627C3.39969 3.50499 2.66638 4.91671 2.45638 6.43509C1.44311 6.66341 0.554244 7.23392 -0.0414276 8.03828C-0.637099 8.84264 -0.898085 9.82482 -0.774825 10.7983C-0.651564 11.7718 -0.15264 12.6688 0.627199 13.319C1.40704 13.9691 2.41348 14.3272 3.4554 14.3251C3.73727 14.3251 4.00759 14.2197 4.2069 14.0322C4.40621 13.8447 4.51818 13.5903 4.51818 13.3251C4.51818 13.0599 4.40621 12.8055 4.2069 12.618C4.00759 12.4304 3.73727 12.3251 3.4554 12.3251C2.89166 12.3251 2.35102 12.1144 1.9524 11.7393C1.55378 11.3642 1.32984 10.8555 1.32984 10.3251C1.32984 9.79466 1.55378 9.28595 1.9524 8.91088C2.35102 8.5358 2.89166 8.32509 3.4554 8.32509C3.73727 8.32509 4.00759 8.21973 4.2069 8.0322C4.40621 7.84466 4.51818 7.59031 4.51818 7.32509C4.52089 6.14237 4.9691 4.99884 5.78317 4.09767C6.59725 3.1965 7.72447 2.59604 8.96458 2.40296C10.2047 2.20989 11.4774 2.4367 12.5566 3.0431C13.6358 3.6495 14.4516 4.59623 14.859 5.71509C14.9198 5.88693 15.029 6.04002 15.175 6.15802C15.321 6.27603 15.4983 6.35451 15.688 6.38509C16.3959 6.51096 17.0376 6.85869 17.5086 7.37162C17.9795 7.88456 18.252 8.53245 18.2816 9.20973C18.3112 9.88701 18.0961 10.5538 17.6715 11.1013C17.2468 11.6489 16.6376 12.045 15.9431 12.2251C15.6697 12.2914 15.4354 12.4572 15.2919 12.686C15.1484 12.9148 15.1074 13.1878 15.1779 13.4451C15.2483 13.7024 15.4245 13.9227 15.6677 14.0578C15.9108 14.1928 16.201 14.2314 16.4745 14.1651C17.5929 13.887 18.5844 13.2731 19.2983 12.4166C20.0122 11.56 20.4095 10.5077 20.4299 9.41936C20.4504 8.33102 20.0928 7.26613 19.4115 6.38641C18.7302 5.50669 17.7624 4.86019 16.6551 4.54509ZM10.5867 6.61509C10.4856 6.52405 10.3664 6.45268 10.2359 6.40509C9.97719 6.30507 9.68697 6.30507 9.42822 6.40509C9.29777 6.45268 9.17858 6.52405 9.07751 6.61509L5.88916 9.61509C5.68904 9.80339 5.57661 10.0588 5.57661 10.3251C5.57661 10.5914 5.68904 10.8468 5.88916 11.0351C6.08929 11.2234 6.36072 11.3292 6.64374 11.3292C6.92676 11.3292 7.19819 11.2234 7.39831 11.0351L8.7693 9.73509V15.3251C8.7693 15.5903 8.88127 15.8447 9.08058 16.0322C9.27989 16.2197 9.55021 16.3251 9.83208 16.3251C10.1139 16.3251 10.3843 16.2197 10.5836 16.0322C10.7829 15.8447 10.8949 15.5903 10.8949 15.3251V9.73509L12.2658 11.0351C12.3646 11.1288 12.4822 11.2032 12.6117 11.254C12.7412 11.3048 12.8801 11.3309 13.0204 11.3309C13.1607 11.3309 13.2996 11.3048 13.4291 11.254C13.5587 11.2032 13.6762 11.1288 13.775 11.0351C13.8746 10.9421 13.9537 10.8315 14.0076 10.7097C14.0616 10.5878 14.0894 10.4571 14.0894 10.3251C14.0894 10.1931 14.0616 10.0624 14.0076 9.94051C13.9537 9.81865 13.8746 9.70805 13.775 9.61509L10.5867 6.61509Z"
fill="currentColor"
/>
</svg>
);
};
export const CloudUploadSolidIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="24"
height="16"
viewBox="0 0 24 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
d="M19.4 6C18.7 2.6 15.7 0 12 0C9.1 0 6.6 1.6 5.4 4C2.3 4.4 0 6.9 0 10C0 13.3 2.7 16 6 16H19C21.8 16 24 13.8 24 11C24 8.4 21.9 6.2 19.4 6ZM14 9V13H10V9H7L12 4L17 9H14Z"
fill="currentColor"
/>
</svg>
);
};
export const CrownIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="20"
height="15"
viewBox="0 0 20 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
d="M18.649 3.74215C18.4702 3.59292 18.2531 3.49694 18.0224 3.46517C17.7917 3.4334 17.5568 3.46711 17.3443 3.56246L13.3912 5.32028L11.0943 1.17965C10.9845 0.986336 10.8255 0.825572 10.6334 0.713731C10.4412 0.60189 10.2229 0.542969 10.0006 0.542969C9.77826 0.542969 9.55992 0.60189 9.36779 0.713731C9.17566 0.825572 9.0166 0.986336 8.90682 1.17965L6.60995 5.32028L2.65682 3.56246C2.44394 3.46725 2.20866 3.43349 1.97759 3.465C1.74652 3.49651 1.52888 3.59203 1.34926 3.74077C1.16964 3.8895 1.03521 4.08552 0.961163 4.30666C0.887119 4.5278 0.876414 4.76525 0.930259 4.99215L2.91463 13.4531C2.95258 13.6169 3.02338 13.7713 3.12276 13.9069C3.22213 14.0426 3.34801 14.1566 3.49276 14.2422C3.68873 14.3595 3.9128 14.4215 4.1412 14.4218C4.25222 14.4216 4.36268 14.4059 4.46932 14.375C8.08637 13.3749 11.907 13.3749 15.524 14.375C15.8543 14.4618 16.2055 14.414 16.5006 14.2422C16.6462 14.1577 16.7728 14.044 16.8723 13.9082C16.9718 13.7724 17.0421 13.6174 17.0787 13.4531L19.0709 4.99215C19.1241 4.76518 19.1128 4.52785 19.0383 4.30696C18.9637 4.08608 18.8289 3.89044 18.649 3.74215Z"
fill="currentColor"
/>
</svg>
);
};
export const PersonIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
d="M9.99935 1.6665C5.39518 1.6665 1.66602 5.39567 1.66602 9.99984C1.66602 14.604 5.39518 18.3332 9.99935 18.3332C14.6035 18.3332 18.3327 14.604 18.3327 9.99984C18.3327 5.39567 14.6035 1.6665 9.99935 1.6665ZM9.99935 4.1665C11.3785 4.1665 12.4993 5.28734 12.4993 6.6665C12.4993 8.04984 11.3785 9.1665 9.99935 9.1665C8.62018 9.1665 7.49935 8.04984 7.49935 6.6665C7.49935 5.28734 8.62018 4.1665 9.99935 4.1665ZM9.99935 15.9998C7.91185 15.9998 6.07852 14.9332 4.99935 13.3165C5.02018 11.6623 8.33685 10.7498 9.99935 10.7498C11.6618 10.7498 14.9743 11.6623 14.9993 13.3165C13.9202 14.9332 12.0868 15.9998 9.99935 15.9998Z"
fill="currentColor"
/>
</svg>
);
};
export const CheckRoundedIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="18"
height="17"
viewBox="0 0 18 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
d="M9 1C7.36831 1 5.77325 1.48385 4.41655 2.39038C3.05984 3.2969 2.00242 4.58537 1.378 6.09286C0.753575 7.60035 0.590197 9.25915 0.908525 10.8595C1.22685 12.4598 2.01259 13.9298 3.16637 15.0836C4.32016 16.2374 5.79017 17.0231 7.39051 17.3415C8.99085 17.6598 10.6497 17.4964 12.1571 16.872C13.6646 16.2476 14.9531 15.1902 15.8596 13.8335C16.7661 12.4767 17.25 10.8817 17.25 9.25C17.25 7.06196 16.3808 4.96354 14.8336 3.41637C13.2865 1.86919 11.188 1 9 1ZM13.2803 7.53025L8.03025 12.7802C7.88961 12.9209 7.69888 12.9998 7.5 12.9998C7.30113 12.9998 7.1104 12.9209 6.96975 12.7802L4.71975 10.5302C4.58314 10.3888 4.50754 10.1993 4.50925 10.0027C4.51096 9.80605 4.58983 9.61794 4.72889 9.47889C4.86795 9.33983 5.05606 9.26095 5.2527 9.25924C5.44935 9.25753 5.6388 9.33313 5.78025 9.46975L7.5 11.1895L12.2198 6.46975C12.3612 6.33313 12.5507 6.25754 12.7473 6.25924C12.944 6.26095 13.1321 6.33983 13.2711 6.47889C13.4102 6.61794 13.4891 6.80605 13.4908 7.0027C13.4925 7.19935 13.4169 7.3888 13.2803 7.53025Z"
fill="currentColor"
/>
</svg>
);
};
export const ClockIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="23"
height="23"
viewBox="0 0 23 23"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
d="M11.1703 1.8623C16.3127 1.8623 20.4812 6.03079 20.4812 11.1732C20.4812 16.3156 16.3127 20.4841 11.1703 20.4841C6.02786 20.4841 1.85938 16.3156 1.85938 11.1732C1.85938 6.03079 6.02786 1.8623 11.1703 1.8623ZM15.4496 6.89391C15.2596 6.70304 14.9598 6.67697 14.7391 6.83153C12.0483 8.71978 10.5306 9.83895 10.1824 10.1853C9.63769 10.7309 9.63769 11.6155 10.1824 12.1611C10.728 12.7058 11.6125 12.7058 12.1582 12.1611C12.3621 11.9562 13.4784 10.4376 15.5082 7.60154C15.6646 7.38366 15.6395 7.08385 15.4496 6.89391ZM16.2913 10.2421C15.7773 10.2421 15.3602 10.6592 15.3602 11.1732C15.3602 11.6872 15.7773 12.1043 16.2913 12.1043C16.8052 12.1043 17.2223 11.6872 17.2223 11.1732C17.2223 10.6592 16.8052 10.2421 16.2913 10.2421ZM6.04928 10.2421C5.53532 10.2421 5.11819 10.6592 5.11819 11.1732C5.11819 11.6872 5.53532 12.1043 6.04928 12.1043C6.56324 12.1043 6.98037 11.6872 6.98037 11.1732C6.98037 10.6592 6.56324 10.2421 6.04928 10.2421ZM8.20754 6.89391C7.84442 6.53079 7.25411 6.53079 6.89098 6.89391C6.52786 7.25704 6.52786 7.84642 6.89098 8.21047C7.25411 8.5736 7.84349 8.5736 8.20754 8.21047C8.57067 7.84735 8.57067 7.25704 8.20754 6.89391ZM11.1703 5.12112C10.6563 5.12112 10.2392 5.53825 10.2392 6.05221C10.2392 6.56617 10.6563 6.9833 11.1703 6.9833C11.6842 6.9833 12.1014 6.56617 12.1014 6.05221C12.1014 5.53825 11.6842 5.12112 11.1703 5.12112Z"
fill="currentColor"
/>
</svg>
);
};
export const CircleLockIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="23"
height="24"
viewBox="0 0 23 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
d="M11.1722 2.56738C16.3146 2.56738 20.4831 6.73587 20.4831 11.8783C20.4831 13.8671 19.8593 15.7106 18.7969 17.2237L15.8277 11.8783H18.6209C18.6208 10.1615 18.0277 8.49755 16.9419 7.16779C15.8561 5.83804 14.3443 4.92415 12.6623 4.58073C10.9802 4.23731 9.23121 4.48544 7.71106 5.28315C6.19092 6.08086 4.99298 7.37917 4.31989 8.95846C3.64681 10.5377 3.5399 12.301 4.01726 13.9501C4.49461 15.5991 5.52693 17.0326 6.93957 18.0082C8.3522 18.9837 10.0584 19.4413 11.7697 19.3036C13.4809 19.1659 15.092 18.4414 16.3305 17.2525L17.2597 18.9238C15.5699 20.388 13.4081 21.1925 11.1722 21.1892C6.02982 21.1892 1.86133 17.0207 1.86133 11.8783C1.86133 6.73587 6.02982 2.56738 11.1722 2.56738ZM11.1722 7.22283C11.913 7.22283 12.6235 7.51712 13.1474 8.04096C13.6712 8.5648 13.9655 9.27528 13.9655 10.0161V10.9472H14.8966V15.6026H7.44786V10.9472H8.37895V10.0161C8.37895 9.27528 8.67324 8.5648 9.19708 8.04096C9.72092 7.51712 10.4314 7.22283 11.1722 7.22283ZM11.1722 9.08501C10.9442 9.08504 10.7241 9.16877 10.5536 9.32031C10.3832 9.47185 10.2743 9.68067 10.2477 9.90716L10.2411 10.0161V10.9472H12.1033V10.0161C12.1033 9.78804 12.0196 9.56793 11.868 9.39751C11.7165 9.22709 11.5076 9.11821 11.2812 9.09153L11.1722 9.08501Z"
fill="currentColor"
/>
</svg>
);
};
export const DriveIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="23"
height="24"
viewBox="0 0 23 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
d="M10.2417 2.68262V7.33806H7.44842L11.1728 11.0624L14.8971 7.33806H12.1039V2.68262H18.6215C18.8684 2.68262 19.1053 2.78071 19.2799 2.95533C19.4545 3.12994 19.5526 3.36677 19.5526 3.61371V20.3733C19.5526 20.6203 19.4545 20.8571 19.2799 21.0317C19.1053 21.2063 18.8684 21.3044 18.6215 21.3044H3.72406C3.47712 21.3044 3.24029 21.2063 3.06568 21.0317C2.89107 20.8571 2.79297 20.6203 2.79297 20.3733V3.61371C2.79297 3.36677 2.89107 3.12994 3.06568 2.95533C3.24029 2.78071 3.47712 2.68262 3.72406 2.68262H10.2417ZM17.6904 15.7179H4.65515V19.4422H17.6904V15.7179ZM15.8282 16.649V18.5111H13.966V16.649H15.8282Z"
fill="currentColor"
/>
</svg>
);
};
export const PageIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="21"
height="21"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
d="M10.3276 4.21337V15.0287H0.59375V0.96875H5.46067"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M5.46191 0.96875L10.3288 4.21337"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M5.46191 0.96875V4.21337"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M10.3288 4.21289H5.46191"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M2.75684 8.53906H5.46068"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M2.75684 10.7021H7.62376"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
export const TrashIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="21"
height="21"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
d="M10.0769 14.3093H2.69194C2.3247 14.3093 1.9725 14.1634 1.71282 13.9037C1.45314 13.6441 1.30725 13.2919 1.30725 12.9246V4.15492C1.30725 4.03251 1.35588 3.91511 1.44244 3.82855C1.529 3.74199 1.6464 3.69336 1.76881 3.69336C1.89123 3.69336 2.00863 3.74199 2.09519 3.82855C2.18175 3.91511 2.23038 4.03251 2.23038 4.15492V12.9246C2.23038 13.047 2.279 13.1644 2.36556 13.251C2.45212 13.3375 2.56952 13.3862 2.69194 13.3862H10.0769C10.1994 13.3862 10.3168 13.3375 10.4033 13.251C10.4899 13.1644 10.5385 13.047 10.5385 12.9246V4.15492C10.5385 4.03251 10.5871 3.91511 10.6737 3.82855C10.7602 3.74199 10.8776 3.69336 11.0001 3.69336C11.1225 3.69336 11.2399 3.74199 11.3264 3.82855C11.413 3.91511 11.4616 4.03251 11.4616 4.15492V12.9246C11.4616 13.2919 11.3157 13.6441 11.0561 13.9037C10.7964 14.1634 10.4442 14.3093 10.0769 14.3093Z"
fill="currentColor"
/>
<path
d="M11.9237 3.23172H0.846206C0.723792 3.23172 0.606392 3.18309 0.519832 3.09653C0.433272 3.00997 0.384644 2.89257 0.384644 2.77016C0.384644 2.64774 0.433272 2.53034 0.519832 2.44378C0.606392 2.35722 0.723792 2.30859 0.846206 2.30859H11.9237C12.0461 2.30859 12.1635 2.35722 12.2501 2.44378C12.3366 2.53034 12.3853 2.64774 12.3853 2.77016C12.3853 2.89257 12.3366 3.00997 12.2501 3.09653C12.1635 3.18309 12.0461 3.23172 11.9237 3.23172Z"
fill="currentColor"
/>
<path
d="M8.23121 3.23129C8.1088 3.23129 7.9914 3.18266 7.90484 3.0961C7.81828 3.00954 7.76965 2.89214 7.76965 2.76973V1.38504H5.00027V2.76973C5.00027 2.89214 4.95164 3.00954 4.86508 3.0961C4.77853 3.18266 4.66112 3.23129 4.53871 3.23129C4.4163 3.23129 4.2989 3.18266 4.21234 3.0961C4.12578 3.00954 4.07715 2.89214 4.07715 2.76973V0.923477C4.07715 0.801063 4.12578 0.683662 4.21234 0.597103C4.2989 0.510543 4.4163 0.461914 4.53871 0.461914H8.23121C8.35362 0.461914 8.47103 0.510543 8.55759 0.597103C8.64415 0.683662 8.69277 0.801063 8.69277 0.923477V2.76973C8.69277 2.89214 8.64415 3.00954 8.55759 3.0961C8.47103 3.18266 8.35362 3.23129 8.23121 3.23129Z"
fill="currentColor"
/>
<path
d="M6.3849 12.0012C6.26249 12.0012 6.14509 11.9526 6.05853 11.866C5.97197 11.7795 5.92334 11.6621 5.92334 11.5396V5.07777C5.92334 4.95536 5.97197 4.83796 6.05853 4.7514C6.14509 4.66484 6.26249 4.61621 6.3849 4.61621C6.50732 4.61621 6.62472 4.66484 6.71128 4.7514C6.79784 4.83796 6.84646 4.95536 6.84646 5.07777V11.5396C6.84646 11.6621 6.79784 11.7795 6.71128 11.866C6.62472 11.9526 6.50732 12.0012 6.3849 12.0012Z"
fill="currentColor"
/>
<path
d="M8.69313 11.0778C8.57072 11.0778 8.45332 11.0292 8.36676 10.9426C8.2802 10.8561 8.23157 10.7387 8.23157 10.6163V6.00062C8.23157 5.87821 8.2802 5.76081 8.36676 5.67425C8.45332 5.58769 8.57072 5.53906 8.69313 5.53906C8.81554 5.53906 8.93294 5.58769 9.0195 5.67425C9.10606 5.76081 9.15469 5.87821 9.15469 6.00062V10.6163C9.15469 10.7387 9.10606 10.8561 9.0195 10.9426C8.93294 11.0292 8.81554 11.0778 8.69313 11.0778Z"
fill="currentColor"
/>
<path
d="M4.07716 11.0778C3.95475 11.0778 3.83735 11.0292 3.75079 10.9426C3.66423 10.8561 3.6156 10.7387 3.6156 10.6163V6.00062C3.6156 5.87821 3.66423 5.76081 3.75079 5.67425C3.83735 5.58769 3.95475 5.53906 4.07716 5.53906C4.19958 5.53906 4.31698 5.58769 4.40354 5.67425C4.4901 5.76081 4.53873 5.87821 4.53873 6.00062V10.6163C4.53873 10.7387 4.4901 10.8561 4.40354 10.9426C4.31698 11.0292 4.19958 11.0778 4.07716 11.0778Z"
fill="currentColor"
/>
</svg>
);
};
export const CloudCheckIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="21"
height="21"
viewBox="0 0 72 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M58.2 18C56.1 7.8 47.1 0 36 0C27.3 0 19.8 4.8 16.2 12C6.9 13.2 0 20.7 0 30C0 39.9 8.1 48 18 48H57C65.4 48 72 41.4 72 33C72 25.2 65.7 18.6 58.2 18ZM30 39L19.5 28.5L23.7 24.3L30 30.6L45.6 15L49.8 19.2L30 39Z"
fill="currentColor"
/>
</svg>
);
};
export const BoxCheckedIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="21"
height="21"
viewBox="0 0 21 21"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.5 4.5H15.5C16.6046 4.5 17.5 5.39543 17.5 6.5V16.5C17.5 17.6046 16.6046 18.5 15.5 18.5H5.5C4.39543 18.5 3.5 17.6046 3.5 16.5V6.5C3.5 5.39543 4.39543 4.5 5.5 4.5Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M7.5 11.5L9.5 13.5L13.5 9.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
export const PictureIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="22"
height="22"
viewBox="0 0 22 22"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M22 19.5556V2.44444C22 1.1 20.9 0 19.5556 0H2.44444C1.1 0 0 1.1 0 2.44444V19.5556C0 20.9 1.1 22 2.44444 22H19.5556C20.9 22 22 20.9 22 19.5556ZM6.72222 12.8333L9.77778 16.5L14.0556 11L19.5556 18.3333H2.44444L6.72222 12.8333Z"
fill="currentColor"
/>
</svg>
);
};
export const XlsxIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="22"
height="22"
viewBox="0 0 22 22"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M19.5556 0H2.44444C1.1 0 0 1.1 0 2.44444V19.5556C0 20.9 1.1 22 2.44444 22H19.5556C20.9 22 22 20.9 22 19.5556V2.44444C22 1.1 20.9 0 19.5556 0ZM16.1333 17.1111H13.6889L11 12.4667L8.31111 17.1111H5.86667L9.77778 11L5.86667 4.88889H8.31111L11 9.53333L13.6889 4.88889H16.1333L12.2222 11L16.1333 17.1111Z"
fill="currentColor"
/>
</svg>
);
};
export const FileIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
d="M10.791 13.7082H3.20768C2.56339 13.7082 2.04102 13.1858 2.04102 12.5415V1.45817C2.04102 0.813879 2.56339 0.291504 3.20768 0.291504H8.74147C8.74206 0.291504 8.74293 0.291504 8.74352 0.291504H8.74935C8.84268 0.291504 8.92231 0.338462 8.97568 0.406712L11.8425 3.2735C11.911 3.32688 11.9577 3.4065 11.9577 3.49984V3.50596C11.9577 3.50655 11.9577 3.50684 11.9577 3.50742V12.5415C11.9577 13.1858 11.4353 13.7082 10.791 13.7082ZM9.04102 1.27763V3.20817H10.9716L9.04102 1.27763ZM11.3743 3.7915H8.74935C8.58806 3.7915 8.45768 3.66084 8.45768 3.49984V0.874837H3.20768C2.88568 0.874837 2.62435 1.13617 2.62435 1.45817V12.5415C2.62435 12.8635 2.88568 13.1248 3.20768 13.1248H10.791C11.113 13.1248 11.3743 12.8635 11.3743 12.5415V3.7915ZM9.62435 11.3748H4.37435C4.21306 11.3748 4.08268 11.2445 4.08268 11.0832C4.08268 10.9222 4.21306 10.7915 4.37435 10.7915H9.62435C9.78564 10.7915 9.91602 10.9222 9.91602 11.0832C9.91602 11.2445 9.78564 11.3748 9.62435 11.3748ZM9.62435 9.0415H4.37435C4.21306 9.0415 4.08268 8.91113 4.08268 8.74984C4.08268 8.58884 4.21306 8.45817 4.37435 8.45817H9.62435C9.78564 8.45817 9.91602 8.58884 9.91602 8.74984C9.91602 8.91113 9.78564 9.0415 9.62435 9.0415ZM9.62435 6.70817H4.37435C4.21306 6.70817 4.08268 6.5778 4.08268 6.4165C4.08268 6.2555 4.21306 6.12484 4.37435 6.12484H9.62435C9.78564 6.12484 9.91602 6.2555 9.91602 6.4165C9.91602 6.5778 9.78564 6.70817 9.62435 6.70817Z"
fill="currentColor"
/>
</svg>
);
};
export const MoreIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
d="M19 8H5C4.73478 8 4.48043 7.89464 4.29289 7.70711C4.10536 7.51957 4 7.26522 4 7C4 6.73478 4.10536 6.48043 4.29289 6.29289C4.48043 6.10536 4.73478 6 5 6H19C19.2652 6 19.5196 6.10536 19.7071 6.29289C19.8946 6.48043 20 6.73478 20 7C20 7.26522 19.8946 7.51957 19.7071 7.70711C19.5196 7.89464 19.2652 8 19 8Z"
fill="currentColor"
/>
<path
d="M19 13H5C4.73478 13 4.48043 12.8946 4.29289 12.7071C4.10536 12.5196 4 12.2652 4 12C4 11.7348 4.10536 11.4804 4.29289 11.2929C4.48043 11.1054 4.73478 11 5 11H19C19.2652 11 19.5196 11.1054 19.7071 11.2929C19.8946 11.4804 20 11.7348 20 12C20 12.2652 19.8946 12.5196 19.7071 12.7071C19.5196 12.8946 19.2652 13 19 13Z"
fill="currentColor"
/>
<path
d="M19 18H5C4.73478 18 4.48043 17.8946 4.29289 17.7071C4.10536 17.5196 4 17.2652 4 17C4 16.7348 4.10536 16.4804 4.29289 16.2929C4.48043 16.1054 4.73478 16 5 16H19C19.2652 16 19.5196 16.1054 19.7071 16.2929C19.8946 16.4804 20 16.7348 20 17C20 17.2652 19.8946 17.5196 19.7071 17.7071C19.5196 17.8946 19.2652 18 19 18Z"
fill="currentColor"
/>
</svg>
);
};
export const FingerPrintIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="23"
height="23"
viewBox="0 0 23 23"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
d="M11.1728 0.931152C13.3952 0.931152 15.5267 1.81402 17.0982 3.38554C18.6697 4.95706 19.5526 7.0885 19.5526 9.31096V13.0353C19.5535 14.3774 19.2318 15.7001 18.6144 16.8918C17.997 18.0835 17.1021 19.1092 16.0051 19.8826C16.4465 18.4654 16.7035 16.9673 16.7519 15.4161L16.7593 14.8975V13.0344H14.8971V14.8975L14.8944 15.1908C14.8574 17.2213 14.4039 19.2225 13.562 21.0706C12.4834 21.3893 11.3519 21.4887 10.2342 21.363C11.3872 19.5425 12.0306 17.4461 12.0974 15.2923L12.1039 14.8975V8.37987H10.2417V14.8975L10.238 15.1657C10.1874 17.2324 9.5097 19.235 8.29479 20.9077C7.40642 20.5826 6.57967 20.1091 5.84975 19.5073C6.82259 18.2772 7.38005 16.77 7.44191 15.2029L7.44843 14.8975V9.31096L7.45308 9.12474C7.47869 8.5973 7.61679 8.08142 7.85811 7.61172L7.96425 7.41899L6.61883 6.07356C5.99989 6.94167 5.64371 7.96949 5.59277 9.03443L5.58625 9.31096V14.8975L5.58252 15.107C5.54313 16.2036 5.17995 17.264 4.53877 18.1545C3.40438 16.6894 2.79013 14.8882 2.79298 13.0353V9.31096C2.79298 7.0885 3.67585 4.95706 5.24737 3.38554C6.81889 1.81402 8.95032 0.931152 11.1728 0.931152ZM11.1728 3.72442C10.0592 3.72442 9.02197 4.0503 8.15047 4.61175L7.93632 4.757L9.28081 6.10243C9.7871 5.80291 10.3575 5.62832 10.9447 5.59312L11.1728 5.5866L11.359 5.59126C12.2803 5.63731 13.1517 6.02362 13.8045 6.67537C14.4573 7.32712 14.845 8.19794 14.8925 9.11916L14.8971 9.31096V11.1731H16.7593V9.31096C16.7593 7.82932 16.1707 6.40836 15.1231 5.36068C14.0754 4.313 12.6544 3.72442 11.1728 3.72442Z"
fill="currentColor"
/>
</svg>
);
};
export const EditIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="13"
height="13"
viewBox="0 0 13 13"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
d="M7.72289 2.8954L9.96834 5.14086L3.06357 12.0456H0.818115V9.80018L7.72289 2.8954ZM8.5088 2.1095L9.79993 0.818359L12.0454 3.06381L10.7543 4.35495L8.5088 2.1095Z"
fill="currentColor"
/>
</svg>
);
};
export const ThemeIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
d="M9.99935 18.3332C14.6017 18.3332 18.3327 14.6022 18.3327 9.99984C18.3327 5.39746 14.6017 1.6665 9.99935 1.6665C5.39697 1.6665 1.66602 5.39746 1.66602 9.99984C1.66602 14.6022 5.39697 18.3332 9.99935 18.3332ZM9.99935 16.6665V3.33317C13.6813 3.33317 16.666 6.31794 16.666 9.99984C16.666 13.6818 13.6813 16.6665 9.99935 16.6665Z"
fill="currentColor"
/>
</svg>
);
};
export const ChevronDownIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="13"
height="7"
viewBox="0 0 13 7"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
d="M3.00406 0.571289C2.11315 0.571289 1.66699 1.64843 2.29695 2.2784L5.59966 5.5811C5.99018 5.97163 6.62335 5.97163 7.01387 5.58111L10.3166 2.2784C10.9465 1.64843 10.5004 0.571289 9.60948 0.571289H3.00406Z"
fill="currentColor"
/>
</svg>
);
};
export const FolderIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="28"
height="22"
viewBox="0 0 28 22"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
d="M11 0H2.75C1.2375 0 0 1.2375 0 2.75V19.25C0 20.7625 1.2375 22 2.75 22H24.75C26.2625 22 27.5 20.7625 27.5 19.25V5.5C27.5 3.9875 26.2625 2.75 24.75 2.75H13.75L11 0Z"
fill="currentColor"
/>
</svg>
);
};
export const RecentIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
d="M6 0C4.81331 0 3.65328 0.351894 2.66658 1.01118C1.67989 1.67047 0.910851 2.60754 0.456726 3.7039C0.00259972 4.80026 -0.11622 6.00666 0.115291 7.17054C0.346802 8.33443 0.918247 9.40353 1.75736 10.2426C2.59648 11.0818 3.66558 11.6532 4.82946 11.8847C5.99335 12.1162 7.19975 11.9974 8.2961 11.5433C9.39246 11.0892 10.3295 10.3201 10.9888 9.33342C11.6481 8.34673 12 7.18669 12 6C11.9983 4.40923 11.3656 2.88411 10.2407 1.75926C9.1159 0.634414 7.59077 0.00172054 6 0ZM6 11C5.0111 11 4.0444 10.7068 3.22215 10.1573C2.39991 9.60794 1.75904 8.82705 1.38061 7.91342C1.00217 6.99979 0.90315 5.99445 1.09608 5.02455C1.289 4.05464 1.76521 3.16373 2.46447 2.46447C3.16373 1.7652 4.05465 1.289 5.02455 1.09607C5.99446 0.903148 6.99979 1.00216 7.91342 1.3806C8.82705 1.75904 9.60794 2.3999 10.1574 3.22215C10.7068 4.04439 11 5.01109 11 6C10.9985 7.32564 10.4713 8.59656 9.53393 9.53393C8.59656 10.4713 7.32564 10.9985 6 11Z"
fill="currentColor"
/>
<path
d="M6.5 5.793V3C6.5 2.86739 6.44732 2.74021 6.35355 2.64645C6.25979 2.55268 6.13261 2.5 6 2.5C5.86739 2.5 5.74021 2.55268 5.64645 2.64645C5.55268 2.74021 5.5 2.86739 5.5 3V6C5.50003 6.1326 5.55273 6.25975 5.6465 6.3535L7.1465 7.8535C7.2408 7.94458 7.3671 7.99498 7.4982 7.99384C7.6293 7.9927 7.75471 7.94011 7.84741 7.84741C7.94011 7.75471 7.9927 7.6293 7.99384 7.4982C7.99498 7.3671 7.94458 7.2408 7.8535 7.1465L6.5 5.793Z"
fill="currentColor"
/>
</svg>
);
};
export const ExclamationCircleIcon = ({
className,
}: {
className?: string;
}) => {
return (
<svg
aria-hidden="true"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z"
/>
</svg>
);
};
export const DownloadIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"
/>
</svg>
);
};

View File

@ -0,0 +1,41 @@
// Copied from https://github.com/hustcc/filesize.js
const si = {
radix: 1e3,
unit: ["b", "kb", "Mb", "Gb", "Tb", "Pb", "Eb", "Zb", "Yb"],
};
const iec = {
radix: 1024,
unit: ["b", "Kib", "Mib", "Gib", "Tib", "Pib", "Eib", "Zib", "Yib"],
};
const jedec = {
radix: 1024,
unit: ["b", "Kb", "Mb", "Gb", "Tb", "Pb", "Eb", "Zb", "Yb"],
};
export const SPECS = {
si,
iec,
jedec,
} as const;
/**
* file size
* @param bytes Number of file size bytes
* @param fixed Number of decimal, default is 1.
* @param spec String of file size spec, default is jedec. Options: si, iec, jedec.
*/
export default function filesize(bytes: number, fixed = 1, spec: keyof typeof SPECS = "jedec"): string {
let _bytes = Math.abs(bytes);
const { radix, unit } = SPECS[spec];
let loop = 0;
// calculate
while (_bytes >= radix) {
_bytes /= radix;
++loop;
}
return `${_bytes.toFixed(fixed)} ${unit[loop]}`;
}

View File

@ -0,0 +1,18 @@
import React from "react";
import {Sdk} from "@lumeweb/portal-sdk";
export const SdkContext = React.createContext<
Partial<Sdk>
>({});
export const SdkContextProvider: React.FC< {sdk: Sdk, children: React.ReactNode}> = ({sdk, children}) => {
return (
<SdkContext.Provider value={sdk}>
{children}
</SdkContext.Provider>
);
};
export function useSdk(): Partial<Sdk>{
return React.useContext(SdkContext);
}

View File

@ -0,0 +1,206 @@
// Copied from https://github.com/transloadit/uppy/blob/main/packages/%40uppy/drop-target/src/index.ts
// Is a less invasive implementation that allows for better unstyled integration
import {
type Uppy,
type PluginOptions,
type BasePlugin as TUppyBasePlugin
} from "@uppy/core"
// @ts-expect-error -- Uppy types are all over the place it really is weird
import UppyBasePlugin from "@uppy/core/lib/BasePlugin"
import type { IndexedObject } from "@uppy/utils"
import getDroppedFiles from "@uppy/utils/lib/getDroppedFiles"
import toArray from "@uppy/utils/lib/toArray"
export type DropTargetOptions = PluginOptions & {
target?: HTMLElement | string | null
onDrop?: (event: DragEvent) => void
onDragOver?: (event: DragEvent) => void
onDragLeave?: (event: DragEvent) => void
}
const BasePlugin = UppyBasePlugin as typeof TUppyBasePlugin<DropTargetOptions>
type Meta = {
relativePath?: string | null
}
type Body = IndexedObject<unknown>
// Default options
const defaultOpts = {
target: null,
} satisfies DropTargetOptions
interface DragEventWithFileTransfer extends DragEvent {
dataTransfer: NonNullable<DragEvent["dataTransfer"]>
}
function isFileTransfer(event: DragEvent): event is DragEventWithFileTransfer {
return event.dataTransfer?.types?.some((type) => type === "Files") ?? false
}
/**
* Drop Target plugin
*
*/
class DropTarget<M extends Meta, B extends Body> extends BasePlugin {
static VERSION = "lume-internal"
private removeDragOverDataAttr: ReturnType<typeof setTimeout> | undefined
private nodes?: Array<HTMLElement>
public opts: DropTargetOptions
constructor(uppy: Uppy<M, B>, opts?: DropTargetOptions) {
super(uppy, { ...defaultOpts, ...opts })
this.opts = opts || defaultOpts
this.type = "acquirer"
this.id = this.opts.id || "DropTarget"
// @ts-expect-error TODO: remove in major
this.title = "Drop Target"
}
addFiles = (files: Array<File>): void => {
const descriptors = files.map((file) => ({
source: this.id,
name: file.name,
type: file.type,
data: file,
meta: {
// path of the file relative to the ancestor directory the user selected.
// e.g. 'docs/Old Prague/airbnb.pdf'
relativePath: (file as { relativePath?: string }).relativePath || null
} as Meta
}))
try {
this.uppy.addFiles(descriptors)
} catch (err) {
this.uppy.log(err as string)
}
}
handleDrop = async (event: DragEvent): Promise<void> => {
if (!isFileTransfer(event)) {
return
}
event.preventDefault()
event.stopPropagation()
clearTimeout(this.removeDragOverDataAttr)
// Remove dragover class
if (event.currentTarget) {
(event.currentTarget as HTMLElement).dataset.uppyIsDragOver = "false"
this.setPluginState({ isDraggingOver: false })
}
// Let any acquirer plugin (Url/Webcam/etc.) handle drops to the root
this.uppy.iteratePlugins((plugin) => {
if (plugin.type === "acquirer") {
// @ts-expect-error Every Plugin with .type acquirer can define handleRootDrop(event)
plugin.handleRootDrop?.(event)
}
})
// Add all dropped files, handle errors
let executedDropErrorOnce = false
const logDropError = (error: Error): void => {
this.uppy.log(error.message, "error")
// In practice all drop errors are most likely the same,
// so let's just show one to avoid overwhelming the user
if (!executedDropErrorOnce) {
this.uppy.info(error.message, "error")
executedDropErrorOnce = true
}
}
const files = await getDroppedFiles(event.dataTransfer, { logDropError })
if (files.length > 0) {
this.uppy.log("[DropTarget] Files were dropped")
this.addFiles(files)
}
this.opts.onDrop?.(event)
}
handleDragOver = (event: DragEvent): void => {
if (!isFileTransfer(event)) {
return
}
event.preventDefault()
event.stopPropagation()
// Add a small (+) icon on drop
// (and prevent browsers from interpreting this as files being _moved_ into the browser,
// https://github.com/transloadit/uppy/issues/1978)
event.dataTransfer.dropEffect = "copy" // eslint-disable-line no-param-reassign
clearTimeout(this.removeDragOverDataAttr)
;(event.currentTarget as HTMLElement).dataset.uppyIsDragOver = "true"
this.setPluginState({ isDraggingOver: true })
this.opts.onDragOver?.(event)
}
handleDragLeave = (event: DragEvent): void => {
if (!isFileTransfer(event)) {
return
}
event.preventDefault()
event.stopPropagation()
const { currentTarget } = event
clearTimeout(this.removeDragOverDataAttr)
// Timeout against flickering, this solution is taken from drag-drop library.
// Solution with 'pointer-events: none' didn't work across browsers.
this.removeDragOverDataAttr = setTimeout(() => {
(currentTarget as HTMLElement).dataset.uppyIsDragOver = "false"
this.setPluginState({ isDraggingOver: false })
}, 50)
this.opts.onDragLeave?.(event)
}
addListeners = (): void => {
const { target } = this.opts
if (target instanceof Element) {
this.nodes = [target]
} else if (typeof target === "string") {
this.nodes = toArray(document.querySelectorAll(target))
}
if (!this.nodes || this.nodes.length === 0) {
throw new Error(`"${target}" does not match any HTML elements`)
}
for (const node of this.nodes) {
node.addEventListener("dragover", this.handleDragOver, false)
node.addEventListener("dragleave", this.handleDragLeave, false)
node.addEventListener("drop", this.handleDrop, false)
}
}
removeListeners = (): void => {
if (this.nodes) {
for (const node of this.nodes) {
node.removeEventListener("dragover", this.handleDragOver, false)
node.removeEventListener("dragleave", this.handleDragLeave, false)
node.removeEventListener("drop", this.handleDrop, false)
}
}
}
install(): void {
this.setPluginState({ isDraggingOver: false })
this.addListeners()
}
uninstall(): void {
this.removeListeners()
}
}
export default DropTarget

View File

@ -0,0 +1,63 @@
import Uppy, {BasePlugin, DefaultPluginOptions} from '@uppy/core';
import {PROTOCOL_S5, Sdk} from "@lumeweb/portal-sdk";
import {S5Client} from "@lumeweb/s5-js";
import {AxiosProgressEvent} from "axios";
interface UppyFileUploadOptions extends DefaultPluginOptions {
sdk: Sdk;
}
export default class UppyFileUpload extends BasePlugin {
private _sdk: Sdk;
constructor(uppy: Uppy, opts?: UppyFileUploadOptions) {
super(uppy, opts);
this.id = opts?.id || 'file-upload';
this.type = 'uploader';
this._sdk = opts?.sdk as Sdk;
}
install() {
this.uppy.addUploader(this.handleUpload.bind(this));
}
private async handleUpload(fileIDs: string[]) {
for (const fileID of fileIDs) {
const file = this.uppy.getFile(fileID);
if (!file) {
continue;
}
// @ts-ignore
if (file.uploader !== 'file') {
continue;
}
const uploadLimit = await this._sdk.account().uploadLimit();
let data = file.data;
if (file.data instanceof Blob) {
data = new File([data], file.name, {type: file.type});
}
try {
await this._sdk.protocols().get<S5Client>(PROTOCOL_S5).getSdk().uploadFile(data as File, {
largeFileSize: uploadLimit,
onUploadProgress: (progressEvent: AxiosProgressEvent) => {
this.uppy.emit('upload-progress', this.uppy.getFile(file.id), {
uploader: this,
bytesUploaded: progressEvent.loaded,
bytesTotal: progressEvent.total,
})
}
});
this.uppy.emit('upload-success', file, {uploadURL: null});
} catch (err) {
this.uppy.emit('upload-error', file, err);
}
}
}
}

255
app/components/lib/uppy.ts Normal file
View File

@ -0,0 +1,255 @@
import Uppy, { debugLogger, FailedUppyFile, type State, type UppyFile } from "@uppy/core";
import NoopUrlStorage from "tus-js-client/lib.es5/noopUrlStorage.js";
import Tus from "@uppy/tus";
import toArray from "@uppy/utils/lib/toArray";
import {
type ChangeEvent,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import DropTarget, { type DropTargetOptions } from "./uppy-dropzone";
import { useSdk } from "~/components/lib/sdk-context";
import UppyFileUpload from "~/components/lib/uppy-file-upload";
import { PROTOCOL_S5, type Sdk } from "@lumeweb/portal-sdk";
import type { S5Client, HashProgressEvent } from "@lumeweb/s5-js";
import { useInvalidate } from "@refinedev/core";
const LISTENING_EVENTS = [
"upload-progress",
"upload",
"upload-success",
"upload-error",
"file-added",
"file-removed",
"files-added",
"preprocess-progress"
] as const;
export function useUppy() {
const invalidate = useInvalidate()
const sdk = useSdk();
const [uploadLimit, setUploadLimit] = useState<number>(0);
useEffect(() => {
async function getUploadLimit() {
try {
const limit = await sdk.account!().uploadLimit();
setUploadLimit(limit);
} catch (err) {
console.log("Error occured while fetching upload limit", err);
}
}
getUploadLimit();
}, [sdk.account]);
const inputRef = useRef<HTMLInputElement>(null);
const [targetRef, _setTargetRef] = useState<HTMLElement | null>(null);
const uppyInstance = useRef<Uppy>();
const setRef = useCallback(
(element: HTMLElement | null) => _setTargetRef(element),
[],
);
const [, setUppyState] = useState<State>();
const [state, setState] = useState<
"completed" | "idle" | "initializing" | "error" | "uploading"
>("initializing");
const [inputProps, setInputProps] = useState<{
ref: typeof inputRef;
type: "file";
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
}
>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [failedFiles, setFailedFiles] = useState<FailedUppyFile<Record<string, any>, Record<string, any>>[]>([])
const getRootProps = useMemo(
() => () => {
return {
ref: setRef,
onClick: () => {
if (inputRef.current) {
//@ts-expect-error -- dumb html
inputRef.current.value = null;
inputRef.current.click();
}
},
role: "presentation",
};
},
[setRef],
);
const removeFile = useCallback(
(id: string) => {
uppyInstance.current?.removeFile(id);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[targetRef, uppyInstance],
);
const cancelAll = useCallback(
() => uppyInstance.current?.cancelAll({ reason: "user" }),
// eslint-disable-next-line react-hooks/exhaustive-deps
[targetRef, uppyInstance],
);
useEffect(() => {
if (!targetRef) return;
const tusPreprocessor = async (fileIDs: string[]) => {
for (const fileID of fileIDs) {
const file = uppyInstance.current?.getFile(fileID) as UppyFile;
// @ts-ignore
if (file.uploader === "tus") {
const hashProgressCb = (event: HashProgressEvent) => {
uppyInstance.current?.emit("preprocess-progress", file, {
mode: "determinate",
message: "Hashing file...",
value: Math.round((event.bytes / event.total) * 100),
});
};
const options = await sdk.protocols!()
.get<S5Client>(PROTOCOL_S5)
.getSdk()
.getTusOptions(
file.data as File,
{},
{ onHashProgress: hashProgressCb },
);
uppyInstance.current?.setFileState(fileID, {
tus: options,
meta: {
...options.metadata,
...file.meta,
},
});
}
}
};
const uppy = new Uppy({
logger: debugLogger,
onBeforeUpload: (files) => {
for (const file of Object.entries(files)) {
// @ts-ignore
file[1].uploader = file[1].size > uploadLimit ? "tus" : "file";
}
return true;
},
}).use(DropTarget, {
target: targetRef,
} as DropTargetOptions);
uppyInstance.current = uppy;
setInputProps({
ref: inputRef,
type: "file",
onChange: (event) => {
const files = toArray(event.target.files);
if (files.length > 0) {
uppyInstance.current?.log("[DragDrop] Files selected through input");
uppyInstance.current?.addFiles(files);
}
uppy.iteratePlugins((plugin) => {
uppy.removePlugin(plugin);
});
uppy.use(UppyFileUpload, { sdk: sdk as Sdk });
let useTus = false;
uppyInstance.current?.getFiles().forEach((file) => {
if (file.size > uploadLimit) {
useTus = true;
}
});
if (useTus) {
uppy.use(Tus, { limit: 1, parallelUploads: 1, chunkSize: 1024 * 1024, urlStorage: new NoopUrlStorage() });
uppy.addPreProcessor(tusPreprocessor);
}
// We clear the input after a file is selected, because otherwise
// change event is not fired in Chrome and Safari when a file
// with the same name is selected.
// ___Why not use value="" on <input/> instead?
// Because if we use that method of clearing the input,
// Chrome will not trigger change if we drop the same file twice (Issue #768).
// @ts-expect-error TS freaks out, but this is fine
// eslint-disable-next-line no-param-reassign
event.target.value = null;
},
});
uppy.on("complete", (result) => {
if (result.failed.length === 0) {
console.log("Upload successful üòÄ");
setState("completed");
} else {
console.warn("Upload failed üòû");
setState("error");
}
console.log("successful files:", result.successful);
console.log("failed files:", result.failed);
setFailedFiles(result.failed);
invalidate({
resource: "file",
invalidates: ["list"]
})
});
const setStateCb = (event: (typeof LISTENING_EVENTS)[number]) => {
switch (event) {
case "upload":
case "upload-progress":
case "preprocess-progress":
setState("uploading");
break;
case "upload-error":
setState("error");
break;
case "upload-success":
setState("completed");
break;
default:
break;
}
setUppyState(uppy.getState());
};
for (const event of LISTENING_EVENTS) {
uppy.on(event, function cb() {
setStateCb(event);
});
}
setState("idle");
}, [targetRef, invalidate, sdk, uploadLimit]);
useEffect(() => {
return () => {
uppyInstance.current?.cancelAll({ reason: "unmount" });
uppyInstance.current?.logout();
uppyInstance.current?.close();
uppyInstance.current = undefined;
};
}, []);
return {
getFiles: () => uppyInstance.current?.getFiles() ?? [],
error: uppyInstance.current?.getState,
failedFiles,
state,
upload: () =>
uppyInstance.current?.upload() ??
new Error("[useUppy] Uppy has not initialized yet."),
getInputProps: () => inputProps,
getRootProps,
removeFile,
cancelAll,
};
}

View File

@ -0,0 +1,78 @@
import { cn } from "~/utils";
import { Avatar } from "./ui/avatar";
import { Button } from "./ui/button";
import { EditIcon, FingerPrintIcon } from "./icons";
const ManagementCardAvatar = ({ src, button, onClick }: { src?: string; button?: React.ReactNode; onClick?: () => void }) => {
return (
<div className="flex justify-center">
<div className="relative w-fit h-fit">
<Avatar className="border-2 border-ring h-28 w-28" />
{!button
? <Button
onClick={onClick}
variant="outline"
className="absolute bottom-0 right-0 z-50 flex items-center w-10 h-10 p-0 border-white rounded-full justiyf-center hover:bg-secondary-2">
<EditIcon />
</Button>
: button
}
</div>
</div>
);
};
const ManagementCardTitle = ({
children,
className,
}: React.PropsWithChildren<{ className?: string }>) => {
return (
<div className={cn("flex items-center gap-x-2 font-semibold", className)}>
<FingerPrintIcon className="text-ring" />
{children}
</div>
);
};
const ManagementCardContent = ({
children,
className,
}: React.PropsWithChildren<{ className?: string }>) => {
return (
<div className={cn("mt-4 mb-8 text-sm text-primary-2", className)}>
{children}
</div>
);
};
const ManagementCardFooter = ({
children,
className,
}: React.PropsWithChildren<{ className?: string }>) => {
return <div className={className}>{children}</div>;
};
const ManagementCard = ({
children,
variant,
}: React.PropsWithChildren<{ variant?: string }>) => {
return (
<div
className={cn(
"rounded-lg p-8 border w-full border-[--variant-color]",
!variant && "[--variant-color:theme(colors.border)]",
variant === "accent" && "[--variant-color:theme(colors.primary-1.DEFAULT)]",
)}>
{children}
</div>
);
};
export {
ManagementCard,
ManagementCardAvatar,
ManagementCardContent,
ManagementCardFooter,
ManagementCardTitle,
};

View File

@ -0,0 +1,126 @@
import { useMemo } from "react";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "./ui/accordion";
import { Progress } from "./ui/progress";
import { usePinning } from "~/hooks/usePinning";
import { Tabs, TabsTrigger, TabsList, TabsContent } from "./ui/tabs";
import { Button } from "./ui/button";
import { Cross2Icon } from "@radix-ui/react-icons";
import type { PinningStatus } from "~/data/pinning";
export const PinningNetworkBanner = () => {
const { progressData: data } = usePinning();
const itemsQueued = useMemo(
() =>
data?.items.filter((item: PinningStatus) =>
item.status.includes("queued"),
) || [],
[data],
);
const itemsProcessing = useMemo(
() =>
data?.items.filter((item: PinningStatus) =>
item.status.includes("processing"),
) || [],
[data],
);
const completedItems = useMemo(
() =>
data?.items.filter((item: PinningStatus) =>
item.status.includes("completed"),
) || [],
[data],
);
return (
<div
className={`bg-background border border-border rounded-lg absolute w-1/3 bottom-4 right-4 ${
!data?.items.length ? "hidden" : "block"
}`}>
<Accordion type="single" defaultValue="item-1" collapsible>
<AccordionItem value="item-1">
<AccordionTrigger className="font-bold bg-primary px-4 rounded-tr-lg rounded-tl-lg">
{`${completedItems.length}/${data?.items.length} items completed`}
</AccordionTrigger>
<AccordionContent>
<Tabs className="w-full" defaultValue="inProgress">
<TabsList className="rounded-none">
<TabsTrigger value="queued">Queued</TabsTrigger>
<TabsTrigger value="inProgress">In Progress</TabsTrigger>
<TabsTrigger value="completed">Completed</TabsTrigger>
</TabsList>
<TabsContent value="queued">
{itemsQueued.length ? (
itemsQueued.map((item: PinningStatus) => (
<PinCidItem key={item.id} item={item} />
))
) : (
<div className="text-muted text-sm flex justify-center items-center h-10">
Nothing yet.
</div>
)}
</TabsContent>
<TabsContent value="inProgress">
{itemsProcessing.length ? (
itemsProcessing.map((item: PinningStatus) => (
<PinCidItem key={item.id} item={item} />
))
) : (
<div className="text-primary-2 text-sm flex justify-center items-center h-10">
Nothing yet.
</div>
)}
</TabsContent>
<TabsContent value="completed">
{completedItems.length ? (
completedItems.map((item: PinningStatus) => (
<PinCidItem key={item.id} item={item} />
))
) : (
<div className="text-muted text-sm flex justify-center items-center h-10">
Nothing yet.
</div>
)}
</TabsContent>
</Tabs>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
};
const PinCidItem = ({ item }: { item: PinningStatus }) => {
const { unpin } = usePinning();
return (
<div className="px-4 mb-4">
<div className="relative flex flex-col items-center rounded-lg py-2 px-4 hover:bg-primary/50 group">
<div className="flex justify-between items-center w-full">
<span className="font-semibold">{item.id}</span>
<span className="group-hover:hidden">{item.progress}%</span>
{item.status === "completed" ? (
<Button
variant="ghost"
className="absolute top-2 right-2 hidden group-hover:flex rounded-full h-3"
onClick={() =>
unpin({
cid: item.id,
})
}>
<Cross2Icon />
</Button>
) : null}
</div>
<Progress value={item.progress} className="h-2" />
</div>
</div>
);
};

View File

@ -0,0 +1,102 @@
import {
ChevronLeftIcon,
ChevronRightIcon,
DoubleArrowLeftIcon,
DoubleArrowRightIcon
} from "@radix-ui/react-icons"
import type { Table } from "@tanstack/react-table"
import { Button } from "./ui/button"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from "./ui/select"
interface DataTablePaginationProps<TData> {
table: Table<TData>
}
export function DataTablePagination<TData>({
table
}: DataTablePaginationProps<TData>) {
return (
<div className="flex items-center justify-between px-2 border border-t-2 border-x-0 h-14">
<div className="flex items-center space-x-2">
<p className="text-sm font-bold text-primary-1">Rows per page</p>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value))
}}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center space-x-6 lg:space-x-8">
<div className="flex items-center justify-center text-sm font-bold text-primary-1">
Showing
<span className="text-white mx-1">
{` ${
table.getState().pagination.pageIndex *
table.getState().pagination.pageSize +
1
} to ${
(table.getState().pagination.pageIndex + 1) * table.getRowCount()
} `}
</span>
of {table.getRowCount()}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to first page</span>
<DoubleArrowLeftIcon className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<ChevronLeftIcon className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
<ChevronRightIcon className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to last page</span>
<DoubleArrowRightIcon className="h-4 w-4" />
</Button>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,58 @@
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDownIcon } from "@radix-ui/react-icons";
import { cn } from "~/utils";
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}>
{children}
<ChevronDownIcon className="h-4 w-4 text-muted-foreground transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export {
Accordion,
AccordionItem,
AccordionTrigger,
AccordionContent,
};

View File

@ -0,0 +1,48 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "~/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

@ -11,19 +11,21 @@ const buttonVariants = cva(
variant: { variant: {
default: default:
"bg-primary text-primary-foreground hover:bg-primary/50", "bg-primary text-primary-foreground hover:bg-primary/50",
// TODO: name it better
accent: "bg-ring text-primary-1-foreground hover:bg-ring/75 font-bold",
destructive: destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", "bg-destructive text-white shadow-sm hover:bg-destructive/90",
outline: outline:
"border border-input bg-background shadow-sm hover:bg-primary-2/5", "border border-input bg-background shadow-sm hover:bg-primary-2/5",
secondary: secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground", ghost: "hover:bg-secondary-1 hover:text-secondary-1-foreground text-primary-2",
link: "text-primary-1 underline-offset-4 hover:underline", link: "text-primary-1 underline-offset-4 hover:underline",
}, },
size: { size: {
default: "h-9 px-4 py-2", default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs", sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8", lg: "h-16 rounded-md",
icon: "h-9 w-9", icon: "h-9 w-9",
}, },
}, },

View File

@ -7,7 +7,7 @@ import { cn } from "~/utils"
const Checkbox = React.forwardRef< const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>, React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> & { React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> & {
className: string className?: string
} }
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root <CheckboxPrimitive.Root

View File

@ -0,0 +1,120 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cn } from "~/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="w-4 h-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@ -0,0 +1,206 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons"
import { cn } from "~/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-primary p-1 text-white shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean,
variant?: string
}
>(({ className, inset, variant, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-md px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
!variant && "focus:bg-primary-2/50 focus:text-primary-2-foreground",
variant === "destructive" && "focus:bg-destructive/50 focus:text-white",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<DotFilledIcon className="h-4 w-4 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-primary-2/75", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

View File

@ -4,10 +4,13 @@ import { cn } from "~/utils"
import { EyeOpenIcon, EyeNoneIcon } from "@radix-ui/react-icons" import { EyeOpenIcon, EyeNoneIcon } from "@radix-ui/react-icons"
export interface InputProps export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {} extends React.InputHTMLAttributes<HTMLInputElement> {
fullWidth?: boolean,
leftIcon?: React.ReactNode
}
const Input = React.forwardRef<HTMLInputElement, InputProps>( const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => { ({ className, type, fullWidth, leftIcon, ...props }, ref) => {
const [showPassword, setShowPassword] = React.useState<boolean>(false) const [showPassword, setShowPassword] = React.useState<boolean>(false)
const [mask, setMask] = React.useState<boolean>(false) const [mask, setMask] = React.useState<boolean>(false)
const toggleShowPassword = () => { const toggleShowPassword = () => {
@ -15,19 +18,25 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
setMask((mask) => !mask) setMask((mask) => !mask)
} }
return ( return (
<div className="relative"> <div className={`relative ${fullWidth ? "w-full" : ""}`}>
{leftIcon && (
<div className="absolute left-4 top-1/2 -translate-y-1/2">
{leftIcon}
</div>
)}
<input <input
type={type && !mask ? type : "text"} type={type && !mask ? type : "text"}
className={cn( className={cn(
"flex h-14 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-input-placeholder focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", "flex h-14 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-input-placeholder focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className className,
leftIcon && "pl-10"
)} )}
ref={ref} ref={ref}
{...props} {...props}
/> />
{type === "password" ? <button {type === "password" ? <button
type="button" type="button"
className="absolute right-4 top-5 text-input" className="absolute right-4 top-5 text-ring"
onClick={toggleShowPassword} onClick={toggleShowPassword}
onKeyDown={toggleShowPassword} onKeyDown={toggleShowPassword}
> >

View File

@ -0,0 +1,26 @@
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "~/utils"
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, max, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-3 w-full overflow-hidden rounded-full bg-primary",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className={`h-full w-full flex-1 bg-ring rounded-r-full transition-all`}
style={{ transform: `translateX(-${100 - Math.floor(((value || 0)*100)/(max || 100))}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }

View File

@ -0,0 +1,162 @@
import * as React from "react"
import {
CaretSortIcon,
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
} from "@radix-ui/react-icons"
import * as SelectPrimitive from "@radix-ui/react-select"
import { cn } from "~/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<CaretSortIcon className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@ -0,0 +1,15 @@
import { cn } from "~/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-primary/10", className)}
{...props}
/>
)
}
export { Skeleton }

120
app/components/ui/table.tsx Normal file
View File

@ -0,0 +1,120 @@
import * as React from "react"
import { cn } from "~/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm rounded-lg border-x-0", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("border-b-2", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0 rounded-lg border-x-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"h-14 border border-t-2 border-x-0 bg-secondary-2 font-medium",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-y-1 border-x-0 transition-colors hover:bg-secondary-1 text-primary-2 data-[state=selected]:text-ring ",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-14 px-6 border-x-1 first:border-l-0 last:border-r-0 text-left align-middle font-bold text-ring [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn(
"h-14 px-6 align-middle border-x-1 first:border-l-0 last:border-r-0 [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@ -0,0 +1,53 @@
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "~/utils";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-primary-dark p-1 text-muted-foreground",
className,
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-primary-1 data-[state=active]:shadow",
className,
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className,
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "~/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }

127
app/components/ui/toast.tsx Normal file
View File

@ -0,0 +1,127 @@
import * as React from "react"
import { Cross2Icon } from "@radix-ui/react-icons"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "~/utils"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants> & { cancelMutation?: () => void }
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<Cross2Icon className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}

View File

@ -0,0 +1,42 @@
import {
Toast,
ToastAction,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "~/components/ui/toast";
import { useToast } from "~/components/ui/use-toast";
export function Toaster() {
const { toasts } = useToast();
return (
<ToastProvider>
{toasts.map(
({ id, title, description, action, cancelMutation, ...props }) => {
const undoButton = cancelMutation ? (
<ToastAction altText="Undo" onClick={cancelMutation}>
Undo
</ToastAction>
) : undefined;
return (
<Toast key={id} {...props} variant={"default"}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
{undoButton}
<ToastClose />
</Toast>
);
},
)}
<ToastViewport />
</ToastProvider>
);
}

View File

@ -0,0 +1,28 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "~/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@ -0,0 +1,192 @@
// Inspired by react-hot-toast library
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "~/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }

View File

@ -0,0 +1,52 @@
import { useGetIdentity } from "@refinedev/core"
import { Identity } from "~/data/auth-provider"
import {
CrownIcon,
PersonIcon,
CloudIcon,
CheckRoundedIcon,
AddIcon,
CloudDownloadIcon
} from "./icons"
import { Avatar } from "./ui/avatar"
import { Button } from "./ui/button"
export const UpgradeAccountBanner = () => {
const { data: identity } = useGetIdentity<Identity>();
return (
<div className="flex items-center justify-between p-8 border border-ring rounded-lg bg-secondary-1">
<div className="flex items-center gap-x-4">
<Avatar className="border-2 border-ring h-20 w-20" />
<div>
<div className="flex items-center gap-x-2 font-bold">
{`${identity?.firstName} ${identity?.lastName}`}
<CrownIcon className="text-ring" />
</div>
<div className="flex gap-x-5 mt-2">
<div className="flex items-center gap-x-2 text-white text-sm">
<PersonIcon />
Lite Account (upgrade)
</div>
<div className="flex items-center gap-x-2 text-white text-sm">
<CloudIcon />
120 GB / 130 GB
</div>
<div className="flex items-center gap-x-2 text-white text-sm">
<CloudDownloadIcon />
10 GB / 25 GB
</div>
<div className="flex items-center gap-x-2 text-white text-sm">
<CheckRoundedIcon />
0% Free Usage
</div>
</div>
</div>
</div>
<Button className="gap-x-2 py-6" variant="accent">
<AddIcon />
Upgrade to Premium
</Button>
</div>
)
}

View File

@ -0,0 +1,40 @@
import { AddIcon } from "./icons";
import { Button } from "./ui/button";
import { Progress } from "./ui/progress";
interface UsageCardProps {
label: string,
monthlyUsage: number, // Asumming that the minimium is 1GB
currentUsage: number,
icon: React.ReactNode,
button?: React.ReactNode;
}
export const UsageCard = ({ label, monthlyUsage, currentUsage, icon, button }: UsageCardProps) => {
return (
<div className="p-8 border rounded-lg w-full">
<div className="flex items-center justify-between mb-8">
<div className="text-primary-2 text-sm">
<div className="flex items-center gap-x-2 text-lg font-bold text-white mb-2">
{icon}
{label}
</div>
Montly {label.toLocaleLowerCase()} limit is {monthlyUsage} GB
</div>
{!button ? (
<Button className="gap-x-2 h-12">
<AddIcon />
Add More
</Button>
) : (
button
)}
</div>
<Progress max={monthlyUsage} value={currentUsage} />
<div className="flex items-center justify-between mt-4 font-semibold text-sm">
<span className="text-primary-2">{currentUsage} GB used</span>
<span className="text-white">{monthlyUsage - currentUsage} GB left</span>
</div>
</div>
)
}

View File

@ -0,0 +1,73 @@
import {
AnimatedAxis, // any of these can be non-animated equivalents
AnimatedLineSeries,
XYChart,
buildChartTheme
} from "@visx/xychart"
import { curveCardinal } from "@visx/curve"
import { InfoIcon } from "./icons"
type Coords = {
x: string
y: string
}
type UsageChartProps = {
label: string
dataset: Coords[]
}
const accessors = {
xAccessor: (d: Coords) => d.x,
yAccessor: (d: Coords) => d.y
}
const customTheme = buildChartTheme({
colors: ["hsl(var(--ring))"],
backgroundColor: "hsl(var(--primary-2))",
gridColor: "hsl(var(--primary-2))",
gridColorDark: "hsl(var(--primary-2))",
tickLength: 8,
xAxisLineStyles: {
strokeWidth: 1
}
})
export const UsageChart = ({ label, dataset }: UsageChartProps) => {
return (
<div className="p-8 border rounded-lg w-full">
<div className="flex items-center justify-between">
<span className="font-bold text-lg">{label}</span>
<InfoIcon className="text-ring" />
</div>
<div>
<XYChart
height={400}
xScale={{ type: "band" }}
yScale={{ type: "linear" }}
theme={customTheme}
>
<AnimatedAxis
orientation="bottom"
hideTicks
tickTransform="translate(50 0)"
tickLabelProps={{ className: "text-sm" }}
/>
<AnimatedAxis
orientation="left"
hideTicks
tickLabelProps={{ className: "text-sm" }}
/>
<AnimatedLineSeries
className="stroke-ring"
curve={curveCardinal}
strokeWidth={4}
dataKey="usage"
data={dataset}
{...accessors}
/>
</XYChart>
</div>
</div>
)
}

View File

@ -0,0 +1,65 @@
import type {DataProvider, UpdateParams, UpdateResponse, HttpError} from "@refinedev/core";
import {SdkProvider} from "~/data/sdk-provider.js";
type AccountParams = {
email?: string;
password?: string;
}
type AccountData = AccountParams;
export const accountProvider: SdkProvider = {
getList: () => {
console.log("Not implemented");
return Promise.resolve({
data: [],
});
},
getOne: () => {
console.log("Not implemented");
return Promise.resolve({
data: {},
});
},
// @ts-ignore
async update<TVariables extends AccountParams = AccountParams>(
params: UpdateParams<AccountParams>,
): Promise<UpdateResponse<AccountData>> {
if (params.variables.email && params.variables.password) {
const ret = await accountProvider.sdk?.account().updateEmail(params.variables.email, params.variables.password);
if (ret) {
if (ret instanceof Error) {
return Promise.reject(ret satisfies HttpError)
}
} else {
return Promise.reject();
}
return {
data:
{
email: params.variables.email,
},
};
}
// Return an empty response if params.variables is undefined
return {
data: {} as AccountParams,
};
},
create: () => {
console.log("Not implemented");
return Promise.resolve({
data: {},
});
},
deleteOne: () => {
console.log("Not implemented");
return Promise.resolve({
data: {},
});
},
getApiUrl: () => "",
}

205
app/data/auth-provider.ts Normal file
View File

@ -0,0 +1,205 @@
import type {AuthProvider, HttpError, UpdatePasswordFormTypes} from "@refinedev/core"
import type {
AuthActionResponse,
CheckResponse,
IdentityResponse,
OnErrorResponse,
SuccessNotificationResponse
// @ts-ignore
} from "@refinedev/core/dist/interfaces/bindings/auth"
import {Sdk, AccountError} from "@lumeweb/portal-sdk";
import type {AccountInfoResponse} from "@lumeweb/portal-sdk";
export type AuthFormRequest = {
email: string;
password: string;
rememberMe: boolean;
redirectTo?: string;
}
export type RegisterFormRequest = {
email: string;
password: string;
firstName: string;
lastName: string;
}
export type Identity = {
id: string;
firstName: string;
lastName: string;
email: string;
verified: boolean;
}
export interface UpdatePasswordFormRequest extends UpdatePasswordFormTypes {
currentPassword: string;
}
export const createPortalAuthProvider = (sdk: Sdk): AuthProvider => {
const maybeSetupAuth = (): void => {
let jwt = sdk.account().jwtToken;
if (jwt) {
sdk.setAuthToken(jwt);
if (import.meta.env.DEV) {
localStorage.setItem("jwt", jwt);
}
}
if (import.meta.env.DEV) {
let jwt = localStorage.getItem("jwt");
if (jwt) {
sdk.setAuthToken(jwt);
}
}
};
type ResponseResult = {
ret: boolean | Error;
successNotification?: SuccessNotificationResponse;
redirectToSuccess?: string;
redirectToError?: string;
successCb?: () => void;
}
interface CheckResponseResult extends ResponseResult {
authenticated?: boolean;
}
const handleResponse = (result: ResponseResult): AuthActionResponse => {
if (result.ret) {
if (result.ret instanceof AccountError) {
return {
success: false,
error: result.ret satisfies HttpError,
redirectTo: result.redirectToError
}
}
result.successCb?.();
return {
success: true,
successNotification: result.successNotification,
redirectTo: result.redirectToSuccess,
}
}
return {
success: false,
redirectTo: result.redirectToError
}
}
const handleCheckResponse = (result: CheckResponseResult): CheckResponse => {
const response = handleResponse(result);
const success = response.success;
delete response.success;
return {
...response,
authenticated: success
}
}
return {
async login(params: AuthFormRequest): Promise<AuthActionResponse> {
const ret = await sdk.account().login({
email: params.email,
password: params.password,
});
maybeSetupAuth();
return handleResponse({
ret, redirectToSuccess: "/dashboard", redirectToError: "/login", successCb: () => {
sdk.setAuthToken(sdk.account().jwtToken);
}, successNotification: {
message: "Login Successful",
description: "You have successfully logged in."
}
});
},
async logout(params: any): Promise<AuthActionResponse> {
let ret = await sdk.account().logout();
if (ret){
if (import.meta.env.DEV) {
localStorage.removeItem("jwt");
}
}
return handleResponse({ret, redirectToSuccess: "/login"});
},
async check(params?: any): Promise<CheckResponse> {
maybeSetupAuth();
const ret = await sdk.account().ping();
maybeSetupAuth();
return handleCheckResponse({ret, redirectToError: "/login", successCb: maybeSetupAuth});
},
async onError(error: any): Promise<OnErrorResponse> {
return {};
},
async register(params: RegisterFormRequest): Promise<AuthActionResponse> {
const ret = await sdk.account().register({
email: params.email,
password: params.password,
first_name: params.firstName,
last_name: params.lastName,
});
return handleResponse({
ret, redirectToSuccess: "/login", successNotification: {
message: "Registration Successful",
description: "You have successfully registered. Please check your email to verify your account.",
}
});
},
async forgotPassword(params: any): Promise<AuthActionResponse> {
return {success: true};
},
async updatePassword(params: UpdatePasswordFormRequest): Promise<AuthActionResponse> {
maybeSetupAuth();
const ret = await sdk.account().updatePassword(params.currentPassword, params.password as string);
return handleResponse({
ret, successNotification: {
message: "Password Updated",
description: "Your password has been updated successfully.",
}
});
},
async getPermissions(params?: Record<string, any>): Promise<AuthActionResponse> {
return {success: true};
},
async getIdentity(params?: Identity): Promise<IdentityResponse> {
maybeSetupAuth();
const ret = await sdk.account().info();
if (!ret) {
return {identity: null};
}
const acct = ret as AccountInfoResponse;
return {
id: acct.id,
firstName: acct.first_name,
lastName: acct.last_name,
email: acct.email,
verified: acct.verified,
};
},
};
};

134
app/data/file-provider.ts Normal file
View File

@ -0,0 +1,134 @@
import type {SdkProvider} from "~/data/sdk-provider.js";
import type {S5Client} from "@lumeweb/s5-js";
import {PROTOCOL_S5} from "@lumeweb/portal-sdk";
import {Multihash} from "@lumeweb/libs5/lib/multihash.js";
import type {AxiosProgressEvent} from "axios";
import {CID, CID_TYPES, METADATA_TYPES, metadataMagicByte, Unpacker} from "@lumeweb/libs5";
async function getIsManifest(s5: S5Client, hash: string): Promise<boolean | number> {
let type: number | null;
try {
const abort = new AbortController();
const resp = s5.downloadData(hash, {
onDownloadProgress: (progressEvent: AxiosProgressEvent) => {
if (progressEvent.loaded >= 10) {
abort.abort();
}
},
httpConfig: {
signal: abort.signal,
},
});
const data = await resp;
const unpacker = Unpacker.fromPacked(Buffer.from(data));
try {
const magic = unpacker.unpackInt();
if (magic !== metadataMagicByte) {
return false;
}
type = unpacker.unpackInt();
if (!type || !Object.values(METADATA_TYPES).includes(type)) {
return false;
}
} catch (e) {
return false;
}
} catch (e) {
return false;
}
switch (type) {
case METADATA_TYPES.DIRECTORY:
return CID_TYPES.DIRECTORY;
case METADATA_TYPES.WEBAPP:
return CID_TYPES.METADATA_WEBAPP;
case METADATA_TYPES.MEDIA:
return CID_TYPES.METADATA_MEDIA;
case METADATA_TYPES.USER_IDENTITY:
return CID_TYPES.USER_IDENTITY;
}
return 0;
}
export interface FileItem {
cid: string;
type: string;
size: number;
mimeType: string;
pinned: string;
}
export const fileProvider: SdkProvider = {
sdk: undefined,
async getList() {
const items: FileItem[] = [];
try {
const s5 = fileProvider.sdk?.protocols().get<S5Client>(PROTOCOL_S5)!.getSdk()!;
const pinList = await s5.accountPins();
for (const pin of pinList!.pins) {
const manifest = await getIsManifest(s5, pin.hash) as number;
if (manifest) {
const mHash = Multihash.fromBase64Url(pin.hash);
items.push({
cid: new CID(manifest, mHash, pin.size).toString(),
type: "manifest",
mimeType: "application/octet-stream",
size: pin.size,
pinned: pin.pinned_at,
});
} else {
items.push({
cid: new CID(CID_TYPES.RAW, Multihash.fromBase64Url(pin.hash), pin.size).toString(),
type: "raw",
mimeType: pin.mime_type,
size: pin.size,
pinned: pin.pinned_at,
});
}
}
} catch (e) {
return Promise.reject(e);
}
return {
data: items,
total: items.length,
};
},
getOne() {
console.log("Not implemented");
return Promise.resolve({
data: {
id: 1
},
});
},
update() {
console.log("Not implemented");
return Promise.resolve({
data: {},
});
},
create() {
console.log("Not implemented");
return Promise.resolve({
data: {},
});
},
deleteOne() {
console.log("Not implemented");
return Promise.resolve({
data: {},
});
},
getApiUrl() {
return "";
},
} satisfies SdkProvider;

View File

@ -0,0 +1,40 @@
import type {
NotificationProvider,
OpenNotificationParams,
} from "@refinedev/core";
import type { ToastActionElement } from "~/components/ui/toast";
import { toast } from "~/components/ui/use-toast";
interface Provider extends Omit<NotificationProvider, "open"> {
open: (
params: Omit<OpenNotificationParams, "type"> & {
action?: ToastActionElement;
type: "default" | "destructive";
},
) => void;
}
export const notificationProvider = () => {
return {
open: ({
key,
message,
description,
undoableTimeout,
cancelMutation,
action,
type,
}) => {
toast({
variant: type,
key,
title: message,
description,
duration: undoableTimeout,
action,
cancelMutation,
});
},
close: () => {},
} satisfies Provider;
};

103
app/data/pinning.ts Normal file
View File

@ -0,0 +1,103 @@
import { PROTOCOL_S5, Sdk } from "@lumeweb/portal-sdk";
import { useSdk } from "~/components/lib/sdk-context.js";
import { S5Client } from "@lumeweb/s5-js";
import { S5Error } from "@lumeweb/s5-js/lib/client.js";
export interface PinningStatus {
id: string;
progress: number;
status: "processing" | "completed";
}
// biome-ignore lint/complexity/noStaticOnlyClass: <explanation>
export class PinningProcess {
private static instances: Map<string, PinningStatus> = new Map();
private static sdk?: Sdk;
static async pin(id: string): Promise<{ success: boolean; message: string }> {
try {
const s5 = PinningProcess.sdk?.protocols().get<S5Client>("s5")!.getSdk()!;
console.log({ s5 });
if (PinningProcess.instances.has(id)) {
return { success: false, message: "ID is already being processed" };
}
const pinningStatus: PinningStatus = {
id,
progress: 0,
status: "processing",
};
const response = await s5.pin(id);
console.log({ response });
PinningProcess.instances.set(id, pinningStatus);
return { success: true, message: "Pinning process started" };
} catch (e) {
console.error(e);
return { success: false, message: "Pinning process failed" };
}
}
static async unpin(
id: string,
): Promise<{ success: boolean; message: string }> {
if (!PinningProcess.instances.has(id)) {
return { success: false, message: "ID not found or not being processed" };
}
await PinningProcess.sdk
?.protocols()
.get<S5Client>(PROTOCOL_S5)!
.getSdk()
.unpin(id);
return { success: true, message: "Pinning process removed" };
}
static *pollAllProgress(): Generator<PinningStatus[], void, unknown> {
let allStatuses = Array.from(PinningProcess.instances.values());
let inProgress = allStatuses.some((status) => {
PinningProcess.checkStatus(status.id);
return status.status !== "processing";
});
while (inProgress) {
yield allStatuses;
allStatuses = Array.from(PinningProcess.instances.values());
inProgress = allStatuses.some((status) => {
PinningProcess.checkStatus(status.id);
return status.status !== "completed";
});
}
yield allStatuses ?? []; // Yield the final statuses
}
private static async checkStatus(id: string) {
const s5 = PinningProcess.sdk
?.protocols()
.get<S5Client>(PROTOCOL_S5)!
.getSdk()!;
try {
const ret = await s5.pinStatus(id);
const status = PinningProcess.instances.get(id);
if (!status) {
return;
}
status.progress = ret.progress;
status.status = ret.status as any;
} catch (e) {
if ((e as S5Error).statusCode == 404) {
PinningProcess.instances.delete(id);
}
return;
}
}
public static setupSdk(sdk: Sdk) {
PinningProcess.sdk = sdk;
}
}

23
app/data/providers.ts Normal file
View File

@ -0,0 +1,23 @@
import type {AuthProvider} from "@refinedev/core";
import {fileProvider} from "~/data/file-provider.js";
import {Sdk} from "@lumeweb/portal-sdk";
import {accountProvider} from "~/data/account-provider.js";
import type {SdkProvider} from "~/data/sdk-provider.js";
import {createPortalAuthProvider} from "~/data/auth-provider.js";
export interface DataProviders {
default: SdkProvider;
auth: AuthProvider;
[key: string]: SdkProvider | AuthProvider;
}
export function getProviders(sdk: Sdk) {
accountProvider.sdk = sdk;
fileProvider.sdk = sdk;
return {
default: accountProvider,
auth: createPortalAuthProvider(sdk),
files: fileProvider,
};
}

18
app/data/resources.ts Normal file
View File

@ -0,0 +1,18 @@
import type {ResourceProps} from "@refinedev/core";
const resources: ResourceProps[] = [
{
name: 'account',
meta: {
dataProviderName: 'default',
}
},
{
name: 'file',
meta: {
dataProviderName: 'files',
}
}
];
export default resources;

6
app/data/sdk-provider.ts Normal file
View File

@ -0,0 +1,6 @@
import {DataProvider} from "@refinedev/core";
import {Sdk} from "@lumeweb/portal-sdk";
export interface SdkProvider extends DataProvider {
sdk?: Sdk;
}

75
app/hooks/usePinning.ts Normal file
View File

@ -0,0 +1,75 @@
import { useInvalidate, useNotification } from "@refinedev/core";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useCallback, useContext } from "react";
import { PinningProcess } from "~/data/pinning";
import { PinningContext } from "~/providers/PinningProvider";
export const usePinning = () => {
const queryClient = useQueryClient();
const invalidate = useInvalidate();
const context = useContext(PinningContext);
const { open } = useNotification();
const { status: pinStatus, data: pinData, mutate: pinMutation } = useMutation({
mutationKey: ["pin-mutation"],
mutationFn: async (variables: { cid: string }) => {
const { cid } = variables;
const response = await PinningProcess.pin(cid);
if (!response.success) {
open?.({
type: "error",
message: `Error pinning ${cid}`,
description: response.message,
});
return Promise.reject(response);
}
queryClient.invalidateQueries({ queryKey: ["pin-progress", "file"] });
invalidate({ resource: "file", invalidates: ["list"] });
return Promise.resolve(response);
},
});
const { status: unpinStatus, data: unpinData, mutate: unpinMutation } = useMutation({
mutationKey: ["unpin-mutation"],
mutationFn: async (variables: { cid: string }) => {
const { cid } = variables;
const response = await PinningProcess.unpin(cid);
if (!response.success) {
open?.({
type: "error",
message: `Error removing ${cid}`,
description: response.message,
});
return Promise.reject(response);
}
queryClient.invalidateQueries({ queryKey: ["pin-progress"] });
invalidate({ resource: "file", invalidates: ["list"] });
return Promise.resolve(response);
},
});
const bulkPin = useCallback(
(cids: string[]) => {
for (const cid of cids) {
pinMutation({ cid });
}
},
[pinMutation],
);
return {
progressStatus: context.query.status,
progressData: context.query.data,
fetchProgress: context.query.refetch,
pinStatus,
pinData,
unpinStatus,
unpinData,
pin: pinMutation,
unpin: unpinMutation,
bulkPin,
};
};

BIN
app/images/.DS_Store vendored

Binary file not shown.

BIN
app/images/QR.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,78 @@
import { useInvalidate } from "@refinedev/core";
import {
type QueryClient,
type UseQueryResult,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import { createContext, useContext, useEffect } from "react";
import { PinningProcess, type PinningStatus } from "~/data/pinning";
export interface IPinningData {
cid: string;
progress: number;
}
export interface IPinningContextType {
query: UseQueryResult<
{
items: PinningStatus[];
lastUpdated: number;
},
Error
>;
queryClient: QueryClient;
}
export const PinningContext = createContext<IPinningContextType>(
{} as IPinningContextType,
);
export const PinningProvider = ({ children }: React.PropsWithChildren) => {
const invalidate = useInvalidate();
const queryClient = useQueryClient();
const queryResult = useQuery({
queryKey: ["pin-progress"],
refetchInterval: (query) => {
if (!query.state.data?.items || query.state.data.items.length === 0) {
return false;
}
return 1000;
},
refetchIntervalInBackground: true,
queryFn: () => {
const response = PinningProcess.pollAllProgress();
const result = response.next();
return {
items: result.value || [],
lastUpdated: Date.now(),
};
},
});
useEffect(() => {
if (
queryResult.isSuccess &&
queryResult.fetchStatus === "idle" &&
queryResult.isFetched
) {
const hasCompletedItems = queryResult.data.items.some(item => item.status === 'completed');
if (hasCompletedItems) {
invalidate({ resource: "file", invalidates: ["list"] });
}
}
}, [
queryResult.fetchStatus,
queryResult.isSuccess,
queryResult.isFetched,
invalidate,
queryResult.data,
]);
return (
<PinningContext.Provider value={{ query: queryResult, queryClient }}>
{children}
</PinningContext.Provider>
);
};

View File

@ -1,22 +1,28 @@
import { import {Links, Meta, Outlet, Scripts, ScrollRestoration,} from "@remix-run/react";
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
import stylesheet from "./tailwind.css?url"; import stylesheet from "./tailwind.css?url";
import { LinksFunction } from "@remix-run/node"; import type {LinksFunction} from "@remix-run/node";
// Supports weights 200-800 // Supports weights 200-800
import '@fontsource-variable/manrope'; import "@fontsource-variable/manrope";
import {Refine} from "@refinedev/core";
import routerProvider from "@refinedev/remix-router";
import {notificationProvider} from "~/data/notification-provider";
import {SdkContextProvider, useSdk} from "~/components/lib/sdk-context";
import {Toaster} from "~/components/ui/toaster";
import {getProviders} from "~/data/providers.js";
import {Sdk} from "@lumeweb/portal-sdk";
import resources from "~/data/resources.js";
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
import {useEffect, useMemo, useState} from "react";
import {PinningProcess} from "~/data/pinning.js";
export const links: LinksFunction = () => [ export const links: LinksFunction = () => [
{ rel: "stylesheet", href: stylesheet }, { rel: "stylesheet", href: stylesheet },
// { rel: "stylesheet", href: manropeStylesheet },
]; ];
const queryClient = new QueryClient();
export function Layout({ children }: { children: React.ReactNode }) { export function Layout({ children }: { children: React.ReactNode }) {
return ( return (
<html lang="en"> <html lang="en">
@ -26,7 +32,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
<Meta /> <Meta />
<Links /> <Links />
</head> </head>
<body> <body className="max-h-screen">
{children} {children}
<ScrollRestoration /> <ScrollRestoration />
<Scripts /> <Scripts />
@ -35,8 +41,74 @@ export function Layout({ children }: { children: React.ReactNode }) {
); );
} }
export default function App() { function App() {
return <Outlet />; return (
<>
<Outlet />
<Toaster />
</>
);
}
export default function Root() {
const [portalUrl, setPortalUrl] = useState(import.meta.env.VITE_PORTAL_URL);
const [sdk, setSdk] = useState<Sdk| undefined>(portalUrl ? Sdk.create(portalUrl) : undefined);
useEffect(() => {
if (!portalUrl) {
fetch("/api/meta")
.then((response) => response.json())
.then((data) => {
setPortalUrl(`https://${data.domain}`);
})
.catch((error) => {
console.error("Failed to fetch portal url:", error);
});
}
}, [portalUrl]);
useEffect(() => {
if (portalUrl) {
setSdk(Sdk.create(portalUrl));
}
}, [portalUrl]);
if (!portalUrl) {
return <p>Loading...</p>;
}
return (
<SdkContextProvider sdk={sdk as Sdk}>
<SdkWrapper />
</SdkContextProvider>
);
}
function SdkWrapper() {
const sdk = useSdk();
PinningProcess.setupSdk(sdk as Sdk);
const providers = useMemo(() => getProviders(sdk as Sdk), [sdk]);
if (!sdk) {
return <p>Loading...</p>;
}
return (
<QueryClientProvider client={queryClient}>
<Refine
authProvider={providers.auth}
routerProvider={routerProvider}
notificationProvider={notificationProvider}
dataProvider={{
default: providers.default,
files: providers.files,
}}
resources={resources}
options={{disableTelemetry: true}}>
<App/>
</Refine>
</QueryClientProvider>
)
} }
export function HydrateFallback() { export function HydrateFallback() {

View File

@ -1,80 +1,12 @@
import type { MetaFunction } from "@remix-run/node" import {Authenticated} from "@refinedev/core";
import { Link } from "@remix-run/react" import {Navigate} from "@remix-run/react";
import { Button } from "~/components/ui/button"
import logoPng from "~/images/lume-logo.png?url"
import lumeColorLogoPng from "~/images/lume-color-logo.png?url"
import discordLogoPng from "~/images/discord-logo.png?url"
import lumeBgPng from "~/images/lume-bg-image.png?url"
import { Field, FieldCheckbox } from "~/components/forms"
export const meta: MetaFunction = () => {
return [
{ title: "New Remix SPA" },
{ name: "description", content: "Welcome to Remix (SPA Mode)!" }
]
}
export default function Index() { export default function Index() {
return ( return (
<div className="p-10 h-screen relative overflow-clip"> <Authenticated key={"index"} loading={
<header> <>Checking Login Status</>
<img src={logoPng} alt="Lume logo" className="h-10"></img> }>
</header> <Navigate to="/dashboard" replace/>
<form className="w-full p-2 max-w-md space-y-4 mt-12 bg-background"> </Authenticated>
<h2 className="text-3xl font-bold !mb-12">Welcome back! 🎉</h2> )
<Field
inputProps={{ name: "email" }}
labelProps={{ children: "Email" }}
/>
<Field
inputProps={{ name: "password", type: "password" }}
labelProps={{ children: "Password" }}
/>
<FieldCheckbox
inputProps={{ name: "rememberMe" }}
labelProps={{ children: "Remember Me" }}
/>
<Button className="w-full h-14">Login</Button>
<p className="text-input-placeholder">
Forgot your password?{" "}
<Link
to="/sign-up"
className="text-primary-1 text-md hover:underline hover:underline-offset-4"
>
Reset Password
</Link>
</p>
<Button className="w-full h-14" variant={"outline"}>
Create an Account
</Button>
</form>
<img src={lumeBgPng} alt="Lume background" className="absolute top-0 right-0 md:w-2/3 object-cover z-[-1]"></img>
<footer className="absolute bottom-5">
<ul className="flex flex-row">
<li>
<Link to="https://discord.lumeweb.com">
<Button
variant={"link"}
className="flex flex-row gap-x-2 text-input-placeholder"
>
<img className="h-5" src={discordLogoPng} alt="Discord Logo" />
Connect with us
</Button>
</Link>
</li>
<li>
<Link to="https://lumeweb.com">
<Button
variant={"link"}
className="flex flex-row gap-x-2 text-input-placeholder"
>
<img className="h-5" src={lumeColorLogoPng} alt="Lume Logo" />
Connect with us
</Button>
</Link>
</li>
</ul>
</footer>
</div>
)
} }

561
app/routes/account.tsx Normal file
View File

@ -0,0 +1,561 @@
import { getFormProps, useForm } from "@conform-to/react";
import { getZodConstraint, parseWithZod } from "@conform-to/zod";
import { DialogClose } from "@radix-ui/react-dialog";
import { Cross2Icon } from "@radix-ui/react-icons";
import {
Authenticated,
type BaseKey,
useGetIdentity,
useUpdate,
useUpdatePassword,
} from "@refinedev/core";
import { useEffect, useMemo, useState } from "react";
import { z } from "zod";
import { Field } from "~/components/forms";
import { GeneralLayout } from "~/components/general-layout";
import {
AddIcon,
CloudCheckIcon,
CloudIcon,
CloudUploadIcon,
CrownIcon,
EditIcon,
} from "~/components/icons";
import { useUppy } from "~/components/lib/uppy";
import {
ManagementCard,
ManagementCardAvatar,
ManagementCardContent,
ManagementCardFooter,
ManagementCardTitle,
} from "~/components/management-card";
import { Button } from "~/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "~/components/ui/dialog";
import { Input } from "~/components/ui/input";
import { UsageCard } from "~/components/usage-card";
import QRImg from "~/images/QR.png";
import type { UpdatePasswordFormRequest } from "~/data/auth-provider";
export default function MyAccount() {
const { data: identity } = useGetIdentity<{ email: string }>();
const [openModal, setModal] = useState({
changeEmail: false,
changePassword: false,
setupTwoFactor: false,
changeAvatar: false,
});
const closeModal = () => {
setModal({
changeEmail: false,
changePassword: false,
setupTwoFactor: false,
changeAvatar: false,
});
};
const isModalOpen = Object.values(openModal).some(isOpen => isOpen);
return (
<Authenticated key="account">
<GeneralLayout>
<h1 className="text-lg font-bold mb-4">My Account</h1>
<Dialog
onOpenChange={(open) => {
if (!open) {
closeModal();
}
}}
open={isModalOpen}
>
<UsageCard
label="Usage"
currentUsage={2}
monthlyUsage={10}
icon={<CloudIcon className="text-ring" />}
button={
<Button variant="accent" className="gap-x-2 h-12">
<AddIcon />
Upgrade to Premium
</Button>
}
/>
<h2 className="font-bold my-8">Account Management</h2>
<div className="grid grid-cols-3 gap-x-8">
<ManagementCard>
<ManagementCardAvatar
button={
<DialogTrigger
asChild
className="absolute bottom-0 right-0 z-50">
<Button
onClick={() =>
setModal({ ...openModal, changeAvatar: true })
}
variant="outline"
className=" flex items-center w-10 h-10 p-0 border-white rounded-full justiyf-center hover:bg-secondary-2">
<EditIcon />
</Button>
</DialogTrigger>
}
/>
</ManagementCard>
<ManagementCard>
<ManagementCardTitle>Email Address</ManagementCardTitle>
<ManagementCardContent className="text-ring font-semibold">
{identity?.email}
</ManagementCardContent>
<ManagementCardFooter>
<DialogTrigger asChild>
<Button
className="h-12 gap-x-2"
onClick={() =>
setModal({ ...openModal, changeEmail: true })
}>
<AddIcon />
Change Email Address
</Button>
</DialogTrigger>
</ManagementCardFooter>
</ManagementCard>
<ManagementCard>
<ManagementCardTitle>Account Type</ManagementCardTitle>
<ManagementCardContent className="text-ring font-semibold flex gap-x-2">
Lite Premium Account
<CrownIcon />
</ManagementCardContent>
<ManagementCardFooter>
<Button className="h-12 gap-x-2">
<AddIcon />
Upgrade to Premium
</Button>
</ManagementCardFooter>
</ManagementCard>
</div>
<h2 className="font-bold my-8">Security</h2>
<div className="grid grid-cols-3 gap-x-8">
<ManagementCard>
<ManagementCardTitle>Password</ManagementCardTitle>
<ManagementCardContent>
<PasswordDots className="mt-6" />
</ManagementCardContent>
<ManagementCardFooter>
<DialogTrigger asChild>
<Button
className="h-12 gap-x-2"
onClick={() =>
setModal({ ...openModal, changePassword: true })
}>
<AddIcon />
Change Password
</Button>
</DialogTrigger>
</ManagementCardFooter>
</ManagementCard>
<ManagementCard>
<ManagementCardTitle>
Two-Factor Authentication
</ManagementCardTitle>
<ManagementCardContent>
Improve security by enabling 2FA.
</ManagementCardContent>
<ManagementCardFooter>
<DialogTrigger asChild>
<Button
className="h-12 gap-x-2"
onClick={() =>
setModal({ ...openModal, setupTwoFactor: true })
}>
<AddIcon />
Enable Two-Factor Authorization
</Button>
</DialogTrigger>
</ManagementCardFooter>
</ManagementCard>
</div>
<h2 className="font-bold my-8">More</h2>
<div className="grid grid-cols-3 gap-x-8">
<ManagementCard variant="accent">
<ManagementCardTitle>Invite a Friend</ManagementCardTitle>
<ManagementCardContent>
Get 1 GB per friend invited for free (max 5 GB).
</ManagementCardContent>
<ManagementCardFooter>
<Button variant="accent" className="h-12 gap-x-2">
<AddIcon />
Send Invitation
</Button>
</ManagementCardFooter>
</ManagementCard>
<ManagementCard>
<ManagementCardTitle>Read our Resources</ManagementCardTitle>
<ManagementCardContent>
Navigate helpful articles or get assistance.
</ManagementCardContent>
<ManagementCardFooter>
<Button className="h-12 gap-x-2">
<AddIcon />
Open Support Centre
</Button>
</ManagementCardFooter>
</ManagementCard>
<ManagementCard>
<ManagementCardTitle>Delete Account</ManagementCardTitle>
<ManagementCardContent>
Once initiated, this action cannot be undone.
</ManagementCardContent>
<ManagementCardFooter>
<Button className="h-12 gap-x-2" variant="destructive">
<AddIcon />
Delete my Account
</Button>
</ManagementCardFooter>
</ManagementCard>
</div>
<DialogContent>
{openModal.changeAvatar && <ChangeAvatarForm close={closeModal} />}
{openModal.changeEmail && (
<ChangeEmailForm currentValue={identity?.email || ""} close={closeModal} />
)}
{openModal.changePassword && <ChangePasswordForm close={closeModal} />}
{openModal.setupTwoFactor && <SetupTwoFactorDialog close={closeModal} />}
</DialogContent>
</Dialog>
</GeneralLayout>
</Authenticated>
);
}
const ChangeEmailSchema = z
.object({
email: z.string().email(),
password: z.string(),
retypePassword: z.string(),
})
.superRefine((data, ctx) => {
if (data.password !== data.retypePassword) {
return ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["retypePassword"],
message: "Passwords do not match",
});
}
return true;
});
const ChangeEmailForm = ({ currentValue, close }: { currentValue: string, close: () => void }) => {
const { data: identity } = useGetIdentity<{ id: BaseKey }>();
const { mutate: updateEmail, isSuccess } = useUpdate();
const [form, fields] = useForm({
id: "login",
constraint: getZodConstraint(ChangeEmailSchema),
onValidate({ formData }) {
return parseWithZod(formData, { schema: ChangeEmailSchema });
},
shouldValidate: "onSubmit",
onSubmit(e) {
e.preventDefault();
const data = Object.fromEntries(new FormData(e.currentTarget).entries());
console.log(identity);
updateEmail({
resource: "account",
id: "",
values: {
email: data.email.toString(),
password: data.password.toString(),
},
});
},
});
useEffect(() => {
if (isSuccess) {
close();
}
}, [isSuccess, close]);
return (
<>
<DialogHeader>
<DialogTitle className="mb-8">Change Email</DialogTitle>
</DialogHeader>
<div className="rounded-full px-4 py-2 w-fit text-sm bg-ring font-bold text-secondary-1">
{currentValue}
</div>
<form {...getFormProps(form)}>
<Field
className="mt-8"
inputProps={{ name: fields.email.name }}
labelProps={{ children: "New Email Address" }}
errors={fields.email.errors}
/>
<Field
inputProps={{ name: fields.password.name, type: "password" }}
labelProps={{ children: "Password" }}
errors={fields.password.errors}
/>
<Field
inputProps={{
name: fields.retypePassword.name,
type: "password",
}}
labelProps={{ children: "Retype Password" }}
errors={fields.retypePassword.errors}
/>
<Button className="w-full h-14">Change Email Address</Button>
</form>
</>
);
};
const ChangePasswordSchema = z
.object({
currentPassword: z.string(),
newPassword: z.string(),
retypePassword: z.string(),
})
.superRefine((data, ctx) => {
if (data.newPassword !== data.retypePassword) {
return ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["retypePassword"],
message: "Passwords do not match",
});
}
return true;
});
const ChangePasswordForm = ({ close }: { close: () => void }) => {
const { mutate: updatePassword, isSuccess } =
useUpdatePassword<UpdatePasswordFormRequest>();
const [form, fields] = useForm({
id: "login",
constraint: getZodConstraint(ChangePasswordSchema),
onValidate({ formData }) {
return parseWithZod(formData, { schema: ChangePasswordSchema });
},
shouldValidate: "onSubmit",
onSubmit(e) {
e.preventDefault();
const data = Object.fromEntries(new FormData(e.currentTarget).entries());
updatePassword({
currentPassword: data.currentPassword.toString(),
password: data.newPassword.toString(),
});
},
});
useEffect(() => {
if (isSuccess) {
close();
}
}, [isSuccess, close]);
return (
<>
<DialogHeader>
<DialogTitle className="mb-8">Change Password</DialogTitle>
</DialogHeader>
<form {...getFormProps(form)}>
<Field
inputProps={{
name: fields.currentPassword.name,
type: "password",
}}
labelProps={{ children: "Current Password" }}
errors={fields.currentPassword.errors}
/>
<Field
inputProps={{ name: fields.newPassword.name, type: "password" }}
labelProps={{ children: "New Password" }}
errors={fields.newPassword.errors}
/>
<Field
inputProps={{
name: fields.retypePassword.name,
type: "password",
}}
labelProps={{ children: "Retype Password" }}
errors={fields.retypePassword.errors}
/>
<Button className="w-full h-14">Change Password</Button>
</form>
</>
);
};
const SetupTwoFactorDialog = ({ close }: { close: () => void }) => {
const [continueModal, setContinue] = useState<boolean>(false);
return (
<>
<DialogHeader>
<DialogTitle className="mb-8">Setup Two-Factor</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-y-6">
{continueModal ? (
<>
<p className="text-sm text-primary-2">
Enter the authentication code generated in your two-factor
application to confirm your setup.
</p>
<Input fullWidth className="text-center" />
<Button className="w-full h-14">Confirm</Button>
</>
) : (
<>
<div className="p-6 flex justify-center border bg-secondary-2">
<img src={QRImg} alt="QR" className="h-36 w-36" />
</div>
<p className="font-semibold">
Dont have access to scan? Use this code instead.
</p>
<div className="p-4 border text-primary-2 text-center font-bold">
HHH7MFGAMPJ44OM44FGAMPJ44O232
</div>
<Button className="w-full h-14" onClick={() => setContinue(true)}>
Continue
</Button>
</>
)}
</div>
</>
);
};
const ChangeAvatarForm = ({ close }: { close: () => void }) => {
const {
getRootProps,
getInputProps,
getFiles,
upload,
state,
removeFile,
cancelAll,
} = useUppy();
const isUploading = state === "uploading";
const isCompleted = state === "completed";
const hasStarted = state !== "idle" && state !== "initializing";
const file = getFiles()?.[0];
const imagePreview = useMemo(() => {
if (file) {
return URL.createObjectURL(file.data);
}
}, [file]);
useEffect(() => {
if (isCompleted) {
close();
}
}, [isCompleted, close]);
return (
<>
<DialogHeader className="mb-6">
<DialogTitle>Edit Avatar</DialogTitle>
</DialogHeader>
{!hasStarted && !getFiles().length ? (
<div
{...getRootProps()}
className="border border-border rounded text-primary-2 bg-primary-dark h-48 flex flex-col items-center justify-center">
<input
hidden
aria-hidden
name="uppyFiles[]"
key={new Date().toISOString()}
multiple
{...getInputProps()}
/>
<CloudUploadIcon className="w-24 h-24 stroke stroke-primary-dark" />
<p>Drag & Drop Files or Browse</p>
</div>
) : null}
{!hasStarted && file && (
<div className="border border-border rounded p-4 bg-primary-dark relative">
<Button
className="absolute top-1/2 right-1/2 rounded-full bg-gray-800/50 hover:bg-primary p-2 text-sm"
onClick={() => removeFile(file?.id)}>
<Cross2Icon className="size-4" />
</Button>
<img
className="w-full h-48 object-contain"
src={imagePreview}
alt="New Avatar Preview"
/>
</div>
)}
{hasStarted ? (
<div className="flex flex-col items-center gap-y-2 w-full text-primary-1">
<CloudCheckIcon className="w-32 h-32" />
{isCompleted ? "Upload completed" : "0% completed"}
</div>
) : null}
{isUploading ? (
<DialogClose asChild onClick={cancelAll}>
<Button size={"lg"} className="mt-6">
Cancel
</Button>
</DialogClose>
) : null}
{isCompleted ? (
<DialogClose asChild>
<Button size={"lg"} className="mt-6">
Close
</Button>
</DialogClose>
) : null}
{!hasStarted && !isCompleted && !isUploading ? (
<Button size={"lg"} className="mt-6" onClick={upload}>
Upload
</Button>
) : null}
</>
);
};
const PasswordDots = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="219"
height="7"
viewBox="0 0 219 7"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<circle cx="3.7771" cy="3.5" r="3.5" fill="currentColor" />
<circle cx="31.7771" cy="3.5" r="3.5" fill="currentColor" />
<circle cx="45.7771" cy="3.5" r="3.5" fill="currentColor" />
<circle cx="17.7771" cy="3.5" r="3.5" fill="currentColor" />
<circle cx="59.7771" cy="3.5" r="3.5" fill="currentColor" />
<circle cx="73.7771" cy="3.5" r="3.5" fill="currentColor" />
<circle cx="87.7771" cy="3.5" r="3.5" fill="currentColor" />
<circle cx="101.777" cy="3.5" r="3.5" fill="currentColor" />
<circle cx="131.5" cy="3.5" r="3.5" fill="currentColor" />
<circle cx="117.5" cy="3.5" r="3.5" fill="currentColor" />
<circle cx="145.5" cy="3.5" r="3.5" fill="currentColor" />
<circle cx="159.5" cy="3.5" r="3.5" fill="currentColor" />
<circle cx="173.5" cy="3.5" r="3.5" fill="currentColor" />
<circle cx="187.5" cy="3.5" r="3.5" fill="currentColor" />
<circle cx="201.5" cy="3.5" r="3.5" fill="currentColor" />
<circle cx="215.5" cy="3.5" r="3.5" fill="currentColor" />
</svg>
);
};

View File

@ -0,0 +1,103 @@
import type {MetaFunction} from "@remix-run/node";
import {useSearchParams} from "@remix-run/react";
import {Button} from "~/components/ui/button.js";
import logoPng from "~/images/lume-logo.png?url";
import lumeBgPng from "~/images/lume-bg-image.png?url";
import {Authenticated, HttpError, useGetIdentity, useGo, useNotification} from "@refinedev/core";
import {useEffect} from "react";
import {useMutation, useQuery} from "@tanstack/react-query";
import {useSdk} from "~/components/lib/sdk-context.js";
import {Identity} from "~/data/auth-provider.js";
export const meta: MetaFunction = () => {
return [{ title: "Verify Email" }];
};
export default function Verify() {
return (
<Authenticated v3LegacyAuthProviderCompatible key={""}>
<VerifyAuthenticated />
</Authenticated>
);
}
function VerifyAuthenticated() {
const go = useGo();
const {open} = useNotification();
const [searchParams] = useSearchParams();
const token = searchParams.get("token");
const sdk = useSdk();
const user = useGetIdentity<Identity>();
const exchangeToken = useQuery({
queryKey: ["exchange-token", token],
retry: false,
enabled: !!user.data?.email && !!token,
queryFn: async () => {
const ret= await sdk.account!().verifyEmail({
email: user.data?.email as string,
token: token!,
})
if (ret instanceof Error) {
return Promise.reject(ret)
}
return ret
},
});
const verifyAgain = useMutation({
mutationFn: () => {
return sdk.account!().requestEmailVerification()
},
onMutate() {
open?.({
type: "success",
message: "Email sent",
description: "Please check your email inbox and click the link",
})
}
});
useEffect(() => {
if (exchangeToken.isSuccess) {
go({ to: "/dashboard" });
}
}, [go, exchangeToken.isSuccess]);
return (
<div className="p-10 h-screen relative">
<header>
<img src={logoPng} alt="Lume logo" className="h-10"/>
</header>
<main className="flex flex-col items-center justify-center h-full">
<h1 className="text-2xl">
{exchangeToken.isLoading
? "Verifying your email." : null}
{exchangeToken.isSuccess && !exchangeToken.isLoading ? "Your email has been verified" : null}
{exchangeToken.isError && !exchangeToken.isLoading ? "Something went wrong, please try again" : null}
</h1>
{exchangeToken.isError ? (
<div>
<p className="opacity-60">{exchangeToken.error.message}</p>
<Button onClick={() => {
verifyAgain.mutate()
}}>
Send verification email again
</Button>
</div>
) : null}
{exchangeToken.isSuccess ? <p className="opacity-60">Redirecting to your dashboard</p> : null}
</main>
<div className="fixed inset-0 -z-10 overflow-clip">
<img
src={lumeBgPng}
alt="Lume background"
className="absolute top-0 left-0 right-0 object-cover z-[-1]"
/>
</div>
</div>
)
}

69
app/routes/dashboard.tsx Normal file
View File

@ -0,0 +1,69 @@
import { GeneralLayout } from "~/components/general-layout";
import {
CloudIcon,
CloudDownloadIcon,
CloudUploadSolidIcon,
} from "~/components/icons";
import { UpgradeAccountBanner } from "~/components/upgrade-account-banner";
import { UsageCard } from "~/components/usage-card";
import { UsageChart } from "~/components/usage-chart";
import { Authenticated } from "@refinedev/core";
export default function Dashboard() {
return (
<Authenticated key="dashboard">
<GeneralLayout>
<h1 className="font-bold mb-4 text-3xl">Dashboard</h1>
<UpgradeAccountBanner />
<h2 className="font-bold mb-8 mt-10 text-2xl">Current Usage</h2>
<div className="grid grid-cols-2 gap-8">
<UsageCard
label="Storage"
currentUsage={120}
monthlyUsage={130}
icon={<CloudIcon className="text-ring" />}
/>
<UsageCard
label="Download"
currentUsage={2}
monthlyUsage={10}
icon={<CloudDownloadIcon className="text-ring" />}
/>
<UsageCard
label="Upload"
currentUsage={5}
monthlyUsage={15}
icon={<CloudUploadSolidIcon className="text-ring" />}
/>
</div>
<h2 className="font-bold mb-8 mt-10 text-2xl">Historical Usage</h2>
<div className="grid gap-8 grid-cols-2">
<UsageChart
dataset={[
{ x: "3/2", y: "50" },
{ x: "3/3", y: "10" },
{ x: "3/4", y: "20" },
]}
label="Storage"
/>
<UsageChart
dataset={[
{ x: "3/2", y: "50" },
{ x: "3/3", y: "10" },
{ x: "3/4", y: "20" },
]}
label="Download"
/>
<UsageChart
dataset={[
{ x: "3/2", y: "50" },
{ x: "3/3", y: "10" },
{ x: "3/4", y: "20" },
]}
label="Upload"
/>
</div>
</GeneralLayout>
</Authenticated>
);
}

View File

@ -0,0 +1,126 @@
import { TrashIcon } from "@radix-ui/react-icons";
import type { ColumnDef } from "@tanstack/react-table";
import { DownloadIcon, FileIcon, MoreIcon } from "~/components/icons";
import { Checkbox } from "~/components/ui/checkbox";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu";
import { cn } from "~/utils";
import type { FileItem } from "~/data/file-provider";
import { usePinning } from "~/hooks/usePinning";
import filesize from "~/components/lib/filesize";
import { Button } from "~/components/ui/button";
import {useSdk} from "~/components/lib/sdk-context.js";
import {S5Client} from "@lumeweb/s5-js";
import {PROTOCOL_S5} from "@lumeweb/portal-sdk";
// This type is used to define the shape of our data.
// You can use a Zod schema here if you want.
export type File = {
name: string;
cid: string;
size: string;
createdOn: string;
};
export const columns: ColumnDef<FileItem>[] = [
// {
// id: "select",
// size: 20,
// header: ({ table }) => (
// <Checkbox
// checked={
// table.getIsAllPageRowsSelected() ||
// (table.getIsSomePageRowsSelected() && "indeterminate")
// }
// onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
// aria-label="Select all"
// />
// ),
// cell: ({ row }) => (
// <Checkbox
// checked={row.getIsSelected()}
// onCheckedChange={(value) => row.toggleSelected(!!value)}
// aria-label="Select row"
// />
// ),
// enableSorting: false,
// enableHiding: false,
// },
{
accessorKey: "name",
header: () => null,
},
{
accessorKey: "cid",
header: "CID",
cell: ({ row }) => (
<div className="flex items-center gap-x-2">
<FileIcon />
{row.getValue("name") ?? row.getValue("cid")}
</div>
),
},
{
accessorKey: "size",
header: "Size",
cell: ({ row }) => {
const size = row.getValue("size");
return size ? filesize(size as number, 2) : "";
},
},
{
accessorKey: "pinned",
header: "Pinned On",
cell: ({ row }) => new Date(row.getValue("pinned")).toLocaleString(),
},
{
accessorKey: "actions",
header: () => null,
size: 20,
cell: ({ row }) => {
const { unpin } = usePinning();
const sdk = useSdk();
const downloadFile = () => {
const cid = row.getValue("cid");
const portalUrl = sdk.protocols!().get<S5Client>(PROTOCOL_S5).getSdk().portalUrl;
window.open(`${portalUrl}/s5/download/${cid}`,"_blank");
};
return (
<div className="flex space-x-2 w-10 items-center justify-between">
<Button size={"icon"} variant="ghost" onClick={downloadFile}>
<DownloadIcon className="w-5"/>
</Button>
<DropdownMenu>
<DropdownMenuTrigger
className={cn(
"opacity-0 group-hover:opacity-100 data-[state=open]:opacity-100",
row.getIsSelected() && "block",
)}>
<MoreIcon />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuGroup>
<DropdownMenuItem
variant="destructive"
onClick={() => {
unpin(row.getValue("cid"));
}}>
<TrashIcon className="mr-2" />
Delete
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
},
},
];

View File

@ -0,0 +1,162 @@
import { GeneralLayout } from "~/components/general-layout";
import { FileCard, FileCardList, FileTypes } from "~/components/file-card";
import { DataTable } from "~/components/data-table";
import { columns } from "./columns";
import { Input } from "~/components/ui/input";
import { Button } from "~/components/ui/button";
import { AddIcon } from "~/components/icons";
import { Authenticated } from "@refinedev/core";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "~/components/ui/dialog";
import { TextareaField } from "~/components/forms";
import { z } from "zod";
import { getFormProps, useForm } from "@conform-to/react";
import { getZodConstraint, parseWithZod } from "@conform-to/zod";
import { usePinning } from "~/hooks/usePinning";
import { CID } from "@lumeweb/libs5";
import { useEffect, useState } from "react";
export default function FileManager() {
const [open, setOpen] = useState(false);
const closeModal = () => setOpen(false);
return (
<Authenticated key="file-manager">
<GeneralLayout>
<Dialog open={open} onOpenChange={setOpen}>
<h1 className="font-bold mb-4 text-lg">File Manager</h1>
<FileCardList>
<FileCard
fileName="Backups"
size="33 files"
type={FileTypes.Folder}
createdAt="2 days ago"
/>
<FileCard
fileName="Backups"
size="33 files"
type={FileTypes.Folder}
createdAt="2 days ago"
/>
<FileCard
fileName="Backups"
size="33 files"
type={FileTypes.Folder}
createdAt="2 days ago"
/>
<FileCard
fileName="Backups"
size="33 files"
type={FileTypes.Folder}
createdAt="2 days ago"
/>
</FileCardList>
<h2 className="font-bold text-l mt-8">Files</h2>
<div className="flex items-center space-x-4 my-6 w-full">
<Input
fullWidth
leftIcon={<AddIcon />}
placeholder="Search files by name or CID"
className="border-ring font-medium w-full grow h-12 flex-1 bg-primary-2/10"
/>
{/* We dont yet have any functionality for selecting so im commenting this out */}
{/* <Button className="h-12 gap-x-2">
<AddIcon />
Select All
</Button> */}
<DialogTrigger asChild>
<Button className="h-12 gap-x-2">
<AddIcon />
Pin Content
</Button>
</DialogTrigger>
</div>
<DataTable
columns={columns}
resource="file"
dataProviderName="files"
/>
<DialogContent>
<PinFilesForm close={closeModal} />
</DialogContent>
</Dialog>
</GeneralLayout>
</Authenticated>
);
}
const PinFilesSchema = z.object({
cids: z
.string()
.transform((value) => value.split(","))
.refine(
(value) => {
return value.every((cid) => {
try {
CID.decode(cid);
} catch (e) {
return false;
}
return true;
});
},
(val) => ({
message: `${val} is not a valid CID`,
}),
),
});
const PinFilesForm = ({ close }: { close: () => void }) => {
const { bulkPin, pinStatus, fetchProgress } = usePinning();
const [form, fields] = useForm({
id: "pin-files",
constraint: getZodConstraint(PinFilesSchema),
onValidate({ formData }) {
return parseWithZod(formData, { schema: PinFilesSchema });
},
shouldValidate: "onSubmit",
onSubmit(e, { submission }) {
e.preventDefault();
if (submission?.status === "success") {
const value = submission.value;
bulkPin(value.cids);
}
},
});
useEffect(() => {
if (pinStatus === "success") {
fetchProgress();
close();
}
}, [pinStatus, fetchProgress, close]);
return (
<>
<DialogHeader>
<DialogTitle>Pin Content</DialogTitle>
</DialogHeader>
<form {...getFormProps(form)} className="w-full flex flex-col gap-y-4">
<TextareaField
textareaProps={{
name: fields.cids.name,
placeholder: "Comma separated CIDs",
}}
labelProps={{ htmlFor: "cids", children: "Content to Pin" }}
errors={fields.cids.errors || pinStatus === "error" ? ["An error occurred, please try again"] : []}
/>
<Button type="submit" className="w-full" disabled={pinStatus === "pending"}>
{pinStatus === "pending" ? "Processing..." : "Pin Content"}
</Button>
</form>
</>
);
};

69
app/routes/login.otp.tsx Normal file
View File

@ -0,0 +1,69 @@
import { getFormProps, useForm } from "@conform-to/react";
import { getZodConstraint, parseWithZod } from "@conform-to/zod";
import { useGo, useIsAuthenticated, useParsed } from "@refinedev/core";
import { Link } from "@remix-run/react";
import { useEffect } from "react";
import { z } from "zod";
import { Field } from "~/components/forms";
import { Button } from "~/components/ui/button";
import type { LoginParams } from "./login";
const OtpSchema = z.object({
otp: z.string().length(6, { message: "OTP must be 6 characters" }),
});
export default function OtpForm() {
const { isLoading: isAuthLoading, data: authData } = useIsAuthenticated();
const go = useGo();
const parsed = useParsed<LoginParams>();
// TODO: Add support for resending the OTP
const [form, fields] = useForm({
id: "otp",
constraint: getZodConstraint(OtpSchema),
onValidate({ formData }) {
return parseWithZod(formData, { schema: OtpSchema });
},
shouldValidate: "onSubmit",
});
const valid = true;
const to = parsed.params?.to ?? "/dashboard";
useEffect(() => {
if (!isAuthLoading) {
if (authData?.authenticated && valid) {
go({ to, type: "push" });
}
}
}, [isAuthLoading, authData, to, go]);
return (
<form
className="w-full p-2 max-w-md mt-12 bg-background"
{...getFormProps(form)}>
<span className="block !mb-8 space-y-2">
<h2 className="text-3xl font-bold">Check your inbox</h2>
<p className="text-input-placeholder">
We will need the six digit confirmation code you received in your
email in order to verify your account and get started. Didnt receive
a code?{" "}
<Button type="button" variant={"link"} className="text-md h-0">
Resend now
</Button>
</p>
</span>
<Field
inputProps={{ name: fields.otp.name }}
labelProps={{ children: "Confirmation Code" }}
errors={fields.otp.errors}
/>
<Button className="w-full h-14">Verify</Button>
<p className="text-input-placeholder w-full text-left">
<Link
to="/login"
className="text-primary-1 text-md hover:underline hover:underline-offset-4">
Back to Login
</Link>
</p>
</form>
);
}

149
app/routes/login.tsx Normal file
View File

@ -0,0 +1,149 @@
import type { MetaFunction } from "@remix-run/node";
import { Link } from "@remix-run/react";
import { z } from "zod";
import { Button } from "~/components/ui/button";
import logoPng from "~/images/lume-logo.png?url";
import lumeColorLogoPng from "~/images/lume-color-logo.png?url";
import discordLogoPng from "~/images/discord-logo.png?url";
import lumeBgPng from "~/images/lume-bg-image.png?url";
import { Field, FieldCheckbox } from "~/components/forms";
import { getFormProps, useForm } from "@conform-to/react";
import { getZodConstraint, parseWithZod } from "@conform-to/zod";
import {
useGo,
useLogin,
useParsed,
} from "@refinedev/core";
import type { AuthFormRequest } from "~/data/auth-provider";
import { useEffect } from "react";
export const meta: MetaFunction = () => {
return [
{ title: "Login" },
{ name: "description", content: "Welcome to Lume!" },
];
};
export type LoginParams = {
to: string;
};
export default function Login() {
return (
<div className="p-10 h-screen relative">
<header>
<img src={logoPng} alt="Lume logo" className="h-10" />
</header>
<div className="fixed inset-0 -z-10 overflow-clip">
<img
src={lumeBgPng}
alt="Lume background"
className="absolute top-0 right-0 md:w-2/3 object-cover z-[-1]"
/>
</div>
<LoginForm />
<footer className="my-5">
<ul className="flex flex-row">
<li>
<Link to="https://discord.lumeweb.com">
<Button
variant={"link"}
className="flex flex-row gap-x-2 text-input-placeholder">
<img className="h-5" src={discordLogoPng} alt="Discord Logo" />
Connect with us
</Button>
</Link>
</li>
<li>
<Link to="https://lumeweb.com">
<Button
variant={"link"}
className="flex flex-row gap-x-2 text-input-placeholder">
<img className="h-5" src={lumeColorLogoPng} alt="Lume Logo" />
Connect with us
</Button>
</Link>
</li>
</ul>
</footer>
</div>
);
}
const LoginSchema = z.object({
email: z.string().email(),
password: z.string(),
rememberMe: z.boolean().optional(),
});
const LoginForm = () => {
const login = useLogin<AuthFormRequest>();
const go = useGo();
const parsed = useParsed<LoginParams>();
const [form, fields] = useForm({
id: "login",
constraint: getZodConstraint(LoginSchema),
onValidate({ formData }) {
return parseWithZod(formData, { schema: LoginSchema });
},
shouldValidate: "onSubmit",
onSubmit(e, { submission }) {
e.preventDefault();
if (submission?.status === "success") {
const data = submission.value;
login.mutate({
email: data.email,
password: data.password,
rememberMe: data.rememberMe ?? false,
redirectTo: parsed.params?.to,
});
}
},
});
useEffect(() => {
if (form.status === "success") {
go({ to: "/login/otp", type: "push" });
}
}, [form.status, go]);
return (
<form
className="w-full p-2 max-w-md space-y-3 mt-12 bg-background"
{...getFormProps(form)}>
<h2 className="text-3xl font-bold !mb-12">Welcome back! 🎉</h2>
<Field
inputProps={{ name: fields.email.name }}
labelProps={{ children: "Email" }}
errors={fields.email.errors}
/>
<Field
inputProps={{ name: fields.password.name, type: "password" }}
labelProps={{ children: "Password" }}
errors={fields.password.errors}
/>
<FieldCheckbox
inputProps={{ name: fields.rememberMe.name, form: form.id }}
labelProps={{ children: "Remember Me" }}
errors={fields.rememberMe.errors}
/>
<Button className="w-full h-14">Login</Button>
<p className="inline-block text-input-placeholder">
Forgot your password?{" "}
<Link
to="/reset-password"
className="text-primary-1 text-md hover:underline hover:underline-offset-4">
Reset Password
</Link>
</p>
<Link to="/register" className="block">
<Button type="button" className="w-full h-14" variant={"outline"}>
Create an Account
</Button>
</Link>
</form>
);
};

182
app/routes/register.tsx Normal file
View File

@ -0,0 +1,182 @@
import type { MetaFunction } from "@remix-run/node"
import { Link } from "@remix-run/react"
import { Button } from "~/components/ui/button"
import logoPng from "~/images/lume-logo.png?url"
import lumeColorLogoPng from "~/images/lume-color-logo.png?url"
import discordLogoPng from "~/images/discord-logo.png?url"
import lumeBgPng from "~/images/lume-bg-image.png?url"
import { Field, FieldCheckbox } from "~/components/forms"
import { getFormProps, useForm } from "@conform-to/react"
import { z } from "zod"
import { getZodConstraint, parseWithZod } from "@conform-to/zod"
import {useLogin, useNotification, useRegister} from "@refinedev/core";
import {AuthFormRequest, RegisterFormRequest} from "~/data/auth-provider.js";
export const meta: MetaFunction = () => {
return [{ title: "Sign Up" }];
};
const RegisterSchema = z
.object({
firstName: z.string().min(1),
lastName: z.string().min(1),
email: z.string().email(),
password: z
.string()
.min(8, { message: "Password must be at least 8 characters" }),
confirmPassword: z
.string()
.min(8, { message: "Password must be at least 8 characters" }),
termsOfService: z.boolean({
required_error: "You must agree to the terms of service",
}),
})
.superRefine((data, ctx) => {
if (data.password !== data.confirmPassword) {
return ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["confirmPassword"],
message: "Passwords do not match",
});
}
return true;
});
export default function Register() {
const register = useRegister<RegisterFormRequest>()
const login = useLogin<AuthFormRequest>();
const { open } = useNotification();
const [form, fields] = useForm({
id: "register",
constraint: getZodConstraint(RegisterSchema),
onValidate({formData}) {
return parseWithZod(formData, {schema: RegisterSchema});
},
onSubmit(e) {
e.preventDefault();
const data = Object.fromEntries(new FormData(e.currentTarget).entries());
register.mutate({
email: data.email.toString(),
password: data.password.toString(),
firstName: data.firstName.toString(),
lastName: data.lastName.toString(),
})
}
});
return (
<div className="p-10 h-screen relative">
<header>
<img src={logoPng} alt="Lume logo" className="h-10" />
</header>
<form
className="w-full p-2 max-w-md space-y-4 mt-12 bg-background"
{...getFormProps(form)}>
<span className="!mb-12 space-y-2">
<h2 className="text-3xl font-bold">All roads lead to Lume</h2>
<p className="text-input-placeholder">
🤘 Get 50 GB free storage and download for free,{" "}
<b
className="text-primar
y-2">
forever
</b>
.{" "}
</p>
</span>
<div className="flex gap-4">
<Field
className="flex-1"
inputProps={{ name: fields.firstName.name }}
labelProps={{ children: "First Name" }}
errors={fields.firstName.errors}
/>
<Field
className="flex-1"
inputProps={{ name: fields.lastName.name }}
labelProps={{ children: "Last Name" }}
errors={fields.lastName.errors}
/>
</div>
<Field
inputProps={{ name: fields.email.name }}
labelProps={{ children: "Email" }}
errors={fields.email.errors}
/>
<Field
inputProps={{ name: fields.password.name, type: "password" }}
labelProps={{ children: "Password" }}
errors={fields.password.errors}
/>
<Field
inputProps={{ name: fields.confirmPassword.name, type: "password" }}
labelProps={{ children: "Confirm Password" }}
errors={fields.confirmPassword.errors}
/>
<FieldCheckbox
inputProps={{ name: fields.termsOfService.name, form: form.id }}
labelProps={{
children: (
<span>
I agree to the
<Link
to="/terms-of-service"
className="text-primary-1 text-md hover:underline hover:underline-offset-4 mx-1">
Terms of Service
</Link>
and
<Link
to="/privacy-policy"
className="text-primary-1 text-md hover:underline hover:underline-offset-4 mx-1">
Privacy Policy
</Link>
</span>
),
}}
errors={fields.termsOfService.errors}
/>
<Button className="w-full h-14">Create Account</Button>
<p className="text-input-placeholder w-full text-right">
Already have an account?{" "}
<Link
to="/login"
className="text-primary-1 text-md hover:underline hover:underline-offset-4">
Login here instead
</Link>
</p>
</form>
<div className="fixed inset-0 -z-10 overflow-clip">
<img
src={lumeBgPng}
alt="Lume background"
className="absolute top-0 right-0 md:w-2/3 object-cover z-[-1]"
/>
</div>
<footer className="my-5">
<ul className="flex flex-row">
<li>
<Link to="https://discord.lumeweb.com">
<Button
variant={"link"}
className="flex flex-row gap-x-2 text-input-placeholder">
<img className="h-5" src={discordLogoPng} alt="Discord Logo" />
Connect with us
</Button>
</Link>
</li>
<li>
<Link to="https://lumeweb.com">
<Button
variant={"link"}
className="flex flex-row gap-x-2 text-input-placeholder">
<img className="h-5" src={lumeColorLogoPng} alt="Lume Logo" />
Connect with us
</Button>
</Link>
</li>
</ul>
</footer>
</div>
);
}

View File

@ -0,0 +1,109 @@
import type { MetaFunction } from "@remix-run/node";
import { Link } from "@remix-run/react";
import { Button } from "~/components/ui/button";
import logoPng from "~/images/lume-logo.png?url";
import lumeColorLogoPng from "~/images/lume-color-logo.png?url";
import discordLogoPng from "~/images/discord-logo.png?url";
import lumeBgPng from "~/images/lume-bg-image.png?url";
import { Field } from "~/components/forms";
import { getFormProps, useForm } from "@conform-to/react";
import { z } from "zod";
import { getZodConstraint, parseWithZod } from "@conform-to/zod";
import { ToastAction } from "~/components/ui/toast";
import { useNotification } from "@refinedev/core";
export const meta: MetaFunction = () => {
return [{ title: "Sign Up" }];
};
const RecoverPasswordSchema = z.object({
email: z.string().email(),
});
export default function RecoverPassword() {
const { open } = useNotification();
const [form, fields] = useForm({
id: "sign-up",
constraint: getZodConstraint(RecoverPasswordSchema),
onValidate({ formData }) {
return parseWithZod(formData, { schema: RecoverPasswordSchema });
},
onSubmit(e) {
e.preventDefault();
open?.({
type: "success",
message: "Password reset email sent",
description: "Check your email for a link to reset your password. If it doesnt appear within a few minutes, check your spam folder.",
action: <ToastAction altText="Cancel">Cancel</ToastAction>,
cancelMutation: () => {
console.log("cancel mutation");
},
})
}
});
// TODO: another detail is the reset password has no screen to either accept a new pass or
// just say an email has been sent.. if i were to generate a pass for them. imho i think
// a screen that just says a password reset email has been sent would be good, then a separate
// route to accept the reset token and send that to the api when would then trigger a new email
// with the pass.
return (
<div className="p-10 h-screen relative">
<header>
<img src={logoPng} alt="Lume logo" className="h-10" />
</header>
<form
className="w-full p-2 max-w-md space-y-4 mt-12 bg-background"
{...getFormProps(form)}>
<span className="!mb-12 space-y-2">
<h2 className="text-3xl font-bold">Reset your password</h2>
</span>
<Field
inputProps={{ name: fields.email.name }}
labelProps={{ children: "Email Address" }}
errors={fields.email.errors}
/>
<Button className="w-full h-14">Reset Password</Button>
<p className="text-input-placeholder w-full text-left">
<Link
to="/login"
className="text-primary-1 text-md hover:underline hover:underline-offset-4">
Back to Login
</Link>
</p>
</form>
<div className="fixed inset-0 -z-10 overflow-clip">
<img
src={lumeBgPng}
alt="Lume background"
className="absolute top-0 right-0 md:w-2/3 object-cover z-[-1]"
/>
</div>
<footer className="my-5">
<ul className="flex flex-row">
<li>
<Link to="https://discord.lumeweb.com">
<Button
variant={"link"}
className="flex flex-row gap-x-2 text-input-placeholder">
<img className="h-5" src={discordLogoPng} alt="Discord Logo" />
Connect with us
</Button>
</Link>
</li>
<li>
<Link to="https://lumeweb.com">
<Button
variant={"link"}
className="flex flex-row gap-x-2 text-input-placeholder">
<img className="h-5" src={lumeColorLogoPng} alt="Lume Logo" />
Connect with us
</Button>
</Link>
</li>
</ul>
</footer>
</div>
);
}

View File

@ -4,8 +4,8 @@
@layer base { @layer base {
:root { :root {
--background: 240, 33%, 3%; --background: 0 0% 3.9%;
--foreground: 0, 0%, 88%; --foreground: 0 0% 98%;
--card: 0 0% 0%; --card: 0 0% 0%;
--card-foreground: 0 0% 3.9%; --card-foreground: 0 0% 3.9%;
@ -16,11 +16,14 @@
--primary: 242 51% 14%; --primary: 242 51% 14%;
--primary-1: 241 90% 82%; --primary-1: 241 90% 82%;
--primary-2: 241 21% 42%; --primary-2: 241 21% 42%;
--primary-dark: 240 33% 4%;
--primary-foreground: 0 0% 88%; --primary-foreground: 0 0% 88%;
--primary-1-foreground: 240 50% 9%; --primary-1-foreground: 240 50% 9%;
--secondary: 0 0% 96.1%; --secondary: 0 0% 96.1%;
--secondary-1: 242 52% 9%;
--secondary-foreground: 0 0% 9%; --secondary-foreground: 0 0% 9%;
--secondary-1-foreground: 0 0% 100%;
--muted: 0 0% 96.1%; --muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%; --muted-foreground: 0 0% 45.1%;
@ -29,12 +32,12 @@
--accent-foreground: 0 0% 9%; --accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%; --destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 72% 51%;
--border: 240 50% 17%; --border: 240 50% 17%;
--input: 240 50% 17%; --input: 240 50% 17%;
--input-placeholder: 241 21% 42%; --input-placeholder: 241 21% 42%;
--ring: 0 0% 3.9%; --ring: 241 90% 82%;
--radius: 5px; --radius: 5px;
} }
@ -75,6 +78,6 @@
@apply border-border; @apply border-border;
} }
body { body {
@apply bg-background text-foreground font-sans; @apply bg-background text-foreground font-sans h-screen;
} }
} }

11303
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,52 +1,84 @@
{ {
"name": "", "name": "lume-portal-dashboard",
"private": true, "private": true,
"sideEffects": false, "sideEffects": false,
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "remix vite:build", "build": "remix vite:build",
"dev": "remix vite:dev", "dev": "remix vite:dev",
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
"preview": "vite preview", "preview": "vite preview",
"typecheck": "tsc" "typecheck": "tsc",
}, "patch-package": "patch-package",
"dependencies": { "postinstall": "patch-package"
"@conform-to/react": "^1.0.2", },
"@conform-to/zod": "^1.0.2", "dependencies": {
"@fontsource-variable/manrope": "^5.0.19", "@conform-to/react": "^1.0.2",
"@radix-ui/react-checkbox": "^1.0.4", "@conform-to/zod": "^1.0.2",
"@radix-ui/react-icons": "^1.3.0", "@fontsource-variable/manrope": "^5.0.19",
"@radix-ui/react-label": "^2.0.2", "@lumeweb/portal-sdk": "0.0.0-20240329003300",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-accordion": "^1.1.2",
"@remix-run/node": "^2.8.0", "@radix-ui/react-avatar": "^1.0.4",
"@remix-run/react": "^2.8.0", "@radix-ui/react-checkbox": "^1.0.4",
"class-variance-authority": "^0.7.0", "@radix-ui/react-dialog": "^1.0.5",
"clsx": "^2.1.0", "@radix-ui/react-dropdown-menu": "^2.0.6",
"react": "^18.2.0", "@radix-ui/react-icons": "^1.3.0",
"react-dom": "^18.2.0", "@radix-ui/react-label": "^2.0.2",
"tailwind-merge": "^2.2.1", "@radix-ui/react-popover": "^1.0.7",
"tailwindcss-animate": "^1.0.7" "@radix-ui/react-progress": "^1.0.3",
}, "@radix-ui/react-select": "^2.0.0",
"devDependencies": { "@radix-ui/react-slot": "^1.0.2",
"@remix-run/dev": "^2.8.0", "@radix-ui/react-tabs": "^1.0.4",
"@types/react": "^18.2.20", "@radix-ui/react-toast": "^1.1.5",
"@types/react-dom": "^18.2.7", "@radix-ui/react-tooltip": "^1.0.7",
"@typescript-eslint/eslint-plugin": "^6.7.4", "@refinedev/cli": "^2.16.1",
"@typescript-eslint/parser": "^6.7.4", "@refinedev/core": "https://gitpkg.now.sh/LumeWeb/refine/packages/core?remix",
"autoprefixer": "^10.4.18", "@refinedev/devtools-internal": "https://gitpkg.now.sh/LumeWeb/refine/packages/devtools-internal?remix",
"eslint": "^8.38.0", "@refinedev/devtools-shared": "https://gitpkg.now.sh/LumeWeb/refine/packages/devtools-shared?remix",
"eslint-import-resolver-typescript": "^3.6.1", "@refinedev/react-table": "https://gitpkg.now.sh/LumeWeb/refine/packages/react-table?remix",
"eslint-plugin-import": "^2.28.1", "@refinedev/remix-router": "https://gitpkg.now.sh/LumeWeb/refine/packages/remix?remix",
"eslint-plugin-jsx-a11y": "^6.7.1", "@remix-run/node": "^2.8.0",
"eslint-plugin-react": "^7.33.2", "@remix-run/react": "^2.8.0",
"eslint-plugin-react-hooks": "^4.6.0", "@tanstack/react-query": "^5.28.6",
"tailwindcss": "^3.4.1", "@tanstack/react-table": "^8.13.2",
"typescript": "^5.1.6", "@uppy/core": "^3.9.3",
"vite": "^5.1.0", "@uppy/tus": "^3.5.4",
"vite-tsconfig-paths": "^4.2.1" "@uppy/utils": "^5.7.4",
}, "@visx/visx": "^3.10.2",
"engines": { "class-variance-authority": "^0.7.0",
"node": ">=18.0.0" "clsx": "^2.1.0",
}, "date-fns": "^3.6.0",
"packageManager": "npm" "react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7",
"tus-js-client": "^4.1.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@remix-run/dev": "^2.8.0",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"autoprefixer": "^10.4.18",
"eslint": "^8.38.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"tailwindcss": "^3.4.1",
"typescript": "^5.1.6",
"vite": "^5.1.0",
"vite-plugin-node-polyfills": "^0.21.0",
"vite-tsconfig-paths": "^4.2.1"
},
"engines": {
"node": ">=18.0.0"
},
"overrides": {
"tus-js-client": "^4.1.0"
},
"packageManager": "npm"
} }

View File

@ -0,0 +1,13 @@
diff --git a/node_modules/@uppy/tus/lib/index.js b/node_modules/@uppy/tus/lib/index.js
index 1e0a1bb..ba95bb5 100644
--- a/node_modules/@uppy/tus/lib/index.js
+++ b/node_modules/@uppy/tus/lib/index.js
@@ -506,7 +506,7 @@ function _getCompanionClientArgs2(file) {
}
async function _uploadFiles2(files) {
const filesFiltered = filterNonFailedFiles(files);
- const filesToEmit = filterFilesToEmitUploadStarted(filesFiltered);
+ const filesToEmit = filterFilesToEmitUploadStarted(filesFiltered).filter(file => file?.uploader == 'tus');
this.uppy.emit('upload-start', filesToEmit);
await Promise.allSettled(filesFiltered.map(file => {
if (file.isRemote) {

View File

@ -0,0 +1,36 @@
diff --git a/node_modules/tus-js-client/lib.es5/upload.js b/node_modules/tus-js-client/lib.es5/upload.js
index fc3734c..e4ea6cb 100644
--- a/node_modules/tus-js-client/lib.es5/upload.js
+++ b/node_modules/tus-js-client/lib.es5/upload.js
@@ -731,7 +731,6 @@ var BaseUpload = /*#__PURE__*/function () {
parallelUploads: 1,
// Reset this option as we are not doing a parallel upload.
parallelUploadBoundaries: null,
- metadata: {},
// Add the header to indicate the this is a partial upload.
headers: _objectSpread(_objectSpread({}, _this3.options.headers), {}, {
'Upload-Concat': 'partial'
diff --git a/node_modules/tus-js-client/lib.esm/upload.js b/node_modules/tus-js-client/lib.esm/upload.js
index 99cc7ff..f8f832d 100644
--- a/node_modules/tus-js-client/lib.esm/upload.js
+++ b/node_modules/tus-js-client/lib.esm/upload.js
@@ -279,7 +279,6 @@ var BaseUpload = /*#__PURE__*/function () {
parallelUploads: 1,
// Reset this option as we are not doing a parallel upload.
parallelUploadBoundaries: null,
- metadata: {},
// Add the header to indicate the this is a partial upload.
headers: _objectSpread(_objectSpread({}, _this3.options.headers), {}, {
'Upload-Concat': 'partial'
diff --git a/node_modules/tus-js-client/lib/upload.js b/node_modules/tus-js-client/lib/upload.js
index e564de3..10fba60 100644
--- a/node_modules/tus-js-client/lib/upload.js
+++ b/node_modules/tus-js-client/lib/upload.js
@@ -332,7 +332,6 @@ class BaseUpload {
parallelUploads: 1,
// Reset this option as we are not doing a parallel upload.
parallelUploadBoundaries: null,
- metadata: {},
// Add the header to indicate the this is a partial upload.
headers: {
...this.options.headers,

View File

@ -20,7 +20,7 @@ const config = {
border: "hsl(var(--border))", border: "hsl(var(--border))",
input: { input: {
DEFAULT: "hsl(var(--input))", DEFAULT: "hsl(var(--input))",
placeholder: "hsl(var(--input-placeholder))", placeholder: "hsl(var(--input-placeholder))"
}, },
ring: "hsl(var(--ring))", ring: "hsl(var(--ring))",
background: "hsl(var(--background))", background: "hsl(var(--background))",
@ -34,12 +34,19 @@ const config = {
foreground: "hsl(var(--primary-1-foreground))" foreground: "hsl(var(--primary-1-foreground))"
}, },
"primary-2": { "primary-2": {
DEFAULT: "hsl(var(--primary-2))", DEFAULT: "hsl(var(--primary-2))"
},
"primary-dark": {
DEFAULT: "hsl(var(--primary-dark))"
}, },
secondary: { secondary: {
DEFAULT: "hsl(var(--secondary))", DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))" foreground: "hsl(var(--secondary-foreground))"
}, },
"secondary-1": {
DEFAULT: "hsl(var(--secondary-1))",
foreground: "hsl(var(--secondary-1-foreground))"
},
destructive: { destructive: {
DEFAULT: "hsl(var(--destructive))", DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))" foreground: "hsl(var(--destructive-foreground))"
@ -66,6 +73,9 @@ const config = {
md: "calc(var(--radius) - 2px)", md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)" sm: "calc(var(--radius) - 4px)"
}, },
borderWidth: {
1: '1px'
},
keyframes: { keyframes: {
"accordion-down": { "accordion-down": {
from: { height: "0" }, from: { height: "0" },

9
vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_PORTAL_API_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

View File

@ -1,25 +1,32 @@
import { vitePlugin as remix } from "@remix-run/dev"; import {vitePlugin as remix} from "@remix-run/dev"
import { defineConfig } from "vite"; import {defineConfig} from "vite"
import tsconfigPaths from "vite-tsconfig-paths"; import tsconfigPaths from "vite-tsconfig-paths"
import {nodePolyfills} from 'vite-plugin-node-polyfills'
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
remix({ remix({
ssr: false, ssr: false,
ignoredRouteFiles: ["**/*.css"], ignoredRouteFiles: ["**/*.css"]
}),
}), tsconfigPaths(),
tsconfigPaths(), nodePolyfills({protocolImports: false}),
], ],
server: { build: {
fs: { minify: false
// Restrict files that could be served by Vite's dev server. Accessing
// files outside this directory list that aren't imported from an allowed
// file will result in a 403. Both directories and files can be provided.
// If you're comfortable with Vite's dev server making any file within the
// project root available, you can remove this option. See more:
// https://vitejs.dev/config/server-options.html#server-fs-allow
allow: ["app", "node_modules/@fontsource-variable/manrope"],
}, },
}, server: {
}); fs: {
// Restrict files that could be served by Vite's dev server. Accessing
// files outside this directory list that aren't imported from an allowed
// file will result in a 403. Both directories and files can be provided.
// If you're comfortable with Vite's dev server making any file within the
// project root available, you can remove this option. See more:
// https://vitejs.dev/config/server-options.html#server-fs-allow
allow: [
"app",
"node_modules/@fontsource-variable/manrope",
]
}
}
})