Merge branch 'feature/page-manager' into 'main'
TASKS-1823 WebManager module See merge request crux/admin!30
@@ -1,3 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
.next
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
.git
|
.git
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ yarn-error.log
|
|||||||
# Platform specific
|
# Platform specific
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
/apps/backend/public/
|
/apps/backend/public/
|
||||||
/apps/backend/storage/
|
/apps/backend/storage/
|
||||||
/apps/backend/.adonisjs/
|
|
||||||
/apps/backend/database/schema.ts
|
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
npm run test:admin
|
npm run test:admin
|
||||||
|
npm run test:backend
|
||||||
|
npm run test:web
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
# Dependencies and generated output
|
|
||||||
**/node_modules/**
|
**/node_modules/**
|
||||||
**/build/**
|
**/build/**
|
||||||
**/dist/**
|
**/dist/**
|
||||||
**/coverage/**
|
**/coverage/**
|
||||||
|
**/.next/**
|
||||||
|
|
||||||
apps/backend/public/**
|
apps/backend/public/**
|
||||||
apps/backend/storage/**
|
apps/backend/storage/**
|
||||||
|
|
||||||
# Handlebars generator templates contain application syntax that is not valid HTML.
|
|
||||||
apps/backend/app/templates/**
|
apps/backend/app/templates/**
|
||||||
|
apps/backend/.adonisjs/**
|
||||||
|
apps/backend/database/schema.ts
|
||||||
|
|
||||||
|
apps/web/.next/**
|
||||||
|
|||||||
@@ -9,17 +9,56 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Added an `ApiException` base class whose self-handling renders module exceptions as
|
||||||
|
`{ message, code }` JSON responses, so exception classes no longer need registration in the
|
||||||
|
global HTTP exception handler.
|
||||||
- Added `@crux-kit/editor-tiptap`, a free, headless rich-text editor package built on TipTap. The `light` preset covers bold, italic, underline,
|
- Added `@crux-kit/editor-tiptap`, a free, headless rich-text editor package built on TipTap. The `light` preset covers bold, italic, underline,
|
||||||
strikethrough, headings, lists, blockquote, text alignment, links, and undo/redo, reading and
|
strikethrough, headings, lists, blockquote, text alignment, links, and undo/redo, reading and
|
||||||
writing content as plain HTML so it themes with the shared design-system tokens.
|
writing content as plain HTML so it themes with the shared design-system tokens.
|
||||||
- Added the `InputTiptapEditor` composed form field.
|
- Added the `InputTiptapEditor` composed form field.
|
||||||
|
- Added `useIntlDate` and `useIntlName` hooks under `apps/admin/src/app/i18n`, each returning a
|
||||||
|
formatter bound to the active UI language. Reading the language from context is what subscribes a
|
||||||
|
component to a language switch; components that read the `IntlDate`/`IntlName` singletons directly
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- Moved module generation fully server-side. The admin devtools now sends the raw generator config
|
||||||
|
(`{ name, table, generate, form, datatable }`) instead of pre-rendered source strings, and the
|
||||||
|
six frontend source builders were ported verbatim from the admin to
|
||||||
|
`apps/backend/app/services/module_generator`.
|
||||||
|
- Refactored `module_controller.ts` (~500 lines) into a thin HTTP controller delegating to focused
|
||||||
|
`module_generator` service modules: an orchestrator, schema-derived context building, naming and
|
||||||
|
DB-type helpers, a template renderer, and `builders/` + `writers/` subdirectories.
|
||||||
|
- Split the root `.env` into per-workspace env files (`apps/backend/.env`, `apps/admin/.env`,
|
||||||
|
`apps/web/.env`) so each app only ingests the variables it uses. Every `docker-compose.yml`
|
||||||
|
service now loads its own app's env file, and the admin (Vite) and web (Next.js) apps rely on
|
||||||
|
their frameworks' native env loading instead of custom root-file merging.
|
||||||
|
- Renamed cross-app URL env variables: `PAGE_MANAGER_API_URL` → `BACKEND_API_URL` in the web app
|
||||||
|
and `VITE_PAGE_MANAGER_WEB_URL` → `VITE_WEB_URL` in the admin app.
|
||||||
|
- File Manager exceptions now extend `ApiException`.
|
||||||
|
- Extracted the admin i18n context out of `I18nProvider` into `app/i18n/context.ts` and
|
||||||
|
`app/i18n/useLanguage.ts`, so components reach the active language through a side-effect-free
|
||||||
|
module instead of importing the provider and executing its module body.
|
||||||
|
- The selected language is now persisted to local storage as its code rather than as the whole
|
||||||
|
`Language` object.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed admin menu highlighting, which marked every ancestor entry active at once.
|
||||||
|
- Fixes the containing section staying collapsed when a sub-route is opened directly.
|
||||||
|
- Fixed backend functional test discovery: the `tests/**/*.spec.(ts|js)` globs in `adonisrc.ts`
|
||||||
|
are not valid for the native `fs.glob` used by Japa v5, so the suite silently ran zero tests.
|
||||||
|
- Fixed the backend `lint` script path (`../scripts` → `../../scripts`), which made
|
||||||
|
`npm run lint` fail before linting.
|
||||||
|
- Removed the stale Japa v1-era `@japa/spec-reporter` wiring from `tests/bootstrap.ts`; the
|
||||||
|
runner's built-in spec reporter is used instead.
|
||||||
|
- Renamed `app/exceptions/file_manager_exeptions.ts` to `file_manager_exceptions.ts`, fixing the
|
||||||
|
filename typo, and updated all imports.
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
|
- Removed the root `.env` file.
|
||||||
|
|
||||||
## [1.2.0] - 2026-07-17
|
## [1.2.0] - 2026-07-17
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -1,3 +1,18 @@
|
|||||||
|
# CRUX
|
||||||
|
|
||||||
|
A monorepo starter for products that need an admin panel, an API, and a public website. It ships an
|
||||||
|
AdonisJS backend, a React admin panel with a module generator, and a Next.js website that renders
|
||||||
|
the pages editors publish through the built-in CMS.
|
||||||
|
|
||||||
|
| App | Source | Dev port | What it is |
|
||||||
|
| --------------- | --------------- | -------- | ------------------------------------------------------ |
|
||||||
|
| Backend API | `apps/backend/` | 3333 | AdonisJS — data, auth, permissions, public content API |
|
||||||
|
| Admin panel | `apps/admin/` | 5173 | React + Vite — the internal UI |
|
||||||
|
| Public website | `apps/web/` | 3000 | Next.js — the site visitors see |
|
||||||
|
| Shared packages | `packages/` | — | UI, hooks, services, types, utils |
|
||||||
|
|
||||||
|
Full documentation lives in [`docs/`](./docs/Readme.md).
|
||||||
|
|
||||||
### Tech Stack
|
### Tech Stack
|
||||||
|
|
||||||
#### Backend
|
#### Backend
|
||||||
@@ -6,94 +21,157 @@
|
|||||||
- TypeScript
|
- TypeScript
|
||||||
- Lucid ORM
|
- Lucid ORM
|
||||||
- PostgreSQL / MySQL (configurable)
|
- PostgreSQL / MySQL (configurable)
|
||||||
|
- Redis, S3 / MinIO storage
|
||||||
|
|
||||||
#### Frontend
|
#### Admin panel
|
||||||
|
|
||||||
- React
|
- React
|
||||||
- TypeScript
|
- TypeScript
|
||||||
- Vite
|
- Vite
|
||||||
- React Query / Mobx / Axios
|
- React Query / MobX / Axios
|
||||||
- Component-based UI system
|
- Radix UI + Tailwind component system
|
||||||
|
- React Hook Form + Zod
|
||||||
|
|
||||||
|
#### Public website
|
||||||
|
|
||||||
|
- Next.js (App Router, React Server Components)
|
||||||
|
- TypeScript
|
||||||
|
- next-intl
|
||||||
|
- Tailwind CSS
|
||||||
|
|
||||||
### Getting Started
|
### Getting Started
|
||||||
|
|
||||||
#### Prerequisites
|
#### Prerequisites
|
||||||
|
|
||||||
Node.js >= v24.18.0
|
- Node.js `24.18.0` (see `.nvmrc`; the package manifest requires `>=24.0.0`)
|
||||||
npm >= 11.15.0
|
- npm `>=11.6.2` (the repo pins `npm@11.15.0`)
|
||||||
Database PostgreSQL
|
- PostgreSQL
|
||||||
|
|
||||||
#### Installation
|
#### Installation
|
||||||
|
|
||||||
```
|
```bash
|
||||||
git clone gitlab@git.rexsoftproduction.com:crux/admin.git
|
git clone gitlab@git.rexsoftproduction.com:crux/admin.git
|
||||||
cd crux
|
|
||||||
npm install
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd crux && npm install --force
|
||||||
|
```
|
||||||
|
|
||||||
|
> `--force` is required. `typescript-eslint` does not yet declare support for the TypeScript
|
||||||
|
> version this repo uses, so a plain `npm install` fails with `ERESOLVE`. Never substitute
|
||||||
|
> `--legacy-peer-deps` — it strips nested peer dependencies and breaks the backend at runtime. Use
|
||||||
|
> `npm ci --force` for a reproducible install, and `npm install <package> --force` when adding one.
|
||||||
|
> See [troubleshooting.md](./troubleshooting.md).
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
Environment variables are defined in root `.env`
|
Each workspace defines its own environment variables. Copy the workspace's `.env.example` to `.env` and fill in the values:
|
||||||
|
|
||||||
```
|
| Env file | Covers |
|
||||||
TZ=UTC
|
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
PORT=3333
|
| `apps/backend/.env` | Server (`PORT`, `APP_KEY`), database (`DB_*`), storage (`AWS_*`, `S3_*`), and the public site link (`WEB_APP_URL`, `WEB_REVALIDATE_SECRET`) used to purge its cache |
|
||||||
HOST=localhost
|
| `apps/admin/.env` | `VITE_API_URL` (backend), `VITE_WEB_URL` (public site, used for page previews), `VITE_TINYMCE_API_KEY` |
|
||||||
LOG_LEVEL=info
|
| `apps/web/.env` | `BACKEND_API_URL` (backend base URL for server rendering), `REVALIDATE_SECRET` (must match the backend's `WEB_REVALIDATE_SECRET`), `SITE_URL` (canonical origin) |
|
||||||
APP_KEY=
|
|
||||||
NODE_ENV=development
|
`docker-compose.yml` loads each service's env from the same per-app files. Inside Docker, service
|
||||||
DB_HOST=127.0.0.1
|
URLs must use compose service names (`http://backend:3333`), not `localhost`.
|
||||||
DB_PORT=5432
|
|
||||||
DB_USER=postgres
|
### Running
|
||||||
DB_PASSWORD=postgres
|
|
||||||
DB_DATABASE=adonis
|
#### Locally
|
||||||
VITE_API_URL=http://0.0.0.0:3333
|
|
||||||
VITE_TINYMCE_API_KEY=
|
| Command | Description |
|
||||||
|
| ----------------------- | ----------------------------------------------- |
|
||||||
|
| `npm run dev:backend` | Start the AdonisJS backend in development mode |
|
||||||
|
| `npm run dev:admin-app` | Start the React admin application |
|
||||||
|
| `npm run dev:web` | Start the Next.js web application |
|
||||||
|
| `npm run dev:all` | Start backend, admin, and web apps concurrently |
|
||||||
|
|
||||||
|
#### With Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
### Module Generator
|
Brings up everything: backend (3333), admin (5173), web (3000), PostgreSQL (5432), Redis (6379),
|
||||||
|
MinIO (9000 / console 9001) and pgAdmin (5050).
|
||||||
|
|
||||||
The module generator allows you to create full CRUD modules by selecting:
|
Two quirks worth knowing when a change appears not to take effect:
|
||||||
|
|
||||||
|
- The web container keeps `.next` on an anonymous volume that survives rebuilds. Clear it with
|
||||||
|
`docker compose up -d --force-recreate --renew-anon-volumes web`.
|
||||||
|
- Backend HMR does not always pick up edits on macOS (bind-mount file events); restart
|
||||||
|
`crux_backend_adonis` to load them.
|
||||||
|
|
||||||
|
### Scripts
|
||||||
|
|
||||||
|
#### Build
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
| ----------------------- | ------------------------------------------ |
|
||||||
|
| `npm run build:backend` | Build the backend application |
|
||||||
|
| `npm run build:admin` | Type-check and build the admin application |
|
||||||
|
| `npm run build:web` | Type-check and build the Next.js web app |
|
||||||
|
|
||||||
|
#### Test
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
| ---------------------- | ---------------------------------------------- |
|
||||||
|
| `npm run test:backend` | Backend functional tests (requires a database) |
|
||||||
|
| `npm run test:admin` | Admin unit tests (Vitest) |
|
||||||
|
| `npm run test:web` | Web app unit tests (Vitest) |
|
||||||
|
|
||||||
|
#### Lint and format
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
| ---------------------- | -------------------------- |
|
||||||
|
| `npm run lint` | ESLint across the monorepo |
|
||||||
|
| `npm run lint:fix` | ESLint with autofix |
|
||||||
|
| `npm run format` | Prettier write |
|
||||||
|
| `npm run format:check` | Prettier check, no writes |
|
||||||
|
|
||||||
|
#### Versions
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
| ----------------------- | ------------------------------------ |
|
||||||
|
| `npm run version:patch` | Update patch version of all packages |
|
||||||
|
| `npm run version:minor` | Update minor version of all packages |
|
||||||
|
| `npm run version:major` | Update major version of all packages |
|
||||||
|
|
||||||
|
> Versions are updated without creating git tags.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
#### Module Generator
|
||||||
|
|
||||||
|
Create full CRUD modules by selecting:
|
||||||
|
|
||||||
- Database table
|
- Database table
|
||||||
- Fields and types
|
- Fields and types
|
||||||
- Validation rules
|
- Validation rules
|
||||||
- List view configuration
|
- List view configuration
|
||||||
|
|
||||||
#### Generated Output
|
**Generated output** — Backend: controllers, validators, routes. Frontend: pages, forms, tables, API.
|
||||||
|
|
||||||
**Backend**: controllers, validators, routes
|
#### Authentication & Authorization
|
||||||
**Frontend**: pages, forms, tables, API
|
|
||||||
|
|
||||||
### Authentication & Authorization
|
- JWT / session-based authentication
|
||||||
|
|
||||||
- JWT / Session-based authentication
|
|
||||||
- Role & permission system
|
- Role & permission system
|
||||||
- Policy-based access control
|
- Policy-based access control
|
||||||
|
|
||||||
### Scripts
|
#### Content Management
|
||||||
|
|
||||||
#### Development
|
Web Manager is the built-in CMS: editors build localized pages from prebuilt blocks in the admin
|
||||||
|
panel, preview them privately, and publish to the Next.js site, which purges only the cache entries
|
||||||
|
the change affects. See [Web Manager](./docs/core/web-manager/Readme.md).
|
||||||
|
|
||||||
| Command | Description |
|
### Documentation
|
||||||
| ----------------------- | ---------------------------------------------- |
|
|
||||||
| `npm run dev:backend` | Start the AdonisJS backend in development mode |
|
|
||||||
| `npm run dev:admin-app` | Start the React admin application |
|
|
||||||
| `npm run dev:all` | Start backend and admin app concurrently |
|
|
||||||
|
|
||||||
#### Build
|
| Topic | Where |
|
||||||
|
| ------------------------ | ---------------------------------------------------- |
|
||||||
| Command | Description |
|
| Documentation home | [docs/Readme.md](./docs/Readme.md) |
|
||||||
| ----------------------- | ----------------------------- |
|
| Public website | [apps/web/Readme.md](./apps/web/Readme.md) |
|
||||||
| `npm run build:backend` | Build the backend application |
|
| Architecture | [docs/architecture/](./docs/architecture/Readme.md) |
|
||||||
| `npm run build:admin` | Build the admin application |
|
| Core modules | [docs/core/](./docs/Readme.md#core-modules) |
|
||||||
|
| Shared packages | [docs/packages/Readme.md](./docs/packages/Readme.md) |
|
||||||
#### Versions
|
| Install and setup issues | [troubleshooting.md](./troubleshooting.md) |
|
||||||
|
|
||||||
| Command | Description |
|
|
||||||
| --------------- | ------------------------------------ |
|
|
||||||
| `npm run patch` | Update Patch version of all packages |
|
|
||||||
| `npm run minor` | Update Minor version of all packages |
|
|
||||||
| `npm run major` | Update Major version of all packages |
|
|
||||||
|
|
||||||
> Versions are updated without creating git tags.
|
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
VITE_API_URL=http://0.0.0.0:3333
|
VITE_API_URL=http://localhost:3333
|
||||||
VITE_TINYMCE_API_KEY=
|
VITE_TINYMCE_API_KEY=
|
||||||
|
VITE_WEB_URL=http://localhost:3000
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Metronic 9 | All-in-One Tailwind based HTML/React/Next.js Template for Modern Web Applications
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- Node.js 22.14.0 or higher
|
|
||||||
- Npm or Yarn
|
|
||||||
- Tailwind CSS 4.x
|
|
||||||
- React 19.x
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" role="img">
|
||||||
|
<path
|
||||||
|
fill="#E05454"
|
||||||
|
d="m 7.7215115,7.835445 4.3544155,-2.303813 0,4.987339 L 7.7468165,13 7.7215165,7.784809"></path>
|
||||||
|
<path
|
||||||
|
fill="#C13383"
|
||||||
|
d="M 1.9240383,5.531658 6.2531745,7.886082 6.2278445,13 1.8987349,10.518997 1.9240399,5.556964"></path>
|
||||||
|
<path
|
||||||
|
fill="#792CA2"
|
||||||
|
d="M 6.9873475,1 1.8987345,3.936711 6.9873475,6.670899 12.101266,3.936711 6.9873475,1"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 503 B |
@@ -0,0 +1,11 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" role="img">
|
||||||
|
<path
|
||||||
|
fill="#1D4ED8"
|
||||||
|
d="m 7.7215115,7.835445 4.3544155,-2.303813 0,4.987339 L 7.7468165,13 7.7215165,7.784809"></path>
|
||||||
|
<path
|
||||||
|
fill="#0057B8"
|
||||||
|
d="M 1.9240383,5.531658 6.2531745,7.886082 6.2278445,13 1.8987349,10.518997 1.9240399,5.556964"></path>
|
||||||
|
<path
|
||||||
|
fill="#FFD700"
|
||||||
|
d="M 6.9873475,1 1.8987345,3.936711 6.9873475,6.670899 12.101266,3.936711 6.9873475,1"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 502 B |
@@ -0,0 +1,33 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300">
|
||||||
|
<rect width="400" height="300" fill="#f7f4ec"/>
|
||||||
|
<rect x="48" y="36" width="168" height="16" rx="8" fill="#1f291f"/>
|
||||||
|
<rect x="48" y="62" width="120" height="9" rx="4.5" fill="#b9bfab"/>
|
||||||
|
<g>
|
||||||
|
<rect x="284" y="36" width="68" height="24" rx="12" fill="none" stroke="#1f291f" stroke-width="1.5"/>
|
||||||
|
<rect x="298" y="45" width="40" height="6" rx="3" fill="#1f291f"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="48" y="96" width="94" height="156" rx="12" fill="#fffdf8" stroke="#d9dacf"/>
|
||||||
|
<rect x="62" y="114" width="56" height="10" rx="5" fill="#1f291f"/>
|
||||||
|
<rect x="62" y="136" width="66" height="7" rx="3.5" fill="#ccd1c0"/>
|
||||||
|
<rect x="62" y="150" width="66" height="7" rx="3.5" fill="#ccd1c0"/>
|
||||||
|
<rect x="62" y="164" width="48" height="7" rx="3.5" fill="#ccd1c0"/>
|
||||||
|
<rect x="62" y="222" width="36" height="7" rx="3.5" fill="#1f291f"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="153" y="96" width="94" height="156" rx="12" fill="#fffdf8" stroke="#d9dacf"/>
|
||||||
|
<rect x="167" y="114" width="56" height="10" rx="5" fill="#1f291f"/>
|
||||||
|
<rect x="167" y="136" width="66" height="7" rx="3.5" fill="#ccd1c0"/>
|
||||||
|
<rect x="167" y="150" width="66" height="7" rx="3.5" fill="#ccd1c0"/>
|
||||||
|
<rect x="167" y="164" width="48" height="7" rx="3.5" fill="#ccd1c0"/>
|
||||||
|
<rect x="167" y="222" width="36" height="7" rx="3.5" fill="#1f291f"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="258" y="96" width="94" height="156" rx="12" fill="#fffdf8" stroke="#d9dacf"/>
|
||||||
|
<rect x="272" y="114" width="56" height="10" rx="5" fill="#1f291f"/>
|
||||||
|
<rect x="272" y="136" width="66" height="7" rx="3.5" fill="#ccd1c0"/>
|
||||||
|
<rect x="272" y="150" width="66" height="7" rx="3.5" fill="#ccd1c0"/>
|
||||||
|
<rect x="272" y="164" width="48" height="7" rx="3.5" fill="#ccd1c0"/>
|
||||||
|
<rect x="272" y="222" width="36" height="7" rx="3.5" fill="#1f291f"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300">
|
||||||
|
<rect width="400" height="300" fill="#f7f4ec"/>
|
||||||
|
<g>
|
||||||
|
<rect x="40" y="108" width="320" height="84" rx="14" fill="#fffdf8" stroke="#d9dacf"/>
|
||||||
|
<circle cx="82" cy="150" r="15" fill="none" stroke="#1f291f" stroke-width="2"/>
|
||||||
|
<path d="M82 144v2m0 4v8" stroke="#1f291f" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<rect x="112" y="134" width="140" height="10" rx="5" fill="#1f291f"/>
|
||||||
|
<rect x="112" y="156" width="112" height="9" rx="4.5" fill="#ccd1c0"/>
|
||||||
|
<rect x="272" y="136" width="68" height="28" rx="14" fill="#c7ff3f"/>
|
||||||
|
<rect x="286" y="147" width="40" height="6" rx="3" fill="#1f291f"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 692 B |
@@ -0,0 +1,23 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300">
|
||||||
|
<rect width="400" height="300" fill="#f7f4ec"/>
|
||||||
|
<rect x="92" y="40" width="216" height="18" rx="9" fill="#1f291f"/>
|
||||||
|
<rect x="128" y="72" width="144" height="10" rx="5" fill="#b9bfab"/>
|
||||||
|
<g fill="#ccd1c0">
|
||||||
|
<rect x="48" y="116" width="144" height="10" rx="5"/>
|
||||||
|
<rect x="48" y="136" width="144" height="10" rx="5"/>
|
||||||
|
<rect x="48" y="156" width="144" height="10" rx="5"/>
|
||||||
|
<rect x="48" y="176" width="104" height="10" rx="5"/>
|
||||||
|
<rect x="208" y="116" width="144" height="10" rx="5"/>
|
||||||
|
<rect x="208" y="136" width="144" height="10" rx="5"/>
|
||||||
|
<rect x="208" y="156" width="144" height="10" rx="5"/>
|
||||||
|
<rect x="208" y="176" width="104" height="10" rx="5"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#1f291f">
|
||||||
|
<rect x="48" y="204" width="56" height="10" rx="5"/>
|
||||||
|
<rect x="208" y="204" width="56" height="10" rx="5"/>
|
||||||
|
</g>
|
||||||
|
<g stroke="#1f291f" stroke-width="2" fill="none" stroke-linecap="round">
|
||||||
|
<path d="M110 209h10m0 0-4-4m4 4-4 4"/>
|
||||||
|
<path d="M270 209h10m0 0-4-4m4 4-4 4"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300">
|
||||||
|
<rect width="400" height="300" fill="#f7f4ec"/>
|
||||||
|
<rect x="124" y="32" width="152" height="16" rx="8" fill="#1f291f"/>
|
||||||
|
<g>
|
||||||
|
<rect x="48" y="72" width="304" height="44" rx="10" fill="#fffdf8" stroke="#d9dacf"/>
|
||||||
|
<rect x="68" y="89" width="152" height="10" rx="5" fill="#1f291f"/>
|
||||||
|
<path d="M322 88v12m-6-6h12" stroke="#1f291f" stroke-width="2" stroke-linecap="round" fill="none"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="48" y="128" width="304" height="84" rx="10" fill="#fffdf8" stroke="#a8c930"/>
|
||||||
|
<rect x="68" y="145" width="184" height="10" rx="5" fill="#1f291f"/>
|
||||||
|
<path d="M316 150h12" stroke="#1f291f" stroke-width="2" stroke-linecap="round" fill="none"/>
|
||||||
|
<rect x="68" y="169" width="264" height="9" rx="4.5" fill="#ccd1c0"/>
|
||||||
|
<rect x="68" y="187" width="208" height="9" rx="4.5" fill="#ccd1c0"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="48" y="224" width="304" height="44" rx="10" fill="#fffdf8" stroke="#d9dacf"/>
|
||||||
|
<rect x="68" y="241" width="168" height="10" rx="5" fill="#1f291f"/>
|
||||||
|
<path d="M322 240v12m-6-6h12" stroke="#1f291f" stroke-width="2" stroke-linecap="round" fill="none"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,16 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300">
|
||||||
|
<rect width="400" height="300" fill="#f7f4ec"/>
|
||||||
|
<g>
|
||||||
|
<rect x="48" y="78" width="152" height="144" rx="12" fill="#e7e9dc" stroke="#d9dacf"/>
|
||||||
|
<circle cx="166" cy="110" r="12" fill="#c7ff3f"/>
|
||||||
|
<path d="M64 194 104 142l28 34 18-20 30 38z" fill="#b9bfab"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="224" y="86" width="128" height="14" rx="7" fill="#1f291f"/>
|
||||||
|
<rect x="224" y="116" width="128" height="9" rx="4.5" fill="#ccd1c0"/>
|
||||||
|
<rect x="224" y="134" width="128" height="9" rx="4.5" fill="#ccd1c0"/>
|
||||||
|
<rect x="224" y="152" width="96" height="9" rx="4.5" fill="#ccd1c0"/>
|
||||||
|
<rect x="224" y="180" width="84" height="26" rx="13" fill="#c7ff3f"/>
|
||||||
|
<rect x="240" y="190" width="52" height="6" rx="3" fill="#1f291f"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 800 B |
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300">
|
||||||
|
<rect width="400" height="300" fill="#f7f4ec"/>
|
||||||
|
<g>
|
||||||
|
<rect x="48" y="48" width="304" height="176" rx="14" fill="#e7e9dc" stroke="#d9dacf"/>
|
||||||
|
<circle cx="288" cy="96" r="17" fill="#c7ff3f"/>
|
||||||
|
<path d="M76 196 156 96l56 66 34-38 58 72z" fill="#b9bfab"/>
|
||||||
|
</g>
|
||||||
|
<rect x="140" y="248" width="120" height="9" rx="4.5" fill="#b9bfab"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 415 B |
@@ -0,0 +1,4 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
import { type I18nProviderProps } from '#app/i18n/types';
|
||||||
|
|
||||||
|
export default createContext<I18nProviderProps | null>(null);
|
||||||
@@ -1,7 +1,105 @@
|
|||||||
{
|
{
|
||||||
|
"ACTION.BACK": "رجوع",
|
||||||
|
"ACTION.SAVE": "حفظ",
|
||||||
|
"ACTION.CREATE": "إنشاء",
|
||||||
|
"ACTION.INCLUDE": "تضمين",
|
||||||
|
"ACTION.GENERATE": "توليد",
|
||||||
|
"ACTION.REMOVE": "إزالة",
|
||||||
|
"ACTION.CANCEL": "إلغاء",
|
||||||
|
"ACTION.CONFIRM": "تأكيد",
|
||||||
|
"ACTION.SUBMIT": "إرسال",
|
||||||
|
"ACTION.SEARCH": "بحث",
|
||||||
|
"ACTION.SWAP": "تبديل",
|
||||||
|
"ACTION.MERGE": "دمج",
|
||||||
|
"ACTION.SPLIT": "تقسيم",
|
||||||
|
"ACTION.RESET": "إعادة تعيين",
|
||||||
|
"ACTION.PREVIEW": "معاينة",
|
||||||
|
"ACTION.UPLOAD": "رفع",
|
||||||
|
"ACTION.DELETE": "حذف",
|
||||||
|
"ACTION.CLOSE": "إغلاق",
|
||||||
|
"ACTION.CLEAR": "مسح",
|
||||||
|
"ACTION.CLEAR_FILTERS": "مسح عوامل التصفية",
|
||||||
|
"ACTION.EDIT": "تعديل",
|
||||||
|
"STATE.CREATING": "جارٍ الإنشاء",
|
||||||
|
"STATE.GENERATING": "جارٍ التوليد",
|
||||||
|
"STATE.SAVING": "جارٍ الحفظ",
|
||||||
|
"STATE.REMOVING": "جارٍ الإزالة",
|
||||||
|
"STATE.LOADING": "جارٍ التحميل",
|
||||||
|
"STATE.UPDATING": "جارٍ التحديث",
|
||||||
|
"ERROR": "خطأ",
|
||||||
|
"PERMISSION": "صلاحية",
|
||||||
|
"PERMISSION.PERMISSIONS": "الصلاحيات",
|
||||||
|
"PERMISSION.PERMISSION_NAME": "اسم الصلاحية",
|
||||||
|
"PERMISSION.SLUG": "Slug",
|
||||||
|
"PERMISSION.EDIT_PERMISSION": "تعديل الصلاحية",
|
||||||
|
"ROLE": "دور",
|
||||||
|
"ROLE.ROLES": "الأدوار",
|
||||||
|
"ROLE.ROLE_NAME": "اسم الدور",
|
||||||
|
"ROLE.SLUG": "Slug",
|
||||||
|
"ROLE.EDIT_ROLE": "تعديل الدور",
|
||||||
|
"USER": "مستخدم",
|
||||||
|
"USER.EDIT_USER": "تعديل المستخدم",
|
||||||
|
"USER.STATUS.ACTIVE": "نشط",
|
||||||
|
"USER.STATUS.INACTIVE": "غير نشط",
|
||||||
"USER.MENU.MY_PROFILE": "الملف الشخصي الخاص بي",
|
"USER.MENU.MY_PROFILE": "الملف الشخصي الخاص بي",
|
||||||
"USER.MENU.MY_ACCOUNT": "حسابي",
|
"USER.MENU.MY_ACCOUNT": "حسابي",
|
||||||
|
"USER.MENU.LANGUAGE": "لغة",
|
||||||
"USER.MENU.DARK_MODE": "الوضع المظلم",
|
"USER.MENU.DARK_MODE": "الوضع المظلم",
|
||||||
"USER.MENU.LOGOUT": "تسجيل الخروج",
|
"USER.MENU.LOGOUT": "تسجيل الخروج",
|
||||||
"USER.MENU.LANGUAGE": "لغة"
|
"SUCCESS": "تم بنجاح",
|
||||||
|
"ACTIONS": "الإجراءات",
|
||||||
|
"FILTER": "تصفية",
|
||||||
|
"SELECTED": "محدد",
|
||||||
|
"NAME": "الاسم",
|
||||||
|
"FIRST_NAME": "الاسم الأول",
|
||||||
|
"LAST_NAME": "اسم العائلة",
|
||||||
|
"EMAIL": "البريد الإلكتروني",
|
||||||
|
"SELECT": "اختر",
|
||||||
|
"SELECT_OPTION": "اختر خياراً",
|
||||||
|
"SELECT_ALL": "تحديد الكل",
|
||||||
|
"SELECT_ALL_WITH_OPTS_LENGTH": "تحديد الكل ({length} خيارات)",
|
||||||
|
"STATUS": "الحالة",
|
||||||
|
"PASSWORD": "كلمة المرور",
|
||||||
|
"NEW_PASSWORD": "كلمة المرور الجديدة",
|
||||||
|
"PASSWORD_CONFIRM": "تأكيد كلمة المرور",
|
||||||
|
"MODULE_NAME": "اسم الوحدة",
|
||||||
|
"RETURN_HOME": "العودة إلى الرئيسية",
|
||||||
|
"NO_ITEMS_FOUND": "لم يتم العثور على عناصر",
|
||||||
|
"NO_RESULTS_FOUND": "لم يتم العثور على نتائج",
|
||||||
|
"PLUS_MORE_WITH_NUMBER": "+ {number} أخرى",
|
||||||
|
"ADMIN.ADMIN_MANAGEMENT": "إدارة المشرفين",
|
||||||
|
"ADMIN.ADMINS": "المشرفون",
|
||||||
|
"DASHBOARD": "لوحة التحكم",
|
||||||
|
"FORM.ERROR.REQUIRED": "مطلوب",
|
||||||
|
"FORM.ERROR.INVALID_EMAIL_ADDRESS": "عنوان بريد إلكتروني غير صالح",
|
||||||
|
"FORM.ERROR.PASSWORDS_MUST_MATCH": "يجب أن تتطابق كلمتا المرور",
|
||||||
|
"FORM.ERROR.MIN_PASSWORD_LENGTH_DESC": "الحد الأدنى لطول كلمة المرور - 8 أحرف.",
|
||||||
|
"FORM.ERROR.SOMETHING_WENT_WRONG": "حدث خطأ ما",
|
||||||
|
"TABLE.COMMON.COLUMNS": "الأعمدة",
|
||||||
|
"TABLE.COMMON.NO_DATA_AVAILABLE": "لا توجد بيانات متاحة",
|
||||||
|
"TABLE.COMMON.ROWS_PER_PAGE": "عدد الصفوف في الصفحة",
|
||||||
|
"TABLE.COMMON.GO_TO_PREVIOUS_PAGE": "الانتقال إلى الصفحة السابقة",
|
||||||
|
"TABLE.COMMON.GO_TO_NEXT_PAGE": "الانتقال إلى الصفحة التالية",
|
||||||
|
"PROFILE.PROFILE_SETTINGS": "إعدادات الملف الشخصي",
|
||||||
|
"PROFILE.AVATAR_DESCRIPTION": "نوصي باستخدام صور لا تقل أبعادها عن 98 × 98 بكسل (بحجم يصل إلى 4 ميغابايت) بصيغة PNG أو JPG أو JPEG.",
|
||||||
|
"PROFILE.AVATAR_ALT": "الصورة الرمزية",
|
||||||
|
"PROFILE.AVATAR_SETTINGS": "إعدادات الصورة الرمزية",
|
||||||
|
"PROFILE.AVATAR_CROP_ME": "قص الصورة",
|
||||||
|
"PROFILE.EDIT_PROFILE_IMAGE": "تعديل صورة الملف الشخصي",
|
||||||
|
"MODULE.COMMON.REMOVE_ITEM_QUESTION": "هل أنت متأكد من رغبتك في إزالة {target}؟",
|
||||||
|
"MODULE.COMMON.ARE_YOU_SURE?": "هل أنت متأكد؟",
|
||||||
|
"MODULE.COMMON.REMOVE_ITEM_QUESTION_DESC": "لا يمكن التراجع عن هذا الإجراء.",
|
||||||
|
"MODULE.COMMON.CREATE_NEW_ENTITY": "إنشاء {entity} جديد",
|
||||||
|
"MODULE.COMMON.CREATE_NEW_USER": "إنشاء مستخدم جديد",
|
||||||
|
"MESSAGE.PLEASE_WAIT": "يرجى الانتظار",
|
||||||
|
"MESSAGE.PLEASE_WAIT_THIS_MAY_TAKE_FEW_MOMENTS": "يرجى الانتظار، قد يستغرق هذا بعض الوقت",
|
||||||
|
"COMMON.SELECT_FILE_TO_VIEW_ITS_CONTENT": "اختر ملفاً لعرض محتوياته",
|
||||||
|
"COMPONENT.FILE_INPUT.SELECTED_NUM_OF_FILES": "تم اختيار {num} ملف",
|
||||||
|
"COMPONENT.FILE_INPUT.SELECT_FILE_NO_FILE_SELECTED": "اختر ملفاً، لم يتم اختيار أي ملف",
|
||||||
|
"COMPONENT.FILE_INPUT.OPTION.FROM_CLOUD_STORAGE": "من التخزين السحابي",
|
||||||
|
"COMPONENT.FILE_INPUT.OPTION.FROM_LOCAL_MACHINE": "من الجهاز المحلي",
|
||||||
|
"COMPONENT.FILE_INPUT.OPTION.CLEAR": "مسح",
|
||||||
|
"COMPONENT.FILE_INPUT.SHOW_SELECTED_FILES": "عرض الملفات المختارة",
|
||||||
|
"COMPONENT.FILE_INPUT.SELECTED_FILES": "الملفات المختارة",
|
||||||
|
"COMPONENT.FILE_INPUT.REMOVE_NAME": "إزالة {name}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,28 +20,23 @@
|
|||||||
"ACTION.CLEAR": "Clear",
|
"ACTION.CLEAR": "Clear",
|
||||||
"ACTION.CLEAR_FILTERS": "Clear filters",
|
"ACTION.CLEAR_FILTERS": "Clear filters",
|
||||||
"ACTION.EDIT": "Edit",
|
"ACTION.EDIT": "Edit",
|
||||||
|
|
||||||
"STATE.CREATING": "Creating",
|
"STATE.CREATING": "Creating",
|
||||||
"STATE.GENERATING": "Generating",
|
"STATE.GENERATING": "Generating",
|
||||||
"STATE.SAVING": "Saving",
|
"STATE.SAVING": "Saving",
|
||||||
"STATE.REMOVING": "Removing",
|
"STATE.REMOVING": "Removing",
|
||||||
"STATE.LOADING": "Loading",
|
"STATE.LOADING": "Loading",
|
||||||
"STATE.UPDATING": "Updating",
|
"STATE.UPDATING": "Updating",
|
||||||
|
|
||||||
"ERROR": "Error",
|
"ERROR": "Error",
|
||||||
|
|
||||||
"PERMISSION": "Permission",
|
"PERMISSION": "Permission",
|
||||||
"PERMISSION.PERMISSIONS": "Permissions",
|
"PERMISSION.PERMISSIONS": "Permissions",
|
||||||
"PERMISSION.PERMISSION_NAME": "Permission name",
|
"PERMISSION.PERMISSION_NAME": "Permission name",
|
||||||
"PERMISSION.SLUG": "Slug",
|
"PERMISSION.SLUG": "Slug",
|
||||||
"PERMISSION.EDIT_PERMISSION": "Edit permission",
|
"PERMISSION.EDIT_PERMISSION": "Edit permission",
|
||||||
|
|
||||||
"ROLE": "Role",
|
"ROLE": "Role",
|
||||||
"ROLE.ROLES": "Roles",
|
"ROLE.ROLES": "Roles",
|
||||||
"ROLE.ROLE_NAME": "Role name",
|
"ROLE.ROLE_NAME": "Role name",
|
||||||
"ROLE.SLUG": "Slug",
|
"ROLE.SLUG": "Slug",
|
||||||
"ROLE.EDIT_ROLE": "Edit role",
|
"ROLE.EDIT_ROLE": "Edit role",
|
||||||
|
|
||||||
"USER": "User",
|
"USER": "User",
|
||||||
"USER.EDIT_USER": "Edit user",
|
"USER.EDIT_USER": "Edit user",
|
||||||
"USER.STATUS.ACTIVE": "Active",
|
"USER.STATUS.ACTIVE": "Active",
|
||||||
@@ -51,7 +46,6 @@
|
|||||||
"USER.MENU.LANGUAGE": "Language",
|
"USER.MENU.LANGUAGE": "Language",
|
||||||
"USER.MENU.DARK_MODE": "Dark Mode",
|
"USER.MENU.DARK_MODE": "Dark Mode",
|
||||||
"USER.MENU.LOGOUT": "Logout",
|
"USER.MENU.LOGOUT": "Logout",
|
||||||
|
|
||||||
"SUCCESS": "Success",
|
"SUCCESS": "Success",
|
||||||
"ACTIONS": "Actions",
|
"ACTIONS": "Actions",
|
||||||
"FILTER": "Filter",
|
"FILTER": "Filter",
|
||||||
@@ -73,42 +67,33 @@
|
|||||||
"NO_ITEMS_FOUND": "No items found",
|
"NO_ITEMS_FOUND": "No items found",
|
||||||
"NO_RESULTS_FOUND": "No results found",
|
"NO_RESULTS_FOUND": "No results found",
|
||||||
"PLUS_MORE_WITH_NUMBER": "+ {number} more",
|
"PLUS_MORE_WITH_NUMBER": "+ {number} more",
|
||||||
|
|
||||||
"ADMIN.ADMIN_MANAGEMENT": "Admin Management",
|
"ADMIN.ADMIN_MANAGEMENT": "Admin Management",
|
||||||
"ADMIN.ADMINS": "Admins",
|
"ADMIN.ADMINS": "Admins",
|
||||||
|
|
||||||
"DASHBOARD": "Dashboard",
|
"DASHBOARD": "Dashboard",
|
||||||
|
|
||||||
"FORM.ERROR.REQUIRED": "Required",
|
"FORM.ERROR.REQUIRED": "Required",
|
||||||
"FORM.ERROR.INVALID_EMAIL_ADDRESS": "Invalid email address",
|
"FORM.ERROR.INVALID_EMAIL_ADDRESS": "Invalid email address",
|
||||||
"FORM.ERROR.PASSWORDS_MUST_MATCH": "Passwords must match",
|
"FORM.ERROR.PASSWORDS_MUST_MATCH": "Passwords must match",
|
||||||
"FORM.ERROR.MIN_PASSWORD_LENGTH_DESC": "Min. password length - 8 chars.",
|
"FORM.ERROR.MIN_PASSWORD_LENGTH_DESC": "Min. password length - 8 chars.",
|
||||||
"FORM.ERROR.SOMETHING_WENT_WRONG": "Something went wrong",
|
"FORM.ERROR.SOMETHING_WENT_WRONG": "Something went wrong",
|
||||||
|
|
||||||
"TABLE.COMMON.COLUMNS": "Columns",
|
"TABLE.COMMON.COLUMNS": "Columns",
|
||||||
"TABLE.COMMON.NO_DATA_AVAILABLE": "No data available",
|
"TABLE.COMMON.NO_DATA_AVAILABLE": "No data available",
|
||||||
"TABLE.COMMON.ROWS_PER_PAGE": "Rows per page",
|
"TABLE.COMMON.ROWS_PER_PAGE": "Rows per page",
|
||||||
"TABLE.COMMON.GO_TO_PREVIOUS_PAGE": "Go to previous page",
|
"TABLE.COMMON.GO_TO_PREVIOUS_PAGE": "Go to previous page",
|
||||||
"TABLE.COMMON.GO_TO_NEXT_PAGE": "Go to next page",
|
"TABLE.COMMON.GO_TO_NEXT_PAGE": "Go to next page",
|
||||||
|
|
||||||
"PROFILE.PROFILE_SETTINGS": "Profile Settings",
|
"PROFILE.PROFILE_SETTINGS": "Profile Settings",
|
||||||
"PROFILE.AVATAR_DESCRIPTION": "We recommend using images that are at least 98 x 98 pixels in size (file size up to 4 MB) in PNG, JPG, JPEG format.",
|
"PROFILE.AVATAR_DESCRIPTION": "We recommend using images that are at least 98 x 98 pixels in size (file size up to 4 MB) in PNG, JPG, JPEG format.",
|
||||||
"PROFILE.AVATAR_ALT": "Avatar",
|
"PROFILE.AVATAR_ALT": "Avatar",
|
||||||
"PROFILE.AVATAR_SETTINGS": "Avatar Settings",
|
"PROFILE.AVATAR_SETTINGS": "Avatar Settings",
|
||||||
"PROFILE.AVATAR_CROP_ME": "Crop me",
|
"PROFILE.AVATAR_CROP_ME": "Crop me",
|
||||||
"PROFILE.EDIT_PROFILE_IMAGE": "Edit profile image",
|
"PROFILE.EDIT_PROFILE_IMAGE": "Edit profile image",
|
||||||
|
|
||||||
"MODULE.COMMON.REMOVE_ITEM_QUESTION": "Are you sure you want to remove {target}?",
|
"MODULE.COMMON.REMOVE_ITEM_QUESTION": "Are you sure you want to remove {target}?",
|
||||||
"MODULE.COMMON.ARE_YOU_SURE?": "Are you sure?",
|
"MODULE.COMMON.ARE_YOU_SURE?": "Are you sure?",
|
||||||
"MODULE.COMMON.REMOVE_ITEM_QUESTION_DESC": "This action cannot be undone.",
|
"MODULE.COMMON.REMOVE_ITEM_QUESTION_DESC": "This action cannot be undone.",
|
||||||
"MODULE.COMMON.CREATE_NEW_ENTITY": "Create new {entity}",
|
"MODULE.COMMON.CREATE_NEW_ENTITY": "Create new {entity}",
|
||||||
"MODULE.COMMON.CREATE_NEW_USER": "Create new user",
|
"MODULE.COMMON.CREATE_NEW_USER": "Create new user",
|
||||||
|
|
||||||
"MESSAGE.PLEASE_WAIT": "Please wait",
|
"MESSAGE.PLEASE_WAIT": "Please wait",
|
||||||
"MESSAGE.PLEASE_WAIT_THIS_MAY_TAKE_FEW_MOMENTS": "Please wait, this may take few moments",
|
"MESSAGE.PLEASE_WAIT_THIS_MAY_TAKE_FEW_MOMENTS": "Please wait, this may take few moments",
|
||||||
|
|
||||||
"COMMON.SELECT_FILE_TO_VIEW_ITS_CONTENT": "Select a file to view its contents",
|
"COMMON.SELECT_FILE_TO_VIEW_ITS_CONTENT": "Select a file to view its contents",
|
||||||
|
|
||||||
"COMPONENT.FILE_INPUT.SELECTED_NUM_OF_FILES": "{num} file(s) selected",
|
"COMPONENT.FILE_INPUT.SELECTED_NUM_OF_FILES": "{num} file(s) selected",
|
||||||
"COMPONENT.FILE_INPUT.SELECT_FILE_NO_FILE_SELECTED": "Select file, no file selected",
|
"COMPONENT.FILE_INPUT.SELECT_FILE_NO_FILE_SELECTED": "Select file, no file selected",
|
||||||
"COMPONENT.FILE_INPUT.OPTION.FROM_CLOUD_STORAGE": "From cloud storage",
|
"COMPONENT.FILE_INPUT.OPTION.FROM_CLOUD_STORAGE": "From cloud storage",
|
||||||
|
|||||||
@@ -20,28 +20,23 @@
|
|||||||
"ACTION.CLEAR": "Очистити",
|
"ACTION.CLEAR": "Очистити",
|
||||||
"ACTION.CLEAR_FILTERS": "Очистити фільтри",
|
"ACTION.CLEAR_FILTERS": "Очистити фільтри",
|
||||||
"ACTION.EDIT": "Редагувати",
|
"ACTION.EDIT": "Редагувати",
|
||||||
|
|
||||||
"STATE.CREATING": "Створення",
|
"STATE.CREATING": "Створення",
|
||||||
"STATE.GENERATING": "Генерування",
|
"STATE.GENERATING": "Генерування",
|
||||||
"STATE.SAVING": "Збереження",
|
"STATE.SAVING": "Збереження",
|
||||||
"STATE.REMOVING": "Видалення",
|
"STATE.REMOVING": "Видалення",
|
||||||
"STATE.LOADING": "Завантаження",
|
"STATE.LOADING": "Завантаження",
|
||||||
"STATE.UPDATING": "Оновлюється",
|
"STATE.UPDATING": "Оновлюється",
|
||||||
|
|
||||||
"ERROR": "Помилка",
|
"ERROR": "Помилка",
|
||||||
|
|
||||||
"PERMISSION": "Дозвіл",
|
"PERMISSION": "Дозвіл",
|
||||||
"PERMISSION.PERMISSIONS": "Дозволи",
|
"PERMISSION.PERMISSIONS": "Дозволи",
|
||||||
"PERMISSION.PERMISSION_NAME": "Назва дозволу",
|
"PERMISSION.PERMISSION_NAME": "Назва дозволу",
|
||||||
"PERMISSION.SLUG": "Slug",
|
"PERMISSION.SLUG": "Slug",
|
||||||
"PERMISSION.EDIT_PERMISSION": "Редагувати дозвіл",
|
"PERMISSION.EDIT_PERMISSION": "Редагувати дозвіл",
|
||||||
|
|
||||||
"ROLE": "Роль",
|
"ROLE": "Роль",
|
||||||
"ROLE.ROLES": "Ролі",
|
"ROLE.ROLES": "Ролі",
|
||||||
"ROLE.ROLE_NAME": "Назва ролі",
|
"ROLE.ROLE_NAME": "Назва ролі",
|
||||||
"ROLE.SLUG": "Slug",
|
"ROLE.SLUG": "Slug",
|
||||||
"ROLE.EDIT_ROLE": "Редагувати роль",
|
"ROLE.EDIT_ROLE": "Редагувати роль",
|
||||||
|
|
||||||
"USER": "Користувач",
|
"USER": "Користувач",
|
||||||
"USER.EDIT_USER": "Редагувати користувача",
|
"USER.EDIT_USER": "Редагувати користувача",
|
||||||
"USER.STATUS.ACTIVE": "Активний",
|
"USER.STATUS.ACTIVE": "Активний",
|
||||||
@@ -51,7 +46,6 @@
|
|||||||
"USER.MENU.LANGUAGE": "Мова",
|
"USER.MENU.LANGUAGE": "Мова",
|
||||||
"USER.MENU.DARK_MODE": "Темний режим",
|
"USER.MENU.DARK_MODE": "Темний режим",
|
||||||
"USER.MENU.LOGOUT": "Вийти",
|
"USER.MENU.LOGOUT": "Вийти",
|
||||||
|
|
||||||
"SUCCESS": "Успіх",
|
"SUCCESS": "Успіх",
|
||||||
"ACTIONS": "Дії",
|
"ACTIONS": "Дії",
|
||||||
"FILTER": "Фільтр",
|
"FILTER": "Фільтр",
|
||||||
@@ -73,42 +67,33 @@
|
|||||||
"NO_ITEMS_FOUND": "Не знайдено жодного елемента",
|
"NO_ITEMS_FOUND": "Не знайдено жодного елемента",
|
||||||
"NO_RESULTS_FOUND": "Результатів не знайдено",
|
"NO_RESULTS_FOUND": "Результатів не знайдено",
|
||||||
"PLUS_MORE_WITH_NUMBER": "+ {number} ще",
|
"PLUS_MORE_WITH_NUMBER": "+ {number} ще",
|
||||||
|
|
||||||
"ADMIN.ADMIN_MANAGEMENT": "Адмін Управління",
|
"ADMIN.ADMIN_MANAGEMENT": "Адмін Управління",
|
||||||
"ADMIN.ADMINS": "Адміністратори",
|
"ADMIN.ADMINS": "Адміністратори",
|
||||||
|
|
||||||
"DASHBOARD": "Дашборд",
|
"DASHBOARD": "Дашборд",
|
||||||
|
|
||||||
"FORM.ERROR.REQUIRED": "Обов'язково",
|
"FORM.ERROR.REQUIRED": "Обов'язково",
|
||||||
"FORM.ERROR.INVALID_EMAIL_ADDRESS": "Недійсна адреса електронної пошти",
|
"FORM.ERROR.INVALID_EMAIL_ADDRESS": "Недійсна адреса електронної пошти",
|
||||||
"FORM.ERROR.PASSWORDS_MUST_MATCH": "Паролі мають збігатися",
|
"FORM.ERROR.PASSWORDS_MUST_MATCH": "Паролі мають збігатися",
|
||||||
"FORM.ERROR.MIN_PASSWORD_LENGTH_DESC": "Мінімальна довжина пароля - 8 символів.",
|
"FORM.ERROR.MIN_PASSWORD_LENGTH_DESC": "Мінімальна довжина пароля - 8 символів.",
|
||||||
"FORM.ERROR.SOMETHING_WENT_WRONG": "Щось пішло не так",
|
"FORM.ERROR.SOMETHING_WENT_WRONG": "Щось пішло не так",
|
||||||
|
|
||||||
"TABLE.COMMON.COLUMNS": "Колонки",
|
"TABLE.COMMON.COLUMNS": "Колонки",
|
||||||
"TABLE.COMMON.NO_DATA_AVAILABLE": "Немає доступних даних",
|
"TABLE.COMMON.NO_DATA_AVAILABLE": "Немає доступних даних",
|
||||||
"TABLE.COMMON.ROWS_PER_PAGE": "Рядків на сторінці",
|
"TABLE.COMMON.ROWS_PER_PAGE": "Рядків на сторінці",
|
||||||
"TABLE.COMMON.GO_TO_PREVIOUS_PAGE": "Перейти на попередню сторінку",
|
"TABLE.COMMON.GO_TO_PREVIOUS_PAGE": "Перейти на попередню сторінку",
|
||||||
"TABLE.COMMON.GO_TO_NEXT_PAGE": "Перейти на наступну сторінку",
|
"TABLE.COMMON.GO_TO_NEXT_PAGE": "Перейти на наступну сторінку",
|
||||||
|
|
||||||
"PROFILE.PROFILE_SETTINGS": "Налаштування профілю",
|
"PROFILE.PROFILE_SETTINGS": "Налаштування профілю",
|
||||||
"PROFILE.AVATAR_DESCRIPTION": "Радимо використовувати зображення розміром принаймні 98 x 98 пікселів (розмір файлу - до 4 МБ) у форматі PNG, JPG , JPEG.",
|
"PROFILE.AVATAR_DESCRIPTION": "Радимо використовувати зображення розміром принаймні 98 x 98 пікселів (розмір файлу - до 4 МБ) у форматі PNG, JPG , JPEG.",
|
||||||
"PROFILE.AVATAR_ALT": "Аватар",
|
"PROFILE.AVATAR_ALT": "Аватар",
|
||||||
"PROFILE.AVATAR_SETTINGS": "Налаштування аватара",
|
"PROFILE.AVATAR_SETTINGS": "Налаштування аватара",
|
||||||
"PROFILE.AVATAR_CROP_ME": "Обріж мене",
|
"PROFILE.AVATAR_CROP_ME": "Обріж мене",
|
||||||
"PROFILE.EDIT_PROFILE_IMAGE": "Редагувати зображення профілю",
|
"PROFILE.EDIT_PROFILE_IMAGE": "Редагувати зображення профілю",
|
||||||
|
|
||||||
"MODULE.COMMON.REMOVE_ITEM_QUESTION": "Ви впевнені, що хочете видалити {target}?",
|
"MODULE.COMMON.REMOVE_ITEM_QUESTION": "Ви впевнені, що хочете видалити {target}?",
|
||||||
"MODULE.COMMON.ARE_YOU_SURE?": "Ви впевнені?",
|
"MODULE.COMMON.ARE_YOU_SURE?": "Ви впевнені?",
|
||||||
"MODULE.COMMON.REMOVE_ITEM_QUESTION_DESC": "Цю дію не можна скасувати.",
|
"MODULE.COMMON.REMOVE_ITEM_QUESTION_DESC": "Цю дію не можна скасувати.",
|
||||||
"MODULE.COMMON.CREATE_NEW_ENTITY": "Створити новий {entity}",
|
"MODULE.COMMON.CREATE_NEW_ENTITY": "Створити новий {entity}",
|
||||||
"MODULE.COMMON.CREATE_NEW_USER": "Створити нового користувача",
|
"MODULE.COMMON.CREATE_NEW_USER": "Створити нового користувача",
|
||||||
|
|
||||||
"MESSAGE.PLEASE_WAIT": "Будь ласка, зачекайте",
|
"MESSAGE.PLEASE_WAIT": "Будь ласка, зачекайте",
|
||||||
"MESSAGE.PLEASE_WAIT_THIS_MAY_TAKE_FEW_MOMENTS": "Зачекайте хвилинку, це може зайняти деякий час",
|
"MESSAGE.PLEASE_WAIT_THIS_MAY_TAKE_FEW_MOMENTS": "Зачекайте хвилинку, це може зайняти деякий час",
|
||||||
|
|
||||||
"COMMON.SELECT_FILE_TO_VIEW_ITS_CONTENT": "Виберіть файл, щоб переглянути його вміст",
|
"COMMON.SELECT_FILE_TO_VIEW_ITS_CONTENT": "Виберіть файл, щоб переглянути його вміст",
|
||||||
|
|
||||||
"COMPONENT.FILE_INPUT.SELECTED_NUM_OF_FILES": "Вибрано {num} файл(и)",
|
"COMPONENT.FILE_INPUT.SELECTED_NUM_OF_FILES": "Вибрано {num} файл(и)",
|
||||||
"COMPONENT.FILE_INPUT.SELECT_FILE_NO_FILE_SELECTED": "Виберіть файл, файл не вибрано",
|
"COMPONENT.FILE_INPUT.SELECT_FILE_NO_FILE_SELECTED": "Виберіть файл, файл не вибрано",
|
||||||
"COMPONENT.FILE_INPUT.OPTION.FROM_CLOUD_STORAGE": "З хмарного сховища",
|
"COMPONENT.FILE_INPUT.OPTION.FROM_CLOUD_STORAGE": "З хмарного сховища",
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import useLanguage from '#app/i18n/useLanguage';
|
||||||
|
import { IntlDate } from '#infrastructure/IntlDate';
|
||||||
|
|
||||||
|
type DisplayFormat = Parameters<typeof IntlDate.formatToDisplay>[0];
|
||||||
|
type SourceFormat = Parameters<typeof IntlDate.parseSource>[1];
|
||||||
|
|
||||||
|
const useIntlDate = () => {
|
||||||
|
const { currenLanguage } = useLanguage();
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
(value: string, format: DisplayFormat, sourceFormat: SourceFormat = 'UNIVERSAL_ISO') =>
|
||||||
|
IntlDate.parseSource(value, sourceFormat).formatToDisplay(format),
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[currenLanguage.code]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useIntlDate;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import useLanguage from '#app/i18n/useLanguage';
|
||||||
|
import { IntlName } from '#infrastructure/IntlName';
|
||||||
|
|
||||||
|
type NameInput = Parameters<typeof IntlName.formatToDisplay>[0];
|
||||||
|
type NameFormat = Parameters<typeof IntlName.formatToDisplay>[1];
|
||||||
|
|
||||||
|
const useIntlName = () => {
|
||||||
|
const { currenLanguage } = useLanguage();
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => (input: NameInput, format: NameFormat) => IntlName.formatToDisplay(input, format),
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[currenLanguage.code]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useIntlName;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import TranslationsContext from '#app/i18n/context';
|
||||||
|
|
||||||
|
const useLanguage = () => {
|
||||||
|
const context = useContext(TranslationsContext);
|
||||||
|
|
||||||
|
if (!context) throw new Error('useLanguage must be used within I18nProvider');
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useLanguage;
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { JSX, useCallback } from 'react';
|
import { JSX, useMemo } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { cn } from '@crux-kit/utils';
|
import { cn } from '@crux-kit/utils';
|
||||||
import { MenuConfig, MenuItem } from '#app/config/types';
|
import { MenuConfig, MenuItem } from '#app/config/types';
|
||||||
import { useMenuConfig } from '#app/menu';
|
import { useMenuConfig } from '#app/menu';
|
||||||
|
import { createMenuPathMatcher } from '#shared/lib/menuPath';
|
||||||
import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuLabel, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu';
|
import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuLabel, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu';
|
||||||
import { Badge } from '#shared/ui/Badge';
|
import { Badge } from '#shared/ui/Badge';
|
||||||
|
|
||||||
@@ -13,9 +14,9 @@ export function SidebarMenu() {
|
|||||||
const menuConfig = useMenuConfig();
|
const menuConfig = useMenuConfig();
|
||||||
|
|
||||||
// Memoize matchPath to prevent unnecessary re-renders
|
// Memoize matchPath to prevent unnecessary re-renders
|
||||||
const matchPath = useCallback(
|
const matchPath = useMemo(
|
||||||
(path: string): boolean => path === pathname || (path.length > 1 && pathname.startsWith(path)),
|
() => createMenuPathMatcher(menuConfig, pathname),
|
||||||
[pathname]
|
[menuConfig, pathname]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Global classNames for consistent styling
|
// Global classNames for consistent styling
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { ChevronDown } from 'lucide-react';
|
import { ChevronDown } from 'lucide-react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { cn } from '@crux-kit/utils';
|
import { cn } from '@crux-kit/utils';
|
||||||
import { MenuConfig } from '#app/config/types';
|
import { MenuConfig } from '#app/config/types';
|
||||||
import { useMenuConfig } from '#app/menu';
|
import { useMenuConfig } from '#app/menu';
|
||||||
import { useMenu } from '#shared/hooks/useMenu';
|
import { useMenu } from '#shared/hooks/useMenu';
|
||||||
|
import { createMenuPathMatcher } from '#shared/lib/menuPath';
|
||||||
import { Menubar, MenubarContent, MenubarItem, MenubarMenu, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger } from '#shared/ui/MenuBar';
|
import { Menubar, MenubarContent, MenubarItem, MenubarMenu, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger } from '#shared/ui/MenuBar';
|
||||||
|
|
||||||
export function NavbarMenu() {
|
export function NavbarMenu() {
|
||||||
@@ -24,7 +26,12 @@ export function NavbarMenu() {
|
|||||||
navbarMenu = menuConfig?.[3];
|
navbarMenu = menuConfig?.[3];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { isActive, hasActiveChild } = useMenu(pathname);
|
const { hasActiveChild } = useMenu(pathname);
|
||||||
|
// A leaf highlights only when nothing deeper in the menu matches the route more closely.
|
||||||
|
const isActive = useMemo(
|
||||||
|
() => createMenuPathMatcher((navbarMenu?.children ?? []) as MenuConfig, pathname),
|
||||||
|
[navbarMenu, pathname]
|
||||||
|
);
|
||||||
|
|
||||||
const buildMenu = (items: MenuConfig) => {
|
const buildMenu = (items: MenuConfig) => {
|
||||||
return items.map((item, index) => {
|
return items.map((item, index) => {
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { JSX, useCallback } from 'react';
|
import { JSX, useMemo } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { cn } from '@crux-kit/utils';
|
import { cn } from '@crux-kit/utils';
|
||||||
import { MenuConfig, MenuItem } from '#app/config/types';
|
import { MenuConfig, MenuItem } from '#app/config/types';
|
||||||
import { useMenuConfig } from '#app/menu';
|
import { useMenuConfig } from '#app/menu';
|
||||||
import { useMenu } from '#shared/hooks/useMenu';
|
import { useMenu } from '#shared/hooks/useMenu';
|
||||||
|
import { createMenuPathMatcher } from '#shared/lib/menuPath';
|
||||||
import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuLabel, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu';
|
import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuLabel, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu';
|
||||||
import { Badge } from '#shared/ui/Badge';
|
import { Badge } from '#shared/ui/Badge';
|
||||||
|
|
||||||
export function SidebarMenuDefault() {
|
export function SidebarMenuDefault() {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
const fullMenuConfig = useMenuConfig();
|
||||||
|
|
||||||
// Memoize matchPath to prevent unnecessary re-renders
|
// Memoize matchPath to prevent unnecessary re-renders
|
||||||
const matchPath = useCallback(
|
const matchPath = useMemo(
|
||||||
(path: string): boolean => path === pathname || (path.length > 1 && pathname.startsWith(path)),
|
() => createMenuPathMatcher(fullMenuConfig, pathname),
|
||||||
[pathname]
|
[fullMenuConfig, pathname]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Global classNames for consistent styling
|
// Global classNames for consistent styling
|
||||||
@@ -165,8 +167,7 @@ export function SidebarMenuDefault() {
|
|||||||
|
|
||||||
// Memoize the menu to prevent unnecessary re-renders
|
// Memoize the menu to prevent unnecessary re-renders
|
||||||
const { getChildren } = useMenu(pathname);
|
const { getChildren } = useMenu(pathname);
|
||||||
const menuConfig = useMenuConfig();
|
const menuItems = getChildren(fullMenuConfig, 0);
|
||||||
const menuItems = getChildren(menuConfig, 0);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccordionMenu
|
<AccordionMenu
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { JSX, useCallback } from 'react';
|
import { JSX, useMemo } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { cn } from '@crux-kit/utils';
|
import { cn } from '@crux-kit/utils';
|
||||||
import { MenuConfig, MenuItem } from '#app/config/types';
|
import { MenuConfig, MenuItem } from '#app/config/types';
|
||||||
import { useMenuConfig } from '#app/menu';
|
import { useMenuConfig } from '#app/menu';
|
||||||
import { useMenu } from '#shared/hooks/useMenu';
|
import { useMenu } from '#shared/hooks/useMenu';
|
||||||
|
import { createMenuPathMatcher } from '#shared/lib/menuPath';
|
||||||
import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuLabel, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu';
|
import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuLabel, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu';
|
||||||
import { Badge } from '#shared/ui/Badge';
|
import { Badge } from '#shared/ui/Badge';
|
||||||
|
|
||||||
@@ -12,9 +13,9 @@ export function SidebarMenuDefault() {
|
|||||||
const menuConfig = useMenuConfig();
|
const menuConfig = useMenuConfig();
|
||||||
|
|
||||||
// Memoize matchPath to prevent unnecessary re-renders
|
// Memoize matchPath to prevent unnecessary re-renders
|
||||||
const matchPath = useCallback(
|
const matchPath = useMemo(
|
||||||
(path: string): boolean => path === pathname || (path.length > 1 && pathname.startsWith(path)),
|
() => createMenuPathMatcher(menuConfig, pathname),
|
||||||
[pathname]
|
[menuConfig, pathname]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Global classNames for consistent styling
|
// Global classNames for consistent styling
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ const Header = () => {
|
|||||||
const [isSheetOpen, setIsSheetOpen] = useState(false);
|
const [isSheetOpen, setIsSheetOpen] = useState(false);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
// Close sheet when route changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsSheetOpen(false);
|
setIsSheetOpen(false);
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { IntlName } from '#infrastructure/IntlName';
|
import useIntlName from '#app/i18n/useIntlName';
|
||||||
import { useAuth } from '#core/Auth/providers/AuthProvider';
|
import { useAuth } from '#core/Auth/providers/AuthProvider';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '#shared/ui/Avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from '#shared/ui/Avatar';
|
||||||
import { UserDropdownMenu } from '#shared/widgets/UserDropdownMenu';
|
import { UserDropdownMenu } from '#shared/widgets/UserDropdownMenu';
|
||||||
|
|
||||||
export function SidebarFooter() {
|
export function SidebarFooter() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const formatName = useIntlName();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-center flex h-14 shrink-0 justify-between ps-4 pe-3.5">
|
<div className="flex-center flex h-14 shrink-0 justify-between ps-4 pe-3.5">
|
||||||
@@ -13,9 +14,7 @@ export function SidebarFooter() {
|
|||||||
trigger={
|
trigger={
|
||||||
<Avatar className="size-9 cursor-pointer">
|
<Avatar className="size-9 cursor-pointer">
|
||||||
<AvatarImage src={undefined} alt="User avatar" />
|
<AvatarImage src={undefined} alt="User avatar" />
|
||||||
<AvatarFallback className="text-sm">
|
<AvatarFallback className="text-sm">{formatName(user, 'INITIALS_LF')}</AvatarFallback>
|
||||||
{IntlName.formatToDisplay(user, 'INITIALS_LF')}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
</Avatar>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { JSX, useCallback } from 'react';
|
import { Fragment, JSX, useMemo } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { cn } from '@crux-kit/utils';
|
import { cn } from '@crux-kit/utils';
|
||||||
import { MenuConfig, MenuItem } from '#app/config/types';
|
import { MenuConfig, MenuItem } from '#app/config/types';
|
||||||
import { useMenuConfig } from '#app/menu';
|
import { useMenuConfig } from '#app/menu';
|
||||||
import { useAuth } from '#core/Auth/providers/AuthProvider';
|
import { useAuth } from '#core/Auth/providers/AuthProvider';
|
||||||
|
import { createMenuPathMatcher } from '#shared/lib/menuPath';
|
||||||
import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu';
|
import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu';
|
||||||
import { getPermittedMenuItems } from '../lib';
|
import { getPermittedMenuItems } from '../lib';
|
||||||
|
|
||||||
export function SidebarMenuPrimary() {
|
export function SidebarMenuPrimary() {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const menuConfig = useMenuConfig();
|
||||||
|
|
||||||
const userPermissions = user?.permissions ?? [];
|
const userPermissions = user?.permissions ?? [];
|
||||||
// Memoize matchPath to prevent unnecessary re-renders
|
|
||||||
const matchPath = useCallback(
|
const matchPath = useMemo(
|
||||||
(path: string): boolean => path === pathname || (path.length > 1 && pathname.startsWith(path)),
|
() => createMenuPathMatcher(menuConfig, pathname),
|
||||||
[pathname]
|
[menuConfig, pathname]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Global classNames for consistent styling
|
|
||||||
const classNames: AccordionMenuClassNames = {
|
const classNames: AccordionMenuClassNames = {
|
||||||
root: 'space-y-2.5 px-3.5',
|
root: 'space-y-2.5 px-3.5',
|
||||||
group: 'gap-px',
|
group: 'gap-px',
|
||||||
@@ -33,8 +35,6 @@ export function SidebarMenuPrimary() {
|
|||||||
indicator: ''
|
indicator: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuConfig = useMenuConfig();
|
|
||||||
|
|
||||||
const buildMenu = (items: MenuConfig): JSX.Element[] => {
|
const buildMenu = (items: MenuConfig): JSX.Element[] => {
|
||||||
const menuItems = getPermittedMenuItems(items, userPermissions);
|
const menuItems = getPermittedMenuItems(items, userPermissions);
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ export function SidebarMenuPrimary() {
|
|||||||
if (!item.heading && !item.disabled) {
|
if (!item.heading && !item.disabled) {
|
||||||
return buildMenuItemChild(item, index, level);
|
return buildMenuItemChild(item, index, level);
|
||||||
} else {
|
} else {
|
||||||
return <></>;
|
return <Fragment key={index}></Fragment>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// DEMO 6
|
// DEMO 6
|
||||||
// https://keenthemes.com/metronic/tailwind/react/demo6
|
// https://keenthemes.com/metronic/tailwind/react/demo6
|
||||||
|
|
||||||
import React from 'react';
|
import { Fragment, useEffect, useMemo } from 'react';
|
||||||
import { Outlet, useLocation } from 'react-router-dom';
|
import { Outlet, useLocation } from 'react-router-dom';
|
||||||
import { useBodyClass, useIsMobile } from '@crux-kit/hooks';
|
import { useBodyClass, useIsMobile } from '@crux-kit/hooks';
|
||||||
import { LayoutNames } from '#app/config/types';
|
import { LayoutNames } from '#app/config/types';
|
||||||
@@ -23,14 +23,15 @@ export const MultipleMenusLayout = () => {
|
|||||||
bg-muted!
|
bg-muted!
|
||||||
`);
|
`);
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
setOption('layout', LayoutNames.MULTIPLE_MENUS);
|
setOption('layout', LayoutNames.MULTIPLE_MENUS);
|
||||||
}, [setOption]);
|
}, [setOption]);
|
||||||
|
|
||||||
const toolbar = React.useMemo(() => {
|
const toolbar = useMemo(() => {
|
||||||
if (pathname.includes('/devtools')) return null;
|
if (pathname.includes('/devtools')) return null;
|
||||||
if (pathname.includes('/access-control')) return null;
|
if (pathname.includes('/access-control')) return null;
|
||||||
if (pathname.includes('/file-manager')) return null;
|
if (pathname.includes('/file-manager')) return null;
|
||||||
|
if (pathname.includes('/web-studio')) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
@@ -39,13 +40,13 @@ export const MultipleMenusLayout = () => {
|
|||||||
);
|
);
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
const footer = React.useMemo(() => {
|
const footer = useMemo(() => {
|
||||||
if (pathname.includes('/file-manager')) return null;
|
if (pathname.includes('/file-manager')) return null;
|
||||||
return <Footer />;
|
return <Footer />;
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<Fragment>
|
||||||
<div className="flex grow">
|
<div className="flex grow">
|
||||||
{!isMobile && <Sidebar />}
|
{!isMobile && <Sidebar />}
|
||||||
|
|
||||||
@@ -63,6 +64,6 @@ export const MultipleMenusLayout = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import { cn } from '@crux-kit/utils';
|
|||||||
import { MenuConfig } from '#app/config/types';
|
import { MenuConfig } from '#app/config/types';
|
||||||
import { useMenuConfig } from '#app/menu';
|
import { useMenuConfig } from '#app/menu';
|
||||||
import { useMenu } from '#shared/hooks/useMenu';
|
import { useMenu } from '#shared/hooks/useMenu';
|
||||||
|
import { createMenuPathMatcher } from '#shared/lib/menuPath';
|
||||||
import { Button } from '#shared/ui/Button';
|
import { Button } from '#shared/ui/Button';
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from '#shared/ui/DropdownMenu';
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from '#shared/ui/DropdownMenu';
|
||||||
|
|
||||||
export function SidebarMenu() {
|
export function SidebarMenu() {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const { isActive, hasActiveChild } = useMenu(pathname);
|
const { hasActiveChild } = useMenu(pathname);
|
||||||
const config = useMenuConfig();
|
const config = useMenuConfig();
|
||||||
|
|
||||||
const menuConfig: MenuConfig = [
|
const menuConfig: MenuConfig = [
|
||||||
@@ -40,6 +41,9 @@ export function SidebarMenu() {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// A leaf highlights only when nothing deeper in the menu matches the route more closely.
|
||||||
|
const isActive = createMenuPathMatcher(menuConfig, pathname);
|
||||||
|
|
||||||
const buildMenu = (items: MenuConfig) => {
|
const buildMenu = (items: MenuConfig) => {
|
||||||
return items.map((item, index) => (
|
return items.map((item, index) => (
|
||||||
<div key={index} className="flex flex-col items-center">
|
<div key={index} className="flex flex-col items-center">
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { JSX, useCallback } from 'react';
|
import { JSX, useMemo } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { cn } from '@crux-kit/utils';
|
import { cn } from '@crux-kit/utils';
|
||||||
import { MenuConfig, MenuItem } from '#app/config/types';
|
import { MenuConfig, MenuItem } from '#app/config/types';
|
||||||
import { useMenuConfig } from '#app/menu';
|
import { useMenuConfig } from '#app/menu';
|
||||||
|
import { createMenuPathMatcher } from '#shared/lib/menuPath';
|
||||||
import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuLabel, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu';
|
import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuLabel, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu';
|
||||||
|
|
||||||
export function SidebarMenuPrimary() {
|
export function SidebarMenuPrimary() {
|
||||||
@@ -12,9 +13,9 @@ export function SidebarMenuPrimary() {
|
|||||||
const menuConfig = useMenuConfig();
|
const menuConfig = useMenuConfig();
|
||||||
|
|
||||||
// Memoize matchPath to prevent unnecessary re-renders
|
// Memoize matchPath to prevent unnecessary re-renders
|
||||||
const matchPath = useCallback(
|
const matchPath = useMemo(
|
||||||
(path: string): boolean => path === pathname || (path.length > 1 && pathname.startsWith(path)),
|
() => createMenuPathMatcher(menuConfig, pathname),
|
||||||
[pathname]
|
[menuConfig, pathname]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Global classNames for consistent styling
|
// Global classNames for consistent styling
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { type MenuConfig } from './config/types';
|
import { type MenuConfig } from './config/types';
|
||||||
import { createModuleMenuConfig } from './services/modules';
|
import { createModuleMenuConfig } from './services/modules';
|
||||||
@@ -6,7 +6,7 @@ import { createModuleMenuConfig } from './services/modules';
|
|||||||
export const useMenuConfig = (): MenuConfig => {
|
export const useMenuConfig = (): MenuConfig => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
return React.useMemo(() => {
|
return useMemo(() => {
|
||||||
return createModuleMenuConfig(intl);
|
return createModuleMenuConfig(intl);
|
||||||
}, [intl]);
|
}, [intl]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import { BASE_URL } from '#app/config/app';
|
import { BASE_URL } from '#app/config/app';
|
||||||
import { AuthProvider } from '#core/Auth/providers/AuthProvider';
|
import { AuthProvider } from '#core/Auth/providers/AuthProvider';
|
||||||
|
|||||||
@@ -1,51 +1,48 @@
|
|||||||
import { DirectionProvider as RadixDirectionProvider } from '@radix-ui/react-direction';
|
import { DirectionProvider } from '@radix-ui/react-direction';
|
||||||
import { createContext, useContext, useEffect, useState, type PropsWithChildren } from 'react';
|
import { useEffect, useState, type PropsWithChildren } from 'react';
|
||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
import { LocalStorageService } from '@crux-kit/local-storage';
|
import { LocalStorageService } from '@crux-kit/local-storage';
|
||||||
import { I18N_DEFAULT_LANGUAGE, I18N_LANGUAGES } from '#app/i18n/config';
|
import { I18N_DEFAULT_LANGUAGE, I18N_LANGUAGES } from '#app/i18n/config';
|
||||||
import { I18nProviderProps, type Language } from '#app/i18n/types';
|
import TranslationsContext from '#app/i18n/context';
|
||||||
|
import { type Language, type LanguageCode } from '#app/i18n/types';
|
||||||
import { IntlDate } from '#infrastructure/IntlDate';
|
import { IntlDate } from '#infrastructure/IntlDate';
|
||||||
|
import { IntlName } from '#infrastructure/IntlName';
|
||||||
import '@formatjs/intl-relativetimeformat/polyfill';
|
import '@formatjs/intl-relativetimeformat/polyfill';
|
||||||
import '@formatjs/intl-relativetimeformat/locale-data/en';
|
import '@formatjs/intl-relativetimeformat/locale-data/en';
|
||||||
import '@formatjs/intl-relativetimeformat/locale-data/de';
|
|
||||||
import '@formatjs/intl-relativetimeformat/locale-data/es';
|
|
||||||
import '@formatjs/intl-relativetimeformat/locale-data/fr';
|
|
||||||
import '@formatjs/intl-relativetimeformat/locale-data/ja';
|
|
||||||
import '@formatjs/intl-relativetimeformat/locale-data/zh';
|
|
||||||
|
|
||||||
const storage = new LocalStorageService({ prefix: 'i18n' });
|
const storage = new LocalStorageService({ prefix: 'i18n' });
|
||||||
|
|
||||||
|
const resolveLanguage = (code?: LanguageCode): Language => {
|
||||||
|
return I18N_LANGUAGES.find((lang) => lang.code === code) ?? I18N_DEFAULT_LANGUAGE;
|
||||||
|
};
|
||||||
|
|
||||||
const getInitialLanguage = () => {
|
const getInitialLanguage = () => {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const langParam = urlParams.get('lang');
|
const langParam = urlParams.get('lang');
|
||||||
|
|
||||||
// Check if langParam matches a supported language in I18N_LANGUAGES
|
|
||||||
if (langParam) {
|
if (langParam) {
|
||||||
const matchedLanguage = I18N_LANGUAGES.find((lang) => lang.code === langParam);
|
const matchedLanguage = I18N_LANGUAGES.find((lang) => lang.code === langParam);
|
||||||
if (matchedLanguage) {
|
if (matchedLanguage) {
|
||||||
storage.set('config', matchedLanguage);
|
storage.set('config', matchedLanguage.code);
|
||||||
return matchedLanguage;
|
return matchedLanguage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return storage.get<Language>('config') ?? I18N_DEFAULT_LANGUAGE;
|
return resolveLanguage(storage.get<LanguageCode>('config'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialProps: I18nProviderProps = {
|
const initialLanguage = getInitialLanguage();
|
||||||
languages: I18N_LANGUAGES,
|
|
||||||
currenLanguage: getInitialLanguage(),
|
|
||||||
changeLanguage: (_: Language) => {},
|
|
||||||
isRTL: () => false
|
|
||||||
};
|
|
||||||
|
|
||||||
const TranslationsContext = createContext<I18nProviderProps>(initialProps);
|
IntlDate.setLocaleGlobally(initialLanguage.code);
|
||||||
const useLanguage = () => useContext(TranslationsContext);
|
IntlName.setLanguage(initialLanguage.code);
|
||||||
|
|
||||||
const I18nProvider = ({ children }: PropsWithChildren) => {
|
const I18nProvider = ({ children }: PropsWithChildren) => {
|
||||||
const [currenLanguage, setCurrenLanguage] = useState(initialProps.currenLanguage);
|
const [currenLanguage, setCurrenLanguage] = useState(initialLanguage);
|
||||||
|
|
||||||
const changeLanguage = (language: Language) => {
|
const changeLanguage = (language: Language) => {
|
||||||
storage.set('config', language);
|
storage.set('config', language.code);
|
||||||
|
IntlDate.setLocaleGlobally(language.code);
|
||||||
|
IntlName.setLanguage(language.code);
|
||||||
setCurrenLanguage(language);
|
setCurrenLanguage(language);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,7 +51,6 @@ const I18nProvider = ({ children }: PropsWithChildren) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
IntlDate.setLocaleGlobally(currenLanguage.code); // TODO: currently changes only after page reload!
|
|
||||||
document.documentElement.setAttribute('dir', currenLanguage.direction);
|
document.documentElement.setAttribute('dir', currenLanguage.direction);
|
||||||
}, [currenLanguage]);
|
}, [currenLanguage]);
|
||||||
|
|
||||||
@@ -70,13 +66,12 @@ const I18nProvider = ({ children }: PropsWithChildren) => {
|
|||||||
<IntlProvider
|
<IntlProvider
|
||||||
messages={currenLanguage.messages}
|
messages={currenLanguage.messages}
|
||||||
locale={currenLanguage.code}
|
locale={currenLanguage.code}
|
||||||
defaultLocale={getInitialLanguage().code}
|
defaultLocale={I18N_DEFAULT_LANGUAGE.code}
|
||||||
>
|
>
|
||||||
<RadixDirectionProvider dir={currenLanguage.direction}>{children}</RadixDirectionProvider>
|
<DirectionProvider dir={currenLanguage.direction}>{children}</DirectionProvider>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
</TranslationsContext.Provider>
|
</TranslationsContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { useLanguage };
|
|
||||||
export default I18nProvider;
|
export default I18nProvider;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { MutationCache, QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { MutationCache, QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ReactNode, useState } from 'react';
|
import { useState, type ReactNode } from 'react';
|
||||||
import { handleQueryError } from '#infrastructure/reactQuery';
|
import { handleQueryError } from '#infrastructure/reactQuery';
|
||||||
|
|
||||||
export const queryClient = new QueryClient({
|
export const queryClient = new QueryClient({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { LocalStorageService } from '@crux-kit/local-storage';
|
import { LocalStorageService } from '@crux-kit/local-storage';
|
||||||
import { APP_SETTINGS } from '#app/config/settings';
|
import { APP_SETTINGS } from '#app/config/settings';
|
||||||
import { Settings } from '#app/config/types';
|
import { Settings } from '#app/config/types';
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
||||||
import * as React from 'react';
|
import { type ComponentProps } from 'react';
|
||||||
|
|
||||||
export default function ThemeProvider({
|
export default function ThemeProvider({ children }: ComponentProps<typeof NextThemesProvider>) {
|
||||||
children
|
|
||||||
}: React.ComponentProps<typeof NextThemesProvider>) {
|
|
||||||
return (
|
return (
|
||||||
<NextThemesProvider
|
<NextThemesProvider
|
||||||
attribute="class"
|
attribute="class"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Navigate, RouteObject, useRoutes } from 'react-router';
|
import { Navigate, useRoutes, type RouteObject } from 'react-router';
|
||||||
import { MultipleMenusLayout } from '#app/layouts/06__MultipleMenus/layout';
|
import { MultipleMenusLayout } from '#app/layouts/06__MultipleMenus/layout';
|
||||||
import { getModuleRoutes } from '#app/services/modules';
|
import { getModuleRoutes } from '#app/services/modules';
|
||||||
import { AuthGuard } from '#core/Auth';
|
import { AuthGuard } from '#core/Auth';
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
export * from './menus';
|
export { createModuleMenuConfig } from './menus';
|
||||||
export * from './messages';
|
export { collectModuleMessages } from './messages';
|
||||||
export * from './registry';
|
export { appModules } from './registry';
|
||||||
export * from './routes';
|
export { getModuleRoutes } from './routes';
|
||||||
export * from './types';
|
|
||||||
|
export type { AppMessages } from './messages';
|
||||||
|
export type {
|
||||||
|
AppModuleArea,
|
||||||
|
AppModuleConfig,
|
||||||
|
AppModuleMessages,
|
||||||
|
AppModuleRouteScope
|
||||||
|
} from './types';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IntlShape } from 'react-intl';
|
import { type IntlShape } from 'react-intl';
|
||||||
import { MenuConfig } from '#app/config/types';
|
import { MenuConfig } from '#app/config/types';
|
||||||
import { appModules } from './registry';
|
import { appModules } from './registry';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LanguageCode } from '#app/i18n/types';
|
import { type LanguageCode } from '#app/i18n/types';
|
||||||
import { appModules } from './registry';
|
import { appModules } from './registry';
|
||||||
|
|
||||||
export type AppMessages = Record<LanguageCode, Record<string, string>>;
|
export type AppMessages = Record<LanguageCode, Record<string, string>>;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AppModuleConfig } from './types';
|
import { type AppModuleConfig } from './types';
|
||||||
|
|
||||||
const coreModules = import.meta.glob('#core/**/config/module.ts', {
|
const coreModules = import.meta.glob('#core/**/config/module.ts', {
|
||||||
eager: true,
|
eager: true,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { appModules } from './registry';
|
import { appModules } from './registry';
|
||||||
import { AppModuleRouteScope } from './types';
|
import { type AppModuleRouteScope } from './types';
|
||||||
|
|
||||||
export const getModuleRoutes = (scope: AppModuleRouteScope) => {
|
export const getModuleRoutes = (scope: AppModuleRouteScope) => {
|
||||||
return appModules
|
return appModules
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Users } from 'lucide-react';
|
import { Users } from 'lucide-react';
|
||||||
import { IntlShape } from 'react-intl';
|
import { type IntlShape } from 'react-intl';
|
||||||
import { MenuItem } from '#app/config/types';
|
import { MenuItem } from '#app/config/types';
|
||||||
|
|
||||||
export const createAccessControlMenu = (intl: IntlShape): MenuItem => ({
|
export const createAccessControlMenu = (intl: IntlShape): MenuItem => ({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AppModuleConfig } from '#app/services/modules/types';
|
import { type AppModuleConfig } from '#app/services/modules/types';
|
||||||
import { createAccessControlMenu } from './menu';
|
import { createAccessControlMenu } from './menu';
|
||||||
import { accessControlMessages } from './messages';
|
import { accessControlMessages } from './messages';
|
||||||
import { accessControlRoute } from './routing';
|
import { accessControlRoute } from './routing';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Navigate, RouteObject } from 'react-router-dom';
|
import { Navigate, type RouteObject } from 'react-router-dom';
|
||||||
import AdminListPage from '../pages/AdminListPage';
|
import AdminListPage from '../pages/AdminListPage';
|
||||||
import RoleListPage from '../pages/RoleListPage';
|
import RoleListPage from '../pages/RoleListPage';
|
||||||
import AdminManagementLayout from '../view/layouts/AdminManagementLayout';
|
import AdminManagementLayout from '../view/layouts/AdminManagementLayout';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { axiosClient } from '#infrastructure/axios';
|
import { axiosClient } from '#infrastructure/axios';
|
||||||
import { ListQueryParams, SuccessListResponse, SuccessMutationResponse } from '#infrastructure/reactQuery';
|
import type { ListQueryParams, SuccessListResponse, SuccessMutationResponse } from '#infrastructure/reactQuery';
|
||||||
import { AdminDTO } from './dto';
|
import type { AdminDTO } from './AccessControlDTO';
|
||||||
|
|
||||||
const ROOT_API_ENDPOINT = '/admin';
|
const ROOT_API_ENDPOINT = '/admin';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { axiosClient } from '#infrastructure/axios';
|
import { axiosClient } from '#infrastructure/axios';
|
||||||
import { PermissionDTO } from './dto';
|
import type { PermissionDTO } from './AccessControlDTO';
|
||||||
|
|
||||||
const ROOT_API_ENDPOINT = '/permission';
|
const ROOT_API_ENDPOINT = '/permission';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { axiosClient } from '#infrastructure/axios';
|
import { axiosClient } from '#infrastructure/axios';
|
||||||
import { ListQueryParams, SuccessListResponse, SuccessMutationResponse } from '#infrastructure/reactQuery';
|
import type { ListQueryParams, SuccessListResponse, SuccessMutationResponse } from '#infrastructure/reactQuery';
|
||||||
import { RoleDTO } from './dto';
|
import type { RoleDTO } from './AccessControlDTO';
|
||||||
|
|
||||||
const ROOT_API_ENDPOINT = '/role';
|
const ROOT_API_ENDPOINT = '/role';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AdminEntity, AdminStatusEnum } from '../../entities/AdminEntity';
|
import type { AdminDTO } from '#core/AccessControl/data/api/AccessControlDTO';
|
||||||
import { AdminDTO } from '../api/dto';
|
import RoleMapper from '#core/AccessControl/data/mappers/RoleMapper';
|
||||||
import RoleMapper from './RoleMapper';
|
import type { AdminEntity, AdminStatusEnum } from '#core/AccessControl/entities/AdminEntity';
|
||||||
|
|
||||||
export default class AdminMapper {
|
export default class AdminMapper {
|
||||||
private readonly roleMapper = new RoleMapper();
|
private readonly roleMapper = new RoleMapper();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PermissionEntity } from '../../entities/PermissionEntity';
|
import type { PermissionDTO } from '#core/AccessControl/data/api/AccessControlDTO';
|
||||||
import { PermissionDTO } from '../api/dto';
|
import type { PermissionEntity } from '#core/AccessControl/entities/PermissionEntity';
|
||||||
|
|
||||||
export default class PermissionMapper {
|
export default class PermissionMapper {
|
||||||
public toEntity = (dto: PermissionDTO): PermissionEntity => {
|
public toEntity = (dto: PermissionDTO): PermissionEntity => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { RoleEntity } from '../../entities/RoleEntity';
|
import type { RoleDTO } from '#core/AccessControl/data/api/AccessControlDTO';
|
||||||
import { RoleDTO } from '../api/dto';
|
import PermissionMapper from '#core/AccessControl/data/mappers/PermissionMapper';
|
||||||
import PermissionMapper from './PermissionMapper';
|
import type { RoleEntity } from '#core/AccessControl/entities/RoleEntity';
|
||||||
|
|
||||||
export default class RoleMapper {
|
export default class RoleMapper {
|
||||||
private readonly permissionMapper = new PermissionMapper();
|
private readonly permissionMapper = new PermissionMapper();
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { keepPreviousData, useMutation, useQuery } from '@tanstack/react-query';
|
import { keepPreviousData, useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import React from 'react';
|
import { useMemo } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { ErrorResponse, getPaginationInfo, ListQueryParams, SuccessMutationResponse } from '#infrastructure/reactQuery';
|
import { getPaginationInfo, type ErrorResponse, type ListQueryParams, type SuccessMutationResponse } from '#infrastructure/reactQuery';
|
||||||
import AdminAPI from '../api/AdminAPI';
|
import AdminAPI from '#core/AccessControl/data/api/AdminAPI';
|
||||||
import AdminMapper from '../mappers/AdminMapper';
|
import AdminMapper from '#core/AccessControl/data/mappers/AdminMapper';
|
||||||
import { adminDetailedKeyBuilder, adminListKeyBuilder, DetailedPayload } from './keys';
|
import { adminDetailedKeyBuilder, adminListKeyBuilder, type DetailedPayload } from './keys';
|
||||||
|
|
||||||
const api = new AdminAPI();
|
const api = new AdminAPI();
|
||||||
const mapper = new AdminMapper();
|
const mapper = new AdminMapper();
|
||||||
@@ -19,7 +19,7 @@ export const useGetList = (params: ListQueryParams = { page: 1, perPage: 10 }) =
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const list = React.useMemo(() => {
|
const list = useMemo(() => {
|
||||||
return mapper.toEntities(data?.data ?? []);
|
return mapper.toEntities(data?.data ?? []);
|
||||||
}, [data?.data]);
|
}, [data?.data]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createKeyBuilder, ListQueryParams } from '#infrastructure/reactQuery';
|
import { createKeyBuilder, type ListQueryParams } from '#infrastructure/reactQuery';
|
||||||
|
|
||||||
export interface DetailedPayload {
|
export interface DetailedPayload {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
||||||
import PermissionAPI from '../api/PermissionAPI';
|
import PermissionAPI from '#core/AccessControl/data/api/PermissionAPI';
|
||||||
import PermissionMapper from '../mappers/PermissionMapper';
|
import PermissionMapper from '#core/AccessControl/data/mappers/PermissionMapper';
|
||||||
import { permissionListKeyBuilder } from './keys';
|
import { permissionListKeyBuilder } from './keys';
|
||||||
|
|
||||||
const api = new PermissionAPI();
|
const api = new PermissionAPI();
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { keepPreviousData, useMutation, useQuery } from '@tanstack/react-query';
|
import { keepPreviousData, useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { ErrorResponse, getPaginationInfo, ListQueryParams, SuccessMutationResponse } from '#infrastructure/reactQuery';
|
import { getPaginationInfo, type ErrorResponse, type ListQueryParams, type SuccessMutationResponse } from '#infrastructure/reactQuery';
|
||||||
import RoleAPI from '../api/RoleAPI';
|
import RoleAPI from '#core/AccessControl/data/api/RoleAPI';
|
||||||
import RoleMapper from '../mappers/RoleMapper';
|
import RoleMapper from '#core/AccessControl/data/mappers/RoleMapper';
|
||||||
import { DetailedPayload, roleDetailedKeyBuilder, roleListKeyBuilder } from './keys';
|
import { roleDetailedKeyBuilder, roleListKeyBuilder, type DetailedPayload } from './keys';
|
||||||
|
|
||||||
const api = new RoleAPI();
|
const api = new RoleAPI();
|
||||||
const mapper = new RoleMapper();
|
const mapper = new RoleMapper();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { IsoDateString } from '@crux-kit/types';
|
import type { IsoDateString } from '@crux-kit/types';
|
||||||
import { RoleEntity } from './RoleEntity';
|
import type { RoleEntity } from './RoleEntity';
|
||||||
|
|
||||||
export enum AdminStatusEnum {
|
export enum AdminStatusEnum {
|
||||||
ACTIVE = 'active',
|
ACTIVE = 'active',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { IsoDateString } from '@crux-kit/types';
|
import type { IsoDateString } from '@crux-kit/types';
|
||||||
import { PermissionEntity } from './PermissionEntity';
|
import type { PermissionEntity } from './PermissionEntity';
|
||||||
|
|
||||||
export interface RoleEntity {
|
export interface RoleEntity {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { AdminTable } from '#core/AccessControl/view/widgets/AdminTable';
|
||||||
import { PermissionPageGate } from '#core/Permission';
|
import { PermissionPageGate } from '#core/Permission';
|
||||||
import { Container } from '#shared/widgets/common/Container';
|
import { Container } from '#shared/widgets/common/Container';
|
||||||
import { AdminTable } from '../../view/widgets/AdminTable';
|
|
||||||
|
|
||||||
const AdminListPage: React.FC = () => {
|
const AdminListPage: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { RoleTable } from '#core/AccessControl/view/widgets/RoleTable';
|
||||||
import { PermissionPageGate } from '#core/Permission';
|
import { PermissionPageGate } from '#core/Permission';
|
||||||
import { Container } from '#shared/widgets/common/Container';
|
import { Container } from '#shared/widgets/common/Container';
|
||||||
import { RoleTable } from '../../view/widgets/RoleTable';
|
|
||||||
|
|
||||||
const RoleListPage: React.FC = () => {
|
const RoleListPage: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { FormElement, FormGrid, GridElement, InputCheckboxGroup, InputSelect, In
|
|||||||
import { Button } from '#shared/ui/Button';
|
import { Button } from '#shared/ui/Button';
|
||||||
import { Form as FormProvider } from '#shared/ui/form';
|
import { Form as FormProvider } from '#shared/ui/form';
|
||||||
import { Separator } from '#shared/ui/Separator';
|
import { Separator } from '#shared/ui/Separator';
|
||||||
import { ADMIN_FORM_ID, createValidationSchema, FormValues, SubmitFunc } from './form';
|
import { ADMIN_FORM_ID, createValidationSchema, type FormValues, type SubmitFunc } from './form';
|
||||||
|
|
||||||
export const AdminForm: React.FC<{
|
export const AdminForm: React.FC<{
|
||||||
roles: Array<{ label: string; value: string }>;
|
roles: Array<{ label: string; value: string }>;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { UseFormReturn } from 'react-hook-form';
|
import { type UseFormReturn } from 'react-hook-form';
|
||||||
import { IntlShape } from 'react-intl';
|
import { type IntlShape } from 'react-intl';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
import { uid } from '@crux-kit/utils';
|
import { uid } from '@crux-kit/utils';
|
||||||
import { AdminEntity, AdminStatusEnum } from '#core/AccessControl/entities/AdminEntity';
|
import { AdminStatusEnum, type AdminEntity } from '#core/AccessControl/entities/AdminEntity';
|
||||||
|
|
||||||
export const ADMIN_FORM_ID = uid();
|
export const ADMIN_FORM_ID = uid();
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { extractValidationErrors } from '#infrastructure/reactQuery';
|
|||||||
import { useCreate, useUpdate } from '#core/AccessControl/data/queries/admin';
|
import { useCreate, useUpdate } from '#core/AccessControl/data/queries/admin';
|
||||||
import { invalidateAdminDetailed, invalidateAdminList } from '#core/AccessControl/data/queries/cache';
|
import { invalidateAdminDetailed, invalidateAdminList } from '#core/AccessControl/data/queries/cache';
|
||||||
import { useAuth } from '#core/Auth/providers/AuthProvider';
|
import { useAuth } from '#core/Auth/providers/AuthProvider';
|
||||||
import { SubmitFunc } from './form';
|
import { type SubmitFunc } from './form';
|
||||||
|
|
||||||
interface Params {
|
interface Params {
|
||||||
onSuccess?: (id?: number) => void;
|
onSuccess?: (id?: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSubmit = (params?: Params) => {
|
export const useSubmit = (params?: Params) => {
|
||||||
const onSuccess = params?.onSuccess;
|
const onSuccess = params?.onSuccess;
|
||||||
const { verify } = useAuth();
|
const { verify } = useAuth();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useIntl } from 'react-intl';
|
|||||||
import { FormElement, FormGrid, GridElement, InputText } from '#shared/forms/composed';
|
import { FormElement, FormGrid, GridElement, InputText } from '#shared/forms/composed';
|
||||||
import { FormControl, FormField, FormItem, FormLabel, FormMessage, Form as FormProvider } from '#shared/ui/form';
|
import { FormControl, FormField, FormItem, FormLabel, FormMessage, Form as FormProvider } from '#shared/ui/form';
|
||||||
import { Switch, SwitchWrapper } from '#shared/ui/Switch';
|
import { Switch, SwitchWrapper } from '#shared/ui/Switch';
|
||||||
import { createValidationSchema, FormValues, ROLE_FORM_ID, SubmitFunc } from './form';
|
import { createValidationSchema, ROLE_FORM_ID, type FormValues, type SubmitFunc } from './form';
|
||||||
|
|
||||||
export const RoleForm: React.FC<{
|
export const RoleForm: React.FC<{
|
||||||
permissions: Record<string, Array<{ label: string; value: string }>>;
|
permissions: Record<string, Array<{ label: string; value: string }>>;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { UseFormReturn } from 'react-hook-form';
|
|||||||
import { IntlShape } from 'react-intl';
|
import { IntlShape } from 'react-intl';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
import { uid } from '@crux-kit/utils';
|
import { uid } from '@crux-kit/utils';
|
||||||
import { RoleEntity } from '#core/AccessControl/entities/RoleEntity';
|
import { type RoleEntity } from '#core/AccessControl/entities/RoleEntity';
|
||||||
|
|
||||||
export const ROLE_FORM_ID = uid();
|
export const ROLE_FORM_ID = uid();
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { ErrorOption, UseFormReturn } from 'react-hook-form';
|
import type { ErrorOption, UseFormReturn } from 'react-hook-form';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { ErrorResponse, extractValidationErrors } from '#infrastructure/reactQuery';
|
import { extractValidationErrors, type ErrorResponse } from '#infrastructure/reactQuery';
|
||||||
import { invalidateRoleDetailed, invalidateRoleList } from '#core/AccessControl/data/queries/cache';
|
import { invalidateRoleDetailed, invalidateRoleList } from '#core/AccessControl/data/queries/cache';
|
||||||
import { useCreate, useUpdate } from '#core/AccessControl/data/queries/role';
|
import { useCreate, useUpdate } from '#core/AccessControl/data/queries/role';
|
||||||
import { useAuth } from '#core/Auth/providers/AuthProvider';
|
import { useAuth } from '#core/Auth/providers/AuthProvider';
|
||||||
import { FormValues, SubmitFunc } from './form';
|
import type { FormValues, SubmitFunc } from './form';
|
||||||
|
|
||||||
interface Params {
|
interface Params {
|
||||||
onSuccess?: (id?: number) => void;
|
onSuccess?: (id?: number) => void;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { Outlet, useLocation } from 'react-router';
|
import { Outlet, useLocation } from 'react-router';
|
||||||
|
import { createAccessControlMenu } from '#core/AccessControl/config/menu';
|
||||||
|
import AccessControlLogo from '#core/AccessControl/view/ui/AccessControlLogo';
|
||||||
import { useMenu } from '#shared/hooks/useMenu';
|
import { useMenu } from '#shared/hooks/useMenu';
|
||||||
import { Container } from '#shared/widgets/common/Container';
|
import { Container } from '#shared/widgets/common/Container';
|
||||||
import { createAccessControlMenu } from '../../config/menu';
|
|
||||||
import AccessControlLogo from '../ui/AccessControlLogo';
|
|
||||||
|
|
||||||
const AdminManagementLayout: React.FC = () => {
|
const AdminManagementLayout: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Column, ColumnDef, ColumnFiltersState, getCoreRowModel, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, PaginationState, SortingState, useReactTable } from '@tanstack/react-table';
|
import { getCoreRowModel, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table';
|
||||||
|
import type { Column, ColumnDef, ColumnFiltersState, PaginationState, SortingState } from '@tanstack/react-table';
|
||||||
import { LoaderCircleIcon, SearchIcon, Settings2, SquarePen } from 'lucide-react';
|
import { LoaderCircleIcon, SearchIcon, Settings2, SquarePen } from 'lucide-react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IntlShape, useIntl } from 'react-intl';
|
import { useIntl, type IntlShape } from 'react-intl';
|
||||||
import { useDebounceSearch } from '@crux-kit/hooks';
|
import { useDebounceSearch } from '@crux-kit/hooks';
|
||||||
import { IntlName } from '#infrastructure/IntlName';
|
import useIntlName from '#app/i18n/useIntlName';
|
||||||
import { mapTableStateToListQueryParams } from '#infrastructure/reactQuery';
|
import { mapTableStateToListQueryParams } from '#infrastructure/reactQuery';
|
||||||
import { PermissionGate } from '#core/Permission';
|
import { PermissionGate } from '#core/Permission';
|
||||||
import { Badge } from '#shared/ui/Badge';
|
import { Badge } from '#shared/ui/Badge';
|
||||||
@@ -15,7 +16,7 @@ import { ScrollArea, ScrollBar } from '#shared/ui/ScrollArea';
|
|||||||
import { Skeleton } from '#shared/ui/Skeleton';
|
import { Skeleton } from '#shared/ui/Skeleton';
|
||||||
import { DataGrid, DataGridColumnFilter, DataGridColumnHeader, DataGridColumnVisibility, DataGridPagination, DataGridTable, DataGridToolbarFilters, useDataGrid } from '#shared/widgets/DataGrid';
|
import { DataGrid, DataGridColumnFilter, DataGridColumnHeader, DataGridColumnVisibility, DataGridPagination, DataGridTable, DataGridToolbarFilters, useDataGrid } from '#shared/widgets/DataGrid';
|
||||||
import { useGetList } from '../../data/queries/admin';
|
import { useGetList } from '../../data/queries/admin';
|
||||||
import { AdminEntity, AdminStatusEnum } from '../../entities/AdminEntity';
|
import { AdminStatusEnum, type AdminEntity } from '../../entities/AdminEntity';
|
||||||
import { DeleteAdminTrigger } from './DeleteAdminTrigger';
|
import { DeleteAdminTrigger } from './DeleteAdminTrigger';
|
||||||
import { ManageAdminTrigger } from './ManageAdminTrigger';
|
import { ManageAdminTrigger } from './ManageAdminTrigger';
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ const getStatusOptions = (intl: IntlShape) => [
|
|||||||
|
|
||||||
export const AdminTable = () => {
|
export const AdminTable = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const formatName = useIntlName();
|
||||||
|
|
||||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
@@ -180,7 +182,7 @@ export const AdminTable = () => {
|
|||||||
cell: (info) => {
|
cell: (info) => {
|
||||||
const data = info.row.original;
|
const data = info.row.original;
|
||||||
const id = data.id;
|
const id = data.id;
|
||||||
const name = IntlName.formatToDisplay(data, 'SURNAME_NAME');
|
const name = formatName(data, 'SURNAME_NAME');
|
||||||
const editModalTitle =
|
const editModalTitle =
|
||||||
intl.formatMessage({ id: 'ACCESS_CONTROL.ADMIN.EDIT' }) + ' ' + name;
|
intl.formatMessage({ id: 'ACCESS_CONTROL.ADMIN.EDIT' }) + ' ' + name;
|
||||||
|
|
||||||
@@ -207,7 +209,7 @@ export const AdminTable = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[intl, statusOptions]
|
[intl, statusOptions, formatName]
|
||||||
);
|
);
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { LoaderCircleIcon } from 'lucide-react';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { useDisclosure } from '@crux-kit/hooks';
|
import { useDisclosure } from '@crux-kit/hooks';
|
||||||
|
import { useGetDetailed } from '#core/AccessControl/data/queries/admin';
|
||||||
|
import { useGetList } from '#core/AccessControl/data/queries/role';
|
||||||
|
import { ADMIN_FORM_ID, AdminForm, initialValues, prepareInitialValues, useSubmit } from '#core/AccessControl/view/forms/AdminForm';
|
||||||
import { Button } from '#shared/ui/Button';
|
import { Button } from '#shared/ui/Button';
|
||||||
import { Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '#shared/ui/Dialog';
|
import { Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '#shared/ui/Dialog';
|
||||||
import { Skeleton } from '#shared/ui/Skeleton';
|
import { Skeleton } from '#shared/ui/Skeleton';
|
||||||
import { useGetDetailed } from '../../data/queries/admin';
|
|
||||||
import { useGetList } from '../../data/queries/role';
|
|
||||||
import { ADMIN_FORM_ID, AdminForm, initialValues, prepareInitialValues, useSubmit } from '../forms/AdminForm';
|
|
||||||
|
|
||||||
export const ManageAdminTrigger: React.FC<
|
export const ManageAdminTrigger: React.FC<
|
||||||
React.PropsWithChildren<{
|
React.PropsWithChildren<{
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { LoaderCircleIcon } from 'lucide-react';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { useDisclosure } from '@crux-kit/hooks';
|
import { useDisclosure } from '@crux-kit/hooks';
|
||||||
|
import { useGetList } from '#core/AccessControl/data/queries/permission';
|
||||||
|
import { useGetDetailed } from '#core/AccessControl/data/queries/role';
|
||||||
|
import { initialValues, prepareInitialValues, ROLE_FORM_ID, RoleForm, useSubmit } from '#core/AccessControl/view/forms/RoleForm';
|
||||||
import { Button } from '#shared/ui/Button';
|
import { Button } from '#shared/ui/Button';
|
||||||
import { Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '#shared/ui/Dialog';
|
import { Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '#shared/ui/Dialog';
|
||||||
import { Skeleton } from '#shared/ui/Skeleton';
|
import { Skeleton } from '#shared/ui/Skeleton';
|
||||||
import { useGetList } from '../../data/queries/permission';
|
|
||||||
import { useGetDetailed } from '../../data/queries/role';
|
|
||||||
import { initialValues, prepareInitialValues, ROLE_FORM_ID, RoleForm, useSubmit } from '../forms/RoleForm';
|
|
||||||
|
|
||||||
export const ManageRoleTrigger: React.FC<
|
export const ManageRoleTrigger: React.FC<
|
||||||
React.PropsWithChildren<{
|
React.PropsWithChildren<{
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import { Column, ColumnDef, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, PaginationState, SortingState, useReactTable } from '@tanstack/react-table';
|
import { getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table';
|
||||||
|
import type { Column, ColumnDef, PaginationState, SortingState } from '@tanstack/react-table';
|
||||||
import { LoaderCircleIcon, SearchIcon, Settings2, SquarePen } from 'lucide-react';
|
import { LoaderCircleIcon, SearchIcon, Settings2, SquarePen } from 'lucide-react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { useDebounceSearch } from '@crux-kit/hooks';
|
import { useDebounceSearch } from '@crux-kit/hooks';
|
||||||
import { mapTableStateToListQueryParams } from '#infrastructure/reactQuery';
|
import { mapTableStateToListQueryParams } from '#infrastructure/reactQuery';
|
||||||
|
import { useGetList } from '#core/AccessControl/data/queries/role';
|
||||||
|
import { RoleEntity } from '#core/AccessControl/entities/RoleEntity';
|
||||||
|
import { DeleteRoleTrigger } from '#core/AccessControl/view/widgets/DeleteRoleTrigger';
|
||||||
|
import { ManageRoleTrigger } from '#core/AccessControl/view/widgets/ManageRoleTrigger';
|
||||||
import { PermissionGate } from '#core/Permission';
|
import { PermissionGate } from '#core/Permission';
|
||||||
import { Button } from '#shared/ui/Button';
|
import { Button } from '#shared/ui/Button';
|
||||||
import { Card, CardFooter, CardHeader, CardTable, CardTitle, CardToolbar } from '#shared/ui/Card';
|
import { Card, CardFooter, CardHeader, CardTable, CardTitle, CardToolbar } from '#shared/ui/Card';
|
||||||
@@ -12,10 +17,6 @@ import { Input, InputWrapper } from '#shared/ui/Input';
|
|||||||
import { ScrollArea, ScrollBar } from '#shared/ui/ScrollArea';
|
import { ScrollArea, ScrollBar } from '#shared/ui/ScrollArea';
|
||||||
import { Skeleton } from '#shared/ui/Skeleton';
|
import { Skeleton } from '#shared/ui/Skeleton';
|
||||||
import { DataGrid, DataGridColumnHeader, DataGridColumnVisibility, DataGridPagination, DataGridTable, useDataGrid } from '#shared/widgets/DataGrid';
|
import { DataGrid, DataGridColumnHeader, DataGridColumnVisibility, DataGridPagination, DataGridTable, useDataGrid } from '#shared/widgets/DataGrid';
|
||||||
import { useGetList } from '../../data/queries/role';
|
|
||||||
import { RoleEntity } from '../../entities/RoleEntity';
|
|
||||||
import { DeleteRoleTrigger } from './DeleteRoleTrigger';
|
|
||||||
import { ManageRoleTrigger } from './ManageRoleTrigger';
|
|
||||||
|
|
||||||
interface IColumnFilterProps<TData, TValue> {
|
interface IColumnFilterProps<TData, TValue> {
|
||||||
column: Column<TData, TValue>;
|
column: Column<TData, TValue>;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AppModuleConfig } from '#app/services/modules/types';
|
import type { AppModuleConfig } from '#app/services/modules/types';
|
||||||
import { accountSettingsMessages } from './messages';
|
import { accountSettingsMessages } from './messages';
|
||||||
import { accountSettingsRoute } from './routing';
|
import { accountSettingsRoute } from './routing';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Navigate, Route, RouteObject, Routes } from 'react-router-dom';
|
import { Navigate, Route, Routes, type RouteObject } from 'react-router-dom';
|
||||||
import ProfilePage from '../pages/Profile';
|
import ProfilePage from '#core/AccountSettings/pages/Profile';
|
||||||
import AccountLayout from '../view/layouts/AccountLayout';
|
import AccountLayout from '#core/AccountSettings/view/layouts/AccountLayout';
|
||||||
|
|
||||||
function Routing() {
|
function Routing() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import React from 'react';
|
|||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Toolbar, ToolbarHeading } from '#app/layouts/01__Default/components/Toolbar';
|
import { Toolbar, ToolbarHeading } from '#app/layouts/01__Default/components/Toolbar';
|
||||||
import { ErrorResponse, extractValidationErrors, isValidationError } from '#infrastructure/reactQuery';
|
import { extractValidationErrors, isValidationError, type ErrorResponse } from '#infrastructure/reactQuery';
|
||||||
|
import { prepareFormValues, ProfileForm, SubmitFunc } from '#core/AccountSettings/view/forms/ProfileForm';
|
||||||
import { useAuth } from '#core/Auth/providers/AuthProvider';
|
import { useAuth } from '#core/Auth/providers/AuthProvider';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '#shared/ui/Card';
|
import { Card, CardContent, CardHeader, CardTitle } from '#shared/ui/Card';
|
||||||
import { Container } from '#shared/widgets/common/Container';
|
import { Container } from '#shared/widgets/common/Container';
|
||||||
import { prepareFormValues, ProfileForm, SubmitFunc } from '../../view/forms/ProfileForm';
|
|
||||||
|
|
||||||
const ProfilePage: React.FC = () => {
|
const ProfilePage: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Button } from '#shared/ui/Button';
|
|||||||
import { Form as FormProvider } from '#shared/ui/form';
|
import { Form as FormProvider } from '#shared/ui/form';
|
||||||
import AvatarCropperField from './fields/AvatarCropperField';
|
import AvatarCropperField from './fields/AvatarCropperField';
|
||||||
import PasswordField from './fields/PasswordField';
|
import PasswordField from './fields/PasswordField';
|
||||||
import { createValidationSchema, FormValues, SubmitFunc } from './form';
|
import { createValidationSchema, type FormValues, type SubmitFunc } from './form';
|
||||||
|
|
||||||
export const ProfileForm: React.FC<{
|
export const ProfileForm: React.FC<{
|
||||||
initialValues: FormValues;
|
initialValues: FormValues;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { UseFormReturn } from 'react-hook-form';
|
import type { UseFormReturn } from 'react-hook-form';
|
||||||
import { IntlShape } from 'react-intl';
|
import type { IntlShape } from 'react-intl';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
import { AuthUser } from '#core/Auth/entities';
|
import type { AuthUser } from '#core/Auth/entities';
|
||||||
|
|
||||||
export const createValidationSchema = (intl: IntlShape) => {
|
export const createValidationSchema = (intl: IntlShape) => {
|
||||||
return z
|
return z
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AppModuleConfig } from '#app/services/modules/types';
|
import type { AppModuleConfig } from '#app/services/modules/types';
|
||||||
import { authMessages } from './messages';
|
import { authMessages } from './messages';
|
||||||
import { authRoute } from './routing';
|
import { authRoute } from './routing';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Navigate, Route, RouteObject, Routes } from 'react-router-dom';
|
import { Navigate, Route, Routes, type RouteObject } from 'react-router-dom';
|
||||||
import { SignInPage } from '../pages/SignInPage';
|
import SignInPage from '#core/Auth/pages/SignInPage';
|
||||||
import { BrandedLayout } from '../view/layouts/Branded';
|
import BrandedLayout from '#core/Auth/view/layouts/Branded';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles all authentication related routes.
|
* Handles all authentication related routes.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { axiosClient } from '#infrastructure/axios';
|
import { axiosClient } from '#infrastructure/axios';
|
||||||
import { AuthModel } from '#core/Auth/entities';
|
import type { AuthModel } from '#core/Auth/entities';
|
||||||
import { GetUserResponse, UpdateUserPayload } from './dto';
|
import type { GetUserResponse, UpdateUserPayload } from './dto';
|
||||||
|
|
||||||
export const AuthApi = {
|
export const AuthApi = {
|
||||||
async login(email: string, password: string): Promise<AuthModel> {
|
async login(email: string, password: string): Promise<AuthModel> {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AuthUser } from '#core/Auth/entities';
|
import type { AuthUser } from '#core/Auth/entities';
|
||||||
|
|
||||||
export type UpdateUserPayload = Partial<AuthUser>;
|
export type UpdateUserPayload = Partial<AuthUser>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AuthUser } from '../../entities';
|
import type { GetUserResponse } from '#core/Auth/data/api/dto';
|
||||||
import { GetUserResponse } from '../api/dto';
|
import type { AuthUser } from '#core/Auth/entities';
|
||||||
|
|
||||||
export default class UserMapper {
|
export default class UserMapper {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { LocalStorageService } from '@crux-kit/local-storage';
|
import { LocalStorageService } from '@crux-kit/local-storage';
|
||||||
import { APP_VERSION } from '#app/config/app';
|
import { APP_VERSION } from '#app/config/app';
|
||||||
import { AuthModel } from '#core/Auth/entities';
|
import type { AuthModel } from '#core/Auth/entities';
|
||||||
|
|
||||||
const storage = new LocalStorageService({
|
const storage = new LocalStorageService({
|
||||||
prefix: `auth-v${APP_VERSION || '1.0'}`
|
prefix: `auth-v${APP_VERSION || '1.0'}`
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export * from './auth';
|
export type { AuthModel } from './auth';
|
||||||
export * from './user';
|
export type { AuthUser } from './user';
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useAuth } from '../providers/AuthProvider';
|
|||||||
import { SignInForm } from '../view/forms/SignInForm/Component';
|
import { SignInForm } from '../view/forms/SignInForm/Component';
|
||||||
import { SubmitFunc } from '../view/forms/SignInForm/form';
|
import { SubmitFunc } from '../view/forms/SignInForm/form';
|
||||||
|
|
||||||
export function SignInPage() {
|
export default function SignInPage() {
|
||||||
const { login } = useAuth();
|
const { login } = useAuth();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { PropsWithChildren, useState } from 'react';
|
import { useState, type PropsWithChildren } from 'react';
|
||||||
import { queryClient } from '#app/providers/QueryProvider';
|
import { queryClient } from '#app/providers/QueryProvider';
|
||||||
import { AuthApi } from '#core/Auth/data/api/api';
|
import { AuthApi } from '#core/Auth/data/api/api';
|
||||||
import { UpdateUserPayload } from '#core/Auth/data/api/dto';
|
import type { UpdateUserPayload } from '#core/Auth/data/api/dto';
|
||||||
import UserMapper from '#core/Auth/data/mappers/UserMapper';
|
import UserMapper from '#core/Auth/data/mappers/UserMapper';
|
||||||
import * as authHelper from '#core/Auth/data/stores/localStorage';
|
import { getAuth, removeAuth, setAuth as setStoredAuth } from '#core/Auth/data/stores/localStorage';
|
||||||
import { AuthModel, AuthUser } from '#core/Auth/entities';
|
import type { AuthModel, AuthUser } from '#core/Auth/entities';
|
||||||
import { AuthContext } from './context';
|
import { AuthContext } from './context';
|
||||||
|
|
||||||
export function AuthProvider({ children }: PropsWithChildren) {
|
export function AuthProvider({ children }: PropsWithChildren) {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [auth, setAuth] = useState<AuthModel | undefined>(authHelper.getAuth());
|
const [auth, setAuth] = useState<AuthModel | undefined>(getAuth());
|
||||||
const [currentUser, setCurrentUser] = useState<AuthUser | undefined>();
|
const [currentUser, setCurrentUser] = useState<AuthUser | undefined>();
|
||||||
|
|
||||||
const verify = async () => {
|
const verify = async () => {
|
||||||
@@ -29,9 +29,9 @@ export function AuthProvider({ children }: PropsWithChildren) {
|
|||||||
const saveAuth = (auth: AuthModel | undefined) => {
|
const saveAuth = (auth: AuthModel | undefined) => {
|
||||||
setAuth(auth);
|
setAuth(auth);
|
||||||
if (auth) {
|
if (auth) {
|
||||||
authHelper.setAuth(auth);
|
setStoredAuth(auth);
|
||||||
} else {
|
} else {
|
||||||
authHelper.removeAuth();
|
removeAuth();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createContext, useContext } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
import { GetUserResponse, UpdateUserPayload } from '#core/Auth/data/api/dto';
|
import type { GetUserResponse, UpdateUserPayload } from '#core/Auth/data/api/dto';
|
||||||
import { AuthModel, AuthUser } from '#core/Auth/entities';
|
import type { AuthModel, AuthUser } from '#core/Auth/entities';
|
||||||
|
|
||||||
export const AuthContext = createContext<{
|
export const AuthContext = createContext<{
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useIntl } from 'react-intl';
|
|||||||
import { FormElement, FormGrid, GridElement, InputText } from '#shared/forms/composed';
|
import { FormElement, FormGrid, GridElement, InputText } from '#shared/forms/composed';
|
||||||
import { Button } from '#shared/ui/Button';
|
import { Button } from '#shared/ui/Button';
|
||||||
import { Form } from '#shared/ui/form';
|
import { Form } from '#shared/ui/form';
|
||||||
import { createValidationSchema, FormValues, SubmitFunc } from './form';
|
import { createValidationSchema, type FormValues, type SubmitFunc } from './form';
|
||||||
|
|
||||||
export const SignInForm: React.FC<{
|
export const SignInForm: React.FC<{
|
||||||
initialValues: FormValues;
|
initialValues: FormValues;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IntlShape } from 'react-intl';
|
import type { IntlShape } from 'react-intl';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
|
|
||||||
export const createValidationSchema = (intl: IntlShape) =>
|
export const createValidationSchema = (intl: IntlShape) =>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { toAbsoluteUrl } from '#shared/lib/helpers';
|
|||||||
import { Badge } from '#shared/ui/Badge';
|
import { Badge } from '#shared/ui/Badge';
|
||||||
import { Card, CardContent } from '#shared/ui/Card';
|
import { Card, CardContent } from '#shared/ui/Card';
|
||||||
|
|
||||||
export function BrandedLayout() {
|
export default function BrandedLayout() {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Link, Outlet } from 'react-router-dom';
|
|||||||
import { toAbsoluteUrl } from '#shared/lib/helpers';
|
import { toAbsoluteUrl } from '#shared/lib/helpers';
|
||||||
import { Card, CardContent } from '#shared/ui/Card';
|
import { Card, CardContent } from '#shared/ui/Card';
|
||||||
|
|
||||||
export function ClassicLayout() {
|
export default function ClassicLayout() {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<style>
|
<style>
|
||||||
@@ -26,7 +26,7 @@ export function ClassicLayout() {
|
|||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<Card className="w-full max-w-[25rem]">
|
<Card className="w-full max-w-100">
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AppModuleConfig } from '#app/services/modules/types';
|
import type { AppModuleConfig } from '#app/services/modules/types';
|
||||||
import { fallbackScreensMessages } from './messages';
|
import { fallbackScreensMessages } from './messages';
|
||||||
import { fallbackScreensRoute } from './routing';
|
import { fallbackScreensRoute } from './routing';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FolderOpen, Wrench } from 'lucide-react';
|
import { FolderOpen, Wrench } from 'lucide-react';
|
||||||
import { IntlShape } from 'react-intl';
|
import type { IntlShape } from 'react-intl';
|
||||||
import { MenuItem } from '#app/config/types';
|
import { MenuItem } from '#app/config/types';
|
||||||
|
|
||||||
export const createFileManagerMenu = (intl: IntlShape): MenuItem => ({
|
export const createFileManagerMenu = (intl: IntlShape): MenuItem => ({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AppModuleConfig } from '#app/services/modules/types';
|
import type { AppModuleConfig } from '#app/services/modules/types';
|
||||||
import { createFileManagerMenu } from './menu';
|
import { createFileManagerMenu } from './menu';
|
||||||
import { fileManagerMessages } from './messages';
|
import { fileManagerMessages } from './messages';
|
||||||
import { fileManagerRoute } from './routing';
|
import { fileManagerRoute } from './routing';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { AxiosProgressEvent, GenericAbortSignal } from 'axios';
|
import type { AxiosProgressEvent, GenericAbortSignal } from 'axios';
|
||||||
import { axiosClient } from '#infrastructure/axios';
|
import { axiosClient } from '#infrastructure/axios';
|
||||||
import { ensureLeadingSlash } from '#core/FileManager/shared/path/leadingSlash';
|
import { ensureLeadingSlash } from '#core/FileManager/shared/path/leadingSlash';
|
||||||
import * as DTO from './dto';
|
import type { CreateDirectoryResponseDTO, DuplicateFileEntriesResponseDTO, GetEntriesResponseDTO, GetFileResponseDTO, RemoveEntryResponseDTO, RenameEntryResponseDTO, SearchEntriesResponseDTO, UploadFileResponseDTO } from './dto';
|
||||||
|
|
||||||
export class Api {
|
export class Api {
|
||||||
getEntries = async (
|
getEntries = async (
|
||||||
@@ -15,7 +15,7 @@ export class Api {
|
|||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
return await axiosClient
|
return await axiosClient
|
||||||
.get<DTO.GetEntriesResponseDTO>('/file-manager' + ensureLeadingSlash(path), {
|
.get<GetEntriesResponseDTO>('/file-manager' + ensureLeadingSlash(path), {
|
||||||
params: payload
|
params: payload
|
||||||
})
|
})
|
||||||
.then((data) => data.data);
|
.then((data) => data.data);
|
||||||
@@ -23,7 +23,7 @@ export class Api {
|
|||||||
|
|
||||||
getFile = async (path: string) => {
|
getFile = async (path: string) => {
|
||||||
return await axiosClient
|
return await axiosClient
|
||||||
.get<DTO.GetFileResponseDTO>('/file-manager/files/details' + ensureLeadingSlash(path))
|
.get<GetFileResponseDTO>('/file-manager/files/details' + ensureLeadingSlash(path))
|
||||||
.then((data) => data.data);
|
.then((data) => data.data);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -37,19 +37,19 @@ export class Api {
|
|||||||
|
|
||||||
createDirectory = async (payload: { path: string; name: string }) => {
|
createDirectory = async (payload: { path: string; name: string }) => {
|
||||||
return await axiosClient
|
return await axiosClient
|
||||||
.post<DTO.CreateDirectoryResponseDTO>('/file-manager/folders', payload)
|
.post<CreateDirectoryResponseDTO>('/file-manager/folders', payload)
|
||||||
.then((data) => data.data);
|
.then((data) => data.data);
|
||||||
};
|
};
|
||||||
|
|
||||||
renameEntry = async (payload: { path: string; name: string }) => {
|
renameEntry = async (payload: { path: string; name: string }) => {
|
||||||
return await axiosClient
|
return await axiosClient
|
||||||
.put<DTO.RenameEntryResponseDTO>('/file-manager/entries/rename', payload)
|
.put<RenameEntryResponseDTO>('/file-manager/entries/rename', payload)
|
||||||
.then((data) => data.data);
|
.then((data) => data.data);
|
||||||
};
|
};
|
||||||
|
|
||||||
removeEntry = async (path: string) => {
|
removeEntry = async (path: string) => {
|
||||||
return await axiosClient
|
return await axiosClient
|
||||||
.delete<DTO.RemoveEntryResponseDTO>('/file-manager' + ensureLeadingSlash(path))
|
.delete<RemoveEntryResponseDTO>('/file-manager' + ensureLeadingSlash(path))
|
||||||
.then((data) => data.data);
|
.then((data) => data.data);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ export class Api {
|
|||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
return await axiosClient
|
return await axiosClient
|
||||||
.post<DTO.UploadFileResponseDTO>('/file-manager/files', formData, {
|
.post<UploadFileResponseDTO>('/file-manager/files', formData, {
|
||||||
signal: params.signal,
|
signal: params.signal,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
@@ -74,13 +74,13 @@ export class Api {
|
|||||||
|
|
||||||
duplicateFiles = async (paths: string[]) => {
|
duplicateFiles = async (paths: string[]) => {
|
||||||
return await axiosClient
|
return await axiosClient
|
||||||
.post<DTO.DuplicateFileEntriesResponseDTO>('/file-manager/files/duplicate', { paths })
|
.post<DuplicateFileEntriesResponseDTO>('/file-manager/files/duplicate', { paths })
|
||||||
.then((data) => data.data);
|
.then((data) => data.data);
|
||||||
};
|
};
|
||||||
|
|
||||||
searchEntries = async (query: string) => {
|
searchEntries = async (query: string) => {
|
||||||
return await axiosClient
|
return await axiosClient
|
||||||
.get<DTO.SearchEntriesResponseDTO>('/file-manager/search', {
|
.get<SearchEntriesResponseDTO>('/file-manager/search', {
|
||||||
params: { q: query }
|
params: { q: query }
|
||||||
})
|
})
|
||||||
.then((data) => data.data);
|
.then((data) => data.data);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { runInAction } from 'mobx';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import EntryMapper from '#core/FileManager/data/mappers/EntryMapper';
|
import EntryMapper from '#core/FileManager/data/mappers/EntryMapper';
|
||||||
import { fetchEntries, useGetEntries } from '#core/FileManager/data/queries/entries';
|
import { fetchEntries, useGetEntries } from '#core/FileManager/data/queries/entries';
|
||||||
import { entriesKeyBuilder, EntriesQueryPayload } from '#core/FileManager/data/queries/keys';
|
import { entriesKeyBuilder, type EntriesQueryPayload } from '#core/FileManager/data/queries/keys';
|
||||||
import EntriesStore from '#core/FileManager/data/stores/entriesStore';
|
import EntriesStore from '#core/FileManager/data/stores/entriesStore';
|
||||||
import ExplorerStore from '#core/FileManager/data/stores/explorerStore';
|
import ExplorerStore from '#core/FileManager/data/stores/explorerStore';
|
||||||
import { getSettingsData } from '#core/FileManager/data/stores/localStorage';
|
import { getSettingsData } from '#core/FileManager/data/stores/localStorage';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Entry, EntryType, FileEntry, FolderEntry } from '#core/FileManager/entities/entry';
|
import { EntryType, type Entry, type FileEntry, type FolderEntry } from '#core/FileManager/entities/entry';
|
||||||
import { ensureLeadingSlash } from '#core/FileManager/shared/path/leadingSlash';
|
import { ensureLeadingSlash } from '#core/FileManager/shared/path/leadingSlash';
|
||||||
import { EntryDTO } from '../api/dto';
|
import { type EntryDTO } from '../api/dto';
|
||||||
|
|
||||||
export default class EntryMapper {
|
export default class EntryMapper {
|
||||||
constructor(private readonly parent: string) {}
|
constructor(private readonly parent: string) {}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { InfiniteData, QueryKey } from '@tanstack/react-query';
|
import type { InfiniteData, QueryKey } from '@tanstack/react-query';
|
||||||
import { queryClient } from '#app/providers/QueryProvider';
|
import { queryClient } from '#app/providers/QueryProvider';
|
||||||
import { EntryDTO } from '#core/FileManager/data/api/dto';
|
import type { EntryDTO } from '#core/FileManager/data/api/dto';
|
||||||
import { AUDIO_EXTENSIONS, getFileExtension, IMAGE_EXTENSIONS, PDF_EXTENSIONS, PLAINTEXT_EXTENSIONS, VIDEO_EXTENSIONS } from '#core/FileManager/shared/file/extension';
|
import { AUDIO_EXTENSIONS, getFileExtension, IMAGE_EXTENSIONS, PDF_EXTENSIONS, PLAINTEXT_EXTENSIONS, VIDEO_EXTENSIONS } from '#core/FileManager/shared/file/extension';
|
||||||
import { ensureLeadingSlash } from '#core/FileManager/shared/path/leadingSlash';
|
import { ensureLeadingSlash } from '#core/FileManager/shared/path/leadingSlash';
|
||||||
import { fetchEntries } from './entries';
|
import { fetchEntries } from './entries';
|
||||||
import { entriesKeyBuilder, EntriesQueryPayload } from './keys';
|
import { entriesKeyBuilder, type EntriesQueryPayload } from './keys';
|
||||||
|
|
||||||
const ARCHIVE_EXTENSIONS = new Set(['zip', 'rar', '7z', 'tar', 'gz']);
|
const ARCHIVE_EXTENSIONS = new Set(['zip', 'rar', '7z', 'tar', 'gz']);
|
||||||
|
|
||||||
|
|||||||