feat: create forum category on frontend

This commit is contained in:
FrenchGithubUser
2025-12-13 17:24:55 +01:00
parent 334787ede6
commit cd1e009841
5 changed files with 290 additions and 2 deletions

View File

@@ -67,7 +67,9 @@
"action": "Action | Actions",
"default": "Default",
"locked": "Locked",
"sticky": "Sticky"
"sticky": "Sticky",
"save": "Save",
"create": "Create"
},
"auth": {
"remember_me": "Remember me"
@@ -127,7 +129,10 @@
"search": "Search forum",
"post_edited_success": "Post edited successfully",
"edit_thread": "Edit thread",
"thread_edited_success": "Thread edited successfully"
"thread_edited_success": "Thread edited successfully",
"create_category": "Create forum category",
"edit_category": "Edit forum category",
"category_name": "Category name"
},
"user": {
"username": "Username",
@@ -448,6 +453,7 @@
"password_mismatch": "Passwords do not match"
},
"error": {
"field_required": "This field is required",
"write_more_than_x_chars": "Write more than {0} characters",
"enter_at_least_x_tags": "Enter at least {0} tags",
"select_date": "Select a date",

View File

@@ -165,6 +165,22 @@ const router = createRouter({
},
component: () => import('../views/forum/NewForumThreadView.vue'),
},
{
path: '/forum/category/new',
name: 'NewForumCategory',
meta: {
documentTitle: 'Create forum category',
},
component: () => import('../views/forum/CreateOrEditForumCategoryView.vue'),
},
{
path: '/forum/category/:id/edit',
name: 'EditForumCategory',
meta: {
documentTitle: 'Edit forum category',
},
component: () => import('../views/forum/CreateOrEditForumCategoryView.vue'),
},
{
path: '/wiki/article/:id',
name: 'WikiArticle',

View File

@@ -344,6 +344,10 @@ export interface EditedCssSheet {
'old_name': string;
'preview_image_url': string;
}
export interface EditedForumCategory {
'id': number;
'name': string;
}
export interface EditedForumPost {
'content': string;
'id': number;
@@ -556,6 +560,12 @@ export const Features = {
export type Features = typeof Features[keyof typeof Features];
export interface ForumCategory {
'created_at': string;
'created_by_id': number;
'id': number;
'name': string;
}
export interface ForumCategoryHierarchy {
'id': number;
'name': string;
@@ -1388,6 +1398,7 @@ export interface Torrent {
'trumpable'?: string | null;
'updated_at': string;
'upload_factor': number;
'upload_method': string;
'uploaded_as_anonymous': boolean;
'video_codec'?: VideoCodec | null;
'video_resolution'?: VideoResolution | null;
@@ -1681,6 +1692,7 @@ export interface User {
export interface UserApplication {
'applied_from_ip': string;
'body': string;
'created_at': string;
'email': string;
@@ -1767,6 +1779,9 @@ export interface UserCreatedEditionGroup {
}
export interface UserCreatedForumCategory {
'name': string;
}
export interface UserCreatedForumPost {
'content': string;
'forum_thread_id': number;
@@ -4015,6 +4030,45 @@ export const getTMDBData = async (url: string, options?: RawAxiosRequestConfig):
*/
export const ForumApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @param {UserCreatedForumCategory} userCreatedForumCategory
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createForumCategory: async (userCreatedForumCategory: UserCreatedForumCategory, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'userCreatedForumCategory' is not null or undefined
assertParamExists('createForumCategory', 'userCreatedForumCategory', userCreatedForumCategory)
const localVarPath = `/api/forum/category`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication http required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(userCreatedForumCategory, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {UserCreatedForumPost} userCreatedForumPost
@@ -4093,6 +4147,45 @@ export const ForumApiAxiosParamCreator = function (configuration?: Configuration
options: localVarRequestOptions,
};
},
/**
*
* @param {EditedForumCategory} editedForumCategory
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
editForumCategory: async (editedForumCategory: EditedForumCategory, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'editedForumCategory' is not null or undefined
assertParamExists('editForumCategory', 'editedForumCategory', editedForumCategory)
const localVarPath = `/api/forum/category`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication http required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(editedForumCategory, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {EditedForumPost} editedForumPost
@@ -4330,6 +4423,18 @@ export const ForumApiAxiosParamCreator = function (configuration?: Configuration
export const ForumApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = ForumApiAxiosParamCreator(configuration)
return {
/**
*
* @param {UserCreatedForumCategory} userCreatedForumCategory
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async createForumCategory(userCreatedForumCategory: UserCreatedForumCategory, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ForumCategory>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.createForumCategory(userCreatedForumCategory, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['ForumApi.createForumCategory']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @param {UserCreatedForumPost} userCreatedForumPost
@@ -4354,6 +4459,18 @@ export const ForumApiFp = function(configuration?: Configuration) {
const localVarOperationServerBasePath = operationServerMap['ForumApi.createForumThread']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @param {EditedForumCategory} editedForumCategory
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async editForumCategory(editedForumCategory: EditedForumCategory, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ForumCategory>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.editForumCategory(editedForumCategory, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['ForumApi.editForumCategory']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @param {EditedForumPost} editedForumPost
@@ -4437,6 +4554,15 @@ export const ForumApiFp = function(configuration?: Configuration) {
export const ForumApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = ForumApiFp(configuration)
return {
/**
*
* @param {UserCreatedForumCategory} userCreatedForumCategory
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createForumCategory(userCreatedForumCategory: UserCreatedForumCategory, options?: RawAxiosRequestConfig): AxiosPromise<ForumCategory> {
return localVarFp.createForumCategory(userCreatedForumCategory, options).then((request) => request(axios, basePath));
},
/**
*
* @param {UserCreatedForumPost} userCreatedForumPost
@@ -4455,6 +4581,15 @@ export const ForumApiFactory = function (configuration?: Configuration, basePath
createForumThread(userCreatedForumThread: UserCreatedForumThread, options?: RawAxiosRequestConfig): AxiosPromise<ForumThread> {
return localVarFp.createForumThread(userCreatedForumThread, options).then((request) => request(axios, basePath));
},
/**
*
* @param {EditedForumCategory} editedForumCategory
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
editForumCategory(editedForumCategory: EditedForumCategory, options?: RawAxiosRequestConfig): AxiosPromise<ForumCategory> {
return localVarFp.editForumCategory(editedForumCategory, options).then((request) => request(axios, basePath));
},
/**
*
* @param {EditedForumPost} editedForumPost
@@ -4518,6 +4653,16 @@ export const ForumApiFactory = function (configuration?: Configuration, basePath
* ForumApi - object-oriented interface
*/
export class ForumApi extends BaseAPI {
/**
*
* @param {UserCreatedForumCategory} userCreatedForumCategory
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public createForumCategory(userCreatedForumCategory: UserCreatedForumCategory, options?: RawAxiosRequestConfig) {
return ForumApiFp(this.configuration).createForumCategory(userCreatedForumCategory, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {UserCreatedForumPost} userCreatedForumPost
@@ -4538,6 +4683,16 @@ export class ForumApi extends BaseAPI {
return ForumApiFp(this.configuration).createForumThread(userCreatedForumThread, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {EditedForumCategory} editedForumCategory
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public editForumCategory(editedForumCategory: EditedForumCategory, options?: RawAxiosRequestConfig) {
return ForumApiFp(this.configuration).editForumCategory(editedForumCategory, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {EditedForumPost} editedForumPost
@@ -4606,6 +4761,12 @@ export const forumApi = new ForumApi(undefined, undefined, globalAxios);
export const createForumCategory = async (userCreatedForumCategory: UserCreatedForumCategory, options?: RawAxiosRequestConfig): Promise<ForumCategory> => {
const response = await forumApi.createForumCategory(userCreatedForumCategory, options);
return response.data;
};
export const createForumPost = async (userCreatedForumPost: UserCreatedForumPost, options?: RawAxiosRequestConfig): Promise<ForumPost> => {
const response = await forumApi.createForumPost(userCreatedForumPost, options);
return response.data;
@@ -4618,6 +4779,12 @@ export const createForumThread = async (userCreatedForumThread: UserCreatedForum
};
export const editForumCategory = async (editedForumCategory: EditedForumCategory, options?: RawAxiosRequestConfig): Promise<ForumCategory> => {
const response = await forumApi.editForumCategory(editedForumCategory, options);
return response.data;
};
export const editForumPost = async (editedForumPost: EditedForumPost, options?: RawAxiosRequestConfig): Promise<ForumPost> => {
const response = await forumApi.editForumPost(editedForumPost, options);
return response.data;

View File

@@ -0,0 +1,93 @@
<template>
<ContentContainer :title="isEditMode ? t('forum.edit_category') : t('forum.create_category')">
<Form v-slot="$form" :initialValues="formData" :resolver @submit="onFormSubmit">
<FloatLabel variant="in">
<InputText v-model="formData.name" name="name" :class="{ 'p-invalid': $form.name?.invalid }" />
<label for="name">{{ t('forum.category_name') }}</label>
</FloatLabel>
<Message v-if="$form.name?.invalid" severity="error">
{{ $form.name.error?.message }}
</Message>
<div class="actions">
<Button type="submit" :label="isEditMode ? t('general.save') : t('general.create')" :loading="loading" />
</div>
</Form>
</ContentContainer>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { Button, FloatLabel, InputText, Message } from 'primevue'
import { Form, type FormResolverOptions, type FormSubmitEvent } from '@primevue/forms'
import ContentContainer from '@/components/ContentContainer.vue'
import { createForumCategory, editForumCategory, getForum, type EditedForumCategory, type UserCreatedForumCategory } from '@/services/api-schema/api'
import { showToast } from '@/main'
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const isEditMode = computed(() => route.path.includes('/edit'))
const loading = ref(false)
const formData = ref<UserCreatedForumCategory | EditedForumCategory>({
name: '',
})
const resolver = ({ values }: FormResolverOptions) => {
const errors: Partial<Record<keyof UserCreatedForumCategory, { message: string }[]>> = {}
if (!values.name || values.name.trim().length === 0) {
errors.name = [{ message: t('error.field_required') }]
} else if (values.name.trim().length < 2) {
errors.name = [{ message: t('error.write_more_than_x_chars', [1]) }]
}
return { errors }
}
const onFormSubmit = async ({ valid }: FormSubmitEvent) => {
if (!valid) return
loading.value = true
try {
if (isEditMode.value) {
await editForumCategory(formData.value as EditedForumCategory)
} else {
await createForumCategory(formData.value as UserCreatedForumCategory)
}
router.push('/forum')
} catch {
loading.value = false
}
}
onMounted(async () => {
if (isEditMode.value) {
const categoryId = Number(route.params.id)
// Fetch forum overview to get the category data
const forumOverview = await getForum()
const category = forumOverview.forum_categories.find((cat) => cat.id === categoryId)
if (category) {
formData.value = {
id: category.id,
name: category.name,
}
} else {
showToast('', 'forum category not found', 'error', 2000)
router.push('/forum')
}
}
})
</script>
<style scoped>
.actions {
margin-top: 20px;
display: flex;
gap: 10px;
}
</style>

View File

@@ -13,6 +13,9 @@
<RouterLink to="/forum/search">
<i class="pi pi-search" v-tooltip.top="t('forum.search')" />
</RouterLink>
<RouterLink to="/forum/category/new" v-if="userStore.class === 'staff'">
<i class="pi pi-plus" v-tooltip.top="t('forum.create_category')" />
</RouterLink>
</div>
<ForumCategoryOverview class="forum-category" v-for="category in forumOverview.forum_categories" :key="category.id" :forum-category="category" />
</div>
@@ -27,8 +30,10 @@ import ContentContainer from '@/components/ContentContainer.vue'
import { useI18n } from 'vue-i18n'
import { RouterLink } from 'vue-router'
import { getForum, type ForumOverview } from '@/services/api-schema'
import { useUserStore } from '@/stores/user'
const { t } = useI18n()
const userStore = useUserStore()
const forumOverview = ref<null | ForumOverview>(null)
@@ -44,6 +49,7 @@ onMounted(async () => {
justify-content: center;
> a {
margin: 0 10px;
color: white;
}
}
.forum-category {