feat: meilisearch

This commit is contained in:
2024-06-25 09:59:40 +08:00
parent a5ced3cf95
commit 23d93ec5b5
22 changed files with 1066 additions and 413 deletions

View File

@@ -20,6 +20,7 @@
"@mantine/core": "^7.6.2", "@mantine/core": "^7.6.2",
"@mantine/hooks": "^7.6.2", "@mantine/hooks": "^7.6.2",
"@mantine/notifications": "^7.6.2", "@mantine/notifications": "^7.6.2",
"@meilisearch/instant-meilisearch": "^0.18.1",
"@reduxjs/toolkit": "^2.2.1", "@reduxjs/toolkit": "^2.2.1",
"@sentry/react": "^7.108.0", "@sentry/react": "^7.108.0",
"@sentry/vite-plugin": "^2.16.0", "@sentry/vite-plugin": "^2.16.0",
@@ -28,14 +29,23 @@
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"i18next": "^23.10.1", "i18next": "^23.10.1",
"i18next-browser-languagedetector": "^7.2.0", "i18next-browser-languagedetector": "^7.2.0",
"instantsearch.js": "^4.71.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"meilisearch": "^0.40.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-i18next": "^14.1.0", "react-i18next": "^14.1.0",
"react-instantsearch": "^7.11.1",
"react-redux": "^9.1.0", "react-redux": "^9.1.0",
"react-router-dom": "^6.22.3", "react-router-dom": "^6.22.3",
"remark": "^15.0.1", "rehype-highlight": "^7.0.0",
"remark-html": "^16.0.1" "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": { "devDependencies": {
"@babel/core": "^7.24.1", "@babel/core": "^7.24.1",

557
pnpm-lock.yaml generated
View File

@@ -14,6 +14,9 @@ dependencies:
'@mantine/notifications': '@mantine/notifications':
specifier: ^7.6.2 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) 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': '@reduxjs/toolkit':
specifier: ^2.2.1 specifier: ^2.2.1
version: 2.2.1(react-redux@9.1.0)(react@18.2.0) version: 2.2.1(react-redux@9.1.0)(react@18.2.0)
@@ -38,9 +41,15 @@ dependencies:
i18next-browser-languagedetector: i18next-browser-languagedetector:
specifier: ^7.2.0 specifier: ^7.2.0
version: 7.2.0 version: 7.2.0
instantsearch.js:
specifier: ^4.71.1
version: 4.71.1(algoliasearch@4.23.3)
lodash: lodash:
specifier: ^4.17.21 specifier: ^4.17.21
version: 4.17.21 version: 4.17.21
meilisearch:
specifier: ^0.40.0
version: 0.40.0
react: react:
specifier: ^18.2.0 specifier: ^18.2.0
version: 18.2.0 version: 18.2.0
@@ -50,18 +59,39 @@ dependencies:
react-i18next: react-i18next:
specifier: ^14.1.0 specifier: ^14.1.0
version: 14.1.0(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.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: react-redux:
specifier: ^9.1.0 specifier: ^9.1.0
version: 9.1.0(@types/react@18.2.67)(react@18.2.0)(redux@5.0.1) version: 9.1.0(@types/react@18.2.67)(react@18.2.0)(redux@5.0.1)
react-router-dom: react-router-dom:
specifier: ^6.22.3 specifier: ^6.22.3
version: 6.22.3(react-dom@18.2.0)(react@18.2.0) version: 6.22.3(react-dom@18.2.0)(react@18.2.0)
remark: rehype-highlight:
specifier: ^15.0.1 specifier: ^7.0.0
version: 15.0.1 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: remark-html:
specifier: ^16.0.1 specifier: ^16.0.1
version: 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: devDependencies:
'@babel/core': '@babel/core':
@@ -235,6 +265,116 @@ packages:
resolution: {integrity: sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==} resolution: {integrity: sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==}
dev: true 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: /@ampproject/remapping@2.3.0:
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
@@ -2207,6 +2347,14 @@ packages:
react: 18.2.0 react: 18.2.0
dev: true 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: /@ndelangen/get-tarball@3.0.9:
resolution: {integrity: sha512-9JKTEik4vq+yGosHYhZ1tiH/3WpUS0Nh0kej4Agndhox8pAdWhEx5knFVRcb/ya9knCRCs1rPxNrSXTDdfVqpA==} resolution: {integrity: sha512-9JKTEik4vq+yGosHYhZ1tiH/3WpUS0Nh0kej4Agndhox8pAdWhEx5knFVRcb/ya9knCRCs1rPxNrSXTDdfVqpA==}
dependencies: dependencies:
@@ -3618,6 +3766,10 @@ packages:
resolution: {integrity: sha512-w5jZ0ee+HaPOaX25X2/2oGR/7rgAQSYII7X7pp0m9KgBfMP7uKfMfTvcpl5Dj+eDBbpxKGiqE+flqDr6XTd2RA==} resolution: {integrity: sha512-w5jZ0ee+HaPOaX25X2/2oGR/7rgAQSYII7X7pp0m9KgBfMP7uKfMfTvcpl5Dj+eDBbpxKGiqE+flqDr6XTd2RA==}
dev: true dev: true
/@types/dom-speech-recognition@0.0.1:
resolution: {integrity: sha512-udCxb8DvjcDKfk1WTBzDsxFbLgYxmQGKrE/ricoMqHRNjSlSUCcamVTA5lIQqzY10mY5qCY0QDwBfFEwhfoDPw==}
dev: false
/@types/ejs@3.1.5: /@types/ejs@3.1.5:
resolution: {integrity: sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==} resolution: {integrity: sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==}
dev: true dev: true
@@ -3674,6 +3826,10 @@ packages:
resolution: {integrity: sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw==} resolution: {integrity: sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw==}
dev: true dev: true
/@types/google.maps@3.55.10:
resolution: {integrity: sha512-XbDu2MIvcKgN+MBrufjWcsQRtXTbrBGBKperbhMLnPSq4770+pvlR66Oqq/Ub4AVkmGc9QciCfwPZpVCLaKAOw==}
dev: false
/@types/graceful-fs@4.1.9: /@types/graceful-fs@4.1.9:
resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==}
dependencies: dependencies:
@@ -3685,6 +3841,10 @@ packages:
dependencies: dependencies:
'@types/unist': 3.0.2 '@types/unist': 3.0.2
/@types/hogan.js@3.0.5:
resolution: {integrity: sha512-/uRaY3HGPWyLqOyhgvW9Aa43BNnLZrNeQxl2p8wqId4UHMfPKolSB+U7BlZyO1ng7MkLnyEAItsBzCG0SDhqrA==}
dev: false
/@types/http-errors@2.0.4: /@types/http-errors@2.0.4:
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
dev: true dev: true
@@ -3779,7 +3939,6 @@ packages:
/@types/qs@6.9.13: /@types/qs@6.9.13:
resolution: {integrity: sha512-iLR+1vTTJ3p0QaOUq6ACbY1mzKTODFDT/XedZI8BksOotFmL4ForwDfRQ/DZeuTHR7/2i4lI1D203gdfxuqTlA==} resolution: {integrity: sha512-iLR+1vTTJ3p0QaOUq6ACbY1mzKTODFDT/XedZI8BksOotFmL4ForwDfRQ/DZeuTHR7/2i4lI1D203gdfxuqTlA==}
dev: true
/@types/range-parser@1.2.7: /@types/range-parser@1.2.7:
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
@@ -4245,6 +4404,10 @@ packages:
deprecated: Use your platform's native atob() and btoa() methods instead deprecated: Use your platform's native atob() and btoa() methods instead
dev: true dev: true
/abbrev@1.1.1:
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
dev: false
/accepts@1.3.8: /accepts@1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@@ -4372,6 +4535,35 @@ packages:
uri-js: 4.4.1 uri-js: 4.4.1
dev: true 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: /ansi-escapes@4.3.2:
resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -5242,6 +5434,14 @@ packages:
- ts-node - ts-node
dev: true 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: /cross-fetch@4.0.0:
resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==}
dependencies: dependencies:
@@ -5973,6 +6173,11 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
dev: true 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: /escodegen@2.1.0:
resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==}
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
@@ -6962,7 +7167,6 @@ packages:
resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==}
dependencies: dependencies:
'@types/hast': 3.0.4 '@types/hast': 3.0.4
dev: true
/hast-util-parse-selector@4.0.0: /hast-util-parse-selector@4.0.0:
resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
@@ -7031,6 +7235,15 @@ packages:
'@types/hast': 3.0.4 '@types/hast': 3.0.4
dev: true 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: /hast-util-whitespace@3.0.0:
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
dependencies: dependencies:
@@ -7047,6 +7260,19 @@ packages:
space-separated-tokens: 2.0.2 space-separated-tokens: 2.0.2
dev: false 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: /hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
dependencies: dependencies:
@@ -7057,6 +7283,10 @@ packages:
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
dev: true dev: true
/htm@3.1.1:
resolution: {integrity: sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==}
dev: false
/html-encoding-sniffer@3.0.0: /html-encoding-sniffer@3.0.0:
resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -7218,6 +7448,32 @@ packages:
semver: 6.3.1 semver: 6.3.1
dev: true 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: /internal-slot@1.0.7:
resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -8348,6 +8604,14 @@ packages:
get-func-name: 2.0.2 get-func-name: 2.0.2
dev: true 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: /lru-cache@10.2.0:
resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==}
engines: {node: 14 || >=16.14} engines: {node: 14 || >=16.14}
@@ -8425,6 +8689,10 @@ packages:
resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==}
dev: true 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): /markdown-to-jsx@7.3.2(react@18.2.0):
resolution: {integrity: sha512-B+28F5ucp83aQm+OxNrPkS8z0tMKaeHiy0lHJs3LqCyDQFtWuenaIrkaVTgAm1pf1AU85LXltva86hlaT17i8Q==} resolution: {integrity: sha512-B+28F5ucp83aQm+OxNrPkS8z0tMKaeHiy0lHJs3LqCyDQFtWuenaIrkaVTgAm1pf1AU85LXltva86hlaT17i8Q==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
@@ -8434,6 +8702,15 @@ packages:
react: 18.2.0 react: 18.2.0
dev: true 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: /mdast-util-from-markdown@2.0.0:
resolution: {integrity: sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA==} resolution: {integrity: sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA==}
dependencies: dependencies:
@@ -8453,6 +8730,75 @@ packages:
- supports-color - supports-color
dev: false 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: /mdast-util-phrasing@4.1.0:
resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
dependencies: dependencies:
@@ -8498,6 +8844,14 @@ packages:
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
dev: true 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: /memoizee@0.4.15:
resolution: {integrity: sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==} resolution: {integrity: sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==}
dependencies: dependencies:
@@ -8556,6 +8910,78 @@ packages:
micromark-util-types: 2.0.0 micromark-util-types: 2.0.0
dev: false 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: /micromark-factory-destination@2.0.0:
resolution: {integrity: sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==} resolution: {integrity: sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==}
dependencies: dependencies:
@@ -8813,6 +9239,11 @@ packages:
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
dev: true 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: /mkdirp@1.0.4:
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -8886,6 +9317,13 @@ packages:
/node-releases@2.0.14: /node-releases@2.0.14:
resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} 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: /normalize-package-data@2.5.0:
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
dependencies: dependencies:
@@ -9357,6 +9795,10 @@ packages:
source-map-js: 1.2.0 source-map-js: 1.2.0
dev: true dev: true
/preact@10.22.0:
resolution: {integrity: sha512-RRurnSjJPj4rp5K6XoP45Ui33ncb7e4H7WiOHVpjbkvqvA3U+N8Z6Qbo0AE6leGYBV66n8EhEaFixvIu3SkxFw==}
dev: false
/prelude-ls@1.2.1: /prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@@ -9498,6 +9940,11 @@ packages:
side-channel: 1.0.6 side-channel: 1.0.6
dev: true dev: true
/qs@6.9.7:
resolution: {integrity: sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==}
engines: {node: '>=0.6'}
dev: false
/querystringify@2.2.0: /querystringify@2.2.0:
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
dev: true dev: true
@@ -9582,6 +10029,36 @@ packages:
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0(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: /react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@@ -9904,6 +10381,23 @@ packages:
unist-util-visit: 5.0.0 unist-util-visit: 5.0.0
dev: true 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: /rehype-slug@6.0.0:
resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==} resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==}
dependencies: dependencies:
@@ -9914,6 +10408,27 @@ packages:
unist-util-visit: 5.0.0 unist-util-visit: 5.0.0
dev: true 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: /remark-html@16.0.1:
resolution: {integrity: sha512-B9JqA5i0qZe0Nsf49q3OXyGvyXuZFDzAP2iOFLEumymuYJITVpiH1IgsTEwTpdptDmZlMDMWeDmSawdaJIGCXQ==} resolution: {integrity: sha512-B9JqA5i0qZe0Nsf49q3OXyGvyXuZFDzAP2iOFLEumymuYJITVpiH1IgsTEwTpdptDmZlMDMWeDmSawdaJIGCXQ==}
dependencies: dependencies:
@@ -9935,6 +10450,16 @@ packages:
- supports-color - supports-color
dev: false 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: /remark-stringify@11.0.0:
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
dependencies: dependencies:
@@ -9943,17 +10468,6 @@ packages:
unified: 11.0.4 unified: 11.0.4
dev: false 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: /require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -10139,6 +10653,10 @@ packages:
ajv-keywords: 5.1.0(ajv@8.12.0) ajv-keywords: 5.1.0(ajv@8.12.0)
dev: true dev: true
/search-insights@2.14.0:
resolution: {integrity: sha512-OLN6MsPMCghDOqlCtsIsYgtsC0pnwVTyT9Mu6A3ewOj1DxvzZF6COrn2g86E/c05xbktB0XN04m/t1Z+n+fTGw==}
dev: false
/semver@5.7.2: /semver@5.7.2:
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
hasBin: true hasBin: true
@@ -11041,6 +11559,13 @@ packages:
crypto-random-string: 2.0.0 crypto-random-string: 2.0.0
dev: true 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: /unist-util-is@6.0.0:
resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==}
dependencies: dependencies:

View File

@@ -1,5 +1,5 @@
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
export default { module.exports = {
plugins: { plugins: {
"postcss-preset-mantine": {}, "postcss-preset-mantine": {},
"postcss-simple-vars": { "postcss-simple-vars": {

1
public/_redirects Normal file
View File

@@ -0,0 +1 @@
/* /index.html 200

View File

@@ -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;
}
}

View File

@@ -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<Paragraph>) {
const { results } = useHits(props);
return (
<Box className={styles["img-wrapper"]}>
{results?.hits?.map((hit) => (
<Box key={hit.id}>
<ParagraphCard {...hit} key={`paragraph-card-${hit.id}`} />
</Box>
))}
</Box>
);
}

View File

@@ -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 (
<Center>
<Group>
<MantinePagination
total={nbPages}
value={currentRefinement + 1}
onChange={(value) => refine(value - 1)}
/>
</Group>
</Center>
);
}

View File

@@ -8,47 +8,33 @@ dayjs.extend(relativeTime);
export function ParagraphCard({ export function ParagraphCard({
cover, cover,
title, title,
"@timestamp": time, time,
author, author,
tags, tags,
_id, id,
}: Paragraph) { }: Paragraph) {
const url = `/paragraph/${_id}`; const url = `/paragraph/${id}`;
return ( return (
<Card withBorder radius="md" padding="lg" shadow="sm"> <Card withBorder radius="md" padding="lg" shadow="sm">
<Card.Section> <Card.Section>
<Link to={url}> <Link to={url} target="_blank">
{cover && <Image src={cover} height={140} width={140} />} {cover && <Image src={cover} height={140} width={140} />}
</Link> </Link>
</Card.Section> </Card.Section>
<Group justify="space-between" mt="md" mb="xs"> <Group justify="space-between" mt="md" mb="xs">
<Text component={Link} to={url}> <Text component={Link} to={url} target="_blank">
{title} {title}
</Text> </Text>
<Group> <Group>
{tags.map((tag, index) => ( {tags.map((tag, index) => (
<> <Badge key={index}>{tag}</Badge>
<Badge
component={Link}
key={index}
to={`/tag/${encodeURIComponent(tag)}`}
>
{tag}
</Badge>
</>
))} ))}
</Group> </Group>
</Group> </Group>
<Group> <Group>
<Group> <Group>
<Text <Text size="xs">{author}</Text>
size="xs"
component={Link}
to={`/author/${encodeURIComponent(author)}`}
>
{author}
</Text>
</Group> </Group>
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed">

View File

@@ -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<ComboboxItem>) {
return (
<Group justify="space-between" w="100%">
<Box
style={{
gap: "0.5em",
display: "flex",
justifyContent: "flex-start",
alignItems: "center",
}}
>
{currentVal.includes(props.option.value) && (
<CheckIcon
style={{
opacity: "0.4",
width: "0.8em",
minWidth: "0.8em",
height: "0.8em",
}}
/>
)}
{props.option.label}
</Box>
<Badge>
{items.find((item) => item.value === props.option.value)?.count}
</Badge>
</Group>
);
}
return (
<Group justify="space-between" align="center" my="md">
<Group>
<Select
data={sortItems}
value={currentRefinement}
defaultValue={sortItems[0].value}
onChange={(value) => value && refineSortBy(value)}
label="Sort by"
/>
<MultiSelect
styles={{
wrapper: {
width: rem(300),
},
}}
data={items.map((item) => ({
value: item.label,
label: SourceLabelMap[item.label],
}))}
renderOption={SelectItem}
value={currentVal}
label="Source"
clearable
onChange={(values) => {
console.log(values);
const old = new Set(currentVal);
const new_ = new Set(values);
const diff = old.difference(new_).union(new_.difference(old));
diff.forEach((value) => refine(value));
}}
/>
</Group>
<Group w={rem(96)}>
<Select
data={hitsPerPageItems.map((item) => ({
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"
/>
</Group>
</Group>
);
}

View File

@@ -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 (
<Affix position={{ bottom: 20, right: 20 }}>
<Transition transition="slide-up" mounted={scroll.y > 0}>
{(transitionStyles) => (
<Button
leftSection={
<IconArrowUp style={{ width: rem(16), height: rem(16) }} />
}
style={transitionStyles}
onClick={() => scrollTo({ y: 0 })}
>
Scroll to top
</Button>
)}
</Transition>
</Affix>
);
}

View File

@@ -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 (
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<TextInput
placeholder="Search"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
type="search"
leftSection={
<IconSearch
style={{ width: rem(16), height: rem(16) }}
stroke={1.5}
/>
}
value={inputValue}
onChange={(event) => setQuery(event.currentTarget.value)}
/>
</form>
);
}

9
src/constants.ts Normal file
View File

@@ -0,0 +1,9 @@
export const SourceLabelMap: Record<string, string> = {
tttang: "跳跳糖",
secin: "Sec-In",
seebug: "Seebug",
wechat: "微信公众号",
xianzhi: "先知",
anquanke: "安全客",
freebuf: "FreeBuf",
};

View File

@@ -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<any, any>) => {
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<any, any> = 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<SearchResponse> {
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"],
});
}
}

View File

@@ -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<T>(query: ZincQueryForSDK) {
const [params, setParams] = useSearchParams({
page: "1",
size: "10",
});
const { state: options } = useOptionsState();
const [total, setTotal] = useState(0);
const [data, setData] = useState<T[]>([]);
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: (
<Pagination
size={isMobile ? "sm" : "md"}
total={Math.ceil(total / take)}
onChange={setPage}
value={page}
/>
),
};
}

View File

@@ -1,55 +1,69 @@
import SearchBox from "@/component/SearchBox/SearchBox";
import { import {
Affix,
AppShell, AppShell,
Avatar, Avatar,
Button,
Center, Center,
Group, Group,
Text,
TextInput,
Transition,
UnstyledButton,
rem, rem,
UnstyledButton,
} from "@mantine/core"; } from "@mantine/core";
import { Suspense, useCallback, useState } from "react"; import { Suspense, useEffect, useState } from "react";
import { Outlet, useNavigate } from "react-router"; import { Outlet, useLocation } from "react-router";
import { TitleContext } from "@/component/Header/Header"; import ScrollTop from "@/component/ScrollTop/ScrollTop";
import Loading from "@/page/Loading"; import Loading from "@/page/Loading";
import { useForm } from "@mantine/form"; import { useHeadroom } from "@mantine/hooks";
import { useHeadroom, useMediaQuery, useWindowScroll } from "@mantine/hooks"; import { IconSettings } from "@tabler/icons-react";
import { IconArrowUp, IconSearch, IconSettings } from "@tabler/icons-react";
import { Link } from "react-router-dom"; 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() { export default function MainLayout() {
const [title, setTitle] = useState("");
const pinned = useHeadroom({ fixedAt: 60 }); const pinned = useHeadroom({ fixedAt: 60 });
const selector = useAppSelector((state) => state.options);
const [scroll, scrollTo] = useWindowScroll(); const path = useLocation().pathname;
const isMobile = useMediaQuery("(max-width: 768px)");
const navigate = useNavigate(); const isSearchPage = path === "/";
const form = useForm({ const [searchClient, setSearchClient] =
initialValues: { useState<InstantMeiliSearchInstance>();
search: "",
},
});
const search = useCallback( useEffect(() => {
function submit({ search }: { search: string }) { const { meilisearchUrl, meilisearchToken } = selector;
console.log(search); const { searchClient } = instantMeiliSearch(
navigate(`/search/${encodeURIComponent(search)}`); meilisearchUrl,
}, meilisearchToken,
[navigate], {
); finitePagination: true,
meiliSearchParams: {
hybrid: {},
attributesToRetrieve: [
"cover",
"title",
"time",
"author",
"tags",
"id",
],
},
},
);
setSearchClient(searchClient);
}, [selector, setSearchClient]);
return ( const shell = (
<TitleContext.Provider value={[title, setTitle]}> <>
<AppShell <AppShell
header={{ height: 60, collapsed: !pinned, offset: false }} header={{ height: 60, collapsed: !pinned, offset: false }}
padding="md"
h="100vh" h="100vh"
> >
<AppShell.Header> <AppShell.Header>
@@ -58,26 +72,9 @@ export default function MainLayout() {
<Avatar fw={700} component={Link} to="/"> <Avatar fw={700} component={Link} to="/">
DS DS
</Avatar> </Avatar>
{!isMobile && (
<Text size="lg" fw={700} ml="sm">
{title}
</Text>
)}
</Group> </Group>
<Group> <Group>
<form onSubmit={form.onSubmit(search)}> {isSearchPage && <SearchBox />}
<TextInput
placeholder="Search"
{...form.getInputProps("search")}
leftSection={
<IconSearch
style={{ width: rem(16), height: rem(16) }}
stroke={1.5}
/>
}
/>
</form>
<UnstyledButton component={Link} to="/settings"> <UnstyledButton component={Link} to="/settings">
<Center> <Center>
<IconSettings /> <IconSettings />
@@ -92,21 +89,26 @@ export default function MainLayout() {
</Suspense> </Suspense>
</AppShell.Main> </AppShell.Main>
</AppShell> </AppShell>
<Affix position={{ bottom: 20, right: 20 }}> <ScrollTop />
<Transition transition="slide-up" mounted={scroll.y > 0}> </>
{(transitionStyles) => ( );
<Button
leftSection={ return (
<IconArrowUp style={{ width: rem(16), height: rem(16) }} /> <>
} {searchClient &&
style={transitionStyles} (isSearchPage ? (
onClick={() => scrollTo({ y: 0 })} <InstantSearch
> searchClient={searchClient!}
Scroll to top indexName="paragraph"
</Button> routing={{
)} stateMapping: singleIndex("paragraph"),
</Transition> }}
</Affix> >
</TitleContext.Provider> {shell}
</InstantSearch>
) : (
shell
))}
</>
); );
} }

View File

@@ -1,4 +1,3 @@
import { TitleContext } from "@/component/Header/Header";
import { import {
Badge, Badge,
Container, Container,
@@ -9,7 +8,6 @@ import {
} from "@mantine/core"; } from "@mantine/core";
import dayjs from "dayjs"; import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime"; import relativeTime from "dayjs/plugin/relativeTime";
import { useContext, useEffect } from "react";
import { useLoaderData } from "react-router"; import { useLoaderData } from "react-router";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
@@ -55,12 +53,10 @@ function stripStyles(content: string) {
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
export default function ParagraphPage() { export default function ParagraphPage() {
const [_title, setTitle] = useContext(TitleContext);
const paragraph = useLoaderData() as Paragraph; const paragraph = useLoaderData() as Paragraph;
console.log(paragraph);
useEffect(() => { const content = stripStyles(paragraph.content);
setTitle(paragraph.title);
}, [setTitle, paragraph.title]);
return ( return (
<Container py="2rem"> <Container py="2rem">
@@ -69,7 +65,7 @@ export default function ParagraphPage() {
<Group> <Group>
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
{" "} {" "}
{dayjs().to(dayjs(paragraph["@timestamp"]))} {dayjs().to(dayjs(paragraph.time))}
</Text> </Text>
<Text <Text
ml="1rem" ml="1rem"
@@ -109,7 +105,7 @@ export default function ParagraphPage() {
lineBreak: "anywhere", lineBreak: "anywhere",
}} }}
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: stripStyles(paragraph.content), __html: content,
}} }}
/> />
</TypographyStylesProvider> </TypographyStylesProvider>

View File

@@ -1,51 +1,19 @@
import { Container, Grid, Group } from "@mantine/core"; import { Container, Grid, Stack } from "@mantine/core";
import { useContext, useEffect } from "react";
import { useLoaderData, useLocation, useParams } from "react-router";
import { ParagraphCard } from "../component/ParagraphCard/ParagraphCard"; import Hits from "@/component/Hits/Hits";
import Pagination from "@/component/Pagination/Pagination";
import { TitleContext } from "@/component/Header/Header"; import Refinement from "@/component/Refinement/Refinement";
import { usePaginationData } from "@/helper/hooks";
export default function SearchPage() { export default function SearchPage() {
const [_title, setTitle] = useContext(TitleContext);
const params = useLoaderData() as ZincQueryForSDK;
const {
page,
pagination,
data: paragraphs,
} = usePaginationData<Paragraph>(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 ( return (
<Container> <Container>
<Grid my="md"> <Stack>
{paragraphs.map((paragraph) => { <Refinement />
return ( <Grid my="md">
<Grid.Col span={{ base: 12, sm: 6 }} key={paragraph._id}> <Hits />
<ParagraphCard {...paragraph} key={`${paragraph._id}_card`} /> </Grid>
</Grid.Col> <Pagination />
); </Stack>
})}
</Grid>
<Group justify="center">{pagination}</Group>
</Container> </Container>
); );
} }

View File

@@ -5,7 +5,11 @@ import { TitleContext } from "@/component/Header/Header";
import { ThemeSetting } from "@/component/Settings/Theme"; import { ThemeSetting } from "@/component/Settings/Theme";
import store from "@/store"; import store from "@/store";
import { useOptionsState } from "@/store/module/options"; import { useOptionsState } from "@/store/module/options";
import { setS3Url, setZincsearchUrl } from "@/store/reducer/options"; import {
setMeilisearchToken,
setMeilisearchUrl,
setS3Url,
} from "@/store/reducer/options";
interface SettingItem { interface SettingItem {
title: string; title: string;
@@ -40,13 +44,25 @@ export default function SettingsPage() {
), ),
}, },
{ {
title: "Zincsearch URL", title: "Meilisearch URL",
description: "The URL of your Zincsearch instance", description: "The URL of your Meilisearch instance",
value: ( value: (
<TextInput <TextInput
value={options.zincsearchUrl} value={options.meilisearchUrl}
onChange={(e) => { onChange={(e) => {
store.dispatch(setZincsearchUrl(e.currentTarget.value)); store.dispatch(setMeilisearchUrl(e.currentTarget.value));
}}
/>
),
},
{
title: "Meilisearch Token",
description: "The token of your Meilisearch instance",
value: (
<TextInput
value={options.meilisearchToken}
onChange={(e) => {
store.dispatch(setMeilisearchToken(e.currentTarget.value));
}} }}
/> />
), ),

View File

@@ -1,116 +1,77 @@
/* eslint-disable react-refresh/only-export-components */
import { lazy } from "react"; import { lazy } from "react";
import { createHashRouter } from "react-router-dom"; import { createBrowserRouter } from "react-router-dom";
import { remark } from "remark";
import remarkHtml from "remark-html";
import { SearchApi } from "@/helper/api";
import MainLayout from "@/layout/MainLayout"; import MainLayout from "@/layout/MainLayout";
import SearchPage from "@/page/Search"; import SearchPage from "@/page/Search";
import store from "@/store"; import store from "@/store";
import { markdownToHtml } from "@/utils/remark";
import { MeiliSearch } from "meilisearch";
const NotFound = lazy(() => import("@/page/Exception/NotFound")); const NotFound = lazy(() => import("@/page/Exception/NotFound"));
const ErrorPage = lazy(() => import("@/page/Exception/ErrorPage")); const ErrorPage = lazy(() => import("@/page/Exception/ErrorPage"));
const LoadingPage = lazy(async () => import("@/page/Loading")); const LoadingPage = lazy(async () => import("@/page/Loading"));
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ParagraphPage = lazy(async () => import("@/page/Paragraph")); const ParagraphPage = lazy(async () => import("@/page/Paragraph"));
const SettingsPage = lazy(async () => import("@/page/Settings")); const SettingsPage = lazy(async () => import("@/page/Settings"));
const router = createHashRouter([ const router = createBrowserRouter(
{ [
path: "/", {
element: <MainLayout />, path: "/",
errorElement: <ErrorPage />, element: <MainLayout />,
children: [ errorElement: <ErrorPage />,
{ children: [
path: "/", {
element: <SearchPage />, path: "/",
loader() { element: <SearchPage />,
return {}; loader() {
return {};
},
}, },
}, {
{ path: "/settings",
path: "/settings", element: <SettingsPage />,
element: <SettingsPage />,
},
{
path: "/tag/:tag",
element: <SearchPage />,
loader({ params: { tag } }) {
if (!tag) {
return { redirect: "/" };
}
return {
search_type: "querystring",
query: {
term: `tags:${JSON.stringify(tag)}`,
},
};
}, },
},
{
path: "/author/:author",
element: <SearchPage />,
loader({ params: { author } }) {
if (!author) {
return { redirect: "/" };
}
return {
search_type: "querystring",
query: {
term: `author:${JSON.stringify(author)}`,
},
};
},
},
{
path: "/search/:search",
element: <SearchPage />,
loader({ params: { search } }) {
if (!search) {
return { redirect: "/" };
}
return {
search_type: "querystring",
query: {
term: search,
},
};
},
},
{
path: "/paragraph/:id",
element: <ParagraphPage />,
async loader({ params: { id } }) {
if (!id) {
return { redirect: "/" };
}
const paragraph = await SearchApi.getParagraph( {
store.getState().options.zincsearchUrl, path: "/paragraph/:id",
id, element: <ParagraphPage />,
).then((p) => async loader({ params: { id } }) {
SearchApi.wrapParagraph(store.getState().options.s3Url, p), if (!id) {
); return { redirect: "/" };
}
console.log(paragraph.markdown); const meilisearch = new MeiliSearch({
if (paragraph.markdown) { host: store.getState().options.meilisearchUrl,
paragraph.content = ( apiKey: store.getState().options.meilisearchToken,
await remark().use(remarkHtml).process(paragraph.content) });
).toString();
}
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",
path: "/loading", element: <LoadingPage />,
element: <LoadingPage />, },
}, {
{ path: "*",
path: "*", element: <NotFound />,
element: <NotFound />, },
}, ],
]); {},
);
export default router; export default router;

View File

@@ -1,29 +1,35 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import { PayloadAction, createSlice } from "@reduxjs/toolkit";
export interface OptionsState { export interface OptionsState {
zincsearchUrl: string; meilisearchUrl: string;
meilisearchToken: string;
s3Url: string; s3Url: string;
} }
const ZINCSEARCH_URL = "https://zincsearch.yoshino-s.xyz"; const initialState: OptionsState = {
const MINIO_URL = "https://minio-hdd.yoshino-s.xyz"; meilisearchUrl: "https://meilisearch.yoshino-s.xyz/",
meilisearchToken:
"a568afad53a4dd124c508b9acd26ec35ff65665c07020913533cd7b176a28a04",
s3Url: "https://minio-hdd.yoshino-s.xyz",
};
const optionsSlice = createSlice({ const optionsSlice = createSlice({
name: "stats", name: "stats",
initialState: { initialState,
zincsearchUrl: ZINCSEARCH_URL,
s3Url: MINIO_URL,
} as OptionsState,
reducers: { reducers: {
setZincsearchUrl: (state, action: PayloadAction<string | undefined>) => { setMeilisearchUrl: (state, action: PayloadAction<string | undefined>) => {
state.zincsearchUrl = action.payload ?? ZINCSEARCH_URL; state.meilisearchUrl = action.payload ?? initialState.meilisearchUrl;
},
setMeilisearchToken: (state, action: PayloadAction<string | undefined>) => {
state.meilisearchToken = action.payload ?? initialState.meilisearchToken;
}, },
setS3Url: (state, action: PayloadAction<string | undefined>) => { setS3Url: (state, action: PayloadAction<string | undefined>) => {
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; export default optionsSlice.reducer;

View File

@@ -1,6 +1,6 @@
declare interface Paragraph { declare interface Paragraph {
_id: string; id: string;
"@timestamp": string; time: string;
content: string; content: string;
markdown: string; markdown: string;
title: string; title: string;

71
src/utils/remark.ts Normal file
View File

@@ -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();
}