v12.0.0 - initial commit
This commit is contained in:
commit
e2c49ea43c
1145 changed files with 97211 additions and 0 deletions
5
.dockerignore
Normal file
5
.dockerignore
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
node_modules
|
||||
packages/*/node_modules
|
||||
.git
|
||||
.direnv
|
||||
.envrc
|
||||
51
.github/workflows/build.yml
vendored
Normal file
51
.github/workflows/build.yml
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
name: build
|
||||
|
||||
on: [ workflow_dispatch ]
|
||||
|
||||
jobs:
|
||||
everything:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Turbo cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-turbo-
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.11.0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages with Turbo
|
||||
run: pnpm run build
|
||||
|
||||
- name: Package production build
|
||||
run: |
|
||||
# Create production-ready server package using pnpm deploy
|
||||
pnpm deploy --filter=./packages/server --prod lamassu-server --legacy
|
||||
|
||||
# Copy built admin UI to public directory
|
||||
cp -r packages/admin-ui/build lamassu-server/public
|
||||
|
||||
# Create tarball
|
||||
tar -zcf lamassu-server.tar.gz lamassu-server/
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lamassu-server.tar.gz
|
||||
path: lamassu-server.tar.gz
|
||||
92
.github/workflows/docker-build.yml
vendored
Normal file
92
.github/workflows/docker-build.yml
vendored
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
name: Docker Build and Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
env:
|
||||
DOCKERHUB_SERVER_REPO: lamassu/lamassu-server
|
||||
DOCKERHUB_ADMIN_REPO: lamassu/lamassu-admin-server
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Turbo cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-turbo-
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.11.0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages with Turbo
|
||||
run: pnpm run build
|
||||
|
||||
- name: Verify build artifacts
|
||||
run: |
|
||||
echo "=== Verifying typesafe-db build ==="
|
||||
ls -la packages/typesafe-db/lib/
|
||||
echo "=== Verifying admin-ui build ==="
|
||||
ls -la packages/admin-ui/build/
|
||||
|
||||
- name: Package production build
|
||||
run: |
|
||||
# Create production-ready server package using pnpm deploy
|
||||
pnpm deploy --filter=./packages/server --prod lamassu-server --legacy
|
||||
|
||||
# Copy built admin UI to public directory
|
||||
cp -r packages/admin-ui/build lamassu-server/public
|
||||
|
||||
# Copy Dockerfile to lamassu-server context
|
||||
cp build/server.Dockerfile lamassu-server/
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
|
||||
- name: Build and push server image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: lamassu-server
|
||||
push: true
|
||||
target: l-s
|
||||
file: lamassu-server/server.Dockerfile
|
||||
tags: ${{ env.DOCKERHUB_SERVER_REPO }}:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Build and push admin server image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: lamassu-server
|
||||
push: true
|
||||
target: l-a-s
|
||||
file: lamassu-server/server.Dockerfile
|
||||
tags: ${{ env.DOCKERHUB_ADMIN_REPO }}:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
40
.github/workflows/test.yml
vendored
Normal file
40
.github/workflows/test.yml
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
name: Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ dev ]
|
||||
push:
|
||||
branches: [ dev ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Turbo cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-turbo-
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.11.0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm run test
|
||||
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
**/node_modules
|
||||
**/.env
|
||||
.pnpm-store/
|
||||
|
||||
.idea/
|
||||
.settings/
|
||||
.turbo/
|
||||
|
||||
packages/server/.lamassu
|
||||
packages/server/certs/
|
||||
packages/server/tests/stress/machines
|
||||
packages/server/tests/stress/config.json
|
||||
packages/typesafe-db/lib/
|
||||
5
.husky/pre-commit
Executable file
5
.husky/pre-commit
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
# Run linting
|
||||
npx lint-staged
|
||||
8
.prettierrc
Normal file
8
.prettierrc
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"arrowParens": "avoid",
|
||||
"bracketSameLine": true
|
||||
}
|
||||
3
.tool-versions
Normal file
3
.tool-versions
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
nodejs 22
|
||||
pnpm 10
|
||||
python 3
|
||||
22
.vscode/launch.json
vendored
Normal file
22
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible Node.js debug attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"program": "${workspaceRoot}/bin/lamassu-server",
|
||||
"cwd": "${workspaceRoot}",
|
||||
"args": [""]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Attach by Process ID",
|
||||
"processId": "${command:PickProcess}"
|
||||
}
|
||||
]
|
||||
}
|
||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"vsicons.presets.angular": false
|
||||
}
|
||||
54
CRYPTO_README.md
Normal file
54
CRYPTO_README.md
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
## Adding a new cryptocurrency to a Lamassu ATM
|
||||
|
||||
### Structure
|
||||
|
||||
In order to install new coins onto a Lamassu system, there are three points to pay attention:
|
||||
- **The blockchain daemon:** This is a file which will need to be running on the lamassu-server and communicates with the blockchain network. This generally has an integrated wallet, but it may occur for the daemon and the wallet to be completely seperate processes (e.g. XMR). This manager is currently built into the lamassu-server project, but in the future, it will be moved to either lamassu-coins or a new library;
|
||||
- **The wallet plugin:** This uses the capabilities of the RPC (Remote Procedure Call) API built into the blockchain daemon and wallet to create a linking API to standardize with the Lamassu ecosystem. It is built into the lamassu-server project;
|
||||
- **The coin constants:** This has all the information about an implemented coin, including its code, unit scale, daemon RPC ports and all other information to make lamassu-server, lamassu-admin-server and lamassu-machine know about the supported coins. It is built into the lamassu-coins package;
|
||||
|
||||
I'll be using XMR as example for all the steps in this guide.
|
||||
|
||||
#### Blockchain
|
||||
|
||||
Steps to implement a daemon:
|
||||
- Create a file in `lamassu-server/lib/blockchain/<name_of_coin>.js`;
|
||||
- Go to `lamassu-server/lib/blockchain/common.js` and add a new entry to the `BINARIES` object. Each entry has two mandatory fields (`url` and `dir`), and an optional one (`files`).
|
||||
- To get the `url` needed to download the blockchain daemon, you need to access the releases of the daemon of the coin you're working with. For example, for XMR, the daemon can be found in their GitHub releases (https://github.com/monero-project/monero-gui/releases). Get the URL for the Linux 64-bit distribution and note the extension of the file, which will most likely be `.tar.gz` or `.tar.bz2`. For `.tar.bz2`, the coin you're working with needs to be added to the following snippet of code, responsible for the extraction of the downloaded file (`common.js`):
|
||||
```
|
||||
coinRec.cryptoCode === 'XMR'
|
||||
? es(`tar -xf ${downloadFile}`)
|
||||
: es(`tar -xzf ${downloadFile}`)
|
||||
```
|
||||
- To get the `dir`, simply download the file, extract it, and take note of the folder inside the zipped file and the path towards the actual files you want. In XMR's case, `dir` = `monero-x86_64-linux-gnu-v0.17.2.0`, but for BTC it is `bitcoin-0.20.1/bin`
|
||||
- Inside the directory specified in the `dir` field, there can be multiple files inside. In that case, you want to specity the `files` field. This is a multi-dimensional array, where each entry contains a pair of [<file_in_the_downloaded_folder>, <name_with_with_the_file_is_saved_in_the_server>].
|
||||
```
|
||||
[
|
||||
['monerod', 'monerod'],
|
||||
['monero-wallet-rpc', 'monero-wallet-rpc']
|
||||
]
|
||||
```
|
||||
This means that the `monerod` found inside the distribution folder will be saved as `monerod` on the server. Same for the `monero-wallet-rpc`.
|
||||
- Go to `lamassu-server/lib/blockchain/install.js` and add a new entry on the `PLUGINS` object. This entry must import the file created in step 1.
|
||||
- Go to the file created in step one and import the object (which isn't created right now) containing all the information needed of a coin `const coinRec = utils.getCryptoCurrency('<coin_code>')`.
|
||||
- The coin blockchain plugin contains two functions: `setup` and `buildConfig`.
|
||||
- The build config has all the required flags to operate the downloaded daemon, and each coin has their particular set of flags and options, so that specification won't be covered here.
|
||||
- The setup function has a similar structure in any coin, and the differences between them is generally related to how a daemon is ran.
|
||||
|
||||
#### Wallet plugin
|
||||
|
||||
Steps to implement a wallet plugin:
|
||||
- Create a file in `lamassu-server/lib/plugins/wallet/<name_of_daemon>/<name_of_daemon>.js`
|
||||
- The wallet plugin serves as a middleware between the RPC calls supported by each daemon, and the processes ran inside the lamassu-server ecosystem. This includes address creation, balance lookup, making transactions, etc. As such, this file needs to export the following functions:
|
||||
- `balance`: Responds with the amount of usable balance the operator wallet has;
|
||||
- `sendCoins`: Responsible for creating a transaction and responds with an object containing the fee of the transaction and the transactionID;
|
||||
- `newAddress`: Generates a new address for the operator wallet. Used for machine transactions and funding page;
|
||||
- `getStatus`: Responsible for getting the status of a cash-out transaction (transaction from an operator address to a client address).
|
||||
- `newFunding`: Creates the response to the funding page, with the amount of balance the operator has, the pending balance and a new funding address;
|
||||
- `cryptoNetwork`: Responds with the crypto network the wallet is operating in, based on the port of the RPC calls used.
|
||||
|
||||
#### Coin utils
|
||||
|
||||
Steps to work on lamassu-coins:
|
||||
- Create a new object on `lamassu-coins/config/consts.js` containing all the information relative to a coin. If you're using a wallet built into the daemon, use BTC as template. Otherwise, if the wallet process is separated from the daemon, use XMR as template;
|
||||
- Create a new file on `lamassu-coins/plugins/<coin_code>.js`. This file should handle URL parsing and address validation. Despite most coins in lamassu-coins operating on base58 or bech32 validation, this validation can implemented on some variation of the existing algorithms, as is the case with XMR. When this happens, the implementation of this variation needs to be created from scratch. With the validator created, the machine should be able to recognize a valid address. To test this out, simply edit the `lamassu-machine/device-config.json` file with the new coin address to validate.
|
||||
121
LICENSE
Normal file
121
LICENSE
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
APPENDIX A SOFTWARE LICENSE AGREEMENT
|
||||
|
||||
This Appendix A Software License Agreement (hereinafter referred to as the "Agreement") is made between Lamassu Industries AG, a Swiss corporation, (hereinafter referred to as "Lamassu") and Lamassu’s customer to whom Lamassu’s proprietary software or products containing embedded or pre-loaded proprietary software, or both is made available (hereinafter referred to as "Licensee"). Licensee and Lamassu enter into an agreement to which this appendix is attached (hereinafter referred to as the "Primary Agreement").
|
||||
|
||||
WHEREAS, Lamassu has developed and is the owner of the Lamassu software program that runs Lamassu Crypto ATMs (hereinafter referred to as the "Software"). Crypto ATMs means automated teller machines that allow a person to purchase or sell Bitcoin and other cryptocurrencies by using cash or debit card (hereinafter referred to as the "Crypto ATMs"). Software (i) means proprietary software in source code or object code format, and adaptations, translations, de-compilations, disassemblies, emulations, or derivative works of such software; (ii) means any modifications, enhancements, new versions and new releases of the software provided by Lamassu; and (iii) may contain one or more items of software owned by a third party supplier. The term "Software" does not include any third party software provided under separate license or third party software not licensable under the terms of this Agreement.
|
||||
|
||||
WHEREAS, Lamassu desires to grant to Licensee, and Licensee desires to obtain from Lamassu, a limited, non-exclusive license to use the Software subject to terms and conditions set forth in this Agreement.
|
||||
|
||||
NOW, THEREFORE, in consideration of the mutual promises set forth herein, Lamassu and Licensee (hereinafter referred to as the "Parties") agree as follows:
|
||||
|
||||
1. Scope
|
||||
|
||||
1.1. Lamassu and Licensee enter into this Agreement in connection with Lamassu's delivery of certain proprietary Software or products containing embedded or pre-loaded proprietary Software, or both. This Agreement contains the terms and conditions of the license Lamassu is providing to Licensee, and Licensee’s use of the Software and documentation.
|
||||
|
||||
1.2. The license granted in this Agreement is on a per Lamassu product basis and/or per non Lamassu Crypto ATM basis according to the Primary Agreement. For example, if Licensee intends to use the Software on 10 Lamassu Crypto ATMs, Licensee must obtain and pay for 10 license seats in accordance with the Primary Agreement. If Licensee purchases only one Lamassu Crypto ATM, Licensee obtains only one license seat for the Software, Licensee is permitted to use the Software on the purchased Lamassu Crypto ATM but not on other Lamassu and/or non Lamassu Crypto ATMs owned by Licensee, unless additional license seats are obtained and paid by Licensee. The same applies if Licensee intends to use the Software on non Lamassu Crypto ATMs. Licensee must obtain and pay a separate license seat for each non Lamassu Crypto ATM in accordance with the Primary Agreement.
|
||||
|
||||
2. Granting of License
|
||||
|
||||
2.1. Subject to the provisions of this Agreement and the payment of applicable fees outlined in the Primary Agreement, Lamassu grants to Licensee a personal, limited, non-transferable, non-sublicensable and non-exclusive license under Lamassu copyrights embodied in the Software to use the Software, in original source code or object code form, and the documentation solely in connection with Licensee's use of the Lamassu products or to modify, adapt, create derivative works of the Software for the purpose of running it on non Lamassu Crypto ATMs (hereinafter referred to as the "Contractual Use").
|
||||
|
||||
2.2. If the Software licensed under this Agreement contains or is derived from Open Source Software, the terms and conditions governing the use of such Open Source Software are in the Open Source Software Licenses of the copyright owner and not this Agreement. If there is a conflict between the terms and conditions of this Agreement and the terms and conditions of the Open Source Software Licenses governing Licensee's use of the Open Source Software, the terms and conditions of the license grant of the applicable Open Source Software Licenses will take precedence over the license grants in this Agreement.
|
||||
|
||||
2.3. Licensee may copy the Software, so far as a copy is necessary for the Contractual Use of the Software. Necessary copies are for example, the installation of the Software on a storage medium and loading the Software into the working memory. Licensee are also entitled to make a reasonable number of copies of the Software for backup purposes only. This right also includes the regular production of backup copies for the purpose of quickly restoring data stocks after a system failure and the temporary use of the Software on an alternative system. Copies of the Software made for backup purposes must be marked accordingly and may not be used for other purposes. Licensee is entitled to edit and modify the Software (including error corrections) but is not permitted to publish any version of the Software with modifications with regard to the Source Code, the attributions and/ or any other part of the Software.
|
||||
|
||||
2.4. Lamassu reserves for itself all other rights and interest not explicitly granted under this Agreement. The right of Licensee to decrypt according to Art. 21 URG is reserved.
|
||||
|
||||
3. Limitations on Use
|
||||
|
||||
3.1. Licensee may use the Software only for (i) Licensee's internal business purposes, (ii) the Contractual Use and (iii) only in accordance with the documentation. Any other use of the Software is strictly prohibited. Without limiting the general nature of these restrictions, Licensee will not make the Software available for use by third parties on a "time sharing," "application service provider," or "service bureau" basis or for any other similar commercial rental or sharing arrangement.
|
||||
|
||||
3.2. Licensee will not, and will not allow or enable any third party to: (i) reverse engineer, disassemble, peel components, decompile, reprogram or otherwise reduce the Software or any portion to a human perceptible form or otherwise attempt to recreate the source code; (ii) modify, adapt, create derivative works of, or merge the Software outside of the Contractual Use; (iii) copy, reproduce, distribute, lend, or lease the Software or documentation to any third party, grant any sublicense or other rights in the Software or documentation to any third party, or take any action that would cause the Software or documentation to be placed in the public domain; (iv) remove, or in any way alter or obscure, any copyright notice or other notice of Lamassu's proprietary rights; or (v) provide, copy, transmit, disclose, divulge or make the Software or documentation available to any third party.
|
||||
|
||||
4. Delivery of the Software
|
||||
|
||||
4.1. The delivery of the Software takes place with consultation between the Parties. Lamassu shall register Licensee within its system and provide it with an access code.
|
||||
|
||||
4.2. Licensee shall download the Software through their own access code and install it on their own server or Crypto ATM. Licensee shall not alter the Software's functionality or provide its access code to third parties. Licensee is responsible for maintaining the confidentiality of their access data, their account information and all activities resulting from accessing the Software using their username and access data.
|
||||
|
||||
4.3. Licensee is responsible for the installation and the launch of the Software.
|
||||
|
||||
5. Updates & upgrades & bug fixes
|
||||
|
||||
5.1. Licensee agrees and acknowledges that malfunctions of the Software cannot be completely excluded, even with the greatest care, and that the uninterrupted functionality of the Software cannot be guaranteed.
|
||||
|
||||
5.2. Lamassu shall provide Licensee free of charge with all updates, upgrades, bug fixes and code corrections to correct Software malfunctions and defects in order to bring the Software into substantial conformity with its operating specifications.
|
||||
|
||||
5.3. Lamassu may, but will not be required to, provide these services if Licensee has modified the Software or is in default.
|
||||
|
||||
5.4. The ownership and any intellectual property rights to the work results continuously created by providing updates, upgrades and bug fixes belong fully and exclusively to Lamassu.
|
||||
|
||||
5.5. If Licensee provides Lamassu with any feedback, ideas or suggestions regarding the Software, Lamassu may use all such feedback without restriction and shall not be subject to any non-disclosure or non-use obligations in respect of such feedback, and Licensee hereby grants Lamassu a worldwide, non-exclusive, perpetual, irrevocable, royalty-free, fully paid, sub-licensable and transferable license to use, edit, aggregate, reproduce, distribute, prepare derivative works of, display, perform, and otherwise fully exploit such feedback, ideas or suggestions regarding the Software, for any use or purpose whatsoever.
|
||||
|
||||
6. Warranty of Title and Functionality
|
||||
|
||||
6.1. Lamassu hereby represents and warrants to Licensee that Lamassu is the owner of the Software and has the right to grant to Licensee the rights set forth in this Agreement.
|
||||
|
||||
6.2. If Licensee is not in breach of any of its obligations under this Agreement and the Primary Agreement, Lamassu warrants that the Software will perform the functions described in this Agreement if used in accordance with the Agreement. Failure to do so shall constitute a defect in the Software that is subject to warranty (hereinafter referred to as the "Defect"). This warranty shall not apply to the Software if modified by anyone, even if such modification is allowed under the terms of this Agreement, or if used improperly or in an operating environment not approved by Lamassu.
|
||||
|
||||
6.3. Lamassu does not warrant that Licensee's use of the Software or the Lamassu products will be uninterrupted, error-free, completely free of security vulnerabilities, or that the Software or the Lamassu products will meet Licensee's particular requirements. Lamassu makes no representations or warranties with respect to any third party software included in the Software. Licensee explicitly agrees and acknowledges that Lamassu does not warrant that the Software will satisfy or fulfill any regulators, customers and/or other authorities' requirements and expectations. It's in the sole responsibility of Licensee to clarify with the competent regulators whether the Software and other implemented measures fulfil the applicable requirements.
|
||||
|
||||
6.4. Licensee acknowledges, however, that malfunctions of the Software cannot be completely ruled out, even with the greatest care, and that the uninterrupted functionality of the Software cannot be guaranteed.
|
||||
|
||||
6.5. Lamassu's sole obligation to Licensee and Licensee's exclusive remedy under this warranty is to use reasonable efforts to remedy any material Software defect covered by this warranty. These efforts will involve either replacing the media or attempting to correct significant, demonstrable program or documentation errors or security vulnerabilities. If Lamassu cannot correct the defect within a reasonable time, then at Lamassu's option, Lamassu will replace the defective Software with functionally-equivalent Software, license to Licensee substitute Software which will accomplish the same objective, or terminate the license and refund Licensee's paid license fee.
|
||||
|
||||
6.6. Licensee must give Lamassu sufficient notice of any defect within 5 calendar days of its discovery.
|
||||
|
||||
6.7. The express warranties set forth in this Section 6 are in lieu of, and Lamassu disclaims, any and all other warranties (express or implied, oral or written) with respect to the Software or documentation, including, without limitation, any and all implied warranties of condition, title, non-infringement, merchantability, or fitness for a particular purpose or use by Licensee (whether or not Lamassu knows, has reason to know, has been advised, or is otherwise aware of any such purpose or use), whether arising by law, by reason of custom or usage of trade, or by course of dealing. In addition, Lamassu disclaims any warranty to any person other than Licensee with respect to the Software or documentation. Any further warranty claims of Licensee are expressly excluded.
|
||||
|
||||
7. Limitation of Liability
|
||||
|
||||
7.1. Lamassu shall not be responsible for, and shall not pay, any amount of incidental, consequential or other indirect damages, whether based on lost revenue or otherwise, regardless of whether Licensee was advised of the possibility of such losses in advance by Lamassu.
|
||||
|
||||
7.2. In no event shall Lamassu's liability hereunder exceed the amount paid by Licensee under the Primary Agreement, regardless of whether Licensee’s claim is based on contract, tort, strict liability, product liability or otherwise.
|
||||
|
||||
7.3. This limitation of liability provision survives the expiration or termination of this Agreement and applies notwithstanding any contrary provision in this Agreement.
|
||||
|
||||
8. Term and termination
|
||||
|
||||
8.1. Licensee's right to use the Software and documentation will begin when the Primary Agreement is signed by both Parties and will continue (i) for the life of the Lamassu Products with which or for which the Software and documentation have been provided by Lamassu, unless Licensee breaches this Agreement, in which case this Agreement and Licensee's right to use the Software and documentation may be terminated immediately upon notice by Lamassu or (ii) for the duration of the Primary Agreement, unless Licensee breaches this Agreement or the Primary Agreement, in which case this Agreement and Licensee's right to use the Software and documentation may be terminated immediately upon notice by Lamassu.
|
||||
|
||||
8.2. After termination of this Agreement, Licensee shall no longer use the Software. Upon first request by Lamassu, Licensee shall remove or delete and destroy all copies of the Software and documentation.
|
||||
|
||||
8.3. Licensee acknowledges that Lamassu made a considerable investment of resources in the development, marketing, and distribution of the Software and documentation and that Licensee's breach of this Agreement will result in irreparable harm to Lamassu for which solely monetary damages would be inadequate. If Licensee breaches this Agreement, Lamassu may terminate this Agreement and be entitled to all available remedies at law or in equity (including immediate injunctive relief and repossession of all non-embedded Software and associated documentation).
|
||||
|
||||
8.4. Sections 3, 6, 7, 9, 10, 11, 12, 13, 14, and 15 survive the termination of this Agreement.
|
||||
|
||||
9. Intellectual Property Rights
|
||||
|
||||
9.1. The Software is and includes intellectual property of Lamassu. All associated intellectual property rights, including, without limitation, worldwide patent, trademark, copyright and trade secret rights, are reserved by Lamassu. Lamassu retains all right, title and interest in and copyrights to the Software, regardless of form or media in or on which the original or other copies may subsequently exist. This Agreement does not constitute a sale of the Software and no title or proprietary rights to the Software are transferred to Licensee hereby. Licensee acknowledges that the Software is a unique, confidential and valuable asset of Lamassu.
|
||||
|
||||
9.2. All intellectual property developed, originated, or prepared by Lamassu in connection with providing the Software, Lamassu products, documentation or related services, remains vested exclusively in Lamassu, and Licensee will not have any shared development or other intellectual property rights.
|
||||
|
||||
10. Confidentiality
|
||||
|
||||
10.1. Licensee acknowledges that the Software contains proprietary trade secrets of Lamassu and Licensee hereby agrees to maintain the confidentiality of the Software using at least as great a degree of care as it uses to maintain the confidentiality of its own confidential information. Licensee agrees to promptly communicate the terms and conditions of this Agreement to those persons employed by Licensee who come into contact with the Software. Licensee is responsible in the event of a breach of confidentiality by any of its employees or agents. Licensee shall use reasonable efforts to ensure its compliance with its confidentiality obligations under this Agreement, including, without limitation, preventing the use of any portion of the Software for the purpose of deriving the source code of the Software.
|
||||
|
||||
11. Successors
|
||||
|
||||
11.1. This Agreement will be binding upon and will inure to the benefit of the Parties hereto and their respective representatives, successors and assigns except as otherwise provided herein.
|
||||
|
||||
12. Severability
|
||||
|
||||
12.1. In the event any provision of this Agreement is determined to be invalid or unenforceable, the remainder of this Agreement shall remain in force as if such provision were not a part of it.
|
||||
|
||||
13. Non-assignment
|
||||
|
||||
13.1. This Agreement, any claims and the licenses granted by it may not be assigned, sublicensed, or otherwise transferred by Licensee without the prior written consent of Lamassu.
|
||||
|
||||
13.2. This Agreement and any claims hereunder may not be assigned by Lamassu to any third party without the prior written consent of Licensee.
|
||||
|
||||
14. Agreement
|
||||
|
||||
14.1. This Agreement sets forth the entire understanding between the Parties with respect to the subject matter hereof, and merges and supersedes all prior agreements, discussions and understandings, express or implied, concerning such matters. This Agreement shall take precedence over any additional or conflicting term, which may be contained in Licensees' purchase order or Lamassu‘s order acknowledgment forms.
|
||||
|
||||
15. Governing law / place of jurisdiction
|
||||
|
||||
15.1. This Agreement shall be governed by and construed in accordance with the substantive laws of Switzerland under exclusion of its conflict of laws rules. The United Nations Convention on Contracts on the International Sale of Goods is expressly excluded.
|
||||
|
||||
15.2. The Parties hereby irrevocably and unconditionally agree to the exclusive jurisdiction of the courts of Lucerne, Switzerland.
|
||||
|
||||
IN WITNESS WHEREOF, the Parties have caused this Agreement to be executed.
|
||||
62
README.md
Normal file
62
README.md
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# lamassu-server
|
||||
|
||||
Lamassu remote server.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
We do not generally accept outside pull requests for new features. Please consult with us before putting a lot of work into a pull request.
|
||||
|
||||
## Development
|
||||
|
||||
### Requirements
|
||||
|
||||
- Nodejs 22
|
||||
- PNPM 10+
|
||||
- Postgres Database
|
||||
- Python 3 (to be deprecated, required by a single dependency installation)
|
||||
- OpenSSL (for cert-gen.sh, it will set up the server self-signed certificates)
|
||||
|
||||
There's a shell.nix file that you can use to set up your env in case you're a nix user. (most reliable way of installing native deps)
|
||||
There's also a .tool-versions for asdf and mise users.
|
||||
|
||||
This project uses Turbo for monorepo management. Install dependencies:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
Prepare environment files:
|
||||
|
||||
```bash
|
||||
bash packages/server/tools/cert-gen.sh
|
||||
```
|
||||
|
||||
On packages/server/.env you can alter variables such as the postgres connection info.
|
||||
|
||||
After configuring the postgres connection, run:
|
||||
|
||||
```bash
|
||||
node packages/server/bin/lamassu-migrate
|
||||
```
|
||||
|
||||
### Start development environment:
|
||||
|
||||
If you've already done the setup, you can run:
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
### Creating a user
|
||||
|
||||
```bash
|
||||
node packages/server/bin/lamassu-register admin@example.com superuser
|
||||
```
|
||||
|
||||
### Pairing a machine
|
||||
|
||||
To get the pairing token from the QRCode open the browser console before picking the name of the machine, the token should appear on the terminal.
|
||||
It's also possible to inspect the qrCode, the token is on the data-cy="" attr.
|
||||
Lastly, you can always scan it with a phone and copy the contents over.
|
||||
|
||||
Now continue with lamassu-machine instructions from the `INSTALL.md` file in [lamassu-machine repository](https://github.com/lamassu/lamassu-machine)
|
||||
55
build/docker-compose.yaml
Normal file
55
build/docker-compose.yaml
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
version: "3.8"
|
||||
|
||||
services:
|
||||
lamassu-server:
|
||||
image: lamassu/lamassu-server:latest
|
||||
restart: on-failure
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ./lamassu-data:/lamassu-data
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres123
|
||||
- POSTGRES_HOST=localhost
|
||||
- POSTGRES_PORT=5432
|
||||
- POSTGRES_DB=lamassu
|
||||
- CA_PATH=/lamassu-data/certs/Lamassu_OP_Root_CA.pem
|
||||
- CERT_PATH=/lamassu-data/certs/Lamassu_OP.pem
|
||||
- KEY_PATH=/lamassu-data/private/Lamassu_OP.key
|
||||
- MNEMONIC_PATH=/lamassu-data/mnemonics/mnemonic.txt
|
||||
- OFAC_DATA_DIR=/lamassu-data/ofac
|
||||
- ID_PHOTO_CARD_DIR=/lamassu-data/idphotocard
|
||||
- FRONT_CAMERA_DIR=/lamassu-data/frontcamera
|
||||
- OPERATOR_DATA_DIR=/lamassu-data/operatordata
|
||||
- COIN_ATM_RADAR_URL=https://coinatmradar.info/api/lamassu/
|
||||
- HOSTNAME=localhost
|
||||
- LOG_LEVEL=info
|
||||
|
||||
lamassu-admin-server:
|
||||
image: lamassu/lamassu-admin-server:latest
|
||||
restart: on-failure
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ./lamassu-data:/lamassu-data
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres123
|
||||
- POSTGRES_HOST=localhost
|
||||
- POSTGRES_PORT=5432
|
||||
- POSTGRES_DB=lamassu
|
||||
- CA_PATH=/lamassu-data/certs/Lamassu_OP_Root_CA.pem
|
||||
- CERT_PATH=/lamassu-data/certs/Lamassu_OP.pem
|
||||
- KEY_PATH=/lamassu-data/private/Lamassu_OP.key
|
||||
- MNEMONIC_PATH=/lamassu-data/mnemonics/mnemonic.txt
|
||||
- OFAC_DATA_DIR=/lamassu-data/ofac
|
||||
- ID_PHOTO_CARD_DIR=/lamassu-data/idphotocard
|
||||
- FRONT_CAMERA_DIR=/lamassu-data/frontcamera
|
||||
- OPERATOR_DATA_DIR=/lamassu-data/operatordata
|
||||
- COIN_ATM_RADAR_URL=https://coinatmradar.info/api/lamassu/
|
||||
- HOSTNAME=172.29.0.3
|
||||
- LOG_LEVEL=info
|
||||
depends_on:
|
||||
lamassu-server:
|
||||
condition: service_started
|
||||
17
build/server.Dockerfile
Normal file
17
build/server.Dockerfile
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
FROM node:22-bullseye AS base
|
||||
RUN apt install openssl ca-certificates
|
||||
|
||||
WORKDIR /lamassu-server
|
||||
|
||||
# Copy the pre-built production package from CI (with node_modules)
|
||||
COPY . ./
|
||||
|
||||
FROM base AS l-s
|
||||
RUN chmod +x /lamassu-server/bin/lamassu-server-entrypoint.sh
|
||||
EXPOSE 3000
|
||||
ENTRYPOINT [ "/lamassu-server/bin/lamassu-server-entrypoint.sh" ]
|
||||
|
||||
FROM base AS l-a-s
|
||||
RUN chmod +x /lamassu-server/bin/lamassu-admin-server-entrypoint.sh
|
||||
EXPOSE 443
|
||||
ENTRYPOINT [ "/lamassu-server/bin/lamassu-admin-server-entrypoint.sh" ]
|
||||
85
eslint.config.mjs
Normal file
85
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import pluginReact from 'eslint-plugin-react'
|
||||
import json from '@eslint/json'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
import ts from 'typescript-eslint'
|
||||
import reactCompiler from 'eslint-plugin-react-compiler'
|
||||
import eslintConfigPrettier from 'eslint-config-prettier/flat'
|
||||
import vitest from 'eslint-plugin-vitest'
|
||||
|
||||
const jsConfig = defineConfig([
|
||||
globalIgnores([
|
||||
'**/.lamassu',
|
||||
'**/build',
|
||||
'**/package.json',
|
||||
'**/package-lock.json',
|
||||
'**/currencies.json',
|
||||
'**/countries.json',
|
||||
'**/languages.json',
|
||||
'packages/typesafe-db/lib/**/*.js'
|
||||
]),
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs,jsx}'],
|
||||
plugins: { js },
|
||||
extends: ['js/recommended'],
|
||||
},
|
||||
{
|
||||
files: ['packages/admin-ui/**/*.{js,mjs,jsx}'],
|
||||
languageOptions: {
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
...globals.browser,
|
||||
process: 'readonly',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/server/**/*.{js,cjs}'],
|
||||
languageOptions: { sourceType: 'commonjs', globals: globals.node },
|
||||
},
|
||||
{
|
||||
...pluginReact.configs.flat.recommended,
|
||||
settings: { react: { version: 'detect' } },
|
||||
files: ['packages/admin-ui/**/*.{jsx,js}'],
|
||||
},
|
||||
{ ...reactCompiler.configs.recommended },
|
||||
eslintConfigPrettier,
|
||||
{
|
||||
files: ['**/*.json'],
|
||||
plugins: { json },
|
||||
language: 'json/json',
|
||||
extends: ['json/recommended'],
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'react/prop-types': 'off',
|
||||
'react/display-name': 'off',
|
||||
'react/no-unescaped-entities': 'off',
|
||||
'react-compiler/react-compiler': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
// update this to match your test files
|
||||
files: ['**/*.spec.js', '**/*.test.js'],
|
||||
plugins: {
|
||||
vitest,
|
||||
},
|
||||
rules: {
|
||||
...vitest.configs.recommended.rules, // you can also use vitest.configs.all.rules to enable all rules
|
||||
'vitest/max-nested-describe': ['error', { max: 3 }], // you can also modify rules' behavior using option like this
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
export default ts.config(
|
||||
jsConfig,
|
||||
{
|
||||
files: ['packages/typesafe-db/src/**/*.ts'],
|
||||
extends: [ts.configs.recommended],
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "warn", // TODO: remove this line
|
||||
"@typescript-eslint/consistent-type-imports": "error",
|
||||
},
|
||||
},
|
||||
)
|
||||
49
package.json
Normal file
49
package.json
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"name": "lamassu-server",
|
||||
"description": "bitcoin atm client server protocol module",
|
||||
"version": "12.0.0",
|
||||
"license": "./LICENSE",
|
||||
"author": "Lamassu (https://lamassu.is)",
|
||||
"packageManager": "pnpm@10.11.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/lamassu/lamassu-server.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/css": "^0.7.0",
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@eslint/json": "^0.12.0",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-compiler": "^19.1.0-rc.1",
|
||||
"eslint-plugin-vitest": "^0.5.4",
|
||||
"globals": "^16.1.0",
|
||||
"husky": "^8.0.0",
|
||||
"lint-staged": "^16.0.0",
|
||||
"prettier": "^3.5.3",
|
||||
"turbo": "^2.5.3",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.38.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "husky install",
|
||||
"build": "turbo build",
|
||||
"dev": "turbo dev",
|
||||
"test": "turbo test"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx,json,css,scss,md}": [
|
||||
"prettier --write",
|
||||
"eslint --fix"
|
||||
]
|
||||
}
|
||||
}
|
||||
23
packages/admin-ui/.gitignore
vendored
Normal file
23
packages/admin-ui/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
.eslintcache
|
||||
36
packages/admin-ui/README.md
Normal file
36
packages/admin-ui/README.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
## Dev Environment
|
||||
|
||||
### formatting
|
||||
|
||||
You can configure a eslint plugin to format code on save.
|
||||
The configuration for vscode is already on the repo, all you need to do is install the eslint plugin.
|
||||
|
||||
This project has a husky pre commit hook to format the staged changes using our styleguide.
|
||||
To take advantage of that make sure to run `git commit` from within this folder.
|
||||
|
||||
## Available Scripts
|
||||
|
||||
From the root directory (recommended with Turbo):
|
||||
|
||||
- `pnpm run dev` - Start development environment
|
||||
- `pnpm run build` - Build for production
|
||||
- `pnpm run admin:dev` - Start only admin UI development
|
||||
|
||||
In the admin-ui package directory, you can run:
|
||||
|
||||
### `pnpm start` or `pnpm run dev`
|
||||
|
||||
Runs the app in development mode with Vite.
|
||||
Open [http://localhost:5173](http://localhost:5173) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `pnpm test`
|
||||
|
||||
Launches the test runner with vitest.
|
||||
|
||||
### `pnpm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
20
packages/admin-ui/index.html
Normal file
20
packages/admin-ui/index.html
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||
/>
|
||||
<meta name="robots" content="noindex"/>
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<title>Lamassu Admin</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root" class="root"></div>
|
||||
<script type="module" src="/src/index.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
81
packages/admin-ui/package.json
Normal file
81
packages/admin-ui/package.json
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
{
|
||||
"name": "lamassu-admin",
|
||||
"version": "12.0.0",
|
||||
"license": "../LICENSE",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.13.8",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@lamassu/coins": "v1.6.1",
|
||||
"@mui/icons-material": "^7.1.0",
|
||||
"@mui/material": "^7.1.0",
|
||||
"@mui/x-date-pickers": "^8.3.1",
|
||||
"@simplewebauthn/browser": "^3.0.0",
|
||||
"apollo-upload-client": "^18.0.0",
|
||||
"bignumber.js": "9.0.0",
|
||||
"classnames": "2.2.6",
|
||||
"countries-and-timezones": "^2.4.0",
|
||||
"d3": "^6.2.0",
|
||||
"date-fns": "^2.26.0",
|
||||
"date-fns-tz": "^1.1.6",
|
||||
"downshift": "9.0.9",
|
||||
"file-saver": "2.0.2",
|
||||
"formik": "2.2.0",
|
||||
"immer": "^10.1.1",
|
||||
"jss-plugin-extend": "^10.0.0",
|
||||
"jszip": "^3.6.0",
|
||||
"libphonenumber-js": "^1.11.15",
|
||||
"match-sorter": "^4.2.0",
|
||||
"material-react-table": "^3.2.1",
|
||||
"pretty-ms": "^2.1.0",
|
||||
"qrcode.react": "4.2.0",
|
||||
"ramda": "^0.30.1",
|
||||
"react": "18.3.1",
|
||||
"react-copy-to-clipboard": "^5.0.2",
|
||||
"react-dom": "18.3.1",
|
||||
"react-dropzone": "^11.4.2",
|
||||
"react-number-format": "^4.4.1",
|
||||
"react-otp-input": "3.1.1",
|
||||
"react-virtualized": "^9.21.2",
|
||||
"ua-parser-js": "1.0.40",
|
||||
"uuid": "11.1.0",
|
||||
"wouter": "^3.7.0",
|
||||
"yup": "1.6.1",
|
||||
"zustand": "^4.5.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"@vitejs/plugin-react-swc": "^3.7.2",
|
||||
"esbuild-plugin-react-virtualized": "^1.0.4",
|
||||
"globals": "^15.13.0",
|
||||
"prettier": "3.4.1",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"vite": "^6.0.1",
|
||||
"vite-plugin-svgr": "^4.3.0",
|
||||
"vitest": "^3.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,md,json}": "eslint --cache --fix",
|
||||
"*.{js,jsx,css,md,json}": "prettier --write"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
BIN
packages/admin-ui/public/assets/wizard/fullexample.locale.png
Normal file
BIN
packages/admin-ui/public/assets/wizard/fullexample.locale.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
BIN
packages/admin-ui/public/assets/wizard/fullexample.twilio.png
Normal file
BIN
packages/admin-ui/public/assets/wizard/fullexample.twilio.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 160 KiB |
BIN
packages/admin-ui/public/assets/wizard/fullexample.wallet.png
Normal file
BIN
packages/admin-ui/public/assets/wizard/fullexample.wallet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
BIN
packages/admin-ui/public/favicon.ico
Normal file
BIN
packages/admin-ui/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
packages/admin-ui/public/fonts/BPmono/BPmono.ttf
Normal file
BIN
packages/admin-ui/public/fonts/BPmono/BPmono.ttf
Normal file
Binary file not shown.
BIN
packages/admin-ui/public/fonts/BPmono/BPmonoBold.ttf
Normal file
BIN
packages/admin-ui/public/fonts/BPmono/BPmonoBold.ttf
Normal file
Binary file not shown.
BIN
packages/admin-ui/public/fonts/BPmono/BPmonoItalic.ttf
Normal file
BIN
packages/admin-ui/public/fonts/BPmono/BPmonoItalic.ttf
Normal file
Binary file not shown.
BIN
packages/admin-ui/public/fonts/MontHeavy/mont-bold-webfont.woff
Normal file
BIN
packages/admin-ui/public/fonts/MontHeavy/mont-bold-webfont.woff
Normal file
Binary file not shown.
BIN
packages/admin-ui/public/fonts/MontHeavy/mont-bold-webfont.woff2
Normal file
BIN
packages/admin-ui/public/fonts/MontHeavy/mont-bold-webfont.woff2
Normal file
Binary file not shown.
BIN
packages/admin-ui/public/fonts/MontHeavy/mont-heavy-webfont.woff
Normal file
BIN
packages/admin-ui/public/fonts/MontHeavy/mont-heavy-webfont.woff
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
packages/admin-ui/public/fonts/Rubik/Rubik-Black.otf
Normal file
BIN
packages/admin-ui/public/fonts/Rubik/Rubik-Black.otf
Normal file
Binary file not shown.
BIN
packages/admin-ui/public/fonts/Rubik/Rubik-Bold.otf
Normal file
BIN
packages/admin-ui/public/fonts/Rubik/Rubik-Bold.otf
Normal file
Binary file not shown.
BIN
packages/admin-ui/public/fonts/Rubik/Rubik-Medium.otf
Normal file
BIN
packages/admin-ui/public/fonts/Rubik/Rubik-Medium.otf
Normal file
Binary file not shown.
15
packages/admin-ui/public/manifest.json
Normal file
15
packages/admin-ui/public/manifest.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
2
packages/admin-ui/public/robots.txt
Normal file
2
packages/admin-ui/public/robots.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow: /
|
||||
204
packages/admin-ui/public/wizard-background.svg
Normal file
204
packages/admin-ui/public/wizard-background.svg
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="1440px" height="800px" viewBox="0 0 1440 800" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>welcome-page-background</title>
|
||||
<defs>
|
||||
<polygon id="path-1" points="0.620193054 0 101.619191 0 101.619191 100.386818 0.620193054 100.386818"></polygon>
|
||||
<polygon id="path-3" points="0.0225128411 0.23984597 99.9173917 0.23984597 99.9173917 98.7927534 0.0225128411 98.7927534"></polygon>
|
||||
<polygon id="path-5" points="0.80162247 0.532954466 522.863527 0.532954466 522.863527 522.05824 0.80162247 522.05824"></polygon>
|
||||
<polygon id="path-7" points="0 0.00538631356 325.54897 0.00538631356 325.54897 316.901388 0 316.901388"></polygon>
|
||||
<polygon id="path-9" points="0 0.0986886392 325.54897 0.0986886392 325.54897 317.206983 0 317.206983"></polygon>
|
||||
</defs>
|
||||
<g id="welcome-page-background" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Group-103" transform="translate(156.000000, -89.000000)">
|
||||
<path d="M229.204348,254 C229.204348,278.853349 209.057697,299 184.204348,299 C159.350998,299 139.204348,278.853349 139.204348,254 C139.204348,229.147776 159.350998,209 184.204348,209 C209.057697,209 229.204348,229.147776 229.204348,254" id="Fill-1" fill="#CCD8FF"></path>
|
||||
<path d="M228.204348,248 C228.204348,272.852224 208.056572,293 183.204348,293 C158.350998,293 138.204348,272.852224 138.204348,248 C138.204348,223.147776 158.350998,203 183.204348,203 C208.056572,203 228.204348,223.147776 228.204348,248" id="Fill-3" fill="#EBEFFF"></path>
|
||||
<path d="M205.545763,271.344802 C208.506388,268.344798 210.87851,264.771048 212.466337,260.875909 C214.051901,256.977376 214.801111,252.763112 214.750183,248.580534 C214.614375,240.187087 210.948677,232.043734 204.975368,226.385485 C201.992108,223.554097 198.4758,221.321352 194.676558,219.85926 C190.880711,218.383589 186.810984,217.712521 182.779736,217.770235 C174.700263,217.893585 166.789419,221.241005 161.146577,226.949047 C155.436962,232.58919 152.081361,240.500554 151.959134,248.580534 C151.894625,252.613734 152.572535,256.683147 154.047189,260.480964 C155.510525,264.279912 157.744575,267.79708 160.577313,270.780109 C166.238263,276.752957 174.384508,280.419502 182.779736,280.550774 C186.962637,280.601698 191.178358,279.852546 195.076062,278.265973 C198.971501,276.678268 202.545528,274.305198 205.545763,271.344802 M205.545763,271.344802 C199.613196,277.3516 191.273424,280.87782 182.779736,280.996644 C178.537984,281.055489 174.258885,280.341418 170.267248,278.784268 C166.273347,277.241829 162.577093,274.886866 159.448971,271.909495 C153.179146,265.958149 149.340293,257.388164 149.206748,248.580534 C149.154688,244.188601 149.949168,239.766113 151.610558,235.671804 C153.278739,231.58089 155.773088,227.827207 158.879707,224.682352 C162.02141,221.571447 165.778778,219.080685 169.868876,217.411502 C173.961239,215.740055 178.387463,214.952427 182.779736,215.002219 C191.590311,215.128964 200.163221,218.976574 206.115027,225.245914 C209.09376,228.374926 211.448905,232.072026 212.991463,236.067881 C214.547602,240.05921 215.26286,244.33911 215.200614,248.580534 C215.08065,257.074697 211.553025,265.412694 205.545763,271.344802" id="Fill-5" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-7" fill="#CCD8FF" points="178.603997 259 173.204348 253.599236 188.803583 238 194.204348 243.39965"></polygon>
|
||||
<polygon id="Fill-9" fill="#DDE4FB" points="178.724114 259 174.204348 254.480493 188.684582 240 193.204348 244.520596"></polygon>
|
||||
<path d="M267.204348,207.999438 C267.204348,233.405315 246.609663,254 221.203786,254 C195.799033,254 175.204348,233.405315 175.204348,207.999438 C175.204348,182.593562 195.799033,162 221.203786,162 C246.609663,162 267.204348,182.593562 267.204348,207.999438" id="Fill-11" fill="#CCD8FF"></path>
|
||||
<path d="M266.204348,201.999438 C266.204348,227.405315 245.609663,248 220.204909,248 C194.799033,248 174.204348,227.405315 174.204348,201.999438 C174.204348,176.594685 194.799033,156 220.204909,156 C245.609663,156 266.204348,176.594685 266.204348,201.999438" id="Fill-13" fill="#EBEFFF"></path>
|
||||
<path d="M244.398769,225.197623 C247.407939,222.150475 249.817745,218.521956 251.430642,214.564592 C253.041294,210.606105 253.805653,206.324385 253.755144,202.075213 C253.620456,193.548811 249.900803,185.271568 243.834199,179.51734 C240.803703,176.637421 237.229963,174.365809 233.370009,172.877588 C229.511178,171.374778 225.375112,170.690151 221.274964,170.746268 C213.060075,170.867481 205.01467,174.271532 199.274686,180.075142 C193.469603,185.812535 190.057489,193.858576 189.937392,202.075213 C189.873415,206.176237 190.565939,210.313175 192.066596,214.172896 C193.556028,218.034861 195.830023,221.607263 198.710116,224.637576 C204.466936,230.704936 212.746924,234.424364 221.274964,234.554555 C225.525515,234.603938 229.80637,233.841871 233.765096,232.229071 C237.722699,230.616271 241.351437,228.205489 244.398769,225.197623 M244.398769,225.197623 C238.373694,231.299775 229.901775,234.878911 221.274964,234.996756 C216.968292,235.055118 212.623459,234.326721 208.571574,232.744224 C204.516322,231.175195 200.765241,228.782371 197.591077,225.757669 C191.229281,219.712756 187.339022,211.012392 187.206578,202.075213 C187.15607,197.618409 187.96308,193.131301 189.650056,188.977528 C191.342644,184.827122 193.873669,181.019029 197.026507,177.828222 C200.21414,174.671084 204.02583,172.143579 208.175365,170.449971 C212.327144,168.752996 216.819012,167.952769 221.274964,168.002152 C230.214926,168.126731 238.919184,172.025734 244.964462,178.387146 C247.989345,181.56224 250.382315,185.314217 251.951438,189.369224 C253.532908,193.421987 254.262472,197.76768 254.200739,202.075213 C254.082887,210.701504 250.500167,219.172912 244.398769,225.197623" id="Fill-15" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-17" fill="#CCD8FF" points="215.604006 214 210.204348 208.600342 225.803601 193 231.204348 198.399658"></polygon>
|
||||
<polygon id="Fill-19" fill="#DDE4FB" points="215.961564 214 211.204348 209.242784 226.446013 194 231.204348 198.757216"></polygon>
|
||||
<path d="M222.204348,194.5 C222.204348,222.943392 199.146866,246 170.704911,246 C142.26183,246 119.204348,222.943392 119.204348,194.5 C119.204348,166.056608 142.26183,143 170.704911,143 C199.146866,143 222.204348,166.056608 222.204348,194.5" id="Fill-21" fill="#CCD8FF"></path>
|
||||
<path d="M168.704348,137 C197.14774,137 220.204348,160.057482 220.204348,188.499437 C220.204348,216.942518 197.14774,240 168.704348,240 C140.260956,240 117.204348,216.942518 117.204348,188.499437 C117.204348,160.057482 140.260956,137 168.704348,137" id="Fill-23" fill="#EBEFFF"></path>
|
||||
<polygon id="Fill-25" fill="#CCD8FF" points="181.204348 193.82838 175.032728 200 157.204348 182.17162 163.375967 176"></polygon>
|
||||
<polygon id="Fill-27" fill="#DDE4FB" points="182.204348 193.766262 176.970348 199 160.204348 182.233738 165.437245 177"></polygon>
|
||||
<path d="M194.37424,214.026713 C197.706715,210.608764 200.377816,206.537466 202.165974,202.097178 C203.953017,197.654633 204.808126,192.849869 204.759136,188.077829 C204.628865,178.504411 200.533696,169.189398 193.814188,162.697663 C190.457218,159.448975 186.496773,156.880717 182.210094,155.193746 C177.92787,153.492106 173.330547,152.704477 168.769966,152.760897 C159.633218,152.87938 150.671276,156.712584 144.280229,163.259611 C137.821262,169.733291 134.031171,178.815852 133.915375,188.077829 C133.85191,192.702048 134.635759,197.361247 136.313688,201.703363 C137.980482,206.047736 140.514632,210.062614 143.72129,213.463638 C150.129039,220.274712 159.322573,224.426127 168.769966,224.553637 C173.478632,224.603287 178.2207,223.736669 182.603133,221.924445 C186.984452,220.111093 191.001682,217.40404 194.37424,214.026713 M194.37424,214.026713 C187.700383,220.87164 178.316454,224.883132 168.769966,224.997102 C164.004516,225.055779 159.201209,224.224142 154.726363,222.442385 C150.248176,220.674168 146.108469,217.984042 142.611208,214.589789 C135.59887,207.806925 131.333347,198.065374 131.206417,188.077829 C131.155199,183.098162 132.054845,178.0869 133.917602,173.448012 C135.784812,168.813638 138.57505,164.561794 142.051156,160.999408 C145.562892,157.472003 149.760496,154.646468 154.333324,152.752998 C158.907266,150.856144 163.856431,149.952288 168.769966,150.001938 C178.627099,150.122678 188.241506,154.455767 194.935405,161.561356 C198.284581,165.107944 200.938982,169.303367 202.682602,173.842955 C204.440697,178.379158 205.262403,183.247112 205.201165,188.077829 C205.087595,197.752805 201.12715,207.263032 194.37424,214.026713" id="Fill-29" fill="#CCD8FF"></path>
|
||||
<path d="M266.204348,240.5 C266.204348,270.599732 241.80408,295 211.704348,295 C181.604616,295 157.204348,270.599732 157.204348,240.5 C157.204348,210.400268 181.604616,186 211.704348,186 C241.80408,186 266.204348,210.400268 266.204348,240.5" id="Fill-31" fill="#CCD8FF"></path>
|
||||
<path d="M265.204348,234.5 C265.204348,264.599732 240.80408,289 210.704348,289 C180.604616,289 156.204348,264.599732 156.204348,234.5 C156.204348,204.400268 180.604616,180 210.704348,180 C240.80408,180 265.204348,204.400268 265.204348,234.5" id="Fill-33" fill="#EBEFFF"></path>
|
||||
<path d="M236.788402,261.441702 C240.306252,257.838502 243.126293,253.544277 245.014812,248.861913 C246.902223,244.176182 247.809376,239.110164 247.759533,234.074435 C247.633262,223.97493 243.322344,214.137926 236.230153,207.273673 C232.689042,203.838742 228.504395,201.121759 223.97638,199.333621 C219.450581,197.529777 214.590276,196.689554 209.766523,196.744522 C200.102402,196.860067 190.61661,200.910862 183.854495,207.831204 C177.021491,214.677508 173.014065,224.285667 172.901086,234.074435 C172.837951,238.962088 173.675323,243.885638 175.455294,248.469285 C177.221973,253.057419 179.906881,257.294432 183.298462,260.881927 C190.078299,268.06589 199.793372,272.43191 209.766523,272.556429 C214.738699,272.605788 219.740782,271.685917 224.366268,269.77326 C228.990647,267.860602 233.229568,265.004517 236.788402,261.441702 M236.788402,261.441702 C229.743839,268.659319 219.837146,272.886236 209.766523,272.997293 C204.737857,273.055627 199.673747,272.171654 194.956327,270.288163 C190.236691,268.419255 185.877037,265.579997 182.193041,262.001476 C174.81065,254.847801 170.331371,244.585637 170.206209,234.074435 C170.156365,228.833417 171.107824,223.560989 173.070554,218.681188 C175.037715,213.804753 177.97738,209.333285 181.637008,205.585374 C185.333188,201.874482 189.752655,198.900609 194.566439,196.907182 C199.382439,194.909268 204.591649,193.953501 209.766523,194.001738 C220.148392,194.120648 230.28215,198.667276 237.345542,206.144027 C240.880008,209.876233 243.683433,214.292733 245.528754,219.073816 C247.386259,223.851533 248.261292,228.981493 248.201479,234.074435 C248.089608,244.276022 243.914929,254.307097 236.788402,261.441702" id="Fill-35" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-37" fill="#CCD8FF" points="204.89042 248 198.204348 241.313928 217.518276 222 224.204348 228.686072"></polygon>
|
||||
<polygon id="Fill-39" fill="#DDE4FB" points="203.914098 248 198.204348 242.28998 216.494597 224 222.204348 229.708886"></polygon>
|
||||
<path d="M277.204348,57 C277.204348,84.6141928 254.595559,107 226.703783,107 C198.814266,107 176.204348,84.6141928 176.204348,57 C176.204348,29.3858072 198.814266,7 226.703783,7 C254.595559,7 277.204348,29.3858072 277.204348,57" id="Fill-41" fill="#CCD8FF"></path>
|
||||
<g id="Group-45" transform="translate(175.204348, 0.000000)">
|
||||
<mask id="mask-2" fill="white">
|
||||
<use xlink:href="#path-1"></use>
|
||||
</mask>
|
||||
<g id="Clip-44"></g>
|
||||
<path d="M101.619191,50.1931844 C101.619191,77.9143178 79.0097498,100.386818 51.1196919,100.386818 C23.229634,100.386818 0.620193054,77.9143178 0.620193054,50.1931844 C0.620193054,22.4720511 23.229634,-0.000448888889 51.1196919,-0.000448888889 C79.0097498,-0.000448888889 101.619191,22.4720511 101.619191,50.1931844" id="Fill-43" fill="#EBEFFF" mask="url(#mask-2)"></path>
|
||||
</g>
|
||||
<path d="M93.2043478,281.5 C93.2043478,307.1816 72.3848256,328 46.7043478,328 C21.0227479,328 0.204347826,307.1816 0.204347826,281.5 C0.204347826,255.8184 21.0227479,235 46.7043478,235 C72.3848256,235 93.2043478,255.8184 93.2043478,281.5" id="Fill-52" fill="#CCD8FF"></path>
|
||||
<path d="M84.0671984,303.176414 C68.7818604,323.811564 39.6630839,328.148189 19.0279337,312.862851 C-1.60721653,297.577513 -5.94384073,268.458736 9.34149728,247.823586 C24.6257133,227.188436 53.7456118,222.851811 74.380762,238.137149 C95.0159122,253.422487 99.3525364,282.541264 84.0671984,303.176414" id="Fill-54" fill="#EBEFFF"></path>
|
||||
<polygon id="Fill-56" fill="#CCD8FF" points="35.3802117 282 34.2043478 274.309201 57.0273588 271 58.2043478 278.690799"></polygon>
|
||||
<polygon id="Fill-58" fill="#DDE4FB" points="35.2257594 282 34.2043478 275.229471 56.1829362 272 57.2043478 278.769404"></polygon>
|
||||
<path d="M70.2525697,299.051431 C73.3058447,295.957796 75.7543212,292.274039 77.3918948,288.256931 C79.0272158,284.23757 79.8032049,279.891615 79.7525234,275.577194 C79.6184992,266.920196 75.8455478,258.514338 69.6860639,252.669429 C66.6091376,249.744722 62.981473,247.436039 59.059856,245.92357 C55.1427441,244.397587 50.9418161,243.699351 46.7769283,243.75566 C38.4336376,243.877289 30.259285,247.333556 24.4286673,253.23027 C18.5316006,259.058286 15.0672428,267.23215 14.9467336,275.577194 C14.8825372,279.742959 15.5875722,283.944762 17.1125204,287.862765 C18.6273322,291.785274 20.9361532,295.413848 23.8632877,298.489464 C29.7107992,304.649706 38.1182864,308.422431 46.7769283,308.554195 C51.0916079,308.602621 55.4389489,307.827805 59.45855,306.190329 C63.4747723,304.552853 67.1587496,302.10565 70.2525697,299.051431 M70.2525697,299.051431 C64.1347572,305.245458 55.5335543,308.879663 46.7769283,308.996786 C42.4036835,309.055348 37.9943985,308.314317 33.8824446,306.707249 C29.7671119,305.113694 25.9603729,302.68451 22.7392861,299.613398 C16.2847236,293.476807 12.3394553,284.64525 12.2065573,275.577194 C12.1558759,271.055554 12.9769151,266.502381 14.6888214,262.287064 C16.4052328,258.076251 18.9742185,254.212305 22.1739065,250.975644 C25.4073821,247.771642 29.2738126,245.206189 33.4848769,243.487628 C37.6970674,241.765688 42.2550179,240.952581 46.7769283,241.002134 C55.8489055,241.12714 64.6821167,245.081182 70.8190756,251.535359 C73.8903706,254.757379 76.3208271,258.565016 77.9133504,262.681229 C79.5205151,266.794063 80.2627166,271.204211 80.2007726,275.577194 C80.0825159,284.334422 76.4469675,292.933984 70.2525697,299.051431" id="Fill-60" fill="#CCD8FF"></path>
|
||||
<path d="M408.204348,185.5 C408.204348,211.181229 387.609434,232 362.204348,232 C336.799261,232 316.204348,211.181229 316.204348,185.5 C316.204348,159.818771 336.799261,139 362.204348,139 C387.609434,139 408.204348,159.818771 408.204348,185.5" id="Fill-62" fill="#CCD8FF"></path>
|
||||
<path d="M399.067041,207.176225 C383.782506,227.811912 354.66362,232.147767 334.02786,216.863286 C313.39323,201.577676 309.05623,172.458893 324.340765,151.823206 C339.62643,131.188648 368.745315,126.851664 389.381075,142.137274 C410.015706,157.421755 414.352706,186.540538 399.067041,207.176225" id="Fill-64" fill="#EBEFFF"></path>
|
||||
<polygon id="Fill-66" fill="#CCD8FF" points="350.381185 186 349.204348 178.30871 372.027511 175 373.204348 182.69129"></polygon>
|
||||
<polygon id="Fill-68" fill="#DDE4FB" points="351.182248 186 350.204348 179.230299 371.227546 176 372.204348 182.769701"></polygon>
|
||||
<path d="M385.252853,203.051745 C388.306547,199.958563 390.75381,196.274855 392.390608,192.259045 C394.02514,188.239836 394.801599,183.894193 394.75059,179.580286 C394.615702,170.925271 390.842224,162.523014 384.682694,156.680085 C381.606329,153.755787 377.979075,151.449219 374.060507,149.938331 C370.143073,148.412709 365.943393,147.716772 361.78112,147.772311 C353.440703,147.895856 345.271447,151.350609 339.442903,157.244543 C333.549749,163.069337 330.085722,171.239236 329.964435,179.580286 C329.898691,183.744577 330.602605,187.945139 332.128319,191.862338 C333.64043,195.782938 335.948271,199.409974 338.873878,202.486154 C344.718291,208.645316 353.124451,212.4197 361.78112,212.55118 C366.095285,212.601052 370.441188,211.825773 374.460639,210.189073 C378.475556,208.552373 382.159485,206.105257 385.252853,203.051745 M385.252853,203.051745 C379.136396,209.24491 370.537537,212.878747 361.78112,212.996626 C357.409145,213.056699 352.999765,212.315423 348.886232,210.709326 C344.771566,209.11683 340.964083,206.687849 337.742628,203.617336 C331.287249,197.481978 327.340344,188.650144 327.206589,179.580286 C327.155581,175.057825 327.975113,170.504761 329.686724,166.288331 C331.405135,162.077569 333.973684,158.212508 337.173602,154.974245 C340.407526,151.771118 344.276219,149.20499 348.486101,147.486681 C352.699383,145.764973 357.258387,144.95229 361.78112,145.002162 C370.853788,145.127974 379.687285,149.083711 385.824145,155.538702 C388.894842,158.761097 391.323969,162.569486 392.91656,166.685038 C394.522753,170.798324 395.264073,175.208574 395.200597,179.580286 C395.082711,188.337312 391.446389,196.935655 385.252853,203.051745" id="Fill-70" fill="#CCD8FF"></path>
|
||||
<path d="M415.204348,171.5 C415.204348,198.28583 393.49043,220 366.70491,220 C339.918266,220 318.204348,198.28583 318.204348,171.5 C318.204348,144.71417 339.918266,123 366.70491,123 C393.49043,123 415.204348,144.71417 415.204348,171.5" id="Fill-72" fill="#CCD8FF"></path>
|
||||
<path d="M403.674044,194.366501 C387.730722,215.889089 357.360294,220.413003 335.837651,204.46972 C314.315008,188.527562 309.792207,158.156087 325.733282,136.633499 C341.676605,115.110911 372.048157,110.586997 393.570799,126.53028 C415.093442,142.472438 419.617367,172.843913 403.674044,194.366501" id="Fill-74" fill="#EBEFFF"></path>
|
||||
<polygon id="Fill-76" fill="#CCD8FF" points="353.430202 172 352.204348 163.610213 375.978493 160 377.204348 168.389787"></polygon>
|
||||
<polygon id="Fill-78" fill="#DDE4FB" points="353.270227 173 352.204348 165.552182 375.139611 162 376.204348 169.446629"></polygon>
|
||||
<path d="M389.814082,189.612327 C393.006514,186.380604 395.564756,182.529438 397.27854,178.33238 C398.987803,174.131931 399.802868,169.58898 399.751997,165.078809 C399.618602,156.03021 395.683455,147.235943 389.245458,141.114997 C386.029286,138.052829 382.236577,135.63384 378.135252,134.046803 C374.036187,132.446201 369.638679,131.710331 365.279608,131.76685 C356.543377,131.886669 347.982368,135.503848 341.876728,141.677921 C335.70213,147.781911 332.07673,156.343322 331.956901,165.078809 C331.892464,169.439771 332.63518,173.836905 334.235917,177.936751 C335.82309,182.038859 338.243415,185.832375 341.308104,189.048274 C347.431831,195.485722 356.227977,199.421665 365.279608,199.551658 C369.790162,199.601394 374.333499,198.787529 378.533175,197.07502 C382.731721,195.362512 386.582084,192.804488 389.814082,189.612327 M389.814082,189.612327 C383.420174,196.084818 374.429589,199.879465 365.279608,199.997023 C360.710269,200.054672 356.104756,199.275848 351.811252,197.593859 C347.514355,195.925436 343.540772,193.384367 340.181031,190.176381 C333.446853,183.76267 329.337614,174.541124 329.20648,165.078809 C329.155609,160.360651 330.014762,155.610843 331.802027,151.21484 C333.594944,146.821097 336.275276,142.791334 339.612407,139.414923 C342.985714,136.072423 347.020342,133.395711 351.412198,131.601816 C355.806314,129.80453 360.559918,128.953363 365.279608,129.001968 C374.746119,129.126309 383.96958,133.243111 390.383836,139.976716 C393.593225,143.338432 396.13451,147.312808 397.800815,151.609338 C399.482946,155.904739 400.264097,160.50986 400.200791,165.078809 C400.084354,174.229143 396.287123,183.217833 389.814082,189.612327" id="Fill-80" fill="#CCD8FF"></path>
|
||||
<g id="Group-84" transform="translate(321.204348, 103.000000)">
|
||||
<mask id="mask-4" fill="white">
|
||||
<use xlink:href="#path-3"></use>
|
||||
</mask>
|
||||
<g id="Clip-83"></g>
|
||||
<path d="M99.9175048,49.5162997 C99.9175048,76.7312191 77.5544069,98.7927534 49.9688772,98.7927534 C22.3833475,98.7927534 0.0225128411,76.7312191 0.0225128411,49.5162997 C0.0225128411,22.3013803 22.3833475,0.23984597 49.9688772,0.23984597 C77.5544069,0.23984597 99.9175048,22.3013803 99.9175048,49.5162997" id="Fill-82" fill="#CCD8FF" mask="url(#mask-4)"></path>
|
||||
</g>
|
||||
<path d="M410.477409,176.962082 C394.205793,198.928168 363.208983,203.544993 341.242862,187.273403 C319.275621,171.002935 314.659909,140.00393 330.930404,118.038966 C347.202019,96.0717589 378.199951,91.4549336 400.166072,107.726523 C422.133313,123.998113 426.749025,154.995996 410.477409,176.962082" id="Fill-85" fill="#EBEFFF"></path>
|
||||
<polygon id="Fill-87" fill="#CCD8FF" points="359.429201 154 358.204348 145.6092 381.978394 142 383.204348 150.389678"></polygon>
|
||||
<polygon id="Fill-89" fill="#DDE4FB" points="359.269774 154 358.204348 146.553596 381.137805 143 382.204348 150.446404"></polygon>
|
||||
<path d="M395.667007,171.465831 C398.906259,168.186399 401.501922,164.280232 403.239839,160.020895 C404.976635,155.759315 405.805228,151.151293 405.754773,146.573543 C405.622467,137.391135 401.634228,128.464353 395.101903,122.247448 C391.839105,119.136192 387.989899,116.678581 383.825626,115.065212 C379.665838,113.437269 375.201074,112.687204 370.77331,112.743263 C361.903207,112.860986 353.205774,116.535071 347.004214,122.805792 C340.732016,129.004759 337.052118,137.7017 336.933267,146.573543 C336.870477,151.003299 337.626191,155.46781 339.254226,159.629605 C340.868807,163.793643 343.327679,167.643751 346.440232,170.906365 C352.659732,177.43832 361.589261,181.427455 370.77331,181.555269 C375.35132,181.6046 379.959602,180.777174 384.221423,179.038235 C388.481001,177.301537 392.387389,174.7049 395.667007,171.465831 M395.667007,171.465831 C389.17841,178.031421 380.056029,181.882651 370.77331,181.997011 C366.139239,182.055312 361.467046,181.261521 357.112163,179.552853 C352.756158,177.859882 348.727555,175.280062 345.322359,172.025296 C338.496271,165.518007 334.335361,156.166301 334.206419,146.573543 C334.155963,141.790618 335.027164,136.9763 336.840204,132.519637 C338.657729,128.067458 341.374485,123.983024 344.758377,120.560082 C348.178149,117.171897 352.265057,114.458658 356.717487,112.640115 C361.171039,110.818208 365.990115,109.952663 370.77331,110.001994 C380.368854,110.123081 389.723331,114.291603 396.230989,121.118427 C399.48706,124.524551 402.067025,128.554047 403.760093,132.912048 C405.466615,137.266684 406.262693,141.939734 406.201025,146.573543 C406.085538,155.856857 402.231847,164.977601 395.667007,171.465831" id="Fill-91" fill="#CCD8FF"></path>
|
||||
<path d="M404.204348,160.5 C404.204348,187.838382 382.041859,210 354.703787,210 C327.365715,210 305.204348,187.838382 305.204348,160.5 C305.204348,133.161618 327.365715,111 354.703787,111 C382.041859,111 404.204348,133.161618 404.204348,160.5" id="Fill-93" fill="#CCD8FF"></path>
|
||||
<path d="M393.477781,184.962097 C377.206243,206.928166 346.209579,211.544988 324.242441,195.27341 C302.276425,179.002954 297.659614,148.003972 313.930031,126.037903 C330.20157,104.071834 361.200477,99.455012 383.166493,115.72659 C405.132509,131.998167 409.74932,162.996028 393.477781,184.962097" id="Fill-95" fill="#EBEFFF"></path>
|
||||
<polygon id="Fill-97" fill="#CCD8FF" points="342.430248 162 341.204348 153.6092 364.978448 150 366.204348 158.389678"></polygon>
|
||||
<polygon id="Fill-99" fill="#DDE4FB" points="342.27089 162 341.204348 154.553596 364.138922 151 365.204348 158.446404"></polygon>
|
||||
<path d="M378.665993,179.465831 C381.906368,176.18752 384.502032,172.281353 386.239949,168.020895 C387.975624,163.759315 388.804219,159.151293 388.753763,154.574664 C388.622578,145.392256 384.634338,136.464353 378.102011,130.247448 C374.839211,127.136192 370.988883,124.678581 366.82573,123.065212 C362.66482,121.437269 358.201175,120.687204 353.77341,120.743263 C344.902183,120.862107 336.205868,124.535071 330.003185,130.805792 C323.732106,137.004759 320.052207,145.702821 319.933355,154.574664 C319.869445,159.003298 320.62628,163.46781 322.253195,167.629605 C323.867776,171.793642 326.326649,175.643751 329.440324,178.906365 C335.659826,185.439442 344.589358,189.428576 353.77341,189.55639 C358.3503,189.6046 362.959705,188.777174 367.221527,187.039356 C371.481107,185.301537 375.387496,182.7049 378.665993,179.465831 M378.665993,179.465831 C372.178516,186.032542 363.05501,189.882651 353.77341,189.997011 C349.138216,190.055312 344.467143,189.261521 340.112258,187.553974 C335.755131,185.859882 331.727648,183.281183 328.32133,180.025296 C321.495239,173.518007 317.335449,164.167422 317.206507,154.574664 C317.15493,149.790618 318.027252,144.977422 319.839171,140.520758 C321.656697,136.067458 324.374575,131.983024 327.758469,128.560082 C331.17712,125.173018 335.265151,122.458658 339.717583,120.640115 C344.171136,118.818208 348.989092,117.952663 353.77341,118.001994 C363.368957,118.124202 372.723437,122.292724 379.231097,129.118427 C382.487169,132.525672 385.066014,136.555168 386.760203,140.912048 C388.466726,145.267806 389.261683,149.939734 389.201136,154.574664 C389.084528,163.856857 385.231957,172.978723 378.665993,179.465831" id="Fill-101" fill="#CCD8FF"></path>
|
||||
</g>
|
||||
<g id="Group-227" transform="translate(877.000000, 162.000000)">
|
||||
<g id="Group-5" transform="translate(290.000000, 287.000000)" fill="#BCC7EB">
|
||||
<polygon id="Fill-1" points="154.396031 13.4109453 194.481169 0.534068468 40.9980706 154.185792 0.91132549 167.062669"></polygon>
|
||||
<polygon id="Fill-3" points="0.91132549 167.062669 368.998071 535.553816 409.083208 522.676939 40.9980706 154.185792"></polygon>
|
||||
</g>
|
||||
<polygon id="Fill-6" fill="#6A7187" points="421.861628 332.45245 345 406.954054 351.015146 413 441 326"></polygon>
|
||||
<polygon id="Fill-7" fill="#A3AFD1" points="328.704117 422 422 332.134023 304.792566 216 210 302.656133"></polygon>
|
||||
<g id="Group-226">
|
||||
<path d="M441.040903,325.419702 L323.705647,209.142381 C319.333014,210.486423 304.514022,215.573204 304.514022,215.573204 L421.849279,331.850525 L441.040903,325.419702 Z" id="Fill-8" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-10" fill="#EBEFFF" points="441.040903 325.419702 444.204905 322.149628 323.650928 202.656896 320.486926 205.92697"></polygon>
|
||||
<polygon id="Fill-12" fill="#DDE4FB" points="306.180032 209.08788 323.649641 202.657057 227.08801 295.903992 209.618402 302.334815"></polygon>
|
||||
<polygon id="Fill-14" fill="#EBEFFF" points="326.869006 205.872468 323.650285 202.657057 227.088654 295.903992 230.307375 299.119404"></polygon>
|
||||
<path d="M347.642953,415.396242 L227.088976,295.905117 C222.716343,297.249159 209.617758,302.335941 209.617758,302.335941 L328.451329,421.827065 L347.642953,415.396242 Z" id="Fill-16" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-18" fill="#EBEFFF" points="347.642953 415.396242 350.806956 412.127776 230.252979 292.635044 227.088976 295.905117"></polygon>
|
||||
<polygon id="Fill-20" fill="#CCD8FF" points="425.736206 360.456434 371.595709 414.542872 255.564035 298.628893 309.702923 244.544062"></polygon>
|
||||
<polygon id="Fill-22" fill="#EBEFFF" points="426.020741 360.385695 371.878635 414.472133 255.846961 298.558154 309.987458 244.473323"></polygon>
|
||||
<polygon id="Stroke-24" stroke="#CCD8FF" stroke-width="2.835" points="371.879278 396.557628 273.779099 298.558314 309.988101 262.386542 408.088281 360.385855"></polygon>
|
||||
<path d="M354.229744,329.471924 C354.229744,322.13757 348.276719,316.190667 340.933207,316.190667 C333.591304,316.190667 327.639889,322.13757 327.639889,329.471924 C327.639889,336.806278 333.591304,342.754789 340.933207,342.754789 C348.276719,342.754789 354.229744,336.806278 354.229744,329.471924" id="Fill-26" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-28" fill="#CCD8FF" points="426.069505 360.38891 371.929008 414.475348 255.897334 298.561369 310.036221 244.476539"></polygon>
|
||||
<polygon id="Fill-30" fill="#EBEFFF" points="426.353879 360.318171 372.211772 414.404609 256.180098 298.49063 310.320595 244.4058"></polygon>
|
||||
<polygon id="Stroke-32" stroke="#CCD8FF" stroke-width="2.835" points="372.212416 396.490104 274.112237 298.490791 310.321239 262.319018 408.421418 360.318332"></polygon>
|
||||
<path d="M354.562881,329.4044 C354.562881,322.070047 348.609857,316.123143 341.266345,316.123143 C333.924442,316.123143 327.971417,322.070047 327.971417,329.4044 C327.971417,336.738754 333.924442,342.687266 341.266345,342.687266 C348.609857,342.687266 354.562881,336.738754 354.562881,329.4044" id="Fill-34" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-36" fill="#CCD8FF" points="426.399424 360.316724 372.258927 414.403162 256.227252 298.489183 310.36614 244.404353"></polygon>
|
||||
<polygon id="Fill-38" fill="#EBEFFF" points="426.683958 360.245985 372.541852 414.332423 256.510178 298.418444 310.650675 244.333614"></polygon>
|
||||
<polygon id="Stroke-40" stroke="#CCD8FF" stroke-width="2.835" points="372.542496 396.417757 274.442317 298.418444 310.651319 262.246672 408.751498 360.245985"></polygon>
|
||||
<path d="M354.892961,329.332214 C354.892961,321.997861 348.939937,316.050957 341.596425,316.050957 C334.254522,316.050957 328.301497,321.997861 328.301497,329.332214 C328.301497,336.666568 334.254522,342.61508 341.596425,342.61508 C348.939937,342.61508 354.892961,336.666568 354.892961,329.332214" id="Fill-42" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-44" fill="#CCD8FF" points="426.726446 360.250647 372.584339 414.335478 256.554275 298.421499 310.693162 244.336668"></polygon>
|
||||
<polygon id="Fill-46" fill="#EBEFFF" points="427.01082 360.180069 372.868713 414.264899 256.837039 298.35092 310.977536 244.26609"></polygon>
|
||||
<polygon id="Stroke-48" stroke="#CCD8FF" stroke-width="2.835" points="372.869357 396.350395 274.769178 298.351081 310.97818 262.179309 409.078359 360.178622"></polygon>
|
||||
<path d="M355.218374,329.264691 C355.218374,321.930337 349.266959,315.983433 341.923447,315.983433 C334.581544,315.983433 328.628519,321.930337 328.628519,329.264691 C328.628519,336.600652 334.581544,342.547556 341.923447,342.547556 C349.266959,342.547556 355.218374,336.600652 355.218374,329.264691" id="Fill-50" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-52" fill="#CCD8FF" points="427.059583 360.183285 372.917477 414.268115 256.887412 298.355743 311.0263 244.269306"></polygon>
|
||||
<polygon id="Fill-54" fill="#EBEFFF" points="427.344118 360.112545 373.202012 414.197376 257.170338 298.285004 311.310835 244.198566"></polygon>
|
||||
<polygon id="Stroke-56" stroke="#CCD8FF" stroke-width="2.835" points="373.202656 396.284318 275.102476 298.285004 311.311478 262.111624 409.411658 360.112545"></polygon>
|
||||
<path d="M355.551511,329.198775 C355.551511,321.864421 349.600096,315.91591 342.256584,315.91591 C334.914682,315.91591 328.961657,321.864421 328.961657,329.198775 C328.961657,336.533129 334.914682,342.480032 342.256584,342.480032 C349.600096,342.480032 355.551511,336.533129 355.551511,329.198775" id="Fill-58" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-60" fill="#CCD8FF" points="429.178145 354.684931 378.566976 412.080027 255.433194 303.725481 306.044363 246.330384"></polygon>
|
||||
<polygon id="Fill-62" fill="#EBEFFF" points="429.456404 354.596989 378.845234 411.992086 255.711452 303.635931 306.322622 246.240835"></polygon>
|
||||
<polygon id="Stroke-64" stroke="#CCD8FF" stroke-width="2.835" points="377.711601 394.114558 273.606898 302.504267 307.456577 264.118684 411.559671 355.728975"></polygon>
|
||||
<path d="M355.853266,328.277238 C355.389771,320.955746 349.071421,315.397907 341.744003,315.860926 C334.416584,316.323945 328.851416,322.635798 329.316521,329.955683 C329.780017,337.277175 336.096757,342.836621 343.424175,342.371995 C350.753203,341.908975 356.316762,335.59873 355.853266,328.277238" id="Fill-66" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-68" fill="#CCD8FF" points="427.733744 360.037144 373.591638 414.123582 257.559964 298.209603 311.700461 244.124773"></polygon>
|
||||
<polygon id="Fill-70" fill="#EBEFFF" points="428.01667 359.966566 373.876173 414.053004 257.844499 298.139025 311.983386 244.054195"></polygon>
|
||||
<polygon id="Stroke-72" stroke="#CCD8FF" stroke-width="2.835" points="373.875207 396.138338 275.775028 298.139025 311.98403 261.967252 410.084209 359.966566"></polygon>
|
||||
<path d="M356.225672,329.052795 C356.225672,321.718441 350.272648,315.771538 342.929136,315.771538 C335.587233,315.771538 329.635818,321.718441 329.635818,329.052795 C329.635818,336.387149 335.587233,342.33566 342.929136,342.33566 C350.272648,342.33566 356.225672,336.387149 356.225672,329.052795" id="Fill-74" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-76" fill="#CCD8FF" points="428.067043 359.971228 373.924937 414.056058 257.893262 298.142079 312.033759 244.057249"></polygon>
|
||||
<polygon id="Fill-78" fill="#EBEFFF" points="428.349968 359.90065 374.207862 413.98548 258.177797 298.071501 312.316685 243.986671"></polygon>
|
||||
<polygon id="Stroke-80" stroke="#CCD8FF" stroke-width="2.835" points="374.208506 396.070814 276.108327 298.071501 312.317329 261.899729 410.417508 359.899042"></polygon>
|
||||
<path d="M356.558971,328.986879 C356.558971,321.650918 350.605947,315.704014 343.262435,315.704014 C335.920532,315.704014 329.969117,321.650918 329.969117,328.986879 C329.969117,336.321233 335.920532,342.268137 343.262435,342.268137 C350.605947,342.268137 356.558971,336.321233 356.558971,328.986879" id="Fill-82" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-84" fill="#CCD8FF" points="428.395513 359.897434 374.255016 413.983872 258.223342 298.069893 312.363839 243.985063"></polygon>
|
||||
<polygon id="Fill-86" fill="#EBEFFF" points="428.679887 359.826856 374.537781 413.913294 258.507716 297.999315 312.646604 243.914485"></polygon>
|
||||
<polygon id="Stroke-88" stroke="#CCD8FF" stroke-width="2.835" points="374.538425 395.998628 276.438246 297.999315 312.647248 261.827543 410.747427 359.826856"></polygon>
|
||||
<path d="M356.88889,328.913086 C356.88889,321.578732 350.935865,315.631828 343.592353,315.631828 C336.250451,315.631828 330.299036,321.578732 330.299036,328.913086 C330.299036,336.247439 336.250451,342.195951 343.592353,342.195951 C350.935865,342.195951 356.88889,336.247439 356.88889,328.913086" id="Fill-90" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-92" fill="#CCD8FF" points="425.660888 367.190309 366.957854 416.297682 261.605896 290.618499 320.30893 241.511126"></polygon>
|
||||
<polygon id="Fill-94" fill="#EBEFFF" points="425.949929 367.144651 367.246895 416.252024 261.894937 290.57284 320.597971 241.465467"></polygon>
|
||||
<polygon id="Stroke-96" stroke="#CCD8FF" stroke-width="2.835" points="368.828736 398.408579 279.757069 292.152089 319.017418 259.309875 408.089085 365.564758"></polygon>
|
||||
<path d="M357.165539,330.029316 C357.812502,322.722293 352.408269,316.275393 345.095335,315.629095 C337.780792,314.982797 331.325647,320.381473 330.678684,327.688496 C330.031721,334.995519 335.435953,341.442419 342.750497,342.088717 C350.06504,342.735014 356.520185,337.336338 357.165539,330.029316" id="Fill-98" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-100" fill="#CCD8FF" points="429.055512 359.763995 374.915015 413.850433 258.883341 297.936454 313.022229 243.850016"></polygon>
|
||||
<polygon id="Fill-102" fill="#EBEFFF" points="429.340047 359.693417 375.197941 413.779854 259.166267 297.865875 313.306764 243.779438"></polygon>
|
||||
<polygon id="Stroke-104" stroke="#CCD8FF" stroke-width="2.835" points="375.198584 395.865189 277.098405 297.865875 313.307407 261.694103 411.407587 359.693417"></polygon>
|
||||
<path d="M357.54905,328.779646 C357.54905,321.445292 351.596025,315.496781 344.252513,315.496781 C336.91061,315.496781 330.959195,321.445292 330.959195,328.779646 C330.959195,336.114 336.91061,342.060903 344.252513,342.060903 C351.596025,342.060903 357.54905,336.114 357.54905,328.779646" id="Fill-106" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-108" fill="#CCD8FF" points="429.385592 359.691809 375.245095 413.776639 259.213421 297.86266 313.352308 243.77783"></polygon>
|
||||
<polygon id="Fill-110" fill="#EBEFFF" points="429.670127 359.62107 375.528021 413.7059 259.496346 297.793529 313.636843 243.707091"></polygon>
|
||||
<polygon id="Stroke-112" stroke="#CCD8FF" stroke-width="2.835" points="375.528664 395.791395 277.428485 297.793689 313.637487 261.620309 411.737666 359.619623"></polygon>
|
||||
<path d="M357.879129,328.707299 C357.879129,321.372945 351.926105,315.424434 344.582593,315.424434 C337.24069,315.424434 331.289275,321.372945 331.289275,328.707299 C331.289275,336.041653 337.24069,341.988557 344.582593,341.988557 C351.926105,341.988557 357.879129,336.041653 357.879129,328.707299" id="Fill-114" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-116" fill="#CCD8FF" points="429.712453 359.618015 375.571956 413.704453 259.540282 297.790474 313.67917 243.705644"></polygon>
|
||||
<polygon id="Fill-118" fill="#EBEFFF" points="429.996988 359.547276 375.854882 413.633714 259.823208 297.719735 313.963705 243.634905"></polygon>
|
||||
<polygon id="Stroke-120" stroke="#CCD8FF" stroke-width="2.835" points="375.855525 395.719209 277.755346 297.719896 313.964348 261.548123 412.064528 359.547437"></polygon>
|
||||
<path d="M358.205991,328.633506 C358.205991,321.299152 352.252966,315.352248 344.909454,315.352248 C337.567551,315.352248 331.614527,321.299152 331.614527,328.633506 C331.614527,335.967859 337.567551,341.916371 344.909454,341.916371 C352.252966,341.916371 358.205991,335.967859 358.205991,328.633506" id="Fill-122" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-124" fill="#CCD8FF" points="430.045752 359.552099 375.903645 413.636929 259.873581 297.724558 314.012468 243.63812"></polygon>
|
||||
<polygon id="Fill-126" fill="#EBEFFF" points="430.330126 359.48136 376.188019 413.56619 260.156345 297.653819 314.296842 243.567381"></polygon>
|
||||
<polygon id="Stroke-128" stroke="#CCD8FF" stroke-width="2.835" points="376.188663 395.651686 278.088484 297.65398 314.297486 261.4806 412.397665 359.479913"></polygon>
|
||||
<path d="M358.539128,328.56759 C358.539128,321.233236 352.586104,315.284724 345.242592,315.284724 C337.900689,315.284724 331.947665,321.233236 331.947665,328.56759 C331.947665,335.901943 337.900689,341.848847 345.242592,341.848847 C352.586104,341.848847 358.539128,335.901943 358.539128,328.56759" id="Fill-130" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-132" fill="#CCD8FF" points="430.375671 359.478306 376.233564 413.564743 260.2035 297.650764 314.342387 243.565934"></polygon>
|
||||
<polygon id="Fill-134" fill="#EBEFFF" points="430.660205 359.407566 376.518099 413.494004 260.486425 297.580025 314.626922 243.495195"></polygon>
|
||||
<polygon id="Stroke-136" stroke="#CCD8FF" stroke-width="2.835" points="376.518743 395.5795 278.418564 297.580186 314.627566 261.408414 412.727745 359.407727"></polygon>
|
||||
<path d="M358.867599,328.493796 C358.867599,321.159442 352.916184,315.212538 345.572672,315.212538 C338.230769,315.212538 332.277744,321.159442 332.277744,328.493796 C332.277744,335.82815 338.230769,341.776661 345.572672,341.776661 C352.916184,341.776661 358.867599,335.82815 358.867599,328.493796" id="Fill-138" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-140" fill="#CCD8FF" points="430.702693 359.41239 376.560586 413.49722 260.530522 297.584848 314.669409 243.498411"></polygon>
|
||||
<polygon id="Fill-142" fill="#EBEFFF" points="430.987067 359.341651 376.84496 413.426481 260.813286 297.514109 314.953783 243.427672"></polygon>
|
||||
<polygon id="Stroke-144" stroke="#CCD8FF" stroke-width="2.835" points="376.845604 395.511976 278.745425 297.51427 314.954427 261.34089 413.054606 359.341811"></polygon>
|
||||
<path d="M359.194621,328.42788 C359.194621,321.093526 353.243206,315.145015 345.899694,315.145015 C338.557791,315.145015 332.604766,321.093526 332.604766,328.42788 C332.604766,335.762234 338.557791,341.709137 345.899694,341.709137 C353.243206,341.709137 359.194621,335.762234 359.194621,328.42788" id="Fill-146" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-148" fill="#CCD8FF" points="431.03583 359.344866 376.893724 413.431304 260.86205 297.517325 315.002547 243.432495"></polygon>
|
||||
<polygon id="Fill-150" fill="#EBEFFF" points="431.318756 359.274127 377.178259 413.360565 261.146585 297.446586 315.287082 243.361756"></polygon>
|
||||
<polygon id="Stroke-152" stroke="#CCD8FF" stroke-width="2.835" points="377.178903 395.44606 279.078723 297.446747 315.287725 261.274974 413.387905 359.274288"></polygon>
|
||||
<path d="M359.527758,328.360356 C359.527758,321.026003 353.576343,315.077491 346.232831,315.077491 C338.890929,315.077491 332.937904,321.026003 332.937904,328.360356 C332.937904,335.69471 338.890929,341.643221 346.232831,341.643221 C353.576343,341.643221 359.527758,335.69471 359.527758,328.360356" id="Fill-154" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-156" fill="#CCD8FF" points="431.36591 359.27268 377.223804 413.35751 261.19213 297.445139 315.332627 243.358701"></polygon>
|
||||
<polygon id="Fill-158" fill="#EBEFFF" points="431.648836 359.201941 377.508339 413.286771 261.476665 297.3744 315.617162 243.287962"></polygon>
|
||||
<polygon id="Stroke-160" stroke="#CCD8FF" stroke-width="2.835" points="377.508821 395.373713 279.408642 297.3744 315.617644 261.20102 413.717824 359.201941"></polygon>
|
||||
<path d="M359.857838,328.28817 C359.857838,320.953817 353.906423,315.005305 346.562911,315.005305 C339.221008,315.005305 333.267984,320.953817 333.267984,328.28817 C333.267984,335.622524 339.221008,341.569428 346.562911,341.569428 C353.906423,341.569428 359.857838,335.622524 359.857838,328.28817" id="Fill-162" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-172" fill="#CCD8FF" points="342.560271 170.137188 288.418164 116.052358 404.451448 0.138379092 458.593554 54.2232092"></polygon>
|
||||
<polygon id="Fill-174" fill="#EBEFFF" points="342.631082 170.420627 288.488976 116.335797 404.52226 0.421817621 458.664366 54.5066477"></polygon>
|
||||
<polygon id="Stroke-176" stroke="#CCD8FF" stroke-width="2.835" points="306.421919 116.334832 404.522099 18.3355185 440.731101 54.5072908 342.630921 152.506604"></polygon>
|
||||
<path d="M373.576671,98.7034442 C380.918574,98.7034442 386.873208,92.7565406 386.873208,85.4221868 C386.873208,78.0862253 380.918574,72.1409294 373.576671,72.1409294 C366.234768,72.1409294 360.281744,78.0862253 360.281744,85.4221868 C360.281744,92.7565406 366.234768,98.7034442 373.576671,98.7034442" id="Fill-178" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-180" fill="#CCD8FF" points="113.614576 667.707175 39.2733855 686.014121 0.000160936051 526.851249 74.3429604 508.545911"></polygon>
|
||||
<polygon id="Fill-182" fill="#EBEFFF" points="113.892834 667.793509 39.5516439 686.100455 0.278419368 526.937583 74.6212188 508.632245"></polygon>
|
||||
<polygon id="Stroke-184" stroke="#CCD8FF" stroke-width="2.835" points="48.8284807 670.768729 15.625764 536.205363 65.3453476 523.962684 98.5480643 658.52605"></polygon>
|
||||
<path d="M68.4644492,604.237363 C72.26254,597.959272 70.2476207,589.795342 63.9646772,585.999548 C57.6801244,582.205363 49.5077918,584.21821 45.7080916,590.494694 C41.9100008,596.771177 43.9249202,604.938322 50.2078636,608.732508 C56.4924164,612.526694 64.6663584,610.513846 68.4644492,604.237363" id="Fill-186" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-197" fill="#CCD8FF" points="612.302611 238.37835 541.722496 268.026052 478.115741 116.916178 548.697465 87.2684761"></polygon>
|
||||
<polygon id="Fill-199" fill="#EBEFFF" points="612.591813 238.41999 542.011698 268.067692 478.404943 116.957818 548.986667 87.3101157"></polygon>
|
||||
<polygon id="Stroke-201" stroke="#CCD8FF" stroke-width="2.835" points="548.784692 251.480991 495.00952 123.724652 542.213673 103.896816 595.988845 231.653156"></polygon>
|
||||
<path d="M557.809181,182.706071 C560.583719,175.913514 557.319935,168.162765 550.521997,165.39108 C543.722448,162.621003 535.962112,165.879823 533.189184,172.672379 C530.416256,179.463329 533.67843,187.215686 540.476368,189.985763 C547.275916,192.757448 555.034643,189.498628 557.809181,182.706071" id="Fill-203" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-205" fill="#CCD8FF" points="288.693848 712.234677 227.089137 666.812165 324.535916 534.920806 386.139017 580.34171"></polygon>
|
||||
<polygon id="Fill-207" fill="#EBEFFF" points="288.805537 712.503164 227.202436 667.08226 324.647605 535.190901 386.252316 580.611805"></polygon>
|
||||
<polygon id="Stroke-209" stroke="#CCD8FF" stroke-width="2.835" points="244.933082 664.4117 327.319466 552.904442 368.520704 583.280435 286.134321 694.790908"></polygon>
|
||||
<path d="M308.707211,636.98022 C315.968646,635.88698 320.96732,629.120147 319.872954,621.867786 C318.780199,614.613817 312.0064,609.620283 304.744966,610.713523 C297.483531,611.806763 292.484857,618.573597 293.580832,625.827565 C294.673588,633.079926 301.447386,638.07346 308.707211,636.98022" id="Fill-211" fill="#CCD8FF"></path>
|
||||
<polygon id="Fill-213" fill="#CCD8FF" points="477.226408 790.23831 405.711255 817.564486 347.088689 664.457841 418.600623 637.131666"></polygon>
|
||||
<polygon id="Fill-215" fill="#EBEFFF" points="477.514001 790.288471 405.998848 817.614646 347.376282 664.508002 418.888216 637.181826"></polygon>
|
||||
<g id="Group-223" transform="translate(329.918905, 286.740873)">
|
||||
<mask id="mask-6" fill="white">
|
||||
<use xlink:href="#path-5"></use>
|
||||
</mask>
|
||||
<g id="Clip-222"></g>
|
||||
<polygon id="Fill-221" fill="#DDE4FB" mask="url(#mask-6)" points="522.863688 368.588254 369.235743 522.05824 0.80162247 154.00294 154.429567 0.532954466"></polygon>
|
||||
</g>
|
||||
<polygon id="Fill-224" fill="#BCC7EB" points="441.040903 325.419702 445.749891 325.83449 358.636816 412.857996 350.807278 412.128097"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Group-7" transform="translate(40.000000, 572.000000)">
|
||||
<g id="Group-3" transform="translate(0.207729, 0.000000)">
|
||||
<mask id="mask-8" fill="white">
|
||||
<use xlink:href="#path-7"></use>
|
||||
</mask>
|
||||
<g id="Clip-2"></g>
|
||||
<path d="M324.29049,64.5305141 L266.17857,99.2065236 L228.516424,89.6598214 L238.051117,51.9502402 L288.008735,22.1402264 L296.161962,27.8615687 L296.161962,17.275308 C295.010741,16.3499394 293.829395,15.4730475 292.63191,14.6198554 L292.325276,14.388244 L292.315593,14.3936303 C267.762306,-2.93737224 234.347844,-5.30519568 206.875619,11.0885883 C173.227685,31.1655334 160.03383,72.4871765 174.17556,107.776148 L7.18394257,274.978094 C-2.39486271,284.567887 -2.39486271,300.117097 7.18394257,309.709044 C16.7627479,319.298836 32.2934739,319.298836 41.8712033,309.709044 L205.878253,145.494348 C229.967824,160.332564 261.261672,161.606966 287.239461,146.105156 C316.173844,128.839866 329.977738,95.8680864 324.29049,64.5305141" id="Fill-1" fill="#CCD8FF" mask="url(#mask-8)"></path>
|
||||
</g>
|
||||
<g id="Group-6" transform="translate(0.207729, 10.000000)">
|
||||
<mask id="mask-10" fill="white">
|
||||
<use xlink:href="#path-9"></use>
|
||||
</mask>
|
||||
<g id="Clip-5"></g>
|
||||
<path d="M324.29049,64.6713249 L266.17857,99.3699758 L228.516424,89.8170402 L238.051117,52.0828368 L296.161962,17.3852639 C271.34185,-2.54740729 235.810002,-6.0831361 206.875619,11.1934266 C173.227685,31.2845588 160.03383,72.6331825 174.17556,107.945196 L7.18394257,275.255237 C-2.39486271,284.852369 -2.39486271,300.411732 7.18394257,310.009942 C16.7627479,319.605996 32.2934739,319.605996 41.8712033,310.009942 L205.878253,145.688023 C229.9689,160.535928 261.261672,161.811162 287.239461,146.29923 C316.173844,129.022667 329.977738,96.0293588 324.29049,64.6713249" id="Fill-4" fill="#EBEFFF" mask="url(#mask-10)"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 48 KiB |
54
packages/admin-ui/src/App.jsx
Normal file
54
packages/admin-ui/src/App.jsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import CssBaseline from '@mui/material/CssBaseline'
|
||||
import { ThemeProvider, StyledEngineProvider } from '@mui/material/styles'
|
||||
import React, { useState } from 'react'
|
||||
import { Router } from 'wouter'
|
||||
import ApolloProvider from './utils/apollo'
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers'
|
||||
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFnsV2'
|
||||
|
||||
import AppContext from './AppContext'
|
||||
import theme from './styling/theme'
|
||||
|
||||
import Main from './Main'
|
||||
import './styling/global/global.css'
|
||||
import useLocationWithConfirmation from './routing/useLocationWithConfirmation.js'
|
||||
|
||||
const App = () => {
|
||||
const [wizardTested, setWizardTested] = useState(false)
|
||||
const [userData, setUserData] = useState(null)
|
||||
const [isDirtyForm, setDirtyForm] = useState(false)
|
||||
|
||||
const setRole = role => {
|
||||
if (userData && role && userData.role !== role) {
|
||||
setUserData({ ...userData, role })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
wizardTested,
|
||||
setWizardTested,
|
||||
userData,
|
||||
setUserData,
|
||||
setRole,
|
||||
isDirtyForm,
|
||||
setDirtyForm,
|
||||
}}>
|
||||
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
||||
<Router hook={useLocationWithConfirmation}>
|
||||
<ApolloProvider>
|
||||
<StyledEngineProvider enableCssLayer>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Main />
|
||||
</ThemeProvider>
|
||||
</StyledEngineProvider>
|
||||
</ApolloProvider>
|
||||
</Router>
|
||||
</LocalizationProvider>
|
||||
</AppContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
3
packages/admin-ui/src/AppContext.js
Normal file
3
packages/admin-ui/src/AppContext.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import React from 'react'
|
||||
|
||||
export default React.createContext()
|
||||
104
packages/admin-ui/src/Main.jsx
Normal file
104
packages/admin-ui/src/Main.jsx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { useLocation } from 'wouter'
|
||||
import React, { useContext, useState } from 'react'
|
||||
import { gql, useQuery } from '@apollo/client'
|
||||
import Slide from '@mui/material/Slide'
|
||||
import Grid from '@mui/material/Grid'
|
||||
|
||||
import Header from './components/layout/Header.jsx'
|
||||
import Sidebar from './components/layout/Sidebar.jsx'
|
||||
import TitleSection from './components/layout/TitleSection.jsx'
|
||||
import { getParent, hasSidebar, Routes, tree } from './routing/routes.jsx'
|
||||
import Wizard from './pages/Wizard/Wizard.jsx'
|
||||
|
||||
import AppContext from './AppContext.js'
|
||||
|
||||
const GET_USER_DATA = gql`
|
||||
query userData {
|
||||
userData {
|
||||
id
|
||||
username
|
||||
role
|
||||
enabled
|
||||
last_accessed
|
||||
last_accessed_from
|
||||
last_accessed_address
|
||||
}
|
||||
restrictionLevel
|
||||
}
|
||||
`
|
||||
|
||||
const Main = () => {
|
||||
const [location, navigate] = useLocation()
|
||||
const { wizardTested, userData, setUserData } = useContext(AppContext)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [restrictionLevel, setRestrictionLevel] = useState(null)
|
||||
|
||||
useQuery(GET_USER_DATA, {
|
||||
onCompleted: userResponse => {
|
||||
if (!userData && userResponse?.userData) {
|
||||
setUserData(userResponse.userData)
|
||||
}
|
||||
if (userResponse?.restrictionLevel !== undefined) {
|
||||
setRestrictionLevel(userResponse.restrictionLevel)
|
||||
}
|
||||
setLoading(false)
|
||||
},
|
||||
})
|
||||
|
||||
const sidebar = hasSidebar(location)
|
||||
const parent = sidebar ? getParent(location) : {}
|
||||
|
||||
const is404 = location === '/404'
|
||||
|
||||
const isSelected = it => location === it.route
|
||||
|
||||
const onClick = it => navigate(it.route)
|
||||
|
||||
const contentClassName = sidebar ? 'flex-1 ml-12 pt-4' : 'w-[1200px]'
|
||||
|
||||
// Show loading state until userData is fetched
|
||||
if (loading) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
if (!wizardTested && !is404 && userData) {
|
||||
return <Wizard />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full min-h-full">
|
||||
{!is404 && wizardTested && (
|
||||
<Header
|
||||
tree={tree}
|
||||
user={userData}
|
||||
restrictionLevel={restrictionLevel}
|
||||
/>
|
||||
)}
|
||||
<main className="flex flex-1 flex-col my-0 mx-auto h-full w-[1200px]">
|
||||
{sidebar && !is404 && wizardTested && (
|
||||
<Slide direction="left" in={true} mountOnEnter unmountOnExit>
|
||||
<div>
|
||||
<TitleSection title={parent.title}></TitleSection>
|
||||
</div>
|
||||
</Slide>
|
||||
)}
|
||||
|
||||
<Grid sx={{ flex: 1, height: 1 }} container>
|
||||
{sidebar && !is404 && wizardTested && (
|
||||
<Sidebar
|
||||
data={parent.children}
|
||||
isSelected={isSelected}
|
||||
displayName={it => it.label}
|
||||
onClick={onClick}
|
||||
/>
|
||||
)}
|
||||
<div className={contentClassName}>
|
||||
<Routes />
|
||||
</div>
|
||||
</Grid>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Main
|
||||
48
packages/admin-ui/src/components/Carousel.jsx
Normal file
48
packages/admin-ui/src/components/Carousel.jsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import React, { memo, useState } from 'react'
|
||||
import styles from './Carousel.module.css'
|
||||
import LeftArrow from '../styling/icons/arrow/carousel-left-arrow.svg?react'
|
||||
import RightArrow from '../styling/icons/arrow/carousel-right-arrow.svg?react'
|
||||
|
||||
export const Carousel = memo(({ photosData, slidePhoto }) => {
|
||||
const [activeIndex, setActiveIndex] = useState(0)
|
||||
|
||||
const handlePrev = () => {
|
||||
const newIndex = activeIndex === 0 ? photosData.length - 1 : activeIndex - 1
|
||||
setActiveIndex(newIndex)
|
||||
slidePhoto(newIndex)
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
const newIndex = activeIndex === photosData.length - 1 ? 0 : activeIndex + 1
|
||||
setActiveIndex(newIndex)
|
||||
slidePhoto(newIndex)
|
||||
}
|
||||
|
||||
if (!photosData || photosData.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.carouselContainer}>
|
||||
{photosData.length > 1 && (
|
||||
<button onClick={handlePrev} className={styles.navButton}>
|
||||
<LeftArrow />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className={styles.imageContainer}>
|
||||
<img
|
||||
className={styles.image}
|
||||
src={`/${photosData[activeIndex]?.photoDir}/${photosData[activeIndex]?.path}`}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
{photosData.length > 1 && (
|
||||
<button onClick={handleNext} className={styles.navButton}>
|
||||
<RightArrow />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
45
packages/admin-ui/src/components/Carousel.module.css
Normal file
45
packages/admin-ui/src/components/Carousel.module.css
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
.carouselContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.imageContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.image {
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.navButton {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
color: transparent;
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.navButton:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
21
packages/admin-ui/src/components/CollapsibleCard.jsx
Normal file
21
packages/admin-ui/src/components/CollapsibleCard.jsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react'
|
||||
|
||||
import Paper from '@mui/material/Paper'
|
||||
import classnames from 'classnames'
|
||||
|
||||
const cardState = Object.freeze({
|
||||
DEFAULT: 'default',
|
||||
SHRUNK: 'shrunk',
|
||||
EXPANDED: 'expanded',
|
||||
})
|
||||
|
||||
const CollapsibleCard = ({ className, state, shrunkComponent, children }) => {
|
||||
return (
|
||||
<Paper className={classnames('p-6', className)}>
|
||||
{state === cardState.SHRUNK ? shrunkComponent : children}
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
export default CollapsibleCard
|
||||
export { cardState }
|
||||
105
packages/admin-ui/src/components/ConfirmDialog.jsx
Normal file
105
packages/admin-ui/src/components/ConfirmDialog.jsx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import Dialog from '@mui/material/Dialog'
|
||||
import DialogActions from '@mui/material/DialogActions'
|
||||
import DialogContent from '@mui/material/DialogContent'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import InputLabel from '@mui/material/InputLabel'
|
||||
import React, { memo, useState } from 'react'
|
||||
import { H4, P } from './typography'
|
||||
import CloseIcon from '../styling/icons/action/close/zodiac.svg?react'
|
||||
|
||||
import { Button } from './buttons'
|
||||
import { TextInput } from './inputs'
|
||||
|
||||
import ErrorMessage from './ErrorMessage'
|
||||
import SvgIcon from '@mui/material/SvgIcon'
|
||||
|
||||
export const DialogTitle = ({ children, onClose }) => {
|
||||
return (
|
||||
<div className="p-4 pr-3 flex justify-between">
|
||||
{children}
|
||||
{onClose && (
|
||||
<IconButton aria-label="close" onClick={onClose} className="p-0 -mt-1">
|
||||
<SvgIcon fontSize="small">
|
||||
<CloseIcon />
|
||||
</SvgIcon>
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ConfirmDialog = memo(
|
||||
({
|
||||
title = 'Confirm action',
|
||||
errorMessage = 'This action requires confirmation',
|
||||
open,
|
||||
toBeConfirmed,
|
||||
saveButtonAlwaysEnabled = false,
|
||||
message,
|
||||
confirmationMessage = `Write '${toBeConfirmed}' to confirm this action`,
|
||||
onConfirmed,
|
||||
onDismissed,
|
||||
initialValue = '',
|
||||
disabled = false,
|
||||
...props
|
||||
}) => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [error, setError] = useState(false)
|
||||
const handleChange = event => setValue(event.target.value)
|
||||
|
||||
const innerOnClose = () => {
|
||||
setValue('')
|
||||
setError(false)
|
||||
onDismissed()
|
||||
}
|
||||
|
||||
const isOnErrorState =
|
||||
(!saveButtonAlwaysEnabled && toBeConfirmed !== value) || value === ''
|
||||
|
||||
return (
|
||||
<Dialog open={open} aria-labelledby="form-dialog-title" {...props}>
|
||||
<DialogTitle id="customized-dialog-title" onClose={innerOnClose}>
|
||||
<H4 noMargin>{title}</H4>
|
||||
</DialogTitle>
|
||||
{errorMessage && (
|
||||
<DialogTitle>
|
||||
<ErrorMessage>
|
||||
{errorMessage.split(':').map(error => (
|
||||
<>
|
||||
{error}
|
||||
<br />
|
||||
</>
|
||||
))}
|
||||
</ErrorMessage>
|
||||
</DialogTitle>
|
||||
)}
|
||||
<DialogContent className="w-108 p-4 pr-7">
|
||||
{message && <P>{message}</P>}
|
||||
<InputLabel htmlFor="confirm-input">{confirmationMessage}</InputLabel>
|
||||
<TextInput
|
||||
disabled={disabled}
|
||||
name="confirm-input"
|
||||
autoFocus
|
||||
id="confirm-input"
|
||||
type="text"
|
||||
size="sm"
|
||||
fullWidth
|
||||
value={value}
|
||||
touched={{}}
|
||||
error={error}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions className="p-8 pt-4">
|
||||
<Button
|
||||
color="green"
|
||||
disabled={isOnErrorState}
|
||||
onClick={() => onConfirmed(value)}>
|
||||
Confirm
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
},
|
||||
)
|
||||
80
packages/admin-ui/src/components/CopyToClipboard.jsx
Normal file
80
packages/admin-ui/src/components/CopyToClipboard.jsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import classnames from 'classnames'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { CopyToClipboard as ReactCopyToClipboard } from 'react-copy-to-clipboard'
|
||||
import Popover from './Popper.jsx'
|
||||
import CopyIcon from '../styling/icons/action/copy/copy.svg?react'
|
||||
|
||||
import { comet } from '../styling/variables.js'
|
||||
|
||||
import { Label1, Mono } from './typography/index.jsx'
|
||||
|
||||
const CopyToClipboard = ({
|
||||
className,
|
||||
buttonClassname,
|
||||
children,
|
||||
value,
|
||||
wrapperClassname,
|
||||
removeSpace = true,
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (anchorEl) setTimeout(() => setAnchorEl(null), 3000)
|
||||
}, [anchorEl])
|
||||
|
||||
const handleClick = event => {
|
||||
setAnchorEl(anchorEl ? null : event.currentTarget)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const open = Boolean(anchorEl)
|
||||
const id = open ? 'simple-popper' : undefined
|
||||
|
||||
const text = value
|
||||
? value
|
||||
: removeSpace
|
||||
? R.replace(/\s/g, '')(children ?? '')
|
||||
: children
|
||||
|
||||
return (
|
||||
<div className={classnames('flex items-center', wrapperClassname)}>
|
||||
{children && (
|
||||
<>
|
||||
<Mono
|
||||
noMargin
|
||||
className={classnames('linebreak-anywhere', className)}>
|
||||
{children}
|
||||
</Mono>
|
||||
<div className={buttonClassname}>
|
||||
<ReactCopyToClipboard text={text}>
|
||||
<button
|
||||
className="border-0 bg-transparent cursor-pointer"
|
||||
aria-describedby={id}
|
||||
onClick={event => handleClick(event)}>
|
||||
<CopyIcon />
|
||||
</button>
|
||||
</ReactCopyToClipboard>
|
||||
</div>
|
||||
<Popover
|
||||
id={id}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
bgColor={comet}
|
||||
className="py-1 px-2"
|
||||
placement="top">
|
||||
<Label1 noMargin className="text-white rounded-sm">
|
||||
Copied to clipboard!
|
||||
</Label1>
|
||||
</Popover>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CopyToClipboard
|
||||
65
packages/admin-ui/src/components/DeleteDialog.jsx
Normal file
65
packages/admin-ui/src/components/DeleteDialog.jsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import Dialog from '@mui/material/Dialog'
|
||||
import DialogActions from '@mui/material/DialogActions'
|
||||
import DialogContent from '@mui/material/DialogContent'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import React from 'react'
|
||||
import { H4, P } from './typography'
|
||||
import CloseIcon from '../styling/icons/action/close/zodiac.svg?react'
|
||||
|
||||
import { Button } from './buttons'
|
||||
|
||||
import ErrorMessage from './ErrorMessage'
|
||||
import SvgIcon from '@mui/material/SvgIcon'
|
||||
|
||||
export const DialogTitle = ({ children, close }) => {
|
||||
return (
|
||||
<div className="p-4 pr-3 flex justify-between m-0">
|
||||
{children}
|
||||
{close && (
|
||||
<IconButton aria-label="close" onClick={close} className="p-0 -mt-1">
|
||||
<SvgIcon fontSize="small">
|
||||
<CloseIcon />
|
||||
</SvgIcon>
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const DeleteDialog = ({
|
||||
title = 'Confirm Delete',
|
||||
open = false,
|
||||
onConfirmed,
|
||||
onDismissed,
|
||||
item = 'item',
|
||||
confirmationMessage = `Are you sure you want to delete this ${item}?`,
|
||||
extraMessage,
|
||||
errorMessage = '',
|
||||
}) => {
|
||||
return (
|
||||
<Dialog open={open} aria-labelledby="form-dialog-title">
|
||||
<DialogTitle close={() => onDismissed()}>
|
||||
<H4 className="m-0">{title}</H4>
|
||||
</DialogTitle>
|
||||
{errorMessage && (
|
||||
<DialogTitle>
|
||||
<ErrorMessage>
|
||||
{errorMessage.split(':').map(error => (
|
||||
<>
|
||||
{error}
|
||||
<br />
|
||||
</>
|
||||
))}
|
||||
</ErrorMessage>
|
||||
</DialogTitle>
|
||||
)}
|
||||
<DialogContent className="w-108 p-4 pr-7">
|
||||
{confirmationMessage && <P>{confirmationMessage}</P>}
|
||||
{extraMessage}
|
||||
</DialogContent>
|
||||
<DialogActions className="p-8 pt-4">
|
||||
<Button onClick={onConfirmed}>Confirm</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
18
packages/admin-ui/src/components/ErrorMessage.jsx
Normal file
18
packages/admin-ui/src/components/ErrorMessage.jsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import classnames from 'classnames'
|
||||
import React from 'react'
|
||||
import ErrorIcon from '../styling/icons/warning-icon/tomato.svg?react'
|
||||
|
||||
import { Info3 } from './typography'
|
||||
|
||||
const ErrorMessage = ({ className, children }) => {
|
||||
return (
|
||||
<div className={classnames('flex items-center', className)}>
|
||||
<ErrorIcon className="mr-3" />
|
||||
<Info3 className="flex items-center text-tomato m-0 whitespace-break-spaces">
|
||||
{children}
|
||||
</Info3>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ErrorMessage
|
||||
56
packages/admin-ui/src/components/ImagePopper.jsx
Normal file
56
packages/admin-ui/src/components/ImagePopper.jsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import ClickAwayListener from '@mui/material/ClickAwayListener'
|
||||
import classnames from 'classnames'
|
||||
import React, { memo, useState } from 'react'
|
||||
import Popper from './Popper'
|
||||
import ZoomIconInverse from '../styling/icons/circle buttons/search/white.svg?react'
|
||||
import ZoomIcon from '../styling/icons/circle buttons/search/zodiac.svg?react'
|
||||
|
||||
import { FeatureButton } from './buttons'
|
||||
|
||||
const ImagePopper = memo(
|
||||
({ className, width, height, popupWidth, popupHeight, src }) => {
|
||||
const [popperAnchorEl, setPopperAnchorEl] = useState(null)
|
||||
|
||||
const handleOpenPopper = event => {
|
||||
setPopperAnchorEl(popperAnchorEl ? null : event.currentTarget)
|
||||
}
|
||||
|
||||
const handleClosePopper = () => {
|
||||
setPopperAnchorEl(null)
|
||||
}
|
||||
|
||||
const popperOpen = Boolean(popperAnchorEl)
|
||||
|
||||
const Image = ({ className, style }) => (
|
||||
<img className={classnames(className)} style={style} src={src} alt="" />
|
||||
)
|
||||
|
||||
return (
|
||||
<ClickAwayListener onClickAway={handleClosePopper}>
|
||||
<div className={classnames('flex flex-row', className)}>
|
||||
<Image
|
||||
className="object-cover rounded-tl-lg"
|
||||
style={{ width, height }}
|
||||
/>
|
||||
<FeatureButton
|
||||
Icon={ZoomIcon}
|
||||
InverseIcon={ZoomIconInverse}
|
||||
className="rounded-none rounded-tr-lg rounded-br-lg"
|
||||
style={{ height }}
|
||||
onClick={handleOpenPopper}
|
||||
/>
|
||||
<Popper open={popperOpen} anchorEl={popperAnchorEl} placement="top">
|
||||
<div className="py-2 px-4">
|
||||
<Image
|
||||
className="object-cover"
|
||||
style={{ width: popupWidth, height: popupHeight }}
|
||||
/>
|
||||
</div>
|
||||
</Popper>
|
||||
</div>
|
||||
</ClickAwayListener>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export default ImagePopper
|
||||
39
packages/admin-ui/src/components/InformativeDialog.jsx
Normal file
39
packages/admin-ui/src/components/InformativeDialog.jsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import Dialog from '@mui/material/Dialog'
|
||||
import DialogContent from '@mui/material/DialogContent'
|
||||
import SvgIcon from '@mui/material/SvgIcon'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import React, { memo } from 'react'
|
||||
import { H1 } from './typography'
|
||||
import CloseIcon from '../styling/icons/action/close/zodiac.svg?react'
|
||||
|
||||
export const InformativeDialog = memo(
|
||||
({ title = '', open, onDissmised, data, ...props }) => {
|
||||
const innerOnClose = () => {
|
||||
onDissmised()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
PaperProps={{
|
||||
style: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
open={open}
|
||||
aria-labelledby="form-dialog-title"
|
||||
{...props}>
|
||||
<div className="flex justify-end pt-4 pr-3 pb-0 pl-4">
|
||||
<IconButton aria-label="close" onClick={innerOnClose}>
|
||||
<SvgIcon fontSize="small">
|
||||
<CloseIcon />
|
||||
</SvgIcon>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
<H1 className="mt-0 mr-4 mb-2 ml-5">{title}</H1>
|
||||
<DialogContent>{data}</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
},
|
||||
)
|
||||
235
packages/admin-ui/src/components/LogsDownloaderPopper.jsx
Normal file
235
packages/admin-ui/src/components/LogsDownloaderPopper.jsx
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
import { useLazyQuery } from '@apollo/client'
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener'
|
||||
import classnames from 'classnames'
|
||||
import { format, set } from 'date-fns/fp'
|
||||
import FileSaver from 'file-saver'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState, useCallback } from 'react'
|
||||
import Arrow from '../styling/icons/arrow/download_logs.svg?react'
|
||||
import DownloadInverseIcon from '../styling/icons/button/download/white.svg?react'
|
||||
import Download from '../styling/icons/button/download/zodiac.svg?react'
|
||||
|
||||
import { FeatureButton, Link } from './buttons'
|
||||
import { formatDate } from '../utils/timezones'
|
||||
|
||||
import Popper from './Popper'
|
||||
import DateRangePicker from './date-range-picker/DateRangePicker'
|
||||
import { RadioGroup } from './inputs'
|
||||
import { H4, Info1, Label1, Label2 } from './typography/index.jsx'
|
||||
|
||||
const DateContainer = ({ date, children }) => {
|
||||
return (
|
||||
<div className="h-11 w-25">
|
||||
<Label1 noMargin>{children}</Label1>
|
||||
{date && (
|
||||
<>
|
||||
<div className="flex">
|
||||
<Info1 noMargin className="mr-2">
|
||||
{format('d', date)}
|
||||
</Info1>
|
||||
<div className="flex flex-col">
|
||||
<Label2 noMargin>{`${format(
|
||||
'MMM',
|
||||
date,
|
||||
)} ${format('yyyy', date)}`}</Label2>
|
||||
<Label1 noMargin className="text-comet">
|
||||
{format('EEEE', date)}
|
||||
</Label1>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ALL = 'all'
|
||||
const RANGE = 'range'
|
||||
const ADVANCED = 'advanced'
|
||||
const SIMPLIFIED = 'simplified'
|
||||
|
||||
const LogsDownloaderPopover = ({
|
||||
name,
|
||||
query,
|
||||
args,
|
||||
title,
|
||||
getLogs,
|
||||
timezone,
|
||||
simplified,
|
||||
className,
|
||||
}) => {
|
||||
const [selectedRadio, setSelectedRadio] = useState(ALL)
|
||||
const [selectedAdvancedRadio, setSelectedAdvancedRadio] = useState(ADVANCED)
|
||||
|
||||
const [range, setRange] = useState({ from: null, until: null })
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const [fetchLogs] = useLazyQuery(query, {
|
||||
onCompleted: data => createLogsFile(getLogs(data), range),
|
||||
})
|
||||
|
||||
const dateRangePickerClasses = {
|
||||
'block h-full': selectedRadio === RANGE,
|
||||
hidden: selectedRadio === ALL,
|
||||
}
|
||||
|
||||
const handleRadioButtons = evt => {
|
||||
const selectedRadio = R.path(['target', 'value'])(evt)
|
||||
setSelectedRadio(selectedRadio)
|
||||
if (selectedRadio === ALL) setRange({ from: null, until: null })
|
||||
}
|
||||
|
||||
const handleAdvancedRadioButtons = evt => {
|
||||
const selectedAdvancedRadio = R.path(['target', 'value'])(evt)
|
||||
setSelectedAdvancedRadio(selectedAdvancedRadio)
|
||||
}
|
||||
|
||||
const handleRangeChange = useCallback(
|
||||
(from, until) => {
|
||||
setRange({ from, until })
|
||||
},
|
||||
[setRange],
|
||||
)
|
||||
|
||||
const downloadLogs = (range, args) => {
|
||||
if (selectedRadio === ALL) {
|
||||
fetchLogs({
|
||||
variables: {
|
||||
...args,
|
||||
simplified: selectedAdvancedRadio === SIMPLIFIED,
|
||||
excludeTestingCustomers: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (!range || !range.from) return
|
||||
if (range.from && !range.until) range.until = new Date()
|
||||
|
||||
if (selectedRadio === RANGE) {
|
||||
fetchLogs({
|
||||
variables: {
|
||||
...args,
|
||||
from: range.from,
|
||||
until: range.until,
|
||||
simplified: selectedAdvancedRadio === SIMPLIFIED,
|
||||
excludeTestingCustomers: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const createLogsFile = (logs, range) => {
|
||||
const formatDateFile = date => {
|
||||
return formatDate(date, timezone, 'yyyy-MM-dd_HH-mm')
|
||||
}
|
||||
|
||||
const blob = new window.Blob([logs], {
|
||||
type: 'text/plain;charset=utf-8',
|
||||
})
|
||||
|
||||
FileSaver.saveAs(
|
||||
blob,
|
||||
selectedRadio === ALL
|
||||
? `${formatDateFile(new Date())}_${name}.csv`
|
||||
: `${formatDateFile(range.from)}_${formatDateFile(
|
||||
range.until,
|
||||
)}_${name}.csv`,
|
||||
)
|
||||
}
|
||||
|
||||
const handleOpenRangePicker = event => {
|
||||
setAnchorEl(anchorEl ? null : event.currentTarget)
|
||||
}
|
||||
|
||||
const handleClickAway = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const radioButtonOptions = [
|
||||
{ display: 'All logs', code: ALL },
|
||||
{ display: 'Date range', code: RANGE },
|
||||
]
|
||||
|
||||
const advancedRadioButtonOptions = [
|
||||
{ display: 'Advanced logs', code: ADVANCED },
|
||||
{ display: 'Simplified logs', code: SIMPLIFIED },
|
||||
]
|
||||
|
||||
const open = Boolean(anchorEl)
|
||||
const id = open ? 'date-range-popover' : undefined
|
||||
|
||||
return (
|
||||
<ClickAwayListener onClickAway={handleClickAway}>
|
||||
<div className={className}>
|
||||
<FeatureButton
|
||||
Icon={Download}
|
||||
InverseIcon={DownloadInverseIcon}
|
||||
onClick={handleOpenRangePicker}
|
||||
variant="contained"
|
||||
/>
|
||||
<Popper id={id} open={open} anchorEl={anchorEl} placement="bottom">
|
||||
<div className="w-70">
|
||||
<H4 noMargin className="p-4 pb-0">
|
||||
{title}
|
||||
</H4>
|
||||
<div className="py-1 px-4">
|
||||
<RadioGroup
|
||||
name="logs-select"
|
||||
value={selectedRadio}
|
||||
options={radioButtonOptions}
|
||||
ariaLabel="logs-select"
|
||||
onChange={handleRadioButtons}
|
||||
className="flex flex-row justify-between text-zodiac"
|
||||
/>
|
||||
</div>
|
||||
{selectedRadio === RANGE && (
|
||||
<div className={classnames(dateRangePickerClasses)}>
|
||||
<div className="flex justify-between items-center py-0 px-4 bg-zircon relative min-h-20">
|
||||
{range && (
|
||||
<>
|
||||
<DateContainer date={range.from}>From</DateContainer>
|
||||
<div className="absolute left-31 top-6">
|
||||
<Arrow className="m-auto" />
|
||||
</div>
|
||||
<DateContainer date={range.until}>To</DateContainer>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<DateRangePicker
|
||||
maxDate={set(
|
||||
{
|
||||
hours: 23,
|
||||
minutes: 59,
|
||||
seconds: 59,
|
||||
milliseconds: 999,
|
||||
},
|
||||
new Date(),
|
||||
)}
|
||||
onRangeChange={handleRangeChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{simplified && (
|
||||
<div className="py-1 px-4">
|
||||
<RadioGroup
|
||||
name="simplified-tx-logs"
|
||||
value={selectedAdvancedRadio}
|
||||
options={advancedRadioButtonOptions}
|
||||
ariaLabel="simplified-tx-logs"
|
||||
onChange={handleAdvancedRadioButtons}
|
||||
className="flex flex-row justify-between text-zodiac"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="py-3 px-4">
|
||||
<Link color="primary" onClick={() => downloadLogs(range, args)}>
|
||||
Download
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</Popper>
|
||||
</div>
|
||||
</ClickAwayListener>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogsDownloaderPopover
|
||||
96
packages/admin-ui/src/components/Modal.jsx
Normal file
96
packages/admin-ui/src/components/Modal.jsx
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import MaterialModal from '@mui/material/Modal'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import SvgIcon from '@mui/material/SvgIcon'
|
||||
import Paper from '@mui/material/Paper'
|
||||
import classnames from 'classnames'
|
||||
import React from 'react'
|
||||
import { H1, H4 } from './typography'
|
||||
import CloseIcon from '../styling/icons/action/close/zodiac.svg?react'
|
||||
|
||||
const Modal = ({
|
||||
width,
|
||||
height,
|
||||
minHeight = 400,
|
||||
infoPanelHeight,
|
||||
title,
|
||||
small,
|
||||
xl,
|
||||
infoPanel,
|
||||
handleClose,
|
||||
children,
|
||||
className,
|
||||
closeOnEscape,
|
||||
closeOnBackdropClick,
|
||||
...props
|
||||
}) => {
|
||||
const TitleCase = small ? H4 : H1
|
||||
|
||||
const innerClose = (evt, reason) => {
|
||||
if (!closeOnBackdropClick && reason === 'backdropClick') return
|
||||
if (!closeOnEscape && reason === 'escapeKeyDown') return
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const marginBySize = xl ? 0 : small ? 12 : 16
|
||||
const paddingBySize = xl ? 88 : small ? 16 : 32
|
||||
return (
|
||||
<MaterialModal
|
||||
onClose={innerClose}
|
||||
className="flex justify-center flex-col items-center"
|
||||
{...props}>
|
||||
<>
|
||||
<Paper
|
||||
style={{ width, height, minHeight: height ?? minHeight }}
|
||||
className={classnames(
|
||||
'flex flex-col max-h-[90vh] rounded-lg outline-0',
|
||||
className,
|
||||
)}>
|
||||
<div className="flex">
|
||||
{title && (
|
||||
<TitleCase
|
||||
className={
|
||||
small ? 'mt-5 mr-0 mb-2 ml-4' : 'mt-7 mr-0 mb-2 ml-8'
|
||||
}>
|
||||
{title}
|
||||
</TitleCase>
|
||||
)}
|
||||
<div
|
||||
className="ml-auto"
|
||||
style={{ marginRight: marginBySize, marginTop: marginBySize }}>
|
||||
<IconButton
|
||||
className="p-0 mb-auto ml-auto"
|
||||
onClick={() => handleClose()}>
|
||||
<SvgIcon fontSize={xl ? 'large' : 'small'}>
|
||||
<CloseIcon />
|
||||
</SvgIcon>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="w-full flex flex-col flex-1"
|
||||
style={{ paddingRight: paddingBySize, paddingLeft: paddingBySize }}>
|
||||
{children}
|
||||
</div>
|
||||
</Paper>
|
||||
{infoPanel && (
|
||||
<Paper
|
||||
style={{
|
||||
width,
|
||||
height: infoPanelHeight,
|
||||
minHeight: infoPanelHeight ?? 200,
|
||||
}}
|
||||
className={classnames(
|
||||
'mt-4 flex flex-col max-h-[90vh] overflow-y-auto rounded-lg outline-0',
|
||||
className,
|
||||
)}>
|
||||
<div className="w-full flex flex-col flex-1 py-0 px-6">
|
||||
{infoPanel}
|
||||
</div>
|
||||
</Paper>
|
||||
)}
|
||||
</>
|
||||
</MaterialModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default Modal
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
import { useQuery, useMutation, gql } from '@apollo/client'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import ActionButton from '../buttons/ActionButton'
|
||||
import { H5 } from '../typography'
|
||||
import NotificationIconZodiac from '../../styling/icons/menu/notification-zodiac.svg?react'
|
||||
import ClearAllIconInverse from '../../styling/icons/stage/spring/empty.svg?react'
|
||||
import ClearAllIcon from '../../styling/icons/stage/zodiac/empty.svg?react'
|
||||
import ShowUnreadIcon from '../../styling/icons/stage/zodiac/full.svg?react'
|
||||
|
||||
import NotificationRow from './NotificationRow'
|
||||
import classes from './NotificationCenter.module.css'
|
||||
|
||||
const GET_NOTIFICATIONS = gql`
|
||||
query getNotifications {
|
||||
notifications {
|
||||
id
|
||||
type
|
||||
detail
|
||||
message
|
||||
created
|
||||
read
|
||||
valid
|
||||
}
|
||||
hasUnreadNotifications
|
||||
machines {
|
||||
deviceId
|
||||
name
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const TOGGLE_CLEAR_NOTIFICATION = gql`
|
||||
mutation toggleClearNotification($id: ID!, $read: Boolean!) {
|
||||
toggleClearNotification(id: $id, read: $read) {
|
||||
id
|
||||
read
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const CLEAR_ALL_NOTIFICATIONS = gql`
|
||||
mutation clearAllNotifications {
|
||||
clearAllNotifications {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const NotificationCenter = ({
|
||||
close,
|
||||
hasUnreadProp,
|
||||
buttonCoords,
|
||||
popperRef,
|
||||
refetchHasUnreadHeader,
|
||||
}) => {
|
||||
const { data, loading } = useQuery(GET_NOTIFICATIONS, {
|
||||
pollInterval: 60000,
|
||||
})
|
||||
const [xOffset, setXoffset] = useState(300)
|
||||
|
||||
const [showingUnread, setShowingUnread] = useState(false)
|
||||
const machines = R.compose(
|
||||
R.map(R.prop('name')),
|
||||
R.indexBy(R.prop('deviceId')),
|
||||
)(R.path(['machines'])(data) ?? [])
|
||||
const notifications = R.path(['notifications'])(data) ?? []
|
||||
const [hasUnread, setHasUnread] = useState(hasUnreadProp)
|
||||
|
||||
const [toggleClearNotification] = useMutation(TOGGLE_CLEAR_NOTIFICATION, {
|
||||
onError: () => console.error('Error while clearing notification'),
|
||||
refetchQueries: () => ['getNotifications'],
|
||||
})
|
||||
const [clearAllNotifications] = useMutation(CLEAR_ALL_NOTIFICATIONS, {
|
||||
onError: () => console.error('Error while clearing all notifications'),
|
||||
refetchQueries: () => ['getNotifications'],
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setXoffset(popperRef.current.getBoundingClientRect().x)
|
||||
if (data && data.hasUnreadNotifications !== hasUnread) {
|
||||
refetchHasUnreadHeader()
|
||||
setHasUnread(!hasUnread)
|
||||
}
|
||||
}, [popperRef, data, hasUnread, refetchHasUnreadHeader])
|
||||
|
||||
const buildNotifications = () => {
|
||||
const notificationsToShow =
|
||||
!showingUnread || !hasUnread
|
||||
? notifications
|
||||
: R.filter(R.propEq(false, 'read'))(notifications)
|
||||
return notificationsToShow.map(n => {
|
||||
return (
|
||||
<NotificationRow
|
||||
key={n.id}
|
||||
id={n.id}
|
||||
type={n.type}
|
||||
detail={n.detail}
|
||||
message={n.message}
|
||||
deviceName={machines[n.detail.deviceId]}
|
||||
created={n.created}
|
||||
read={n.read}
|
||||
valid={n.valid}
|
||||
toggleClear={() =>
|
||||
toggleClearNotification({
|
||||
variables: { id: n.id, read: !n.read },
|
||||
})
|
||||
}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.container}>
|
||||
<div className={classes.header}>
|
||||
<H5 className={classes.headerText}>Notifications</H5>
|
||||
<button
|
||||
onClick={close}
|
||||
className={classes.notificationIcon}
|
||||
style={{
|
||||
top: buttonCoords?.y ?? 0,
|
||||
left: buttonCoords?.x ? buttonCoords.x - xOffset : 0,
|
||||
}}>
|
||||
<NotificationIconZodiac />
|
||||
{hasUnread && <div className={classes.hasUnread} />}
|
||||
</button>
|
||||
</div>
|
||||
<div className={classes.actionButtons}>
|
||||
{hasUnread && (
|
||||
<ActionButton
|
||||
color="primary"
|
||||
Icon={ShowUnreadIcon}
|
||||
InverseIcon={ClearAllIconInverse}
|
||||
className={classes.clearAllButton}
|
||||
onClick={() => setShowingUnread(!showingUnread)}>
|
||||
{showingUnread ? 'Show all' : 'Show unread'}
|
||||
</ActionButton>
|
||||
)}
|
||||
{hasUnread && (
|
||||
<ActionButton
|
||||
color="primary"
|
||||
Icon={ClearAllIcon}
|
||||
InverseIcon={ClearAllIconInverse}
|
||||
className={classes.clearAllButton}
|
||||
onClick={clearAllNotifications}>
|
||||
Mark all as read
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
<div className={classes.notificationsList}>
|
||||
{!loading && buildNotifications()}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotificationCenter
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
.container {
|
||||
width: 40vw;
|
||||
height: 110vh;
|
||||
right: 0;
|
||||
background-color: white;
|
||||
box-shadow: 0 0 14px 0 rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1920px) {
|
||||
.container {
|
||||
width: 30vw;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.headerText {
|
||||
margin-top: 20px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
display: flex;
|
||||
margin-left: 16px;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.notificationIcon {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
box-shadow: 0 0 0 transparent;
|
||||
border: 0 solid transparent;
|
||||
text-shadow: 0 0 0 transparent;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.clearAllButton {
|
||||
margin-top: -16px;
|
||||
margin-left: 8px;
|
||||
background-color: var(--zircon);
|
||||
}
|
||||
|
||||
.notificationsList {
|
||||
height: 90vh;
|
||||
max-height: 100vh;
|
||||
margin-top: 24px;
|
||||
margin-left: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background-color: white;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.notificationRow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
padding-top: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.notificationRow > *:first-child {
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.notificationContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.unread {
|
||||
background-color: var(--spring3);
|
||||
}
|
||||
|
||||
.notificationRowIcon {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.notificationRowIcon > * {
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
.readIconWrapper {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.unreadIcon {
|
||||
margin-top: 5px;
|
||||
margin-left: 8px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-color: var(--spring);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.readIcon {
|
||||
margin-left: 8px;
|
||||
margin-top: 5px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 1px solid var(--comet);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.notificationTitle {
|
||||
margin: 0;
|
||||
color: var(--comet);
|
||||
}
|
||||
|
||||
.notificationBody {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.notificationSubtitle {
|
||||
margin: 0;
|
||||
margin-bottom: 8px;
|
||||
color: var(--comet);
|
||||
}
|
||||
|
||||
.stripes {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
opacity: 60%;
|
||||
}
|
||||
|
||||
.hasUnread {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 16px;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
background-color: var(--spring);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import classnames from 'classnames'
|
||||
import prettyMs from 'pretty-ms'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
import { Label1, Label2, TL2 } from '../typography'
|
||||
import Wrench from '../../styling/icons/action/wrench/zodiac.svg?react'
|
||||
import Transaction from '../../styling/icons/arrow/transaction.svg?react'
|
||||
import WarningIcon from '../../styling/icons/warning-icon/tomato.svg?react'
|
||||
|
||||
import classes from './NotificationCenter.module.css'
|
||||
|
||||
const types = {
|
||||
transaction: {
|
||||
display: 'Transactions',
|
||||
icon: <Transaction height={16} width={16} />,
|
||||
},
|
||||
highValueTransaction: {
|
||||
display: 'Transactions',
|
||||
icon: <Transaction height={16} width={16} />,
|
||||
},
|
||||
fiatBalance: {
|
||||
display: 'Maintenance',
|
||||
icon: <Wrench height={16} width={16} />,
|
||||
},
|
||||
cryptoBalance: {
|
||||
display: 'Maintenance',
|
||||
icon: <Wrench height={16} width={16} />,
|
||||
},
|
||||
compliance: {
|
||||
display: 'Compliance',
|
||||
icon: <WarningIcon height={16} width={16} />,
|
||||
},
|
||||
error: { display: 'Error', icon: <WarningIcon height={16} width={16} /> },
|
||||
}
|
||||
|
||||
const NotificationRow = ({
|
||||
id,
|
||||
type,
|
||||
message,
|
||||
deviceName,
|
||||
created,
|
||||
read,
|
||||
valid,
|
||||
toggleClear,
|
||||
}) => {
|
||||
const typeDisplay = R.path([type, 'display'])(types) ?? null
|
||||
const icon = R.path([type, 'icon'])(types) ?? (
|
||||
<Wrench height={16} width={16} />
|
||||
)
|
||||
const age = prettyMs(new Date().getTime() - new Date(created).getTime(), {
|
||||
compact: true,
|
||||
verbose: true,
|
||||
})
|
||||
const notificationTitle =
|
||||
typeDisplay && deviceName
|
||||
? `${typeDisplay} - ${deviceName}`
|
||||
: !typeDisplay && deviceName
|
||||
? `${deviceName}`
|
||||
: `${typeDisplay}`
|
||||
|
||||
const iconClass = {
|
||||
[classes.readIcon]: read,
|
||||
[classes.unreadIcon]: !read,
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
classes.notificationRow,
|
||||
!read && valid ? classes.unread : '',
|
||||
)}>
|
||||
<div className={classes.notificationRowIcon}>
|
||||
<div>{icon}</div>
|
||||
</div>
|
||||
<div className={classes.notificationContent}>
|
||||
<Label2 className={classes.notificationTitle}>
|
||||
{notificationTitle}
|
||||
</Label2>
|
||||
<TL2 className={classes.notificationBody}>{message}</TL2>
|
||||
<Label1 className={classes.notificationSubtitle}>{age}</Label1>
|
||||
</div>
|
||||
<div className={classes.readIconWrapper}>
|
||||
<div
|
||||
onClick={() => toggleClear(id)}
|
||||
className={classnames(iconClass)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotificationRow
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import NotificationCenter from './NotificationCenter'
|
||||
|
||||
export default NotificationCenter
|
||||
77
packages/admin-ui/src/components/Popper.jsx
Normal file
77
packages/admin-ui/src/components/Popper.jsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import MaterialPopper from '@mui/material/Popper'
|
||||
import Paper from '@mui/material/Paper'
|
||||
import classnames from 'classnames'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { white } from '../styling/variables'
|
||||
import classes from './Popper.module.css'
|
||||
|
||||
const Popover = ({ children, bgColor = white, className, ...props }) => {
|
||||
const [arrowRef, setArrowRef] = useState(null)
|
||||
|
||||
const flipPlacements = {
|
||||
top: ['bottom'],
|
||||
bottom: ['top'],
|
||||
left: ['right'],
|
||||
right: ['left'],
|
||||
}
|
||||
|
||||
const modifiers = [
|
||||
{
|
||||
name: 'flip',
|
||||
enabled: R.defaultTo(false, props.flip),
|
||||
options: {
|
||||
allowedAutoPlacements: flipPlacements[props.placement],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
enabled: true,
|
||||
options: {
|
||||
rootBoundary: 'scrollParent',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
enabled: true,
|
||||
options: {
|
||||
offset: [0, 10],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'arrow',
|
||||
enabled: R.defaultTo(true, props.showArrow),
|
||||
options: {
|
||||
element: arrowRef,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'computeStyles',
|
||||
options: {
|
||||
gpuAcceleration: false,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<MaterialPopper
|
||||
disablePortal={false}
|
||||
modifiers={modifiers}
|
||||
className={classnames(classes.tooltip, 'z-3000 rounded-sm')}
|
||||
{...props}>
|
||||
<Paper style={{ backgroundColor: bgColor }} className={className}>
|
||||
<span
|
||||
className={classes.newArrow}
|
||||
data-popper-arrow
|
||||
ref={setArrowRef}
|
||||
/>
|
||||
{children}
|
||||
</Paper>
|
||||
</MaterialPopper>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Popover
|
||||
33
packages/admin-ui/src/components/Popper.module.css
Normal file
33
packages/admin-ui/src/components/Popper.module.css
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
.newArrow,
|
||||
.newArrow::before {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
.newArrow {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.newArrow::before {
|
||||
visibility: visible;
|
||||
content: '';
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.tooltip[data-popper-placement^='top'] > div > span {
|
||||
bottom: -4px;
|
||||
}
|
||||
|
||||
.tooltip[data-popper-placement^='bottom'] > div > span {
|
||||
top: -4px;
|
||||
}
|
||||
|
||||
.tooltip[data-popper-placement^='left'] > div > span {
|
||||
right: -4px;
|
||||
}
|
||||
|
||||
.tooltip[data-popper-placement^='right'] > div > span {
|
||||
left: -4px;
|
||||
}
|
||||
19
packages/admin-ui/src/components/PromptWhenDirty.jsx
Normal file
19
packages/admin-ui/src/components/PromptWhenDirty.jsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { useFormikContext } from 'formik'
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
import useDirtyHandler from '../routing/dirtyHandler.js'
|
||||
|
||||
const PromptWhenDirty = () => {
|
||||
const setIsDirty = useDirtyHandler(state => state.setIsDirty)
|
||||
const formik = useFormikContext()
|
||||
|
||||
const hasChanges = formik.dirty && formik.submitCount === 0
|
||||
|
||||
useEffect(() => {
|
||||
setIsDirty(hasChanges)
|
||||
}, [hasChanges])
|
||||
|
||||
return <></>
|
||||
}
|
||||
|
||||
export default PromptWhenDirty
|
||||
83
packages/admin-ui/src/components/SearchBox.jsx
Normal file
83
packages/admin-ui/src/components/SearchBox.jsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import InputBase from '@mui/material/InputBase'
|
||||
import Paper from '@mui/material/Paper'
|
||||
import MAutocomplete from '@mui/material/Autocomplete'
|
||||
import classnames from 'classnames'
|
||||
import React, { memo, useState } from 'react'
|
||||
import { P } from './typography'
|
||||
import SearchIcon from '../styling/icons/circle buttons/search/zodiac.svg?react'
|
||||
|
||||
const SearchBox = memo(
|
||||
({
|
||||
loading = false,
|
||||
filters = [],
|
||||
options = [],
|
||||
inputPlaceholder = '',
|
||||
onChange,
|
||||
...props
|
||||
}) => {
|
||||
const [popupOpen, setPopupOpen] = useState(false)
|
||||
|
||||
const inputClasses = {
|
||||
'flex flex-1 h-8 px-2 py-2 font-md items-center rounded-2xl bg-zircon text-comet': true,
|
||||
'rounded-b-none': popupOpen,
|
||||
}
|
||||
|
||||
const innerOnChange = filters => onChange(filters)
|
||||
|
||||
return (
|
||||
<MAutocomplete
|
||||
loading={loading}
|
||||
value={filters}
|
||||
options={options}
|
||||
getOptionLabel={it => it.label || it.value}
|
||||
renderOption={(props, it) => (
|
||||
<li {...props}>
|
||||
<div className="flex flex-row w-full h-8">
|
||||
<P className="m-0 whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{it.label || it.value}
|
||||
</P>
|
||||
<P className="m-0 ml-auto text-sm text-come">{it.type}</P>
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
autoHighlight
|
||||
disableClearable
|
||||
clearOnEscape
|
||||
multiple
|
||||
filterSelectedOptions
|
||||
isOptionEqualToValue={(option, value) => option.type === value.type}
|
||||
renderInput={params => {
|
||||
return (
|
||||
<InputBase
|
||||
ref={params.InputProps.ref}
|
||||
{...params}
|
||||
className={classnames(inputClasses)}
|
||||
startAdornment={<SearchIcon className="mr-3" />}
|
||||
placeholder={inputPlaceholder}
|
||||
inputProps={{
|
||||
className: 'font-bold',
|
||||
...params.inputProps,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
onOpen={() => setPopupOpen(true)}
|
||||
onClose={() => setPopupOpen(false)}
|
||||
onChange={(_, filters) => innerOnChange(filters)}
|
||||
{...props}
|
||||
slots={{
|
||||
paper: ({ children }) => (
|
||||
<Paper
|
||||
elevation={0}
|
||||
className="flex flex-col rounded-b-xl bg-zircon shadow-2xl">
|
||||
<div className="w-[88%] h-[1px] my-p mx-auto border-1 border-comet" />
|
||||
{children}
|
||||
</Paper>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export default SearchBox
|
||||
53
packages/admin-ui/src/components/SearchFilter.jsx
Normal file
53
packages/admin-ui/src/components/SearchFilter.jsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import Chip from '@mui/material/Chip'
|
||||
import React from 'react'
|
||||
import { P, Label3 } from './typography'
|
||||
import CloseIcon from '../styling/icons/action/close/zodiac.svg?react'
|
||||
import FilterIcon from '../styling/icons/button/filter/white.svg?react'
|
||||
import ReverseFilterIcon from '../styling/icons/button/filter/zodiac.svg?react'
|
||||
|
||||
import { ActionButton } from './buttons'
|
||||
import { onlyFirstToUpper, singularOrPlural } from '../utils/string'
|
||||
|
||||
const SearchFilter = ({
|
||||
filters,
|
||||
onFilterDelete,
|
||||
deleteAllFilters,
|
||||
entries = 0,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<P className="mx-0">{'Filters:'}</P>
|
||||
<div className="flex mb-4">
|
||||
<div className="mt-auto">
|
||||
{filters.map((f, idx) => (
|
||||
<Chip
|
||||
key={idx}
|
||||
label={`${onlyFirstToUpper(f.type)}: ${f.label || f.value}`}
|
||||
onDelete={() => onFilterDelete(f)}
|
||||
deleteIcon={<CloseIcon className="w-2 h-2 mx-2" />}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex ml-auto justify-end flex-row">
|
||||
{
|
||||
<Label3 className="text-comet m-auto mr-3">{`${entries} ${singularOrPlural(
|
||||
entries,
|
||||
`entry`,
|
||||
`entries`,
|
||||
)}`}</Label3>
|
||||
}
|
||||
<ActionButton
|
||||
altTextColor
|
||||
color="secondary"
|
||||
Icon={ReverseFilterIcon}
|
||||
InverseIcon={FilterIcon}
|
||||
onClick={deleteAllFilters}>
|
||||
Delete filters
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchFilter
|
||||
23
packages/admin-ui/src/components/Status.jsx
Normal file
23
packages/admin-ui/src/components/Status.jsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import Chip from '@mui/material/Chip'
|
||||
import React from 'react'
|
||||
|
||||
const Status = ({ status }) => {
|
||||
return <Chip color={status.type} label={status.label} />
|
||||
}
|
||||
|
||||
const MainStatus = ({ statuses }) => {
|
||||
const mainStatus =
|
||||
statuses.find(s => s.type === 'error') ||
|
||||
statuses.find(s => s.type === 'warning') ||
|
||||
statuses[0]
|
||||
const plus = { label: `+${statuses.length - 1}`, type: mainStatus.type }
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Status status={mainStatus} />
|
||||
{statuses.length > 1 && <Status status={plus} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { Status, MainStatus }
|
||||
61
packages/admin-ui/src/components/Stepper.jsx
Normal file
61
packages/admin-ui/src/components/Stepper.jsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import classnames from 'classnames'
|
||||
import * as R from 'ramda'
|
||||
import React, { memo } from 'react'
|
||||
import CompleteStageIconSpring from '../styling/icons/stage/spring/complete.svg?react'
|
||||
import CurrentStageIconSpring from '../styling/icons/stage/spring/current.svg?react'
|
||||
import EmptyStageIconSpring from '../styling/icons/stage/spring/empty.svg?react'
|
||||
import CompleteStageIconZodiac from '../styling/icons/stage/zodiac/complete.svg?react'
|
||||
import CurrentStageIconZodiac from '../styling/icons/stage/zodiac/current.svg?react'
|
||||
import EmptyStageIconZodiac from '../styling/icons/stage/zodiac/empty.svg?react'
|
||||
|
||||
import classes from './Stepper.module.css'
|
||||
|
||||
const Stepper = memo(({ steps, currentStep, color = 'spring', className }) => {
|
||||
if (currentStep < 1 || currentStep > steps)
|
||||
throw Error('Value of currentStage is invalid')
|
||||
if (steps < 1) throw Error('Value of stages is invalid')
|
||||
|
||||
const separatorClasses = {
|
||||
'w-7 h-[2px] border-2 z-1': true,
|
||||
'border-spring': color === 'spring',
|
||||
'border-zodiac': color === 'zodiac',
|
||||
}
|
||||
|
||||
const separatorEmptyClasses = {
|
||||
'w-7 h-[2px] border-2 z-1': true,
|
||||
'border-dust': color === 'spring',
|
||||
'border-comet': color === 'zodiac',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classnames(className, 'flex items-center')}>
|
||||
{R.range(1, currentStep).map(idx => (
|
||||
<div key={idx} className="flex items-center m-0">
|
||||
{idx > 1 && <div className={classnames(separatorClasses)} />}
|
||||
<div className={classes.stage}>
|
||||
{color === 'spring' && <CompleteStageIconSpring />}
|
||||
{color === 'zodiac' && <CompleteStageIconZodiac />}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center m-0">
|
||||
{currentStep > 1 && <div className={classnames(separatorClasses)} />}
|
||||
<div className={classes.stage}>
|
||||
{color === 'spring' && <CurrentStageIconSpring />}
|
||||
{color === 'zodiac' && <CurrentStageIconZodiac />}
|
||||
</div>
|
||||
</div>
|
||||
{R.range(currentStep + 1, steps + 1).map(idx => (
|
||||
<div key={idx} className="flex items-center m-0">
|
||||
<div className={classnames(separatorEmptyClasses)} />
|
||||
<div className={classes.stage}>
|
||||
{color === 'spring' && <EmptyStageIconSpring />}
|
||||
{color === 'zodiac' && <EmptyStageIconZodiac />}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default Stepper
|
||||
12
packages/admin-ui/src/components/Stepper.module.css
Normal file
12
packages/admin-ui/src/components/Stepper.module.css
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
.stage {
|
||||
display: flex;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.stage > svg {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
15
packages/admin-ui/src/components/Subtitle.jsx
Normal file
15
packages/admin-ui/src/components/Subtitle.jsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import classnames from 'classnames'
|
||||
import React, { memo } from 'react'
|
||||
|
||||
import { TL1 } from './typography'
|
||||
|
||||
const Subtitle = memo(({ children, className, extraMarginTop }) => {
|
||||
const classNames = {
|
||||
'text-comet my-4': true,
|
||||
'mt-18': extraMarginTop,
|
||||
}
|
||||
|
||||
return <TL1 className={classnames(classNames, className)}>{children}</TL1>
|
||||
})
|
||||
|
||||
export default Subtitle
|
||||
118
packages/admin-ui/src/components/TableFilters.jsx
Normal file
118
packages/admin-ui/src/components/TableFilters.jsx
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import {
|
||||
Autocomplete,
|
||||
TextField,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
} from '@mui/material'
|
||||
import { AsyncAutocomplete } from './inputs/base/AsyncAutocomplete.jsx'
|
||||
|
||||
export const SelectFilter = ({ column, options = [] }) => {
|
||||
const columnFilterValue = column.getFilterValue()
|
||||
|
||||
return (
|
||||
<FormControl variant="standard" size="small" fullWidth>
|
||||
<Select
|
||||
value={columnFilterValue || ''}
|
||||
onChange={event => {
|
||||
column.setFilterValue(event.target.value || undefined)
|
||||
}}
|
||||
displayEmpty
|
||||
variant="standard">
|
||||
<MenuItem value="">All</MenuItem>
|
||||
{options.map(option => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
export const AutocompleteFilter = ({
|
||||
column,
|
||||
options = [],
|
||||
placeholder = 'Filter...',
|
||||
renderOption,
|
||||
getOptionLabel = option => option.label || '',
|
||||
}) => {
|
||||
const columnFilterValue = column.getFilterValue()
|
||||
const selectedOption =
|
||||
options.find(option => option.value === columnFilterValue) || null
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
options={options}
|
||||
value={selectedOption}
|
||||
onChange={(event, newValue) => {
|
||||
column.setFilterValue(newValue?.value || '')
|
||||
}}
|
||||
getOptionLabel={getOptionLabel}
|
||||
isOptionEqualToValue={(option, value) => option?.value === value?.value}
|
||||
renderOption={renderOption}
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
{...params}
|
||||
variant="standard"
|
||||
placeholder={placeholder}
|
||||
size="small"
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
size="small"
|
||||
fullWidth
|
||||
slotProps={{
|
||||
listbox: {
|
||||
style: { maxHeight: 200 },
|
||||
},
|
||||
popper: {
|
||||
style: { width: 'auto' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const TextFilter = ({ column, placeholder = 'Filter...' }) => {
|
||||
const columnFilterValue = column.getFilterValue()
|
||||
|
||||
return (
|
||||
<TextField
|
||||
value={columnFilterValue ?? ''}
|
||||
onChange={event => {
|
||||
column.setFilterValue(event.target.value || undefined)
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
variant="standard"
|
||||
size="small"
|
||||
fullWidth
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const AsyncAutocompleteFilter = ({ column, ...props }) => {
|
||||
const [selectedOption, setSelectedOption] = useState(null)
|
||||
const columnFilterValue = column.getFilterValue()
|
||||
const getOptionId = props.getOptionId || (option => option.id)
|
||||
|
||||
useEffect(() => {
|
||||
if (!columnFilterValue) {
|
||||
setSelectedOption(null)
|
||||
}
|
||||
}, [columnFilterValue])
|
||||
|
||||
const handleChange = (event, newValue) => {
|
||||
column.setFilterValue(newValue ? getOptionId(newValue) : '')
|
||||
setSelectedOption(newValue)
|
||||
}
|
||||
|
||||
return (
|
||||
<AsyncAutocomplete
|
||||
{...props}
|
||||
value={selectedOption}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
9
packages/admin-ui/src/components/Title.jsx
Normal file
9
packages/admin-ui/src/components/Title.jsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import React, { memo } from 'react'
|
||||
|
||||
import { H1 } from './typography'
|
||||
|
||||
const Title = memo(({ children }) => {
|
||||
return <H1 className="my-6">{children}</H1>
|
||||
})
|
||||
|
||||
export default Title
|
||||
97
packages/admin-ui/src/components/Tooltip.jsx
Normal file
97
packages/admin-ui/src/components/Tooltip.jsx
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import ClickAwayListener from '@mui/material/ClickAwayListener'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState, memo } from 'react'
|
||||
import Popper from './Popper'
|
||||
import HelpIcon from '../styling/icons/action/help/zodiac.svg?react'
|
||||
|
||||
const usePopperHandler = () => {
|
||||
const [helpPopperAnchorEl, setHelpPopperAnchorEl] = useState(null)
|
||||
|
||||
const handleOpenHelpPopper = event => {
|
||||
setHelpPopperAnchorEl(helpPopperAnchorEl ? null : event.currentTarget)
|
||||
}
|
||||
|
||||
const openHelpPopper = event => {
|
||||
setHelpPopperAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleCloseHelpPopper = () => {
|
||||
setHelpPopperAnchorEl(null)
|
||||
}
|
||||
|
||||
const helpPopperOpen = Boolean(helpPopperAnchorEl)
|
||||
|
||||
return {
|
||||
helpPopperAnchorEl,
|
||||
helpPopperOpen,
|
||||
handleOpenHelpPopper,
|
||||
openHelpPopper,
|
||||
handleCloseHelpPopper,
|
||||
}
|
||||
}
|
||||
|
||||
const HelpTooltip = memo(({ children, width }) => {
|
||||
const handler = usePopperHandler(width)
|
||||
|
||||
return (
|
||||
<ClickAwayListener onClickAway={handler.handleCloseHelpPopper}>
|
||||
<div className="relative" onMouseLeave={handler.handleCloseHelpPopper}>
|
||||
{handler.helpPopperOpen && (
|
||||
<div className="absolute bg-transparent h-10 -left-1/2 w-[200%]"></div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="flex justify-center align-center border-0 bg-transparent outline-0 cursor-pointer px-1"
|
||||
onMouseEnter={handler.openHelpPopper}>
|
||||
<HelpIcon />
|
||||
</button>
|
||||
<Popper
|
||||
open={handler.helpPopperOpen}
|
||||
anchorEl={handler.helpPopperAnchorEl}
|
||||
arrowEnabled={true}
|
||||
placement="bottom">
|
||||
<div className="py-2 px-4" style={{ width }}>
|
||||
{children}
|
||||
</div>
|
||||
</Popper>
|
||||
</div>
|
||||
</ClickAwayListener>
|
||||
)
|
||||
})
|
||||
|
||||
const HoverableTooltip = memo(({ parentElements, children, width }) => {
|
||||
const handler = usePopperHandler(width)
|
||||
|
||||
return (
|
||||
<ClickAwayListener onClickAway={handler.handleCloseHelpPopper}>
|
||||
<div>
|
||||
{!R.isNil(parentElements) && (
|
||||
<div
|
||||
onMouseLeave={handler.handleCloseHelpPopper}
|
||||
onMouseEnter={handler.handleOpenHelpPopper}>
|
||||
{parentElements}
|
||||
</div>
|
||||
)}
|
||||
{R.isNil(parentElements) && (
|
||||
<button
|
||||
type="button"
|
||||
onMouseEnter={handler.handleOpenHelpPopper}
|
||||
onMouseLeave={handler.handleCloseHelpPopper}
|
||||
className="border-0 bg-transparent outline-0 cursor-pointer mt-1">
|
||||
<HelpIcon />
|
||||
</button>
|
||||
)}
|
||||
<Popper
|
||||
open={handler.helpPopperOpen}
|
||||
anchorEl={handler.helpPopperAnchorEl}
|
||||
placement="bottom">
|
||||
<div className="py-2 px-4" style={{ width }}>
|
||||
{children}
|
||||
</div>
|
||||
</Popper>
|
||||
</div>
|
||||
</ClickAwayListener>
|
||||
)
|
||||
})
|
||||
|
||||
export { HoverableTooltip, HelpTooltip }
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
import IconButton from '@mui/material/IconButton'
|
||||
import { useFormikContext, Form, Formik, Field as FormikField } from 'formik'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState, memo } from 'react'
|
||||
import PromptWhenDirty from '../PromptWhenDirty'
|
||||
import { H4 } from '../typography'
|
||||
import EditIconDisabled from '../../styling/icons/action/edit/disabled.svg?react'
|
||||
import EditIcon from '../../styling/icons/action/edit/enabled.svg?react'
|
||||
import FalseIcon from '../../styling/icons/table/false.svg?react'
|
||||
import TrueIcon from '../../styling/icons/table/true.svg?react'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import { Link } from '../buttons'
|
||||
import { RadioGroup } from '../inputs/formik'
|
||||
import { Table, TableBody, TableRow, TableCell } from '../table'
|
||||
import SvgIcon from '@mui/material/SvgIcon'
|
||||
|
||||
const BooleanCell = ({ name }) => {
|
||||
const { values } = useFormikContext()
|
||||
return values[name] === 'true' ? <TrueIcon /> : <FalseIcon />
|
||||
}
|
||||
|
||||
const BooleanPropertiesTable = memo(
|
||||
({ title, disabled, data, elements, save, forcedEditing = false }) => {
|
||||
const [editing, setEditing] = useState(forcedEditing)
|
||||
|
||||
const initialValues = R.fromPairs(
|
||||
elements.map(it => [it.name, data[it.name]?.toString() ?? 'false']),
|
||||
)
|
||||
|
||||
const validationSchema = Yup.object().shape(
|
||||
R.fromPairs(
|
||||
elements.map(it => [
|
||||
it.name,
|
||||
Yup.mixed().oneOf(['true', 'false', true, false]).required(),
|
||||
]),
|
||||
),
|
||||
)
|
||||
|
||||
const innerSave = async values => {
|
||||
const toBoolean = num => R.equals(num, 'true')
|
||||
save(R.mapObjIndexed(toBoolean, R.filter(R.complement(R.isNil))(values)))
|
||||
setEditing(false)
|
||||
}
|
||||
|
||||
const radioButtonOptions = [
|
||||
{ display: 'Yes', code: 'true' },
|
||||
{ display: 'No', code: 'false' },
|
||||
]
|
||||
return (
|
||||
<div className="flex w-sm flex-col ">
|
||||
<Formik
|
||||
validateOnBlur={false}
|
||||
validateOnChange={false}
|
||||
enableReinitialize
|
||||
onSubmit={innerSave}
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}>
|
||||
{({ resetForm }) => {
|
||||
return (
|
||||
<Form>
|
||||
<div className="flex items-center">
|
||||
<H4>{title}</H4>
|
||||
{editing ? (
|
||||
<div className="ml-auto">
|
||||
<Link type="submit" color="primary">
|
||||
Save
|
||||
</Link>
|
||||
<Link
|
||||
type="reset"
|
||||
className="ml-5"
|
||||
onClick={() => {
|
||||
resetForm()
|
||||
setEditing(false)
|
||||
}}
|
||||
color="secondary">
|
||||
Cancel
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<IconButton
|
||||
className="my-auto mx-3"
|
||||
onClick={() => setEditing(true)}>
|
||||
<SvgIcon fontSize="small">
|
||||
{disabled ? <EditIconDisabled /> : <EditIcon />}
|
||||
</SvgIcon>
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
<PromptWhenDirty />
|
||||
<Table className="w-full">
|
||||
<TableBody className="w-full">
|
||||
{elements.map((it, idx) => (
|
||||
<TableRow
|
||||
key={idx}
|
||||
size="sm"
|
||||
className="h-auto py-2 px-4 flex items-center justify-between min-h-8 even:bg-transparent odd:bg-zircon">
|
||||
<TableCell className="p-0 w-50">{it.display}</TableCell>
|
||||
<TableCell className="p-0 flex">
|
||||
{editing && (
|
||||
<FormikField
|
||||
component={RadioGroup}
|
||||
name={it.name}
|
||||
options={radioButtonOptions}
|
||||
className="flex flex-row m-[-15px] p-0"
|
||||
/>
|
||||
)}
|
||||
{!editing && <BooleanCell name={it.name} />}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Form>
|
||||
)
|
||||
}}
|
||||
</Formik>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export default BooleanPropertiesTable
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import BooleanPropertiesTable from './BooleanPropertiesTable'
|
||||
|
||||
export { BooleanPropertiesTable }
|
||||
49
packages/admin-ui/src/components/buttons/ActionButton.jsx
Normal file
49
packages/admin-ui/src/components/buttons/ActionButton.jsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import classnames from 'classnames'
|
||||
import React, { memo } from 'react'
|
||||
|
||||
import moduleStyles from './ActionButton.module.css'
|
||||
|
||||
const ActionButton = memo(
|
||||
({
|
||||
className,
|
||||
altTextColor,
|
||||
Icon,
|
||||
InverseIcon,
|
||||
color,
|
||||
center,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const classNames = {
|
||||
[moduleStyles.actionButton]: true,
|
||||
[moduleStyles.altText]: altTextColor || color !== 'primary',
|
||||
[moduleStyles.primary]: color === 'primary',
|
||||
[moduleStyles.secondary]: color === 'secondary',
|
||||
[moduleStyles.spring]: color === 'spring',
|
||||
[moduleStyles.tomato]: color === 'tomato',
|
||||
[moduleStyles.center]: center,
|
||||
}
|
||||
|
||||
return (
|
||||
<button className={classnames(classNames, className)} {...props}>
|
||||
{Icon && (
|
||||
<div className={moduleStyles.actionButtonIcon}>
|
||||
<Icon />
|
||||
</div>
|
||||
)}
|
||||
{InverseIcon && (
|
||||
<div
|
||||
className={classnames(
|
||||
moduleStyles.actionButtonIcon,
|
||||
moduleStyles.actionButtonIconActive,
|
||||
)}>
|
||||
<InverseIcon />
|
||||
</div>
|
||||
)}
|
||||
{children && <div>{children}</div>}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export default ActionButton
|
||||
145
packages/admin-ui/src/components/buttons/ActionButton.module.css
Normal file
145
packages/admin-ui/src/components/buttons/ActionButton.module.css
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
.actionButton {
|
||||
composes: p from '../typography/typography.module.css';
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
height: 28px;
|
||||
outline: 0;
|
||||
border-radius: 6px;
|
||||
padding: 0 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.actionButton.altText {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.primary {
|
||||
background-color: var(--zircon);
|
||||
}
|
||||
|
||||
.primary:hover {
|
||||
background-color: var(--zircon2);
|
||||
}
|
||||
|
||||
.primary:active {
|
||||
background-color: var(--comet);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.primary .actionButtonIconActive {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.primary:active .actionButtonIcon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.primary:active .actionButtonIconActive {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background-color: var(--comet);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.secondary:hover {
|
||||
background-color: var(--comet2);
|
||||
}
|
||||
|
||||
.secondary:active {
|
||||
background-color: var(--comet3);
|
||||
}
|
||||
|
||||
.secondary .actionButtonIcon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.secondary .actionButtonIconActive {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.secondary:active .actionButtonIcon {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.secondary:active .actionButtonIconActive {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.spring {
|
||||
background-color: var(--spring2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.spring:hover {
|
||||
background-color: var(--spring);
|
||||
}
|
||||
|
||||
.spring:active {
|
||||
background-color: var(--spring4);
|
||||
}
|
||||
|
||||
.spring .actionButtonIcon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.spring .actionButtonIconActive {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.spring:active .actionButtonIcon {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.spring:active .actionButtonIconActive {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tomato {
|
||||
background-color: var(--tomato);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tomato:hover {
|
||||
background-color: var(--tomato);
|
||||
}
|
||||
|
||||
.tomato:active {
|
||||
background-color: var(--tomato);
|
||||
}
|
||||
|
||||
.tomato .actionButtonIcon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tomato .actionButtonIconActive {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tomato:active .actionButtonIcon {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tomato:active .actionButtonIconActive {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.actionButtonIcon {
|
||||
display: flex;
|
||||
padding-right: 7px;
|
||||
}
|
||||
|
||||
.actionButtonIcon svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.center {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.actionButtonIconActive {
|
||||
}
|
||||
16
packages/admin-ui/src/components/buttons/AddButton.jsx
Normal file
16
packages/admin-ui/src/components/buttons/AddButton.jsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import classnames from 'classnames'
|
||||
import React, { memo } from 'react'
|
||||
import AddIcon from '../../styling/icons/button/add/zodiac.svg?react'
|
||||
|
||||
import classes from './AddButton.module.css'
|
||||
|
||||
const SimpleButton = memo(({ className, children, ...props }) => {
|
||||
return (
|
||||
<button className={classnames(classes.button, className)} {...props}>
|
||||
<AddIcon />
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
})
|
||||
|
||||
export default SimpleButton
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
.button {
|
||||
composes: p from '../typography/typography.module.css';
|
||||
border: none;
|
||||
background-color: var(--zircon);
|
||||
cursor: pointer;
|
||||
outline: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 167px;
|
||||
height: 48px;
|
||||
color: var(--zodiac);
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: var(--zircon2);
|
||||
}
|
||||
|
||||
.button:active {
|
||||
background-color: var(--comet);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.button:active svg g * {
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
.button svg {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import {
|
||||
white,
|
||||
fontColor,
|
||||
subheaderColor,
|
||||
subheaderDarkColor,
|
||||
offColor,
|
||||
offDarkColor,
|
||||
} from '../../styling/variables'
|
||||
|
||||
const colors = (color1, color2, color3) => {
|
||||
return {
|
||||
backgroundColor: color1,
|
||||
'&:hover': {
|
||||
backgroundColor: color2,
|
||||
},
|
||||
'&:active': {
|
||||
backgroundColor: color3,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const buttonHeight = 32
|
||||
|
||||
export default {
|
||||
baseButton: {
|
||||
extend: colors(subheaderColor, subheaderDarkColor, offColor),
|
||||
cursor: 'pointer',
|
||||
border: 'none',
|
||||
outline: 0,
|
||||
height: buttonHeight,
|
||||
color: fontColor,
|
||||
'&:active': {
|
||||
color: white,
|
||||
},
|
||||
},
|
||||
primary: {
|
||||
extend: colors(subheaderColor, subheaderDarkColor, offColor),
|
||||
'&:active': {
|
||||
color: white,
|
||||
'& $buttonIcon': {
|
||||
display: 'none',
|
||||
},
|
||||
'& $buttonIconActive': {
|
||||
display: 'block',
|
||||
},
|
||||
},
|
||||
'& $buttonIconActive': {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
secondary: {
|
||||
extend: colors(offColor, offDarkColor, white),
|
||||
color: white,
|
||||
'&:active': {
|
||||
color: fontColor,
|
||||
'& $buttonIcon': {
|
||||
display: 'flex',
|
||||
},
|
||||
'& $buttonIconActive': {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
'& $buttonIcon': {
|
||||
display: 'none',
|
||||
},
|
||||
'& $buttonIconActive': {
|
||||
display: 'flex',
|
||||
},
|
||||
},
|
||||
}
|
||||
43
packages/admin-ui/src/components/buttons/Button.jsx
Normal file
43
packages/admin-ui/src/components/buttons/Button.jsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import classnames from 'classnames'
|
||||
import React, { memo } from 'react'
|
||||
|
||||
import moduleStyles from './Button.module.css'
|
||||
import { spacer } from '../../styling/variables.js'
|
||||
|
||||
const pickSize = size => {
|
||||
switch (size) {
|
||||
case 'xl':
|
||||
return spacer * 7.625
|
||||
case 'sm':
|
||||
return spacer * 4
|
||||
case 'lg':
|
||||
default:
|
||||
return spacer * 5
|
||||
}
|
||||
}
|
||||
|
||||
const ActionButton = memo(
|
||||
({ size = 'lg', children, className, buttonClassName, ...props }) => {
|
||||
const height = pickSize(size)
|
||||
|
||||
return (
|
||||
<div className={className} style={{ height: height + height / 12 }}>
|
||||
<button
|
||||
className={classnames(
|
||||
buttonClassName,
|
||||
moduleStyles.button,
|
||||
'text-white',
|
||||
{
|
||||
[moduleStyles.buttonSm]: size === 'sm',
|
||||
[moduleStyles.buttonXl]: size === 'xl',
|
||||
},
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export default ActionButton
|
||||
49
packages/admin-ui/src/components/buttons/Button.module.css
Normal file
49
packages/admin-ui/src/components/buttons/Button.module.css
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
.button {
|
||||
composes: h3 from '../typography/typography.module.css';
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
outline: 0;
|
||||
font-weight: 900;
|
||||
background-color: var(--spring);
|
||||
height: 40px;
|
||||
padding: 0 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 3px var(--spring4);
|
||||
}
|
||||
|
||||
.buttonXl {
|
||||
composes: h1 from '../typography/typography.module.css';
|
||||
height: 61px;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.buttonSm {
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
background-color: var(--dust);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.button:disabled:hover {
|
||||
background-color: var(--dust);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.button:disabled:active {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: var(--spring2);
|
||||
box-shadow: 0 3px var(--spring4);
|
||||
}
|
||||
|
||||
.button:active {
|
||||
margin-top: 2px;
|
||||
background-color: var(--spring2);
|
||||
box-shadow: 0 2px var(--spring4);
|
||||
}
|
||||
37
packages/admin-ui/src/components/buttons/FeatureButton.jsx
Normal file
37
packages/admin-ui/src/components/buttons/FeatureButton.jsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import classnames from 'classnames'
|
||||
import React, { memo } from 'react'
|
||||
|
||||
import classes from './FeatureButton.module.css'
|
||||
|
||||
const FeatureButton = memo(
|
||||
({ className, Icon, InverseIcon, children, ...props }) => {
|
||||
return (
|
||||
<button
|
||||
className={classnames(
|
||||
classes.baseButton,
|
||||
classes.roundButton,
|
||||
classes.primary,
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
{Icon && (
|
||||
<div className={classes.buttonIcon}>
|
||||
<Icon />
|
||||
</div>
|
||||
)}
|
||||
{InverseIcon && (
|
||||
<div
|
||||
className={classnames(
|
||||
classes.buttonIcon,
|
||||
classes.buttonIconActive,
|
||||
)}>
|
||||
<InverseIcon />
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export default FeatureButton
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
.baseButton {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
outline: 0;
|
||||
height: 32px;
|
||||
color: var(--zodiac);
|
||||
}
|
||||
|
||||
.roundButton {
|
||||
width: 32px;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.roundButton .buttonIcon {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.roundButton .buttonIcon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.roundButton .buttonIcon svg g {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.baseButton:active {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.primary {
|
||||
background-color: var(--zircon);
|
||||
}
|
||||
|
||||
.primary:hover {
|
||||
background-color: var(--zircon2);
|
||||
}
|
||||
|
||||
.primary:active {
|
||||
background-color: var(--comet);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.primary .buttonIconActive {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.primary:active .buttonIcon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.primary:active .buttonIconActive {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background-color: var(--comet);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.secondary:hover {
|
||||
background-color: var(--comet2);
|
||||
}
|
||||
|
||||
.secondary:active {
|
||||
background-color: white;
|
||||
color: var(--zodiac);
|
||||
}
|
||||
|
||||
.secondary .buttonIcon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.secondary .buttonIconActive {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.secondary:active .buttonIcon {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.secondary:active .buttonIconActive {
|
||||
display: none;
|
||||
}
|
||||
79
packages/admin-ui/src/components/buttons/IDButton.jsx
Normal file
79
packages/admin-ui/src/components/buttons/IDButton.jsx
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import ClickAwayListener from '@mui/material/ClickAwayListener'
|
||||
import classnames from 'classnames'
|
||||
import React, { useState, memo } from 'react'
|
||||
import Popover from '../Popper'
|
||||
|
||||
import classes from './IDButton.module.css'
|
||||
|
||||
const IDButton = memo(
|
||||
({
|
||||
name,
|
||||
className,
|
||||
Icon,
|
||||
InverseIcon,
|
||||
children,
|
||||
popoverClassname,
|
||||
...props
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
|
||||
const open = Boolean(anchorEl)
|
||||
const id = open ? `simple-popper-${name}` : undefined
|
||||
|
||||
const classNames = {
|
||||
[classes.idButton]: true,
|
||||
[classes.primary]: true,
|
||||
[classes.open]: open,
|
||||
[classes.closed]: !open,
|
||||
}
|
||||
|
||||
const iconClassNames = {
|
||||
[classes.buttonIcon]: true,
|
||||
}
|
||||
|
||||
const handleClick = event => {
|
||||
setAnchorEl(anchorEl ? null : event.currentTarget)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<button
|
||||
aria-describedby={id}
|
||||
onClick={handleClick}
|
||||
className={classnames(classNames, className)}
|
||||
{...props}>
|
||||
{Icon && !open && (
|
||||
<div className={classnames(iconClassNames)}>
|
||||
<Icon />
|
||||
</div>
|
||||
)}
|
||||
{InverseIcon && open && (
|
||||
<div className={classnames(iconClassNames)}>
|
||||
<InverseIcon />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</ClickAwayListener>
|
||||
<Popover
|
||||
className={popoverClassname}
|
||||
id={id}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
placement="top"
|
||||
flip>
|
||||
<div className={classes.popoverContent}>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</Popover>
|
||||
</>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export default IDButton
|
||||
58
packages/admin-ui/src/components/buttons/IDButton.module.css
Normal file
58
packages/admin-ui/src/components/buttons/IDButton.module.css
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
.idButton {
|
||||
width: 34px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
border-radius: 4px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.buttonIcon {
|
||||
margin: auto;
|
||||
line-height: 1px;
|
||||
}
|
||||
|
||||
.buttonIcon svg {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.closed {
|
||||
background-color: var(--zircon);
|
||||
}
|
||||
|
||||
.closed:hover {
|
||||
background-color: var(--zircon2);
|
||||
}
|
||||
|
||||
.closed:active {
|
||||
background-color: var(--comet);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.open {
|
||||
background-color: var(--comet);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.open:hover {
|
||||
background-color: var(--comet2);
|
||||
}
|
||||
|
||||
.open:active {
|
||||
background-color: var(--comet3);
|
||||
}
|
||||
|
||||
.popoverContent {
|
||||
composes: info2 from '../typography/typography.module.css';
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.popoverContent img {
|
||||
height: 145px;
|
||||
min-width: 200px;
|
||||
}
|
||||
27
packages/admin-ui/src/components/buttons/Link.jsx
Normal file
27
packages/admin-ui/src/components/buttons/Link.jsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import classnames from 'classnames'
|
||||
import React, { memo } from 'react'
|
||||
|
||||
import classes from './Link.module.css'
|
||||
|
||||
const Link = memo(
|
||||
({ submit, className, children, color = 'primary', ...props }) => {
|
||||
const classNames = {
|
||||
[classes.link]: true,
|
||||
[classes.primary]: color === 'primary',
|
||||
[classes.secondary]: color === 'secondary',
|
||||
[classes.noColor]: color === 'noColor',
|
||||
[classes.action]: color === 'action',
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type={submit ? 'submit' : 'button'}
|
||||
className={classnames(classNames, className)}
|
||||
{...props}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export default Link
|
||||
47
packages/admin-ui/src/components/buttons/Link.module.css
Normal file
47
packages/admin-ui/src/components/buttons/Link.module.css
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
.link {
|
||||
composes: h4 from '../typography/typography.module.css';
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.primary {
|
||||
box-shadow: inset 0 -4px 0 0 rgba(72, 246, 148, 0.8);
|
||||
}
|
||||
|
||||
.primary:hover {
|
||||
box-shadow: none;
|
||||
background-color: rgba(72, 246, 148, 0.8);
|
||||
}
|
||||
|
||||
.secondary {
|
||||
box-shadow: inset 0 -4px 0 0 rgba(255, 88, 74, 0.8);
|
||||
}
|
||||
|
||||
.secondary:hover {
|
||||
box-shadow: none;
|
||||
background-color: rgba(255, 88, 74, 0.8);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.noColor {
|
||||
box-shadow: inset 0 -4px 0 0 rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.noColor:hover {
|
||||
box-shadow: none;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.action {
|
||||
box-shadow: inset 0 -4px 0 0 rgba(72, 246, 148, 0.8);
|
||||
color: var(--zircon);
|
||||
}
|
||||
|
||||
.action:hover {
|
||||
box-shadow: none;
|
||||
background-color: rgba(72, 246, 148, 0.8);
|
||||
}
|
||||
62
packages/admin-ui/src/components/buttons/SubpageButton.jsx
Normal file
62
packages/admin-ui/src/components/buttons/SubpageButton.jsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import classnames from 'classnames'
|
||||
import React, { memo, useState } from 'react'
|
||||
import { H4 } from '../typography'
|
||||
import CancelIconInverse from '../../styling/icons/button/cancel/white.svg?react'
|
||||
|
||||
import classes from './SubpageButton.module.css'
|
||||
|
||||
const SubpageButton = memo(
|
||||
({
|
||||
className,
|
||||
Icon,
|
||||
InverseIcon,
|
||||
toggle,
|
||||
forceDisable = false,
|
||||
children,
|
||||
}) => {
|
||||
const [active, setActive] = useState(false)
|
||||
const isActive = forceDisable ? false : active
|
||||
const classNames = {
|
||||
[classes.button]: true,
|
||||
[classes.normal]: !isActive,
|
||||
[classes.active]: isActive,
|
||||
}
|
||||
|
||||
const normalButton = <Icon className={classes.buttonIcon} />
|
||||
|
||||
const activeButton = (
|
||||
<>
|
||||
<InverseIcon
|
||||
className={classnames(
|
||||
classes.buttonIcon,
|
||||
classes.buttonIconActiveLeft,
|
||||
)}
|
||||
/>
|
||||
<H4 className="text-white">{children}</H4>
|
||||
<CancelIconInverse
|
||||
className={classnames(
|
||||
classes.buttonIcon,
|
||||
classes.buttonIconActiveRight,
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
const innerToggle = () => {
|
||||
forceDisable = false
|
||||
const newActiveState = !isActive
|
||||
toggle(newActiveState)
|
||||
setActive(newActiveState)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={classnames(classNames, className)}
|
||||
onClick={innerToggle}>
|
||||
{isActive ? activeButton : normalButton}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export default SubpageButton
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
.button {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
outline: 0;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
color: white;
|
||||
border-radius: 16px;
|
||||
background-color: var(--zircon);
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: var(--zircon2);
|
||||
}
|
||||
|
||||
.normal {
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.active {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: var(--comet);
|
||||
font-weight: bold;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.active:hover {
|
||||
background-color: var(--comet);
|
||||
}
|
||||
|
||||
.buttonIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.buttonIcon g {
|
||||
stroke-width: 1.8px;
|
||||
}
|
||||
|
||||
.buttonIconActiveLeft {
|
||||
margin-right: 12px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.buttonIconActiveRight {
|
||||
margin-right: 5px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import baseButtonStyles from './BaseButton.styles'
|
||||
import { offColor, white } from '../../styling/variables'
|
||||
|
||||
const { baseButton } = baseButtonStyles
|
||||
|
||||
export default {
|
||||
button: {
|
||||
extend: baseButton,
|
||||
padding: 0,
|
||||
color: white,
|
||||
borderRadius: baseButton.height / 2,
|
||||
},
|
||||
normalButton: {
|
||||
width: baseButton.height,
|
||||
},
|
||||
activeButton: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: offColor,
|
||||
fontWeight: 'bold',
|
||||
padding: '0 5px',
|
||||
'&:hover': {
|
||||
backgroundColor: offColor,
|
||||
},
|
||||
},
|
||||
buttonIcon: {
|
||||
width: 16,
|
||||
height: 16,
|
||||
overflow: 'visible',
|
||||
'& g': {
|
||||
strokeWidth: 1.8,
|
||||
},
|
||||
},
|
||||
buttonIconActiveLeft: {
|
||||
marginRight: 12,
|
||||
marginLeft: 4,
|
||||
},
|
||||
buttonIconActiveRight: {
|
||||
marginRight: 5,
|
||||
marginLeft: 20,
|
||||
},
|
||||
white: {
|
||||
color: white,
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import React from 'react'
|
||||
import InverseLinkIcon from '../../styling/icons/action/external link/white.svg?react'
|
||||
import LinkIcon from '../../styling/icons/action/external link/zodiac.svg?react'
|
||||
|
||||
import { ActionButton } from './'
|
||||
|
||||
const SupportLinkButton = ({ link, label }) => {
|
||||
return (
|
||||
<a
|
||||
className="no-underline text-zodiac"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={link}>
|
||||
<ActionButton
|
||||
className="mb-1 leading-none"
|
||||
color="primary"
|
||||
Icon={LinkIcon}
|
||||
InverseIcon={InverseLinkIcon}>
|
||||
{label}
|
||||
</ActionButton>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export default SupportLinkButton
|
||||
19
packages/admin-ui/src/components/buttons/index.js
Normal file
19
packages/admin-ui/src/components/buttons/index.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import ActionButton from './ActionButton'
|
||||
import AddButton from './AddButton'
|
||||
import Button from './Button'
|
||||
import FeatureButton from './FeatureButton'
|
||||
import IDButton from './IDButton'
|
||||
import Link from './Link'
|
||||
import SubpageButton from './SubpageButton'
|
||||
import SupportLinkButton from './SupportLinkButton'
|
||||
|
||||
export {
|
||||
Button,
|
||||
Link,
|
||||
ActionButton,
|
||||
FeatureButton,
|
||||
IDButton,
|
||||
AddButton,
|
||||
SupportLinkButton,
|
||||
SubpageButton,
|
||||
}
|
||||
138
packages/admin-ui/src/components/date-range-picker/Calendar.jsx
Normal file
138
packages/admin-ui/src/components/date-range-picker/Calendar.jsx
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import {
|
||||
add,
|
||||
differenceInMonths,
|
||||
format,
|
||||
getDay,
|
||||
getDaysInMonth,
|
||||
isAfter,
|
||||
isSameDay,
|
||||
isSameMonth,
|
||||
lastDayOfMonth,
|
||||
startOfMonth,
|
||||
startOfWeek,
|
||||
sub,
|
||||
} from 'date-fns/fp'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import Arrow from '../../styling/icons/arrow/month_change.svg?react'
|
||||
import RightArrow from '../../styling/icons/arrow/month_change_right.svg?react'
|
||||
|
||||
import Tile from './Tile'
|
||||
import classes from './Calendar.module.css'
|
||||
|
||||
const Calendar = ({ minDate, maxDate, handleSelect, ...props }) => {
|
||||
const [currentDisplayedMonth, setCurrentDisplayedMonth] = useState(new Date())
|
||||
|
||||
const weekdays = Array.from(Array(7)).map((_, i) =>
|
||||
format('EEEEE', add({ days: i }, startOfWeek(new Date()))),
|
||||
)
|
||||
|
||||
const monthLength = month => getDaysInMonth(month)
|
||||
|
||||
const monthdays = month => {
|
||||
const lastMonth = sub({ months: 1 }, month)
|
||||
const lastMonthRange = R.range(0, getDay(startOfMonth(month))).reverse()
|
||||
const lastMonthDays = R.map(i =>
|
||||
sub({ days: i }, lastDayOfMonth(lastMonth)),
|
||||
)(lastMonthRange)
|
||||
|
||||
const thisMonthRange = R.range(0, monthLength(month))
|
||||
const thisMonthDays = R.map(i => add({ days: i }, startOfMonth(month)))(
|
||||
thisMonthRange,
|
||||
)
|
||||
|
||||
const nextMonth = add({ months: 1 }, month)
|
||||
const nextMonthRange = R.range(
|
||||
0,
|
||||
42 - lastMonthDays.length - thisMonthDays.length,
|
||||
)
|
||||
const nextMonthDays = R.map(i => add({ days: i }, startOfMonth(nextMonth)))(
|
||||
nextMonthRange,
|
||||
)
|
||||
|
||||
return R.concat(R.concat(lastMonthDays, thisMonthDays), nextMonthDays)
|
||||
}
|
||||
|
||||
const getRow = (month, row) => monthdays(month).slice(row * 7 - 7, row * 7)
|
||||
|
||||
const handleNavPrev = currentMonth => {
|
||||
const prevMonth = sub({ months: 1 }, currentMonth)
|
||||
if (!minDate) setCurrentDisplayedMonth(prevMonth)
|
||||
else {
|
||||
setCurrentDisplayedMonth(
|
||||
isSameMonth(minDate, prevMonth) ||
|
||||
differenceInMonths(minDate, prevMonth) > 0
|
||||
? prevMonth
|
||||
: currentDisplayedMonth,
|
||||
)
|
||||
}
|
||||
}
|
||||
const handleNavNext = currentMonth => {
|
||||
const nextMonth = add({ months: 1 }, currentMonth)
|
||||
if (!maxDate) setCurrentDisplayedMonth(nextMonth)
|
||||
else {
|
||||
setCurrentDisplayedMonth(
|
||||
isSameMonth(maxDate, nextMonth) ||
|
||||
differenceInMonths(nextMonth, maxDate) > 0
|
||||
? nextMonth
|
||||
: currentDisplayedMonth,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.wrapper}>
|
||||
<div className={classes.navbar}>
|
||||
<button
|
||||
className={classes.button}
|
||||
onClick={() => handleNavPrev(currentDisplayedMonth)}>
|
||||
<Arrow />
|
||||
</button>
|
||||
<span>
|
||||
{`${format('MMMM', currentDisplayedMonth)} ${format(
|
||||
'yyyy',
|
||||
currentDisplayedMonth,
|
||||
)}`}
|
||||
</span>
|
||||
<button
|
||||
className={classes.button}
|
||||
onClick={() => handleNavNext(currentDisplayedMonth)}>
|
||||
<RightArrow />
|
||||
</button>
|
||||
</div>
|
||||
<table className={classes.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
{weekdays.map((day, key) => (
|
||||
<th key={key}>{day}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{R.range(1, 8).map((row, key) => (
|
||||
<tr key={key}>
|
||||
{getRow(currentDisplayedMonth, row).map((day, key) => (
|
||||
<td key={key} onClick={() => handleSelect(day)}>
|
||||
<Tile
|
||||
isDisabled={
|
||||
(maxDate && isAfter(maxDate, day)) ||
|
||||
(minDate && isAfter(day, minDate))
|
||||
}
|
||||
isLowerBound={isSameDay(props.from, day)}
|
||||
isUpperBound={isSameDay(props.to, day)}
|
||||
isBetween={
|
||||
isAfter(props.from, day) && isAfter(day, props.to)
|
||||
}>
|
||||
{format('d', day)}
|
||||
</Tile>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Calendar
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
font-size: 14px;
|
||||
font-family: var(--museo);
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 15px 15px;
|
||||
color: var(--zodiac);
|
||||
}
|
||||
|
||||
.navbar button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background-color: var(--zircon);
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.navbar button svg {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
color: var(--zodiac);
|
||||
}
|
||||
|
||||
.table tr:first-child {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.table tr:last-child {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
margin: 0;
|
||||
padding: 3px 0 3px 0;
|
||||
}
|
||||
|
||||
.table th {
|
||||
font-size: 13px;
|
||||
font-family: var(--museo);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import classnames from 'classnames'
|
||||
import { compareAsc, differenceInDays, set } from 'date-fns/fp'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
|
||||
import Calendar from './Calendar'
|
||||
|
||||
const DateRangePicker = ({ minDate, maxDate, className, onRangeChange }) => {
|
||||
const [from, setFrom] = useState(null)
|
||||
const [to, setTo] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
onRangeChange(from, to)
|
||||
}, [from, onRangeChange, to])
|
||||
|
||||
const handleSelect = day => {
|
||||
if (
|
||||
(maxDate && compareAsc(maxDate, day) > 0) ||
|
||||
(minDate && differenceInDays(day, minDate) > 0)
|
||||
)
|
||||
return
|
||||
|
||||
if (from && !to) {
|
||||
if (differenceInDays(from, day) >= 0) {
|
||||
setTo(
|
||||
set({ hours: 23, minutes: 59, seconds: 59, milliseconds: 999 }, day),
|
||||
)
|
||||
} else {
|
||||
setTo(
|
||||
set(
|
||||
{ hours: 23, minutes: 59, seconds: 59, milliseconds: 999 },
|
||||
R.clone(from),
|
||||
),
|
||||
)
|
||||
setFrom(day)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
setFrom(day)
|
||||
setTo(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classnames('bg-white rounded-xl', className)}>
|
||||
<Calendar
|
||||
from={from}
|
||||
to={to}
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
handleSelect={handleSelect}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DateRangePicker
|
||||
41
packages/admin-ui/src/components/date-range-picker/Tile.jsx
Normal file
41
packages/admin-ui/src/components/date-range-picker/Tile.jsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import classnames from 'classnames'
|
||||
import React from 'react'
|
||||
|
||||
import classes from './Tile.module.css'
|
||||
|
||||
const Tile = ({
|
||||
isLowerBound,
|
||||
isUpperBound,
|
||||
isBetween,
|
||||
isDisabled,
|
||||
children,
|
||||
}) => {
|
||||
const selected = isLowerBound || isUpperBound
|
||||
|
||||
const rangeClasses = {
|
||||
[classes.between]: isBetween && !(isLowerBound && isUpperBound),
|
||||
[classes.lowerBound]: isLowerBound && !isUpperBound,
|
||||
[classes.upperBound]: isUpperBound && !isLowerBound,
|
||||
}
|
||||
|
||||
const buttonWrapperClasses = {
|
||||
[classes.wrapper]: true,
|
||||
[classes.selected]: selected,
|
||||
}
|
||||
|
||||
const buttonClasses = {
|
||||
[classes.button]: true,
|
||||
[classes.disabled]: isDisabled,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.wrapper}>
|
||||
<div className={classnames(rangeClasses)} />
|
||||
<div className={classnames(buttonWrapperClasses)}>
|
||||
<button className={classnames(buttonClasses)}>{children}</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tile
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
.wrapper {
|
||||
height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.button {
|
||||
outline: none;
|
||||
font-size: 13px;
|
||||
font-family: var(--museo);
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
color: var(--zodiac);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.lowerBound {
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.upperBound {
|
||||
right: 50%;
|
||||
}
|
||||
|
||||
.selected {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--spring2);
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.between {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
background-color: var(--spring3);
|
||||
}
|
||||
|
||||
.disabled {
|
||||
color: var(--dust);
|
||||
cursor: default;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import React from 'react'
|
||||
|
||||
export default React.createContext()
|
||||
127
packages/admin-ui/src/components/editableTable/Header.jsx
Normal file
127
packages/admin-ui/src/components/editableTable/Header.jsx
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import classnames from 'classnames'
|
||||
import * as R from 'ramda'
|
||||
import React, { useContext } from 'react'
|
||||
import { Td, THead, TDoubleLevelHead, ThDoubleLevel } from '../fake-table/Table'
|
||||
|
||||
import { sentenceCase } from '../../utils/string'
|
||||
|
||||
import TableCtx from './Context'
|
||||
|
||||
const groupSecondHeader = elements => {
|
||||
const doubleHeader = R.prop('doubleHeader')
|
||||
const sameDoubleHeader = (a, b) => doubleHeader(a) === doubleHeader(b)
|
||||
const group = R.pipe(
|
||||
R.groupWith(sameDoubleHeader),
|
||||
R.map(group =>
|
||||
R.isNil(doubleHeader(group[0])) // No doubleHeader
|
||||
? group
|
||||
: [
|
||||
{
|
||||
width: R.sum(R.map(R.prop('width'), group)),
|
||||
elements: group,
|
||||
name: doubleHeader(group[0]),
|
||||
},
|
||||
],
|
||||
),
|
||||
R.reduce(R.concat, []),
|
||||
)
|
||||
|
||||
return R.all(R.pipe(doubleHeader, R.isNil), elements)
|
||||
? [elements, THead]
|
||||
: [group(elements), TDoubleLevelHead]
|
||||
}
|
||||
|
||||
const Header = () => {
|
||||
const {
|
||||
elements,
|
||||
enableEdit,
|
||||
enableEditText,
|
||||
editWidth,
|
||||
enableDelete,
|
||||
deleteWidth,
|
||||
enableToggle,
|
||||
toggleWidth,
|
||||
orderedBy,
|
||||
DEFAULT_COL_SIZE,
|
||||
} = useContext(TableCtx)
|
||||
|
||||
const mapElement2 = (it, idx) => {
|
||||
const { width, elements, name } = it
|
||||
|
||||
if (elements && elements.length) {
|
||||
return (
|
||||
<ThDoubleLevel key={idx} width={width} title={name}>
|
||||
{elements.map(mapElement)}
|
||||
</ThDoubleLevel>
|
||||
)
|
||||
}
|
||||
|
||||
return mapElement(it, idx)
|
||||
}
|
||||
|
||||
const mapElement = (
|
||||
{ name, display, width = DEFAULT_COL_SIZE, header, textAlign },
|
||||
idx,
|
||||
) => {
|
||||
const orderClasses = classnames({
|
||||
'whitespace-nowrap':
|
||||
R.isNil(header) &&
|
||||
!R.isNil(orderedBy) &&
|
||||
R.equals(name, orderedBy.code),
|
||||
})
|
||||
|
||||
const attachOrderedByToComplexHeader = header => {
|
||||
if (!R.isNil(orderedBy) && R.equals(name, orderedBy.code)) {
|
||||
try {
|
||||
const cloneHeader = R.clone(header)
|
||||
const children = R.path(['props', 'children'], cloneHeader)
|
||||
const spanChild = R.find(it => R.equals(it.type, 'span'), children)
|
||||
spanChild.props.children = R.append(' -', spanChild.props.children)
|
||||
return cloneHeader
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return header
|
||||
}
|
||||
}
|
||||
return header
|
||||
}
|
||||
|
||||
return (
|
||||
<Td header key={idx} width={width} textAlign={textAlign}>
|
||||
{!R.isNil(header) ? (
|
||||
<>{attachOrderedByToComplexHeader(header) ?? header}</>
|
||||
) : (
|
||||
<span className={orderClasses}>
|
||||
{!R.isNil(display) ? display : sentenceCase(name)}{' '}
|
||||
{!R.isNil(orderedBy) && R.equals(name, orderedBy.code) && '-'}
|
||||
</span>
|
||||
)}
|
||||
</Td>
|
||||
)
|
||||
}
|
||||
|
||||
const [innerElements, HeaderElement] = groupSecondHeader(elements)
|
||||
|
||||
return (
|
||||
<HeaderElement>
|
||||
{innerElements.map(mapElement2)}
|
||||
{enableEdit && (
|
||||
<Td header width={editWidth} textAlign="center">
|
||||
{enableEditText ?? `Edit`}
|
||||
</Td>
|
||||
)}
|
||||
{enableDelete && (
|
||||
<Td header width={deleteWidth} textAlign="center">
|
||||
Delete
|
||||
</Td>
|
||||
)}
|
||||
{enableToggle && (
|
||||
<Td header width={toggleWidth} textAlign="center">
|
||||
Enable
|
||||
</Td>
|
||||
)}
|
||||
</HeaderElement>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue