Compare commits
255
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f39be632f | ||
|
|
9559b40e67 | ||
|
|
914d94497d | ||
|
|
8eb9b70c9b | ||
|
|
bf55c21e79 | ||
|
|
9d44e11496 | ||
|
|
478e9671d4 | ||
|
|
c44fd22031 | ||
|
|
62dc6453f1 | ||
|
|
7663ad6c74 | ||
|
|
2f7de5d103 | ||
|
|
4e1f9bc431 | ||
|
|
6b1d574698 | ||
|
|
b196d0de78 | ||
|
|
c7107a5034 | ||
|
|
173a4e17d2 | ||
|
|
70b197675e | ||
|
|
11dcccc085 | ||
|
|
e3e08da259 | ||
|
|
6447c315ce | ||
|
|
f1a6afb860 | ||
|
|
5c52a208b9 | ||
|
|
4fdc0bde5c | ||
|
|
3d4ebe9588 | ||
|
|
62ea7b0e5b | ||
|
|
8747c38f40 | ||
|
|
d633fc5ccb | ||
|
|
0d7a35367f | ||
|
|
af141ec772 | ||
|
|
5784fec3d1 | ||
|
|
95cd33ac06 | ||
|
|
2a591ee503 | ||
|
|
f5aa24d77e | ||
|
|
7874439e4c | ||
|
|
7140d436a5 | ||
|
|
ba0c89fc6c | ||
|
|
d91e87f484 | ||
|
|
b5caabed20 | ||
|
|
8814596d6e | ||
|
|
c60d07c3a0 | ||
|
|
54ecf02d9a | ||
|
|
1ab31d19f3 | ||
|
|
2f2ce34373 | ||
|
|
0f764ca084 | ||
|
|
8b2ee8d937 | ||
|
|
ed80837810 | ||
|
|
d82877b28d | ||
|
|
cf67c82f5e | ||
|
|
5a7b51bfec | ||
|
|
206dcae196 | ||
|
|
56bfbe2c77 | ||
|
|
8aac3086b8 | ||
|
|
9255500bfc | ||
|
|
e99b87fc1e | ||
|
|
ec953ff262 | ||
|
|
68559741ba | ||
|
|
74a7f0a701 | ||
|
|
75bcf9df58 | ||
|
|
6b5fd8d17f | ||
|
|
a1069f487b | ||
|
|
420164d616 | ||
|
|
f0a6fc4fdf | ||
|
|
e306499bce | ||
|
|
1472a09718 | ||
|
|
e7e4ce7f57 | ||
|
|
93c24d2f5c | ||
|
|
cdbaad5bd9 | ||
|
|
b1b27be731 | ||
|
|
01f005ca41 | ||
|
|
1326a6222b | ||
|
|
aea0b34533 | ||
|
|
d4af86f26c | ||
|
|
940247b7bf | ||
|
|
2b0c523047 | ||
|
|
4ad09fad4f | ||
|
|
665eb9a67f | ||
|
|
d1faeab042 | ||
|
|
76fa99f7bd | ||
|
|
77764c51ee | ||
|
|
8a986d2e58 | ||
|
|
e57b161be9 | ||
|
|
8de06df8c4 | ||
|
|
8e359cf145 | ||
|
|
52e598a66e | ||
|
|
0a60cc0363 | ||
|
|
75788f7cb5 | ||
|
|
b58a7ad053 | ||
|
|
f763126b38 | ||
|
|
e79bf28bdc | ||
|
|
d0b3bc60df | ||
|
|
117a2e00c4 | ||
|
|
f8e88623a8 | ||
|
|
705475e971 | ||
|
|
2b0a51520a | ||
|
|
55145a38ff | ||
|
|
25d1039376 | ||
|
|
7a6b0d5da4 | ||
|
|
e090d3cfd3 | ||
|
|
1c1ac25857 | ||
|
|
3ce40b1ab8 | ||
|
|
1494df9eb3 | ||
|
|
a70959e4a6 | ||
|
|
b6f48a910f | ||
|
|
e4d101237e | ||
|
|
7556086f71 | ||
|
|
7b619e6b31 | ||
|
|
76ab2695db | ||
|
|
418068e702 | ||
|
|
12b28ffb3f | ||
|
|
526fa24eab | ||
|
|
a13f6a34c4 | ||
|
|
f659876845 | ||
|
|
643a743d70 | ||
|
|
ab4cc80628 | ||
|
|
fb3795bd70 | ||
|
|
5c14c68899 | ||
|
|
5a878e89d0 | ||
|
|
bdb07220ed | ||
|
|
641d5e6dda | ||
|
|
b24339f232 | ||
|
|
ff324fe6e7 | ||
|
|
fee9b8a0de | ||
|
|
ad9488f54e | ||
|
|
ed77fc3a9d | ||
|
|
babd8e672f | ||
|
|
8ceac493ef | ||
|
|
fee808789f | ||
|
|
dd03c554c9 | ||
|
|
3ab4fcb277 | ||
|
|
11e3cac440 | ||
|
|
1d7f3b15a4 | ||
|
|
b7ad3c036b | ||
|
|
7b2ff593a4 | ||
|
|
916f3c0921 | ||
|
|
846f1dfa6c | ||
|
|
3f631ff382 | ||
|
|
43a370e162 | ||
|
|
64320c7aca | ||
|
|
61cedfb2b7 | ||
|
|
a9ce980d03 | ||
|
|
8409f08a2d | ||
|
|
ef2cccf16e | ||
|
|
0285302ecc | ||
|
|
668ea7418d | ||
|
|
b12bdfcc22 | ||
|
|
65de627870 | ||
|
|
03d1dcaefb | ||
|
|
14768bde28 | ||
|
|
9555ff43c7 | ||
|
|
75acb1ade6 | ||
|
|
fd12ca6e21 | ||
|
|
ff2f84d46b | ||
|
|
1ffce112d1 | ||
|
|
382df34f06 | ||
|
|
124f300362 | ||
|
|
f782899768 | ||
|
|
cdb83bb47d | ||
|
|
72bcc0d020 | ||
|
|
ac901b46c3 | ||
|
|
c1b3d325f6 | ||
|
|
7cad2fec27 | ||
|
|
7edc2a71d8 | ||
|
|
913f7a9e42 | ||
|
|
dd98757b2a | ||
|
|
1e3ee23120 | ||
|
|
74d8e0a892 | ||
|
|
50af063f64 | ||
|
|
80a66d74ca | ||
|
|
6c2908681a | ||
|
|
90593c0d7c | ||
|
|
8fb958e772 | ||
|
|
fc39ab3fb9 | ||
|
|
58e7ef2527 | ||
|
|
fec32380ae | ||
|
|
3b8d87bd17 | ||
|
|
131ced10fc | ||
|
|
886752effd | ||
|
|
a5d54a637e | ||
|
|
1b4edf16fb | ||
|
|
4042d34585 | ||
|
|
135c3ff454 | ||
|
|
f8d5791fab | ||
|
|
ffa4b1695b | ||
|
|
98214ff578 | ||
|
|
57ceac0c24 | ||
|
|
a526bc5a3e | ||
|
|
fac4516353 | ||
|
|
4db13fb924 | ||
|
|
c97ee22e66 | ||
|
|
98737f92af | ||
|
|
74637f9d2a | ||
|
|
69dbc2fe7a | ||
|
|
d157e200f6 | ||
|
|
594540cff5 | ||
|
|
8e47e8cd3d | ||
|
|
c5d7625a06 | ||
|
|
7763c5e288 | ||
|
|
957988d977 | ||
|
|
0c04c06d4e | ||
|
|
a7ef014b92 | ||
|
|
3822d971cb | ||
|
|
eb65c41d15 | ||
|
|
0c3fdbc539 | ||
|
|
7cb985116e | ||
|
|
2297494d21 | ||
|
|
19a61b6ce4 | ||
|
|
492aed200a | ||
|
|
be2f33b5d1 | ||
|
|
5d48cecee8 | ||
|
|
f0bfe1a152 | ||
|
|
d2ff386f97 | ||
|
|
42eaa75437 | ||
|
|
7fb4e75956 | ||
|
|
87c549960e | ||
|
|
4ac0f1e87f | ||
|
|
5465bee630 | ||
|
|
60a3d46e59 | ||
|
|
e100d54eed | ||
|
|
848d50ec43 | ||
|
|
a9a8686295 | ||
|
|
30a9309bd8 | ||
|
|
ab64cd192b | ||
|
|
5fc2c8c14b | ||
|
|
ea7ce450f1 | ||
|
|
ce77b43705 | ||
|
|
bd2ba804ee | ||
|
|
bd75f7b0fc | ||
|
|
32e4f7514d | ||
|
|
c9e1d35daf | ||
|
|
3be6ae91f2 | ||
|
|
931104fbcf | ||
|
|
1d5cec94a5 | ||
|
|
faeec01a6d | ||
|
|
b8db987579 | ||
|
|
1fad85d48d | ||
|
|
eb70c9f5f0 | ||
|
|
ce025df7b7 | ||
|
|
daf5797fed | ||
|
|
0baa7d9891 | ||
|
|
f6cd0f668f | ||
|
|
9ae83cc34d | ||
|
|
06ebb76948 | ||
|
|
3a8ba154b2 | ||
|
|
089801109e | ||
|
|
a8a4b5c2b4 | ||
|
|
089bda2de0 | ||
|
|
e18e2e26da | ||
|
|
104cf194ac | ||
|
|
de7cdb4aef | ||
|
|
6bdf9891e8 | ||
|
|
27d37ece7d | ||
|
|
b9afca3608 | ||
|
|
b796464241 | ||
|
|
8bf3875d9a | ||
|
|
3b22ce75db |
No files matched your search
@@ -88,3 +88,4 @@ jobs:
|
||||
docker compose down
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
docker system prune -af
|
||||
+3
-1
@@ -1,3 +1,5 @@
|
||||
.idea
|
||||
.vscode
|
||||
.VSCodeCounter
|
||||
.VSCodeCounter
|
||||
*.sh
|
||||
mongo
|
||||
@@ -0,0 +1,145 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Checkmate is an open-source uptime and infrastructure monitoring application. It monitors server hardware, uptime, response times, and incidents with real-time alerts. The companion agent [Capture](https://github.com/bluewave-labs/capture) provides infrastructure metrics (CPU, RAM, disk, temperature).
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Client (React/Vite)
|
||||
```bash
|
||||
cd client
|
||||
npm install
|
||||
npm run dev # Start dev server at http://localhost:5173
|
||||
npm run build # TypeScript check + production build
|
||||
npm run lint # ESLint (strict, max-warnings 0)
|
||||
npm run format # Prettier formatting
|
||||
npm run format-check # Check formatting
|
||||
```
|
||||
|
||||
### Server (Node.js/Express)
|
||||
```bash
|
||||
cd server
|
||||
npm install
|
||||
npm run dev # Start with hot-reload (nodemon + tsx) at http://localhost:52345
|
||||
npm run build # TypeScript compile + path alias resolution
|
||||
npm run test # Run Mocha tests with c8 coverage
|
||||
npm run lint # ESLint v9
|
||||
npm run lint-fix # Auto-fix lint issues
|
||||
npm run format # Prettier formatting
|
||||
```
|
||||
|
||||
### Docker Development
|
||||
```bash
|
||||
cd docker/dev
|
||||
./build_images.sh
|
||||
docker run -d -p 27017:27017 -v uptime_mongo_data:/data/db --name uptime_database_mongo mongo:6.0
|
||||
```
|
||||
|
||||
## Environment Setup
|
||||
|
||||
### Server `.env` (minimum required)
|
||||
```env
|
||||
CLIENT_HOST="http://localhost:5173"
|
||||
JWT_SECRET="my_secret_key_change_this"
|
||||
DB_CONNECTION_STRING="mongodb://localhost:27017/uptime_db"
|
||||
TOKEN_TTL="99d"
|
||||
ORIGIN="localhost"
|
||||
LOG_LEVEL="debug"
|
||||
```
|
||||
|
||||
### Client `.env`
|
||||
```env
|
||||
VITE_APP_API_BASE_URL="http://localhost:52345/api/v1"
|
||||
VITE_APP_LOG_LEVEL="debug"
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Monorepo Structure
|
||||
- `/client` - React 18 + TypeScript + Vite + MUI frontend
|
||||
- `/server` - Node.js 20+ + Express + TypeScript backend
|
||||
- `/docker` - Multi-environment Docker configs (dev, staging, prod, arm, mono)
|
||||
|
||||
### Backend Layers
|
||||
```
|
||||
server/src/
|
||||
├── controllers/ # Route handlers (authController, monitorController, etc.)
|
||||
├── service/ # Business logic
|
||||
│ ├── business/ # Core monitoring logic
|
||||
│ ├── infrastructure/ # Server/system utilities
|
||||
│ └── system/ # App-level settings
|
||||
├── db/
|
||||
│ ├── models/ # Mongoose schemas (Monitor, Check, Incident, User, etc.)
|
||||
│ ├── migration/ # Database migrations (run on startup)
|
||||
│ └── modules/ # Database-specific modules
|
||||
├── middleware/v1/ # verifyJWT, rateLimiter, sanitization, responseHandler
|
||||
├── routes/v1/ # API route definitions
|
||||
├── validation/ # Joi input validation schemas
|
||||
└── repositories/ # Data access layer
|
||||
```
|
||||
|
||||
### Frontend Structure
|
||||
```
|
||||
client/src/
|
||||
├── Components/ # Reusable UI components
|
||||
├── Pages/ # Page components (Auth, Uptime, Infrastructure, Incidents, etc.)
|
||||
├── Features/ # Redux slices (Auth, UI)
|
||||
├── Hooks/ # Custom React hooks
|
||||
├── Utils/ # Utilities (NetworkService.js is main API client)
|
||||
├── Validation/ # Input validation
|
||||
└── locales/ # i18n translations
|
||||
```
|
||||
|
||||
### API
|
||||
- Base URL: `/api/v1`
|
||||
- Documentation: `http://localhost:52345/api-docs` (Swagger UI)
|
||||
- OpenAPI spec: `/server/openapi.json`
|
||||
|
||||
### Key Technologies
|
||||
- **State Management**: Redux Toolkit + Redux-Persist
|
||||
- **Data Fetching**: SWR + Axios
|
||||
- **Database**: MongoDB with Mongoose ODM
|
||||
- **Queue/Cache**: Redis + BullMQ + Pulse (cron scheduling)
|
||||
- **i18n**: i18next + react-i18next (translations via PoEditor)
|
||||
|
||||
## Code Conventions
|
||||
|
||||
### Internationalization
|
||||
All user-facing strings must use the translation function:
|
||||
```javascript
|
||||
t('your.key') // Never hardcode UI strings
|
||||
```
|
||||
|
||||
### Branching
|
||||
- Always branch from `develop` (not master)
|
||||
- Use descriptive names: `feat/add-alerts`, `fix/login-error`
|
||||
- PRs target `develop` branch
|
||||
|
||||
### Formatting
|
||||
- **Client**: Prettier with `printWidth: 90`, tabs, double quotes
|
||||
- **Server**: Prettier with `printWidth: 150`, tabs, double quotes
|
||||
- Both use ESLint with strict settings
|
||||
|
||||
### Testing
|
||||
Server tests use Mocha + Chai + Sinon:
|
||||
```bash
|
||||
npm test # Run all tests with coverage
|
||||
npm test -- --grep "pattern" # Run specific tests
|
||||
```
|
||||
Test files: `server/tests/**/*.test.js`
|
||||
|
||||
## Database Models
|
||||
|
||||
Key Mongoose models in `/server/src/db/models/`:
|
||||
- **Monitor** - Monitoring configuration (website, infrastructure, port, etc.)
|
||||
- **Check** - Individual monitoring check results
|
||||
- **Incident** - Downtime incidents
|
||||
- **User** - User accounts
|
||||
- **Team** - Team/workspace management
|
||||
- **StatusPage** - Public status pages
|
||||
- **Notification** - Alert configuration (email, Discord, Slack, webhooks)
|
||||
- **MaintenanceWindow** - Scheduled maintenance periods
|
||||
- **AppSettings** - Global application settings
|
||||
+29
-4
@@ -10,6 +10,31 @@ PS: **We work closely with contributors on our [Discord channel](https://discord
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 🚀 Quick Setup Checklist
|
||||
|
||||
Before you dive in, make sure you have these installed:
|
||||
|
||||
```bash
|
||||
# Check Node.js (v16+ required)
|
||||
node --version
|
||||
|
||||
# Check npm
|
||||
npm --version
|
||||
|
||||
# Check Docker
|
||||
docker --version
|
||||
|
||||
# Check Git
|
||||
git --version
|
||||
```
|
||||
|
||||
**New to contributing?** Start here:
|
||||
1. Pick a [`good-first-issue`](https://github.com/bluewave-labs/checkmate/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
|
||||
2. Comment that you'd like to work on it
|
||||
3. Follow the [setup guide](#set-up-checkmate-locally) below
|
||||
4. Join our [Discord](https://discord.com/invite/NAb6H3UTjK) if you get stuck
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [How do I...?](#how-do-i)
|
||||
@@ -66,7 +91,7 @@ cd Checkmate
|
||||
Navigate to the Docker dev directory:
|
||||
|
||||
```bash
|
||||
cd server/docker/dev
|
||||
cd docker/dev
|
||||
```
|
||||
|
||||
Build the Docker images:
|
||||
@@ -78,13 +103,13 @@ Build the Docker images:
|
||||
Run MongoDB container:
|
||||
|
||||
```bash
|
||||
docker run -d -p 27017:27017 -v $(pwd)/mongo/data:/data/db --name uptime_database_mongo uptime_database_mongo
|
||||
docker run -d -p 27017:27017 -v uptime_mongo_data:/data/db --name uptime_database_mongo mongo:6.0
|
||||
```
|
||||
|
||||
Navigate back to the root directory:
|
||||
|
||||
```bash
|
||||
cd ../../..
|
||||
cd ../..
|
||||
```
|
||||
|
||||
#### Step 3: Set Up the Backend (Server)
|
||||
@@ -161,7 +186,7 @@ Start the frontend:
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The client will run at `http://localhost:5173`.
|
||||
The client will run at `http://localhost:5173`.
|
||||
|
||||
#### Step 5: Access the Application
|
||||
|
||||
|
||||
+11325
File diff suppressed because it is too large.
Load diff
@@ -52,7 +52,7 @@ Usage instructions can be found [here](https://docs.checkmate.so/checkmate-2.1).
|
||||
|
||||
See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart).
|
||||
|
||||
Alternatively, you can also use [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
|
||||
Alternatively, you can also use [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
|
||||
|
||||
### Using a Custom CA
|
||||
|
||||
@@ -91,6 +91,7 @@ Feel free to ask questions or share your ideas - we'd love to hear from you!
|
||||
- Website monitoring
|
||||
- Page speed monitoring
|
||||
- Infrastructure monitoring (memory, disk usage, CPU performance, network etc) - requires [Capture](https://github.com/bluewave-labs/capture) agent
|
||||
- Selective disk monitoring with mountpoint selection
|
||||
- Docker monitoring
|
||||
- Ping monitoring
|
||||
- SSL monitoring
|
||||
@@ -162,6 +163,7 @@ Here's how you can contribute:
|
||||
4. Open an issue if you believe you've encountered a bug.
|
||||
5. Check for good-first-issue's if you are a newcomer.
|
||||
6. Make a pull request to add new features/make quality-of-life improvements/fix bugs.
|
||||
7. Check out this interactive walkthrough of the `Checkmate` codebase on CodeCanvas [here](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true). To refine existing dataflow simulation or create new ones, follow the quick tutorial [here](https://docs.code-canvas.com/updating-diagram).
|
||||
|
||||
<a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
|
||||
|
||||
@@ -21,7 +21,12 @@ cd checkmate/charts/helm/checkmate
|
||||
Edit `values.yaml` to update:
|
||||
- `client.ingress.host` and `server.ingress.host` with your domain names
|
||||
- `server.protocol` (usually http or https)
|
||||
- **If upgrading**: Migrate persistence settings from flat structure to nested:
|
||||
- Old: `persistence.mongodbSize` → New: `persistence.mongo.size`
|
||||
- Old: `persistence.redisSize` → New: `persistence.redis.size`
|
||||
- Add: `persistence.mongo.storageClass` and `persistence.redis.storageClass` (leave empty for default)
|
||||
- Secrets under the `secrets` section (`JWT_SECRET`, email credentials, API keys, etc.) — replace all change_me values
|
||||
- **For TLS/HTTPS**: Configure ingress TLS settings (see section below)
|
||||
|
||||
### 3. Deploy the Helm chart
|
||||
```bash
|
||||
@@ -37,3 +42,75 @@ kubectl get svc
|
||||
```
|
||||
|
||||
Once all pods are `Running` and `Ready`, you can access Checkmate via the configured ingress hosts.
|
||||
|
||||
## Enabling TLS/HTTPS with cert-manager
|
||||
|
||||
If you have [cert-manager](https://cert-manager.io/) installed in your cluster, you can enable automatic TLS certificate provisioning using Let's Encrypt or other certificate issuers.
|
||||
|
||||
### Prerequisites
|
||||
- cert-manager installed in your cluster
|
||||
- A ClusterIssuer or Issuer configured (e.g., `letsencrypt-prod`)
|
||||
|
||||
### Configuration
|
||||
|
||||
Edit `values.yaml` to enable TLS (and update protocols to https):
|
||||
|
||||
```yaml
|
||||
client:
|
||||
protocol: https
|
||||
ingress:
|
||||
enabled: true
|
||||
host: checkmate.example.com
|
||||
className: nginx
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
tls:
|
||||
enabled: true
|
||||
secretName: checkmate-client-tls
|
||||
|
||||
server:
|
||||
protocol: https
|
||||
ingress:
|
||||
enabled: true
|
||||
host: checkmate.example.com
|
||||
className: nginx
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
tls:
|
||||
enabled: true
|
||||
secretName: checkmate-server-tls
|
||||
```
|
||||
|
||||
### Alternative: Using --set flags
|
||||
|
||||
You can also enable TLS during installation using Helm's `--set` flags:
|
||||
|
||||
```bash
|
||||
helm install checkmate ./charts/helm/checkmate \
|
||||
--set client.protocol=https \
|
||||
--set server.protocol=https \
|
||||
--set client.ingress.annotations."cert-manager\.io/cluster-issuer"="letsencrypt-prod" \
|
||||
--set client.ingress.tls.enabled=true \
|
||||
--set client.ingress.tls.secretName=checkmate-client-tls \
|
||||
--set server.ingress.annotations."cert-manager\.io/cluster-issuer"="letsencrypt-prod" \
|
||||
--set server.ingress.tls.enabled=true \
|
||||
--set server.ingress.tls.secretName=checkmate-server-tls
|
||||
```
|
||||
|
||||
### Verification
|
||||
|
||||
After deployment, cert-manager will automatically create the TLS secrets. You can verify the certificate status:
|
||||
|
||||
```bash
|
||||
# Check certificates
|
||||
kubectl get certificates
|
||||
|
||||
# Check certificate details
|
||||
kubectl describe certificate checkmate-client-tls
|
||||
kubectl describe certificate checkmate-server-tls
|
||||
|
||||
# Verify the secrets were created
|
||||
kubectl get secrets | grep checkmate-tls
|
||||
```
|
||||
|
||||
The ingress will automatically use these secrets to enable HTTPS access to your Checkmate instance.
|
||||
@@ -1,12 +0,0 @@
|
||||
{{- if eq .Values.client.ingress.host "change_me" }}
|
||||
{{- fail "client.ingress.host must be overridden and not set to 'change_me'" }}
|
||||
{{- end }}
|
||||
|
||||
{{- if eq .Values.server.ingress.host "change_me" }}
|
||||
{{- fail "server.ingress.host must be overridden and not set to 'change_me'" }}
|
||||
{{- end }}
|
||||
|
||||
{{- $protocol := .Values.server.protocol }}
|
||||
{{- if not (or (eq $protocol "http") (eq $protocol "https")) }}
|
||||
{{- fail "server.protocol must be either 'http' or 'https'" }}
|
||||
{{- end }}
|
||||
@@ -11,6 +11,12 @@ metadata:
|
||||
{{- end }}
|
||||
spec:
|
||||
ingressClassName: {{ .Values.client.ingress.className }}
|
||||
{{- if .Values.client.ingress.tls.enabled }}
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ .Values.client.ingress.host }}
|
||||
secretName: {{ default (printf "%s-client-tls" .Release.Name) .Values.client.ingress.tls.secretName }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- host: {{ .Values.client.ingress.host }}
|
||||
http:
|
||||
|
||||
@@ -37,8 +37,8 @@ spec:
|
||||
- metadata:
|
||||
name: checkmate-mongo-persistent-storage
|
||||
spec:
|
||||
storageClassName: "gp3"
|
||||
storageClassName: {{ .Values.persistence.mongo.storageClass | quote }}
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.persistence.mongodbSize }}
|
||||
storage: {{ .Values.persistence.mongo.size | default "5Gi" | quote }}
|
||||
@@ -0,0 +1,48 @@
|
||||
{{- if eq .Values.client.ingress.host "change_me" }}
|
||||
{{- fail "client.ingress.host must be overridden and not set to 'change_me'" }}
|
||||
{{- end }}
|
||||
|
||||
{{- if eq .Values.server.ingress.host "change_me" }}
|
||||
{{- fail "server.ingress.host must be overridden and not set to 'change_me'" }}
|
||||
{{- end }}
|
||||
|
||||
{{- $serverProtocol := .Values.server.protocol }}
|
||||
{{- if not (or (eq $serverProtocol "http") (eq $serverProtocol "https")) }}
|
||||
{{- fail "server.protocol must be either 'http' or 'https'" }}
|
||||
{{- end }}
|
||||
|
||||
{{- $clientProtocol := .Values.client.protocol }}
|
||||
{{- if not (or (eq $clientProtocol "http") (eq $clientProtocol "https")) }}
|
||||
{{- fail "client.protocol must be either 'http' or 'https'" }}
|
||||
{{- end }}
|
||||
|
||||
{{/* Enforce protocol when TLS is enabled to avoid mixed-content */}}
|
||||
{{- if and .Values.client.ingress.tls.enabled (ne $clientProtocol "https") }}
|
||||
{{- fail "client.ingress.tls.enabled is true but client.protocol is not 'https'. Set client.protocol: https to avoid mixed content." }}
|
||||
{{- end }}
|
||||
|
||||
{{- if and .Values.server.ingress.tls.enabled (ne $serverProtocol "https") }}
|
||||
{{- fail "server.ingress.tls.enabled is true but server.protocol is not 'https'. Set server.protocol: https to ensure correct API base URL." }}
|
||||
{{- end }}
|
||||
|
||||
{{/* If client runs on https, API must also be https to avoid mixed content */}}
|
||||
{{- if and (eq $clientProtocol "https") (ne $serverProtocol "https") }}
|
||||
{{- fail "client.protocol is 'https' but server.protocol is not. Set server.protocol: https to prevent browser mixed-content issues." }}
|
||||
{{- end }}
|
||||
|
||||
{{/* Fail early if TLS enabled without cert-manager annotations (cluster-issuer or issuer) */}}
|
||||
{{- $cAnn := .Values.client.ingress.annotations | default dict }}
|
||||
{{- $sAnn := .Values.server.ingress.annotations | default dict }}
|
||||
|
||||
{{- $clientHasIssuer := or (hasKey $cAnn "cert-manager.io/cluster-issuer") (hasKey $cAnn "cert-manager.io/issuer") }}
|
||||
{{- $serverHasIssuer := or (hasKey $sAnn "cert-manager.io/cluster-issuer") (hasKey $sAnn "cert-manager.io/issuer") }}
|
||||
|
||||
{{- if and .Values.client.ingress.tls.enabled (not $clientHasIssuer) }}
|
||||
{{- fail "client.ingress.tls.enabled is true but no cert-manager issuer annotation found. Add 'cert-manager.io/cluster-issuer' or 'cert-manager.io/issuer'." }}
|
||||
{{- end }}
|
||||
|
||||
{{- if and .Values.server.ingress.tls.enabled (not $serverHasIssuer) }}
|
||||
{{- fail "server.ingress.tls.enabled is true but no cert-manager issuer annotation found. Add 'cert-manager.io/cluster-issuer' or 'cert-manager.io/issuer'." }}
|
||||
{{- end }}
|
||||
|
||||
{{/* Secret name can be omitted; we default to <release>-client|server-tls in templates */}}
|
||||
@@ -25,9 +25,9 @@ spec:
|
||||
- metadata:
|
||||
name: checkmate-redis-persistent-storage
|
||||
spec:
|
||||
storageClassName: "gp3"
|
||||
storageClassName: {{ .Values.persistence.redis.storageClass | quote }}
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.persistence.redisSize }}
|
||||
storage: {{ .Values.persistence.redis.size | default "1Gi" | quote }}
|
||||
{{- end }}
|
||||
@@ -18,6 +18,12 @@ metadata:
|
||||
#nginx.ingress.kubernetes.io/cors-allow-credentials: "true"*/}}
|
||||
spec:
|
||||
ingressClassName: {{ .Values.server.ingress.className }}
|
||||
{{- if .Values.server.ingress.tls.enabled }}
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ .Values.server.ingress.host }}
|
||||
secretName: {{ default (printf "%s-server-tls" .Release.Name) .Values.server.ingress.tls.secretName }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- host: {{ .Values.server.ingress.host }}
|
||||
http:
|
||||
|
||||
@@ -7,6 +7,15 @@ client:
|
||||
host: change_me
|
||||
className: nginx
|
||||
annotations: {}
|
||||
# Example annotations for cert-manager:
|
||||
# annotations:
|
||||
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
tls:
|
||||
enabled: false
|
||||
# secretName: {{ .Release.Name }}-client-tls # Optional; defaults to <release>-client-tls if omitted
|
||||
# Note: when enabling TLS, also set client.protocol: https and add
|
||||
# a cert-manager issuer annotation (e.g. cert-manager.io/cluster-issuer: "letsencrypt-prod").
|
||||
# The secret will be automatically created by cert-manager when using the cert-manager.io/cluster-issuer annotation
|
||||
|
||||
server:
|
||||
image: ghcr.io/bluewave-labs/checkmate-backend:v3.2.0
|
||||
@@ -17,6 +26,15 @@ server:
|
||||
host: change_me
|
||||
className: nginx
|
||||
annotations: {}
|
||||
# Example annotations for cert-manager:
|
||||
# annotations:
|
||||
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
tls:
|
||||
enabled: false
|
||||
# secretName: {{ .Release.Name }}-server-tls # Optional; defaults to <release>-server-tls if omitted
|
||||
# Note: when enabling TLS, also set server.protocol: https and add
|
||||
# a cert-manager issuer annotation (e.g. cert-manager.io/cluster-issuer: "letsencrypt-prod").
|
||||
# The secret will be automatically created by cert-manager when using the cert-manager.io/cluster-issuer annotation
|
||||
|
||||
redis:
|
||||
enabled: false
|
||||
@@ -44,5 +62,9 @@ secrets:
|
||||
# REFRESH_TOKEN_TTL: 99d
|
||||
|
||||
persistence:
|
||||
mongodbSize: 5Gi
|
||||
redisSize: 1Gi
|
||||
mongo:
|
||||
size: 5Gi
|
||||
storageClass: ""
|
||||
redis:
|
||||
size: 1Gi
|
||||
storageClass: ""
|
||||
+2
-1
@@ -28,4 +28,5 @@ dist-ssr
|
||||
|
||||
# Scripts
|
||||
*.sh
|
||||
!env.sh
|
||||
!env.sh
|
||||
.env.development
|
||||
Generated
+649
-62
@@ -12,7 +12,6 @@
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@hello-pangea/dnd": "^18.0.0",
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
"@mui/icons-material": "6.4.11",
|
||||
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
|
||||
"@mui/material": "6.4.11",
|
||||
"@mui/x-charts": "^7.5.1",
|
||||
@@ -25,6 +24,7 @@
|
||||
"human-interval": "2.0.1",
|
||||
"i18next": "25.4.2",
|
||||
"joi": "17.13.3",
|
||||
"lucide-react": "^0.562.0",
|
||||
"mui-color-input": "^6.0.0",
|
||||
"pretty-ms": "9.3.0",
|
||||
"react": "18.3.1",
|
||||
@@ -97,6 +97,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz",
|
||||
"integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
@@ -408,6 +409,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
|
||||
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
@@ -451,6 +453,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz",
|
||||
"integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
@@ -496,6 +499,262 @@
|
||||
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
|
||||
"integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
|
||||
"integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
|
||||
"integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
|
||||
"integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
|
||||
"integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
|
||||
"integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
|
||||
"integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
|
||||
"integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
|
||||
"integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
|
||||
@@ -512,6 +771,134 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
|
||||
"integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
||||
@@ -802,32 +1189,6 @@
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/icons-material": {
|
||||
"version": "6.4.11",
|
||||
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.11.tgz",
|
||||
"integrity": "sha512-+jjJGIrB1awNbMv4ZVPPdN/p7O1UKFZ+xqRvNIQ8B1KnlID5hPMPBLM6UUbRF4bu3UDCbu79rn9Nye5LGNzmeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@mui/material": "^6.4.11",
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/lab": {
|
||||
"version": "6.0.0-dev.240424162023-9968b4889d",
|
||||
"resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-dev.240424162023-9968b4889d.tgz",
|
||||
@@ -989,6 +1350,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.11.tgz",
|
||||
"integrity": "sha512-k2D3FLJS+/qD0qnd6ZlAjGFvaaxe1Dl10NyvpeDzIebMuYdn8VqYe6XBgGueEAtnzSJM4V03VD9kb5Fi24dnTA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@mui/core-downloads-tracker": "^6.4.11",
|
||||
@@ -1153,7 +1515,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.0.tgz",
|
||||
"integrity": "sha512-4Kck4jxhqF6YxNwJdSae1WgDfXVg0lIH6JVJ7gtuFfuKcQCgomJxPvUEOySTFRPz1IZzwz5OAcToskRdffElDA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.1",
|
||||
"@mui/utils": "^7.1.0",
|
||||
@@ -1181,7 +1542,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz",
|
||||
"integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.1",
|
||||
"@mui/types": "^7.4.2",
|
||||
@@ -1212,7 +1572,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.0.tgz",
|
||||
"integrity": "sha512-m0mJ0c6iRC+f9hMeRe0W7zZX1wme3oUX0+XTVHjPG7DJz6OdQ6K/ggEOq7ZdwilcpdsDUwwMfOmvO71qDkYd2w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.1",
|
||||
"@emotion/cache": "^11.13.5",
|
||||
@@ -1288,7 +1647,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz",
|
||||
"integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.1",
|
||||
"@mui/types": "^7.4.2",
|
||||
@@ -1666,9 +2024,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@remix-run/router": {
|
||||
"version": "1.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
|
||||
"integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
|
||||
"version": "1.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
|
||||
"integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -1696,6 +2054,123 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz",
|
||||
"integrity": "sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.0.tgz",
|
||||
"integrity": "sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.0.tgz",
|
||||
"integrity": "sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.0.tgz",
|
||||
"integrity": "sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.0.tgz",
|
||||
"integrity": "sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.0.tgz",
|
||||
"integrity": "sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.0.tgz",
|
||||
"integrity": "sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.0.tgz",
|
||||
"integrity": "sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.0.tgz",
|
||||
"integrity": "sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz",
|
||||
@@ -1709,6 +2184,71 @@
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.0.tgz",
|
||||
"integrity": "sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.0.tgz",
|
||||
"integrity": "sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.0.tgz",
|
||||
"integrity": "sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.0.tgz",
|
||||
"integrity": "sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.0.tgz",
|
||||
"integrity": "sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz",
|
||||
@@ -1735,6 +2275,45 @@
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.0.tgz",
|
||||
"integrity": "sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.0.tgz",
|
||||
"integrity": "sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.0.tgz",
|
||||
"integrity": "sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@sideway/address": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
|
||||
@@ -2133,6 +2712,7 @@
|
||||
"integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.12.0"
|
||||
}
|
||||
@@ -2154,6 +2734,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.22.tgz",
|
||||
"integrity": "sha512-vUhG0YmQZ7kL/tmKLrD3g5zXbXXreZXB3pmROW8bg3CnLnpjkRVwUlLne7Ufa2r9yJ8+/6B73RzhAek5TBKh2Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.0.2"
|
||||
@@ -2217,6 +2798,7 @@
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2523,6 +3105,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001716",
|
||||
"electron-to-chromium": "^1.5.149",
|
||||
@@ -2935,7 +3518,8 @@
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
@@ -3342,6 +3926,7 @@
|
||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
@@ -4149,6 +4734,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6"
|
||||
},
|
||||
@@ -4685,9 +5271,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
@@ -4852,6 +5438,15 @@
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.562.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz",
|
||||
"integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -5416,6 +6011,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
@@ -5429,6 +6025,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz",
|
||||
"integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
@@ -5515,12 +6112,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "6.30.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
|
||||
"integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
|
||||
"version": "6.30.3",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
|
||||
"integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.23.0"
|
||||
"@remix-run/router": "1.23.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -5530,13 +6127,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "6.30.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz",
|
||||
"integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==",
|
||||
"version": "6.30.3",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
|
||||
"integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.23.0",
|
||||
"react-router": "6.30.1"
|
||||
"@remix-run/router": "1.23.2",
|
||||
"react-router": "6.30.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -5632,7 +6229,8 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/redux-persist": {
|
||||
"version": "6.0.0",
|
||||
@@ -5770,6 +6368,7 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz",
|
||||
"integrity": "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.7"
|
||||
},
|
||||
@@ -6405,6 +7004,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -6524,6 +7124,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
|
||||
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
@@ -6744,20 +7345,6 @@
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
|
||||
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
||||
+1
-1
@@ -17,7 +17,6 @@
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@hello-pangea/dnd": "^18.0.0",
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
"@mui/icons-material": "6.4.11",
|
||||
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
|
||||
"@mui/material": "6.4.11",
|
||||
"@mui/x-charts": "^7.5.1",
|
||||
@@ -30,6 +29,7 @@
|
||||
"human-interval": "2.0.1",
|
||||
"i18next": "25.4.2",
|
||||
"joi": "17.13.3",
|
||||
"lucide-react": "^0.562.0",
|
||||
"mui-color-input": "^6.0.0",
|
||||
"pretty-ms": "9.3.0",
|
||||
"react": "18.3.1",
|
||||
|
||||
+6
-3
@@ -10,6 +10,7 @@ import { logger } from "./Utils/Logger"; // Import the logger
|
||||
import { networkService } from "./main";
|
||||
import { Routes } from "./Routes";
|
||||
import AppLayout from "@/Components/v1/Layouts/AppLayout";
|
||||
import { UserGuideSidebarProvider } from "@/Components/v1/UserGuide";
|
||||
|
||||
function App() {
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
@@ -25,9 +26,11 @@ function App() {
|
||||
return (
|
||||
<ThemeProvider theme={mode === "light" ? lightTheme : darkTheme}>
|
||||
<CssBaseline />
|
||||
<AppLayout>
|
||||
<Routes />
|
||||
</AppLayout>
|
||||
<UserGuideSidebarProvider>
|
||||
<AppLayout>
|
||||
<Routes />
|
||||
</AppLayout>
|
||||
</UserGuideSidebarProvider>
|
||||
<ToastContainer />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
@@ -16,7 +16,7 @@ const langMap = {
|
||||
const LanguageSelector = () => {
|
||||
const { i18n } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { language } = useSelector((state) => state.ui);
|
||||
const { language = "en" } = useSelector((state) => state.ui);
|
||||
const dispatch = useDispatch();
|
||||
const handleChange = (event) => {
|
||||
const newLang = event.target.value;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Settings from "../../../assets/icons/settings-bold.svg?react";
|
||||
import Icon from "../Icon";
|
||||
import Dialog from "../Dialog/index.jsx";
|
||||
|
||||
// Utils
|
||||
@@ -12,7 +12,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import { createToast } from "../../../Utils/toastUtils.jsx";
|
||||
|
||||
import PropTypes from "prop-types";
|
||||
import { usePauseMonitor, useDeleteMonitor } from "../../../Hooks/v1/monitorHooks.js";
|
||||
import { usePauseMonitor, useDeleteMonitor } from "../../../Hooks/monitorHooks.js";
|
||||
|
||||
const ActionsMenu = ({
|
||||
monitor,
|
||||
@@ -31,7 +31,8 @@ const ActionsMenu = ({
|
||||
const handleRemove = async (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
let monitor = { _id: actions.id };
|
||||
let monitor = { id: actions.id };
|
||||
console.log(monitor);
|
||||
await deleteMonitor({ monitor });
|
||||
updateRowCallback();
|
||||
};
|
||||
@@ -39,7 +40,7 @@ const ActionsMenu = ({
|
||||
const handlePause = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await pauseMonitor({ monitorId: monitor._id });
|
||||
await pauseMonitor({ monitorId: monitor.id });
|
||||
pauseCallback();
|
||||
} catch (error) {
|
||||
createToast({ body: "Failed to pause monitor." });
|
||||
@@ -72,7 +73,7 @@ const ActionsMenu = ({
|
||||
aria-label="monitor actions"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
openMenu(event, monitor._id, monitor.type === "ping" ? null : monitor.url);
|
||||
openMenu(event, monitor.id, monitor.type === "ping" ? null : monitor.url);
|
||||
}}
|
||||
sx={{
|
||||
"&:focus": {
|
||||
@@ -83,7 +84,10 @@ const ActionsMenu = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Settings />
|
||||
<Icon
|
||||
name="Settings"
|
||||
size={20}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
@@ -197,6 +201,7 @@ const ActionsMenu = ({
|
||||
confirmationButtonLabel="Delete"
|
||||
/* Do we need stop propagation? */
|
||||
onConfirm={(e) => {
|
||||
console.log(e);
|
||||
e.stopPropagation();
|
||||
handleRemove(e);
|
||||
}}
|
||||
@@ -210,7 +215,7 @@ const ActionsMenu = ({
|
||||
|
||||
ActionsMenu.propTypes = {
|
||||
monitor: PropTypes.shape({
|
||||
_id: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
isActive: PropTypes.bool,
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Button, IconButton, Stack, Typography } from "@mui/material";
|
||||
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
||||
import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined";
|
||||
import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import Icon from "../Icon";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
@@ -13,9 +10,24 @@ import "./index.css";
|
||||
*/
|
||||
|
||||
const icons = {
|
||||
info: <InfoOutlinedIcon />,
|
||||
error: <ErrorOutlineOutlinedIcon />,
|
||||
warning: <WarningAmberOutlinedIcon />,
|
||||
info: (
|
||||
<Icon
|
||||
name="Info"
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
error: (
|
||||
<Icon
|
||||
name="AlertCircle"
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
warning: (
|
||||
<Icon
|
||||
name="AlertTriangle"
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -97,10 +109,9 @@ const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CloseIcon
|
||||
sx={{
|
||||
fontSize: "20px",
|
||||
}}
|
||||
<Icon
|
||||
name="X"
|
||||
size={20}
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
import LeftArrow from "../../../assets/icons/left-arrow.svg?react";
|
||||
import LeftArrowDouble from "../../../assets/icons/left-arrow-double.svg?react";
|
||||
import LeftArrowLong from "../../../assets/icons/left-arrow-long.svg?react";
|
||||
import Icon from "../Icon";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const ArrowLeft = ({ type, color = "#667085", ...props }) => {
|
||||
if (type === "double") {
|
||||
return (
|
||||
<LeftArrowDouble
|
||||
style={{ color }}
|
||||
<Icon
|
||||
name="ChevronsLeft"
|
||||
color={color}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
} else if (type === "long") {
|
||||
return (
|
||||
<LeftArrowLong
|
||||
style={{ color }}
|
||||
<Icon
|
||||
name="ArrowLeft"
|
||||
color={color}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<LeftArrow
|
||||
style={{ color }}
|
||||
<Icon
|
||||
name="ChevronLeft"
|
||||
color={color}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import RightArrow from "../../../assets/icons/right-arrow.svg?react";
|
||||
import RightArrowDouble from "../../../assets/icons/right-arrow-double.svg?react";
|
||||
import Icon from "../Icon";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const ArrowRight = ({ type, color = "#667085", ...props }) => {
|
||||
if (type === "double") {
|
||||
return (
|
||||
<RightArrowDouble
|
||||
style={{ color }}
|
||||
<Icon
|
||||
name="ChevronsRight"
|
||||
color={color}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<RightArrow
|
||||
style={{ color }}
|
||||
<Icon
|
||||
name="ChevronRight"
|
||||
color={color}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -103,7 +103,7 @@ const BarChart = ({ checks = [] }) => {
|
||||
</>
|
||||
}
|
||||
placement="top"
|
||||
key={`check-${check?._id}`}
|
||||
key={`check-${check?.id}`}
|
||||
slotProps={{
|
||||
popper: {
|
||||
className: "bar-tooltip",
|
||||
@@ -145,7 +145,7 @@ const BarChart = ({ checks = [] }) => {
|
||||
position="relative"
|
||||
width="9px"
|
||||
height="100%"
|
||||
backgroundColor={theme.palette.primary.lowContrast} // CAIO_REVIEW
|
||||
backgroundColor={theme.palette.primary.lowContrast}
|
||||
sx={{
|
||||
borderRadius: theme.spacing(1.5),
|
||||
/*
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import "./check.css";
|
||||
import PropTypes from "prop-types";
|
||||
import CheckGrey from "../../../assets/icons/check.svg?react";
|
||||
import CheckOutlined from "../../../assets/icons/check-outlined.svg?react";
|
||||
import Icon from "../Icon";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
@@ -35,16 +34,16 @@ const Check = ({ text, noHighlightText, variant = "info", outlined = false }) =>
|
||||
alignItems="center"
|
||||
>
|
||||
{outlined ? (
|
||||
<CheckOutlined alt="check" />
|
||||
<Icon
|
||||
name="CheckCircle"
|
||||
size={20}
|
||||
/>
|
||||
) : (
|
||||
<Box
|
||||
lineHeight={0}
|
||||
sx={{
|
||||
"& svg > path": { fill: colors[variant] },
|
||||
}}
|
||||
>
|
||||
<CheckGrey alt="form checks" />
|
||||
</Box>
|
||||
<Icon
|
||||
name="Check"
|
||||
size={16}
|
||||
color={colors[variant]}
|
||||
/>
|
||||
)}
|
||||
<Typography
|
||||
component="span"
|
||||
|
||||
@@ -8,8 +8,7 @@ import IconButton from "@mui/material/IconButton";
|
||||
import Container from "@mui/material/Container";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Drawer from "@mui/material/Drawer";
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
import CloseRoundedIcon from "@mui/icons-material/CloseRounded";
|
||||
import Icon from "../Icon";
|
||||
import ThemeSwitch from "../ThemeSwitch/index.jsx";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useSelector } from "react-redux";
|
||||
@@ -153,7 +152,11 @@ const AppAppBar = () => {
|
||||
aria-label="Menu button"
|
||||
onClick={toggleDrawer(true)}
|
||||
>
|
||||
<MenuIcon sx={{ color: theme.palette.text.primary }} />
|
||||
<Icon
|
||||
name="Menu"
|
||||
size={24}
|
||||
color={theme.palette.text.primary}
|
||||
/>
|
||||
</IconButton>
|
||||
<Drawer
|
||||
anchor="top"
|
||||
@@ -176,7 +179,11 @@ const AppAppBar = () => {
|
||||
}}
|
||||
>
|
||||
<IconButton onClick={toggleDrawer(false)}>
|
||||
<CloseRoundedIcon sx={{ color: theme.palette.text.primary }} />
|
||||
<Icon
|
||||
name="X"
|
||||
size={24}
|
||||
color={theme.palette.text.primary}
|
||||
/>
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Checkbox, FormControl, ListItemText, MenuItem, Select } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline";
|
||||
import Icon from "../Icon";
|
||||
|
||||
/**
|
||||
* A reusable filter header component that displays a dropdown menu with selectable options.
|
||||
@@ -29,9 +29,10 @@ const FilterHeader = ({ header, options, value, onChange, multiple = true }) =>
|
||||
<Select
|
||||
multiple={multiple}
|
||||
IconComponent={(props) => (
|
||||
<AddCircleOutlineIcon
|
||||
<Icon
|
||||
{...props}
|
||||
sx={{ fontSize: "medium" }}
|
||||
name="PlusCircle"
|
||||
size={18}
|
||||
/>
|
||||
)}
|
||||
displayEmpty
|
||||
|
||||
@@ -40,13 +40,13 @@ const GenericFallback = ({ children }) => {
|
||||
<Box
|
||||
component="img"
|
||||
src={mode === "light" ? OutputAnimation : DarkmodeOutput}
|
||||
Background="transparent"
|
||||
alt="Loading animation"
|
||||
sx={{
|
||||
zIndex: 1,
|
||||
border: "none",
|
||||
borderRadius: theme.spacing(8),
|
||||
width: "100%",
|
||||
backgroundColor: "transparent",
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Stack, Typography } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Dot from "../Dot/index.jsx";
|
||||
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
|
||||
/**
|
||||
* Host component.
|
||||
* This subcomponent receives a params object and displays the host details.
|
||||
@@ -14,7 +15,7 @@ import Dot from "../Dot/index.jsx";
|
||||
* @param {number} params.percentage - The percentage to display.
|
||||
* @returns {React.ElementType} Returns a div element with the host details.
|
||||
*/
|
||||
const Host = ({ url, title, percentageColor, percentage, showURL }) => {
|
||||
const Host = ({ url, title, percentageColor, percentage, showURL, status }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack>
|
||||
@@ -24,19 +25,34 @@ const Host = ({ url, title, percentageColor, percentage, showURL }) => {
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
{title}
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
|
||||
{percentageColor && percentage && (
|
||||
<>
|
||||
<Dot />
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
color: percentageColor,
|
||||
fontWeight: 500,
|
||||
color: percentageColor,
|
||||
}}
|
||||
>
|
||||
{percentage}%
|
||||
</Typography>
|
||||
{status && (
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -51,6 +67,7 @@ Host.propTypes = {
|
||||
percentage: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
showURL: PropTypes.bool,
|
||||
status: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Host;
|
||||
@@ -2,7 +2,7 @@ import i18n from "../../../Utils/i18n.js";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useEffect } from "react";
|
||||
const I18nLoader = () => {
|
||||
const language = useSelector((state) => state.ui.language);
|
||||
const language = useSelector((state) => state.ui.language ?? "en");
|
||||
|
||||
useEffect(() => {
|
||||
if (language && i18n.language !== language) {
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Maps legacy icon names to Lucide icon names
|
||||
* Use: import { iconMap } from './iconMap'; LucideIcons[iconMap['monitors']]
|
||||
*/
|
||||
export const iconMap = {
|
||||
// Navigation/Sidebar icons
|
||||
monitors: "Globe",
|
||||
incidents: "AlertTriangle",
|
||||
integrations: "Link",
|
||||
"page-speed": "Gauge",
|
||||
settings: "Settings",
|
||||
notifications: "Bell",
|
||||
maintenance: "Wrench",
|
||||
"status-pages": "Wifi",
|
||||
docs: "FileText",
|
||||
discussions: "MessageCircle",
|
||||
support: "HelpCircle",
|
||||
changeLog: "Code",
|
||||
logs: "Database",
|
||||
|
||||
// User icons
|
||||
user: "User",
|
||||
"user-two": "Users",
|
||||
"user-edit": "UserPen",
|
||||
groups: "Users",
|
||||
lock: "Lock",
|
||||
key: "Key",
|
||||
logout: "LogOut",
|
||||
|
||||
// Action icons
|
||||
edit: "Pencil",
|
||||
"trash-bin": "Trash2",
|
||||
search: "Search",
|
||||
"close-icon": "X",
|
||||
check: "Check",
|
||||
"check-icon": "CheckCircle",
|
||||
"check-outlined": "CheckCircle",
|
||||
|
||||
// Arrow icons
|
||||
"left-arrow": "ChevronLeft",
|
||||
"right-arrow": "ChevronRight",
|
||||
"up-arrow": "ChevronUp",
|
||||
"down-arrow": "ChevronDown",
|
||||
"left-arrow-double": "ChevronsLeft",
|
||||
"right-arrow-double": "ChevronsRight",
|
||||
"left-arrow-long": "ArrowLeft",
|
||||
"top-right-arrow": "ArrowUpRight",
|
||||
"open-in-new-page": "ExternalLink",
|
||||
"selector-vertical": "ChevronsUpDown",
|
||||
|
||||
// Status/Alert icons
|
||||
"alert-icon": "Bell",
|
||||
"warning-icon": "AlertCircle",
|
||||
"history-icon": "History",
|
||||
|
||||
// Data/Chart icons
|
||||
dashboard: "LayoutGrid",
|
||||
"monitor-graph-line": "TrendingUp",
|
||||
"response-time-icon": "TrendingUp",
|
||||
"uptime-icon": "Activity",
|
||||
"speedometer-icon": "Gauge",
|
||||
"spedometer-icon": "Gauge",
|
||||
"ruler-icon": "Ruler",
|
||||
"performance-report": "Layers",
|
||||
"average-response-icon": "BarChart3",
|
||||
"distributed-uptime": "Network",
|
||||
|
||||
// Misc icons
|
||||
calendar: "Calendar",
|
||||
"calendar-check": "CalendarCheck",
|
||||
"clock-snooze": "Clock",
|
||||
mail: "Mail",
|
||||
email: "Mail",
|
||||
folder: "Folder",
|
||||
"dots-vertical": "MoreVertical",
|
||||
"pause-icon": "PauseCircle",
|
||||
"resume-icon": "PlayCircle",
|
||||
"checkbox-filled": "CheckSquare",
|
||||
"checkbox-outline": "Square",
|
||||
"radio-checked": "Circle",
|
||||
"cpu-chip": "Cpu",
|
||||
certificate: "Award",
|
||||
"interval-check": "RefreshCw",
|
||||
};
|
||||
|
||||
/**
|
||||
* MUI icon to Lucide mapping
|
||||
*/
|
||||
export const muiToLucideMap = {
|
||||
AddCircleOutline: "PlusCircle",
|
||||
ArrowDownwardRounded: "ArrowDown",
|
||||
ArrowUpwardRounded: "ArrowUp",
|
||||
ArrowDropDown: "ChevronDown",
|
||||
ArrowOutward: "ExternalLink",
|
||||
CheckCircle: "CheckCircle2",
|
||||
Clear: "X",
|
||||
Close: "X",
|
||||
CloseRounded: "X",
|
||||
CloudUpload: "Upload",
|
||||
Delete: "Trash2",
|
||||
DeleteOutlineRounded: "Trash2",
|
||||
Email: "Mail",
|
||||
ErrorOutline: "AlertTriangle",
|
||||
ErrorOutlineOutlined: "AlertTriangle",
|
||||
Image: "ImageIcon",
|
||||
InfoOutlined: "Info",
|
||||
KeyboardArrowDown: "ChevronDown",
|
||||
Menu: "Menu",
|
||||
PauseCircleOutline: "PauseCircle",
|
||||
PauseOutlined: "Pause",
|
||||
PlayArrowOutlined: "Play",
|
||||
PlayCircleOutlineRounded: "PlayCircle",
|
||||
RadioButtonChecked: "Circle",
|
||||
ReorderRounded: "GripVertical",
|
||||
SettingsOutlined: "Settings",
|
||||
Share: "Share2",
|
||||
Visibility: "Eye",
|
||||
VisibilityOff: "EyeOff",
|
||||
WarningAmberOutlined: "AlertTriangle",
|
||||
};
|
||||
@@ -0,0 +1,196 @@
|
||||
import { useTheme } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
Activity,
|
||||
AlertCircle,
|
||||
AlertTriangle,
|
||||
ArrowDown,
|
||||
ArrowLeft,
|
||||
ArrowUp,
|
||||
ArrowUpRight,
|
||||
Bell,
|
||||
Calendar,
|
||||
Check,
|
||||
CheckCircle,
|
||||
CheckSquare,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronsLeft,
|
||||
ChevronsRight,
|
||||
ChevronsUpDown,
|
||||
Circle,
|
||||
CircleDot,
|
||||
Clock,
|
||||
Code,
|
||||
Cpu,
|
||||
Database,
|
||||
ExternalLink,
|
||||
Eye,
|
||||
EyeOff,
|
||||
FileText,
|
||||
Gauge,
|
||||
Globe,
|
||||
GripVertical,
|
||||
HelpCircle,
|
||||
History,
|
||||
Image as ImageIcon,
|
||||
Info,
|
||||
Key,
|
||||
Layers,
|
||||
Link,
|
||||
Lock,
|
||||
LogOut,
|
||||
Mail,
|
||||
Menu,
|
||||
MessageCircle,
|
||||
MoreVertical,
|
||||
Pause,
|
||||
PauseCircle,
|
||||
Play,
|
||||
PlayCircle,
|
||||
PlusCircle,
|
||||
RefreshCw,
|
||||
Ruler,
|
||||
Search,
|
||||
Settings,
|
||||
Square,
|
||||
Trash2,
|
||||
TrendingUp,
|
||||
Upload,
|
||||
User,
|
||||
Users,
|
||||
Wifi,
|
||||
Wrench,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
|
||||
/**
|
||||
* Map of icon names to Lucide icon components.
|
||||
* Only icons explicitly imported here will be included in the bundle (tree-shaking).
|
||||
* To add a new icon: import it above and add it to this object.
|
||||
*/
|
||||
const iconComponents = {
|
||||
Activity,
|
||||
AlertCircle,
|
||||
AlertTriangle,
|
||||
ArrowDown,
|
||||
ArrowLeft,
|
||||
ArrowUp,
|
||||
ArrowUpRight,
|
||||
Bell,
|
||||
Calendar,
|
||||
Check,
|
||||
CheckCircle,
|
||||
CheckSquare,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronsLeft,
|
||||
ChevronsRight,
|
||||
ChevronsUpDown,
|
||||
Circle,
|
||||
CircleDot,
|
||||
Clock,
|
||||
Code,
|
||||
Cpu,
|
||||
Database,
|
||||
ExternalLink,
|
||||
Eye,
|
||||
EyeOff,
|
||||
FileText,
|
||||
Gauge,
|
||||
Globe,
|
||||
GripVertical,
|
||||
HelpCircle,
|
||||
History,
|
||||
Image: ImageIcon,
|
||||
Info,
|
||||
Key,
|
||||
Layers,
|
||||
Link,
|
||||
Lock,
|
||||
LogOut,
|
||||
Mail,
|
||||
Menu,
|
||||
MessageCircle,
|
||||
MoreVertical,
|
||||
Pause,
|
||||
PauseCircle,
|
||||
Play,
|
||||
PlayCircle,
|
||||
PlusCircle,
|
||||
RefreshCw,
|
||||
Ruler,
|
||||
Search,
|
||||
Settings,
|
||||
Square,
|
||||
Trash2,
|
||||
TrendingUp,
|
||||
Upload,
|
||||
User,
|
||||
Users,
|
||||
Wifi,
|
||||
Wrench,
|
||||
X,
|
||||
};
|
||||
|
||||
/**
|
||||
* Theme-aware icon component wrapping Lucide icons
|
||||
*
|
||||
* @param {string} name - Lucide icon name (e.g., "Bell", "Check", "ArrowLeft")
|
||||
* @param {number} size - Icon size in pixels (default: 20)
|
||||
* @param {string} color - Direct color value OR theme path like "primary.contrastText"
|
||||
* @param {number} strokeWidth - Stroke width (default: 1.5)
|
||||
* @param {string} fill - Fill color (default: "none")
|
||||
*/
|
||||
const Icon = ({ name, size = 20, color, strokeWidth = 1.5, fill = "none", ...props }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
// Resolve color from theme path or use direct value
|
||||
const resolveColor = (colorValue) => {
|
||||
if (!colorValue) {
|
||||
return theme.palette.primary.contrastTextTertiary; // Default icon color
|
||||
}
|
||||
|
||||
// If it's a theme path like "primary.contrastText"
|
||||
if (typeof colorValue === "string" && colorValue.includes(".")) {
|
||||
const parts = colorValue.split(".");
|
||||
let resolved = theme.palette;
|
||||
for (const part of parts) {
|
||||
resolved = resolved?.[part];
|
||||
}
|
||||
return resolved || colorValue;
|
||||
}
|
||||
|
||||
// Direct color value
|
||||
return colorValue;
|
||||
};
|
||||
|
||||
const LucideIcon = iconComponents[name];
|
||||
|
||||
if (!LucideIcon) {
|
||||
console.warn(`Icon "${name}" not found in Icon component`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<LucideIcon
|
||||
size={size}
|
||||
color={resolveColor(color)}
|
||||
strokeWidth={strokeWidth}
|
||||
fill={fill}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Icon.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
size: PropTypes.number,
|
||||
color: PropTypes.string,
|
||||
strokeWidth: PropTypes.number,
|
||||
fill: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Icon;
|
||||
@@ -1,8 +1,7 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { FormControlLabel, Checkbox as MuiCheckbox } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import CheckboxOutline from "../../../../assets/icons/checkbox-outline.svg?react";
|
||||
import CheckboxFilled from "../../../../assets/icons/checkbox-filled.svg?react";
|
||||
import Icon from "../../Icon";
|
||||
|
||||
/**
|
||||
* Checkbox Component
|
||||
@@ -65,8 +64,8 @@ const Checkbox = ({
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
icon={<CheckboxOutline />}
|
||||
checkedIcon={<CheckboxFilled />}
|
||||
icon={<Icon name="Square" />}
|
||||
checkedIcon={<Icon name="CheckSquare" />}
|
||||
inputProps={{
|
||||
"aria-label": "controlled checkbox",
|
||||
id: id,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// Components
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
|
||||
import Icon from "../../Icon";
|
||||
import Image from "../../Image/index.jsx";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import ProgressUpload from "../../ProgressBars/index.jsx";
|
||||
import ImageIcon from "@mui/icons-material/Image";
|
||||
|
||||
// Utils
|
||||
import PropTypes from "prop-types";
|
||||
@@ -176,7 +175,10 @@ const ImageUpload = ({
|
||||
boxShadow: theme.shape.boxShadow,
|
||||
}}
|
||||
>
|
||||
<CloudUploadIcon />
|
||||
<Icon
|
||||
name="Upload"
|
||||
size={24}
|
||||
/>
|
||||
</IconButton>
|
||||
<Typography
|
||||
component="h2"
|
||||
@@ -203,7 +205,12 @@ const ImageUpload = ({
|
||||
</Box>
|
||||
{(localError || progress.isLoading || progress.value !== 0) && (
|
||||
<ProgressUpload
|
||||
icon={<ImageIcon />}
|
||||
icon={
|
||||
<Icon
|
||||
name="Image"
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
label={file?.name || "Upload failed"}
|
||||
size={file?.size}
|
||||
progress={progress.value}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { FormControlLabel, Radio as MUIRadio, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import RadioChecked from "../../../../assets/icons/radio-checked.svg?react";
|
||||
import Icon from "../../Icon";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
@@ -47,7 +47,12 @@ const Radio = ({
|
||||
<MUIRadio
|
||||
id={id}
|
||||
size={size}
|
||||
checkedIcon={<RadioChecked />}
|
||||
checkedIcon={
|
||||
<Icon
|
||||
name="CircleDot"
|
||||
size={16}
|
||||
/>
|
||||
}
|
||||
sx={{
|
||||
color: "transparent",
|
||||
width: 16,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
Checkbox,
|
||||
} from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import SearchIcon from "../../../../assets/icons/search.svg?react";
|
||||
import Icon from "../../Icon";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import FieldWrapper from "../FieldWrapper/index.jsx";
|
||||
@@ -31,20 +31,13 @@ import FieldWrapper from "../FieldWrapper/index.jsx";
|
||||
const SearchAdornment = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box
|
||||
height={16}
|
||||
sx={{
|
||||
"& svg": {
|
||||
width: 16,
|
||||
height: 16,
|
||||
"& path": {
|
||||
stroke: theme.palette.primary.contrastTextTertiary,
|
||||
strokeWidth: 1.2,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SearchIcon />
|
||||
<Box height={16}>
|
||||
<Icon
|
||||
name="Search"
|
||||
size={16}
|
||||
color="primary.contrastTextTertiary"
|
||||
strokeWidth={1.2}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -85,7 +78,12 @@ const Search = ({
|
||||
const enhancedOptions = React.useMemo(() => {
|
||||
return multiple && isAdorned
|
||||
? [
|
||||
{ [filteredBy]: t("selectAll"), isSelectAll: true, _id: "select_all" },
|
||||
{
|
||||
[filteredBy]: t("selectAll"),
|
||||
isSelectAll: true,
|
||||
_id: "select_all",
|
||||
id: "select_all",
|
||||
},
|
||||
...options,
|
||||
]
|
||||
: options;
|
||||
@@ -93,7 +91,7 @@ const Search = ({
|
||||
const isOptionSelected = (option) => {
|
||||
if (!multiple && !isAdorned) return false;
|
||||
if (Array.isArray(value)) {
|
||||
return value.some((item) => item._id === option._id);
|
||||
return value.some((item) => (item._id ?? item.id) === (option._id ?? option.id));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -145,7 +143,9 @@ const Search = ({
|
||||
disableClearable
|
||||
options={enhancedOptions}
|
||||
getOptionLabel={(option) => option[filteredBy]}
|
||||
isOptionEqualToValue={(option, value) => option._id === value._id} // Compare by unique identifier
|
||||
isOptionEqualToValue={(option, value) =>
|
||||
(option._id ?? option.id) === (value?._id ?? value?.id)
|
||||
} // Compare by unique identifier
|
||||
renderInput={(params) => (
|
||||
<FieldWrapper
|
||||
label={label}
|
||||
@@ -206,7 +206,7 @@ const Search = ({
|
||||
return filtered;
|
||||
}}
|
||||
getOptionKey={(option) => {
|
||||
return option._id;
|
||||
return option._id ?? option.id;
|
||||
}}
|
||||
renderOption={(props, option) => {
|
||||
const { key, ...optionProps } = props;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { MenuItem, Select as MuiSelect, Stack, Typography } from "@mui/material";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import Icon from "../../Icon";
|
||||
import FieldWrapper from "../FieldWrapper/index.jsx";
|
||||
|
||||
import "./index.css";
|
||||
@@ -62,6 +62,7 @@ const Select = ({
|
||||
fieldWrapperSx = {},
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const getItemValue = (item) => item?._id ?? item?.id;
|
||||
const itemStyles = {
|
||||
fontSize: "var(--env-var-font-size-medium)",
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
@@ -97,7 +98,13 @@ const Select = ({
|
||||
error={error}
|
||||
name={name}
|
||||
inputProps={{ id: id }}
|
||||
IconComponent={KeyboardArrowDownIcon}
|
||||
IconComponent={(props) => (
|
||||
<Icon
|
||||
name="ChevronDown"
|
||||
size={20}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
MenuProps={{ disableScrollLock: true }}
|
||||
sx={{
|
||||
fontSize: 13,
|
||||
@@ -123,7 +130,7 @@ const Select = ({
|
||||
...sx,
|
||||
}}
|
||||
renderValue={(selected) => {
|
||||
const selectedItem = items.find((item) => item._id === selected);
|
||||
const selectedItem = items.find((item) => getItemValue(item) === selected);
|
||||
const displayName = selectedItem ? selectedItem.name : placeholder;
|
||||
return (
|
||||
<Typography
|
||||
@@ -152,17 +159,25 @@ const Select = ({
|
||||
{placeholder}
|
||||
</MenuItem>
|
||||
)}
|
||||
{items.map((item) => (
|
||||
<MenuItem
|
||||
value={item._id}
|
||||
key={`${id}-${item._id}`}
|
||||
sx={{
|
||||
...itemStyles,
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
{items
|
||||
.map((item) => {
|
||||
const itemValue = getItemValue(item);
|
||||
if (itemValue === undefined || itemValue === null) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<MenuItem
|
||||
value={itemValue}
|
||||
key={`${id}-${itemValue}`}
|
||||
sx={{
|
||||
...itemStyles,
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
);
|
||||
})
|
||||
.filter(Boolean)}
|
||||
</MuiSelect>
|
||||
</FieldWrapper>
|
||||
);
|
||||
@@ -179,8 +194,8 @@ Select.propTypes = {
|
||||
.isRequired,
|
||||
items: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool])
|
||||
.isRequired,
|
||||
_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
|
||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
|
||||
|
||||
name: PropTypes.string.isRequired,
|
||||
})
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { Stack, Typography, InputAdornment, IconButton } from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import PropTypes from "prop-types";
|
||||
import VisibilityOff from "@mui/icons-material/VisibilityOff";
|
||||
import Visibility from "@mui/icons-material/Visibility";
|
||||
import ReorderRoundedIcon from "@mui/icons-material/ReorderRounded";
|
||||
import DeleteIcon from "../../../../../assets/icons/trash-bin.svg?react";
|
||||
import Icon from "../../../Icon";
|
||||
|
||||
export const HttpAdornment = ({ https }) => {
|
||||
const theme = useTheme();
|
||||
@@ -56,7 +53,17 @@ export const PasswordEndAdornment = ({ fieldType, setFieldType }) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{fieldType === "password" ? <VisibilityOff /> : <Visibility />}
|
||||
{fieldType === "password" ? (
|
||||
<Icon
|
||||
name="EyeOff"
|
||||
size={20}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
name="Eye"
|
||||
size={20}
|
||||
/>
|
||||
)}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
);
|
||||
|
||||
@@ -147,11 +147,9 @@ const StatusLabel = ({ status, text, customStyles }) => {
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
width={7}
|
||||
height={7}
|
||||
bgcolor={theme.palette[themeColor].lowContrast}
|
||||
borderRadius="50%"
|
||||
marginRight="5px"
|
||||
borderRadius="0%"
|
||||
marginRight="1px"
|
||||
/>
|
||||
</BaseLabel>
|
||||
);
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
.home-layout {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* TODO go for this approach for responsiveness. The aside needs to be taken care of */
|
||||
/* @media (max-width: 1000px) {
|
||||
.home-layout {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
} */
|
||||
|
||||
/* .home-layout aside {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
max-width: var(--env-var-side-bar-width);
|
||||
} */
|
||||
|
||||
.home-layout > div {
|
||||
min-height: calc(100vh - var(--env-var-spacing-2) * 2);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.home-content-wrapper {
|
||||
padding: var(--env-var-spacing-2);
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
flex: 1;
|
||||
}
|
||||
@@ -1,20 +1,44 @@
|
||||
import Sidebar from "../../Sidebar/index.jsx";
|
||||
import { Outlet } from "react-router";
|
||||
import { Stack } from "@mui/material";
|
||||
|
||||
import "./index.css";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { UserGuideSidebar, TAB_BAR_WIDTH } from "../../UserGuide";
|
||||
|
||||
const HomeLayout = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className="home-layout"
|
||||
flexDirection="row"
|
||||
gap={14}
|
||||
sx={{
|
||||
position: "relative",
|
||||
minHeight: "100vh",
|
||||
margin: "0 auto",
|
||||
padding: 0,
|
||||
overflowX: "hidden",
|
||||
width: "100%",
|
||||
"& > div": {
|
||||
minHeight: `calc(100vh - ${theme.spacing(4)} * 2)`,
|
||||
flex: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Sidebar />
|
||||
<Stack className="home-content-wrapper">
|
||||
<Stack
|
||||
sx={{
|
||||
padding: theme.spacing(4),
|
||||
paddingRight: `${TAB_BAR_WIDTH + 16}px`,
|
||||
maxWidth: 1400,
|
||||
margin: "0 auto",
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
overflowX: "hidden",
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
</Stack>
|
||||
<UserGuideSidebar />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
import Button from "@mui/material/Button";
|
||||
import ButtonGroup from "@mui/material/ButtonGroup";
|
||||
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
|
||||
import Icon from "../Icon";
|
||||
import ClickAwayListener from "@mui/material/ClickAwayListener";
|
||||
import Grow from "@mui/material/Grow";
|
||||
import Paper from "@mui/material/Paper";
|
||||
@@ -11,7 +11,7 @@ import MenuList from "@mui/material/MenuList";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createToast } from "../../../Utils/toastUtils.jsx";
|
||||
import { useExportMonitors } from "../../../Hooks/v1/monitorHooks.js";
|
||||
import { useExportMonitors } from "../../../Hooks/monitorHooks.js";
|
||||
|
||||
const MonitorActions = ({ isLoading }) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
@@ -70,7 +70,10 @@ const MonitorActions = ({ isLoading }) => {
|
||||
aria-haspopup="menu"
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<ArrowDropDownIcon />
|
||||
<Icon
|
||||
name="ChevronDown"
|
||||
size={20}
|
||||
/>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Popper
|
||||
|
||||
@@ -3,19 +3,16 @@ import Status from "./status.jsx";
|
||||
import Skeleton from "./skeleton.jsx";
|
||||
import Button from "@mui/material/Button";
|
||||
import { Tooltip } from "@mui/material";
|
||||
import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
|
||||
import PauseOutlinedIcon from "@mui/icons-material/PauseOutlined";
|
||||
import PlayArrowOutlinedIcon from "@mui/icons-material/PlayArrowOutlined";
|
||||
import EmailIcon from "@mui/icons-material/Email";
|
||||
import Icon from "../Icon";
|
||||
|
||||
// Utils
|
||||
import PropTypes from "prop-types";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { usePauseMonitor } from "../../../Hooks/v1/monitorHooks.js";
|
||||
import { useSendTestEmail } from "../../../Hooks/v1/useSendTestEmail.js";
|
||||
import { usePauseMonitor } from "../../../Hooks/monitorHooks.js";
|
||||
import { useSendTestEmail } from "../../../Hooks/useSendTestEmail.js";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTestAllNotifications } from "../../../Hooks/v1/useNotifications.js";
|
||||
import { useTestAllNotifications } from "../../../Hooks/useNotifications.js";
|
||||
/**
|
||||
* MonitorDetailsControlHeader component displays the control header for monitor details.
|
||||
* It includes status display, pause/resume button, and a configure button for admins.
|
||||
@@ -66,7 +63,7 @@ const MonitorDetailsControlHeader = ({
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Tooltip
|
||||
key={monitor?._id}
|
||||
key={monitor?.id}
|
||||
placement="bottom"
|
||||
title={tooltipTitle}
|
||||
>
|
||||
@@ -75,10 +72,15 @@ const MonitorDetailsControlHeader = ({
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
loading={isSending}
|
||||
startIcon={<EmailIcon />}
|
||||
startIcon={
|
||||
<Icon
|
||||
name="Mail"
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
disabled={isTestNotificationsDisabled}
|
||||
onClick={() => {
|
||||
testAllNotifications({ monitorId: monitor?._id });
|
||||
testAllNotifications({ monitorId: monitor?.id });
|
||||
}}
|
||||
sx={{
|
||||
whiteSpace: "nowrap",
|
||||
@@ -92,7 +94,7 @@ const MonitorDetailsControlHeader = ({
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={(e) => {
|
||||
navigate(`/incidents/${monitor?._id}`);
|
||||
navigate(`/incidents/${monitor?.id}`);
|
||||
}}
|
||||
>
|
||||
{t("menu.incidents")}
|
||||
@@ -103,11 +105,21 @@ const MonitorDetailsControlHeader = ({
|
||||
color="secondary"
|
||||
loading={isPausing}
|
||||
startIcon={
|
||||
monitor?.isActive ? <PauseOutlinedIcon /> : <PlayArrowOutlinedIcon />
|
||||
monitor?.isActive ? (
|
||||
<Icon
|
||||
name="Pause"
|
||||
size={18}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
name="Play"
|
||||
size={18}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
pauseMonitor({
|
||||
monitorId: monitor?._id,
|
||||
monitorId: monitor?.id,
|
||||
triggerUpdate,
|
||||
});
|
||||
}}
|
||||
@@ -119,8 +131,13 @@ const MonitorDetailsControlHeader = ({
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
startIcon={<SettingsOutlinedIcon />}
|
||||
onClick={() => navigate(`/${path}/configure/${monitor._id}`)}
|
||||
startIcon={
|
||||
<Icon
|
||||
name="Settings"
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
onClick={() => navigate(`/${path}/configure/${monitor.id}`)}
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
|
||||
@@ -7,7 +7,7 @@ import Dot from "../Dot/index.jsx";
|
||||
import { formatDurationRounded } from "../../../Utils/timeUtils.js";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useMonitorUtils } from "../../../Hooks/v1/useMonitorUtils.js";
|
||||
import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js";
|
||||
import { formatMonitorUrl } from "../../../Utils/utils.js";
|
||||
/**
|
||||
* Status component displays the status information of a monitor.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
|
||||
import Icon from "../Icon";
|
||||
import Search from "../Inputs/Search/index.jsx";
|
||||
|
||||
// Utils
|
||||
@@ -102,11 +102,13 @@ const NotificationConfig = ({
|
||||
>
|
||||
{notification.notificationName}
|
||||
</Typography>
|
||||
<DeleteOutlineRoundedIcon
|
||||
<Icon
|
||||
name="Trash2"
|
||||
size={20}
|
||||
onClick={() => {
|
||||
handleDelete(notification._id);
|
||||
}}
|
||||
sx={{ cursor: "pointer" }}
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
{index < selectedNotifications.length - 1 && <Divider />}
|
||||
</Stack>
|
||||
|
||||
@@ -3,7 +3,7 @@ import Fallback from "../Fallback/index.jsx";
|
||||
import { Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useIsAdmin } from "../../../Hooks/v1/useIsAdmin.js";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
|
||||
import PropTypes from "prop-types";
|
||||
const PageStateWrapper = ({
|
||||
networkError,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Box, IconButton, LinearProgress, Stack, Typography } from "@mui/material";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined";
|
||||
import Icon from "../Icon";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
@@ -51,7 +50,10 @@ const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) =>
|
||||
alignItems={error ? "center" : "flex-start"}
|
||||
>
|
||||
{error ? (
|
||||
<ErrorOutlineOutlinedIcon />
|
||||
<Icon
|
||||
name="AlertTriangle"
|
||||
size={20}
|
||||
/>
|
||||
) : icon ? (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -127,10 +129,9 @@ const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) =>
|
||||
}
|
||||
}
|
||||
>
|
||||
<CloseIcon
|
||||
sx={{
|
||||
fontSize: "20px",
|
||||
}}
|
||||
<Icon
|
||||
name="X"
|
||||
size={20}
|
||||
/>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import React from "react";
|
||||
import { Step, Stepper, StepLabel, Typography } from "@mui/material";
|
||||
import RadioButtonCheckedIcon from "@mui/icons-material/RadioButtonChecked";
|
||||
import CheckCircle from "@mui/icons-material/CheckCircle";
|
||||
import Icon from "../Icon";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const CustomStepIcon = (props) => {
|
||||
const { completed, active } = props;
|
||||
return completed ? (
|
||||
<CheckCircle color="accent" />
|
||||
<Icon
|
||||
name="CheckCircle2"
|
||||
size={24}
|
||||
color="accent.main"
|
||||
/>
|
||||
) : (
|
||||
<RadioButtonCheckedIcon color={active ? "accent" : "disabled"} />
|
||||
<Icon
|
||||
name="Circle"
|
||||
size={24}
|
||||
color={active ? "accent.main" : "text.disabled"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import html2canvas from "html2canvas";
|
||||
import ShareIcon from "@mui/icons-material/Share";
|
||||
import Icon from "../Icon";
|
||||
import { Button } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
@@ -55,7 +55,13 @@ const ShareComponent = ({ elementToCapture, fileName = "screenshot" }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<ShareIcon sx={{ color: theme.palette.success.main }} />}
|
||||
startIcon={
|
||||
<Icon
|
||||
name="Share2"
|
||||
size={18}
|
||||
color="success.main"
|
||||
/>
|
||||
}
|
||||
color="success"
|
||||
onClick={captureAndShare}
|
||||
>
|
||||
|
||||
@@ -7,8 +7,7 @@ import Avatar from "../../Avatar/index.jsx";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import DotsVertical from "../../../../assets/icons/dots-vertical.svg?react";
|
||||
import LogoutSvg from "../../../../assets/icons/logout.svg?react";
|
||||
import Icon from "../../Icon";
|
||||
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useSelector } from "react-redux";
|
||||
@@ -81,6 +80,10 @@ const AuthFooter = ({ collapsed, accountMenuItems }) => {
|
||||
gap: theme.spacing(2),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
pl: theme.spacing(4),
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
"& svg": {
|
||||
stroke: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
@@ -160,19 +163,14 @@ const AuthFooter = ({ collapsed, accountMenuItems }) => {
|
||||
ml: "50px",
|
||||
"&:focus": { outline: "none" },
|
||||
alignSelf: "center",
|
||||
|
||||
"& svg": {
|
||||
width: "22px",
|
||||
height: "22px",
|
||||
},
|
||||
"& svg path": {
|
||||
/* Vertical three dots */
|
||||
stroke: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
}}
|
||||
onClick={(event) => openPopup(event)}
|
||||
>
|
||||
<DotsVertical />
|
||||
<Icon
|
||||
name="MoreVertical"
|
||||
size={22}
|
||||
color="primary.contrastTextTertiary"
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
@@ -256,12 +254,13 @@ const AuthFooter = ({ collapsed, accountMenuItems }) => {
|
||||
gap: theme.spacing(4),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
pl: theme.spacing(4),
|
||||
"& svg path": {
|
||||
stroke: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<LogoutSvg />
|
||||
<Icon
|
||||
name="LogOut"
|
||||
size={20}
|
||||
color="primary.contrastTextTertiary"
|
||||
/>
|
||||
{t("menu.logOut", "Log out")}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
@@ -52,12 +52,11 @@ const NavItem = ({ item, collapsed, selected, onClick }) => {
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: 0,
|
||||
color: iconStroke,
|
||||
"& svg": {
|
||||
height: 20,
|
||||
width: 20,
|
||||
opacity: 0.81,
|
||||
},
|
||||
"& svg path": {
|
||||
stroke: iconStroke,
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -8,22 +8,7 @@ import NavItem from "./components/navItem.jsx";
|
||||
import AuthFooter from "./components/authFooter.jsx";
|
||||
|
||||
import StarPrompt from "../StarPrompt/index.jsx";
|
||||
import LockSvg from "../../../assets/icons/lock.svg?react";
|
||||
import UserSvg from "../../../assets/icons/user.svg?react";
|
||||
import TeamSvg from "../../../assets/icons/user-two.svg?react";
|
||||
import Support from "../../../assets/icons/support.svg?react";
|
||||
import Maintenance from "../../../assets/icons/maintenance.svg?react";
|
||||
import Monitors from "../../../assets/icons/monitors.svg?react";
|
||||
import Incidents from "../../../assets/icons/incidents.svg?react";
|
||||
import Integrations from "../../../assets/icons/integrations.svg?react";
|
||||
import PageSpeed from "../../../assets/icons/page-speed.svg?react";
|
||||
import Settings from "../../../assets/icons/settings.svg?react";
|
||||
import ChangeLog from "../../../assets/icons/changeLog.svg?react";
|
||||
import Docs from "../../../assets/icons/docs.svg?react";
|
||||
import StatusPages from "../../../assets/icons/status-pages.svg?react";
|
||||
import Discussions from "../../../assets/icons/discussions.svg?react";
|
||||
import Notifications from "../../../assets/icons/notifications.svg?react";
|
||||
import Logs from "../../../assets/icons/logs.svg?react";
|
||||
import Icon from "../Icon";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
@@ -39,43 +24,41 @@ const URL_MAP = {
|
||||
};
|
||||
|
||||
const getMenu = (t) => [
|
||||
{ name: t("menu.uptime"), path: "uptime", icon: <Monitors /> },
|
||||
{ name: t("menu.pagespeed"), path: "pagespeed", icon: <PageSpeed /> },
|
||||
|
||||
{ name: t("menu.infrastructure"), path: "infrastructure", icon: <Integrations /> },
|
||||
{ name: t("menu.uptime"), path: "uptime", icon: <Icon name="Globe" /> },
|
||||
{ name: t("menu.pagespeed"), path: "pagespeed", icon: <Icon name="Gauge" /> },
|
||||
{ name: t("menu.infrastructure"), path: "infrastructure", icon: <Icon name="Link" /> },
|
||||
{
|
||||
name: t("menu.notifications"),
|
||||
path: "notifications",
|
||||
icon: <Notifications />,
|
||||
icon: <Icon name="Bell" />,
|
||||
},
|
||||
{ name: t("menu.incidents"), path: "incidents", icon: <Incidents /> },
|
||||
|
||||
{ name: t("menu.statusPages"), path: "status", icon: <StatusPages /> },
|
||||
{ name: t("menu.maintenance"), path: "maintenance", icon: <Maintenance /> },
|
||||
{ name: t("menu.logs"), path: "logs", icon: <Logs /> },
|
||||
|
||||
{ name: t("menu.checks"), path: "checks", icon: <Icon name="FileText" /> },
|
||||
{ name: t("menu.incidents"), path: "incidents", icon: <Icon name="AlertTriangle" /> },
|
||||
{ name: t("menu.statusPages"), path: "status", icon: <Icon name="Wifi" /> },
|
||||
{ name: t("menu.maintenance"), path: "maintenance", icon: <Icon name="Wrench" /> },
|
||||
{ name: t("menu.logs"), path: "logs", icon: <Icon name="Database" /> },
|
||||
{
|
||||
name: t("menu.settings"),
|
||||
icon: <Settings />,
|
||||
icon: <Icon name="Settings" />,
|
||||
path: "settings",
|
||||
},
|
||||
];
|
||||
|
||||
const getOtherMenuItems = (t) => [
|
||||
{ name: t("menu.support"), path: "support", icon: <Support /> },
|
||||
{ name: t("menu.support"), path: "support", icon: <Icon name="HelpCircle" /> },
|
||||
{
|
||||
name: t("menu.discussions"),
|
||||
path: "discussions",
|
||||
icon: <Discussions />,
|
||||
icon: <Icon name="MessageCircle" />,
|
||||
},
|
||||
{ name: t("menu.docs"), path: "docs", icon: <Docs /> },
|
||||
{ name: t("menu.changelog"), path: "changelog", icon: <ChangeLog /> },
|
||||
{ name: t("menu.docs"), path: "docs", icon: <Icon name="FileText" /> },
|
||||
{ name: t("menu.changelog"), path: "changelog", icon: <Icon name="Code" /> },
|
||||
];
|
||||
|
||||
const getAccountMenuItems = (t) => [
|
||||
{ name: t("menu.profile"), path: "account/profile", icon: <UserSvg /> },
|
||||
{ name: t("menu.password"), path: "account/password", icon: <LockSvg /> },
|
||||
{ name: t("menu.team"), path: "account/team", icon: <TeamSvg /> },
|
||||
{ name: t("menu.profile"), path: "account/profile", icon: <Icon name="User" /> },
|
||||
{ name: t("menu.password"), path: "account/password", icon: <Icon name="Lock" /> },
|
||||
{ name: t("menu.team"), path: "account/team", icon: <Icon name="Users" /> },
|
||||
];
|
||||
|
||||
const Sidebar = () => {
|
||||
@@ -83,7 +66,7 @@ const Sidebar = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
// Redux state
|
||||
const collapsed = useSelector((state) => state.ui.sidebar.collapsed);
|
||||
const collapsed = useSelector((state) => state.ui.sidebar?.collapsed ?? false);
|
||||
|
||||
const menu = getMenu(t);
|
||||
const otherMenuItems = getOtherMenuItems(t);
|
||||
@@ -106,6 +89,8 @@ const Sidebar = () => {
|
||||
gap={theme.spacing(6)}
|
||||
sx={{
|
||||
transition: "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)",
|
||||
backgroundColor: theme.palette.primaryBackground.main,
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
<CollapseButton collapsed={collapsed} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { Typography, IconButton, Stack, Box } from "@mui/material";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import Icon from "../Icon";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -67,7 +67,10 @@ const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" })
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CloseIcon sx={{ fontSize: "1.25rem" }} />
|
||||
<Icon
|
||||
name="X"
|
||||
size={20}
|
||||
/>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Stack, Typography } from "@mui/material";
|
||||
import Image from "../Image/index.jsx";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import PropTypes from "prop-types";
|
||||
import { useMonitorUtils } from "../../../Hooks/v1/useMonitorUtils.js";
|
||||
import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js";
|
||||
|
||||
/**
|
||||
* StatBox Component
|
||||
|
||||
@@ -2,7 +2,7 @@ import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Stack, TablePagination, Typography } from "@mui/material";
|
||||
import { TablePaginationActions } from "./Actions/index.jsx";
|
||||
import SelectorVertical from "../../../../assets/icons/selector-vertical.svg?react";
|
||||
import Icon from "../../Icon";
|
||||
|
||||
Pagination.propTypes = {
|
||||
paginationLabel: PropTypes.string, // Label for the pagination.
|
||||
@@ -100,7 +100,12 @@ function Pagination({
|
||||
},
|
||||
},
|
||||
inputProps: { id: "pagination-dropdown" },
|
||||
IconComponent: SelectorVertical,
|
||||
IconComponent: () => (
|
||||
<Icon
|
||||
name="ChevronsUpDown"
|
||||
size={16}
|
||||
/>
|
||||
),
|
||||
sx: {
|
||||
ml: theme.spacing(4),
|
||||
mr: theme.spacing(12),
|
||||
|
||||
@@ -3,19 +3,31 @@ import IconButton from "@mui/material/IconButton";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Button from "@mui/material/Button";
|
||||
import ToastBody from "./body.jsx";
|
||||
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
||||
import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined";
|
||||
import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import Icon from "../Icon";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const icons = {
|
||||
info: <InfoOutlinedIcon />,
|
||||
error: <ErrorOutlineOutlinedIcon />,
|
||||
warning: <WarningAmberOutlinedIcon />,
|
||||
info: (
|
||||
<Icon
|
||||
name="Info"
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
error: (
|
||||
<Icon
|
||||
name="AlertCircle"
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
warning: (
|
||||
<Icon
|
||||
name="AlertTriangle"
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => {
|
||||
@@ -50,7 +62,10 @@ const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => {
|
||||
)}
|
||||
{title && (
|
||||
<IconButton onClick={onClick}>
|
||||
<CloseIcon />
|
||||
<Icon
|
||||
name="X"
|
||||
size={20}
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -63,7 +78,10 @@ const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => {
|
||||
<ToastBody body={body} />
|
||||
{!title && (
|
||||
<IconButton onClick={onClick}>
|
||||
<CloseIcon />
|
||||
<Icon
|
||||
name="X"
|
||||
size={20}
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
/* ArticlePage styles - hover states and nested selectors (Dark theme) */
|
||||
|
||||
/* Breadcrumb hover */
|
||||
.user-guide-breadcrumb-link {
|
||||
transition: color 150ms ease;
|
||||
}
|
||||
|
||||
.user-guide-breadcrumb-link:hover {
|
||||
color: #1570ef;
|
||||
}
|
||||
|
||||
/* ToC link hover */
|
||||
.user-guide-toc-link {
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
.user-guide-toc-link:hover {
|
||||
color: #1570ef;
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
/* Navigation button hover */
|
||||
.user-guide-nav-button .nav-text {
|
||||
transition: color 150ms ease;
|
||||
}
|
||||
|
||||
.user-guide-nav-button:hover .nav-text {
|
||||
color: #1570ef;
|
||||
}
|
||||
|
||||
/* Article body typography */
|
||||
.user-guide-article-body h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.user-guide-article-body h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.user-guide-article-body p {
|
||||
font-size: 13px;
|
||||
color: #d0d5dd;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 16px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.user-guide-article-body ul,
|
||||
.user-guide-article-body ol {
|
||||
padding-left: 24px;
|
||||
margin-bottom: 16px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.user-guide-article-body li {
|
||||
font-size: 13px;
|
||||
color: #d0d5dd;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.user-guide-article-body a {
|
||||
color: #1570ef;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.user-guide-article-body a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.user-guide-article-body code {
|
||||
background-color: #2d2d2d;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-family: "Geist Mono", "Fira Code", "Consolas", monospace;
|
||||
color: #d0d5dd;
|
||||
}
|
||||
|
||||
.user-guide-article-body pre {
|
||||
background-color: #1c2130;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
margin-bottom: 16px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.user-guide-article-body pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.user-guide-article-body blockquote {
|
||||
border-left: 3px solid #1570ef;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
padding: 12px 16px;
|
||||
border-radius: 0 4px 4px 0;
|
||||
margin-bottom: 16px;
|
||||
margin-top: 0;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.user-guide-article-body blockquote p {
|
||||
margin-bottom: 0;
|
||||
color: #d0d5dd;
|
||||
}
|
||||
|
||||
.user-guide-article-body table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.user-guide-article-body th,
|
||||
.user-guide-article-body td {
|
||||
border: 1px solid #344054;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.user-guide-article-body th {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.user-guide-article-body td {
|
||||
color: #d0d5dd;
|
||||
}
|
||||
|
||||
.user-guide-article-body img {
|
||||
max-width: 100%;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #344054;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.user-guide-article-body hr {
|
||||
border: none;
|
||||
border-top: 1px solid #344054;
|
||||
margin: 32px 0;
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Rocket,
|
||||
Globe,
|
||||
Gauge,
|
||||
Link,
|
||||
Bell,
|
||||
AlertTriangle,
|
||||
Settings,
|
||||
FileText,
|
||||
} from "lucide-react";
|
||||
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
|
||||
import "./ArticlePage.css";
|
||||
|
||||
// Map icon names to actual Lucide components
|
||||
const iconMap = {
|
||||
Rocket,
|
||||
Globe,
|
||||
Gauge,
|
||||
Link,
|
||||
Bell,
|
||||
AlertTriangle,
|
||||
Settings,
|
||||
FileText,
|
||||
};
|
||||
|
||||
const ArticlePage = ({
|
||||
collection,
|
||||
article,
|
||||
onBack,
|
||||
onBackToHome,
|
||||
onNextArticle,
|
||||
onPrevArticle,
|
||||
nextArticle,
|
||||
prevArticle,
|
||||
tocItems,
|
||||
children,
|
||||
mode = "in-app",
|
||||
}) => {
|
||||
const { colors, typography, spacing, border, isDark } = useUserGuideTheme();
|
||||
const IconComponent = iconMap[collection.icon] || Rocket;
|
||||
const isInApp = mode === "in-app";
|
||||
const [activeSection, setActiveSection] = useState("");
|
||||
|
||||
// Track active section on scroll
|
||||
useEffect(() => {
|
||||
if (!tocItems || tocItems.length === 0) return;
|
||||
|
||||
const handleScroll = () => {
|
||||
const sections = tocItems
|
||||
.map((item) => {
|
||||
const id = item.href.replace("#", "");
|
||||
const element = document.getElementById(id);
|
||||
return { id: item.href, element };
|
||||
})
|
||||
.filter((s) => s.element);
|
||||
|
||||
if (sections.length === 0) return;
|
||||
|
||||
let currentSection = sections[0].id;
|
||||
|
||||
for (const section of sections) {
|
||||
if (section.element) {
|
||||
const rect = section.element.getBoundingClientRect();
|
||||
if (rect.top <= 150) {
|
||||
currentSection = section.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setActiveSection(currentSection);
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
document.addEventListener("scroll", handleScroll, { passive: true, capture: true });
|
||||
handleScroll();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
document.removeEventListener("scroll", handleScroll, { capture: true });
|
||||
};
|
||||
}, [tocItems]);
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: "100%", backgroundColor: colors.background.alt }}>
|
||||
{/* Main content area */}
|
||||
<div
|
||||
style={{
|
||||
maxWidth: isInApp ? "100%" : 1100,
|
||||
margin: "0 auto",
|
||||
padding: isInApp ? spacing.lg : spacing["3xl"],
|
||||
}}
|
||||
>
|
||||
{/* Article Content */}
|
||||
<div style={{ maxWidth: isInApp ? "100%" : 680, minWidth: 0 }}>
|
||||
{/* Title */}
|
||||
<h1
|
||||
style={{
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize["3xl"],
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.text.primary,
|
||||
marginBottom: spacing.lg,
|
||||
lineHeight: typography.lineHeight.snug,
|
||||
marginTop: 0,
|
||||
}}
|
||||
>
|
||||
{article.title}
|
||||
</h1>
|
||||
|
||||
{/* Description */}
|
||||
<p
|
||||
style={{
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.md,
|
||||
color: colors.text.secondary,
|
||||
marginBottom: spacing.lg,
|
||||
lineHeight: typography.lineHeight.normal,
|
||||
marginTop: 0,
|
||||
}}
|
||||
>
|
||||
{article.description}
|
||||
</p>
|
||||
|
||||
{/* Table of Contents - at top */}
|
||||
{tocItems && tocItems.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
marginBottom: spacing["2xl"],
|
||||
paddingBottom: isInApp ? 0 : spacing.lg,
|
||||
borderBottom: isInApp ? "none" : border.default,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: isInApp
|
||||
? isDark
|
||||
? "rgba(255, 255, 255, 0.05)"
|
||||
: "rgba(0, 0, 0, 0.02)"
|
||||
: "transparent",
|
||||
borderRadius: isInApp ? border.radius : 0,
|
||||
padding: isInApp ? spacing.md : 0,
|
||||
border: isInApp ? border.default : "none",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
display: "block",
|
||||
fontSize: 12,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.text.secondary,
|
||||
marginBottom: spacing.sm,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.5px",
|
||||
}}
|
||||
>
|
||||
In this article
|
||||
</span>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: isInApp ? "column" : "row",
|
||||
flexWrap: isInApp ? "nowrap" : "wrap",
|
||||
gap: isInApp ? 0 : spacing.xs,
|
||||
columnGap: isInApp ? 0 : spacing.lg,
|
||||
}}
|
||||
>
|
||||
{tocItems.map((item) => {
|
||||
const isActive = activeSection === item.href;
|
||||
return (
|
||||
<a
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="user-guide-toc-link"
|
||||
onClick={(e) => {
|
||||
if (isInApp) {
|
||||
e.preventDefault();
|
||||
const id = item.href.replace("#", "");
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: isInApp ? 13 : 12,
|
||||
color: isActive ? colors.brand.primary : colors.text.secondary,
|
||||
fontWeight: isActive
|
||||
? typography.fontWeight.medium
|
||||
: typography.fontWeight.normal,
|
||||
textDecoration: "none",
|
||||
padding: isInApp ? "2px 0" : `${spacing.xs} 0`,
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Article Body */}
|
||||
<div className="user-guide-article-body">
|
||||
{children || (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: colors.background.white,
|
||||
border: border.default,
|
||||
borderRadius: border.radius,
|
||||
padding: spacing.xl,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{ fontSize: typography.fontSize.sm, color: colors.text.muted }}
|
||||
>
|
||||
Article content coming soon.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
{(prevArticle || nextArticle) && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginTop: spacing["2xl"],
|
||||
paddingTop: spacing.xl,
|
||||
borderTop: border.default,
|
||||
}}
|
||||
>
|
||||
{prevArticle ? (
|
||||
<div
|
||||
onClick={onPrevArticle}
|
||||
className="user-guide-nav-button"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: spacing.sm,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<ChevronLeft
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
color={colors.text.muted}
|
||||
/>
|
||||
<div>
|
||||
<span
|
||||
style={{
|
||||
display: "block",
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.text.muted,
|
||||
}}
|
||||
>
|
||||
Previous
|
||||
</span>
|
||||
<span
|
||||
className="nav-text"
|
||||
style={{
|
||||
display: "block",
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.text.primary,
|
||||
fontWeight: typography.fontWeight.medium,
|
||||
}}
|
||||
>
|
||||
{prevArticle.title}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
{nextArticle && (
|
||||
<div
|
||||
onClick={onNextArticle}
|
||||
className="user-guide-nav-button"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: spacing.sm,
|
||||
textAlign: "right",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
style={{
|
||||
display: "block",
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.text.muted,
|
||||
}}
|
||||
>
|
||||
Next
|
||||
</span>
|
||||
<span
|
||||
className="nav-text"
|
||||
style={{
|
||||
display: "block",
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.text.primary,
|
||||
fontWeight: typography.fontWeight.medium,
|
||||
}}
|
||||
>
|
||||
{nextArticle.title}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronRight
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
color={colors.text.muted}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ArticlePage;
|
||||
@@ -0,0 +1,31 @@
|
||||
/* CollectionPage styles - hover states */
|
||||
|
||||
/* Back link hover */
|
||||
.user-guide-back-link {
|
||||
transition: color 150ms ease;
|
||||
}
|
||||
|
||||
.user-guide-back-link:hover {
|
||||
color: #1570ef;
|
||||
}
|
||||
|
||||
/* Article row hover */
|
||||
.user-guide-article-row {
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
.user-guide-article-row:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
[data-mui-color-scheme="dark"] .user-guide-article-row:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.user-guide-article-row .arrow-icon {
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
.user-guide-article-row:hover .arrow-icon {
|
||||
color: #1570ef;
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import {
|
||||
ArrowRight,
|
||||
FileText,
|
||||
Rocket,
|
||||
Globe,
|
||||
Gauge,
|
||||
Link,
|
||||
Bell,
|
||||
AlertTriangle,
|
||||
Settings,
|
||||
} from "lucide-react";
|
||||
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
|
||||
import "./CollectionPage.css";
|
||||
|
||||
// Map icon names to actual Lucide components
|
||||
const iconMap = {
|
||||
Rocket,
|
||||
Globe,
|
||||
Gauge,
|
||||
Link,
|
||||
Bell,
|
||||
AlertTriangle,
|
||||
Settings,
|
||||
FileText,
|
||||
};
|
||||
|
||||
const CollectionPage = ({ collection, onBack, onArticleClick }) => {
|
||||
const { colors, typography, spacing, border } = useUserGuideTheme();
|
||||
const IconComponent = iconMap[collection.icon] || Rocket;
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: "100%", backgroundColor: colors.background.alt }}>
|
||||
{/* Header */}
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: colors.background.white,
|
||||
borderBottom: border.default,
|
||||
padding: `${spacing.lg} ${spacing.lg}`,
|
||||
}}
|
||||
>
|
||||
<div style={{ maxWidth: 900, margin: "0 auto" }}>
|
||||
{/* Collection Header */}
|
||||
<div style={{ display: "flex", alignItems: "flex-start", gap: spacing.md }}>
|
||||
<IconComponent
|
||||
size={24}
|
||||
strokeWidth={1.5}
|
||||
color={colors.brand.primary}
|
||||
style={{ marginTop: 4 }}
|
||||
/>
|
||||
<div>
|
||||
<h1
|
||||
style={{
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize["2xl"],
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.text.primary,
|
||||
marginBottom: spacing.xs,
|
||||
marginTop: 0,
|
||||
}}
|
||||
>
|
||||
{collection.title}
|
||||
</h1>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.text.secondary,
|
||||
lineHeight: typography.lineHeight.normal,
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{collection.description}
|
||||
</p>
|
||||
<span
|
||||
style={{
|
||||
display: "block",
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.text.muted,
|
||||
marginTop: spacing.sm,
|
||||
}}
|
||||
>
|
||||
{collection.articleCount} articles in this collection
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Articles List */}
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 900,
|
||||
margin: "0 auto",
|
||||
padding: `${spacing.lg} ${spacing.lg}`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: colors.background.white,
|
||||
border: border.default,
|
||||
borderRadius: border.radius,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{collection.articles.map((article, index) => (
|
||||
<div
|
||||
key={article.id}
|
||||
onClick={() => onArticleClick(article.id)}
|
||||
className="user-guide-article-row"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: spacing.lg,
|
||||
borderBottom:
|
||||
index < collection.articles.length - 1 ? border.default : "none",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "flex-start", gap: spacing.md }}>
|
||||
<FileText
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
color={colors.text.muted}
|
||||
style={{ marginTop: 2, flexShrink: 0 }}
|
||||
/>
|
||||
<div>
|
||||
<span
|
||||
style={{
|
||||
display: "block",
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: typography.fontWeight.medium,
|
||||
color: colors.text.primary,
|
||||
marginBottom: spacing.xs,
|
||||
}}
|
||||
>
|
||||
{article.title}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
display: "block",
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.text.secondary,
|
||||
lineHeight: typography.lineHeight.normal,
|
||||
}}
|
||||
>
|
||||
{article.description}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight
|
||||
className="arrow-icon"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
color={colors.text.muted}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
marginLeft: 16,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionPage;
|
||||
@@ -0,0 +1,45 @@
|
||||
/* ContentRenderer styles - hover states and list styling */
|
||||
|
||||
/* List styling - colors handled by inline styles for theme support */
|
||||
.user-guide-content-list li {
|
||||
/* Font styles handled inline for theme-aware colors */
|
||||
}
|
||||
|
||||
/* Table row hover */
|
||||
.user-guide-table-row {
|
||||
transition: background-color 150ms ease;
|
||||
}
|
||||
|
||||
.user-guide-table-row:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04) !important;
|
||||
}
|
||||
|
||||
[data-mui-color-scheme="dark"] .user-guide-table-row:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05) !important;
|
||||
}
|
||||
|
||||
/* Article links hover */
|
||||
.user-guide-article-link-item {
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
.user-guide-article-link-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
[data-mui-color-scheme="dark"] .user-guide-article-link-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.user-guide-article-link-item:hover .link-title {
|
||||
color: #1570ef;
|
||||
}
|
||||
|
||||
.user-guide-article-link-item .arrow-icon {
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
.user-guide-article-link-item:hover .arrow-icon {
|
||||
transform: translateX(4px);
|
||||
color: #1570ef;
|
||||
}
|
||||
@@ -0,0 +1,925 @@
|
||||
import { useState } from "react";
|
||||
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
|
||||
import ImageLightbox from "./ImageLightbox";
|
||||
import {
|
||||
CheckCircle,
|
||||
Clock,
|
||||
Rocket,
|
||||
Shield,
|
||||
Brain,
|
||||
AlertTriangle,
|
||||
Server,
|
||||
Database,
|
||||
Terminal,
|
||||
FileText,
|
||||
FolderKanban,
|
||||
Gauge,
|
||||
LayoutDashboard,
|
||||
Menu,
|
||||
Settings,
|
||||
Info,
|
||||
Lightbulb,
|
||||
ArrowRight,
|
||||
Globe,
|
||||
Bell,
|
||||
Link,
|
||||
} from "lucide-react";
|
||||
import "./ContentRenderer.css";
|
||||
|
||||
// Icon mapping
|
||||
const iconMap = {
|
||||
CheckCircle,
|
||||
Clock,
|
||||
Rocket,
|
||||
Shield,
|
||||
Brain,
|
||||
AlertTriangle,
|
||||
Server,
|
||||
Database,
|
||||
Terminal,
|
||||
FileText,
|
||||
FolderKanban,
|
||||
Gauge,
|
||||
LayoutDashboard,
|
||||
Menu,
|
||||
Settings,
|
||||
Info,
|
||||
Lightbulb,
|
||||
Globe,
|
||||
Bell,
|
||||
Link,
|
||||
};
|
||||
|
||||
const getIcon = (name) => iconMap[name] || Info;
|
||||
|
||||
const ContentRenderer = ({ content, onNavigate }) => {
|
||||
const { colors, typography, spacing, border, chipStyles, imageStyles, isDark } =
|
||||
useUserGuideTheme();
|
||||
const [lightboxImage, setLightboxImage] = useState(null);
|
||||
|
||||
// Parse basic markdown in text (inside component to access onNavigate)
|
||||
const parseMarkdown = (text) => {
|
||||
const parts = [];
|
||||
let key = 0;
|
||||
|
||||
// Supports: **bold**, `code`, [[link text]](collection/article)
|
||||
const combinedRegex = /\*\*([^*]+)\*\*|`([^`]+)`|\[\[([^\]]+)\]\]\(([^)]+)\)/g;
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
|
||||
const getChipStyle = (codeContent) => {
|
||||
const isUrl = /^https?:\/\//.test(codeContent);
|
||||
return isUrl ? chipStyles.url : chipStyles.code;
|
||||
};
|
||||
|
||||
while ((match = combinedRegex.exec(text)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
parts.push(text.slice(lastIndex, match.index));
|
||||
}
|
||||
|
||||
if (match[1]) {
|
||||
// Bold: **text**
|
||||
parts.push(<strong key={key++}>{match[1]}</strong>);
|
||||
} else if (match[2]) {
|
||||
// Code: `text`
|
||||
const style = getChipStyle(match[2]);
|
||||
parts.push(
|
||||
<span
|
||||
key={key++}
|
||||
style={style}
|
||||
>
|
||||
{match[2]}
|
||||
</span>
|
||||
);
|
||||
} else if (match[3] && match[4]) {
|
||||
// Article link: [[text]](collection/article)
|
||||
const linkText = match[3];
|
||||
const linkPath = match[4];
|
||||
const [collectionId, articleId] = linkPath.split("/");
|
||||
parts.push(
|
||||
<span
|
||||
key={key++}
|
||||
onClick={() => onNavigate?.(collectionId, articleId)}
|
||||
style={{
|
||||
color: colors.brand.primary,
|
||||
cursor: "pointer",
|
||||
textDecoration: "underline",
|
||||
textDecorationColor: "transparent",
|
||||
transition: "text-decoration-color 150ms ease",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.textDecorationColor = colors.brand.primary;
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.textDecorationColor = "transparent";
|
||||
}}
|
||||
>
|
||||
{linkText}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
|
||||
if (lastIndex < text.length) {
|
||||
parts.push(text.slice(lastIndex));
|
||||
}
|
||||
|
||||
return parts.length > 0 ? parts : text;
|
||||
};
|
||||
|
||||
const renderBlock = (block, index) => {
|
||||
switch (block.type) {
|
||||
case "heading":
|
||||
return renderHeading(block, index);
|
||||
case "paragraph":
|
||||
return renderParagraph(block, index);
|
||||
case "callout":
|
||||
return renderCallout(block, index);
|
||||
case "bullet-list":
|
||||
return renderBulletList(block, index);
|
||||
case "ordered-list":
|
||||
return renderOrderedList(block, index);
|
||||
case "checklist":
|
||||
return renderChecklist(block, index);
|
||||
case "icon-cards":
|
||||
return renderIconCards(block, index);
|
||||
case "grid-cards":
|
||||
return renderGridCards(block, index);
|
||||
case "time-estimate":
|
||||
return renderTimeEstimate(block, index);
|
||||
case "info-box":
|
||||
return renderInfoBox(block, index);
|
||||
case "code":
|
||||
return renderCode(block, index);
|
||||
case "requirements":
|
||||
return renderRequirements(block, index);
|
||||
case "table":
|
||||
return renderTable(block, index);
|
||||
case "image":
|
||||
return renderImage(block, index);
|
||||
case "article-links":
|
||||
return renderArticleLinks(block, index);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const renderHeading = (block, index) => {
|
||||
const Tag = block.level === 2 ? "h2" : "h3";
|
||||
return (
|
||||
<Tag
|
||||
key={index}
|
||||
id={block.id}
|
||||
style={{
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize:
|
||||
block.level === 2 ? typography.fontSize["2xl"] : typography.fontSize.lg,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.text.primary,
|
||||
marginBottom: spacing.lg,
|
||||
marginTop: index > 0 ? spacing["2xl"] : 0,
|
||||
lineHeight: typography.lineHeight.normal,
|
||||
}}
|
||||
>
|
||||
{block.text}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
const renderParagraph = (block, index) => (
|
||||
<p
|
||||
key={index}
|
||||
style={{
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.text.secondary,
|
||||
lineHeight: typography.lineHeight.relaxed,
|
||||
marginBottom: spacing.lg,
|
||||
marginTop: 0,
|
||||
}}
|
||||
>
|
||||
{parseMarkdown(block.text)}
|
||||
</p>
|
||||
);
|
||||
|
||||
const renderCallout = (block, index) => {
|
||||
const calloutColors = {
|
||||
info: colors.brand.primary,
|
||||
tip: colors.brand.primary,
|
||||
warning: colors.brand.warning,
|
||||
success: colors.brand.success,
|
||||
};
|
||||
const accentColor = calloutColors[block.variant || "info"];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
border: border.default,
|
||||
borderLeftWidth: "3px",
|
||||
borderLeftColor: accentColor,
|
||||
backgroundColor: isDark ? "rgba(255, 255, 255, 0.03)" : "rgba(0, 0, 0, 0.02)",
|
||||
paddingLeft: spacing.lg,
|
||||
paddingTop: spacing.md,
|
||||
paddingBottom: spacing.md,
|
||||
paddingRight: spacing.lg,
|
||||
borderRadius: border.radius,
|
||||
marginBottom: spacing["2xl"],
|
||||
}}
|
||||
>
|
||||
{block.title && (
|
||||
<span
|
||||
style={{
|
||||
display: "block",
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.text.primary,
|
||||
marginBottom: spacing.xs,
|
||||
}}
|
||||
>
|
||||
{block.title}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
style={{
|
||||
display: "block",
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.text.secondary,
|
||||
lineHeight: typography.lineHeight.normal,
|
||||
}}
|
||||
>
|
||||
{parseMarkdown(block.text)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderBulletList = (block, index) => (
|
||||
<ul
|
||||
key={index}
|
||||
className="user-guide-content-list"
|
||||
style={{
|
||||
paddingLeft: spacing.xl,
|
||||
marginBottom: spacing["2xl"],
|
||||
marginTop: 0,
|
||||
}}
|
||||
>
|
||||
{block.items.map((item, i) => (
|
||||
<li
|
||||
key={i}
|
||||
style={{
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.text.secondary,
|
||||
lineHeight: typography.lineHeight.normal,
|
||||
marginBottom: spacing.sm,
|
||||
}}
|
||||
>
|
||||
{item.bold && (
|
||||
<strong style={{ color: colors.text.primary }}>{item.bold}</strong>
|
||||
)}
|
||||
{item.bold && <span style={{ color: colors.text.secondary }}> — </span>}
|
||||
<span style={{ color: colors.text.secondary }}>
|
||||
{item.bold ? parseMarkdown(item.text) : parseMarkdown(item.text)}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
||||
const renderOrderedList = (block, index) => (
|
||||
<ol
|
||||
key={index}
|
||||
className="user-guide-content-list"
|
||||
style={{
|
||||
paddingLeft: spacing.xl,
|
||||
marginBottom: spacing.lg,
|
||||
marginTop: 0,
|
||||
}}
|
||||
>
|
||||
{block.items.map((item, i) => (
|
||||
<li
|
||||
key={i}
|
||||
style={{
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.text.secondary,
|
||||
lineHeight: typography.lineHeight.normal,
|
||||
marginBottom: spacing.sm,
|
||||
}}
|
||||
>
|
||||
{item.bold && (
|
||||
<strong style={{ color: colors.text.primary }}>{item.bold}</strong>
|
||||
)}
|
||||
{item.bold && " "}
|
||||
<span style={{ color: colors.text.secondary }}>
|
||||
{item.bold ? parseMarkdown(item.text) : parseMarkdown(item.text)}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
|
||||
const renderChecklist = (block, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{ marginBottom: spacing["2xl"], paddingLeft: spacing.sm }}
|
||||
>
|
||||
{block.items.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
gap: "10px",
|
||||
marginBottom: spacing.sm,
|
||||
}}
|
||||
>
|
||||
<CheckCircle
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
color={colors.brand.primary}
|
||||
style={{ flexShrink: 0, marginTop: 3 }}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.text.secondary,
|
||||
lineHeight: typography.lineHeight.tight,
|
||||
}}
|
||||
>
|
||||
{parseMarkdown(item)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderIconCards = (block, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: spacing.md,
|
||||
marginBottom: spacing["2xl"],
|
||||
}}
|
||||
>
|
||||
{block.items.map((item, i) => {
|
||||
const IconComponent = getIcon(item.icon);
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: spacing.md,
|
||||
padding: spacing.sm,
|
||||
backgroundColor: colors.background.white,
|
||||
border: border.default,
|
||||
borderRadius: border.radius,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
width: "20px",
|
||||
height: "22px",
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
size={20}
|
||||
strokeWidth={1.5}
|
||||
color={colors.brand.primary}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
style={{
|
||||
display: "block",
|
||||
fontSize: typography.fontSize.md,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.text.primary,
|
||||
lineHeight: "22px",
|
||||
marginBottom: "2px",
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
display: "block",
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.text.secondary,
|
||||
lineHeight: typography.lineHeight.normal,
|
||||
}}
|
||||
>
|
||||
{parseMarkdown(item.description)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderGridCards = (block, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(auto-fit, minmax(${block.columns === 2 ? "250px" : "200px"}, 1fr))`,
|
||||
gap: spacing.md,
|
||||
marginBottom: spacing["2xl"],
|
||||
}}
|
||||
>
|
||||
{block.items.map((item, i) => {
|
||||
const IconComponent = item.icon ? getIcon(item.icon) : null;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
padding: "14px",
|
||||
backgroundColor: colors.background.white,
|
||||
border: border.default,
|
||||
borderRadius: border.radius,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
gap: spacing.sm,
|
||||
marginBottom: "6px",
|
||||
}}
|
||||
>
|
||||
{IconComponent && (
|
||||
<span
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
width: "16px",
|
||||
height: "18px",
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
color={colors.brand.primary}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
style={{
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.text.primary,
|
||||
lineHeight: "18px",
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
style={{
|
||||
display: "block",
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.text.secondary,
|
||||
lineHeight: typography.lineHeight.snug,
|
||||
}}
|
||||
>
|
||||
{parseMarkdown(item.description)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderTimeEstimate = (block, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: spacing.sm,
|
||||
padding: `${spacing.md} ${spacing.lg}`,
|
||||
backgroundColor: isDark ? "rgba(255, 255, 255, 0.05)" : "rgba(0, 0, 0, 0.03)",
|
||||
border: border.default,
|
||||
borderRadius: border.radius,
|
||||
marginBottom: spacing.xl,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
height: "20px",
|
||||
}}
|
||||
>
|
||||
<Clock
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
color={colors.brand.primary}
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.text.primary,
|
||||
lineHeight: "20px",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{parseMarkdown(block.text)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderInfoBox = (block, index) => {
|
||||
const IconComponent = block.icon ? getIcon(block.icon) : FolderKanban;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: spacing.md,
|
||||
padding: spacing.lg,
|
||||
backgroundColor: colors.background.white,
|
||||
border: border.default,
|
||||
borderRadius: border.radius,
|
||||
marginBottom: spacing.lg,
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
size={20}
|
||||
strokeWidth={1.5}
|
||||
color={colors.brand.primary}
|
||||
style={{ flexShrink: 0, marginTop: 2 }}
|
||||
/>
|
||||
<div>
|
||||
<span
|
||||
style={{
|
||||
display: "block",
|
||||
fontSize: typography.fontSize.md,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.text.primary,
|
||||
marginBottom: spacing.sm,
|
||||
}}
|
||||
>
|
||||
{block.title}
|
||||
</span>
|
||||
<ul style={{ margin: 0, paddingLeft: "20px" }}>
|
||||
{block.items.map((item, i) => (
|
||||
<li
|
||||
key={i}
|
||||
style={{
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.text.secondary,
|
||||
lineHeight: typography.lineHeight.normal,
|
||||
marginBottom: spacing.xs,
|
||||
}}
|
||||
>
|
||||
{parseMarkdown(item)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderCode = (block, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
backgroundColor: colors.background.code,
|
||||
borderRadius: border.radius,
|
||||
padding: "14px 16px",
|
||||
overflow: "auto",
|
||||
marginBottom: spacing.lg,
|
||||
}}
|
||||
>
|
||||
<pre
|
||||
style={{
|
||||
fontSize: typography.fontSize.sm,
|
||||
fontFamily: typography.fontFamily.mono,
|
||||
color: "#E5E7EB",
|
||||
margin: 0,
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-all",
|
||||
}}
|
||||
>
|
||||
{block.code}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderRequirements = (block, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: spacing.md,
|
||||
marginBottom: spacing["2xl"],
|
||||
}}
|
||||
>
|
||||
{block.items.map((req, i) => {
|
||||
const IconComponent = getIcon(req.icon);
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
padding: spacing.lg,
|
||||
backgroundColor: colors.background.white,
|
||||
border: border.default,
|
||||
borderRadius: border.radius,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: spacing.sm,
|
||||
marginBottom: spacing.md,
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
color={colors.brand.primary}
|
||||
style={{ flexShrink: 0 }}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.text.primary,
|
||||
}}
|
||||
>
|
||||
{req.title}
|
||||
</span>
|
||||
</div>
|
||||
<ul style={{ margin: 0, paddingLeft: "20px" }}>
|
||||
{req.items.map((item, j) => (
|
||||
<li
|
||||
key={j}
|
||||
style={{
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.text.secondary,
|
||||
lineHeight: typography.lineHeight.normal,
|
||||
marginBottom: spacing.xs,
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderTableCell = (value) => {
|
||||
if (value === "✓") {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "rgba(19, 113, 91, 0.2)",
|
||||
color: colors.brand.primary,
|
||||
fontSize: typography.fontSize.sm,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
}}
|
||||
>
|
||||
✓
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (value === "✗" || value === "—") {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "rgba(102, 112, 133, 0.2)",
|
||||
color: colors.text.muted,
|
||||
fontSize: typography.fontSize.sm,
|
||||
}}
|
||||
>
|
||||
—
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return parseMarkdown(value);
|
||||
};
|
||||
|
||||
const renderTable = (block, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
border: border.default,
|
||||
borderRadius: border.radius,
|
||||
overflow: "hidden",
|
||||
marginBottom: spacing["2xl"],
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: block.columns.map((col) => col.width || "1fr").join(" "),
|
||||
backgroundColor: isDark ? "rgba(255, 255, 255, 0.05)" : "rgba(0, 0, 0, 0.03)",
|
||||
borderBottom: border.default,
|
||||
padding: `${spacing.md} ${spacing.lg}`,
|
||||
gap: spacing.md,
|
||||
}}
|
||||
>
|
||||
{block.columns.map((col) => (
|
||||
<span
|
||||
key={col.key}
|
||||
style={{
|
||||
fontSize: typography.fontSize.sm,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.text.primary,
|
||||
lineHeight: typography.lineHeight.tight,
|
||||
}}
|
||||
>
|
||||
{col.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{/* Rows */}
|
||||
{block.rows.map((row, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="user-guide-table-row"
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: block.columns.map((col) => col.width || "1fr").join(" "),
|
||||
borderBottom: i < block.rows.length - 1 ? border.default : "none",
|
||||
backgroundColor:
|
||||
i % 2 === 1
|
||||
? isDark
|
||||
? "rgba(255, 255, 255, 0.02)"
|
||||
: "rgba(0, 0, 0, 0.02)"
|
||||
: "transparent",
|
||||
padding: `10px ${spacing.lg}`,
|
||||
gap: spacing.md,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{block.columns.map((col, j) => (
|
||||
<div
|
||||
key={col.key}
|
||||
style={{
|
||||
fontSize: typography.fontSize.sm,
|
||||
fontWeight:
|
||||
j === 0 ? typography.fontWeight.medium : typography.fontWeight.normal,
|
||||
color: j === 0 ? colors.text.primary : colors.text.secondary,
|
||||
lineHeight: typography.lineHeight.normal,
|
||||
}}
|
||||
>
|
||||
{renderTableCell(row[col.key])}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderImage = (block, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
marginBottom: spacing["2xl"],
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={block.src}
|
||||
alt={block.alt}
|
||||
onClick={() =>
|
||||
setLightboxImage({ src: block.src, alt: block.alt, caption: block.caption })
|
||||
}
|
||||
style={{
|
||||
...imageStyles.image,
|
||||
cursor: "zoom-in",
|
||||
}}
|
||||
/>
|
||||
{block.caption && (
|
||||
<p
|
||||
style={{
|
||||
...imageStyles.caption,
|
||||
marginTop: spacing.sm,
|
||||
marginBottom: 0,
|
||||
}}
|
||||
>
|
||||
{block.caption}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderArticleLinks = (block, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{ marginBottom: spacing["2xl"] }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: "1px",
|
||||
backgroundColor: colors.border.default,
|
||||
marginBottom: spacing.xl,
|
||||
}}
|
||||
/>
|
||||
{block.title && (
|
||||
<span
|
||||
style={{
|
||||
display: "block",
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.lg,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.text.primary,
|
||||
marginBottom: spacing.md,
|
||||
}}
|
||||
>
|
||||
{block.title}
|
||||
</span>
|
||||
)}
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
{block.items.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
onClick={() => onNavigate?.(item.collectionId, item.articleId)}
|
||||
className="user-guide-article-link-item"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: spacing.sm,
|
||||
margin: `0 -${spacing.sm}`,
|
||||
borderRadius: "4px",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="link-title"
|
||||
style={{
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: typography.fontWeight.medium,
|
||||
color: colors.text.primary,
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</span>
|
||||
<ArrowRight
|
||||
className="arrow-icon"
|
||||
size={18}
|
||||
strokeWidth={1.5}
|
||||
color={colors.text.muted}
|
||||
style={{ flexShrink: 0, marginLeft: 16 }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>{content.blocks.map((block, index) => renderBlock(block, index))}</div>
|
||||
|
||||
{/* Image Lightbox */}
|
||||
{lightboxImage && (
|
||||
<ImageLightbox
|
||||
src={lightboxImage.src}
|
||||
alt={lightboxImage.alt}
|
||||
caption={lightboxImage.caption}
|
||||
onClose={() => setLightboxImage(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContentRenderer;
|
||||
@@ -0,0 +1,14 @@
|
||||
/* HelpSection styles */
|
||||
|
||||
.user-guide-help-button {
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
.user-guide-help-button:hover {
|
||||
border-color: #1570ef;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
[data-mui-color-scheme="dark"] .user-guide-help-button:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
|
||||
import "./HelpSection.css";
|
||||
|
||||
const HelpSection = () => {
|
||||
const { colors, typography, spacing, border } = useUserGuideTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: spacing.xl,
|
||||
minHeight: "100%",
|
||||
}}
|
||||
>
|
||||
{/* Contact us */}
|
||||
<div style={{ marginBottom: spacing["2xl"] }}>
|
||||
<h2
|
||||
style={{
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.lg,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.text.primary,
|
||||
marginBottom: spacing.sm,
|
||||
marginTop: 0,
|
||||
}}
|
||||
>
|
||||
Contact us
|
||||
</h2>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.text.secondary,
|
||||
lineHeight: typography.lineHeight.normal,
|
||||
margin: 0,
|
||||
marginBottom: spacing.md,
|
||||
}}
|
||||
>
|
||||
Can't find what you need? Our support team is here to help.
|
||||
</p>
|
||||
<a
|
||||
href="https://discord.com/invite/NAb6H3UTjK"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="user-guide-help-button"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: spacing.sm,
|
||||
width: "100%",
|
||||
padding: `${spacing.md} ${spacing.lg}`,
|
||||
backgroundColor: colors.background.white,
|
||||
border: border.default,
|
||||
borderRadius: border.radius,
|
||||
textDecoration: "none",
|
||||
cursor: "pointer",
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: typography.fontWeight.medium,
|
||||
color: colors.text.primary,
|
||||
}}
|
||||
>
|
||||
Join our Discord
|
||||
<ExternalLink
|
||||
size={14}
|
||||
strokeWidth={1.5}
|
||||
color={colors.text.muted}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Ask the community */}
|
||||
<div>
|
||||
<h2
|
||||
style={{
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.lg,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.text.primary,
|
||||
marginBottom: spacing.sm,
|
||||
marginTop: 0,
|
||||
}}
|
||||
>
|
||||
GitHub discussions
|
||||
</h2>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.text.secondary,
|
||||
lineHeight: typography.lineHeight.normal,
|
||||
margin: 0,
|
||||
marginBottom: spacing.md,
|
||||
}}
|
||||
>
|
||||
Questions about features, how-tos, or use cases? Join the community discussion.
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/bluewave-labs/checkmate/discussions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="user-guide-help-button"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: spacing.sm,
|
||||
width: "100%",
|
||||
padding: `${spacing.md} ${spacing.lg}`,
|
||||
backgroundColor: colors.background.white,
|
||||
border: border.default,
|
||||
borderRadius: border.radius,
|
||||
textDecoration: "none",
|
||||
cursor: "pointer",
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: typography.fontWeight.medium,
|
||||
color: colors.text.primary,
|
||||
}}
|
||||
>
|
||||
GitHub discussions
|
||||
<ExternalLink
|
||||
size={14}
|
||||
strokeWidth={1.5}
|
||||
color={colors.text.muted}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HelpSection;
|
||||
@@ -0,0 +1,125 @@
|
||||
import { useEffect, useCallback } from "react";
|
||||
import { X } from "lucide-react";
|
||||
import { typography, spacing } from "./styles/theme";
|
||||
|
||||
const ImageLightbox = ({ src, alt, caption, onClose }) => {
|
||||
// Close on escape key
|
||||
const handleKeyDown = useCallback(
|
||||
(e) => {
|
||||
if (e.key === "Escape") {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[onClose]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
document.body.style.overflow = "hidden";
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
}, [handleKeyDown]);
|
||||
|
||||
// Close when clicking backdrop
|
||||
const handleBackdropClick = (e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={handleBackdropClick}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.9)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
zIndex: 2000,
|
||||
padding: spacing.xl,
|
||||
cursor: "zoom-out",
|
||||
}}
|
||||
>
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 16,
|
||||
right: 16,
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
transition: "background-color 150ms ease",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = "rgba(255, 255, 255, 0.2)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = "rgba(255, 255, 255, 0.1)";
|
||||
}}
|
||||
title="Close (Esc)"
|
||||
>
|
||||
<X
|
||||
size={24}
|
||||
strokeWidth={1.5}
|
||||
color="#fff"
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Image container */}
|
||||
<div
|
||||
style={{
|
||||
maxWidth: "90vw",
|
||||
maxHeight: "90vh",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: caption ? "calc(90vh - 40px)" : "90vh",
|
||||
objectFit: "contain",
|
||||
borderRadius: "4px",
|
||||
boxShadow: "0 8px 32px rgba(0, 0, 0, 0.3)",
|
||||
cursor: "default",
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
{caption && (
|
||||
<p
|
||||
style={{
|
||||
marginTop: spacing.md,
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: "rgba(255, 255, 255, 0.8)",
|
||||
textAlign: "center",
|
||||
fontStyle: "italic",
|
||||
}}
|
||||
>
|
||||
{caption}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageLightbox;
|
||||
@@ -0,0 +1,638 @@
|
||||
# Checkmate User Guide Style Guide
|
||||
|
||||
This document defines the writing standards, formatting rules, and content patterns for all Checkmate user documentation.
|
||||
|
||||
---
|
||||
|
||||
## Voice & Tone
|
||||
|
||||
### Core Principles
|
||||
|
||||
1. **Second person, active voice** - Address the reader directly
|
||||
|
||||
- ✅ "You can configure monitors from the dashboard"
|
||||
- ❌ "Monitors can be configured from the dashboard"
|
||||
|
||||
2. **Professional but approachable** - Clear and helpful, not stiff or overly casual
|
||||
|
||||
- ✅ "Select the notification channel you want to use"
|
||||
- ❌ "Go ahead and pick whatever notification channel works for you!"
|
||||
- ❌ "The user shall select the appropriate notification channel"
|
||||
|
||||
3. **Direct and instructional** - Tell users what to do, not what the system does
|
||||
|
||||
- ✅ "Click Save to apply your changes"
|
||||
- ❌ "The Save button will apply your changes"
|
||||
|
||||
4. **User-focused** - Lead with what users can accomplish
|
||||
- ✅ "You can track server performance with infrastructure monitors"
|
||||
- ❌ "Infrastructure monitors use the Capture agent to collect metrics"
|
||||
|
||||
### Language Patterns
|
||||
|
||||
**Opening phrases:**
|
||||
|
||||
- "The [Feature] allows you to..."
|
||||
- "[Feature] is..."
|
||||
- "You can..."
|
||||
- "To [action]:"
|
||||
|
||||
**Navigation instructions:**
|
||||
|
||||
- "Navigate to [location]"
|
||||
- "Click [element]"
|
||||
- "Select [option]"
|
||||
- "From the [page], click..."
|
||||
|
||||
**Avoid:**
|
||||
|
||||
- Passive voice where active is clearer
|
||||
- Jargon without explanation
|
||||
- Assuming prior knowledge
|
||||
- Marketing language ("powerful", "seamless", "robust")
|
||||
|
||||
---
|
||||
|
||||
## Content Structure
|
||||
|
||||
### Standard Article Pattern
|
||||
|
||||
Every article should follow this flow:
|
||||
|
||||
```
|
||||
1. OVERVIEW (H2)
|
||||
- 1-2 paragraphs explaining what and why
|
||||
- Set context for the topic
|
||||
|
||||
2. VISUAL (Optional)
|
||||
- Screenshot showing the feature
|
||||
- Include after intro, not before
|
||||
|
||||
3. MAIN CONTENT (H2/H3 sections)
|
||||
- Procedures with ordered lists
|
||||
- Features with bullet lists
|
||||
- Reference info in tables
|
||||
|
||||
4. TIPS & WARNINGS (Callouts)
|
||||
- Best practices
|
||||
- Common pitfalls
|
||||
- Important restrictions
|
||||
|
||||
5. RELATED ARTICLES (Links)
|
||||
- 2-4 related topics
|
||||
- Logical next steps
|
||||
```
|
||||
|
||||
### Heading Hierarchy
|
||||
|
||||
Use only two heading levels within articles:
|
||||
|
||||
- **H2** - Major sections (Overview, Configuration, Settings)
|
||||
- **H3** - Subsections within H2s (Creating a monitor, Editing settings)
|
||||
|
||||
Never use H4 or deeper. If content requires more nesting, restructure into separate sections.
|
||||
|
||||
### Section Length
|
||||
|
||||
| Element | Target Length |
|
||||
| ------------- | -------------- |
|
||||
| Paragraphs | 2-4 sentences |
|
||||
| H2 sections | 100-300 words |
|
||||
| H3 sections | 50-150 words |
|
||||
| Full articles | 800-1500 words |
|
||||
|
||||
---
|
||||
|
||||
## Formatting Rules
|
||||
|
||||
### Lists
|
||||
|
||||
**Bullet lists** - Use for features, options, characteristics (unordered items)
|
||||
|
||||
```javascript
|
||||
{
|
||||
type: 'bullet-list',
|
||||
items: [
|
||||
{ text: 'HTTP monitors check website availability' },
|
||||
{ text: 'Ping monitors verify server reachability' },
|
||||
{ text: 'Port monitors test specific service ports' }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Ordered lists** - Use for step-by-step procedures (sequential actions)
|
||||
|
||||
```javascript
|
||||
{
|
||||
type: 'ordered-list',
|
||||
items: [
|
||||
{ text: 'Navigate to **Uptime** in the sidebar' },
|
||||
{ text: 'Click **Create monitor**' },
|
||||
{ text: 'Enter the URL you want to monitor' },
|
||||
{ text: 'Click **Save** to create the monitor' }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**List guidelines:**
|
||||
|
||||
- Keep procedures to 4-6 steps maximum
|
||||
- If longer, break into multiple procedures
|
||||
- Start each item with an action verb for procedures
|
||||
- Use parallel structure (all items same grammatical form)
|
||||
|
||||
### Callouts
|
||||
|
||||
Use callouts sparingly for important information that shouldn't be missed.
|
||||
|
||||
**Types:**
|
||||
|
||||
| Variant | Use For | Example |
|
||||
| --------- | --------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `info` | Additional context, clarification | "Monitors check at the interval you specify, starting from creation time." |
|
||||
| `tip` | Best practices, recommendations | "Use descriptive names to easily identify monitors in alerts." |
|
||||
| `warning` | Restrictions, potential issues | "Deleting a monitor removes all associated check history." |
|
||||
| `success` | Confirmations, positive outcomes | "Your monitor is now active and checking every 5 minutes." |
|
||||
|
||||
```javascript
|
||||
{
|
||||
type: 'callout',
|
||||
variant: 'tip',
|
||||
title: 'Best practice',
|
||||
text: 'Group related monitors together for easier management.'
|
||||
}
|
||||
```
|
||||
|
||||
**Callout guidelines:**
|
||||
|
||||
- Maximum 2-3 callouts per article
|
||||
- Keep callout text concise (1-2 sentences)
|
||||
- Title is optional but helps scannability
|
||||
- Don't use callouts for routine information
|
||||
|
||||
### Inline Formatting
|
||||
|
||||
**Bold** - Use for:
|
||||
|
||||
- UI element names: "Click **Save**"
|
||||
- Key terms on first use: "**Uptime** measures availability over time"
|
||||
- Emphasis in lists: "**Name** — The display name for your monitor"
|
||||
|
||||
**Code/monospace** - Use for:
|
||||
|
||||
- URLs: `https://example.com`
|
||||
- File paths: `/api/v1/monitors`
|
||||
- Technical values: `200`, `timeout`
|
||||
- Commands: `npm run dev`
|
||||
|
||||
```javascript
|
||||
// In paragraph text, use backticks in markdown:
|
||||
{
|
||||
text: "Enter the URL (e.g., `https://example.com`) in the field";
|
||||
}
|
||||
```
|
||||
|
||||
### Tables
|
||||
|
||||
Use tables for:
|
||||
|
||||
- Feature comparisons
|
||||
- Configuration options
|
||||
- Reference data with multiple attributes
|
||||
|
||||
```javascript
|
||||
{
|
||||
type: 'table',
|
||||
columns: [
|
||||
{ key: 'type', label: 'Monitor Type', width: '1fr' },
|
||||
{ key: 'use', label: 'Best For', width: '2fr' }
|
||||
],
|
||||
rows: [
|
||||
{ type: 'HTTP', use: 'Websites and APIs' },
|
||||
{ type: 'Ping', use: 'Server availability' },
|
||||
{ type: 'Port', use: 'Specific services (databases, mail servers)' }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Content Types
|
||||
|
||||
### Block Types Reference
|
||||
|
||||
| Type | Purpose | When to Use |
|
||||
| --------------- | ----------------- | ------------------------- |
|
||||
| `heading` | Section titles | Start each major topic |
|
||||
| `paragraph` | Body text | Explanations, context |
|
||||
| `bullet-list` | Unordered items | Features, options |
|
||||
| `ordered-list` | Sequential steps | Procedures |
|
||||
| `checklist` | Checkable items | Requirements, checklists |
|
||||
| `callout` | Important notices | Tips, warnings |
|
||||
| `code` | Code blocks | Commands, config examples |
|
||||
| `table` | Structured data | Comparisons, reference |
|
||||
| `image` | Screenshots | UI demonstration |
|
||||
| `icon-cards` | Feature grids | Capability overviews |
|
||||
| `article-links` | Related content | End-of-article navigation |
|
||||
|
||||
### Icon Cards
|
||||
|
||||
Use for presenting 4-8 related features or concepts:
|
||||
|
||||
```javascript
|
||||
{
|
||||
type: 'icon-cards',
|
||||
items: [
|
||||
{
|
||||
icon: 'Globe',
|
||||
title: 'HTTP monitoring',
|
||||
description: 'Check website availability and response times'
|
||||
},
|
||||
{
|
||||
icon: 'Server',
|
||||
title: 'Infrastructure',
|
||||
description: 'Monitor CPU, memory, and disk usage'
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Article Links
|
||||
|
||||
Always end articles with related content:
|
||||
|
||||
```javascript
|
||||
{
|
||||
type: 'article-links',
|
||||
title: 'Related articles',
|
||||
items: [
|
||||
{
|
||||
collectionId: 'uptime-monitoring',
|
||||
articleId: 'http-monitors',
|
||||
title: 'Creating HTTP monitors',
|
||||
description: 'Set up your first website monitor'
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Images & Screenshots
|
||||
|
||||
### When to Include Images
|
||||
|
||||
- After introducing a feature (not before)
|
||||
- For complex UI that benefits from visual reference
|
||||
- For multi-step workflows
|
||||
- Skip for simple, text-describable actions
|
||||
|
||||
### Image Requirements
|
||||
|
||||
```javascript
|
||||
{
|
||||
type: 'image',
|
||||
src: '/images/user-guide/monitor-create.png',
|
||||
alt: 'Monitor creation form showing URL field, name field, and interval selector',
|
||||
caption: 'The monitor creation form with required fields highlighted'
|
||||
}
|
||||
```
|
||||
|
||||
**Alt text guidelines:**
|
||||
|
||||
- Describe what the image shows (15-30 words)
|
||||
- Include key UI elements visible
|
||||
- Make it useful for screen readers
|
||||
|
||||
**Caption guidelines:**
|
||||
|
||||
- Explain the context or what to notice
|
||||
- Keep to one sentence
|
||||
- Optional but recommended
|
||||
|
||||
### Screenshot Standards
|
||||
|
||||
- Capture at consistent browser width (1200px recommended)
|
||||
- Use light theme for screenshots
|
||||
- Highlight relevant areas if needed
|
||||
- Avoid personal data in screenshots
|
||||
- Crop to show relevant UI only
|
||||
|
||||
---
|
||||
|
||||
## Writing Procedures
|
||||
|
||||
### Step-by-Step Instructions
|
||||
|
||||
**Format:**
|
||||
|
||||
```javascript
|
||||
{
|
||||
type: 'ordered-list',
|
||||
items: [
|
||||
{ text: 'Navigate to **Uptime** in the sidebar' },
|
||||
{ text: 'Click **Create monitor** in the top right' },
|
||||
{ text: 'Enter the URL you want to monitor' },
|
||||
{ text: 'Set the check interval (default: 5 minutes)' },
|
||||
{ text: 'Click **Save** to create the monitor' }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Guidelines:**
|
||||
|
||||
- Start each step with an action verb (Click, Enter, Select, Navigate)
|
||||
- One action per step
|
||||
- Include the location of UI elements
|
||||
- Bold all clickable elements
|
||||
- Add context in parentheses when helpful
|
||||
|
||||
### Describing Features
|
||||
|
||||
**Format:**
|
||||
|
||||
```javascript
|
||||
{
|
||||
type: 'bullet-list',
|
||||
items: [
|
||||
{ bold: 'Name', text: 'A descriptive label for your monitor' },
|
||||
{ bold: 'URL', text: 'The address to check (must include protocol)' },
|
||||
{ bold: 'Interval', text: 'How often to perform checks (1-60 minutes)' }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Terminology
|
||||
|
||||
### Consistent Terms
|
||||
|
||||
Use these terms consistently throughout documentation:
|
||||
|
||||
| Term | Use | Don't Use |
|
||||
| ------------ | ------------------------- | --------------------- |
|
||||
| Monitor | The configured check | Watcher, probe, check |
|
||||
| Check | A single monitoring event | Ping, request, poll |
|
||||
| Incident | A detected problem | Outage, alert, issue |
|
||||
| Notification | Alert sent to user | Message, email, ping |
|
||||
| Dashboard | Main overview page | Home, overview |
|
||||
| Sidebar | Left navigation | Menu, nav |
|
||||
|
||||
### Feature Names
|
||||
|
||||
Always match UI exactly:
|
||||
|
||||
- **Uptime** (not "Uptime Monitoring")
|
||||
- **PageSpeed** (not "Page Speed" or "Pagespeed")
|
||||
- **Infrastructure** (not "Server Monitoring")
|
||||
- **Status pages** (not "Status Pages" - lowercase 'p')
|
||||
|
||||
---
|
||||
|
||||
## Article Templates
|
||||
|
||||
### Getting Started Article
|
||||
|
||||
```javascript
|
||||
{
|
||||
blocks: [
|
||||
{
|
||||
type: "heading",
|
||||
level: 2,
|
||||
text: "Overview",
|
||||
id: "overview",
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "[1-2 sentences: What this feature/concept is]",
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "[1-2 sentences: Why it matters to users]",
|
||||
},
|
||||
{
|
||||
type: "heading",
|
||||
level: 2,
|
||||
text: "[Main topic]",
|
||||
id: "main-topic",
|
||||
},
|
||||
// ... content blocks
|
||||
{
|
||||
type: "article-links",
|
||||
title: "Next steps",
|
||||
items: [
|
||||
// 2-3 logical next articles
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### How-To Article
|
||||
|
||||
```javascript
|
||||
{
|
||||
blocks: [
|
||||
{
|
||||
type: "heading",
|
||||
level: 2,
|
||||
text: "Overview",
|
||||
id: "overview",
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "[Brief description of what user will accomplish]",
|
||||
},
|
||||
{
|
||||
type: "heading",
|
||||
level: 2,
|
||||
text: "[Action: Creating/Configuring/Setting up...]",
|
||||
id: "action",
|
||||
},
|
||||
{
|
||||
type: "ordered-list",
|
||||
items: [
|
||||
// 4-6 steps
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "callout",
|
||||
variant: "tip",
|
||||
text: "[Best practice or helpful hint]",
|
||||
},
|
||||
{
|
||||
type: "article-links",
|
||||
title: "Related articles",
|
||||
items: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### Reference Article
|
||||
|
||||
```javascript
|
||||
{
|
||||
blocks: [
|
||||
{
|
||||
type: 'heading',
|
||||
level: 2,
|
||||
text: 'Overview',
|
||||
id: 'overview'
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
text: '[What this reference covers]'
|
||||
},
|
||||
{
|
||||
type: 'heading',
|
||||
level: 2,
|
||||
text: '[Category 1]',
|
||||
id: 'category-1'
|
||||
},
|
||||
{
|
||||
type: 'table',
|
||||
columns: [...],
|
||||
rows: [...]
|
||||
},
|
||||
// More categories as needed
|
||||
{
|
||||
type: 'article-links',
|
||||
title: 'Related articles',
|
||||
items: []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
Before publishing any article, verify:
|
||||
|
||||
### Content
|
||||
|
||||
- [ ] Overview explains what AND why
|
||||
- [ ] Procedures are tested and accurate
|
||||
- [ ] All UI element names match exactly
|
||||
- [ ] No assumptions about user knowledge
|
||||
- [ ] Related articles are linked
|
||||
|
||||
### Formatting
|
||||
|
||||
- [ ] Only H2 and H3 headings used
|
||||
- [ ] All headings have unique IDs
|
||||
- [ ] Lists use correct type (bullet vs ordered)
|
||||
- [ ] Callouts are used sparingly (max 3)
|
||||
- [ ] Bold used for UI elements
|
||||
|
||||
### Style
|
||||
|
||||
- [ ] Active voice throughout
|
||||
- [ ] Second person ("you")
|
||||
- [ ] Action verbs start procedure steps
|
||||
- [ ] Consistent terminology
|
||||
- [ ] No marketing language
|
||||
|
||||
### Technical
|
||||
|
||||
- [ ] All links work
|
||||
- [ ] Images have alt text
|
||||
- [ ] Code examples are correct
|
||||
- [ ] No placeholder content
|
||||
|
||||
---
|
||||
|
||||
## Available Icons
|
||||
|
||||
For icon-cards and other icon references, use these Lucide icon names:
|
||||
|
||||
**Navigation:**
|
||||
|
||||
- `Globe` - Uptime/HTTP
|
||||
- `Gauge` - PageSpeed
|
||||
- `Server` - Infrastructure
|
||||
- `Bell` - Notifications
|
||||
- `AlertTriangle` - Incidents
|
||||
- `Wifi` - Status pages
|
||||
- `Wrench` - Maintenance
|
||||
- `Settings` - Settings
|
||||
- `Users` - Team
|
||||
|
||||
**Actions:**
|
||||
|
||||
- `CheckCircle` - Success/complete
|
||||
- `Clock` - Time/scheduling
|
||||
- `Info` - Information
|
||||
- `Lightbulb` - Tips
|
||||
- `ArrowRight` - Navigation
|
||||
|
||||
**Features:**
|
||||
|
||||
- `Database` - Data/storage
|
||||
- `Terminal` - Commands
|
||||
- `FileText` - Documentation
|
||||
- `Shield` - Security
|
||||
- `Rocket` - Getting started
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
Articles are stored in `/content/index.js`:
|
||||
|
||||
```javascript
|
||||
export const articleContents = {
|
||||
"collection-id": {
|
||||
"article-id": {
|
||||
blocks: [
|
||||
// Content blocks
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Article metadata is in `/content/userGuideConfig.js`:
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: 'article-id',
|
||||
title: 'Article Title',
|
||||
description: 'Brief description for search and previews',
|
||||
keywords: ['search', 'terms', 'for', 'this', 'article']
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Good Paragraph
|
||||
|
||||
> "HTTP monitors check your websites and APIs at regular intervals. When a check fails—due to a timeout, error response, or connection issue—Checkmate creates an incident and sends notifications to your configured channels."
|
||||
|
||||
### Good Procedure
|
||||
|
||||
> 1. Navigate to **Uptime** in the sidebar
|
||||
> 2. Click **Create monitor** in the top right
|
||||
> 3. Enter the URL you want to monitor (include `https://`)
|
||||
> 4. Set your preferred check interval
|
||||
> 5. Click **Save** to create the monitor
|
||||
|
||||
### Good Callout
|
||||
|
||||
> **Tip:** Use descriptive monitor names like "Production API" or "Marketing Website" to quickly identify issues in notifications.
|
||||
|
||||
### Good Feature List
|
||||
|
||||
> - **Name** — A descriptive label for easy identification
|
||||
> - **URL** — The address to monitor (must include protocol)
|
||||
> - **Interval** — How often to check (1-60 minutes)
|
||||
> - **Timeout** — How long to wait before marking as failed
|
||||
|
||||
---
|
||||
|
||||
_Last updated: January 2025_
|
||||
@@ -0,0 +1,12 @@
|
||||
/* SearchResults styles (Dark theme) */
|
||||
|
||||
.user-guide-search-result-item {
|
||||
transition:
|
||||
background-color 150ms ease,
|
||||
border-color 150ms ease;
|
||||
}
|
||||
|
||||
.user-guide-search-result-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05) !important;
|
||||
border-color: #4a5568 !important;
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import { FileText, ArrowRight } from "lucide-react";
|
||||
import { searchArticles } from "./content/userGuideConfig";
|
||||
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
|
||||
import "./SearchResults.css";
|
||||
|
||||
const SearchResults = ({ query, onNavigate, onClearSearch }) => {
|
||||
const { colors, typography, spacing, border } = useUserGuideTheme();
|
||||
const results = searchArticles(query);
|
||||
|
||||
const handleResultClick = (result) => {
|
||||
onNavigate(result.collectionId, result.articleId);
|
||||
onClearSearch();
|
||||
};
|
||||
|
||||
if (query.trim().length < 2) {
|
||||
return (
|
||||
<div style={{ padding: spacing.lg }}>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.text.muted,
|
||||
textAlign: "center",
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
Type at least 2 characters to search...
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
return (
|
||||
<div style={{ padding: spacing.xl }}>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.text.primary,
|
||||
textAlign: "center",
|
||||
marginBottom: spacing.sm,
|
||||
marginTop: 0,
|
||||
}}
|
||||
>
|
||||
No results found for "{query}"
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.text.muted,
|
||||
textAlign: "center",
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
Try different keywords or browse by topic
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: spacing.lg }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: spacing.md,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.text.muted,
|
||||
}}
|
||||
>
|
||||
{results.length} result{results.length !== 1 ? "s" : ""} for "{query}"
|
||||
</span>
|
||||
<button
|
||||
onClick={onClearSearch}
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.brand.primary,
|
||||
cursor: "pointer",
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
Clear search
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: spacing.sm }}>
|
||||
{results.map((result) => (
|
||||
<div
|
||||
key={`${result.collectionId}-${result.articleId}`}
|
||||
onClick={() => handleResultClick(result)}
|
||||
className="user-guide-search-result-item"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
gap: spacing.md,
|
||||
padding: spacing.md,
|
||||
backgroundColor: colors.background.white,
|
||||
border: border.default,
|
||||
borderRadius: border.radius,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<FileText
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
color={colors.text.muted}
|
||||
style={{ flexShrink: 0, marginTop: 2 }}
|
||||
/>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<span
|
||||
style={{
|
||||
display: "block",
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: typography.fontWeight.medium,
|
||||
color: colors.text.primary,
|
||||
marginBottom: 2,
|
||||
}}
|
||||
>
|
||||
{result.articleTitle}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
display: "block",
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.text.secondary,
|
||||
lineHeight: typography.lineHeight.normal,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{result.articleDescription}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
display: "block",
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.text.muted,
|
||||
marginTop: spacing.xs,
|
||||
}}
|
||||
>
|
||||
in {result.collectionTitle}
|
||||
</span>
|
||||
</div>
|
||||
<ArrowRight
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
color={colors.text.muted}
|
||||
style={{ flexShrink: 0, marginTop: 2 }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchResults;
|
||||
@@ -0,0 +1,40 @@
|
||||
/* SidebarHeader styles - Dark theme */
|
||||
|
||||
.user-guide-header-icon-button {
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
.user-guide-header-icon-button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
color: #1570ef !important;
|
||||
}
|
||||
|
||||
.user-guide-header-icon-button.active {
|
||||
background-color: #0d0d0d;
|
||||
color: #1570ef;
|
||||
}
|
||||
|
||||
.user-guide-breadcrumb-dropdown-trigger {
|
||||
transition: background-color 150ms ease;
|
||||
}
|
||||
|
||||
.user-guide-breadcrumb-dropdown-trigger:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.user-guide-breadcrumb-item {
|
||||
transition: background-color 150ms ease;
|
||||
}
|
||||
|
||||
.user-guide-breadcrumb-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.user-guide-search-input:focus {
|
||||
outline: none;
|
||||
border-color: #1570ef;
|
||||
}
|
||||
|
||||
.user-guide-search-input::placeholder {
|
||||
color: #667085;
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import {
|
||||
Home,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Search,
|
||||
ExternalLink,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
|
||||
import "./SidebarHeader.css";
|
||||
|
||||
const SidebarHeader = ({
|
||||
breadcrumbs,
|
||||
onHomeClick,
|
||||
onBack,
|
||||
onForward,
|
||||
canGoBack,
|
||||
canGoForward,
|
||||
onClose,
|
||||
onOpenInNewTab,
|
||||
isSearchOpen,
|
||||
onSearchToggle,
|
||||
searchQuery,
|
||||
onSearchChange,
|
||||
showOpenInNewTab = true,
|
||||
showNavigation = true,
|
||||
}) => {
|
||||
const { colors, typography, spacing, border, isDark } = useUserGuideTheme();
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const dropdownRef = useRef(null);
|
||||
const searchInputRef = useRef(null);
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
setIsDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
// Focus search input when opened
|
||||
useEffect(() => {
|
||||
if (isSearchOpen && searchInputRef.current) {
|
||||
searchInputRef.current.focus();
|
||||
}
|
||||
}, [isSearchOpen]);
|
||||
|
||||
// Get current breadcrumb (last item)
|
||||
const currentBreadcrumb = breadcrumbs[breadcrumbs.length - 1];
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: colors.background.white,
|
||||
borderBottom: border.default,
|
||||
padding: `${spacing.sm} ${spacing.md}`,
|
||||
}}
|
||||
>
|
||||
{/* Main header row */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: spacing.sm,
|
||||
}}
|
||||
>
|
||||
{/* Left side: Navigation + Breadcrumb dropdown */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: spacing.xs,
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
{/* Navigation buttons - only shown when showNavigation is true */}
|
||||
{showNavigation && (
|
||||
<>
|
||||
{/* Back button */}
|
||||
<button
|
||||
onClick={onBack}
|
||||
disabled={!canGoBack}
|
||||
className="user-guide-header-icon-button"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 28,
|
||||
height: 28,
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
borderRadius: border.radius,
|
||||
cursor: canGoBack ? "pointer" : "default",
|
||||
color: canGoBack ? colors.text.secondary : colors.border.default,
|
||||
flexShrink: 0,
|
||||
opacity: canGoBack ? 1 : 0.5,
|
||||
}}
|
||||
title="Back"
|
||||
>
|
||||
<ChevronLeft
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Forward button */}
|
||||
<button
|
||||
onClick={onForward}
|
||||
disabled={!canGoForward}
|
||||
className="user-guide-header-icon-button"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 28,
|
||||
height: 28,
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
borderRadius: border.radius,
|
||||
cursor: canGoForward ? "pointer" : "default",
|
||||
color: canGoForward ? colors.text.secondary : colors.border.default,
|
||||
flexShrink: 0,
|
||||
opacity: canGoForward ? 1 : 0.5,
|
||||
}}
|
||||
title="Forward"
|
||||
>
|
||||
<ChevronRight
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Home button */}
|
||||
<button
|
||||
onClick={breadcrumbs.length > 1 ? onHomeClick : undefined}
|
||||
disabled={breadcrumbs.length <= 1}
|
||||
className="user-guide-header-icon-button"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 28,
|
||||
height: 28,
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
borderRadius: border.radius,
|
||||
cursor: breadcrumbs.length > 1 ? "pointer" : "default",
|
||||
color:
|
||||
breadcrumbs.length > 1
|
||||
? colors.text.secondary
|
||||
: colors.border.default,
|
||||
flexShrink: 0,
|
||||
opacity: breadcrumbs.length > 1 ? 1 : 0.5,
|
||||
}}
|
||||
title="Home"
|
||||
>
|
||||
<Home
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Breadcrumb dropdown */}
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
style={{ position: "relative", flex: 1, minWidth: 0 }}
|
||||
>
|
||||
<button
|
||||
onClick={() => breadcrumbs.length > 1 && setIsDropdownOpen(!isDropdownOpen)}
|
||||
className="user-guide-breadcrumb-dropdown-trigger"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
padding: "4px 8px",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
borderRadius: border.radius,
|
||||
cursor: breadcrumbs.length > 1 ? "pointer" : "default",
|
||||
maxWidth: "100%",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.sm,
|
||||
fontWeight: typography.fontWeight.medium,
|
||||
color: colors.text.primary,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{currentBreadcrumb?.label || "User guide"}
|
||||
</span>
|
||||
{breadcrumbs.length > 1 && (
|
||||
<ChevronDown
|
||||
size={14}
|
||||
strokeWidth={1.5}
|
||||
color={colors.text.muted}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
transform: isDropdownOpen ? "rotate(180deg)" : "rotate(0)",
|
||||
transition: "transform 150ms ease",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Dropdown menu */}
|
||||
{isDropdownOpen && breadcrumbs.length > 1 && (
|
||||
<div
|
||||
className="user-guide-breadcrumb-dropdown"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "100%",
|
||||
left: 0,
|
||||
marginTop: 4,
|
||||
backgroundColor: colors.background.white,
|
||||
border: border.default,
|
||||
borderRadius: border.radius,
|
||||
boxShadow: isDark
|
||||
? "0 4px 12px rgba(0, 0, 0, 0.3)"
|
||||
: "0 4px 12px rgba(0, 0, 0, 0.1)",
|
||||
minWidth: 180,
|
||||
maxWidth: 280,
|
||||
zIndex: 100,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{breadcrumbs.slice(0, -1).map((item, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => {
|
||||
item.onClick();
|
||||
setIsDropdownOpen(false);
|
||||
}}
|
||||
className="user-guide-breadcrumb-item"
|
||||
style={{
|
||||
display: "block",
|
||||
width: "100%",
|
||||
padding: "10px 14px",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
textAlign: "left",
|
||||
cursor: "pointer",
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.text.primary,
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right side: Actions */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: spacing.xs,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{/* Open in new tab - only shown when showOpenInNewTab is true */}
|
||||
{showOpenInNewTab && (
|
||||
<button
|
||||
onClick={onOpenInNewTab}
|
||||
className="user-guide-header-icon-button"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 28,
|
||||
height: 28,
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
borderRadius: border.radius,
|
||||
cursor: "pointer",
|
||||
color: colors.text.secondary,
|
||||
}}
|
||||
title="Open in new tab"
|
||||
>
|
||||
<ExternalLink
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Search toggle */}
|
||||
<button
|
||||
onClick={onSearchToggle}
|
||||
className={`user-guide-header-icon-button ${isSearchOpen ? "active" : ""}`}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 28,
|
||||
height: 28,
|
||||
backgroundColor: isSearchOpen ? colors.background.alt : "transparent",
|
||||
border: "none",
|
||||
borderRadius: border.radius,
|
||||
cursor: "pointer",
|
||||
color: isSearchOpen ? colors.brand.primary : colors.text.secondary,
|
||||
}}
|
||||
title="Search"
|
||||
>
|
||||
<Search
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="user-guide-header-icon-button"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 28,
|
||||
height: 28,
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
borderRadius: border.radius,
|
||||
cursor: "pointer",
|
||||
color: colors.text.secondary,
|
||||
}}
|
||||
title="Close"
|
||||
>
|
||||
<X
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search input (collapsible) */}
|
||||
{isSearchOpen && (
|
||||
<div style={{ marginTop: spacing.sm }}>
|
||||
<div style={{ position: "relative" }}>
|
||||
<Search
|
||||
size={14}
|
||||
strokeWidth={1.5}
|
||||
color={colors.text.muted}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 10,
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
placeholder="Search articles..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="user-guide-search-input"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 32,
|
||||
fontSize: typography.fontSize.sm,
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
backgroundColor: colors.background.alt,
|
||||
borderRadius: border.radius,
|
||||
border: border.default,
|
||||
paddingLeft: 32,
|
||||
paddingRight: 12,
|
||||
boxSizing: "border-box",
|
||||
color: colors.text.primary,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidebarHeader;
|
||||
@@ -0,0 +1,11 @@
|
||||
/* SidebarWrapper styles (Dark theme) */
|
||||
|
||||
.user-guide-sidebar-container {
|
||||
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Push main content when sidebar is open */
|
||||
body.user-guide-sidebar-open .home-content-wrapper {
|
||||
margin-right: var(--help-sidebar-padding, 440px);
|
||||
transition: margin-right 300ms ease-in-out;
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
|
||||
import TabBar from "./TabBar";
|
||||
import SidebarHeader from "./SidebarHeader";
|
||||
import UserGuideLanding from "./UserGuideLanding";
|
||||
import CollectionPage from "./CollectionPage";
|
||||
import ArticlePage from "./ArticlePage";
|
||||
import ContentRenderer from "./ContentRenderer";
|
||||
import HelpSection from "./HelpSection";
|
||||
import SearchResults from "./SearchResults";
|
||||
import { getCollection, getArticle } from "./content/userGuideConfig";
|
||||
import { getArticleContent } from "./content";
|
||||
import { extractToc } from "./content/contentTypes";
|
||||
import {
|
||||
useUserGuideSidebarContext,
|
||||
DEFAULT_CONTENT_WIDTH,
|
||||
} from "./UserGuideSidebarContext";
|
||||
import "./SidebarWrapper.css";
|
||||
|
||||
const STORAGE_KEY = "checkmate-sidebar-state";
|
||||
const MIN_CONTENT_WIDTH = DEFAULT_CONTENT_WIDTH;
|
||||
const MAX_CONTENT_WIDTH = DEFAULT_CONTENT_WIDTH * 2; // 100% wider
|
||||
|
||||
const SidebarWrapper = ({ isOpen, onClose, onOpen, initialPath, onOpenInNewTab }) => {
|
||||
const { setContentWidth: setContextContentWidth } = useUserGuideSidebarContext();
|
||||
const { colors, border } = useUserGuideTheme();
|
||||
const [activeTab, setActiveTab] = useState("user-guide");
|
||||
const [collectionId, setCollectionId] = useState(undefined);
|
||||
const [articleId, setArticleId] = useState(undefined);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||
const [contentWidth, setContentWidthLocal] = useState(DEFAULT_CONTENT_WIDTH);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [isHoveringHandle, setIsHoveringHandle] = useState(false);
|
||||
const [mouseY, setMouseY] = useState(0);
|
||||
const resizeRef = useRef(null);
|
||||
const handleRef = useRef(null);
|
||||
|
||||
// Sync content width with context whenever it changes
|
||||
const setContentWidth = useCallback(
|
||||
(width) => {
|
||||
setContentWidthLocal(width);
|
||||
setContextContentWidth(width);
|
||||
},
|
||||
[setContextContentWidth]
|
||||
);
|
||||
|
||||
// Initialize context with current width on mount
|
||||
useEffect(() => {
|
||||
setContextContentWidth(DEFAULT_CONTENT_WIDTH);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []); // Only run once on mount
|
||||
|
||||
// Navigation history
|
||||
const [history, setHistory] = useState([
|
||||
{ collectionId: undefined, articleId: undefined },
|
||||
]);
|
||||
const [historyIndex, setHistoryIndex] = useState(0);
|
||||
const isNavigatingRef = useRef(false);
|
||||
|
||||
// Parse initial path on mount
|
||||
useEffect(() => {
|
||||
if (initialPath) {
|
||||
const parts = initialPath.split("/");
|
||||
if (parts.length >= 1 && parts[0]) {
|
||||
setCollectionId(parts[0]);
|
||||
if (parts.length >= 2 && parts[1]) {
|
||||
setArticleId(parts[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [initialPath]);
|
||||
|
||||
// Persist sidebar state to localStorage
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const state = { activeTab, collectionId, articleId };
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||
}
|
||||
}, [isOpen, activeTab, collectionId, articleId]);
|
||||
|
||||
// Restore state from localStorage on mount
|
||||
useEffect(() => {
|
||||
const savedState = localStorage.getItem(STORAGE_KEY);
|
||||
if (savedState && !initialPath) {
|
||||
try {
|
||||
const {
|
||||
activeTab: savedTab,
|
||||
collectionId: savedCollection,
|
||||
articleId: savedArticle,
|
||||
} = JSON.parse(savedState);
|
||||
if (savedTab) setActiveTab(savedTab);
|
||||
if (savedCollection) setCollectionId(savedCollection);
|
||||
if (savedArticle) setArticleId(savedArticle);
|
||||
} catch (e) {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
}, [initialPath]);
|
||||
|
||||
// Track navigation changes in history
|
||||
useEffect(() => {
|
||||
if (isNavigatingRef.current) {
|
||||
isNavigatingRef.current = false;
|
||||
return;
|
||||
}
|
||||
// Only track if this is a new navigation (not from back/forward)
|
||||
const currentEntry = history[historyIndex];
|
||||
if (
|
||||
currentEntry?.collectionId !== collectionId ||
|
||||
currentEntry?.articleId !== articleId
|
||||
) {
|
||||
// Truncate forward history and add new entry
|
||||
const newHistory = [
|
||||
...history.slice(0, historyIndex + 1),
|
||||
{ collectionId, articleId },
|
||||
];
|
||||
setHistory(newHistory);
|
||||
setHistoryIndex(newHistory.length - 1);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [collectionId, articleId]);
|
||||
|
||||
// Navigation handlers
|
||||
const handleNavigate = useCallback((newCollectionId, newArticleId) => {
|
||||
setCollectionId(newCollectionId);
|
||||
setArticleId(newArticleId);
|
||||
}, []);
|
||||
|
||||
const handleBackToHome = useCallback(() => {
|
||||
setCollectionId(undefined);
|
||||
setArticleId(undefined);
|
||||
}, []);
|
||||
|
||||
// Back/Forward navigation
|
||||
const canGoBack = historyIndex > 0;
|
||||
const canGoForward = historyIndex < history.length - 1;
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
if (historyIndex > 0) {
|
||||
isNavigatingRef.current = true;
|
||||
const prevIndex = historyIndex - 1;
|
||||
const prevEntry = history[prevIndex];
|
||||
setHistoryIndex(prevIndex);
|
||||
setCollectionId(prevEntry.collectionId);
|
||||
setArticleId(prevEntry.articleId);
|
||||
}
|
||||
}, [historyIndex, history]);
|
||||
|
||||
const handleForward = useCallback(() => {
|
||||
if (historyIndex < history.length - 1) {
|
||||
isNavigatingRef.current = true;
|
||||
const nextIndex = historyIndex + 1;
|
||||
const nextEntry = history[nextIndex];
|
||||
setHistoryIndex(nextIndex);
|
||||
setCollectionId(nextEntry.collectionId);
|
||||
setArticleId(nextEntry.articleId);
|
||||
}
|
||||
}, [historyIndex, history]);
|
||||
|
||||
const handleBackToCollection = useCallback(() => {
|
||||
setArticleId(undefined);
|
||||
}, []);
|
||||
|
||||
const handleHomeClick = useCallback(() => {
|
||||
setCollectionId(undefined);
|
||||
setArticleId(undefined);
|
||||
}, []);
|
||||
|
||||
// Get current collection and article
|
||||
const collection = collectionId ? getCollection(collectionId) : undefined;
|
||||
const article =
|
||||
collectionId && articleId ? getArticle(collectionId, articleId) : undefined;
|
||||
|
||||
// Get prev/next articles for navigation
|
||||
const getAdjacentArticles = () => {
|
||||
if (!collection || !articleId) return { prev: undefined, next: undefined };
|
||||
const currentIndex = collection.articles.findIndex((a) => a.id === articleId);
|
||||
return {
|
||||
prev: currentIndex > 0 ? collection.articles[currentIndex - 1] : undefined,
|
||||
next:
|
||||
currentIndex < collection.articles.length - 1
|
||||
? collection.articles[currentIndex + 1]
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const { prev: prevArticle, next: nextArticle } = getAdjacentArticles();
|
||||
|
||||
// Get article content
|
||||
const articleContent =
|
||||
collectionId && articleId ? getArticleContent(collectionId, articleId) : undefined;
|
||||
|
||||
// Auto-generate TOC from content headings
|
||||
const tocItems = articleContent ? extractToc(articleContent.blocks) : undefined;
|
||||
|
||||
// Build breadcrumb items for dropdown
|
||||
const buildBreadcrumbs = () => {
|
||||
let items = [];
|
||||
switch (activeTab) {
|
||||
case "user-guide":
|
||||
items = [{ label: "User guide", onClick: handleHomeClick }];
|
||||
if (collection) {
|
||||
items.push({ label: collection.title, onClick: () => setArticleId(undefined) });
|
||||
}
|
||||
if (article) {
|
||||
items.push({ label: article.title, onClick: () => {} });
|
||||
}
|
||||
return items;
|
||||
default:
|
||||
return [{ label: "Help", onClick: () => {} }];
|
||||
}
|
||||
};
|
||||
|
||||
// Handle "Open in new tab"
|
||||
const handleOpenInNewTab = () => {
|
||||
let path = "https://bluewavelabs.gitbook.io/checkmate";
|
||||
if (onOpenInNewTab) {
|
||||
onOpenInNewTab();
|
||||
} else {
|
||||
window.open(path, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
};
|
||||
|
||||
// Clear search and close search bar
|
||||
const handleClearSearch = useCallback(() => {
|
||||
setSearchQuery("");
|
||||
setIsSearchOpen(false);
|
||||
}, []);
|
||||
|
||||
// Render User Guide content
|
||||
const renderUserGuideContent = () => {
|
||||
// Show search results when search is active
|
||||
if (isSearchOpen && searchQuery.trim().length > 0) {
|
||||
return (
|
||||
<SearchResults
|
||||
query={searchQuery}
|
||||
onNavigate={(colId, artId) => {
|
||||
handleNavigate(colId, artId);
|
||||
handleClearSearch();
|
||||
}}
|
||||
onClearSearch={handleClearSearch}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (collection && article) {
|
||||
return (
|
||||
<ArticlePage
|
||||
collection={collection}
|
||||
article={article}
|
||||
onBack={handleBackToCollection}
|
||||
onBackToHome={handleBackToHome}
|
||||
prevArticle={prevArticle}
|
||||
nextArticle={nextArticle}
|
||||
onPrevArticle={
|
||||
prevArticle ? () => handleNavigate(collectionId, prevArticle.id) : undefined
|
||||
}
|
||||
onNextArticle={
|
||||
nextArticle ? () => handleNavigate(collectionId, nextArticle.id) : undefined
|
||||
}
|
||||
tocItems={tocItems}
|
||||
mode="in-app"
|
||||
>
|
||||
{articleContent && (
|
||||
<ContentRenderer
|
||||
content={articleContent}
|
||||
onNavigate={handleNavigate}
|
||||
/>
|
||||
)}
|
||||
</ArticlePage>
|
||||
);
|
||||
}
|
||||
|
||||
if (collection) {
|
||||
return (
|
||||
<CollectionPage
|
||||
collection={collection}
|
||||
onArticleClick={(id) => handleNavigate(collectionId, id)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<UserGuideLanding
|
||||
onNavigate={handleNavigate}
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
mode="in-app"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const contentArea = (tabValue) => {
|
||||
switch (tabValue) {
|
||||
case "user-guide":
|
||||
return renderUserGuideContent();
|
||||
default:
|
||||
return <HelpSection />;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle tab click - open sidebar if closed, close if clicking active tab, or switch tab
|
||||
const handleTabClick = (tab) => {
|
||||
if (!isOpen) {
|
||||
onOpen();
|
||||
setActiveTab(tab);
|
||||
} else if (activeTab === tab) {
|
||||
// Clicking the already-active tab closes the sidebar
|
||||
onClose();
|
||||
} else {
|
||||
// Clicking a different tab switches to it
|
||||
setActiveTab(tab);
|
||||
}
|
||||
};
|
||||
|
||||
// Resize handlers
|
||||
const handleResizeStart = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
setIsResizing(true);
|
||||
resizeRef.current = { startX: e.clientX, startWidth: contentWidth };
|
||||
},
|
||||
[contentWidth]
|
||||
);
|
||||
|
||||
const handleResizeMove = useCallback(
|
||||
(e) => {
|
||||
if (!isResizing || !resizeRef.current) return;
|
||||
|
||||
const deltaX = resizeRef.current.startX - e.clientX;
|
||||
const newWidth = Math.min(
|
||||
MAX_CONTENT_WIDTH,
|
||||
Math.max(MIN_CONTENT_WIDTH, resizeRef.current.startWidth + deltaX)
|
||||
);
|
||||
setContentWidth(newWidth);
|
||||
},
|
||||
[isResizing, setContentWidth]
|
||||
);
|
||||
|
||||
const handleResizeEnd = useCallback(() => {
|
||||
setIsResizing(false);
|
||||
setIsHoveringHandle(false);
|
||||
resizeRef.current = null;
|
||||
}, []);
|
||||
|
||||
// Add/remove resize event listeners
|
||||
useEffect(() => {
|
||||
if (isResizing) {
|
||||
document.addEventListener("mousemove", handleResizeMove);
|
||||
document.addEventListener("mouseup", handleResizeEnd);
|
||||
document.body.style.cursor = "ew-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", handleResizeMove);
|
||||
document.removeEventListener("mouseup", handleResizeEnd);
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
};
|
||||
}, [isResizing, handleResizeMove, handleResizeEnd]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="user-guide-sidebar-container"
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
right: 0,
|
||||
height: "100vh",
|
||||
display: "flex",
|
||||
zIndex: 1100,
|
||||
}}
|
||||
>
|
||||
{/* Resize Handle - positioned absolutely on the left edge */}
|
||||
<div
|
||||
ref={handleRef}
|
||||
onMouseDown={handleResizeStart}
|
||||
onMouseEnter={() => setIsHoveringHandle(true)}
|
||||
onMouseLeave={() => !isResizing && setIsHoveringHandle(false)}
|
||||
onMouseMove={(e) => {
|
||||
if (handleRef.current) {
|
||||
const rect = handleRef.current.getBoundingClientRect();
|
||||
setMouseY(e.clientY - rect.top);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 8,
|
||||
height: "100%",
|
||||
cursor: "ew-resize",
|
||||
zIndex: 20,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
}}
|
||||
>
|
||||
{/* Visual indicator line */}
|
||||
<div
|
||||
style={{
|
||||
width: isResizing ? 2 : 1,
|
||||
height: "100%",
|
||||
backgroundColor:
|
||||
isResizing || isHoveringHandle ? "#4CAF93" : colors.border.default,
|
||||
transition: isResizing ? "none" : "all 150ms ease",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{/* Grab indicator follows mouse */}
|
||||
{(isHoveringHandle || isResizing) && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: -1,
|
||||
top: mouseY - 10,
|
||||
width: 3,
|
||||
height: 20,
|
||||
backgroundColor: "#555",
|
||||
borderRadius: 1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Bar - Always visible */}
|
||||
<TabBar
|
||||
activeTab={isOpen ? activeTab : undefined}
|
||||
onTabChange={handleTabClick}
|
||||
/>
|
||||
|
||||
{/* Main Sidebar Content - Slides in/out */}
|
||||
<div
|
||||
style={{
|
||||
width: isOpen ? contentWidth : 0,
|
||||
height: "100%",
|
||||
backgroundColor: colors.background.white,
|
||||
borderLeft: isOpen ? border.default : "none",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
transition: isResizing ? "none" : "width 300ms ease-in-out",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{isOpen && (
|
||||
<>
|
||||
{/* Header */}
|
||||
<SidebarHeader
|
||||
showOpenInNewTab={activeTab === "user-guide"}
|
||||
showNavigation={activeTab === "user-guide"}
|
||||
breadcrumbs={buildBreadcrumbs()}
|
||||
onHomeClick={handleHomeClick}
|
||||
onBack={handleBack}
|
||||
onForward={handleForward}
|
||||
canGoBack={canGoBack}
|
||||
canGoForward={canGoForward}
|
||||
onClose={onClose}
|
||||
onOpenInNewTab={handleOpenInNewTab}
|
||||
isSearchOpen={isSearchOpen}
|
||||
onSearchToggle={() => setIsSearchOpen(!isSearchOpen)}
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
/>
|
||||
|
||||
{/* Content Area */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
overflowY: "auto",
|
||||
backgroundColor: colors.background.alt,
|
||||
}}
|
||||
>
|
||||
{contentArea(activeTab)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidebarWrapper;
|
||||
@@ -0,0 +1,13 @@
|
||||
/* TabBar styles */
|
||||
|
||||
.user-guide-tab-item {
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
.user-guide-tab-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.user-guide-tab-item.active:hover {
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { BookOpen, HelpCircle } from "lucide-react";
|
||||
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
|
||||
import "./TabBar.css";
|
||||
|
||||
const TabItem = ({ label, icon, isActive, onClick, colors, typography }) => {
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={`user-guide-tab-item ${isActive ? "active" : ""}`}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "16px 8px",
|
||||
cursor: "pointer",
|
||||
backgroundColor: isActive ? colors.background.white : "transparent",
|
||||
borderLeft: isActive
|
||||
? `2px solid ${colors.brand.primary}`
|
||||
: "2px solid transparent",
|
||||
borderBottom: `1px solid ${colors.border.default}`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
color: isActive ? colors.brand.primary : colors.text.muted,
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
<span
|
||||
style={{
|
||||
writingMode: "vertical-rl",
|
||||
textOrientation: "mixed",
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: 12,
|
||||
fontWeight: typography.fontWeight.normal,
|
||||
color: isActive ? colors.brand.primary : colors.text.muted,
|
||||
letterSpacing: "0.5px",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TabBar = ({ activeTab, onTabChange }) => {
|
||||
const { colors, typography } = useUserGuideTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: 40,
|
||||
height: "100%",
|
||||
backgroundColor: colors.background.sidebar,
|
||||
borderLeft: `1px solid ${colors.border.default}`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "stretch",
|
||||
}}
|
||||
>
|
||||
<TabItem
|
||||
id="user-guide"
|
||||
label="User guide"
|
||||
icon={
|
||||
<BookOpen
|
||||
size={18}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
}
|
||||
isActive={activeTab === "user-guide"}
|
||||
onClick={() => onTabChange("user-guide")}
|
||||
colors={colors}
|
||||
typography={typography}
|
||||
/>
|
||||
<TabItem
|
||||
id="help"
|
||||
label="Help"
|
||||
icon={
|
||||
<HelpCircle
|
||||
size={18}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
}
|
||||
isActive={activeTab === "help"}
|
||||
onClick={() => onTabChange("help")}
|
||||
colors={colors}
|
||||
typography={typography}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabBar;
|
||||
@@ -0,0 +1,39 @@
|
||||
/* UserGuideLanding styles - hover states */
|
||||
|
||||
/* Collection card hover */
|
||||
.user-guide-collection-card {
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
.user-guide-collection-card:hover {
|
||||
border-color: rgba(21, 112, 239, 0.3) !important;
|
||||
background-color: rgba(21, 112, 239, 0.02) !important;
|
||||
box-shadow: 0px 2px 8px -2px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
[data-mui-color-scheme="dark"] .user-guide-collection-card:hover {
|
||||
border-color: rgba(21, 112, 239, 0.5) !important;
|
||||
background-color: rgba(21, 112, 239, 0.05) !important;
|
||||
box-shadow: 0px 4px 16px -4px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Fast find item hover */
|
||||
.user-guide-fast-find-item {
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
.user-guide-fast-find-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
[data-mui-color-scheme="dark"] .user-guide-fast-find-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.user-guide-fast-find-item .arrow-icon {
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
.user-guide-fast-find-item:hover .arrow-icon {
|
||||
color: #1570ef;
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import {
|
||||
ArrowRight,
|
||||
Rocket,
|
||||
Globe,
|
||||
Gauge,
|
||||
Server,
|
||||
Bell,
|
||||
AlertTriangle,
|
||||
Settings,
|
||||
Wifi,
|
||||
Wrench,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { collections, fastFinds, getCollection } from "./content/userGuideConfig";
|
||||
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
|
||||
import "./UserGuideLanding.css";
|
||||
|
||||
// Map icon names to actual Lucide components
|
||||
const iconMap = {
|
||||
Rocket,
|
||||
Globe,
|
||||
Gauge,
|
||||
Server,
|
||||
Bell,
|
||||
AlertTriangle,
|
||||
Settings,
|
||||
Wifi,
|
||||
Wrench,
|
||||
Users,
|
||||
};
|
||||
|
||||
const UserGuideLanding = ({
|
||||
onNavigate,
|
||||
searchQuery,
|
||||
onSearchChange,
|
||||
mode = "in-app",
|
||||
}) => {
|
||||
const { colors, typography, spacing, border } = useUserGuideTheme();
|
||||
const isInApp = mode === "in-app";
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: "100%", backgroundColor: colors.background.alt }}>
|
||||
{/* Main Content */}
|
||||
<div
|
||||
style={{
|
||||
maxWidth: isInApp ? "100%" : 1200,
|
||||
margin: "0 auto",
|
||||
padding: isInApp ? spacing.lg : `${spacing["3xl"]} ${spacing["3xl"]}`,
|
||||
}}
|
||||
>
|
||||
{/* Collections Grid */}
|
||||
<h2
|
||||
style={{
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: isInApp ? typography.fontSize.lg : typography.fontSize.xl,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.text.primary,
|
||||
marginBottom: isInApp ? spacing.md : spacing.xl,
|
||||
marginTop: 0,
|
||||
}}
|
||||
>
|
||||
Browse by topic
|
||||
</h2>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: isInApp
|
||||
? "1fr"
|
||||
: "repeat(auto-fill, minmax(340px, 1fr))",
|
||||
gap: isInApp ? spacing.md : spacing.lg,
|
||||
marginBottom: isInApp ? spacing.xl : spacing["4xl"],
|
||||
}}
|
||||
>
|
||||
{collections.map((collection) => {
|
||||
const IconComponent = iconMap[collection.icon] || Rocket;
|
||||
return (
|
||||
<div
|
||||
key={collection.id}
|
||||
onClick={() => onNavigate(collection.id)}
|
||||
className="user-guide-collection-card"
|
||||
style={{
|
||||
backgroundColor: colors.background.white,
|
||||
border: border.default,
|
||||
borderRadius: border.radius,
|
||||
padding: spacing.lg,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "flex-start", gap: spacing.md }}
|
||||
>
|
||||
<IconComponent
|
||||
size={20}
|
||||
strokeWidth={1.5}
|
||||
color={colors.brand.primary}
|
||||
/>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: spacing.xs,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.text.primary,
|
||||
}}
|
||||
>
|
||||
{collection.title}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.text.secondary,
|
||||
backgroundColor: colors.background.alt,
|
||||
padding: "2px 6px",
|
||||
borderRadius: border.radius,
|
||||
}}
|
||||
>
|
||||
{collection.articleCount} articles
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.text.secondary,
|
||||
lineHeight: typography.lineHeight.normal,
|
||||
display: "block",
|
||||
}}
|
||||
>
|
||||
{collection.description}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* About our docs - Only shown in in-app mode */}
|
||||
{isInApp && (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: colors.background.white,
|
||||
border: border.default,
|
||||
borderRadius: border.radius,
|
||||
padding: spacing.lg,
|
||||
}}
|
||||
>
|
||||
<h3
|
||||
style={{
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.text.primary,
|
||||
marginBottom: spacing.md,
|
||||
marginTop: 0,
|
||||
}}
|
||||
>
|
||||
About our docs
|
||||
</h3>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.text.secondary,
|
||||
lineHeight: typography.lineHeight.normal,
|
||||
margin: 0,
|
||||
marginBottom: spacing.md,
|
||||
}}
|
||||
>
|
||||
There are a few ways to explore our docs:
|
||||
</p>
|
||||
|
||||
<div style={{ marginBottom: spacing.md }}>
|
||||
<h4
|
||||
style={{
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.sm,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.text.primary,
|
||||
marginBottom: spacing.xs,
|
||||
marginTop: 0,
|
||||
}}
|
||||
>
|
||||
In the product
|
||||
</h4>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.text.secondary,
|
||||
lineHeight: typography.lineHeight.normal,
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
Look for help icons and tooltips throughout the interface.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserGuideLanding;
|
||||
@@ -0,0 +1,102 @@
|
||||
import { createContext, useContext, useState, useCallback, useEffect } from "react";
|
||||
|
||||
const SIDEBAR_STATE_KEY = "checkmate-sidebar-open";
|
||||
|
||||
// Constants for sidebar dimensions
|
||||
export const TAB_BAR_WIDTH = 41; // 40px width + 1px border
|
||||
export const DEFAULT_CONTENT_WIDTH = 400;
|
||||
// Gap when sidebar is closed (need more space since tab bar is narrow)
|
||||
export const CLOSED_GAP = 24;
|
||||
// Gap when sidebar is open (less needed since content edge is clear)
|
||||
export const OPEN_GAP = 8;
|
||||
|
||||
const UserGuideSidebarContext = createContext(null);
|
||||
|
||||
export const UserGuideSidebarProvider = ({ children }) => {
|
||||
const [isOpen, setIsOpen] = useState(() => {
|
||||
if (typeof window === "undefined") return false;
|
||||
const saved = localStorage.getItem(SIDEBAR_STATE_KEY);
|
||||
return saved === "true";
|
||||
});
|
||||
|
||||
const [currentPath, setCurrentPath] = useState(undefined);
|
||||
const [contentWidth, setContentWidth] = useState(DEFAULT_CONTENT_WIDTH);
|
||||
|
||||
// Calculate total sidebar width and required padding
|
||||
const totalSidebarWidth = isOpen ? TAB_BAR_WIDTH + contentWidth : TAB_BAR_WIDTH;
|
||||
const gap = isOpen ? OPEN_GAP : CLOSED_GAP;
|
||||
const requiredPaddingRight = totalSidebarWidth + gap;
|
||||
|
||||
// Set CSS custom properties on document root for global access
|
||||
useEffect(() => {
|
||||
if (typeof document === "undefined") return;
|
||||
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty("--help-sidebar-width", `${totalSidebarWidth}px`);
|
||||
root.style.setProperty("--help-sidebar-padding", `${requiredPaddingRight}px`);
|
||||
}, [totalSidebarWidth, requiredPaddingRight]);
|
||||
|
||||
// Update body class for content push effect
|
||||
useEffect(() => {
|
||||
if (typeof document === "undefined") return;
|
||||
|
||||
if (isOpen) {
|
||||
document.body.classList.add("user-guide-sidebar-open");
|
||||
} else {
|
||||
document.body.classList.remove("user-guide-sidebar-open");
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.body.classList.remove("user-guide-sidebar-open");
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
// Persist state to localStorage
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.setItem(SIDEBAR_STATE_KEY, String(isOpen));
|
||||
}, [isOpen]);
|
||||
|
||||
const open = useCallback((path) => {
|
||||
setCurrentPath(path);
|
||||
setIsOpen(true);
|
||||
}, []);
|
||||
|
||||
const close = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
}, []);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setIsOpen((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<UserGuideSidebarContext.Provider
|
||||
value={{
|
||||
isOpen,
|
||||
open,
|
||||
close,
|
||||
toggle,
|
||||
currentPath,
|
||||
contentWidth,
|
||||
setContentWidth,
|
||||
totalSidebarWidth,
|
||||
requiredPaddingRight,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</UserGuideSidebarContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useUserGuideSidebarContext = () => {
|
||||
const context = useContext(UserGuideSidebarContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useUserGuideSidebarContext must be used within a UserGuideSidebarProvider"
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export default UserGuideSidebarContext;
|
||||
@@ -0,0 +1,176 @@
|
||||
// Content block types for the templatic article system
|
||||
|
||||
/**
|
||||
* @typedef {'heading' | 'paragraph' | 'callout' | 'bullet-list' | 'ordered-list' | 'checklist' | 'icon-cards' | 'grid-cards' | 'time-estimate' | 'info-box' | 'code' | 'requirements' | 'table' | 'image' | 'article-links'} ContentBlockType
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} HeadingBlock
|
||||
* @property {'heading'} type
|
||||
* @property {string} id - Used for TOC anchors
|
||||
* @property {2 | 3} level - h2 or h3
|
||||
* @property {string} text
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ParagraphBlock
|
||||
* @property {'paragraph'} type
|
||||
* @property {string} text - Supports basic markdown: **bold**, *italic*, `code`
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CalloutBlock
|
||||
* @property {'callout'} type
|
||||
* @property {'info' | 'tip' | 'warning' | 'success'} [variant]
|
||||
* @property {string} [title]
|
||||
* @property {string} text
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ListItem
|
||||
* @property {string} text
|
||||
* @property {string} [bold] - Optional bold prefix
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} BulletListBlock
|
||||
* @property {'bullet-list'} type
|
||||
* @property {ListItem[]} items
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} OrderedListBlock
|
||||
* @property {'ordered-list'} type
|
||||
* @property {ListItem[]} items
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ChecklistBlock
|
||||
* @property {'checklist'} type
|
||||
* @property {string[]} items
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} IconCardItem
|
||||
* @property {string} icon - Icon name from lucide-react
|
||||
* @property {string} title
|
||||
* @property {string} description
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} IconCardsBlock
|
||||
* @property {'icon-cards'} type
|
||||
* @property {IconCardItem[]} items
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} GridCardItem
|
||||
* @property {string} title
|
||||
* @property {string} description
|
||||
* @property {string} [icon]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} GridCardsBlock
|
||||
* @property {'grid-cards'} type
|
||||
* @property {GridCardItem[]} items
|
||||
* @property {number} [columns]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TimeEstimateBlock
|
||||
* @property {'time-estimate'} type
|
||||
* @property {string} text
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} InfoBoxBlock
|
||||
* @property {'info-box'} type
|
||||
* @property {string} [icon]
|
||||
* @property {string} title
|
||||
* @property {string[]} items
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CodeBlock
|
||||
* @property {'code'} type
|
||||
* @property {string} code
|
||||
* @property {string} [language]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} RequirementItem
|
||||
* @property {string} icon
|
||||
* @property {string} title
|
||||
* @property {string[]} items
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} RequirementsBlock
|
||||
* @property {'requirements'} type
|
||||
* @property {RequirementItem[]} items
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TableColumn
|
||||
* @property {string} key
|
||||
* @property {string} label
|
||||
* @property {string} [width]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TableBlock
|
||||
* @property {'table'} type
|
||||
* @property {TableColumn[]} columns
|
||||
* @property {Object[]} rows
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ImageBlock
|
||||
* @property {'image'} type
|
||||
* @property {string} src
|
||||
* @property {string} alt
|
||||
* @property {string} [caption]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ArticleLink
|
||||
* @property {string} collectionId
|
||||
* @property {string} articleId
|
||||
* @property {string} title
|
||||
* @property {string} [description]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ArticleLinksBlock
|
||||
* @property {'article-links'} type
|
||||
* @property {string} [title] - e.g., "Next steps", "Related articles"
|
||||
* @property {ArticleLink[]} items
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ArticleContent
|
||||
* @property {ContentBlock[]} blocks
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TocItem
|
||||
* @property {string} label
|
||||
* @property {string} href
|
||||
* @property {number} level
|
||||
*/
|
||||
|
||||
/**
|
||||
* Helper to extract TOC from content blocks
|
||||
* @param {ContentBlock[]} blocks
|
||||
* @returns {TocItem[]}
|
||||
*/
|
||||
export const extractToc = (blocks) => {
|
||||
return blocks
|
||||
.filter((block) => block.type === "heading")
|
||||
.map((block) => ({
|
||||
label: block.text,
|
||||
href: `#${block.id}`,
|
||||
level: block.level,
|
||||
}));
|
||||
};
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,604 @@
|
||||
// Icon names as strings - resolved to actual components in UserGuideLanding.jsx
|
||||
// Available icons: Rocket, Globe, Gauge, Server, Bell, AlertTriangle, Settings, Wifi, Wrench, Users
|
||||
|
||||
/**
|
||||
* @typedef {Object} Article
|
||||
* @property {string} id
|
||||
* @property {string} title
|
||||
* @property {string} description
|
||||
* @property {string[]} keywords
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Collection
|
||||
* @property {string} id
|
||||
* @property {string} title
|
||||
* @property {string} description
|
||||
* @property {string} icon
|
||||
* @property {number} articleCount
|
||||
* @property {Article[]} articles
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} FastFind
|
||||
* @property {string} id
|
||||
* @property {string} title
|
||||
* @property {string} collectionId
|
||||
* @property {string} articleId
|
||||
*/
|
||||
|
||||
// Collections configuration for Checkmate
|
||||
export const collections = [
|
||||
{
|
||||
id: "getting-started",
|
||||
title: "Getting started",
|
||||
description: "Essential first steps for new users.",
|
||||
icon: "Rocket",
|
||||
articleCount: 4,
|
||||
articles: [
|
||||
{
|
||||
id: "what-is-checkmate",
|
||||
title: "What is Checkmate",
|
||||
description: "Overview of monitoring capabilities and core features.",
|
||||
keywords: ["introduction", "overview", "platform", "monitoring", "uptime"],
|
||||
},
|
||||
{
|
||||
id: "quick-start",
|
||||
title: "Quick start guide",
|
||||
description: "Create your first monitor in 5 minutes.",
|
||||
keywords: ["quick", "start", "first", "monitor", "setup", "tutorial"],
|
||||
},
|
||||
{
|
||||
id: "dashboard",
|
||||
title: "Understanding the dashboard",
|
||||
description: "Navigation and key metrics explained.",
|
||||
keywords: ["dashboard", "navigation", "interface", "home", "metrics"],
|
||||
},
|
||||
{
|
||||
id: "roles-permissions",
|
||||
title: "User roles and permissions",
|
||||
description: "User, Admin, and Superadmin roles explained.",
|
||||
keywords: ["role", "permission", "admin", "superadmin", "user", "access"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "uptime-monitoring",
|
||||
title: "Uptime monitoring",
|
||||
description: "Monitor website and service availability.",
|
||||
icon: "Globe",
|
||||
articleCount: 9,
|
||||
articles: [
|
||||
{
|
||||
id: "http-monitors",
|
||||
title: "Creating HTTP monitors",
|
||||
description: "URL monitoring with response validation.",
|
||||
keywords: ["http", "https", "url", "website", "monitor", "create"],
|
||||
},
|
||||
{
|
||||
id: "ping-monitors",
|
||||
title: "Ping monitors",
|
||||
description: "ICMP-based availability checks.",
|
||||
keywords: ["ping", "icmp", "availability", "monitor"],
|
||||
},
|
||||
{
|
||||
id: "port-monitors",
|
||||
title: "Port monitors",
|
||||
description: "TCP port availability monitoring.",
|
||||
keywords: ["port", "tcp", "availability", "monitor", "service"],
|
||||
},
|
||||
{
|
||||
id: "docker-monitors",
|
||||
title: "Docker monitors",
|
||||
description: "Monitor Docker container status and health.",
|
||||
keywords: ["docker", "container", "monitoring", "health", "status"],
|
||||
},
|
||||
{
|
||||
id: "bulk-import",
|
||||
title: "Bulk import monitors",
|
||||
description: "Import multiple monitors from a CSV file.",
|
||||
keywords: ["bulk", "import", "csv", "multiple", "batch", "upload"],
|
||||
},
|
||||
{
|
||||
id: "intervals-timing",
|
||||
title: "Monitor intervals and timing",
|
||||
description: "Configure how often to check your services.",
|
||||
keywords: ["interval", "timing", "frequency", "check", "schedule"],
|
||||
},
|
||||
{
|
||||
id: "json-path-matching",
|
||||
title: "JSON path matching",
|
||||
description: "Validate API response content.",
|
||||
keywords: ["json", "api", "response", "validation", "match", "path"],
|
||||
},
|
||||
{
|
||||
id: "ssl-tls-monitoring",
|
||||
title: "SSL/TLS monitoring",
|
||||
description: "Certificate tracking and error handling.",
|
||||
keywords: ["ssl", "tls", "certificate", "https", "security", "expiry"],
|
||||
},
|
||||
{
|
||||
id: "uptime-metrics",
|
||||
title: "Understanding uptime metrics",
|
||||
description: "Response time, status codes, and uptime percentage.",
|
||||
keywords: [
|
||||
"metrics",
|
||||
"response",
|
||||
"time",
|
||||
"status",
|
||||
"code",
|
||||
"uptime",
|
||||
"percentage",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "pagespeed",
|
||||
title: "PageSpeed monitoring",
|
||||
description: "Track website performance and Core Web Vitals.",
|
||||
icon: "Gauge",
|
||||
articleCount: 5,
|
||||
articles: [
|
||||
{
|
||||
id: "creating-pagespeed",
|
||||
title: "Creating PageSpeed monitors",
|
||||
description: "Setup and configuration for performance monitoring.",
|
||||
keywords: ["pagespeed", "create", "setup", "performance", "lighthouse"],
|
||||
},
|
||||
{
|
||||
id: "understanding-scores",
|
||||
title: "Understanding scores",
|
||||
description: "Performance, Accessibility, SEO, and Best Practices explained.",
|
||||
keywords: ["score", "performance", "accessibility", "seo", "best", "practices"],
|
||||
},
|
||||
{
|
||||
id: "core-web-vitals",
|
||||
title: "Core Web Vitals explained",
|
||||
description: "LCP, FCP, CLS, TBT, and Speed Index.",
|
||||
keywords: ["core", "web", "vitals", "lcp", "fcp", "cls", "tbt", "speed", "index"],
|
||||
},
|
||||
{
|
||||
id: "pagespeed-api",
|
||||
title: "PageSpeed API configuration",
|
||||
description: "Setting up your Google PageSpeed API key.",
|
||||
keywords: ["api", "key", "google", "pagespeed", "configure", "setup"],
|
||||
},
|
||||
{
|
||||
id: "performance-trends",
|
||||
title: "Performance trends",
|
||||
description: "Tracking improvements over time.",
|
||||
keywords: ["trend", "history", "improvement", "tracking", "chart"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "infrastructure",
|
||||
title: "Infrastructure monitoring",
|
||||
description: "Monitor server hardware with Capture agent.",
|
||||
icon: "Server",
|
||||
articleCount: 6,
|
||||
articles: [
|
||||
{
|
||||
id: "installing-capture",
|
||||
title: "Installing Capture agent",
|
||||
description: "Setup the monitoring agent on your servers.",
|
||||
keywords: ["install", "capture", "agent", "setup", "server"],
|
||||
},
|
||||
{
|
||||
id: "cpu-monitoring",
|
||||
title: "CPU monitoring",
|
||||
description: "Usage, temperature, and core metrics.",
|
||||
keywords: ["cpu", "processor", "usage", "temperature", "cores"],
|
||||
},
|
||||
{
|
||||
id: "memory-monitoring",
|
||||
title: "Memory monitoring",
|
||||
description: "RAM usage and availability tracking.",
|
||||
keywords: ["memory", "ram", "usage", "available", "free"],
|
||||
},
|
||||
{
|
||||
id: "disk-monitoring",
|
||||
title: "Disk monitoring",
|
||||
description: "Storage space and read/write speeds.",
|
||||
keywords: ["disk", "storage", "space", "read", "write", "speed"],
|
||||
},
|
||||
{
|
||||
id: "network-monitoring",
|
||||
title: "Network monitoring",
|
||||
description: "Bandwidth and packet statistics.",
|
||||
keywords: ["network", "bandwidth", "packet", "interface", "traffic"],
|
||||
},
|
||||
{
|
||||
id: "hardware-thresholds",
|
||||
title: "Setting hardware thresholds",
|
||||
description: "Alert when resources become critical.",
|
||||
keywords: ["threshold", "alert", "critical", "warning", "limit"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "incidents",
|
||||
title: "Incidents & alerts",
|
||||
description: "Respond to downtime and issues.",
|
||||
icon: "AlertTriangle",
|
||||
articleCount: 4,
|
||||
articles: [
|
||||
{
|
||||
id: "understanding-incidents",
|
||||
title: "Understanding incidents",
|
||||
description: "How incidents are detected and created.",
|
||||
keywords: ["incident", "detection", "downtime", "outage", "alert"],
|
||||
},
|
||||
{
|
||||
id: "incident-timeline",
|
||||
title: "Incident timeline",
|
||||
description: "Tracking start, duration, and resolution.",
|
||||
keywords: ["timeline", "duration", "start", "end", "resolution"],
|
||||
},
|
||||
{
|
||||
id: "resolving-incidents",
|
||||
title: "Resolving incidents",
|
||||
description: "Manual vs automatic resolution.",
|
||||
keywords: ["resolve", "manual", "automatic", "acknowledge", "fix"],
|
||||
},
|
||||
{
|
||||
id: "incident-history",
|
||||
title: "Incident history and analysis",
|
||||
description: "Learn from past outages.",
|
||||
keywords: ["history", "analysis", "report", "past", "outage"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "checks",
|
||||
title: "Checks",
|
||||
description: "View monitoring check results and history.",
|
||||
icon: "FileText",
|
||||
articleCount: 3,
|
||||
articles: [
|
||||
{
|
||||
id: "understanding-checks",
|
||||
title: "Understanding checks",
|
||||
description: "What checks are and how they work.",
|
||||
keywords: ["check", "result", "monitoring", "status", "response"],
|
||||
},
|
||||
{
|
||||
id: "check-details",
|
||||
title: "Check details and timing",
|
||||
description: "Response times, status codes, and timing breakdown.",
|
||||
keywords: ["response", "time", "timing", "breakdown", "dns", "tls"],
|
||||
},
|
||||
{
|
||||
id: "filtering-checks",
|
||||
title: "Filtering and date ranges",
|
||||
description: "Filter checks by date range and status.",
|
||||
keywords: ["filter", "date", "range", "pagination", "sort"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "logs",
|
||||
title: "Logs & diagnostics",
|
||||
description: "System logs, queue monitoring, and diagnostics.",
|
||||
icon: "Database",
|
||||
articleCount: 3,
|
||||
articles: [
|
||||
{
|
||||
id: "application-logs",
|
||||
title: "Application logs",
|
||||
description: "View and filter system logs by level.",
|
||||
keywords: ["log", "error", "warning", "info", "debug", "filter"],
|
||||
},
|
||||
{
|
||||
id: "queue-monitoring",
|
||||
title: "Queue monitoring",
|
||||
description: "Monitor background jobs and queue status.",
|
||||
keywords: ["queue", "job", "background", "bullmq", "worker"],
|
||||
},
|
||||
{
|
||||
id: "system-diagnostics",
|
||||
title: "System diagnostics",
|
||||
description: "CPU, memory, and heap usage metrics.",
|
||||
keywords: ["diagnostics", "cpu", "memory", "heap", "performance"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "notifications",
|
||||
title: "Notifications",
|
||||
description: "Get alerted when things go wrong.",
|
||||
icon: "Bell",
|
||||
articleCount: 7,
|
||||
articles: [
|
||||
{
|
||||
id: "email-notifications",
|
||||
title: "Email notifications",
|
||||
description: "SMTP setup and configuration.",
|
||||
keywords: ["email", "smtp", "mail", "setup", "configure"],
|
||||
},
|
||||
{
|
||||
id: "slack-integration",
|
||||
title: "Slack integration",
|
||||
description: "Webhook setup for Slack channels.",
|
||||
keywords: ["slack", "webhook", "integration", "channel"],
|
||||
},
|
||||
{
|
||||
id: "discord-integration",
|
||||
title: "Discord integration",
|
||||
description: "Channel notifications via webhooks.",
|
||||
keywords: ["discord", "webhook", "integration", "channel"],
|
||||
},
|
||||
{
|
||||
id: "pagerduty-integration",
|
||||
title: "PagerDuty integration",
|
||||
description: "On-call management integration.",
|
||||
keywords: ["pagerduty", "oncall", "integration", "escalation"],
|
||||
},
|
||||
{
|
||||
id: "webhook-notifications",
|
||||
title: "Webhook notifications",
|
||||
description: "Custom HTTP callbacks for alerts.",
|
||||
keywords: ["webhook", "http", "callback", "custom", "api"],
|
||||
},
|
||||
{
|
||||
id: "matrix-integration",
|
||||
title: "Matrix integration",
|
||||
description: "Decentralized messaging notifications.",
|
||||
keywords: ["matrix", "messaging", "integration", "homeserver"],
|
||||
},
|
||||
{
|
||||
id: "testing-notifications",
|
||||
title: "Testing notifications",
|
||||
description: "Verify your notification setup works.",
|
||||
keywords: ["test", "verify", "notification", "check"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "status-pages",
|
||||
title: "Status pages",
|
||||
description: "Share uptime with your users.",
|
||||
icon: "Wifi",
|
||||
articleCount: 5,
|
||||
articles: [
|
||||
{
|
||||
id: "creating-status-page",
|
||||
title: "Creating a status page",
|
||||
description: "Public-facing uptime dashboard setup.",
|
||||
keywords: ["status", "page", "create", "public", "dashboard"],
|
||||
},
|
||||
{
|
||||
id: "customizing-appearance",
|
||||
title: "Customizing appearance",
|
||||
description: "Logo, colors, and branding options.",
|
||||
keywords: ["customize", "logo", "color", "brand", "style"],
|
||||
},
|
||||
{
|
||||
id: "adding-monitors",
|
||||
title: "Adding monitors to status page",
|
||||
description: "Main monitors and sub-components.",
|
||||
keywords: ["add", "monitor", "component", "group"],
|
||||
},
|
||||
{
|
||||
id: "display-options",
|
||||
title: "Display options",
|
||||
description: "Charts, uptime percentage, and login links.",
|
||||
keywords: ["display", "chart", "uptime", "percentage", "option"],
|
||||
},
|
||||
{
|
||||
id: "sharing",
|
||||
title: "Sharing your status page",
|
||||
description: "Share your status page URL with users.",
|
||||
keywords: ["share", "url", "public", "link", "access"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "maintenance",
|
||||
title: "Maintenance windows",
|
||||
description: "Schedule planned downtime.",
|
||||
icon: "Wrench",
|
||||
articleCount: 3,
|
||||
articles: [
|
||||
{
|
||||
id: "creating-maintenance",
|
||||
title: "Creating maintenance windows",
|
||||
description: "Suppress alerts during planned maintenance.",
|
||||
keywords: ["maintenance", "create", "schedule", "suppress", "alert"],
|
||||
},
|
||||
{
|
||||
id: "recurring-maintenance",
|
||||
title: "One-time vs recurring",
|
||||
description: "Different scheduling options.",
|
||||
keywords: ["recurring", "one-time", "schedule", "repeat", "frequency"],
|
||||
},
|
||||
{
|
||||
id: "managing-maintenance",
|
||||
title: "Managing active maintenance",
|
||||
description: "Edit, extend, or cancel maintenance windows.",
|
||||
keywords: ["manage", "edit", "cancel", "extend", "active"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "team-management",
|
||||
title: "Team management",
|
||||
description: "Collaborate with your team.",
|
||||
icon: "Users",
|
||||
articleCount: 3,
|
||||
articles: [
|
||||
{
|
||||
id: "inviting-members",
|
||||
title: "Inviting team members",
|
||||
description: "Send email invitations to your team.",
|
||||
keywords: ["invite", "team", "member", "email", "add"],
|
||||
},
|
||||
{
|
||||
id: "user-roles",
|
||||
title: "User roles",
|
||||
description: "Admin vs User permissions explained.",
|
||||
keywords: ["role", "admin", "user", "permission", "access"],
|
||||
},
|
||||
{
|
||||
id: "managing-access",
|
||||
title: "Managing team access",
|
||||
description: "Add and remove team members.",
|
||||
keywords: ["manage", "access", "remove", "team", "member"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "settings",
|
||||
title: "Settings & configuration",
|
||||
description: "Customize your Checkmate instance.",
|
||||
icon: "Settings",
|
||||
articleCount: 6,
|
||||
articles: [
|
||||
{
|
||||
id: "account-settings",
|
||||
title: "Account settings",
|
||||
description: "Manage your profile, password, and account.",
|
||||
keywords: ["account", "profile", "password", "avatar", "photo", "delete"],
|
||||
},
|
||||
{
|
||||
id: "email-configuration",
|
||||
title: "Email configuration",
|
||||
description: "SMTP server setup for notifications.",
|
||||
keywords: ["email", "smtp", "configure", "server", "mail"],
|
||||
},
|
||||
{
|
||||
id: "global-thresholds",
|
||||
title: "Global thresholds",
|
||||
description: "Default alert levels for all monitors.",
|
||||
keywords: ["threshold", "global", "default", "alert", "level"],
|
||||
},
|
||||
{
|
||||
id: "appearance",
|
||||
title: "Appearance settings",
|
||||
description: "Theme and display preferences.",
|
||||
keywords: ["appearance", "theme", "dark", "light", "display", "mode"],
|
||||
},
|
||||
{
|
||||
id: "timezone-settings",
|
||||
title: "Timezone settings",
|
||||
description: "Display preferences for dates and times.",
|
||||
keywords: ["timezone", "time", "date", "display", "preference"],
|
||||
},
|
||||
{
|
||||
id: "export-monitors",
|
||||
title: "Export monitors",
|
||||
description: "Download your monitors as JSON.",
|
||||
keywords: ["export", "download", "backup", "json", "monitors"],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Fast Finds - Popular/quick access articles
|
||||
export const fastFinds = [
|
||||
{
|
||||
id: "ff-1",
|
||||
title: "Creating your first monitor",
|
||||
collectionId: "getting-started",
|
||||
articleId: "quick-start",
|
||||
},
|
||||
{
|
||||
id: "ff-2",
|
||||
title: "Setting up email notifications",
|
||||
collectionId: "notifications",
|
||||
articleId: "email-notifications",
|
||||
},
|
||||
{
|
||||
id: "ff-3",
|
||||
title: "Installing Capture agent",
|
||||
collectionId: "infrastructure",
|
||||
articleId: "installing-capture",
|
||||
},
|
||||
{
|
||||
id: "ff-4",
|
||||
title: "Creating a status page",
|
||||
collectionId: "status-pages",
|
||||
articleId: "creating-status-page",
|
||||
},
|
||||
{
|
||||
id: "ff-5",
|
||||
title: "Understanding Core Web Vitals",
|
||||
collectionId: "pagespeed",
|
||||
articleId: "core-web-vitals",
|
||||
},
|
||||
];
|
||||
|
||||
// Helper functions
|
||||
export const getCollection = (collectionId) => {
|
||||
return collections.find((c) => c.id === collectionId);
|
||||
};
|
||||
|
||||
export const getArticle = (collectionId, articleId) => {
|
||||
const collection = getCollection(collectionId);
|
||||
return collection?.articles.find((a) => a.id === articleId);
|
||||
};
|
||||
|
||||
export const getTotalArticleCount = () => {
|
||||
return collections.reduce((sum, collection) => sum + collection.articleCount, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {Object} SearchResult
|
||||
* @property {string} collectionId
|
||||
* @property {string} collectionTitle
|
||||
* @property {string} articleId
|
||||
* @property {string} articleTitle
|
||||
* @property {string} articleDescription
|
||||
* @property {'title' | 'description' | 'keyword'} matchType
|
||||
*/
|
||||
|
||||
/**
|
||||
* Search articles by query
|
||||
* @param {string} query
|
||||
* @returns {SearchResult[]}
|
||||
*/
|
||||
export const searchArticles = (query) => {
|
||||
if (!query || query.trim().length < 2) return [];
|
||||
|
||||
const normalizedQuery = query.toLowerCase().trim();
|
||||
const results = [];
|
||||
|
||||
for (const collection of collections) {
|
||||
for (const article of collection.articles) {
|
||||
let matchType = null;
|
||||
|
||||
// Check title match (highest priority)
|
||||
if (article.title.toLowerCase().includes(normalizedQuery)) {
|
||||
matchType = "title";
|
||||
}
|
||||
// Check description match
|
||||
else if (article.description.toLowerCase().includes(normalizedQuery)) {
|
||||
matchType = "description";
|
||||
}
|
||||
// Check keywords match
|
||||
else if (
|
||||
article.keywords.some((kw) => kw.toLowerCase().includes(normalizedQuery))
|
||||
) {
|
||||
matchType = "keyword";
|
||||
}
|
||||
|
||||
if (matchType) {
|
||||
results.push({
|
||||
collectionId: collection.id,
|
||||
collectionTitle: collection.title,
|
||||
articleId: article.id,
|
||||
articleTitle: article.title,
|
||||
articleDescription: article.description,
|
||||
matchType,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by match type priority: title > description > keyword
|
||||
return results.sort((a, b) => {
|
||||
const priority = { title: 0, description: 1, keyword: 2 };
|
||||
return priority[a.matchType] - priority[b.matchType];
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import SidebarWrapper from "./SidebarWrapper";
|
||||
import {
|
||||
UserGuideSidebarProvider,
|
||||
useUserGuideSidebarContext,
|
||||
TAB_BAR_WIDTH,
|
||||
DEFAULT_CONTENT_WIDTH,
|
||||
} from "./UserGuideSidebarContext";
|
||||
|
||||
// Main component that wraps SidebarWrapper with context consumer
|
||||
const UserGuideSidebar = () => {
|
||||
const { isOpen, open, close } = useUserGuideSidebarContext();
|
||||
|
||||
return (
|
||||
<SidebarWrapper
|
||||
isOpen={isOpen}
|
||||
onOpen={open}
|
||||
onClose={close}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
UserGuideSidebar,
|
||||
UserGuideSidebarProvider,
|
||||
useUserGuideSidebarContext,
|
||||
TAB_BAR_WIDTH,
|
||||
DEFAULT_CONTENT_WIDTH,
|
||||
};
|
||||
|
||||
export default UserGuideSidebar;
|
||||
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* Design Tokens for UserGuide
|
||||
* Supports both light and dark themes via MUI theme integration
|
||||
*/
|
||||
|
||||
// Typography (theme-independent)
|
||||
export const typography = {
|
||||
fontFamily: {
|
||||
sans: "'Inter', system-ui, -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif",
|
||||
mono: "'Geist Mono', 'Fira Code', 'Consolas', monospace",
|
||||
},
|
||||
fontSize: {
|
||||
xs: 12,
|
||||
sm: 12,
|
||||
base: 13,
|
||||
md: 14,
|
||||
lg: 15,
|
||||
xl: 16,
|
||||
"2xl": 20,
|
||||
"3xl": 22,
|
||||
"4xl": 26,
|
||||
"5xl": 30,
|
||||
},
|
||||
lineHeight: {
|
||||
tight: 1.2,
|
||||
snug: 1.3,
|
||||
normal: 1.4,
|
||||
relaxed: 1.5,
|
||||
},
|
||||
fontWeight: {
|
||||
normal: 400,
|
||||
medium: 500,
|
||||
semibold: 600,
|
||||
bold: 700,
|
||||
},
|
||||
};
|
||||
|
||||
// Spacing (theme-independent)
|
||||
export const spacing = {
|
||||
xs: "4px",
|
||||
sm: "8px",
|
||||
md: "12px",
|
||||
lg: "16px",
|
||||
xl: "24px",
|
||||
"2xl": "32px",
|
||||
"3xl": "40px",
|
||||
"4xl": "48px",
|
||||
};
|
||||
|
||||
// Brand colors (theme-independent) - Checkmate blue
|
||||
export const brandColors = {
|
||||
primary: "#1570EF",
|
||||
primaryDark: "#1054B0",
|
||||
primaryLight: "#5BA3F5",
|
||||
success: "#17B26A",
|
||||
warning: "#F79009",
|
||||
error: "#F04438",
|
||||
};
|
||||
|
||||
/**
|
||||
* Get theme-aware colors based on MUI theme mode
|
||||
* @param {'light' | 'dark'} mode - The current theme mode
|
||||
* @returns {Object} Theme-aware color tokens
|
||||
*/
|
||||
export const getColors = (mode) => {
|
||||
const isDark = mode === "dark";
|
||||
|
||||
return {
|
||||
text: {
|
||||
primary: isDark ? "#ffffff" : "#1C2130",
|
||||
secondary: isDark ? "#d0d5dd" : "#475467",
|
||||
muted: isDark ? "#98a2b3" : "#667085",
|
||||
white: "#ffffff",
|
||||
whiteMuted: "rgba(255, 255, 255, 0.8)",
|
||||
},
|
||||
brand: brandColors,
|
||||
border: {
|
||||
default: isDark ? "#344054" : "#d0d5dd",
|
||||
},
|
||||
background: {
|
||||
white: isDark ? "#1a1a1a" : "#ffffff",
|
||||
alt: isDark ? "#0d0d0d" : "#f9fafb",
|
||||
code: isDark ? "#1C2130" : "#f3f4f6",
|
||||
sidebar: isDark ? "#101010" : "#ffffff",
|
||||
hover: isDark ? "#2a2a2a" : "#f3f4f6",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get theme-aware border styles
|
||||
* @param {'light' | 'dark'} mode - The current theme mode
|
||||
* @returns {Object} Border styles
|
||||
*/
|
||||
export const getBorder = (mode) => {
|
||||
const colors = getColors(mode);
|
||||
return {
|
||||
radius: "4px",
|
||||
default: `1px solid ${colors.border.default}`,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get theme-aware image styles
|
||||
* @param {'light' | 'dark'} mode - The current theme mode
|
||||
* @returns {Object} Image styles
|
||||
*/
|
||||
export const getImageStyles = (mode) => {
|
||||
const colors = getColors(mode);
|
||||
const border = getBorder(mode);
|
||||
const isDark = mode === "dark";
|
||||
|
||||
return {
|
||||
image: {
|
||||
width: "100%",
|
||||
borderRadius: border.radius,
|
||||
border: border.default,
|
||||
display: "block",
|
||||
boxShadow: isDark ? "0 2px 8px rgba(0, 0, 0, 0.3)" : "0 2px 8px rgba(0, 0, 0, 0.1)",
|
||||
},
|
||||
caption: {
|
||||
fontSize: 10,
|
||||
color: colors.text.muted,
|
||||
textAlign: "center",
|
||||
fontStyle: "italic",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get theme-aware chip styles
|
||||
* @param {'light' | 'dark'} mode - The current theme mode
|
||||
* @returns {Object} Chip styles
|
||||
*/
|
||||
export const getChipStyles = (mode) => {
|
||||
const isDark = mode === "dark";
|
||||
|
||||
return {
|
||||
url: {
|
||||
backgroundColor: isDark ? "rgba(21, 112, 239, 0.2)" : "rgba(21, 112, 239, 0.1)",
|
||||
color: isDark ? "#5BA3F5" : "#1570EF",
|
||||
padding: "2px 8px",
|
||||
borderRadius: "4px",
|
||||
fontFamily: typography.fontFamily.mono,
|
||||
fontSize: "0.9em",
|
||||
},
|
||||
code: {
|
||||
backgroundColor: isDark ? "#2d2d2d" : "#f3f4f6",
|
||||
color: isDark ? "#d0d5dd" : "#1C2130",
|
||||
padding: "2px 8px",
|
||||
borderRadius: "4px",
|
||||
fontFamily: typography.fontFamily.mono,
|
||||
fontSize: "0.9em",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get theme-aware component styles
|
||||
* @param {'light' | 'dark'} mode - The current theme mode
|
||||
* @returns {Object} Component styles
|
||||
*/
|
||||
export const getComponentStyles = (mode) => {
|
||||
const colors = getColors(mode);
|
||||
const border = getBorder(mode);
|
||||
|
||||
return {
|
||||
card: {
|
||||
backgroundColor: colors.background.white,
|
||||
border: border.default,
|
||||
borderRadius: border.radius,
|
||||
},
|
||||
heading: {
|
||||
h2: {
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize["2xl"],
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.text.primary,
|
||||
lineHeight: typography.lineHeight.normal,
|
||||
},
|
||||
h3: {
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.lg,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.text.primary,
|
||||
lineHeight: typography.lineHeight.normal,
|
||||
},
|
||||
},
|
||||
paragraph: {
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.text.primary,
|
||||
lineHeight: typography.lineHeight.relaxed,
|
||||
},
|
||||
listItem: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.text.primary,
|
||||
lineHeight: typography.lineHeight.snug,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// Legacy exports for backwards compatibility (dark mode defaults)
|
||||
// These will be replaced by useUserGuideTheme hook usage
|
||||
export const colors = getColors("dark");
|
||||
export const border = getBorder("dark");
|
||||
export const imageStyles = getImageStyles("dark");
|
||||
export const chipStyles = getChipStyles("dark");
|
||||
export const componentStyles = getComponentStyles("dark");
|
||||
|
||||
// Export everything as a single theme object (dark mode default)
|
||||
export const theme = {
|
||||
colors,
|
||||
typography,
|
||||
spacing,
|
||||
border,
|
||||
imageStyles,
|
||||
chipStyles,
|
||||
componentStyles,
|
||||
};
|
||||
|
||||
export default theme;
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Hook to get theme-aware styles for UserGuide components
|
||||
* Uses MUI's theme system from Checkmate
|
||||
*/
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { typography, spacing, brandColors } from "./theme";
|
||||
|
||||
/**
|
||||
* Returns theme-aware styles for UserGuide components
|
||||
*/
|
||||
export const useUserGuideTheme = () => {
|
||||
const theme = useTheme();
|
||||
const isDark = theme.palette.mode === "dark";
|
||||
|
||||
const colors = {
|
||||
text: {
|
||||
primary: theme.palette.primary.contrastText,
|
||||
secondary: theme.palette.primary.contrastTextSecondary,
|
||||
muted: theme.palette.primary.contrastTextTertiary,
|
||||
white: "#ffffff",
|
||||
},
|
||||
brand: brandColors,
|
||||
border: {
|
||||
default: theme.palette.primary.lowContrast,
|
||||
},
|
||||
background: {
|
||||
white: isDark ? "#1a1a1a" : "#ffffff",
|
||||
alt: theme.palette.primaryBackground.main,
|
||||
code: isDark ? "#1C2130" : "#f3f4f6",
|
||||
sidebar: theme.palette.primaryBackground.main,
|
||||
hover: isDark ? "#2a2a2a" : "#f3f4f6",
|
||||
},
|
||||
};
|
||||
|
||||
const border = {
|
||||
radius: "4px",
|
||||
default: `1px solid ${colors.border.default}`,
|
||||
};
|
||||
|
||||
const imageStyles = {
|
||||
image: {
|
||||
width: "100%",
|
||||
borderRadius: border.radius,
|
||||
border: border.default,
|
||||
display: "block",
|
||||
boxShadow: isDark ? "0 2px 8px rgba(0, 0, 0, 0.3)" : "0 2px 8px rgba(0, 0, 0, 0.1)",
|
||||
},
|
||||
caption: {
|
||||
fontSize: 10,
|
||||
color: colors.text.muted,
|
||||
textAlign: "center",
|
||||
fontStyle: "italic",
|
||||
},
|
||||
};
|
||||
|
||||
const chipStyles = {
|
||||
url: {
|
||||
backgroundColor: isDark ? "rgba(21, 112, 239, 0.2)" : "rgba(21, 112, 239, 0.1)",
|
||||
color: isDark ? "#5BA3F5" : "#1570EF",
|
||||
padding: "2px 8px",
|
||||
borderRadius: "4px",
|
||||
fontFamily: typography.fontFamily.mono,
|
||||
fontSize: "0.9em",
|
||||
},
|
||||
code: {
|
||||
backgroundColor: isDark ? "#2d2d2d" : "#f3f4f6",
|
||||
color: isDark ? "#d0d5dd" : "#1C2130",
|
||||
padding: "2px 8px",
|
||||
borderRadius: "4px",
|
||||
fontFamily: typography.fontFamily.mono,
|
||||
fontSize: "0.9em",
|
||||
},
|
||||
};
|
||||
|
||||
const componentStyles = {
|
||||
card: {
|
||||
backgroundColor: colors.background.white,
|
||||
border: border.default,
|
||||
borderRadius: border.radius,
|
||||
},
|
||||
heading: {
|
||||
h2: {
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize["2xl"],
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.text.primary,
|
||||
lineHeight: typography.lineHeight.normal,
|
||||
},
|
||||
h3: {
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.lg,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.text.primary,
|
||||
lineHeight: typography.lineHeight.normal,
|
||||
},
|
||||
},
|
||||
paragraph: {
|
||||
fontFamily: typography.fontFamily.sans,
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.text.primary,
|
||||
lineHeight: typography.lineHeight.relaxed,
|
||||
},
|
||||
listItem: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.text.primary,
|
||||
lineHeight: typography.lineHeight.snug,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
colors,
|
||||
typography,
|
||||
spacing,
|
||||
border,
|
||||
imageStyles,
|
||||
chipStyles,
|
||||
componentStyles,
|
||||
isDark,
|
||||
theme,
|
||||
};
|
||||
};
|
||||
|
||||
export default useUserGuideTheme;
|
||||
@@ -1,53 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Settings from "@/assets/icons/settings-bold.svg?react";
|
||||
|
||||
export type ActionMenuItem = {
|
||||
id: number | string;
|
||||
label: React.ReactNode;
|
||||
action: Function;
|
||||
closeMenu?: boolean;
|
||||
};
|
||||
|
||||
export const ActionsMenu = ({ items }: { items: ActionMenuItem[] }) => {
|
||||
const [anchorEl, setAnchorEl] = useState<null | any>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const handleClick = (e: React.MouseEvent<any>) => {
|
||||
e.stopPropagation();
|
||||
setAnchorEl(e.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = (e: React.MouseEvent<any>) => {
|
||||
e.stopPropagation();
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<IconButton onClick={handleClick}>
|
||||
<Settings />
|
||||
</IconButton>
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<MenuItem
|
||||
key={item.id}
|
||||
onClick={(e: React.MouseEvent<HTMLLIElement>) => {
|
||||
e.stopPropagation();
|
||||
if (item.closeMenu) handleClose(e);
|
||||
item.action();
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import LeftArrow from "@/assets/icons/left-arrow.svg?react";
|
||||
import LeftArrowDouble from "@/assets/icons/left-arrow-double.svg?react";
|
||||
import LeftArrowLong from "@/assets/icons/left-arrow-long.svg?react";
|
||||
|
||||
export const ArrowLeft = ({
|
||||
type,
|
||||
color = "#667085",
|
||||
...props
|
||||
}: {
|
||||
type?: string;
|
||||
color?: string | undefined;
|
||||
[key: string]: any;
|
||||
}) => {
|
||||
if (type === "double") {
|
||||
return (
|
||||
<LeftArrowDouble
|
||||
style={{ color }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
} else if (type === "long") {
|
||||
return (
|
||||
<LeftArrowLong
|
||||
style={{ color }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<LeftArrow
|
||||
style={{ color }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
import RightArrow from "@/assets/icons/right-arrow.svg?react";
|
||||
import RightArrowDouble from "@/assets/icons/right-arrow-double.svg?react";
|
||||
|
||||
export const ArrowRight = ({
|
||||
type,
|
||||
color = "#667085",
|
||||
...props
|
||||
}: {
|
||||
type?: string;
|
||||
color?: string | undefined;
|
||||
[key: string]: any;
|
||||
}) => {
|
||||
if (type === "double") {
|
||||
return (
|
||||
<RightArrowDouble
|
||||
style={{ color }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<RightArrow
|
||||
style={{ color }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import { HeaderAuth } from "@/Components/v2/Auth";
|
||||
import Logo from "@/assets/icons/checkmate-icon.svg?react";
|
||||
|
||||
import type { StackProps } from "@mui/material/Stack";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { Typography } from "@mui/material";
|
||||
|
||||
interface AuthBasePageProps extends StackProps {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AuthBasePage: React.FC<AuthBasePageProps> = ({
|
||||
children,
|
||||
title,
|
||||
subtitle,
|
||||
...props
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(10)}
|
||||
minHeight="100vh"
|
||||
{...props}
|
||||
>
|
||||
<HeaderAuth />
|
||||
<Stack
|
||||
alignItems="center"
|
||||
margin="auto"
|
||||
width="100%"
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Box
|
||||
width={{ xs: 60, sm: 70, md: 80 }}
|
||||
mb={theme.spacing(10)}
|
||||
>
|
||||
<Logo style={{ width: "100%", height: "100%" }} />
|
||||
</Box>
|
||||
<Typography variant="h1">{title}</Typography>
|
||||
<Typography variant="h1">{subtitle}</Typography>
|
||||
{children}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Logo from "@/assets/icons/checkmate-icon.svg?react";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LanguageSelector, ThemeSwitch } from "@/Components/v2/Inputs";
|
||||
|
||||
export const HeaderAuth = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Stack
|
||||
width={"100%"}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="flex-end"
|
||||
py={theme.spacing(4)}
|
||||
px={theme.spacing(12)}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<LanguageSelector />
|
||||
<ThemeSwitch color="red" />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
export { HeaderAuth } from "./HeaderAuth";
|
||||
export { AuthBasePage } from "./AuthBasePage";
|
||||
@@ -1,23 +0,0 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import type { SxProps } from "@mui/material/styles";
|
||||
|
||||
type BaseBoxProps = React.PropsWithChildren<{ sx?: SxProps }>;
|
||||
|
||||
export const BaseBox: React.FC<BaseBoxProps> = ({ children, sx }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,80 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { ErrorFallback, EmptyFallback } from "./Fallback";
|
||||
|
||||
import type { StackProps } from "@mui/material/Stack";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
interface BasePageProps extends StackProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const BasePage: React.FC<BasePageProps> = ({
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
spacing={theme.spacing(10)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
interface BasePageWithStatesProps extends StackProps {
|
||||
loading: boolean;
|
||||
error: any;
|
||||
items: any[];
|
||||
page: string;
|
||||
actionLink?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const isEmpty = (items: any[]) => {
|
||||
if (!items) return true;
|
||||
if (Array.isArray(items) && items.length === 0) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
export const BasePageWithStates: React.FC<BasePageWithStatesProps> = ({
|
||||
loading,
|
||||
error,
|
||||
items,
|
||||
page,
|
||||
actionLink,
|
||||
children,
|
||||
...props
|
||||
}: BasePageWithStatesProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorFallback
|
||||
title="Something went wrong..."
|
||||
subtitle="Please try again later"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isEmpty(items)) {
|
||||
return (
|
||||
<EmptyFallback
|
||||
page={page}
|
||||
title={t(`${page}Monitor.fallback.title`)}
|
||||
bullets={t(`${page}Monitor.fallback.checks`, { returnObjects: true })}
|
||||
actionButtonText={t(`${page}Monitor.fallback.actionButton`)}
|
||||
actionLink={actionLink || ""}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <BasePage {...props}>{children}</BasePage>;
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import CheckOutlined from "@/assets/icons/check-outlined.svg?react";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
export const BulletPointCheck = ({
|
||||
text,
|
||||
variant = "info",
|
||||
}: {
|
||||
text: string;
|
||||
noHighlightText?: string;
|
||||
variant?: "success" | "error" | "info";
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const colors: Record<string, string | undefined> = {
|
||||
success: theme.palette.success.main,
|
||||
error: theme.palette.error.main,
|
||||
info: theme.palette.primary.contrastTextSecondary,
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
className="check"
|
||||
gap={theme.spacing(6)}
|
||||
alignItems="center"
|
||||
>
|
||||
<CheckOutlined />
|
||||
<Typography
|
||||
component="span"
|
||||
color={
|
||||
variant === "info"
|
||||
? theme.palette.primary.contrastTextSecondary
|
||||
: colors[variant]
|
||||
}
|
||||
fontWeight={450}
|
||||
sx={{
|
||||
opacity: 0.9,
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
export const Dot = ({
|
||||
color = "gray",
|
||||
size = "4px",
|
||||
style,
|
||||
}: {
|
||||
color?: string;
|
||||
size?: string;
|
||||
style?: React.CSSProperties;
|
||||
}) => {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
content: '""',
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: color,
|
||||
opacity: 0.8,
|
||||
...style,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,158 +0,0 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import OutputAnimation from "@/assets/Animations/output.gif";
|
||||
import DarkmodeOutput from "@/assets/Animations/darkmodeOutput.gif";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { BulletPointCheck } from "@/Components/v2/DesignElements";
|
||||
import { Button } from "@/Components/v2/Inputs";
|
||||
|
||||
import { useNavigate } from "react-router";
|
||||
import { useMediaQuery } from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import type { BoxProps } from "@mui/material";
|
||||
|
||||
interface BaseFallbackProps extends BoxProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const BaseFallback: React.FC<BaseFallbackProps> = ({ children, ...props }) => {
|
||||
const theme = useTheme();
|
||||
const mode = useSelector((state: any) => state.ui.mode);
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
|
||||
return (
|
||||
<Box
|
||||
margin={isSmall ? "inherit" : "auto"}
|
||||
marginTop={isSmall ? "33%" : "auto"}
|
||||
width={{
|
||||
sm: "90%",
|
||||
md: "70%",
|
||||
lg: "50%",
|
||||
xl: "40%",
|
||||
}}
|
||||
padding={theme.spacing(16)}
|
||||
bgcolor={theme.palette.primary.main}
|
||||
position="relative"
|
||||
border={1}
|
||||
borderColor={theme.palette.primary.lowContrast}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
borderStyle: "dashed",
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<Stack
|
||||
alignItems="center"
|
||||
gap={theme.spacing(20)}
|
||||
sx={{
|
||||
width: "fit-content",
|
||||
margin: "auto",
|
||||
marginTop: "100px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src={mode === "light" ? OutputAnimation : DarkmodeOutput}
|
||||
bgcolor="transparent"
|
||||
alt="Loading animation"
|
||||
width="100%"
|
||||
sx={{
|
||||
zIndex: 1,
|
||||
border: "none",
|
||||
borderRadius: theme.spacing(8),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
alignItems="center"
|
||||
maxWidth={"300px"}
|
||||
zIndex={1}
|
||||
>
|
||||
{children}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const ErrorFallback = ({
|
||||
title,
|
||||
subtitle,
|
||||
}: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<BaseFallback>
|
||||
<Typography
|
||||
variant="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography>{subtitle}</Typography>
|
||||
</BaseFallback>
|
||||
);
|
||||
};
|
||||
|
||||
export const EmptyFallback = ({
|
||||
page,
|
||||
title,
|
||||
bullets,
|
||||
actionButtonText,
|
||||
actionLink,
|
||||
}: {
|
||||
page: string;
|
||||
title: string;
|
||||
bullets: any;
|
||||
actionButtonText: string;
|
||||
actionLink: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<BaseFallback>
|
||||
<Stack
|
||||
gap={theme.spacing(10)}
|
||||
zIndex={1}
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography
|
||||
component="h1"
|
||||
color={theme.palette.primary.contrastText}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Stack
|
||||
sx={{
|
||||
flexWrap: "wrap",
|
||||
gap: theme.spacing(2),
|
||||
maxWidth: { xs: "90%", md: "80%", lg: "75%" },
|
||||
}}
|
||||
>
|
||||
{bullets?.map((bullet: string, index: number) => (
|
||||
<BulletPointCheck
|
||||
text={bullet}
|
||||
key={`${(page + "Monitors").trim().split(" ")[0]}-${index}`}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={() => navigate(actionLink)}
|
||||
>
|
||||
{actionButtonText}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</BaseFallback>
|
||||
);
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
export const PulseDot = ({ color }: { color: string }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
width="26px"
|
||||
height="24px"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Box
|
||||
minWidth="18px"
|
||||
minHeight="18px"
|
||||
sx={{
|
||||
position: "relative",
|
||||
backgroundColor: color,
|
||||
borderRadius: "50%",
|
||||
"&::before": {
|
||||
content: `""`,
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: "inherit",
|
||||
borderRadius: "50%",
|
||||
animation: "ripple 1.8s ease-out infinite",
|
||||
},
|
||||
"&::after": {
|
||||
content: `""`,
|
||||
position: "absolute",
|
||||
width: "7px",
|
||||
height: "7px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: theme.palette.accent.contrastText,
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,68 +0,0 @@
|
||||
import { Fragment } from "react";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useMediaQuery } from "@mui/material";
|
||||
export const SplitBox = ({
|
||||
left,
|
||||
right,
|
||||
}: {
|
||||
left: React.ReactNode;
|
||||
right: React.ReactNode;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
return (
|
||||
<Stack
|
||||
direction={isSmall ? "column" : "row"}
|
||||
bgcolor={theme.palette.primary.main}
|
||||
border={1}
|
||||
borderColor={theme.palette.primary.lowContrast}
|
||||
borderRadius={theme.spacing(2)}
|
||||
>
|
||||
<Box
|
||||
padding={theme.spacing(15)}
|
||||
borderRight={isSmall ? 0 : 1}
|
||||
borderBottom={isSmall ? 1 : 0}
|
||||
borderColor={theme.palette.primary.lowContrast}
|
||||
flex={0.7}
|
||||
>
|
||||
{left}
|
||||
</Box>
|
||||
<Box
|
||||
flex={1}
|
||||
padding={theme.spacing(15)}
|
||||
>
|
||||
{right}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const ConfigBox = ({
|
||||
title,
|
||||
subtitle,
|
||||
rightContent,
|
||||
}: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
rightContent: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<SplitBox
|
||||
left={
|
||||
<Fragment>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography component="p">{subtitle}</Typography>
|
||||
</Fragment>
|
||||
}
|
||||
right={rightContent}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useMediaQuery } from "@mui/material";
|
||||
import type { PaletteKey } from "@/Utils/Theme/v2/theme";
|
||||
import { BaseBox } from "@/Components/v2/DesignElements";
|
||||
|
||||
type GradientBox = React.PropsWithChildren<{ palette?: PaletteKey }>;
|
||||
|
||||
export const GradientBox: React.FC<GradientBox> = ({ children, palette }) => {
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const bg = palette
|
||||
? `linear-gradient(to bottom right, ${theme.palette[palette].main} 30%, ${theme.palette[palette].lowContrast} 70%)`
|
||||
: `linear-gradient(340deg, ${theme.palette.tertiary.main} 10%, ${theme.palette.primary.main} 45%)`;
|
||||
|
||||
return (
|
||||
<BaseBox
|
||||
sx={{
|
||||
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
|
||||
width: isSmall
|
||||
? `calc(50% - (1 * ${theme.spacing(8)} / 2))`
|
||||
: `calc(25% - (3 * ${theme.spacing(8)} / 4))`,
|
||||
|
||||
background: bg,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
|
||||
type StatBoxProps = React.PropsWithChildren<{
|
||||
title: string;
|
||||
subtitle: string;
|
||||
palette?: PaletteKey;
|
||||
}>;
|
||||
|
||||
export const StatBox: React.FC<StatBoxProps> = ({
|
||||
title,
|
||||
subtitle,
|
||||
palette,
|
||||
children,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const textColor = palette ? theme.palette[palette].contrastText : "inherit";
|
||||
|
||||
return (
|
||||
<GradientBox palette={palette}>
|
||||
<Stack>
|
||||
<Typography color={textColor}>{title}</Typography>
|
||||
<Typography color={textColor}>{subtitle}</Typography>
|
||||
{children}
|
||||
</Stack>
|
||||
</GradientBox>
|
||||
);
|
||||
};
|
||||
@@ -1,100 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Box from "@mui/material/Box";
|
||||
import { BaseBox } from "@/Components/v2/DesignElements";
|
||||
import Background from "@/assets/Images/background-grid.svg?react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
type StatusBoxProps = React.PropsWithChildren<{}>;
|
||||
|
||||
export const BGBox: React.FC<StatusBoxProps> = ({ children }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<BaseBox
|
||||
sx={{
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
flex: 1,
|
||||
padding: theme.spacing(8),
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
position="absolute"
|
||||
top="-10%"
|
||||
left="5%"
|
||||
>
|
||||
<Background />
|
||||
</Box>
|
||||
{children}
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
|
||||
const StatusBox = ({
|
||||
label,
|
||||
n,
|
||||
color,
|
||||
}: {
|
||||
label: string;
|
||||
n: number;
|
||||
color: string | undefined;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<BGBox>
|
||||
<Stack spacing={theme.spacing(8)}>
|
||||
<Typography
|
||||
variant={"h2"}
|
||||
textTransform="uppercase"
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h1"
|
||||
color={color}
|
||||
>
|
||||
{n}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</BGBox>
|
||||
);
|
||||
};
|
||||
|
||||
export const UpStatusBox = ({ n }: { n: number }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<StatusBox
|
||||
label={t("monitorStatus.up")}
|
||||
n={n}
|
||||
color={theme.palette.success.lowContrast}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DownStatusBox = ({ n }: { n: number }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<StatusBox
|
||||
label={t("monitorStatus.down")}
|
||||
n={n}
|
||||
color={theme.palette.error.lowContrast}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const PausedStatusBox = ({ n }: { n: number }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<StatusBox
|
||||
label={t("monitorStatus.paused")}
|
||||
n={n}
|
||||
color={theme.palette.warning.lowContrast}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import { BaseBox } from "@/Components/v2/DesignElements";
|
||||
import type { MonitorStatus } from "@/Types/Monitor";
|
||||
|
||||
import { getStatusPalette } from "@/Utils/MonitorUtils";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
export const StatusLabel = ({
|
||||
status,
|
||||
isActive,
|
||||
}: {
|
||||
status: MonitorStatus;
|
||||
isActive?: boolean;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const palette = getStatusPalette(status);
|
||||
const transformedText = status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
|
||||
|
||||
return (
|
||||
<BaseBox
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: theme.spacing(3, 5),
|
||||
color: theme.palette[palette].main,
|
||||
borderColor: theme.palette[palette].lowContrast,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
width={7}
|
||||
height={7}
|
||||
bgcolor={theme.palette[palette].lowContrast}
|
||||
borderRadius="50%"
|
||||
marginRight="5px"
|
||||
/>
|
||||
{isActive === false ? "Paused" : transformedText}
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
@@ -1,215 +0,0 @@
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableContainer from "@mui/material/TableContainer";
|
||||
import TableHead from "@mui/material/TableHead";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import LastPageIcon from "@mui/icons-material/LastPage";
|
||||
import FirstPageIcon from "@mui/icons-material/FirstPage";
|
||||
import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft";
|
||||
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight";
|
||||
|
||||
import Box from "@mui/material/Box";
|
||||
import TablePagination from "@mui/material/TablePagination";
|
||||
import type { TablePaginationProps } from "@mui/material/TablePagination";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useMediaQuery } from "@mui/material";
|
||||
export type Header<T> = {
|
||||
id: number | string;
|
||||
content: React.ReactNode;
|
||||
onClick?: (event: React.MouseEvent<HTMLTableCellElement | null>, row: T) => void;
|
||||
render: (row: T) => React.ReactNode;
|
||||
};
|
||||
|
||||
type DataTableProps<T extends { id?: string | number; _id?: string | number }> = {
|
||||
headers: Header<T>[];
|
||||
data: T[];
|
||||
onRowClick?: (row: T) => void;
|
||||
};
|
||||
|
||||
export function DataTable<
|
||||
T extends {
|
||||
id?: string | number;
|
||||
_id?: string | number;
|
||||
onRowClick?: (row: T) => void;
|
||||
},
|
||||
>({ headers, data, onRowClick }: DataTableProps<T>) {
|
||||
const theme = useTheme();
|
||||
if (data.length === 0 || headers.length === 0) return <div>No data</div>;
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table
|
||||
stickyHeader
|
||||
sx={{
|
||||
"&.MuiTable-root :is(.MuiTableHead-root, .MuiTableBody-root) :is(th, td)": {
|
||||
paddingLeft: theme.spacing(8),
|
||||
},
|
||||
"& :is(th)": {
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
color: theme.palette.secondary.contrastText,
|
||||
fontWeight: 600,
|
||||
},
|
||||
"& :is(td)": {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastTextSecondary,
|
||||
},
|
||||
"& .MuiTableBody-root .MuiTableRow-root:last-child .MuiTableCell-root": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{headers.map((header, idx) => {
|
||||
return (
|
||||
<TableCell
|
||||
align={idx === 0 ? "left" : "center"}
|
||||
key={header.id}
|
||||
>
|
||||
{header.content}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.map((row) => {
|
||||
const key = row.id || row._id || Math.random();
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={key}
|
||||
sx={{ cursor: onRowClick ? "pointer" : "default" }}
|
||||
onClick={() => (onRowClick ? onRowClick(row) : null)}
|
||||
>
|
||||
{headers.map((header, index) => {
|
||||
return (
|
||||
<TableCell
|
||||
align={index === 0 ? "left" : "center"}
|
||||
key={header.id}
|
||||
onClick={
|
||||
header.onClick ? (e) => header.onClick!(e, row) : undefined
|
||||
}
|
||||
>
|
||||
{header.render(row)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
interface TablePaginationActionsProps {
|
||||
count: number;
|
||||
page: number;
|
||||
rowsPerPage: number;
|
||||
onPageChange: (event: React.MouseEvent<HTMLButtonElement>, newPage: number) => void;
|
||||
}
|
||||
|
||||
function TablePaginationActions(props: TablePaginationActionsProps) {
|
||||
const theme = useTheme();
|
||||
const { count, page, rowsPerPage, onPageChange } = props;
|
||||
|
||||
const handleFirstPageButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
onPageChange(event, 0);
|
||||
};
|
||||
|
||||
const handleBackButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
onPageChange(event, page - 1);
|
||||
};
|
||||
|
||||
const handleNextButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
onPageChange(event, page + 1);
|
||||
};
|
||||
|
||||
const handleLastPageButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{ flexShrink: 0, ml: 2.5 }}
|
||||
className="table-pagination-actions"
|
||||
>
|
||||
<IconButton
|
||||
onClick={handleFirstPageButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="first page"
|
||||
>
|
||||
{theme.direction === "rtl" ? <LastPageIcon /> : <FirstPageIcon />}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleBackButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="previous page"
|
||||
>
|
||||
{theme.direction === "rtl" ? <KeyboardArrowRight /> : <KeyboardArrowLeft />}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleNextButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="next page"
|
||||
>
|
||||
{theme.direction === "rtl" ? <KeyboardArrowLeft /> : <KeyboardArrowRight />}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleLastPageButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="last page"
|
||||
>
|
||||
{theme.direction === "rtl" ? <FirstPageIcon /> : <LastPageIcon />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export const Pagination: React.FC<TablePaginationProps> = ({ ...props }) => {
|
||||
const isSmall = useMediaQuery((theme: any) => theme.breakpoints.down("sm"));
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<TablePagination
|
||||
ActionsComponent={TablePaginationActions}
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
{...props}
|
||||
sx={{
|
||||
"& .MuiTablePagination-toolbar": {
|
||||
display: isSmall ? "grid" : "flex",
|
||||
},
|
||||
"& .MuiTablePagination-selectLabel": {
|
||||
gridColumn: "1",
|
||||
gridRow: "1",
|
||||
justifySelf: "center",
|
||||
},
|
||||
"& .MuiTablePagination-select": {
|
||||
gridColumn: "2",
|
||||
gridRow: "1",
|
||||
justifySelf: "center",
|
||||
},
|
||||
"& .MuiTablePagination-displayedRows": {
|
||||
gridColumn: "2",
|
||||
gridRow: "2",
|
||||
justifySelf: "center ",
|
||||
},
|
||||
"& .table-pagination-actions": {
|
||||
gridColumn: "1",
|
||||
gridRow: "2",
|
||||
justifySelf: "center",
|
||||
},
|
||||
"& .MuiSelect-select": {
|
||||
border: 1,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Loaded 100 of 615 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user