parent
d34585e4b0
commit
4cdc59e51c
30 changed files with 683 additions and 676 deletions
22
.github/workflows/test.yml
vendored
22
.github/workflows/test.yml
vendored
|
@ -21,6 +21,28 @@ jobs:
|
||||||
- name: Run Immich Server E2E Test
|
- name: Run Immich Server E2E Test
|
||||||
run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich-server-test
|
run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich-server-test
|
||||||
|
|
||||||
|
doc-tests:
|
||||||
|
name: Run documentation checks
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./docs
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Run npm install
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run formatter
|
||||||
|
run: npm run format
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
|
- name: Run tsc
|
||||||
|
run: npm run check
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
server-unit-tests:
|
server-unit-tests:
|
||||||
name: Run server unit test suites and checks
|
name: Run server unit test suites and checks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
2
docs/.prettierignore
Normal file
2
docs/.prettierignore
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
build/
|
||||||
|
.docusaurus/
|
6
docs/.prettierrc
Normal file
6
docs/.prettierrc
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 120,
|
||||||
|
"semi": true
|
||||||
|
}
|
|
@ -33,9 +33,8 @@ The motion part will now be uploaded and can be played on the mobile app and the
|
||||||
src="https://media.giphy.com/media/fTrGceZd7t1ewi8ESc/giphy.gif"
|
src="https://media.giphy.com/media/fTrGceZd7t1ewi8ESc/giphy.gif"
|
||||||
width="100%"
|
width="100%"
|
||||||
style={{
|
style={{
|
||||||
borderRadius: "10px",
|
borderRadius: '10px',
|
||||||
boxShadow:
|
boxShadow: 'rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px',
|
||||||
"rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px",
|
|
||||||
}}
|
}}
|
||||||
title="LivePhoto playback on the web"
|
title="LivePhoto playback on the web"
|
||||||
/>
|
/>
|
||||||
|
@ -73,9 +72,8 @@ The web will have the option to sign in with OAuth.
|
||||||
width="50%"
|
width="50%"
|
||||||
title="Web Sign in with OAuth"
|
title="Web Sign in with OAuth"
|
||||||
style={{
|
style={{
|
||||||
borderRadius: "10px",
|
borderRadius: '10px',
|
||||||
boxShadow:
|
boxShadow: 'rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px',
|
||||||
"rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px",
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -86,9 +84,8 @@ sign-in button.
|
||||||
src="https://media.giphy.com/media/3iy3SaNkVYtlkEiw06/giphy.gif"
|
src="https://media.giphy.com/media/3iy3SaNkVYtlkEiw06/giphy.gif"
|
||||||
title="Mobile sign in with OAuth"
|
title="Mobile sign in with OAuth"
|
||||||
style={{
|
style={{
|
||||||
borderRadius: "10px",
|
borderRadius: '10px',
|
||||||
boxShadow:
|
boxShadow: 'rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px',
|
||||||
"rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px",
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -98,9 +95,8 @@ sign-in button.
|
||||||
src="https://media.giphy.com/media/LStqgGESXW8XnuCv5y/giphy.gif"
|
src="https://media.giphy.com/media/LStqgGESXW8XnuCv5y/giphy.gif"
|
||||||
width="300"
|
width="300"
|
||||||
style={{
|
style={{
|
||||||
borderRadius: "10px",
|
borderRadius: '10px',
|
||||||
boxShadow:
|
boxShadow: 'rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px',
|
||||||
"rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px",
|
|
||||||
}}
|
}}
|
||||||
title="Support the project"
|
title="Support the project"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -22,11 +22,11 @@ The initial approach of Immich is to become a backup tool, primarily for mobile
|
||||||
|
|
||||||
### Why does my uploaded photo show up with the wrong date or time in Immich?
|
### Why does my uploaded photo show up with the wrong date or time in Immich?
|
||||||
|
|
||||||
When a photo is initially uploaded Immich uses the create date of the file to determine where it belongs in the timeline. After that, background jobs will run that extract [exif metadata](https://en.wikipedia.org/wiki/Exif), including the CreateDate, to provide a more accurate date for the photo. If that is not available it will fallback to the modified date. If you want to ensure your photo has the right date, check the exif metadata before uploading.
|
When a photo is initially uploaded Immich uses the create date of the file to determine where it belongs in the timeline. After that, background jobs will run that extract [exif metadata](https://en.wikipedia.org/wiki/Exif), including the CreateDate, to provide a more accurate date for the photo. If that is not available it will fallback to the modified date. If you want to ensure your photo has the right date, check the exif metadata before uploading.
|
||||||
|
|
||||||
If the timezone is incorrect in an uploaded photo, check the ``DateTimeOriginal`` exif field of the uploaded file. Immich uses the very competent library [exiftool-vendored.js](https://github.com/photostructure/exiftool-vendored.js#dates) to handle timezone parsing, but in some cases (like photos taken with DSLR cameras) it has to fallback on the local timezone. If you are using docker, this fallback will be UTC. (Note that even the photo backup app that can't be named [has the same bug!](https://photo.stackexchange.com/a/126978)) In Immich, it is possible to change this assumed fallback timezone system-wide by setting the timezone in the microservices docker container. You might need to run the "Extract Metadata" job after to effect the change.
|
If the timezone is incorrect in an uploaded photo, check the `DateTimeOriginal` exif field of the uploaded file. Immich uses the very competent library [exiftool-vendored.js](https://github.com/photostructure/exiftool-vendored.js#dates) to handle timezone parsing, but in some cases (like photos taken with DSLR cameras) it has to fallback on the local timezone. If you are using docker, this fallback will be UTC. (Note that even the photo backup app that can't be named [has the same bug!](https://photo.stackexchange.com/a/126978)) In Immich, it is possible to change this assumed fallback timezone system-wide by setting the timezone in the microservices docker container. You might need to run the "Extract Metadata" job after to effect the change.
|
||||||
|
|
||||||
As an example, the following modification of ```docker-compose.yml``` will set the timezone of the microservices container to be ``Europe/Stockholm``
|
As an example, the following modification of `docker-compose.yml` will set the timezone of the microservices container to be `Europe/Stockholm`
|
||||||
|
|
||||||
```
|
```
|
||||||
environment:
|
environment:
|
||||||
|
@ -34,9 +34,11 @@ As an example, the following modification of ```docker-compose.yml``` will set t
|
||||||
```
|
```
|
||||||
|
|
||||||
### Why are only photos and not videos being uploaded to Immich?
|
### Why are only photos and not videos being uploaded to Immich?
|
||||||
|
|
||||||
This often happens when using a reverse proxy or cloudflare tunnel in front of Immich. Make sure to set your reverse proxy to allow large POST requests. In `nginx`, set `client_max_body_size 50000M;` or similar. Cloudflare tunnels are limited to 100 mb file sizes.
|
This often happens when using a reverse proxy or cloudflare tunnel in front of Immich. Make sure to set your reverse proxy to allow large POST requests. In `nginx`, set `client_max_body_size 50000M;` or similar. Cloudflare tunnels are limited to 100 mb file sizes.
|
||||||
|
|
||||||
### Why is Immich slow on low-memory systems like the Raspberry Pi?
|
### Why is Immich slow on low-memory systems like the Raspberry Pi?
|
||||||
|
|
||||||
Immich uses optional machine-learning features to enhance search results. This feature, however, can be too heavy to run on a Raspberry Pi. To disable machine learning, comment out the `immich-machine-learning` section of your docker-compose.yml and set `IMMICH_MACHINE_LEARNING_URL=false` in your .env file.
|
Immich uses optional machine-learning features to enhance search results. This feature, however, can be too heavy to run on a Raspberry Pi. To disable machine learning, comment out the `immich-machine-learning` section of your docker-compose.yml and set `IMMICH_MACHINE_LEARNING_URL=false` in your .env file.
|
||||||
|
|
||||||
### What happens to existing files after I choose a new [Storage Template](/docs/administration/storage-template.mdx)?
|
### What happens to existing files after I choose a new [Storage Template](/docs/administration/storage-template.mdx)?
|
||||||
|
@ -44,6 +46,7 @@ Immich uses optional machine-learning features to enhance search results. This f
|
||||||
Template changes will only apply to new assets. To retroactively apply the template to previously uploaded assets, run the Storage Migration Job, available on the [Jobs](/docs/administration/jobs.md) page.
|
Template changes will only apply to new assets. To retroactively apply the template to previously uploaded assets, run the Storage Migration Job, available on the [Jobs](/docs/administration/jobs.md) page.
|
||||||
|
|
||||||
### In the uploads folder, why are photos stored in the wrong date?
|
### In the uploads folder, why are photos stored in the wrong date?
|
||||||
|
|
||||||
This is fixed by running the storage migration job.
|
This is fixed by running the storage migration job.
|
||||||
|
|
||||||
### Why is object detection not very good?
|
### Why is object detection not very good?
|
||||||
|
@ -83,4 +86,4 @@ After removing the the containers and volumes, the **Files** can be cleaned up (
|
||||||
|
|
||||||
### Why iOS app shows duplicate photos on the timeline while the web doesn't?
|
### Why iOS app shows duplicate photos on the timeline while the web doesn't?
|
||||||
|
|
||||||
If you are using `My Photo Stream`, the Photos app temporarily creates duplicates of photos taken in the last 30 days. These photos are included in the `Recents` album and thus shown up twice. To fix this, you can disable `My Photo Stream` in the native Photos app or choose a different album in the backup screen in Immich.
|
If you are using `My Photo Stream`, the Photos app temporarily creates duplicates of photos taken in the last 30 days. These photos are included in the `Recents` album and thus shown up twice. To fix this, you can disable `My Photo Stream` in the native Photos app or choose a different album in the backup screen in Immich.
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
{
|
{
|
||||||
"label": "Administration",
|
"label": "Administration",
|
||||||
"position": 4
|
"position": 4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,6 @@ Immich is a full-stack [TypeScript](https://www.typescriptlang.org/) application
|
||||||
- [Redis](https://redis.io/) for job queuing.
|
- [Redis](https://redis.io/) for job queuing.
|
||||||
- [Typesense](https://typesense.org/) for search.
|
- [Typesense](https://typesense.org/) for search.
|
||||||
|
|
||||||
|
|
||||||
### Web Server
|
### Web Server
|
||||||
|
|
||||||
- [NGINX](https://www.nginx.com/) for internal communication between containers and load balancing when scaling.
|
- [NGINX](https://www.nginx.com/) for internal communication between containers and load balancing when scaling.
|
||||||
|
|
|
@ -14,17 +14,19 @@ Background backup is available thanks to the contribution effort of [@zoodyy](ht
|
||||||
|
|
||||||
If background backup is enabled. The app will periodically check if there are any new photos or videos in the selected album(s) to be uploaded to the cloud. If there are, it will upload them to the cloud in the background.
|
If background backup is enabled. The app will periodically check if there are any new photos or videos in the selected album(s) to be uploaded to the cloud. If there are, it will upload them to the cloud in the background.
|
||||||
|
|
||||||
|
|
||||||
:::info Note
|
:::info Note
|
||||||
|
|
||||||
#### General
|
#### General
|
||||||
|
|
||||||
- The app must be in the background for the backup worker to start running.
|
- The app must be in the background for the backup worker to start running.
|
||||||
- If you reopen the app and the first page you see is the backup page, the counts will not reflect the background uploaded result. You have to navigate out of the page and come back to see the updated counts.
|
- If you reopen the app and the first page you see is the backup page, the counts will not reflect the background uploaded result. You have to navigate out of the page and come back to see the updated counts.
|
||||||
|
|
||||||
#### Android
|
#### Android
|
||||||
|
|
||||||
- It is a well-known problem that some Android models are very strict with battery optimization settings, which can cause a problem with the background worker. Please visit [Don't kill my app](https://dontkillmyapp.com/) for a guide on disabling this setting on your phone.
|
- It is a well-known problem that some Android models are very strict with battery optimization settings, which can cause a problem with the background worker. Please visit [Don't kill my app](https://dontkillmyapp.com/) for a guide on disabling this setting on your phone.
|
||||||
|
|
||||||
#### iOS
|
#### iOS
|
||||||
|
|
||||||
- You must enable **Background App Refresh** for the app to work in the background. You can enable it in the Settings app under General > Background App Refresh.
|
- You must enable **Background App Refresh** for the app to work in the background. You can enable it in the Settings app under General > Background App Refresh.
|
||||||
|
|
||||||
<div style={{textAlign: 'center'}}>
|
<div style={{textAlign: 'center'}}>
|
||||||
|
|
|
@ -49,7 +49,6 @@ The API key can be obtained in the user setting panel on the web interface.
|
||||||
|
|
||||||
![Obtain Api Key](./img/obtain-api-key.png)
|
![Obtain Api Key](./img/obtain-api-key.png)
|
||||||
|
|
||||||
|
|
||||||
### Run via Docker
|
### Run via Docker
|
||||||
|
|
||||||
You can run the CLI inside of a docker container to avoid needing to install anything.
|
You can run the CLI inside of a docker container to avoid needing to install anything.
|
||||||
|
@ -74,6 +73,7 @@ immich upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api
|
||||||
|
|
||||||
:::tip Internal networking
|
:::tip Internal networking
|
||||||
If you are running the CLI container on the same machine as your Immich server, you may not be able to reach the external address. In that case, try the following steps:
|
If you are running the CLI container on the same machine as your Immich server, you may not be able to reach the external address. In that case, try the following steps:
|
||||||
|
|
||||||
1. Find the internal Docker network used by Immich via `docker network ls`.
|
1. Find the internal Docker network used by Immich via `docker network ls`.
|
||||||
2. Adapt the above command to pass the `--network <immich_network>` argument to `docker run`, substituting `<immich_network>` with the result from step 1.
|
2. Adapt the above command to pass the `--network <immich_network>` argument to `docker run`, substituting `<immich_network>` with the result from step 1.
|
||||||
3. Use `--server http://immich-server:3001/` for the upload command instead of the external address.
|
3. Use `--server http://immich-server:3001/` for the upload command instead of the external address.
|
||||||
|
@ -81,6 +81,7 @@ If you are running the CLI container on the same machine as your Immich server,
|
||||||
```bash title="Upload to internal address"
|
```bash title="Upload to internal address"
|
||||||
docker run --network immich_default -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest upload --key HFEJ38DNSDUEG --server http://immich-server:3001/
|
docker run --network immich_default -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest upload --key HFEJ38DNSDUEG --server http://immich-server:3001/
|
||||||
```
|
```
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### Run from source
|
### Run from source
|
||||||
|
|
|
@ -4,7 +4,7 @@ Immich supports [Reverse Geocoding](https://en.wikipedia.org/wiki/Reverse_geocod
|
||||||
|
|
||||||
## Extraction
|
## Extraction
|
||||||
|
|
||||||
During Exif Extraction, assets with latitudes and longitudes are reverse geocoded to determine their City, State, and Country.
|
During Exif Extraction, assets with latitudes and longitudes are reverse geocoded to determine their City, State, and Country.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Search
|
# Search
|
||||||
|
|
||||||
Immich uses Typesense as the primary search database to enable high performance search mechanism.
|
Immich uses Typesense as the primary search database to enable high performance search mechanism.
|
||||||
|
|
||||||
Typesense is a powerful search engine that can be integrated with popular natural language processing (NLP) models like CLIP and SBERT to provide highly accurate and relevant search results. Here are some benefits of using Typesense integrated search for CLIP and SBERT:
|
Typesense is a powerful search engine that can be integrated with popular natural language processing (NLP) models like CLIP and SBERT to provide highly accurate and relevant search results. Here are some benefits of using Typesense integrated search for CLIP and SBERT:
|
||||||
|
|
||||||
|
@ -10,11 +10,11 @@ Faster Search Response Times: Typesense is optimized for lightning-fast search r
|
||||||
|
|
||||||
Enhanced Semantic Search Capabilities: CLIP and SBERT are powerful NLP models that can extract the semantic meaning from text, enabling more nuanced search queries. By integrating with Typesense, these models can help to improve the accuracy of semantic search, enabling users to find the most relevant results based on the true meaning of their query.
|
Enhanced Semantic Search Capabilities: CLIP and SBERT are powerful NLP models that can extract the semantic meaning from text, enabling more nuanced search queries. By integrating with Typesense, these models can help to improve the accuracy of semantic search, enabling users to find the most relevant results based on the true meaning of their query.
|
||||||
|
|
||||||
Greater Search Flexibility: Typesense provides flexible search capabilities, including fuzzy search, partial search, enabling users to find the information they need quickly and easily. When integrated with CLIP and SBERT, Typesense can offer even greater flexibility, allowing users to refine their search queries using natural language and providing more accurate and relevant results.
|
Greater Search Flexibility: Typesense provides flexible search capabilities, including fuzzy search, partial search, enabling users to find the information they need quickly and easily. When integrated with CLIP and SBERT, Typesense can offer even greater flexibility, allowing users to refine their search queries using natural language and providing more accurate and relevant results.
|
||||||
|
|
||||||
(Generated by Chat-GPT4)
|
(Generated by Chat-GPT4)
|
||||||
|
|
||||||
Some search examples:
|
Some search examples:
|
||||||
<img src={require('./img/search-ex-2.webp').default} title='Search Example 1' />
|
<img src={require('./img/search-ex-2.webp').default} title='Search Example 1' />
|
||||||
|
|
||||||
<img src={require('./img/search-ex-3.webp').default} title='Search Example 2' />
|
<img src={require('./img/search-ex-3.webp').default} title='Search Example 2' />
|
||||||
|
|
|
@ -10,7 +10,7 @@ View your User ID and email, and update your first and last name.
|
||||||
|
|
||||||
## Change Password
|
## Change Password
|
||||||
|
|
||||||
Users can change their own passwords.
|
Users can change their own passwords.
|
||||||
|
|
||||||
![Change Password](./img/user-change-password.png)
|
![Change Password](./img/user-change-password.png)
|
||||||
|
|
||||||
|
|
|
@ -13,37 +13,36 @@ Install Immich using Portainer's Stack feature.
|
||||||
5. Replace `.env` with `stack.env` for all containers that need to use environment variables in the web editor.
|
5. Replace `.env` with `stack.env` for all containers that need to use environment variables in the web editor.
|
||||||
|
|
||||||
<img
|
<img
|
||||||
src={require('./img/dot-env.png').default}
|
src={require('./img/dot-env.png').default}
|
||||||
width="50%"
|
width="50%"
|
||||||
style={{border: '1px solid #ddd'}}
|
style={{border: '1px solid #ddd'}}
|
||||||
alt="Dot Env Example"
|
alt="Dot Env Example"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
8. Click on "**Advanced Mode**" in the **Environment Variables** section.
|
8. Click on "**Advanced Mode**" in the **Environment Variables** section.
|
||||||
|
|
||||||
<img
|
<img
|
||||||
src={require('./img/env-1.png').default}
|
src={require('./img/env-1.png').default}
|
||||||
width="50%"
|
width="50%"
|
||||||
style={{border: '1px solid #ddd'}}
|
style={{border: '1px solid #ddd'}}
|
||||||
alt="Dot Env Example"
|
alt="Dot Env Example"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
9. Copy the content of the `example.env` file from the [GitHub repository](https://github.com/immich-app/immich/releases/latest/download/example.env) and paste into the editor.
|
9. Copy the content of the `example.env` file from the [GitHub repository](https://github.com/immich-app/immich/releases/latest/download/example.env) and paste into the editor.
|
||||||
10. Switch back to "**Simple Mode**".
|
10. Switch back to "**Simple Mode**".
|
||||||
|
|
||||||
<img
|
<img
|
||||||
src={require('./img/env-2.png').default}
|
src={require('./img/env-2.png').default}
|
||||||
width="50%"
|
width="50%"
|
||||||
style={{border: '1px solid #ddd'}}
|
style={{border: '1px solid #ddd'}}
|
||||||
alt="Dot Env Example"
|
alt="Dot Env Example"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
* Populate custom database information if necessary.
|
- Populate custom database information if necessary.
|
||||||
* Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets.
|
- Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets.
|
||||||
|
|
||||||
11. Click on "**Deploy the stack**".
|
11. Click on "**Deploy the stack**".
|
||||||
|
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
For more information on how to use the application, please refer to the [Post Installation](/docs/install/post-install.mdx) guide.
|
For more information on how to use the application, please refer to the [Post Installation](/docs/install/post-install.mdx) guide.
|
||||||
:::
|
:::
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
sidebar_position: 10
|
sidebar_position: 10
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
# Requirements
|
# Requirements
|
||||||
|
|
||||||
Hardware and software requirements for Immich
|
Hardware and software requirements for Immich
|
||||||
|
|
||||||
## Software
|
## Software
|
||||||
|
|
|
@ -5,6 +5,7 @@ sidebar_position: 60
|
||||||
# Unraid
|
# Unraid
|
||||||
|
|
||||||
Immich can easily be installed and updated on Unraid via:
|
Immich can easily be installed and updated on Unraid via:
|
||||||
|
|
||||||
1. [Docker Compose Manager](https://forums.unraid.net/topic/114415-plugin-docker-compose-manager/) plugin from the Unraid Community Apps
|
1. [Docker Compose Manager](https://forums.unraid.net/topic/114415-plugin-docker-compose-manager/) plugin from the Unraid Community Apps
|
||||||
2. Community made template on the Unraid Community Apps
|
2. Community made template on the Unraid Community Apps
|
||||||
|
|
||||||
|
@ -20,7 +21,7 @@ In order to install Immich from the Unraid CA, you will need an existing Redis a
|
||||||
|
|
||||||
Once you have Redis and PostgreSQL running, search for Immich on the Unraid CA, choose either of the templates listed and fill out the example variables.
|
Once you have Redis and PostgreSQL running, search for Immich on the Unraid CA, choose either of the templates listed and fill out the example variables.
|
||||||
|
|
||||||
For more information about setting up the community image see [here](https://github.com/imagegenius/docker-immich#application-setup)
|
For more information about setting up the community image see [here](https://github.com/imagegenius/docker-immich#application-setup)
|
||||||
|
|
||||||
## Docker-Compose Method (Official)
|
## Docker-Compose Method (Official)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ sidebar_position: 1
|
||||||
|
|
||||||
# Introduction
|
# Introduction
|
||||||
|
|
||||||
<img src={require('./img/feature-panel.png').default} alt='Immich' />
|
<img src={require('./img/feature-panel.png').default} alt="Immich" />
|
||||||
|
|
||||||
## Welcome!
|
## Welcome!
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,6 @@ If you feel like this is the right cause and the app is something you see yourse
|
||||||
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
|
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
|
||||||
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
|
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
|
||||||
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
There are lots of non-monetary ways to contribute to Immich as well.
|
There are lots of non-monetary ways to contribute to Immich as well.
|
||||||
|
|
|
@ -7,8 +7,7 @@ const darkCodeTheme = require('prism-react-renderer/themes/dracula');
|
||||||
/** @type {import('@docusaurus/types').Config} */
|
/** @type {import('@docusaurus/types').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
title: 'Immich',
|
title: 'Immich',
|
||||||
tagline:
|
tagline: 'High performance self-hosted photo and video backup solution directly from your mobile phone',
|
||||||
'High performance self-hosted photo and video backup solution directly from your mobile phone',
|
|
||||||
url: 'https://documentation.immich.app',
|
url: 'https://documentation.immich.app',
|
||||||
baseUrl: '/',
|
baseUrl: '/',
|
||||||
onBrokenLinks: 'throw',
|
onBrokenLinks: 'throw',
|
||||||
|
|
22
docs/package-lock.json
generated
22
docs/package-lock.json
generated
|
@ -25,6 +25,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@docusaurus/module-type-aliases": "2.1.0",
|
"@docusaurus/module-type-aliases": "2.1.0",
|
||||||
"@tsconfig/docusaurus": "^1.0.5",
|
"@tsconfig/docusaurus": "^1.0.5",
|
||||||
|
"prettier": "^2.8.8",
|
||||||
"typescript": "^4.7.4"
|
"typescript": "^4.7.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -10538,6 +10539,21 @@
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "2.8.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
|
||||||
|
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin-prettier.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pretty-error": {
|
"node_modules/pretty-error": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz",
|
||||||
|
@ -22086,6 +22102,12 @@
|
||||||
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
|
||||||
"integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA=="
|
"integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA=="
|
||||||
},
|
},
|
||||||
|
"prettier": {
|
||||||
|
"version": "2.8.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
|
||||||
|
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"pretty-error": {
|
"pretty-error": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz",
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"docusaurus": "docusaurus",
|
"docusaurus": "docusaurus",
|
||||||
|
"format": "prettier --check .",
|
||||||
|
"format:fix": "prettier --write .",
|
||||||
"start": "docusaurus start",
|
"start": "docusaurus start",
|
||||||
"build": "docusaurus build",
|
"build": "docusaurus build",
|
||||||
"swizzle": "docusaurus swizzle",
|
"swizzle": "docusaurus swizzle",
|
||||||
|
@ -12,7 +14,7 @@
|
||||||
"serve": "docusaurus serve",
|
"serve": "docusaurus serve",
|
||||||
"write-translations": "docusaurus write-translations",
|
"write-translations": "docusaurus write-translations",
|
||||||
"write-heading-ids": "docusaurus write-heading-ids",
|
"write-heading-ids": "docusaurus write-heading-ids",
|
||||||
"typecheck": "tsc"
|
"check": "tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docusaurus/core": "2.1.0",
|
"@docusaurus/core": "2.1.0",
|
||||||
|
@ -32,6 +34,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@docusaurus/module-type-aliases": "2.1.0",
|
"@docusaurus/module-type-aliases": "2.1.0",
|
||||||
"@tsconfig/docusaurus": "^1.0.5",
|
"@tsconfig/docusaurus": "^1.0.5",
|
||||||
|
"prettier": "^2.8.8",
|
||||||
"typescript": "^4.7.4"
|
"typescript": "^4.7.4"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
|
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
|
||||||
const sidebars = {
|
const sidebars = {
|
||||||
// By default, Docusaurus generates a sidebar from the docs folder structure
|
// By default, Docusaurus generates a sidebar from the docs folder structure
|
||||||
tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
|
tutorialSidebar: [{ type: 'autogenerated', dirName: '.' }],
|
||||||
|
|
||||||
// But you can create a sidebar manually
|
// But you can create a sidebar manually
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -14,8 +14,8 @@ const FeatureList: FeatureItem[] = [
|
||||||
Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
|
Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
Docusaurus was designed from the ground up to be easily installed and
|
Docusaurus was designed from the ground up to be easily installed and used to get your website up and running
|
||||||
used to get your website up and running quickly.
|
quickly.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -24,8 +24,8 @@ const FeatureList: FeatureItem[] = [
|
||||||
Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
|
Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
Docusaurus lets you focus on your docs, and we'll do the chores. Go
|
Docusaurus lets you focus on your docs, and we'll do the chores. Go ahead and move your docs into the{' '}
|
||||||
ahead and move your docs into the <code>docs</code> directory.
|
<code>docs</code> directory.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -34,14 +34,14 @@ const FeatureList: FeatureItem[] = [
|
||||||
Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
|
Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
Extend or customize your website layout by reusing React. Docusaurus can
|
Extend or customize your website layout by reusing React. Docusaurus can be extended while reusing the same
|
||||||
be extended while reusing the same header and footer.
|
header and footer.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function Feature({title, Svg, description}: FeatureItem) {
|
function Feature({ title, Svg, description }: FeatureItem) {
|
||||||
return (
|
return (
|
||||||
<div className={clsx('col col--4')}>
|
<div className={clsx('col col--4')}>
|
||||||
<div className="text--center">
|
<div className="text--center">
|
||||||
|
|
|
@ -7,12 +7,12 @@
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@import url("https://fonts.googleapis.com/css2?family=Overpass:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap");
|
@import url('https://fonts.googleapis.com/css2?family=Overpass:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap');
|
||||||
@import url("https://fonts.googleapis.com/css2?family=Snowburst+One&display=swap");
|
@import url('https://fonts.googleapis.com/css2?family=Snowburst+One&display=swap');
|
||||||
|
|
||||||
html,
|
html,
|
||||||
button {
|
button {
|
||||||
font-family: "Overpass", sans-serif;
|
font-family: 'Overpass', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* You can override the default Infima variables here. */
|
/* You can override the default Infima variables here. */
|
||||||
|
@ -29,7 +29,7 @@ button {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* For readability concerns, you should choose a lighter palette in dark mode. */
|
/* For readability concerns, you should choose a lighter palette in dark mode. */
|
||||||
[data-theme="dark"] {
|
[data-theme='dark'] {
|
||||||
--ifm-color-primary: #adcbfa;
|
--ifm-color-primary: #adcbfa;
|
||||||
--ifm-color-primary-dark: #85b2f8;
|
--ifm-color-primary-dark: #85b2f8;
|
||||||
--ifm-color-primary-darker: #71a5f6;
|
--ifm-color-primary-darker: #71a5f6;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import Link from "@docusaurus/Link";
|
import Link from '@docusaurus/Link';
|
||||||
import Layout from "@theme/Layout";
|
import Layout from '@theme/Layout';
|
||||||
|
|
||||||
function HomepageHeader() {
|
function HomepageHeader() {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,297 +1,256 @@
|
||||||
import Hogan from "hogan.js";
|
import Hogan from 'hogan.js';
|
||||||
import LunrSearchAdapter from "./lunar-search";
|
import LunrSearchAdapter from './lunar-search';
|
||||||
import autocomplete from "autocomplete.js";
|
import autocomplete from 'autocomplete.js';
|
||||||
import templates from "./templates";
|
import templates from './templates';
|
||||||
import utils from "./utils";
|
import utils from './utils';
|
||||||
import $ from "autocomplete.js/zepto";
|
import $ from 'autocomplete.js/zepto';
|
||||||
|
|
||||||
class DocSearch {
|
class DocSearch {
|
||||||
constructor({
|
constructor({
|
||||||
searchDocs,
|
searchDocs,
|
||||||
searchIndex,
|
searchIndex,
|
||||||
inputSelector,
|
inputSelector,
|
||||||
debug = false,
|
debug = false,
|
||||||
baseUrl = '/',
|
baseUrl = '/',
|
||||||
queryDataCallback = null,
|
queryDataCallback = null,
|
||||||
autocompleteOptions = {
|
autocompleteOptions = {
|
||||||
debug: false,
|
debug: false,
|
||||||
hint: false,
|
hint: false,
|
||||||
autoselect: true
|
autoselect: true,
|
||||||
|
},
|
||||||
|
transformData = false,
|
||||||
|
queryHook = false,
|
||||||
|
handleSelected = false,
|
||||||
|
enhancedSearchInput = false,
|
||||||
|
layout = 'collumns',
|
||||||
|
}) {
|
||||||
|
this.input = DocSearch.getInputFromSelector(inputSelector);
|
||||||
|
this.queryDataCallback = queryDataCallback || null;
|
||||||
|
const autocompleteOptionsDebug =
|
||||||
|
autocompleteOptions && autocompleteOptions.debug ? autocompleteOptions.debug : false;
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
autocompleteOptions.debug = debug || autocompleteOptionsDebug;
|
||||||
|
this.autocompleteOptions = autocompleteOptions;
|
||||||
|
this.autocompleteOptions.cssClasses = this.autocompleteOptions.cssClasses || {};
|
||||||
|
this.autocompleteOptions.cssClasses.prefix = this.autocompleteOptions.cssClasses.prefix || 'ds';
|
||||||
|
const inputAriaLabel = this.input && typeof this.input.attr === 'function' && this.input.attr('aria-label');
|
||||||
|
this.autocompleteOptions.ariaLabel = this.autocompleteOptions.ariaLabel || inputAriaLabel || 'search input';
|
||||||
|
|
||||||
|
this.isSimpleLayout = layout === 'simple';
|
||||||
|
|
||||||
|
this.client = new LunrSearchAdapter(searchDocs, searchIndex, baseUrl);
|
||||||
|
|
||||||
|
if (enhancedSearchInput) {
|
||||||
|
this.input = DocSearch.injectSearchBox(this.input);
|
||||||
|
}
|
||||||
|
this.autocomplete = autocomplete(this.input, autocompleteOptions, [
|
||||||
|
{
|
||||||
|
source: this.getAutocompleteSource(transformData, queryHook),
|
||||||
|
templates: {
|
||||||
|
suggestion: DocSearch.getSuggestionTemplate(this.isSimpleLayout),
|
||||||
|
footer: templates.footer,
|
||||||
|
empty: DocSearch.getEmptyTemplate(),
|
||||||
},
|
},
|
||||||
transformData = false,
|
},
|
||||||
queryHook = false,
|
]);
|
||||||
handleSelected = false,
|
|
||||||
enhancedSearchInput = false,
|
const customHandleSelected = handleSelected;
|
||||||
layout = "collumns"
|
this.handleSelected = customHandleSelected || this.handleSelected;
|
||||||
}) {
|
|
||||||
this.input = DocSearch.getInputFromSelector(inputSelector);
|
// We prevent default link clicking if a custom handleSelected is defined
|
||||||
this.queryDataCallback = queryDataCallback || null;
|
if (customHandleSelected) {
|
||||||
const autocompleteOptionsDebug =
|
$('.algolia-autocomplete').on('click', '.ds-suggestions a', (event) => {
|
||||||
autocompleteOptions && autocompleteOptions.debug
|
event.preventDefault();
|
||||||
? autocompleteOptions.debug
|
});
|
||||||
: false;
|
}
|
||||||
|
|
||||||
|
this.autocomplete.on('autocomplete:selected', this.handleSelected.bind(null, this.autocomplete.autocomplete));
|
||||||
|
|
||||||
|
this.autocomplete.on('autocomplete:shown', this.handleShown.bind(null, this.input));
|
||||||
|
|
||||||
|
if (enhancedSearchInput) {
|
||||||
|
DocSearch.bindSearchBoxEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static injectSearchBox(input) {
|
||||||
|
input.before(templates.searchBox);
|
||||||
|
const newInput = input.prev().prev().find('input');
|
||||||
|
input.remove();
|
||||||
|
return newInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bindSearchBoxEvent() {
|
||||||
|
$('.searchbox [type="reset"]').on('click', function () {
|
||||||
|
$('input#docsearch').focus();
|
||||||
|
$(this).addClass('hide');
|
||||||
|
autocomplete.autocomplete.setVal('');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('input#docsearch').on('keyup', () => {
|
||||||
|
const searchbox = document.querySelector('input#docsearch');
|
||||||
|
const reset = document.querySelector('.searchbox [type="reset"]');
|
||||||
|
reset.className = 'searchbox__reset';
|
||||||
|
if (searchbox.value.length === 0) {
|
||||||
|
reset.className += ' hide';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the matching input from a CSS selector, null if none matches
|
||||||
|
* @function getInputFromSelector
|
||||||
|
* @param {string} selector CSS selector that matches the search
|
||||||
|
* input of the page
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
static getInputFromSelector(selector) {
|
||||||
|
const input = $(selector).filter('input');
|
||||||
|
return input.length ? $(input[0]) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the `source` method to be passed to autocomplete.js. It will query
|
||||||
|
* the Algolia index and call the callbacks with the formatted hits.
|
||||||
|
* @function getAutocompleteSource
|
||||||
|
* @param {function} transformData An optional function to transform the hits
|
||||||
|
* @param {function} queryHook An optional function to transform the query
|
||||||
|
* @returns {function} Method to be passed as the `source` option of
|
||||||
|
* autocomplete
|
||||||
|
*/
|
||||||
|
getAutocompleteSource(transformData, queryHook) {
|
||||||
|
return (query, callback) => {
|
||||||
|
if (queryHook) {
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
autocompleteOptions.debug = debug || autocompleteOptionsDebug;
|
query = queryHook(query) || query;
|
||||||
this.autocompleteOptions = autocompleteOptions;
|
}
|
||||||
this.autocompleteOptions.cssClasses =
|
this.client.search(query).then((hits) => {
|
||||||
this.autocompleteOptions.cssClasses || {};
|
if (this.queryDataCallback && typeof this.queryDataCallback == 'function') {
|
||||||
this.autocompleteOptions.cssClasses.prefix =
|
this.queryDataCallback(hits);
|
||||||
this.autocompleteOptions.cssClasses.prefix || "ds";
|
|
||||||
const inputAriaLabel =
|
|
||||||
this.input &&
|
|
||||||
typeof this.input.attr === "function" &&
|
|
||||||
this.input.attr("aria-label");
|
|
||||||
this.autocompleteOptions.ariaLabel =
|
|
||||||
this.autocompleteOptions.ariaLabel || inputAriaLabel || "search input";
|
|
||||||
|
|
||||||
this.isSimpleLayout = layout === "simple";
|
|
||||||
|
|
||||||
this.client = new LunrSearchAdapter(searchDocs, searchIndex, baseUrl);
|
|
||||||
|
|
||||||
if (enhancedSearchInput) {
|
|
||||||
this.input = DocSearch.injectSearchBox(this.input);
|
|
||||||
}
|
}
|
||||||
this.autocomplete = autocomplete(this.input, autocompleteOptions, [
|
if (transformData) {
|
||||||
{
|
hits = transformData(hits) || hits;
|
||||||
source: this.getAutocompleteSource(transformData, queryHook),
|
|
||||||
templates: {
|
|
||||||
suggestion: DocSearch.getSuggestionTemplate(this.isSimpleLayout),
|
|
||||||
footer: templates.footer,
|
|
||||||
empty: DocSearch.getEmptyTemplate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
const customHandleSelected = handleSelected;
|
|
||||||
this.handleSelected = customHandleSelected || this.handleSelected;
|
|
||||||
|
|
||||||
// We prevent default link clicking if a custom handleSelected is defined
|
|
||||||
if (customHandleSelected) {
|
|
||||||
$(".algolia-autocomplete").on("click", ".ds-suggestions a", event => {
|
|
||||||
event.preventDefault();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
callback(DocSearch.formatHits(hits));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
this.autocomplete.on(
|
// Given a list of hits returned by the API, will reformat them to be used in
|
||||||
"autocomplete:selected",
|
// a Hogan template
|
||||||
this.handleSelected.bind(null, this.autocomplete.autocomplete)
|
static formatHits(receivedHits) {
|
||||||
);
|
const clonedHits = utils.deepClone(receivedHits);
|
||||||
|
const hits = clonedHits.map((hit) => {
|
||||||
|
if (hit._highlightResult) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
hit._highlightResult = utils.mergeKeyWithParent(hit._highlightResult, 'hierarchy');
|
||||||
|
}
|
||||||
|
return utils.mergeKeyWithParent(hit, 'hierarchy');
|
||||||
|
});
|
||||||
|
|
||||||
this.autocomplete.on(
|
// Group hits by category / subcategory
|
||||||
"autocomplete:shown",
|
let groupedHits = utils.groupBy(hits, 'lvl0');
|
||||||
this.handleShown.bind(null, this.input)
|
$.each(groupedHits, (level, collection) => {
|
||||||
);
|
const groupedHitsByLvl1 = utils.groupBy(collection, 'lvl1');
|
||||||
|
const flattenedHits = utils.flattenAndFlagFirst(groupedHitsByLvl1, 'isSubCategoryHeader');
|
||||||
|
groupedHits[level] = flattenedHits;
|
||||||
|
});
|
||||||
|
groupedHits = utils.flattenAndFlagFirst(groupedHits, 'isCategoryHeader');
|
||||||
|
|
||||||
if (enhancedSearchInput) {
|
// Translate hits into smaller objects to be send to the template
|
||||||
DocSearch.bindSearchBoxEvent();
|
return groupedHits.map((hit) => {
|
||||||
}
|
const url = DocSearch.formatURL(hit);
|
||||||
|
const category = utils.getHighlightedValue(hit, 'lvl0');
|
||||||
|
const subcategory = utils.getHighlightedValue(hit, 'lvl1') || category;
|
||||||
|
const displayTitle = utils
|
||||||
|
.compact([
|
||||||
|
utils.getHighlightedValue(hit, 'lvl2') || subcategory,
|
||||||
|
utils.getHighlightedValue(hit, 'lvl3'),
|
||||||
|
utils.getHighlightedValue(hit, 'lvl4'),
|
||||||
|
utils.getHighlightedValue(hit, 'lvl5'),
|
||||||
|
utils.getHighlightedValue(hit, 'lvl6'),
|
||||||
|
])
|
||||||
|
.join('<span class="aa-suggestion-title-separator" aria-hidden="true"> › </span>');
|
||||||
|
const text = utils.getSnippetedValue(hit, 'content');
|
||||||
|
const isTextOrSubcategoryNonEmpty = (subcategory && subcategory !== '') || (displayTitle && displayTitle !== '');
|
||||||
|
const isLvl1EmptyOrDuplicate = !subcategory || subcategory === '' || subcategory === category;
|
||||||
|
const isLvl2 = displayTitle && displayTitle !== '' && displayTitle !== subcategory;
|
||||||
|
const isLvl1 = !isLvl2 && subcategory && subcategory !== '' && subcategory !== category;
|
||||||
|
const isLvl0 = !isLvl1 && !isLvl2;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLvl0,
|
||||||
|
isLvl1,
|
||||||
|
isLvl2,
|
||||||
|
isLvl1EmptyOrDuplicate,
|
||||||
|
isCategoryHeader: hit.isCategoryHeader,
|
||||||
|
isSubCategoryHeader: hit.isSubCategoryHeader,
|
||||||
|
isTextOrSubcategoryNonEmpty,
|
||||||
|
category,
|
||||||
|
subcategory,
|
||||||
|
title: displayTitle,
|
||||||
|
text,
|
||||||
|
url,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static formatURL(hit) {
|
||||||
|
const { url, anchor } = hit;
|
||||||
|
if (url) {
|
||||||
|
const containsAnchor = url.indexOf('#') !== -1;
|
||||||
|
if (containsAnchor) return url;
|
||||||
|
else if (anchor) return `${hit.url}#${hit.anchor}`;
|
||||||
|
return url;
|
||||||
|
} else if (anchor) return `#${hit.anchor}`;
|
||||||
|
/* eslint-disable */
|
||||||
|
console.warn('no anchor nor url for : ', JSON.stringify(hit));
|
||||||
|
/* eslint-enable */
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getEmptyTemplate() {
|
||||||
|
return (args) => Hogan.compile(templates.empty).render(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getSuggestionTemplate(isSimpleLayout) {
|
||||||
|
const stringTemplate = isSimpleLayout ? templates.suggestionSimple : templates.suggestion;
|
||||||
|
const template = Hogan.compile(stringTemplate);
|
||||||
|
return (suggestion) => template.render(suggestion);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSelected(input, event, suggestion, datasetNumber, context = {}) {
|
||||||
|
// Do nothing if click on the suggestion, as it's already a <a href>, the
|
||||||
|
// browser will take care of it. This allow Ctrl-Clicking on results and not
|
||||||
|
// having the main window being redirected as well
|
||||||
|
if (context.selectionMethod === 'click') {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
static injectSearchBox(input) {
|
input.setVal('');
|
||||||
input.before(templates.searchBox);
|
window.location.assign(suggestion.url);
|
||||||
const newInput = input
|
}
|
||||||
.prev()
|
|
||||||
.prev()
|
handleShown(input) {
|
||||||
.find("input");
|
const middleOfInput = input.offset().left + input.width() / 2;
|
||||||
input.remove();
|
let middleOfWindow = $(document).width() / 2;
|
||||||
return newInput;
|
|
||||||
|
if (isNaN(middleOfWindow)) {
|
||||||
|
middleOfWindow = 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bindSearchBoxEvent() {
|
const alignClass = middleOfInput - middleOfWindow >= 0 ? 'algolia-autocomplete-right' : 'algolia-autocomplete-left';
|
||||||
$('.searchbox [type="reset"]').on("click", function () {
|
const otherAlignClass =
|
||||||
$("input#docsearch").focus();
|
middleOfInput - middleOfWindow < 0 ? 'algolia-autocomplete-right' : 'algolia-autocomplete-left';
|
||||||
$(this).addClass("hide");
|
const autocompleteWrapper = $('.algolia-autocomplete');
|
||||||
autocomplete.autocomplete.setVal("");
|
if (!autocompleteWrapper.hasClass(alignClass)) {
|
||||||
});
|
autocompleteWrapper.addClass(alignClass);
|
||||||
|
|
||||||
$("input#docsearch").on("keyup", () => {
|
|
||||||
const searchbox = document.querySelector("input#docsearch");
|
|
||||||
const reset = document.querySelector('.searchbox [type="reset"]');
|
|
||||||
reset.className = "searchbox__reset";
|
|
||||||
if (searchbox.value.length === 0) {
|
|
||||||
reset.className += " hide";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
if (autocompleteWrapper.hasClass(otherAlignClass)) {
|
||||||
* Returns the matching input from a CSS selector, null if none matches
|
autocompleteWrapper.removeClass(otherAlignClass);
|
||||||
* @function getInputFromSelector
|
|
||||||
* @param {string} selector CSS selector that matches the search
|
|
||||||
* input of the page
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
static getInputFromSelector(selector) {
|
|
||||||
const input = $(selector).filter("input");
|
|
||||||
return input.length ? $(input[0]) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the `source` method to be passed to autocomplete.js. It will query
|
|
||||||
* the Algolia index and call the callbacks with the formatted hits.
|
|
||||||
* @function getAutocompleteSource
|
|
||||||
* @param {function} transformData An optional function to transform the hits
|
|
||||||
* @param {function} queryHook An optional function to transform the query
|
|
||||||
* @returns {function} Method to be passed as the `source` option of
|
|
||||||
* autocomplete
|
|
||||||
*/
|
|
||||||
getAutocompleteSource(transformData, queryHook) {
|
|
||||||
return (query, callback) => {
|
|
||||||
if (queryHook) {
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
query = queryHook(query) || query;
|
|
||||||
}
|
|
||||||
this.client.search(query).then(hits => {
|
|
||||||
if (
|
|
||||||
this.queryDataCallback &&
|
|
||||||
typeof this.queryDataCallback == "function"
|
|
||||||
) {
|
|
||||||
this.queryDataCallback(hits);
|
|
||||||
}
|
|
||||||
if (transformData) {
|
|
||||||
hits = transformData(hits) || hits;
|
|
||||||
}
|
|
||||||
callback(DocSearch.formatHits(hits));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given a list of hits returned by the API, will reformat them to be used in
|
|
||||||
// a Hogan template
|
|
||||||
static formatHits(receivedHits) {
|
|
||||||
const clonedHits = utils.deepClone(receivedHits);
|
|
||||||
const hits = clonedHits.map(hit => {
|
|
||||||
if (hit._highlightResult) {
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
hit._highlightResult = utils.mergeKeyWithParent(
|
|
||||||
hit._highlightResult,
|
|
||||||
"hierarchy"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return utils.mergeKeyWithParent(hit, "hierarchy");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Group hits by category / subcategory
|
|
||||||
let groupedHits = utils.groupBy(hits, "lvl0");
|
|
||||||
$.each(groupedHits, (level, collection) => {
|
|
||||||
const groupedHitsByLvl1 = utils.groupBy(collection, "lvl1");
|
|
||||||
const flattenedHits = utils.flattenAndFlagFirst(
|
|
||||||
groupedHitsByLvl1,
|
|
||||||
"isSubCategoryHeader"
|
|
||||||
);
|
|
||||||
groupedHits[level] = flattenedHits;
|
|
||||||
});
|
|
||||||
groupedHits = utils.flattenAndFlagFirst(groupedHits, "isCategoryHeader");
|
|
||||||
|
|
||||||
// Translate hits into smaller objects to be send to the template
|
|
||||||
return groupedHits.map(hit => {
|
|
||||||
const url = DocSearch.formatURL(hit);
|
|
||||||
const category = utils.getHighlightedValue(hit, "lvl0");
|
|
||||||
const subcategory = utils.getHighlightedValue(hit, "lvl1") || category;
|
|
||||||
const displayTitle = utils
|
|
||||||
.compact([
|
|
||||||
utils.getHighlightedValue(hit, "lvl2") || subcategory,
|
|
||||||
utils.getHighlightedValue(hit, "lvl3"),
|
|
||||||
utils.getHighlightedValue(hit, "lvl4"),
|
|
||||||
utils.getHighlightedValue(hit, "lvl5"),
|
|
||||||
utils.getHighlightedValue(hit, "lvl6")
|
|
||||||
])
|
|
||||||
.join(
|
|
||||||
'<span class="aa-suggestion-title-separator" aria-hidden="true"> › </span>'
|
|
||||||
);
|
|
||||||
const text = utils.getSnippetedValue(hit, "content");
|
|
||||||
const isTextOrSubcategoryNonEmpty =
|
|
||||||
(subcategory && subcategory !== "") ||
|
|
||||||
(displayTitle && displayTitle !== "");
|
|
||||||
const isLvl1EmptyOrDuplicate =
|
|
||||||
!subcategory || subcategory === "" || subcategory === category;
|
|
||||||
const isLvl2 =
|
|
||||||
displayTitle && displayTitle !== "" && displayTitle !== subcategory;
|
|
||||||
const isLvl1 =
|
|
||||||
!isLvl2 &&
|
|
||||||
(subcategory && subcategory !== "" && subcategory !== category);
|
|
||||||
const isLvl0 = !isLvl1 && !isLvl2;
|
|
||||||
|
|
||||||
return {
|
|
||||||
isLvl0,
|
|
||||||
isLvl1,
|
|
||||||
isLvl2,
|
|
||||||
isLvl1EmptyOrDuplicate,
|
|
||||||
isCategoryHeader: hit.isCategoryHeader,
|
|
||||||
isSubCategoryHeader: hit.isSubCategoryHeader,
|
|
||||||
isTextOrSubcategoryNonEmpty,
|
|
||||||
category,
|
|
||||||
subcategory,
|
|
||||||
title: displayTitle,
|
|
||||||
text,
|
|
||||||
url
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static formatURL(hit) {
|
|
||||||
const { url, anchor } = hit;
|
|
||||||
if (url) {
|
|
||||||
const containsAnchor = url.indexOf("#") !== -1;
|
|
||||||
if (containsAnchor) return url;
|
|
||||||
else if (anchor) return `${hit.url}#${hit.anchor}`;
|
|
||||||
return url;
|
|
||||||
} else if (anchor) return `#${hit.anchor}`;
|
|
||||||
/* eslint-disable */
|
|
||||||
console.warn("no anchor nor url for : ", JSON.stringify(hit));
|
|
||||||
/* eslint-enable */
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getEmptyTemplate() {
|
|
||||||
return args => Hogan.compile(templates.empty).render(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
static getSuggestionTemplate(isSimpleLayout) {
|
|
||||||
const stringTemplate = isSimpleLayout
|
|
||||||
? templates.suggestionSimple
|
|
||||||
: templates.suggestion;
|
|
||||||
const template = Hogan.compile(stringTemplate);
|
|
||||||
return suggestion => template.render(suggestion);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSelected(input, event, suggestion, datasetNumber, context = {}) {
|
|
||||||
// Do nothing if click on the suggestion, as it's already a <a href>, the
|
|
||||||
// browser will take care of it. This allow Ctrl-Clicking on results and not
|
|
||||||
// having the main window being redirected as well
|
|
||||||
if (context.selectionMethod === "click") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.setVal("");
|
|
||||||
window.location.assign(suggestion.url);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleShown(input) {
|
|
||||||
const middleOfInput = input.offset().left + input.width() / 2;
|
|
||||||
let middleOfWindow = $(document).width() / 2;
|
|
||||||
|
|
||||||
if (isNaN(middleOfWindow)) {
|
|
||||||
middleOfWindow = 900;
|
|
||||||
}
|
|
||||||
|
|
||||||
const alignClass =
|
|
||||||
middleOfInput - middleOfWindow >= 0
|
|
||||||
? "algolia-autocomplete-right"
|
|
||||||
: "algolia-autocomplete-left";
|
|
||||||
const otherAlignClass =
|
|
||||||
middleOfInput - middleOfWindow < 0
|
|
||||||
? "algolia-autocomplete-right"
|
|
||||||
: "algolia-autocomplete-left";
|
|
||||||
const autocompleteWrapper = $(".algolia-autocomplete");
|
|
||||||
if (!autocompleteWrapper.hasClass(alignClass)) {
|
|
||||||
autocompleteWrapper.addClass(alignClass);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autocompleteWrapper.hasClass(otherAlignClass)) {
|
|
||||||
autocompleteWrapper.removeClass(otherAlignClass);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DocSearch;
|
export default DocSearch;
|
||||||
|
|
|
@ -11,8 +11,7 @@
|
||||||
color: #3a33d1;
|
color: #3a33d1;
|
||||||
}
|
}
|
||||||
/* Highligted search terms in the main category headers */
|
/* Highligted search terms in the main category headers */
|
||||||
.algolia-docsearch-suggestion--category-header
|
.algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--highlight {
|
||||||
.algolia-docsearch-suggestion--highlight {
|
|
||||||
background-color: #4d47d5;
|
background-color: #4d47d5;
|
||||||
}
|
}
|
||||||
/* Currently selected suggestion */
|
/* Currently selected suggestion */
|
||||||
|
@ -343,9 +342,7 @@
|
||||||
background: inherit;
|
background: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.algolia-autocomplete
|
.algolia-autocomplete .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight {
|
||||||
.algolia-docsearch-suggestion--text
|
|
||||||
.algolia-docsearch-suggestion--highlight {
|
|
||||||
padding: 0 0 1px;
|
padding: 0 0 1px;
|
||||||
background: inherit;
|
background: inherit;
|
||||||
box-shadow: inset 0 -2px 0 0 rgba(69, 142, 225, 0.8);
|
box-shadow: inset 0 -2px 0 0 rgba(69, 142, 225, 0.8);
|
||||||
|
@ -419,14 +416,11 @@
|
||||||
.algolia-autocomplete
|
.algolia-autocomplete
|
||||||
.algolia-docsearch-suggestion.algolia-docsearch-suggestion__main
|
.algolia-docsearch-suggestion.algolia-docsearch-suggestion__main
|
||||||
.algolia-docsearch-suggestion--category-header,
|
.algolia-docsearch-suggestion--category-header,
|
||||||
.algolia-autocomplete
|
.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary {
|
||||||
.algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary {
|
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.algolia-autocomplete
|
.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column .algolia-docsearch-suggestion--highlight {
|
||||||
.algolia-docsearch-suggestion--subcategory-column
|
|
||||||
.algolia-docsearch-suggestion--highlight {
|
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
@ -459,9 +453,7 @@
|
||||||
margin-top: -8px;
|
margin-top: -8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.algolia-autocomplete
|
.algolia-autocomplete .algolia-docsearch-suggestion--no-results .algolia-docsearch-suggestion--text {
|
||||||
.algolia-docsearch-suggestion--no-results
|
|
||||||
.algolia-docsearch-suggestion--text {
|
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
@ -477,14 +469,10 @@
|
||||||
color: #222222;
|
color: #222222;
|
||||||
background-color: #ebebeb;
|
background-color: #ebebeb;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||||
monospace;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.algolia-autocomplete
|
.algolia-autocomplete .algolia-docsearch-suggestion code .algolia-docsearch-suggestion--highlight {
|
||||||
.algolia-docsearch-suggestion
|
|
||||||
code
|
|
||||||
.algolia-docsearch-suggestion--highlight {
|
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import React, { useRef, useCallback, useState } from "react";
|
import React, { useRef, useCallback, useState } from 'react';
|
||||||
import classnames from "classnames";
|
import classnames from 'classnames';
|
||||||
import { useHistory } from "@docusaurus/router";
|
import { useHistory } from '@docusaurus/router';
|
||||||
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||||
import { usePluginData } from '@docusaurus/useGlobalData';
|
import { usePluginData } from '@docusaurus/useGlobalData';
|
||||||
import useIsBrowser from "@docusaurus/useIsBrowser";
|
import useIsBrowser from '@docusaurus/useIsBrowser';
|
||||||
const Search = props => {
|
const Search = (props) => {
|
||||||
const initialized = useRef(false);
|
const initialized = useRef(false);
|
||||||
const searchBarRef = useRef(null);
|
const searchBarRef = useRef(null);
|
||||||
const [indexReady, setIndexReady] = useState(false);
|
const [indexReady, setIndexReady] = useState(false);
|
||||||
|
@ -13,65 +13,62 @@ const Search = props => {
|
||||||
const isBrowser = useIsBrowser();
|
const isBrowser = useIsBrowser();
|
||||||
const { baseUrl } = siteConfig;
|
const { baseUrl } = siteConfig;
|
||||||
const initAlgolia = (searchDocs, searchIndex, DocSearch) => {
|
const initAlgolia = (searchDocs, searchIndex, DocSearch) => {
|
||||||
new DocSearch({
|
new DocSearch({
|
||||||
searchDocs,
|
searchDocs,
|
||||||
searchIndex,
|
searchIndex,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
inputSelector: "#search_input_react",
|
inputSelector: '#search_input_react',
|
||||||
// Override algolia's default selection event, allowing us to do client-side
|
// Override algolia's default selection event, allowing us to do client-side
|
||||||
// navigation and avoiding a full page refresh.
|
// navigation and avoiding a full page refresh.
|
||||||
handleSelected: (_input, _event, suggestion) => {
|
handleSelected: (_input, _event, suggestion) => {
|
||||||
const url = suggestion.url || "/";
|
const url = suggestion.url || '/';
|
||||||
// Use an anchor tag to parse the absolute url into a relative url
|
// Use an anchor tag to parse the absolute url into a relative url
|
||||||
// Alternatively, we can use new URL(suggestion.url) but its not supported in IE
|
// Alternatively, we can use new URL(suggestion.url) but its not supported in IE
|
||||||
const a = document.createElement("a");
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
// Algolia use closest parent element id #__docusaurus when a h1 page title does not have an id
|
// Algolia use closest parent element id #__docusaurus when a h1 page title does not have an id
|
||||||
// So, we can safely remove it. See https://github.com/facebook/docusaurus/issues/1828 for more details.
|
// So, we can safely remove it. See https://github.com/facebook/docusaurus/issues/1828 for more details.
|
||||||
|
|
||||||
history.push(url);
|
history.push(url);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const pluginData = usePluginData('docusaurus-lunr-search');
|
const pluginData = usePluginData('docusaurus-lunr-search');
|
||||||
const getSearchDoc = () =>
|
const getSearchDoc = () =>
|
||||||
process.env.NODE_ENV === "production"
|
process.env.NODE_ENV === 'production'
|
||||||
? fetch(`${baseUrl}${pluginData.fileNames.searchDoc}`).then((content) => content.json())
|
? fetch(`${baseUrl}${pluginData.fileNames.searchDoc}`).then((content) => content.json())
|
||||||
: Promise.resolve([]);
|
: Promise.resolve([]);
|
||||||
|
|
||||||
const getLunrIndex = () =>
|
const getLunrIndex = () =>
|
||||||
process.env.NODE_ENV === "production"
|
process.env.NODE_ENV === 'production'
|
||||||
? fetch(`${baseUrl}${pluginData.fileNames.lunrIndex}`).then((content) => content.json())
|
? fetch(`${baseUrl}${pluginData.fileNames.lunrIndex}`).then((content) => content.json())
|
||||||
: Promise.resolve([]);
|
: Promise.resolve([]);
|
||||||
|
|
||||||
const loadAlgolia = () => {
|
const loadAlgolia = () => {
|
||||||
if (!initialized.current) {
|
if (!initialized.current) {
|
||||||
Promise.all([
|
Promise.all([getSearchDoc(), getLunrIndex(), import('./DocSearch'), import('./algolia.css')]).then(
|
||||||
getSearchDoc(),
|
([searchDocs, searchIndex, { default: DocSearch }]) => {
|
||||||
getLunrIndex(),
|
if (searchDocs.length === 0) {
|
||||||
import("./DocSearch"),
|
return;
|
||||||
import("./algolia.css")
|
}
|
||||||
]).then(([searchDocs, searchIndex, { default: DocSearch }]) => {
|
initAlgolia(searchDocs, searchIndex, DocSearch);
|
||||||
if (searchDocs.length === 0) {
|
setIndexReady(true);
|
||||||
return;
|
},
|
||||||
}
|
);
|
||||||
initAlgolia(searchDocs, searchIndex, DocSearch);
|
|
||||||
setIndexReady(true);
|
|
||||||
});
|
|
||||||
initialized.current = true;
|
initialized.current = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleSearchIconClick = useCallback(
|
const toggleSearchIconClick = useCallback(
|
||||||
e => {
|
(e) => {
|
||||||
if (!searchBarRef.current.contains(e.target)) {
|
if (!searchBarRef.current.contains(e.target)) {
|
||||||
searchBarRef.current.focus();
|
searchBarRef.current.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
props.handleSearchBarToggle && props.handleSearchBarToggle(!props.isSearchBarExpanded);
|
props.handleSearchBarToggle && props.handleSearchBarToggle(!props.isSearchBarExpanded);
|
||||||
},
|
},
|
||||||
[props.isSearchBarExpanded]
|
[props.isSearchBarExpanded],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isBrowser) {
|
if (isBrowser) {
|
||||||
|
@ -83,8 +80,8 @@ const Search = props => {
|
||||||
<span
|
<span
|
||||||
aria-label="expand searchbar"
|
aria-label="expand searchbar"
|
||||||
role="button"
|
role="button"
|
||||||
className={classnames("search-icon", {
|
className={classnames('search-icon', {
|
||||||
"search-icon-hidden": props.isSearchBarExpanded
|
'search-icon-hidden': props.isSearchBarExpanded,
|
||||||
})}
|
})}
|
||||||
onClick={toggleSearchIconClick}
|
onClick={toggleSearchIconClick}
|
||||||
onKeyDown={toggleSearchIconClick}
|
onKeyDown={toggleSearchIconClick}
|
||||||
|
@ -96,9 +93,9 @@ const Search = props => {
|
||||||
placeholder={indexReady ? 'Search' : 'Loading...'}
|
placeholder={indexReady ? 'Search' : 'Loading...'}
|
||||||
aria-label="Search"
|
aria-label="Search"
|
||||||
className={classnames(
|
className={classnames(
|
||||||
"navbar__search-input",
|
'navbar__search-input',
|
||||||
{ "search-bar-expanded": props.isSearchBarExpanded },
|
{ 'search-bar-expanded': props.isSearchBarExpanded },
|
||||||
{ "search-bar": !props.isSearchBarExpanded }
|
{ 'search-bar': !props.isSearchBarExpanded },
|
||||||
)}
|
)}
|
||||||
onClick={loadAlgolia}
|
onClick={loadAlgolia}
|
||||||
onMouseOver={loadAlgolia}
|
onMouseOver={loadAlgolia}
|
||||||
|
|
|
@ -1,147 +1,161 @@
|
||||||
import lunr from "@generated/lunr.client";
|
import lunr from '@generated/lunr.client';
|
||||||
lunr.tokenizer.separator = /[\s\-/]+/;
|
lunr.tokenizer.separator = /[\s\-/]+/;
|
||||||
|
|
||||||
class LunrSearchAdapter {
|
class LunrSearchAdapter {
|
||||||
constructor(searchDocs, searchIndex, baseUrl = '/') {
|
constructor(searchDocs, searchIndex, baseUrl = '/') {
|
||||||
this.searchDocs = searchDocs;
|
this.searchDocs = searchDocs;
|
||||||
this.lunrIndex = lunr.Index.load(searchIndex);
|
this.lunrIndex = lunr.Index.load(searchIndex);
|
||||||
this.baseUrl = baseUrl;
|
this.baseUrl = baseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
getLunrResult(input) {
|
getLunrResult(input) {
|
||||||
return this.lunrIndex.query(function (query) {
|
return this.lunrIndex.query(function (query) {
|
||||||
const tokens = lunr.tokenizer(input);
|
const tokens = lunr.tokenizer(input);
|
||||||
query.term(tokens, {
|
query.term(tokens, {
|
||||||
boost: 10
|
boost: 10,
|
||||||
});
|
});
|
||||||
query.term(tokens, {
|
query.term(tokens, {
|
||||||
wildcard: lunr.Query.wildcard.TRAILING
|
wildcard: lunr.Query.wildcard.TRAILING,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getHit(doc, formattedTitle, formattedContent) {
|
getHit(doc, formattedTitle, formattedContent) {
|
||||||
return {
|
return {
|
||||||
hierarchy: {
|
hierarchy: {
|
||||||
lvl0: doc.pageTitle || doc.title,
|
lvl0: doc.pageTitle || doc.title,
|
||||||
lvl1: doc.type === 0 ? null : doc.title
|
lvl1: doc.type === 0 ? null : doc.title,
|
||||||
|
},
|
||||||
|
url: doc.url,
|
||||||
|
_snippetResult: formattedContent
|
||||||
|
? {
|
||||||
|
content: {
|
||||||
|
value: formattedContent,
|
||||||
|
matchLevel: 'full',
|
||||||
},
|
},
|
||||||
url: doc.url,
|
}
|
||||||
_snippetResult: formattedContent ? {
|
: null,
|
||||||
content: {
|
_highlightResult: {
|
||||||
value: formattedContent,
|
hierarchy: {
|
||||||
matchLevel: "full"
|
lvl0: {
|
||||||
}
|
value: doc.type === 0 ? formattedTitle || doc.title : doc.pageTitle,
|
||||||
} : null,
|
},
|
||||||
_highlightResult: {
|
lvl1:
|
||||||
hierarchy: {
|
doc.type === 0
|
||||||
lvl0: {
|
? null
|
||||||
value: doc.type === 0 ? formattedTitle || doc.title : doc.pageTitle,
|
: {
|
||||||
},
|
value: formattedTitle || doc.title,
|
||||||
lvl1:
|
},
|
||||||
doc.type === 0
|
},
|
||||||
? null
|
},
|
||||||
: {
|
};
|
||||||
value: formattedTitle || doc.title
|
}
|
||||||
}
|
getTitleHit(doc, position, length) {
|
||||||
}
|
const start = position[0];
|
||||||
}
|
const end = position[0] + length;
|
||||||
};
|
let formattedTitle =
|
||||||
}
|
doc.title.substring(0, start) +
|
||||||
getTitleHit(doc, position, length) {
|
'<span class="algolia-docsearch-suggestion--highlight">' +
|
||||||
const start = position[0];
|
doc.title.substring(start, end) +
|
||||||
const end = position[0] + length;
|
'</span>' +
|
||||||
let formattedTitle = doc.title.substring(0, start) + '<span class="algolia-docsearch-suggestion--highlight">' + doc.title.substring(start, end) + '</span>' + doc.title.substring(end, doc.title.length);
|
doc.title.substring(end, doc.title.length);
|
||||||
return this.getHit(doc, formattedTitle)
|
return this.getHit(doc, formattedTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
getKeywordHit(doc, position, length) {
|
getKeywordHit(doc, position, length) {
|
||||||
const start = position[0];
|
const start = position[0];
|
||||||
const end = position[0] + length;
|
const end = position[0] + length;
|
||||||
let formattedTitle = doc.title + '<br /><i>Keywords: ' + doc.keywords.substring(0, start) + '<span class="algolia-docsearch-suggestion--highlight">' + doc.keywords.substring(start, end) + '</span>' + doc.keywords.substring(end, doc.keywords.length) + '</i>'
|
let formattedTitle =
|
||||||
return this.getHit(doc, formattedTitle)
|
doc.title +
|
||||||
}
|
'<br /><i>Keywords: ' +
|
||||||
|
doc.keywords.substring(0, start) +
|
||||||
getContentHit(doc, position) {
|
'<span class="algolia-docsearch-suggestion--highlight">' +
|
||||||
const start = position[0];
|
doc.keywords.substring(start, end) +
|
||||||
const end = position[0] + position[1];
|
'</span>' +
|
||||||
let previewStart = start;
|
doc.keywords.substring(end, doc.keywords.length) +
|
||||||
let previewEnd = end;
|
'</i>';
|
||||||
let ellipsesBefore = true;
|
return this.getHit(doc, formattedTitle);
|
||||||
let ellipsesAfter = true;
|
}
|
||||||
for (let k = 0; k < 3; k++) {
|
|
||||||
const nextSpace = doc.content.lastIndexOf(' ', previewStart - 2);
|
|
||||||
const nextDot = doc.content.lastIndexOf('.', previewStart - 2);
|
|
||||||
if ((nextDot > 0) && (nextDot > nextSpace)) {
|
|
||||||
previewStart = nextDot + 1;
|
|
||||||
ellipsesBefore = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (nextSpace < 0) {
|
|
||||||
previewStart = 0;
|
|
||||||
ellipsesBefore = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
previewStart = nextSpace + 1;
|
|
||||||
}
|
|
||||||
for (let k = 0; k < 10; k++) {
|
|
||||||
const nextSpace = doc.content.indexOf(' ', previewEnd + 1);
|
|
||||||
const nextDot = doc.content.indexOf('.', previewEnd + 1);
|
|
||||||
if ((nextDot > 0) && (nextDot < nextSpace)) {
|
|
||||||
previewEnd = nextDot;
|
|
||||||
ellipsesAfter = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (nextSpace < 0) {
|
|
||||||
previewEnd = doc.content.length;
|
|
||||||
ellipsesAfter = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
previewEnd = nextSpace;
|
|
||||||
}
|
|
||||||
let preview = doc.content.substring(previewStart, start);
|
|
||||||
if (ellipsesBefore) {
|
|
||||||
preview = '... ' + preview;
|
|
||||||
}
|
|
||||||
preview += '<span class="algolia-docsearch-suggestion--highlight">' + doc.content.substring(start, end) + '</span>';
|
|
||||||
preview += doc.content.substring(end, previewEnd);
|
|
||||||
if (ellipsesAfter) {
|
|
||||||
preview += ' ...';
|
|
||||||
}
|
|
||||||
return this.getHit(doc, null, preview);
|
|
||||||
|
|
||||||
|
getContentHit(doc, position) {
|
||||||
|
const start = position[0];
|
||||||
|
const end = position[0] + position[1];
|
||||||
|
let previewStart = start;
|
||||||
|
let previewEnd = end;
|
||||||
|
let ellipsesBefore = true;
|
||||||
|
let ellipsesAfter = true;
|
||||||
|
for (let k = 0; k < 3; k++) {
|
||||||
|
const nextSpace = doc.content.lastIndexOf(' ', previewStart - 2);
|
||||||
|
const nextDot = doc.content.lastIndexOf('.', previewStart - 2);
|
||||||
|
if (nextDot > 0 && nextDot > nextSpace) {
|
||||||
|
previewStart = nextDot + 1;
|
||||||
|
ellipsesBefore = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (nextSpace < 0) {
|
||||||
|
previewStart = 0;
|
||||||
|
ellipsesBefore = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
previewStart = nextSpace + 1;
|
||||||
}
|
}
|
||||||
search(input) {
|
for (let k = 0; k < 10; k++) {
|
||||||
return new Promise((resolve, rej) => {
|
const nextSpace = doc.content.indexOf(' ', previewEnd + 1);
|
||||||
const results = this.getLunrResult(input);
|
const nextDot = doc.content.indexOf('.', previewEnd + 1);
|
||||||
const hits = [];
|
if (nextDot > 0 && nextDot < nextSpace) {
|
||||||
results.length > 5 && (results.length = 5);
|
previewEnd = nextDot;
|
||||||
this.titleHitsRes = []
|
ellipsesAfter = false;
|
||||||
this.contentHitsRes = []
|
break;
|
||||||
results.forEach(result => {
|
}
|
||||||
const doc = this.searchDocs[result.ref];
|
if (nextSpace < 0) {
|
||||||
const { metadata } = result.matchData;
|
previewEnd = doc.content.length;
|
||||||
for (let i in metadata) {
|
ellipsesAfter = false;
|
||||||
if (metadata[i].title) {
|
break;
|
||||||
if (!this.titleHitsRes.includes(result.ref)) {
|
}
|
||||||
const position = metadata[i].title.position[0]
|
previewEnd = nextSpace;
|
||||||
hits.push(this.getTitleHit(doc, position, input.length));
|
|
||||||
this.titleHitsRes.push(result.ref);
|
|
||||||
}
|
|
||||||
} else if (metadata[i].content) {
|
|
||||||
const position = metadata[i].content.position[0]
|
|
||||||
hits.push(this.getContentHit(doc, position))
|
|
||||||
} else if (metadata[i].keywords) {
|
|
||||||
const position = metadata[i].keywords.position[0]
|
|
||||||
hits.push(this.getKeywordHit(doc, position, input.length));
|
|
||||||
this.titleHitsRes.push(result.ref);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
hits.length > 5 && (hits.length = 5);
|
|
||||||
resolve(hits);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
let preview = doc.content.substring(previewStart, start);
|
||||||
|
if (ellipsesBefore) {
|
||||||
|
preview = '... ' + preview;
|
||||||
|
}
|
||||||
|
preview += '<span class="algolia-docsearch-suggestion--highlight">' + doc.content.substring(start, end) + '</span>';
|
||||||
|
preview += doc.content.substring(end, previewEnd);
|
||||||
|
if (ellipsesAfter) {
|
||||||
|
preview += ' ...';
|
||||||
|
}
|
||||||
|
return this.getHit(doc, null, preview);
|
||||||
|
}
|
||||||
|
search(input) {
|
||||||
|
return new Promise((resolve, rej) => {
|
||||||
|
const results = this.getLunrResult(input);
|
||||||
|
const hits = [];
|
||||||
|
results.length > 5 && (results.length = 5);
|
||||||
|
this.titleHitsRes = [];
|
||||||
|
this.contentHitsRes = [];
|
||||||
|
results.forEach((result) => {
|
||||||
|
const doc = this.searchDocs[result.ref];
|
||||||
|
const { metadata } = result.matchData;
|
||||||
|
for (let i in metadata) {
|
||||||
|
if (metadata[i].title) {
|
||||||
|
if (!this.titleHitsRes.includes(result.ref)) {
|
||||||
|
const position = metadata[i].title.position[0];
|
||||||
|
hits.push(this.getTitleHit(doc, position, input.length));
|
||||||
|
this.titleHitsRes.push(result.ref);
|
||||||
|
}
|
||||||
|
} else if (metadata[i].content) {
|
||||||
|
const position = metadata[i].content.position[0];
|
||||||
|
hits.push(this.getContentHit(doc, position));
|
||||||
|
} else if (metadata[i].keywords) {
|
||||||
|
const position = metadata[i].keywords.position[0];
|
||||||
|
hits.push(this.getKeywordHit(doc, position, input.length));
|
||||||
|
this.titleHitsRes.push(result.ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
hits.length > 5 && (hits.length = 5);
|
||||||
|
resolve(hits);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LunrSearchAdapter;
|
export default LunrSearchAdapter;
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
import $ from "autocomplete.js/zepto";
|
import $ from 'autocomplete.js/zepto';
|
||||||
|
|
||||||
const utils = {
|
const utils = {
|
||||||
/*
|
/*
|
||||||
* Move the content of an object key one level higher.
|
* Move the content of an object key one level higher.
|
||||||
* eg.
|
* eg.
|
||||||
* {
|
* {
|
||||||
* name: 'My name',
|
* name: 'My name',
|
||||||
* hierarchy: {
|
* hierarchy: {
|
||||||
* lvl0: 'Foo',
|
* lvl0: 'Foo',
|
||||||
* lvl1: 'Bar'
|
* lvl1: 'Bar'
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
* Will be converted to
|
* Will be converted to
|
||||||
* {
|
* {
|
||||||
* name: 'My name',
|
* name: 'My name',
|
||||||
* lvl0: 'Foo',
|
* lvl0: 'Foo',
|
||||||
* lvl1: 'Bar'
|
* lvl1: 'Bar'
|
||||||
* }
|
* }
|
||||||
* @param {Object} object Main object
|
* @param {Object} object Main object
|
||||||
* @param {String} property Main object key to move up
|
* @param {String} property Main object key to move up
|
||||||
* @return {Object}
|
* @return {Object}
|
||||||
* @throws Error when key is not an attribute of Object or is not an object itself
|
* @throws Error when key is not an attribute of Object or is not an object itself
|
||||||
*/
|
*/
|
||||||
mergeKeyWithParent(object, property) {
|
mergeKeyWithParent(object, property) {
|
||||||
if (object[property] === undefined) {
|
if (object[property] === undefined) {
|
||||||
return object;
|
return object;
|
||||||
|
@ -34,36 +34,36 @@ const utils = {
|
||||||
return newObject;
|
return newObject;
|
||||||
},
|
},
|
||||||
/*
|
/*
|
||||||
* Group all objects of a collection by the value of the specified attribute
|
* Group all objects of a collection by the value of the specified attribute
|
||||||
* If the attribute is a string, use the lowercase form.
|
* If the attribute is a string, use the lowercase form.
|
||||||
*
|
*
|
||||||
* eg.
|
* eg.
|
||||||
* groupBy([
|
* groupBy([
|
||||||
* {name: 'Tim', category: 'dev'},
|
* {name: 'Tim', category: 'dev'},
|
||||||
* {name: 'Vincent', category: 'dev'},
|
* {name: 'Vincent', category: 'dev'},
|
||||||
* {name: 'Ben', category: 'sales'},
|
* {name: 'Ben', category: 'sales'},
|
||||||
* {name: 'Jeremy', category: 'sales'},
|
* {name: 'Jeremy', category: 'sales'},
|
||||||
* {name: 'AlexS', category: 'dev'},
|
* {name: 'AlexS', category: 'dev'},
|
||||||
* {name: 'AlexK', category: 'sales'}
|
* {name: 'AlexK', category: 'sales'}
|
||||||
* ], 'category');
|
* ], 'category');
|
||||||
* =>
|
* =>
|
||||||
* {
|
* {
|
||||||
* 'devs': [
|
* 'devs': [
|
||||||
* {name: 'Tim', category: 'dev'},
|
* {name: 'Tim', category: 'dev'},
|
||||||
* {name: 'Vincent', category: 'dev'},
|
* {name: 'Vincent', category: 'dev'},
|
||||||
* {name: 'AlexS', category: 'dev'}
|
* {name: 'AlexS', category: 'dev'}
|
||||||
* ],
|
* ],
|
||||||
* 'sales': [
|
* 'sales': [
|
||||||
* {name: 'Ben', category: 'sales'},
|
* {name: 'Ben', category: 'sales'},
|
||||||
* {name: 'Jeremy', category: 'sales'},
|
* {name: 'Jeremy', category: 'sales'},
|
||||||
* {name: 'AlexK', category: 'sales'}
|
* {name: 'AlexK', category: 'sales'}
|
||||||
* ]
|
* ]
|
||||||
* }
|
* }
|
||||||
* @param {array} collection Array of objects to group
|
* @param {array} collection Array of objects to group
|
||||||
* @param {String} property The attribute on which apply the grouping
|
* @param {String} property The attribute on which apply the grouping
|
||||||
* @return {array}
|
* @return {array}
|
||||||
* @throws Error when one of the element does not have the specified property
|
* @throws Error when one of the element does not have the specified property
|
||||||
*/
|
*/
|
||||||
groupBy(collection, property) {
|
groupBy(collection, property) {
|
||||||
const newCollection = {};
|
const newCollection = {};
|
||||||
$.each(collection, (index, item) => {
|
$.each(collection, (index, item) => {
|
||||||
|
@ -84,94 +84,94 @@ const utils = {
|
||||||
return newCollection;
|
return newCollection;
|
||||||
},
|
},
|
||||||
/*
|
/*
|
||||||
* Return an array of all the values of the specified object
|
* Return an array of all the values of the specified object
|
||||||
* eg.
|
* eg.
|
||||||
* values({
|
* values({
|
||||||
* foo: 42,
|
* foo: 42,
|
||||||
* bar: true,
|
* bar: true,
|
||||||
* baz: 'yep'
|
* baz: 'yep'
|
||||||
* })
|
* })
|
||||||
* =>
|
* =>
|
||||||
* [42, true, yep]
|
* [42, true, yep]
|
||||||
* @param {object} object Object to extract values from
|
* @param {object} object Object to extract values from
|
||||||
* @return {array}
|
* @return {array}
|
||||||
*/
|
*/
|
||||||
values(object) {
|
values(object) {
|
||||||
return Object.keys(object).map(key => object[key]);
|
return Object.keys(object).map((key) => object[key]);
|
||||||
},
|
},
|
||||||
/*
|
/*
|
||||||
* Flattens an array
|
* Flattens an array
|
||||||
* eg.
|
* eg.
|
||||||
* flatten([1, 2, [3, 4], [5, 6]])
|
* flatten([1, 2, [3, 4], [5, 6]])
|
||||||
* =>
|
* =>
|
||||||
* [1, 2, 3, 4, 5, 6]
|
* [1, 2, 3, 4, 5, 6]
|
||||||
* @param {array} array Array to flatten
|
* @param {array} array Array to flatten
|
||||||
* @return {array}
|
* @return {array}
|
||||||
*/
|
*/
|
||||||
flatten(array) {
|
flatten(array) {
|
||||||
const results = [];
|
const results = [];
|
||||||
array.forEach(value => {
|
array.forEach((value) => {
|
||||||
if (!Array.isArray(value)) {
|
if (!Array.isArray(value)) {
|
||||||
results.push(value);
|
results.push(value);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
value.forEach(subvalue => {
|
value.forEach((subvalue) => {
|
||||||
results.push(subvalue);
|
results.push(subvalue);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return results;
|
return results;
|
||||||
},
|
},
|
||||||
/*
|
/*
|
||||||
* Flatten all values of an object into an array, marking each first element of
|
* Flatten all values of an object into an array, marking each first element of
|
||||||
* each group with a specific flag
|
* each group with a specific flag
|
||||||
* eg.
|
* eg.
|
||||||
* flattenAndFlagFirst({
|
* flattenAndFlagFirst({
|
||||||
* 'devs': [
|
* 'devs': [
|
||||||
* {name: 'Tim', category: 'dev'},
|
* {name: 'Tim', category: 'dev'},
|
||||||
* {name: 'Vincent', category: 'dev'},
|
* {name: 'Vincent', category: 'dev'},
|
||||||
* {name: 'AlexS', category: 'dev'}
|
* {name: 'AlexS', category: 'dev'}
|
||||||
* ],
|
* ],
|
||||||
* 'sales': [
|
* 'sales': [
|
||||||
* {name: 'Ben', category: 'sales'},
|
* {name: 'Ben', category: 'sales'},
|
||||||
* {name: 'Jeremy', category: 'sales'},
|
* {name: 'Jeremy', category: 'sales'},
|
||||||
* {name: 'AlexK', category: 'sales'}
|
* {name: 'AlexK', category: 'sales'}
|
||||||
* ]
|
* ]
|
||||||
* , 'isTop');
|
* , 'isTop');
|
||||||
* =>
|
* =>
|
||||||
* [
|
* [
|
||||||
* {name: 'Tim', category: 'dev', isTop: true},
|
* {name: 'Tim', category: 'dev', isTop: true},
|
||||||
* {name: 'Vincent', category: 'dev', isTop: false},
|
* {name: 'Vincent', category: 'dev', isTop: false},
|
||||||
* {name: 'AlexS', category: 'dev', isTop: false},
|
* {name: 'AlexS', category: 'dev', isTop: false},
|
||||||
* {name: 'Ben', category: 'sales', isTop: true},
|
* {name: 'Ben', category: 'sales', isTop: true},
|
||||||
* {name: 'Jeremy', category: 'sales', isTop: false},
|
* {name: 'Jeremy', category: 'sales', isTop: false},
|
||||||
* {name: 'AlexK', category: 'sales', isTop: false}
|
* {name: 'AlexK', category: 'sales', isTop: false}
|
||||||
* ]
|
* ]
|
||||||
* @param {object} object Object to flatten
|
* @param {object} object Object to flatten
|
||||||
* @param {string} flag Flag to set to true on first element of each group
|
* @param {string} flag Flag to set to true on first element of each group
|
||||||
* @return {array}
|
* @return {array}
|
||||||
*/
|
*/
|
||||||
flattenAndFlagFirst(object, flag) {
|
flattenAndFlagFirst(object, flag) {
|
||||||
const values = this.values(object).map(collection =>
|
const values = this.values(object).map((collection) =>
|
||||||
collection.map((item, index) => {
|
collection.map((item, index) => {
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
item[flag] = index === 0;
|
item[flag] = index === 0;
|
||||||
return item;
|
return item;
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
return this.flatten(values);
|
return this.flatten(values);
|
||||||
},
|
},
|
||||||
/*
|
/*
|
||||||
* Removes all empty strings, null, false and undefined elements array
|
* Removes all empty strings, null, false and undefined elements array
|
||||||
* eg.
|
* eg.
|
||||||
* compact([42, false, null, undefined, '', [], 'foo']);
|
* compact([42, false, null, undefined, '', [], 'foo']);
|
||||||
* =>
|
* =>
|
||||||
* [42, [], 'foo']
|
* [42, [], 'foo']
|
||||||
* @param {array} array Array to compact
|
* @param {array} array Array to compact
|
||||||
* @return {array}
|
* @return {array}
|
||||||
*/
|
*/
|
||||||
compact(array) {
|
compact(array) {
|
||||||
const results = [];
|
const results = [];
|
||||||
array.forEach(value => {
|
array.forEach((value) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -239,11 +239,7 @@ const utils = {
|
||||||
* @return {string}
|
* @return {string}
|
||||||
**/
|
**/
|
||||||
getSnippetedValue(object, property) {
|
getSnippetedValue(object, property) {
|
||||||
if (
|
if (!object._snippetResult || !object._snippetResult[property] || !object._snippetResult[property].value) {
|
||||||
!object._snippetResult ||
|
|
||||||
!object._snippetResult[property] ||
|
|
||||||
!object._snippetResult[property].value
|
|
||||||
) {
|
|
||||||
return object[property];
|
return object[property];
|
||||||
}
|
}
|
||||||
let snippet = object._snippetResult[property].value;
|
let snippet = object._snippetResult[property].value;
|
||||||
|
@ -257,11 +253,11 @@ const utils = {
|
||||||
return snippet;
|
return snippet;
|
||||||
},
|
},
|
||||||
/*
|
/*
|
||||||
* Deep clone an object.
|
* Deep clone an object.
|
||||||
* Note: This will not clone functions and dates
|
* Note: This will not clone functions and dates
|
||||||
* @param {object} object Object to clone
|
* @param {object} object Object to clone
|
||||||
* @return {object}
|
* @return {object}
|
||||||
*/
|
*/
|
||||||
deepClone(object) {
|
deepClone(object) {
|
||||||
return JSON.parse(JSON.stringify(object));
|
return JSON.parse(JSON.stringify(object));
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,25 +4,25 @@ module.exports = {
|
||||||
corePlugins: {
|
corePlugins: {
|
||||||
preflight: false, // disable Tailwind's reset
|
preflight: false, // disable Tailwind's reset
|
||||||
},
|
},
|
||||||
content: ["./src/**/*.{js,jsx,ts,tsx}", "../docs/**/*.mdx"], // my markdown stuff is in ../docs, not /src
|
content: ['./src/**/*.{js,jsx,ts,tsx}', '../docs/**/*.mdx'], // my markdown stuff is in ../docs, not /src
|
||||||
darkMode: ["class", '[data-theme="dark"]'], // hooks into docusaurus' dark mode settigns
|
darkMode: ['class', '[data-theme="dark"]'], // hooks into docusaurus' dark mode settigns
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
// Light Theme
|
// Light Theme
|
||||||
"immich-primary": "#4250af",
|
'immich-primary': '#4250af',
|
||||||
"immich-bg": "white",
|
'immich-bg': 'white',
|
||||||
"immich-fg": "black",
|
'immich-fg': 'black',
|
||||||
"immich-gray": "#F6F6F4",
|
'immich-gray': '#F6F6F4',
|
||||||
|
|
||||||
// Dark Theme
|
// Dark Theme
|
||||||
"immich-dark-primary": "#adcbfa",
|
'immich-dark-primary': '#adcbfa',
|
||||||
"immich-dark-bg": "black",
|
'immich-dark-bg': 'black',
|
||||||
"immich-dark-fg": "#e5e7eb",
|
'immich-dark-fg': '#e5e7eb',
|
||||||
"immich-dark-gray": "#212121",
|
'immich-dark-gray': '#212121',
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
"immich-title": ["Snowburst One", "cursive"],
|
'immich-title': ['Snowburst One', 'cursive'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue