Compare commits
214 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
553be7da33 | ||
|
|
260b850e1a | ||
|
|
726a4fddd3 | ||
|
|
318c094f27 | ||
|
|
6e0d3807e4 | ||
|
|
10e9dc06dd | ||
|
|
e84687e5fc | ||
|
|
361d41065c | ||
|
|
4c0fbc0b42 | ||
|
|
ef8e380956 | ||
|
|
5db28b1607 | ||
|
|
dbfd4cf050 | ||
|
|
ffd298a2b6 | ||
|
|
9b1b5906e7 | ||
|
|
19bd14c63d | ||
|
|
b69343af56 | ||
|
|
94ee90eebb | ||
|
|
72b3097ad1 | ||
|
|
225f910fe8 | ||
|
|
10d9ffc740 | ||
|
|
4202d25d62 | ||
|
|
6a905e1b49 | ||
|
|
72e08f484f | ||
|
|
64dbb9c025 | ||
|
|
af2e0235bf | ||
|
|
bf85818f8b | ||
|
|
1840713179 | ||
|
|
b11bffb7cf | ||
|
|
bfb26a9402 | ||
|
|
c3b11be2d0 | ||
|
|
ecfb89de40 | ||
|
|
e1eab70f93 | ||
|
|
adb341c0fa | ||
|
|
25ccdffeb9 | ||
|
|
b98d399a9c | ||
|
|
f36e7b8abb | ||
|
|
667322d14e | ||
|
|
9b440c0da3 | ||
|
|
2586733a98 | ||
|
|
7bc779b296 | ||
|
|
6064dcb6a6 | ||
|
|
7c7b0cc970 | ||
|
|
c182397dd9 | ||
|
|
dc5ee3bdf3 | ||
|
|
c8e1295a4b | ||
|
|
331c55240b | ||
|
|
65037f9b56 | ||
|
|
39853d79ce | ||
|
|
8530550347 | ||
|
|
ba8e9ef63c | ||
|
|
119f2d7e51 | ||
|
|
b0be26300e | ||
|
|
0400188ea7 | ||
|
|
879581224a | ||
|
|
7e5602c881 | ||
|
|
4870ea3e40 | ||
|
|
61c55acd50 | ||
|
|
c45421d27e | ||
|
|
b396d2604f | ||
|
|
28b6dcd1db | ||
|
|
1dd74ea7da | ||
|
|
64923b03d9 | ||
|
|
2ba9d517a8 | ||
|
|
471a9f7407 | ||
|
|
bdf871b476 | ||
|
|
ab860eeea1 | ||
|
|
50d760f3b8 | ||
|
|
73d06e15fb | ||
|
|
49d57024b9 | ||
|
|
31deb5010f | ||
|
|
e86eb7798f | ||
|
|
2896423766 | ||
|
|
696d0c582d | ||
|
|
e94cae620a | ||
|
|
c9c6f2b0c9 | ||
|
|
b8fe799ac6 | ||
|
|
4cb8539143 | ||
|
|
16b86870c4 | ||
|
|
d4ce2a3ed6 | ||
|
|
a474f3e4ee | ||
|
|
9a49fbb747 | ||
|
|
e3d47d78e0 | ||
|
|
d62189f086 | ||
|
|
bb1b3d7d9a | ||
|
|
13aeeefb22 | ||
|
|
8cdc9c3e29 | ||
|
|
3e31a4d38e | ||
|
|
0cb3db6b89 | ||
|
|
b7e8c51b29 | ||
|
|
e60db9f57a | ||
|
|
2c707e86aa | ||
|
|
5c6541e1a7 | ||
|
|
da81783c8e | ||
|
|
6a90a124b3 | ||
|
|
bd6edbbec6 | ||
|
|
53ab06f97e | ||
|
|
6904018585 | ||
|
|
8c14b3ccf9 | ||
|
|
8557820e6e | ||
|
|
3782499da5 | ||
|
|
97ca45964a | ||
|
|
7fa464b38f | ||
|
|
fe5fa99b4a | ||
|
|
9bf8b337f6 | ||
|
|
06caa2ca5e | ||
|
|
1145ee39b6 | ||
|
|
68111616fe | ||
|
|
7662c11bb5 | ||
|
|
1aaa575480 | ||
|
|
3529e46b11 | ||
|
|
006b1a61bf | ||
|
|
f5eb36ff00 | ||
|
|
a97c9b0c0f | ||
|
|
08daeb87bc | ||
|
|
ebc7ba9684 | ||
|
|
8392dcef20 | ||
|
|
20a37b678f | ||
|
|
d3bd894c2a | ||
|
|
e75ff14975 | ||
|
|
ab1e2a32a0 | ||
|
|
22cd5c8b93 | ||
|
|
5c8b1c4fc4 | ||
|
|
a71b50e33f | ||
|
|
d4c1148025 | ||
|
|
0d11244506 | ||
|
|
e786b1e44f | ||
|
|
509873db55 | ||
|
|
c5178ee288 | ||
|
|
4045628166 | ||
|
|
f8b2d64a26 | ||
|
|
62ba99f6cd | ||
|
|
2fad4d06bd | ||
|
|
c9e58e17da | ||
|
|
8e03719a51 | ||
|
|
c4df55060b | ||
|
|
47c636e810 | ||
|
|
38d18fc433 | ||
|
|
7b6fd5ed6a | ||
|
|
8603395329 | ||
|
|
468b1912b8 | ||
|
|
c243256180 | ||
|
|
a9370881f4 | ||
|
|
81a63cd1b7 | ||
|
|
f03219fd42 | ||
|
|
2e8f3d7d1f | ||
|
|
9ddb08cc9c | ||
|
|
d44eed6581 | ||
|
|
59b9baa579 | ||
|
|
027ac94e80 | ||
|
|
b1cec402c3 | ||
|
|
f2a7f83e12 | ||
|
|
477ff8241e | ||
|
|
95bae5929c | ||
|
|
4545a6bbf5 | ||
|
|
b3a97431b3 | ||
|
|
b757046de8 | ||
|
|
0d1a1b899a | ||
|
|
71be4101a5 | ||
|
|
db453d0f74 | ||
|
|
1d450428c9 | ||
|
|
e16601d113 | ||
|
|
56b52d0808 | ||
|
|
3fc0a2c64f | ||
|
|
f3f2006f14 | ||
|
|
2139f48df3 | ||
|
|
09483ada01 | ||
|
|
c593334be8 | ||
|
|
af7d078293 | ||
|
|
96ab5dd9a7 | ||
|
|
9a3ef24619 | ||
|
|
3287752e45 | ||
|
|
1c560f9a5b | ||
|
|
534cf3571a | ||
|
|
cf17aa61cc | ||
|
|
32f81cefe7 | ||
|
|
15bb08e5f3 | ||
|
|
0a6346b383 | ||
|
|
43ee6899ae | ||
|
|
5159edbea1 | ||
|
|
79dd1f3296 | ||
|
|
7a3ac58e4d | ||
|
|
527eb373a9 | ||
|
|
16ecf59196 | ||
|
|
66dd59f076 | ||
|
|
e95168288c | ||
|
|
4f5121b337 | ||
|
|
f0152d84d8 | ||
|
|
cc324dd8ec | ||
|
|
d332245cfc | ||
|
|
ac9ebe4160 | ||
|
|
b51e8861b7 | ||
|
|
51fcba7109 | ||
|
|
ce3afc6916 | ||
|
|
fb1d2c3e80 | ||
|
|
d372b66e44 | ||
|
|
4417168ef4 | ||
|
|
9a4fdc6f01 | ||
|
|
405537202d | ||
|
|
873d590606 | ||
|
|
463670ebdf | ||
|
|
eeb3d4eb23 | ||
|
|
651003f1d7 | ||
|
|
2ee923d773 | ||
|
|
e9c758b63d | ||
|
|
6a462f640a | ||
|
|
6120eb53cd | ||
|
|
a3a3846848 | ||
|
|
f547c56c1f | ||
|
|
242d09e932 | ||
|
|
036922328f | ||
|
|
c7257b8c2f | ||
|
|
eecd8b0faa | ||
|
|
63940ab5ff | ||
|
|
62f62db17c |
@@ -20,6 +20,7 @@ module.exports = {
|
||||
},
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/no-children-prop': 'off',
|
||||
"unused-imports/no-unused-imports": "warn",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-imports": "off",
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -1,9 +1,7 @@
|
||||
name: 🐛 Bug Report
|
||||
description: Report something that's broken, or not working like intented!
|
||||
title: '[🐛 Bug] <title>'
|
||||
title: '<title>'
|
||||
labels: ['🐛 Bug']
|
||||
assignees:
|
||||
- ajnart
|
||||
body:
|
||||
- type: dropdown
|
||||
id: environment
|
||||
|
||||
17
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -1,14 +1,13 @@
|
||||
name: ✨ Feature Request
|
||||
description: Request a feature to help improve Homarr!
|
||||
title: '[✨ Feature] <title>'
|
||||
title: '<title>'
|
||||
labels: ['✨ Feature']
|
||||
assignees:
|
||||
- ajnart
|
||||
body:
|
||||
- type: textarea
|
||||
id: feature
|
||||
attributes:
|
||||
label: Describe the feature you would like to see
|
||||
label: Description
|
||||
description: Describe the feature you would like to see. Tell us how you imagine it and try to provide as much useful information as possible. **PLEASE** use images/screenshots, include details about X & Y when requesting changes like X & Y service does, make your description atleast 300 characters. Having an unclear issue with too little detail will result in your issue being marked as invalid and closed.
|
||||
placeholder: An outline of the feature you would like to see implemented, include as much detail as possible!
|
||||
validations:
|
||||
required: true
|
||||
@@ -23,3 +22,13 @@ body:
|
||||
- High (App breaking feature)
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: idiot-check
|
||||
attributes:
|
||||
label: Please tick the boxes
|
||||
description: Before submitting, please ensure that
|
||||
options:
|
||||
- label: You've read the [docs](https://github.com/ajnart/homarr#readme)
|
||||
required: true
|
||||
- label: You've checked for [duplicate issues](https://github.com/ajnart/homarr/issues)
|
||||
required: true
|
||||
|
||||
23
.github/ISSUE_TEMPLATE/idea.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: 🤔 Idea
|
||||
description: Tell us your idea! We may implement it.
|
||||
title: '<title>'
|
||||
labels: ['🤔 Idea']
|
||||
body:
|
||||
- type: textarea
|
||||
id: feature
|
||||
attributes:
|
||||
label: Description
|
||||
description: Tell us your idea! Please add as much details as possible.
|
||||
placeholder: Maybe move ... to ...! Maybe add the version of Homarr somewhere...! Etc.
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: idiot-check
|
||||
attributes:
|
||||
label: Please tick the boxes
|
||||
description: Before submitting, please ensure that
|
||||
options:
|
||||
- label: You've read the [docs](https://github.com/ajnart/homarr#readme)
|
||||
required: true
|
||||
- label: You've checked for [duplicate issues](https://github.com/ajnart/homarr/issues)
|
||||
required: true
|
||||
23
.github/ISSUE_TEMPLATE/module.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: 🏗️ Module request
|
||||
description: Request for a module to be added / an integration with you favourite service !
|
||||
title: '<title>'
|
||||
body:
|
||||
- type: textarea
|
||||
id: name
|
||||
attributes:
|
||||
label: Name the integration
|
||||
description: Please describe the name of the Module/Integration you want to see and info that could help us with adding it. For example screenshots/mockups for inspiration. API links or already existing JavaScript/TypeScript integration of the API
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: idiot-check
|
||||
attributes:
|
||||
label: Please tick the boxes
|
||||
description: Before submitting, please ensure that
|
||||
options:
|
||||
- label: You've read the [docs](https://github.com/ajnart/homarr#readme)
|
||||
required: true
|
||||
- label: You've checked for [duplicate issues](https://github.com/ajnart/homarr/issues)
|
||||
required: true
|
||||
- label: You're not just putting an idea out there and actually give usefull information about how to implement your module idea
|
||||
required: true
|
||||
23
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
*Thank you for contributing to Homarr! So that your Pull Request can be handled effectively, please populate the following fields (delete sections that are not applicable)*
|
||||
|
||||
### Category
|
||||
> One of: Bugfix / Feature / Code style update / Refactoring Only / Build related changes / Documentation / Other (Please specify!)
|
||||
|
||||
### Overview
|
||||
> Briefly outline your new changes...
|
||||
|
||||
### Issue Number _(if applicable)_
|
||||
> Related issue: #00
|
||||
|
||||
### New Vars _(if applicable)_
|
||||
> If you've added any new build scripts, environmental variables, config file options, dependency please outline here.
|
||||
|
||||
### Screenshot _(if applicable)_
|
||||
> If you've introduced any significant UI changes, please include a screenshot.
|
||||
|
||||
### Code Quality Checklist _(Please complete)_
|
||||
- [ ] All changes are backwards compatible
|
||||
- [ ] There are no (new) build warnings or errors
|
||||
- [ ] _(If a new config option is added)_ Attribute is outlined in the schema and documented
|
||||
- [ ] _(If a new dependency is added)_ Package is essential, and has been checked out for security or performance
|
||||
- [ ] Bumps version, if new feature added
|
||||
27
.github/release-note.md
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
## 🦞 Homarr [v0.0.0](https://github.com/ajnart/homarr/compare/v0.0.0...v0.0.0) (2022-01-01)
|
||||
|
||||
<!-- Small release message -->
|
||||
|
||||
<!-- Bigger announcement marked in bold -->
|
||||
|
||||
### Upgrade Steps
|
||||
*Upgrading without a mounted config? Make sure to download your config from the settings first! You can add it back later by drag and dropping it into your browser.*
|
||||
* `docker pull ghcr.io/ajnart/homarr:latest`
|
||||
* `docker stop [container_id]`
|
||||
* `docker rm [container_id]`
|
||||
* `docker run --name homarr -p 7575:7575 -v /data/docker/homarr:/app/data/configs -d ghcr.io/ajnart/homarr:latest`
|
||||
* *(or use our [docker_compose.yml](https://github.com/ajnart/homarr#-installation))*
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
### New Features
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
### UI Changes
|
||||
|
||||
### GitHub Changes
|
||||
|
||||
### Other Changes
|
||||
|
||||
_**Special thanks to our contributors: @ajnart, @c00ldude1oo, @walkxcode, and of course all people using our project.**_
|
||||
32
.github/workflows/docker.yml
vendored
@@ -1,18 +1,28 @@
|
||||
name: Build and publish Docker image
|
||||
name: Master docker CI
|
||||
# Workflow to build and publish docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths-ignore:
|
||||
- '.github/**'
|
||||
- '**.md'
|
||||
tags:
|
||||
- v*
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
IMAGE_NAME: homarr
|
||||
# Use docker.io for Docker Hub if empty
|
||||
REGISTRY: ghcr.io
|
||||
# github.repository as <account>/<repo>
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
# Push image to GitHub Packages.
|
||||
# See also https://docs.docker.com/docker-hub/builds/
|
||||
build:
|
||||
yarn_install_and_build:
|
||||
# Will run yarn install && yarn build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Setup
|
||||
@@ -20,9 +30,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Get yarn cache directory path
|
||||
# to help speed up build times
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- name: Yarn cache
|
||||
# to help speed up build times
|
||||
uses: actions/cache@v3
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
@@ -43,6 +55,7 @@ jobs:
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn build
|
||||
- name: Cache build output
|
||||
# to copy needed files to docker build job
|
||||
uses: actions/cache@v2
|
||||
id: restore-build
|
||||
with:
|
||||
@@ -52,11 +65,11 @@ jobs:
|
||||
./public/
|
||||
./.next/static/
|
||||
./.next/standalone/
|
||||
./packages.jsan
|
||||
./packages.json
|
||||
key: ${{ github.sha }}
|
||||
|
||||
docker:
|
||||
needs: [build]
|
||||
docker_image_build_and_push:
|
||||
needs: [yarn_install_and_build]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
@@ -73,14 +86,14 @@ jobs:
|
||||
./public/
|
||||
./.next/static/
|
||||
./.next/standalone/
|
||||
./packages.jsan
|
||||
./packages.json
|
||||
key: ${{ github.sha }}
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
# list of Docker images to use as base name for tags
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
# generate Docker tags based on the following events/attributes
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
@@ -90,7 +103,6 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Login to GHCR
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
@@ -102,6 +114,6 @@ jobs:
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
136
.github/workflows/docker_dev.yml
vendored
Normal file
@@ -0,0 +1,136 @@
|
||||
name: Development CI
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
paths-ignore:
|
||||
- '.github/**'
|
||||
- '**.md'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '.github/**'
|
||||
- '**.md'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tags:
|
||||
requierd: true
|
||||
description: 'Tags to deploy to'
|
||||
|
||||
env:
|
||||
# Use docker.io for Docker Hub if empty
|
||||
REGISTRY: ghcr.io
|
||||
# github.repository as <account>/<repo>
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
# Push image to GitHub Packages.
|
||||
# See also https://docs.docker.com/docker-hub/builds/
|
||||
yarn_install_and_build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Setup
|
||||
uses: actions/setup-node@v3
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- name: Yarn cache
|
||||
uses: actions/cache@v3
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: ${{ runner.os }}-yarn-
|
||||
|
||||
- name: Nextjs cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
# See here for caching with `yarn` https://github.com/actions/cache/blob/main/examples.md#node---yarn or you can leverage caching with actions/setup-node https://github.com/actions/setup-node
|
||||
path: |
|
||||
~/.npm
|
||||
${{ github.workspace }}/.next/cache
|
||||
# Generate a new cache whenever packages or source files change.
|
||||
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
|
||||
# If source files changed but packages didn't, rebuild from a prior cache.
|
||||
restore-keys: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
|
||||
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn build
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@v2
|
||||
id: restore-build
|
||||
with:
|
||||
path: |
|
||||
./next.config.js
|
||||
./pages/
|
||||
./public/
|
||||
./.next/static/
|
||||
./.next/standalone/
|
||||
./packages.json
|
||||
key: ${{ github.sha }}
|
||||
|
||||
docker_image_build_and_push:
|
||||
needs: [yarn_install_and_build]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/cache@v2
|
||||
id: restore-build
|
||||
with:
|
||||
path: |
|
||||
./next.config.js
|
||||
./pages/
|
||||
./public/
|
||||
./.next/static/
|
||||
./.next/standalone/
|
||||
./packages.json
|
||||
key: ${{ github.sha }}
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
# list of Docker images to use as base name for tags
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
# generate Docker tags based on the following events/attributes
|
||||
tags: |
|
||||
type=ref,event=pr
|
||||
tpye=raw,value=dev,priority=1
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to GHCR
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
3
.gitignore
vendored
@@ -35,4 +35,5 @@ yarn-error.log*
|
||||
*.tsbuildinfo
|
||||
|
||||
# storybook
|
||||
storybook-static
|
||||
storybook-static
|
||||
data/configs
|
||||
@@ -1,13 +1,9 @@
|
||||
module.exports = {
|
||||
stories: ['../components/**/*.story.mdx', '../components/**/*.story.*'],
|
||||
stories: ['../src/components/**/*.story.mdx', '../src/components/**/*.story.*'],
|
||||
addons: [
|
||||
'storybook-dark-mode',
|
||||
'@storybook/addon-links',
|
||||
'storybook-addon-mock/register',
|
||||
'@storybook/addon-essentials',
|
||||
{
|
||||
name: 'storybook-addon-turbo-build',
|
||||
options: { optimizationLevel: 2 },
|
||||
},
|
||||
],
|
||||
typescript: {
|
||||
check: false,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useDarkMode } from 'storybook-dark-mode';
|
||||
import { MantineProvider, ColorSchemeProvider } from '@mantine/core';
|
||||
import { NotificationsProvider } from '@mantine/notifications';
|
||||
|
||||
@@ -7,11 +6,7 @@ export const parameters = { layout: 'fullscreen' };
|
||||
function ThemeWrapper(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<ColorSchemeProvider colorScheme="light" toggleColorScheme={() => {}}>
|
||||
<MantineProvider
|
||||
theme={{ colorScheme: useDarkMode() ? 'dark' : 'light' }}
|
||||
withGlobalStyles
|
||||
withNormalizeCSS
|
||||
>
|
||||
<MantineProvider withGlobalStyles withNormalizeCSS>
|
||||
<NotificationsProvider>{props.children}</NotificationsProvider>
|
||||
</MantineProvider>
|
||||
</ColorSchemeProvider>
|
||||
|
||||
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
ajnart@pm.me.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
106
CONTRIBUTING.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Contributing to Homarr
|
||||
|
||||
First off, thanks for taking the time to contribute! ❤️
|
||||
|
||||
All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
|
||||
|
||||
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
|
||||
> - Star the project
|
||||
> - Tweet about it
|
||||
> - Refer this project in your project's readme
|
||||
> - Mention the project at local meetups and tell your friends/colleagues
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
- [I Have a Question](#i-have-a-question)
|
||||
- [I Want To Contribute](#i-want-to-contribute)
|
||||
- [Reporting Bugs](#reporting-bugs)
|
||||
- [Suggesting Enhancements](#suggesting-enhancements)
|
||||
- [Styleguides](#styleguides)
|
||||
- [Commit Messages](#commit-messages)
|
||||
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project and everyone participating in it is governed by the
|
||||
[Homarr Code of Conduct](https://github.com/ajnart/homarr/blob/master/CODE_OF_CONDUCT.md).
|
||||
By participating, you are expected to uphold this code. Please report unacceptable behavior
|
||||
to [@ajnart](https://github.com/ajnart).
|
||||
|
||||
|
||||
## I Have a Question
|
||||
|
||||
> If you want to ask a question, we assume that you have read the available [Documentation](https://github.com/ajnart/homarr/#readme).
|
||||
|
||||
Before you ask a question, it is best to search for existing [Issues](https://github.com/ajnart/homarr/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first.
|
||||
|
||||
If you then still feel the need to ask a question and need clarification, we recommend the following:
|
||||
|
||||
- Open an [Issue](https://github.com/ajnart/homarr/issues/new).
|
||||
- Provide as much context as you can about what you're running into.
|
||||
- Provide project and platform versions (nodejs, docker, etc), depending on what seems relevant.
|
||||
|
||||
We will then take care of the issue as soon as possible.
|
||||
|
||||
## I Want To Contribute
|
||||
|
||||
> ### Legal Notice
|
||||
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.
|
||||
|
||||
### Reporting Bugs
|
||||
|
||||
#### Before Submitting a Bug Report
|
||||
|
||||
A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible.
|
||||
|
||||
- Make sure that you are using the latest version.
|
||||
- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://github.com/ajnart/homarr/#readme). If you are looking for support, you might want to check [this section](#i-have-a-question)).
|
||||
- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/ajnart/homarr/issues?q=is%3Aopen+is%3Aissue+label%3A%22%F0%9F%90%9B+Bug%22).
|
||||
- Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue.
|
||||
- Collect information about the bug:
|
||||
- Stack trace (Traceback)
|
||||
- OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
|
||||
- Version of yarn, nodejs, docker, npm, next, depending on what seems relevant.
|
||||
- Possibly your input and the output
|
||||
- Can you reliably reproduce the issue? And can you also reproduce it with older versions?
|
||||
|
||||
#### How Do I Submit a Good Bug Report?
|
||||
|
||||
> You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to ajnart@pm.me.
|
||||
|
||||
We use GitHub issues to track bugs and errors. If you run into an issue with the project:
|
||||
|
||||
- Open an [Issue](https://github.com/ajnart/homarr/issues/new).
|
||||
- Explain the behavior you would expect and the actual behavior.
|
||||
- Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case.
|
||||
- Provide the information you collected in the previous section.
|
||||
|
||||
|
||||
### Suggesting Enhancements
|
||||
|
||||
This section guides you through submitting an enhancement suggestion for Homarr, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions.
|
||||
|
||||
#### Before Submitting an Enhancement
|
||||
|
||||
- Make sure that you are using the latest version.
|
||||
- Read the [documentation](https://github.com/ajnart/homarr/#readme) carefully and find out if the functionality is already covered, maybe by an individual configuration.
|
||||
- Perform a [search](https://github.com/ajnart/homarr/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
|
||||
- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library.
|
||||
|
||||
#### How Do I Submit a Good Enhancement Suggestion?
|
||||
|
||||
Enhancement suggestions are tracked as [GitHub issues](https://github.com/ajnart/homarr//issues).
|
||||
|
||||
- Use a **clear and descriptive title** for the issue to identify the suggestion.
|
||||
- Provide a **step-by-step description of the suggested enhancement** in as many details as possible.
|
||||
- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you.
|
||||
- You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. <!-- this should only be included if the project has a GUI -->
|
||||
- **Explain why this enhancement would be useful** to most Homarr users. You may also want to point out the other projects that solved it better and which could serve as inspiration.
|
||||
|
||||
## Styleguides
|
||||
### Commit Messages
|
||||
|
||||
Homarr uses [GitMoji](https://gitmoji.dev/).
|
||||
We would appreciate it if everyone keeps their commit messages withing these rulings.
|
||||
|
||||
13
Dockerfile
@@ -1,19 +1,12 @@
|
||||
FROM node:16-alpine
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV production
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
COPY /next.config.js ./
|
||||
COPY /public ./public
|
||||
COPY /package.json ./package.json
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --chown=nextjs:nodejs /.next/standalone ./
|
||||
COPY --chown=nextjs:nodejs /.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
# Automatically leverage output traces to reduce image size. https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY /.next/standalone ./
|
||||
COPY /.next/static ./.next/static
|
||||
EXPOSE 7575
|
||||
ENV PORT 7575
|
||||
VOLUME /app/data/configs
|
||||
|
||||
162
README.md
@@ -1,24 +1,52 @@
|
||||
<p align = "center">
|
||||
<h3 align = "center"> Homarr <h3>
|
||||
<h3 align="center">Homarr</h3>
|
||||
<br>
|
||||
<p align="center">
|
||||
<i>Don't forget to star the repo if you enjoy the Homarr project!</i>
|
||||
<br>
|
||||
<img src="https://img.shields.io/github/stars/ajnart/homarr?label=%E2%AD%90%20Stars&style=flat-square?branch=master&kill_cache=1%22">
|
||||
<a href="https://github.com/ajnart/homarr/actions/workflows/docker.yml">
|
||||
<img title="Docker CI Status" src="https://github.com/ajnart/homarr/actions/workflows/docker.yml/badge.svg" alt="CI Status"></a>
|
||||
<a href="https://github.com/ajnart/homarr/releases/latest">
|
||||
<img alt="GitHub release (latest SemVer)" src="https://img.shields.io/github/v/release/ajnart/homarr"></a>
|
||||
<a href="https://github.com/ajnart/homarr/pkgs/container/homarr">
|
||||
<img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/ajnart/homarr?label=Downloads%20"></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<img align="end" width=600 src="https://user-images.githubusercontent.com/71191962/169860380-856634fb-4f41-47cb-ba54-6a9e7b3b9c81.gif" />
|
||||
|
||||
</p>
|
||||
<p align = "center">
|
||||
A homepage for <i>your</i> server.
|
||||
<br/>
|
||||
<a href = "https://github.com/ajnart/homarr/deployments/activity_log?environment=Production" > <strong> Demo ↗️ </strong> </a> • <a href = "#install" > <strong> Install ➡️ </strong> </a>
|
||||
<a href = "https://homarr.netlify.app/" > <strong> Demo ↗️ </strong> </a> • <a href = "#-installation" > <strong> Install ➡️ </strong> </a>
|
||||
<br />
|
||||
<br />
|
||||
<a href = "https://discord.gg/aCsmEV5RgA" > <img src="https://discordapp.com/api/guilds/972958686051962910/widget.png?style=shield" > </a>
|
||||
</p>
|
||||
<i>Join the discord!</i>
|
||||
<br />
|
||||
<a href = "https://discord.gg/aCsmEV5RgA" > <img title="Discord" src="https://discordapp.com/api/guilds/972958686051962910/widget.png?style=shield" > </a>
|
||||
<br/>
|
||||
<br/>
|
||||
</p>
|
||||
|
||||
# 📃 Table of Contents
|
||||
- [📃 Table of Contents](#-table-of-contents)
|
||||
- [🚀 Getting Started](#-getting-started)
|
||||
- [ℹ️ About](#ℹ️-about)
|
||||
- [💥 Known Issues](#-known-issues)
|
||||
- [⚡ Installation](#-installation)
|
||||
- [Deploying from Docker Image 🐳](#deploying-from-docker-image-)
|
||||
- [Building from Source 🛠️](#building-from-source-️)
|
||||
- [🐳 Deploying from Docker Image](#-deploying-from-docker-image)
|
||||
- [🛠️ Building from Source](#%EF%B8%8F-building-from-source)
|
||||
- [📖 Guides](#-guides)
|
||||
- [🔁 Drag and Drop (Rearrange)](#-drag-and-drop-rearrange)
|
||||
- [🔧 Configuration](#-configuration)
|
||||
- [🧩 Integrations](#--integrations)
|
||||
- [🧑🤝🧑 Multiple Configs](#-multiple-configs)
|
||||
- [🐻 Icons](#-icons)
|
||||
- [📊 Modules](#-modules)
|
||||
- [🔍 Search Bar](#-search-bar)
|
||||
- [💖 Contributing](#-contributing)
|
||||
- [🍏 Request Icons](#-request-icons)
|
||||
|
||||
|
||||
<!-- Getting Started -->
|
||||
# 🚀 Getting Started
|
||||
@@ -29,9 +57,14 @@ Homarr is a simple and lightweight homepage for your server, that helps you easi
|
||||
|
||||
**[⤴️ Back to Top](#-table-of-contents)**
|
||||
|
||||
## 💥 Known Issues
|
||||
- Posters on the Calendar get blocked by adblockers. (IMDb posters)
|
||||
|
||||
**[⤴️ Back to Top](#-table-of-contents)**
|
||||
|
||||
## ⚡ Installation
|
||||
|
||||
### Deploying from Docker Image 🐳
|
||||
### 🐳 Deploying from Docker Image
|
||||
> Supported architectures: x86-64, ARM, ARM64
|
||||
|
||||
_Requirements_:
|
||||
@@ -39,7 +72,7 @@ _Requirements_:
|
||||
|
||||
**Standard Docker Install**
|
||||
```sh
|
||||
docker run --name homarr -p 7575:7575 -d ghcr.io/ajnart/homarr
|
||||
docker run --name homarr -p 7575:7575 -v /data/docker/homarr:/app/data/configs -d ghcr.io/ajnart/homarr:latest
|
||||
```
|
||||
|
||||
**Docker Compose**
|
||||
@@ -52,35 +85,126 @@ version: '3'
|
||||
services:
|
||||
homarr:
|
||||
container_name: homarr
|
||||
image: ghcr.io/ajnart/homarr
|
||||
image: ghcr.io/ajnart/homarr:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /data/docker/homarr:/app/data/configs
|
||||
ports:
|
||||
- '7575:7575'
|
||||
```
|
||||
|
||||
### Building from Source 🛠️
|
||||
***Getting EACCESS errors in the logs? Try running `sudo chmod 775 /directory-you-mounted-to`!***
|
||||
|
||||
### 🛠️ Building from Source
|
||||
|
||||
_Requirements_:
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [NodeJS](https://nodejs.org/en/) _(Latest or LTS)_
|
||||
- [Yarn](https://yarnpkg.com/)
|
||||
- Some web server
|
||||
|
||||
**Installing**
|
||||
|
||||
- Clone the GitHub repo: `git clone https://github.com/ajnart/homarr.git` & `cd homarr`
|
||||
- Install all dependencies: `yarn install`
|
||||
- Build the source: `yarn export`
|
||||
- Start a web server (Any web server will work):
|
||||
- _Examples:_
|
||||
- NodeJS serve: `npm i -g serve` or `yarn global add serve` & `serve ./out`
|
||||
- python http.server: `python -m http.server 7474 --directory out`
|
||||
|
||||
- Build the source: `yarn build`
|
||||
- Start the NextJS web server: ``yarn start``
|
||||
- *Note: If you want to update the code in real time, launch with ``yarn dev``*
|
||||
|
||||
## 📖 Guides
|
||||
|
||||
### 🔁 Drag and Drop (Rearrange)
|
||||
You can rearrange items by Drag and Dropping them to any position. To Drag an Drop, click and hold an icon for 250ms and then drag it to the desired position.
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### 🧩 Integrations
|
||||
|
||||
Homarr natively integrates with your services. Here is a list of all supported services.
|
||||
|
||||
**Emby**
|
||||
*The Emby integration is still in development.*
|
||||
|
||||
**Lidarr**
|
||||
*The Lidarr integration is still in development.*
|
||||
|
||||
**Sonarr**
|
||||
*Sonarr needs an API key.*<br>
|
||||
Make a new API key in `Advanced > Security > Create new API key`<br>
|
||||
**Current integration:** Upcoming media is displayed in the **Calendar** module.
|
||||
|
||||
**Plex**
|
||||
*The Plex integration is still in development.*
|
||||
|
||||
**Radarr**
|
||||
*Radarr needs an API key.*<br>
|
||||
Make a new API key in `Advanced > Security > Create new API key`<br>
|
||||
**Current integration:** Upcoming media is displayed in the **Calendar** module.
|
||||
|
||||
**qBittorent**
|
||||
*The qBittorent integration is still in development.*
|
||||
|
||||
**[⤴️ Back to Top](#-table-of-contents)**
|
||||
|
||||
### 🧑🤝🧑 Multiple Configs
|
||||
|
||||
Homarr allows the usage of multiple configs. You can add a new config in two ways.
|
||||
|
||||
**Drag-and-Drop**
|
||||
1. Download your config from the Homarr settings.
|
||||
2. Change the name of the `.json` file and the name in the `.json` file to any name you want *(just make sure it's different)*.
|
||||
3. Drag-and-Drop the file into the Homarr tab in your browser.
|
||||
4. Change the config in settings.
|
||||
|
||||
**Using a filebrowser**
|
||||
1. Locate your mounted `default.json` file.
|
||||
2. Duplicate your `default.json` file.
|
||||
3. Change the name of the `.json` file and the name in the `.json` file to any name you want *(just make sure it's different)*.
|
||||
4. Refresh the Homarr tab in your browser.
|
||||
5. Change the config in settings.
|
||||
|
||||
**[⤴️ Back to Top](#-table-of-contents)**
|
||||
|
||||
### 🐻 Icons
|
||||
|
||||
The icons used in Homarr are automatically requested from the [dashboard-icons](https://github.com/walkxhub/dashboard-icons) repo.
|
||||
|
||||
Icons are requested in the following way: <br>
|
||||
`Grab name > Replace ' ' with '-' > .toLower() > https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/{name}.png`
|
||||
|
||||
**[⤴️ Back to Top](#-table-of-contents)**
|
||||
|
||||
### 📊 Modules
|
||||
|
||||
Modules are blocks shown on the sides of the Homarr dashboard that display information. They can be enabled in settings.
|
||||
|
||||
**Clock Module**
|
||||
The Clock Module will display your current time and date.
|
||||
|
||||
**Calendar Module**
|
||||
The Calendar Module uses [integrations](#--integrations-1) to display new content.
|
||||
|
||||
**Weather Module**
|
||||
The Weather Module uses your devices location to display the current, highest, and lowest temperature.
|
||||
|
||||
**[⤴️ Back to Top](#-table-of-contents)**
|
||||
|
||||
### 🔍 Search Bar
|
||||
|
||||
The Search Bar will open any Search Query after the Query URL you've specified in settings.
|
||||
|
||||
*(Eg. `https://www.google.com/search?q=*Your Query will be inserted here*`)*
|
||||
|
||||
**[⤴️ Back to Top](#-table-of-contents)**
|
||||
|
||||
# 💖 Contributing
|
||||
You can contribute by [Starting a discussion](https://github.com/ajnart/homarr/discussions), [Submitting Bugs](https://github.com/ajnart/homarr/issues/new), [Requesting Features](https://github.com/ajnart/homarr/issues/new), or [Making a pull request](https://github.com/ajnart/homarr/compare)!
|
||||
**Please read our [Contribution Guidelines](/CONTRIBUTING.md)**
|
||||
|
||||
All contributions are highly appreciated.
|
||||
|
||||
**[⤴️ Back to Top](#-table-of-contents)**
|
||||
|
||||
## 🍏 Request Icons
|
||||
|
||||
The icons used in Homarr are automatically requested from the [dashboard-icons](https://github.com/walkxhub/dashboard-icons) repo. You can make a icon request by creating an [issue](https://github.com/walkxhub/dashboard-icons/issues/new/choose).
|
||||
|
||||
**[⤴️ Back to Top](#-table-of-contents)**
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Text, AspectRatio, SimpleGrid, Card, Image, Group, Space } from '@mantine/core';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { serviceItem } from '../../tools/types';
|
||||
import AddItemShelfItem from './AddAppShelfItem';
|
||||
import { AppShelfItemWrapper } from './AppShelfItemWrapper';
|
||||
import AppShelfMenu from './AppShelfMenu';
|
||||
|
||||
const AppShelf = () => {
|
||||
const { config } = useConfig();
|
||||
|
||||
return (
|
||||
<SimpleGrid m="xl" cols={5} spacing="xl">
|
||||
{config.services.map((service) => (
|
||||
<AppShelfItem key={service.name} service={service} />
|
||||
))}
|
||||
<AddItemShelfItem />
|
||||
</SimpleGrid>
|
||||
);
|
||||
};
|
||||
|
||||
export function AppShelfItem(props: any) {
|
||||
const { service }: { service: serviceItem } = props;
|
||||
const [hovering, setHovering] = useState(false);
|
||||
return (
|
||||
<motion.div
|
||||
key={service.name}
|
||||
onHoverStart={() => {
|
||||
setHovering(true);
|
||||
}}
|
||||
onHoverEnd={() => {
|
||||
setHovering(false);
|
||||
}}
|
||||
>
|
||||
<AppShelfItemWrapper hovering={hovering}>
|
||||
<Card.Section>
|
||||
<Group position="apart" mx="lg">
|
||||
<Space />
|
||||
<Text
|
||||
// TODO: #1 Remove this hack to get the text to be centered.
|
||||
ml={15}
|
||||
style={{
|
||||
alignSelf: 'center',
|
||||
alignContent: 'center',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
justifyItems: 'center',
|
||||
}}
|
||||
mt="sm"
|
||||
weight={500}
|
||||
>
|
||||
{service.name}
|
||||
</Text>
|
||||
<motion.div
|
||||
style={{
|
||||
alignSelf: 'flex-end',
|
||||
}}
|
||||
animate={{
|
||||
opacity: hovering ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<AppShelfMenu service={service} />
|
||||
</motion.div>
|
||||
</Group>
|
||||
</Card.Section>
|
||||
<Card.Section>
|
||||
<AspectRatio ratio={5 / 3} m="xl">
|
||||
<motion.i
|
||||
whileHover={{
|
||||
cursor: 'pointer',
|
||||
scale: 1.1,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
onClick={() => {
|
||||
window.open(service.url);
|
||||
}}
|
||||
style={{
|
||||
maxWidth: '50%',
|
||||
marginBottom: 10,
|
||||
}}
|
||||
src={service.icon}
|
||||
/>
|
||||
</motion.i>
|
||||
</AspectRatio>
|
||||
</Card.Section>
|
||||
</AppShelfItemWrapper>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AppShelf;
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useMantineTheme, Card } from '@mantine/core';
|
||||
|
||||
export function AppShelfItemWrapper(props: any) {
|
||||
const { children, hovering } = props;
|
||||
const theme = useMantineTheme();
|
||||
return (
|
||||
<Card
|
||||
style={{
|
||||
boxShadow: hovering ? '0px 0px 3px rgba(0, 0, 0, 0.5)' : '0px 0px 1px rgba(0, 0, 0, 0.5)',
|
||||
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[1],
|
||||
|
||||
//TODO: #3 Fix this temporary fix and make the width and height dynamic / responsive
|
||||
width: 200,
|
||||
height: 180,
|
||||
}}
|
||||
radius="md"
|
||||
>
|
||||
{children}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import { TextInput, Text, Popover, Box } from '@mantine/core';
|
||||
import { useForm } from '@mantine/hooks';
|
||||
import { useState } from 'react';
|
||||
import { Search, BrandYoutube, Download } from 'tabler-icons-react';
|
||||
import { useConfig } from '../../tools/state';
|
||||
|
||||
export default function SearchBar(props: any) {
|
||||
const { config, setConfig } = useConfig();
|
||||
const [opened, setOpened] = useState(false);
|
||||
const [icon, setIcon] = useState(<Search />);
|
||||
const querryUrl = config.settings.searchUrl || 'https://www.google.com/search?q=';
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
querry: '',
|
||||
},
|
||||
});
|
||||
|
||||
if (config.settings.searchBar === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<form
|
||||
onChange={() => {
|
||||
// If querry contains !yt or !t add "Searching on YouTube" or "Searching torrent"
|
||||
const querry = form.values.querry.trim();
|
||||
const isYoutube = querry.startsWith('!yt');
|
||||
const isTorrent = querry.startsWith('!t');
|
||||
if (isYoutube) {
|
||||
setIcon(<BrandYoutube size={22} />);
|
||||
} else if (isTorrent) {
|
||||
setIcon(<Download size={22} />);
|
||||
} else {
|
||||
setIcon(<Search size={22} />);
|
||||
}
|
||||
}}
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
// Find if querry is prefixed by !yt or !t
|
||||
const querry = values.querry.trim();
|
||||
const isYoutube = querry.startsWith('!yt');
|
||||
const isTorrent = querry.startsWith('!t');
|
||||
if (isYoutube) {
|
||||
window.open(`https://www.youtube.com/results?search_query=${querry.substring(3)}`);
|
||||
} else if (isTorrent) {
|
||||
window.open(`https://bitsearch.to/search?q=${querry.substring(3)}`);
|
||||
} else {
|
||||
window.open(`${querryUrl}${values.querry}`);
|
||||
}
|
||||
})}
|
||||
>
|
||||
<Popover
|
||||
opened={opened}
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
position="bottom"
|
||||
placement="start"
|
||||
withArrow
|
||||
trapFocus={false}
|
||||
transition="pop-top-left"
|
||||
onFocusCapture={() => setOpened(true)}
|
||||
onBlurCapture={() => setOpened(false)}
|
||||
target={
|
||||
<TextInput
|
||||
variant="filled"
|
||||
color="blue"
|
||||
icon={icon}
|
||||
radius="md"
|
||||
size="md"
|
||||
placeholder="Search the web"
|
||||
{...props}
|
||||
{...form.getInputProps('querry')}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Text>
|
||||
tip: You can prefix your querry with <b>!yt</b> or <b>!t</b> to research on youtube or
|
||||
for a torrent
|
||||
</Text>
|
||||
</Popover>
|
||||
</form>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Group, Switch } from '@mantine/core';
|
||||
import * as Modules from '../modules';
|
||||
import { useConfig } from '../../tools/state';
|
||||
|
||||
export default function ModuleEnabler(props: any) {
|
||||
const { config, setConfig } = useConfig();
|
||||
const modules = Object.values(Modules).map((module) => module);
|
||||
const enabledModules = config.settings.enabledModules ?? [];
|
||||
modules.filter((module) => enabledModules.includes(module.title));
|
||||
return (
|
||||
<Group direction="column">
|
||||
{modules.map((module) => (
|
||||
<Switch
|
||||
key={module.title}
|
||||
size="md"
|
||||
checked={enabledModules.includes(module.title)}
|
||||
label={`Enable ${module.title} module`}
|
||||
onChange={(e) => {
|
||||
if (e.currentTarget.checked) {
|
||||
setConfig({
|
||||
...config,
|
||||
settings: {
|
||||
...config.settings,
|
||||
enabledModules: [...enabledModules, module.title],
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setConfig({
|
||||
...config,
|
||||
settings: {
|
||||
...config.settings,
|
||||
enabledModules: enabledModules.filter((m) => m !== module.title),
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Welcome } from './Welcome';
|
||||
|
||||
export default {
|
||||
title: 'Welcome',
|
||||
};
|
||||
|
||||
export const Usage = () => <Welcome />;
|
||||
@@ -1,14 +0,0 @@
|
||||
import { createStyles } from '@mantine/core';
|
||||
|
||||
export default createStyles((theme) => ({
|
||||
title: {
|
||||
color: theme.colorScheme === 'dark' ? theme.white : theme.black,
|
||||
fontSize: 100,
|
||||
fontWeight: 900,
|
||||
letterSpacing: -2,
|
||||
|
||||
[theme.fn.smallerThan('md')]: {
|
||||
fontSize: 50,
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -1,12 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { Welcome } from './Welcome';
|
||||
|
||||
describe('Welcome component', () => {
|
||||
it('has correct Next.js theming section link', () => {
|
||||
render(<Welcome />);
|
||||
expect(screen.getByText('this guide')).toHaveAttribute(
|
||||
'href',
|
||||
'https://mantine.dev/theming/next/'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Title, Text, Anchor } from '@mantine/core';
|
||||
import useStyles from './Welcome.styles';
|
||||
|
||||
export function Welcome() {
|
||||
const { classes } = useStyles();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title className={classes.title} align="center" mt={100}>
|
||||
Welcome to{' '}
|
||||
<Text inherit variant="gradient" component="span">
|
||||
Mantine
|
||||
</Text>
|
||||
</Title>
|
||||
<Text color="dimmed" align="center" size="lg" sx={{ maxWidth: 580 }} mx="auto" mt="xl">
|
||||
This starter Next.js project includes a minimal setup for server side rendering, if you want
|
||||
to learn more on Mantine + Next.js integration follow{' '}
|
||||
<Anchor href="https://mantine.dev/theming/next/" size="lg">
|
||||
this guide
|
||||
</Anchor>
|
||||
. To get started edit index.tsx file.
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Aside as MantineAside } from '@mantine/core';
|
||||
import { CalendarModule } from '../modules/calendar/CalendarModule';
|
||||
import ModuleWrapper from '../modules/moduleWrapper';
|
||||
|
||||
export default function Aside() {
|
||||
return (
|
||||
<MantineAside
|
||||
height="100%"
|
||||
hiddenBreakpoint="md"
|
||||
hidden
|
||||
width={{
|
||||
base: 'auto',
|
||||
}}
|
||||
>
|
||||
<ModuleWrapper module={CalendarModule} />
|
||||
</MantineAside>
|
||||
);
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import React from 'react';
|
||||
import { createStyles, Anchor, Text, Group, ActionIcon } from '@mantine/core';
|
||||
import { BrandGithub } from 'tabler-icons-react';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
footer: {
|
||||
borderTop: `1px solid ${
|
||||
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[2]
|
||||
}`,
|
||||
},
|
||||
|
||||
inner: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: `${theme.spacing.md}px ${theme.spacing.md}px`,
|
||||
|
||||
[theme.fn.smallerThan('sm')]: {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
},
|
||||
|
||||
links: {
|
||||
[theme.fn.smallerThan('sm')]: {
|
||||
marginTop: theme.spacing.lg,
|
||||
marginBottom: theme.spacing.sm,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
interface FooterCenteredProps {
|
||||
links: { link: string; label: string }[];
|
||||
}
|
||||
|
||||
export function Footer({ links }: FooterCenteredProps) {
|
||||
const { classes } = useStyles();
|
||||
const items = links.map((link) => (
|
||||
<Anchor<'a'>
|
||||
color="dimmed"
|
||||
key={link.label}
|
||||
href={link.link}
|
||||
sx={{ lineHeight: 1 }}
|
||||
onClick={(event) => event.preventDefault()}
|
||||
size="sm"
|
||||
>
|
||||
{link.label}
|
||||
</Anchor>
|
||||
));
|
||||
|
||||
return (
|
||||
<Group
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
right: 15,
|
||||
}}
|
||||
direction="row"
|
||||
align="center"
|
||||
mb={15}
|
||||
>
|
||||
<Group className={classes.links}>{items}</Group>
|
||||
<Group spacing="xs" position="right" noWrap>
|
||||
<ActionIcon<'a'> component="a" href="https://github.com/ajnart/homarr" size="lg">
|
||||
<BrandGithub size={18} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: '0.75rem',
|
||||
textAlign: 'center',
|
||||
color: '#a0aec0',
|
||||
}}
|
||||
>
|
||||
Made with ❤️ by @
|
||||
<Anchor href="https://github.com/ajnart" style={{ color: 'inherit', fontStyle: 'inherit' }}>
|
||||
ajnart
|
||||
</Anchor>
|
||||
</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
createStyles,
|
||||
Header as Head,
|
||||
Container,
|
||||
Group,
|
||||
Burger,
|
||||
Drawer,
|
||||
Center,
|
||||
} from '@mantine/core';
|
||||
import { useBooleanToggle } from '@mantine/hooks';
|
||||
import { NextLink } from '@mantine/next';
|
||||
import { Logo } from './Logo';
|
||||
import { SettingsMenuButton } from '../Settings/SettingsMenu';
|
||||
import CalendarComponent from '../modules/calendar/CalendarModule';
|
||||
|
||||
const HEADER_HEIGHT = 60;
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
root: {
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
},
|
||||
|
||||
dropdown: {
|
||||
position: 'absolute',
|
||||
top: HEADER_HEIGHT,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 0,
|
||||
borderTopRightRadius: 0,
|
||||
borderTopLeftRadius: 0,
|
||||
borderTopWidth: 0,
|
||||
overflow: 'hidden',
|
||||
|
||||
[theme.fn.largerThan('md')]: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
|
||||
header: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
},
|
||||
|
||||
links: {
|
||||
[theme.fn.smallerThan('md')]: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
|
||||
burger: {
|
||||
[theme.fn.largerThan('md')]: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
|
||||
link: {
|
||||
display: 'block',
|
||||
lineHeight: 1,
|
||||
padding: '8px 12px',
|
||||
borderRadius: theme.radius.sm,
|
||||
textDecoration: 'none',
|
||||
color: theme.colorScheme === 'dark' ? theme.colors.dark[0] : theme.colors.gray[7],
|
||||
fontSize: theme.fontSizes.sm,
|
||||
fontWeight: 500,
|
||||
|
||||
'&:hover': {
|
||||
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0],
|
||||
},
|
||||
|
||||
[theme.fn.smallerThan('sm')]: {
|
||||
borderRadius: 0,
|
||||
padding: theme.spacing.md,
|
||||
},
|
||||
},
|
||||
|
||||
linkActive: {
|
||||
'&, &:hover': {
|
||||
backgroundColor:
|
||||
theme.colorScheme === 'dark'
|
||||
? theme.fn.rgba(theme.colors[theme.primaryColor][9], 0.25)
|
||||
: theme.colors[theme.primaryColor][0],
|
||||
color: theme.colors[theme.primaryColor][theme.colorScheme === 'dark' ? 3 : 7],
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
interface HeaderResponsiveProps {
|
||||
links: { link: string; label: string }[];
|
||||
}
|
||||
|
||||
export function Header({ links }: HeaderResponsiveProps) {
|
||||
const [opened, toggleOpened] = useBooleanToggle(false);
|
||||
const [active, setActive] = useState('/');
|
||||
const { classes, cx } = useStyles();
|
||||
|
||||
const items = (
|
||||
<>
|
||||
{links.map((link) => (
|
||||
<NextLink
|
||||
key={link.label}
|
||||
href={link.link}
|
||||
className={cx(classes.link, { [classes.linkActive]: active === link.link })}
|
||||
onClick={(event) => {
|
||||
setActive(link.link);
|
||||
toggleOpened(false);
|
||||
}}
|
||||
>
|
||||
{link.label}
|
||||
</NextLink>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<Head height={HEADER_HEIGHT} mb={10} className={classes.root}>
|
||||
<Container className={classes.header}>
|
||||
<Group>
|
||||
<NextLink style={{ textDecoration: 'none' }} href="/">
|
||||
<Logo style={{ fontSize: 22 }} />
|
||||
</NextLink>
|
||||
</Group>
|
||||
<Group spacing={5} className={classes.links}>
|
||||
{items}
|
||||
</Group>
|
||||
<Group>
|
||||
<SettingsMenuButton />
|
||||
|
||||
<Burger
|
||||
opened={opened}
|
||||
onClick={() => toggleOpened()}
|
||||
className={classes.burger}
|
||||
size="sm"
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Drawer
|
||||
opened={opened}
|
||||
overlayOpacity={0.55}
|
||||
overlayBlur={3}
|
||||
onClose={() => toggleOpened()}
|
||||
position="right"
|
||||
>
|
||||
{opened ?? (
|
||||
<Center>
|
||||
<CalendarComponent />
|
||||
</Center>
|
||||
)}
|
||||
</Drawer>
|
||||
</Container>
|
||||
</Head>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { AppShell, Center, createStyles } from '@mantine/core';
|
||||
import { Header } from './Header';
|
||||
import { Footer } from './Footer';
|
||||
import Aside from './Aside';
|
||||
import Navbar from './Navbar';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
main: {
|
||||
[theme.fn.largerThan('md')]: {
|
||||
width: 1200,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export default function Layout({ children, style }: any) {
|
||||
const { classes, cx } = useStyles();
|
||||
return (
|
||||
<AppShell
|
||||
navbar={<Navbar />}
|
||||
aside={<Aside />}
|
||||
header={<Header links={[]} />}
|
||||
footer={<Footer links={[]} />}
|
||||
>
|
||||
<Center>
|
||||
<main
|
||||
className={cx(classes.main)}
|
||||
style={{
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
</Center>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Text } from '@mantine/core';
|
||||
import * as React from 'react';
|
||||
|
||||
export function Logo({ style }: any) {
|
||||
return (
|
||||
<Text
|
||||
sx={style}
|
||||
weight="bold"
|
||||
variant="gradient"
|
||||
gradient={{ from: 'red', to: 'orange', deg: 145 }}
|
||||
>
|
||||
Homarr
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Navbar as MantineNavbar } from '@mantine/core';
|
||||
import { DateModule } from '../modules/date/DateModule';
|
||||
import ModuleWrapper from '../modules/moduleWrapper';
|
||||
|
||||
export default function Navbar() {
|
||||
return (
|
||||
<MantineNavbar
|
||||
height="100%"
|
||||
hiddenBreakpoint="md"
|
||||
hidden
|
||||
width={{
|
||||
base: 'auto',
|
||||
}}
|
||||
>
|
||||
<ModuleWrapper module={DateModule} />
|
||||
</MantineNavbar>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './date';
|
||||
export * from './calendar';
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Card, useMantineTheme } from '@mantine/core';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { IModule } from './modules';
|
||||
|
||||
export default function ModuleWrapper(props: any) {
|
||||
const { module }: { module: IModule } = props;
|
||||
const { config } = useConfig();
|
||||
const enabledModules = config.settings.enabledModules ?? [];
|
||||
// Remove 'Module' from enabled modules titles
|
||||
const isShown = enabledModules.includes(module.title);
|
||||
const theme = useMantineTheme();
|
||||
if (!isShown) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Card
|
||||
hidden={!isShown}
|
||||
mx="sm"
|
||||
radius="lg"
|
||||
shadow="sm"
|
||||
style={{
|
||||
// Make background color of the card depend on the theme
|
||||
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[5] : 'white',
|
||||
}}
|
||||
>
|
||||
<module.component />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"name": "config",
|
||||
"services": [],
|
||||
"settings": {
|
||||
"searchBar": true,
|
||||
"searchUrl": "https://duckduckgo.com/?q=",
|
||||
"enabledModules": [
|
||||
"Date",
|
||||
"Calendar"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"name": "config_new",
|
||||
"services": [
|
||||
{
|
||||
"type": "Other",
|
||||
"name": "example",
|
||||
"icon": "https://c.tenor.com/o656qFKDzeUAAAAC/rick-astley-never-gonna-give-you-up.gif",
|
||||
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"searchBar": true,
|
||||
"searchUrl": "https://google.com/search?q=",
|
||||
"enabledModules": []
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,14 @@
|
||||
"name": "default",
|
||||
"services": [
|
||||
{
|
||||
"type": "Other",
|
||||
"name": "example",
|
||||
"id": "09c45847-8afc-4c1a-9697-f03192de948a",
|
||||
"type": "Other",
|
||||
"icon": "https://c.tenor.com/o656qFKDzeUAAAAC/rick-astley-never-gonna-give-you-up.gif",
|
||||
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"searchBar": true,
|
||||
"searchUrl": "https://bing.com/search?q=",
|
||||
"enabledModules": []
|
||||
"searchUrl": "https://bing.com/search?q="
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
export const REPO_URL = 'ajnart/homarr';
|
||||
export const CURRENT_VERSION = 'v0.2.0';
|
||||
export const CURRENT_VERSION = 'v0.5.0';
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
const nextJest = require('next/jest');
|
||||
|
||||
const createJestConfig = nextJest({
|
||||
dir: './',
|
||||
});
|
||||
|
||||
const customJestConfig = {
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
||||
moduleNameMapper: {
|
||||
'^@/components/(.*)$': '<rootDir>/components/$1',
|
||||
'^@/pages/(.*)$': '<rootDir>/pages/$1',
|
||||
},
|
||||
testEnvironment: 'jest-environment-jsdom',
|
||||
};
|
||||
|
||||
module.exports = createJestConfig(customJestConfig);
|
||||
@@ -1 +0,0 @@
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
@@ -1,13 +1,16 @@
|
||||
const { env } = require('process');
|
||||
|
||||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
enabled: process.env.ANALYZE === 'true',
|
||||
});
|
||||
|
||||
module.exports = withBundleAnalyzer({
|
||||
reactStrictMode: true,
|
||||
reactStrictMode: false,
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
experimental: {
|
||||
outputStandalone: true,
|
||||
},
|
||||
basePath: env.BASE_URL,
|
||||
});
|
||||
|
||||
151
package.json
@@ -1,87 +1,80 @@
|
||||
{
|
||||
"name": "homarr",
|
||||
"version": "0.1.6",
|
||||
"version": "0.5.0",
|
||||
"private": "false",
|
||||
"description": "Homarr - A homepage for your server.",
|
||||
"description": "Homarr - A homepage for your server.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ajnart/homarr"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"analyze": "ANALYZE=true next build",
|
||||
"start": "next start --port 7575",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"export": "next build && next export",
|
||||
"lint": "next lint",
|
||||
"jest": "jest",
|
||||
"jest:watch": "jest --watch",
|
||||
"prettier:check": "prettier --check \"**/*.{ts,tsx}\"",
|
||||
"prettier:write": "prettier --write \"**/*.{ts,tsx}\"",
|
||||
"test": "npm run prettier:check && npm run lint && npm run typecheck && npm run jest",
|
||||
"storybook": "start-storybook -p 7001",
|
||||
"storybook:build": "build-storybook",
|
||||
"ci": "yarn test && yarn lint --fix && yarn typecheck && yarn prettier:write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mantine/core": "^4.2.4",
|
||||
"@mantine/dates": "^4.2.4",
|
||||
"@mantine/dropzone": "^4.2.4",
|
||||
"@mantine/form": "^4.2.4",
|
||||
"@mantine/hooks": "^4.2.4",
|
||||
"@mantine/modals": "^4.2.4",
|
||||
"@mantine/next": "^4.2.4",
|
||||
"@mantine/notifications": "^4.2.4",
|
||||
"@mantine/prism": "^4.2.4",
|
||||
"@mantine/rte": "^4.2.4",
|
||||
"@mantine/spotlight": "^4.2.4",
|
||||
"@modulz/radix-icons": "^4.0.0",
|
||||
"axios": "^0.27.2",
|
||||
"cookies-next": "^2.0.4",
|
||||
"dayjs": "^1.11.2",
|
||||
"framer-motion": "^6.3.1",
|
||||
"js-file-download": "^0.4.12",
|
||||
"next": "12.1.5-canary.4",
|
||||
"prism-react-renderer": "^1.3.1",
|
||||
"react": "18.0.0",
|
||||
"react-dom": "18.0.0",
|
||||
"tabler-icons-react": "^1.46.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.8",
|
||||
"@next/bundle-analyzer": "^12.1.4",
|
||||
"@next/eslint-plugin-next": "^12.1.4",
|
||||
"@storybook/addon-essentials": "^6.4.22",
|
||||
"@storybook/addon-links": "^6.4.22",
|
||||
"@storybook/react": "^6.4.22",
|
||||
"@testing-library/dom": "^8.12.0",
|
||||
"@testing-library/jest-dom": "^5.16.3",
|
||||
"@testing-library/react": "^13.0.0",
|
||||
"@testing-library/user-event": "^14.0.4",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/node": "^17.0.23",
|
||||
"@types/react": "17.0.43",
|
||||
"@typescript-eslint/eslint-plugin": "^5.16.0",
|
||||
"@typescript-eslint/parser": "^5.16.0",
|
||||
"babel-loader": "^8.2.4",
|
||||
"eslint": "^8.11.0",
|
||||
"eslint-config-airbnb": "19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^16.1.4",
|
||||
"eslint-config-mantine": "1.1.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-jest": "^26.1.3",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-storybook": "^0.5.11",
|
||||
"eslint-plugin-testing-library": "^5.2.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"jest": "^27.5.1",
|
||||
"prettier": "^2.6.2",
|
||||
"storybook-addon-turbo-build": "^1.1.0",
|
||||
"storybook-dark-mode": "^1.0.9",
|
||||
"ts-jest": "^27.1.4",
|
||||
"typescript": "4.6.3"
|
||||
}
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"analyze": "ANALYZE=true next build",
|
||||
"start": "next start",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"export": "next build && next export",
|
||||
"lint": "next lint",
|
||||
"jest": "jest",
|
||||
"jest:watch": "jest --watch",
|
||||
"prettier:check": "prettier --check \"**/*.{ts,tsx}\"",
|
||||
"prettier:write": "prettier --write \"**/*.{ts,tsx}\"",
|
||||
"test": "npm run prettier:check && npm run lint && npm run typecheck && npm run jest",
|
||||
"storybook": "start-storybook -p 7001",
|
||||
"storybook:build": "build-storybook",
|
||||
"ci": "yarn test && yarn lint --fix && yarn typecheck && yarn prettier:write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.0.1",
|
||||
"@dnd-kit/sortable": "^7.0.0",
|
||||
"@mantine/core": "^4.2.6",
|
||||
"@mantine/dates": "^4.2.6",
|
||||
"@mantine/dropzone": "^4.2.6",
|
||||
"@mantine/form": "^4.2.6",
|
||||
"@mantine/hooks": "^4.2.6",
|
||||
"@mantine/next": "^4.2.6",
|
||||
"@mantine/notifications": "^4.2.6",
|
||||
"@mantine/prism": "^4.2.6",
|
||||
"axios": "^0.27.2",
|
||||
"cookies-next": "^2.0.4",
|
||||
"dayjs": "^1.11.2",
|
||||
"framer-motion": "^6.3.1",
|
||||
"js-file-download": "^0.4.12",
|
||||
"next": "12.1.6",
|
||||
"prism-react-renderer": "^1.3.1",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"tabler-icons-react": "^1.46.0",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.8",
|
||||
"@next/bundle-analyzer": "^12.1.4",
|
||||
"@next/eslint-plugin-next": "^12.1.4",
|
||||
"@storybook/react": "^6.5.4",
|
||||
"@types/node": "^17.0.23",
|
||||
"@types/react": "17.0.43",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.16.0",
|
||||
"@typescript-eslint/parser": "^5.16.0",
|
||||
"eslint": "^8.11.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^16.1.0",
|
||||
"eslint-config-mantine": "1.1.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-jest": "^26.1.3",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-storybook": "^0.5.11",
|
||||
"eslint-plugin-testing-library": "^5.2.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"jest": "^28.1.0",
|
||||
"prettier": "^2.6.2",
|
||||
"require-from-string": "^2.0.2",
|
||||
"typescript": "4.6.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "17.0.30"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
public/favicon.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 438 B After Width: | Height: | Size: 14 KiB |
BIN
public/imgs/favicon.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
1
public/imgs/logo-color.svg
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/imgs/logo.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
247
public/imgs/logo.svg
Normal file
@@ -0,0 +1,247 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="1000.000000pt" height="1000.000000pt" viewBox="0 0 1000.000000 1000.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,1000.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M6470 9752 c-179 -11 -423 -57 -605 -113 -94 -29 -116 -37 -212 -73
|
||||
-381 -144 -693 -333 -1030 -621 -6 -5 -63 -61 -126 -123 l-114 -113 -179 88
|
||||
c-223 110 -305 143 -386 158 -12 3 -34 8 -48 11 -14 3 -100 8 -191 10 -154 4
|
||||
-171 3 -253 -21 -159 -46 -241 -93 -355 -201 -55 -53 -101 -100 -101 -104 0
|
||||
-10 76 18 110 41 39 26 206 108 253 124 61 20 202 42 294 44 56 1 105 -6 178
|
||||
-24 55 -15 110 -28 122 -31 76 -15 317 -112 410 -165 l52 -30 -78 -97 c-152
|
||||
-190 -244 -324 -230 -333 8 -5 17 -9 20 -9 4 0 12 -13 19 -30 18 -44 5 -139
|
||||
-39 -269 -20 -62 -34 -114 -31 -116 4 -2 24 14 45 35 22 22 43 40 49 40 5 0
|
||||
23 -10 40 -23 56 -42 210 -135 276 -167 36 -17 72 -35 80 -39 37 -20 139 -43
|
||||
187 -43 35 0 64 6 88 21 l36 21 57 -38 c31 -21 76 -46 100 -55 52 -21 149 -32
|
||||
188 -23 l28 7 -29 23 -29 24 54 34 c78 49 180 84 191 67 37 -57 119 -207 165
|
||||
-304 75 -157 122 -306 134 -421 16 -157 43 -193 152 -199 56 -4 62 -2 90 26
|
||||
29 29 30 33 25 89 -13 122 -191 536 -326 759 -53 87 -58 100 -41 102 6 1 13 2
|
||||
18 3 4 1 39 5 77 10 104 11 156 17 195 21 19 3 62 7 95 10 112 11 246 45 470
|
||||
120 370 124 716 314 905 497 278 269 418 532 380 714 l-12 58 -35 -43 c-48
|
||||
-57 -116 -90 -216 -102 -117 -15 -165 -29 -198 -56 -23 -19 -32 -22 -48 -14
|
||||
-51 27 -95 7 -160 -75 -30 -37 -60 -64 -73 -66 -13 -2 -50 2 -83 10 -79 17
|
||||
-112 10 -189 -43 -107 -73 -120 -78 -196 -70 -84 8 -97 2 -176 -82 -56 -59
|
||||
-63 -63 -94 -58 -18 3 -37 8 -41 11 -4 2 -25 7 -47 10 -37 5 -43 2 -111 -60
|
||||
l-71 -66 -65 0 c-90 0 -160 -28 -221 -90 -47 -47 -52 -50 -103 -50 -49 0 -57
|
||||
-3 -94 -41 -48 -47 -65 -57 -92 -53 -11 1 -39 6 -62 9 -29 4 -43 11 -43 21 0
|
||||
30 20 52 53 58 23 4 38 15 45 31 16 35 49 53 112 59 66 7 99 29 169 113 28 34
|
||||
60 67 72 73 11 6 55 14 97 16 84 5 91 9 133 71 20 29 40 45 65 52 35 10 47 9
|
||||
141 -10 61 -12 82 -4 163 65 57 49 103 62 173 52 41 -6 43 -5 85 44 68 79 92
|
||||
86 268 80 84 -3 113 9 175 75 95 100 156 119 227 73 44 -29 61 -28 110 6 57
|
||||
39 107 56 141 50 26 -6 38 1 92 49 72 66 120 79 184 53 21 -9 39 -15 40 -14 1
|
||||
2 9 14 18 28 51 75 -5 176 -155 283 -146 104 -224 134 -478 182 -61 11 -221
|
||||
34 -280 40 -69 8 -327 12 -400 7z"/>
|
||||
<path d="M8256 8794 c-3 -8 -3 -46 0 -82 4 -47 1 -86 -10 -128 -9 -34 -16 -93
|
||||
-16 -131 0 -44 -7 -92 -20 -128 -23 -66 -25 -108 -9 -186 9 -47 8 -60 -10
|
||||
-102 -26 -60 -26 -101 -1 -196 20 -74 20 -75 0 -145 -13 -48 -19 -102 -19
|
||||
-166 1 -135 -25 -322 -56 -403 -28 -75 -31 -112 -14 -185 13 -54 8 -78 -42
|
||||
-189 -23 -52 -23 -53 -5 -108 l17 -55 -35 -79 c-33 -71 -36 -87 -36 -168 0
|
||||
-122 -47 -273 -85 -273 -21 0 -19 21 14 138 17 57 32 131 36 165 3 34 17 104
|
||||
31 155 23 81 25 99 15 137 -17 67 -14 90 21 158 34 67 36 86 57 467 6 95 14
|
||||
160 25 190 19 49 15 93 -10 121 -15 16 -14 23 10 83 31 78 32 109 6 176 -27
|
||||
70 -25 89 15 146 40 56 46 111 19 189 -23 69 -21 113 9 188 26 62 27 74 21
|
||||
156 -6 80 -5 93 15 124 28 45 27 54 -10 73 -92 47 -184 -42 -338 -326 -161
|
||||
-298 -197 -386 -285 -705 -103 -375 -123 -584 -96 -1025 12 -204 25 -312 52
|
||||
-458 11 -62 18 -116 14 -119 -3 -3 -79 10 -168 30 -175 39 -357 74 -429 83
|
||||
-24 3 -51 7 -58 10 -24 8 -159 18 -241 19 l-75 0 -3 -81 c-4 -127 -9 -124 233
|
||||
-139 55 -3 111 -7 125 -10 14 -2 48 -6 77 -9 28 -3 77 -9 110 -15 32 -6 76
|
||||
-13 98 -16 22 -4 56 -10 75 -15 19 -5 51 -11 70 -14 47 -8 85 -15 148 -30 l32
|
||||
-8 0 -64 c0 -77 13 -110 75 -195 66 -90 81 -119 63 -130 -21 -14 -51 -10 -91
|
||||
11 -44 24 -45 20 -18 -71 45 -149 37 -167 -56 -132 -10 4 5 -19 34 -50 28 -32
|
||||
73 -92 99 -135 138 -222 189 -263 378 -302 107 -22 170 -49 187 -82 13 -23 14
|
||||
-73 3 -120 -5 -25 -3 -28 21 -28 34 0 52 10 136 75 73 56 115 70 181 60 68
|
||||
-11 138 46 104 84 -20 22 -21 18 62 191 35 74 80 183 100 242 l35 107 56 -40
|
||||
c74 -53 164 -142 201 -199 91 -142 192 -648 151 -754 -19 -47 -5 -41 24 11 58
|
||||
105 70 156 69 308 -1 121 -5 152 -27 226 -32 107 -94 250 -142 329 -45 75
|
||||
-166 204 -234 250 -28 19 -51 39 -51 44 0 5 29 99 65 210 35 111 73 235 84
|
||||
276 30 112 75 399 86 550 4 47 9 99 11 115 8 66 4 496 -6 593 -32 335 -162
|
||||
788 -297 1042 -130 243 -283 386 -487 456 -82 28 -82 28 -90 8z"/>
|
||||
<path d="M6300 7695 c-1 -143 -35 -481 -66 -659 -49 -276 -106 -466 -195 -644
|
||||
-56 -112 -56 -113 -35 -127 41 -29 54 -19 102 76 149 298 211 539 229 904 7
|
||||
142 -10 484 -25 499 -7 7 -10 -12 -10 -49z"/>
|
||||
<path d="M4095 7698 c-3 -7 -6 -56 -7 -108 -3 -89 -4 -95 -23 -92 -11 2 -35
|
||||
19 -52 39 -18 20 -34 34 -34 32 -1 -2 -3 -33 -4 -69 -4 -82 -13 -153 -27 -207
|
||||
l-10 -43 -46 40 c-24 22 -48 40 -52 40 -11 0 -24 -221 -17 -309 6 -83 37 -178
|
||||
66 -205 73 -66 245 -71 421 -12 58 19 130 45 160 56 30 12 69 26 86 31 31 9
|
||||
31 10 36 87 3 56 12 94 33 140 15 34 25 65 22 68 -3 4 -18 7 -32 7 -40 0 -35
|
||||
14 50 133 59 81 99 160 91 180 -3 8 -44 13 -123 17 -65 2 -127 7 -138 10 -127
|
||||
33 -204 66 -321 137 -74 45 -73 45 -79 28z"/>
|
||||
<path d="M3450 7544 c-123 -70 -410 -433 -412 -522 -2 -71 84 -177 137 -170
|
||||
19 3 41 40 96 158 9 19 31 61 48 93 22 38 36 81 42 127 10 68 62 184 136 303
|
||||
28 44 16 47 -47 11z"/>
|
||||
<path d="M4972 7109 c-21 -6 -32 -15 -29 -23 3 -6 13 -40 22 -75 17 -60 19
|
||||
-62 46 -56 15 3 45 2 66 -4 90 -23 132 -20 98 9 -33 28 -15 32 98 26 109 -7
|
||||
226 -20 273 -32 22 -6 24 -4 18 27 -4 19 -9 37 -13 41 -7 8 -256 73 -316 82
|
||||
-71 12 -229 14 -263 5z"/>
|
||||
<path d="M4810 7085 c-36 -7 -81 -18 -100 -24 -67 -21 -66 -18 -35 -135 15
|
||||
-58 33 -106 38 -106 14 0 135 68 142 80 3 5 26 12 50 16 25 3 45 8 45 11 0 3
|
||||
-6 31 -14 62 -8 31 -17 68 -20 84 -7 30 -9 31 -106 12z"/>
|
||||
<path d="M7315 6989 c-227 -53 -672 -275 -915 -457 -73 -55 -231 -196 -284
|
||||
-252 l-48 -53 26 -25 25 -25 83 84 c119 121 227 202 478 361 113 72 265 152
|
||||
437 232 216 100 260 123 268 136 7 12 -15 11 -70 -1z"/>
|
||||
<path d="M2974 6954 c-7 -29 1 -307 14 -484 10 -129 25 -174 70 -212 48 -40
|
||||
97 -53 127 -34 13 9 29 25 35 37 20 37 22 160 6 313 -9 82 -16 172 -16 202 l0
|
||||
53 -30 -9 c-17 -5 -33 -9 -35 -10 -32 -5 -130 93 -140 141 -8 36 -23 37 -31 3z"/>
|
||||
<path d="M2484 6858 c-73 -124 -162 -305 -184 -376 -24 -78 -17 -102 47 -142
|
||||
57 -36 90 -39 103 -7 17 41 49 232 59 359 6 69 13 131 16 139 11 30 22 109 15
|
||||
109 -4 0 -29 -37 -56 -82z"/>
|
||||
<path d="M4563 6824 c-64 -39 -317 -95 -423 -94 -96 0 -119 4 -180 25 l-42 15
|
||||
7 -28 c8 -32 34 -67 105 -142 28 -30 77 -84 108 -120 30 -36 89 -100 131 -142
|
||||
l76 -78 -63 0 c-34 0 -62 -3 -62 -7 0 -32 78 -102 230 -208 47 -33 135 -96
|
||||
195 -141 61 -44 120 -86 133 -93 29 -16 50 -4 127 69 33 31 80 70 105 86 25
|
||||
16 47 31 50 35 5 6 110 66 185 106 85 45 76 75 -57 189 -57 48 -208 192 -336
|
||||
318 -127 127 -235 232 -240 233 -4 1 -26 -9 -49 -23z"/>
|
||||
<path d="M5840 6684 c-30 -8 -82 -14 -115 -13 -33 0 -66 0 -73 0 -10 -1 -12
|
||||
-21 -10 -82 3 -75 5 -84 29 -101 49 -37 193 -36 216 1 14 23 29 211 17 210 -5
|
||||
-1 -34 -7 -64 -15z"/>
|
||||
<path d="M3914 6633 c-49 -114 -125 -336 -135 -397 -15 -86 -7 -135 27 -175
|
||||
46 -55 140 -50 163 7 5 15 11 59 13 97 3 56 13 91 51 179 26 60 47 114 47 122
|
||||
0 7 -20 36 -45 64 -25 28 -54 68 -65 90 -28 54 -37 56 -56 13z"/>
|
||||
<path d="M5620 6407 c-12 -34 -36 -88 -53 -120 -16 -31 -28 -60 -25 -62 3 -3
|
||||
41 3 84 13 43 11 112 20 153 21 41 1 78 4 81 6 4 3 10 30 14 60 3 31 13 72 21
|
||||
92 14 33 14 35 -3 28 -9 -4 -51 -9 -92 -11 -57 -2 -85 1 -117 16 l-41 18 -22
|
||||
-61z"/>
|
||||
<path d="M2257 6308 c7 -119 45 -313 89 -448 54 -167 71 -198 116 -202 42 -4
|
||||
98 9 120 29 22 20 19 36 -48 243 -61 190 -84 277 -84 314 0 17 -7 29 -17 32
|
||||
-46 15 -100 41 -134 66 -22 16 -40 28 -42 28 -2 0 -2 -28 0 -62z"/>
|
||||
<path d="M5935 6220 c-31 -44 -212 -222 -264 -259 -54 -38 -24 2 65 89 91 89
|
||||
149 158 141 167 -7 6 -266 -22 -357 -39 -225 -42 -501 -216 -745 -470 -199
|
||||
-206 -250 -272 -266 -349 -32 -150 60 -415 211 -604 152 -190 373 -343 525
|
||||
-361 161 -19 456 6 511 43 23 16 231 377 296 513 36 76 104 281 153 464 35
|
||||
128 30 177 -34 351 -69 185 -71 193 -71 248 0 44 -2 48 -16 36 -24 -20 -69
|
||||
-114 -108 -229 -43 -124 -64 -154 -35 -49 33 121 103 284 150 351 11 14 19 30
|
||||
19 35 0 5 -25 24 -55 42 -30 18 -59 39 -66 47 -17 21 -24 18 -54 -26z"/>
|
||||
<path d="M6446 6230 c-44 -16 -83 -35 -88 -42 -9 -14 -2 -158 11 -204 12 -45
|
||||
34 -52 78 -23 21 14 50 33 66 43 l27 17 -1 119 c0 66 -4 120 -7 119 -4 0 -43
|
||||
-13 -86 -29z"/>
|
||||
<path d="M3010 6219 c0 -59 289 -869 310 -869 4 0 14 24 20 53 25 105 54 140
|
||||
113 133 20 -2 37 -3 37 -1 0 1 -24 70 -54 152 -30 83 -83 241 -119 351 l-63
|
||||
200 -28 -34 c-23 -30 -33 -34 -72 -34 -53 0 -108 23 -129 54 -15 20 -15 20
|
||||
-15 -5z"/>
|
||||
<path d="M6244 6124 c-99 -79 -102 -86 -80 -181 10 -43 30 -114 43 -157 25
|
||||
-80 25 -80 38 -50 7 16 36 60 64 99 42 56 51 74 46 95 -4 14 -12 73 -19 133
|
||||
-7 59 -15 109 -17 112 -3 2 -36 -20 -75 -51z"/>
|
||||
<path d="M3980 6022 c0 -6 -14 -19 -32 -28 -32 -16 -118 -16 -161 1 -12 5 12
|
||||
-54 79 -191 53 -110 143 -298 201 -419 57 -121 111 -227 119 -235 13 -13 18
|
||||
-11 41 20 15 19 50 59 80 88 29 29 53 57 53 63 0 6 -63 122 -139 257 -77 136
|
||||
-162 294 -190 351 -28 57 -51 99 -51 93z"/>
|
||||
<path d="M6710 5911 c0 -5 20 -53 44 -107 73 -168 156 -442 156 -520 0 -45 4
|
||||
-55 27 -74 30 -23 30 -23 118 14 33 13 76 26 95 28 31 4 35 8 38 38 6 62 -52
|
||||
170 -92 170 -12 0 -14 -5 -6 -25 10 -25 4 -55 -10 -55 -4 0 -17 30 -29 68 -34
|
||||
105 -161 299 -270 415 -51 54 -71 68 -71 48z"/>
|
||||
<path d="M2571 5645 c-46 -37 -105 -47 -129 -23 -24 24 -11 -5 45 -100 127
|
||||
-218 184 -296 292 -402 58 -58 138 -126 176 -152 l70 -46 5 53 c5 56 23 85 72
|
||||
115 l28 17 -55 37 c-69 48 -253 262 -405 473 -25 34 -47 63 -50 63 -3 0 -25
|
||||
-16 -49 -35z"/>
|
||||
<path d="M3451 5493 c-38 -19 -78 -74 -87 -121 -6 -27 0 -40 35 -86 81 -105
|
||||
479 -476 511 -476 6 0 122 145 128 159 7 19 -301 349 -441 473 -42 37 -85 68
|
||||
-95 68 -9 0 -32 -8 -51 -17z"/>
|
||||
<path d="M6261 5478 c-5 -18 -26 -95 -46 -172 -43 -164 -75 -256 -122 -350
|
||||
-19 -37 -33 -69 -31 -70 87 -59 168 -125 365 -303 50 -44 100 -86 112 -92 18
|
||||
-10 21 -9 21 9 0 11 -9 38 -20 60 -35 69 -24 89 27 46 15 -13 96 -70 178 -127
|
||||
152 -105 292 -181 354 -193 l33 -6 -16 25 c-76 115 -126 250 -166 440 -12 61
|
||||
-31 146 -41 190 l-18 80 -88 83 c-47 46 -159 137 -247 203 -89 65 -187 139
|
||||
-218 164 -32 25 -59 45 -62 45 -2 0 -9 -15 -15 -32z"/>
|
||||
<path d="M4348 5238 c-144 -142 -397 -435 -508 -588 -30 -41 -57 -77 -61 -80
|
||||
-3 -3 -47 -66 -96 -140 -171 -258 -272 -462 -317 -640 -15 -63 -15 -69 5 -140
|
||||
25 -90 96 -235 155 -315 75 -101 203 -224 329 -313 233 -166 540 -300 669
|
||||
-293 26 1 149 124 355 352 192 213 185 205 287 334 298 379 438 604 535 862
|
||||
22 57 39 106 39 108 0 3 -10 0 -22 -6 -59 -31 -124 -39 -318 -35 -214 4 -266
|
||||
13 -379 68 -226 110 -420 341 -526 628 -25 66 -28 90 -29 192 -1 65 -2 118 -2
|
||||
118 -1 0 -53 -51 -116 -112z"/>
|
||||
<path d="M7119 5202 c-40 -20 -88 -35 -123 -39 l-58 -6 6 -30 c3 -16 6 -54 6
|
||||
-83 0 -70 22 -84 90 -59 25 9 71 25 103 34 32 9 62 23 66 29 8 13 -4 139 -17
|
||||
169 -6 17 -12 16 -73 -15z"/>
|
||||
<path d="M7547 5062 c-107 -101 -138 -112 -183 -67 -19 19 -25 35 -24 63 1 20
|
||||
2 41 1 45 -1 5 -19 -14 -41 -42 -25 -31 -60 -61 -91 -77 -55 -27 -60 -29 -159
|
||||
-49 -63 -13 -90 -32 -90 -64 0 -75 49 -233 138 -441 53 -125 57 -130 106 -130
|
||||
22 0 68 7 101 14 33 8 105 17 160 20 55 4 108 9 118 12 16 5 16 7 -3 31 -13
|
||||
17 -17 30 -11 36 5 5 76 16 158 24 81 7 159 16 173 19 l25 6 -22 18 c-13 10
|
||||
-23 22 -23 27 0 4 63 8 139 8 124 0 181 9 143 22 -31 11 -107 70 -110 84 -2
|
||||
10 16 31 46 53 28 20 53 43 56 51 17 46 -45 76 -176 85 -80 6 -105 12 -145 35
|
||||
-71 41 -115 93 -163 189 -23 47 -46 86 -52 86 -5 0 -37 -26 -71 -58z"/>
|
||||
<path d="M3110 5050 c-30 -32 -60 -89 -60 -113 0 -30 218 -189 335 -245 28
|
||||
-13 70 -34 95 -45 25 -12 83 -46 129 -76 l83 -54 19 24 c38 49 99 140 99 148
|
||||
0 7 -271 201 -294 210 -6 2 -38 23 -71 47 -94 65 -234 134 -273 134 -25 0 -41
|
||||
-8 -62 -30z"/>
|
||||
<path d="M6768 4376 c-23 -32 -125 -100 -168 -114 -19 -5 -61 -13 -94 -16 -32
|
||||
-4 -66 -11 -77 -16 -18 -10 -17 -11 3 -45 16 -29 19 -47 16 -93 -3 -31 -9 -65
|
||||
-13 -76 -8 -18 -4 -18 61 -11 90 9 200 47 272 92 31 20 91 69 132 111 69 68
|
||||
73 75 56 86 -11 6 -48 30 -84 54 -82 56 -84 56 -104 28z"/>
|
||||
<path d="M7617 4137 l-168 -258 19 -42 c29 -64 55 -82 118 -81 64 0 107 30
|
||||
167 114 45 64 47 78 8 51 -59 -42 -69 -12 -20 62 37 57 51 147 47 306 l-3 105
|
||||
-168 -257z"/>
|
||||
<path d="M5727 4246 c-18 -28 -87 -175 -87 -186 0 -10 16 -15 90 -29 142 -26
|
||||
251 -34 442 -35 240 -1 239 -2 245 86 5 62 -18 117 -57 135 -14 6 -63 14 -110
|
||||
18 -47 4 -98 9 -115 10 -39 4 -264 17 -337 20 -49 2 -60 -1 -71 -19z"/>
|
||||
<path d="M7380 3778 c-132 -121 -284 -208 -435 -248 -44 -12 -84 -26 -89 -31
|
||||
-12 -11 56 -94 103 -124 51 -34 83 -37 155 -15 61 19 75 27 181 101 28 19 86
|
||||
56 130 82 57 34 95 66 133 112 28 35 52 66 52 69 0 4 -18 3 -39 0 -51 -8 -98
|
||||
18 -120 68 l-16 36 -55 -50z"/>
|
||||
<path d="M5472 3742 c-73 -100 -95 -133 -91 -136 11 -10 139 -84 169 -98 83
|
||||
-39 250 -94 340 -113 8 -2 54 -12 101 -24 99 -24 102 -24 94 -11 -3 5 5 31 19
|
||||
57 23 45 32 58 91 124 l23 27 -96 36 c-272 104 -445 166 -472 171 -16 3 -40 9
|
||||
-54 15 -15 5 -26 7 -26 5 0 -2 -11 0 -24 5 -23 8 -29 3 -74 -58z"/>
|
||||
<path d="M3247 3598 c-86 -146 -197 -363 -197 -384 0 -31 93 -210 170 -329
|
||||
111 -171 289 -335 485 -450 61 -36 171 -90 205 -100 14 -4 30 -12 37 -17 6 -5
|
||||
34 -12 60 -15 l49 -5 175 150 c194 167 261 232 239 233 -16 1 -36 4 -90 15
|
||||
-19 4 -40 8 -47 10 -15 3 -103 30 -168 51 -27 9 -66 26 -85 37 -38 22 -192
|
||||
127 -200 136 -3 3 -36 31 -75 62 -150 119 -296 276 -368 395 -21 34 -59 116
|
||||
-86 183 l-48 122 -56 -94z"/>
|
||||
<path d="M6223 3515 c-36 -15 -91 -77 -99 -112 -9 -34 5 -72 36 -94 79 -58
|
||||
450 -92 655 -60 86 13 205 49 205 63 0 5 -4 7 -8 4 -5 -3 -31 5 -58 18 -57 26
|
||||
-130 103 -139 147 -4 16 -10 27 -14 25 -26 -16 -282 -25 -341 -12 -140 31
|
||||
-200 36 -237 21z"/>
|
||||
<path d="M5280 3493 c0 -4 -13 -24 -29 -43 -124 -152 -122 -149 -103 -167 10
|
||||
-10 60 -58 112 -107 52 -50 145 -138 205 -196 169 -162 200 -185 252 -185 39
|
||||
0 49 5 79 37 19 22 34 48 34 62 0 55 -269 393 -388 487 -94 75 -162 122 -162
|
||||
112z"/>
|
||||
<path d="M7664 3103 c-32 -43 -63 -83 -68 -89 -6 -6 -29 -31 -51 -55 -63 -68
|
||||
-155 -144 -205 -169 -113 -56 -129 -89 -73 -151 19 -21 51 -48 70 -60 l34 -21
|
||||
22 20 c12 11 39 25 59 31 32 11 108 79 108 98 0 4 -5 2 -12 -5 -18 -18 -38
|
||||
-15 -38 6 0 10 34 69 75 132 74 113 127 229 138 303 3 20 4 37 2 37 -2 0 -30
|
||||
-35 -61 -77z"/>
|
||||
<path d="M2978 3107 c-21 -34 -53 -87 -73 -116 -66 -103 -66 -103 10 -253 37
|
||||
-73 93 -169 124 -212 69 -96 201 -232 279 -284 125 -86 361 -182 444 -182 15
|
||||
0 47 16 75 36 53 40 173 154 173 166 0 3 -26 12 -57 18 -159 33 -220 63 -387
|
||||
189 -235 178 -341 305 -483 576 -35 69 -65 125 -66 125 -1 0 -19 -28 -39 -63z"/>
|
||||
<path d="M5845 2923 c28 -66 6 -126 -53 -142 -20 -6 -43 -11 -51 -11 -25 0 7
|
||||
-27 125 -106 208 -138 505 -293 577 -301 57 -7 65 7 28 50 -35 39 -53 80 -45
|
||||
101 3 8 21 20 41 27 20 6 39 21 45 35 13 28 4 42 -32 49 -44 8 -95 28 -145 55
|
||||
-90 49 -178 96 -320 170 -77 40 -144 77 -149 81 -17 15 -29 10 -21 -8z"/>
|
||||
<path d="M2810 2819 c0 -6 -41 -92 -91 -192 l-90 -182 80 -160 c44 -88 100
|
||||
-186 125 -217 98 -123 325 -272 503 -329 71 -22 72 -22 115 -4 28 13 87 63
|
||||
172 149 l130 130 -55 11 c-174 36 -347 131 -537 295 -107 92 -167 175 -260
|
||||
357 -70 139 -92 173 -92 142z"/>
|
||||
<path d="M7160 2680 c-33 -16 -114 -36 -305 -74 -88 -18 -270 -25 -297 -12
|
||||
-15 7 -18 3 -18 -20 0 -39 -12 -56 -51 -68 -18 -6 -34 -17 -37 -24 -6 -18 66
|
||||
-83 122 -111 55 -27 122 -24 310 13 321 64 429 96 464 139 11 12 7 16 -20 21
|
||||
-57 12 -118 81 -118 136 0 23 -4 24 -50 0z"/>
|
||||
<path d="M2517 2261 c-49 -79 -87 -151 -87 -165 0 -136 113 -336 251 -444 112
|
||||
-88 271 -171 355 -186 16 -3 40 -8 54 -11 106 -21 112 -20 169 31 29 26 75 80
|
||||
103 118 l50 71 -29 3 c-128 12 -337 119 -478 245 -87 78 -147 164 -231 334
|
||||
l-70 144 -87 -140z"/>
|
||||
<path d="M1575 2228 c-31 -18 -52 -48 -91 -126 -55 -113 -65 -150 -71 -254
|
||||
-10 -168 27 -325 92 -394 30 -31 38 -35 65 -29 71 16 287 113 626 281 l251
|
||||
125 -24 87 c-59 212 -46 189 -106 201 -28 6 -65 13 -82 16 -28 5 -46 8 -105
|
||||
20 -14 2 -41 7 -60 10 -19 3 -71 12 -115 20 -44 8 -96 17 -115 20 -19 3 -46 7
|
||||
-60 10 -117 21 -185 25 -205 13z"/>
|
||||
<path d="M2365 1759 c-44 -21 -168 -81 -275 -133 -240 -116 -441 -226 -497
|
||||
-272 l-42 -34 15 -53 c21 -69 86 -190 145 -269 96 -128 273 -278 329 -278 27
|
||||
0 34 8 165 175 52 66 277 383 375 529 l91 136 -102 120 c-56 66 -107 120 -113
|
||||
119 -6 0 -47 -18 -91 -40z"/>
|
||||
<path d="M2635 1413 c-44 -60 -136 -187 -205 -283 -69 -96 -147 -204 -172
|
||||
-240 -26 -36 -58 -79 -71 -95 -14 -17 -35 -47 -47 -68 -31 -53 -16 -84 71
|
||||
-147 96 -70 184 -113 294 -145 309 -90 341 -79 360 120 7 71 65 426 111 673
|
||||
13 73 24 140 24 148 0 8 -24 25 -52 38 -29 13 -92 42 -140 65 -48 22 -88 41
|
||||
-90 41 -2 0 -39 -48 -83 -107z"/>
|
||||
<path d="M3130 1390 c-14 -4 -39 -8 -57 -9 -17 -1 -34 -5 -37 -10 -22 -36
|
||||
-127 -689 -133 -833 -7 -162 -9 -160 106 -157 47 2 100 6 116 10 17 4 44 10
|
||||
60 12 86 13 298 79 368 114 66 33 74 54 47 134 -29 88 -159 348 -283 564 -95
|
||||
166 -105 180 -133 182 -16 1 -40 -2 -54 -7z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 16 KiB |
@@ -6,65 +6,43 @@ import {
|
||||
Image,
|
||||
Button,
|
||||
Select,
|
||||
AspectRatio,
|
||||
Text,
|
||||
Card,
|
||||
LoadingOverlay,
|
||||
ActionIcon,
|
||||
Tooltip,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useState } from 'react';
|
||||
import { Apps } from 'tabler-icons-react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { ServiceTypeList } from '../../tools/types';
|
||||
import { AppShelfItemWrapper } from './AppShelfItemWrapper';
|
||||
|
||||
export default function AddItemShelfItem(props: any) {
|
||||
export function AddItemShelfButton(props: any) {
|
||||
const [opened, setOpened] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
size="xl"
|
||||
radius="lg"
|
||||
radius="md"
|
||||
title={<Title order={3}>Add service</Title>}
|
||||
opened={props.opened || opened}
|
||||
onClose={() => setOpened(false)}
|
||||
title="Add a service"
|
||||
>
|
||||
<AddAppShelfItemForm setOpened={setOpened} />
|
||||
</Modal>
|
||||
<AppShelfItemWrapper>
|
||||
<Card.Section>
|
||||
<Group position="center" mx="lg">
|
||||
<Text
|
||||
// TODO: #1 Remove this hack to get the text to be centered.
|
||||
ml={15}
|
||||
style={{
|
||||
alignSelf: 'center',
|
||||
alignContent: 'center',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
justifyItems: 'center',
|
||||
}}
|
||||
mt="sm"
|
||||
weight={500}
|
||||
>
|
||||
Add a service
|
||||
</Text>
|
||||
</Group>
|
||||
</Card.Section>
|
||||
<Card.Section>
|
||||
<AspectRatio ratio={5 / 3} m="xl">
|
||||
<motion.i
|
||||
whileHover={{
|
||||
cursor: 'pointer',
|
||||
scale: 1.1,
|
||||
}}
|
||||
>
|
||||
<Apps style={{ cursor: 'pointer' }} onClick={() => setOpened(true)} size={60} />
|
||||
</motion.i>
|
||||
</AspectRatio>
|
||||
</Card.Section>
|
||||
</AppShelfItemWrapper>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
radius="md"
|
||||
size="xl"
|
||||
color="blue"
|
||||
style={props.style}
|
||||
onClick={() => setOpened(true)}
|
||||
>
|
||||
<Tooltip label="Add a service">
|
||||
<Apps />
|
||||
</Tooltip>
|
||||
</ActionIcon>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -74,15 +52,11 @@ function MatchIcon(name: string, form: any) {
|
||||
`https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/${name
|
||||
.replace(/\s+/g, '-')
|
||||
.toLowerCase()}.png`
|
||||
)
|
||||
.then((res) => {
|
||||
if (res.status === 200) {
|
||||
form.setFieldValue('icon', res.url);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Do nothing
|
||||
});
|
||||
).then((res) => {
|
||||
if (res.ok) {
|
||||
form.setFieldValue('icon', res.url);
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -94,9 +68,10 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
id: props.id ?? uuidv4(),
|
||||
type: props.type ?? 'Other',
|
||||
name: props.name ?? '',
|
||||
icon: props.icon ?? '',
|
||||
icon: props.icon ?? '/favicon.svg',
|
||||
url: props.url ?? '',
|
||||
apiKey: props.apiKey ?? (undefined as unknown as string),
|
||||
},
|
||||
@@ -104,15 +79,18 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
||||
apiKey: () => null,
|
||||
// Validate icon with a regex
|
||||
icon: (value: string) => {
|
||||
if (!value.match(/^https?:\/\/.+\.(png|jpg|jpeg|gif)$/)) {
|
||||
// Regex to match everything that ends with and icon extension
|
||||
if (!value.match(/\.(png|jpg|jpeg|gif|svg)$/)) {
|
||||
return 'Please enter a valid icon URL';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
// Validate url with a regex http/https
|
||||
url: (value: string) => {
|
||||
if (!value.match(/^https?:\/\/.+\/$/)) {
|
||||
return 'Please enter a valid URL (that ends with a /)';
|
||||
try {
|
||||
const _isValid = new URL(value);
|
||||
} catch (e) {
|
||||
return 'Please enter a valid URL';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
@@ -122,16 +100,24 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
||||
return (
|
||||
<>
|
||||
<Center>
|
||||
<Image height={120} width={120} src={form.values.icon} alt="Placeholder" withPlaceholder />
|
||||
<Image
|
||||
height={120}
|
||||
width={120}
|
||||
fit="contain"
|
||||
src={form.values.icon}
|
||||
alt="Placeholder"
|
||||
withPlaceholder
|
||||
/>
|
||||
</Center>
|
||||
<form
|
||||
onSubmit={form.onSubmit(() => {
|
||||
// If service already exists, update it.
|
||||
if (config.services && config.services.find((s) => s.name === form.values.name)) {
|
||||
if (config.services && config.services.find((s) => s.id === form.values.id)) {
|
||||
setConfig({
|
||||
...config,
|
||||
// replace the found item by matching ID
|
||||
services: config.services.map((s) => {
|
||||
if (s.name === form.values.name) {
|
||||
if (s.id === form.values.id) {
|
||||
return {
|
||||
...form.values,
|
||||
};
|
||||
@@ -174,7 +160,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
||||
<TextInput
|
||||
required
|
||||
label="Service url"
|
||||
placeholder="http://localhost:8989"
|
||||
placeholder="http://localhost:7575"
|
||||
{...form.getInputProps('url')}
|
||||
/>
|
||||
<Select
|
||||
@@ -1,4 +1,6 @@
|
||||
import AppShelf, { AppShelfItem } from './AppShelf';
|
||||
import { SimpleGrid } from '@mantine/core';
|
||||
import AppShelf from './AppShelf';
|
||||
import { AppShelfItem } from './AppShelfItem';
|
||||
|
||||
export default {
|
||||
title: 'Item Shelf',
|
||||
@@ -16,3 +18,10 @@ export default {
|
||||
|
||||
export const Default = (args: any) => <AppShelf {...args} />;
|
||||
export const One = (args: any) => <AppShelfItem {...args} />;
|
||||
export const Ten = (args: any) => (
|
||||
<SimpleGrid>
|
||||
{Array.from(Array(10)).map((_, i) => (
|
||||
<AppShelfItem {...args} key={i} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
);
|
||||
79
src/components/AppShelf/AppShelf.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Grid } from '@mantine/core';
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
MouseSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import { arrayMove, SortableContext } from '@dnd-kit/sortable';
|
||||
import { useConfig } from '../../tools/state';
|
||||
|
||||
import { SortableAppShelfItem, AppShelfItem } from './AppShelfItem';
|
||||
|
||||
const AppShelf = (props: any) => {
|
||||
const [activeId, setActiveId] = useState(null);
|
||||
const { config, setConfig } = useConfig();
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor, {
|
||||
// Require the mouse to move by 10 pixels before activating
|
||||
activationConstraint: {
|
||||
delay: 250,
|
||||
tolerance: 5,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
function handleDragStart(event: any) {
|
||||
const { active } = event;
|
||||
|
||||
setActiveId(active.id);
|
||||
}
|
||||
|
||||
function handleDragEnd(event: any) {
|
||||
const { active, over } = event;
|
||||
|
||||
if (active.id !== over.id) {
|
||||
const newConfig = { ...config };
|
||||
const activeIndex = newConfig.services.findIndex((e) => e.id === active.id);
|
||||
const overIndex = newConfig.services.findIndex((e) => e.id === over.id);
|
||||
newConfig.services = arrayMove(newConfig.services, activeIndex, overIndex);
|
||||
setConfig(newConfig);
|
||||
}
|
||||
|
||||
setActiveId(null);
|
||||
}
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext items={config.services}>
|
||||
<Grid gutter="xl" align="center">
|
||||
{config.services.map((service) => (
|
||||
<Grid.Col key={service.id} span={6} xl={2} xs={4} sm={3} md={3}>
|
||||
<SortableAppShelfItem service={service} key={service.id} id={service.id} />
|
||||
</Grid.Col>
|
||||
))}
|
||||
</Grid>
|
||||
</SortableContext>
|
||||
<DragOverlay
|
||||
style={{
|
||||
// Add a shadow to the drag overlay
|
||||
boxShadow: '0 0 10px rgba(0, 0, 0, 0.5)',
|
||||
}}
|
||||
>
|
||||
{activeId ? (
|
||||
<AppShelfItem service={config.services.find((e) => e.id === activeId)} id={activeId} />
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppShelf;
|
||||
123
src/components/AppShelf/AppShelfItem.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import {
|
||||
Text,
|
||||
Card,
|
||||
Anchor,
|
||||
AspectRatio,
|
||||
Image,
|
||||
Center,
|
||||
createStyles,
|
||||
} from '@mantine/core';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useState } from 'react';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { serviceItem } from '../../tools/types';
|
||||
import PingComponent from '../modules/ping/PingModule';
|
||||
import AppShelfMenu from './AppShelfMenu';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
item: {
|
||||
transition: 'box-shadow 150ms ease, transform 100ms ease',
|
||||
|
||||
'&:hover': {
|
||||
boxShadow: `${theme.shadows.md} !important`,
|
||||
transform: 'scale(1.05)',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export function SortableAppShelfItem(props: any) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
|
||||
id: props.id,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
|
||||
<AppShelfItem service={props.service} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AppShelfItem(props: any) {
|
||||
const { service }: { service: serviceItem } = props;
|
||||
const [hovering, setHovering] = useState(false);
|
||||
const { classes, theme } = useStyles();
|
||||
return (
|
||||
<motion.div
|
||||
animate={{
|
||||
scale: [0.9, 1.06, 1],
|
||||
rotate: [0, 5, 0],
|
||||
}}
|
||||
transition={{ duration: 0.6, type: 'spring', damping: 10, mass: 0.75, stiffness: 100 }}
|
||||
key={service.name}
|
||||
onHoverStart={() => {
|
||||
setHovering(true);
|
||||
}}
|
||||
onHoverEnd={() => {
|
||||
setHovering(false);
|
||||
}}
|
||||
>
|
||||
<Card withBorder radius="lg" shadow="md" className={classes.item}>
|
||||
<Card.Section>
|
||||
<Anchor
|
||||
target="_blank"
|
||||
href={service.url}
|
||||
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}
|
||||
>
|
||||
<Text mt="sm" align="center" lineClamp={1} weight={550}>
|
||||
{service.name}
|
||||
</Text>
|
||||
</Anchor>
|
||||
<motion.div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 15,
|
||||
right: 15,
|
||||
alignSelf: 'flex-end',
|
||||
}}
|
||||
animate={{
|
||||
opacity: hovering ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<AppShelfMenu service={service} />
|
||||
</motion.div>
|
||||
</Card.Section>
|
||||
<Center>
|
||||
<Card.Section>
|
||||
<AspectRatio
|
||||
ratio={3 / 5}
|
||||
m="xl"
|
||||
style={{
|
||||
width: 150,
|
||||
height: 90,
|
||||
}}
|
||||
>
|
||||
<motion.i
|
||||
whileHover={{
|
||||
cursor: 'pointer',
|
||||
scale: 1.1,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
width={80}
|
||||
height={80}
|
||||
src={service.icon}
|
||||
fit="contain"
|
||||
onClick={() => {
|
||||
window.open(service.url);
|
||||
}}
|
||||
/>
|
||||
</motion.i>
|
||||
</AspectRatio>
|
||||
<PingComponent url={service.url} />
|
||||
</Card.Section>
|
||||
</Center>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Menu, Modal, Text } from '@mantine/core';
|
||||
import { Menu, Modal, Text, useMantineTheme } from '@mantine/core';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { useState } from 'react';
|
||||
import { Check, Edit, Trash } from 'tabler-icons-react';
|
||||
@@ -8,12 +8,13 @@ import { AddAppShelfItemForm } from './AddAppShelfItem';
|
||||
export default function AppShelfMenu(props: any) {
|
||||
const { service } = props;
|
||||
const { config, setConfig } = useConfig();
|
||||
const theme = useMantineTheme();
|
||||
const [opened, setOpened] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
size="xl"
|
||||
radius="lg"
|
||||
radius="md"
|
||||
opened={props.opened || opened}
|
||||
onClose={() => setOpened(false)}
|
||||
title="Modify a service"
|
||||
@@ -21,6 +22,7 @@ export default function AppShelfMenu(props: any) {
|
||||
<AddAppShelfItemForm
|
||||
setOpened={setOpened}
|
||||
name={service.name}
|
||||
id={service.id}
|
||||
type={service.type}
|
||||
url={service.url}
|
||||
icon={service.icon}
|
||||
@@ -28,7 +30,16 @@ export default function AppShelfMenu(props: any) {
|
||||
message="Save service"
|
||||
/>
|
||||
</Modal>
|
||||
<Menu position="right">
|
||||
<Menu
|
||||
position="right"
|
||||
radius="md"
|
||||
styles={{
|
||||
body: {
|
||||
backgroundColor:
|
||||
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Menu.Label>Settings</Menu.Label>
|
||||
<Menu.Item
|
||||
color="primary"
|
||||
@@ -44,7 +55,7 @@ export default function AppShelfMenu(props: any) {
|
||||
onClick={(e: any) => {
|
||||
setConfig({
|
||||
...config,
|
||||
services: config.services.filter((s) => s.name !== service.name),
|
||||
services: config.services.filter((s) => s.id !== service.id),
|
||||
});
|
||||
showNotification({
|
||||
autoClose: 5000,
|
||||
2
src/components/AppShelf/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as AppShelf } from './AppShelf';
|
||||
export * from './AppShelfItem';
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { createStyles, Switch, Group, useMantineColorScheme } from '@mantine/core';
|
||||
import { createStyles, Switch, Group, useMantineColorScheme, Kbd } from '@mantine/core';
|
||||
import { Sun, MoonStars } from 'tabler-icons-react';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
@@ -40,6 +40,9 @@ export function ColorSchemeSwitch() {
|
||||
<Switch checked={colorScheme === 'dark'} onChange={() => toggleColorScheme()} size="md" />
|
||||
</div>
|
||||
Switch to {colorScheme === 'dark' ? 'light' : 'dark'} mode
|
||||
<Group spacing={2}>
|
||||
<Kbd>Ctrl</Kbd>+<Kbd>J</Kbd>
|
||||
</Group>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { useRouter } from 'next/router';
|
||||
import { setCookies } from 'cookies-next';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { Config } from '../../tools/types';
|
||||
import { migrateToIdConfig } from '../../tools/migrate';
|
||||
|
||||
function getIconColor(status: DropzoneStatus, theme: MantineTheme) {
|
||||
return status.accepted
|
||||
@@ -84,7 +85,8 @@ export default function LoadConfigComponent(props: any) {
|
||||
message: undefined,
|
||||
});
|
||||
setCookies('config-name', newConfig.name, { maxAge: 60 * 60 * 24 * 30 });
|
||||
setConfig(newConfig);
|
||||
const migratedConfig = migrateToIdConfig(newConfig);
|
||||
setConfig(migratedConfig);
|
||||
});
|
||||
}}
|
||||
accept={['application/json']}
|
||||
32
src/components/Settings/ModuleEnabler.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Group, Switch } from '@mantine/core';
|
||||
import * as Modules from '../modules';
|
||||
import { useConfig } from '../../tools/state';
|
||||
|
||||
export default function ModuleEnabler(props: any) {
|
||||
const { config, setConfig } = useConfig();
|
||||
const modules = Object.values(Modules).map((module) => module);
|
||||
return (
|
||||
<Group direction="column">
|
||||
{modules.map((module) => (
|
||||
<Switch
|
||||
key={module.title}
|
||||
size="md"
|
||||
checked={config.modules?.[module.title]?.enabled ?? false}
|
||||
label={`Enable ${module.title} module`}
|
||||
onChange={(e) => {
|
||||
setConfig({
|
||||
...config,
|
||||
modules: {
|
||||
...config.modules,
|
||||
[module.title]: {
|
||||
...config.modules?.[module.title],
|
||||
enabled: e.currentTarget.checked,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,15 @@ import {
|
||||
ActionIcon,
|
||||
Group,
|
||||
Modal,
|
||||
Switch,
|
||||
Title,
|
||||
Text,
|
||||
Tooltip,
|
||||
SegmentedControl,
|
||||
Indicator,
|
||||
Alert,
|
||||
TextInput,
|
||||
} from '@mantine/core';
|
||||
import { useColorScheme } from '@mantine/hooks';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AlertCircle, Settings as SettingsIcon } from 'tabler-icons-react';
|
||||
import { CURRENT_VERSION, REPO_URL } from '../../data/constants';
|
||||
import { useState } from 'react';
|
||||
import { Settings as SettingsIcon } from 'tabler-icons-react';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { ColorSchemeSwitch } from '../ColorSchemeToggle/ColorSchemeSwitch';
|
||||
import ConfigChanger from '../Config/ConfigChanger';
|
||||
@@ -24,58 +21,62 @@ function SettingsMenu(props: any) {
|
||||
const { config, setConfig } = useConfig();
|
||||
const colorScheme = useColorScheme();
|
||||
const { current, latest } = props;
|
||||
|
||||
const matches = [
|
||||
{ label: 'Google', value: 'https://google.com/search?q=' },
|
||||
{ label: 'DuckDuckGo', value: 'https://duckduckgo.com/?q=' },
|
||||
{ label: 'Bing', value: 'https://bing.com/search?q=' },
|
||||
{ label: 'Custom', value: 'Custom' },
|
||||
];
|
||||
|
||||
const [customSearchUrl, setCustomSearchUrl] = useState(config.settings.searchUrl);
|
||||
const [searchUrl, setSearchUrl] = useState(
|
||||
matches.find((match) => match.value === config.settings.searchUrl)?.value ?? 'Custom'
|
||||
);
|
||||
|
||||
return (
|
||||
<Group direction="column" grow>
|
||||
<Alert
|
||||
icon={<AlertCircle size={16} />}
|
||||
title="Update available"
|
||||
radius="lg"
|
||||
hidden={current === latest}
|
||||
>
|
||||
Version {latest} is available. Current : {current}
|
||||
</Alert>
|
||||
<Group>
|
||||
<Group grow direction="column" spacing={0}>
|
||||
<Text>Search engine</Text>
|
||||
<SegmentedControl
|
||||
fullWidth
|
||||
title="Search engine"
|
||||
value={
|
||||
// Match config.settings.searchUrl with a key in the matches array
|
||||
matches.find((match) => match.value === config.settings.searchUrl)?.value ?? 'Google'
|
||||
searchUrl
|
||||
}
|
||||
onChange={
|
||||
// Set config.settings.searchUrl to the value of the selected item
|
||||
(e) =>
|
||||
(e) => {
|
||||
setSearchUrl(e);
|
||||
setConfig({
|
||||
...config,
|
||||
settings: {
|
||||
...config.settings,
|
||||
searchUrl: e,
|
||||
},
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
data={matches}
|
||||
/>
|
||||
<Text>Search engine</Text>
|
||||
</Group>
|
||||
<Group direction="column">
|
||||
<Switch
|
||||
size="md"
|
||||
onChange={(e) =>
|
||||
setConfig({
|
||||
...config,
|
||||
settings: {
|
||||
...config.settings,
|
||||
searchBar: e.currentTarget.checked,
|
||||
},
|
||||
})
|
||||
}
|
||||
checked={config.settings.searchBar}
|
||||
label="Enable search bar"
|
||||
/>
|
||||
{searchUrl === 'Custom' && (
|
||||
<TextInput
|
||||
label="Query URL"
|
||||
placeholder="Custom query url"
|
||||
value={customSearchUrl}
|
||||
onChange={(event) => {
|
||||
setCustomSearchUrl(event.currentTarget.value);
|
||||
setConfig({
|
||||
...config,
|
||||
settings: {
|
||||
...config.settings,
|
||||
searchUrl: event.currentTarget.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
<ModuleEnabler />
|
||||
<ColorSchemeSwitch />
|
||||
@@ -96,47 +97,28 @@ function SettingsMenu(props: any) {
|
||||
}
|
||||
|
||||
export function SettingsMenuButton(props: any) {
|
||||
const [update, setUpdate] = useState(false);
|
||||
const [opened, setOpened] = useState(false);
|
||||
const [latestVersion, setLatestVersion] = useState(CURRENT_VERSION);
|
||||
useEffect(() => {
|
||||
// Fetch Data here when component first mounted
|
||||
fetch(`https://api.github.com/repos/${REPO_URL}/releases/latest`).then((res) => {
|
||||
res.json().then((data) => {
|
||||
setLatestVersion(data.tag_name);
|
||||
if (data.tag_name !== CURRENT_VERSION) {
|
||||
setUpdate(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
size="md"
|
||||
size="xl"
|
||||
radius="md"
|
||||
title={<Title order={3}>Settings</Title>}
|
||||
opened={props.opened || opened}
|
||||
onClose={() => setOpened(false)}
|
||||
>
|
||||
<SettingsMenu current={CURRENT_VERSION} latest={latestVersion} />
|
||||
<SettingsMenu />
|
||||
</Modal>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
radius="xl"
|
||||
radius="md"
|
||||
size="xl"
|
||||
color="blue"
|
||||
style={props.style}
|
||||
onClick={() => setOpened(true)}
|
||||
>
|
||||
<Tooltip label="Settings">
|
||||
<Indicator
|
||||
size={12}
|
||||
disabled={CURRENT_VERSION === latestVersion}
|
||||
offset={-3}
|
||||
position="top-end"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</Indicator>
|
||||
<SettingsIcon />
|
||||
</Tooltip>
|
||||
</ActionIcon>
|
||||
</>
|
||||
24
src/components/layout/Aside.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Aside as MantineAside, Group } from '@mantine/core';
|
||||
import { WeatherModule, DateModule, CalendarModule } from '../modules';
|
||||
import { ModuleWrapper } from '../modules/moduleWrapper';
|
||||
|
||||
export default function Aside(props: any) {
|
||||
return (
|
||||
<MantineAside
|
||||
hiddenBreakpoint="md"
|
||||
hidden
|
||||
style={{
|
||||
border: 'none',
|
||||
}}
|
||||
width={{
|
||||
base: 'auto',
|
||||
}}
|
||||
>
|
||||
<Group mt="sm" grow direction="column">
|
||||
<ModuleWrapper module={CalendarModule} />
|
||||
<ModuleWrapper module={DateModule} />
|
||||
<ModuleWrapper module={WeatherModule} />
|
||||
</Group>
|
||||
</MantineAside>
|
||||
);
|
||||
}
|
||||
146
src/components/layout/Footer.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
createStyles,
|
||||
Anchor,
|
||||
Text,
|
||||
Group,
|
||||
ActionIcon,
|
||||
Footer as FooterComponent,
|
||||
Alert,
|
||||
useMantineTheme,
|
||||
} from '@mantine/core';
|
||||
import { AlertCircle, BrandGithub } from 'tabler-icons-react';
|
||||
import { CURRENT_VERSION, REPO_URL } from '../../../data/constants';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
footer: {
|
||||
borderTop: `1px solid ${
|
||||
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[2]
|
||||
}`,
|
||||
},
|
||||
|
||||
inner: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: `${theme.spacing.md}px ${theme.spacing.md}px`,
|
||||
|
||||
[theme.fn.smallerThan('sm')]: {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
},
|
||||
|
||||
links: {
|
||||
[theme.fn.smallerThan('sm')]: {
|
||||
marginTop: theme.spacing.lg,
|
||||
marginBottom: theme.spacing.sm,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
interface FooterCenteredProps {
|
||||
links: { link: string; label: string }[];
|
||||
}
|
||||
|
||||
export function Footer({ links }: FooterCenteredProps) {
|
||||
const [update, setUpdate] = useState(false);
|
||||
const theme = useMantineTheme();
|
||||
const { classes } = useStyles();
|
||||
const items = links.map((link) => (
|
||||
<Anchor<'a'>
|
||||
color="dimmed"
|
||||
key={link.label}
|
||||
href={link.link}
|
||||
sx={{ lineHeight: 1 }}
|
||||
onClick={(event) => event.preventDefault()}
|
||||
size="sm"
|
||||
>
|
||||
{link.label}
|
||||
</Anchor>
|
||||
));
|
||||
|
||||
const [latestVersion, setLatestVersion] = useState(CURRENT_VERSION);
|
||||
const [isOpen, setOpen] = useState(true);
|
||||
useEffect(() => {
|
||||
// Fetch Data here when component first mounted
|
||||
fetch(`https://api.github.com/repos/${REPO_URL}/releases/latest`).then((res) => {
|
||||
res.json().then((data) => {
|
||||
setLatestVersion(data.tag_name);
|
||||
if (data.tag_name !== CURRENT_VERSION) {
|
||||
setUpdate(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FooterComponent
|
||||
p={5}
|
||||
height="auto"
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
clear: 'both',
|
||||
position: 'fixed',
|
||||
bottom: '0',
|
||||
left: '0',
|
||||
}}
|
||||
>
|
||||
<Group position="apart" direction="row" style={{ alignItems: 'end' }} mr="xs" mb="xs">
|
||||
<Group position="left">
|
||||
<Alert
|
||||
// onClick open latest release page
|
||||
onClose={() => setOpen(false)}
|
||||
icon={<AlertCircle size={16} />}
|
||||
title={`Updated version: ${latestVersion} is available. Current version: ${CURRENT_VERSION}`}
|
||||
withCloseButton
|
||||
radius="lg"
|
||||
hidden={CURRENT_VERSION === latestVersion || !isOpen}
|
||||
variant="outline"
|
||||
styles={{
|
||||
root: {
|
||||
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
|
||||
},
|
||||
|
||||
closeButton: {
|
||||
marginLeft: '5px',
|
||||
},
|
||||
}}
|
||||
children={undefined}
|
||||
/>
|
||||
</Group>
|
||||
<Group position="right">
|
||||
<Group spacing={0}>
|
||||
<ActionIcon<'a'> component="a" href="https://github.com/ajnart/homarr" size="lg">
|
||||
<BrandGithub size={18} />
|
||||
</ActionIcon>
|
||||
<Text
|
||||
style={{
|
||||
position: 'relative',
|
||||
fontSize: '0.90rem',
|
||||
color: 'gray',
|
||||
}}
|
||||
>
|
||||
{CURRENT_VERSION}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: '0.90rem',
|
||||
textAlign: 'center',
|
||||
color: '#a0aec0',
|
||||
}}
|
||||
>
|
||||
Made with ❤️ by @
|
||||
<Anchor
|
||||
href="https://github.com/ajnart"
|
||||
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}
|
||||
>
|
||||
ajnart
|
||||
</Anchor>
|
||||
</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
</FooterComponent>
|
||||
);
|
||||
}
|
||||
35
src/components/layout/Header.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { createStyles, Header as Head, Group, Box } from '@mantine/core';
|
||||
import { Logo } from './Logo';
|
||||
import SearchBar from '../modules/search/SearchModule';
|
||||
import { AddItemShelfButton } from '../AppShelf/AddAppShelfItem';
|
||||
import { SettingsMenuButton } from '../Settings/SettingsMenu';
|
||||
|
||||
const HEADER_HEIGHT = 60;
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
hide: {
|
||||
[theme.fn.smallerThan('xs')]: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export function Header(props: any) {
|
||||
const { classes, cx } = useStyles();
|
||||
|
||||
return (
|
||||
<Head height="auto">
|
||||
<Group m="xs" position="apart">
|
||||
<Box className={classes.hide}>
|
||||
<Logo style={{ fontSize: 22 }} />
|
||||
</Box>
|
||||
<Group noWrap>
|
||||
<SearchBar />
|
||||
<SettingsMenuButton />
|
||||
<AddItemShelfButton />
|
||||
</Group>
|
||||
</Group>
|
||||
</Head>
|
||||
);
|
||||
}
|
||||
24
src/components/layout/Layout.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { AppShell, createStyles } from '@mantine/core';
|
||||
import { Header } from './Header';
|
||||
import { Footer } from './Footer';
|
||||
import Aside from './Aside';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
main: {},
|
||||
}));
|
||||
|
||||
export default function Layout({ children, style }: any) {
|
||||
const { classes, cx } = useStyles();
|
||||
return (
|
||||
<AppShell aside={<Aside />} header={<Header />} footer={<Footer links={[]} />}>
|
||||
<main
|
||||
className={cx(classes.main)}
|
||||
style={{
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
33
src/components/layout/Logo.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Group, Image, Text } from '@mantine/core';
|
||||
import { NextLink } from '@mantine/next';
|
||||
import * as React from 'react';
|
||||
|
||||
export function Logo({ style }: any) {
|
||||
return (
|
||||
<Group spacing="xs">
|
||||
<Image
|
||||
width={50}
|
||||
src="/imgs/logo.png"
|
||||
style={{
|
||||
position: 'relative',
|
||||
}}
|
||||
/>
|
||||
<NextLink
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
position: 'relative',
|
||||
}}
|
||||
href="/"
|
||||
>
|
||||
<Text
|
||||
sx={style}
|
||||
weight="bold"
|
||||
variant="gradient"
|
||||
gradient={{ from: 'red', to: 'orange', deg: 145 }}
|
||||
>
|
||||
Homarr
|
||||
</Text>
|
||||
</NextLink>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
24
src/components/layout/Navbar.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Group, Navbar as MantineNavbar } from '@mantine/core';
|
||||
import { WeatherModule, DateModule } from '../modules';
|
||||
import { ModuleWrapper } from '../modules/moduleWrapper';
|
||||
|
||||
export default function Navbar() {
|
||||
return (
|
||||
<MantineNavbar
|
||||
hiddenBreakpoint="lg"
|
||||
hidden
|
||||
style={{
|
||||
border: 'none',
|
||||
}}
|
||||
width={{
|
||||
base: 'auto',
|
||||
}}
|
||||
>
|
||||
<Group mt="sm" direction="column" align="center">
|
||||
<ModuleWrapper module={DateModule} />
|
||||
<ModuleWrapper module={WeatherModule} />
|
||||
<ModuleWrapper module={WeatherModule} />
|
||||
</Group>
|
||||
</MantineNavbar>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
/* eslint-disable react/no-children-prop */
|
||||
import { Popover, Box, ScrollArea, Divider, Indicator } from '@mantine/core';
|
||||
import { Popover, Box, ScrollArea, Divider, Indicator, useMantineTheme } from '@mantine/core';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Calendar } from '@mantine/dates';
|
||||
import { CalendarIcon } from '@modulz/radix-icons';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { Check } from 'tabler-icons-react';
|
||||
import { Calendar as CalendarIcon, Check } from 'tabler-icons-react';
|
||||
import { RadarrMediaDisplay, SonarrMediaDisplay } from './MediaDisplay';
|
||||
import { useConfig } from '../../../tools/state';
|
||||
import { IModule } from '../modules';
|
||||
@@ -33,40 +32,42 @@ export default function CalendarComponent(props: any) {
|
||||
const radarrService = filtered.filter((service) => service.type === 'Radarr').at(0);
|
||||
const nextMonth = new Date(new Date().setMonth(new Date().getMonth() + 2)).toISOString();
|
||||
if (sonarrService && sonarrService.apiKey) {
|
||||
fetch(
|
||||
`${sonarrService?.url}api/calendar?apikey=${sonarrService?.apiKey}&end=${nextMonth}`
|
||||
).then((response) => {
|
||||
response.ok &&
|
||||
response.json().then((data) => {
|
||||
setSonarrMedias(data);
|
||||
showNotification({
|
||||
title: 'Sonarr',
|
||||
icon: <Check />,
|
||||
color: 'green',
|
||||
autoClose: 1500,
|
||||
radius: 'md',
|
||||
message: `Loaded ${data.length} releases`,
|
||||
const baseUrl = new URL(sonarrService.url).origin;
|
||||
fetch(`${baseUrl}/api/calendar?apikey=${sonarrService?.apiKey}&end=${nextMonth}`).then(
|
||||
(response) => {
|
||||
response.ok &&
|
||||
response.json().then((data) => {
|
||||
setSonarrMedias(data);
|
||||
showNotification({
|
||||
title: 'Sonarr',
|
||||
icon: <Check />,
|
||||
color: 'green',
|
||||
autoClose: 1500,
|
||||
radius: 'md',
|
||||
message: `Loaded ${data.length} releases`,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
if (radarrService && radarrService.apiKey) {
|
||||
fetch(
|
||||
`${radarrService?.url}api/v3/calendar?apikey=${radarrService?.apiKey}&end=${nextMonth}`
|
||||
).then((response) => {
|
||||
response.ok &&
|
||||
response.json().then((data) => {
|
||||
setRadarrMedias(data);
|
||||
showNotification({
|
||||
title: 'Radarr',
|
||||
icon: <Check />,
|
||||
color: 'green',
|
||||
autoClose: 1500,
|
||||
radius: 'md',
|
||||
message: `Loaded ${data.length} releases`,
|
||||
const baseUrl = new URL(radarrService.url).origin;
|
||||
fetch(`${baseUrl}/api/v3/calendar?apikey=${radarrService?.apiKey}&end=${nextMonth}`).then(
|
||||
(response) => {
|
||||
response.ok &&
|
||||
response.json().then((data) => {
|
||||
setRadarrMedias(data);
|
||||
showNotification({
|
||||
title: 'Radarr',
|
||||
icon: <Check />,
|
||||
color: 'green',
|
||||
autoClose: 1500,
|
||||
radius: 'md',
|
||||
message: `Loaded ${data.length} releases`,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}, [config.services]);
|
||||
|
||||
@@ -94,6 +95,7 @@ function DayComponent(props: any) {
|
||||
radarrmedias,
|
||||
}: { renderdate: Date; sonarrmedias: []; radarrmedias: [] } = props;
|
||||
const [opened, setOpened] = useState(false);
|
||||
const theme = useMantineTheme();
|
||||
|
||||
const day = renderdate.getDate();
|
||||
// Itterate over the medias and filter the ones that are on the same day
|
||||
@@ -127,8 +129,7 @@ function DayComponent(props: any) {
|
||||
width={700}
|
||||
onClose={() => setOpened(false)}
|
||||
opened={opened}
|
||||
// TODO: Fix this !! WTF ?
|
||||
target={` ${day}`}
|
||||
target={day}
|
||||
>
|
||||
<ScrollArea style={{ height: 400 }}>
|
||||
{sonarrFiltered.map((media: any, index: number) => (
|
||||
@@ -30,7 +30,7 @@ function MediaDisplay(props: { media: IMedia }) {
|
||||
})}
|
||||
>
|
||||
<Group direction="column">
|
||||
<Group>
|
||||
<Group noWrap>
|
||||
<Title order={3}>{media.title}</Title>
|
||||
<Anchor href={`https://www.imdb.com/title/${media.imdbId}`} target="_blank">
|
||||
<ActionIcon>
|
||||
@@ -48,7 +48,9 @@ function MediaDisplay(props: { media: IMedia }) {
|
||||
Season {media.seasonNumber} episode {media.episodeNumber}
|
||||
</Text>
|
||||
)}
|
||||
<Text align="justify">{media.overview}</Text>
|
||||
<Text lineClamp={12} align="justify">
|
||||
{media.overview}
|
||||
</Text>
|
||||
</Group>
|
||||
{/*Add the genres at the bottom of the poster*/}
|
||||
<Group>
|
||||
@@ -2,6 +2,7 @@ import { Group, Text, Title } from '@mantine/core';
|
||||
import dayjs from 'dayjs';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Clock } from 'tabler-icons-react';
|
||||
import { useConfig } from '../../../tools/state';
|
||||
import { IModule } from '../modules';
|
||||
|
||||
export const DateModule: IModule = {
|
||||
@@ -9,33 +10,31 @@ export const DateModule: IModule = {
|
||||
description: 'Show the current time and date in a card',
|
||||
icon: Clock,
|
||||
component: DateComponent,
|
||||
options: {
|
||||
full: {
|
||||
name: 'Display full time (24-hour)',
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default function DateComponent(props: any) {
|
||||
const [date, setDate] = useState(new Date());
|
||||
const hours = date.getHours();
|
||||
const minutes = date.getMinutes();
|
||||
|
||||
const { config } = useConfig();
|
||||
const isFullTime = config?.modules?.[DateModule.title]?.options?.full?.value ?? false;
|
||||
const formatString = isFullTime ? 'HH:mm' : 'h:mm A';
|
||||
// Change date on minute change
|
||||
// Note: Using 10 000ms instead of 1000ms to chill a little :)
|
||||
useEffect(() => {
|
||||
setInterval(() => {
|
||||
setDate(new Date());
|
||||
}, 10000);
|
||||
}, 1000 * 60);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Group p="sm" direction="column">
|
||||
<Title>
|
||||
{hours < 10 ? `0${hours}` : hours}:{minutes < 10 ? `0${minutes}` : minutes}
|
||||
</Title>
|
||||
<Text size="xl">
|
||||
{
|
||||
// Use dayjs to format the date
|
||||
// https://day.js.org/en/getting-started/installation/
|
||||
dayjs(date).format('dddd, MMMM D')
|
||||
}
|
||||
</Text>
|
||||
<Title>{dayjs(date).format(formatString)}</Title>
|
||||
<Text size="xl">{dayjs(date).format('dddd, MMMM D')}</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
5
src/components/modules/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './date';
|
||||
export * from './calendar';
|
||||
export * from './search';
|
||||
export * from './ping';
|
||||
export * from './weather';
|
||||
88
src/components/modules/moduleWrapper.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Card, Menu, Switch, useMantineTheme } from '@mantine/core';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { IModule } from './modules';
|
||||
|
||||
export function ModuleWrapper(props: any) {
|
||||
const { module }: { module: IModule } = props;
|
||||
const { config, setConfig } = useConfig();
|
||||
const enabledModules = config.modules ?? {};
|
||||
// Remove 'Module' from enabled modules titles
|
||||
const isShown = enabledModules[module.title]?.enabled ?? false;
|
||||
const theme = useMantineTheme();
|
||||
const items: JSX.Element[] = [];
|
||||
if (module.options) {
|
||||
const keys = Object.keys(module.options);
|
||||
const values = Object.values(module.options);
|
||||
// Get the value and the name of the option
|
||||
const types = values.map((v) => typeof v.value);
|
||||
// Loop over all the types with a for each loop
|
||||
types.forEach((type, index) => {
|
||||
const optionName = `${module.title}.${keys[index]}`;
|
||||
const moduleInConfig = config.modules?.[module.title];
|
||||
// TODO: Add support for other types
|
||||
if (type === 'boolean') {
|
||||
items.push(
|
||||
<Switch
|
||||
defaultChecked={
|
||||
// Set default checked to the value of the option if it exists
|
||||
moduleInConfig?.options?.[keys[index]]?.value ?? false
|
||||
}
|
||||
key={keys[index]}
|
||||
onClick={(e) => {
|
||||
setConfig({
|
||||
...config,
|
||||
modules: {
|
||||
...config.modules,
|
||||
[module.title]: {
|
||||
...config.modules[module.title],
|
||||
options: {
|
||||
...config.modules[module.title].options,
|
||||
[keys[index]]: {
|
||||
...config.modules[module.title].options?.[keys[index]],
|
||||
value: e.currentTarget.checked,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
label={values[index].name}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!isShown) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Card hidden={!isShown} mx="sm" withBorder radius="lg" shadow="sm">
|
||||
{module.options && (
|
||||
<Menu
|
||||
size="md"
|
||||
shadow="xl"
|
||||
closeOnItemClick={false}
|
||||
radius="md"
|
||||
position="left"
|
||||
styles={{
|
||||
root: {
|
||||
position: 'absolute',
|
||||
top: 15,
|
||||
right: 15,
|
||||
},
|
||||
body: {
|
||||
backgroundColor:
|
||||
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Menu.Label>Settings</Menu.Label>
|
||||
{items.map((item) => (
|
||||
<Menu.Item key={item.key}>{item}</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
)}
|
||||
<module.component />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -7,5 +7,14 @@ export interface IModule {
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
component: React.ComponentType;
|
||||
props?: any;
|
||||
options?: Option;
|
||||
}
|
||||
|
||||
interface Option {
|
||||
[x: string]: OptionValues;
|
||||
}
|
||||
|
||||
export interface OptionValues {
|
||||
name: string;
|
||||
value: boolean;
|
||||
}
|
||||
16
src/components/modules/ping/PingModule.story.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { serviceItem } from '../../../tools/types';
|
||||
import PingComponent from './PingModule';
|
||||
|
||||
export default {
|
||||
title: 'Modules/Search bar',
|
||||
};
|
||||
|
||||
const service: serviceItem = {
|
||||
id: '1',
|
||||
type: 'Other',
|
||||
name: 'YouTube',
|
||||
icon: 'https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/youtube.png',
|
||||
url: 'https://youtube.com/',
|
||||
};
|
||||
|
||||
export const Default = (args: any) => <PingComponent service={service} />;
|
||||
60
src/components/modules/ping/PingModule.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Indicator, Tooltip } from '@mantine/core';
|
||||
import axios from 'axios';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Plug } from 'tabler-icons-react';
|
||||
import { useConfig } from '../../../tools/state';
|
||||
import { IModule } from '../modules';
|
||||
|
||||
export const PingModule: IModule = {
|
||||
title: 'Ping Services',
|
||||
description: 'Pings your services and shows their status as an indicator',
|
||||
icon: Plug,
|
||||
component: PingComponent,
|
||||
};
|
||||
|
||||
export default function PingComponent(props: any) {
|
||||
type State = 'loading' | 'down' | 'online';
|
||||
const { config } = useConfig();
|
||||
|
||||
const { url }: { url: string } = props;
|
||||
const [isOnline, setOnline] = useState<State>('loading');
|
||||
const exists = config.modules?.[PingModule.title]?.enabled ?? false;
|
||||
useEffect(() => {
|
||||
if (!exists) {
|
||||
return;
|
||||
}
|
||||
axios
|
||||
.get('/api/modules/ping', { params: { url } })
|
||||
.then(() => {
|
||||
setOnline('online');
|
||||
})
|
||||
.catch(() => {
|
||||
setOnline('down');
|
||||
});
|
||||
}, []);
|
||||
if (!exists) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Tooltip
|
||||
radius="lg"
|
||||
style={{ position: 'absolute', bottom: 20, right: 20 }}
|
||||
label={isOnline === 'loading' ? 'Loading...' : isOnline === 'online' ? 'Online' : 'Offline'}
|
||||
>
|
||||
<motion.div
|
||||
animate={{
|
||||
scale: isOnline === 'online' ? [1, 0.8, 1] : 1,
|
||||
}}
|
||||
transition={{ repeat: Infinity, duration: 2.5, ease: 'easeInOut' }}
|
||||
>
|
||||
<Indicator
|
||||
size={13}
|
||||
color={isOnline === 'online' ? 'green' : isOnline === 'down' ? 'red' : 'yellow'}
|
||||
>
|
||||
{null}
|
||||
</Indicator>
|
||||
</motion.div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
1
src/components/modules/ping/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { PingModule } from './PingModule';
|
||||
9
src/components/modules/readme.md
Normal file
@@ -0,0 +1,9 @@
|
||||
**Each module has a set of rules:**
|
||||
- Exported Typed IModule element (Unique Name, description, component, ...)
|
||||
- Needs to be in a new folder
|
||||
- Needs to be exported in the modules/newmodule/index.tsx of the new folder
|
||||
- Needs to be imported in the modules/index.tsx file
|
||||
- Needs to look good when wrapped with the modules/ModuleWrapper component
|
||||
- Needs to be put somewhere fitting in the app (While waiting for the big AppStore overhall)
|
||||
- Any API Calls need to be safe and done on the widget itself (via useEffect or similar)
|
||||
- You can't add a package (unless there is a very specific need for it. Contact [@Ajnart](ajnart@pm.me) or make a [Discussion](https://github.com/ajnart/homarr/discussions/new).
|
||||
@@ -1,4 +1,4 @@
|
||||
import SearchBar from './SearchBar';
|
||||
import SearchBar from './SearchModule';
|
||||
|
||||
export default {
|
||||
title: 'Search bar',
|
||||
121
src/components/modules/search/SearchModule.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { TextInput, Kbd, createStyles, Text, Popover } from '@mantine/core';
|
||||
import { useForm, useHotkeys } from '@mantine/hooks';
|
||||
import { useRef, useState } from 'react';
|
||||
import { Search, BrandYoutube, Download } from 'tabler-icons-react';
|
||||
import { useConfig } from '../../../tools/state';
|
||||
import { IModule } from '../modules';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
hide: {
|
||||
[theme.fn.smallerThan('sm')]: {
|
||||
display: 'none',
|
||||
},
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
export const SearchModule: IModule = {
|
||||
title: 'Search Bar',
|
||||
description: 'Show the current time and date in a card',
|
||||
icon: Search,
|
||||
component: SearchBar,
|
||||
};
|
||||
|
||||
export default function SearchBar(props: any) {
|
||||
const { config, setConfig } = useConfig();
|
||||
const [opened, setOpened] = useState(false);
|
||||
const [icon, setIcon] = useState(<Search />);
|
||||
const queryUrl = config.settings.searchUrl ?? 'https://www.google.com/search?q=';
|
||||
const textInput = useRef<HTMLInputElement>();
|
||||
useHotkeys([['ctrl+K', () => textInput.current && textInput.current.focus()]]);
|
||||
|
||||
const { classes, cx } = useStyles();
|
||||
const rightSection = (
|
||||
<div className={classes.hide}>
|
||||
<Kbd>Ctrl</Kbd>
|
||||
<span style={{ margin: '0 5px' }}>+</span>
|
||||
<Kbd>K</Kbd>
|
||||
</div>
|
||||
);
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
query: '',
|
||||
},
|
||||
});
|
||||
|
||||
// If enabled modules doesn't contain the module, return null
|
||||
// If module in enabled
|
||||
|
||||
const exists = config.modules?.[SearchModule.title]?.enabled ?? false;
|
||||
if (!exists) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
onChange={() => {
|
||||
// If query contains !yt or !t add "Searching on YouTube" or "Searching torrent"
|
||||
const query = form.values.query.trim();
|
||||
const isYoutube = query.startsWith('!yt');
|
||||
const isTorrent = query.startsWith('!t');
|
||||
if (isYoutube) {
|
||||
setIcon(<BrandYoutube size={22} />);
|
||||
} else if (isTorrent) {
|
||||
setIcon(<Download size={22} />);
|
||||
} else {
|
||||
setIcon(<Search size={22} />);
|
||||
}
|
||||
}}
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
const query = values.query.trim();
|
||||
const isYoutube = query.startsWith('!yt');
|
||||
const isTorrent = query.startsWith('!t');
|
||||
form.setValues({ query: '' });
|
||||
setTimeout(() => {
|
||||
if (isYoutube) {
|
||||
window.open(`https://www.youtube.com/results?search_query=${query.substring(3)}`);
|
||||
} else if (isTorrent) {
|
||||
window.open(`https://bitsearch.to/search?q=${query.substring(3)}`);
|
||||
} else {
|
||||
window.open(`${queryUrl}${values.query}`);
|
||||
}
|
||||
}, 20);
|
||||
})}
|
||||
>
|
||||
<Popover
|
||||
opened={opened}
|
||||
position="bottom"
|
||||
placement="start"
|
||||
width={260}
|
||||
withArrow
|
||||
radius="md"
|
||||
trapFocus={false}
|
||||
transition="pop-bottom-right"
|
||||
onFocusCapture={() => setOpened(true)}
|
||||
onBlurCapture={() => setOpened(false)}
|
||||
target={
|
||||
<TextInput
|
||||
variant="filled"
|
||||
icon={icon}
|
||||
ref={textInput}
|
||||
rightSectionWidth={90}
|
||||
rightSection={rightSection}
|
||||
radius="md"
|
||||
size="md"
|
||||
styles={{ rightSection: { pointerEvents: 'none' } }}
|
||||
placeholder="Search the web..."
|
||||
{...props}
|
||||
{...form.getInputProps('query')}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Text>
|
||||
tip: Use the prefixes <b>!yt</b> and <b>!t</b> in front of your query to search on YouTube
|
||||
or for a Torrent respectively.
|
||||
</Text>
|
||||
</Popover>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
1
src/components/modules/search/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { SearchModule } from './SearchModule';
|
||||
41
src/components/modules/weather/WeatherInterface.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
// To parse this data:
|
||||
//
|
||||
// import { Convert, WeatherResponse } from "./file";
|
||||
//
|
||||
// const weatherResponse = Convert.toWeatherResponse(json);
|
||||
//
|
||||
// These functions will throw an error if the JSON doesn't
|
||||
// match the expected interface, even if the JSON is valid.
|
||||
|
||||
export interface WeatherResponse {
|
||||
current_weather: CurrentWeather;
|
||||
utc_offset_seconds: number;
|
||||
latitude: number;
|
||||
elevation: number;
|
||||
longitude: number;
|
||||
generationtime_ms: number;
|
||||
daily_units: DailyUnits;
|
||||
daily: Daily;
|
||||
}
|
||||
|
||||
export interface CurrentWeather {
|
||||
winddirection: number;
|
||||
windspeed: number;
|
||||
time: string;
|
||||
weathercode: number;
|
||||
temperature: number;
|
||||
}
|
||||
|
||||
export interface Daily {
|
||||
temperature_2m_max: number[];
|
||||
time: Date[];
|
||||
temperature_2m_min: number[];
|
||||
weathercode: number[];
|
||||
}
|
||||
|
||||
export interface DailyUnits {
|
||||
temperature_2m_max: string;
|
||||
temperature_2m_min: string;
|
||||
time: string;
|
||||
weathercode: string;
|
||||
}
|
||||
172
src/components/modules/weather/WeatherModule.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
import { Group, Space, Title, Tooltip } from '@mantine/core';
|
||||
import axios from 'axios';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
ArrowDownRight,
|
||||
ArrowUpRight,
|
||||
Cloud,
|
||||
CloudFog,
|
||||
CloudRain,
|
||||
CloudSnow,
|
||||
CloudStorm,
|
||||
QuestionMark,
|
||||
Snowflake,
|
||||
Sun,
|
||||
} from 'tabler-icons-react';
|
||||
import { useConfig } from '../../../tools/state';
|
||||
import { IModule } from '../modules';
|
||||
import { WeatherResponse } from './WeatherInterface';
|
||||
|
||||
export const WeatherModule: IModule = {
|
||||
title: 'Weather (beta)',
|
||||
description: 'Look up the current weather in your location',
|
||||
icon: Sun,
|
||||
component: WeatherComponent,
|
||||
options: {
|
||||
freedomunit: {
|
||||
name: 'Display in Fahrenheit',
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// 0 Clear sky
|
||||
// 1, 2, 3 Mainly clear, partly cloudy, and overcast
|
||||
// 45, 48 Fog and depositing rime fog
|
||||
// 51, 53, 55 Drizzle: Light, moderate, and dense intensity
|
||||
// 56, 57 Freezing Drizzle: Light and dense intensity
|
||||
// 61, 63, 65 Rain: Slight, moderate and heavy intensity
|
||||
// 66, 67 Freezing Rain: Light and heavy intensity
|
||||
// 71, 73, 75 Snow fall: Slight, moderate, and heavy intensity
|
||||
// 77 Snow grains
|
||||
// 80, 81, 82 Rain showers: Slight, moderate, and violent
|
||||
// 85, 86Snow showers slight and heavy
|
||||
// 95 *Thunderstorm: Slight or moderate
|
||||
// 96, 99 *Thunderstorm with slight and heavy hail
|
||||
export function WeatherIcon(props: any) {
|
||||
const { code } = props;
|
||||
let data: { icon: any; name: string };
|
||||
switch (code) {
|
||||
case 0: {
|
||||
data = { icon: Sun, name: 'Clear' };
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
case 2:
|
||||
case 3: {
|
||||
data = { icon: Cloud, name: 'Mainly clear' };
|
||||
break;
|
||||
}
|
||||
case 45:
|
||||
case 48: {
|
||||
data = { icon: CloudFog, name: 'Fog' };
|
||||
break;
|
||||
}
|
||||
case 51:
|
||||
case 53:
|
||||
case 55: {
|
||||
data = { icon: Cloud, name: 'Drizzle' };
|
||||
break;
|
||||
}
|
||||
case 56:
|
||||
case 57: {
|
||||
data = { icon: Snowflake, name: 'Freezing drizzle' };
|
||||
break;
|
||||
}
|
||||
case 61:
|
||||
case 63:
|
||||
case 65: {
|
||||
data = { icon: CloudRain, name: 'Rain' };
|
||||
break;
|
||||
}
|
||||
case 66:
|
||||
case 67: {
|
||||
data = { icon: CloudRain, name: 'Freezing rain' };
|
||||
break;
|
||||
}
|
||||
case 71:
|
||||
case 73:
|
||||
case 75: {
|
||||
data = { icon: CloudSnow, name: 'Snow fall' };
|
||||
break;
|
||||
}
|
||||
case 77: {
|
||||
data = { icon: CloudSnow, name: 'Snow grains' };
|
||||
break;
|
||||
}
|
||||
case 80:
|
||||
case 81:
|
||||
case 82: {
|
||||
data = { icon: CloudRain, name: 'Rain showers' };
|
||||
|
||||
break;
|
||||
}
|
||||
case 85:
|
||||
case 86: {
|
||||
data = { icon: CloudSnow, name: 'Snow showers' };
|
||||
break;
|
||||
}
|
||||
case 95: {
|
||||
data = { icon: CloudStorm, name: 'Thunderstorm' };
|
||||
break;
|
||||
}
|
||||
case 96:
|
||||
case 99: {
|
||||
data = { icon: CloudStorm, name: 'Thunderstorm with hail' };
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
data = { icon: QuestionMark, name: 'Unknown' };
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Tooltip label={data.name}>
|
||||
<data.icon size={50} />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WeatherComponent(props: any) {
|
||||
// Get location from browser
|
||||
const [location, setLocation] = useState({ lat: 0, lng: 0 });
|
||||
const { config } = useConfig();
|
||||
const [weather, setWeather] = useState({} as WeatherResponse);
|
||||
const isFahrenheit: boolean =
|
||||
config?.modules?.[WeatherModule.title]?.options?.freedomunit?.value ?? false;
|
||||
|
||||
if ('geolocation' in navigator && location.lat === 0 && location.lng === 0) {
|
||||
navigator.geolocation.getCurrentPosition((position) => {
|
||||
setLocation({ lat: position.coords.latitude, lng: position.coords.longitude });
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
axios
|
||||
.get(
|
||||
`https://api.open-meteo.com/v1/forecast?latitude=${location.lat}&longitude=${location.lng}&daily=weathercode,temperature_2m_max,temperature_2m_min¤t_weather=true&timezone=Europe%2FLondon`
|
||||
)
|
||||
.then((res) => {
|
||||
setWeather(res.data);
|
||||
});
|
||||
}, []);
|
||||
if (!weather.current_weather) {
|
||||
return null;
|
||||
}
|
||||
function usePerferedUnit(value: number): string {
|
||||
return isFahrenheit ? `${(value * (9 / 5)).toFixed(1)}°F` : `${value.toFixed(1)}°C`;
|
||||
}
|
||||
return (
|
||||
<Group position="left" direction="column">
|
||||
<Title>{usePerferedUnit(weather.current_weather.temperature)}</Title>
|
||||
<Group spacing={0}>
|
||||
<WeatherIcon code={weather.current_weather.weathercode} />
|
||||
<Space mx="sm" />
|
||||
<span>{usePerferedUnit(weather.daily.temperature_2m_max[0])}</span>
|
||||
<ArrowUpRight size={16} style={{ right: 15 }} />
|
||||
<Space mx="sm" />
|
||||
<span>{usePerferedUnit(weather.daily.temperature_2m_min[0])}</span>
|
||||
<ArrowDownRight size={16} />
|
||||
</Group>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
1
src/components/modules/weather/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { WeatherModule } from './WeatherModule';
|
||||
94
src/pages/404.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
createStyles,
|
||||
Container,
|
||||
Title,
|
||||
Text,
|
||||
Button,
|
||||
Group,
|
||||
useMantineTheme,
|
||||
} from '@mantine/core';
|
||||
import { NextLink } from '@mantine/next';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
root: {
|
||||
paddingTop: 80,
|
||||
paddingBottom: 80,
|
||||
},
|
||||
|
||||
inner: {
|
||||
position: 'relative',
|
||||
},
|
||||
|
||||
image: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
zIndex: 0,
|
||||
opacity: 0.75,
|
||||
},
|
||||
|
||||
content: {
|
||||
paddingTop: 220,
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
|
||||
[theme.fn.smallerThan('sm')]: {
|
||||
paddingTop: 120,
|
||||
},
|
||||
},
|
||||
|
||||
title: {
|
||||
fontFamily: `Greycliff CF, ${theme.fontFamily}`,
|
||||
textAlign: 'center',
|
||||
fontWeight: 900,
|
||||
fontSize: 38,
|
||||
|
||||
[theme.fn.smallerThan('sm')]: {
|
||||
fontSize: 32,
|
||||
},
|
||||
},
|
||||
|
||||
description: {
|
||||
maxWidth: 540,
|
||||
margin: 'auto',
|
||||
marginTop: theme.spacing.xl,
|
||||
marginBottom: theme.spacing.xl * 1.5,
|
||||
},
|
||||
}));
|
||||
|
||||
export function Illustration(props: React.ComponentPropsWithoutRef<'svg'>) {
|
||||
const theme = useMantineTheme();
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 362 145" {...props}>
|
||||
<path
|
||||
fill={theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0]}
|
||||
d="M62.6 142c-2.133 0-3.2-1.067-3.2-3.2V118h-56c-2 0-3-1-3-3V92.8c0-1.333.4-2.733 1.2-4.2L58.2 4c.8-1.333 2.067-2 3.8-2h28c2 0 3 1 3 3v85.4h11.2c.933 0 1.733.333 2.4 1 .667.533 1 1.267 1 2.2v21.2c0 .933-.333 1.733-1 2.4-.667.533-1.467.8-2.4.8H93v20.8c0 2.133-1.067 3.2-3.2 3.2H62.6zM33 90.4h26.4V51.2L33 90.4zM181.67 144.6c-7.333 0-14.333-1.333-21-4-6.666-2.667-12.866-6.733-18.6-12.2-5.733-5.467-10.266-13-13.6-22.6-3.333-9.6-5-20.667-5-33.2 0-12.533 1.667-23.6 5-33.2 3.334-9.6 7.867-17.133 13.6-22.6 5.734-5.467 11.934-9.533 18.6-12.2 6.667-2.8 13.667-4.2 21-4.2 7.467 0 14.534 1.4 21.2 4.2 6.667 2.667 12.8 6.733 18.4 12.2 5.734 5.467 10.267 13 13.6 22.6 3.334 9.6 5 20.667 5 33.2 0 12.533-1.666 23.6-5 33.2-3.333 9.6-7.866 17.133-13.6 22.6-5.6 5.467-11.733 9.533-18.4 12.2-6.666 2.667-13.733 4-21.2 4zm0-31c9.067 0 15.6-3.733 19.6-11.2 4.134-7.6 6.2-17.533 6.2-29.8s-2.066-22.2-6.2-29.8c-4.133-7.6-10.666-11.4-19.6-11.4-8.933 0-15.466 3.8-19.6 11.4-4 7.6-6 17.533-6 29.8s2 22.2 6 29.8c4.134 7.467 10.667 11.2 19.6 11.2zM316.116 142c-2.134 0-3.2-1.067-3.2-3.2V118h-56c-2 0-3-1-3-3V92.8c0-1.333.4-2.733 1.2-4.2l56.6-84.6c.8-1.333 2.066-2 3.8-2h28c2 0 3 1 3 3v85.4h11.2c.933 0 1.733.333 2.4 1 .666.533 1 1.267 1 2.2v21.2c0 .933-.334 1.733-1 2.4-.667.533-1.467.8-2.4.8h-11.2v20.8c0 2.133-1.067 3.2-3.2 3.2h-27.2zm-29.6-51.6h26.4V51.2l-26.4 39.2z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default function NothingFoundBackground() {
|
||||
const { classes } = useStyles();
|
||||
|
||||
return (
|
||||
<Container className={classes.root}>
|
||||
<div className={classes.inner}>
|
||||
<Illustration className={classes.image} />
|
||||
<div className={classes.content}>
|
||||
<Title className={classes.title}>Config not found</Title>
|
||||
<Text color="dimmed" size="lg" align="center" className={classes.description}>
|
||||
The config you are trying to access does not exist. Please check the URL and try again.
|
||||
</Text>
|
||||
<Group position="center">
|
||||
<NextLink href="/">
|
||||
<Button size="md">Take me back to home page</Button>
|
||||
</NextLink>
|
||||
</Group>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
54
src/pages/[slug].tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { GetServerSidePropsContext } from 'next';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { useEffect } from 'react';
|
||||
import AppShelf from '../components/AppShelf/AppShelf';
|
||||
import LoadConfigComponent from '../components/Config/LoadConfig';
|
||||
import { Config } from '../tools/types';
|
||||
import { useConfig } from '../tools/state';
|
||||
|
||||
export async function getServerSideProps(
|
||||
context: GetServerSidePropsContext
|
||||
): Promise<{ props: { config: Config } }> {
|
||||
const configByUrl = context.query.slug;
|
||||
const configPath = path.join(process.cwd(), 'data/configs', `${configByUrl}.json`);
|
||||
const configExists = fs.existsSync(configPath);
|
||||
if (!configExists) {
|
||||
// Redirect to 404
|
||||
context.res.writeHead(301, { Location: '/404' });
|
||||
context.res.end();
|
||||
return {
|
||||
props: {
|
||||
config: {
|
||||
name: 'Default config',
|
||||
services: [],
|
||||
settings: {
|
||||
searchUrl: 'https://www.google.com/search?q=',
|
||||
},
|
||||
modules: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
const config = fs.readFileSync(configPath, 'utf8');
|
||||
// Print loaded config
|
||||
return {
|
||||
props: {
|
||||
config: JSON.parse(config),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function HomePage(props: any) {
|
||||
const { config: initialConfig }: { config: Config } = props;
|
||||
const { setConfig } = useConfig();
|
||||
useEffect(() => {
|
||||
setConfig(initialConfig);
|
||||
}, [initialConfig]);
|
||||
return (
|
||||
<>
|
||||
<AppShelf />
|
||||
<LoadConfigComponent />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { getCookie, setCookies } from 'cookies-next';
|
||||
import Head from 'next/head';
|
||||
import { MantineProvider, ColorScheme, ColorSchemeProvider } from '@mantine/core';
|
||||
import { NotificationsProvider } from '@mantine/notifications';
|
||||
import { useHotkeys } from '@mantine/hooks';
|
||||
import Layout from '../components/layout/Layout';
|
||||
import { ConfigProvider } from '../tools/state';
|
||||
import { theme } from '../tools/theme';
|
||||
@@ -18,11 +19,12 @@ export default function App(props: AppProps & { colorScheme: ColorScheme }) {
|
||||
setColorScheme(nextColorScheme);
|
||||
setCookies('color-scheme', nextColorScheme, { maxAge: 60 * 60 * 24 * 30 });
|
||||
};
|
||||
useHotkeys([['mod+J', () => toggleColorScheme()]]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Homarr - A homepage for your server!</title>
|
||||
<title>Homarr 🦞</title>
|
||||
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
|
||||
<link rel="shortcut icon" href="/favicon.svg" />
|
||||
</Head>
|
||||
@@ -6,7 +6,6 @@ const stylesServer = createStylesServer();
|
||||
export default class _Document extends Document {
|
||||
static async getInitialProps(ctx: DocumentContext) {
|
||||
const initialProps = await Document.getInitialProps(ctx);
|
||||
|
||||
// Add your app specific logic here
|
||||
|
||||
return {
|
||||
29
src/pages/api/modules/ping.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import axios from 'axios';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
async function Get(req: NextApiRequest, res: NextApiResponse) {
|
||||
// Parse req.body as a ServiceItem
|
||||
const { url } = req.query;
|
||||
await axios
|
||||
.get(url as string)
|
||||
.then((response) => {
|
||||
res.status(200).json(response.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
res.status(500).json(error);
|
||||
});
|
||||
// // Make a request to the URL
|
||||
// const response = await axios.get(url);
|
||||
// // Return the response
|
||||
}
|
||||
|
||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
// Filter out if the reuqest is a POST or a GET
|
||||
if (req.method === 'GET') {
|
||||
return Get(req, res);
|
||||
}
|
||||
return res.status(405).json({
|
||||
statusCode: 405,
|
||||
message: 'Method not allowed',
|
||||
});
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Group } from '@mantine/core';
|
||||
import { getCookie, setCookies } from 'cookies-next';
|
||||
import { GetServerSidePropsContext } from 'next';
|
||||
import path from 'path';
|
||||
@@ -6,9 +5,9 @@ import fs from 'fs';
|
||||
import { useEffect } from 'react';
|
||||
import AppShelf from '../components/AppShelf/AppShelf';
|
||||
import LoadConfigComponent from '../components/Config/LoadConfig';
|
||||
import SearchBar from '../components/SearchBar/SearchBar';
|
||||
import { Config } from '../tools/types';
|
||||
import { useConfig } from '../tools/state';
|
||||
import { migrateToIdConfig } from '../tools/migrate';
|
||||
|
||||
export async function getServerSideProps({
|
||||
req,
|
||||
@@ -28,10 +27,9 @@ export async function getServerSideProps({
|
||||
name: cookie.toString(),
|
||||
services: [],
|
||||
settings: {
|
||||
enabledModules: [],
|
||||
searchBar: true,
|
||||
searchUrl: 'https://www.google.com/search?q=',
|
||||
},
|
||||
modules: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -50,14 +48,12 @@ export default function HomePage(props: any) {
|
||||
const { config: initialConfig }: { config: Config } = props;
|
||||
const { config, loadConfig, setConfig, getConfigs } = useConfig();
|
||||
useEffect(() => {
|
||||
setConfig(initialConfig);
|
||||
const migratedConfig = migrateToIdConfig(initialConfig);
|
||||
setConfig(migratedConfig);
|
||||
}, [initialConfig]);
|
||||
return (
|
||||
<>
|
||||
<SearchBar />
|
||||
<Group align="start" position="apart" noWrap>
|
||||
<AppShelf />
|
||||
</Group>
|
||||
<AppShelf />
|
||||
<LoadConfigComponent />
|
||||
</>
|
||||
);
|
||||
14
src/tools/migrate.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Config } from './types';
|
||||
|
||||
export function migrateToIdConfig(config: Config): Config {
|
||||
// Set the config and add an ID to all the services that don't have one
|
||||
const services = config.services.map((service) => ({
|
||||
...service,
|
||||
id: service.id ?? uuidv4(),
|
||||
}));
|
||||
return {
|
||||
...config,
|
||||
services,
|
||||
};
|
||||
}
|
||||
@@ -17,10 +17,9 @@ const configContext = createContext<configContextType>({
|
||||
name: 'default',
|
||||
services: [],
|
||||
settings: {
|
||||
searchBar: true,
|
||||
searchUrl: 'https://www.google.com/search?q=',
|
||||
enabledModules: [],
|
||||
searchUrl: 'https://google.com/search?q=',
|
||||
},
|
||||
modules: {},
|
||||
},
|
||||
setConfig: () => {},
|
||||
loadConfig: async (name: string) => {},
|
||||
@@ -44,10 +43,9 @@ export function ConfigProvider({ children }: Props) {
|
||||
name: 'default',
|
||||
services: [],
|
||||
settings: {
|
||||
searchBar: true,
|
||||
searchUrl: 'https://www.google.com/search?q=',
|
||||
enabledModules: [],
|
||||
},
|
||||
modules: {},
|
||||
});
|
||||
|
||||
async function loadConfig(configName: string) {
|
||||