From 23d93ec5b540ec3b634ada5b4bf090b53f4c17c2 Mon Sep 17 00:00:00 2001 From: yoshino-s Date: Tue, 25 Jun 2024 09:59:40 +0800 Subject: [PATCH] feat: meilisearch --- package.json | 14 +- pnpm-lock.yaml | 557 +++++++++++++++++- postcss.config.js => postcss.config.cjs | 2 +- public/_redirects | 1 + src/component/Hits/Hits.module.css | 19 + src/component/Hits/Hits.tsx | 17 + src/component/Pagination/Pagination.tsx | 18 + src/component/ParagraphCard/ParagraphCard.tsx | 28 +- src/component/Refinement/Refinement.tsx | 123 ++++ src/component/ScrollTop/ScrollTop.tsx | 25 + src/component/SearchBox/SearchBox.tsx | 43 ++ src/constants.ts | 9 + src/helper/api.ts | 72 --- src/helper/hooks.tsx | 71 --- src/layout/MainLayout.tsx | 132 +++-- src/page/Paragraph.tsx | 12 +- src/page/Search.tsx | 54 +- src/page/Settings.tsx | 26 +- src/router/index.tsx | 153 ++--- src/store/reducer/options.ts | 28 +- src/types/paragraph.d.ts | 4 +- src/utils/remark.ts | 71 +++ 22 files changed, 1066 insertions(+), 413 deletions(-) rename postcss.config.js => postcss.config.cjs (95%) create mode 100644 public/_redirects create mode 100644 src/component/Hits/Hits.module.css create mode 100644 src/component/Hits/Hits.tsx create mode 100644 src/component/Pagination/Pagination.tsx create mode 100644 src/component/Refinement/Refinement.tsx create mode 100644 src/component/ScrollTop/ScrollTop.tsx create mode 100644 src/component/SearchBox/SearchBox.tsx create mode 100644 src/constants.ts delete mode 100644 src/helper/api.ts delete mode 100644 src/helper/hooks.tsx create mode 100644 src/utils/remark.ts diff --git a/package.json b/package.json index 29bd6d3..7460cc5 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@mantine/core": "^7.6.2", "@mantine/hooks": "^7.6.2", "@mantine/notifications": "^7.6.2", + "@meilisearch/instant-meilisearch": "^0.18.1", "@reduxjs/toolkit": "^2.2.1", "@sentry/react": "^7.108.0", "@sentry/vite-plugin": "^2.16.0", @@ -28,14 +29,23 @@ "dayjs": "^1.11.10", "i18next": "^23.10.1", "i18next-browser-languagedetector": "^7.2.0", + "instantsearch.js": "^4.71.1", "lodash": "^4.17.21", + "meilisearch": "^0.40.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^14.1.0", + "react-instantsearch": "^7.11.1", "react-redux": "^9.1.0", "react-router-dom": "^6.22.3", - "remark": "^15.0.1", - "remark-html": "^16.0.1" + "rehype-highlight": "^7.0.0", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.0", + "remark-gfm": "^4.0.0", + "remark-html": "^16.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "unified": "^11.0.4" }, "devDependencies": { "@babel/core": "^7.24.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ce583d..5c68ebe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ dependencies: '@mantine/notifications': specifier: ^7.6.2 version: 7.6.2(@mantine/core@7.6.2)(@mantine/hooks@7.6.2)(react-dom@18.2.0)(react@18.2.0) + '@meilisearch/instant-meilisearch': + specifier: ^0.18.1 + version: 0.18.1 '@reduxjs/toolkit': specifier: ^2.2.1 version: 2.2.1(react-redux@9.1.0)(react@18.2.0) @@ -38,9 +41,15 @@ dependencies: i18next-browser-languagedetector: specifier: ^7.2.0 version: 7.2.0 + instantsearch.js: + specifier: ^4.71.1 + version: 4.71.1(algoliasearch@4.23.3) lodash: specifier: ^4.17.21 version: 4.17.21 + meilisearch: + specifier: ^0.40.0 + version: 0.40.0 react: specifier: ^18.2.0 version: 18.2.0 @@ -50,18 +59,39 @@ dependencies: react-i18next: specifier: ^14.1.0 version: 14.1.0(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0) + react-instantsearch: + specifier: ^7.11.1 + version: 7.11.1(algoliasearch@4.23.3)(react-dom@18.2.0)(react@18.2.0) react-redux: specifier: ^9.1.0 version: 9.1.0(@types/react@18.2.67)(react@18.2.0)(redux@5.0.1) react-router-dom: specifier: ^6.22.3 version: 6.22.3(react-dom@18.2.0)(react@18.2.0) - remark: - specifier: ^15.0.1 - version: 15.0.1 + rehype-highlight: + specifier: ^7.0.0 + version: 7.0.0 + rehype-sanitize: + specifier: ^6.0.0 + version: 6.0.0 + rehype-stringify: + specifier: ^10.0.0 + version: 10.0.0 + remark-gfm: + specifier: ^4.0.0 + version: 4.0.0 remark-html: specifier: ^16.0.1 version: 16.0.1 + remark-parse: + specifier: ^11.0.0 + version: 11.0.0 + remark-rehype: + specifier: ^11.1.0 + version: 11.1.0 + unified: + specifier: ^11.0.4 + version: 11.0.4 devDependencies: '@babel/core': @@ -235,6 +265,116 @@ packages: resolution: {integrity: sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==} dev: true + /@algolia/cache-browser-local-storage@4.23.3: + resolution: {integrity: sha512-vRHXYCpPlTDE7i6UOy2xE03zHF2C8MEFjPN2v7fRbqVpcOvAUQK81x3Kc21xyb5aSIpYCjWCZbYZuz8Glyzyyg==} + dependencies: + '@algolia/cache-common': 4.23.3 + dev: false + + /@algolia/cache-common@4.23.3: + resolution: {integrity: sha512-h9XcNI6lxYStaw32pHpB1TMm0RuxphF+Ik4o7tcQiodEdpKK+wKufY6QXtba7t3k8eseirEMVB83uFFF3Nu54A==} + dev: false + + /@algolia/cache-in-memory@4.23.3: + resolution: {integrity: sha512-yvpbuUXg/+0rbcagxNT7un0eo3czx2Uf0y4eiR4z4SD7SiptwYTpbuS0IHxcLHG3lq22ukx1T6Kjtk/rT+mqNg==} + dependencies: + '@algolia/cache-common': 4.23.3 + dev: false + + /@algolia/client-account@4.23.3: + resolution: {integrity: sha512-hpa6S5d7iQmretHHF40QGq6hz0anWEHGlULcTIT9tbUssWUriN9AUXIFQ8Ei4w9azD0hc1rUok9/DeQQobhQMA==} + dependencies: + '@algolia/client-common': 4.23.3 + '@algolia/client-search': 4.23.3 + '@algolia/transporter': 4.23.3 + dev: false + + /@algolia/client-analytics@4.23.3: + resolution: {integrity: sha512-LBsEARGS9cj8VkTAVEZphjxTjMVCci+zIIiRhpFun9jGDUlS1XmhCW7CTrnaWeIuCQS/2iPyRqSy1nXPjcBLRA==} + dependencies: + '@algolia/client-common': 4.23.3 + '@algolia/client-search': 4.23.3 + '@algolia/requester-common': 4.23.3 + '@algolia/transporter': 4.23.3 + dev: false + + /@algolia/client-common@4.23.3: + resolution: {integrity: sha512-l6EiPxdAlg8CYhroqS5ybfIczsGUIAC47slLPOMDeKSVXYG1n0qGiz4RjAHLw2aD0xzh2EXZ7aRguPfz7UKDKw==} + dependencies: + '@algolia/requester-common': 4.23.3 + '@algolia/transporter': 4.23.3 + dev: false + + /@algolia/client-personalization@4.23.3: + resolution: {integrity: sha512-3E3yF3Ocr1tB/xOZiuC3doHQBQ2zu2MPTYZ0d4lpfWads2WTKG7ZzmGnsHmm63RflvDeLK/UVx7j2b3QuwKQ2g==} + dependencies: + '@algolia/client-common': 4.23.3 + '@algolia/requester-common': 4.23.3 + '@algolia/transporter': 4.23.3 + dev: false + + /@algolia/client-search@4.23.3: + resolution: {integrity: sha512-P4VAKFHqU0wx9O+q29Q8YVuaowaZ5EM77rxfmGnkHUJggh28useXQdopokgwMeYw2XUht49WX5RcTQ40rZIabw==} + dependencies: + '@algolia/client-common': 4.23.3 + '@algolia/requester-common': 4.23.3 + '@algolia/transporter': 4.23.3 + dev: false + + /@algolia/events@4.0.1: + resolution: {integrity: sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==} + dev: false + + /@algolia/logger-common@4.23.3: + resolution: {integrity: sha512-y9kBtmJwiZ9ZZ+1Ek66P0M68mHQzKRxkW5kAAXYN/rdzgDN0d2COsViEFufxJ0pb45K4FRcfC7+33YB4BLrZ+g==} + dev: false + + /@algolia/logger-console@4.23.3: + resolution: {integrity: sha512-8xoiseoWDKuCVnWP8jHthgaeobDLolh00KJAdMe9XPrWPuf1by732jSpgy2BlsLTaT9m32pHI8CRfrOqQzHv3A==} + dependencies: + '@algolia/logger-common': 4.23.3 + dev: false + + /@algolia/recommend@4.23.3: + resolution: {integrity: sha512-9fK4nXZF0bFkdcLBRDexsnGzVmu4TSYZqxdpgBW2tEyfuSSY54D4qSRkLmNkrrz4YFvdh2GM1gA8vSsnZPR73w==} + dependencies: + '@algolia/cache-browser-local-storage': 4.23.3 + '@algolia/cache-common': 4.23.3 + '@algolia/cache-in-memory': 4.23.3 + '@algolia/client-common': 4.23.3 + '@algolia/client-search': 4.23.3 + '@algolia/logger-common': 4.23.3 + '@algolia/logger-console': 4.23.3 + '@algolia/requester-browser-xhr': 4.23.3 + '@algolia/requester-common': 4.23.3 + '@algolia/requester-node-http': 4.23.3 + '@algolia/transporter': 4.23.3 + dev: false + + /@algolia/requester-browser-xhr@4.23.3: + resolution: {integrity: sha512-jDWGIQ96BhXbmONAQsasIpTYWslyjkiGu0Quydjlowe+ciqySpiDUrJHERIRfELE5+wFc7hc1Q5hqjGoV7yghw==} + dependencies: + '@algolia/requester-common': 4.23.3 + dev: false + + /@algolia/requester-common@4.23.3: + resolution: {integrity: sha512-xloIdr/bedtYEGcXCiF2muajyvRhwop4cMZo+K2qzNht0CMzlRkm8YsDdj5IaBhshqfgmBb3rTg4sL4/PpvLYw==} + dev: false + + /@algolia/requester-node-http@4.23.3: + resolution: {integrity: sha512-zgu++8Uj03IWDEJM3fuNl34s746JnZOWn1Uz5taV1dFyJhVM/kTNw9Ik7YJWiUNHJQXcaD8IXD1eCb0nq/aByA==} + dependencies: + '@algolia/requester-common': 4.23.3 + dev: false + + /@algolia/transporter@4.23.3: + resolution: {integrity: sha512-Wjl5gttqnf/gQKJA+dafnD0Y6Yw97yvfY8R9h0dQltX1GXTgNs1zWgvtWW0tHl1EgMdhAyw189uWiZMnL3QebQ==} + dependencies: + '@algolia/cache-common': 4.23.3 + '@algolia/logger-common': 4.23.3 + '@algolia/requester-common': 4.23.3 + dev: false + /@ampproject/remapping@2.3.0: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -2207,6 +2347,14 @@ packages: react: 18.2.0 dev: true + /@meilisearch/instant-meilisearch@0.18.1: + resolution: {integrity: sha512-KxBoEaI1+CQQaSbSZZEllIUwsNofALH0RG+/bFMkNhavuzJn982zOTC7xYtYmXC6nsj32MmAe+rDBtazycDJQQ==} + dependencies: + meilisearch: 0.40.0 + transitivePeerDependencies: + - encoding + dev: false + /@ndelangen/get-tarball@3.0.9: resolution: {integrity: sha512-9JKTEik4vq+yGosHYhZ1tiH/3WpUS0Nh0kej4Agndhox8pAdWhEx5knFVRcb/ya9knCRCs1rPxNrSXTDdfVqpA==} dependencies: @@ -3618,6 +3766,10 @@ packages: resolution: {integrity: sha512-w5jZ0ee+HaPOaX25X2/2oGR/7rgAQSYII7X7pp0m9KgBfMP7uKfMfTvcpl5Dj+eDBbpxKGiqE+flqDr6XTd2RA==} dev: true + /@types/dom-speech-recognition@0.0.1: + resolution: {integrity: sha512-udCxb8DvjcDKfk1WTBzDsxFbLgYxmQGKrE/ricoMqHRNjSlSUCcamVTA5lIQqzY10mY5qCY0QDwBfFEwhfoDPw==} + dev: false + /@types/ejs@3.1.5: resolution: {integrity: sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==} dev: true @@ -3674,6 +3826,10 @@ packages: resolution: {integrity: sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw==} dev: true + /@types/google.maps@3.55.10: + resolution: {integrity: sha512-XbDu2MIvcKgN+MBrufjWcsQRtXTbrBGBKperbhMLnPSq4770+pvlR66Oqq/Ub4AVkmGc9QciCfwPZpVCLaKAOw==} + dev: false + /@types/graceful-fs@4.1.9: resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} dependencies: @@ -3685,6 +3841,10 @@ packages: dependencies: '@types/unist': 3.0.2 + /@types/hogan.js@3.0.5: + resolution: {integrity: sha512-/uRaY3HGPWyLqOyhgvW9Aa43BNnLZrNeQxl2p8wqId4UHMfPKolSB+U7BlZyO1ng7MkLnyEAItsBzCG0SDhqrA==} + dev: false + /@types/http-errors@2.0.4: resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} dev: true @@ -3779,7 +3939,6 @@ packages: /@types/qs@6.9.13: resolution: {integrity: sha512-iLR+1vTTJ3p0QaOUq6ACbY1mzKTODFDT/XedZI8BksOotFmL4ForwDfRQ/DZeuTHR7/2i4lI1D203gdfxuqTlA==} - dev: true /@types/range-parser@1.2.7: resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} @@ -4245,6 +4404,10 @@ packages: deprecated: Use your platform's native atob() and btoa() methods instead dev: true + /abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + dev: false + /accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -4372,6 +4535,35 @@ packages: uri-js: 4.4.1 dev: true + /algoliasearch-helper@3.21.0(algoliasearch@4.23.3): + resolution: {integrity: sha512-hjVOrL15I3Y3K8xG0icwG1/tWE+MocqBrhW6uVBWpU+/kVEMK0BnM2xdssj6mZM61eJ4iRxHR0djEI3ENOpR8w==} + peerDependencies: + algoliasearch: '>= 3.1 < 6' + dependencies: + '@algolia/events': 4.0.1 + algoliasearch: 4.23.3 + dev: false + + /algoliasearch@4.23.3: + resolution: {integrity: sha512-Le/3YgNvjW9zxIQMRhUHuhiUjAlKY/zsdZpfq4dlLqg6mEm0nL6yk+7f2hDOtLpxsgE4jSzDmvHL7nXdBp5feg==} + dependencies: + '@algolia/cache-browser-local-storage': 4.23.3 + '@algolia/cache-common': 4.23.3 + '@algolia/cache-in-memory': 4.23.3 + '@algolia/client-account': 4.23.3 + '@algolia/client-analytics': 4.23.3 + '@algolia/client-common': 4.23.3 + '@algolia/client-personalization': 4.23.3 + '@algolia/client-search': 4.23.3 + '@algolia/logger-common': 4.23.3 + '@algolia/logger-console': 4.23.3 + '@algolia/recommend': 4.23.3 + '@algolia/requester-browser-xhr': 4.23.3 + '@algolia/requester-common': 4.23.3 + '@algolia/requester-node-http': 4.23.3 + '@algolia/transporter': 4.23.3 + dev: false + /ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -5242,6 +5434,14 @@ packages: - ts-node dev: true + /cross-fetch@3.1.8: + resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==} + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + dev: false + /cross-fetch@4.0.0: resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} dependencies: @@ -5973,6 +6173,11 @@ packages: engines: {node: '>=10'} dev: true + /escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + dev: false + /escodegen@2.1.0: resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} engines: {node: '>=6.0'} @@ -6962,7 +7167,6 @@ packages: resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} dependencies: '@types/hast': 3.0.4 - dev: true /hast-util-parse-selector@4.0.0: resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} @@ -7031,6 +7235,15 @@ packages: '@types/hast': 3.0.4 dev: true + /hast-util-to-text@4.0.2: + resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.2 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + dev: false + /hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} dependencies: @@ -7047,6 +7260,19 @@ packages: space-separated-tokens: 2.0.2 dev: false + /highlight.js@11.9.0: + resolution: {integrity: sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==} + engines: {node: '>=12.0.0'} + dev: false + + /hogan.js@3.0.2: + resolution: {integrity: sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==} + hasBin: true + dependencies: + mkdirp: 0.3.0 + nopt: 1.0.10 + dev: false + /hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} dependencies: @@ -7057,6 +7283,10 @@ packages: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: true + /htm@3.1.1: + resolution: {integrity: sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==} + dev: false + /html-encoding-sniffer@3.0.0: resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} engines: {node: '>=12'} @@ -7218,6 +7448,32 @@ packages: semver: 6.3.1 dev: true + /instantsearch-ui-components@0.6.0: + resolution: {integrity: sha512-Jj3F9D46ef8VtzVZTgWsy79P25Q5nhI5XzK0NqfUVVI5yI3vA/3NkvKYtBHBlz50DDyBm6t9kIn/ZfpOwENm2A==} + dependencies: + '@babel/runtime': 7.24.1 + dev: false + + /instantsearch.js@4.71.1(algoliasearch@4.23.3): + resolution: {integrity: sha512-4AvEPadnDBf0NsCCw+a1GjmMFEZ3zQzQhCe51cFPLYRXnRyKw5bLvRVaNQckiDG+vl7bPyJrWn5YAh5UhBwA+w==} + peerDependencies: + algoliasearch: '>= 3.1 < 6' + dependencies: + '@algolia/events': 4.0.1 + '@types/dom-speech-recognition': 0.0.1 + '@types/google.maps': 3.55.10 + '@types/hogan.js': 3.0.5 + '@types/qs': 6.9.13 + algoliasearch: 4.23.3 + algoliasearch-helper: 3.21.0(algoliasearch@4.23.3) + hogan.js: 3.0.2 + htm: 3.1.1 + instantsearch-ui-components: 0.6.0 + preact: 10.22.0 + qs: 6.9.7 + search-insights: 2.14.0 + dev: false + /internal-slot@1.0.7: resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} engines: {node: '>= 0.4'} @@ -8348,6 +8604,14 @@ packages: get-func-name: 2.0.2 dev: true + /lowlight@3.1.0: + resolution: {integrity: sha512-CEbNVoSikAxwDMDPjXlqlFYiZLkDJHwyGu/MfOsJnF3d7f3tds5J3z8s/l9TMXhzfsJCCJEAsD78842mwmg0PQ==} + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + highlight.js: 11.9.0 + dev: false + /lru-cache@10.2.0: resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} engines: {node: 14 || >=16.14} @@ -8425,6 +8689,10 @@ packages: resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} dev: true + /markdown-table@3.0.3: + resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==} + dev: false + /markdown-to-jsx@7.3.2(react@18.2.0): resolution: {integrity: sha512-B+28F5ucp83aQm+OxNrPkS8z0tMKaeHiy0lHJs3LqCyDQFtWuenaIrkaVTgAm1pf1AU85LXltva86hlaT17i8Q==} engines: {node: '>= 10'} @@ -8434,6 +8702,15 @@ packages: react: 18.2.0 dev: true + /mdast-util-find-and-replace@3.0.1: + resolution: {integrity: sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==} + dependencies: + '@types/mdast': 4.0.3 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + dev: false + /mdast-util-from-markdown@2.0.0: resolution: {integrity: sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA==} dependencies: @@ -8453,6 +8730,75 @@ packages: - supports-color dev: false + /mdast-util-gfm-autolink-literal@2.0.0: + resolution: {integrity: sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg==} + dependencies: + '@types/mdast': 4.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.1 + micromark-util-character: 2.1.0 + dev: false + + /mdast-util-gfm-footnote@2.0.0: + resolution: {integrity: sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==} + dependencies: + '@types/mdast': 4.0.3 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.0 + mdast-util-to-markdown: 2.1.0 + micromark-util-normalize-identifier: 2.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + dependencies: + '@types/mdast': 4.0.3 + mdast-util-from-markdown: 2.0.0 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + dev: false + + /mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + dependencies: + '@types/mdast': 4.0.3 + devlop: 1.1.0 + markdown-table: 3.0.3 + mdast-util-from-markdown: 2.0.0 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + dev: false + + /mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + dependencies: + '@types/mdast': 4.0.3 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.0 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + dev: false + + /mdast-util-gfm@3.0.0: + resolution: {integrity: sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==} + dependencies: + mdast-util-from-markdown: 2.0.0 + mdast-util-gfm-autolink-literal: 2.0.0 + mdast-util-gfm-footnote: 2.0.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + dev: false + /mdast-util-phrasing@4.1.0: resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} dependencies: @@ -8498,6 +8844,14 @@ packages: engines: {node: '>= 0.6'} dev: true + /meilisearch@0.40.0: + resolution: {integrity: sha512-BoRhQMr2mBFLEeCfsvPluksGb01IaOiWvV3Deu3iEY+yYJ4jdGTu+IQi5FCjKlNQ7/TMWSN2XUToSgvH1tj0BQ==} + dependencies: + cross-fetch: 3.1.8 + transitivePeerDependencies: + - encoding + dev: false + /memoizee@0.4.15: resolution: {integrity: sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==} dependencies: @@ -8556,6 +8910,78 @@ packages: micromark-util-types: 2.0.0 dev: false + /micromark-extension-gfm-autolink-literal@2.0.0: + resolution: {integrity: sha512-rTHfnpt/Q7dEAK1Y5ii0W8bhfJlVJFnJMHIPisfPK3gpVNuOP0VnRl96+YJ3RYWV/P4gFeQoGKNlT3RhuvpqAg==} + dependencies: + micromark-util-character: 2.1.0 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + dev: false + + /micromark-extension-gfm-footnote@2.0.0: + resolution: {integrity: sha512-6Rzu0CYRKDv3BfLAUnZsSlzx3ak6HAoI85KTiijuKIz5UxZxbUI+pD6oHgw+6UtQuiRwnGRhzMmPRv4smcz0fg==} + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.0 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + dev: false + + /micromark-extension-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-c3BR1ClMp5fxxmwP6AoOY2fXO9U8uFMKs4ADD66ahLTNcwzSCyRVU4k7LPV5Nxo/VJiR4TdzxRQY2v3qIUceCw==} + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-classify-character: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + dev: false + + /micromark-extension-gfm-table@2.0.0: + resolution: {integrity: sha512-PoHlhypg1ItIucOaHmKE8fbin3vTLpDOUg8KAr8gRCF1MOZI9Nquq2i/44wFvviM4WuxJzc3demT8Y3dkfvYrw==} + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + dev: false + + /micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + dependencies: + micromark-util-types: 2.0.0 + dev: false + + /micromark-extension-gfm-task-list-item@2.0.1: + resolution: {integrity: sha512-cY5PzGcnULaN5O7T+cOzfMoHjBW7j+T9D2sucA5d/KbsBTPcYdebm9zUd9zzdgJGCwahV+/W78Z3nbulBYVbTw==} + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + dev: false + + /micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + dependencies: + micromark-extension-gfm-autolink-literal: 2.0.0 + micromark-extension-gfm-footnote: 2.0.0 + micromark-extension-gfm-strikethrough: 2.0.0 + micromark-extension-gfm-table: 2.0.0 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.0.1 + micromark-util-combine-extensions: 2.0.0 + micromark-util-types: 2.0.0 + dev: false + /micromark-factory-destination@2.0.0: resolution: {integrity: sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==} dependencies: @@ -8813,6 +9239,11 @@ packages: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} dev: true + /mkdirp@0.3.0: + resolution: {integrity: sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew==} + deprecated: Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.) + dev: false + /mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} @@ -8886,6 +9317,13 @@ packages: /node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + /nopt@1.0.10: + resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==} + hasBin: true + dependencies: + abbrev: 1.1.1 + dev: false + /normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: @@ -9357,6 +9795,10 @@ packages: source-map-js: 1.2.0 dev: true + /preact@10.22.0: + resolution: {integrity: sha512-RRurnSjJPj4rp5K6XoP45Ui33ncb7e4H7WiOHVpjbkvqvA3U+N8Z6Qbo0AE6leGYBV66n8EhEaFixvIu3SkxFw==} + dev: false + /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -9498,6 +9940,11 @@ packages: side-channel: 1.0.6 dev: true + /qs@6.9.7: + resolution: {integrity: sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==} + engines: {node: '>=0.6'} + dev: false + /querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} dev: true @@ -9582,6 +10029,36 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + /react-instantsearch-core@7.11.1(algoliasearch@4.23.3)(react@18.2.0): + resolution: {integrity: sha512-FsfEvefr3AZtsN4NYLxrqWFvzlrR73ejNX9f/1NBDERxaM+dZFKRfWpRXrA1QSwFeAppRmyC5+TMmga8OK8M0g==} + peerDependencies: + algoliasearch: '>= 3.1 < 5' + react: '>= 16.8.0 < 19' + dependencies: + '@babel/runtime': 7.24.1 + algoliasearch: 4.23.3 + algoliasearch-helper: 3.21.0(algoliasearch@4.23.3) + instantsearch.js: 4.71.1(algoliasearch@4.23.3) + react: 18.2.0 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false + + /react-instantsearch@7.11.1(algoliasearch@4.23.3)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-XiCH2SfifJAk5kfdT1Mi3HxX3MzeJL72oF1kUltgmD7fqEi8HsK+vn2wGxAvSGhuLzltV2Lg3xIXd4OAocjeWA==} + peerDependencies: + algoliasearch: '>= 3.1 < 5' + react: '>= 16.8.0 < 19' + react-dom: '>= 16.8.0 < 19' + dependencies: + '@babel/runtime': 7.24.1 + algoliasearch: 4.23.3 + instantsearch-ui-components: 0.6.0 + instantsearch.js: 4.71.1(algoliasearch@4.23.3) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-instantsearch-core: 7.11.1(algoliasearch@4.23.3)(react@18.2.0) + dev: false + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -9904,6 +10381,23 @@ packages: unist-util-visit: 5.0.0 dev: true + /rehype-highlight@7.0.0: + resolution: {integrity: sha512-QtobgRgYoQaK6p1eSr2SD1i61f7bjF2kZHAQHxeCHAuJf7ZUDMvQ7owDq9YTkmar5m5TSUol+2D3bp3KfJf/oA==} + dependencies: + '@types/hast': 3.0.4 + hast-util-to-text: 4.0.2 + lowlight: 3.1.0 + unist-util-visit: 5.0.0 + vfile: 6.0.1 + dev: false + + /rehype-sanitize@6.0.0: + resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==} + dependencies: + '@types/hast': 3.0.4 + hast-util-sanitize: 5.0.1 + dev: false + /rehype-slug@6.0.0: resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==} dependencies: @@ -9914,6 +10408,27 @@ packages: unist-util-visit: 5.0.0 dev: true + /rehype-stringify@10.0.0: + resolution: {integrity: sha512-1TX1i048LooI9QoecrXy7nGFFbFSufxVRAfc6Y9YMRAi56l+oB0zP51mLSV312uRuvVLPV1opSlJmslozR1XHQ==} + dependencies: + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.0 + unified: 11.0.4 + dev: false + + /remark-gfm@4.0.0: + resolution: {integrity: sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==} + dependencies: + '@types/mdast': 4.0.3 + mdast-util-gfm: 3.0.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.4 + transitivePeerDependencies: + - supports-color + dev: false + /remark-html@16.0.1: resolution: {integrity: sha512-B9JqA5i0qZe0Nsf49q3OXyGvyXuZFDzAP2iOFLEumymuYJITVpiH1IgsTEwTpdptDmZlMDMWeDmSawdaJIGCXQ==} dependencies: @@ -9935,6 +10450,16 @@ packages: - supports-color dev: false + /remark-rehype@11.1.0: + resolution: {integrity: sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g==} + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.3 + mdast-util-to-hast: 13.1.0 + unified: 11.0.4 + vfile: 6.0.1 + dev: false + /remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} dependencies: @@ -9943,17 +10468,6 @@ packages: unified: 11.0.4 dev: false - /remark@15.0.1: - resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} - dependencies: - '@types/mdast': 4.0.3 - remark-parse: 11.0.0 - remark-stringify: 11.0.0 - unified: 11.0.4 - transitivePeerDependencies: - - supports-color - dev: false - /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -10139,6 +10653,10 @@ packages: ajv-keywords: 5.1.0(ajv@8.12.0) dev: true + /search-insights@2.14.0: + resolution: {integrity: sha512-OLN6MsPMCghDOqlCtsIsYgtsC0pnwVTyT9Mu6A3ewOj1DxvzZF6COrn2g86E/c05xbktB0XN04m/t1Z+n+fTGw==} + dev: false + /semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -11041,6 +11559,13 @@ packages: crypto-random-string: 2.0.0 dev: true + /unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + dependencies: + '@types/unist': 3.0.2 + unist-util-is: 6.0.0 + dev: false + /unist-util-is@6.0.0: resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} dependencies: diff --git a/postcss.config.js b/postcss.config.cjs similarity index 95% rename from postcss.config.js rename to postcss.config.cjs index f47d9c1..c035083 100644 --- a/postcss.config.js +++ b/postcss.config.cjs @@ -1,5 +1,5 @@ // eslint-disable-next-line no-undef -export default { +module.exports = { plugins: { "postcss-preset-mantine": {}, "postcss-simple-vars": { diff --git a/public/_redirects b/public/_redirects new file mode 100644 index 0000000..f824337 --- /dev/null +++ b/public/_redirects @@ -0,0 +1 @@ +/* /index.html 200 \ No newline at end of file diff --git a/src/component/Hits/Hits.module.css b/src/component/Hits/Hits.module.css new file mode 100644 index 0000000..6049aba --- /dev/null +++ b/src/component/Hits/Hits.module.css @@ -0,0 +1,19 @@ +.img-wrapper{ + column-count: 3; + column-gap: 10px; + + counter-reset: count; + margin: 0 auto; + + @media (max-width: $mantine-breakpoint-md) { + column-count: 2; + } + @media (max-width: $mantine-breakpoint-sm) { + column-count: 1; + } + + &>div{ + position: relative; + margin-bottom: 10px; + } +} diff --git a/src/component/Hits/Hits.tsx b/src/component/Hits/Hits.tsx new file mode 100644 index 0000000..3a3330c --- /dev/null +++ b/src/component/Hits/Hits.tsx @@ -0,0 +1,17 @@ +import { Box } from "@mantine/core"; +import { useHits, UseHitsProps } from "react-instantsearch"; +import { ParagraphCard } from "../ParagraphCard/ParagraphCard"; +import styles from "./Hits.module.css"; + +export default function Hits(props: UseHitsProps) { + const { results } = useHits(props); + return ( + + {results?.hits?.map((hit) => ( + + + + ))} + + ); +} diff --git a/src/component/Pagination/Pagination.tsx b/src/component/Pagination/Pagination.tsx new file mode 100644 index 0000000..b3c71e8 --- /dev/null +++ b/src/component/Pagination/Pagination.tsx @@ -0,0 +1,18 @@ +import { Center, Group, Pagination as MantinePagination } from "@mantine/core"; +import { usePagination, UsePaginationProps } from "react-instantsearch"; + +export default function Pagination(props: UsePaginationProps) { + const { currentRefinement, nbPages, refine } = usePagination(props); + + return ( +
+ + refine(value - 1)} + /> + +
+ ); +} diff --git a/src/component/ParagraphCard/ParagraphCard.tsx b/src/component/ParagraphCard/ParagraphCard.tsx index 0e829dd..6087be5 100644 --- a/src/component/ParagraphCard/ParagraphCard.tsx +++ b/src/component/ParagraphCard/ParagraphCard.tsx @@ -8,47 +8,33 @@ dayjs.extend(relativeTime); export function ParagraphCard({ cover, title, - "@timestamp": time, + time, author, tags, - _id, + id, }: Paragraph) { - const url = `/paragraph/${_id}`; + const url = `/paragraph/${id}`; return ( - + {cover && } - + {title} {tags.map((tag, index) => ( - <> - - {tag} - - + {tag} ))} - - {author} - + {author} • diff --git a/src/component/Refinement/Refinement.tsx b/src/component/Refinement/Refinement.tsx new file mode 100644 index 0000000..d91f1aa --- /dev/null +++ b/src/component/Refinement/Refinement.tsx @@ -0,0 +1,123 @@ +import { SourceLabelMap } from "@/constants"; +import { + Badge, + Box, + CheckIcon, + ComboboxItem, + ComboboxLikeRenderOptionInput, + Group, + MultiSelect, + rem, + Select, +} from "@mantine/core"; +import { useEffect } from "react"; +import { + useHitsPerPage, + useRefinementList, + useSortBy, +} from "react-instantsearch"; + +const sortItems = [{ value: "paragraph:time:desc", label: "Newest" }]; +const hitsPerPageItems = [ + { value: 20, label: "20", default: true }, + { value: 40, label: "40" }, + { value: 60, label: "60" }, + { value: 100, label: "100" }, +]; + +export default function Refinement() { + const { items, refine } = useRefinementList({ attribute: "source" }); + const { currentRefinement, refine: refineSortBy } = useSortBy({ + items: sortItems, + }); + const hitsPerPage = useHitsPerPage({ + items: hitsPerPageItems, + }); + + const currentVal = items + .filter((item) => item.isRefined) + .map((item) => item.value); + + useEffect(() => { + refineSortBy(sortItems[0].value); + }, [refineSortBy]); + + function SelectItem(props: ComboboxLikeRenderOptionInput) { + return ( + + + {currentVal.includes(props.option.value) && ( + + )} + {props.option.label} + + + {items.find((item) => item.value === props.option.value)?.count} + + + ); + } + + return ( + + + ({ + value: item.value.toString(), + label: item.label.toString(), + }))} + value={hitsPerPage.items + .find((item) => item.isRefined) + ?.value.toString()} + onChange={(value) => value && hitsPerPage.refine(parseInt(value))} + label="Hits per page" + /> + + + ); +} diff --git a/src/component/ScrollTop/ScrollTop.tsx b/src/component/ScrollTop/ScrollTop.tsx new file mode 100644 index 0000000..2771bfc --- /dev/null +++ b/src/component/ScrollTop/ScrollTop.tsx @@ -0,0 +1,25 @@ +import { Affix, Button, rem, Transition } from "@mantine/core"; +import { useWindowScroll } from "@mantine/hooks"; +import { IconArrowUp } from "@tabler/icons-react"; + +export default function ScrollTop() { + const [scroll, scrollTo] = useWindowScroll(); + + return ( + + 0}> + {(transitionStyles) => ( + + )} + + + ); +} diff --git a/src/component/SearchBox/SearchBox.tsx b/src/component/SearchBox/SearchBox.tsx new file mode 100644 index 0000000..29af120 --- /dev/null +++ b/src/component/SearchBox/SearchBox.tsx @@ -0,0 +1,43 @@ +import { rem, TextInput } from "@mantine/core"; +import { IconSearch } from "@tabler/icons-react"; +import { useCallback, useState } from "react"; +import { useSearchBox, UseSearchBoxProps } from "react-instantsearch"; + +export default function SearchBox(props: UseSearchBoxProps) { + const { query, refine } = useSearchBox(props); + const [inputValue, setInputValue] = useState(query); + + const setQuery = useCallback( + (value: string) => { + setInputValue(value); + refine(value); + }, + [refine, setInputValue], + ); + + return ( +
{ + e.preventDefault(); + e.stopPropagation(); + }} + > + + } + value={inputValue} + onChange={(event) => setQuery(event.currentTarget.value)} + /> + + ); +} diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..03d6c18 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,9 @@ +export const SourceLabelMap: Record = { + tttang: "跳跳糖", + secin: "Sec-In", + seebug: "Seebug", + wechat: "微信公众号", + xianzhi: "先知", + anquanke: "安全客", + freebuf: "FreeBuf", +}; diff --git a/src/helper/api.ts b/src/helper/api.ts deleted file mode 100644 index c902386..0000000 --- a/src/helper/api.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { notifications } from "@mantine/notifications"; -import axios, { AxiosResponse } from "axios"; -import { merge } from "lodash"; - -export interface PaginationParams { - skip?: number; - take?: number; -} - -const api = axios.create({ - withCredentials: false, - auth: { - username: "viewer", - password: "publicviewer1", - }, -}); - -api.interceptors.response.use( - (value: AxiosResponse) => { - if (value.data.error) { - notifications.show({ - title: "API Error on " + value.config.url, - message: value.data.error, - color: "red", - autoClose: true, - }); - } - return value; - }, - (error: any) => { - const value: AxiosResponse = error.response; - if (value.data.status !== 200) { - notifications.show({ - title: "API Error on " + value.config.url, - message: JSON.stringify(value.data), - color: "red", - autoClose: true, - }); - } - }, -); - -export class SearchApi { - static async search( - baseUrl: string, - query: ZincQueryForSDK, - ): Promise { - const { data } = await api.post( - new URL("/api/paragraph/_search", baseUrl).toString(), - query, - ); - return data; - } - static wrapParagraph(s3Url: string, paragraph: Paragraph) { - const RE = /https:\/\/s3\.yoshino-s\.xyz/g; - if (paragraph.cover) { - paragraph.cover = paragraph.cover.replace(RE, s3Url); - } - paragraph.content = paragraph.content?.replace(RE, s3Url); - - return paragraph; - } - static async getParagraph(baseUrl: string, id: string) { - const { data } = await api.get( - new URL(`/api/paragraph/_doc/${id}`, baseUrl).toString(), - ); - return merge(data._source, { - _id: data._id, - "@timestamp": data["@timestamp"], - }); - } -} diff --git a/src/helper/hooks.tsx b/src/helper/hooks.tsx deleted file mode 100644 index 4b3501f..0000000 --- a/src/helper/hooks.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { Pagination } from "@mantine/core"; -import { useEffect, useState } from "react"; -import { useSearchParams } from "react-router-dom"; - -import { useOptionsState } from "@/store/module/options"; -import { useDebounceCallback, useMediaQuery } from "@mantine/hooks"; -import { merge } from "lodash"; -import { SearchApi } from "./api"; - -export function usePaginationData(query: ZincQueryForSDK) { - const [params, setParams] = useSearchParams({ - page: "1", - size: "10", - }); - const { state: options } = useOptionsState(); - const [total, setTotal] = useState(0); - const [data, setData] = useState([]); - const [take, _] = useState(parseInt(params.get("size") || "10")); - const [page, setPage] = useState(parseInt(params.get("page") || "1")); - const isMobile = useMediaQuery("(max-width: 768px)"); - - const update = useDebounceCallback(async function update() { - console.log("query", query, page, take, options); - const resp = await SearchApi.search( - options.zincsearchUrl, - merge( - { - search_type: "matchall", - sort_fields: ["-@timestamp"], - _source: ["title", "cover", "author", "tags"], - }, - query, - { - from: (page - 1) * take, - max_results: take, - }, - ), - ); - setTotal(resp.hits.total.value); - setData( - resp.hits.hits.map((hit) => - SearchApi.wrapParagraph( - options.s3Url, - merge({ _id: hit._id, "@timestamp": hit["@timestamp"] }, hit._source), - ), - ) as T[], - ); - }, 200); - - useEffect(update, [query, page, take, options, update]); - - useEffect(() => { - setParams({ - size: take.toString(), - page: page.toString(), - }); - }, [take, page, setParams]); - - return { - data, - page, - pagination: ( - - ), - }; -} diff --git a/src/layout/MainLayout.tsx b/src/layout/MainLayout.tsx index 392d77d..8c8c78b 100644 --- a/src/layout/MainLayout.tsx +++ b/src/layout/MainLayout.tsx @@ -1,55 +1,69 @@ +import SearchBox from "@/component/SearchBox/SearchBox"; import { - Affix, AppShell, Avatar, - Button, Center, Group, - Text, - TextInput, - Transition, - UnstyledButton, rem, + UnstyledButton, } from "@mantine/core"; -import { Suspense, useCallback, useState } from "react"; -import { Outlet, useNavigate } from "react-router"; +import { Suspense, useEffect, useState } from "react"; +import { Outlet, useLocation } from "react-router"; -import { TitleContext } from "@/component/Header/Header"; +import ScrollTop from "@/component/ScrollTop/ScrollTop"; import Loading from "@/page/Loading"; -import { useForm } from "@mantine/form"; -import { useHeadroom, useMediaQuery, useWindowScroll } from "@mantine/hooks"; -import { IconArrowUp, IconSearch, IconSettings } from "@tabler/icons-react"; +import { useHeadroom } from "@mantine/hooks"; +import { IconSettings } from "@tabler/icons-react"; import { Link } from "react-router-dom"; +import { useAppSelector } from "@/store"; +import { + instantMeiliSearch, + InstantMeiliSearchInstance, +} from "@meilisearch/instant-meilisearch"; +import { InstantSearch } from "react-instantsearch"; + +import { singleIndex } from "instantsearch.js/es/lib/stateMappings"; + export default function MainLayout() { - const [title, setTitle] = useState(""); const pinned = useHeadroom({ fixedAt: 60 }); + const selector = useAppSelector((state) => state.options); - const [scroll, scrollTo] = useWindowScroll(); - const isMobile = useMediaQuery("(max-width: 768px)"); + const path = useLocation().pathname; - const navigate = useNavigate(); + const isSearchPage = path === "/"; - const form = useForm({ - initialValues: { - search: "", - }, - }); + const [searchClient, setSearchClient] = + useState(); - const search = useCallback( - function submit({ search }: { search: string }) { - console.log(search); - navigate(`/search/${encodeURIComponent(search)}`); - }, - [navigate], - ); + useEffect(() => { + const { meilisearchUrl, meilisearchToken } = selector; + const { searchClient } = instantMeiliSearch( + meilisearchUrl, + meilisearchToken, + { + finitePagination: true, + meiliSearchParams: { + hybrid: {}, + attributesToRetrieve: [ + "cover", + "title", + "time", + "author", + "tags", + "id", + ], + }, + }, + ); + setSearchClient(searchClient); + }, [selector, setSearchClient]); - return ( - + const shell = ( + <> @@ -58,26 +72,9 @@ export default function MainLayout() { DS - {!isMobile && ( - - {title} - - )}
- -
- - } - /> - + {isSearchPage && }
@@ -92,21 +89,26 @@ export default function MainLayout() { - - 0}> - {(transitionStyles) => ( - - )} - - - + + + ); + + return ( + <> + {searchClient && + (isSearchPage ? ( + + {shell} + + ) : ( + shell + ))} + ); } diff --git a/src/page/Paragraph.tsx b/src/page/Paragraph.tsx index ba52592..e4047b6 100644 --- a/src/page/Paragraph.tsx +++ b/src/page/Paragraph.tsx @@ -1,4 +1,3 @@ -import { TitleContext } from "@/component/Header/Header"; import { Badge, Container, @@ -9,7 +8,6 @@ import { } from "@mantine/core"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; -import { useContext, useEffect } from "react"; import { useLoaderData } from "react-router"; import { Link } from "react-router-dom"; @@ -55,12 +53,10 @@ function stripStyles(content: string) { dayjs.extend(relativeTime); export default function ParagraphPage() { - const [_title, setTitle] = useContext(TitleContext); const paragraph = useLoaderData() as Paragraph; + console.log(paragraph); - useEffect(() => { - setTitle(paragraph.title); - }, [setTitle, paragraph.title]); + const content = stripStyles(paragraph.content); return ( @@ -69,7 +65,7 @@ export default function ParagraphPage() { {" "} - {dayjs().to(dayjs(paragraph["@timestamp"]))} + {dayjs().to(dayjs(paragraph.time))} diff --git a/src/page/Search.tsx b/src/page/Search.tsx index aedd26c..61d2120 100644 --- a/src/page/Search.tsx +++ b/src/page/Search.tsx @@ -1,51 +1,19 @@ -import { Container, Grid, Group } from "@mantine/core"; -import { useContext, useEffect } from "react"; -import { useLoaderData, useLocation, useParams } from "react-router"; +import { Container, Grid, Stack } from "@mantine/core"; -import { ParagraphCard } from "../component/ParagraphCard/ParagraphCard"; - -import { TitleContext } from "@/component/Header/Header"; -import { usePaginationData } from "@/helper/hooks"; +import Hits from "@/component/Hits/Hits"; +import Pagination from "@/component/Pagination/Pagination"; +import Refinement from "@/component/Refinement/Refinement"; export default function SearchPage() { - const [_title, setTitle] = useContext(TitleContext); - - const params = useLoaderData() as ZincQueryForSDK; - - const { - page, - pagination, - data: paragraphs, - } = usePaginationData(params); - - const location = useLocation(); - const param = useParams(); - - useEffect(() => { - let action = "Index"; - if (location.pathname.startsWith("/search/")) { - action = "Search"; - } else if (location.pathname.startsWith("/tag/")) { - action = `Tag ${param.tag}`; - } else if (location.pathname.startsWith("/author/")) { - action = `Author ${param.author}`; - } - const title = `${action} Page ${page}`; - setTitle(title); - }, [page, location, param, setTitle]); - return ( - - {paragraphs.map((paragraph) => { - return ( - - - - ); - })} - - {pagination} + + + + + + + ); } diff --git a/src/page/Settings.tsx b/src/page/Settings.tsx index ea766cd..98cdd40 100644 --- a/src/page/Settings.tsx +++ b/src/page/Settings.tsx @@ -5,7 +5,11 @@ import { TitleContext } from "@/component/Header/Header"; import { ThemeSetting } from "@/component/Settings/Theme"; import store from "@/store"; import { useOptionsState } from "@/store/module/options"; -import { setS3Url, setZincsearchUrl } from "@/store/reducer/options"; +import { + setMeilisearchToken, + setMeilisearchUrl, + setS3Url, +} from "@/store/reducer/options"; interface SettingItem { title: string; @@ -40,13 +44,25 @@ export default function SettingsPage() { ), }, { - title: "Zincsearch URL", - description: "The URL of your Zincsearch instance", + title: "Meilisearch URL", + description: "The URL of your Meilisearch instance", value: ( { - store.dispatch(setZincsearchUrl(e.currentTarget.value)); + store.dispatch(setMeilisearchUrl(e.currentTarget.value)); + }} + /> + ), + }, + { + title: "Meilisearch Token", + description: "The token of your Meilisearch instance", + value: ( + { + store.dispatch(setMeilisearchToken(e.currentTarget.value)); }} /> ), diff --git a/src/router/index.tsx b/src/router/index.tsx index 4526dd1..518372b 100644 --- a/src/router/index.tsx +++ b/src/router/index.tsx @@ -1,116 +1,77 @@ +/* eslint-disable react-refresh/only-export-components */ import { lazy } from "react"; -import { createHashRouter } from "react-router-dom"; -import { remark } from "remark"; -import remarkHtml from "remark-html"; +import { createBrowserRouter } from "react-router-dom"; -import { SearchApi } from "@/helper/api"; import MainLayout from "@/layout/MainLayout"; import SearchPage from "@/page/Search"; import store from "@/store"; +import { markdownToHtml } from "@/utils/remark"; +import { MeiliSearch } from "meilisearch"; const NotFound = lazy(() => import("@/page/Exception/NotFound")); const ErrorPage = lazy(() => import("@/page/Exception/ErrorPage")); const LoadingPage = lazy(async () => import("@/page/Loading")); +// eslint-disable-next-line @typescript-eslint/no-unused-vars const ParagraphPage = lazy(async () => import("@/page/Paragraph")); const SettingsPage = lazy(async () => import("@/page/Settings")); -const router = createHashRouter([ - { - path: "/", - element: , - errorElement: , - children: [ - { - path: "/", - element: , - loader() { - return {}; +const router = createBrowserRouter( + [ + { + path: "/", + element: , + errorElement: , + children: [ + { + path: "/", + element: , + loader() { + return {}; + }, }, - }, - { - path: "/settings", - element: , - }, - { - path: "/tag/:tag", - element: , - loader({ params: { tag } }) { - if (!tag) { - return { redirect: "/" }; - } - return { - search_type: "querystring", - query: { - term: `tags:${JSON.stringify(tag)}`, - }, - }; + { + path: "/settings", + element: , }, - }, - { - path: "/author/:author", - element: , - loader({ params: { author } }) { - if (!author) { - return { redirect: "/" }; - } - return { - search_type: "querystring", - query: { - term: `author:${JSON.stringify(author)}`, - }, - }; - }, - }, - { - path: "/search/:search", - element: , - loader({ params: { search } }) { - if (!search) { - return { redirect: "/" }; - } - return { - search_type: "querystring", - query: { - term: search, - }, - }; - }, - }, - { - path: "/paragraph/:id", - element: , - async loader({ params: { id } }) { - if (!id) { - return { redirect: "/" }; - } - const paragraph = await SearchApi.getParagraph( - store.getState().options.zincsearchUrl, - id, - ).then((p) => - SearchApi.wrapParagraph(store.getState().options.s3Url, p), - ); + { + path: "/paragraph/:id", + element: , + async loader({ params: { id } }) { + if (!id) { + return { redirect: "/" }; + } - console.log(paragraph.markdown); - if (paragraph.markdown) { - paragraph.content = ( - await remark().use(remarkHtml).process(paragraph.content) - ).toString(); - } + const meilisearch = new MeiliSearch({ + host: store.getState().options.meilisearchUrl, + apiKey: store.getState().options.meilisearchToken, + }); - return paragraph; + const paragraph: Paragraph = await meilisearch + .index("paragraph") + .getDocument(id); + + if (paragraph.markdown) { + paragraph.content = await markdownToHtml(paragraph.content); + } else { + paragraph.content = "NO HTML!"; + } + + return paragraph; + }, }, - }, - ], - }, - { - path: "/loading", - element: , - }, - { - path: "*", - element: , - }, -]); + ], + }, + { + path: "/loading", + element: , + }, + { + path: "*", + element: , + }, + ], + {}, +); export default router; diff --git a/src/store/reducer/options.ts b/src/store/reducer/options.ts index 7fd67e1..60fdd8b 100644 --- a/src/store/reducer/options.ts +++ b/src/store/reducer/options.ts @@ -1,29 +1,35 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; export interface OptionsState { - zincsearchUrl: string; + meilisearchUrl: string; + meilisearchToken: string; s3Url: string; } -const ZINCSEARCH_URL = "https://zincsearch.yoshino-s.xyz"; -const MINIO_URL = "https://minio-hdd.yoshino-s.xyz"; +const initialState: OptionsState = { + meilisearchUrl: "https://meilisearch.yoshino-s.xyz/", + meilisearchToken: + "a568afad53a4dd124c508b9acd26ec35ff65665c07020913533cd7b176a28a04", + s3Url: "https://minio-hdd.yoshino-s.xyz", +}; const optionsSlice = createSlice({ name: "stats", - initialState: { - zincsearchUrl: ZINCSEARCH_URL, - s3Url: MINIO_URL, - } as OptionsState, + initialState, reducers: { - setZincsearchUrl: (state, action: PayloadAction) => { - state.zincsearchUrl = action.payload ?? ZINCSEARCH_URL; + setMeilisearchUrl: (state, action: PayloadAction) => { + state.meilisearchUrl = action.payload ?? initialState.meilisearchUrl; + }, + setMeilisearchToken: (state, action: PayloadAction) => { + state.meilisearchToken = action.payload ?? initialState.meilisearchToken; }, setS3Url: (state, action: PayloadAction) => { - state.s3Url = action.payload ?? MINIO_URL; + state.s3Url = action.payload ?? initialState.s3Url; }, }, }); -export const { setS3Url, setZincsearchUrl } = optionsSlice.actions; +export const { setS3Url, setMeilisearchUrl, setMeilisearchToken } = + optionsSlice.actions; export default optionsSlice.reducer; diff --git a/src/types/paragraph.d.ts b/src/types/paragraph.d.ts index 373702d..4d15ab3 100644 --- a/src/types/paragraph.d.ts +++ b/src/types/paragraph.d.ts @@ -1,6 +1,6 @@ declare interface Paragraph { - _id: string; - "@timestamp": string; + id: string; + time: string; content: string; markdown: string; title: string; diff --git a/src/utils/remark.ts b/src/utils/remark.ts new file mode 100644 index 0000000..bf94d45 --- /dev/null +++ b/src/utils/remark.ts @@ -0,0 +1,71 @@ +import rehypeHighlight from "rehype-highlight"; +import rehypeStringify from "rehype-stringify"; +import remarkGfm from "remark-gfm"; +import remarkParse from "remark-parse"; +import remarkRehype from "remark-rehype"; +import { unified } from "unified"; + +export async function markdownToHtml(markdown: string) { + const result = await unified() + .use(remarkParse) + .use(remarkRehype) + .use(remarkGfm) + .use(rehypeHighlight, { + detect: true, + }) + // .use(rehypeSanitize, { + // ...defaultSchema, + // attributes: { + // ...defaultSchema.attributes, + // span: [ + // ...(defaultSchema.attributes?.span || []), + // [ + // "className", + // "hljs-addition", + // "hljs-attr", + // "hljs-attribute", + // "hljs-built_in", + // "hljs-bullet", + // "hljs-char", + // "hljs-code", + // "hljs-comment", + // "hljs-deletion", + // "hljs-doctag", + // "hljs-emphasis", + // "hljs-formula", + // "hljs-keyword", + // "hljs-link", + // "hljs-literal", + // "hljs-meta", + // "hljs-name", + // "hljs-number", + // "hljs-operator", + // "hljs-params", + // "hljs-property", + // "hljs-punctuation", + // "hljs-quote", + // "hljs-regexp", + // "hljs-section", + // "hljs-selector-attr", + // "hljs-selector-class", + // "hljs-selector-id", + // "hljs-selector-pseudo", + // "hljs-selector-tag", + // "hljs-string", + // "hljs-strong", + // "hljs-subst", + // "hljs-symbol", + // "hljs-tag", + // "hljs-template-tag", + // "hljs-template-variable", + // "hljs-title", + // "hljs-type", + // "hljs-variable", + // ], + // ], + // }, + // }) + .use(rehypeStringify) + .process(markdown); + return result.toString(); +}