dynamic form updater
This commit is contained in:
26
package-lock.json
generated
26
package-lock.json
generated
@@ -18,11 +18,13 @@
|
|||||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
"@tailwindcss/postcss": "^4.0.4",
|
"@tailwindcss/postcss": "^4.0.4",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"@types/lodash-es": "^4.17.12",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-plugin-svelte": "^2.46.1",
|
"eslint-plugin-svelte": "^2.46.1",
|
||||||
"globals": "^15.14.0",
|
"globals": "^15.14.0",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"prettier-plugin-svelte": "^3.3.3",
|
"prettier-plugin-svelte": "^3.3.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
@@ -1674,6 +1676,23 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/lodash": {
|
||||||
|
"version": "4.17.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz",
|
||||||
|
"integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/lodash-es": {
|
||||||
|
"version": "4.17.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||||
|
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/lodash": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/resolve": {
|
"node_modules/@types/resolve": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||||
@@ -3264,6 +3283,13 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash-es": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.defaults": {
|
"node_modules/lodash.defaults": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||||
|
|||||||
@@ -24,11 +24,13 @@
|
|||||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
"@tailwindcss/postcss": "^4.0.4",
|
"@tailwindcss/postcss": "^4.0.4",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"@types/lodash-es": "^4.17.12",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-plugin-svelte": "^2.46.1",
|
"eslint-plugin-svelte": "^2.46.1",
|
||||||
"globals": "^15.14.0",
|
"globals": "^15.14.0",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"prettier-plugin-svelte": "^3.3.3",
|
"prettier-plugin-svelte": "^3.3.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import type { AssistRules } from '$models/config';
|
import { configFile, type AssistRules } from '$models/config';
|
||||||
|
|
||||||
const { config, id }: { config: AssistRules; id: string } = $props();
|
const { config, id }: { config: AssistRules; id: string } = $props();
|
||||||
const editedConfig = $state({ ...config });
|
const editedConfig = $state({ ...config });
|
||||||
@@ -9,12 +9,17 @@
|
|||||||
|
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="?/assistRules"
|
action="?/update"
|
||||||
use:enhance={() => {
|
use:enhance={() => {
|
||||||
formLoading = true;
|
formLoading = true;
|
||||||
|
return async ({ update }) => {
|
||||||
|
await update({ invalidateAll: true, reset: false });
|
||||||
|
formLoading = false;
|
||||||
|
};
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input type="hidden" name="id" value={id} />
|
<input type="hidden" name="id" value={id} />
|
||||||
|
<input type="hidden" name="file" value={configFile.assistRules} />
|
||||||
<div class="sm:mx-auto sm:w-full sm:max-w-7xl">
|
<div class="sm:mx-auto sm:w-full sm:max-w-7xl">
|
||||||
<div class="border-b border-gray-900/10 pb-12">
|
<div class="border-b border-gray-900/10 pb-12">
|
||||||
<h2 class="text-base/7 font-semibold text-gray-900">Assist Rules</h2>
|
<h2 class="text-base/7 font-semibold text-gray-900">Assist Rules</h2>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import type { Configuration } from '$models/config';
|
import { configFile, type Configuration } from '$models/config';
|
||||||
|
|
||||||
const { config, id }: { config: Configuration; id: string } = $props();
|
const { config, id }: { config: Configuration; id: string } = $props();
|
||||||
const editedConfig = $state({ ...config });
|
const editedConfig = $state({ ...config });
|
||||||
@@ -9,12 +9,17 @@
|
|||||||
|
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="?/configuration"
|
action="?/update"
|
||||||
use:enhance={() => {
|
use:enhance={() => {
|
||||||
formLoading = true;
|
formLoading = true;
|
||||||
|
return async ({ update }) => {
|
||||||
|
await update({ invalidateAll: true, reset: false });
|
||||||
|
formLoading = false;
|
||||||
|
};
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input type="hidden" name="id" value={id} />
|
<input type="hidden" name="id" value={id} />
|
||||||
|
<input type="hidden" name="file" value={configFile.configuration} />
|
||||||
<div class="sm:mx-auto sm:w-full sm:max-w-7xl">
|
<div class="sm:mx-auto sm:w-full sm:max-w-7xl">
|
||||||
<div class="border-b border-gray-900/10 pb-12">
|
<div class="border-b border-gray-900/10 pb-12">
|
||||||
<h2 class="text-base/7 font-semibold text-gray-900">Configuration</h2>
|
<h2 class="text-base/7 font-semibold text-gray-900">Configuration</h2>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import type { EventConfig } from '$models/config';
|
import { configFile, type EventConfig } from '$models/config';
|
||||||
import type { Track } from '$models/lookups';
|
import type { Track } from '$models/lookups';
|
||||||
|
|
||||||
const { config, tracks, id }: { config: EventConfig; tracks: Track[]; id: string } = $props();
|
const { config, tracks, id }: { config: EventConfig; tracks: Track[]; id: string } = $props();
|
||||||
@@ -11,12 +11,17 @@
|
|||||||
|
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="?/event"
|
action="?/update"
|
||||||
use:enhance={() => {
|
use:enhance={() => {
|
||||||
formLoading = true;
|
formLoading = true;
|
||||||
|
return async ({ update }) => {
|
||||||
|
await update({ invalidateAll: true, reset: false });
|
||||||
|
formLoading = false;
|
||||||
|
};
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input type="hidden" name="id" value={id} />
|
<input type="hidden" name="id" value={id} />
|
||||||
|
<input type="hidden" name="file" value={configFile.event} />
|
||||||
<div class="sm:mx-auto sm:w-full sm:max-w-7xl">
|
<div class="sm:mx-auto sm:w-full sm:max-w-7xl">
|
||||||
<div class="border-b border-gray-900/10 pb-12">
|
<div class="border-b border-gray-900/10 pb-12">
|
||||||
<h2 class="text-base/7 font-semibold text-gray-900">Event</h2>
|
<h2 class="text-base/7 font-semibold text-gray-900">Event</h2>
|
||||||
@@ -215,7 +220,7 @@
|
|||||||
<div class="input-block">
|
<div class="input-block">
|
||||||
<input
|
<input
|
||||||
bind:value={session.hourOfDay}
|
bind:value={session.hourOfDay}
|
||||||
name={`sessions[${index}][hourOfDay]`}
|
name={`sessions[${index}].hourOfDay`}
|
||||||
disabled={formLoading}
|
disabled={formLoading}
|
||||||
type="number"
|
type="number"
|
||||||
class="form form-input"
|
class="form form-input"
|
||||||
@@ -232,7 +237,7 @@
|
|||||||
<div class="input-block">
|
<div class="input-block">
|
||||||
<input
|
<input
|
||||||
bind:value={session.dayOfWeekend}
|
bind:value={session.dayOfWeekend}
|
||||||
name={`sessions[${index}][dayOfWeekend]`}
|
name={`sessions[${index}].dayOfWeekend`}
|
||||||
disabled={formLoading}
|
disabled={formLoading}
|
||||||
type="number"
|
type="number"
|
||||||
class="form form-input"
|
class="form form-input"
|
||||||
@@ -249,7 +254,7 @@
|
|||||||
<div class="input-block">
|
<div class="input-block">
|
||||||
<input
|
<input
|
||||||
bind:value={session.timeMultiplier}
|
bind:value={session.timeMultiplier}
|
||||||
name={`sessions[${index}][timeMultiplier]`}
|
name={`sessions[${index}].timeMultiplier`}
|
||||||
disabled={formLoading}
|
disabled={formLoading}
|
||||||
type="number"
|
type="number"
|
||||||
class="form form-input"
|
class="form form-input"
|
||||||
@@ -265,7 +270,7 @@
|
|||||||
<div class="mt-2 grid grid-cols-1">
|
<div class="mt-2 grid grid-cols-1">
|
||||||
<select
|
<select
|
||||||
bind:value={session.sessionType}
|
bind:value={session.sessionType}
|
||||||
name={`sessions[${index}][sessionType]`}
|
name={`sessions[${index}].sessionType`}
|
||||||
disabled={formLoading}
|
disabled={formLoading}
|
||||||
class="form form-select"
|
class="form form-select"
|
||||||
>
|
>
|
||||||
@@ -284,7 +289,7 @@
|
|||||||
<div class="input-block">
|
<div class="input-block">
|
||||||
<input
|
<input
|
||||||
bind:value={session.sessionDurationMinutes}
|
bind:value={session.sessionDurationMinutes}
|
||||||
name={`sessions[${index}][sessionDurationMinutes]`}
|
name={`sessions[${index}].sessionDurationMinutes`}
|
||||||
disabled={formLoading}
|
disabled={formLoading}
|
||||||
type="number"
|
type="number"
|
||||||
class="form form-input"
|
class="form form-input"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import type { EventRules } from '$models/config';
|
import { configFile, type EventRules } from '$models/config';
|
||||||
|
|
||||||
const { config, id }: { config: EventRules; id: string } = $props();
|
const { config, id }: { config: EventRules; id: string } = $props();
|
||||||
const editedConfig = $state({ ...config });
|
const editedConfig = $state({ ...config });
|
||||||
@@ -9,12 +9,17 @@
|
|||||||
|
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="?/eventRules"
|
action="?/update"
|
||||||
use:enhance={() => {
|
use:enhance={() => {
|
||||||
formLoading = true;
|
formLoading = true;
|
||||||
|
return async ({ update }) => {
|
||||||
|
await update({ invalidateAll: true, reset: false });
|
||||||
|
formLoading = false;
|
||||||
|
};
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input type="hidden" name="id" value={id} />
|
<input type="hidden" name="id" value={id} />
|
||||||
|
<input type="hidden" name="file" value={configFile.eventRules} />
|
||||||
<div class="sm:mx-auto sm:w-full sm:max-w-7xl">
|
<div class="sm:mx-auto sm:w-full sm:max-w-7xl">
|
||||||
<div class="border-b border-gray-900/10 pb-12">
|
<div class="border-b border-gray-900/10 pb-12">
|
||||||
<h2 class="text-base/7 font-semibold text-gray-900">Event Rules</h2>
|
<h2 class="text-base/7 font-semibold text-gray-900">Event Rules</h2>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import type { ServerSettings } from '$models/config';
|
import { configFile, type ServerSettings } from '$models/config';
|
||||||
|
|
||||||
const { config, id }: { config: ServerSettings; id: string } = $props();
|
const { config, id }: { config: ServerSettings; id: string } = $props();
|
||||||
const editedConfig = $state({ ...config });
|
const editedConfig = $state({ ...config });
|
||||||
@@ -10,12 +10,17 @@
|
|||||||
|
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="?/settings"
|
action="?/update"
|
||||||
use:enhance={() => {
|
use:enhance={() => {
|
||||||
formLoading = true;
|
formLoading = true;
|
||||||
|
return async ({ update }) => {
|
||||||
|
await update({ invalidateAll: true, reset: false });
|
||||||
|
formLoading = false;
|
||||||
|
};
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input type="hidden" name="id" value={id} />
|
<input type="hidden" name="id" value={id} />
|
||||||
|
<input type="hidden" name="file" value={configFile.settings} />
|
||||||
<div class="sm:mx-auto sm:w-full sm:max-w-7xl">
|
<div class="sm:mx-auto sm:w-full sm:max-w-7xl">
|
||||||
<div class="border-b border-gray-900/10 pb-12">
|
<div class="border-b border-gray-900/10 pb-12">
|
||||||
<h2 class="text-base/7 font-semibold text-gray-900">Settings</h2>
|
<h2 class="text-base/7 font-semibold text-gray-900">Settings</h2>
|
||||||
|
|||||||
@@ -7,4 +7,8 @@
|
|||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>ACC Server Manager</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>ACC Server Manager - Dashboard</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.server-grid {
|
.server-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { checkAuth } from '$api/authService';
|
|||||||
import { getTracks } from '$api/lookupService';
|
import { getTracks } from '$api/lookupService';
|
||||||
import { redirect } from '@sveltejs/kit';
|
import { redirect } from '@sveltejs/kit';
|
||||||
import type { RequestEvent } from '@sveltejs/kit';
|
import type { RequestEvent } from '@sveltejs/kit';
|
||||||
import { configFile, type Session } from '$models/config';
|
import { configFile, type Config, type Session } from '$models/config';
|
||||||
|
import { set } from 'lodash-es';
|
||||||
|
|
||||||
export const load = async (event: RequestEvent) => {
|
export const load = async (event: RequestEvent) => {
|
||||||
const isAuth = await checkAuth(event);
|
const isAuth = await checkAuth(event);
|
||||||
@@ -31,47 +32,39 @@ type SessionField =
|
|||||||
| 'hourOfDay';
|
| 'hourOfDay';
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
event: async (event: RequestEvent) => {
|
update: async (event: RequestEvent) => {
|
||||||
|
const { id, restart, file, data } = await destructureFormData(event);
|
||||||
|
|
||||||
|
const sessions: Array<Record<SessionField, string | number>> = [];
|
||||||
|
|
||||||
|
await updateConfig(event, id, file, data, true, restart);
|
||||||
|
}
|
||||||
|
} satisfies Actions;
|
||||||
|
|
||||||
|
async function destructureFormData(
|
||||||
|
event: RequestEvent
|
||||||
|
): Promise<{ id: string; restart: boolean; data: Config; file: configFile }> {
|
||||||
const formData = await event.request.formData();
|
const formData = await event.request.formData();
|
||||||
const id = formData.get('id') as string;
|
const id = formData.get('id') as string;
|
||||||
const restart = formData.get('restart') === 'true';
|
const restart = formData.get('restart') === 'true';
|
||||||
|
const file = formData.get('file') as configFile;
|
||||||
const object: any = {};
|
const object: any = {};
|
||||||
const sessions: Array<Record<SessionField, string | number>> = [];
|
|
||||||
formData.forEach((value, key) => {
|
|
||||||
const sessionMatch = key.match(/sessions\[(\d+)\]\[(\w+)\]/);
|
|
||||||
if (sessionMatch) {
|
|
||||||
const index = parseInt(sessionMatch[1]);
|
|
||||||
const field = sessionMatch[2] as SessionField;
|
|
||||||
|
|
||||||
if (!sessions[index]) {
|
|
||||||
sessions[index] = {
|
|
||||||
hourOfDay: 0,
|
|
||||||
dayOfWeekend: 0,
|
|
||||||
timeMultiplier: 0,
|
|
||||||
sessionType: '',
|
|
||||||
sessionDurationMinutes: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign the value to the corresponding session field
|
|
||||||
sessions[index][field] = value !== '' && !Number.isNaN(+value) ? +value : (value as string);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
object.sessions = sessions;
|
|
||||||
formData.forEach((value, key) => {
|
formData.forEach((value, key) => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'id':
|
case 'id':
|
||||||
case 'restart':
|
case 'restart':
|
||||||
case 'sessions':
|
case 'file':
|
||||||
return;
|
return;
|
||||||
default:
|
default:
|
||||||
object[key] = value != '' && !Number.isNaN(+value) ? +value : value;
|
set(object, key, parseFormField(value));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await updateConfig(event, id, configFile.event, object, true, restart);
|
return { id, restart, data: object, file };
|
||||||
redirect(303, '/dashboard');
|
}
|
||||||
}
|
|
||||||
} satisfies Actions;
|
function parseFormField(value: FormDataEntryValue): string | number {
|
||||||
|
return value !== '' && !Number.isNaN(+value) ? +value : (value as string);
|
||||||
|
}
|
||||||
|
|
||||||
function tryParse(str: string) {
|
function tryParse(str: string) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -14,6 +14,10 @@
|
|||||||
let tab = $state(configFile.event);
|
let tab = $state(configFile.event);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{server.name}</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<aside class="fixed top-0 left-64 z-40 h-screen w-48">
|
<aside class="fixed top-0 left-64 z-40 h-screen w-48">
|
||||||
<div class="h-full overflow-y-auto bg-gray-50 px-1 py-4 dark:bg-gray-700">
|
<div class="h-full overflow-y-auto bg-gray-50 px-1 py-4 dark:bg-gray-700">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
let { error } = get(authStore);
|
let { error } = get(authStore);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>ACC Server Manager - Login</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
|
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
|
||||||
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||||
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">
|
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">
|
||||||
|
|||||||
Reference in New Issue
Block a user