From e32fced0360fc259cb1c5617f8c05685c6cc9d38 Mon Sep 17 00:00:00 2001 From: ahmedriad1 Date: Wed, 27 Mar 2024 09:33:38 +0200 Subject: [PATCH 1/7] update theme colors & add mobile menu & update global search --- package.json | 1 + pnpm-lock.yaml | 399 +++++++++++++++--- .../author/[authorSlug]/page.tsx | 6 +- .../century/[centurySlug]/page.tsx | 2 +- .../(entityPages)/genre/[genreSlug]/page.tsx | 2 +- .../region/[regionSlug]/page.tsx | 2 +- .../(rootEntityPages)/authors/page.tsx | 2 +- .../(rootEntityPages)/root-entity-page.tsx | 4 +- .../[locale]/(rootEntityPages)/texts/page.tsx | 10 +- .../_components/sidebar/collapsed-sidebar.tsx | 2 +- .../_components/sidebar/sidebar-resizer.tsx | 2 +- .../[bookId]/_components/sidebar/wrapper.tsx | 2 +- src/app/_components/navbar/index.tsx | 60 +-- src/app/_components/navbar/links.ts | 63 +++ .../_components/navbar/locale-switcher.tsx | 2 +- src/app/_components/navbar/mobile-menu.tsx | 19 + .../navbar/mobile-navigation-menu.tsx | 106 +++++ .../_components/navbar/navigation-menu.tsx | 75 +--- src/app/_components/navbar/search.tsx | 97 +++-- src/app/_components/navbar/theme-toggle.tsx | 4 +- src/components/author-search-result.tsx | 2 +- src/components/authors-filter/client.tsx | 43 +- src/components/authors-filter/index.tsx | 2 +- src/components/book-search-result/index.tsx | 6 +- .../book-search-result/info-dialog.tsx | 2 +- src/components/genres-filter/client.tsx | 40 +- src/components/regions-filter/client.tsx | 41 +- .../search-results/filter-container.tsx | 65 ++- src/components/search-results/search-bar.tsx | 2 +- src/components/ui/checkbox/index.tsx | 2 +- src/components/ui/dotted-list/index.tsx | 11 +- src/lib/locale/server.ts | 2 - src/lib/search.ts | 203 --------- src/server/services/books.ts | 2 +- src/server/services/regions.ts | 2 +- src/server/services/years.ts | 2 +- src/server/typesense/author.ts | 61 +++ src/server/typesense/book.ts | 105 +++++ src/server/typesense/config.ts | 72 ++++ src/server/typesense/genre.ts | 25 +- src/server/typesense/global.ts | 31 ++ src/server/typesense/region.ts | 26 +- src/server/typesense/utils.ts | 22 + src/styles/globals.css | 20 +- src/types/book.ts | 2 +- src/types/global-search-document.ts | 15 + 46 files changed, 1127 insertions(+), 539 deletions(-) create mode 100644 src/app/_components/navbar/links.ts create mode 100644 src/app/_components/navbar/mobile-menu.tsx create mode 100644 src/app/_components/navbar/mobile-navigation-menu.tsx delete mode 100644 src/lib/search.ts create mode 100644 src/server/typesense/author.ts create mode 100644 src/server/typesense/book.ts create mode 100644 src/server/typesense/config.ts create mode 100644 src/server/typesense/global.ts create mode 100644 src/types/global-search-document.ts diff --git a/package.json b/package.json index 2551878e..56cbe374 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.7", + "@radix-ui/themes": "^3.0.0", "@t3-oss/env-nextjs": "^0.7.1", "@tanstack/react-query": "^5.22.2", "@tanstack/react-virtual": "^3.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1aa2b58d..39813706 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,6 +65,9 @@ dependencies: '@radix-ui/react-tooltip': specifier: ^1.0.7 version: 1.0.7(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/themes': + specifier: ^3.0.0 + version: 3.0.0(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) '@t3-oss/env-nextjs': specifier: ^0.7.1 version: 0.7.3(typescript@5.3.3)(zod@3.22.4) @@ -97,16 +100,16 @@ dependencies: version: 0.356.0(react@18.2.0) next: specifier: ^14.0.4 - version: 14.1.0(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0) + version: 14.0.4(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0) next-intl: specifier: ^3.9.0 - version: 3.9.0(next@14.1.0)(react@18.2.0) + version: 3.9.0(next@14.0.4)(react@18.2.0) next-themes: specifier: ^0.2.1 - version: 0.2.1(next@14.1.0)(react-dom@18.2.0)(react@18.2.0) + version: 0.2.1(next@14.0.4)(react-dom@18.2.0)(react@18.2.0) next-typesafe-url: specifier: ^4.0.5 - version: 4.0.5(next@14.1.0)(react@18.2.0)(zod@3.22.4) + version: 4.0.5(next@14.0.4)(react@18.2.0)(zod@3.22.4) nprogress: specifier: ^0.2.0 version: 0.2.0 @@ -174,7 +177,7 @@ devDependencies: version: 7.6.17(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) '@storybook/nextjs': specifier: ^7.6.17 - version: 7.6.17(@swc/core@1.4.2)(esbuild@0.18.20)(next@14.1.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)(webpack@5.90.3) + version: 7.6.17(@swc/core@1.4.2)(esbuild@0.18.20)(next@14.0.4)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)(webpack@5.90.3) '@storybook/react': specifier: ^7.6.17 version: 7.6.17(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) @@ -2329,8 +2332,8 @@ packages: tar-fs: 2.1.1 dev: true - /@next/env@14.1.0: - resolution: {integrity: sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw==} + /@next/env@14.0.4: + resolution: {integrity: sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ==} /@next/eslint-plugin-next@14.1.0: resolution: {integrity: sha512-x4FavbNEeXx/baD/zC/SdrvkjSby8nBn8KcCREqk6UuwvwoAPZmaV8TFCAuo/cpovBRTIY67mHhe86MQQm/68Q==} @@ -2338,72 +2341,72 @@ packages: glob: 10.3.10 dev: true - /@next/swc-darwin-arm64@14.1.0: - resolution: {integrity: sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ==} + /@next/swc-darwin-arm64@14.0.4: + resolution: {integrity: sha512-mF05E/5uPthWzyYDyptcwHptucf/jj09i2SXBPwNzbgBNc+XnwzrL0U6BmPjQeOL+FiB+iG1gwBeq7mlDjSRPg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] requiresBuild: true optional: true - /@next/swc-darwin-x64@14.1.0: - resolution: {integrity: sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g==} + /@next/swc-darwin-x64@14.0.4: + resolution: {integrity: sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] requiresBuild: true optional: true - /@next/swc-linux-arm64-gnu@14.1.0: - resolution: {integrity: sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ==} + /@next/swc-linux-arm64-gnu@14.0.4: + resolution: {integrity: sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] requiresBuild: true optional: true - /@next/swc-linux-arm64-musl@14.1.0: - resolution: {integrity: sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g==} + /@next/swc-linux-arm64-musl@14.0.4: + resolution: {integrity: sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] requiresBuild: true optional: true - /@next/swc-linux-x64-gnu@14.1.0: - resolution: {integrity: sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ==} + /@next/swc-linux-x64-gnu@14.0.4: + resolution: {integrity: sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A==} engines: {node: '>= 10'} cpu: [x64] os: [linux] requiresBuild: true optional: true - /@next/swc-linux-x64-musl@14.1.0: - resolution: {integrity: sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg==} + /@next/swc-linux-x64-musl@14.0.4: + resolution: {integrity: sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] requiresBuild: true optional: true - /@next/swc-win32-arm64-msvc@14.1.0: - resolution: {integrity: sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ==} + /@next/swc-win32-arm64-msvc@14.0.4: + resolution: {integrity: sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] requiresBuild: true optional: true - /@next/swc-win32-ia32-msvc@14.1.0: - resolution: {integrity: sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw==} + /@next/swc-win32-ia32-msvc@14.0.4: + resolution: {integrity: sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] requiresBuild: true optional: true - /@next/swc-win32-x64-msvc@14.1.0: - resolution: {integrity: sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg==} + /@next/swc-win32-x64-msvc@14.0.4: + resolution: {integrity: sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -2487,6 +2490,10 @@ packages: webpack: 5.90.3(@swc/core@1.4.2)(esbuild@0.18.20) dev: true + /@radix-ui/colors@3.0.0: + resolution: {integrity: sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==} + dev: false + /@radix-ui/number@1.0.1: resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} dependencies: @@ -2503,6 +2510,27 @@ packages: dependencies: '@babel/runtime': 7.24.0 + /@radix-ui/react-accessible-icon@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-duVGKeWPSUILr/MdlPxV+GeULTc2rS1aihGdQ3N2qCUPMgxYLxvAsHJM3mCVLF8d5eK+ympmB22mb1F3a5biNw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.0 + '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-accordion@1.1.2(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-fDG7jcoNKVjSK6yfmuAs0EnPDro0WMXIhMtXdTBWqEioVW206ku+4Lw07e+13lUkFkpoEQ2PdeMIAGpdqEAmDg==} peerDependencies: @@ -2532,6 +2560,32 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-alert-dialog@1.0.5(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-OrVIOcZL0tl6xibeuGt5/+UxoT2N27KCFOPjFyfXMnchxSHZ/OW7cCX2nGlIYJrbHK/fczPcFzAwvNBB6XBNMA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.0 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.57)(react@18.2.0) + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==} peerDependencies: @@ -2552,6 +2606,51 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + /@radix-ui/react-aspect-ratio@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-fXR5kbMan9oQqMuacfzlGG/SQMcmMlZ4wrvpckv8SgUulD0MMpspxJrxg/Gp/ISV3JfV1AeSWTYK9GvxA4ySwA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.0 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@radix-ui/react-avatar@1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.0 + '@radix-ui/react-context': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-checkbox@1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==} peerDependencies: @@ -2653,6 +2752,32 @@ packages: '@types/react': 18.2.57 react: 18.2.0 + /@radix-ui/react-context-menu@2.1.5(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-R5XaDj06Xul1KGb+WP8qiOh7tKJNz2durpLBXAGZjSVtctcRFCuEvy2gtMwRJGePwQQE5nV77gs4FwRi8T+r2g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.0 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-context': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-menu': 2.0.6(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-context@1.0.0(react@18.2.0): resolution: {integrity: sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==} peerDependencies: @@ -2924,6 +3049,32 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-form@0.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-kgE+Z/haV6fxE5WqIXj05KkaXa3OkZASoTDy25yX2EIp/x0c54rOH/vFr5nOZTg7n7T1z8bSyXmiVIFP9bbhPQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.0 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-label': 2.0.2(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-hover-card@1.0.7(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-OcUN2FU0YpmajD/qkph3XzMcK/NmSk9hGWnjV68p6QiZMgILugusgQwnLSDs3oFSJYGKf3Y49zgFedhGh04k9A==} peerDependencies: @@ -3294,6 +3445,58 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + /@radix-ui/react-progress@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-5G6Om/tYSxjSeEdrb1VfKkfZfn/1IlPWd731h2RfPuSbIfNUgfqAwbKfJCg/PP6nuUCTrYzalwHSpSinoWoCag==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.0 + '@radix-ui/react-context': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@radix-ui/react-radio-group@1.1.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.0 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==} peerDependencies: @@ -3508,6 +3711,33 @@ packages: '@types/react': 18.2.57 react: 18.2.0 + /@radix-ui/react-switch@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.0 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-tabs@1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==} peerDependencies: @@ -3561,7 +3791,6 @@ packages: '@types/react-dom': 18.2.19 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - dev: true /@radix-ui/react-toggle@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Pkqg3+Bc98ftZGsl60CLANXQBBQ4W3mTFS9EJvNxKMZ7magklKV69/id1mlAlOFDDfHvlCms0fx8fA4CMKDJHg==} @@ -3584,7 +3813,6 @@ packages: '@types/react-dom': 18.2.19 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - dev: true /@radix-ui/react-toolbar@1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-tBgmM/O7a07xbaEkYJWYTXkIdU/1pW4/KZORR43toC/4XWyBCURK0ei9kMUdp+gTPPKBgYLxXmRSH1EVcIDp8Q==} @@ -3803,6 +4031,60 @@ packages: dependencies: '@babel/runtime': 7.24.0 + /@radix-ui/themes@3.0.0(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-PHRmrs9EQaO4h/gmRao1m/iZHnFcdB0L4u22OTU7EUeDDVBqzICrUhY0U+xihWTbiReVlt1b9AgM7CMiGXaJvw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/colors': 3.0.0 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-accessible-icon': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-alert-dialog': 1.0.5(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-aspect-ratio': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-avatar': 1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-checkbox': 1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-context-menu': 2.1.5(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-dropdown-menu': 2.0.6(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-form': 0.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-hover-card': 1.0.7(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-navigation-menu': 1.1.4(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-popover': 1.0.7(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-progress': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-radio-group': 1.1.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-scroll-area': 1.0.5(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-select': 2.0.0(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slider': 1.1.2(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-switch': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-tabs': 1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-toggle-group': 1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-tooltip': 1.0.7(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 + classnames: 2.5.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll-bar: 2.3.4(@types/react@18.2.57)(react@18.2.0) + dev: false + /@react-email/render@0.0.12: resolution: {integrity: sha512-S8WRv/PqECEi6x0QJBj0asnAb5GFtJaHlnByxLETLkgJjc76cxMYDH4r9wdbuJ4sjkcbpwP3LPnVzwS+aIjT7g==} engines: {node: '>=18.0.0'} @@ -4435,7 +4717,7 @@ packages: resolution: {integrity: sha512-TXJJd5RAKakWx4BtpwvSNdgTDkKM6RkXU8GK34S/LhidQ5Pjz3wcnqb0TxEkfhK/ztbP8nKHqXFwLfa2CYkvQw==} dev: true - /@storybook/nextjs@7.6.17(@swc/core@1.4.2)(esbuild@0.18.20)(next@14.1.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)(webpack@5.90.3): + /@storybook/nextjs@7.6.17(@swc/core@1.4.2)(esbuild@0.18.20)(next@14.0.4)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)(webpack@5.90.3): resolution: {integrity: sha512-bD9x6HzH/fxiFnghOQfDM60tNUNxFNVVCZi6OvTRxVVz/5xdqbVnYVOuaJeUSLuUnGs7ALYfx8+2OTJQ9NrwRA==} engines: {node: '>=16.0.0'} peerDependencies: @@ -4481,7 +4763,7 @@ packages: fs-extra: 11.2.0 image-size: 1.1.1 loader-utils: 3.2.1 - next: 14.1.0(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0) + next: 14.0.4(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0) node-polyfill-webpack-plugin: 2.0.1(webpack@5.90.3) pnp-webpack-plugin: 1.7.0(typescript@5.3.3) postcss: 8.4.35 @@ -6525,6 +6807,10 @@ packages: clsx: 2.0.0 dev: false + /classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + dev: false + /clean-css@5.3.3: resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} engines: {node: '>= 10.0'} @@ -8642,7 +8928,6 @@ packages: /glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - dev: true /glob@10.3.10: resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} @@ -10135,7 +10420,7 @@ packages: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} dev: true - /next-intl@3.9.0(next@14.1.0)(react@18.2.0): + /next-intl@3.9.0(next@14.0.4)(react@18.2.0): resolution: {integrity: sha512-ZqaEipDR5ycwknZ7hMtT2qFrj+y0xrQy9IsvDlGdaXJeh5ld+PBtVrN+tmlYrmJ679vkE8c3yZ2HBr55jbnx1w==} peerDependencies: next: ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 @@ -10143,19 +10428,19 @@ packages: dependencies: '@formatjs/intl-localematcher': 0.2.32 negotiator: 0.6.3 - next: 14.1.0(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0) + next: 14.0.4(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 use-intl: 3.9.5(react@18.2.0) dev: false - /next-themes@0.2.1(next@14.1.0)(react-dom@18.2.0)(react@18.2.0): + /next-themes@0.2.1(next@14.0.4)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} peerDependencies: next: '*' react: '*' react-dom: '*' dependencies: - next: 14.1.0(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0) + next: 14.0.4(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -10164,7 +10449,7 @@ packages: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} dev: true - /next-typesafe-url@4.0.5(next@14.1.0)(react@18.2.0)(zod@3.22.4): + /next-typesafe-url@4.0.5(next@14.0.4)(react@18.2.0)(zod@3.22.4): resolution: {integrity: sha512-OGym0vt5rFnDknPnLMsdOewVjlS73/pmiahpru58korslJGQdRxjg8kDzhE2UN8/pRuk4nouqnaNfl2s2Ji4Zg==} hasBin: true peerDependencies: @@ -10174,13 +10459,13 @@ packages: dependencies: chokidar: 3.6.0 meow: 9.0.0 - next: 14.1.0(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0) + next: 14.0.4(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 zod: 3.22.4 dev: false - /next@14.1.0(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==} + /next@14.0.4(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA==} engines: {node: '>=18.17.0'} hasBin: true peerDependencies: @@ -10194,7 +10479,7 @@ packages: sass: optional: true dependencies: - '@next/env': 14.1.0 + '@next/env': 14.0.4 '@swc/helpers': 0.5.2 busboy: 1.6.0 caniuse-lite: 1.0.30001589 @@ -10203,16 +10488,17 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) styled-jsx: 5.1.1(@babel/core@7.23.9)(react@18.2.0) + watchpack: 2.4.0 optionalDependencies: - '@next/swc-darwin-arm64': 14.1.0 - '@next/swc-darwin-x64': 14.1.0 - '@next/swc-linux-arm64-gnu': 14.1.0 - '@next/swc-linux-arm64-musl': 14.1.0 - '@next/swc-linux-x64-gnu': 14.1.0 - '@next/swc-linux-x64-musl': 14.1.0 - '@next/swc-win32-arm64-msvc': 14.1.0 - '@next/swc-win32-ia32-msvc': 14.1.0 - '@next/swc-win32-x64-msvc': 14.1.0 + '@next/swc-darwin-arm64': 14.0.4 + '@next/swc-darwin-x64': 14.0.4 + '@next/swc-linux-arm64-gnu': 14.0.4 + '@next/swc-linux-arm64-musl': 14.0.4 + '@next/swc-linux-x64-gnu': 14.0.4 + '@next/swc-linux-x64-musl': 14.0.4 + '@next/swc-win32-arm64-msvc': 14.0.4 + '@next/swc-win32-ia32-msvc': 14.0.4 + '@next/swc-win32-x64-msvc': 14.0.4 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -11328,6 +11614,22 @@ packages: engines: {node: '>=0.10.0'} dev: true + /react-remove-scroll-bar@2.3.4(@types/react@18.2.57)(react@18.2.0): + resolution: {integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.57 + react: 18.2.0 + react-style-singleton: 2.2.1(@types/react@18.2.57)(react@18.2.0) + tslib: 2.6.2 + dev: false + /react-remove-scroll-bar@2.3.5(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw==} engines: {node: '>=10'} @@ -13005,7 +13307,6 @@ packages: dependencies: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 - dev: true /wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} diff --git a/src/app/[locale]/(entityPages)/author/[authorSlug]/page.tsx b/src/app/[locale]/(entityPages)/author/[authorSlug]/page.tsx index 2060453c..c4209053 100644 --- a/src/app/[locale]/(entityPages)/author/[authorSlug]/page.tsx +++ b/src/app/[locale]/(entityPages)/author/[authorSlug]/page.tsx @@ -2,7 +2,7 @@ import BookSearchResult from "@/components/book-search-result"; import GenresFilter from "@/components/genres-filter"; import SearchResults from "@/components/search-results"; -import { searchBooks } from "@/lib/search"; +import { searchBooks } from "@/server/typesense/book"; import { findAuthorBySlug } from "@/server/services/authors"; import { notFound } from "next/navigation"; import { withParamValidation } from "next-typesafe-url/app/hoc"; @@ -13,10 +13,10 @@ import { ExpandibleList } from "@/components/ui/expandible-list"; import TruncatedText from "@/components/ui/truncated-text"; import { Button } from "@/components/ui/button"; import { Link } from "@/navigation"; -import { toTitleCase } from "@/lib/string"; +// import { toTitleCase } from "@/lib/string"; import DottedList from "@/components/ui/dotted-list"; import { getLocale, getTranslations } from "next-intl/server"; -import { AppLocale } from "~/i18n.config"; +import type { AppLocale } from "~/i18n.config"; type AuthorPageProps = InferPagePropsType; diff --git a/src/app/[locale]/(entityPages)/century/[centurySlug]/page.tsx b/src/app/[locale]/(entityPages)/century/[centurySlug]/page.tsx index 779944f8..71ae5f10 100644 --- a/src/app/[locale]/(entityPages)/century/[centurySlug]/page.tsx +++ b/src/app/[locale]/(entityPages)/century/[centurySlug]/page.tsx @@ -1,4 +1,4 @@ -import { searchBooks } from "@/lib/search"; +import { searchBooks } from "@/server/typesense/book"; import { notFound } from "next/navigation"; import { withParamValidation } from "next-typesafe-url/app/hoc"; import { Route, type RouteType } from "./routeType"; diff --git a/src/app/[locale]/(entityPages)/genre/[genreSlug]/page.tsx b/src/app/[locale]/(entityPages)/genre/[genreSlug]/page.tsx index ef0101b0..783631d7 100644 --- a/src/app/[locale]/(entityPages)/genre/[genreSlug]/page.tsx +++ b/src/app/[locale]/(entityPages)/genre/[genreSlug]/page.tsx @@ -1,6 +1,6 @@ import BookSearchResult from "@/components/book-search-result"; import SearchResults from "@/components/search-results"; -import { searchBooks } from "@/lib/search"; +import { searchBooks } from "@/server/typesense/book"; import { notFound } from "next/navigation"; import { withParamValidation } from "next-typesafe-url/app/hoc"; import { Route, type RouteType } from "./routeType"; diff --git a/src/app/[locale]/(entityPages)/region/[regionSlug]/page.tsx b/src/app/[locale]/(entityPages)/region/[regionSlug]/page.tsx index e9102fdf..e79b606d 100644 --- a/src/app/[locale]/(entityPages)/region/[regionSlug]/page.tsx +++ b/src/app/[locale]/(entityPages)/region/[regionSlug]/page.tsx @@ -1,5 +1,5 @@ /* eslint-disable react/jsx-key */ -import { searchBooks } from "@/lib/search"; +import { searchBooks } from "@/server/typesense/book"; import { notFound } from "next/navigation"; import { withParamValidation } from "next-typesafe-url/app/hoc"; import { Route, type RouteType } from "./routeType"; diff --git a/src/app/[locale]/(rootEntityPages)/authors/page.tsx b/src/app/[locale]/(rootEntityPages)/authors/page.tsx index bfb74e16..0db25b6b 100644 --- a/src/app/[locale]/(rootEntityPages)/authors/page.tsx +++ b/src/app/[locale]/(rootEntityPages)/authors/page.tsx @@ -4,13 +4,13 @@ import type { InferPagePropsType } from "next-typesafe-url"; import { withParamValidation } from "next-typesafe-url/app/hoc"; import dynamic from "next/dynamic"; import YearFilterSkeleton from "@/components/year-filter/skeleton"; -import { searchAuthors } from "@/lib/search"; import { gregorianYearToHijriYear } from "@/lib/date"; import AuthorSearchResult from "@/components/author-search-result"; import RegionsFilter from "@/components/regions-filter"; import { countAllAuthors } from "@/server/services/authors"; import RootEntityPage from "../root-entity-page"; import { getTranslations } from "next-intl/server"; +import { searchAuthors } from "@/server/typesense/author"; const YearFilter = dynamic(() => import("@/components/year-filter"), { ssr: false, diff --git a/src/app/[locale]/(rootEntityPages)/root-entity-page.tsx b/src/app/[locale]/(rootEntityPages)/root-entity-page.tsx index d250c37a..ac4a2228 100644 --- a/src/app/[locale]/(rootEntityPages)/root-entity-page.tsx +++ b/src/app/[locale]/(rootEntityPages)/root-entity-page.tsx @@ -15,7 +15,9 @@ export default function RootEntityPage({

{title}

{description && ( -

{description}

+

+ {description} +

)}
diff --git a/src/app/[locale]/(rootEntityPages)/texts/page.tsx b/src/app/[locale]/(rootEntityPages)/texts/page.tsx index 1387b551..71da1777 100644 --- a/src/app/[locale]/(rootEntityPages)/texts/page.tsx +++ b/src/app/[locale]/(rootEntityPages)/texts/page.tsx @@ -1,18 +1,24 @@ import BookSearchResult from "@/components/book-search-result"; import GenresFilter from "@/components/genres-filter"; import SearchResults from "@/components/search-results"; -import { searchBooks } from "@/lib/search"; +import { searchBooks } from "@/server/typesense/book"; import { withParamValidation } from "next-typesafe-url/app/hoc"; import { Route, type RouteType } from "./routeType"; import type { InferPagePropsType } from "next-typesafe-url"; import { booksSorts } from "@/lib/urls"; import RegionsFilter from "@/components/regions-filter"; import AuthorsFilter from "@/components/authors-filter"; -import YearFilter from "@/components/year-filter"; import { gregorianYearToHijriYear } from "@/lib/date"; import { countAllBooks } from "@/server/services/books"; import RootEntityPage from "../root-entity-page"; import { getTranslations } from "next-intl/server"; +import YearFilterSkeleton from "@/components/year-filter/skeleton"; +import dynamic from "next/dynamic"; + +const YearFilter = dynamic(() => import("@/components/year-filter"), { + ssr: false, + loading: () => , +}); type TextsPageProps = InferPagePropsType; diff --git a/src/app/[locale]/t/[bookId]/_components/sidebar/collapsed-sidebar.tsx b/src/app/[locale]/t/[bookId]/_components/sidebar/collapsed-sidebar.tsx index 7f0e3d33..eeebbb44 100644 --- a/src/app/[locale]/t/[bookId]/_components/sidebar/collapsed-sidebar.tsx +++ b/src/app/[locale]/t/[bookId]/_components/sidebar/collapsed-sidebar.tsx @@ -17,7 +17,7 @@ export default function CollapsedSidebar({ }; return ( -
+
diff --git a/src/app/[locale]/t/[bookId]/_components/sidebar/sidebar-resizer.tsx b/src/app/[locale]/t/[bookId]/_components/sidebar/sidebar-resizer.tsx index 832e84a0..762a43bd 100644 --- a/src/app/[locale]/t/[bookId]/_components/sidebar/sidebar-resizer.tsx +++ b/src/app/[locale]/t/[bookId]/_components/sidebar/sidebar-resizer.tsx @@ -48,7 +48,7 @@ export default function SidebarResizer({ return ( <> - + diff --git a/src/app/_components/navbar/index.tsx b/src/app/_components/navbar/index.tsx index bcc443d6..75932a20 100644 --- a/src/app/_components/navbar/index.tsx +++ b/src/app/_components/navbar/index.tsx @@ -17,16 +17,18 @@ import { useNavbarStore } from "@/stores/navbar"; import { useReaderScroller } from "../../[locale]/t/[bookId]/_components/context"; import HomepageNavigationMenu from "./navigation-menu"; import LocaleSwitcher from "./locale-switcher"; +import MobileMenu from "./mobile-menu"; +import MobileNavigationMenu from "./mobile-navigation-menu"; -interface ReaderNavbarProps { - sidebarContent?: React.ReactNode; +interface NavbarProps { + mobileMenu?: React.ReactNode; isHomepage?: boolean; } export default function Navbar({ isHomepage, - sidebarContent, -}: ReaderNavbarProps) { + mobileMenu: sidebarContent, +}: NavbarProps) { const { showNavbar, setShowNavbar } = useNavbarStore(); const [isMenuOpen, setIsMenuOpen] = useState(false); const [isSearchOpen, setIsSearchOpen] = useState(false); @@ -86,19 +88,24 @@ export default function Navbar({
)} -
- {/* Mobile menu button */} - +
+ + + {!isHomepage && ( + + )} + + {/* Mobile menu button */}
-
{isMenuOpen && ( -
- {sidebarContent} -
+ {sidebarContent ?? } )} {isSearchOpen && ( -
+ -
+ )} ); diff --git a/src/app/_components/navbar/links.ts b/src/app/_components/navbar/links.ts new file mode 100644 index 00000000..24db76e3 --- /dev/null +++ b/src/app/_components/navbar/links.ts @@ -0,0 +1,63 @@ +import { navigation } from "@/lib/urls"; +import type { NamespaceTranslations } from "@/types/NamespaceTranslations"; + +export type NavItem = { + href?: string; + title: NamespaceTranslations<"common">; + description: NamespaceTranslations<"common">; +}; + +export const toolsItems: NavItem[] = [ + { + title: "navigation.tools.advanced-search.title", + description: "navigation.tools.advanced-search.description", + }, + { + title: "navigation.tools.text-explorer.title", + description: "navigation.tools.text-explorer.description", + href: navigation.books.all(), + }, + { + title: "navigation.tools.author-explorer.title", + description: "navigation.tools.author-explorer.description", + href: navigation.authors.all(), + }, +]; + +export const exploreItems: NavItem[] = [ + { + href: navigation.books.all(), + title: "navigation.explore.texts.title", + description: "navigation.explore.texts.description", + }, + { + href: navigation.authors.all(), + title: "navigation.explore.authors.title", + description: "navigation.explore.authors.description", + }, + { + href: navigation.regions.all(), + title: "navigation.explore.regions.title", + description: "navigation.explore.regions.description", + }, + { + href: navigation.genres.all(), + title: "navigation.explore.genres.title", + description: "navigation.explore.genres.description", + }, +]; + +export const contributeItems: NavItem[] = [ + { + title: "navigation.contribute.add-text.title", + description: "navigation.contribute.add-text.description", + }, + { + title: "navigation.contribute.report-mistake.title", + description: "navigation.contribute.report-mistake.description", + }, + { + title: "navigation.contribute.feedback.title", + description: "navigation.contribute.feedback.description", + }, +]; diff --git a/src/app/_components/navbar/locale-switcher.tsx b/src/app/_components/navbar/locale-switcher.tsx index 4876d44a..0c6a6ec7 100644 --- a/src/app/_components/navbar/locale-switcher.tsx +++ b/src/app/_components/navbar/locale-switcher.tsx @@ -25,7 +25,7 @@ export default function LocaleSwitcher() { diff --git a/src/app/_components/navbar/mobile-menu.tsx b/src/app/_components/navbar/mobile-menu.tsx new file mode 100644 index 00000000..5fedf684 --- /dev/null +++ b/src/app/_components/navbar/mobile-menu.tsx @@ -0,0 +1,19 @@ +"use client"; + +import { RemoveScroll } from "react-remove-scroll"; +import { Slot } from "@radix-ui/react-slot"; +import { Portal } from "@radix-ui/themes"; + +export default function MobileMenu({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + +
{children}
+
+
+ ); +} diff --git a/src/app/_components/navbar/mobile-navigation-menu.tsx b/src/app/_components/navbar/mobile-navigation-menu.tsx new file mode 100644 index 00000000..904f0294 --- /dev/null +++ b/src/app/_components/navbar/mobile-navigation-menu.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { useState } from "react"; +import { + type NavItem, + toolsItems, + exploreItems, + contributeItems, +} from "./links"; +import { Link } from "@/navigation"; +import ComingSoonModal from "@/components/coming-soon-modal"; +import type { NamespaceTranslations } from "@/types/NamespaceTranslations"; +import Container from "@/components/ui/container"; +import { cn } from "@/lib/utils"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; + +const groups: { + title: NamespaceTranslations<"common">; + items: NavItem[]; +}[] = [ + { + title: "navigation.tools.title", + items: toolsItems, + }, + { + title: "navigation.explore.title", + items: exploreItems, + }, + { + title: "navigation.contribute.title", + items: contributeItems, + }, +]; + +export default function MobileNavigationMenu() { + const t = useTranslations("common"); + const [isModalOpen, setIsModalOpen] = useState(false); + + const renderItem = (item: NavItem, idx: number) => { + const className = "py-1"; + + if (item.href) { + return ( + + - {t(item.description)} + + ); + } + + return ( + + ); + }; + + return ( + + + + + {groups.map((group) => ( + + + {t(group.title)} + + + +
    + {group.items.map(renderItem)} +
+
+
+ ))} +
+ + {/*
+ {groups.map((group) => ( +
+

{t(group.title)}

+ +
    + {group.items.map(renderItem)} +
+
+ ))} +
*/} +
+ ); +} diff --git a/src/app/_components/navbar/navigation-menu.tsx b/src/app/_components/navbar/navigation-menu.tsx index 04c4bb67..890ba52b 100644 --- a/src/app/_components/navbar/navigation-menu.tsx +++ b/src/app/_components/navbar/navigation-menu.tsx @@ -8,83 +8,26 @@ import { NavigationMenuList, NavigationMenuTrigger, } from "@/components/ui/navigation-menu"; -import { navigation } from "@/lib/urls"; import { cn } from "@/lib/utils"; import { Link } from "@/navigation"; -import type { NamespaceTranslations } from "@/types/NamespaceTranslations"; import { EnvelopeIcon } from "@heroicons/react/24/outline"; import { useTranslations } from "next-intl"; import React from "react"; - -type NavItem = { - href?: string; - title: NamespaceTranslations<"common">; - description: NamespaceTranslations<"common">; -}; - -const toolsItems: NavItem[] = [ - { - title: "navigation.tools.advanced-search.title", - description: "navigation.tools.advanced-search.description", - }, - { - title: "navigation.tools.text-explorer.title", - description: "navigation.tools.text-explorer.description", - href: navigation.books.all(), - }, - { - title: "navigation.tools.author-explorer.title", - description: "navigation.tools.author-explorer.description", - href: navigation.authors.all(), - }, -]; - -const exploreItems: NavItem[] = [ - { - href: navigation.books.all(), - title: "navigation.explore.texts.title", - description: "navigation.explore.texts.description", - }, - { - href: navigation.authors.all(), - title: "navigation.explore.authors.title", - description: "navigation.explore.authors.description", - }, - { - href: navigation.regions.all(), - title: "navigation.explore.regions.title", - description: "navigation.explore.regions.description", - }, - { - href: navigation.genres.all(), - title: "navigation.explore.genres.title", - description: "navigation.explore.genres.description", - }, -]; - -const contributeItems: NavItem[] = [ - { - title: "navigation.contribute.add-text.title", - description: "navigation.contribute.add-text.description", - }, - { - title: "navigation.contribute.report-mistake.title", - description: "navigation.contribute.report-mistake.description", - }, - { - title: "navigation.contribute.feedback.title", - description: "navigation.contribute.feedback.description", - }, -]; +import { + type NavItem, + toolsItems, + exploreItems, + contributeItems, +} from "./links"; export default function HomepageNavigationMenu() { const t = useTranslations("common"); const [isModalOpen, setIsModalOpen] = React.useState(false); - const renderItem = (item: NavItem) => { + const renderItem = (item: NavItem, idx: number) => { if (item.href) { return ( - + {t(item.description)} ); @@ -92,7 +35,7 @@ export default function HomepageNavigationMenu() { return ( setIsModalOpen(true)} diff --git a/src/app/_components/navbar/search.tsx b/src/app/_components/navbar/search.tsx index f31c8e24..cc0b6a17 100644 --- a/src/app/_components/navbar/search.tsx +++ b/src/app/_components/navbar/search.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/jsx-key */ "use client"; import { Button } from "@/components/ui/button"; @@ -8,7 +9,7 @@ import { CommandItem, CommandList, } from "@/components/ui/command"; -import { searchBooks } from "@/lib/search"; +import { searchAllCollections } from "@/server/typesense/global"; import { navigation } from "@/lib/urls"; import { cn } from "@/lib/utils"; import { Link, useRouter } from "@/navigation"; @@ -16,6 +17,8 @@ import { useQuery } from "@tanstack/react-query"; import { useTranslations } from "next-intl"; import React, { useEffect, useRef, useState } from "react"; import { useBoolean, useDebounceValue } from "usehooks-ts"; +import DottedList from "@/components/ui/dotted-list"; +import type { GlobalSearchDocument } from "@/types/global-search-document"; export default function SearchBar({ autoFocus, @@ -38,7 +41,7 @@ export default function SearchBar({ queryFn: ({ queryKey }) => { const [, query] = queryKey; - return searchBooks(query ?? "", { limit: 5 }); + return searchAllCollections(query ?? "", { limit: 5 }); }, enabled: !!debouncedValue, }); @@ -51,6 +54,12 @@ export default function SearchBar({ e.preventDefault(); inputRef.current?.focus(); } + + // ESC key should blur the input + if (e.key === "Escape") { + inputRef.current?.blur(); + focusedState.setFalse(); + } }; document.addEventListener("keydown", down); @@ -77,14 +86,40 @@ export default function SearchBar({ }, [focusedState.value, focusedState.setFalse]); // this function handles keyboard navigation and selection - const onItemSelect = (id?: string) => { - if (id) { - replace(navigation.books.reader(id)); + const onItemSelect = (href?: string) => { + if (href) { + replace(href); } else { push(`/search?q=${debouncedValue}`); } }; + const getLocalizedType = (type: GlobalSearchDocument["type"]) => { + if (type === "book") { + return entitiesT("text"); + } else if (type === "author") { + return entitiesT("author"); + } else if (type === "genre") { + return entitiesT("genre"); + } + + return null; + }; + + const getHref = (document: GlobalSearchDocument) => { + if (document.type === "book") { + return navigation.books.reader(document.slug); + } else if (document.type === "author") { + return navigation.authors.bySlug(document.slug); + } else if (document.type === "genre") { + return navigation.genres.bySlug(document.slug); + } else if (document.type === "region") { + return navigation.regions.bySlug(document.slug); + } + + return null; + }; + const showList = focusedState.value; const showSeeMore = (data?.results?.found ?? 0) > 5 && hits.length > 0; @@ -164,11 +199,11 @@ export default function SearchBar({ const authorPrimaryLatinName = result.highlight.author ?.primaryLatinName ? result.highlight.author.primaryLatinName.snippet - : result.document.author.primaryLatinName; + : result.document.author?.primaryLatinName; const authorPrimaryArabicName = result.highlight.author ?.primaryArabicName ? result.highlight.author.primaryArabicName.snippet - : result.document.author.primaryArabicName; + : result.document.author?.primaryArabicName; // use latin name if available, otherwise use arabic name const authorName = @@ -178,12 +213,16 @@ export default function SearchBar({ const documentSecondaryName = documentName === primaryLatinName ? null : primaryLatinName; + const type = result.document.type; + const localizedType = getLocalizedType(type); + const href = getHref(result.document); + return ( onItemSelect(result.document.slug)} - href={navigation.books.reader(result.document.slug)} + onSelect={() => onItemSelect(href ?? undefined)} + href={href ?? ""} > {documentName && (

)} -

-

{entitiesT("text")}

- - - - {authorName && ( -

- )} - - {authorName && documentSecondaryName && } - - {documentSecondaryName && ( -

- )} -

+ {localizedType}

, + authorName && ( +

+ ), + documentSecondaryName && ( +

+ ), + ]} + /> ); })} diff --git a/src/app/_components/navbar/theme-toggle.tsx b/src/app/_components/navbar/theme-toggle.tsx index a81d5311..3b0d8b90 100644 --- a/src/app/_components/navbar/theme-toggle.tsx +++ b/src/app/_components/navbar/theme-toggle.tsx @@ -21,9 +21,9 @@ export function ThemeToggle() { diff --git a/src/components/author-search-result.tsx b/src/components/author-search-result.tsx index f3538924..87d4e229 100644 --- a/src/components/author-search-result.tsx +++ b/src/components/author-search-result.tsx @@ -1,5 +1,5 @@ /* eslint-disable react/jsx-key */ -import type { searchAuthors } from "@/lib/search"; +import type { searchAuthors } from "@/server/typesense/author"; import { Link } from "@/navigation"; import { navigation } from "@/lib/urls"; import { cn } from "@/lib/utils"; diff --git a/src/components/authors-filter/client.tsx b/src/components/authors-filter/client.tsx index 31a42319..8347a764 100644 --- a/src/components/authors-filter/client.tsx +++ b/src/components/authors-filter/client.tsx @@ -1,11 +1,8 @@ "use client"; -import { Checkbox } from "@/components/ui/checkbox"; -import { Input } from "@/components/ui/input"; import { useSearchParams } from "next/navigation"; import { useEffect, useMemo, useRef, useState, useTransition } from "react"; import { Button } from "@/components/ui/button"; -import { searchAuthors } from "@/lib/search"; import type { SearchResponse } from "typesense/lib/Typesense/Documents"; import type { AuthorDocument } from "@/types/author"; import FilterContainer from "../search-results/filter-container"; @@ -13,6 +10,7 @@ import type { findAllAuthorIdsWithBooksCount } from "@/server/services/authors"; import { usePathname, useRouter } from "@/navigation"; import { useFormatter, useLocale, useTranslations } from "next-intl"; import type { AppLocale } from "~/i18n.config"; +import { searchAuthors } from "@/server/typesense/author"; const getAuthorsFilterUrlParams = ( authors: string[], @@ -191,14 +189,13 @@ export default function _AuthorsFilter({ : undefined } > - -

+ {data.items.map((item) => { const author = item.document; const authorId = author.id; @@ -217,28 +214,16 @@ export default function _AuthorsFilter({ const title = `${name} (${booksCount})`; return ( -
- handleChange(authorId)} - className="h-4 w-4" - /> - - -
+ handleChange(authorId)} + title={title} + count={booksCount} + > + {name} + ); })} @@ -247,7 +232,7 @@ export default function _AuthorsFilter({ {t("common.load-more")} )} -
+ ); } diff --git a/src/components/authors-filter/index.tsx b/src/components/authors-filter/index.tsx index c068e042..497ee0db 100644 --- a/src/components/authors-filter/index.tsx +++ b/src/components/authors-filter/index.tsx @@ -1,7 +1,7 @@ import type { ComponentProps } from "react"; import _AuthorsFilter from "./client"; -import { searchAuthors } from "@/lib/search"; import { findAllAuthorIdsWithBooksCount } from "@/server/services/authors"; +import { searchAuthors } from "@/server/typesense/author"; type Props = Omit< ComponentProps, diff --git a/src/components/book-search-result/index.tsx b/src/components/book-search-result/index.tsx index 8d022d92..eaf21a83 100644 --- a/src/components/book-search-result/index.tsx +++ b/src/components/book-search-result/index.tsx @@ -1,7 +1,7 @@ /* eslint-disable react/jsx-key */ "use client"; -import type { searchBooks } from "@/lib/search"; +import type { searchBooks } from "@/server/typesense/book"; import { Link } from "@/navigation"; import { navigation } from "@/lib/urls"; import { cn } from "@/lib/utils"; @@ -45,7 +45,7 @@ const BookSearchResult = ({ -
+

setOpen(true)} > diff --git a/src/components/genres-filter/client.tsx b/src/components/genres-filter/client.tsx index 5008e59a..243064e2 100644 --- a/src/components/genres-filter/client.tsx +++ b/src/components/genres-filter/client.tsx @@ -1,7 +1,5 @@ "use client"; -import { Checkbox } from "@/components/ui/checkbox"; -import { Input } from "@/components/ui/input"; import { usePathname, useRouter } from "@/navigation"; import { useEffect, useMemo, useRef, useState, useTransition } from "react"; import Fuse from "fuse.js"; @@ -149,14 +147,13 @@ export default function _GenresFilter({ : undefined } > - setValue(e.target.value)} /> -
+ {matchedGenres.items.map((genre) => { // const count = genreIdToBooksCount[genre.genreId.toLowerCase()] ?? 0; const booksCount = formatter.number(genre.booksCount); @@ -164,31 +161,16 @@ export default function _GenresFilter({ const title = `${genre.genreName} (${booksCount})`; return ( -
handleChange(genre.genreId)} > - handleChange(genre.genreId)} - className="h-4 w-4" - /> - - -
+ {genre.genreName} + ); })} @@ -197,7 +179,7 @@ export default function _GenresFilter({ {t("common.load-more")} )} -
+ ); } diff --git a/src/components/regions-filter/client.tsx b/src/components/regions-filter/client.tsx index 56fbc2d6..218390b5 100644 --- a/src/components/regions-filter/client.tsx +++ b/src/components/regions-filter/client.tsx @@ -1,7 +1,5 @@ "use client"; -import { Checkbox } from "@/components/ui/checkbox"; -import { Input } from "@/components/ui/input"; import { useEffect, useMemo, useRef, useState, useTransition } from "react"; import Fuse from "fuse.js"; import { Button } from "@/components/ui/button"; @@ -153,15 +151,14 @@ export default function _RegionsFilter({ : undefined } > - setValue(e.target.value)} /> {/* make font weight normal */} -
+ {matchedRegions.items.map((region) => { const booksCount = formatter.number(region.count); @@ -170,28 +167,16 @@ export default function _RegionsFilter({ const title = `${name} (${booksCount})`; return ( -
- handleChange(region.slug)} - className="h-4 w-4" - /> - - -
+ handleChange(region.slug)} + title={title} + count={booksCount} + > + {name} + ); })} @@ -205,7 +190,7 @@ export default function _RegionsFilter({ {t("common.load-more")} )} -
+ ); } diff --git a/src/components/search-results/filter-container.tsx b/src/components/search-results/filter-container.tsx index 7b176ae6..402ce974 100644 --- a/src/components/search-results/filter-container.tsx +++ b/src/components/search-results/filter-container.tsx @@ -4,6 +4,9 @@ import Spinner from "../ui/spinner"; import { XMarkIcon } from "@heroicons/react/24/solid"; import { Link } from "@/navigation"; import { useTranslations } from "next-intl"; +import { Input, type InputProps } from "../ui/input"; +import { cn } from "@/lib/utils"; +import { Checkbox } from "../ui/checkbox"; interface FilterContainerProps { title: string; @@ -13,7 +16,7 @@ interface FilterContainerProps { children: React.ReactNode; } -export default function FilterContainer({ +function FilterContainer({ title, isLoading, titleChildren, @@ -23,7 +26,7 @@ export default function FilterContainer({ const t = useTranslations("common"); return ( -
+

@@ -51,3 +54,61 @@ export default function FilterContainer({

); } + +FilterContainer.Input = function FilterContainerInput(props: InputProps) { + return ( + + ); +}; + +FilterContainer.List = function FilterContainerList({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ); +}; + +FilterContainer.Checkbox = function FilterContainerCheckbox({ + title, + children, + id, + count, + ...props +}: React.ComponentProps & { + count?: number | string; +}) { + return ( +
+ + + +
+ ); +}; + +export default FilterContainer; diff --git a/src/components/search-results/search-bar.tsx b/src/components/search-results/search-bar.tsx index 26f4c7fa..ee64700e 100644 --- a/src/components/search-results/search-bar.tsx +++ b/src/components/search-results/search-bar.tsx @@ -60,7 +60,7 @@ export default function SearchBar({ } return ( -
+
{ items: React.ReactNode[]; + dotClassName?: string; } export default function DottedList({ items, className, + dotClassName, ...props }: DottedListProps) { const filteredItems = items.filter((item) => item); // remove null or undefined items @@ -24,7 +26,14 @@ export default function DottedList({ {item} {filteredItems.length !== idx + 1 && ( - + + • + )}
))} diff --git a/src/lib/locale/server.ts b/src/lib/locale/server.ts index 1c41b51a..91a41372 100644 --- a/src/lib/locale/server.ts +++ b/src/lib/locale/server.ts @@ -4,8 +4,6 @@ import { getLocale as baseGetLocale } from "next-intl/server"; import type { AppLocale } from "~/i18n.config"; export const getLocale = async () => { - "use server"; - const locale = await baseGetLocale(); return locale as AppLocale; }; diff --git a/src/lib/search.ts b/src/lib/search.ts deleted file mode 100644 index 3a7f8ad5..00000000 --- a/src/lib/search.ts +++ /dev/null @@ -1,203 +0,0 @@ -"use server"; - -import type { AuthorDocument } from "@/types/author"; -import type { BookDocument } from "@/types/book"; -import type { SearchResponse } from "typesense/lib/Typesense/Documents"; -import { makeMultiSearchRequest, makeSearchRequest } from "./typesense"; -import { type SearchOptions, makePagination } from "@/server/typesense/utils"; - -const AUTHORS_INDEX = "authors"; -const TITLES_INDEX = "books"; - -const DEFAULT_AUTHORS_PER_PAGE = 5; -const DEFAULT_BOOKS_PER_PAGE = 20; - -const authorsQueryWeights = { - 2: ["primaryArabicName", "primaryLatinName"], - 1: ["_nameVariations", "otherArabicNames", "otherLatinNames"], -}; -const authorsQueryBy = Object.values(authorsQueryWeights).flat().join(", "); -const authorsQueryByWeights = Object.keys(authorsQueryWeights) - // @ts-expect-error - TS doesn't like the fact that we're using Object.keys - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access - .map((weight) => new Array(authorsQueryWeights[weight]!.length).fill(weight)) - .flat() - .join(", "); - -export const searchAuthors = async (q: string, options?: SearchOptions) => { - const { limit = DEFAULT_AUTHORS_PER_PAGE, page = 1 } = options ?? {}; - - const yearRange = (options?.filters?.yearRange ?? null) as number[] | null; - const geographies = (options?.filters?.geographies ?? null) as - | string[] - | null; - const regions = (options?.filters?.regions ?? null) as string[] | null; - const ids = (options?.filters?.ids ?? null) as string[] | null; - - const filters: string[] = []; - if (yearRange) filters.push(`year:[${yearRange[0]}..${yearRange[1]}]`); - - if (geographies && geographies.length > 0) { - filters.push( - `geographies:[${geographies.map((geo) => `\`${geo}\``).join(", ")}]`, - ); - } - - if (regions && regions.length > 0) { - filters.push( - `regions:[${regions - .flatMap((region) => { - return ["born", "died", "visited", "resided"].map( - (type) => `\`${type}@${region}\``, - ); - }) - .join(", ")}]`, - ); - } - - if (ids && ids.length > 0) { - filters.push(`id:[${ids.map((id) => `\`${id}\``).join(", ")}]`); - } - - const results = (await makeSearchRequest(AUTHORS_INDEX, { - q: prepareQuery(q), - query_by: authorsQueryBy, - query_by_weights: authorsQueryByWeights, - prioritize_token_position: true, - limit, - page, - ...(options?.sortBy && - options.sortBy !== "relevance" && { sort_by: options.sortBy }), - ...(filters.length > 0 && { filter_by: filters.join(" && ") }), - })) as SearchResponse; - - return { - results, - pagination: makePagination(results.found, results.page, limit), - }; -}; - -const booksQueryWeights = { - 4: ["primaryArabicName", "primaryLatinName"], - 3: ["_nameVariations", "otherArabicNames", "otherLatinNames"], - 2: ["author.primaryArabicName", "author.primaryLatinName"], - 1: [ - "author._nameVariations", - "author.otherArabicNames", - "author.otherLatinNames", - ], -}; - -const booksQueryBy = Object.values(booksQueryWeights).flat().join(", "); -const booksQueryByWeights = Object.keys(booksQueryWeights) - // @ts-expect-error - TS doesn't like the fact that we're using Object.keys - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access - .map((weight) => new Array(booksQueryWeights[weight]!.length).fill(weight)) - .flat() - .join(", "); - -export const searchBooks = async (q: string, options?: SearchOptions) => { - const { limit = DEFAULT_BOOKS_PER_PAGE, page = 1 } = options ?? {}; - - const genres = (options?.filters?.genres ?? null) as string[] | null; - const authors = (options?.filters?.authors ?? null) as string[] | null; - const geographies = (options?.filters?.geographies ?? null) as - | string[] - | null; - const regions = (options?.filters?.regions ?? null) as string[] | null; - const yearRange = (options?.filters?.yearRange ?? null) as number[] | null; - - const filters: string[] = []; - if (yearRange) filters.push(`year:[${yearRange[0]}..${yearRange[1]}]`); - if (genres && genres.length > 0) { - filters.push( - `genreTags:[${genres.map((genre) => `\`${genre}\``).join(", ")}]`, - ); - } - - if (authors && authors.length > 0) { - filters.push(`authorId:[${authors.map((id) => `\`${id}\``).join(", ")}]`); - } - - if (geographies && geographies.length > 0) { - filters.push( - `geographies:[${geographies.map((geo) => `\`${geo}\``).join(", ")}]`, - ); - } - - if (regions && regions.length > 0) { - filters.push( - `regions:[${regions - .flatMap((region) => { - return ["born", "died", "visited", "resided"].map( - (type) => `\`${type}@${region}\``, - ); - }) - .join(", ")}]`, - ); - } - - // const results = (await makeSearchRequest(TITLES_INDEX, { - // q: prepareQuery(q), - // query_by: booksQueryBy, - // query_by_weights: booksQueryByWeights, - // prioritize_token_position: true, - // limit, - // page, - // ...(options?.sortBy && - // options.sortBy !== "relevance" && { sort_by: options.sortBy }), - // ...(filters.length > 0 && { filter_by: filters.join(" && ") }), - // })) as SearchResponse; - - const results = (await makeMultiSearchRequest([ - { - collection: TITLES_INDEX, - q: prepareQuery(q), - query_by: booksQueryBy, - query_by_weights: booksQueryByWeights, - prioritize_token_position: true, - limit, - page, - ...(options?.sortBy && - options.sortBy !== "relevance" && { sort_by: options.sortBy }), - ...(filters.length > 0 && { filter_by: filters.join(" && ") }), - }, - - ...(authors && authors.length > 0 - ? [ - { - collection: AUTHORS_INDEX, - q: "", - query_by: "primaryArabicName", - limit: 100, - page: 1, - filter_by: `id:[${authors.map((id) => `\`${id}\``).join(", ")}]`, - }, - ] - : []), - ])) as { - results: [SearchResponse, SearchResponse]; - }; - - const [booksResults, selectedAuthorsResults] = results.results; - - return { - results: booksResults, - pagination: makePagination(booksResults.found, booksResults.page, limit), - selectedAuthors: selectedAuthorsResults ?? null, - }; - - // return { - // results, - // pagination: makePagination(results.found, results.page, limit), - // }; -}; - -const prepareQuery = (q: string) => { - const final = [q]; - - const queryWithoutAl = q.replace(/(al-)/gi, ""); - if (queryWithoutAl !== q) final.push(queryWithoutAl); - - return final.join(" || "); -}; diff --git a/src/server/services/books.ts b/src/server/services/books.ts index 3ad67d61..e9b63c80 100644 --- a/src/server/services/books.ts +++ b/src/server/services/books.ts @@ -1,4 +1,4 @@ -"use server"; +// "use server"; import { type Block, parseMarkdown } from "@openiti/markdown-parser"; import { cache } from "react"; diff --git a/src/server/services/regions.ts b/src/server/services/regions.ts index 667de548..e8847482 100644 --- a/src/server/services/regions.ts +++ b/src/server/services/regions.ts @@ -1,4 +1,4 @@ -"use server"; +// "use server"; import { cache } from "react"; import { db } from "../db"; diff --git a/src/server/services/years.ts b/src/server/services/years.ts index 258fea76..9c2e7a01 100644 --- a/src/server/services/years.ts +++ b/src/server/services/years.ts @@ -1,4 +1,4 @@ -"use server"; +// "use server"; import { cache } from "react"; import descriptions from "~/data/centuries.json"; diff --git a/src/server/typesense/author.ts b/src/server/typesense/author.ts new file mode 100644 index 00000000..c274530e --- /dev/null +++ b/src/server/typesense/author.ts @@ -0,0 +1,61 @@ +"use server"; + +import type { SearchResponse } from "typesense/lib/Typesense/Documents"; +import { type SearchOptions, makePagination, prepareQuery } from "./utils"; +import type { AuthorDocument } from "@/types/author"; +import { makeSearchRequest } from "@/lib/typesense"; +import { AUTHORS_COLLECTION } from "./config"; + +export const searchAuthors = async (q: string, options?: SearchOptions) => { + const { limit = AUTHORS_COLLECTION.DEFAULT_PER_PAGE, page = 1 } = + options ?? {}; + + const yearRange = (options?.filters?.yearRange ?? null) as number[] | null; + const geographies = (options?.filters?.geographies ?? null) as + | string[] + | null; + const regions = (options?.filters?.regions ?? null) as string[] | null; + const ids = (options?.filters?.ids ?? null) as string[] | null; + + const filters: string[] = []; + if (yearRange) filters.push(`year:[${yearRange[0]}..${yearRange[1]}]`); + + if (geographies && geographies.length > 0) { + filters.push( + `geographies:[${geographies.map((geo) => `\`${geo}\``).join(", ")}]`, + ); + } + + if (regions && regions.length > 0) { + filters.push( + `regions:[${regions + .flatMap((region) => { + return ["born", "died", "visited", "resided"].map( + (type) => `\`${type}@${region}\``, + ); + }) + .join(", ")}]`, + ); + } + + if (ids && ids.length > 0) { + filters.push(`id:[${ids.map((id) => `\`${id}\``).join(", ")}]`); + } + + const results = (await makeSearchRequest(AUTHORS_COLLECTION.INDEX, { + q: prepareQuery(q), + query_by: AUTHORS_COLLECTION.queryBy, + query_by_weights: AUTHORS_COLLECTION.queryByWeights, + prioritize_token_position: true, + limit, + page, + ...(options?.sortBy && + options.sortBy !== "relevance" && { sort_by: options.sortBy }), + ...(filters.length > 0 && { filter_by: filters.join(" && ") }), + })) as SearchResponse; + + return { + results, + pagination: makePagination(results.found, results.page, limit), + }; +}; diff --git a/src/server/typesense/book.ts b/src/server/typesense/book.ts new file mode 100644 index 00000000..71879634 --- /dev/null +++ b/src/server/typesense/book.ts @@ -0,0 +1,105 @@ +"use server"; + +import type { SearchResponse } from "typesense/lib/Typesense/Documents"; +import type { AuthorDocument } from "@/types/author"; +import type { BookDocument } from "@/types/book"; +import { type SearchOptions, makePagination, prepareQuery } from "./utils"; +import { makeMultiSearchRequest } from "@/lib/typesense"; +import { AUTHORS_COLLECTION, BOOKS_COLLECTION } from "./config"; + +export const searchBooks = async (q: string, options?: SearchOptions) => { + const { limit = BOOKS_COLLECTION.DEFAULT_PER_PAGE, page = 1 } = options ?? {}; + + const genres = (options?.filters?.genres ?? null) as string[] | null; + const authors = (options?.filters?.authors ?? null) as string[] | null; + const geographies = (options?.filters?.geographies ?? null) as + | string[] + | null; + const regions = (options?.filters?.regions ?? null) as string[] | null; + const yearRange = (options?.filters?.yearRange ?? null) as number[] | null; + + const filters: string[] = []; + if (yearRange) filters.push(`year:[${yearRange[0]}..${yearRange[1]}]`); + if (genres && genres.length > 0) { + filters.push( + `genreTags:[${genres.map((genre) => `\`${genre}\``).join(", ")}]`, + ); + } + + if (authors && authors.length > 0) { + filters.push(`authorId:[${authors.map((id) => `\`${id}\``).join(", ")}]`); + } + + if (geographies && geographies.length > 0) { + filters.push( + `geographies:[${geographies.map((geo) => `\`${geo}\``).join(", ")}]`, + ); + } + + if (regions && regions.length > 0) { + filters.push( + `regions:[${regions + .flatMap((region) => { + return ["born", "died", "visited", "resided"].map( + (type) => `\`${type}@${region}\``, + ); + }) + .join(", ")}]`, + ); + } + + // const results = (await makeSearchRequest(TITLES_INDEX, { + // q: prepareQuery(q), + // query_by: booksQueryBy, + // query_by_weights: booksQueryByWeights, + // prioritize_token_position: true, + // limit, + // page, + // ...(options?.sortBy && + // options.sortBy !== "relevance" && { sort_by: options.sortBy }), + // ...(filters.length > 0 && { filter_by: filters.join(" && ") }), + // })) as SearchResponse; + + const results = (await makeMultiSearchRequest([ + { + collection: BOOKS_COLLECTION.INDEX, + q: prepareQuery(q), + query_by: BOOKS_COLLECTION.queryBy, + query_by_weights: BOOKS_COLLECTION.queryByWeights, + prioritize_token_position: true, + limit, + page, + ...(options?.sortBy && + options.sortBy !== "relevance" && { sort_by: options.sortBy }), + ...(filters.length > 0 && { filter_by: filters.join(" && ") }), + }, + + ...(authors && authors.length > 0 + ? [ + { + collection: AUTHORS_COLLECTION.INDEX, + q: "", + query_by: "primaryArabicName", + limit: 100, + page: 1, + filter_by: `id:[${authors.map((id) => `\`${id}\``).join(", ")}]`, + }, + ] + : []), + ])) as { + results: [SearchResponse, SearchResponse]; + }; + + const [booksResults, selectedAuthorsResults] = results.results; + + return { + results: booksResults, + pagination: makePagination(booksResults.found, booksResults.page, limit), + selectedAuthors: selectedAuthorsResults ?? null, + }; + + // return { + // results, + // pagination: makePagination(results.found, results.page, limit), + // }; +}; diff --git a/src/server/typesense/config.ts b/src/server/typesense/config.ts new file mode 100644 index 00000000..c3fbf19d --- /dev/null +++ b/src/server/typesense/config.ts @@ -0,0 +1,72 @@ +import { weightsMapToQueryBy, weightsMapToQueryWeights } from "./utils"; + +const authorQueryWeights = { + 2: ["primaryArabicName", "primaryLatinName"], + 1: ["_nameVariations", "otherArabicNames", "otherLatinNames"], +}; + +export const AUTHORS_COLLECTION = { + INDEX: "authors", + DEFAULT_PER_PAGE: 5, + queryBy: weightsMapToQueryBy(authorQueryWeights), + queryByWeights: weightsMapToQueryWeights(authorQueryWeights), +}; + +const booksQueryWeights = { + 4: ["primaryArabicName", "primaryLatinName"], + 3: ["_nameVariations", "otherArabicNames", "otherLatinNames"], + 2: ["author.primaryArabicName", "author.primaryLatinName"], + 1: [ + "author._nameVariations", + "author.otherArabicNames", + "author.otherLatinNames", + ], +}; + +export const BOOKS_COLLECTION = { + INDEX: "books", + DEFAULT_PER_PAGE: 20, + queryBy: weightsMapToQueryBy(booksQueryWeights), + queryByWeights: weightsMapToQueryWeights(booksQueryWeights), +}; + +const genresQueryWeights = { + 1: ["name"], +}; + +export const GENRES_COLLECTION = { + INDEX: "genres", + DEFAULT_PER_PAGE: 5, + queryBy: weightsMapToQueryBy(genresQueryWeights), + queryByWeights: weightsMapToQueryWeights(genresQueryWeights), +}; + +const regionsQueryWeights = { + 2: ["name", "arabicName", "currentName"], + 1: ["subLocations"], +}; + +export const REGIONS_COLLECTION = { + INDEX: "regions", + DEFAULT_PER_PAGE: 5, + queryBy: weightsMapToQueryBy(regionsQueryWeights), + queryByWeights: weightsMapToQueryWeights(regionsQueryWeights), +}; + +const globalSearchQueryWeights = { + 4: ["primaryArabicName", "primaryLatinName"], + 3: ["_nameVariations", "otherArabicNames", "otherLatinNames"], + 2: ["author.primaryArabicName", "author.primaryLatinName"], + 1: [ + "author._nameVariations", + "author.otherArabicNames", + "author.otherLatinNames", + ], +}; + +export const GLOBAL_SEARCH_COLLECTION = { + INDEX: "all_documents", + DEFAULT_PER_PAGE: 20, + queryBy: weightsMapToQueryBy(globalSearchQueryWeights), + queryByWeights: weightsMapToQueryWeights(globalSearchQueryWeights), +}; diff --git a/src/server/typesense/genre.ts b/src/server/typesense/genre.ts index 89f8cc7b..89955683 100644 --- a/src/server/typesense/genre.ts +++ b/src/server/typesense/genre.ts @@ -2,31 +2,18 @@ import { makeSearchRequest } from "@/lib/typesense"; import { makePagination, type SearchOptions } from "./utils"; import type { SearchResponse } from "typesense/lib/Typesense/Documents"; import type { GenreDocument } from "@/types/genre"; - -const INDEX = "genres"; -const DEFAULT_PER_PAGE = 5; - -const queryWeights = { - 1: ["name"], -}; - -const queryBy = Object.values(queryWeights).flat().join(", "); -const queryByWeights = Object.keys(queryWeights) - // @ts-expect-error - TS doesn't like the fact that we're using Object.keys - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access - .map((weight) => new Array(queryWeights[weight]!.length).fill(weight)) - .flat() - .join(", "); +import { GENRES_COLLECTION } from "./config"; export const searchGenres = async (q: string, options?: SearchOptions) => { - const { limit = DEFAULT_PER_PAGE, page = 1 } = options ?? {}; + const { limit = GENRES_COLLECTION.DEFAULT_PER_PAGE, page = 1 } = + options ?? {}; const filters: string[] = []; - const results = (await makeSearchRequest(INDEX, { + const results = (await makeSearchRequest(GENRES_COLLECTION.INDEX, { q, - query_by: queryBy, - query_by_weights: queryByWeights, + query_by: GENRES_COLLECTION.queryBy, + query_by_weights: GENRES_COLLECTION.queryByWeights, prioritize_token_position: true, limit, page, diff --git a/src/server/typesense/global.ts b/src/server/typesense/global.ts new file mode 100644 index 00000000..e0d7255f --- /dev/null +++ b/src/server/typesense/global.ts @@ -0,0 +1,31 @@ +"use server"; + +import { makeSearchRequest } from "@/lib/typesense"; +import { makePagination, prepareQuery, type SearchOptions } from "./utils"; +import { GLOBAL_SEARCH_COLLECTION } from "./config"; +import type { SearchResponse } from "typesense/lib/Typesense/Documents"; +import type { GlobalSearchDocument } from "@/types/global-search-document"; + +export const searchAllCollections = async ( + q: string, + options?: SearchOptions, +) => { + const { limit = GLOBAL_SEARCH_COLLECTION.DEFAULT_PER_PAGE, page = 1 } = + options ?? {}; + + const results = (await makeSearchRequest(GLOBAL_SEARCH_COLLECTION.INDEX, { + q: prepareQuery(q), + query_by: GLOBAL_SEARCH_COLLECTION.queryBy, + query_by_weights: GLOBAL_SEARCH_COLLECTION.queryByWeights, + prioritize_token_position: true, + limit, + page, + ...(options?.sortBy && + options.sortBy !== "relevance" && { sort_by: options.sortBy }), + })) as SearchResponse; + + return { + results, + pagination: makePagination(results.found, results.page, limit), + }; +}; diff --git a/src/server/typesense/region.ts b/src/server/typesense/region.ts index 57eff3e5..d7226b2f 100644 --- a/src/server/typesense/region.ts +++ b/src/server/typesense/region.ts @@ -2,32 +2,18 @@ import { makeSearchRequest } from "@/lib/typesense"; import { makePagination, type SearchOptions } from "./utils"; import type { SearchResponse } from "typesense/lib/Typesense/Documents"; import type { RegionDocument } from "@/types/region"; - -const INDEX = "regions"; -const DEFAULT_PER_PAGE = 5; - -const queryWeights = { - 2: ["name", "arabicName", "currentName"], - 1: ["subLocations"], -}; - -const queryBy = Object.values(queryWeights).flat().join(", "); -const queryByWeights = Object.keys(queryWeights) - // @ts-expect-error - TS doesn't like the fact that we're using Object.keys - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access - .map((weight) => new Array(queryWeights[weight]!.length).fill(weight)) - .flat() - .join(", "); +import { REGIONS_COLLECTION } from "./config"; export const searchRegions = async (q: string, options?: SearchOptions) => { - const { limit = DEFAULT_PER_PAGE, page = 1 } = options ?? {}; + const { limit = REGIONS_COLLECTION.DEFAULT_PER_PAGE, page = 1 } = + options ?? {}; const filters: string[] = []; - const results = (await makeSearchRequest(INDEX, { + const results = (await makeSearchRequest(REGIONS_COLLECTION.INDEX, { q, - query_by: queryBy, - query_by_weights: queryByWeights, + query_by: REGIONS_COLLECTION.queryBy, + query_by_weights: REGIONS_COLLECTION.queryByWeights, prioritize_token_position: true, limit, page, diff --git a/src/server/typesense/utils.ts b/src/server/typesense/utils.ts index 8e9a01c4..2d2009db 100644 --- a/src/server/typesense/utils.ts +++ b/src/server/typesense/utils.ts @@ -22,3 +22,25 @@ export interface SearchOptions { sortBy?: string; filters?: Record; } + +export const prepareQuery = (q: string) => { + const final = [q]; + + const queryWithoutAl = q.replace(/(al-)/gi, ""); + if (queryWithoutAl !== q) final.push(queryWithoutAl); + + return final.join(" || "); +}; + +export const weightsMapToQueryBy = (weightsMap: Record) => + Object.values(weightsMap).flat().join(", "); + +export const weightsMapToQueryWeights = ( + weightsMap: Record, +) => + Object.keys(weightsMap) + // @ts-expect-error - TS doesn't like the fact that we're using Object.keys + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access + .map((weight) => new Array(weightsMap[weight]!.length).fill(weight)) + .flat() + .join(", "); diff --git a/src/styles/globals.css b/src/styles/globals.css index 38582a48..4772ee7c 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -36,32 +36,32 @@ } .dark { - --background: 222.2 84% 4.9%; + --background: 240 5.56% 7.06%; --foreground: 210 40% 98%; - --card: 222.2 84% 4.9%; + --card: 240 3.85% 10.2%; --card-foreground: 210 40% 98%; - --popover: 222.2 84% 4.9%; + --popover: 240 3.85% 10.2%; --popover-foreground: 210 40% 98%; - --primary: 6 37% 35%; + --primary: 6.18 34% 39.22%; --primary-foreground: 4 53% 94%; - --secondary: 217.2 32.6% 17.5%; + --secondary: 234.55 7.69% 40.96%; --secondary-foreground: 210 40% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; + --muted: 226.67 6.04% 29.22%; + --muted-foreground: 234.55 7.69% 71.96%; - --accent: 217.2 32.6% 17.5%; + --accent: 226.67 6.04% 25.22%; --accent-foreground: 210 40% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; + --border: 226.67 6.04% 29.22%; + --input: 226.67 6.04% 29.22%; --ring: 212.7 26.8% 83.9%; } } diff --git a/src/types/book.ts b/src/types/book.ts index 3f351f39..a5102939 100644 --- a/src/types/book.ts +++ b/src/types/book.ts @@ -9,7 +9,7 @@ export type BookDocument = { primaryLatinName: string; otherLatinNames: string[]; _nameVariations: string[]; - author: Omit; + author: Omit; versionIds: string[]; year: number; genreTags: string[]; diff --git a/src/types/global-search-document.ts b/src/types/global-search-document.ts new file mode 100644 index 00000000..02a19939 --- /dev/null +++ b/src/types/global-search-document.ts @@ -0,0 +1,15 @@ +import type { AuthorDocument } from "./author"; + +export type GlobalSearchDocument = { + id: string; + slug: string; + type: "author" | "book" | "genre" | "region"; + primaryArabicName?: string; + otherArabicNames?: string[]; + primaryLatinName?: string; + otherLatinNames?: string[]; + _nameVariations?: string[]; + author?: Omit; + year?: number; + booksCount?: number; +}; From 6103bbb045e47c7b04c24bbae3e447374cf2815d Mon Sep 17 00:00:00 2001 From: ahmedriad1 Date: Wed, 27 Mar 2024 09:40:28 +0200 Subject: [PATCH 2/7] add react-remove-scroll --- package.json | 1 + pnpm-lock.yaml | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/package.json b/package.json index 56cbe374..894bbe53 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "nprogress": "^0.2.0", "react": "18.2.0", "react-dom": "18.2.0", + "react-remove-scroll": "^2.5.9", "react-resizable-panels": "^2.0.9", "react-virtuoso": "^4.7.1", "resend": "^3.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39813706..d29ef2c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -119,6 +119,9 @@ dependencies: react-dom: specifier: 18.2.0 version: 18.2.0(react@18.2.0) + react-remove-scroll: + specifier: ^2.5.9 + version: 2.5.9(@types/react@18.2.57)(react@18.2.0) react-resizable-panels: specifier: ^2.0.9 version: 2.0.9(react-dom@18.2.0)(react@18.2.0) @@ -11645,6 +11648,22 @@ packages: react-style-singleton: 2.2.1(@types/react@18.2.57)(react@18.2.0) tslib: 2.6.2 + /react-remove-scroll-bar@2.3.6(@types/react@18.2.57)(react@18.2.0): + resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.57 + react: 18.2.0 + react-style-singleton: 2.2.1(@types/react@18.2.57)(react@18.2.0) + tslib: 2.6.2 + dev: false + /react-remove-scroll@2.5.4(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-xGVKJJr0SJGQVirVFAUZ2k1QLyO6m+2fy0l8Qawbp5Jgrv3DeLalrfMNBFSlmz5kriGGzsVBtGVnf4pTKIhhWA==} engines: {node: '>=10'} @@ -11682,6 +11701,25 @@ packages: use-callback-ref: 1.3.1(@types/react@18.2.57)(react@18.2.0) use-sidecar: 1.1.2(@types/react@18.2.57)(react@18.2.0) + /react-remove-scroll@2.5.9(@types/react@18.2.57)(react@18.2.0): + resolution: {integrity: sha512-bvHCLBrFfM2OgcrpPY2YW84sPdS2o2HKWJUf1xGyGLnSoEnOTOBpahIarjRuYtN0ryahCeP242yf+5TrBX/pZA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.57 + react: 18.2.0 + react-remove-scroll-bar: 2.3.6(@types/react@18.2.57)(react@18.2.0) + react-style-singleton: 2.2.1(@types/react@18.2.57)(react@18.2.0) + tslib: 2.6.2 + use-callback-ref: 1.3.1(@types/react@18.2.57)(react@18.2.0) + use-sidecar: 1.1.2(@types/react@18.2.57)(react@18.2.0) + dev: false + /react-resizable-panels@2.0.9(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-ZylBvs7oG7Y/INWw3oYGolqgpFvoPW8MPeg9l1fURDeKpxrmUuCHBUmPj47BdZ11MODImu3kZYXG85rbySab7w==} peerDependencies: From 7c812283d20798a5cc2ae94c52543128dde7895d Mon Sep 17 00:00:00 2001 From: ahmedriad1 Date: Wed, 27 Mar 2024 09:45:52 +0200 Subject: [PATCH 3/7] add region text --- src/app/_components/navbar/search.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/_components/navbar/search.tsx b/src/app/_components/navbar/search.tsx index cc0b6a17..d2ad3cb4 100644 --- a/src/app/_components/navbar/search.tsx +++ b/src/app/_components/navbar/search.tsx @@ -101,6 +101,8 @@ export default function SearchBar({ return entitiesT("author"); } else if (type === "genre") { return entitiesT("genre"); + } else if (type === "region") { + return entitiesT("region"); } return null; From d1afb6f94de54af879a1f76f3b30574c735dd52f Mon Sep 17 00:00:00 2001 From: ahmedriad1 Date: Wed, 27 Mar 2024 11:11:04 +0200 Subject: [PATCH 4/7] update carousel component --- package.json | 1 + pnpm-lock.yaml | 25 ++ .../_components/homepage-section/index.tsx | 113 ++------ src/components/ui/button.tsx | 57 ++++ src/components/ui/carousel.tsx | 253 ++++++++++++++++++ 5 files changed, 360 insertions(+), 89 deletions(-) create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/carousel.tsx diff --git a/package.json b/package.json index 894bbe53..f7399c5f 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "clsx": "^2.1.0", "cmdk": "^0.2.1", "drizzle-orm": "^0.29.3", + "embla-carousel-react": "^8.0.0", "fuse.js": "^7.0.0", "js-cookie": "^3.0.5", "lucide-react": "^0.356.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d29ef2c6..35a9817a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,6 +89,9 @@ dependencies: drizzle-orm: specifier: ^0.29.3 version: 0.29.4(@planetscale/database@1.16.0)(@types/react@18.2.57)(mysql2@3.9.1)(react@18.2.0) + embla-carousel-react: + specifier: ^8.0.0 + version: 8.0.0(react@18.2.0) fuse.js: specifier: ^7.0.0 version: 7.0.0 @@ -7732,6 +7735,28 @@ packages: minimalistic-crypto-utils: 1.0.1 dev: true + /embla-carousel-react@8.0.0(react@18.2.0): + resolution: {integrity: sha512-qT0dii8ZwoCtEIBE6ogjqU2+5IwnGfdt2teKjCzW88JRErflhlCpz8KjWnW8xoRZOP8g0clRtsMEFoAgS/elfA==} + peerDependencies: + react: ^16.8.0 || ^17.0.1 || ^18.0.0 + dependencies: + embla-carousel: 8.0.0 + embla-carousel-reactive-utils: 8.0.0(embla-carousel@8.0.0) + react: 18.2.0 + dev: false + + /embla-carousel-reactive-utils@8.0.0(embla-carousel@8.0.0): + resolution: {integrity: sha512-JCw0CqCXI7tbHDRogBb9PoeMLyjEC1vpN0lDOzUjmlfVgtfF+ffLaOK8bVtXVUEbNs/3guGe3NSzA5J5aYzLzw==} + peerDependencies: + embla-carousel: 8.0.0 + dependencies: + embla-carousel: 8.0.0 + dev: false + + /embla-carousel@8.0.0: + resolution: {integrity: sha512-ecixcyqS6oKD2nh5Nj5MObcgoSILWNI/GtBxkidn5ytFaCCmwVHo2SecksaQZHcARMMpIR2dWOlSIdA1LkZFUA==} + dev: false + /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} diff --git a/src/app/_components/homepage-section/index.tsx b/src/app/_components/homepage-section/index.tsx index 120b7140..bfc8255d 100644 --- a/src/app/_components/homepage-section/index.tsx +++ b/src/app/_components/homepage-section/index.tsx @@ -1,14 +1,17 @@ "use client"; -import { Button } from "@/components/ui/button"; import { Link } from "@/navigation"; -import { ChevronRightIcon, ChevronLeftIcon } from "@heroicons/react/20/solid"; -import { useRef, useState } from "react"; -import { useIsomorphicLayoutEffect } from "usehooks-ts"; -import Swiper from "swiper"; -import { Navigation } from "swiper/modules"; +import { ChevronRightIcon } from "@heroicons/react/20/solid"; import ComingSoonModal from "@/components/coming-soon-modal"; +import { + Carousel, + CarouselContent, + CarouselItem, + CarouselNext, + CarouselPrevious, +} from "@/components/ui/carousel"; + const HomepageSection = ({ title, href, @@ -18,58 +21,6 @@ const HomepageSection = ({ href?: string; items: React.ReactNode[]; }) => { - const containerRef = useRef(null); - const swiper = useRef(); - - const [navigation, setNavigation] = useState({ - hasPrev: false, - hasNext: true, - }); - - useIsomorphicLayoutEffect(() => { - swiper.current = new Swiper(containerRef.current!, { - modules: [Navigation], - direction: "horizontal", - freeMode: true, - // watchSlidesProgress: true, - slidesPerView: "auto", - // slidesPerGroupSkip: 1, - // watchOverflow: true, - // breakpoints: { - // 320: { - // slidesPerView: 2.2, - // }, - // 640: { - // slidesPerView: 3.5, - // }, - // 768: { - // slidesPerView: "auto", - // }, - // }, - on: { - slideChange: (e) => { - console.log({ - activeIndex: e.activeIndex, - slides: e.slides.length, - }); - - setNavigation({ - hasPrev: e.activeIndex !== 0, - hasNext: e.activeIndex !== e.slides.length - 2, - }); - }, - }, - }); - }, []); - - const prev = () => { - swiper.current?.slidePrev(); - }; - - const next = () => { - swiper.current?.slideNext(); - }; - const sectionTitle = (

{title}{" "} @@ -78,7 +29,7 @@ const HomepageSection = ({ ); return ( - <> +
{href ? ( {sectionTitle} @@ -87,23 +38,9 @@ const HomepageSection = ({ )}
- + - +
@@ -113,21 +50,19 @@ const HomepageSection = ({ */} -
-
- {items.map((item, idx) => ( -
-
- {item} -
- {idx !== items.length - 1 && ( -
- )} + + {items.map((item, idx) => ( + +
+ {item}
- ))} -
-
- + {idx !== items.length - 1 && ( +
+ )} + + ))} + + ); }; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 00000000..0270f644 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,57 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx new file mode 100644 index 00000000..53730db6 --- /dev/null +++ b/src/components/ui/carousel.tsx @@ -0,0 +1,253 @@ +"use client"; + +import * as React from "react"; +import { ChevronRightIcon, ChevronLeftIcon } from "@heroicons/react/20/solid"; +import useEmblaCarousel, { + type UseEmblaCarouselType, +} from "embla-carousel-react"; + +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; + +type CarouselApi = UseEmblaCarouselType[1]; +type UseCarouselParameters = Parameters; +type CarouselOptions = UseCarouselParameters[0]; +type CarouselPlugin = UseCarouselParameters[1]; + +type CarouselProps = { + opts?: CarouselOptions; + plugins?: CarouselPlugin; + orientation?: "horizontal" | "vertical"; + setApi?: (api: CarouselApi) => void; +}; + +type CarouselContextProps = { + carouselRef: ReturnType[0]; + api: ReturnType[1]; + scrollPrev: () => void; + scrollNext: () => void; + canScrollPrev: boolean; + canScrollNext: boolean; +} & CarouselProps; + +const CarouselContext = React.createContext(null); + +function useCarousel() { + const context = React.useContext(CarouselContext); + + if (!context) { + throw new Error("useCarousel must be used within a "); + } + + return context; +} + +const Carousel = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & CarouselProps +>( + ( + { + orientation = "horizontal", + opts, + setApi, + plugins, + className, + children, + ...props + }, + ref, + ) => { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === "horizontal" ? "x" : "y", + duration: 20, + }, + plugins, + ); + const [canScrollPrev, setCanScrollPrev] = React.useState(false); + const [canScrollNext, setCanScrollNext] = React.useState(false); + + const onSelect = React.useCallback((api: CarouselApi) => { + if (!api) { + return; + } + + setCanScrollPrev(api.canScrollPrev()); + setCanScrollNext(api.canScrollNext()); + }, []); + + const scrollPrev = React.useCallback(() => { + api?.scrollPrev(); + api?.scrollPrev(); + }, [api]); + + const scrollNext = React.useCallback(() => { + api?.scrollNext(); + api?.scrollNext(); + }, [api]); + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "ArrowLeft") { + event.preventDefault(); + scrollPrev(); + } else if (event.key === "ArrowRight") { + event.preventDefault(); + scrollNext(); + } + }, + [scrollPrev, scrollNext], + ); + + React.useEffect(() => { + if (!api || !setApi) { + return; + } + + setApi(api); + }, [api, setApi]); + + React.useEffect(() => { + if (!api) { + return; + } + + onSelect(api); + api.on("reInit", onSelect); + api.on("select", onSelect); + + return () => { + api?.off("select", onSelect); + }; + }, [api, onSelect]); + + return ( + +
+ {children} +
+
+ ); + }, +); +Carousel.displayName = "Carousel"; + +const CarouselContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { carouselRef, orientation } = useCarousel(); + + return ( +
+
+
+ ); +}); +CarouselContent.displayName = "CarouselContent"; + +const CarouselItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { orientation } = useCarousel(); + + return ( +
+ ); +}); +CarouselItem.displayName = "CarouselItem"; + +const CarouselPrevious = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = "ghost", size = "icon", ...props }, ref) => { + const { scrollPrev, canScrollPrev } = useCarousel(); + + return ( + + ); +}); +CarouselPrevious.displayName = "CarouselPrevious"; + +const CarouselNext = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = "ghost", size = "icon", ...props }, ref) => { + const { scrollNext, canScrollNext } = useCarousel(); + + return ( + + ); +}); +CarouselNext.displayName = "CarouselNext"; + +export { + type CarouselApi, + Carousel, + CarouselContent, + CarouselItem, + CarouselPrevious, + CarouselNext, +}; From 98170eb489660fbd4a60882416648e7dc79bfd2b Mon Sep 17 00:00:00 2001 From: ahmedriad1 Date: Thu, 28 Mar 2024 11:24:31 +0200 Subject: [PATCH 5/7] minor fixes --- src/components/ui/badge/index.tsx | 2 +- src/components/ui/button.tsx | 28 ++++++++++++++-------------- src/components/ui/tabs/index.tsx | 2 +- src/server/services/books.ts | 8 +++++++- src/styles/globals.css | 2 +- 5 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/components/ui/badge/index.tsx b/src/components/ui/badge/index.tsx index 188912a6..a1017760 100644 --- a/src/components/ui/badge/index.tsx +++ b/src/components/ui/badge/index.tsx @@ -11,7 +11,7 @@ const badgeVariants = cva( default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80 dark:bg-accent dark:hover:bg-accent/80", destructive: "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", outline: "text-foreground", diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 0270f644..e3a16037 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const buttonVariants = cva( "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", @@ -17,7 +17,7 @@ const buttonVariants = cva( "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", + ghost: "hover:bg-accent/20 focus:bg-accent/20", link: "text-primary underline-offset-4 hover:underline", }, size: { @@ -31,27 +31,27 @@ const buttonVariants = cva( variant: "default", size: "default", }, - } -) + }, +); export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { - asChild?: boolean + asChild?: boolean; } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button" + const Comp = asChild ? Slot : "button"; return ( - ) - } -) -Button.displayName = "Button" + ); + }, +); +Button.displayName = "Button"; -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/src/components/ui/tabs/index.tsx b/src/components/ui/tabs/index.tsx index cc56f162..efca05b6 100644 --- a/src/components/ui/tabs/index.tsx +++ b/src/components/ui/tabs/index.tsx @@ -14,7 +14,7 @@ const TabsList = React.forwardRef< { ); if (!response.ok || response.status >= 300) { - throw new Error("Book not found"); + response = await fetch( + `https://raw.githubusercontent.com/OpenITI/RELEASE/2385733573ab800b5aea09bc846b1d864f475476/data/${record.author.id}/${record.id}/${version}.mARkdown`, + ); + + if (!response.ok || response.status >= 300) { + throw new Error("Book not found"); + } } } diff --git a/src/styles/globals.css b/src/styles/globals.css index 4772ee7c..4e699003 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -45,7 +45,7 @@ --popover: 240 3.85% 10.2%; --popover-foreground: 210 40% 98%; - --primary: 6.18 34% 39.22%; + --primary: 6.35 35.86% 46.47%; --primary-foreground: 4 53% 94%; --secondary: 234.55 7.69% 40.96%; From 556bfe83a58879a01c1640bf9cb3c2431794feef Mon Sep 17 00:00:00 2001 From: ahmedriad1 Date: Thu, 28 Mar 2024 13:20:30 +0200 Subject: [PATCH 6/7] fix button --- src/components/ui/button.tsx | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index e3a16037..17b698c8 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -3,6 +3,7 @@ import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; +import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip"; const buttonVariants = cva( "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", @@ -38,18 +39,40 @@ export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean; + tooltip?: string; + tooltipProps?: React.ComponentProps; } const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { + ( + { + className, + variant, + size, + asChild = false, + tooltip, + tooltipProps, + ...props + }, + ref, + ) => { const Comp = asChild ? Slot : "button"; - return ( + const content = ( ); + + if (!tooltip) return content; + + return ( + + {content} + {tooltip} + + ); }, ); Button.displayName = "Button"; From 11a082ef6d83628ba0f8121e1ca95bf511b70f10 Mon Sep 17 00:00:00 2001 From: ahmedriad1 Date: Sun, 31 Mar 2024 22:09:15 +0200 Subject: [PATCH 7/7] update search --- src/lib/diacritics.ts | 251 ++++++++++++++++++++++++++++++++++ src/server/typesense/utils.ts | 15 +- 2 files changed, 260 insertions(+), 6 deletions(-) create mode 100644 src/lib/diacritics.ts diff --git a/src/lib/diacritics.ts b/src/lib/diacritics.ts new file mode 100644 index 00000000..b29102f1 --- /dev/null +++ b/src/lib/diacritics.ts @@ -0,0 +1,251 @@ +const defaultDiacriticsRemovalMap = [ + { + base: "A", + letters: + /[\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F]/g, + }, + { base: "AA", letters: /[\uA732]/g }, + { base: "AE", letters: /[\u00C6\u01FC\u01E2]/g }, + { base: "AO", letters: /[\uA734]/g }, + { base: "AU", letters: /[\uA736]/g }, + { base: "AV", letters: /[\uA738\uA73A]/g }, + { base: "AY", letters: /[\uA73C]/g }, + { + base: "B", + letters: /[\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181]/g, + }, + { + base: "C", + letters: + /[\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E]/g, + }, + { + base: "D", + letters: + /[\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779]/g, + }, + { base: "DZ", letters: /[\u01F1\u01C4]/g }, + { base: "Dz", letters: /[\u01F2\u01C5]/g }, + { + base: "E", + letters: + /[\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E]/g, + }, + { base: "F", letters: /[\u0046\u24BB\uFF26\u1E1E\u0191\uA77B]/g }, + { + base: "G", + letters: + /[\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E]/g, + }, + { + base: "H", + letters: + /[\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D]/g, + }, + { + base: "I", + letters: + /[\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197]/g, + }, + { base: "J", letters: /[\u004A\u24BF\uFF2A\u0134\u0248]/g }, + { + base: "K", + letters: + /[\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2]/g, + }, + { + base: "L", + letters: + /[\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780]/g, + }, + { base: "LJ", letters: /[\u01C7]/g }, + { base: "Lj", letters: /[\u01C8]/g }, + { base: "M", letters: /[\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C]/g }, + { + base: "N", + letters: + /[\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4]/g, + }, + { base: "NJ", letters: /[\u01CA]/g }, + { base: "Nj", letters: /[\u01CB]/g }, + { + base: "O", + letters: + /[\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C]/g, + }, + { base: "OI", letters: /[\u01A2]/g }, + { base: "OO", letters: /[\uA74E]/g }, + { base: "OU", letters: /[\u0222]/g }, + { + base: "P", + letters: /[\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754]/g, + }, + { base: "Q", letters: /[\u0051\u24C6\uFF31\uA756\uA758\u024A]/g }, + { + base: "R", + letters: + /[\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782]/g, + }, + { + base: "S", + letters: + /[\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784]/g, + }, + { + base: "T", + letters: + /[\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786]/g, + }, + { base: "TZ", letters: /[\uA728]/g }, + { + base: "U", + letters: + /[\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244]/g, + }, + { base: "V", letters: /[\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245]/g }, + { base: "VY", letters: /[\uA760]/g }, + { + base: "W", + letters: /[\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72]/g, + }, + { base: "X", letters: /[\u0058\u24CD\uFF38\u1E8A\u1E8C]/g }, + { + base: "Y", + letters: + /[\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE]/g, + }, + { + base: "Z", + letters: + /[\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762]/g, + }, + { + base: "a", + letters: + /[\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250]/g, + }, + { base: "aa", letters: /[\uA733]/g }, + { base: "ae", letters: /[\u00E6\u01FD\u01E3]/g }, + { base: "ao", letters: /[\uA735]/g }, + { base: "au", letters: /[\uA737]/g }, + { base: "av", letters: /[\uA739\uA73B]/g }, + { base: "ay", letters: /[\uA73D]/g }, + { + base: "b", + letters: /[\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253]/g, + }, + { + base: "c", + letters: + /[\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184]/g, + }, + { + base: "d", + letters: + /[\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A]/g, + }, + { base: "dz", letters: /[\u01F3\u01C6]/g }, + { + base: "e", + letters: + /[\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD]/g, + }, + { base: "f", letters: /[\u0066\u24D5\uFF46\u1E1F\u0192\uA77C]/g }, + { + base: "g", + letters: + /[\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F]/g, + }, + { + base: "h", + letters: + /[\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265]/g, + }, + { base: "hv", letters: /[\u0195]/g }, + { + base: "i", + letters: + /[\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131]/g, + }, + { base: "j", letters: /[\u006A\u24D9\uFF4A\u0135\u01F0\u0249]/g }, + { + base: "k", + letters: + /[\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3]/g, + }, + { + base: "l", + letters: + /[\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747]/g, + }, + { base: "lj", letters: /[\u01C9]/g }, + { base: "m", letters: /[\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F]/g }, + { + base: "n", + letters: + /[\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5]/g, + }, + { base: "nj", letters: /[\u01CC]/g }, + { + base: "o", + letters: + /[\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275]/g, + }, + { base: "oi", letters: /[\u01A3]/g }, + { base: "ou", letters: /[\u0223]/g }, + { base: "oo", letters: /[\uA74F]/g }, + { + base: "p", + letters: /[\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755]/g, + }, + { base: "q", letters: /[\u0071\u24E0\uFF51\u024B\uA757\uA759]/g }, + { + base: "r", + letters: + /[\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783]/g, + }, + { + base: "s", + letters: + /[\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B]/g, + }, + { + base: "t", + letters: + /[\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787]/g, + }, + { base: "tz", letters: /[\uA729]/g }, + { + base: "u", + letters: + /[\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u00FC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289]/g, + }, + { base: "v", letters: /[\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C]/g }, + { base: "vy", letters: /[\uA761]/g }, + { + base: "w", + letters: + /[\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73]/g, + }, + { base: "x", letters: /[\u0078\u24E7\uFF58\u1E8B\u1E8D]/g }, + { + base: "y", + letters: + /[\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF]/g, + }, + { + base: "z", + letters: + /[\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763]/g, + }, +]; + +export function removeDiacritics(str: string) { + for (const i of defaultDiacriticsRemovalMap) { + str = str.replace(i.letters, i.base); + } + + str = str.replace(/[ؐ-ًؕ-ٖٓ-ٟۖ-ٰٰۭ]/g, ""); + + return str; +} diff --git a/src/server/typesense/utils.ts b/src/server/typesense/utils.ts index 2d2009db..ecb28921 100644 --- a/src/server/typesense/utils.ts +++ b/src/server/typesense/utils.ts @@ -1,3 +1,4 @@ +import { removeDiacritics } from "@/lib/diacritics"; import type { Pagination } from "@/types/pagination"; export const makePagination = ( @@ -24,12 +25,14 @@ export interface SearchOptions { } export const prepareQuery = (q: string) => { - const final = [q]; - - const queryWithoutAl = q.replace(/(al-)/gi, ""); - if (queryWithoutAl !== q) final.push(queryWithoutAl); - - return final.join(" || "); + const prepared = removeDiacritics(q) + .replace(/(al-)/gi, "") + .replace(/(al )/gi, "") + .replace(/(ال)/gi, "") + .replace(/-/gi, " ") + .replace(/[‏.»,!?;:"'،؛؟\-_(){}\[\]<>@#\$%\^&\*\+=/\\`~]/gi, ""); + + return prepared; }; export const weightsMapToQueryBy = (weightsMap: Record) =>