upgrade APIlite and change build backend use TypeScript instead of JavaScript
This commit is contained in:
7
backend/composer.lock
generated
7
backend/composer.lock
generated
@ -8,11 +8,11 @@
|
|||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "tpsoft/apilite",
|
"name": "tpsoft/apilite",
|
||||||
"version": "v1.1.0",
|
"version": "v1.2.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://gitea.tpsoft.org/TPsoft.org/APIlite.git",
|
"url": "https://gitea.tpsoft.org/TPsoft.org/APIlite.git",
|
||||||
"reference": "fab8efd780ede046ced076f237351cdba5a8a51f"
|
"reference": "d258bcc91948424711dd79fde57254e1384d0091"
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.2"
|
"php": ">=8.2"
|
||||||
@ -41,6 +41,7 @@
|
|||||||
"description": "A set of tools to simplify the work of creating backend APIs for your frontend projects.",
|
"description": "A set of tools to simplify the work of creating backend APIs for your frontend projects.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"api",
|
"api",
|
||||||
|
"javascript",
|
||||||
"json",
|
"json",
|
||||||
"php",
|
"php",
|
||||||
"rest",
|
"rest",
|
||||||
@ -52,7 +53,7 @@
|
|||||||
"type": "other"
|
"type": "other"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-02-09T06:33:17+00:00"
|
"time": "2026-02-13T08:54:08+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "tpsoft/dbmodel",
|
"name": "tpsoft/dbmodel",
|
||||||
|
|||||||
@ -9,7 +9,7 @@ $backend_api = new TPsoft\Nutrio\API('typescript', 'import.meta.env.VITE_BACKEND
|
|||||||
$output = ob_get_contents();
|
$output = ob_get_contents();
|
||||||
ob_end_clean();
|
ob_end_clean();
|
||||||
|
|
||||||
$ts_path = realpath(__DIR__ . '/../../frontend/src').'/BackendAPI.js';
|
$ts_path = realpath(__DIR__ . '/../../frontend/src').'/BackendAPI.ts';
|
||||||
$suc = file_put_contents($ts_path, $output);
|
$suc = file_put_contents($ts_path, $output);
|
||||||
if ($suc === false) {
|
if ($suc === false) {
|
||||||
echo "✗ TypeScript store into file failed\n";
|
echo "✗ TypeScript store into file failed\n";
|
||||||
|
|||||||
@ -1,161 +0,0 @@
|
|||||||
/**
|
|
||||||
* Generated by APIlite
|
|
||||||
* https://gitea.tpsoft.org/TPsoft.org/APIlite
|
|
||||||
*
|
|
||||||
* 2026-02-13 06:55:45 */
|
|
||||||
|
|
||||||
class BackendAPI {
|
|
||||||
endpoint = import.meta.env.VITE_BACKENDAPI_URL;
|
|
||||||
|
|
||||||
/* ----------------------------------------------------
|
|
||||||
* General API call
|
|
||||||
*/
|
|
||||||
call(method, data, callback) {
|
|
||||||
var xhttp = new XMLHttpRequest();
|
|
||||||
xhttp.withCredentials = true;
|
|
||||||
xhttp.onreadystatechange = function() {
|
|
||||||
if (this.readyState == 4) {
|
|
||||||
if (this.status == 200) {
|
|
||||||
if (callback != null) callback(JSON.parse(this.responseText));
|
|
||||||
} else {
|
|
||||||
if (callback != null) callback({'status': 'ERROR', 'message': 'HTTP STATUS ' + this.status});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var form_data = new FormData();
|
|
||||||
Object.keys(data).forEach(key => {
|
|
||||||
let val = data[key];
|
|
||||||
if (typeof val == 'object') val = JSON.stringify(val);
|
|
||||||
form_data.append(key, val);
|
|
||||||
});
|
|
||||||
xhttp.open('POST', this.endpoint + '?action=' + method);
|
|
||||||
xhttp.send(form_data);
|
|
||||||
}
|
|
||||||
|
|
||||||
callPromise(method, data) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.call(method, data, function(response) {
|
|
||||||
if (method == '__HELP__') {
|
|
||||||
resolve(response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (response.status == 'OK') {
|
|
||||||
resolve(response.data);
|
|
||||||
} else {
|
|
||||||
reject(response.msg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ----------------------------------------------------
|
|
||||||
* API actions
|
|
||||||
*/
|
|
||||||
help() {
|
|
||||||
return this.callPromise('__HELP__', {});
|
|
||||||
}
|
|
||||||
|
|
||||||
health() {
|
|
||||||
return this.callPromise('health', {});
|
|
||||||
}
|
|
||||||
|
|
||||||
userRegistration(email, password) {
|
|
||||||
return this.callPromise('userRegistration', {email: email, password: password});
|
|
||||||
}
|
|
||||||
|
|
||||||
userLogin(email, password) {
|
|
||||||
return this.callPromise('userLogin', {email: email, password: password});
|
|
||||||
}
|
|
||||||
|
|
||||||
userDelete(email, password) {
|
|
||||||
return this.callPromise('userDelete', {email: email, password: password});
|
|
||||||
}
|
|
||||||
|
|
||||||
userLogout(token) {
|
|
||||||
return this.callPromise('userLogout', {token: token});
|
|
||||||
}
|
|
||||||
|
|
||||||
ingredientList(token, query, include_global) {
|
|
||||||
return this.callPromise('ingredientList', {token: token, query: query, include_global: include_global});
|
|
||||||
}
|
|
||||||
|
|
||||||
ingredientGet(token, ingredient_id) {
|
|
||||||
return this.callPromise('ingredientGet', {token: token, ingredient_id: ingredient_id});
|
|
||||||
}
|
|
||||||
|
|
||||||
ingredientCreate(token, name, protein_g_100, carbs_g_100, sugar_g_100, fat_g_100, fiber_g_100, kcal_100) {
|
|
||||||
return this.callPromise('ingredientCreate', {token: token, name: name, protein_g_100: protein_g_100, carbs_g_100: carbs_g_100, sugar_g_100: sugar_g_100, fat_g_100: fat_g_100, fiber_g_100: fiber_g_100, kcal_100: kcal_100});
|
|
||||||
}
|
|
||||||
|
|
||||||
ingredientUpdate(token, ingredient_id, name, protein_g_100, carbs_g_100, sugar_g_100, fat_g_100, fiber_g_100, kcal_100) {
|
|
||||||
return this.callPromise('ingredientUpdate', {token: token, ingredient_id: ingredient_id, name: name, protein_g_100: protein_g_100, carbs_g_100: carbs_g_100, sugar_g_100: sugar_g_100, fat_g_100: fat_g_100, fiber_g_100: fiber_g_100, kcal_100: kcal_100});
|
|
||||||
}
|
|
||||||
|
|
||||||
ingredientDelete(token, ingredient_id) {
|
|
||||||
return this.callPromise('ingredientDelete', {token: token, ingredient_id: ingredient_id});
|
|
||||||
}
|
|
||||||
|
|
||||||
mealList(token, meal_type, with_items, with_totals) {
|
|
||||||
return this.callPromise('mealList', {token: token, meal_type: meal_type, with_items: with_items, with_totals: with_totals});
|
|
||||||
}
|
|
||||||
|
|
||||||
mealGet(token, meal_id, with_items, with_totals) {
|
|
||||||
return this.callPromise('mealGet', {token: token, meal_id: meal_id, with_items: with_items, with_totals: with_totals});
|
|
||||||
}
|
|
||||||
|
|
||||||
mealCreate(token, name, meal_type) {
|
|
||||||
return this.callPromise('mealCreate', {token: token, name: name, meal_type: meal_type});
|
|
||||||
}
|
|
||||||
|
|
||||||
mealUpdate(token, meal_id, name, meal_type) {
|
|
||||||
return this.callPromise('mealUpdate', {token: token, meal_id: meal_id, name: name, meal_type: meal_type});
|
|
||||||
}
|
|
||||||
|
|
||||||
mealDelete(token, meal_id) {
|
|
||||||
return this.callPromise('mealDelete', {token: token, meal_id: meal_id});
|
|
||||||
}
|
|
||||||
|
|
||||||
mealItemList(token, meal_id, with_calculated) {
|
|
||||||
return this.callPromise('mealItemList', {token: token, meal_id: meal_id, with_calculated: with_calculated});
|
|
||||||
}
|
|
||||||
|
|
||||||
mealItemAdd(token, meal_id, ingredient_id, grams, position) {
|
|
||||||
return this.callPromise('mealItemAdd', {token: token, meal_id: meal_id, ingredient_id: ingredient_id, grams: grams, position: position});
|
|
||||||
}
|
|
||||||
|
|
||||||
mealItemUpdate(token, meal_item_id, ingredient_id, grams, position) {
|
|
||||||
return this.callPromise('mealItemUpdate', {token: token, meal_item_id: meal_item_id, ingredient_id: ingredient_id, grams: grams, position: position});
|
|
||||||
}
|
|
||||||
|
|
||||||
mealItemDelete(token, meal_item_id) {
|
|
||||||
return this.callPromise('mealItemDelete', {token: token, meal_item_id: meal_item_id});
|
|
||||||
}
|
|
||||||
|
|
||||||
mealItemReorder(token, meal_id, ordered_item_ids) {
|
|
||||||
return this.callPromise('mealItemReorder', {token: token, meal_id: meal_id, ordered_item_ids: ordered_item_ids});
|
|
||||||
}
|
|
||||||
|
|
||||||
mealTotals(token, meal_id) {
|
|
||||||
return this.callPromise('mealTotals', {token: token, meal_id: meal_id});
|
|
||||||
}
|
|
||||||
|
|
||||||
diaryDayGet(token, day_date, with_totals) {
|
|
||||||
return this.callPromise('diaryDayGet', {token: token, day_date: day_date, with_totals: with_totals});
|
|
||||||
}
|
|
||||||
|
|
||||||
diaryDaySetMeal(token, day_date, meal_type, meal_id) {
|
|
||||||
return this.callPromise('diaryDaySetMeal', {token: token, day_date: day_date, meal_type: meal_type, meal_id: meal_id});
|
|
||||||
}
|
|
||||||
|
|
||||||
diaryDayUnsetMeal(token, day_date, meal_type) {
|
|
||||||
return this.callPromise('diaryDayUnsetMeal', {token: token, day_date: day_date, meal_type: meal_type});
|
|
||||||
}
|
|
||||||
|
|
||||||
diaryRange(token, date_from, date_to) {
|
|
||||||
return this.callPromise('diaryRange', {token: token, date_from: date_from, date_to: date_to});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
export default new BackendAPI();
|
|
||||||
205
frontend/src/BackendAPI.ts
Normal file
205
frontend/src/BackendAPI.ts
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
/**
|
||||||
|
* Generated by APIlite
|
||||||
|
* https://gitea.tpsoft.org/TPsoft.org/APIlite
|
||||||
|
*
|
||||||
|
* 2026-02-13 10:00:58 */
|
||||||
|
|
||||||
|
export interface APIliteActionResponse<T> {
|
||||||
|
status: 'OK';
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface APIliteErrorResponse {
|
||||||
|
status: 'ERROR';
|
||||||
|
msg: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface APIliteMethodParam {
|
||||||
|
name: string;
|
||||||
|
type: string | null;
|
||||||
|
optional: boolean;
|
||||||
|
default: unknown;
|
||||||
|
doc: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface APIliteMethodDoc {
|
||||||
|
name: string;
|
||||||
|
doc: string | null;
|
||||||
|
description: string | null;
|
||||||
|
params: APIliteMethodParam[];
|
||||||
|
return: string | string[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface APIliteHelpResponse {
|
||||||
|
name: string;
|
||||||
|
html_version: string;
|
||||||
|
javascript_version: string;
|
||||||
|
typescript_version: string;
|
||||||
|
actions: APIliteMethodDoc[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class BackendAPI {
|
||||||
|
endpoint: string = import.meta.env.VITE_BACKENDAPI_URL;
|
||||||
|
|
||||||
|
private call(
|
||||||
|
method: string,
|
||||||
|
data: Record<string, unknown>,
|
||||||
|
callback: (response: APIliteHelpResponse | APIliteActionResponse<unknown> | APIliteErrorResponse) => void
|
||||||
|
): void {
|
||||||
|
const xhttp = new XMLHttpRequest();
|
||||||
|
xhttp.withCredentials = true;
|
||||||
|
xhttp.onreadystatechange = function() {
|
||||||
|
if (this.readyState === 4) {
|
||||||
|
if (this.status === 200) {
|
||||||
|
const response = JSON.parse(this.responseText) as APIliteHelpResponse | APIliteActionResponse<unknown> | APIliteErrorResponse;
|
||||||
|
callback(response);
|
||||||
|
} else {
|
||||||
|
callback({ status: 'ERROR', msg: 'HTTP STATUS ' + this.status });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
const rawValue = data[key];
|
||||||
|
if (typeof rawValue === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let value: string | Blob;
|
||||||
|
if (rawValue instanceof Blob) {
|
||||||
|
value = rawValue;
|
||||||
|
} else if (typeof rawValue === 'object' && rawValue !== null) {
|
||||||
|
value = JSON.stringify(rawValue);
|
||||||
|
} else {
|
||||||
|
value = String(rawValue);
|
||||||
|
}
|
||||||
|
formData.append(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
xhttp.open('POST', this.endpoint + '?action=' + method);
|
||||||
|
xhttp.send(formData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private callPromise<T>(method: string, data: Record<string, unknown>): Promise<T> {
|
||||||
|
return new Promise<T>((resolve, reject) => {
|
||||||
|
this.call(method, data, (response) => {
|
||||||
|
if (method === '__HELP__') {
|
||||||
|
resolve(response as T);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (response.status === 'OK') {
|
||||||
|
resolve(response.data as T);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reject(response.msg);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
help(): Promise<APIliteHelpResponse> {
|
||||||
|
return this.callPromise<APIliteHelpResponse>('__HELP__', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
health(): Promise<APIliteActionResponse<unknown[]>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<unknown[]>>('health', { });
|
||||||
|
}
|
||||||
|
|
||||||
|
userRegistration(email: string, password: string): Promise<APIliteActionResponse<unknown[]>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<unknown[]>>('userRegistration', { email, password });
|
||||||
|
}
|
||||||
|
|
||||||
|
userLogin(email: string, password: string): Promise<APIliteActionResponse<unknown[]>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<unknown[]>>('userLogin', { email, password });
|
||||||
|
}
|
||||||
|
|
||||||
|
userDelete(email: string, password: string): Promise<APIliteActionResponse<unknown[]>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<unknown[]>>('userDelete', { email, password });
|
||||||
|
}
|
||||||
|
|
||||||
|
userLogout(token: string): Promise<APIliteActionResponse<unknown[]>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<unknown[]>>('userLogout', { token });
|
||||||
|
}
|
||||||
|
|
||||||
|
ingredientList(token: string, query?: string, include_global?: boolean): Promise<APIliteActionResponse<unknown[]>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<unknown[]>>('ingredientList', { token, query, include_global });
|
||||||
|
}
|
||||||
|
|
||||||
|
ingredientGet(token: string, ingredient_id: number): Promise<APIliteActionResponse<unknown[]>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<unknown[]>>('ingredientGet', { token, ingredient_id });
|
||||||
|
}
|
||||||
|
|
||||||
|
ingredientCreate(token: string, name: string, protein_g_100: number, carbs_g_100: number, sugar_g_100: number, fat_g_100: number, fiber_g_100?: number, kcal_100?: number): Promise<APIliteActionResponse<unknown[]>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<unknown[]>>('ingredientCreate', { token, name, protein_g_100, carbs_g_100, sugar_g_100, fat_g_100, fiber_g_100, kcal_100 });
|
||||||
|
}
|
||||||
|
|
||||||
|
ingredientUpdate(token: string, ingredient_id: number, name: string, protein_g_100: number, carbs_g_100: number, sugar_g_100: number, fat_g_100: number, fiber_g_100?: number, kcal_100?: number): Promise<APIliteActionResponse<unknown[]>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<unknown[]>>('ingredientUpdate', { token, ingredient_id, name, protein_g_100, carbs_g_100, sugar_g_100, fat_g_100, fiber_g_100, kcal_100 });
|
||||||
|
}
|
||||||
|
|
||||||
|
ingredientDelete(token: string, ingredient_id: number): Promise<APIliteActionResponse<unknown[]>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<unknown[]>>('ingredientDelete', { token, ingredient_id });
|
||||||
|
}
|
||||||
|
|
||||||
|
mealList(token: string, meal_type?: string, with_items?: boolean, with_totals?: boolean): Promise<APIliteActionResponse<unknown[]>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<unknown[]>>('mealList', { token, meal_type, with_items, with_totals });
|
||||||
|
}
|
||||||
|
|
||||||
|
mealGet(token: string, meal_id: number, with_items?: boolean, with_totals?: boolean): Promise<APIliteActionResponse<unknown[]>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<unknown[]>>('mealGet', { token, meal_id, with_items, with_totals });
|
||||||
|
}
|
||||||
|
|
||||||
|
mealCreate(token: string, name: string, meal_type: string): Promise<APIliteActionResponse<unknown[]>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<unknown[]>>('mealCreate', { token, name, meal_type });
|
||||||
|
}
|
||||||
|
|
||||||
|
mealUpdate(token: string, meal_id: number, name: string, meal_type: string): Promise<APIliteActionResponse<unknown[]>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<unknown[]>>('mealUpdate', { token, meal_id, name, meal_type });
|
||||||
|
}
|
||||||
|
|
||||||
|
mealDelete(token: string, meal_id: number): Promise<APIliteActionResponse<unknown[]>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<unknown[]>>('mealDelete', { token, meal_id });
|
||||||
|
}
|
||||||
|
|
||||||
|
mealItemList(token: string, meal_id: number, with_calculated?: boolean): Promise<APIliteActionResponse<unknown[]>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<unknown[]>>('mealItemList', { token, meal_id, with_calculated });
|
||||||
|
}
|
||||||
|
|
||||||
|
mealItemAdd(token: string, meal_id: number, ingredient_id: number, grams: number, position?: number): Promise<APIliteActionResponse<unknown[]>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<unknown[]>>('mealItemAdd', { token, meal_id, ingredient_id, grams, position });
|
||||||
|
}
|
||||||
|
|
||||||
|
mealItemUpdate(token: string, meal_item_id: number, ingredient_id: number, grams: number, position: number): Promise<APIliteActionResponse<unknown[]>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<unknown[]>>('mealItemUpdate', { token, meal_item_id, ingredient_id, grams, position });
|
||||||
|
}
|
||||||
|
|
||||||
|
mealItemDelete(token: string, meal_item_id: number): Promise<APIliteActionResponse<unknown[]>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<unknown[]>>('mealItemDelete', { token, meal_item_id });
|
||||||
|
}
|
||||||
|
|
||||||
|
mealItemReorder(token: string, meal_id: number, ordered_item_ids: unknown[]): Promise<APIliteActionResponse<unknown[]>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<unknown[]>>('mealItemReorder', { token, meal_id, ordered_item_ids });
|
||||||
|
}
|
||||||
|
|
||||||
|
mealTotals(token: string, meal_id: number): Promise<APIliteActionResponse<unknown[]>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<unknown[]>>('mealTotals', { token, meal_id });
|
||||||
|
}
|
||||||
|
|
||||||
|
diaryDayGet(token: string, day_date: string, with_totals?: boolean): Promise<APIliteActionResponse<unknown[]>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<unknown[]>>('diaryDayGet', { token, day_date, with_totals });
|
||||||
|
}
|
||||||
|
|
||||||
|
diaryDaySetMeal(token: string, day_date: string, meal_type: string, meal_id: number): Promise<APIliteActionResponse<unknown[]>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<unknown[]>>('diaryDaySetMeal', { token, day_date, meal_type, meal_id });
|
||||||
|
}
|
||||||
|
|
||||||
|
diaryDayUnsetMeal(token: string, day_date: string, meal_type: string): Promise<APIliteActionResponse<unknown[]>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<unknown[]>>('diaryDayUnsetMeal', { token, day_date, meal_type });
|
||||||
|
}
|
||||||
|
|
||||||
|
diaryRange(token: string, date_from: string, date_to: string): Promise<APIliteActionResponse<unknown[]>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<unknown[]>>('diaryRange', { token, date_from, date_to });
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new BackendAPI();
|
||||||
@ -12,7 +12,7 @@ import {
|
|||||||
faSun,
|
faSun,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
import BackendAPI from '@/BackendAPI.js'
|
import BackendAPI from '@/BackendAPI.ts'
|
||||||
import i18n from '@/i18n'
|
import i18n from '@/i18n'
|
||||||
import { SUPPORTED_LOCALES, type AppLocale } from '@/i18n'
|
import { SUPPORTED_LOCALES, type AppLocale } from '@/i18n'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user