Compare commits

...
6 Commits
Author SHA1 Message Date
Igor Martyniuk aae77ebc27 docs: update changelog for v1.3.1 2026-08-31 15:43:21 +03:00
Igor Martyniuk 3556662ddd Merge branch 'fix/misc-fixes' into 'main'
TASKS-1832

See merge request crux/admin!31
2026-08-31 12:38:14 +00:00
Igor Martyniuk ae34b30e4f TASKS-1832 2026-08-31 12:38:14 +00:00
Igor Martyniuk 93fc6ff5a0 docs: update changelog for v1.3.0 2026-08-28 11:43:02 +03:00
Igor Martyniuk e8558c076e Merge branch 'feature/page-manager' into 'main'
TASKS-1823 WebManager module

See merge request crux/admin!30
2026-08-28 08:35:58 +00:00
Igor Martyniuk c93d8c025b TASKS-1823 WebManager module 2026-08-28 08:35:58 +00:00
671 changed files with 26541 additions and 6175 deletions
+1
View File
@@ -1,3 +1,4 @@
node_modules node_modules
.next
npm-debug.log npm-debug.log
.git .git
+2 -2
View File
@@ -26,7 +26,7 @@ yarn-error.log
# Platform specific # Platform specific
.DS_Store .DS_Store
*.tsbuildinfo
/apps/backend/public/ /apps/backend/public/
/apps/backend/storage/ /apps/backend/storage/
/apps/backend/.adonisjs/
/apps/backend/database/schema.ts
+2
View File
@@ -1 +1,3 @@
npm run test:admin npm run test:admin
npm run test:backend
npm run test:web
+6 -3
View File
@@ -1,10 +1,13 @@
# Dependencies and generated output
**/node_modules/** **/node_modules/**
**/build/** **/build/**
**/dist/** **/dist/**
**/coverage/** **/coverage/**
**/.next/**
apps/backend/public/** apps/backend/public/**
apps/backend/storage/** apps/backend/storage/**
# Handlebars generator templates contain application syntax that is not valid HTML.
apps/backend/app/templates/** apps/backend/app/templates/**
apps/backend/.adonisjs/**
apps/backend/database/schema.ts
apps/web/.next/**
+74 -5
View File
@@ -9,17 +9,86 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Added ### 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 ### Changed
### Fixed ### Fixed
### Removed ### 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 ## [1.2.0] - 2026-07-17
### Changed ### Changed
+133 -55
View File
@@ -1,3 +1,18 @@
# CRUX
A monorepo starter for products that need an admin panel, an API, and a public website. It ships an
AdonisJS backend, a React admin panel with a module generator, and a Next.js website that renders
the pages editors publish through the built-in CMS.
| App | Source | Dev port | What it is |
| --------------- | --------------- | -------- | ------------------------------------------------------ |
| Backend API | `apps/backend/` | 3333 | AdonisJS — data, auth, permissions, public content API |
| Admin panel | `apps/admin/` | 5173 | React + Vite — the internal UI |
| Public website | `apps/web/` | 3000 | Next.js — the site visitors see |
| Shared packages | `packages/` | — | UI, hooks, services, types, utils |
Full documentation lives in [`docs/`](./docs/Readme.md).
### Tech Stack ### Tech Stack
#### Backend #### Backend
@@ -6,94 +21,157 @@
- TypeScript - TypeScript
- Lucid ORM - Lucid ORM
- PostgreSQL / MySQL (configurable) - PostgreSQL / MySQL (configurable)
- Redis, S3 / MinIO storage
#### Frontend #### Admin panel
- React - React
- TypeScript - TypeScript
- Vite - Vite
- React Query / Mobx / Axios - React Query / MobX / Axios
- Component-based UI system - Radix UI + Tailwind component system
- React Hook Form + Zod
#### Public website
- Next.js (App Router, React Server Components)
- TypeScript
- next-intl
- Tailwind CSS
### Getting Started ### Getting Started
#### Prerequisites #### Prerequisites
Node.js >= v24.18.0 - Node.js `24.18.0` (see `.nvmrc`; the package manifest requires `>=24.0.0`)
npm >= 11.15.0 - npm `>=11.6.2` (the repo pins `npm@11.15.0`)
Database PostgreSQL - PostgreSQL
#### Installation #### Installation
``` ```bash
git clone gitlab@git.rexsoftproduction.com:crux/admin.git git clone gitlab@git.rexsoftproduction.com:crux/admin.git
cd crux
npm install
``` ```
```bash
cd crux && npm install --force
```
> `--force` is required. `typescript-eslint` does not yet declare support for the TypeScript
> version this repo uses, so a plain `npm install` fails with `ERESOLVE`. Never substitute
> `--legacy-peer-deps` — it strips nested peer dependencies and breaks the backend at runtime. Use
> `npm ci --force` for a reproducible install, and `npm install <package> --force` when adding one.
> See [troubleshooting.md](./troubleshooting.md).
### Configuration ### Configuration
Environment variables are defined in root `.env` Each workspace defines its own environment variables. Copy the workspace's `.env.example` to `.env` and fill in the values:
``` | Env file | Covers |
TZ=UTC | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
PORT=3333 | `apps/backend/.env` | Server (`PORT`, `APP_KEY`), database (`DB_*`), storage (`AWS_*`, `S3_*`), and the public site link (`WEB_APP_URL`, `WEB_REVALIDATE_SECRET`) used to purge its cache |
HOST=localhost | `apps/admin/.env` | `VITE_API_URL` (backend), `VITE_WEB_URL` (public site, used for page previews), `VITE_TINYMCE_API_KEY` |
LOG_LEVEL=info | `apps/web/.env` | `BACKEND_API_URL` (backend base URL for server rendering), `REVALIDATE_SECRET` (must match the backend's `WEB_REVALIDATE_SECRET`), `SITE_URL` (canonical origin) |
APP_KEY=
NODE_ENV=development `docker-compose.yml` loads each service's env from the same per-app files. Inside Docker, service
DB_HOST=127.0.0.1 URLs must use compose service names (`http://backend:3333`), not `localhost`.
DB_PORT=5432
DB_USER=postgres ### Running
DB_PASSWORD=postgres
DB_DATABASE=adonis #### Locally
VITE_API_URL=http://0.0.0.0:3333
VITE_TINYMCE_API_KEY= | Command | Description |
| ----------------------- | ----------------------------------------------- |
| `npm run dev:backend` | Start the AdonisJS backend in development mode |
| `npm run dev:admin-app` | Start the React admin application |
| `npm run dev:web` | Start the Next.js web application |
| `npm run dev:all` | Start backend, admin, and web apps concurrently |
#### With Docker
```bash
docker compose up -d
``` ```
### Module Generator Brings up everything: backend (3333), admin (5173), web (3000), PostgreSQL (5432), Redis (6379),
MinIO (9000 / console 9001) and pgAdmin (5050).
The module generator allows you to create full CRUD modules by selecting: Two quirks worth knowing when a change appears not to take effect:
- The web container keeps `.next` on an anonymous volume that survives rebuilds. Clear it with
`docker compose up -d --force-recreate --renew-anon-volumes web`.
- Backend HMR does not always pick up edits on macOS (bind-mount file events); restart
`crux_backend_adonis` to load them.
### Scripts
#### Build
| Command | Description |
| ----------------------- | ------------------------------------------ |
| `npm run build:backend` | Build the backend application |
| `npm run build:admin` | Type-check and build the admin application |
| `npm run build:web` | Type-check and build the Next.js web app |
#### Test
| Command | Description |
| ---------------------- | ---------------------------------------------- |
| `npm run test:backend` | Backend functional tests (requires a database) |
| `npm run test:admin` | Admin unit tests (Vitest) |
| `npm run test:web` | Web app unit tests (Vitest) |
#### Lint and format
| Command | Description |
| ---------------------- | -------------------------- |
| `npm run lint` | ESLint across the monorepo |
| `npm run lint:fix` | ESLint with autofix |
| `npm run format` | Prettier write |
| `npm run format:check` | Prettier check, no writes |
#### Versions
| Command | Description |
| ----------------------- | ------------------------------------ |
| `npm run version:patch` | Update patch version of all packages |
| `npm run version:minor` | Update minor version of all packages |
| `npm run version:major` | Update major version of all packages |
> Versions are updated without creating git tags.
### Features
#### Module Generator
Create full CRUD modules by selecting:
- Database table - Database table
- Fields and types - Fields and types
- Validation rules - Validation rules
- List view configuration - List view configuration
#### Generated Output **Generated output** — Backend: controllers, validators, routes. Frontend: pages, forms, tables, API.
**Backend**: controllers, validators, routes #### Authentication & Authorization
**Frontend**: pages, forms, tables, API
### Authentication & Authorization - JWT / session-based authentication
- JWT / Session-based authentication
- Role & permission system - Role & permission system
- Policy-based access control - Policy-based access control
### Scripts #### Content Management
#### Development Web Manager is the built-in CMS: editors build localized pages from prebuilt blocks in the admin
panel, preview them privately, and publish to the Next.js site, which purges only the cache entries
the change affects. See [Web Manager](./docs/core/web-manager/Readme.md).
| Command | Description | ### Documentation
| ----------------------- | ---------------------------------------------- |
| `npm run dev:backend` | Start the AdonisJS backend in development mode |
| `npm run dev:admin-app` | Start the React admin application |
| `npm run dev:all` | Start backend and admin app concurrently |
#### Build | Topic | Where |
| ------------------------ | ---------------------------------------------------- |
| Command | Description | | Documentation home | [docs/Readme.md](./docs/Readme.md) |
| ----------------------- | ----------------------------- | | Public website | [apps/web/Readme.md](./apps/web/Readme.md) |
| `npm run build:backend` | Build the backend application | | Architecture | [docs/architecture/](./docs/architecture/Readme.md) |
| `npm run build:admin` | Build the admin application | | Core modules | [docs/core/](./docs/Readme.md#core-modules) |
| Shared packages | [docs/packages/Readme.md](./docs/packages/Readme.md) |
#### Versions | Install and setup issues | [troubleshooting.md](./troubleshooting.md) |
| Command | Description |
| --------------- | ------------------------------------ |
| `npm run patch` | Update Patch version of all packages |
| `npm run minor` | Update Minor version of all packages |
| `npm run major` | Update Major version of all packages |
> Versions are updated without creating git tags.
+3 -2
View File
@@ -1,2 +1,3 @@
VITE_API_URL=http://0.0.0.0:3333 VITE_API_URL=http://localhost:3333
VITE_TINYMCE_API_KEY= VITE_TINYMCE_API_KEY=
VITE_WEB_URL=http://localhost:3000
-8
View File
@@ -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 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@crux-kit/admin-app", "name": "@crux-kit/admin-app",
"private": true, "private": true,
"version": "1.2.0", "version": "1.3.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "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

+4
View File
@@ -0,0 +1,4 @@
import { createContext } from 'react';
import { type I18nProviderProps } from '#app/i18n/types';
export default createContext<I18nProviderProps | null>(null);
+99 -1
View File
@@ -1,7 +1,105 @@
{ {
"ACTION.BACK": "رجوع",
"ACTION.SAVE": "حفظ",
"ACTION.CREATE": "إنشاء",
"ACTION.INCLUDE": "تضمين",
"ACTION.GENERATE": "توليد",
"ACTION.REMOVE": "إزالة",
"ACTION.CANCEL": "إلغاء",
"ACTION.CONFIRM": "تأكيد",
"ACTION.SUBMIT": "إرسال",
"ACTION.SEARCH": "بحث",
"ACTION.SWAP": "تبديل",
"ACTION.MERGE": "دمج",
"ACTION.SPLIT": "تقسيم",
"ACTION.RESET": "إعادة تعيين",
"ACTION.PREVIEW": "معاينة",
"ACTION.UPLOAD": "رفع",
"ACTION.DELETE": "حذف",
"ACTION.CLOSE": "إغلاق",
"ACTION.CLEAR": "مسح",
"ACTION.CLEAR_FILTERS": "مسح عوامل التصفية",
"ACTION.EDIT": "تعديل",
"STATE.CREATING": "جارٍ الإنشاء",
"STATE.GENERATING": "جارٍ التوليد",
"STATE.SAVING": "جارٍ الحفظ",
"STATE.REMOVING": "جارٍ الإزالة",
"STATE.LOADING": "جارٍ التحميل",
"STATE.UPDATING": "جارٍ التحديث",
"ERROR": "خطأ",
"PERMISSION": "صلاحية",
"PERMISSION.PERMISSIONS": "الصلاحيات",
"PERMISSION.PERMISSION_NAME": "اسم الصلاحية",
"PERMISSION.SLUG": "Slug",
"PERMISSION.EDIT_PERMISSION": "تعديل الصلاحية",
"ROLE": "دور",
"ROLE.ROLES": "الأدوار",
"ROLE.ROLE_NAME": "اسم الدور",
"ROLE.SLUG": "Slug",
"ROLE.EDIT_ROLE": "تعديل الدور",
"USER": "مستخدم",
"USER.EDIT_USER": "تعديل المستخدم",
"USER.STATUS.ACTIVE": "نشط",
"USER.STATUS.INACTIVE": "غير نشط",
"USER.MENU.MY_PROFILE": "الملف الشخصي الخاص بي", "USER.MENU.MY_PROFILE": "الملف الشخصي الخاص بي",
"USER.MENU.MY_ACCOUNT": "حسابي", "USER.MENU.MY_ACCOUNT": "حسابي",
"USER.MENU.LANGUAGE": "لغة",
"USER.MENU.DARK_MODE": "الوضع المظلم", "USER.MENU.DARK_MODE": "الوضع المظلم",
"USER.MENU.LOGOUT": "تسجيل الخروج", "USER.MENU.LOGOUT": "تسجيل الخروج",
"USER.MENU.LANGUAGE": "لغة" "SUCCESS": "تم بنجاح",
"ACTIONS": "الإجراءات",
"FILTER": "تصفية",
"SELECTED": "محدد",
"NAME": "الاسم",
"FIRST_NAME": "الاسم الأول",
"LAST_NAME": "اسم العائلة",
"EMAIL": "البريد الإلكتروني",
"SELECT": "اختر",
"SELECT_OPTION": "اختر خياراً",
"SELECT_ALL": "تحديد الكل",
"SELECT_ALL_WITH_OPTS_LENGTH": "تحديد الكل ({length} خيارات)",
"STATUS": "الحالة",
"PASSWORD": "كلمة المرور",
"NEW_PASSWORD": "كلمة المرور الجديدة",
"PASSWORD_CONFIRM": "تأكيد كلمة المرور",
"MODULE_NAME": "اسم الوحدة",
"RETURN_HOME": "العودة إلى الرئيسية",
"NO_ITEMS_FOUND": "لم يتم العثور على عناصر",
"NO_RESULTS_FOUND": "لم يتم العثور على نتائج",
"PLUS_MORE_WITH_NUMBER": "+ {number} أخرى",
"ADMIN.ADMIN_MANAGEMENT": "إدارة المشرفين",
"ADMIN.ADMINS": "المشرفون",
"DASHBOARD": "لوحة التحكم",
"FORM.ERROR.REQUIRED": "مطلوب",
"FORM.ERROR.INVALID_EMAIL_ADDRESS": "عنوان بريد إلكتروني غير صالح",
"FORM.ERROR.PASSWORDS_MUST_MATCH": "يجب أن تتطابق كلمتا المرور",
"FORM.ERROR.MIN_PASSWORD_LENGTH_DESC": "الحد الأدنى لطول كلمة المرور - 8 أحرف.",
"FORM.ERROR.SOMETHING_WENT_WRONG": "حدث خطأ ما",
"TABLE.COMMON.COLUMNS": "الأعمدة",
"TABLE.COMMON.NO_DATA_AVAILABLE": "لا توجد بيانات متاحة",
"TABLE.COMMON.ROWS_PER_PAGE": "عدد الصفوف في الصفحة",
"TABLE.COMMON.GO_TO_PREVIOUS_PAGE": "الانتقال إلى الصفحة السابقة",
"TABLE.COMMON.GO_TO_NEXT_PAGE": "الانتقال إلى الصفحة التالية",
"PROFILE.PROFILE_SETTINGS": "إعدادات الملف الشخصي",
"PROFILE.AVATAR_DESCRIPTION": "نوصي باستخدام صور لا تقل أبعادها عن 98 × 98 بكسل (بحجم يصل إلى 4 ميغابايت) بصيغة PNG أو JPG أو JPEG.",
"PROFILE.AVATAR_ALT": "الصورة الرمزية",
"PROFILE.AVATAR_SETTINGS": "إعدادات الصورة الرمزية",
"PROFILE.AVATAR_CROP_ME": "قص الصورة",
"PROFILE.EDIT_PROFILE_IMAGE": "تعديل صورة الملف الشخصي",
"MODULE.COMMON.REMOVE_ITEM_QUESTION": "هل أنت متأكد من رغبتك في إزالة {target}؟",
"MODULE.COMMON.ARE_YOU_SURE?": "هل أنت متأكد؟",
"MODULE.COMMON.REMOVE_ITEM_QUESTION_DESC": "لا يمكن التراجع عن هذا الإجراء.",
"MODULE.COMMON.CREATE_NEW_ENTITY": "إنشاء {entity} جديد",
"MODULE.COMMON.CREATE_NEW_USER": "إنشاء مستخدم جديد",
"MESSAGE.PLEASE_WAIT": "يرجى الانتظار",
"MESSAGE.PLEASE_WAIT_THIS_MAY_TAKE_FEW_MOMENTS": "يرجى الانتظار، قد يستغرق هذا بعض الوقت",
"COMMON.SELECT_FILE_TO_VIEW_ITS_CONTENT": "اختر ملفاً لعرض محتوياته",
"COMPONENT.FILE_INPUT.SELECTED_NUM_OF_FILES": "تم اختيار {num} ملف",
"COMPONENT.FILE_INPUT.SELECT_FILE_NO_FILE_SELECTED": "اختر ملفاً، لم يتم اختيار أي ملف",
"COMPONENT.FILE_INPUT.OPTION.FROM_CLOUD_STORAGE": "من التخزين السحابي",
"COMPONENT.FILE_INPUT.OPTION.FROM_LOCAL_MACHINE": "من الجهاز المحلي",
"COMPONENT.FILE_INPUT.OPTION.CLEAR": "مسح",
"COMPONENT.FILE_INPUT.SHOW_SELECTED_FILES": "عرض الملفات المختارة",
"COMPONENT.FILE_INPUT.SELECTED_FILES": "الملفات المختارة",
"COMPONENT.FILE_INPUT.REMOVE_NAME": "إزالة {name}"
} }
-15
View File
@@ -20,28 +20,23 @@
"ACTION.CLEAR": "Clear", "ACTION.CLEAR": "Clear",
"ACTION.CLEAR_FILTERS": "Clear filters", "ACTION.CLEAR_FILTERS": "Clear filters",
"ACTION.EDIT": "Edit", "ACTION.EDIT": "Edit",
"STATE.CREATING": "Creating", "STATE.CREATING": "Creating",
"STATE.GENERATING": "Generating", "STATE.GENERATING": "Generating",
"STATE.SAVING": "Saving", "STATE.SAVING": "Saving",
"STATE.REMOVING": "Removing", "STATE.REMOVING": "Removing",
"STATE.LOADING": "Loading", "STATE.LOADING": "Loading",
"STATE.UPDATING": "Updating", "STATE.UPDATING": "Updating",
"ERROR": "Error", "ERROR": "Error",
"PERMISSION": "Permission", "PERMISSION": "Permission",
"PERMISSION.PERMISSIONS": "Permissions", "PERMISSION.PERMISSIONS": "Permissions",
"PERMISSION.PERMISSION_NAME": "Permission name", "PERMISSION.PERMISSION_NAME": "Permission name",
"PERMISSION.SLUG": "Slug", "PERMISSION.SLUG": "Slug",
"PERMISSION.EDIT_PERMISSION": "Edit permission", "PERMISSION.EDIT_PERMISSION": "Edit permission",
"ROLE": "Role", "ROLE": "Role",
"ROLE.ROLES": "Roles", "ROLE.ROLES": "Roles",
"ROLE.ROLE_NAME": "Role name", "ROLE.ROLE_NAME": "Role name",
"ROLE.SLUG": "Slug", "ROLE.SLUG": "Slug",
"ROLE.EDIT_ROLE": "Edit role", "ROLE.EDIT_ROLE": "Edit role",
"USER": "User", "USER": "User",
"USER.EDIT_USER": "Edit user", "USER.EDIT_USER": "Edit user",
"USER.STATUS.ACTIVE": "Active", "USER.STATUS.ACTIVE": "Active",
@@ -51,7 +46,6 @@
"USER.MENU.LANGUAGE": "Language", "USER.MENU.LANGUAGE": "Language",
"USER.MENU.DARK_MODE": "Dark Mode", "USER.MENU.DARK_MODE": "Dark Mode",
"USER.MENU.LOGOUT": "Logout", "USER.MENU.LOGOUT": "Logout",
"SUCCESS": "Success", "SUCCESS": "Success",
"ACTIONS": "Actions", "ACTIONS": "Actions",
"FILTER": "Filter", "FILTER": "Filter",
@@ -73,42 +67,33 @@
"NO_ITEMS_FOUND": "No items found", "NO_ITEMS_FOUND": "No items found",
"NO_RESULTS_FOUND": "No results found", "NO_RESULTS_FOUND": "No results found",
"PLUS_MORE_WITH_NUMBER": "+ {number} more", "PLUS_MORE_WITH_NUMBER": "+ {number} more",
"ADMIN.ADMIN_MANAGEMENT": "Admin Management", "ADMIN.ADMIN_MANAGEMENT": "Admin Management",
"ADMIN.ADMINS": "Admins", "ADMIN.ADMINS": "Admins",
"DASHBOARD": "Dashboard", "DASHBOARD": "Dashboard",
"FORM.ERROR.REQUIRED": "Required", "FORM.ERROR.REQUIRED": "Required",
"FORM.ERROR.INVALID_EMAIL_ADDRESS": "Invalid email address", "FORM.ERROR.INVALID_EMAIL_ADDRESS": "Invalid email address",
"FORM.ERROR.PASSWORDS_MUST_MATCH": "Passwords must match", "FORM.ERROR.PASSWORDS_MUST_MATCH": "Passwords must match",
"FORM.ERROR.MIN_PASSWORD_LENGTH_DESC": "Min. password length - 8 chars.", "FORM.ERROR.MIN_PASSWORD_LENGTH_DESC": "Min. password length - 8 chars.",
"FORM.ERROR.SOMETHING_WENT_WRONG": "Something went wrong", "FORM.ERROR.SOMETHING_WENT_WRONG": "Something went wrong",
"TABLE.COMMON.COLUMNS": "Columns", "TABLE.COMMON.COLUMNS": "Columns",
"TABLE.COMMON.NO_DATA_AVAILABLE": "No data available", "TABLE.COMMON.NO_DATA_AVAILABLE": "No data available",
"TABLE.COMMON.ROWS_PER_PAGE": "Rows per page", "TABLE.COMMON.ROWS_PER_PAGE": "Rows per page",
"TABLE.COMMON.GO_TO_PREVIOUS_PAGE": "Go to previous page", "TABLE.COMMON.GO_TO_PREVIOUS_PAGE": "Go to previous page",
"TABLE.COMMON.GO_TO_NEXT_PAGE": "Go to next page", "TABLE.COMMON.GO_TO_NEXT_PAGE": "Go to next page",
"PROFILE.PROFILE_SETTINGS": "Profile Settings", "PROFILE.PROFILE_SETTINGS": "Profile Settings",
"PROFILE.AVATAR_DESCRIPTION": "We recommend using images that are at least 98 x 98 pixels in size (file size up to 4 MB) in PNG, JPG, JPEG format.", "PROFILE.AVATAR_DESCRIPTION": "We recommend using images that are at least 98 x 98 pixels in size (file size up to 4 MB) in PNG, JPG, JPEG format.",
"PROFILE.AVATAR_ALT": "Avatar", "PROFILE.AVATAR_ALT": "Avatar",
"PROFILE.AVATAR_SETTINGS": "Avatar Settings", "PROFILE.AVATAR_SETTINGS": "Avatar Settings",
"PROFILE.AVATAR_CROP_ME": "Crop me", "PROFILE.AVATAR_CROP_ME": "Crop me",
"PROFILE.EDIT_PROFILE_IMAGE": "Edit profile image", "PROFILE.EDIT_PROFILE_IMAGE": "Edit profile image",
"MODULE.COMMON.REMOVE_ITEM_QUESTION": "Are you sure you want to remove {target}?", "MODULE.COMMON.REMOVE_ITEM_QUESTION": "Are you sure you want to remove {target}?",
"MODULE.COMMON.ARE_YOU_SURE?": "Are you sure?", "MODULE.COMMON.ARE_YOU_SURE?": "Are you sure?",
"MODULE.COMMON.REMOVE_ITEM_QUESTION_DESC": "This action cannot be undone.", "MODULE.COMMON.REMOVE_ITEM_QUESTION_DESC": "This action cannot be undone.",
"MODULE.COMMON.CREATE_NEW_ENTITY": "Create new {entity}", "MODULE.COMMON.CREATE_NEW_ENTITY": "Create new {entity}",
"MODULE.COMMON.CREATE_NEW_USER": "Create new user", "MODULE.COMMON.CREATE_NEW_USER": "Create new user",
"MESSAGE.PLEASE_WAIT": "Please wait", "MESSAGE.PLEASE_WAIT": "Please wait",
"MESSAGE.PLEASE_WAIT_THIS_MAY_TAKE_FEW_MOMENTS": "Please wait, this may take few moments", "MESSAGE.PLEASE_WAIT_THIS_MAY_TAKE_FEW_MOMENTS": "Please wait, this may take few moments",
"COMMON.SELECT_FILE_TO_VIEW_ITS_CONTENT": "Select a file to view its contents", "COMMON.SELECT_FILE_TO_VIEW_ITS_CONTENT": "Select a file to view its contents",
"COMPONENT.FILE_INPUT.SELECTED_NUM_OF_FILES": "{num} file(s) selected", "COMPONENT.FILE_INPUT.SELECTED_NUM_OF_FILES": "{num} file(s) selected",
"COMPONENT.FILE_INPUT.SELECT_FILE_NO_FILE_SELECTED": "Select file, no file selected", "COMPONENT.FILE_INPUT.SELECT_FILE_NO_FILE_SELECTED": "Select file, no file selected",
"COMPONENT.FILE_INPUT.OPTION.FROM_CLOUD_STORAGE": "From cloud storage", "COMPONENT.FILE_INPUT.OPTION.FROM_CLOUD_STORAGE": "From cloud storage",
-15
View File
@@ -20,28 +20,23 @@
"ACTION.CLEAR": "Очистити", "ACTION.CLEAR": "Очистити",
"ACTION.CLEAR_FILTERS": "Очистити фільтри", "ACTION.CLEAR_FILTERS": "Очистити фільтри",
"ACTION.EDIT": "Редагувати", "ACTION.EDIT": "Редагувати",
"STATE.CREATING": "Створення", "STATE.CREATING": "Створення",
"STATE.GENERATING": "Генерування", "STATE.GENERATING": "Генерування",
"STATE.SAVING": "Збереження", "STATE.SAVING": "Збереження",
"STATE.REMOVING": "Видалення", "STATE.REMOVING": "Видалення",
"STATE.LOADING": "Завантаження", "STATE.LOADING": "Завантаження",
"STATE.UPDATING": "Оновлюється", "STATE.UPDATING": "Оновлюється",
"ERROR": "Помилка", "ERROR": "Помилка",
"PERMISSION": "Дозвіл", "PERMISSION": "Дозвіл",
"PERMISSION.PERMISSIONS": "Дозволи", "PERMISSION.PERMISSIONS": "Дозволи",
"PERMISSION.PERMISSION_NAME": "Назва дозволу", "PERMISSION.PERMISSION_NAME": "Назва дозволу",
"PERMISSION.SLUG": "Slug", "PERMISSION.SLUG": "Slug",
"PERMISSION.EDIT_PERMISSION": "Редагувати дозвіл", "PERMISSION.EDIT_PERMISSION": "Редагувати дозвіл",
"ROLE": "Роль", "ROLE": "Роль",
"ROLE.ROLES": "Ролі", "ROLE.ROLES": "Ролі",
"ROLE.ROLE_NAME": "Назва ролі", "ROLE.ROLE_NAME": "Назва ролі",
"ROLE.SLUG": "Slug", "ROLE.SLUG": "Slug",
"ROLE.EDIT_ROLE": "Редагувати роль", "ROLE.EDIT_ROLE": "Редагувати роль",
"USER": "Користувач", "USER": "Користувач",
"USER.EDIT_USER": "Редагувати користувача", "USER.EDIT_USER": "Редагувати користувача",
"USER.STATUS.ACTIVE": "Активний", "USER.STATUS.ACTIVE": "Активний",
@@ -51,7 +46,6 @@
"USER.MENU.LANGUAGE": "Мова", "USER.MENU.LANGUAGE": "Мова",
"USER.MENU.DARK_MODE": "Темний режим", "USER.MENU.DARK_MODE": "Темний режим",
"USER.MENU.LOGOUT": "Вийти", "USER.MENU.LOGOUT": "Вийти",
"SUCCESS": "Успіх", "SUCCESS": "Успіх",
"ACTIONS": "Дії", "ACTIONS": "Дії",
"FILTER": "Фільтр", "FILTER": "Фільтр",
@@ -73,42 +67,33 @@
"NO_ITEMS_FOUND": "Не знайдено жодного елемента", "NO_ITEMS_FOUND": "Не знайдено жодного елемента",
"NO_RESULTS_FOUND": "Результатів не знайдено", "NO_RESULTS_FOUND": "Результатів не знайдено",
"PLUS_MORE_WITH_NUMBER": "+ {number} ще", "PLUS_MORE_WITH_NUMBER": "+ {number} ще",
"ADMIN.ADMIN_MANAGEMENT": "Адмін Управління", "ADMIN.ADMIN_MANAGEMENT": "Адмін Управління",
"ADMIN.ADMINS": "Адміністратори", "ADMIN.ADMINS": "Адміністратори",
"DASHBOARD": "Дашборд", "DASHBOARD": "Дашборд",
"FORM.ERROR.REQUIRED": "Обов'язково", "FORM.ERROR.REQUIRED": "Обов'язково",
"FORM.ERROR.INVALID_EMAIL_ADDRESS": "Недійсна адреса електронної пошти", "FORM.ERROR.INVALID_EMAIL_ADDRESS": "Недійсна адреса електронної пошти",
"FORM.ERROR.PASSWORDS_MUST_MATCH": "Паролі мають збігатися", "FORM.ERROR.PASSWORDS_MUST_MATCH": "Паролі мають збігатися",
"FORM.ERROR.MIN_PASSWORD_LENGTH_DESC": "Мінімальна довжина пароля - 8 символів.", "FORM.ERROR.MIN_PASSWORD_LENGTH_DESC": "Мінімальна довжина пароля - 8 символів.",
"FORM.ERROR.SOMETHING_WENT_WRONG": "Щось пішло не так", "FORM.ERROR.SOMETHING_WENT_WRONG": "Щось пішло не так",
"TABLE.COMMON.COLUMNS": "Колонки", "TABLE.COMMON.COLUMNS": "Колонки",
"TABLE.COMMON.NO_DATA_AVAILABLE": "Немає доступних даних", "TABLE.COMMON.NO_DATA_AVAILABLE": "Немає доступних даних",
"TABLE.COMMON.ROWS_PER_PAGE": "Рядків на сторінці", "TABLE.COMMON.ROWS_PER_PAGE": "Рядків на сторінці",
"TABLE.COMMON.GO_TO_PREVIOUS_PAGE": "Перейти на попередню сторінку", "TABLE.COMMON.GO_TO_PREVIOUS_PAGE": "Перейти на попередню сторінку",
"TABLE.COMMON.GO_TO_NEXT_PAGE": "Перейти на наступну сторінку", "TABLE.COMMON.GO_TO_NEXT_PAGE": "Перейти на наступну сторінку",
"PROFILE.PROFILE_SETTINGS": "Налаштування профілю", "PROFILE.PROFILE_SETTINGS": "Налаштування профілю",
"PROFILE.AVATAR_DESCRIPTION": "Радимо використовувати зображення розміром принаймні 98 x 98 пікселів (розмір файлу - до 4 МБ) у форматі PNG, JPG , JPEG.", "PROFILE.AVATAR_DESCRIPTION": "Радимо використовувати зображення розміром принаймні 98 x 98 пікселів (розмір файлу - до 4 МБ) у форматі PNG, JPG , JPEG.",
"PROFILE.AVATAR_ALT": "Аватар", "PROFILE.AVATAR_ALT": "Аватар",
"PROFILE.AVATAR_SETTINGS": "Налаштування аватара", "PROFILE.AVATAR_SETTINGS": "Налаштування аватара",
"PROFILE.AVATAR_CROP_ME": "Обріж мене", "PROFILE.AVATAR_CROP_ME": "Обріж мене",
"PROFILE.EDIT_PROFILE_IMAGE": "Редагувати зображення профілю", "PROFILE.EDIT_PROFILE_IMAGE": "Редагувати зображення профілю",
"MODULE.COMMON.REMOVE_ITEM_QUESTION": "Ви впевнені, що хочете видалити {target}?", "MODULE.COMMON.REMOVE_ITEM_QUESTION": "Ви впевнені, що хочете видалити {target}?",
"MODULE.COMMON.ARE_YOU_SURE?": "Ви впевнені?", "MODULE.COMMON.ARE_YOU_SURE?": "Ви впевнені?",
"MODULE.COMMON.REMOVE_ITEM_QUESTION_DESC": "Цю дію не можна скасувати.", "MODULE.COMMON.REMOVE_ITEM_QUESTION_DESC": "Цю дію не можна скасувати.",
"MODULE.COMMON.CREATE_NEW_ENTITY": "Створити новий {entity}", "MODULE.COMMON.CREATE_NEW_ENTITY": "Створити новий {entity}",
"MODULE.COMMON.CREATE_NEW_USER": "Створити нового користувача", "MODULE.COMMON.CREATE_NEW_USER": "Створити нового користувача",
"MESSAGE.PLEASE_WAIT": "Будь ласка, зачекайте", "MESSAGE.PLEASE_WAIT": "Будь ласка, зачекайте",
"MESSAGE.PLEASE_WAIT_THIS_MAY_TAKE_FEW_MOMENTS": "Зачекайте хвилинку, це може зайняти деякий час", "MESSAGE.PLEASE_WAIT_THIS_MAY_TAKE_FEW_MOMENTS": "Зачекайте хвилинку, це може зайняти деякий час",
"COMMON.SELECT_FILE_TO_VIEW_ITS_CONTENT": "Виберіть файл, щоб переглянути його вміст", "COMMON.SELECT_FILE_TO_VIEW_ITS_CONTENT": "Виберіть файл, щоб переглянути його вміст",
"COMPONENT.FILE_INPUT.SELECTED_NUM_OF_FILES": "Вибрано {num} файл(и)", "COMPONENT.FILE_INPUT.SELECTED_NUM_OF_FILES": "Вибрано {num} файл(и)",
"COMPONENT.FILE_INPUT.SELECT_FILE_NO_FILE_SELECTED": "Виберіть файл, файл не вибрано", "COMPONENT.FILE_INPUT.SELECT_FILE_NO_FILE_SELECTED": "Виберіть файл, файл не вибрано",
"COMPONENT.FILE_INPUT.OPTION.FROM_CLOUD_STORAGE": "З хмарного сховища", "COMPONENT.FILE_INPUT.OPTION.FROM_CLOUD_STORAGE": "З хмарного сховища",
+20
View File
@@ -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;
+18
View File
@@ -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;
+12
View File
@@ -0,0 +1,12 @@
import { useContext } from 'react';
import TranslationsContext from '#app/i18n/context';
const useLanguage = () => {
const context = useContext(TranslationsContext);
if (!context) throw new Error('useLanguage must be used within I18nProvider');
return context;
};
export default useLanguage;
@@ -1,10 +1,11 @@
'use client'; 'use client';
import { JSX, useCallback } from 'react'; import { JSX, useMemo } from 'react';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { cn } from '@crux-kit/utils'; import { cn } from '@crux-kit/utils';
import { MenuConfig, MenuItem } from '#app/config/types'; import { MenuConfig, MenuItem } from '#app/config/types';
import { useMenuConfig } from '#app/menu'; import { useMenuConfig } from '#app/menu';
import { createMenuPathMatcher } from '#shared/lib/menuPath';
import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuLabel, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu'; import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuLabel, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu';
import { Badge } from '#shared/ui/Badge'; import { Badge } from '#shared/ui/Badge';
@@ -13,9 +14,9 @@ export function SidebarMenu() {
const menuConfig = useMenuConfig(); const menuConfig = useMenuConfig();
// Memoize matchPath to prevent unnecessary re-renders // Memoize matchPath to prevent unnecessary re-renders
const matchPath = useCallback( const matchPath = useMemo(
(path: string): boolean => path === pathname || (path.length > 1 && pathname.startsWith(path)), () => createMenuPathMatcher(menuConfig, pathname),
[pathname] [menuConfig, pathname]
); );
// Global classNames for consistent styling // Global classNames for consistent styling
@@ -1,9 +1,11 @@
import { ChevronDown } from 'lucide-react'; import { ChevronDown } from 'lucide-react';
import { useMemo } from 'react';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { cn } from '@crux-kit/utils'; import { cn } from '@crux-kit/utils';
import { MenuConfig } from '#app/config/types'; import { MenuConfig } from '#app/config/types';
import { useMenuConfig } from '#app/menu'; import { useMenuConfig } from '#app/menu';
import { useMenu } from '#shared/hooks/useMenu'; import { useMenu } from '#shared/hooks/useMenu';
import { createMenuPathMatcher } from '#shared/lib/menuPath';
import { Menubar, MenubarContent, MenubarItem, MenubarMenu, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger } from '#shared/ui/MenuBar'; import { Menubar, MenubarContent, MenubarItem, MenubarMenu, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger } from '#shared/ui/MenuBar';
export function NavbarMenu() { export function NavbarMenu() {
@@ -24,7 +26,12 @@ export function NavbarMenu() {
navbarMenu = menuConfig?.[3]; navbarMenu = menuConfig?.[3];
} }
const { isActive, hasActiveChild } = useMenu(pathname); const { hasActiveChild } = useMenu(pathname);
// A leaf highlights only when nothing deeper in the menu matches the route more closely.
const isActive = useMemo(
() => createMenuPathMatcher((navbarMenu?.children ?? []) as MenuConfig, pathname),
[navbarMenu, pathname]
);
const buildMenu = (items: MenuConfig) => { const buildMenu = (items: MenuConfig) => {
return items.map((item, index) => { return items.map((item, index) => {
@@ -1,21 +1,23 @@
'use client'; 'use client';
import { JSX, useCallback } from 'react'; import { JSX, useMemo } from 'react';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { cn } from '@crux-kit/utils'; import { cn } from '@crux-kit/utils';
import { MenuConfig, MenuItem } from '#app/config/types'; import { MenuConfig, MenuItem } from '#app/config/types';
import { useMenuConfig } from '#app/menu'; import { useMenuConfig } from '#app/menu';
import { useMenu } from '#shared/hooks/useMenu'; import { useMenu } from '#shared/hooks/useMenu';
import { createMenuPathMatcher } from '#shared/lib/menuPath';
import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuLabel, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu'; import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuLabel, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu';
import { Badge } from '#shared/ui/Badge'; import { Badge } from '#shared/ui/Badge';
export function SidebarMenuDefault() { export function SidebarMenuDefault() {
const { pathname } = useLocation(); const { pathname } = useLocation();
const fullMenuConfig = useMenuConfig();
// Memoize matchPath to prevent unnecessary re-renders // Memoize matchPath to prevent unnecessary re-renders
const matchPath = useCallback( const matchPath = useMemo(
(path: string): boolean => path === pathname || (path.length > 1 && pathname.startsWith(path)), () => createMenuPathMatcher(fullMenuConfig, pathname),
[pathname] [fullMenuConfig, pathname]
); );
// Global classNames for consistent styling // Global classNames for consistent styling
@@ -165,8 +167,7 @@ export function SidebarMenuDefault() {
// Memoize the menu to prevent unnecessary re-renders // Memoize the menu to prevent unnecessary re-renders
const { getChildren } = useMenu(pathname); const { getChildren } = useMenu(pathname);
const menuConfig = useMenuConfig(); const menuItems = getChildren(fullMenuConfig, 0);
const menuItems = getChildren(menuConfig, 0);
return ( return (
<AccordionMenu <AccordionMenu
@@ -1,9 +1,10 @@
import { JSX, useCallback } from 'react'; import { JSX, useMemo } from 'react';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { cn } from '@crux-kit/utils'; import { cn } from '@crux-kit/utils';
import { MenuConfig, MenuItem } from '#app/config/types'; import { MenuConfig, MenuItem } from '#app/config/types';
import { useMenuConfig } from '#app/menu'; import { useMenuConfig } from '#app/menu';
import { useMenu } from '#shared/hooks/useMenu'; import { useMenu } from '#shared/hooks/useMenu';
import { createMenuPathMatcher } from '#shared/lib/menuPath';
import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuLabel, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu'; import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuLabel, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu';
import { Badge } from '#shared/ui/Badge'; import { Badge } from '#shared/ui/Badge';
@@ -12,9 +13,9 @@ export function SidebarMenuDefault() {
const menuConfig = useMenuConfig(); const menuConfig = useMenuConfig();
// Memoize matchPath to prevent unnecessary re-renders // Memoize matchPath to prevent unnecessary re-renders
const matchPath = useCallback( const matchPath = useMemo(
(path: string): boolean => path === pathname || (path.length > 1 && pathname.startsWith(path)), () => createMenuPathMatcher(menuConfig, pathname),
[pathname] [menuConfig, pathname]
); );
// Global classNames for consistent styling // Global classNames for consistent styling
@@ -15,7 +15,6 @@ const Header = () => {
const [isSheetOpen, setIsSheetOpen] = useState(false); const [isSheetOpen, setIsSheetOpen] = useState(false);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
// Close sheet when route changes
useEffect(() => { useEffect(() => {
setIsSheetOpen(false); setIsSheetOpen(false);
}, [pathname]); }, [pathname]);
@@ -1,10 +1,11 @@
import { IntlName } from '#infrastructure/IntlName'; import useIntlName from '#app/i18n/useIntlName';
import { useAuth } from '#core/Auth/providers/AuthProvider'; import { useAuth } from '#core/Auth/providers/AuthProvider';
import { Avatar, AvatarFallback, AvatarImage } from '#shared/ui/Avatar'; import { Avatar, AvatarFallback, AvatarImage } from '#shared/ui/Avatar';
import { UserDropdownMenu } from '#shared/widgets/UserDropdownMenu'; import { UserDropdownMenu } from '#shared/widgets/UserDropdownMenu';
export function SidebarFooter() { export function SidebarFooter() {
const { user } = useAuth(); const { user } = useAuth();
const formatName = useIntlName();
return ( return (
<div className="flex-center flex h-14 shrink-0 justify-between ps-4 pe-3.5"> <div className="flex-center flex h-14 shrink-0 justify-between ps-4 pe-3.5">
@@ -13,9 +14,7 @@ export function SidebarFooter() {
trigger={ trigger={
<Avatar className="size-9 cursor-pointer"> <Avatar className="size-9 cursor-pointer">
<AvatarImage src={undefined} alt="User avatar" /> <AvatarImage src={undefined} alt="User avatar" />
<AvatarFallback className="text-sm"> <AvatarFallback className="text-sm">{formatName(user, 'INITIALS_LF')}</AvatarFallback>
{IntlName.formatToDisplay(user, 'INITIALS_LF')}
</AvatarFallback>
</Avatar> </Avatar>
} }
/> />
@@ -1,25 +1,27 @@
'use client'; 'use client';
import { JSX, useCallback } from 'react'; import { Fragment, JSX, useMemo } from 'react';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { cn } from '@crux-kit/utils'; import { cn } from '@crux-kit/utils';
import { MenuConfig, MenuItem } from '#app/config/types'; import { MenuConfig, MenuItem } from '#app/config/types';
import { useMenuConfig } from '#app/menu'; import { useMenuConfig } from '#app/menu';
import { useAuth } from '#core/Auth/providers/AuthProvider'; import { useAuth } from '#core/Auth/providers/AuthProvider';
import { createMenuPathMatcher } from '#shared/lib/menuPath';
import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu'; import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu';
import { getPermittedMenuItems } from '../lib'; import { getPermittedMenuItems } from '../lib';
export function SidebarMenuPrimary() { export function SidebarMenuPrimary() {
const { pathname } = useLocation(); const { pathname } = useLocation();
const { user } = useAuth(); const { user } = useAuth();
const menuConfig = useMenuConfig();
const userPermissions = user?.permissions ?? []; const userPermissions = user?.permissions ?? [];
// Memoize matchPath to prevent unnecessary re-renders
const matchPath = useCallback( const matchPath = useMemo(
(path: string): boolean => path === pathname || (path.length > 1 && pathname.startsWith(path)), () => createMenuPathMatcher(menuConfig, pathname),
[pathname] [menuConfig, pathname]
); );
// Global classNames for consistent styling
const classNames: AccordionMenuClassNames = { const classNames: AccordionMenuClassNames = {
root: 'space-y-2.5 px-3.5', root: 'space-y-2.5 px-3.5',
group: 'gap-px', group: 'gap-px',
@@ -33,8 +35,6 @@ export function SidebarMenuPrimary() {
indicator: '' indicator: ''
}; };
const menuConfig = useMenuConfig();
const buildMenu = (items: MenuConfig): JSX.Element[] => { const buildMenu = (items: MenuConfig): JSX.Element[] => {
const menuItems = getPermittedMenuItems(items, userPermissions); const menuItems = getPermittedMenuItems(items, userPermissions);
@@ -82,7 +82,7 @@ export function SidebarMenuPrimary() {
if (!item.heading && !item.disabled) { if (!item.heading && !item.disabled) {
return buildMenuItemChild(item, index, level); return buildMenuItemChild(item, index, level);
} else { } else {
return <></>; return <Fragment key={index}></Fragment>;
} }
}); });
}; };
@@ -1,7 +1,7 @@
// DEMO 6 // DEMO 6
// https://keenthemes.com/metronic/tailwind/react/demo6 // https://keenthemes.com/metronic/tailwind/react/demo6
import React from 'react'; import { Fragment, useEffect, useMemo } from 'react';
import { Outlet, useLocation } from 'react-router-dom'; import { Outlet, useLocation } from 'react-router-dom';
import { useBodyClass, useIsMobile } from '@crux-kit/hooks'; import { useBodyClass, useIsMobile } from '@crux-kit/hooks';
import { LayoutNames } from '#app/config/types'; import { LayoutNames } from '#app/config/types';
@@ -23,14 +23,15 @@ export const MultipleMenusLayout = () => {
bg-muted! bg-muted!
`); `);
React.useEffect(() => { useEffect(() => {
setOption('layout', LayoutNames.MULTIPLE_MENUS); setOption('layout', LayoutNames.MULTIPLE_MENUS);
}, [setOption]); }, [setOption]);
const toolbar = React.useMemo(() => { const toolbar = useMemo(() => {
if (pathname.includes('/devtools')) return null; if (pathname.includes('/devtools')) return null;
if (pathname.includes('/access-control')) return null; if (pathname.includes('/access-control')) return null;
if (pathname.includes('/file-manager')) return null; if (pathname.includes('/file-manager')) return null;
if (pathname.includes('/web-studio')) return null;
return ( return (
<Toolbar> <Toolbar>
@@ -39,13 +40,13 @@ export const MultipleMenusLayout = () => {
); );
}, [pathname]); }, [pathname]);
const footer = React.useMemo(() => { const footer = useMemo(() => {
if (pathname.includes('/file-manager')) return null; if (pathname.includes('/file-manager')) return null;
return <Footer />; return <Footer />;
}, [pathname]); }, [pathname]);
return ( return (
<React.Fragment> <Fragment>
<div className="flex grow"> <div className="flex grow">
{!isMobile && <Sidebar />} {!isMobile && <Sidebar />}
@@ -63,6 +64,6 @@ export const MultipleMenusLayout = () => {
</div> </div>
</div> </div>
</div> </div>
</React.Fragment> </Fragment>
); );
}; };
@@ -4,12 +4,13 @@ import { cn } from '@crux-kit/utils';
import { MenuConfig } from '#app/config/types'; import { MenuConfig } from '#app/config/types';
import { useMenuConfig } from '#app/menu'; import { useMenuConfig } from '#app/menu';
import { useMenu } from '#shared/hooks/useMenu'; import { useMenu } from '#shared/hooks/useMenu';
import { createMenuPathMatcher } from '#shared/lib/menuPath';
import { Button } from '#shared/ui/Button'; import { Button } from '#shared/ui/Button';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from '#shared/ui/DropdownMenu'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from '#shared/ui/DropdownMenu';
export function SidebarMenu() { export function SidebarMenu() {
const { pathname } = useLocation(); const { pathname } = useLocation();
const { isActive, hasActiveChild } = useMenu(pathname); const { hasActiveChild } = useMenu(pathname);
const config = useMenuConfig(); const config = useMenuConfig();
const menuConfig: MenuConfig = [ const menuConfig: MenuConfig = [
@@ -40,6 +41,9 @@ export function SidebarMenu() {
} }
]; ];
// A leaf highlights only when nothing deeper in the menu matches the route more closely.
const isActive = createMenuPathMatcher(menuConfig, pathname);
const buildMenu = (items: MenuConfig) => { const buildMenu = (items: MenuConfig) => {
return items.map((item, index) => ( return items.map((item, index) => (
<div key={index} className="flex flex-col items-center"> <div key={index} className="flex flex-col items-center">
@@ -1,10 +1,11 @@
'use client'; 'use client';
import { JSX, useCallback } from 'react'; import { JSX, useMemo } from 'react';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { cn } from '@crux-kit/utils'; import { cn } from '@crux-kit/utils';
import { MenuConfig, MenuItem } from '#app/config/types'; import { MenuConfig, MenuItem } from '#app/config/types';
import { useMenuConfig } from '#app/menu'; import { useMenuConfig } from '#app/menu';
import { createMenuPathMatcher } from '#shared/lib/menuPath';
import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuLabel, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu'; import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuLabel, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu';
export function SidebarMenuPrimary() { export function SidebarMenuPrimary() {
@@ -12,9 +13,9 @@ export function SidebarMenuPrimary() {
const menuConfig = useMenuConfig(); const menuConfig = useMenuConfig();
// Memoize matchPath to prevent unnecessary re-renders // Memoize matchPath to prevent unnecessary re-renders
const matchPath = useCallback( const matchPath = useMemo(
(path: string): boolean => path === pathname || (path.length > 1 && pathname.startsWith(path)), () => createMenuPathMatcher(menuConfig, pathname),
[pathname] [menuConfig, pathname]
); );
// Global classNames for consistent styling // Global classNames for consistent styling
+2 -2
View File
@@ -1,4 +1,4 @@
import React from 'react'; import { useMemo } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { type MenuConfig } from './config/types'; import { type MenuConfig } from './config/types';
import { createModuleMenuConfig } from './services/modules'; import { createModuleMenuConfig } from './services/modules';
@@ -6,7 +6,7 @@ import { createModuleMenuConfig } from './services/modules';
export const useMenuConfig = (): MenuConfig => { export const useMenuConfig = (): MenuConfig => {
const intl = useIntl(); const intl = useIntl();
return React.useMemo(() => { return useMemo(() => {
return createModuleMenuConfig(intl); return createModuleMenuConfig(intl);
}, [intl]); }, [intl]);
}; };
@@ -1,4 +1,4 @@
import { type ReactNode } from 'react'; import type { ReactNode } from 'react';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import { BASE_URL } from '#app/config/app'; import { BASE_URL } from '#app/config/app';
import { AuthProvider } from '#core/Auth/providers/AuthProvider'; import { AuthProvider } from '#core/Auth/providers/AuthProvider';
+20 -25
View File
@@ -1,51 +1,48 @@
import { DirectionProvider as RadixDirectionProvider } from '@radix-ui/react-direction'; import { DirectionProvider } from '@radix-ui/react-direction';
import { createContext, useContext, useEffect, useState, type PropsWithChildren } from 'react'; import { useEffect, useState, type PropsWithChildren } from 'react';
import { IntlProvider } from 'react-intl'; import { IntlProvider } from 'react-intl';
import { LocalStorageService } from '@crux-kit/local-storage'; import { LocalStorageService } from '@crux-kit/local-storage';
import { I18N_DEFAULT_LANGUAGE, I18N_LANGUAGES } from '#app/i18n/config'; import { I18N_DEFAULT_LANGUAGE, I18N_LANGUAGES } from '#app/i18n/config';
import { I18nProviderProps, type Language } from '#app/i18n/types'; import TranslationsContext from '#app/i18n/context';
import { type Language, type LanguageCode } from '#app/i18n/types';
import { IntlDate } from '#infrastructure/IntlDate'; import { IntlDate } from '#infrastructure/IntlDate';
import { IntlName } from '#infrastructure/IntlName';
import '@formatjs/intl-relativetimeformat/polyfill'; import '@formatjs/intl-relativetimeformat/polyfill';
import '@formatjs/intl-relativetimeformat/locale-data/en'; import '@formatjs/intl-relativetimeformat/locale-data/en';
import '@formatjs/intl-relativetimeformat/locale-data/de';
import '@formatjs/intl-relativetimeformat/locale-data/es';
import '@formatjs/intl-relativetimeformat/locale-data/fr';
import '@formatjs/intl-relativetimeformat/locale-data/ja';
import '@formatjs/intl-relativetimeformat/locale-data/zh';
const storage = new LocalStorageService({ prefix: 'i18n' }); const storage = new LocalStorageService({ prefix: 'i18n' });
const resolveLanguage = (code?: LanguageCode): Language => {
return I18N_LANGUAGES.find((lang) => lang.code === code) ?? I18N_DEFAULT_LANGUAGE;
};
const getInitialLanguage = () => { const getInitialLanguage = () => {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const langParam = urlParams.get('lang'); const langParam = urlParams.get('lang');
// Check if langParam matches a supported language in I18N_LANGUAGES
if (langParam) { if (langParam) {
const matchedLanguage = I18N_LANGUAGES.find((lang) => lang.code === langParam); const matchedLanguage = I18N_LANGUAGES.find((lang) => lang.code === langParam);
if (matchedLanguage) { if (matchedLanguage) {
storage.set('config', matchedLanguage); storage.set('config', matchedLanguage.code);
return matchedLanguage; return matchedLanguage;
} }
} }
return storage.get<Language>('config') ?? I18N_DEFAULT_LANGUAGE; return resolveLanguage(storage.get<LanguageCode>('config'));
}; };
const initialProps: I18nProviderProps = { const initialLanguage = getInitialLanguage();
languages: I18N_LANGUAGES,
currenLanguage: getInitialLanguage(),
changeLanguage: (_: Language) => {},
isRTL: () => false
};
const TranslationsContext = createContext<I18nProviderProps>(initialProps); IntlDate.setLocaleGlobally(initialLanguage.code);
const useLanguage = () => useContext(TranslationsContext); IntlName.setLanguage(initialLanguage.code);
const I18nProvider = ({ children }: PropsWithChildren) => { const I18nProvider = ({ children }: PropsWithChildren) => {
const [currenLanguage, setCurrenLanguage] = useState(initialProps.currenLanguage); const [currenLanguage, setCurrenLanguage] = useState(initialLanguage);
const changeLanguage = (language: Language) => { const changeLanguage = (language: Language) => {
storage.set('config', language); storage.set('config', language.code);
IntlDate.setLocaleGlobally(language.code);
IntlName.setLanguage(language.code);
setCurrenLanguage(language); setCurrenLanguage(language);
}; };
@@ -54,7 +51,6 @@ const I18nProvider = ({ children }: PropsWithChildren) => {
}; };
useEffect(() => { useEffect(() => {
IntlDate.setLocaleGlobally(currenLanguage.code); // TODO: currently changes only after page reload!
document.documentElement.setAttribute('dir', currenLanguage.direction); document.documentElement.setAttribute('dir', currenLanguage.direction);
}, [currenLanguage]); }, [currenLanguage]);
@@ -70,13 +66,12 @@ const I18nProvider = ({ children }: PropsWithChildren) => {
<IntlProvider <IntlProvider
messages={currenLanguage.messages} messages={currenLanguage.messages}
locale={currenLanguage.code} locale={currenLanguage.code}
defaultLocale={getInitialLanguage().code} defaultLocale={I18N_DEFAULT_LANGUAGE.code}
> >
<RadixDirectionProvider dir={currenLanguage.direction}>{children}</RadixDirectionProvider> <DirectionProvider dir={currenLanguage.direction}>{children}</DirectionProvider>
</IntlProvider> </IntlProvider>
</TranslationsContext.Provider> </TranslationsContext.Provider>
); );
}; };
export { useLanguage };
export default I18nProvider; export default I18nProvider;
@@ -1,5 +1,5 @@
import { MutationCache, QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { MutationCache, QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactNode, useState } from 'react'; import { useState, type ReactNode } from 'react';
import { handleQueryError } from '#infrastructure/reactQuery'; import { handleQueryError } from '#infrastructure/reactQuery';
export const queryClient = new QueryClient({ export const queryClient = new QueryClient({
@@ -1,7 +1,7 @@
'use client'; 'use client';
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { LocalStorageService } from '@crux-kit/local-storage'; import { LocalStorageService } from '@crux-kit/local-storage';
import { APP_SETTINGS } from '#app/config/settings'; import { APP_SETTINGS } from '#app/config/settings';
import { Settings } from '#app/config/types'; import { Settings } from '#app/config/types';
@@ -1,11 +1,9 @@
'use client'; 'use client';
import { ThemeProvider as NextThemesProvider } from 'next-themes'; import { ThemeProvider as NextThemesProvider } from 'next-themes';
import * as React from 'react'; import { type ComponentProps } from 'react';
export default function ThemeProvider({ export default function ThemeProvider({ children }: ComponentProps<typeof NextThemesProvider>) {
children
}: React.ComponentProps<typeof NextThemesProvider>) {
return ( return (
<NextThemesProvider <NextThemesProvider
attribute="class" attribute="class"
@@ -1,4 +1,4 @@
import { Navigate, RouteObject, useRoutes } from 'react-router'; import { Navigate, useRoutes, type RouteObject } from 'react-router';
import { MultipleMenusLayout } from '#app/layouts/06__MultipleMenus/layout'; import { MultipleMenusLayout } from '#app/layouts/06__MultipleMenus/layout';
import { getModuleRoutes } from '#app/services/modules'; import { getModuleRoutes } from '#app/services/modules';
import { AuthGuard } from '#core/Auth'; import { AuthGuard } from '#core/Auth';
+12 -5
View File
@@ -1,5 +1,12 @@
export * from './menus'; export { createModuleMenuConfig } from './menus';
export * from './messages'; export { collectModuleMessages } from './messages';
export * from './registry'; export { appModules } from './registry';
export * from './routes'; export { getModuleRoutes } from './routes';
export * from './types';
export type { AppMessages } from './messages';
export type {
AppModuleArea,
AppModuleConfig,
AppModuleMessages,
AppModuleRouteScope
} from './types';
+1 -1
View File
@@ -1,4 +1,4 @@
import { IntlShape } from 'react-intl'; import { type IntlShape } from 'react-intl';
import { MenuConfig } from '#app/config/types'; import { MenuConfig } from '#app/config/types';
import { appModules } from './registry'; import { appModules } from './registry';
@@ -1,4 +1,4 @@
import { LanguageCode } from '#app/i18n/types'; import { type LanguageCode } from '#app/i18n/types';
import { appModules } from './registry'; import { appModules } from './registry';
export type AppMessages = Record<LanguageCode, Record<string, string>>; export type AppMessages = Record<LanguageCode, Record<string, string>>;
@@ -1,4 +1,4 @@
import { AppModuleConfig } from './types'; import { type AppModuleConfig } from './types';
const coreModules = import.meta.glob('#core/**/config/module.ts', { const coreModules = import.meta.glob('#core/**/config/module.ts', {
eager: true, eager: true,
@@ -1,5 +1,5 @@
import { appModules } from './registry'; import { appModules } from './registry';
import { AppModuleRouteScope } from './types'; import { type AppModuleRouteScope } from './types';
export const getModuleRoutes = (scope: AppModuleRouteScope) => { export const getModuleRoutes = (scope: AppModuleRouteScope) => {
return appModules return appModules
@@ -1,5 +1,5 @@
import { Users } from 'lucide-react'; import { Users } from 'lucide-react';
import { IntlShape } from 'react-intl'; import { type IntlShape } from 'react-intl';
import { MenuItem } from '#app/config/types'; import { MenuItem } from '#app/config/types';
export const createAccessControlMenu = (intl: IntlShape): MenuItem => ({ export const createAccessControlMenu = (intl: IntlShape): MenuItem => ({
@@ -1,4 +1,4 @@
import { AppModuleConfig } from '#app/services/modules/types'; import { type AppModuleConfig } from '#app/services/modules/types';
import { createAccessControlMenu } from './menu'; import { createAccessControlMenu } from './menu';
import { accessControlMessages } from './messages'; import { accessControlMessages } from './messages';
import { accessControlRoute } from './routing'; import { accessControlRoute } from './routing';
@@ -1,4 +1,4 @@
import { Navigate, RouteObject } from 'react-router-dom'; import { Navigate, type RouteObject } from 'react-router-dom';
import AdminListPage from '../pages/AdminListPage'; import AdminListPage from '../pages/AdminListPage';
import RoleListPage from '../pages/RoleListPage'; import RoleListPage from '../pages/RoleListPage';
import AdminManagementLayout from '../view/layouts/AdminManagementLayout'; import AdminManagementLayout from '../view/layouts/AdminManagementLayout';
@@ -1,35 +1,31 @@
import { axiosClient } from '#infrastructure/axios'; import { axiosClient } from '#infrastructure/axios';
import { ListQueryParams, SuccessListResponse, SuccessMutationResponse } from '#infrastructure/reactQuery'; import type { ListQueryParams, SuccessListResponse, SuccessMutationResponse } from '#infrastructure/reactQuery';
import { AdminDTO } from './dto'; import type { AdminDTO } from './AccessControlDTO';
const ROOT_API_ENDPOINT = '/admin';
export default class AdminAPI { export default class AdminAPI {
getList = async (params: ListQueryParams) => { getList = async (params: ListQueryParams) => {
return axiosClient return axiosClient
.get<SuccessListResponse<AdminDTO>>(ROOT_API_ENDPOINT, { params }) .get<SuccessListResponse<AdminDTO>>('/admin', { params })
.then((data) => data.data); .then((data) => data.data);
}; };
getDetailed = async (id: string) => { 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>) => { create = async (payload: Record<string, unknown>) => {
return axiosClient return axiosClient.post<SuccessMutationResponse>('/admin', payload).then((data) => data.data);
.post<SuccessMutationResponse>(ROOT_API_ENDPOINT, payload)
.then((data) => data.data);
}; };
update = async (payload: { id: number } & Record<string, unknown>) => { update = async (payload: { id: number } & Record<string, unknown>) => {
return axiosClient return axiosClient
.put<SuccessMutationResponse>(`${ROOT_API_ENDPOINT}/${payload.id}`, payload) .put<SuccessMutationResponse>(`/admin/${payload.id}`, payload)
.then((data) => data.data); .then((data) => data.data);
}; };
delete = async (payload: { id: number }) => { delete = async (payload: { id: number }) => {
return axiosClient return axiosClient
.delete<SuccessMutationResponse>(`${ROOT_API_ENDPOINT}/${payload.id}`) .delete<SuccessMutationResponse>(`/admin/${payload.id}`)
.then((data) => data.data); .then((data) => data.data);
}; };
} }
@@ -1,10 +1,8 @@
import { axiosClient } from '#infrastructure/axios'; import { axiosClient } from '#infrastructure/axios';
import { PermissionDTO } from './dto'; import type { PermissionDTO } from './AccessControlDTO';
const ROOT_API_ENDPOINT = '/permission';
export default class PermissionAPI { export default class PermissionAPI {
getList = async () => { 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 { axiosClient } from '#infrastructure/axios';
import { ListQueryParams, SuccessListResponse, SuccessMutationResponse } from '#infrastructure/reactQuery'; import type { ListQueryParams, SuccessListResponse, SuccessMutationResponse } from '#infrastructure/reactQuery';
import { RoleDTO } from './dto'; import type { RoleDTO } from './AccessControlDTO';
const ROOT_API_ENDPOINT = '/role';
export default class RoleAPI { export default class RoleAPI {
getList = async (params: ListQueryParams) => { getList = async (params: ListQueryParams) => {
return axiosClient return axiosClient
.get<SuccessListResponse<RoleDTO>>(ROOT_API_ENDPOINT, { params }) .get<SuccessListResponse<RoleDTO>>('/role', { params })
.then((data) => data.data); .then((data) => data.data);
}; };
getDetailed = async (id: string) => { 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[] }) => { create = async (payload: { name: string; permissions: string[] }) => {
return axiosClient return axiosClient.post<SuccessMutationResponse>('/role', payload).then((data) => data.data);
.post<SuccessMutationResponse>(ROOT_API_ENDPOINT, payload)
.then((data) => data.data);
}; };
update = async (payload: { id: number } & Record<string, unknown>) => { update = async (payload: { id: number } & Record<string, unknown>) => {
return axiosClient return axiosClient
.put<SuccessMutationResponse>(`${ROOT_API_ENDPOINT}/${payload.id}`, payload) .put<SuccessMutationResponse>(`/role/${payload.id}`, payload)
.then((data) => data.data); .then((data) => data.data);
}; };
delete = async (payload: { id: number }) => { delete = async (payload: { id: number }) => {
return axiosClient return axiosClient
.delete<SuccessMutationResponse>(`${ROOT_API_ENDPOINT}/${payload.id}`) .delete<SuccessMutationResponse>(`/role/${payload.id}`)
.then((data) => data.data); .then((data) => data.data);
}; };
} }
@@ -1,6 +1,6 @@
import { AdminEntity, AdminStatusEnum } from '../../entities/AdminEntity'; import type { AdminDTO } from '#core/AccessControl/data/api/AccessControlDTO';
import { AdminDTO } from '../api/dto'; import RoleMapper from '#core/AccessControl/data/mappers/RoleMapper';
import RoleMapper from './RoleMapper'; import type { AdminEntity, AdminStatusEnum } from '#core/AccessControl/entities/AdminEntity';
export default class AdminMapper { export default class AdminMapper {
private readonly roleMapper = new RoleMapper(); private readonly roleMapper = new RoleMapper();
@@ -1,5 +1,5 @@
import { PermissionEntity } from '../../entities/PermissionEntity'; import type { PermissionDTO } from '#core/AccessControl/data/api/AccessControlDTO';
import { PermissionDTO } from '../api/dto'; import type { PermissionEntity } from '#core/AccessControl/entities/PermissionEntity';
export default class PermissionMapper { export default class PermissionMapper {
public toEntity = (dto: PermissionDTO): PermissionEntity => { public toEntity = (dto: PermissionDTO): PermissionEntity => {
@@ -1,6 +1,6 @@
import { RoleEntity } from '../../entities/RoleEntity'; import type { RoleDTO } from '#core/AccessControl/data/api/AccessControlDTO';
import { RoleDTO } from '../api/dto'; import PermissionMapper from '#core/AccessControl/data/mappers/PermissionMapper';
import PermissionMapper from './PermissionMapper'; import type { RoleEntity } from '#core/AccessControl/entities/RoleEntity';
export default class RoleMapper { export default class RoleMapper {
private readonly permissionMapper = new PermissionMapper(); private readonly permissionMapper = new PermissionMapper();
@@ -1,10 +1,10 @@
import { keepPreviousData, useMutation, useQuery } from '@tanstack/react-query'; import { keepPreviousData, useMutation, useQuery } from '@tanstack/react-query';
import React from 'react'; import { useMemo } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { ErrorResponse, getPaginationInfo, ListQueryParams, SuccessMutationResponse } from '#infrastructure/reactQuery'; import { getPaginationInfo, type ErrorResponse, type ListQueryParams, type SuccessMutationResponse } from '#infrastructure/reactQuery';
import AdminAPI from '../api/AdminAPI'; import AdminAPI from '#core/AccessControl/data/api/AdminAPI';
import AdminMapper from '../mappers/AdminMapper'; import AdminMapper from '#core/AccessControl/data/mappers/AdminMapper';
import { adminDetailedKeyBuilder, adminListKeyBuilder, DetailedPayload } from './keys'; import { adminDetailedKeyBuilder, adminListKeyBuilder, type DetailedPayload } from './keys';
const api = new AdminAPI(); const api = new AdminAPI();
const mapper = new AdminMapper(); const mapper = new AdminMapper();
@@ -19,7 +19,7 @@ export const useGetList = (params: ListQueryParams = { page: 1, perPage: 10 }) =
} }
}); });
const list = React.useMemo(() => { const list = useMemo(() => {
return mapper.toEntities(data?.data ?? []); return mapper.toEntities(data?.data ?? []);
}, [data?.data]); }, [data?.data]);
@@ -1,4 +1,4 @@
import { createKeyBuilder, ListQueryParams } from '#infrastructure/reactQuery'; import { createKeyBuilder, type ListQueryParams } from '#infrastructure/reactQuery';
export interface DetailedPayload { export interface DetailedPayload {
id: string; id: string;
@@ -1,6 +1,6 @@
import { keepPreviousData, useQuery } from '@tanstack/react-query'; import { keepPreviousData, useQuery } from '@tanstack/react-query';
import PermissionAPI from '../api/PermissionAPI'; import PermissionAPI from '#core/AccessControl/data/api/PermissionAPI';
import PermissionMapper from '../mappers/PermissionMapper'; import PermissionMapper from '#core/AccessControl/data/mappers/PermissionMapper';
import { permissionListKeyBuilder } from './keys'; import { permissionListKeyBuilder } from './keys';
const api = new PermissionAPI(); const api = new PermissionAPI();
@@ -1,10 +1,10 @@
import { keepPreviousData, useMutation, useQuery } from '@tanstack/react-query'; import { keepPreviousData, useMutation, useQuery } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { ErrorResponse, getPaginationInfo, ListQueryParams, SuccessMutationResponse } from '#infrastructure/reactQuery'; import { getPaginationInfo, type ErrorResponse, type ListQueryParams, type SuccessMutationResponse } from '#infrastructure/reactQuery';
import RoleAPI from '../api/RoleAPI'; import RoleAPI from '#core/AccessControl/data/api/RoleAPI';
import RoleMapper from '../mappers/RoleMapper'; import RoleMapper from '#core/AccessControl/data/mappers/RoleMapper';
import { DetailedPayload, roleDetailedKeyBuilder, roleListKeyBuilder } from './keys'; import { roleDetailedKeyBuilder, roleListKeyBuilder, type DetailedPayload } from './keys';
const api = new RoleAPI(); const api = new RoleAPI();
const mapper = new RoleMapper(); const mapper = new RoleMapper();
@@ -1,5 +1,5 @@
import type { IsoDateString } from '@crux-kit/types'; import type { IsoDateString } from '@crux-kit/types';
import { RoleEntity } from './RoleEntity'; import type { RoleEntity } from './RoleEntity';
export enum AdminStatusEnum { export enum AdminStatusEnum {
ACTIVE = 'active', ACTIVE = 'active',
@@ -1,5 +1,5 @@
import type { IsoDateString } from '@crux-kit/types'; import type { IsoDateString } from '@crux-kit/types';
import { PermissionEntity } from './PermissionEntity'; import type { PermissionEntity } from './PermissionEntity';
export interface RoleEntity { export interface RoleEntity {
id: number; id: number;
@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { AdminTable } from '#core/AccessControl/view/widgets/AdminTable';
import { PermissionPageGate } from '#core/Permission'; import { PermissionPageGate } from '#core/Permission';
import { Container } from '#shared/widgets/common/Container'; import { Container } from '#shared/widgets/common/Container';
import { AdminTable } from '../../view/widgets/AdminTable';
const AdminListPage: React.FC = () => { const AdminListPage: React.FC = () => {
return ( return (
@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { RoleTable } from '#core/AccessControl/view/widgets/RoleTable';
import { PermissionPageGate } from '#core/Permission'; import { PermissionPageGate } from '#core/Permission';
import { Container } from '#shared/widgets/common/Container'; import { Container } from '#shared/widgets/common/Container';
import { RoleTable } from '../../view/widgets/RoleTable';
const RoleListPage: React.FC = () => { const RoleListPage: React.FC = () => {
return ( return (
@@ -9,7 +9,7 @@ import { FormElement, FormGrid, GridElement, InputCheckboxGroup, InputSelect, In
import { Button } from '#shared/ui/Button'; import { Button } from '#shared/ui/Button';
import { Form as FormProvider } from '#shared/ui/form'; import { Form as FormProvider } from '#shared/ui/form';
import { Separator } from '#shared/ui/Separator'; import { Separator } from '#shared/ui/Separator';
import { ADMIN_FORM_ID, createValidationSchema, FormValues, SubmitFunc } from './form'; import { ADMIN_FORM_ID, createValidationSchema, type FormValues, type SubmitFunc } from './form';
export const AdminForm: React.FC<{ export const AdminForm: React.FC<{
roles: Array<{ label: string; value: string }>; roles: Array<{ label: string; value: string }>;
@@ -1,8 +1,8 @@
import { UseFormReturn } from 'react-hook-form'; import { type UseFormReturn } from 'react-hook-form';
import { IntlShape } from 'react-intl'; import { type IntlShape } from 'react-intl';
import z from 'zod'; import z from 'zod';
import { uid } from '@crux-kit/utils'; import { uid } from '@crux-kit/utils';
import { AdminEntity, AdminStatusEnum } from '#core/AccessControl/entities/AdminEntity'; import { AdminStatusEnum, type AdminEntity } from '#core/AccessControl/entities/AdminEntity';
export const ADMIN_FORM_ID = uid(); export const ADMIN_FORM_ID = uid();
@@ -2,11 +2,12 @@ import { extractValidationErrors } from '#infrastructure/reactQuery';
import { useCreate, useUpdate } from '#core/AccessControl/data/queries/admin'; import { useCreate, useUpdate } from '#core/AccessControl/data/queries/admin';
import { invalidateAdminDetailed, invalidateAdminList } from '#core/AccessControl/data/queries/cache'; import { invalidateAdminDetailed, invalidateAdminList } from '#core/AccessControl/data/queries/cache';
import { useAuth } from '#core/Auth/providers/AuthProvider'; import { useAuth } from '#core/Auth/providers/AuthProvider';
import { SubmitFunc } from './form'; import { type SubmitFunc } from './form';
interface Params { interface Params {
onSuccess?: (id?: number) => void; onSuccess?: (id?: number) => void;
} }
export const useSubmit = (params?: Params) => { export const useSubmit = (params?: Params) => {
const onSuccess = params?.onSuccess; const onSuccess = params?.onSuccess;
const { verify } = useAuth(); const { verify } = useAuth();
@@ -5,7 +5,7 @@ import { useIntl } from 'react-intl';
import { FormElement, FormGrid, GridElement, InputText } from '#shared/forms/composed'; import { FormElement, FormGrid, GridElement, InputText } from '#shared/forms/composed';
import { FormControl, FormField, FormItem, FormLabel, FormMessage, Form as FormProvider } from '#shared/ui/form'; import { FormControl, FormField, FormItem, FormLabel, FormMessage, Form as FormProvider } from '#shared/ui/form';
import { Switch, SwitchWrapper } from '#shared/ui/Switch'; import { Switch, SwitchWrapper } from '#shared/ui/Switch';
import { createValidationSchema, FormValues, ROLE_FORM_ID, SubmitFunc } from './form'; import { createValidationSchema, ROLE_FORM_ID, type FormValues, type SubmitFunc } from './form';
export const RoleForm: React.FC<{ export const RoleForm: React.FC<{
permissions: Record<string, Array<{ label: string; value: string }>>; permissions: Record<string, Array<{ label: string; value: string }>>;
@@ -2,7 +2,7 @@ import { UseFormReturn } from 'react-hook-form';
import { IntlShape } from 'react-intl'; import { IntlShape } from 'react-intl';
import z from 'zod'; import z from 'zod';
import { uid } from '@crux-kit/utils'; import { uid } from '@crux-kit/utils';
import { RoleEntity } from '#core/AccessControl/entities/RoleEntity'; import { type RoleEntity } from '#core/AccessControl/entities/RoleEntity';
export const ROLE_FORM_ID = uid(); export const ROLE_FORM_ID = uid();
@@ -1,10 +1,10 @@
import { ErrorOption, UseFormReturn } from 'react-hook-form'; import type { ErrorOption, UseFormReturn } from 'react-hook-form';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { ErrorResponse, extractValidationErrors } from '#infrastructure/reactQuery'; import { extractValidationErrors, type ErrorResponse } from '#infrastructure/reactQuery';
import { invalidateRoleDetailed, invalidateRoleList } from '#core/AccessControl/data/queries/cache'; import { invalidateRoleDetailed, invalidateRoleList } from '#core/AccessControl/data/queries/cache';
import { useCreate, useUpdate } from '#core/AccessControl/data/queries/role'; import { useCreate, useUpdate } from '#core/AccessControl/data/queries/role';
import { useAuth } from '#core/Auth/providers/AuthProvider'; import { useAuth } from '#core/Auth/providers/AuthProvider';
import { FormValues, SubmitFunc } from './form'; import type { FormValues, SubmitFunc } from './form';
interface Params { interface Params {
onSuccess?: (id?: number) => void; onSuccess?: (id?: number) => void;
@@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { Outlet, useLocation } from 'react-router'; import { Outlet, useLocation } from 'react-router';
import { createAccessControlMenu } from '#core/AccessControl/config/menu';
import AccessControlLogo from '#core/AccessControl/view/ui/AccessControlLogo';
import { useMenu } from '#shared/hooks/useMenu'; import { useMenu } from '#shared/hooks/useMenu';
import { Container } from '#shared/widgets/common/Container'; import { Container } from '#shared/widgets/common/Container';
import { createAccessControlMenu } from '../../config/menu';
import AccessControlLogo from '../ui/AccessControlLogo';
const AdminManagementLayout: React.FC = () => { const AdminManagementLayout: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
@@ -1,9 +1,10 @@
import { Column, ColumnDef, ColumnFiltersState, getCoreRowModel, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, PaginationState, SortingState, useReactTable } from '@tanstack/react-table'; import { getCoreRowModel, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table';
import type { Column, ColumnDef, ColumnFiltersState, PaginationState, SortingState } from '@tanstack/react-table';
import { LoaderCircleIcon, SearchIcon, Settings2, SquarePen } from 'lucide-react'; import { LoaderCircleIcon, SearchIcon, Settings2, SquarePen } from 'lucide-react';
import React from 'react'; import React from 'react';
import { IntlShape, useIntl } from 'react-intl'; import { useIntl, type IntlShape } from 'react-intl';
import { useDebounceSearch } from '@crux-kit/hooks'; import { useDebounceSearch } from '@crux-kit/hooks';
import { IntlName } from '#infrastructure/IntlName'; import useIntlName from '#app/i18n/useIntlName';
import { mapTableStateToListQueryParams } from '#infrastructure/reactQuery'; import { mapTableStateToListQueryParams } from '#infrastructure/reactQuery';
import { PermissionGate } from '#core/Permission'; import { PermissionGate } from '#core/Permission';
import { Badge } from '#shared/ui/Badge'; import { Badge } from '#shared/ui/Badge';
@@ -15,7 +16,7 @@ import { ScrollArea, ScrollBar } from '#shared/ui/ScrollArea';
import { Skeleton } from '#shared/ui/Skeleton'; import { Skeleton } from '#shared/ui/Skeleton';
import { DataGrid, DataGridColumnFilter, DataGridColumnHeader, DataGridColumnVisibility, DataGridPagination, DataGridTable, DataGridToolbarFilters, useDataGrid } from '#shared/widgets/DataGrid'; import { DataGrid, DataGridColumnFilter, DataGridColumnHeader, DataGridColumnVisibility, DataGridPagination, DataGridTable, DataGridToolbarFilters, useDataGrid } from '#shared/widgets/DataGrid';
import { useGetList } from '../../data/queries/admin'; import { useGetList } from '../../data/queries/admin';
import { AdminEntity, AdminStatusEnum } from '../../entities/AdminEntity'; import { AdminStatusEnum, type AdminEntity } from '../../entities/AdminEntity';
import { DeleteAdminTrigger } from './DeleteAdminTrigger'; import { DeleteAdminTrigger } from './DeleteAdminTrigger';
import { ManageAdminTrigger } from './ManageAdminTrigger'; import { ManageAdminTrigger } from './ManageAdminTrigger';
@@ -36,6 +37,7 @@ const getStatusOptions = (intl: IntlShape) => [
export const AdminTable = () => { export const AdminTable = () => {
const intl = useIntl(); const intl = useIntl();
const formatName = useIntlName();
const [pagination, setPagination] = React.useState<PaginationState>({ const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
@@ -180,7 +182,7 @@ export const AdminTable = () => {
cell: (info) => { cell: (info) => {
const data = info.row.original; const data = info.row.original;
const id = data.id; const id = data.id;
const name = IntlName.formatToDisplay(data, 'SURNAME_NAME'); const name = formatName(data, 'SURNAME_NAME');
const editModalTitle = const editModalTitle =
intl.formatMessage({ id: 'ACCESS_CONTROL.ADMIN.EDIT' }) + ' ' + name; intl.formatMessage({ id: 'ACCESS_CONTROL.ADMIN.EDIT' }) + ' ' + name;
@@ -207,7 +209,7 @@ export const AdminTable = () => {
} }
} }
], ],
[intl, statusOptions] [intl, statusOptions, formatName]
); );
const table = useReactTable({ const table = useReactTable({
@@ -2,12 +2,12 @@ import { LoaderCircleIcon } from 'lucide-react';
import React from 'react'; import React from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useDisclosure } from '@crux-kit/hooks'; import { useDisclosure } from '@crux-kit/hooks';
import { useGetDetailed } from '#core/AccessControl/data/queries/admin';
import { useGetList } from '#core/AccessControl/data/queries/role';
import { ADMIN_FORM_ID, AdminForm, initialValues, prepareInitialValues, useSubmit } from '#core/AccessControl/view/forms/AdminForm';
import { Button } from '#shared/ui/Button'; import { Button } from '#shared/ui/Button';
import { Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '#shared/ui/Dialog'; import { Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '#shared/ui/Dialog';
import { Skeleton } from '#shared/ui/Skeleton'; import { Skeleton } from '#shared/ui/Skeleton';
import { useGetDetailed } from '../../data/queries/admin';
import { useGetList } from '../../data/queries/role';
import { ADMIN_FORM_ID, AdminForm, initialValues, prepareInitialValues, useSubmit } from '../forms/AdminForm';
export const ManageAdminTrigger: React.FC< export const ManageAdminTrigger: React.FC<
React.PropsWithChildren<{ React.PropsWithChildren<{
@@ -2,12 +2,12 @@ import { LoaderCircleIcon } from 'lucide-react';
import React from 'react'; import React from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useDisclosure } from '@crux-kit/hooks'; import { useDisclosure } from '@crux-kit/hooks';
import { useGetList } from '#core/AccessControl/data/queries/permission';
import { useGetDetailed } from '#core/AccessControl/data/queries/role';
import { initialValues, prepareInitialValues, ROLE_FORM_ID, RoleForm, useSubmit } from '#core/AccessControl/view/forms/RoleForm';
import { Button } from '#shared/ui/Button'; import { Button } from '#shared/ui/Button';
import { Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '#shared/ui/Dialog'; import { Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '#shared/ui/Dialog';
import { Skeleton } from '#shared/ui/Skeleton'; import { Skeleton } from '#shared/ui/Skeleton';
import { useGetList } from '../../data/queries/permission';
import { useGetDetailed } from '../../data/queries/role';
import { initialValues, prepareInitialValues, ROLE_FORM_ID, RoleForm, useSubmit } from '../forms/RoleForm';
export const ManageRoleTrigger: React.FC< export const ManageRoleTrigger: React.FC<
React.PropsWithChildren<{ React.PropsWithChildren<{
@@ -1,9 +1,14 @@
import { Column, ColumnDef, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, PaginationState, SortingState, useReactTable } from '@tanstack/react-table'; import { getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table';
import type { Column, ColumnDef, PaginationState, SortingState } from '@tanstack/react-table';
import { LoaderCircleIcon, SearchIcon, Settings2, SquarePen } from 'lucide-react'; import { LoaderCircleIcon, SearchIcon, Settings2, SquarePen } from 'lucide-react';
import React from 'react'; import React from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useDebounceSearch } from '@crux-kit/hooks'; import { useDebounceSearch } from '@crux-kit/hooks';
import { mapTableStateToListQueryParams } from '#infrastructure/reactQuery'; import { mapTableStateToListQueryParams } from '#infrastructure/reactQuery';
import { useGetList } from '#core/AccessControl/data/queries/role';
import { RoleEntity } from '#core/AccessControl/entities/RoleEntity';
import { DeleteRoleTrigger } from '#core/AccessControl/view/widgets/DeleteRoleTrigger';
import { ManageRoleTrigger } from '#core/AccessControl/view/widgets/ManageRoleTrigger';
import { PermissionGate } from '#core/Permission'; import { PermissionGate } from '#core/Permission';
import { Button } from '#shared/ui/Button'; import { Button } from '#shared/ui/Button';
import { Card, CardFooter, CardHeader, CardTable, CardTitle, CardToolbar } from '#shared/ui/Card'; import { Card, CardFooter, CardHeader, CardTable, CardTitle, CardToolbar } from '#shared/ui/Card';
@@ -12,10 +17,6 @@ import { Input, InputWrapper } from '#shared/ui/Input';
import { ScrollArea, ScrollBar } from '#shared/ui/ScrollArea'; import { ScrollArea, ScrollBar } from '#shared/ui/ScrollArea';
import { Skeleton } from '#shared/ui/Skeleton'; import { Skeleton } from '#shared/ui/Skeleton';
import { DataGrid, DataGridColumnHeader, DataGridColumnVisibility, DataGridPagination, DataGridTable, useDataGrid } from '#shared/widgets/DataGrid'; import { DataGrid, DataGridColumnHeader, DataGridColumnVisibility, DataGridPagination, DataGridTable, useDataGrid } from '#shared/widgets/DataGrid';
import { useGetList } from '../../data/queries/role';
import { RoleEntity } from '../../entities/RoleEntity';
import { DeleteRoleTrigger } from './DeleteRoleTrigger';
import { ManageRoleTrigger } from './ManageRoleTrigger';
interface IColumnFilterProps<TData, TValue> { interface IColumnFilterProps<TData, TValue> {
column: Column<TData, TValue>; column: Column<TData, TValue>;
@@ -1,4 +1,4 @@
import { AppModuleConfig } from '#app/services/modules/types'; import type { AppModuleConfig } from '#app/services/modules/types';
import { accountSettingsMessages } from './messages'; import { accountSettingsMessages } from './messages';
import { accountSettingsRoute } from './routing'; import { accountSettingsRoute } from './routing';
@@ -1,6 +1,6 @@
import { Navigate, Route, RouteObject, Routes } from 'react-router-dom'; import { Navigate, Route, Routes, type RouteObject } from 'react-router-dom';
import ProfilePage from '../pages/Profile'; import ProfilePage from '#core/AccountSettings/pages/Profile';
import AccountLayout from '../view/layouts/AccountLayout'; import AccountLayout from '#core/AccountSettings/view/layouts/AccountLayout';
function Routing() { function Routing() {
return ( return (
@@ -2,11 +2,11 @@ import React from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Toolbar, ToolbarHeading } from '#app/layouts/01__Default/components/Toolbar'; import { Toolbar, ToolbarHeading } from '#app/layouts/01__Default/components/Toolbar';
import { ErrorResponse, extractValidationErrors, isValidationError } from '#infrastructure/reactQuery'; import { extractValidationErrors, isValidationError, type ErrorResponse } from '#infrastructure/reactQuery';
import { prepareFormValues, ProfileForm, SubmitFunc } from '#core/AccountSettings/view/forms/ProfileForm';
import { useAuth } from '#core/Auth/providers/AuthProvider'; import { useAuth } from '#core/Auth/providers/AuthProvider';
import { Card, CardContent, CardHeader, CardTitle } from '#shared/ui/Card'; import { Card, CardContent, CardHeader, CardTitle } from '#shared/ui/Card';
import { Container } from '#shared/widgets/common/Container'; import { Container } from '#shared/widgets/common/Container';
import { prepareFormValues, ProfileForm, SubmitFunc } from '../../view/forms/ProfileForm';
const ProfilePage: React.FC = () => { const ProfilePage: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
@@ -8,7 +8,7 @@ import { Button } from '#shared/ui/Button';
import { Form as FormProvider } from '#shared/ui/form'; import { Form as FormProvider } from '#shared/ui/form';
import AvatarCropperField from './fields/AvatarCropperField'; import AvatarCropperField from './fields/AvatarCropperField';
import PasswordField from './fields/PasswordField'; import PasswordField from './fields/PasswordField';
import { createValidationSchema, FormValues, SubmitFunc } from './form'; import { createValidationSchema, type FormValues, type SubmitFunc } from './form';
export const ProfileForm: React.FC<{ export const ProfileForm: React.FC<{
initialValues: FormValues; initialValues: FormValues;
@@ -1,7 +1,7 @@
import { UseFormReturn } from 'react-hook-form'; import type { UseFormReturn } from 'react-hook-form';
import { IntlShape } from 'react-intl'; import type { IntlShape } from 'react-intl';
import z from 'zod'; import z from 'zod';
import { AuthUser } from '#core/Auth/entities'; import type { AuthUser } from '#core/Auth/entities';
export const createValidationSchema = (intl: IntlShape) => { export const createValidationSchema = (intl: IntlShape) => {
return z return z
+1 -1
View File
@@ -1,4 +1,4 @@
import { AppModuleConfig } from '#app/services/modules/types'; import type { AppModuleConfig } from '#app/services/modules/types';
import { authMessages } from './messages'; import { authMessages } from './messages';
import { authRoute } from './routing'; import { authRoute } from './routing';
+3 -3
View File
@@ -1,6 +1,6 @@
import { Navigate, Route, RouteObject, Routes } from 'react-router-dom'; import { Navigate, Route, Routes, type RouteObject } from 'react-router-dom';
import { SignInPage } from '../pages/SignInPage'; import SignInPage from '#core/Auth/pages/SignInPage';
import { BrandedLayout } from '../view/layouts/Branded'; import BrandedLayout from '#core/Auth/view/layouts/Branded';
/** /**
* Handles all authentication related routes. * Handles all authentication related routes.
+2 -2
View File
@@ -1,6 +1,6 @@
import { axiosClient } from '#infrastructure/axios'; import { axiosClient } from '#infrastructure/axios';
import { AuthModel } from '#core/Auth/entities'; import type { AuthModel } from '#core/Auth/entities';
import { GetUserResponse, UpdateUserPayload } from './dto'; import type { GetUserResponse, UpdateUserPayload } from './dto';
export const AuthApi = { export const AuthApi = {
async login(email: string, password: string): Promise<AuthModel> { async login(email: string, password: string): Promise<AuthModel> {
+1 -1
View File
@@ -1,4 +1,4 @@
import { AuthUser } from '#core/Auth/entities'; import type { AuthUser } from '#core/Auth/entities';
export type UpdateUserPayload = Partial<AuthUser>; export type UpdateUserPayload = Partial<AuthUser>;
@@ -1,5 +1,5 @@
import { AuthUser } from '../../entities'; import type { GetUserResponse } from '#core/Auth/data/api/dto';
import { GetUserResponse } from '../api/dto'; import type { AuthUser } from '#core/Auth/entities';
export default class UserMapper { export default class UserMapper {
constructor() {} constructor() {}
@@ -1,6 +1,6 @@
import { LocalStorageService } from '@crux-kit/local-storage'; import { LocalStorageService } from '@crux-kit/local-storage';
import { APP_VERSION } from '#app/config/app'; import { APP_VERSION } from '#app/config/app';
import { AuthModel } from '#core/Auth/entities'; import type { AuthModel } from '#core/Auth/entities';
const storage = new LocalStorageService({ const storage = new LocalStorageService({
prefix: `auth-v${APP_VERSION || '1.0'}` prefix: `auth-v${APP_VERSION || '1.0'}`
+2 -2
View File
@@ -1,2 +1,2 @@
export * from './auth'; export type { AuthModel } from './auth';
export * from './user'; export type { AuthUser } from './user';
@@ -7,7 +7,7 @@ import { useAuth } from '../providers/AuthProvider';
import { SignInForm } from '../view/forms/SignInForm/Component'; import { SignInForm } from '../view/forms/SignInForm/Component';
import { SubmitFunc } from '../view/forms/SignInForm/form'; import { SubmitFunc } from '../view/forms/SignInForm/form';
export function SignInPage() { export default function SignInPage() {
const { login } = useAuth(); const { login } = useAuth();
const intl = useIntl(); const intl = useIntl();
@@ -1,15 +1,15 @@
import { PropsWithChildren, useState } from 'react'; import { useState, type PropsWithChildren } from 'react';
import { queryClient } from '#app/providers/QueryProvider'; import { queryClient } from '#app/providers/QueryProvider';
import { AuthApi } from '#core/Auth/data/api/api'; import { AuthApi } from '#core/Auth/data/api/api';
import { UpdateUserPayload } from '#core/Auth/data/api/dto'; import type { UpdateUserPayload } from '#core/Auth/data/api/dto';
import UserMapper from '#core/Auth/data/mappers/UserMapper'; import UserMapper from '#core/Auth/data/mappers/UserMapper';
import * as authHelper from '#core/Auth/data/stores/localStorage'; import { getAuth, removeAuth, setAuth as setStoredAuth } from '#core/Auth/data/stores/localStorage';
import { AuthModel, AuthUser } from '#core/Auth/entities'; import type { AuthModel, AuthUser } from '#core/Auth/entities';
import { AuthContext } from './context'; import { AuthContext } from './context';
export function AuthProvider({ children }: PropsWithChildren) { export function AuthProvider({ children }: PropsWithChildren) {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [auth, setAuth] = useState<AuthModel | undefined>(authHelper.getAuth()); const [auth, setAuth] = useState<AuthModel | undefined>(getAuth());
const [currentUser, setCurrentUser] = useState<AuthUser | undefined>(); const [currentUser, setCurrentUser] = useState<AuthUser | undefined>();
const verify = async () => { const verify = async () => {
@@ -29,9 +29,9 @@ export function AuthProvider({ children }: PropsWithChildren) {
const saveAuth = (auth: AuthModel | undefined) => { const saveAuth = (auth: AuthModel | undefined) => {
setAuth(auth); setAuth(auth);
if (auth) { if (auth) {
authHelper.setAuth(auth); setStoredAuth(auth);
} else { } else {
authHelper.removeAuth(); removeAuth();
} }
}; };
@@ -1,6 +1,6 @@
import { createContext, useContext } from 'react'; import { createContext, useContext } from 'react';
import { GetUserResponse, UpdateUserPayload } from '#core/Auth/data/api/dto'; import type { GetUserResponse, UpdateUserPayload } from '#core/Auth/data/api/dto';
import { AuthModel, AuthUser } from '#core/Auth/entities'; import type { AuthModel, AuthUser } from '#core/Auth/entities';
export const AuthContext = createContext<{ export const AuthContext = createContext<{
loading: boolean; loading: boolean;
@@ -5,7 +5,7 @@ import { useIntl } from 'react-intl';
import { FormElement, FormGrid, GridElement, InputText } from '#shared/forms/composed'; import { FormElement, FormGrid, GridElement, InputText } from '#shared/forms/composed';
import { Button } from '#shared/ui/Button'; import { Button } from '#shared/ui/Button';
import { Form } from '#shared/ui/form'; import { Form } from '#shared/ui/form';
import { createValidationSchema, FormValues, SubmitFunc } from './form'; import { createValidationSchema, type FormValues, type SubmitFunc } from './form';
export const SignInForm: React.FC<{ export const SignInForm: React.FC<{
initialValues: FormValues; initialValues: FormValues;
@@ -1,4 +1,4 @@
import { IntlShape } from 'react-intl'; import type { IntlShape } from 'react-intl';
import z from 'zod'; import z from 'zod';
export const createValidationSchema = (intl: IntlShape) => export const createValidationSchema = (intl: IntlShape) =>
@@ -6,7 +6,7 @@ import { toAbsoluteUrl } from '#shared/lib/helpers';
import { Badge } from '#shared/ui/Badge'; import { Badge } from '#shared/ui/Badge';
import { Card, CardContent } from '#shared/ui/Card'; import { Card, CardContent } from '#shared/ui/Card';
export function BrandedLayout() { export default function BrandedLayout() {
const intl = useIntl(); const intl = useIntl();
return ( return (
@@ -3,7 +3,7 @@ import { Link, Outlet } from 'react-router-dom';
import { toAbsoluteUrl } from '#shared/lib/helpers'; import { toAbsoluteUrl } from '#shared/lib/helpers';
import { Card, CardContent } from '#shared/ui/Card'; import { Card, CardContent } from '#shared/ui/Card';
export function ClassicLayout() { export default function ClassicLayout() {
return ( return (
<React.Fragment> <React.Fragment>
<style> <style>
@@ -26,7 +26,7 @@ export function ClassicLayout() {
/> />
</Link> </Link>
</div> </div>
<Card className="w-full max-w-[25rem]"> <Card className="w-full max-w-100">
<CardContent className="p-6"> <CardContent className="p-6">
<Outlet /> <Outlet />
</CardContent> </CardContent>
@@ -1,4 +1,4 @@
import { AppModuleConfig } from '#app/services/modules/types'; import type { AppModuleConfig } from '#app/services/modules/types';
import { fallbackScreensMessages } from './messages'; import { fallbackScreensMessages } from './messages';
import { fallbackScreensRoute } from './routing'; import { fallbackScreensRoute } from './routing';
@@ -1,5 +1,5 @@
import { FolderOpen, Wrench } from 'lucide-react'; import { FolderOpen, Wrench } from 'lucide-react';
import { IntlShape } from 'react-intl'; import type { IntlShape } from 'react-intl';
import { MenuItem } from '#app/config/types'; import { MenuItem } from '#app/config/types';
export const createFileManagerMenu = (intl: IntlShape): MenuItem => ({ export const createFileManagerMenu = (intl: IntlShape): MenuItem => ({
@@ -1,4 +1,4 @@
import { AppModuleConfig } from '#app/services/modules/types'; import type { AppModuleConfig } from '#app/services/modules/types';
import { createFileManagerMenu } from './menu'; import { createFileManagerMenu } from './menu';
import { fileManagerMessages } from './messages'; import { fileManagerMessages } from './messages';
import { fileManagerRoute } from './routing'; import { fileManagerRoute } from './routing';
+10 -10
View File
@@ -1,7 +1,7 @@
import { AxiosProgressEvent, GenericAbortSignal } from 'axios'; import type { AxiosProgressEvent, GenericAbortSignal } from 'axios';
import { axiosClient } from '#infrastructure/axios'; import { axiosClient } from '#infrastructure/axios';
import { ensureLeadingSlash } from '#core/FileManager/shared/path/leadingSlash'; import { ensureLeadingSlash } from '#core/FileManager/shared/path/leadingSlash';
import * as DTO from './dto'; import type { CreateDirectoryResponseDTO, DuplicateFileEntriesResponseDTO, GetEntriesResponseDTO, GetFileResponseDTO, RemoveEntryResponseDTO, RenameEntryResponseDTO, SearchEntriesResponseDTO, UploadFileResponseDTO } from './dto';
export class Api { export class Api {
getEntries = async ( getEntries = async (
@@ -15,7 +15,7 @@ export class Api {
} }
) => { ) => {
return await axiosClient return await axiosClient
.get<DTO.GetEntriesResponseDTO>('/file-manager' + ensureLeadingSlash(path), { .get<GetEntriesResponseDTO>('/file-manager' + ensureLeadingSlash(path), {
params: payload params: payload
}) })
.then((data) => data.data); .then((data) => data.data);
@@ -23,7 +23,7 @@ export class Api {
getFile = async (path: string) => { getFile = async (path: string) => {
return await axiosClient return await axiosClient
.get<DTO.GetFileResponseDTO>('/file-manager/files/details' + ensureLeadingSlash(path)) .get<GetFileResponseDTO>('/file-manager/files/details' + ensureLeadingSlash(path))
.then((data) => data.data); .then((data) => data.data);
}; };
@@ -37,19 +37,19 @@ export class Api {
createDirectory = async (payload: { path: string; name: string }) => { createDirectory = async (payload: { path: string; name: string }) => {
return await axiosClient return await axiosClient
.post<DTO.CreateDirectoryResponseDTO>('/file-manager/folders', payload) .post<CreateDirectoryResponseDTO>('/file-manager/folders', payload)
.then((data) => data.data); .then((data) => data.data);
}; };
renameEntry = async (payload: { path: string; name: string }) => { renameEntry = async (payload: { path: string; name: string }) => {
return await axiosClient return await axiosClient
.put<DTO.RenameEntryResponseDTO>('/file-manager/entries/rename', payload) .put<RenameEntryResponseDTO>('/file-manager/entries/rename', payload)
.then((data) => data.data); .then((data) => data.data);
}; };
removeEntry = async (path: string) => { removeEntry = async (path: string) => {
return await axiosClient return await axiosClient
.delete<DTO.RemoveEntryResponseDTO>('/file-manager' + ensureLeadingSlash(path)) .delete<RemoveEntryResponseDTO>('/file-manager' + ensureLeadingSlash(path))
.then((data) => data.data); .then((data) => data.data);
}; };
@@ -61,7 +61,7 @@ export class Api {
} }
) => { ) => {
return await axiosClient return await axiosClient
.post<DTO.UploadFileResponseDTO>('/file-manager/files', formData, { .post<UploadFileResponseDTO>('/file-manager/files', formData, {
signal: params.signal, signal: params.signal,
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
@@ -74,13 +74,13 @@ export class Api {
duplicateFiles = async (paths: string[]) => { duplicateFiles = async (paths: string[]) => {
return await axiosClient return await axiosClient
.post<DTO.DuplicateFileEntriesResponseDTO>('/file-manager/files/duplicate', { paths }) .post<DuplicateFileEntriesResponseDTO>('/file-manager/files/duplicate', { paths })
.then((data) => data.data); .then((data) => data.data);
}; };
searchEntries = async (query: string) => { searchEntries = async (query: string) => {
return await axiosClient return await axiosClient
.get<DTO.SearchEntriesResponseDTO>('/file-manager/search', { .get<SearchEntriesResponseDTO>('/file-manager/search', {
params: { q: query } params: { q: query }
}) })
.then((data) => data.data); .then((data) => data.data);
@@ -3,7 +3,7 @@ import { runInAction } from 'mobx';
import React from 'react'; import React from 'react';
import EntryMapper from '#core/FileManager/data/mappers/EntryMapper'; import EntryMapper from '#core/FileManager/data/mappers/EntryMapper';
import { fetchEntries, useGetEntries } from '#core/FileManager/data/queries/entries'; import { fetchEntries, useGetEntries } from '#core/FileManager/data/queries/entries';
import { entriesKeyBuilder, EntriesQueryPayload } from '#core/FileManager/data/queries/keys'; import { entriesKeyBuilder, type EntriesQueryPayload } from '#core/FileManager/data/queries/keys';
import EntriesStore from '#core/FileManager/data/stores/entriesStore'; import EntriesStore from '#core/FileManager/data/stores/entriesStore';
import ExplorerStore from '#core/FileManager/data/stores/explorerStore'; import ExplorerStore from '#core/FileManager/data/stores/explorerStore';
import { getSettingsData } from '#core/FileManager/data/stores/localStorage'; import { getSettingsData } from '#core/FileManager/data/stores/localStorage';
@@ -1,6 +1,6 @@
import { Entry, EntryType, FileEntry, FolderEntry } from '#core/FileManager/entities/entry'; import { EntryType, type Entry, type FileEntry, type FolderEntry } from '#core/FileManager/entities/entry';
import { ensureLeadingSlash } from '#core/FileManager/shared/path/leadingSlash'; import { ensureLeadingSlash } from '#core/FileManager/shared/path/leadingSlash';
import { EntryDTO } from '../api/dto'; import { type EntryDTO } from '../api/dto';
export default class EntryMapper { export default class EntryMapper {
constructor(private readonly parent: string) {} constructor(private readonly parent: string) {}

Some files were not shown because too many files have changed in this diff Show More