Compare commits

...

247 Commits

Author SHA1 Message Date
daniel31x13 90a3170ffd minor revert 2024-01-12 16:05:15 -05:00
daniel31x13 b7656a3afc minor change 2024-01-12 16:00:02 -05:00
daniel31x13 7109620dc7 initial commit for mobile app 2024-01-12 15:57:49 -05:00
daniel31x13 834d25a99e minor update to README.md 2024-01-08 10:23:24 -05:00
daniel31x13 a67980b29d updated .env.sample 2024-01-05 14:21:58 -05:00
daniel31x13 7880551c4d updated version 2024-01-02 15:15:14 -05:00
daniel31x13 98fbb5b678 bug fixed 2024-01-02 15:11:38 -05:00
Daniel 0c2c837028 Merge pull request #398 from modem7/yarn-cache
Implement docker cache mount for yarn
2024-01-02 12:41:34 -05:00
modem7 a5b166f41d implement docker cache mount for yarn 2024-01-02 17:39:50 +00:00
Daniel 89de1829c2 Merge pull request #395 from linkwarden/dev
Revert "updated README.md"
2024-01-02 07:16:56 -05:00
daniel31x13 fbca98984b Revert "updated README.md"
This reverts commit 4da2310e95.
2024-01-02 07:16:11 -05:00
Daniel 06ab784441 Merge pull request #394 from linkwarden/dev
updated README.md
2024-01-02 07:12:50 -05:00
daniel31x13 4da2310e95 updated README.md 2024-01-02 07:12:18 -05:00
Daniel a8f4072f1c Merge pull request #393 from linkwarden/dev
updated SECURITY.md
2024-01-02 07:01:28 -05:00
daniel31x13 93bcfc67fe updated SECURITY.md 2024-01-02 07:01:04 -05:00
Daniel ba49946974 Merge pull request #391 from linkwarden/dev
Dev
2024-01-01 17:13:16 -05:00
Daniel d16b296b15 Merge pull request #390 from QAComet/qacomet/worker-environment-variables
fix: load environment variables in the worker script
2024-01-01 17:12:46 -05:00
QAComet 3fc61ac5ce fix: load environment variables in the worker script 2024-01-01 15:09:55 -07:00
daniel31x13 ced51e4801 minor fix 2024-01-01 10:37:20 -05:00
Daniel 254c090605 Merge pull request #387 from linkwarden/dev
Dev
2023-12-31 16:05:41 -05:00
daniel31x13 2a83ced9d8 updated README 2023-12-31 16:05:18 -05:00
daniel31x13 52d333f085 updated README 2023-12-31 16:03:19 -05:00
Daniel fbbb97b4cd Merge pull request #385 from linkwarden/dev
added an extra environment variable
2023-12-31 10:46:50 -05:00
daniel31x13 4e29330472 added an extra environment variable 2023-12-31 10:46:09 -05:00
Daniel 44c82ff426 Merge pull request #384 from linkwarden/dev
replaced link outer component with <Link> tag for better accessibility
2023-12-31 10:06:06 -05:00
daniel31x13 29e0370808 replaced link outer component with <Link> tag for better accessibility 2023-12-31 10:05:30 -05:00
Daniel 74399c1708 Merge pull request #383 from linkwarden/dev
update version number
2023-12-31 08:03:36 -05:00
daniel31x13 1dde8a6088 update version number 2023-12-31 08:03:15 -05:00
daniel31x13 e872c25332 small improvement + better error handling 2023-12-31 07:55:45 -05:00
Daniel dea1e12700 Merge pull request #379 from linkwarden/dev
bugs fixed
2023-12-30 09:33:39 -05:00
daniel31x13 055869883a bugs fixed 2023-12-30 09:31:53 -05:00
Daniel a5d3926d84 Merge pull request #376 from linkwarden/dev
hotfix
2023-12-30 00:05:30 -05:00
daniel31x13 eee6a807da hotfix 2023-12-30 00:03:45 -05:00
Daniel e24ae15a73 Merge pull request #375 from linkwarden/dev
Dev
2023-12-29 23:59:34 -05:00
daniel31x13 f1dadf1546 throw error if a link was blocking the queue for too long 2023-12-29 23:59:00 -05:00
daniel31x13 ee6dcdcc5b added prettier settings 2023-12-29 12:21:22 -05:00
Daniel e4cf682217 Merge pull request #372 from linkwarden/dev
minor bug fixed
2023-12-29 10:53:41 -05:00
daniel31x13 e4fc8948fa minor bug fixed 2023-12-29 10:52:08 -05:00
Daniel 9cfdb714c3 Merge pull request #366 from linkwarden/dev
bugs fixed
2023-12-27 19:44:32 -05:00
daniel31x13 ca093008b7 bugs fixed 2023-12-27 19:44:11 -05:00
Daniel 3dc99eff9d Merge pull request #364 from linkwarden/dev
updated README
2023-12-27 18:27:59 -05:00
daniel31x13 530e83ba34 updated README 2023-12-27 18:27:30 -05:00
Daniel 4ae75634ca Merge pull request #363 from linkwarden/dev
updated README
2023-12-27 18:17:44 -05:00
daniel31x13 372ed248f1 updated README 2023-12-27 18:17:16 -05:00
Daniel 8b3b7445c3 Merge pull request #362 from linkwarden/dev
minor change to README
2023-12-27 16:06:50 -05:00
daniel31x13 327bb10a08 minor change to README 2023-12-27 16:06:23 -05:00
Daniel f9f2a8ca64 Merge pull request #361 from linkwarden/dev
Dev
2023-12-27 16:02:20 -05:00
daniel31x13 31c56b5009 minor change 2023-12-27 15:52:06 -05:00
daniel31x13 7cf59bd430 minor improvement 2023-12-27 15:10:12 -05:00
daniel31x13 feb18b8c7a minor fix 2023-12-27 15:06:18 -05:00
daniel31x13 ea9f940517 minor fix 2023-12-27 15:01:48 -05:00
daniel31x13 76a0151e43 minor improvement 2023-12-27 15:00:48 -05:00
daniel31x13 a78d9774a7 updated README 2023-12-27 14:55:34 -05:00
daniel31x13 dfbd56acc9 minor fix 2023-12-25 20:43:31 -05:00
daniel31x13 8a34413482 updated README 2023-12-25 11:18:17 -05:00
daniel31x13 2e5f2deee7 minor fix 2023-12-24 10:45:48 -05:00
daniel31x13 320cddf224 updated public page 2023-12-24 07:30:45 -05:00
daniel31x13 86820c402b bug fix + improvements + bundled up the app 2023-12-24 06:46:08 -05:00
daniel31x13 e27fb90f14 finalized link card 2023-12-23 19:00:53 -05:00
daniel31x13 848a33a53e better link card 2023-12-23 12:11:47 -05:00
daniel31x13 98106b9f25 added seed script + renamed table fields + added field 2023-12-22 13:13:43 -05:00
Daniel 385bdc2343 Merge pull request #358 from linkwarden:feat/customizable-view
Feat/customizable-view
2023-12-21 19:25:41 +03:30
daniel31x13 e9b47a69c5 improvements 2023-12-21 10:55:07 -05:00
daniel31x13 1511ee1def minor change 2023-12-21 05:11:26 -05:00
daniel31x13 cb5e6de8b8 better component naming + folder structure 2023-12-21 05:08:56 -05:00
daniel31x13 b7387b1e08 minor change 2023-12-20 10:20:06 -05:00
daniel31x13 c0bca32462 bug fixed 2023-12-20 02:49:07 -05:00
daniel31x13 79bf67f879 minor fix 2023-12-20 02:28:19 -05:00
daniel31x13 8529602252 improved user experience 2023-12-19 18:28:48 -05:00
Daniel ad895eee17 Merge pull request #352 from YeeJiaWei/collection-sharing-layout
Update collection sharing permission to predefined role
2023-12-20 01:52:58 +03:30
Daniel 31cf3c4f01 Merge branch 'dev' into collection-sharing-layout 2023-12-20 01:52:51 +03:30
daniel31x13 55c43d6f9e many bug fixes and improvements 2023-12-19 17:20:09 -05:00
daniel31x13 b65787358f improve link refresh logic + many changes and improvements 2023-12-19 11:50:43 -05:00
Yee Jia Wei 9e2f70d2eb update collection sharing permission to predefined 2023-12-18 14:56:37 +08:00
Daniel 71b99bb25c Merge pull request #351 from YeeJiaWei/collection-background-fix
Fix collection background stretching
2023-12-18 09:21:13 +03:30
Yee Jia Wei 82452555e5 update bg height to fix height 2023-12-18 13:05:41 +08:00
Daniel ce746f33fd Merge pull request #347 from linkwarden/bootstrap-icons
Bootstrap icons
2023-12-18 08:26:13 +03:30
daniel31x13 7131fde897 final touch 2023-12-17 23:55:38 -05:00
Daniel a6e0af6b6e Merge pull request #350 from YeeJiaWei/sidebar-highlight-link
Sidebar highlight link
2023-12-18 08:10:44 +03:30
Daniel f961ec0109 Merge branch 'bootstrap-icons' into sidebar-highlight-link 2023-12-18 08:10:21 +03:30
daniel31x13 1677e132f3 final touch 2023-12-17 23:32:33 -05:00
Daniel 141ca8f60b Merge pull request #349 from YeeJiaWei/bootstrap-icons
Bootstrap icons
2023-12-18 06:49:02 +03:30
Yee Jia Wei fbc083c373 add icon to link create modal
more option collapse icon
2023-12-18 08:30:14 +08:00
Yee Jia Wei feda50464c update missing collection color picker icon 2023-12-18 08:28:52 +08:00
Yee Jia Wei 5e0f38c0d5 update sidebar main link 2023-12-18 07:54:33 +08:00
Yee Jia Wei e9acb548bf remove fontawesome package 2023-12-18 06:28:42 +08:00
Yee Jia Wei e1f036adb2 minor fix 2023-12-18 06:06:55 +08:00
Yee Jia Wei 1da960a3cf update public list icons 2023-12-18 06:06:49 +08:00
Yee Jia Wei 8c73ce60e9 update collection sharing model icons 2023-12-18 05:59:52 +08:00
Yee Jia Wei a481903b50 update tag page's icons 2023-12-18 05:44:06 +08:00
Yee Jia Wei 6632c0507b remove icon from accent submit button
as it is not used currently
2023-12-18 05:42:25 +08:00
Yee Jia Wei fc679d1150 update collection page icon 2023-12-17 17:32:59 +08:00
Yee Jia Wei d849b37f6c update setting pages's icon 2023-12-17 17:32:33 +08:00
Yee Jia Wei dc6c17f8c4 update setting sidebar icons 2023-12-17 17:18:37 +08:00
Yee Jia Wei e2cf627ccd update not link found icons 2023-12-17 17:18:37 +08:00
Yee Jia Wei de13a109c6 update readable view icons 2023-12-17 17:18:37 +08:00
Yee Jia Wei 15c6213840 update modal icons 2023-12-17 17:18:37 +08:00
Yee Jia Wei 42f9dacffd update announcement bar icon 2023-12-17 17:18:37 +08:00
Yee Jia Wei 4210913277 update search page icons 2023-12-17 17:18:37 +08:00
Yee Jia Wei 5f095b5631 minor fix 2023-12-17 16:47:32 +08:00
Yee Jia Wei cf1306d2c4 update collection action icons 2023-12-17 16:46:21 +08:00
Yee Jia Wei 500f7a338c update link component icons 2023-12-17 16:30:09 +08:00
Yee Jia Wei e910172558 update link action icons 2023-12-17 16:22:08 +08:00
Yee Jia Wei d906391ae2 minor fix 2023-12-17 15:35:09 +08:00
Daniel ae541bf2f5 Merge pull request #346 from YeeJiaWei/bootstrap-icons
Bootstrap icons
2023-12-17 10:02:57 +03:30
Daniel c28c73ce18 Merge branch 'dev' into bootstrap-icons 2023-12-17 10:00:44 +03:30
Daniel f7d8ff9881 Merge pull request #345 from linkwarden/feat/multiple-link-views
Feat/multiple link views
2023-12-17 09:56:54 +03:30
daniel31x13 c5b083e802 implemented list view in other components as well 2023-12-17 01:25:46 -05:00
daniel31x13 4d691e0cce minor fix 2023-12-17 00:11:15 -05:00
daniel31x13 f5e7e373a8 improvements 2023-12-16 15:06:26 -05:00
Yee Jia Wei ef33f2c948 update link, collection header icons 2023-12-16 23:34:29 +08:00
Yee Jia Wei d976761280 add page header component 2023-12-16 23:34:29 +08:00
Yee Jia Wei 04ede17bfd update homepage icons 2023-12-16 23:11:35 +08:00
Yee Jia Wei 9119402dac update navbar icons 2023-12-16 22:46:29 +08:00
Yee Jia Wei d52afd66f3 update sidebar icons 2023-12-16 22:16:25 +08:00
Yee Jia Wei ae87b5698e add bootstrap-icons package 2023-12-16 22:15:38 +08:00
Daniel 1955cca589 Merge pull request #342 from YeeJiaWei/compact-list-view
link compact list view
2023-12-16 16:04:26 +03:30
Yee Jia Wei 8df0eab2a2 add store to localstorage 2023-12-16 12:57:50 +08:00
Yee Jia Wei e0bb7ffa08 update responsive 2023-12-16 12:16:56 +08:00
Yee Jia Wei 7c35fe409f Update LinkCard.tsx 2023-12-16 12:08:28 +08:00
Yee Jia Wei ce9b4b05d4 smaller link icon size 2023-12-16 12:05:35 +08:00
Yee Jia Wei b246cdbc44 change link icon to component 2023-12-16 12:05:19 +08:00
Yee Jia Wei a2b1513dbc remove duplicated link 2023-12-16 11:52:50 +08:00
Yee Jia Wei bcfbdf3e49 link compact list view 2023-12-16 11:25:39 +08:00
daniel31x13 f8ad08f5ed improvements 2023-12-15 16:18:54 -05:00
daniel31x13 530ec69d1c better UX + more consistent layout 2023-12-15 15:47:08 -05:00
daniel31x13 b74ff01ce6 finalized archiveHandler/background worker logic 2023-12-13 17:32:01 -05:00
daniel31x13 a001f70b9d improvements 2023-12-13 06:59:36 -05:00
daniel31x13 ca3eb29c48 Merge branch 'dev' of https://github.com/linkwarden/linkwarden into dev 2023-12-13 06:33:14 -05:00
Daniel 9af695deaf Merge pull request #339 from treyg/manifest-name-update
fix: update manifest name for PWA display
2023-12-13 15:02:48 +03:30
daniel31x13 650fa693bd minor changes 2023-12-12 13:26:53 -05:00
daniel31x13 099024518f minor fix 2023-12-12 10:43:40 -05:00
Trey Gordon 6d227750c3 fix: update manifest name for PWA display 2023-12-11 15:35:44 -05:00
daniel31x13 6ba2aab0ba improved archive logic 2023-12-11 03:05:47 -05:00
daniel31x13 375a55dd37 added auto-archive script + minor improvements 2023-12-10 15:26:44 -05:00
daniel31x13 8e49ccf723 minor change 2023-12-08 11:01:47 -05:00
Daniel ab83d1d0c6 Merge pull request #332 from linkwarden/feat/refactor-login
Feat/refactor login
2023-12-08 01:14:51 +03:30
daniel31x13 fc9de564b6 minor improvements 2023-12-07 16:33:01 -05:00
daniel31x13 8786f8b5fe minor fix 2023-12-07 12:41:27 -05:00
Daniel a6a9402425 Merge pull request #327 from shxshxshxshx/feature/login-refactor
feat: refactored login
2023-12-07 21:05:14 +03:30
Daniel b36dd49e16 Merge branch 'feat/extra-login-providers' into feature/login-refactor 2023-12-07 21:05:02 +03:30
Daniel add781451a Merge pull request #331 from linkwarden/feat/extra-login-providers
Feat/Added Authentic Support
2023-12-07 21:02:17 +03:30
daniel31x13 e6979d4e75 visual improvements 2023-12-07 12:29:45 -05:00
Sebastian Hierholzer 9868ab61c9 fix: typo 2023-12-07 14:49:56 +01:00
Sebastian Hierholzer c367992116 fix: update .env.sample 2023-12-07 13:26:34 +01:00
Sebastian Hierholzer 64d361fa23 fix: undefined variables failing to parse 2023-12-07 13:25:34 +01:00
Sebastian Hierholzer 760c0b0026 Merge remote-tracking branch 'origin/feature/login-refactor' into feature/login-refactor 2023-12-07 13:06:55 +01:00
Sebastian Hierholzer 3f4b7117bd fix: add getLogins boilerplate 2023-12-07 13:06:19 +01:00
Sebastian Hierholzer 361795ed47 feat: refactored login 2023-12-07 13:06:11 +01:00
daniel31x13 93e4897c0b code formatting 2023-12-07 06:35:08 -05:00
Daniel 5b44bbcf59 Merge pull request #321 from Jacq/main
Added Authentik provider and option to disable standard login
2023-12-07 09:15:41 +03:30
Daniel 8ba2cecf06 Merge branch 'feat/extra-login-providers' into main 2023-12-07 09:14:25 +03:30
Daniel 9cd165f2ce Merge pull request #330 from linkwarden/feat/handle-files
Feat/handle files
2023-12-07 09:04:18 +03:30
daniel31x13 ce5b1f444a updated links id page 2023-12-07 00:33:05 -05:00
daniel31x13 4b1017f45b [WIP] 2023-12-06 16:13:11 -05:00
daniel31x13 6f77882ffc minor change 2023-12-05 15:21:28 -05:00
daniel31x13 f8811a49c0 improved UI 2023-12-05 15:17:36 -05:00
daniel31x13 0e6b47d068 improved UI 2023-12-05 04:39:01 -05:00
daniel31x13 a3106e072b improved UI 2023-12-05 04:12:48 -05:00
daniel31x13 1f20180a51 fixes and improvements 2023-12-04 10:24:45 -05:00
daniel31x13 ee05975e10 minor change 2023-12-03 23:52:50 -05:00
daniel31x13 9c65e3e215 implemented basic support for pdf, png and jpg 2023-12-03 23:52:32 -05:00
Sebastian Hierholzer ba72de19ef fix: add getLogins boilerplate 2023-12-03 22:19:02 +01:00
Sebastian Hierholzer ffc927759e feat: refactored login 2023-12-03 21:01:28 +01:00
daniel31x13 33be9e5d83 minor patch 2023-12-02 15:23:26 -05:00
daniel31x13 c447b36540 minor fix 2023-12-02 13:34:28 -05:00
daniel31x13 18e0b8b010 removed extra fields 2023-12-02 13:33:07 -05:00
Daniel 2042b94680 Merge pull request #325 from linkwarden/dev
Dev
2023-12-02 21:58:44 +03:30
Daniel 104c79cd99 Merge branch 'feat/handle-files' into dev 2023-12-02 21:58:23 +03:30
Daniel 1b1e4108ec Merge pull request #324 from linkwarden/feat/improve-ui-ux-1
added daisyUI components
2023-12-02 21:21:44 +03:30
daniel31x13 e253485e3d minor improvement to link card 2023-12-02 12:50:24 -05:00
daniel31x13 e2a5f36008 minor improvement to link cards 2023-12-02 12:48:34 -05:00
daniel31x13 17721c91b6 improved UX 2023-12-02 04:47:53 -05:00
daniel31x13 230110e912 added confirmation when deleting a link + more spacing for dropdowns 2023-12-02 04:42:51 -05:00
daniel31x13 4ac7110fb4 minor improvement 2023-12-01 17:48:48 -05:00
daniel31x13 e8a91bb551 renamed modal contents 2023-12-01 17:44:34 -05:00
daniel31x13 9e4502c015 improved DX 2023-12-01 17:42:45 -05:00
daniel31x13 a36769c521 many bug fixes + add links and collections together + more changes 2023-12-01 16:29:17 -05:00
daniel31x13 a3c6d9b42e more modals replaced 2023-12-01 14:00:52 -05:00
daniel31x13 2c9541734a minor improvement 2023-12-01 12:23:40 -05:00
daniel31x13 d40373032a replaced viewteam modal with new modal 2023-12-01 12:17:00 -05:00
daniel31x13 732a5227d3 recreated modals and many other components 2023-12-01 12:01:56 -05:00
daniel31x13 6d51b6de53 minor change 2023-11-30 11:47:24 -05:00
daniel31x13 2fd21c8219 redesigned link collection page 2023-11-30 06:55:37 -05:00
daniel31x13 cfc308f521 refactoring [WIP] 2023-11-30 06:13:42 -05:00
daniel31x13 5850a423f9 minor fix 2023-11-30 04:39:51 -05:00
daniel31x13 bef8ad976d recreated the rest of the dropdowns using daisyui 2023-11-30 04:36:40 -05:00
daniel31x13 64a1f352cf recreated SortDropdown components 2023-11-29 15:17:51 -05:00
daniel31x13 93e0fe6172 improved modals 2023-11-29 09:41:24 -05:00
daniel31x13 692b9b99e7 implemented new modal 2023-11-29 00:46:31 -05:00
daniel31x13 3b2b9e8279 new modal [WIP] 2023-11-28 14:24:52 -05:00
daniel31x13 82b743fa8d new modal [WIP] 2023-11-28 05:39:45 -05:00
daniel31x13 1ca6d72f82 improved collection card and avatar grouping 2023-11-28 00:29:11 -05:00
daniel31x13 916c69602d recreated many components 2023-11-27 16:38:38 -05:00
Jacq b1dd9d66b6 Update [...nextauth].ts
Added Authentik provider
Added option to disable standard login with NEXT_PUBLIC_DISABLE_LOGIN=true
2023-11-26 12:53:51 +01:00
daniel31x13 b51b08b0f4 changed classNames 2023-11-26 05:17:08 -05:00
daniel31x13 0a398d1fd9 daisy ui [WIP] 2023-11-25 05:54:43 -05:00
daniel31x13 d53dd93bb7 swapped some gray colors with neutral 2023-11-25 05:39:56 -05:00
daniel31x13 3c9d171f4d minor change 2023-11-25 03:33:17 -05:00
daniel31x13 af80614b3a bug fixed 2023-11-25 03:27:34 -05:00
daniel31x13 b88fa446be webpages can now be a image or pdf 2023-11-25 03:19:02 -05:00
daniel31x13 a33d68c03a Merge branch 'use-daisyui-for-better-dx' of https://github.com/linkwarden/linkwarden into use-daisyui-for-better-dx 2023-11-24 13:59:32 -05:00
Daniel ba7024db83 Merge pull request #317 from linkwarden/dev
Dev
2023-11-24 22:27:45 +03:30
Daniel 0f40578ca9 Merge pull request #316 from linkwarden/dev
bug fixed
2023-11-24 21:59:16 +03:30
daniel31x13 676c7c3a5d bug fixed 2023-11-24 13:28:47 -05:00
Daniel 544585afd9 Merge pull request #315 from linkwarden/dev
Dev
2023-11-24 21:22:49 +03:30
daniel31x13 87196b1190 minor change 2023-11-24 12:52:07 -05:00
daniel31x13 94d1bbbfba bug fixed 2023-11-24 12:51:43 -05:00
daniel31x13 cbd0ec6aa7 minor change 2023-11-24 12:39:54 -05:00
Daniel f78eefbb3b Merge pull request #314 from linkwarden/dev
v2.3.0
2023-11-24 20:52:57 +03:30
daniel31x13 a8172a9dbe replace tw colors with semantic colors [WIP] 2023-11-24 09:39:38 -05:00
daniel31x13 75d4fce8ec removed extra classNames 2023-11-24 08:39:55 -05:00
daniel31x13 73954fe78e minor change 2023-11-24 08:09:40 -05:00
daniel31x13 14f9378375 minor fix 2023-11-24 08:07:08 -05:00
daniel31x13 3afd5fef6e used daisyUI for dark mode 2023-11-24 07:50:16 -05:00
daniel31x13 b8b6fe24bc WIP 2023-11-24 03:06:33 -05:00
daniel31x13 828e8eae2e updated README 2023-11-23 15:47:34 -05:00
daniel31x13 1b53fb139d updated README 2023-11-23 15:43:38 -05:00
daniel31x13 c5d9f2c127 improvements 2023-11-23 09:03:47 -05:00
daniel31x13 f25b83bc09 updated README 2023-11-23 05:20:21 -05:00
daniel31x13 9a15ca9684 updated README 2023-11-20 16:03:38 -05:00
daniel31x13 557494747d improvements + updated README 2023-11-20 15:58:32 -05:00
daniel31x13 5968bc6c9c temporarily disabled daisyUI 2023-11-20 12:53:15 -05:00
daniel31x13 cf7b18e012 added apikey model 2023-11-20 12:48:41 -05:00
daniel31x13 9ad277c784 bug fixed 2023-11-19 22:32:23 -05:00
daniel31x13 9f181fb15e bug fixed 2023-11-19 22:28:02 -05:00
daniel31x13 0c6911aaf0 updated route + bug fixed 2023-11-19 16:22:27 -05:00
daniel31x13 bd16136946 minor change to README 2023-11-19 14:45:44 -05:00
daniel31x13 9eee3eea1d minor change to README 2023-11-19 14:44:41 -05:00
daniel31x13 0579395e93 minor change 2023-11-19 14:40:11 -05:00
daniel31x13 988d647521 more concise .env.sample 2023-11-19 09:19:35 -05:00
daniel31x13 e628b3a6d5 rename "Refresh Format" to "Refresh Link" 2023-11-19 09:15:29 -05:00
Daniel c73f13a9b0 Merge pull request #310 from linkwarden/keycloak-integration
Keycloak integration
2023-11-19 17:30:10 +03:30
daniel31x13 9a28552af5 bug fix 2023-11-19 08:56:03 -05:00
daniel31x13 9938d21499 minor changes 2023-11-19 08:38:05 -05:00
Daniel eb78fb71d9 Merge pull request #284 from BTLzdravtech/dev
feat: Basic support for Keycloak (OIDC) + fix s3 integration + custom s3 (minio) support
2023-11-19 16:55:09 +03:30
Daniel e2fdb11a67 Merge pull request #309 from linkwarden/improved-public-page
minor fix
2023-11-19 16:53:49 +03:30
daniel31x13 e2f9439d40 minor fix 2023-11-19 08:18:10 -05:00
Daniel 30c9c86e22 Merge pull request #308 from linkwarden/improved-public-page
Improved public page
2023-11-19 16:43:24 +03:30
daniel31x13 614d92f050 finished the public page 2023-11-19 08:12:37 -05:00
daniel31x13 b50ec09727 minor fix 2023-11-16 06:52:19 -05:00
daniel31x13 01602bafec improved public page [WIP] 2023-11-16 06:51:28 -05:00
daniel31x13 d972ec2dab better public page [WIP] 2023-11-16 03:22:16 -05:00
daniel31x13 021f7c9481 added view team modal 2023-11-15 23:31:13 -05:00
daniel31x13 59815f47d8 refactored public page endpoints 2023-11-15 13:12:06 -05:00
Tomáš Hruška 836dc10c2b fix: s3 integration + custom s3 (minio) support 2023-11-09 11:41:29 +01:00
Tomáš Hruška 946eed3773 feat: basic support for Keycloak (OIDC) 2023-11-09 11:41:08 +01:00
268 changed files with 13261 additions and 5610 deletions
+13 -13
View File
@@ -1,22 +1,22 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
{
"name": "Node.js & TypeScript",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye",
"name": "Node.js & TypeScript",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye",
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "yarn install",
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "yarn install",
// Configure tool-specific properties.
// "customizations": {},
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
"remoteUser": "root"
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
"remoteUser": "root"
}
+360 -4
View File
@@ -1,25 +1,381 @@
NEXTAUTH_SECRET=very_sensitive_secret
NEXTAUTH_URL=http://localhost:3000/api/v1/auth
# Manual installation database settings
DATABASE_URL=postgresql://user:password@localhost:5432/linkwarden
NEXTAUTH_URL=http://localhost:3000
# Docker installation database settings
POSTGRES_PASSWORD=super_secret_password
# Additional Optional Settings
PAGINATION_TAKE_COUNT=
STORAGE_FOLDER=
AUTOSCROLL_TIMEOUT=
NEXT_PUBLIC_DISABLE_REGISTRATION=
NEXT_PUBLIC_CREDENTIALS_ENABLED=
DISABLE_NEW_SSO_USERS=
RE_ARCHIVE_LIMIT=
NEXT_PUBLIC_MAX_FILE_SIZE=
MAX_LINKS_PER_USER=
ARCHIVE_TAKE_COUNT=
BROWSER_TIMEOUT=
IGNORE_UNAUTHORIZED_CA=
# AWS S3 Settings
SPACES_KEY=
SPACES_SECRET=
SPACES_ENDPOINT=
SPACES_BUCKET_NAME=
SPACES_REGION=
SPACES_FORCE_PATH_STYLE=
# SMTP Settings
NEXT_PUBLIC_EMAIL_PROVIDER=
EMAIL_FROM=
EMAIL_SERVER=
# Docker postgres settings
POSTGRES_PASSWORD=
#
# SSO Providers
#
# 42 School
NEXT_PUBLIC_FORTYTWO_ENABLED=
FORTYTWO_CUSTOM_NAME=
FORTYTWO_CLIENT_ID=
FORTYTWO_CLIENT_SECRET=
# Apple
NEXT_PUBLIC_APPLE_ENABLED=
APPLE_CUSTOM_NAME=
APPLE_ID=
APPLE_SECRET=
# Atlassian
NEXT_PUBLIC_ATLASSIAN_ENABLED=
ATLASSIAN_CUSTOM_NAME=
ATLASSIAN_CLIENT_ID=
ATLASSIAN_CLIENT_SECRET=
ATLASSIAN_SCOPE=
# Auth0
NEXT_PUBLIC_AUTH0_ENABLED=
AUTH0_CUSTOM_NAME=
AUTH0_ISSUER=
AUTH0_CLIENT_SECRET=
AUTH0_CLIENT_ID=
# Authentik
NEXT_PUBLIC_AUTHENTIK_ENABLED=
AUTHENTIK_CUSTOM_NAME=
AUTHENTIK_ISSUER=
AUTHENTIK_CLIENT_ID=
AUTHENTIK_CLIENT_SECRET=
# Battle.net
NEXT_PUBLIC_BATTLENET_ENABLED=
BATTLENET_CUSTOM_NAME=
BATTLENET_CLIENT_ID=
BATTLENET_CLIENT_SECRET=
BATLLENET_ISSUER=
# Box
NEXT_PUBLIC_BOX_ENABLED=
BOX_CUSTOM_NAME=
BOX_CLIENT_ID=
BOX_CLIENT_SECRET=
# Bungie
NEXT_PUBLIC_BUNGIE_ENABLED=
BUNGIE_CUSTOM_NAME=
BUNGIE_CLIENT_ID=
BUNGIE_CLIENT_SECRET=
BUNGIE_API_KEY=
# Cognito
NEXT_PUBLIC_COGNITO_ENABLED=
COGNITO_CUSTOM_NAME=
COGNITO_CLIENT_ID=
COGNITO_CLIENT_SECRET=
COGNITO_ISSUER=
# Coinbase
NEXT_PUBLIC_COINBASE_ENABLED=
COINBASE_CUSTOM_NAME=
COINBASE_CLIENT_ID=
COINBASE_CLIENT_SECRET=
# Discord
NEXT_PUBLIC_DISCORD_ENABLED=
DISCORD_CUSTOM_NAME=
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
# Dropbox
NEXT_PUBLIC_DROPBOX_ENABLED=
DROPBOX_CUSTOM_NAME=
DROPBOX_CLIENT_ID=
DROPBOX_CLIENT_SECRET=
# DuendeIndentityServer6
NEXT_PUBLIC_DUENDE_IDS6_ENABLED=
DUENDE_IDS6_CUSTOM_NAME=
DUENDE_IDS6_CLIENT_ID=
DUENDE_IDS6_CLIENT_SECRET=
DUENDE_IDS6_ISSUER=
# EVE Online
NEXT_PUBLIC_EVEONLINE_ENABLED=
EVEONLINE_CUSTOM_NAME=
EVEONLINE_CLIENT_ID=
EVEONLINE_CLIENT_SECRET=
# Facebook
NEXT_PUBLIC_FACEBOOK_ENABLED=
FACEBOOK_CUSTOM_NAME=
FACEBOOK_CLIENT_ID=
FACEBOOK_CLIENT_SECRET=
# FACEIT
NEXT_PUBLIC_FACEIT_ENABLED=
FACEIT_CUSTOM_NAME=
FACEIT_CLIENT_ID=
FACEIT_CLIENT_SECRET=
# Foursquare
NEXT_PUBLIC_FOURSQUARE_ENABLED=
FOURSQUARE_CUSTOM_NAME=
FOURSQUARE_CLIENT_ID=
FOURSQUARE_CLIENT_SECRET=
FOURSQUARE_APIVERSION=
# Freshbooks
NEXT_PUBLIC_FRESHBOOKS_ENABLED=
FRESHBOOKS_CUSTOM_NAME=
FRESHBOOKS_CLIENT_ID=
FRESHBOOKS_CLIENT_SECRET=
# FusionAuth
NEXT_PUBLIC_FUSIONAUTH_ENABLED=
FUSIONAUTH_CUSTOM_NAME=
FUSIONAUTH_CLIENT_ID=
FUSIONAUTH_CLIENT_SECRET=
FUSIONAUTH_ISSUER=
FUSIONAUTH_TENANT_ID=
# GitHub
NEXT_PUBLIC_GITHUB_ENABLED=
GITHUB_CUSTOM_NAME=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
# GitLab
NEXT_PUBLIC_GITLAB_ENABLED=
GITLAB_CUSTOM_NAME=
GITLAB_CLIENT_ID=
GITLAB_CLIENT_SECRET=
# Google
NEXT_PUBLIC_GOOGLE_ENABLED=
GOOGLE_CUSTOM_NAME=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# HubSpot
NEXT_PUBLIC_HUBSPOT_ENABLED=
HUBSPOT_CUSTOM_NAME=
HUBSPOT_CLIENT_ID=
HUBSPOT_CLIENT_SECRET=
# IdentityServer4
NEXT_PUBLIC_IDS4_ENABLED=
IDS4_CUSTOM_NAME=
IDS4_CLIENT_ID=
IDS4_CLIENT_SECRET=
IDS4_ISSUER=
# Kakao
NEXT_PUBLIC_KAKAO_ENABLED=
KAKAO_CUSTOM_NAME=
KAKAO_CLIENT_ID=
KAKAO_CLIENT_SECRET=
# Keycloak
NEXT_PUBLIC_KEYCLOAK_ENABLED=
KEYCLOAK_CUSTOM_NAME=
KEYCLOAK_ISSUER=
KEYCLOAK_CLIENT_ID=
KEYCLOAK_CLIENT_SECRET=
# LINE
NEXT_PUBLIC_LINE_ENABLED=
LINE_CUSTOM_NAME=
LINE_CLIENT_ID=
LINE_CLIENT_SECRET=
# LinkedIn
NEXT_PUBLIC_LINKEDIN_ENABLED=
LINKEDIN_CUSTOM_NAME=
LINKEDIN_CLIENT_ID=
LINKEDIN_CLIENT_SECRET=
# Mailchimp
NEXT_PUBLIC_MAILCHIMP_ENABLED=
MAILCHIMP_CUSTOM_NAME=
MAILCHIMP_CLIENT_ID=
MAILCHIMP_CLIENT_SECRET=
# Mail.ru
NEXT_PUBLIC_MAILRU_ENABLED=
MAILRU_CUSTOM_NAME=
MAILRU_CLIENT_ID=
MAILRU_CLIENT_SECRET=
# Naver
NEXT_PUBLIC_NAVER_ENABLED=
NAVER_CUSTOM_NAME=
NAVER_CLIENT_ID=
NAVER_CLIENT_SECRET=
# Netlify
NEXT_PUBLIC_NETLIFY_ENABLED=
NETLIFY_CUSTOM_NAME=
NETLIFY_CLIENT_ID=
NETLIFY_CLIENT_SECRET=
# Okta
NEXT_PUBLIC_OKTA_ENABLED=
OKTA_CUSTOM_NAME=
OKTA_CLIENT_ID=
OKTA_CLIENT_SECRET=
OKTA_ISSUER=
# OneLogin
NEXT_PUBLIC_ONELOGIN_ENABLED=
ONELOGIN_CUSTOM_NAME=
ONELOGIN_CLIENT_ID=
ONELOGIN_CLIENT_SECRET=
ONELOGIN_ISSUER=
# Osso
NEXT_PUBLIC_OSSO_ENABLED=
OSSO_CUSTOM_NAME=
OSSO_CLIENT_ID=
OSSO_CLIENT_SECRET=
OSSO_ISSUER=
# osu!
NEXT_PUBLIC_OSU_ENABLED=
OSU_CUSTOM_NAME=
OSU_CLIENT_ID=
OSU_CLIENT_SECRET=
# Patreon
NEXT_PUBLIC_PATREON_ENABLED=
PATREON_CUSTOM_NAME=
PATREON_CLIENT_ID=
PATREON_CLIENT_SECRET=
# Pinterest
NEXT_PUBLIC_PINTEREST_ENABLED=
PINTEREST_CUSTOM_NAME=
PINTEREST_CLIENT_ID=
PINTEREST_CLIENT_SECRET=
# Pipedrive
NEXT_PUBLIC_PIPEDRIVE_ENABLED=
PIPEDRIVE_CUSTOM_NAME=
PIPEDRIVE_CLIENT_ID=
PIPEDRIVE_CLIENT_SECRET=
# Reddit
NEXT_PUBLIC_REDDIT_ENABLED=
REDDIT_CUSTOM_NAME=
REDDIT_CLIENT_ID=
REDDIT_CLIENT_SECRET=
# Salesforce
NEXT_PUBLIC_SALESFORCE_ENABLED=
SALESFORCE_CUSTOM_NAME=
SALESFORCE_CLIENT_ID=
SALESFORCE_CLIENT_SECRET=
# Slack
NEXT_PUBLIC_SLACK_ENABLED=
SLACK_CUSTOM_NAME=
SLACK_CLIENT_ID=
SLACK_CLIENT_SECRET=
# Spotify
NEXT_PUBLIC_SPOTIFY_ENABLED=
SPOTIFY_CUSTOM_NAME=
SPOTIFY_CLIENT_ID=
SPOTIFY_CLIENT_SECRET=
# Strava
NEXT_PUBLIC_STRAVA_ENABLED=
STRAVA_CUSTOM_NAME=
STRAVA_CLIENT_ID=
STRAVA_CLIENT_SECRET=
# Todoist
NEXT_PUBLIC_TODOIST_ENABLED=
TODOIST_CUSTOM_NAME=
TODOIST_CLIENT_ID=
TODOIST_CLIENT_SECRET=
# Twitch
NEXT_PUBLIC_TWITCH_ENABLED=
TWITCH_CUSTOM_NAME=
TWITCH_CLIENT_ID=
TWITCH_CLIENT_SECRET=
# United Effects
NEXT_PUBLIC_UNITED_EFFECTS_ENABLED=
UNITED_EFFECTS_CUSTOM_NAME=
UNITED_EFFECTS_CLIENT_ID=
UNITED_EFFECTS_CLIENT_SECRET=
UNITED_EFFECTS_ISSUER=
# VK
NEXT_PUBLIC_VK_ENABLED=
VK_CUSTOM_NAME=
VK_CLIENT_ID=
VK_CLIENT_SECRET=
# Wikimedia
NEXT_PUBLIC_WIKIMEDIA_ENABLED=
WIKIMEDIA_CUSTOM_NAME=
WIKIMEDIA_CLIENT_ID=
WIKIMEDIA_CLIENT_SECRET=
# Wordpress.com
NEXT_PUBLIC_WORDPRESS_ENABLED=
WORDPRESS_CUSTOM_NAME=
WORDPRESS_CLIENT_ID=
WORDPRESS_CLIENT_SECRET=
# Yandex
NEXT_PUBLIC_YANDEX_ENABLED=
YANDEX_CUSTOM_NAME=
YANDEX_CLIENT_ID=
YANDEX_CLIENT_SECRET=
# Zitadel
NEXT_PUBLIC_ZITADEL_ENABLED=
ZITADEL_CUSTOM_NAME=
ZITADEL_CLIENT_ID=
ZITADEL_CLIENT_SECRET=
ZITADEL_ISSUER=
# Zoho
NEXT_PUBLIC_ZOHO_ENABLED=
ZOHO_CUSTOM_NAME=
ZOHO_CLIENT_ID=
ZOHO_CLIENT_SECRET=
# Zoom
NEXT_PUBLIC_ZOOM_ENABLED=
ZOOM_CUSTOM_NAME=
ZOOM_CLIENT_ID=
ZOOM_CLIENT_SECRET=
+2 -1
View File
@@ -1,6 +1,7 @@
{
"extends": "next/core-web-vitals",
"rules": {
"react-hooks/exhaustive-deps": "off"
"react-hooks/exhaustive-deps": "off",
"@next/next/no-img-element": "off"
}
}
+12 -10
View File
@@ -1,17 +1,19 @@
# Security Policy
# Security
## Supported Versions
The Linkwarden team and community take security bugs in Linkwarden seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
| Version | Supported |
| ------- | --------- |
| 1.x.x | ✅ |
# Reporting Security Issues
## Reporting a Vulnerability
**Please do not report security vulnerabilities through public GitHub issues.**
First off, we really appreciate the time you spent!
Instead, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/linkwarden/linkwarden/security/advisories/new) tab.
If you found a vulnerability, these are the ways you can reach us:
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message:
[security@linkwarden.app](mailto:security@linkwarden.app)
Email: [security@linkwarden.app](mailto:security@linkwarden.app)
Or you can directly DM me via Twitter: [@daniel31x13](https://twitter.com/Daniel31X13).
After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
# Preferred Languages
We prefer all communications to be in English.
+11
View File
@@ -0,0 +1,11 @@
node_modules
.next
public
*.lock
*.log
.github
data
pgdata
+4
View File
@@ -0,0 +1,4 @@
{
"trailingComma": "es5",
"tabWidth": 2
}
+1
View File
@@ -0,0 +1 @@
{}
+1 -1
View File
@@ -9,7 +9,7 @@ WORKDIR /data
COPY ./package.json ./yarn.lock ./playwright.config.ts ./
# Increase timeout to pass github actions arm64 build
RUN yarn install --network-timeout 10000000
RUN --mount=type=cache,sharing=locked,target=/usr/local/share/.cache/yarn yarn install --network-timeout 10000000
RUN npx playwright install-deps && \
apt-get clean && \
+56 -33
View File
@@ -2,36 +2,56 @@
<img src="./assets/logo.png" width="100px" />
<h1>Linkwarden</h1>
<a href="https://discord.com/invite/CtuYV47nuJ"><img src="https://img.shields.io/discord/1117993124669702164?logo=discord&style=flat-square" alt="Discord"></a>
<img alt="GitHub commits since latest release (by SemVer including pre-releases)" src="https://img.shields.io/github/commits-since/linkwarden/linkwarden/v1.1.0/dev">
<img src="https://img.shields.io/github/languages/top/linkwarden/linkwarden?style=flat-square" alt="Top Language">
<img src="https://img.shields.io/github/stars/linkwarden/linkwarden?style=flat-square" alt="Github Stars">
<a href="https://discord.com/invite/CtuYV47nuJ"><img src="https://img.shields.io/discord/1117993124669702164?logo=discord&style=flat" alt="Discord"></a>
<a href="https://twitter.com/LinkwardenHQ"><img src="https://img.shields.io/twitter/follow/linkwarden" alt="Twitter"></a>
<img alt="GitHub commits since latest release" src="https://img.shields.io/github/commits-since/linkwarden/linkwarden/latest/dev?style=for-the-badge&label=COMMITS%20SINCE%20LATEST%20RELEASE">
</div>
<div align='center'>
[Website](https://linkwarden.app) | [Getting Started](https://docs.linkwarden.app) | [Features](https://github.com/linkwarden/linkwarden#features) | [Roadmap](https://github.com/orgs/linkwarden/projects/1) | [Screenshots](https://github.com/linkwarden/linkwarden#screenshots) | [Support ❤](https://github.com/linkwarden/linkwarden#support-)
[Website](https://linkwarden.app) | [Getting Started](https://docs.linkwarden.app) | [Features](https://github.com/linkwarden/linkwarden#features) | [Roadmap](https://github.com/orgs/linkwarden/projects/1) | [Support ❤](https://github.com/linkwarden/linkwarden#support-)
</div>
## Intro & motivation
**Linkwarden is a self-hosted, open-source collaborative bookmark manager to collect, organize and archive webpages.** The objective is to organize useful webpages and articles you find across the web in one place, and since useful webpages can go away (see the inevitability of [Link Rot](https://www.howtogeek.com/786227/what-is-link-rot-and-how-does-it-threaten-the-web/)), Linkwarden also saves a copy of each webpage as a Screenshot and PDF, ensuring accessibility even if the original content is no longer available.
**Linkwarden is a self-hosted, open-source collaborative bookmark manager to collect, organize and archive webpages.**
The objective is to organize useful webpages and articles you find across the web in one place, and since useful webpages can go away (see the inevitability of [Link Rot](https://www.howtogeek.com/786227/what-is-link-rot-and-how-does-it-threaten-the-web/)), Linkwarden also saves a copy of each webpage as a Screenshot and PDF, ensuring accessibility even if the original content is no longer available.
Additionally, Linkwarden is designed with collaboration in mind, sharing links with the public and/or allowing multiple users to work together seamlessly.
<img src="./assets/showcase_image.png" />
> **Note**
> [!TIP]
> Our official [Cloud](https://linkwarden.app/#pricing) offering provides the simplest way to begin using Linkwarden and it's the preferred choice for many due to its time-saving benefits. <br> Your subscription supports our hosting infrastructure and ongoing development. <br> Alternatively, if you prefer [self-hosting](https://docs.linkwarden.app/self-hosting/installation) Linkwarden, no problem! You'll still have access to all the premium features.
<img src="./assets/dashboard.png" />
<div align="center">
<img src="./assets/all_links.jpg" width="23%" />
<img src="./assets/list_view.jpg" width="23%" />
<img src="./assets/all_collections.jpg" width="23%" />
<img src="./assets/manage_team.jpg" width="23%" />
<img src="./assets/readable_view.jpg" width="23%" />
<img src="./assets/preserved_formats.jpg" width="23%" />
<img src="./assets/public_page.jpg" width="23%" />
<img src="./assets/light_dashboard.jpg" width="23%" />
</div>
<details>
<summary><b>A bit of a "history"</b></summary>
Linkwarden has been completely rebuilt and redesigned from ground up, so pretty much the only thing it has in common with its predecessor is the idea behind it - bookmark management.
**What happened to the old version?**
We highly recommend that you don't use the old version because it is no longer maintained and has far fewer features. However, if you still want to check it out, we've forked the old version from the current repository into [this repo](https://github.com/linkwarden/linkwarden-old).
We've forked the old version from the current repository into [this repo](https://github.com/linkwarden/linkwarden-old).
</details>
@@ -41,15 +61,30 @@ We highly recommend that you don't use the old version because it is no longer m
- 🏛️ Send your webpage to Wayback Machine ([archive.org](https://archive.org)) for a snapshot. (Optional)
- 📂 Organize links by collection, name, description and multiple tags.
- 👥 Collaborate on gathering links in a collection.
- 🔐 Customize the permissions of each member.
- 🌐 Share your collected links with the world.
- 🎛️ Customize the permissions of each member.
- 🌐 Share your collected links and preserved formats with the world.
- 📌 Pin your favorite links to dashboard.
- 🔍 Full text search, filter and sort for easy retrieval.
- 📱 Responsive design and supports most modern browsers.
- 🌓 Dark/Light mode support.
- 🧩 Browser extension, managed by the community. [Star it here!](https://github.com/linkwarden/browser-extension)
- ⬇️ Import your bookmarks from other browsers.
- ⚡️ Powerful API.
- ⬇️ Import and export your bookmarks.
- 🔐 SSO integration. (Enterprise and Self-hosted users only)
- ✨ And so many more features!
## Like what we're doing? Give us a Star ⭐
![Star Us](https://raw.githubusercontent.com/linkwarden/linkwarden/main/assets/star_repo.gif)
## We're building our Community 🌐
Join and follow us in the following platforms to stay up to date about the most recent features and for support:
<a href="https://discord.com/invite/CtuYV47nuJ"><img src="https://img.shields.io/discord/1117993124669702164?logo=discord&style=flat" alt="Discord"></a>
<a href="https://twitter.com/LinkwardenHQ"><img src="https://img.shields.io/twitter/follow/linkwarden" alt="Twitter"></a>
<a href="https://fosstodon.org/@linkwarden"><img src="https://img.shields.io/mastodon/follow/110748840237143200?domain=https%3A%2F%2Ffosstodon.org" alt="Mastodon"></a>
## Suggestions
@@ -63,32 +98,14 @@ Make sure to check out our [public roadmap](https://github.com/orgs/linkwarden/p
For information on how to get started or to set up your own instance, please visit the [documentation](https://docs.linkwarden.app).
## Main Tech Stack
- NextJS
- TypeScript
- Tailwind
- Prisma
- Zustand
## Development
If you want to contribute, Thanks! Start by checking our [public roadmap](https://github.com/orgs/linkwarden/projects/1), there you'll see a [README for contributers](https://github.com/orgs/linkwarden/projects/1?pane=issue&itemId=34708277) for the rest of the info on how to contribute to this repo.
If you want to contribute, Thanks! Start by checking our [public roadmap](https://github.com/orgs/linkwarden/projects/1), there you'll see a [README for contributers](https://github.com/orgs/linkwarden/projects/1?pane=issue&itemId=34708277) for the rest of the info on how to contribute and the main tech stack.
## Security
If you found a security vulnerability, please do **not** create a public issue, instead send an email to [security@linkwarden.app](mailto:security@linkwarden.app) stating the vulnerability. Thanks!
## Screenshots
<div align="center">
<img src="./assets/collections.png" height="150" />
<img src="./assets/collaborators.png" height="150" />
<img src="./assets/link_details.png" height="150" />
</div>
## Support ❤
Other than using our official [Cloud](https://linkwarden.app/#pricing) offering, any [donations](https://opencollective.com/linkwarden) are highly appreciated as well!
@@ -100,3 +117,9 @@ Here are the other ways to support/cheer this project:
- Referring Linkwarden to a friend.
If you did any of the above, Thanksss! Otherwise thanks.
## Thanks to All the Contributors 💪
Huge thanks to these guys for spending their time helping Linkwarden grow. They rock! ⚡️
<img src="https://contributors-img.web.app/image?repo=linkwarden/linkwarden" alt="Contributors"/>
+101
View File
@@ -0,0 +1,101 @@
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof
# Cordova plugins for Capacitor
capacitor-cordova-android-plugins
# Copied web assets
app/src/main/assets/public
# Generated Config files
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml
+2
View File
@@ -0,0 +1,2 @@
/build/*
!/build/.npmkeep
+54
View File
@@ -0,0 +1,54 @@
apply plugin: 'com.android.application'
android {
namespace "com.example.app"
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.example.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
repositories {
flatDir{
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android')
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
implementation project(':capacitor-cordova-android-plugins')
}
apply from: 'capacitor.build.gradle'
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}
+19
View File
@@ -0,0 +1,19 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
}
if (hasProperty('postBuildExtras')) {
postBuildExtras()
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,26 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.getcapacitor.app", appContext.getPackageName());
}
}
+41
View File
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
@@ -0,0 +1,5 @@
package com.example.app;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>
@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">linkwarden</string>
<string name="title_activity_main">linkwarden</string>
<string name="package_name">com.example.app</string>
<string name="custom_url_scheme">com.example.app</string>
</resources>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:background">@drawable/splash</item>
</style>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>
@@ -0,0 +1,18 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}
+29
View File
@@ -0,0 +1,29 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.0.0'
classpath 'com.google.gms:google-services:4.3.15'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
apply from: "variables.gradle"
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
+3
View File
@@ -0,0 +1,3 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
+22
View File
@@ -0,0 +1,22 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
Binary file not shown.
+6
View File
@@ -0,0 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+244
View File
@@ -0,0 +1,244 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
+92
View File
@@ -0,0 +1,92 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+5
View File
@@ -0,0 +1,5 @@
include ':app'
include ':capacitor-cordova-android-plugins'
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
apply from: 'capacitor.settings.gradle'
+16
View File
@@ -0,0 +1,16 @@
ext {
minSdkVersion = 22
compileSdkVersion = 33
targetSdkVersion = 33
androidxActivityVersion = '1.7.0'
androidxAppCompatVersion = '1.6.1'
androidxCoordinatorLayoutVersion = '1.2.0'
androidxCoreVersion = '1.10.0'
androidxFragmentVersion = '1.5.6'
coreSplashScreenVersion = '1.0.0'
androidxWebkitVersion = '1.6.1'
junitVersion = '4.13.2'
androidxJunitVersion = '1.1.5'
androidxEspressoCoreVersion = '3.5.1'
cordovaAndroidVersion = '10.1.1'
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 663 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

+19
View File
@@ -0,0 +1,19 @@
import { CapacitorConfig } from "@capacitor/cli";
const config: CapacitorConfig = {
appId: "com.example.app",
appName: "linkwarden",
webDir: "out",
server: {
androidScheme: "https",
url: "http://localhost:3000",
cleartext: true,
},
plugins: {
CapacitorHttp: {
enabled: true,
},
},
};
export default config;
+29
View File
@@ -0,0 +1,29 @@
type Props = {
onClick?: Function;
label: string;
loading?: boolean;
className?: string;
type?: "button" | "submit" | "reset" | undefined;
};
export default function AccentSubmitButton({
onClick,
label,
loading,
className,
type,
}: Props) {
return (
<button
type={type ? type : undefined}
className={`border primary-btn-gradient select-none duration-200 bg-black border-[oklch(var(--p))] hover:border-[#0070b5] rounded-lg text-center px-4 py-2 text-white active:scale-95 tracking-wider w-fit flex justify-center items-center gap-2 ${
className || ""
}`}
onClick={() => {
if (loading !== undefined && !loading && onClick) onClick();
}}
>
<p className="font-bold">{label}</p>
</button>
);
}
+8 -10
View File
@@ -1,5 +1,3 @@
import { faClose } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Link from "next/link";
import React, { MouseEventHandler } from "react";
@@ -9,25 +7,25 @@ type Props = {
export default function AnnouncementBar({ toggleAnnouncementBar }: Props) {
return (
<div className="fixed w-full z-20 dark:bg-neutral-900 bg-white">
<div className="fixed w-full z-20 bg-base-200">
<div className="w-full h-10 rainbow flex items-center justify-center">
<div className="w-fit font-semibold">
🎉{" "}
🎉 See what&apos;s new in{" "}
<Link
href="https://blog.linkwarden.app/releases/v2.0"
href="https://blog.linkwarden.app/releases/v2.4"
target="_blank"
className="underline hover:opacity-50 duration-100"
>
Linkwarden v2.0
</Link>{" "}
is now out! 🥳
Linkwarden v2.4
</Link>
! 🥳
</div>
<button
className="fixed top-3 right-3 hover:opacity-50 duration-100"
className="fixed right-3 hover:opacity-50 duration-100"
onClick={toggleAnnouncementBar}
>
<FontAwesomeIcon icon={faClose} className="w-4 h-4" />
<i className="bi-x text-neutral text-2xl"></i>
</button>
</div>
</div>
+5 -13
View File
@@ -1,5 +1,3 @@
import { faSquare, faSquareCheck } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ChangeEventHandler } from "react";
type Props = {
@@ -12,23 +10,17 @@ type Props = {
export default function Checkbox({ label, state, className, onClick }: Props) {
return (
<label
className={`cursor-pointer flex items-center gap-2 ${className || ""}`}
className={`label cursor-pointer flex gap-2 justify-start ${
className || ""
}`}
>
<input
type="checkbox"
checked={state}
onChange={onClick}
className="peer sr-only"
className="checkbox checkbox-primary"
/>
<FontAwesomeIcon
icon={faSquareCheck}
className="w-5 h-5 text-sky-500 dark:text-sky-500 peer-checked:block hidden"
/>
<FontAwesomeIcon
icon={faSquare}
className="w-5 h-5 text-sky-500 dark:text-sky-500 peer-checked:hidden block"
/>
<span className="rounded select-none">{label}</span>
<span className="label-text">{label}</span>
</label>
);
}
+176 -139
View File
@@ -1,31 +1,23 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faEllipsis, faGlobe, faLink } from "@fortawesome/free-solid-svg-icons";
import Link from "next/link";
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
import Dropdown from "./Dropdown";
import { useState } from "react";
import React, { useEffect, useState } from "react";
import ProfilePhoto from "./ProfilePhoto";
import { faCalendarDays } from "@fortawesome/free-regular-svg-icons";
import useModalStore from "@/store/modals";
import usePermissions from "@/hooks/usePermissions";
import { useTheme } from "next-themes";
import useLocalSettingsStore from "@/store/localSettings";
import getPublicUserData from "@/lib/client/getPublicUserData";
import useAccountStore from "@/store/account";
import EditCollectionModal from "./ModalContent/EditCollectionModal";
import EditCollectionSharingModal from "./ModalContent/EditCollectionSharingModal";
import DeleteCollectionModal from "./ModalContent/DeleteCollectionModal";
type Props = {
collection: CollectionIncludingMembersAndLinkCount;
className?: string;
};
type DropdownTrigger =
| {
x: number;
y: number;
}
| false;
export default function CollectionCard({ collection, className }: Props) {
const { setModal } = useModalStore();
const { theme } = useTheme();
const { settings } = useLocalSettingsStore();
const { account } = useAccountStore();
const formattedDate = new Date(collection.createdAt as string).toLocaleString(
"en-US",
@@ -36,144 +28,189 @@ export default function CollectionCard({ collection, className }: Props) {
}
);
const [expandDropdown, setExpandDropdown] = useState<DropdownTrigger>(false);
const permissions = usePermissions(collection.id as number);
const [collectionOwner, setCollectionOwner] = useState({
id: null as unknown as number,
name: "",
username: "",
image: "",
archiveAsScreenshot: undefined as unknown as boolean,
archiveAsPDF: undefined as unknown as boolean,
});
useEffect(() => {
const fetchOwner = async () => {
if (collection && collection.ownerId !== account.id) {
const owner = await getPublicUserData(collection.ownerId as number);
setCollectionOwner(owner);
} else if (collection && collection.ownerId === account.id) {
setCollectionOwner({
id: account.id as number,
name: account.name,
username: account.username as string,
image: account.image as string,
archiveAsScreenshot: account.archiveAsScreenshot as boolean,
archiveAsPDF: account.archiveAsPDF as boolean,
});
}
};
fetchOwner();
}, [collection]);
const [editCollectionModal, setEditCollectionModal] = useState(false);
const [editCollectionSharingModal, setEditCollectionSharingModal] =
useState(false);
const [deleteCollectionModal, setDeleteCollectionModal] = useState(false);
return (
<>
<div className="relative">
<div className="dropdown dropdown-bottom dropdown-end absolute top-3 right-3 z-20">
<div
tabIndex={0}
role="button"
className="btn btn-ghost btn-sm btn-square text-neutral"
>
<i className="bi-three-dots text-xl" title="More"></i>
</div>
<ul className="dropdown-content z-[1] menu shadow bg-base-200 border border-neutral-content rounded-box w-52 mt-1">
{permissions === true ? (
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
setEditCollectionModal(true);
}}
>
Edit Collection Info
</div>
</li>
) : undefined}
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
setEditCollectionSharingModal(true);
}}
>
{permissions === true ? "Share and Collaborate" : "View Team"}
</div>
</li>
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
setDeleteCollectionModal(true);
}}
>
{permissions === true ? "Delete Collection" : "Leave Collection"}
</div>
</li>
</ul>
</div>
<div
className="flex items-center absolute bottom-3 left-3 z-10 btn px-2 btn-ghost rounded-full"
onClick={() => setEditCollectionSharingModal(true)}
>
{collectionOwner.id ? (
<ProfilePhoto
src={collectionOwner.image || undefined}
name={collectionOwner.name}
/>
) : undefined}
{collection.members
.sort((a, b) => (a.userId as number) - (b.userId as number))
.map((e, i) => {
return (
<ProfilePhoto
key={i}
src={e.user.image ? e.user.image : undefined}
name={e.user.name}
className="-ml-3"
/>
);
})
.slice(0, 3)}
{collection.members.length - 3 > 0 ? (
<div className={`avatar drop-shadow-md placeholder -ml-3`}>
<div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
<span>+{collection.members.length - 3}</span>
</div>
</div>
) : null}
</div>
<Link
href={`/collections/${collection.id}`}
style={{
backgroundImage: `linear-gradient(45deg, ${collection.color}30 10%, ${
theme === "dark" ? "#262626" : "#f3f4f6"
} 50%, ${theme === "dark" ? "#262626" : "#f9fafb"} 100%)`,
settings.theme === "dark" ? "oklch(var(--b2))" : "oklch(var(--b2))"
} 50%, ${
settings.theme === "dark" ? "oklch(var(--b2))" : "oklch(var(--b2))"
} 100%)`,
}}
className={`border border-solid border-sky-100 dark:border-neutral-700 self-stretch min-h-[12rem] rounded-2xl shadow duration-100 hover:shadow-none hover:opacity-80 group relative ${
className || ""
}`}
className="card card-compact shadow-md hover:shadow-none duration-200 border border-neutral-content"
>
<div
onClick={(e) => setExpandDropdown({ x: e.clientX, y: e.clientY })}
id={"expand-dropdown" + collection.id}
className="inline-flex absolute top-5 right-5 rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
>
<FontAwesomeIcon
icon={faEllipsis}
id={"expand-dropdown" + collection.id}
className="w-5 h-5 text-gray-500 dark:text-gray-300"
/>
</div>
<Link
href={`/collections/${collection.id}`}
className="flex flex-col gap-2 justify-between min-h-[12rem] h-full select-none p-5"
>
<p className="text-2xl capitalize text-black dark:text-white break-words line-clamp-3 w-4/5">
{collection.name}
</p>
<div className="flex justify-between items-center">
<div className="flex items-center w-full">
{collection.members
.sort((a, b) => (a.userId as number) - (b.userId as number))
.map((e, i) => {
return (
<ProfilePhoto
key={i}
src={e.user.image ? e.user.image : undefined}
className="-mr-3 border-[3px]"
/>
);
})
.slice(0, 4)}
{collection.members.length - 4 > 0 ? (
<div className="h-10 w-10 text-white flex items-center justify-center rounded-full border-[3px] bg-sky-600 dark:bg-sky-600 border-slate-200 dark:border-neutral-700 -mr-3">
+{collection.members.length - 4}
</div>
) : null}
</div>
<div className="text-right w-40">
<div className="text-black dark:text-white font-bold text-sm flex justify-end gap-1 items-center">
<div className="card-body flex flex-col justify-between min-h-[12rem]">
<div className="flex justify-between">
<p className="card-title break-words line-clamp-2 w-full">
{collection.name}
</p>
<div className="w-8 h-8 ml-10"></div>
</div>
<div className="flex justify-end items-center">
<div className="text-right">
<div className="font-bold text-sm flex justify-end gap-1 items-center">
{collection.isPublic ? (
<FontAwesomeIcon
icon={faGlobe}
<i
className="bi-globe-americas drop-shadow text-neutral"
title="This collection is being shared publicly."
className="w-4 h-4 drop-shadow text-gray-500 dark:text-gray-300"
/>
></i>
) : undefined}
<FontAwesomeIcon
icon={faLink}
className="w-5 h-5 text-gray-500 dark:text-gray-300"
/>
<i
className="bi-link-45deg text-lg text-neutral"
title="This collection is being shared publicly."
></i>
{collection._count && collection._count.links}
</div>
<div className="flex items-center justify-end gap-1 text-gray-500 dark:text-gray-300">
<FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" />
<p className="font-bold text-xs">{formattedDate}</p>
<div className="flex items-center justify-end gap-1 text-neutral">
<p className="font-bold text-xs flex gap-1 items-center">
<i
className="bi-calendar3 text-neutral"
title="This collection is being shared publicly."
></i>
{formattedDate}
</p>
</div>
</div>
</div>
</Link>
</div>
{expandDropdown ? (
<Dropdown
points={{ x: expandDropdown.x, y: expandDropdown.y }}
items={[
permissions === true
? {
name: "Edit Collection Info",
onClick: () => {
collection &&
setModal({
modal: "COLLECTION",
state: true,
method: "UPDATE",
isOwner: permissions === true,
active: collection,
});
setExpandDropdown(false);
},
}
: undefined,
{
name: permissions === true ? "Share/Collaborate" : "View Team",
onClick: () => {
collection &&
setModal({
modal: "COLLECTION",
state: true,
method: "UPDATE",
isOwner: permissions === true,
active: collection,
defaultIndex: permissions === true ? 1 : 0,
});
setExpandDropdown(false);
},
},
{
name:
permissions === true ? "Delete Collection" : "Leave Collection",
onClick: () => {
collection &&
setModal({
modal: "COLLECTION",
state: true,
method: "UPDATE",
isOwner: permissions === true,
active: collection,
defaultIndex: permissions === true ? 2 : 1,
});
setExpandDropdown(false);
},
},
]}
onClickOutside={(e: Event) => {
const target = e.target as HTMLInputElement;
if (target.id !== "expand-dropdown" + collection.id)
setExpandDropdown(false);
}}
className="w-fit"
</div>
</Link>
{editCollectionModal ? (
<EditCollectionModal
onClose={() => setEditCollectionModal(false)}
activeCollection={collection}
/>
) : null}
</>
) : undefined}
{editCollectionSharingModal ? (
<EditCollectionSharingModal
onClose={() => setEditCollectionSharingModal(false)}
activeCollection={collection}
/>
) : undefined}
{deleteCollectionModal ? (
<DeleteCollectionModal
onClose={() => setDeleteCollectionModal(false)}
activeCollection={collection}
/>
) : undefined}
</div>
);
}
+13 -21
View File
@@ -1,28 +1,20 @@
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
type Props = {
export default function dashboardItem({
name,
value,
icon,
}: {
name: string;
value: number;
icon: IconProp;
};
export default function dashboardItem({ name, value, icon }: Props) {
icon: string;
}) {
return (
<div className="flex gap-4 items-end">
<div className="p-4 bg-sky-500 bg-opacity-20 dark:bg-opacity-10 rounded-xl select-none">
<FontAwesomeIcon
icon={icon}
className="w-8 h-8 text-sky-500 dark:text-sky-500"
/>
<div className="flex items-center">
<div className="w-[4.7rem] aspect-square flex justify-center items-center bg-primary/20 rounded-xl select-none">
<i className={`${icon} text-primary text-4xl drop-shadow`}></i>
</div>
<div className="flex flex-col justify-center">
<p className="text-gray-500 dark:text-gray-400 text-sm tracking-wider">
{name}
</p>
<p className="font-thin text-6xl text-sky-500 dark:text-sky-500">
{value}
</p>
<div className="ml-4 flex flex-col justify-center">
<p className="text-neutral text-xs tracking-wider">{name}</p>
<p className="font-thin text-6xl text-primary mt-0.5">{value}</p>
</div>
</div>
);
+3 -3
View File
@@ -78,13 +78,13 @@ export default function Dropdown({
onClickOutside={onClickOutside}
className={`${
className || ""
} py-1 shadow-md border border-sky-100 dark:border-neutral-700 bg-gray-50 dark:bg-neutral-800 rounded-md flex flex-col z-20`}
} py-1 shadow-md border border-neutral-content bg-base-200 rounded-md flex flex-col z-20`}
>
{items.map((e, i) => {
const inner = e && (
<div className="cursor-pointer rounded-md">
<div className="flex items-center gap-2 py-1 px-2 hover:bg-slate-200 dark:hover:bg-neutral-700 duration-100">
<p className="text-black dark:text-white select-none">{e.name}</p>
<div className="flex items-center gap-2 py-1 px-2 hover:bg-base-100 duration-100">
<p className="select-none">{e.name}</p>
</div>
</div>
);
+110 -58
View File
@@ -1,9 +1,6 @@
import React, { SetStateAction } from "react";
import ClickAwayHandler from "./ClickAwayHandler";
import Checkbox from "./Checkbox";
import React from "react";
type Props = {
setFilterDropdown: (value: SetStateAction<boolean>) => void;
setSearchFilter: Function;
searchFilter: {
name: boolean;
@@ -15,64 +12,119 @@ type Props = {
};
export default function FilterSearchDropdown({
setFilterDropdown,
setSearchFilter,
searchFilter,
}: Props) {
return (
<ClickAwayHandler
onClickOutside={(e: Event) => {
const target = e.target as HTMLInputElement;
if (target.id !== "filter-dropdown") setFilterDropdown(false);
}}
className="absolute top-8 right-0 border border-sky-100 dark:border-neutral-700 shadow-md bg-gray-50 dark:bg-neutral-800 rounded-md p-2 z-20 w-40"
>
<p className="mb-2 text-black dark:text-white text-center font-semibold">
Filter by
</p>
<div className="flex flex-col gap-2">
<Checkbox
label="Name"
state={searchFilter.name}
onClick={() =>
setSearchFilter({ ...searchFilter, name: !searchFilter.name })
}
/>
<Checkbox
label="Link"
state={searchFilter.url}
onClick={() =>
setSearchFilter({ ...searchFilter, url: !searchFilter.url })
}
/>
<Checkbox
label="Description"
state={searchFilter.description}
onClick={() =>
setSearchFilter({
...searchFilter,
description: !searchFilter.description,
})
}
/>
<Checkbox
label="Text Content"
state={searchFilter.textContent}
onClick={() =>
setSearchFilter({
...searchFilter,
textContent: !searchFilter.textContent,
})
}
/>
<Checkbox
label="Tags"
state={searchFilter.tags}
onClick={() =>
setSearchFilter({ ...searchFilter, tags: !searchFilter.tags })
}
/>
<div className="dropdown dropdown-bottom dropdown-end">
<div
tabIndex={0}
role="button"
className="btn btn-sm btn-square btn-ghost"
>
<i className="bi-funnel text-neutral text-2xl"></i>
</div>
</ClickAwayHandler>
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-box w-44 mt-1">
<li>
<label
className="label cursor-pointer flex justify-start"
tabIndex={0}
role="button"
>
<input
type="checkbox"
name="search-filter-checkbox"
className="checkbox checkbox-primary"
checked={searchFilter.name}
onChange={() => {
setSearchFilter({ ...searchFilter, name: !searchFilter.name });
}}
/>
<span className="label-text">Name</span>
</label>
</li>
<li>
<label
className="label cursor-pointer flex justify-start"
tabIndex={0}
role="button"
>
<input
type="checkbox"
name="search-filter-checkbox"
className="checkbox checkbox-primary"
checked={searchFilter.url}
onChange={() => {
setSearchFilter({ ...searchFilter, url: !searchFilter.url });
}}
/>
<span className="label-text">Link</span>
</label>
</li>
<li>
<label
className="label cursor-pointer flex justify-start"
tabIndex={0}
role="button"
>
<input
type="checkbox"
name="search-filter-checkbox"
className="checkbox checkbox-primary"
checked={searchFilter.description}
onChange={() => {
setSearchFilter({
...searchFilter,
description: !searchFilter.description,
});
}}
/>
<span className="label-text">Description</span>
</label>
</li>
<li>
<label
className="label cursor-pointer flex justify-start"
tabIndex={0}
role="button"
>
<input
type="checkbox"
name="search-filter-checkbox"
className="checkbox checkbox-primary"
checked={searchFilter.textContent}
onChange={() => {
setSearchFilter({
...searchFilter,
textContent: !searchFilter.textContent,
});
}}
/>
<span className="label-text">Full Content</span>
</label>
</li>
<li>
<label
className="label cursor-pointer flex justify-start"
tabIndex={0}
role="button"
>
<input
type="checkbox"
name="search-filter-checkbox"
className="checkbox checkbox-primary"
checked={searchFilter.tags}
onChange={() => {
setSearchFilter({
...searchFilter,
tags: !searchFilter.tags,
});
}}
/>
<span className="label-text">Tags</span>
</label>
</li>
</ul>
</div>
);
}
@@ -1,9 +1,9 @@
import useCollectionStore from "@/store/collections";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import Select from "react-select";
import { styles } from "./styles";
import { Options } from "./types";
import CreatableSelect from "react-select/creatable";
type Props = {
onChange: any;
@@ -43,8 +43,8 @@ export default function CollectionSelection({ onChange, defaultValue }: Props) {
}, [collections]);
return (
<Select
isClearable
<CreatableSelect
isClearable={false}
className="react-select-container"
classNamePrefix="react-select"
onChange={onChange}
+1 -1
View File
@@ -27,7 +27,7 @@ export default function TagSelection({ onChange, defaultValue }: Props) {
return (
<CreatableSelect
isClearable
isClearable={false}
className="react-select-container"
classNamePrefix="react-select"
onChange={onChange}
+14 -6
View File
@@ -8,20 +8,27 @@ export const styles: StylesConfig = {
...styles,
fontFamily: font,
cursor: "pointer",
backgroundColor: state.isSelected ? "#0ea5e9" : "inherit",
backgroundColor: state.isSelected ? "oklch(var(--p))" : "inherit",
"&:hover": {
backgroundColor: state.isSelected ? "#0ea5e9" : "#e2e8f0",
backgroundColor: state.isSelected
? "oklch(var(--p))"
: "oklch(var(--nc))",
},
transition: "all 50ms",
}),
control: (styles) => ({
control: (styles, state) => ({
...styles,
fontFamily: font,
border: "none",
borderRadius: "0.375rem",
border: state.isFocused
? "1px solid oklch(var(--p))"
: "1px solid oklch(var(--nc))",
boxShadow: "none",
minHeight: "2.6rem",
}),
container: (styles) => ({
container: (styles, state) => ({
...styles,
border: "1px solid #e0f2fe",
height: "full",
borderRadius: "0.375rem",
lineHeight: "1.25rem",
// "@media screen and (min-width: 1024px)": {
@@ -58,4 +65,5 @@ export const styles: StylesConfig = {
backgroundColor: "#38bdf8",
},
}),
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
};
-280
View File
@@ -1,280 +0,0 @@
import {
CollectionIncludingMembersAndLinkCount,
LinkIncludingShortenedCollectionAndTags,
} from "@/types/global";
import {
faFolder,
faEllipsis,
faLink,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useEffect, useState } from "react";
import Image from "next/image";
import Dropdown from "./Dropdown";
import useLinkStore from "@/store/links";
import useCollectionStore from "@/store/collections";
import useAccountStore from "@/store/account";
import useModalStore from "@/store/modals";
import { faCalendarDays } from "@fortawesome/free-regular-svg-icons";
import usePermissions from "@/hooks/usePermissions";
import { toast } from "react-hot-toast";
import isValidUrl from "@/lib/client/isValidUrl";
import Link from "next/link";
import unescapeString from "@/lib/client/unescapeString";
import { useRouter } from "next/router";
type Props = {
link: LinkIncludingShortenedCollectionAndTags;
count: number;
className?: string;
};
type DropdownTrigger =
| {
x: number;
y: number;
}
| false;
export default function LinkCard({ link, count, className }: Props) {
const { setModal } = useModalStore();
const router = useRouter();
const permissions = usePermissions(link.collection.id as number);
const [expandDropdown, setExpandDropdown] = useState<DropdownTrigger>(false);
const { collections } = useCollectionStore();
const { links } = useLinkStore();
const { account } = useAccountStore();
let shortendURL;
try {
shortendURL = new URL(link.url).host.toLowerCase();
} catch (error) {
console.log(error);
}
const [collection, setCollection] =
useState<CollectionIncludingMembersAndLinkCount>(
collections.find(
(e) => e.id === link.collection.id
) as CollectionIncludingMembersAndLinkCount
);
useEffect(() => {
setCollection(
collections.find(
(e) => e.id === link.collection.id
) as CollectionIncludingMembersAndLinkCount
);
}, [collections, links]);
const { removeLink, updateLink, getLink } = useLinkStore();
const pinLink = async () => {
const isAlreadyPinned = link?.pinnedBy && link.pinnedBy[0];
const load = toast.loading("Applying...");
setExpandDropdown(false);
const response = await updateLink({
...link,
pinnedBy: isAlreadyPinned ? undefined : [{ id: account.id }],
});
toast.dismiss(load);
response.ok &&
toast.success(`Link ${isAlreadyPinned ? "Unpinned!" : "Pinned!"}`);
};
const updateArchive = async () => {
const load = toast.loading("Sending request...");
setExpandDropdown(false);
const response = await fetch(`/api/v1/links/${link.id}/archive`, {
method: "PUT",
});
const data = await response.json();
toast.dismiss(load);
if (response.ok) {
toast.success(`Link is being archived...`);
getLink(link.id as number);
} else toast.error(data.response);
};
const deleteLink = async () => {
const load = toast.loading("Deleting...");
const response = await removeLink(link.id as number);
toast.dismiss(load);
response.ok && toast.success(`Link Deleted.`);
setExpandDropdown(false);
};
const url = isValidUrl(link.url) ? new URL(link.url) : undefined;
const formattedDate = new Date(link.createdAt as string).toLocaleString(
"en-US",
{
year: "numeric",
month: "short",
day: "numeric",
}
);
return (
<>
<div
className={`h-fit border border-solid border-sky-100 dark:border-neutral-700 bg-gradient-to-tr from-slate-200 dark:from-neutral-800 from-10% to-gray-50 dark:to-[#303030] via-20% shadow hover:shadow-none duration-100 rounded-2xl relative group ${
className || ""
}`}
>
{(permissions === true ||
permissions?.canUpdate ||
permissions?.canDelete) && (
<div
onClick={(e) => {
setExpandDropdown({ x: e.clientX, y: e.clientY });
}}
id={"expand-dropdown" + link.id}
className="text-gray-500 dark:text-gray-300 inline-flex rounded-md cursor-pointer hover:bg-slate-200 dark:hover:bg-neutral-700 absolute right-5 top-5 z-10 duration-100 p-1"
>
<FontAwesomeIcon
icon={faEllipsis}
title="More"
className="w-5 h-5"
id={"expand-dropdown" + link.id}
/>
</div>
)}
<div
onClick={() => router.push("/links/" + link.id)}
className="flex items-start cursor-pointer gap-5 sm:gap-10 h-full w-full p-5"
>
{url && account.displayLinkIcons && (
<Image
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${url.origin}&size=32`}
width={64}
height={64}
alt=""
className={`${
account.blurredFavicons ? "blur-sm " : ""
}absolute w-16 group-hover:opacity-80 duration-100 rounded-2xl bottom-5 right-5 opacity-60 select-none`}
draggable="false"
onError={(e) => {
const target = e.target as HTMLElement;
target.style.display = "none";
}}
/>
)}
<div className="flex justify-between gap-5 w-full h-full z-0">
<div className="flex flex-col justify-between w-full">
<div className="flex items-baseline gap-1">
<p className="text-sm text-gray-500 dark:text-gray-300">
{count + 1}
</p>
<p className="text-lg text-black dark:text-white truncate capitalize w-full pr-8">
{unescapeString(link.name || link.description)}
</p>
</div>
<Link
href={`/collections/${link.collection.id}`}
onClick={(e) => {
e.stopPropagation();
}}
className="flex items-center gap-1 max-w-full w-fit my-1 hover:opacity-70 duration-100"
>
<FontAwesomeIcon
icon={faFolder}
className="w-4 h-4 mt-1 drop-shadow"
style={{ color: collection?.color }}
/>
<p className="text-black dark:text-white truncate capitalize w-full">
{collection?.name}
</p>
</Link>
<Link
href={link.url}
target="_blank"
onClick={(e) => {
e.stopPropagation();
}}
className="flex items-center gap-1 max-w-full w-fit text-gray-500 dark:text-gray-300 hover:opacity-70 duration-100"
>
<FontAwesomeIcon icon={faLink} className="mt-1 w-4 h-4" />
<p className="truncate w-full">{shortendURL}</p>
</Link>
<div className="flex items-center gap-1 text-gray-500 dark:text-gray-300">
<FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" />
<p>{formattedDate}</p>
</div>
</div>
</div>
</div>
</div>
{expandDropdown ? (
<Dropdown
points={{ x: expandDropdown.x, y: expandDropdown.y }}
items={[
permissions === true
? {
name:
link?.pinnedBy && link.pinnedBy[0]
? "Unpin"
: "Pin to Dashboard",
onClick: pinLink,
}
: undefined,
permissions === true || permissions?.canUpdate
? {
name: "Edit",
onClick: () => {
setModal({
modal: "LINK",
state: true,
method: "UPDATE",
active: link,
});
setExpandDropdown(false);
},
}
: undefined,
permissions === true
? {
name: "Refresh Formats",
onClick: updateArchive,
}
: undefined,
permissions === true || permissions?.canDelete
? {
name: "Delete",
onClick: deleteLink,
}
: undefined,
]}
onClickOutside={(e: Event) => {
const target = e.target as HTMLInputElement;
if (target.id !== "expand-dropdown" + link.id)
setExpandDropdown(false);
}}
className="w-40"
/>
) : null}
</>
);
}
-112
View File
@@ -1,112 +0,0 @@
import { faFolder, faLink } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Image from "next/image";
import { faCalendarDays } from "@fortawesome/free-regular-svg-icons";
import isValidUrl from "@/lib/client/isValidUrl";
import A from "next/link";
import unescapeString from "@/lib/client/unescapeString";
import { Link } from "@prisma/client";
type Props = {
link?: Partial<Link>;
className?: string;
settings: {
blurredFavicons: boolean;
displayLinkIcons: boolean;
};
};
export default function LinkPreview({ link, className, settings }: Props) {
if (!link) {
link = {
name: "Linkwarden",
url: "https://linkwarden.app",
createdAt: Date.now() as unknown as Date,
};
}
let shortendURL;
try {
shortendURL = new URL(link.url as string).host.toLowerCase();
} catch (error) {
console.log(error);
}
const url = isValidUrl(link.url as string)
? new URL(link.url as string)
: undefined;
const formattedDate = new Date(link.createdAt as Date).toLocaleString(
"en-US",
{
year: "numeric",
month: "short",
day: "numeric",
}
);
return (
<>
<div
className={`h-fit border border-solid border-sky-100 dark:border-neutral-700 bg-gradient-to-tr from-slate-200 dark:from-neutral-800 from-10% to-gray-50 dark:to-[#303030] via-20% shadow hover:shadow-none duration-100 rounded-2xl relative group ${
className || ""
}`}
>
<div className="flex items-start cursor-pointer gap-5 sm:gap-10 h-full w-full p-5">
{url && settings?.displayLinkIcons && (
<Image
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${url.origin}&size=32`}
width={64}
height={64}
alt=""
className={`${
settings.blurredFavicons ? "blur-sm " : ""
}absolute w-16 group-hover:opacity-80 duration-100 rounded-2xl bottom-5 right-5 opacity-60 select-none`}
draggable="false"
onError={(e) => {
const target = e.target as HTMLElement;
target.style.display = "none";
}}
/>
)}
<div className="flex justify-between gap-5 w-full h-full z-0">
<div className="flex flex-col justify-between w-full">
<div className="flex items-baseline gap-1">
<p className="text-sm text-gray-500 dark:text-gray-300">{1}</p>
<p className="text-lg text-black dark:text-white truncate capitalize w-full pr-8">
{unescapeString(link.name as string)}
</p>
</div>
<div className="flex items-center gap-1 max-w-full w-fit my-1 hover:opacity-70 duration-100">
<FontAwesomeIcon
icon={faFolder}
className="w-4 h-4 mt-1 drop-shadow text-sky-400"
/>
<p className="text-black dark:text-white truncate capitalize w-full">
Landing Pages
</p>
</div>
<A
href={link.url as string}
target="_blank"
onClick={(e) => {
e.stopPropagation();
}}
className="flex items-center gap-1 max-w-full w-fit text-gray-500 dark:text-gray-300 hover:opacity-70 duration-100"
>
<FontAwesomeIcon icon={faLink} className="mt-1 w-4 h-4" />
<p className="truncate w-full">{shortendURL}</p>
</A>
<div className="flex items-center gap-1 text-gray-500 dark:text-gray-300">
<FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" />
<p>{formattedDate}</p>
</div>
</div>
</div>
</div>
</div>
</>
);
}
-138
View File
@@ -1,138 +0,0 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faPen,
faBoxesStacked,
faTrashCan,
} from "@fortawesome/free-solid-svg-icons";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import useModalStore from "@/store/modals";
import useLinkStore from "@/store/links";
import {
CollectionIncludingMembersAndLinkCount,
LinkIncludingShortenedCollectionAndTags,
} from "@/types/global";
import { useSession } from "next-auth/react";
import useCollectionStore from "@/store/collections";
type Props = {
className?: string;
onClick?: Function;
};
export default function SettingsSidebar({ className, onClick }: Props) {
const session = useSession();
const userId = session.data?.user.id;
const { setModal } = useModalStore();
const { links, removeLink } = useLinkStore();
const { collections } = useCollectionStore();
const [linkCollection, setLinkCollection] =
useState<CollectionIncludingMembersAndLinkCount>();
const [link, setLink] = useState<LinkIncludingShortenedCollectionAndTags>();
const router = useRouter();
useEffect(() => {
if (links) setLink(links.find((e) => e.id === Number(router.query.id)));
}, [links]);
useEffect(() => {
if (link)
setLinkCollection(collections.find((e) => e.id === link?.collection.id));
}, [link]);
return (
<div
className={`dark:bg-neutral-900 bg-white h-full lg:w-10 w-62 overflow-y-auto lg:p-0 p-5 border-solid border-white border dark:border-neutral-900 dark:lg:border-r-neutral-900 lg:border-r-white border-r-sky-100 dark:border-r-neutral-700 z-20 flex flex-col gap-5 lg:justify-center justify-start ${
className || ""
}`}
>
<div className="flex flex-col gap-5">
{link?.collection.ownerId === userId ||
linkCollection?.members.some(
(e) => e.userId === userId && e.canUpdate
) ? (
<div
title="Edit"
onClick={() => {
link
? setModal({
modal: "LINK",
state: true,
active: link,
method: "UPDATE",
})
: undefined;
onClick && onClick();
}}
className={`hover:opacity-60 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`}
>
<FontAwesomeIcon
icon={faPen}
className="w-6 h-6 text-gray-500 dark:text-gray-300"
/>
<p className="text-black dark:text-white truncate w-full lg:hidden">
Edit
</p>
</div>
) : undefined}
<div
onClick={() => {
link
? setModal({
modal: "LINK",
state: true,
active: link,
method: "FORMATS",
})
: undefined;
onClick && onClick();
}}
title="Preserved Formats"
className={`hover:opacity-60 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`}
>
<FontAwesomeIcon
icon={faBoxesStacked}
className="w-6 h-6 text-gray-500 dark:text-gray-300"
/>
<p className="text-black dark:text-white truncate w-full lg:hidden">
Preserved Formats
</p>
</div>
{link?.collection.ownerId === userId ||
linkCollection?.members.some(
(e) => e.userId === userId && e.canDelete
) ? (
<div
onClick={() => {
if (link?.id) {
removeLink(link.id);
router.back();
onClick && onClick();
}
}}
title="Delete"
className={`hover:opacity-60 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`}
>
<FontAwesomeIcon
icon={faTrashCan}
className="w-6 h-6 text-gray-500 dark:text-gray-300"
/>
<p className="text-black dark:text-white truncate w-full lg:hidden">
Delete
</p>
</div>
) : undefined}
</div>
</div>
);
}
+16
View File
@@ -0,0 +1,16 @@
import LinkCard from "@/components/LinkViews/LinkCard";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
export default function CardView({
links,
}: {
links: LinkIncludingShortenedCollectionAndTags[];
}) {
return (
<div className="grid min-[1900px]:grid-cols-4 xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
{links.map((e, i) => {
return <LinkCard key={i} link={e} count={i} />;
})}
</div>
);
}
+16
View File
@@ -0,0 +1,16 @@
import LinkGrid from "@/components/LinkViews/LinkGrid";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
export default function GridView({
links,
}: {
links: LinkIncludingShortenedCollectionAndTags[];
}) {
return (
<div className="grid 2xl:grid-cols-3 xl:grid-cols-2 grid-cols-1 gap-5">
{links.map((e, i) => {
return <LinkGrid link={e} count={i} key={i} />;
})}
</div>
);
}
+16
View File
@@ -0,0 +1,16 @@
import LinkList from "@/components/LinkViews/LinkList";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
export default function ListView({
links,
}: {
links: LinkIncludingShortenedCollectionAndTags[];
}) {
return (
<div className="flex flex-col">
{links.map((e, i) => {
return <LinkList key={i} link={e} count={i} />;
})}
</div>
);
}
+205
View File
@@ -0,0 +1,205 @@
import {
ArchivedFormat,
CollectionIncludingMembersAndLinkCount,
LinkIncludingShortenedCollectionAndTags,
} from "@/types/global";
import { useEffect, useRef, useState } from "react";
import useLinkStore from "@/store/links";
import useCollectionStore from "@/store/collections";
import unescapeString from "@/lib/client/unescapeString";
import LinkActions from "@/components/LinkViews/LinkComponents/LinkActions";
import LinkDate from "@/components/LinkViews/LinkComponents/LinkDate";
import LinkCollection from "@/components/LinkViews/LinkComponents/LinkCollection";
import Image from "next/image";
import { previewAvailable } from "@/lib/shared/getArchiveValidity";
import Link from "next/link";
import LinkIcon from "./LinkComponents/LinkIcon";
import LinkGroupedIconURL from "./LinkComponents/LinkGroupedIconURL";
import useOnScreen from "@/hooks/useOnScreen";
type Props = {
link: LinkIncludingShortenedCollectionAndTags;
count: number;
className?: string;
};
export default function LinkGrid({ link, count, className }: Props) {
const { collections } = useCollectionStore();
const { links, getLink } = useLinkStore();
let shortendURL;
try {
shortendURL = new URL(link.url || "").host.toLowerCase();
} catch (error) {
console.log(error);
}
const [collection, setCollection] =
useState<CollectionIncludingMembersAndLinkCount>(
collections.find(
(e) => e.id === link.collection.id
) as CollectionIncludingMembersAndLinkCount
);
useEffect(() => {
setCollection(
collections.find(
(e) => e.id === link.collection.id
) as CollectionIncludingMembersAndLinkCount
);
}, [collections, links]);
const ref = useRef<HTMLDivElement>(null);
const isVisible = useOnScreen(ref);
useEffect(() => {
let interval: any;
if (
isVisible &&
!link.preview?.startsWith("archives") &&
link.preview !== "unavailable"
) {
interval = setInterval(async () => {
getLink(link.id as number);
}, 5000);
}
return () => {
if (interval) {
clearInterval(interval);
}
};
}, [isVisible, link.preview]);
const [showInfo, setShowInfo] = useState(false);
return (
<div
ref={ref}
className="border border-solid border-neutral-content bg-base-200 shadow-md hover:shadow-none duration-100 rounded-2xl relative"
>
<Link
href={link.url || ""}
target="_blank"
className="rounded-2xl cursor-pointer"
>
<div className="relative rounded-t-2xl h-40 overflow-hidden">
{previewAvailable(link) ? (
<Image
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.jpeg}&preview=true`}
width={1280}
height={720}
alt=""
className="rounded-t-2xl select-none object-cover z-10 h-40 w-full shadow opacity-80 scale-105"
style={{ filter: "blur(2px)" }}
draggable="false"
onError={(e) => {
const target = e.target as HTMLElement;
target.style.display = "none";
}}
/>
) : link.preview === "unavailable" ? (
<div className="bg-gray-50 duration-100 h-40 bg-opacity-80"></div>
) : (
<div className="duration-100 h-40 bg-opacity-80 skeleton rounded-none"></div>
)}
<div
style={
{
// background:
// "radial-gradient(circle, rgba(255, 255, 255, 0.5), transparent)",
}
}
className="absolute top-0 left-0 right-0 bottom-0 rounded-t-2xl flex items-center justify-center shadow rounded-md"
>
<LinkIcon link={link} />
</div>
</div>
<hr className="divider my-0 last:hidden border-t border-neutral-content h-[1px]" />
<div className="p-3 mt-1">
<p className="truncate w-full pr-8 text-primary">
{unescapeString(link.name || link.description) || link.url}
</p>
<div title={link.url || ""} className="w-fit">
<div className="flex gap-1 item-center select-none text-neutral mt-1">
<i className="bi-link-45deg text-lg mt-[0.15rem] leading-none"></i>
<p className="text-sm truncate">{shortendURL}</p>
</div>
</div>
</div>
<hr className="divider mt-2 mb-1 last:hidden border-t border-neutral-content h-[1px]" />
<div className="flex justify-between text-xs text-neutral px-3 pb-1">
<div className="cursor-pointer w-fit">
{collection ? (
<LinkCollection link={link} collection={collection} />
) : undefined}
</div>
<LinkDate link={link} />
</div>
</Link>
{showInfo ? (
<div className="p-3 absolute z-30 top-0 left-0 right-0 bottom-0 bg-base-200 rounded-2xl fade-in overflow-y-auto">
<div
onClick={() => setShowInfo(!showInfo)}
className=" float-right btn btn-sm outline-none btn-circle btn-ghost z-10"
>
<i className="bi-x text-neutral text-2xl"></i>
</div>
<p className="text-neutral text-lg font-semibold">Description</p>
<hr className="divider my-2 last:hidden border-t border-neutral-content h-[1px]" />
<p>
{link.description ? (
unescapeString(link.description)
) : (
<span className="text-neutral text-sm">
No description provided.
</span>
)}
</p>
{link.tags[0] ? (
<>
<p className="text-neutral text-lg mt-3 font-semibold">Tags</p>
<hr className="divider my-2 last:hidden border-t border-neutral-content h-[1px]" />
<div className="flex gap-3 items-center flex-wrap mt-2 truncate relative">
<div className="flex gap-1 items-center flex-wrap">
{link.tags.map((e, i) => (
<Link
href={"/tags/" + e.id}
key={i}
onClick={(e) => {
e.stopPropagation();
}}
className="btn btn-xs btn-ghost truncate max-w-[19rem]"
>
#{e.name}
</Link>
))}
</div>
</div>
</>
) : undefined}
</div>
) : undefined}
<LinkActions
link={link}
collection={collection}
position="top-[10.75rem] right-3"
toggleShowInfo={() => setShowInfo(!showInfo)}
linkInfo={showInfo}
/>
</div>
);
}
@@ -0,0 +1,175 @@
import { useState } from "react";
import {
CollectionIncludingMembersAndLinkCount,
LinkIncludingShortenedCollectionAndTags,
} from "@/types/global";
import usePermissions from "@/hooks/usePermissions";
import EditLinkModal from "@/components/ModalContent/EditLinkModal";
import DeleteLinkModal from "@/components/ModalContent/DeleteLinkModal";
import PreservedFormatsModal from "@/components/ModalContent/PreservedFormatsModal";
import useLinkStore from "@/store/links";
import { toast } from "react-hot-toast";
import useAccountStore from "@/store/account";
type Props = {
link: LinkIncludingShortenedCollectionAndTags;
collection: CollectionIncludingMembersAndLinkCount;
position?: string;
toggleShowInfo?: () => void;
linkInfo?: boolean;
};
export default function LinkActions({
link,
toggleShowInfo,
position,
linkInfo,
}: Props) {
const permissions = usePermissions(link.collection.id as number);
const [editLinkModal, setEditLinkModal] = useState(false);
const [deleteLinkModal, setDeleteLinkModal] = useState(false);
const [preservedFormatsModal, setPreservedFormatsModal] = useState(false);
const { account } = useAccountStore();
const { removeLink, updateLink } = useLinkStore();
const pinLink = async () => {
const isAlreadyPinned = link?.pinnedBy && link.pinnedBy[0];
const load = toast.loading("Applying...");
const response = await updateLink({
...link,
pinnedBy: isAlreadyPinned ? undefined : [{ id: account.id }],
});
toast.dismiss(load);
response.ok &&
toast.success(`Link ${isAlreadyPinned ? "Unpinned!" : "Pinned!"}`);
};
const deleteLink = async () => {
const load = toast.loading("Deleting...");
const response = await removeLink(link.id as number);
toast.dismiss(load);
response.ok && toast.success(`Link Deleted.`);
};
return (
<>
<div
className={`dropdown dropdown-left absolute ${
position || "top-3 right-3"
} z-20`}
>
<div
tabIndex={0}
role="button"
className="btn btn-ghost btn-sm btn-square text-neutral"
>
<i title="More" className="bi-three-dots text-xl" />
</div>
<ul className="dropdown-content z-[20] menu shadow bg-base-200 border border-neutral-content rounded-box w-44 mr-1">
{permissions === true ? (
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
pinLink();
}}
>
{link?.pinnedBy && link.pinnedBy[0]
? "Unpin"
: "Pin to Dashboard"}
</div>
</li>
) : undefined}
{linkInfo !== undefined && toggleShowInfo ? (
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
toggleShowInfo();
}}
>
{!linkInfo ? "Show" : "Hide"} Link Details
</div>
</li>
) : undefined}
{permissions === true || permissions?.canUpdate ? (
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
setEditLinkModal(true);
}}
>
Edit Link
</div>
</li>
) : undefined}
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
setPreservedFormatsModal(true);
}}
>
Preserved Formats
</div>
</li>
{permissions === true || permissions?.canDelete ? (
<li>
<div
role="button"
tabIndex={0}
onClick={(e) => {
(document?.activeElement as HTMLElement)?.blur();
e.shiftKey ? deleteLink() : setDeleteLinkModal(true);
}}
>
Delete
</div>
</li>
) : undefined}
</ul>
</div>
{editLinkModal ? (
<EditLinkModal
onClose={() => setEditLinkModal(false)}
activeLink={link}
/>
) : undefined}
{deleteLinkModal ? (
<DeleteLinkModal
onClose={() => setDeleteLinkModal(false)}
activeLink={link}
/>
) : undefined}
{preservedFormatsModal ? (
<PreservedFormatsModal
onClose={() => setPreservedFormatsModal(false)}
activeLink={link}
/>
) : undefined}
{/* {expandedLink ? (
<ExpandedLink onClose={() => setExpandedLink(false)} link={link} />
) : undefined} */}
</>
);
}
@@ -0,0 +1,33 @@
import {
CollectionIncludingMembersAndLinkCount,
LinkIncludingShortenedCollectionAndTags,
} from "@/types/global";
import { useRouter } from "next/router";
import React from "react";
export default function LinkCollection({
link,
collection,
}: {
link: LinkIncludingShortenedCollectionAndTags;
collection: CollectionIncludingMembersAndLinkCount;
}) {
const router = useRouter();
return (
<div
onClick={(e) => {
e.preventDefault();
router.push(`/collections/${link.collection.id}`);
}}
className="flex items-center gap-1 max-w-full w-fit hover:opacity-70 duration-100"
title={collection?.name}
>
<i
className="bi-folder-fill text-lg drop-shadow"
style={{ color: collection?.color }}
></i>
<p className="truncate capitalize">{collection?.name}</p>
</div>
);
}
@@ -0,0 +1,24 @@
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import React from "react";
export default function LinkDate({
link,
}: {
link: LinkIncludingShortenedCollectionAndTags;
}) {
const formattedDate = new Date(link.createdAt as string).toLocaleString(
"en-US",
{
year: "numeric",
month: "short",
day: "numeric",
}
);
return (
<div className="flex items-center gap-1 text-neutral">
<i className="bi-calendar3 text-lg"></i>
<p>{formattedDate}</p>
</div>
);
}
@@ -0,0 +1,53 @@
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import Image from "next/image";
import isValidUrl from "@/lib/shared/isValidUrl";
import React from "react";
import Link from "next/link";
export default function LinkGroupedIconURL({
link,
}: {
link: LinkIncludingShortenedCollectionAndTags;
}) {
const url =
isValidUrl(link.url || "") && link.url ? new URL(link.url) : undefined;
const [showFavicon, setShowFavicon] = React.useState<boolean>(true);
let shortendURL;
try {
shortendURL = new URL(link.url || "").host.toLowerCase();
} catch (error) {
console.log(error);
}
return (
<Link href={link.url || ""} target="_blank">
<div className="bg-white shadow-md rounded-md border-[2px] flex gap-1 item-center justify-center border-white select-none z-10 max-w-full">
{link.url && url && showFavicon ? (
<Image
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${link.url}&size=32`}
width={64}
height={64}
alt=""
className="w-5 h-5 rounded"
draggable="false"
onError={() => {
setShowFavicon(false);
}}
/>
) : showFavicon === false ? (
<i className="bi-link-45deg text-xl leading-none text-black"></i>
) : link.type === "pdf" ? (
<i className={`bi-file-earmark-pdf`}></i>
) : link.type === "image" ? (
<i className={`bi-file-earmark-image`}></i>
) : undefined}
<p className="truncate bg-white text-black mr-1">
<p className="text-sm">{shortendURL}</p>
</p>
</div>
</Link>
);
}
@@ -0,0 +1,48 @@
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import Image from "next/image";
import isValidUrl from "@/lib/shared/isValidUrl";
import React from "react";
export default function LinkIcon({
link,
width,
}: {
link: LinkIncludingShortenedCollectionAndTags;
width?: string;
}) {
const url =
isValidUrl(link.url || "") && link.url ? new URL(link.url) : undefined;
const iconClasses: string =
"bg-white shadow rounded-md border-[2px] flex item-center justify-center border-white select-none z-10" +
" " +
(width || "w-12");
const [showFavicon, setShowFavicon] = React.useState<boolean>(true);
return (
<>
{link.url && url && showFavicon ? (
<Image
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${link.url}&size=32`}
width={64}
height={64}
alt=""
className={iconClasses}
draggable="false"
onError={() => {
setShowFavicon(false);
}}
/>
) : showFavicon === false ? (
<div className={iconClasses}>
<i className="bi-link-45deg text-4xl text-black"></i>
</div>
) : link.type === "pdf" ? (
<i className={`bi-file-earmark-pdf ${iconClasses}`}></i>
) : link.type === "image" ? (
<i className={`bi-file-earmark-image ${iconClasses}`}></i>
) : undefined}
</>
);
}

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