Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aae77ebc27 | ||
|
|
3556662ddd | ||
|
|
ae34b30e4f | ||
|
|
93fc6ff5a0 | ||
|
|
e8558c076e | ||
|
|
c93d8c025b |
@@ -1,3 +1,4 @@
|
||||
node_modules
|
||||
.next
|
||||
npm-debug.log
|
||||
.git
|
||||
|
||||
@@ -26,7 +26,7 @@ yarn-error.log
|
||||
# Platform specific
|
||||
.DS_Store
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
/apps/backend/public/
|
||||
/apps/backend/storage/
|
||||
/apps/backend/.adonisjs/
|
||||
/apps/backend/database/schema.ts
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
npm run test:admin
|
||||
npm run test:backend
|
||||
npm run test:web
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# Dependencies and generated output
|
||||
**/node_modules/**
|
||||
**/build/**
|
||||
**/dist/**
|
||||
**/coverage/**
|
||||
**/.next/**
|
||||
|
||||
apps/backend/public/**
|
||||
apps/backend/storage/**
|
||||
|
||||
# Handlebars generator templates contain application syntax that is not valid HTML.
|
||||
apps/backend/app/templates/**
|
||||
apps/backend/.adonisjs/**
|
||||
apps/backend/database/schema.ts
|
||||
|
||||
apps/web/.next/**
|
||||
|
||||
@@ -9,17 +9,86 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
### Added
|
||||
|
||||
- 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
|
||||
writing content as plain HTML so it themes with the shared design-system tokens.
|
||||
- Added the `InputTiptapEditor` composed form field.
|
||||
|
||||
### Changed
|
||||
|
||||
### Fixed
|
||||
|
||||
### Removed
|
||||
|
||||
## [1.3.1] - 2026-08-31
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed Web Studio site settings rejecting empty optional fields. The backend bodyparser converts
|
||||
empty strings to `null`, so a cleared menu title, footer copyright or legal path failed
|
||||
validation; those fields are now optional in the vine schema (stored without the key) and the
|
||||
admin maps them back to empty strings via `SiteSettingsMapper`.
|
||||
- Backend validation errors on the settings page no longer disappear silently: the shared 422
|
||||
handler skips them expecting the caller to map field errors, which the page never did. Structured
|
||||
field errors are now set on the matching form inputs, the tab holding the first error is opened,
|
||||
and message-only 422s surface as a toast.
|
||||
- Fixed renaming or deleting a folder destroying sibling folders whose name shares its prefix
|
||||
(renaming `aa` overwrote and then deleted `aab`). Drive strips the trailing slash off a listing
|
||||
prefix, so a directory's contents are now filtered on the real `<path>/` prefix, and the S3
|
||||
service deletes the listed keys instead of passing that prefix to `deleteAll`.
|
||||
- Fixed the web container running against the host's `node_modules`: the `web` service keeps
|
||||
`apps/web/node_modules` in an anonymous volume, so the bind mount no longer shadows the
|
||||
dependencies installed in the image.
|
||||
|
||||
## [1.3.0] - 2026-08-28
|
||||
|
||||
### 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,
|
||||
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.
|
||||
- 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
|
||||
|
||||
- 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 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 the root `.env` file.
|
||||
|
||||
## [1.2.0] - 2026-07-17
|
||||
|
||||
### 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
|
||||
|
||||
#### Backend
|
||||
@@ -6,94 +21,157 @@
|
||||
- TypeScript
|
||||
- Lucid ORM
|
||||
- PostgreSQL / MySQL (configurable)
|
||||
- Redis, S3 / MinIO storage
|
||||
|
||||
#### Frontend
|
||||
#### Admin panel
|
||||
|
||||
- React
|
||||
- TypeScript
|
||||
- Vite
|
||||
- React Query / Mobx / Axios
|
||||
- Component-based UI system
|
||||
- React Query / MobX / Axios
|
||||
- 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
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
Node.js >= v24.18.0
|
||||
npm >= 11.15.0
|
||||
Database PostgreSQL
|
||||
- Node.js `24.18.0` (see `.nvmrc`; the package manifest requires `>=24.0.0`)
|
||||
- npm `>=11.6.2` (the repo pins `npm@11.15.0`)
|
||||
- PostgreSQL
|
||||
|
||||
#### Installation
|
||||
|
||||
```
|
||||
```bash
|
||||
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
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
TZ=UTC
|
||||
PORT=3333
|
||||
HOST=localhost
|
||||
LOG_LEVEL=info
|
||||
APP_KEY=
|
||||
NODE_ENV=development
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=5432
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=postgres
|
||||
DB_DATABASE=adonis
|
||||
VITE_API_URL=http://0.0.0.0:3333
|
||||
VITE_TINYMCE_API_KEY=
|
||||
| Env file | Covers |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `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 |
|
||||
| `apps/admin/.env` | `VITE_API_URL` (backend), `VITE_WEB_URL` (public site, used for page previews), `VITE_TINYMCE_API_KEY` |
|
||||
| `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) |
|
||||
|
||||
`docker-compose.yml` loads each service's env from the same per-app files. Inside Docker, service
|
||||
URLs must use compose service names (`http://backend:3333`), not `localhost`.
|
||||
|
||||
### Running
|
||||
|
||||
#### Locally
|
||||
|
||||
| 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
|
||||
- Fields and types
|
||||
- Validation rules
|
||||
- List view configuration
|
||||
|
||||
#### Generated Output
|
||||
**Generated output** — Backend: controllers, validators, routes. Frontend: pages, forms, tables, API.
|
||||
|
||||
**Backend**: controllers, validators, routes
|
||||
**Frontend**: pages, forms, tables, API
|
||||
#### Authentication & Authorization
|
||||
|
||||
### Authentication & Authorization
|
||||
|
||||
- JWT / Session-based authentication
|
||||
- JWT / session-based authentication
|
||||
- Role & permission system
|
||||
- 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 |
|
||||
| ----------------------- | ---------------------------------------------- |
|
||||
| `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 |
|
||||
### Documentation
|
||||
|
||||
#### Build
|
||||
|
||||
| Command | Description |
|
||||
| ----------------------- | ----------------------------- |
|
||||
| `npm run build:backend` | Build the backend application |
|
||||
| `npm run build:admin` | Build the admin application |
|
||||
|
||||
#### Versions
|
||||
|
||||
| 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.
|
||||
| Topic | Where |
|
||||
| ------------------------ | ---------------------------------------------------- |
|
||||
| Documentation home | [docs/Readme.md](./docs/Readme.md) |
|
||||
| Public website | [apps/web/Readme.md](./apps/web/Readme.md) |
|
||||
| Architecture | [docs/architecture/](./docs/architecture/Readme.md) |
|
||||
| Core modules | [docs/core/](./docs/Readme.md#core-modules) |
|
||||
| Shared packages | [docs/packages/Readme.md](./docs/packages/Readme.md) |
|
||||
| Install and setup issues | [troubleshooting.md](./troubleshooting.md) |
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
VITE_API_URL=http://0.0.0.0:3333
|
||||
VITE_API_URL=http://localhost:3333
|
||||
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
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@crux-kit/admin-app",
|
||||
"private": true,
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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_ACCOUNT": "حسابي",
|
||||
"USER.MENU.LANGUAGE": "لغة",
|
||||
"USER.MENU.DARK_MODE": "الوضع المظلم",
|
||||
"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_FILTERS": "Clear filters",
|
||||
"ACTION.EDIT": "Edit",
|
||||
|
||||
"STATE.CREATING": "Creating",
|
||||
"STATE.GENERATING": "Generating",
|
||||
"STATE.SAVING": "Saving",
|
||||
"STATE.REMOVING": "Removing",
|
||||
"STATE.LOADING": "Loading",
|
||||
"STATE.UPDATING": "Updating",
|
||||
|
||||
"ERROR": "Error",
|
||||
|
||||
"PERMISSION": "Permission",
|
||||
"PERMISSION.PERMISSIONS": "Permissions",
|
||||
"PERMISSION.PERMISSION_NAME": "Permission name",
|
||||
"PERMISSION.SLUG": "Slug",
|
||||
"PERMISSION.EDIT_PERMISSION": "Edit permission",
|
||||
|
||||
"ROLE": "Role",
|
||||
"ROLE.ROLES": "Roles",
|
||||
"ROLE.ROLE_NAME": "Role name",
|
||||
"ROLE.SLUG": "Slug",
|
||||
"ROLE.EDIT_ROLE": "Edit role",
|
||||
|
||||
"USER": "User",
|
||||
"USER.EDIT_USER": "Edit user",
|
||||
"USER.STATUS.ACTIVE": "Active",
|
||||
@@ -51,7 +46,6 @@
|
||||
"USER.MENU.LANGUAGE": "Language",
|
||||
"USER.MENU.DARK_MODE": "Dark Mode",
|
||||
"USER.MENU.LOGOUT": "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} more",
|
||||
|
||||
"ADMIN.ADMIN_MANAGEMENT": "Admin Management",
|
||||
"ADMIN.ADMINS": "Admins",
|
||||
|
||||
"DASHBOARD": "Dashboard",
|
||||
|
||||
"FORM.ERROR.REQUIRED": "Required",
|
||||
"FORM.ERROR.INVALID_EMAIL_ADDRESS": "Invalid email address",
|
||||
"FORM.ERROR.PASSWORDS_MUST_MATCH": "Passwords must match",
|
||||
"FORM.ERROR.MIN_PASSWORD_LENGTH_DESC": "Min. password length - 8 chars.",
|
||||
"FORM.ERROR.SOMETHING_WENT_WRONG": "Something went wrong",
|
||||
|
||||
"TABLE.COMMON.COLUMNS": "Columns",
|
||||
"TABLE.COMMON.NO_DATA_AVAILABLE": "No data available",
|
||||
"TABLE.COMMON.ROWS_PER_PAGE": "Rows per page",
|
||||
"TABLE.COMMON.GO_TO_PREVIOUS_PAGE": "Go to previous page",
|
||||
"TABLE.COMMON.GO_TO_NEXT_PAGE": "Go to next page",
|
||||
|
||||
"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_ALT": "Avatar",
|
||||
"PROFILE.AVATAR_SETTINGS": "Avatar Settings",
|
||||
"PROFILE.AVATAR_CROP_ME": "Crop me",
|
||||
"PROFILE.EDIT_PROFILE_IMAGE": "Edit profile image",
|
||||
|
||||
"MODULE.COMMON.REMOVE_ITEM_QUESTION": "Are you sure you want to remove {target}?",
|
||||
"MODULE.COMMON.ARE_YOU_SURE?": "Are you sure?",
|
||||
"MODULE.COMMON.REMOVE_ITEM_QUESTION_DESC": "This action cannot be undone.",
|
||||
"MODULE.COMMON.CREATE_NEW_ENTITY": "Create new {entity}",
|
||||
"MODULE.COMMON.CREATE_NEW_USER": "Create new user",
|
||||
|
||||
"MESSAGE.PLEASE_WAIT": "Please wait",
|
||||
"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",
|
||||
|
||||
"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.OPTION.FROM_CLOUD_STORAGE": "From cloud storage",
|
||||
|
||||
@@ -20,28 +20,23 @@
|
||||
"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": "Активний",
|
||||
@@ -51,7 +46,6 @@
|
||||
"USER.MENU.LANGUAGE": "Мова",
|
||||
"USER.MENU.DARK_MODE": "Темний режим",
|
||||
"USER.MENU.LOGOUT": "Вийти",
|
||||
|
||||
"SUCCESS": "Успіх",
|
||||
"ACTIONS": "Дії",
|
||||
"FILTER": "Фільтр",
|
||||
@@ -73,42 +67,33 @@
|
||||
"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 x 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": "З хмарного сховища",
|
||||
|
||||
@@ -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';
|
||||
|
||||
import { JSX, useCallback } from 'react';
|
||||
import { JSX, useMemo } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { cn } from '@crux-kit/utils';
|
||||
import { MenuConfig, MenuItem } from '#app/config/types';
|
||||
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 { Badge } from '#shared/ui/Badge';
|
||||
|
||||
@@ -13,9 +14,9 @@ export function SidebarMenu() {
|
||||
const menuConfig = useMenuConfig();
|
||||
|
||||
// Memoize matchPath to prevent unnecessary re-renders
|
||||
const matchPath = useCallback(
|
||||
(path: string): boolean => path === pathname || (path.length > 1 && pathname.startsWith(path)),
|
||||
[pathname]
|
||||
const matchPath = useMemo(
|
||||
() => createMenuPathMatcher(menuConfig, pathname),
|
||||
[menuConfig, pathname]
|
||||
);
|
||||
|
||||
// Global classNames for consistent styling
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { cn } from '@crux-kit/utils';
|
||||
import { MenuConfig } from '#app/config/types';
|
||||
import { useMenuConfig } from '#app/menu';
|
||||
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';
|
||||
|
||||
export function NavbarMenu() {
|
||||
@@ -24,7 +26,12 @@ export function NavbarMenu() {
|
||||
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) => {
|
||||
return items.map((item, index) => {
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { JSX, useCallback } from 'react';
|
||||
import { JSX, useMemo } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { cn } from '@crux-kit/utils';
|
||||
import { MenuConfig, MenuItem } from '#app/config/types';
|
||||
import { useMenuConfig } from '#app/menu';
|
||||
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 { Badge } from '#shared/ui/Badge';
|
||||
|
||||
export function SidebarMenuDefault() {
|
||||
const { pathname } = useLocation();
|
||||
const fullMenuConfig = useMenuConfig();
|
||||
|
||||
// Memoize matchPath to prevent unnecessary re-renders
|
||||
const matchPath = useCallback(
|
||||
(path: string): boolean => path === pathname || (path.length > 1 && pathname.startsWith(path)),
|
||||
[pathname]
|
||||
const matchPath = useMemo(
|
||||
() => createMenuPathMatcher(fullMenuConfig, pathname),
|
||||
[fullMenuConfig, pathname]
|
||||
);
|
||||
|
||||
// Global classNames for consistent styling
|
||||
@@ -165,8 +167,7 @@ export function SidebarMenuDefault() {
|
||||
|
||||
// Memoize the menu to prevent unnecessary re-renders
|
||||
const { getChildren } = useMenu(pathname);
|
||||
const menuConfig = useMenuConfig();
|
||||
const menuItems = getChildren(menuConfig, 0);
|
||||
const menuItems = getChildren(fullMenuConfig, 0);
|
||||
|
||||
return (
|
||||
<AccordionMenu
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { JSX, useCallback } from 'react';
|
||||
import { JSX, useMemo } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { cn } from '@crux-kit/utils';
|
||||
import { MenuConfig, MenuItem } from '#app/config/types';
|
||||
import { useMenuConfig } from '#app/menu';
|
||||
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 { Badge } from '#shared/ui/Badge';
|
||||
|
||||
@@ -12,9 +13,9 @@ export function SidebarMenuDefault() {
|
||||
const menuConfig = useMenuConfig();
|
||||
|
||||
// Memoize matchPath to prevent unnecessary re-renders
|
||||
const matchPath = useCallback(
|
||||
(path: string): boolean => path === pathname || (path.length > 1 && pathname.startsWith(path)),
|
||||
[pathname]
|
||||
const matchPath = useMemo(
|
||||
() => createMenuPathMatcher(menuConfig, pathname),
|
||||
[menuConfig, pathname]
|
||||
);
|
||||
|
||||
// Global classNames for consistent styling
|
||||
|
||||
@@ -15,7 +15,6 @@ const Header = () => {
|
||||
const [isSheetOpen, setIsSheetOpen] = useState(false);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
// Close sheet when route changes
|
||||
useEffect(() => {
|
||||
setIsSheetOpen(false);
|
||||
}, [pathname]);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { IntlName } from '#infrastructure/IntlName';
|
||||
import useIntlName from '#app/i18n/useIntlName';
|
||||
import { useAuth } from '#core/Auth/providers/AuthProvider';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '#shared/ui/Avatar';
|
||||
import { UserDropdownMenu } from '#shared/widgets/UserDropdownMenu';
|
||||
|
||||
export function SidebarFooter() {
|
||||
const { user } = useAuth();
|
||||
const formatName = useIntlName();
|
||||
|
||||
return (
|
||||
<div className="flex-center flex h-14 shrink-0 justify-between ps-4 pe-3.5">
|
||||
@@ -13,9 +14,7 @@ export function SidebarFooter() {
|
||||
trigger={
|
||||
<Avatar className="size-9 cursor-pointer">
|
||||
<AvatarImage src={undefined} alt="User avatar" />
|
||||
<AvatarFallback className="text-sm">
|
||||
{IntlName.formatToDisplay(user, 'INITIALS_LF')}
|
||||
</AvatarFallback>
|
||||
<AvatarFallback className="text-sm">{formatName(user, 'INITIALS_LF')}</AvatarFallback>
|
||||
</Avatar>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { JSX, useCallback } from 'react';
|
||||
import { Fragment, JSX, useMemo } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { cn } from '@crux-kit/utils';
|
||||
import { MenuConfig, MenuItem } from '#app/config/types';
|
||||
import { useMenuConfig } from '#app/menu';
|
||||
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 { getPermittedMenuItems } from '../lib';
|
||||
|
||||
export function SidebarMenuPrimary() {
|
||||
const { pathname } = useLocation();
|
||||
const { user } = useAuth();
|
||||
const menuConfig = useMenuConfig();
|
||||
|
||||
const userPermissions = user?.permissions ?? [];
|
||||
// Memoize matchPath to prevent unnecessary re-renders
|
||||
const matchPath = useCallback(
|
||||
(path: string): boolean => path === pathname || (path.length > 1 && pathname.startsWith(path)),
|
||||
[pathname]
|
||||
|
||||
const matchPath = useMemo(
|
||||
() => createMenuPathMatcher(menuConfig, pathname),
|
||||
[menuConfig, pathname]
|
||||
);
|
||||
|
||||
// Global classNames for consistent styling
|
||||
const classNames: AccordionMenuClassNames = {
|
||||
root: 'space-y-2.5 px-3.5',
|
||||
group: 'gap-px',
|
||||
@@ -33,8 +35,6 @@ export function SidebarMenuPrimary() {
|
||||
indicator: ''
|
||||
};
|
||||
|
||||
const menuConfig = useMenuConfig();
|
||||
|
||||
const buildMenu = (items: MenuConfig): JSX.Element[] => {
|
||||
const menuItems = getPermittedMenuItems(items, userPermissions);
|
||||
|
||||
@@ -82,7 +82,7 @@ export function SidebarMenuPrimary() {
|
||||
if (!item.heading && !item.disabled) {
|
||||
return buildMenuItemChild(item, index, level);
|
||||
} else {
|
||||
return <></>;
|
||||
return <Fragment key={index}></Fragment>;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// DEMO 6
|
||||
// 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 { useBodyClass, useIsMobile } from '@crux-kit/hooks';
|
||||
import { LayoutNames } from '#app/config/types';
|
||||
@@ -23,14 +23,15 @@ export const MultipleMenusLayout = () => {
|
||||
bg-muted!
|
||||
`);
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
setOption('layout', LayoutNames.MULTIPLE_MENUS);
|
||||
}, [setOption]);
|
||||
|
||||
const toolbar = React.useMemo(() => {
|
||||
const toolbar = useMemo(() => {
|
||||
if (pathname.includes('/devtools')) return null;
|
||||
if (pathname.includes('/access-control')) return null;
|
||||
if (pathname.includes('/file-manager')) return null;
|
||||
if (pathname.includes('/web-studio')) return null;
|
||||
|
||||
return (
|
||||
<Toolbar>
|
||||
@@ -39,13 +40,13 @@ export const MultipleMenusLayout = () => {
|
||||
);
|
||||
}, [pathname]);
|
||||
|
||||
const footer = React.useMemo(() => {
|
||||
const footer = useMemo(() => {
|
||||
if (pathname.includes('/file-manager')) return null;
|
||||
return <Footer />;
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Fragment>
|
||||
<div className="flex grow">
|
||||
{!isMobile && <Sidebar />}
|
||||
|
||||
@@ -63,6 +64,6 @@ export const MultipleMenusLayout = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,12 +4,13 @@ import { cn } from '@crux-kit/utils';
|
||||
import { MenuConfig } from '#app/config/types';
|
||||
import { useMenuConfig } from '#app/menu';
|
||||
import { useMenu } from '#shared/hooks/useMenu';
|
||||
import { createMenuPathMatcher } from '#shared/lib/menuPath';
|
||||
import { Button } from '#shared/ui/Button';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from '#shared/ui/DropdownMenu';
|
||||
|
||||
export function SidebarMenu() {
|
||||
const { pathname } = useLocation();
|
||||
const { isActive, hasActiveChild } = useMenu(pathname);
|
||||
const { hasActiveChild } = useMenu(pathname);
|
||||
const config = useMenuConfig();
|
||||
|
||||
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) => {
|
||||
return items.map((item, index) => (
|
||||
<div key={index} className="flex flex-col items-center">
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { JSX, useCallback } from 'react';
|
||||
import { JSX, useMemo } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { cn } from '@crux-kit/utils';
|
||||
import { MenuConfig, MenuItem } from '#app/config/types';
|
||||
import { useMenuConfig } from '#app/menu';
|
||||
import { createMenuPathMatcher } from '#shared/lib/menuPath';
|
||||
import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuLabel, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu';
|
||||
|
||||
export function SidebarMenuPrimary() {
|
||||
@@ -12,9 +13,9 @@ export function SidebarMenuPrimary() {
|
||||
const menuConfig = useMenuConfig();
|
||||
|
||||
// Memoize matchPath to prevent unnecessary re-renders
|
||||
const matchPath = useCallback(
|
||||
(path: string): boolean => path === pathname || (path.length > 1 && pathname.startsWith(path)),
|
||||
[pathname]
|
||||
const matchPath = useMemo(
|
||||
() => createMenuPathMatcher(menuConfig, pathname),
|
||||
[menuConfig, pathname]
|
||||
);
|
||||
|
||||
// Global classNames for consistent styling
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { type MenuConfig } from './config/types';
|
||||
import { createModuleMenuConfig } from './services/modules';
|
||||
@@ -6,7 +6,7 @@ import { createModuleMenuConfig } from './services/modules';
|
||||
export const useMenuConfig = (): MenuConfig => {
|
||||
const intl = useIntl();
|
||||
|
||||
return React.useMemo(() => {
|
||||
return useMemo(() => {
|
||||
return createModuleMenuConfig(intl);
|
||||
}, [intl]);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type ReactNode } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { BASE_URL } from '#app/config/app';
|
||||
import { AuthProvider } from '#core/Auth/providers/AuthProvider';
|
||||
|
||||
@@ -1,51 +1,48 @@
|
||||
import { DirectionProvider as RadixDirectionProvider } from '@radix-ui/react-direction';
|
||||
import { createContext, useContext, useEffect, useState, type PropsWithChildren } from 'react';
|
||||
import { DirectionProvider } from '@radix-ui/react-direction';
|
||||
import { useEffect, useState, type PropsWithChildren } from 'react';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { LocalStorageService } from '@crux-kit/local-storage';
|
||||
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 { IntlName } from '#infrastructure/IntlName';
|
||||
import '@formatjs/intl-relativetimeformat/polyfill';
|
||||
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 resolveLanguage = (code?: LanguageCode): Language => {
|
||||
return I18N_LANGUAGES.find((lang) => lang.code === code) ?? I18N_DEFAULT_LANGUAGE;
|
||||
};
|
||||
|
||||
const getInitialLanguage = () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const langParam = urlParams.get('lang');
|
||||
|
||||
// Check if langParam matches a supported language in I18N_LANGUAGES
|
||||
if (langParam) {
|
||||
const matchedLanguage = I18N_LANGUAGES.find((lang) => lang.code === langParam);
|
||||
if (matchedLanguage) {
|
||||
storage.set('config', matchedLanguage);
|
||||
storage.set('config', matchedLanguage.code);
|
||||
return matchedLanguage;
|
||||
}
|
||||
}
|
||||
|
||||
return storage.get<Language>('config') ?? I18N_DEFAULT_LANGUAGE;
|
||||
return resolveLanguage(storage.get<LanguageCode>('config'));
|
||||
};
|
||||
|
||||
const initialProps: I18nProviderProps = {
|
||||
languages: I18N_LANGUAGES,
|
||||
currenLanguage: getInitialLanguage(),
|
||||
changeLanguage: (_: Language) => {},
|
||||
isRTL: () => false
|
||||
};
|
||||
const initialLanguage = getInitialLanguage();
|
||||
|
||||
const TranslationsContext = createContext<I18nProviderProps>(initialProps);
|
||||
const useLanguage = () => useContext(TranslationsContext);
|
||||
IntlDate.setLocaleGlobally(initialLanguage.code);
|
||||
IntlName.setLanguage(initialLanguage.code);
|
||||
|
||||
const I18nProvider = ({ children }: PropsWithChildren) => {
|
||||
const [currenLanguage, setCurrenLanguage] = useState(initialProps.currenLanguage);
|
||||
const [currenLanguage, setCurrenLanguage] = useState(initialLanguage);
|
||||
|
||||
const changeLanguage = (language: Language) => {
|
||||
storage.set('config', language);
|
||||
storage.set('config', language.code);
|
||||
IntlDate.setLocaleGlobally(language.code);
|
||||
IntlName.setLanguage(language.code);
|
||||
setCurrenLanguage(language);
|
||||
};
|
||||
|
||||
@@ -54,7 +51,6 @@ const I18nProvider = ({ children }: PropsWithChildren) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
IntlDate.setLocaleGlobally(currenLanguage.code); // TODO: currently changes only after page reload!
|
||||
document.documentElement.setAttribute('dir', currenLanguage.direction);
|
||||
}, [currenLanguage]);
|
||||
|
||||
@@ -70,13 +66,12 @@ const I18nProvider = ({ children }: PropsWithChildren) => {
|
||||
<IntlProvider
|
||||
messages={currenLanguage.messages}
|
||||
locale={currenLanguage.code}
|
||||
defaultLocale={getInitialLanguage().code}
|
||||
defaultLocale={I18N_DEFAULT_LANGUAGE.code}
|
||||
>
|
||||
<RadixDirectionProvider dir={currenLanguage.direction}>{children}</RadixDirectionProvider>
|
||||
<DirectionProvider dir={currenLanguage.direction}>{children}</DirectionProvider>
|
||||
</IntlProvider>
|
||||
</TranslationsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { useLanguage };
|
||||
export default I18nProvider;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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';
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
/* 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 { APP_SETTINGS } from '#app/config/settings';
|
||||
import { Settings } from '#app/config/types';
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
||||
import * as React from 'react';
|
||||
import { type ComponentProps } from 'react';
|
||||
|
||||
export default function ThemeProvider({
|
||||
children
|
||||
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||
export default function ThemeProvider({ children }: ComponentProps<typeof NextThemesProvider>) {
|
||||
return (
|
||||
<NextThemesProvider
|
||||
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 { getModuleRoutes } from '#app/services/modules';
|
||||
import { AuthGuard } from '#core/Auth';
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
export * from './menus';
|
||||
export * from './messages';
|
||||
export * from './registry';
|
||||
export * from './routes';
|
||||
export * from './types';
|
||||
export { createModuleMenuConfig } from './menus';
|
||||
export { collectModuleMessages } from './messages';
|
||||
export { appModules } from './registry';
|
||||
export { getModuleRoutes } from './routes';
|
||||
|
||||
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 { appModules } from './registry';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LanguageCode } from '#app/i18n/types';
|
||||
import { type LanguageCode } from '#app/i18n/types';
|
||||
import { appModules } from './registry';
|
||||
|
||||
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', {
|
||||
eager: true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { appModules } from './registry';
|
||||
import { AppModuleRouteScope } from './types';
|
||||
import { type AppModuleRouteScope } from './types';
|
||||
|
||||
export const getModuleRoutes = (scope: AppModuleRouteScope) => {
|
||||
return appModules
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Users } from 'lucide-react';
|
||||
import { IntlShape } from 'react-intl';
|
||||
import { type IntlShape } from 'react-intl';
|
||||
import { MenuItem } from '#app/config/types';
|
||||
|
||||
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 { accessControlMessages } from './messages';
|
||||
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 RoleListPage from '../pages/RoleListPage';
|
||||
import AdminManagementLayout from '../view/layouts/AdminManagementLayout';
|
||||
|
||||
@@ -1,35 +1,31 @@
|
||||
import { axiosClient } from '#infrastructure/axios';
|
||||
import { ListQueryParams, SuccessListResponse, SuccessMutationResponse } from '#infrastructure/reactQuery';
|
||||
import { AdminDTO } from './dto';
|
||||
|
||||
const ROOT_API_ENDPOINT = '/admin';
|
||||
import type { ListQueryParams, SuccessListResponse, SuccessMutationResponse } from '#infrastructure/reactQuery';
|
||||
import type { AdminDTO } from './AccessControlDTO';
|
||||
|
||||
export default class AdminAPI {
|
||||
getList = async (params: ListQueryParams) => {
|
||||
return axiosClient
|
||||
.get<SuccessListResponse<AdminDTO>>(ROOT_API_ENDPOINT, { params })
|
||||
.get<SuccessListResponse<AdminDTO>>('/admin', { params })
|
||||
.then((data) => data.data);
|
||||
};
|
||||
|
||||
getDetailed = async (id: string) => {
|
||||
return axiosClient.get<AdminDTO>(`${ROOT_API_ENDPOINT}/${id}`).then((data) => data.data);
|
||||
return axiosClient.get<AdminDTO>(`/admin/${id}`).then((data) => data.data);
|
||||
};
|
||||
|
||||
create = async (payload: Record<string, unknown>) => {
|
||||
return axiosClient
|
||||
.post<SuccessMutationResponse>(ROOT_API_ENDPOINT, payload)
|
||||
.then((data) => data.data);
|
||||
return axiosClient.post<SuccessMutationResponse>('/admin', payload).then((data) => data.data);
|
||||
};
|
||||
|
||||
update = async (payload: { id: number } & Record<string, unknown>) => {
|
||||
return axiosClient
|
||||
.put<SuccessMutationResponse>(`${ROOT_API_ENDPOINT}/${payload.id}`, payload)
|
||||
.put<SuccessMutationResponse>(`/admin/${payload.id}`, payload)
|
||||
.then((data) => data.data);
|
||||
};
|
||||
|
||||
delete = async (payload: { id: number }) => {
|
||||
return axiosClient
|
||||
.delete<SuccessMutationResponse>(`${ROOT_API_ENDPOINT}/${payload.id}`)
|
||||
.delete<SuccessMutationResponse>(`/admin/${payload.id}`)
|
||||
.then((data) => data.data);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { axiosClient } from '#infrastructure/axios';
|
||||
import { PermissionDTO } from './dto';
|
||||
|
||||
const ROOT_API_ENDPOINT = '/permission';
|
||||
import type { PermissionDTO } from './AccessControlDTO';
|
||||
|
||||
export default class PermissionAPI {
|
||||
getList = async () => {
|
||||
return axiosClient.get<Array<PermissionDTO>>(ROOT_API_ENDPOINT).then((data) => data.data);
|
||||
return axiosClient.get<Array<PermissionDTO>>('/permission').then((data) => data.data);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,35 +1,31 @@
|
||||
import { axiosClient } from '#infrastructure/axios';
|
||||
import { ListQueryParams, SuccessListResponse, SuccessMutationResponse } from '#infrastructure/reactQuery';
|
||||
import { RoleDTO } from './dto';
|
||||
|
||||
const ROOT_API_ENDPOINT = '/role';
|
||||
import type { ListQueryParams, SuccessListResponse, SuccessMutationResponse } from '#infrastructure/reactQuery';
|
||||
import type { RoleDTO } from './AccessControlDTO';
|
||||
|
||||
export default class RoleAPI {
|
||||
getList = async (params: ListQueryParams) => {
|
||||
return axiosClient
|
||||
.get<SuccessListResponse<RoleDTO>>(ROOT_API_ENDPOINT, { params })
|
||||
.get<SuccessListResponse<RoleDTO>>('/role', { params })
|
||||
.then((data) => data.data);
|
||||
};
|
||||
|
||||
getDetailed = async (id: string) => {
|
||||
return axiosClient.get<RoleDTO>(`${ROOT_API_ENDPOINT}/${id}`).then((data) => data.data);
|
||||
return axiosClient.get<RoleDTO>(`/role/${id}`).then((data) => data.data);
|
||||
};
|
||||
|
||||
create = async (payload: { name: string; permissions: string[] }) => {
|
||||
return axiosClient
|
||||
.post<SuccessMutationResponse>(ROOT_API_ENDPOINT, payload)
|
||||
.then((data) => data.data);
|
||||
return axiosClient.post<SuccessMutationResponse>('/role', payload).then((data) => data.data);
|
||||
};
|
||||
|
||||
update = async (payload: { id: number } & Record<string, unknown>) => {
|
||||
return axiosClient
|
||||
.put<SuccessMutationResponse>(`${ROOT_API_ENDPOINT}/${payload.id}`, payload)
|
||||
.put<SuccessMutationResponse>(`/role/${payload.id}`, payload)
|
||||
.then((data) => data.data);
|
||||
};
|
||||
|
||||
delete = async (payload: { id: number }) => {
|
||||
return axiosClient
|
||||
.delete<SuccessMutationResponse>(`${ROOT_API_ENDPOINT}/${payload.id}`)
|
||||
.delete<SuccessMutationResponse>(`/role/${payload.id}`)
|
||||
.then((data) => data.data);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AdminEntity, AdminStatusEnum } from '../../entities/AdminEntity';
|
||||
import { AdminDTO } from '../api/dto';
|
||||
import RoleMapper from './RoleMapper';
|
||||
import type { AdminDTO } from '#core/AccessControl/data/api/AccessControlDTO';
|
||||
import RoleMapper from '#core/AccessControl/data/mappers/RoleMapper';
|
||||
import type { AdminEntity, AdminStatusEnum } from '#core/AccessControl/entities/AdminEntity';
|
||||
|
||||
export default class AdminMapper {
|
||||
private readonly roleMapper = new RoleMapper();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PermissionEntity } from '../../entities/PermissionEntity';
|
||||
import { PermissionDTO } from '../api/dto';
|
||||
import type { PermissionDTO } from '#core/AccessControl/data/api/AccessControlDTO';
|
||||
import type { PermissionEntity } from '#core/AccessControl/entities/PermissionEntity';
|
||||
|
||||
export default class PermissionMapper {
|
||||
public toEntity = (dto: PermissionDTO): PermissionEntity => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { RoleEntity } from '../../entities/RoleEntity';
|
||||
import { RoleDTO } from '../api/dto';
|
||||
import PermissionMapper from './PermissionMapper';
|
||||
import type { RoleDTO } from '#core/AccessControl/data/api/AccessControlDTO';
|
||||
import PermissionMapper from '#core/AccessControl/data/mappers/PermissionMapper';
|
||||
import type { RoleEntity } from '#core/AccessControl/entities/RoleEntity';
|
||||
|
||||
export default class RoleMapper {
|
||||
private readonly permissionMapper = new PermissionMapper();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { keepPreviousData, useMutation, useQuery } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { ErrorResponse, getPaginationInfo, ListQueryParams, SuccessMutationResponse } from '#infrastructure/reactQuery';
|
||||
import AdminAPI from '../api/AdminAPI';
|
||||
import AdminMapper from '../mappers/AdminMapper';
|
||||
import { adminDetailedKeyBuilder, adminListKeyBuilder, DetailedPayload } from './keys';
|
||||
import { getPaginationInfo, type ErrorResponse, type ListQueryParams, type SuccessMutationResponse } from '#infrastructure/reactQuery';
|
||||
import AdminAPI from '#core/AccessControl/data/api/AdminAPI';
|
||||
import AdminMapper from '#core/AccessControl/data/mappers/AdminMapper';
|
||||
import { adminDetailedKeyBuilder, adminListKeyBuilder, type DetailedPayload } from './keys';
|
||||
|
||||
const api = new AdminAPI();
|
||||
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 ?? []);
|
||||
}, [data?.data]);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createKeyBuilder, ListQueryParams } from '#infrastructure/reactQuery';
|
||||
import { createKeyBuilder, type ListQueryParams } from '#infrastructure/reactQuery';
|
||||
|
||||
export interface DetailedPayload {
|
||||
id: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
||||
import PermissionAPI from '../api/PermissionAPI';
|
||||
import PermissionMapper from '../mappers/PermissionMapper';
|
||||
import PermissionAPI from '#core/AccessControl/data/api/PermissionAPI';
|
||||
import PermissionMapper from '#core/AccessControl/data/mappers/PermissionMapper';
|
||||
import { permissionListKeyBuilder } from './keys';
|
||||
|
||||
const api = new PermissionAPI();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { keepPreviousData, useMutation, useQuery } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { ErrorResponse, getPaginationInfo, ListQueryParams, SuccessMutationResponse } from '#infrastructure/reactQuery';
|
||||
import RoleAPI from '../api/RoleAPI';
|
||||
import RoleMapper from '../mappers/RoleMapper';
|
||||
import { DetailedPayload, roleDetailedKeyBuilder, roleListKeyBuilder } from './keys';
|
||||
import { getPaginationInfo, type ErrorResponse, type ListQueryParams, type SuccessMutationResponse } from '#infrastructure/reactQuery';
|
||||
import RoleAPI from '#core/AccessControl/data/api/RoleAPI';
|
||||
import RoleMapper from '#core/AccessControl/data/mappers/RoleMapper';
|
||||
import { roleDetailedKeyBuilder, roleListKeyBuilder, type DetailedPayload } from './keys';
|
||||
|
||||
const api = new RoleAPI();
|
||||
const mapper = new RoleMapper();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { IsoDateString } from '@crux-kit/types';
|
||||
import { RoleEntity } from './RoleEntity';
|
||||
import type { RoleEntity } from './RoleEntity';
|
||||
|
||||
export enum AdminStatusEnum {
|
||||
ACTIVE = 'active',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { IsoDateString } from '@crux-kit/types';
|
||||
import { PermissionEntity } from './PermissionEntity';
|
||||
import type { PermissionEntity } from './PermissionEntity';
|
||||
|
||||
export interface RoleEntity {
|
||||
id: number;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { AdminTable } from '#core/AccessControl/view/widgets/AdminTable';
|
||||
import { PermissionPageGate } from '#core/Permission';
|
||||
import { Container } from '#shared/widgets/common/Container';
|
||||
import { AdminTable } from '../../view/widgets/AdminTable';
|
||||
|
||||
const AdminListPage: React.FC = () => {
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { RoleTable } from '#core/AccessControl/view/widgets/RoleTable';
|
||||
import { PermissionPageGate } from '#core/Permission';
|
||||
import { Container } from '#shared/widgets/common/Container';
|
||||
import { RoleTable } from '../../view/widgets/RoleTable';
|
||||
|
||||
const RoleListPage: React.FC = () => {
|
||||
return (
|
||||
|
||||
@@ -9,7 +9,7 @@ import { FormElement, FormGrid, GridElement, InputCheckboxGroup, InputSelect, In
|
||||
import { Button } from '#shared/ui/Button';
|
||||
import { Form as FormProvider } from '#shared/ui/form';
|
||||
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<{
|
||||
roles: Array<{ label: string; value: string }>;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
import { IntlShape } from 'react-intl';
|
||||
import { type UseFormReturn } from 'react-hook-form';
|
||||
import { type IntlShape } from 'react-intl';
|
||||
import z from 'zod';
|
||||
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();
|
||||
|
||||
|
||||
@@ -2,11 +2,12 @@ import { extractValidationErrors } from '#infrastructure/reactQuery';
|
||||
import { useCreate, useUpdate } from '#core/AccessControl/data/queries/admin';
|
||||
import { invalidateAdminDetailed, invalidateAdminList } from '#core/AccessControl/data/queries/cache';
|
||||
import { useAuth } from '#core/Auth/providers/AuthProvider';
|
||||
import { SubmitFunc } from './form';
|
||||
import { type SubmitFunc } from './form';
|
||||
|
||||
interface Params {
|
||||
onSuccess?: (id?: number) => void;
|
||||
}
|
||||
|
||||
export const useSubmit = (params?: Params) => {
|
||||
const onSuccess = params?.onSuccess;
|
||||
const { verify } = useAuth();
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useIntl } from 'react-intl';
|
||||
import { FormElement, FormGrid, GridElement, InputText } from '#shared/forms/composed';
|
||||
import { FormControl, FormField, FormItem, FormLabel, FormMessage, Form as FormProvider } from '#shared/ui/form';
|
||||
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<{
|
||||
permissions: Record<string, Array<{ label: string; value: string }>>;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { UseFormReturn } from 'react-hook-form';
|
||||
import { IntlShape } from 'react-intl';
|
||||
import z from 'zod';
|
||||
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();
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ErrorOption, UseFormReturn } from 'react-hook-form';
|
||||
import type { ErrorOption, UseFormReturn } from 'react-hook-form';
|
||||
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 { useCreate, useUpdate } from '#core/AccessControl/data/queries/role';
|
||||
import { useAuth } from '#core/Auth/providers/AuthProvider';
|
||||
import { FormValues, SubmitFunc } from './form';
|
||||
import type { FormValues, SubmitFunc } from './form';
|
||||
|
||||
interface Params {
|
||||
onSuccess?: (id?: number) => void;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
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 { Container } from '#shared/widgets/common/Container';
|
||||
import { createAccessControlMenu } from '../../config/menu';
|
||||
import AccessControlLogo from '../ui/AccessControlLogo';
|
||||
|
||||
const AdminManagementLayout: React.FC = () => {
|
||||
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 React from 'react';
|
||||
import { IntlShape, useIntl } from 'react-intl';
|
||||
import { useIntl, type IntlShape } from 'react-intl';
|
||||
import { useDebounceSearch } from '@crux-kit/hooks';
|
||||
import { IntlName } from '#infrastructure/IntlName';
|
||||
import useIntlName from '#app/i18n/useIntlName';
|
||||
import { mapTableStateToListQueryParams } from '#infrastructure/reactQuery';
|
||||
import { PermissionGate } from '#core/Permission';
|
||||
import { Badge } from '#shared/ui/Badge';
|
||||
@@ -15,7 +16,7 @@ import { ScrollArea, ScrollBar } from '#shared/ui/ScrollArea';
|
||||
import { Skeleton } from '#shared/ui/Skeleton';
|
||||
import { DataGrid, DataGridColumnFilter, DataGridColumnHeader, DataGridColumnVisibility, DataGridPagination, DataGridTable, DataGridToolbarFilters, useDataGrid } from '#shared/widgets/DataGrid';
|
||||
import { useGetList } from '../../data/queries/admin';
|
||||
import { AdminEntity, AdminStatusEnum } from '../../entities/AdminEntity';
|
||||
import { AdminStatusEnum, type AdminEntity } from '../../entities/AdminEntity';
|
||||
import { DeleteAdminTrigger } from './DeleteAdminTrigger';
|
||||
import { ManageAdminTrigger } from './ManageAdminTrigger';
|
||||
|
||||
@@ -36,6 +37,7 @@ const getStatusOptions = (intl: IntlShape) => [
|
||||
|
||||
export const AdminTable = () => {
|
||||
const intl = useIntl();
|
||||
const formatName = useIntlName();
|
||||
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
@@ -180,7 +182,7 @@ export const AdminTable = () => {
|
||||
cell: (info) => {
|
||||
const data = info.row.original;
|
||||
const id = data.id;
|
||||
const name = IntlName.formatToDisplay(data, 'SURNAME_NAME');
|
||||
const name = formatName(data, 'SURNAME_NAME');
|
||||
const editModalTitle =
|
||||
intl.formatMessage({ id: 'ACCESS_CONTROL.ADMIN.EDIT' }) + ' ' + name;
|
||||
|
||||
@@ -207,7 +209,7 @@ export const AdminTable = () => {
|
||||
}
|
||||
}
|
||||
],
|
||||
[intl, statusOptions]
|
||||
[intl, statusOptions, formatName]
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
|
||||
@@ -2,12 +2,12 @@ import { LoaderCircleIcon } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
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 { Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '#shared/ui/Dialog';
|
||||
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<
|
||||
React.PropsWithChildren<{
|
||||
|
||||
@@ -2,12 +2,12 @@ import { LoaderCircleIcon } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
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 { Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '#shared/ui/Dialog';
|
||||
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<
|
||||
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 React from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useDebounceSearch } from '@crux-kit/hooks';
|
||||
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 { Button } from '#shared/ui/Button';
|
||||
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 { Skeleton } from '#shared/ui/Skeleton';
|
||||
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> {
|
||||
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 { accountSettingsRoute } from './routing';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Navigate, Route, RouteObject, Routes } from 'react-router-dom';
|
||||
import ProfilePage from '../pages/Profile';
|
||||
import AccountLayout from '../view/layouts/AccountLayout';
|
||||
import { Navigate, Route, Routes, type RouteObject } from 'react-router-dom';
|
||||
import ProfilePage from '#core/AccountSettings/pages/Profile';
|
||||
import AccountLayout from '#core/AccountSettings/view/layouts/AccountLayout';
|
||||
|
||||
function Routing() {
|
||||
return (
|
||||
|
||||
@@ -2,11 +2,11 @@ import React from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { toast } from 'sonner';
|
||||
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 { Card, CardContent, CardHeader, CardTitle } from '#shared/ui/Card';
|
||||
import { Container } from '#shared/widgets/common/Container';
|
||||
import { prepareFormValues, ProfileForm, SubmitFunc } from '../../view/forms/ProfileForm';
|
||||
|
||||
const ProfilePage: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Button } from '#shared/ui/Button';
|
||||
import { Form as FormProvider } from '#shared/ui/form';
|
||||
import AvatarCropperField from './fields/AvatarCropperField';
|
||||
import PasswordField from './fields/PasswordField';
|
||||
import { createValidationSchema, FormValues, SubmitFunc } from './form';
|
||||
import { createValidationSchema, type FormValues, type SubmitFunc } from './form';
|
||||
|
||||
export const ProfileForm: React.FC<{
|
||||
initialValues: FormValues;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
import { IntlShape } from 'react-intl';
|
||||
import type { UseFormReturn } from 'react-hook-form';
|
||||
import type { IntlShape } from 'react-intl';
|
||||
import z from 'zod';
|
||||
import { AuthUser } from '#core/Auth/entities';
|
||||
import type { AuthUser } from '#core/Auth/entities';
|
||||
|
||||
export const createValidationSchema = (intl: IntlShape) => {
|
||||
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 { authRoute } from './routing';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Navigate, Route, RouteObject, Routes } from 'react-router-dom';
|
||||
import { SignInPage } from '../pages/SignInPage';
|
||||
import { BrandedLayout } from '../view/layouts/Branded';
|
||||
import { Navigate, Route, Routes, type RouteObject } from 'react-router-dom';
|
||||
import SignInPage from '#core/Auth/pages/SignInPage';
|
||||
import BrandedLayout from '#core/Auth/view/layouts/Branded';
|
||||
|
||||
/**
|
||||
* Handles all authentication related routes.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { axiosClient } from '#infrastructure/axios';
|
||||
import { AuthModel } from '#core/Auth/entities';
|
||||
import { GetUserResponse, UpdateUserPayload } from './dto';
|
||||
import type { AuthModel } from '#core/Auth/entities';
|
||||
import type { GetUserResponse, UpdateUserPayload } from './dto';
|
||||
|
||||
export const AuthApi = {
|
||||
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>;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AuthUser } from '../../entities';
|
||||
import { GetUserResponse } from '../api/dto';
|
||||
import type { GetUserResponse } from '#core/Auth/data/api/dto';
|
||||
import type { AuthUser } from '#core/Auth/entities';
|
||||
|
||||
export default class UserMapper {
|
||||
constructor() {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LocalStorageService } from '@crux-kit/local-storage';
|
||||
import { APP_VERSION } from '#app/config/app';
|
||||
import { AuthModel } from '#core/Auth/entities';
|
||||
import type { AuthModel } from '#core/Auth/entities';
|
||||
|
||||
const storage = new LocalStorageService({
|
||||
prefix: `auth-v${APP_VERSION || '1.0'}`
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './auth';
|
||||
export * from './user';
|
||||
export type { AuthModel } from './auth';
|
||||
export type { AuthUser } from './user';
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useAuth } from '../providers/AuthProvider';
|
||||
import { SignInForm } from '../view/forms/SignInForm/Component';
|
||||
import { SubmitFunc } from '../view/forms/SignInForm/form';
|
||||
|
||||
export function SignInPage() {
|
||||
export default function SignInPage() {
|
||||
const { login } = useAuth();
|
||||
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 { 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 * as authHelper from '#core/Auth/data/stores/localStorage';
|
||||
import { AuthModel, AuthUser } from '#core/Auth/entities';
|
||||
import { getAuth, removeAuth, setAuth as setStoredAuth } from '#core/Auth/data/stores/localStorage';
|
||||
import type { AuthModel, AuthUser } from '#core/Auth/entities';
|
||||
import { AuthContext } from './context';
|
||||
|
||||
export function AuthProvider({ children }: PropsWithChildren) {
|
||||
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 verify = async () => {
|
||||
@@ -29,9 +29,9 @@ export function AuthProvider({ children }: PropsWithChildren) {
|
||||
const saveAuth = (auth: AuthModel | undefined) => {
|
||||
setAuth(auth);
|
||||
if (auth) {
|
||||
authHelper.setAuth(auth);
|
||||
setStoredAuth(auth);
|
||||
} else {
|
||||
authHelper.removeAuth();
|
||||
removeAuth();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
import { GetUserResponse, UpdateUserPayload } from '#core/Auth/data/api/dto';
|
||||
import { AuthModel, AuthUser } from '#core/Auth/entities';
|
||||
import type { GetUserResponse, UpdateUserPayload } from '#core/Auth/data/api/dto';
|
||||
import type { AuthModel, AuthUser } from '#core/Auth/entities';
|
||||
|
||||
export const AuthContext = createContext<{
|
||||
loading: boolean;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useIntl } from 'react-intl';
|
||||
import { FormElement, FormGrid, GridElement, InputText } from '#shared/forms/composed';
|
||||
import { Button } from '#shared/ui/Button';
|
||||
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<{
|
||||
initialValues: FormValues;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IntlShape } from 'react-intl';
|
||||
import type { IntlShape } from 'react-intl';
|
||||
import z from 'zod';
|
||||
|
||||
export const createValidationSchema = (intl: IntlShape) =>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { toAbsoluteUrl } from '#shared/lib/helpers';
|
||||
import { Badge } from '#shared/ui/Badge';
|
||||
import { Card, CardContent } from '#shared/ui/Card';
|
||||
|
||||
export function BrandedLayout() {
|
||||
export default function BrandedLayout() {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Link, Outlet } from 'react-router-dom';
|
||||
import { toAbsoluteUrl } from '#shared/lib/helpers';
|
||||
import { Card, CardContent } from '#shared/ui/Card';
|
||||
|
||||
export function ClassicLayout() {
|
||||
export default function ClassicLayout() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<style>
|
||||
@@ -26,7 +26,7 @@ export function ClassicLayout() {
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<Card className="w-full max-w-[25rem]">
|
||||
<Card className="w-full max-w-100">
|
||||
<CardContent className="p-6">
|
||||
<Outlet />
|
||||
</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 { fallbackScreensRoute } from './routing';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FolderOpen, Wrench } from 'lucide-react';
|
||||
import { IntlShape } from 'react-intl';
|
||||
import type { IntlShape } from 'react-intl';
|
||||
import { MenuItem } from '#app/config/types';
|
||||
|
||||
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 { fileManagerMessages } from './messages';
|
||||
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 { 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 {
|
||||
getEntries = async (
|
||||
@@ -15,7 +15,7 @@ export class Api {
|
||||
}
|
||||
) => {
|
||||
return await axiosClient
|
||||
.get<DTO.GetEntriesResponseDTO>('/file-manager' + ensureLeadingSlash(path), {
|
||||
.get<GetEntriesResponseDTO>('/file-manager' + ensureLeadingSlash(path), {
|
||||
params: payload
|
||||
})
|
||||
.then((data) => data.data);
|
||||
@@ -23,7 +23,7 @@ export class Api {
|
||||
|
||||
getFile = async (path: string) => {
|
||||
return await axiosClient
|
||||
.get<DTO.GetFileResponseDTO>('/file-manager/files/details' + ensureLeadingSlash(path))
|
||||
.get<GetFileResponseDTO>('/file-manager/files/details' + ensureLeadingSlash(path))
|
||||
.then((data) => data.data);
|
||||
};
|
||||
|
||||
@@ -37,19 +37,19 @@ export class Api {
|
||||
|
||||
createDirectory = async (payload: { path: string; name: string }) => {
|
||||
return await axiosClient
|
||||
.post<DTO.CreateDirectoryResponseDTO>('/file-manager/folders', payload)
|
||||
.post<CreateDirectoryResponseDTO>('/file-manager/folders', payload)
|
||||
.then((data) => data.data);
|
||||
};
|
||||
|
||||
renameEntry = async (payload: { path: string; name: string }) => {
|
||||
return await axiosClient
|
||||
.put<DTO.RenameEntryResponseDTO>('/file-manager/entries/rename', payload)
|
||||
.put<RenameEntryResponseDTO>('/file-manager/entries/rename', payload)
|
||||
.then((data) => data.data);
|
||||
};
|
||||
|
||||
removeEntry = async (path: string) => {
|
||||
return await axiosClient
|
||||
.delete<DTO.RemoveEntryResponseDTO>('/file-manager' + ensureLeadingSlash(path))
|
||||
.delete<RemoveEntryResponseDTO>('/file-manager' + ensureLeadingSlash(path))
|
||||
.then((data) => data.data);
|
||||
};
|
||||
|
||||
@@ -61,7 +61,7 @@ export class Api {
|
||||
}
|
||||
) => {
|
||||
return await axiosClient
|
||||
.post<DTO.UploadFileResponseDTO>('/file-manager/files', formData, {
|
||||
.post<UploadFileResponseDTO>('/file-manager/files', formData, {
|
||||
signal: params.signal,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
@@ -74,13 +74,13 @@ export class Api {
|
||||
|
||||
duplicateFiles = async (paths: string[]) => {
|
||||
return await axiosClient
|
||||
.post<DTO.DuplicateFileEntriesResponseDTO>('/file-manager/files/duplicate', { paths })
|
||||
.post<DuplicateFileEntriesResponseDTO>('/file-manager/files/duplicate', { paths })
|
||||
.then((data) => data.data);
|
||||
};
|
||||
|
||||
searchEntries = async (query: string) => {
|
||||
return await axiosClient
|
||||
.get<DTO.SearchEntriesResponseDTO>('/file-manager/search', {
|
||||
.get<SearchEntriesResponseDTO>('/file-manager/search', {
|
||||
params: { q: query }
|
||||
})
|
||||
.then((data) => data.data);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { runInAction } from 'mobx';
|
||||
import React from 'react';
|
||||
import EntryMapper from '#core/FileManager/data/mappers/EntryMapper';
|
||||
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 ExplorerStore from '#core/FileManager/data/stores/explorerStore';
|
||||
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 { EntryDTO } from '../api/dto';
|
||||
import { type EntryDTO } from '../api/dto';
|
||||
|
||||
export default class EntryMapper {
|
||||
constructor(private readonly parent: string) {}
|
||||
|
||||