From 6a17bdc532c4a51787aa8fdf17b0894a835f5274 Mon Sep 17 00:00:00 2001 From: MaryWylde Date: Mon, 1 Jun 2026 13:36:51 +0200 Subject: [PATCH 01/17] library: import source into components/library Import the keepsimple Library feature source from github.com/keepsimpleio/library into keepsimple's existing folder structure, namespaced under `library/` to avoid collisions: - components atoms/molecules/organisms -> src/components/library/ - templates -> src/layouts/library/ - types (+ d.ts type shims) -> src/local-types/library/ - utils -> src/utils/library/ - hooks -> src/hooks/library/ - constants (+ config/seo.config.ts) -> src/constants/library/ - contexts -> src/components/Context/library/ - axios/cookie libs -> src/lib/library/ - client services (src/api + app/api) -> src/api/library/ - code-imported assets -> src/assets/library/ - shared scss -> src/styles/library/ - AGENT.md -> src/components/library/LIBRARY_AGENT.md Excluded per task: package.json/lockfile, node_modules, CI/workflows, .claude/.husky/.idea, eslint/tsconfig, README/docs, Storybook (.storybook + *.stories.*), App-Router routing/infra (app/page, app/layout, loading, app/auth), all NextAuth setup, and route handlers (libraries proxy, geoip /api/user, test-login mock). Imports/deps/auth/route are fixed in later steps. --- src/api/library/auth.ts | 51 ++ src/api/library/library/createLibrary.ts | 18 + .../library/library/getLibraryIdByUsername.ts | 20 + src/api/library/library/getMyLibrary.ts | 21 + src/api/library/library/updateLibrary.ts | 14 + src/api/library/object/createObject.ts | 19 + src/api/library/object/deleteObject.ts | 9 + src/api/library/object/updateObject.ts | 16 + src/api/library/shelf/createShelf.ts | 19 + src/api/library/shelf/deleteShelf.ts | 6 + src/api/library/shelf/getShelvesList.ts | 24 + src/api/library/shelf/updateShelf.ts | 14 + src/api/library/strapi.ts | 93 +++ src/api/library/tag/createTag.ts | 15 + src/api/library/tag/deleteTag.ts | 7 + src/api/library/tag/getTagsList.ts | 29 + src/api/library/tag/updateTag.ts | 17 + src/api/library/upload/uploadFile.ts | 14 + src/api/library/user/updateMe.ts | 9 + src/assets/library/images/avatar.png | Bin 0 -> 4402 bytes src/assets/library/images/shelfBackground.png | Bin 0 -> 46882 bytes src/assets/library/svg/arrow.svg | 3 + src/assets/library/svg/articles.svg | 27 + src/assets/library/svg/audio.svg | 3 + src/assets/library/svg/avatar.svg | 5 + src/assets/library/svg/book-shadow.svg | 24 + src/assets/library/svg/book.svg | 11 + src/assets/library/svg/calendar.svg | 3 + src/assets/library/svg/check.svg | 4 + src/assets/library/svg/close.svg | 3 + src/assets/library/svg/company.svg | 3 + src/assets/library/svg/copy.svg | 3 + src/assets/library/svg/delete.svg | 10 + src/assets/library/svg/discord.svg | 8 + src/assets/library/svg/dots-vertical.svg | 5 + src/assets/library/svg/edit.svg | 10 + src/assets/library/svg/error.svg | 5 + src/assets/library/svg/google.svg | 14 + src/assets/library/svg/hamburger.svg | 3 + src/assets/library/svg/index.ts | 57 ++ src/assets/library/svg/info.svg | 4 + src/assets/library/svg/library.svg | 3 + src/assets/library/svg/logo.svg | 3 + src/assets/library/svg/plus.svg | 3 + src/assets/library/svg/search.svg | 3 + src/assets/library/svg/settings.svg | 4 + src/assets/library/svg/share.svg | 3 + src/assets/library/svg/tools.svg | 6 + src/assets/library/svg/ux-core.svg | 7 + src/assets/library/svg/uxcore.svg | 7 + src/assets/library/svg/video-shadow.svg | 24 + src/assets/library/svg/video.svg | 11 + .../Context/library/AuthContext.tsx | 113 +++ .../Context/library/DashboardContext.tsx | 41 ++ .../Context/library/GlobalStateContext.tsx | 147 ++++ src/components/library/LIBRARY_AGENT.md | 116 +++ .../library/atoms/Avatar/Avatar.module.scss | 17 + .../library/atoms/Avatar/Avatar.tsx | 22 + .../library/atoms/Avatar/Avatar.types.ts | 6 + src/components/library/atoms/Avatar/index.tsx | 2 + .../library/atoms/Icon/Icon.module.scss | 3 + src/components/library/atoms/Icon/Icon.tsx | 23 + .../library/atoms/Icon/Icon.types.ts | 27 + src/components/library/atoms/Icon/index.tsx | 2 + .../library/atoms/Loader/Loader.module.scss | 46 ++ .../library/atoms/Loader/Loader.tsx | 11 + .../library/atoms/Loader/Loader.types.ts | 4 + src/components/library/atoms/Loader/index.tsx | 2 + .../library/atoms/Text/Text.module.scss | 51 ++ src/components/library/atoms/Text/Text.tsx | 19 + .../library/atoms/Text/Text.types.ts | 35 + src/components/library/atoms/Text/index.tsx | 2 + .../library/atoms/Toggle/Toggle.module.scss | 48 ++ .../library/atoms/Toggle/Toggle.tsx | 33 + .../library/atoms/Toggle/Toggle.types.ts | 7 + src/components/library/atoms/Toggle/index.tsx | 2 + .../library/atoms/Tooltip/Tooltip.module.scss | 15 + .../library/atoms/Tooltip/Tooltip.tsx | 34 + .../library/atoms/Tooltip/Tooltip.types.ts | 11 + .../library/atoms/Tooltip/index.tsx | 2 + .../AboutLibraryModal.module.scss | 35 + .../AboutLibraryModal/AboutLibraryModal.tsx | 53 ++ .../AboutLibraryModal.types.ts | 3 + .../molecules/AboutLibraryModal/index.tsx | 2 + .../AddShelfModal/AddShelfModal.module.scss | 68 ++ .../molecules/AddShelfModal/AddShelfModal.tsx | 92 +++ .../AddShelfModal/AddShelfModal.types.ts | 6 + .../library/molecules/AddShelfModal/index.tsx | 2 + .../molecules/AudioCard/AudioCard.module.scss | 57 ++ .../library/molecules/AudioCard/AudioCard.tsx | 59 ++ .../molecules/AudioCard/AudioCard.types.ts | 7 + .../library/molecules/AudioCard/index.tsx | 2 + .../molecules/BookCard/BookCard.module.scss | 77 ++ .../library/molecules/BookCard/BookCard.tsx | 68 ++ .../molecules/BookCard/BookCard.types.ts | 7 + .../library/molecules/BookCard/index.tsx | 2 + .../molecules/Button/Button.module.scss | 73 ++ .../library/molecules/Button/Button.tsx | 49 ++ .../library/molecules/Button/Button.types.ts | 30 + .../library/molecules/Button/index.ts | 2 + .../ConfirmationModal.module.scss | 44 ++ .../ConfirmationModal/ConfirmationModal.tsx | 67 ++ .../ConfirmationModal.types.ts | 17 + .../molecules/ConfirmationModal/index.tsx | 2 + .../CreateTagModal/CreateTagModal.module.scss | 175 +++++ .../CreateTagModal/CreateTagModal.tsx | 311 ++++++++ .../CreateTagModal/CreateTagModal.types.ts | 15 + .../molecules/CreateTagModal/index.tsx | 2 + .../DatePicker/DatePicker.module.scss | 115 +++ .../molecules/DatePicker/DatePicker.tsx | 86 +++ .../molecules/DatePicker/DatePicker.types.ts | 10 + .../library/molecules/DatePicker/index.tsx | 2 + .../molecules/Dropdown/Dropdown.module.scss | 168 +++++ .../library/molecules/Dropdown/Dropdown.tsx | 199 +++++ .../molecules/Dropdown/Dropdown.types.ts | 36 + .../library/molecules/Dropdown/index.tsx | 2 + .../ImageDropzone/ImageDropzone.module.scss | 134 ++++ .../molecules/ImageDropzone/ImageDropzone.tsx | 173 +++++ .../ImageDropzone/ImageDropzone.types.ts | 13 + .../library/molecules/ImageDropzone/index.tsx | 2 + .../library/molecules/Input/Input.module.scss | 45 ++ .../library/molecules/Input/Input.tsx | 54 ++ .../library/molecules/Input/Input.types.ts | 13 + .../library/molecules/Input/index.tsx | 2 + .../library/molecules/Modal/Modal.module.scss | 75 ++ .../library/molecules/Modal/Modal.tsx | 137 ++++ .../library/molecules/Modal/Modal.types.ts | 12 + .../library/molecules/Modal/index.tsx | 3 + .../library/molecules/Modal/useModalClose.ts | 19 + .../molecules/Object/Object.module.scss | 57 ++ .../library/molecules/Object/Object.tsx | 34 + .../library/molecules/Object/Object.types.ts | 12 + .../library/molecules/Object/index.tsx | 2 + .../Pagination/Pagination.module.scss | 53 ++ .../molecules/Pagination/Pagination.tsx | 91 +++ .../molecules/Pagination/Pagination.types.ts | 4 + .../library/molecules/Pagination/index.tsx | 2 + .../molecules/RatingBox/RatingBox.module.scss | 115 +++ .../library/molecules/RatingBox/RatingBox.tsx | 190 +++++ .../molecules/RatingBox/RatingBox.types.ts | 20 + .../library/molecules/RatingBox/index.tsx | 2 + .../ReorderGrid/ReorderGrid.module.scss | 83 +++ .../molecules/ReorderGrid/ReorderGrid.tsx | 126 ++++ .../ReorderGrid/ReorderGrid.types.ts | 17 + .../library/molecules/ReorderGrid/index.tsx | 2 + src/components/library/molecules/SEO/SEO.tsx | 77 ++ .../library/molecules/SEO/SEO.types.ts | 40 ++ .../library/molecules/SEO/index.tsx | 2 + .../SignInModal/SignInModal.module.scss | 47 ++ .../molecules/SignInModal/SignInModal.tsx | 72 ++ .../SignInModal/SignInModal.types.ts | 5 + .../library/molecules/SignInModal/index.tsx | 2 + .../StepIndicator/StepIndicator.module.scss | 110 +++ .../molecules/StepIndicator/StepIndicator.tsx | 66 ++ .../StepIndicator/StepIndicator.types.ts | 9 + .../library/molecules/StepIndicator/index.tsx | 2 + .../library/molecules/Tag/Tag.module.scss | 53 ++ src/components/library/molecules/Tag/Tag.tsx | 66 ++ .../library/molecules/Tag/Tag.types.ts | 7 + .../library/molecules/Tag/index.tsx | 2 + .../TagMultiSelect/TagMultiSelect.module.scss | 89 +++ .../TagMultiSelect/TagMultiSelect.tsx | 138 ++++ .../TagMultiSelect/TagMultiSelect.types.ts | 23 + .../molecules/TagMultiSelect/index.tsx | 2 + .../molecules/Textarea/Textarea.module.scss | 34 + .../library/molecules/Textarea/Textarea.tsx | 37 + .../molecules/Textarea/Textarea.types.ts | 11 + .../library/molecules/Textarea/index.tsx | 2 + .../UserDropDown/UserDropDown.module.scss | 68 ++ .../molecules/UserDropDown/UserDropDown.tsx | 50 ++ .../UserDropDown/UserDropDown.types.ts | 7 + .../library/molecules/UserDropDown/index.tsx | 2 + .../molecules/VideoCard/VideoCard.module.scss | 73 ++ .../library/molecules/VideoCard/VideoCard.tsx | 54 ++ .../molecules/VideoCard/VideoCard.types.ts | 7 + .../library/molecules/VideoCard/index.tsx | 2 + .../AddObjectModal/AddObjectModal.config.ts | 69 ++ .../AddObjectModal/AddObjectModal.module.scss | 117 +++ .../AddObjectModal/AddObjectModal.tsx | 677 ++++++++++++++++++ .../AddObjectModal/AddObjectModal.types.ts | 55 ++ .../organisms/AddObjectModal/index.tsx | 2 + .../EditLibraryModal.module.scss | 84 +++ .../EditLibraryModal/EditLibraryModal.tsx | 313 ++++++++ .../EditLibraryModal.types.ts | 8 + .../organisms/EditLibraryModal/index.tsx | 2 + .../organisms/Header/Header.module.scss | 345 +++++++++ .../library/organisms/Header/Header.tsx | 246 +++++++ .../library/organisms/Header/Header.types.ts | 9 + .../LibraryCard/LibraryCard.module.scss | 103 +++ .../organisms/LibraryCard/LibraryCard.tsx | 67 ++ .../LibraryCard/LibraryCard.types.ts | 10 + .../library/organisms/LibraryCard/index.tsx | 2 + .../ObjectOverviewModal.config.ts | 63 ++ .../ObjectOverviewModal.module.scss | 260 +++++++ .../ObjectOverviewModal.tsx | 538 ++++++++++++++ .../ObjectOverviewModal.types.ts | 27 + .../organisms/ObjectOverviewModal/index.tsx | 2 + .../library/organisms/Shelf/Shelf.module.scss | 157 ++++ .../library/organisms/Shelf/Shelf.tsx | 405 +++++++++++ .../library/organisms/Shelf/Shelf.types.ts | 26 + .../library/organisms/Shelf/index.tsx | 2 + .../organisms/Sidebar/Sidebar.module.scss | 187 +++++ .../library/organisms/Sidebar/Sidebar.tsx | 410 +++++++++++ .../organisms/Sidebar/Sidebar.types.ts | 0 .../library/organisms/Sidebar/index.tsx | 1 + src/constants/library/common.ts | 64 ++ src/constants/library/seo.config.ts | 22 + src/constants/library/tags.ts | 9 + src/hooks/library/useAnchoredPosition.ts | 37 + src/hooks/library/useClickOutside.ts | 20 + src/hooks/library/useIsMobile.tsx | 23 + src/hooks/library/useLockBodyScroll.ts | 39 + src/layouts/library/Home/Home.module.scss | 100 +++ src/layouts/library/Home/Home.tsx | 187 +++++ src/layouts/library/Home/Home.types.ts | 6 + src/layouts/library/Home/index.tsx | 2 + .../library/Library/Library.module.scss | 36 + src/layouts/library/Library/Library.tsx | 276 +++++++ src/layouts/library/Library/Library.types.ts | 3 + src/layouts/library/Library/index.tsx | 2 + src/lib/library/axios/index.ts | 24 + src/lib/library/cookie/index.ts | 14 + src/local-types/library/declaration.d.ts | 7 + src/local-types/library/index.ts | 26 + src/local-types/library/library.ts | 117 +++ src/local-types/library/media.ts | 19 + src/local-types/library/next-auth.d.ts | 10 + src/local-types/library/object.ts | 60 ++ src/local-types/library/shelf.ts | 45 ++ src/local-types/library/strapi.ts | 23 + src/local-types/library/tag.ts | 21 + src/local-types/library/user.ts | 26 + src/styles/library/animations.scss | 39 + src/styles/library/fonts.scss | 2 + src/styles/library/global.scss | 65 ++ src/styles/library/styles.scss | 1 + src/styles/library/themes.scss | 0 src/styles/library/typography.scss | 71 ++ src/styles/library/variables.scss | 100 +++ src/utils/library/color.ts | 38 + src/utils/library/mapStrapiLibraries.ts | 93 +++ src/utils/library/resolveStrapiUrl.ts | 14 + src/utils/library/schema/addObjectSchema.ts | 56 ++ src/utils/library/schema/createTagSchema.ts | 20 + src/utils/library/schema/editLibrarySchema.ts | 32 + src/utils/library/seo.ts | 123 ++++ 246 files changed, 12190 insertions(+) create mode 100644 src/api/library/auth.ts create mode 100644 src/api/library/library/createLibrary.ts create mode 100644 src/api/library/library/getLibraryIdByUsername.ts create mode 100644 src/api/library/library/getMyLibrary.ts create mode 100644 src/api/library/library/updateLibrary.ts create mode 100644 src/api/library/object/createObject.ts create mode 100644 src/api/library/object/deleteObject.ts create mode 100644 src/api/library/object/updateObject.ts create mode 100644 src/api/library/shelf/createShelf.ts create mode 100644 src/api/library/shelf/deleteShelf.ts create mode 100644 src/api/library/shelf/getShelvesList.ts create mode 100644 src/api/library/shelf/updateShelf.ts create mode 100644 src/api/library/strapi.ts create mode 100644 src/api/library/tag/createTag.ts create mode 100644 src/api/library/tag/deleteTag.ts create mode 100644 src/api/library/tag/getTagsList.ts create mode 100644 src/api/library/tag/updateTag.ts create mode 100644 src/api/library/upload/uploadFile.ts create mode 100644 src/api/library/user/updateMe.ts create mode 100644 src/assets/library/images/avatar.png create mode 100644 src/assets/library/images/shelfBackground.png create mode 100644 src/assets/library/svg/arrow.svg create mode 100644 src/assets/library/svg/articles.svg create mode 100644 src/assets/library/svg/audio.svg create mode 100644 src/assets/library/svg/avatar.svg create mode 100644 src/assets/library/svg/book-shadow.svg create mode 100644 src/assets/library/svg/book.svg create mode 100644 src/assets/library/svg/calendar.svg create mode 100644 src/assets/library/svg/check.svg create mode 100644 src/assets/library/svg/close.svg create mode 100644 src/assets/library/svg/company.svg create mode 100644 src/assets/library/svg/copy.svg create mode 100644 src/assets/library/svg/delete.svg create mode 100644 src/assets/library/svg/discord.svg create mode 100644 src/assets/library/svg/dots-vertical.svg create mode 100644 src/assets/library/svg/edit.svg create mode 100644 src/assets/library/svg/error.svg create mode 100644 src/assets/library/svg/google.svg create mode 100644 src/assets/library/svg/hamburger.svg create mode 100644 src/assets/library/svg/index.ts create mode 100644 src/assets/library/svg/info.svg create mode 100644 src/assets/library/svg/library.svg create mode 100644 src/assets/library/svg/logo.svg create mode 100644 src/assets/library/svg/plus.svg create mode 100644 src/assets/library/svg/search.svg create mode 100644 src/assets/library/svg/settings.svg create mode 100644 src/assets/library/svg/share.svg create mode 100644 src/assets/library/svg/tools.svg create mode 100644 src/assets/library/svg/ux-core.svg create mode 100644 src/assets/library/svg/uxcore.svg create mode 100644 src/assets/library/svg/video-shadow.svg create mode 100644 src/assets/library/svg/video.svg create mode 100644 src/components/Context/library/AuthContext.tsx create mode 100644 src/components/Context/library/DashboardContext.tsx create mode 100644 src/components/Context/library/GlobalStateContext.tsx create mode 100644 src/components/library/LIBRARY_AGENT.md create mode 100644 src/components/library/atoms/Avatar/Avatar.module.scss create mode 100644 src/components/library/atoms/Avatar/Avatar.tsx create mode 100644 src/components/library/atoms/Avatar/Avatar.types.ts create mode 100644 src/components/library/atoms/Avatar/index.tsx create mode 100644 src/components/library/atoms/Icon/Icon.module.scss create mode 100644 src/components/library/atoms/Icon/Icon.tsx create mode 100644 src/components/library/atoms/Icon/Icon.types.ts create mode 100644 src/components/library/atoms/Icon/index.tsx create mode 100644 src/components/library/atoms/Loader/Loader.module.scss create mode 100644 src/components/library/atoms/Loader/Loader.tsx create mode 100644 src/components/library/atoms/Loader/Loader.types.ts create mode 100644 src/components/library/atoms/Loader/index.tsx create mode 100644 src/components/library/atoms/Text/Text.module.scss create mode 100644 src/components/library/atoms/Text/Text.tsx create mode 100644 src/components/library/atoms/Text/Text.types.ts create mode 100644 src/components/library/atoms/Text/index.tsx create mode 100644 src/components/library/atoms/Toggle/Toggle.module.scss create mode 100644 src/components/library/atoms/Toggle/Toggle.tsx create mode 100644 src/components/library/atoms/Toggle/Toggle.types.ts create mode 100644 src/components/library/atoms/Toggle/index.tsx create mode 100644 src/components/library/atoms/Tooltip/Tooltip.module.scss create mode 100644 src/components/library/atoms/Tooltip/Tooltip.tsx create mode 100644 src/components/library/atoms/Tooltip/Tooltip.types.ts create mode 100644 src/components/library/atoms/Tooltip/index.tsx create mode 100644 src/components/library/molecules/AboutLibraryModal/AboutLibraryModal.module.scss create mode 100644 src/components/library/molecules/AboutLibraryModal/AboutLibraryModal.tsx create mode 100644 src/components/library/molecules/AboutLibraryModal/AboutLibraryModal.types.ts create mode 100644 src/components/library/molecules/AboutLibraryModal/index.tsx create mode 100644 src/components/library/molecules/AddShelfModal/AddShelfModal.module.scss create mode 100644 src/components/library/molecules/AddShelfModal/AddShelfModal.tsx create mode 100644 src/components/library/molecules/AddShelfModal/AddShelfModal.types.ts create mode 100644 src/components/library/molecules/AddShelfModal/index.tsx create mode 100644 src/components/library/molecules/AudioCard/AudioCard.module.scss create mode 100644 src/components/library/molecules/AudioCard/AudioCard.tsx create mode 100644 src/components/library/molecules/AudioCard/AudioCard.types.ts create mode 100644 src/components/library/molecules/AudioCard/index.tsx create mode 100644 src/components/library/molecules/BookCard/BookCard.module.scss create mode 100644 src/components/library/molecules/BookCard/BookCard.tsx create mode 100644 src/components/library/molecules/BookCard/BookCard.types.ts create mode 100644 src/components/library/molecules/BookCard/index.tsx create mode 100644 src/components/library/molecules/Button/Button.module.scss create mode 100644 src/components/library/molecules/Button/Button.tsx create mode 100644 src/components/library/molecules/Button/Button.types.ts create mode 100644 src/components/library/molecules/Button/index.ts create mode 100644 src/components/library/molecules/ConfirmationModal/ConfirmationModal.module.scss create mode 100644 src/components/library/molecules/ConfirmationModal/ConfirmationModal.tsx create mode 100644 src/components/library/molecules/ConfirmationModal/ConfirmationModal.types.ts create mode 100644 src/components/library/molecules/ConfirmationModal/index.tsx create mode 100644 src/components/library/molecules/CreateTagModal/CreateTagModal.module.scss create mode 100644 src/components/library/molecules/CreateTagModal/CreateTagModal.tsx create mode 100644 src/components/library/molecules/CreateTagModal/CreateTagModal.types.ts create mode 100644 src/components/library/molecules/CreateTagModal/index.tsx create mode 100644 src/components/library/molecules/DatePicker/DatePicker.module.scss create mode 100644 src/components/library/molecules/DatePicker/DatePicker.tsx create mode 100644 src/components/library/molecules/DatePicker/DatePicker.types.ts create mode 100644 src/components/library/molecules/DatePicker/index.tsx create mode 100644 src/components/library/molecules/Dropdown/Dropdown.module.scss create mode 100644 src/components/library/molecules/Dropdown/Dropdown.tsx create mode 100644 src/components/library/molecules/Dropdown/Dropdown.types.ts create mode 100644 src/components/library/molecules/Dropdown/index.tsx create mode 100644 src/components/library/molecules/ImageDropzone/ImageDropzone.module.scss create mode 100644 src/components/library/molecules/ImageDropzone/ImageDropzone.tsx create mode 100644 src/components/library/molecules/ImageDropzone/ImageDropzone.types.ts create mode 100644 src/components/library/molecules/ImageDropzone/index.tsx create mode 100644 src/components/library/molecules/Input/Input.module.scss create mode 100644 src/components/library/molecules/Input/Input.tsx create mode 100644 src/components/library/molecules/Input/Input.types.ts create mode 100644 src/components/library/molecules/Input/index.tsx create mode 100644 src/components/library/molecules/Modal/Modal.module.scss create mode 100644 src/components/library/molecules/Modal/Modal.tsx create mode 100644 src/components/library/molecules/Modal/Modal.types.ts create mode 100644 src/components/library/molecules/Modal/index.tsx create mode 100644 src/components/library/molecules/Modal/useModalClose.ts create mode 100644 src/components/library/molecules/Object/Object.module.scss create mode 100644 src/components/library/molecules/Object/Object.tsx create mode 100644 src/components/library/molecules/Object/Object.types.ts create mode 100644 src/components/library/molecules/Object/index.tsx create mode 100644 src/components/library/molecules/Pagination/Pagination.module.scss create mode 100644 src/components/library/molecules/Pagination/Pagination.tsx create mode 100644 src/components/library/molecules/Pagination/Pagination.types.ts create mode 100644 src/components/library/molecules/Pagination/index.tsx create mode 100644 src/components/library/molecules/RatingBox/RatingBox.module.scss create mode 100644 src/components/library/molecules/RatingBox/RatingBox.tsx create mode 100644 src/components/library/molecules/RatingBox/RatingBox.types.ts create mode 100644 src/components/library/molecules/RatingBox/index.tsx create mode 100644 src/components/library/molecules/ReorderGrid/ReorderGrid.module.scss create mode 100644 src/components/library/molecules/ReorderGrid/ReorderGrid.tsx create mode 100644 src/components/library/molecules/ReorderGrid/ReorderGrid.types.ts create mode 100644 src/components/library/molecules/ReorderGrid/index.tsx create mode 100644 src/components/library/molecules/SEO/SEO.tsx create mode 100644 src/components/library/molecules/SEO/SEO.types.ts create mode 100644 src/components/library/molecules/SEO/index.tsx create mode 100644 src/components/library/molecules/SignInModal/SignInModal.module.scss create mode 100644 src/components/library/molecules/SignInModal/SignInModal.tsx create mode 100644 src/components/library/molecules/SignInModal/SignInModal.types.ts create mode 100644 src/components/library/molecules/SignInModal/index.tsx create mode 100644 src/components/library/molecules/StepIndicator/StepIndicator.module.scss create mode 100644 src/components/library/molecules/StepIndicator/StepIndicator.tsx create mode 100644 src/components/library/molecules/StepIndicator/StepIndicator.types.ts create mode 100644 src/components/library/molecules/StepIndicator/index.tsx create mode 100644 src/components/library/molecules/Tag/Tag.module.scss create mode 100644 src/components/library/molecules/Tag/Tag.tsx create mode 100644 src/components/library/molecules/Tag/Tag.types.ts create mode 100644 src/components/library/molecules/Tag/index.tsx create mode 100644 src/components/library/molecules/TagMultiSelect/TagMultiSelect.module.scss create mode 100644 src/components/library/molecules/TagMultiSelect/TagMultiSelect.tsx create mode 100644 src/components/library/molecules/TagMultiSelect/TagMultiSelect.types.ts create mode 100644 src/components/library/molecules/TagMultiSelect/index.tsx create mode 100644 src/components/library/molecules/Textarea/Textarea.module.scss create mode 100644 src/components/library/molecules/Textarea/Textarea.tsx create mode 100644 src/components/library/molecules/Textarea/Textarea.types.ts create mode 100644 src/components/library/molecules/Textarea/index.tsx create mode 100644 src/components/library/molecules/UserDropDown/UserDropDown.module.scss create mode 100644 src/components/library/molecules/UserDropDown/UserDropDown.tsx create mode 100644 src/components/library/molecules/UserDropDown/UserDropDown.types.ts create mode 100644 src/components/library/molecules/UserDropDown/index.tsx create mode 100644 src/components/library/molecules/VideoCard/VideoCard.module.scss create mode 100644 src/components/library/molecules/VideoCard/VideoCard.tsx create mode 100644 src/components/library/molecules/VideoCard/VideoCard.types.ts create mode 100644 src/components/library/molecules/VideoCard/index.tsx create mode 100644 src/components/library/organisms/AddObjectModal/AddObjectModal.config.ts create mode 100644 src/components/library/organisms/AddObjectModal/AddObjectModal.module.scss create mode 100644 src/components/library/organisms/AddObjectModal/AddObjectModal.tsx create mode 100644 src/components/library/organisms/AddObjectModal/AddObjectModal.types.ts create mode 100644 src/components/library/organisms/AddObjectModal/index.tsx create mode 100644 src/components/library/organisms/EditLibraryModal/EditLibraryModal.module.scss create mode 100644 src/components/library/organisms/EditLibraryModal/EditLibraryModal.tsx create mode 100644 src/components/library/organisms/EditLibraryModal/EditLibraryModal.types.ts create mode 100644 src/components/library/organisms/EditLibraryModal/index.tsx create mode 100644 src/components/library/organisms/Header/Header.module.scss create mode 100644 src/components/library/organisms/Header/Header.tsx create mode 100644 src/components/library/organisms/Header/Header.types.ts create mode 100644 src/components/library/organisms/LibraryCard/LibraryCard.module.scss create mode 100644 src/components/library/organisms/LibraryCard/LibraryCard.tsx create mode 100644 src/components/library/organisms/LibraryCard/LibraryCard.types.ts create mode 100644 src/components/library/organisms/LibraryCard/index.tsx create mode 100644 src/components/library/organisms/ObjectOverviewModal/ObjectOverviewModal.config.ts create mode 100644 src/components/library/organisms/ObjectOverviewModal/ObjectOverviewModal.module.scss create mode 100644 src/components/library/organisms/ObjectOverviewModal/ObjectOverviewModal.tsx create mode 100644 src/components/library/organisms/ObjectOverviewModal/ObjectOverviewModal.types.ts create mode 100644 src/components/library/organisms/ObjectOverviewModal/index.tsx create mode 100644 src/components/library/organisms/Shelf/Shelf.module.scss create mode 100644 src/components/library/organisms/Shelf/Shelf.tsx create mode 100644 src/components/library/organisms/Shelf/Shelf.types.ts create mode 100644 src/components/library/organisms/Shelf/index.tsx create mode 100644 src/components/library/organisms/Sidebar/Sidebar.module.scss create mode 100644 src/components/library/organisms/Sidebar/Sidebar.tsx create mode 100644 src/components/library/organisms/Sidebar/Sidebar.types.ts create mode 100644 src/components/library/organisms/Sidebar/index.tsx create mode 100644 src/constants/library/common.ts create mode 100644 src/constants/library/seo.config.ts create mode 100644 src/constants/library/tags.ts create mode 100644 src/hooks/library/useAnchoredPosition.ts create mode 100644 src/hooks/library/useClickOutside.ts create mode 100644 src/hooks/library/useIsMobile.tsx create mode 100644 src/hooks/library/useLockBodyScroll.ts create mode 100644 src/layouts/library/Home/Home.module.scss create mode 100644 src/layouts/library/Home/Home.tsx create mode 100644 src/layouts/library/Home/Home.types.ts create mode 100644 src/layouts/library/Home/index.tsx create mode 100644 src/layouts/library/Library/Library.module.scss create mode 100644 src/layouts/library/Library/Library.tsx create mode 100644 src/layouts/library/Library/Library.types.ts create mode 100644 src/layouts/library/Library/index.tsx create mode 100644 src/lib/library/axios/index.ts create mode 100644 src/lib/library/cookie/index.ts create mode 100644 src/local-types/library/declaration.d.ts create mode 100644 src/local-types/library/index.ts create mode 100644 src/local-types/library/library.ts create mode 100644 src/local-types/library/media.ts create mode 100644 src/local-types/library/next-auth.d.ts create mode 100644 src/local-types/library/object.ts create mode 100644 src/local-types/library/shelf.ts create mode 100644 src/local-types/library/strapi.ts create mode 100644 src/local-types/library/tag.ts create mode 100644 src/local-types/library/user.ts create mode 100644 src/styles/library/animations.scss create mode 100644 src/styles/library/fonts.scss create mode 100644 src/styles/library/global.scss create mode 100644 src/styles/library/styles.scss create mode 100644 src/styles/library/themes.scss create mode 100644 src/styles/library/typography.scss create mode 100644 src/styles/library/variables.scss create mode 100644 src/utils/library/color.ts create mode 100644 src/utils/library/mapStrapiLibraries.ts create mode 100644 src/utils/library/resolveStrapiUrl.ts create mode 100644 src/utils/library/schema/addObjectSchema.ts create mode 100644 src/utils/library/schema/createTagSchema.ts create mode 100644 src/utils/library/schema/editLibrarySchema.ts create mode 100644 src/utils/library/seo.ts diff --git a/src/api/library/auth.ts b/src/api/library/auth.ts new file mode 100644 index 00000000..55b1d983 --- /dev/null +++ b/src/api/library/auth.ts @@ -0,0 +1,51 @@ +import { signOut } from 'next-auth/react'; + +import { IUser } from '@/types/user'; + +export const logout = async (): Promise => { + await signOut({ + redirect: false, + callbackUrl: '/', + }); + + localStorage.removeItem('provider'); + + // `Secure` is silently dropped by browsers on http://, so we omit it on non-HTTPS + // (i.e. local dev) — otherwise the cookie clear is never applied. + const secure = window.location.protocol === 'https:' ? ' Secure;' : ''; + document.cookie = `accessToken=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT;${secure} SameSite=Strict;`; + + window.location.reload(); +}; + +export const authenticate = async ( + token: unknown, + setAccountData: (value: IUser) => void, + setToken: (value: string | null) => void +): Promise => { + try { + const provider = localStorage.getItem('provider'); + if (!provider) { + console.error('No provider found in query'); + return; + } + const authLink = `${process.env.NEXT_PUBLIC_STRAPI}/api/auth/${provider}/callback?access_token=${token}`; + + const response = await fetch(authLink).then((resp) => resp.json()); + + if (response.user) { + const accessToken = response.jwt; + setAccountData(response.user); + setToken(accessToken); + const secure = window.location.protocol === 'https:' ? ' Secure;' : ''; + document.cookie = `accessToken=${encodeURIComponent( + accessToken || '' + )}; path=/;${secure} SameSite=Strict;`; + } + } catch (e) { + console.error(e); + const secure = window.location.protocol === 'https:' ? ' Secure;' : ''; + document.cookie = `accessToken=; path=/;${secure} SameSite=Strict;`; + localStorage.removeItem('accessToken'); + } +}; diff --git a/src/api/library/library/createLibrary.ts b/src/api/library/library/createLibrary.ts new file mode 100644 index 00000000..24393fc1 --- /dev/null +++ b/src/api/library/library/createLibrary.ts @@ -0,0 +1,18 @@ +import axiosInstance from '@/libraries/axios'; + +// Bootstrap a published library for the given user. Returns the new library id, +// or null on failure. +export const createLibrary = async (userId: number | string): Promise => { + try { + const { data } = await axiosInstance.post<{ data: { id: number } }>('/api/libraries', { + data: { + user: userId, + publishedAt: new Date().toISOString(), + }, + }); + return data?.data?.id ?? null; + } catch (error) { + console.error('createLibrary failed:', error); + return null; + } +}; diff --git a/src/api/library/library/getLibraryIdByUsername.ts b/src/api/library/library/getLibraryIdByUsername.ts new file mode 100644 index 00000000..80ff0ded --- /dev/null +++ b/src/api/library/library/getLibraryIdByUsername.ts @@ -0,0 +1,20 @@ +import axiosInstance from '@/libraries/axios'; + +import type { StrapiLibrariesResponse } from '@/types/library'; + +// Resolve a `/library/[username]` slug to a numeric library id. Returns null +// when the username has no library (or the lookup fails). +export const getLibraryIdByUsername = async (username: string): Promise => { + try { + const { data } = await axiosInstance.get('/api/libraries', { + params: { + 'filters[user][username][$eqi]': username, + 'pagination[pageSize]': 1, + }, + }); + return data.data?.[0]?.id ?? null; + } catch (error) { + console.error('getLibraryIdByUsername failed:', error); + return null; + } +}; diff --git a/src/api/library/library/getMyLibrary.ts b/src/api/library/library/getMyLibrary.ts new file mode 100644 index 00000000..1f1b0528 --- /dev/null +++ b/src/api/library/library/getMyLibrary.ts @@ -0,0 +1,21 @@ +import axiosInstance from '@/libraries/axios'; + +import type { ILibrary } from '@/types/library'; +import type { IStrapiListResponse } from '@/types/strapi'; + +export const getMyLibrary = async (userId: number | string): Promise => { + try { + const { data } = await axiosInstance.get>('/api/libraries', { + params: { + 'filters[user][id][$eq]': userId, + 'pagination[pageSize]': 1, + 'populate[avatar]': true, + 'populate[libraryDetails]': true, + }, + }); + return data.data[0] ?? null; + } catch (error) { + console.error('getMyLibrary failed:', error); + return null; + } +}; diff --git a/src/api/library/library/updateLibrary.ts b/src/api/library/library/updateLibrary.ts new file mode 100644 index 00000000..add03a40 --- /dev/null +++ b/src/api/library/library/updateLibrary.ts @@ -0,0 +1,14 @@ +import axiosInstance from '@/libraries/axios'; + +import type { ILibrarySingleResponse, IUpdateLibraryPayload } from '@/types/library'; + +export const updateLibrary = async ( + id: number, + payload: IUpdateLibraryPayload +): Promise => { + const { data } = await axiosInstance.put(`/api/libraries/${id}`, { + data: payload, + }); + + return data; +}; diff --git a/src/api/library/object/createObject.ts b/src/api/library/object/createObject.ts new file mode 100644 index 00000000..9496398d --- /dev/null +++ b/src/api/library/object/createObject.ts @@ -0,0 +1,19 @@ +import axiosInstance from '@/libraries/axios'; + +import type { ICreateObjectPayload, IObjectSingleResponse } from '@/types/object'; + +export const createObject = async ( + payload: ICreateObjectPayload +): Promise => { + // No `populate` query params here: Strapi users-permissions runs a + // relation-level permission check on each populated field, and the + // Authenticated role typically lacks find/findOne on shelf/tag/upload, + // which surfaces as a 403 on the whole POST. AddObjectModal backfills + // coverImage + tags from local data; the next library refetch returns + // the canonical populated shape. + const { data } = await axiosInstance.post('/api/objects', { + data: payload, + }); + + return data; +}; diff --git a/src/api/library/object/deleteObject.ts b/src/api/library/object/deleteObject.ts new file mode 100644 index 00000000..597c74a3 --- /dev/null +++ b/src/api/library/object/deleteObject.ts @@ -0,0 +1,9 @@ +import axiosInstance from '@/libraries/axios'; + +import type { IObjectSingleResponse } from '@/types/object'; + +export const deleteObject = async (id: number): Promise => { + const { data } = await axiosInstance.delete(`/api/objects/${id}`); + + return data; +}; diff --git a/src/api/library/object/updateObject.ts b/src/api/library/object/updateObject.ts new file mode 100644 index 00000000..cdcb55ac --- /dev/null +++ b/src/api/library/object/updateObject.ts @@ -0,0 +1,16 @@ +import axiosInstance from '@/libraries/axios'; + +import type { IObjectSingleResponse, IUpdateObjectPayload } from '@/types/object'; + +export const updateObject = async ( + id: number, + payload: IUpdateObjectPayload +): Promise => { + // See createObject.ts — populate params on write endpoints trigger a + // relation-permission 403 for the Authenticated role. Skip them here too. + const { data } = await axiosInstance.put(`/api/objects/${id}`, { + data: payload, + }); + + return data; +}; diff --git a/src/api/library/shelf/createShelf.ts b/src/api/library/shelf/createShelf.ts new file mode 100644 index 00000000..8f15bfcc --- /dev/null +++ b/src/api/library/shelf/createShelf.ts @@ -0,0 +1,19 @@ +import axiosInstance from '@/libraries/axios'; + +import type { ICreateShelfPayload, IShelfSingleResponse } from '@/types/shelf'; + +export const createShelf = async (payload: ICreateShelfPayload): Promise => { + const { data } = await axiosInstance.post('/api/single-shelves', { + data: { + visibility: 'public', + order: 0, + objects: [], + // single-shelf has draftAndPublish: true. Direct queries filter by + // publication state, so publish explicitly to keep the new shelf visible. + publishedAt: new Date().toISOString(), + ...payload, + }, + }); + + return data; +}; diff --git a/src/api/library/shelf/deleteShelf.ts b/src/api/library/shelf/deleteShelf.ts new file mode 100644 index 00000000..8cabfc4c --- /dev/null +++ b/src/api/library/shelf/deleteShelf.ts @@ -0,0 +1,6 @@ +import axiosInstance from '@/libraries/axios'; + +// Backend cascades — deletes every object on the shelf too. See docs/shelf-api.md. +export const deleteShelf = async (id: number): Promise => { + await axiosInstance.delete(`/api/single-shelves/${id}`); +}; diff --git a/src/api/library/shelf/getShelvesList.ts b/src/api/library/shelf/getShelvesList.ts new file mode 100644 index 00000000..a5b8509e --- /dev/null +++ b/src/api/library/shelf/getShelvesList.ts @@ -0,0 +1,24 @@ +import axiosInstance from '@/libraries/axios'; + +import type { ObjectType } from '@/types/object'; +import type { IShelf } from '@/types/shelf'; +import type { IStrapiListResponse } from '@/types/strapi'; + +export type IShelvesListResponse = IStrapiListResponse; + +export const getShelvesList = async ( + filterType?: ObjectType +): Promise => { + try { + const params = filterType + ? { 'filters[type][$eq]': filterType, sort: 'order:asc' } + : { sort: 'order:asc' }; + const { data } = await axiosInstance.get('/api/single-shelves', { + params, + }); + return data; + } catch (error) { + console.error(error); + return null; + } +}; diff --git a/src/api/library/shelf/updateShelf.ts b/src/api/library/shelf/updateShelf.ts new file mode 100644 index 00000000..d7454bf1 --- /dev/null +++ b/src/api/library/shelf/updateShelf.ts @@ -0,0 +1,14 @@ +import axiosInstance from '@/libraries/axios'; + +import type { IShelfSingleResponse, IUpdateShelfPayload } from '@/types/shelf'; + +export const updateShelf = async ( + id: number, + payload: IUpdateShelfPayload +): Promise => { + const { data } = await axiosInstance.put(`/api/single-shelves/${id}`, { + data: payload, + }); + + return data; +}; diff --git a/src/api/library/strapi.ts b/src/api/library/strapi.ts new file mode 100644 index 00000000..4736fb26 --- /dev/null +++ b/src/api/library/strapi.ts @@ -0,0 +1,93 @@ +import axiosInstance from '@/libraries/axios'; + +import type { StrapiLibrariesResponse, StrapiSingleLibraryResponse } from '@/types/library'; +import type { IUser } from '@/types/user'; + +export const getUserInfo = async (): Promise => { + try { + const { data } = await axiosInstance.get('/api/users/me'); + + return data ?? null; + } catch (e) { + console.error(e); + + return null; + } +}; + +// Populate the relations the home/sidebar library cards need: avatar (image), +// user (for the `/library/[username]` URL), and shelves + their objects (so the +// per-type counts reflect object totals, not shelf totals). +const LIBRARY_CARD_POPULATE = { + 'populate[avatar]': true, + 'populate[user]': true, + 'populate[singleShelves][populate][objects]': true, +} as const; + +export const getLibrariesList = async (): Promise => { + try { + const { data } = await axiosInstance.get('/api/libraries', { + params: LIBRARY_CARD_POPULATE, + }); + + return data ?? null; + } catch (e) { + console.error(e); + + return null; + } +}; + +export const getLibrariesPaginated = async ( + page = 1, + pageSize = 8 +): Promise => { + try { + const { data } = await axiosInstance.get('/api/libraries', { + params: { + ...LIBRARY_CARD_POPULATE, + 'pagination[page]': page, + 'pagination[pageSize]': pageSize, + }, + }); + + return data ?? null; + } catch (e) { + console.error(e); + + return null; + } +}; + +export const getSingleLibrary = async ( + id: number | string +): Promise => { + try { + const { data } = await axiosInstance.get(`/api/libraries/${id}`, { + params: { + 'populate[avatar]': true, + 'populate[libraryDetails]': true, + 'populate[singleShelves][populate][objects][populate][coverImage]': true, + 'populate[singleShelves][populate][objects][populate][tags]': true, + }, + }); + + return data ?? null; + } catch (e) { + console.error(e); + + return null; + } +}; + +export const getShelves = async (): Promise => { + try { + const { data } = await axiosInstance.get('/api/single-shelves'); + + return data ?? null; + } catch (e) { + console.error(e); + + return null; + } +}; diff --git a/src/api/library/tag/createTag.ts b/src/api/library/tag/createTag.ts new file mode 100644 index 00000000..b86b9d6b --- /dev/null +++ b/src/api/library/tag/createTag.ts @@ -0,0 +1,15 @@ +import axiosInstance from '@/libraries/axios'; + +export interface CreateTagRequest { + name: string; + slug: string; + user: string; + color: string; + description?: string; +} + +export const createTag = async (tagData: CreateTagRequest) => { + const { data } = await axiosInstance.post('/api/tags', { data: tagData }); + + return data; +}; diff --git a/src/api/library/tag/deleteTag.ts b/src/api/library/tag/deleteTag.ts new file mode 100644 index 00000000..05359791 --- /dev/null +++ b/src/api/library/tag/deleteTag.ts @@ -0,0 +1,7 @@ +import axiosInstance from '@/libraries/axios'; + +export const deleteTag = async (tagId: number | string) => { + const { data } = await axiosInstance.delete(`/api/tags/${tagId}`); + + return data; +}; diff --git a/src/api/library/tag/getTagsList.ts b/src/api/library/tag/getTagsList.ts new file mode 100644 index 00000000..964e8add --- /dev/null +++ b/src/api/library/tag/getTagsList.ts @@ -0,0 +1,29 @@ +import axiosInstance from '@/libraries/axios'; + +import { ITag } from '@/types/tag'; + +export interface GetTagsListResponse { + data: ITag[]; +} + +export const getTagsList = async (): Promise => { + try { + // The axios interceptor reads `accessToken` from document.cookie, which + // doesn't exist during SSR. When called from a Server Component, pull the + // token from the request cookies and attach it explicitly. + const headers: Record = {}; + if (typeof window === 'undefined') { + const { cookies } = await import('next/headers'); + const token = (await cookies()).get('accessToken')?.value; + if (token) headers.Authorization = `Bearer ${token}`; + } + + const { data } = await axiosInstance.get('/api/tags', { headers }); + + return data; + } catch (error) { + console.error(error); + + return { data: [] }; + } +}; diff --git a/src/api/library/tag/updateTag.ts b/src/api/library/tag/updateTag.ts new file mode 100644 index 00000000..6ff7dd50 --- /dev/null +++ b/src/api/library/tag/updateTag.ts @@ -0,0 +1,17 @@ +import axiosInstance from '@/libraries/axios'; + +export interface UpdateTagRequest { + name: string; + slug: string; + user: string; + color: string; + description?: string; +} + +export const updateTag = async (tagId: number | string, tagData: UpdateTagRequest) => { + const { data } = await axiosInstance.put(`/api/tags/${tagId}`, { + data: tagData, + }); + + return data; +}; diff --git a/src/api/library/upload/uploadFile.ts b/src/api/library/upload/uploadFile.ts new file mode 100644 index 00000000..32785558 --- /dev/null +++ b/src/api/library/upload/uploadFile.ts @@ -0,0 +1,14 @@ +import axiosInstance from '@/libraries/axios'; + +import type { IUploadedFile } from '@/types/media'; + +export const uploadFile = async (file: File): Promise => { + const formData = new FormData(); + formData.append('files', file); + + const { data } = await axiosInstance.post('/api/upload', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + + return data[0]; +}; diff --git a/src/api/library/user/updateMe.ts b/src/api/library/user/updateMe.ts new file mode 100644 index 00000000..ebdb6abd --- /dev/null +++ b/src/api/library/user/updateMe.ts @@ -0,0 +1,9 @@ +import axiosInstance from '@/libraries/axios'; + +import type { IUpdateMePayload, IUpdateMeResponse } from '@/types/user'; + +export const updateMe = async (payload: IUpdateMePayload): Promise => { + // Note the singular `user` — see docs/user-api.md §1. + const { data } = await axiosInstance.put('/api/user/me', payload); + return data; +}; diff --git a/src/assets/library/images/avatar.png b/src/assets/library/images/avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..a61c9e4ac8d8b4738e0cd05f76a9db5872982cf8 GIT binary patch literal 4402 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 t>%=zvabn9h8J@x9 + + diff --git a/src/assets/library/svg/articles.svg b/src/assets/library/svg/articles.svg new file mode 100644 index 00000000..c56e1ec4 --- /dev/null +++ b/src/assets/library/svg/articles.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/library/svg/audio.svg b/src/assets/library/svg/audio.svg new file mode 100644 index 00000000..1eff0059 --- /dev/null +++ b/src/assets/library/svg/audio.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/library/svg/avatar.svg b/src/assets/library/svg/avatar.svg new file mode 100644 index 00000000..e9a093e1 --- /dev/null +++ b/src/assets/library/svg/avatar.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/library/svg/book-shadow.svg b/src/assets/library/svg/book-shadow.svg new file mode 100644 index 00000000..9ea658cd --- /dev/null +++ b/src/assets/library/svg/book-shadow.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/library/svg/book.svg b/src/assets/library/svg/book.svg new file mode 100644 index 00000000..7ede3c14 --- /dev/null +++ b/src/assets/library/svg/book.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/library/svg/calendar.svg b/src/assets/library/svg/calendar.svg new file mode 100644 index 00000000..e1801180 --- /dev/null +++ b/src/assets/library/svg/calendar.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/library/svg/check.svg b/src/assets/library/svg/check.svg new file mode 100644 index 00000000..ec525091 --- /dev/null +++ b/src/assets/library/svg/check.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/library/svg/close.svg b/src/assets/library/svg/close.svg new file mode 100644 index 00000000..d84a9437 --- /dev/null +++ b/src/assets/library/svg/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/library/svg/company.svg b/src/assets/library/svg/company.svg new file mode 100644 index 00000000..0da5a43a --- /dev/null +++ b/src/assets/library/svg/company.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/library/svg/copy.svg b/src/assets/library/svg/copy.svg new file mode 100644 index 00000000..4d255117 --- /dev/null +++ b/src/assets/library/svg/copy.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/library/svg/delete.svg b/src/assets/library/svg/delete.svg new file mode 100644 index 00000000..cb19a480 --- /dev/null +++ b/src/assets/library/svg/delete.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/library/svg/discord.svg b/src/assets/library/svg/discord.svg new file mode 100644 index 00000000..c03e8e12 --- /dev/null +++ b/src/assets/library/svg/discord.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/assets/library/svg/dots-vertical.svg b/src/assets/library/svg/dots-vertical.svg new file mode 100644 index 00000000..c2ac57ad --- /dev/null +++ b/src/assets/library/svg/dots-vertical.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/library/svg/edit.svg b/src/assets/library/svg/edit.svg new file mode 100644 index 00000000..fb795e43 --- /dev/null +++ b/src/assets/library/svg/edit.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/library/svg/error.svg b/src/assets/library/svg/error.svg new file mode 100644 index 00000000..5f901985 --- /dev/null +++ b/src/assets/library/svg/error.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/library/svg/google.svg b/src/assets/library/svg/google.svg new file mode 100644 index 00000000..bc22fa4d --- /dev/null +++ b/src/assets/library/svg/google.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/library/svg/hamburger.svg b/src/assets/library/svg/hamburger.svg new file mode 100644 index 00000000..18addab6 --- /dev/null +++ b/src/assets/library/svg/hamburger.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/library/svg/index.ts b/src/assets/library/svg/index.ts new file mode 100644 index 00000000..a33e8e51 --- /dev/null +++ b/src/assets/library/svg/index.ts @@ -0,0 +1,57 @@ +import LogoIcon from './logo.svg'; +import CopyIcon from './copy.svg'; +import PlusIcon from './plus.svg'; +import EditIcon from './edit.svg'; +import InfoIcon from './info.svg'; +import BookIcon from './book.svg'; +import ArrowIcon from './arrow.svg'; +import CloseIcon from './close.svg'; +import VideoIcon from './video.svg'; +import AudioIcon from './audio.svg'; +import ToolsIcon from './tools.svg'; +import CheckIcon from './check.svg'; +import ErrorIcon from './error.svg'; +import AvatarIcon from './avatar.svg'; +import SearchIcon from './search.svg'; +import UxcoreIcon from './uxcore.svg'; +import DeleteIcon from './delete.svg'; +import LibraryIcon from './library.svg'; +import CompanyIcon from './company.svg'; +import ArticlesIcon from './articles.svg'; +import SettingsIcon from './settings.svg'; +import HamburgerIcon from './hamburger.svg'; +import ShareIcon from './share.svg'; +import DotsVerticalIcon from './dots-vertical.svg'; +import CalendarIcon from './calendar.svg'; +import BookShadowIcon from './book-shadow.svg'; +import VideoShadowIcon from './video-shadow.svg'; + +export { + LogoIcon, + CopyIcon, + BookIcon, + EditIcon, + PlusIcon, + InfoIcon, + ToolsIcon, + ArrowIcon, + CloseIcon, + ErrorIcon, + CheckIcon, + AudioIcon, + VideoIcon, + UxcoreIcon, + DeleteIcon, + SearchIcon, + AvatarIcon, + LibraryIcon, + CompanyIcon, + SettingsIcon, + ArticlesIcon, + HamburgerIcon, + ShareIcon, + DotsVerticalIcon, + CalendarIcon, + BookShadowIcon, + VideoShadowIcon, +}; diff --git a/src/assets/library/svg/info.svg b/src/assets/library/svg/info.svg new file mode 100644 index 00000000..a8c3805d --- /dev/null +++ b/src/assets/library/svg/info.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/library/svg/library.svg b/src/assets/library/svg/library.svg new file mode 100644 index 00000000..f8e5036e --- /dev/null +++ b/src/assets/library/svg/library.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/library/svg/logo.svg b/src/assets/library/svg/logo.svg new file mode 100644 index 00000000..d2f5b521 --- /dev/null +++ b/src/assets/library/svg/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/library/svg/plus.svg b/src/assets/library/svg/plus.svg new file mode 100644 index 00000000..a641bd40 --- /dev/null +++ b/src/assets/library/svg/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/library/svg/search.svg b/src/assets/library/svg/search.svg new file mode 100644 index 00000000..3aa56f74 --- /dev/null +++ b/src/assets/library/svg/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/library/svg/settings.svg b/src/assets/library/svg/settings.svg new file mode 100644 index 00000000..d02f7179 --- /dev/null +++ b/src/assets/library/svg/settings.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/library/svg/share.svg b/src/assets/library/svg/share.svg new file mode 100644 index 00000000..ff332303 --- /dev/null +++ b/src/assets/library/svg/share.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/library/svg/tools.svg b/src/assets/library/svg/tools.svg new file mode 100644 index 00000000..23023eb7 --- /dev/null +++ b/src/assets/library/svg/tools.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/library/svg/ux-core.svg b/src/assets/library/svg/ux-core.svg new file mode 100644 index 00000000..d6d54f34 --- /dev/null +++ b/src/assets/library/svg/ux-core.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/library/svg/uxcore.svg b/src/assets/library/svg/uxcore.svg new file mode 100644 index 00000000..d6d54f34 --- /dev/null +++ b/src/assets/library/svg/uxcore.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/library/svg/video-shadow.svg b/src/assets/library/svg/video-shadow.svg new file mode 100644 index 00000000..a1720235 --- /dev/null +++ b/src/assets/library/svg/video-shadow.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/library/svg/video.svg b/src/assets/library/svg/video.svg new file mode 100644 index 00000000..dddbd986 --- /dev/null +++ b/src/assets/library/svg/video.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/components/Context/library/AuthContext.tsx b/src/components/Context/library/AuthContext.tsx new file mode 100644 index 00000000..24b5d3fa --- /dev/null +++ b/src/components/Context/library/AuthContext.tsx @@ -0,0 +1,113 @@ +'use client'; + +import React, { + useState, + createContext, + type ReactNode, + useCallback, + useEffect, + useContext, +} from 'react'; +import { Session } from 'next-auth'; +import { useRouter } from 'next/navigation'; +import { SessionProvider, signOut } from 'next-auth/react'; + +import { logout } from '@/api/auth'; +import { getCookie } from '@/libraries/cookie'; + +import { IUser } from '@/types/user'; + +type AuthContextValue = { + accountData: IUser | null; + setAccountData: (value: IUser | null) => void; + token: string | null; + setToken: (value: string | null) => void; + handleProviderSignIn: (provider: string) => void; + handleLogout: () => void; +}; + +const defaultValues: AuthContextValue = { + accountData: null, + setAccountData: () => {}, + token: null, + setToken: () => {}, + handleProviderSignIn: () => {}, + handleLogout: () => {}, +}; + +export const AuthContext = createContext(defaultValues); + +type AuthProviderProps = { + children: ReactNode; + session: Session | null; +}; + +export const AuthProvider = ({ children, session = null }: AuthProviderProps) => { + const router = useRouter(); + + const [token, setToken] = useState(null); + const [accountData, setAccountData] = useState(null); + + const handleProviderSignIn = async (provider: string) => { + // Store current page as return URL before login + if (typeof window !== 'undefined') { + const currentPath = window.location.pathname + window.location.search; + // Don't store auth or dashboard pages as return URLs + if (!currentPath.includes('/auth') && !currentPath.includes('/dashboard')) { + localStorage.setItem('returnUrl', currentPath); + } + } + + if (session && accountData === null) { + await signOut({ redirect: false }); + + sessionStorage.clear(); + document.cookie = 'next-auth.session-token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; + + setTimeout(() => { + router.replace(`/auth?provider=${provider}`); + }, 100); + } else { + router.push(`/auth?provider=${provider}`); + } + }; + + const handleLogout = useCallback(() => { + logout(); + const secure = window.location.protocol === 'https:' ? ' Secure;' : ''; + document.cookie = `accessToken=; path=/;${secure} SameSite=Strict;`; + }, []); + + useEffect(() => { + const accessToken = getCookie('accessToken') as string | undefined; + setToken(accessToken || null); + }, [session]); + + return ( + <> + + {/* @ts-expect-error - NextAuth SessionProvider type compatibility issue */} + {React.createElement(SessionProvider, { session, refetchInterval: 0 }, children)} + + + ); +}; + +export function useAuth(): AuthContextValue { + const context = useContext(AuthContext); + + if (!context) { + throw new Error('Auth component must be used within AuthProvider'); + } + + return context; +} diff --git a/src/components/Context/library/DashboardContext.tsx b/src/components/Context/library/DashboardContext.tsx new file mode 100644 index 00000000..e735661c --- /dev/null +++ b/src/components/Context/library/DashboardContext.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { createContext, useContext, useMemo, useState, type ReactNode } from 'react'; + +import { ITag } from '@/types/tag'; + +interface DashboardContextValue { + tags: ITag[]; + setTags: (tags: ITag[]) => void; +} + +const DashboardContext = createContext(undefined); + +interface DashboardProviderProps { + children: ReactNode; + initialTags?: ITag[]; +} + +export function DashboardProvider({ children, initialTags = [] }: DashboardProviderProps) { + const [tags, setTags] = useState(initialTags); + + const value = useMemo( + () => ({ + tags, + setTags, + }), + [tags] + ); + + return {children}; +} + +export function useDashboard(): DashboardContextValue { + const context = useContext(DashboardContext); + + if (!context) { + throw new Error('useDashboard must be used within a DashboardProvider'); + } + + return context; +} diff --git a/src/components/Context/library/GlobalStateContext.tsx b/src/components/Context/library/GlobalStateContext.tsx new file mode 100644 index 00000000..18c38667 --- /dev/null +++ b/src/components/Context/library/GlobalStateContext.tsx @@ -0,0 +1,147 @@ +'use client'; + +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, + type ReactNode, +} from 'react'; +import { useSession } from 'next-auth/react'; + +import { getLibrariesList, getUserInfo } from '@/api/strapi'; +import { useAuth } from '@/context/AuthContext'; +import { getCookie } from '@/libraries/cookie'; + +import type { IUser } from '@/types/user'; +import type { StrapiSingleShelfEntry } from '@/types/library'; + +interface GlobalStateContextValue { + isGuestMode: boolean; + isSidebarOpen: boolean; + toggleGuestMode: () => void; + toggleSidebar: () => void; + user: IUser | null; + isUserLoading: boolean; + refetchUser: () => Promise; + libraries: unknown | null; + isLibrariesLoading: boolean; + refetchLibraries: () => Promise; + /** + * Shelves of the library currently being viewed — populated by + * `LibraryTemplate` so the Header can render the Jump-to nav without + * having to fetch its own copy. + */ + currentShelves: StrapiSingleShelfEntry[]; + setCurrentShelves: (shelves: StrapiSingleShelfEntry[]) => void; +} + +const GlobalStateContext = createContext(undefined); + +export function GlobalStateProvider({ children }: { children: ReactNode }) { + const { data: session } = useSession(); + const { accountData, setAccountData, token } = useAuth(); + + const [isGuestMode, setIsGuestMode] = useState(false); + const [isSidebarOpen, setIsSidebarOpen] = useState(false); + const [isUserLoading, setIsUserLoading] = useState(false); + const [libraries, setLibraries] = useState(null); + const [isLibrariesLoading, setIsLibrariesLoading] = useState(false); + const [currentShelves, setCurrentShelves] = useState([]); + const didAttemptUserLoad = useRef(false); + const didAttemptLibrariesLoad = useRef(false); + + const refetchUser = useCallback(async () => { + setIsUserLoading(true); + try { + const data = await getUserInfo(); + setAccountData(data); + } finally { + setIsUserLoading(false); + } + }, [setAccountData]); + + const refetchLibraries = useCallback(async () => { + setIsLibrariesLoading(true); + try { + const data = await getLibrariesList(); + setLibraries(data); + } finally { + setIsLibrariesLoading(false); + } + }, []); + + useEffect(() => { + refetchLibraries(); + }, [refetchLibraries]); + + useEffect(() => { + const hasToken = Boolean(getCookie('accessToken')); + if (!hasToken) { + didAttemptUserLoad.current = false; + return; + } + if (accountData || didAttemptUserLoad.current) { + return; + } + didAttemptUserLoad.current = true; + void refetchUser(); + }, [accountData, session, refetchUser]); + + useEffect(() => { + if (!token) { + didAttemptLibrariesLoad.current = false; + setLibraries(null); + return; + } + if (didAttemptLibrariesLoad.current) { + return; + } + didAttemptLibrariesLoad.current = true; + void refetchLibraries(); + }, [token, session, refetchLibraries]); + + const value = useMemo( + () => ({ + isGuestMode, + isSidebarOpen, + toggleGuestMode: () => setIsGuestMode((prev) => !prev), + toggleSidebar: () => setIsSidebarOpen((prev) => !prev), + user: accountData, + isUserLoading, + refetchUser, + libraries, + isLibrariesLoading, + refetchLibraries, + currentShelves, + setCurrentShelves, + }), + [ + isGuestMode, + isSidebarOpen, + accountData, + isUserLoading, + refetchUser, + libraries, + isLibrariesLoading, + refetchLibraries, + currentShelves, + setCurrentShelves, + ] + ); + + return {children}; +} + +export function useGlobalState(): GlobalStateContextValue { + const context = useContext(GlobalStateContext); + + if (!context) { + throw new Error('useGlobalState must be used within a GlobalStateProvider'); + } + + return context; +} diff --git a/src/components/library/LIBRARY_AGENT.md b/src/components/library/LIBRARY_AGENT.md new file mode 100644 index 00000000..6f2daa6c --- /dev/null +++ b/src/components/library/LIBRARY_AGENT.md @@ -0,0 +1,116 @@ +# AGENTS.md + +Guidance for any AI coding agent working in this repo (Cursor, OpenAI Codex, Aider, Jules, Claude Code, etc.). Keep responses tight; follow the rules below over generic "best practices." + +## Project overview + +`library` (also called "keepSimple Library") is a Next.js 15 App Router app for browsing user libraries of books/videos/music. Auth is NextAuth (Google + Discord providers) bridged to a Strapi backend. UI follows atomic design with SCSS Modules. Stack: React 19, TypeScript (strict), Yarn, Node 18.18.0. + +## Commands + +| Command | What it does | +| ------------------------ | -------------------------------------------- | +| `yarn dev` | Run Next dev server at http://localhost:3000 | +| `yarn build` | Production Next build | +| `yarn start` | Run the production build | +| `yarn lint` | `next lint` (ESLint flat config) | +| `yarn format` | Prettier write across the repo | +| `yarn format:check` | Prettier check (CI-friendly, no writes) | +| `yarn storybook` | Storybook at http://localhost:6006 | +| `yarn build-storybook` | Static Storybook build | +| `yarn new:atom Name` | Scaffold an atom (PascalCase required) | +| `yarn new:molecule Name` | Scaffold a molecule | +| `yarn new:organism Name` | Scaffold an organism | +| `yarn new:template Name` | Scaffold a template | + +Env: copy NextAuth/Strapi/provider keys into `.env.local` (there is no `.env.example` — see [src/app/api/auth/[...nextauth]/authОptions.ts](src/app/api/auth/[...nextauth]/authОptions.ts) for required keys). Husky runs `lint-staged` on pre-commit. + +There is **no** `typecheck` or `test` script. To typecheck, run `yarn tsc --noEmit`. Vitest is wired only to run Storybook stories via `@storybook/experimental-addon-test` (Playwright/Chromium). + +## Architecture map + +``` +src/ + app/ # Next App Router (pages, layouts, route handlers) + api/auth/[...nextauth]/ # NextAuth handler + authОptions + refresh token + auth/ # /auth page (post-OAuth callback handling) + library/[username]/ # dynamic library page + components/ + atoms/ # purely presentational primitives (Text, Icon, Avatar, …) + molecules/ # composed-of-atoms, still presentational (Button, Modal, Input, …) + organisms/ # complex sections (Header, Sidebar, LibraryCard) + templates/ # page-level layouts (HomeTemplate, LibraryTemplate) + context/ # React Context providers (AuthContext, GlobalStateContext) — the only state mgmt + api/ # thin HTTP wrappers around axiosInstance (strapi.ts, auth.ts) + libraries/ # third-party SDK adapters (axios/, cookie/) — NOTE: name is "libraries", not "lib" + hooks/ # custom React hooks + utils/ # pure utility functions (e.g. seo.ts) + types/ # shared TypeScript types + constants/ # shared constants (e.g. librariesData, shelfCardData) + config/ # config objects (e.g. seo.config.ts) + styles/ # global SCSS (auto-prepended via next.config.ts) + assets/svg/ # SVG components (loaded via SVGR) + featues/ # [sic] feature-level logic (currently a placeholder file) +generators/ # `yarn new:*` component generator scripts +.storybook/ # Storybook config +public/ # static assets +``` + +Path alias: `@/*` → `src/*`. + +## Conventions (rules) + +- **Component file shape.** Every component lives in its own folder with five files: `Name.tsx`, `Name.types.ts`, `Name.module.scss`, `Name.stories.tsx`, `index.ts(x)`. Always use `yarn new:atom|molecule|organism|template Name` instead of hand-creating. +- **Naming.** PascalCase for component dirs/files and types. camelCase for hooks (`useThing.ts`) and utilities. Hooks files use `.ts` unless they need JSX (`.tsx`). +- **Exports.** Named exports for components (`export function Button(...)`). The component's `index` re-exports both the component and its types: `export * from './Button'; export * from './Button.types';`. +- **Props typing.** Define a `NameProps` interface in `Name.types.ts`. Use TypeScript `enum`s for closed variant sets (see `ButtonType`, `ButtonSize`, `TypographyVariant`). +- **Styling.** SCSS Modules only. Compose classes with `classnames`. The styles entrypoint `styles.scss` is auto-prepended via `next.config.ts` — global SCSS variables/mixins are already available, do not `@use` them per-file. +- **Client/server.** Server components are the default. Add `'use client'` only when you need state, effects, browser APIs, or browser-only providers. Layouts under `src/app/` should stay server-side when possible (see [src/app/layout.tsx](src/app/layout.tsx)). +- **State.** React Context only — `AuthContext` and `GlobalStateContext`. No Redux/Zustand/TanStack Query. Add new global state to `GlobalStateContext` rather than creating new providers unless the concern is genuinely separable. +- **HTTP.** All Strapi calls go through `axiosInstance` ([src/libraries/axios/index.ts](src/libraries/axios/index.ts)) — it auto-attaches the `accessToken` cookie as a Bearer. Wrap calls in `src/api/*.ts`; never call `fetch` for Strapi directly (the exception is the OAuth callback in [src/api/auth.ts](src/api/auth.ts)). +- **SVGs.** Import as React components: `import GoogleIcon from '@/assets/svg/google.svg'` — SVGR is configured in `next.config.ts`. Don't use `next/image` for SVGs. +- **SEO & semantic HTML.** Care about SEO and correct, semantic markup at every level of the atomic hierarchy — accessibility and crawlability are first-class, not afterthoughts. Raster images go through `next/image` (``), never a raw `` (it gives lazy-loading, sizing, and CLS protection; remember to allowlist the host in `next.config.ts` `images.remotePatterns`). Use semantic elements over `
` soup (`
+ + ); +} diff --git a/src/components/library/molecules/AboutLibraryModal/AboutLibraryModal.types.ts b/src/components/library/molecules/AboutLibraryModal/AboutLibraryModal.types.ts new file mode 100644 index 00000000..bbebd7dc --- /dev/null +++ b/src/components/library/molecules/AboutLibraryModal/AboutLibraryModal.types.ts @@ -0,0 +1,3 @@ +export interface AboutLibraryModalProps { + onClose: () => void; +} diff --git a/src/components/library/molecules/AboutLibraryModal/index.tsx b/src/components/library/molecules/AboutLibraryModal/index.tsx new file mode 100644 index 00000000..34987f12 --- /dev/null +++ b/src/components/library/molecules/AboutLibraryModal/index.tsx @@ -0,0 +1,2 @@ +export * from './AboutLibraryModal'; +export * from './AboutLibraryModal.types'; diff --git a/src/components/library/molecules/AddShelfModal/AddShelfModal.module.scss b/src/components/library/molecules/AddShelfModal/AddShelfModal.module.scss new file mode 100644 index 00000000..e0737a33 --- /dev/null +++ b/src/components/library/molecules/AddShelfModal/AddShelfModal.module.scss @@ -0,0 +1,68 @@ +.modal { + width: 100% !important; + max-width: 456px !important; + + .wrapper { + padding: 32px; + border-top: 1px solid var(--brown-border); + } + + .field { + display: flex; + flex-direction: column; + gap: 6px; + margin-bottom: 24px; + } + + .label { + color: var(--gray-darkest); + } + + .content { + gap: 16px; + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 4px; + + .item { + background: var(--off-white); + flex: 1; + padding: 12px 4px; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + min-height: 82px; + flex-direction: column; + border-radius: 4px; + cursor: pointer; + border: 1px solid transparent; + transition: + border-color 0.2s ease, + box-shadow 0.2s ease, + background-color 0.2s ease; + + &:hover { + border-color: var(--brown-border); + } + + &.active { + border-color: var(--brown); + background: var(--white); + } + } + } + + .footer { + display: flex; + align-items: center; + justify-content: center; + gap: 16px; + margin-top: 40px; + + button { + width: auto; + } + } +} diff --git a/src/components/library/molecules/AddShelfModal/AddShelfModal.tsx b/src/components/library/molecules/AddShelfModal/AddShelfModal.tsx new file mode 100644 index 00000000..3c4d0833 --- /dev/null +++ b/src/components/library/molecules/AddShelfModal/AddShelfModal.tsx @@ -0,0 +1,92 @@ +'use client'; + +import classNames from 'classnames'; +import React, { JSX, useState } from 'react'; + +import { Modal, useModalClose } from '../Modal'; +import { Input } from '../Input'; +import { shelfCardData } from '@/constants/common'; +import { Text, TypographyVariant } from '@/components/atoms/Text'; + +import { Button, ButtonSize, ButtonType } from '../Button'; + +import type { AddShelfModalProps, ShelfType } from './AddShelfModal.types'; + +import styles from './AddShelfModal.module.scss'; + +// Matches the single-shelf `name` constraint (`maxLength: 50`) in the backend schema. +const SHELF_NAME_MAX_LENGTH = 50; + +export function AddShelfModal(props: AddShelfModalProps): JSX.Element { + const { onClose, onAddShelf } = props; + const { closeRef, close } = useModalClose(onClose); + const [activeItem, setActiveItem] = useState('books'); + const [name, setName] = useState(''); + + const trimmedName = name.trim(); + const canSubmit = trimmedName.length > 0; + + const handleAddShelf = () => { + if (!canSubmit) return; + onAddShelf(activeItem, trimmedName); + }; + + return ( + +
+
+ + Shelf name + + setName(e.target.value)} + placeholder="My shelf" + placeholderColor="#9E9E9E" + ariaLabel="Shelf name" + maxLength={SHELF_NAME_MAX_LENGTH} + /> +
+ +
+ {shelfCardData.map((item) => { + return ( +
setActiveItem(item.key)} + > + + {item.label} +
+ ); + })} +
+ +
+
+
+
+ ); +} diff --git a/src/components/library/molecules/AddShelfModal/AddShelfModal.types.ts b/src/components/library/molecules/AddShelfModal/AddShelfModal.types.ts new file mode 100644 index 00000000..6d2f7da4 --- /dev/null +++ b/src/components/library/molecules/AddShelfModal/AddShelfModal.types.ts @@ -0,0 +1,6 @@ +export type ShelfType = 'books' | 'videos' | 'audios'; + +export interface AddShelfModalProps { + onClose: () => void; + onAddShelf: (type: ShelfType, name: string) => void; +} diff --git a/src/components/library/molecules/AddShelfModal/index.tsx b/src/components/library/molecules/AddShelfModal/index.tsx new file mode 100644 index 00000000..d5a10cb5 --- /dev/null +++ b/src/components/library/molecules/AddShelfModal/index.tsx @@ -0,0 +1,2 @@ +export * from './AddShelfModal'; +export * from './AddShelfModal.types'; diff --git a/src/components/library/molecules/AudioCard/AudioCard.module.scss b/src/components/library/molecules/AudioCard/AudioCard.module.scss new file mode 100644 index 00000000..54d66d00 --- /dev/null +++ b/src/components/library/molecules/AudioCard/AudioCard.module.scss @@ -0,0 +1,57 @@ +.row { + display: inline-flex; + flex-direction: row; + align-items: stretch; + gap: 10px; +} + +.card { + position: relative; + width: 190px; + height: 190px; + flex-shrink: 0; + cursor: pointer; + background: transparent; + border: none; + padding: 0; + outline: none; + + &:focus-visible { + box-shadow: 0 0 0 2px #017ccc; + border-radius: 2px; + } +} + +.cover { + position: absolute; + inset: 0; + overflow: hidden; +} + +.coverImage { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} + +.coverPlaceholder { + width: 100%; + height: 100%; + background: var(--white); +} + +.tags { + display: flex; + flex-direction: column; + gap: 5px; + width: 12px; + padding-top: 4px; +} + +.tagDot { + width: 12px; + height: 12px; + border-radius: 2px; + display: block; +} diff --git a/src/components/library/molecules/AudioCard/AudioCard.tsx b/src/components/library/molecules/AudioCard/AudioCard.tsx new file mode 100644 index 00000000..b349abc2 --- /dev/null +++ b/src/components/library/molecules/AudioCard/AudioCard.tsx @@ -0,0 +1,59 @@ +import React, { JSX } from 'react'; +import Image from 'next/image'; +import classNames from 'classnames'; + +import { resolveStrapiUrl } from '@/utils/resolveStrapiUrl'; + +import type { AudioCardProps } from './AudioCard.types'; + +import styles from './AudioCard.module.scss'; + +export function AudioCard({ object, onClick, className }: AudioCardProps): JSX.Element { + const { attributes } = object; + const coverUrl = resolveStrapiUrl(attributes.coverImage?.data?.attributes.url); + const tags = attributes.tags?.data ?? []; + const title = attributes.title; + + const handleActivate = () => onClick?.(object); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleActivate(); + } + }; + + return ( +
+
+
+ {coverUrl ? ( + {title} + ) : ( +
+ )} +
+
+ + {tags.length > 0 && ( +
+ {tags.map((tag) => ( + + ))} +
+ )} +
+ ); +} diff --git a/src/components/library/molecules/AudioCard/AudioCard.types.ts b/src/components/library/molecules/AudioCard/AudioCard.types.ts new file mode 100644 index 00000000..dfe1e00d --- /dev/null +++ b/src/components/library/molecules/AudioCard/AudioCard.types.ts @@ -0,0 +1,7 @@ +import type { IObject } from '@/types/object'; + +export interface AudioCardProps { + object: IObject; + onClick?: (object: IObject) => void; + className?: string; +} diff --git a/src/components/library/molecules/AudioCard/index.tsx b/src/components/library/molecules/AudioCard/index.tsx new file mode 100644 index 00000000..dede84f8 --- /dev/null +++ b/src/components/library/molecules/AudioCard/index.tsx @@ -0,0 +1,2 @@ +export * from './AudioCard'; +export * from './AudioCard.types'; diff --git a/src/components/library/molecules/BookCard/BookCard.module.scss b/src/components/library/molecules/BookCard/BookCard.module.scss new file mode 100644 index 00000000..1ca4a506 --- /dev/null +++ b/src/components/library/molecules/BookCard/BookCard.module.scss @@ -0,0 +1,77 @@ +.row { + display: inline-flex; + flex-direction: row; + align-items: stretch; + gap: 10px; +} + +.card { + position: relative; + width: 180px; + height: 208px; + flex-shrink: 0; + cursor: pointer; + background: transparent; + border: none; + padding: 0; + outline: none; + + &:focus-visible { + box-shadow: 0 0 0 2px var(--focus-ring); + border-radius: 2px; + } +} + +.cover { + position: absolute; + top: 0; + left: 33px; + width: 146px; + height: 206px; + border-radius: 1.44px; + overflow: hidden; +} + +.coverImage { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} + +.coverPlaceholder { + width: 100%; + height: 100%; + background: var(--gradient-book-placeholder); +} + +.alpha { + position: absolute; + inset: 0; + background: var(--surface-overlay); + pointer-events: none; +} + +.shadow { + position: absolute; + top: 0; + left: 0; + width: 180px; + height: 208px; + pointer-events: none; +} + +.tags { + display: flex; + flex-direction: column; + gap: 5px; + width: 12px; + padding-top: 4px; +} + +.tagDot { + width: 12px; + height: 12px; + border-radius: 2px; + display: block; +} diff --git a/src/components/library/molecules/BookCard/BookCard.tsx b/src/components/library/molecules/BookCard/BookCard.tsx new file mode 100644 index 00000000..35405890 --- /dev/null +++ b/src/components/library/molecules/BookCard/BookCard.tsx @@ -0,0 +1,68 @@ +import React, { JSX } from 'react'; +import Image from 'next/image'; +import classNames from 'classnames'; + +import { BookShadowIcon } from '@/assets/svg'; +import { resolveStrapiUrl } from '@/utils/resolveStrapiUrl'; + +import type { BookCardProps } from './BookCard.types'; + +import styles from './BookCard.module.scss'; + +export function BookCard({ object, onClick, className }: BookCardProps): JSX.Element { + const { attributes } = object; + const coverUrl = resolveStrapiUrl(attributes.coverImage?.data?.attributes.url); + const tags = attributes.tags?.data ?? []; + const title = attributes.title; + + const handleActivate = () => onClick?.(object); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleActivate(); + } + }; + + return ( +
+
+
+ {coverUrl ? ( + {attributes.title} + ) : ( +
+ )} +
+
+ +
+ + {tags.length > 0 && ( +
+ {tags.map((tag) => ( + + ))} +
+ )} +
+ ); +} diff --git a/src/components/library/molecules/BookCard/BookCard.types.ts b/src/components/library/molecules/BookCard/BookCard.types.ts new file mode 100644 index 00000000..920010e3 --- /dev/null +++ b/src/components/library/molecules/BookCard/BookCard.types.ts @@ -0,0 +1,7 @@ +import type { IObject } from '@/types/object'; + +export interface BookCardProps { + object: IObject; + onClick?: (object: IObject) => void; + className?: string; +} diff --git a/src/components/library/molecules/BookCard/index.tsx b/src/components/library/molecules/BookCard/index.tsx new file mode 100644 index 00000000..0fac764b --- /dev/null +++ b/src/components/library/molecules/BookCard/index.tsx @@ -0,0 +1,2 @@ +export * from './BookCard'; +export * from './BookCard.types'; diff --git a/src/components/library/molecules/Button/Button.module.scss b/src/components/library/molecules/Button/Button.module.scss new file mode 100644 index 00000000..15c06118 --- /dev/null +++ b/src/components/library/molecules/Button/Button.module.scss @@ -0,0 +1,73 @@ +.button { + gap: 8px; + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + border-radius: 10px; + transition: all 0.3s ease-in; + @extend %text-base-semibold; + + &:disabled { + cursor: default; + opacity: 0.5; + } + + &.default { + padding: 2px 8px; + } + + &.wide { + width: 100%; + text-align: center; + padding: 2px 16px; + } + + &.primary { + color: var(--white); + background: var(--brown); + box-shadow: 0px 4px 6px 0px var(--black-transparent-100); + + &:hover:not(:disabled) { + background: var(--brown-100); + } + } + + &.secondary { + color: var(--brown); + background: var(--white); + border: 1px solid var(--beige); + box-shadow: 0px 4px 4px 0px var(--black-transparent-200); + + &:hover:not(:disabled) { + background: var(--white-100); + border: 1px solid var(--beige); + color: var(--brown); + } + } + + &.warning { + background: var(--red-500); + color: var(--white); + box-shadow: 0px 4px 6px 0px var(--black-transparent-100); + + &:hover:not(:disabled) { + background: var(--red-700); + } + } + + &.outlined { + background: var(--beige); + color: var(--brown); + } + + &.text { + background: transparent; + color: var(--white); + + &:hover:not(:disabled) { + box-shadow: 3px 4px 10px 0px var(--black-transparent-200); + } + } +} diff --git a/src/components/library/molecules/Button/Button.tsx b/src/components/library/molecules/Button/Button.tsx new file mode 100644 index 00000000..99ba129f --- /dev/null +++ b/src/components/library/molecules/Button/Button.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import classNames from 'classnames'; + +import { ButtonProps, ButtonSize, ButtonType, IconPosition } from './Button.types'; + +import { TagType, Text, TypographyVariant } from '@/components/atoms/Text'; + +import styles from './Button.module.scss'; + +export const Button: React.FC = (props) => { + const { + size = ButtonSize.Default, + type = ButtonType.Primary, + Icon, + label, + disabled, + ariaLabel, + className, + labelClassName, + buttonType = 'button', + iconPosition = IconPosition.Left, + onClick, + } = props; + + return ( + + ); +}; + +export default Button; diff --git a/src/components/library/molecules/Button/Button.types.ts b/src/components/library/molecules/Button/Button.types.ts new file mode 100644 index 00000000..9d18a3a2 --- /dev/null +++ b/src/components/library/molecules/Button/Button.types.ts @@ -0,0 +1,30 @@ +export enum ButtonType { + Primary = 'primary', + Secondary = 'secondary', + Warning = 'warning', + Outlined = 'outlined', + Text = 'text', +} + +export enum ButtonSize { + Default = 'default', + Wide = 'wide', +} + +export enum IconPosition { + Left = 'left', + Right = 'right', +} +export interface ButtonProps { + size?: ButtonSize; + type?: ButtonType; + label?: string; + Icon?: React.ReactNode; + disabled?: boolean; + ariaLabel: string; + className?: string; + buttonType?: 'button' | 'submit' | 'reset'; + iconPosition?: IconPosition; + labelClassName?: string; + onClick?: (event: React.MouseEvent) => void; +} diff --git a/src/components/library/molecules/Button/index.ts b/src/components/library/molecules/Button/index.ts new file mode 100644 index 00000000..8b371468 --- /dev/null +++ b/src/components/library/molecules/Button/index.ts @@ -0,0 +1,2 @@ +export * from './Button'; +export * from './Button.types'; diff --git a/src/components/library/molecules/ConfirmationModal/ConfirmationModal.module.scss b/src/components/library/molecules/ConfirmationModal/ConfirmationModal.module.scss new file mode 100644 index 00000000..b90a8655 --- /dev/null +++ b/src/components/library/molecules/ConfirmationModal/ConfirmationModal.module.scss @@ -0,0 +1,44 @@ +.modal { + width: 100% !important; + max-width: 391px !important; + background-color: var(--white) !important; + + .wrapper { + padding: 32px; + border-top: 1px solid var(--brown-border); + + .content { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 16px; + + .icon { + color: var(--gray-darkest); + } + + .title { + color: var(--gray-darkest); + } + + .text { + color: var(--black-transparent-300); + max-width: 100%; + } + } + + .footer { + gap: 16px; + display: flex; + align-items: center; + justify-content: center; + padding: 24px 32px 0; + + .cancelButton, + .actionButton { + width: auto !important; + } + } + } +} diff --git a/src/components/library/molecules/ConfirmationModal/ConfirmationModal.tsx b/src/components/library/molecules/ConfirmationModal/ConfirmationModal.tsx new file mode 100644 index 00000000..bfbafdaf --- /dev/null +++ b/src/components/library/molecules/ConfirmationModal/ConfirmationModal.tsx @@ -0,0 +1,67 @@ +'use client'; + +import React from 'react'; + +import { Modal, useModalClose } from '@/components/molecules/Modal'; +import { Text, TypographyVariant } from '@/components/atoms/Text'; +import { Button, ButtonSize, ButtonType } from '@/components/molecules/Button'; + +import type { ConfirmationModalProps } from './ConfirmationModal.types'; + +import styles from './ConfirmationModal.module.scss'; +import { CheckIcon, ErrorIcon } from '@/assets/svg'; + +export function ConfirmationModal(props: ConfirmationModalProps) { + const { + variant = 'delete', + text, + title, + isLoading = false, + actionButtonType = ButtonType.Primary, + actionButtonLabel = 'Delete', + onClose, + onConfirm, + } = props; + + const isSuccess = variant === 'success'; + const { closeRef, close } = useModalClose(onClose); + + return ( + +
+
+
{isSuccess ? : }
+ + {title} + + + {text} + +
+ +
+ {!isSuccess && ( +
+
+
+ ); +} diff --git a/src/components/library/molecules/ConfirmationModal/ConfirmationModal.types.ts b/src/components/library/molecules/ConfirmationModal/ConfirmationModal.types.ts new file mode 100644 index 00000000..a09c96b9 --- /dev/null +++ b/src/components/library/molecules/ConfirmationModal/ConfirmationModal.types.ts @@ -0,0 +1,17 @@ +import { ReactNode } from 'react'; +import { IconName } from '@/components/atoms/Icon'; +import { ButtonType } from '../Button'; + +export type ConfirmationModalVariant = 'success' | 'delete'; + +export interface ConfirmationModalProps { + variant?: ConfirmationModalVariant; + icon?: IconName | ReactNode; + title: string; + text: string; + actionButtonLabel?: string; + actionButtonType?: ButtonType; + onClose: () => void; + onConfirm: () => void; + isLoading?: boolean; +} diff --git a/src/components/library/molecules/ConfirmationModal/index.tsx b/src/components/library/molecules/ConfirmationModal/index.tsx new file mode 100644 index 00000000..c07b3bbd --- /dev/null +++ b/src/components/library/molecules/ConfirmationModal/index.tsx @@ -0,0 +1,2 @@ +export { ConfirmationModal } from './ConfirmationModal'; +export type { ConfirmationModalProps, ConfirmationModalVariant } from './ConfirmationModal.types'; diff --git a/src/components/library/molecules/CreateTagModal/CreateTagModal.module.scss b/src/components/library/molecules/CreateTagModal/CreateTagModal.module.scss new file mode 100644 index 00000000..ab3c8621 --- /dev/null +++ b/src/components/library/molecules/CreateTagModal/CreateTagModal.module.scss @@ -0,0 +1,175 @@ +.modal { + width: 100% !important; + max-width: 518px !important; + background: var(--white) !important; + overflow: hidden; + transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1); + will-change: height; + + form { + display: block; + transition: height 0.4s cubic-bezier(0.4, 0, 0.2, 1); + overflow: hidden; + } +} + +.wrapper { + padding: 32px; + border-top: 1px solid var(--brown-border); + border-bottom: 1px solid var(--brown-border); + max-height: calc(100vh - 250px); + overflow-y: auto; + overflow-x: hidden; + transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1); + + &::-webkit-scrollbar { + display: none; + } + + .labelWrapper { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; + + .label { + margin-bottom: 0; + } + } + + .label { + margin-bottom: 12px; + color: var(--black-transparent-300); + } + + .field { + margin-bottom: 32px; + transition: + opacity 0.4s ease-in-out, + transform 0.3s ease-in-out; + animation: fadeInUp 0.4s ease-in-out; + + .error { + color: var(--red-600); + font-size: 12px; + margin-top: 4px; + margin-bottom: 0; + } + + .color { + gap: 22px 12px; + display: flex; + flex-wrap: wrap; + + .blok { + display: flex; + + div { + width: 26px; + height: 26px; + margin-left: -1px; + cursor: pointer; + transition: all 0.2s ease-in; + + &.active { + outline: 3px solid #616469; + z-index: 1; + position: relative; + } + } + } + } + + .preview { + display: flex; + justify-content: center; + background: var(--white-warm); + border-radius: 4px; + padding: 12px 16px; + + .tag { + min-height: 22px; + min-width: 114px; + display: inline-flex; + align-items: center; + justify-content: center; + text-align: center; + } + } + + &.delete { + display: flex; + align-items: center; + justify-content: space-between; + border: 1px solid var(--gray-100); + border-radius: 4px; + padding: 8px 12px; + + .label { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 0; + color: var(--brown); + } + + .arrow { + width: 14px; + height: 14px; + + path { + fill: var(--gray-darkest); + } + } + } + } + + .noTagFound { + display: block; + animation: fadeInUp 0.4s ease-in-out; + transition: + opacity 0.4s ease-in-out, + transform 0.4s ease-in-out; + } + + .tagsList { + transition: opacity 0.4s ease-in-out; + + .label { + margin-bottom: 12px; + } + } + + .tags { + background: var(--white-warm); + padding: 16px; + border-radius: 4px; + display: flex; + flex-wrap: wrap; + gap: 12px; + transition: opacity 0.4s ease-in-out; + } +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.footer { + gap: 16px; + display: flex; + align-items: center; + justify-content: flex-end; + padding: 24px 32px; + + button { + width: auto !important; + } +} diff --git a/src/components/library/molecules/CreateTagModal/CreateTagModal.tsx b/src/components/library/molecules/CreateTagModal/CreateTagModal.tsx new file mode 100644 index 00000000..7ae4b6b1 --- /dev/null +++ b/src/components/library/molecules/CreateTagModal/CreateTagModal.tsx @@ -0,0 +1,311 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { z } from 'zod'; +import classNames from 'classnames'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; + +import { Tag } from '@/components/molecules/Tag'; +import { Modal, useModalClose } from '@/components/molecules/Modal'; +import { Input } from '@/components/molecules/Input'; +import { IconName } from '@/components/atoms/Icon'; +import { Textarea } from '@/components/molecules/Textarea'; +import { ConfirmationModal } from '@/components/molecules/ConfirmationModal'; +import { Button, ButtonSize, ButtonType } from '@/components/molecules/Button'; +import { Text, TypographyVariant } from '@/components/atoms/Text'; + +import { createTagSchema } from '@/utils/schema/createTagSchema'; + +import { tagColors } from '@/constants/tags'; +import type { CreateTagModalProps, CreateTagFormData } from './CreateTagModal.types'; + +import { ArrowIcon, DeleteIcon, InfoIcon } from '@/assets/svg'; + +import styles from './CreateTagModal.module.scss'; + +export function CreateTagModal(props: CreateTagModalProps) { + const { onClose, onSubmit, isEdit = false, activeTag, tags = [], onDelete, onTagSelect } = props; + const { closeRef, close } = useModalClose(onClose); + const defaultColor = tagColors[0][0]; + const isSelectTag = isEdit && !activeTag; + + const [isDeleting, setIsDeleting] = useState(false); + const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false); + const [showCreateSuccessConfirmation, setShowCreateSuccessConfirmation] = useState(false); + + const { + register, + handleSubmit, + watch, + setValue, + formState: { errors, isSubmitting }, + } = useForm>({ + resolver: zodResolver(createTagSchema), + mode: 'onChange', + defaultValues: { + name: '', + description: '', + color: defaultColor, + }, + }); + + const tagName = watch('name'); + const activeColor = watch('color'); + + const handleColorSelect = (color: string) => { + setValue('color', color, { shouldValidate: true }); + }; + + const onSubmitForm = async (data: CreateTagFormData) => { + if (onSubmit) { + await onSubmit(data); + if (!isEdit) { + setShowCreateSuccessConfirmation(true); + } + } + }; + + const handleDeleteClick = () => { + setShowDeleteConfirmation(true); + }; + + const handleDeleteConfirm = async () => { + if (!onDelete) return; + + setIsDeleting(true); + try { + await onDelete(); + + setShowDeleteConfirmation(false); + onClose(); + } catch (error) { + console.error('Failed to delete tag:', error); + setIsDeleting(false); + } + }; + + useEffect(() => { + if (activeTag) { + setValue('name', activeTag.name || ''); + setValue('description', activeTag.description || ''); + setValue('color', activeTag.color || defaultColor); + } + }, [activeTag, defaultColor, setValue]); + + return ( + <> + {!showCreateSuccessConfirmation && ( + {} : onClose} + closeRef={closeRef} + > +
+
+ {isSelectTag ? ( +
+ {tags.length > 0 && ( +
+ + Select tag : + +
+ {tags.map(({ attributes, id }) => ( + { + if (onTagSelect) { + onTagSelect({ ...attributes, id }); + } + }} + /> + ))} +
+
+ )} +
+ ) : ( + <> +
+ + Tag name + + + {errors.name &&

{errors.name.message}

} +
+ +
+ + Description + +