Added pretty much everything except discussion forums
This commit is contained in:
241
ui/src/components/modRequests/ModRequestForm.vue
Normal file
241
ui/src/components/modRequests/ModRequestForm.vue
Normal file
@@ -0,0 +1,241 @@
|
||||
<script setup lang="ts">
|
||||
import { ModRequestSchema } from '@shared/schemas/modRequest'
|
||||
import { useForm, Field as VeeField } from 'vee-validate'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { ref } from 'vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import Field from '@/components/ui/field/Field.vue'
|
||||
import FieldLabel from '@/components/ui/field/FieldLabel.vue'
|
||||
import FieldError from '@/components/ui/field/FieldError.vue'
|
||||
import FieldDescription from '@/components/ui/field/FieldDescription.vue'
|
||||
import Input from '@/components/ui/input/Input.vue'
|
||||
import Textarea from '@/components/ui/textarea/Textarea.vue'
|
||||
import Checkbox from '@/components/ui/checkbox/Checkbox.vue'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import { postModRequest } from '@/api/modRequests'
|
||||
|
||||
const { handleSubmit, resetForm, errors } = useForm({
|
||||
validationSchema: toTypedSchema(ModRequestSchema),
|
||||
validateOnMount: false,
|
||||
initialValues: {
|
||||
mod_title: '',
|
||||
description: '',
|
||||
mod_link: '',
|
||||
confirmed_tested: false,
|
||||
reason: '',
|
||||
detrimental_effects: '',
|
||||
keybind_conflicts: '',
|
||||
special_considerations: '',
|
||||
},
|
||||
})
|
||||
|
||||
const submitting = ref(false)
|
||||
const emit = defineEmits(['submit', 'close'])
|
||||
|
||||
const submitForm = handleSubmit(async (values) => {
|
||||
if (submitting.value) return
|
||||
submitting.value = true
|
||||
try {
|
||||
await postModRequest(values);
|
||||
emit('submit', values)
|
||||
} catch (err) {
|
||||
console.error('Error submitting mod request:', err)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card class="border-0 shadow-sm">
|
||||
<CardHeader>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<CardTitle>New Mod Request</CardTitle>
|
||||
<CardDescription>Share details about the mod you'd like to see added to our server</CardDescription>
|
||||
</div>
|
||||
<Button type="button" variant="ghost" @click="emit('close')"
|
||||
class="text-muted-foreground hover:text-foreground -mt-1 -mr-2">
|
||||
Back to posts <X></X>
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<Separator class="mb-0" />
|
||||
|
||||
<CardContent>
|
||||
<form @submit.prevent="submitForm" class="space-y-8">
|
||||
<!-- SECTION: Basic Mod Information -->
|
||||
<div class="space-y-5">
|
||||
<div class="space-y-1">
|
||||
<h3 class="font-semibold text-sm text-foreground">Mod Information</h3>
|
||||
<p class="text-xs text-muted-foreground">Core details about the mod</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-5">
|
||||
<!-- Title -->
|
||||
<VeeField name="mod_title" v-slot="{ field, errors: e }">
|
||||
<Field :data-invalid="!!e.length">
|
||||
<FieldLabel class="text-sm font-medium">Mod Title</FieldLabel>
|
||||
<Input v-bind="field" placeholder="Name of the mod" rows="4" class="resize-none mt-2" />
|
||||
<div class="h-4">
|
||||
<FieldError v-if="e.length" :errors="e" />
|
||||
</div>
|
||||
</Field>
|
||||
</VeeField>
|
||||
|
||||
<!-- Description -->
|
||||
<VeeField name="description" v-slot="{ field, errors: e }">
|
||||
<Field :data-invalid="!!e.length">
|
||||
<FieldLabel class="text-sm font-medium">What is this mod?</FieldLabel>
|
||||
<FieldDescription class="text-xs">Brief overview of the mod and its main functionality
|
||||
</FieldDescription>
|
||||
<Textarea v-bind="field" placeholder="Describe what this mod does..." rows="4"
|
||||
class="resize-none mt-2" />
|
||||
<div class="h-4">
|
||||
<FieldError v-if="e.length" :errors="e" />
|
||||
</div>
|
||||
</Field>
|
||||
</VeeField>
|
||||
|
||||
<!-- Mod Link -->
|
||||
<VeeField name="mod_link" v-slot="{ field, errors: e }">
|
||||
<Field :data-invalid="!!e.length">
|
||||
<FieldLabel class="text-sm font-medium">Mod Link</FieldLabel>
|
||||
<FieldDescription class="text-xs">Where can this mod be found?</FieldDescription>
|
||||
<Input v-bind="field" placeholder="https://..." class="mt-2" />
|
||||
<div class="h-4">
|
||||
<FieldError v-if="e.length" :errors="e" />
|
||||
</div>
|
||||
</Field>
|
||||
</VeeField>
|
||||
|
||||
<!-- Reason -->
|
||||
<VeeField name="reason" v-slot="{ field, errors: e }">
|
||||
<Field :data-invalid="!!e.length">
|
||||
<FieldLabel class="text-sm font-medium">Why add this mod?</FieldLabel>
|
||||
<FieldDescription class="text-xs">What benefits does this mod bring to our community and
|
||||
why should we consider it?
|
||||
</FieldDescription>
|
||||
<Textarea v-bind="field" placeholder="Share your thoughts..." rows="3"
|
||||
class="resize-none mt-2" />
|
||||
<div class="h-4">
|
||||
<FieldError v-if="e.length" :errors="e" />
|
||||
</div>
|
||||
</Field>
|
||||
</VeeField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<!-- SECTION: Testing & Verification -->
|
||||
<div class="space-y-5">
|
||||
<div class="space-y-1">
|
||||
<h3 class="font-semibold text-sm text-foreground">Testing & Verification</h3>
|
||||
<p class="text-xs text-muted-foreground">Your experience with this mod</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-5">
|
||||
<!-- Confirmed Tested -->
|
||||
<VeeField name="confirmed_tested" v-slot="{ field, errors: e }">
|
||||
<Field :data-invalid="!!e.length">
|
||||
<div class="flex items-center gap-3 p-3 rounded-md bg-muted/30 border border-border/50">
|
||||
<Checkbox :model-value="field.value" @update:model-value="field.onChange"
|
||||
class="hover:cursor-pointer" />
|
||||
<div class="flex-1">
|
||||
<FieldLabel class="font-medium text-md cursor-pointer">Testing & Stability
|
||||
Confirmation
|
||||
</FieldLabel>
|
||||
<FieldDescription class="text-sm">I confirm that I have personally tested this
|
||||
mod and to the best of my ability have verified that it functions as
|
||||
described without
|
||||
causing game-breaking bugs,
|
||||
critical stability issues, or unintended performance degradation.
|
||||
</FieldDescription>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-4">
|
||||
<FieldError v-if="e.length" :errors="e" />
|
||||
</div>
|
||||
</Field>
|
||||
</VeeField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<!-- SECTION: Compatibility -->
|
||||
<div class="space-y-5">
|
||||
<div class="space-y-1">
|
||||
<h3 class="font-semibold text-sm text-foreground">Compatibility & Conflicts</h3>
|
||||
<p class="text-xs text-muted-foreground">How does it work with our current setup?</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-5">
|
||||
<!-- Detrimental Effects -->
|
||||
<VeeField name="detrimental_effects" v-slot="{ field, errors: e }">
|
||||
<Field :data-invalid="!!e.length">
|
||||
<FieldLabel class="text-sm font-medium">Potential Issues</FieldLabel>
|
||||
<FieldDescription class="text-xs">Any negative impacts or concerns you noticed (Keybind
|
||||
conflicts sould be noted in the dedicated section)?
|
||||
</FieldDescription>
|
||||
<Textarea v-bind="field" placeholder="List any issues... (leave blank if none)" rows="3"
|
||||
class="resize-none mt-2" />
|
||||
<div class="h-4">
|
||||
<FieldError v-if="e.length" :errors="e" />
|
||||
</div>
|
||||
</Field>
|
||||
</VeeField>
|
||||
|
||||
<!-- Keybind Conflicts -->
|
||||
<VeeField name="keybind_conflicts" v-slot="{ field, errors: e }">
|
||||
<Field :data-invalid="!!e.length">
|
||||
<FieldLabel class="text-sm font-medium">Keybind Conflicts</FieldLabel>
|
||||
<FieldDescription class="text-xs">Identify any controls that conflict with the existing
|
||||
modpack along with resolutions for those conflicts.
|
||||
</FieldDescription>
|
||||
<Textarea v-bind="field"
|
||||
placeholder='List specific conflicts and resolutions here, or type "None" if there are no conflicts.'
|
||||
rows="3" class="resize-none mt-2" />
|
||||
<div class="h-4">
|
||||
<FieldError v-if="e.length" :errors="e" />
|
||||
</div>
|
||||
</Field>
|
||||
</VeeField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<!-- SECTION: Additional Notes -->
|
||||
<div class="space-y-5">
|
||||
<div class="space-y-5">
|
||||
<!-- Special Considerations -->
|
||||
<VeeField name="special_considerations" v-slot="{ field, errors: e }">
|
||||
<Field :data-invalid="!!e.length">
|
||||
<FieldLabel class="text-sm font-medium">Additional Information</FieldLabel>
|
||||
<FieldDescription class="text-xs">Anything else we should know?</FieldDescription>
|
||||
<Textarea v-bind="field" placeholder="Add any other important notes... (optional)"
|
||||
rows="4" class="resize-none mt-2" />
|
||||
<div class="h-4">
|
||||
<FieldError v-if="e.length" :errors="e" />
|
||||
</div>
|
||||
</Field>
|
||||
</VeeField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex gap-2 justify-end pt-4">
|
||||
<Button type="button" variant="outline" @click="resetForm">Clear</Button>
|
||||
<Button type="submit" :disabled="submitting">
|
||||
{{ submitting ? 'Submitting...' : 'Submit Request' }}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</template>
|
||||
125
ui/src/components/modRequests/ModRequestList.vue
Normal file
125
ui/src/components/modRequests/ModRequestList.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import type { DiscussionPost } from '@shared/types/discussion';
|
||||
import type { ModRequest } from '@shared/schemas/modRequest';
|
||||
import type { PagedData, pagination } from '@shared/types/pagination';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import Badge from '@/components/ui/badge/Badge.vue';
|
||||
import { getModRequests } from '@/api/modRequests';
|
||||
import Pagination from '../ui/pagination/Pagination.vue';
|
||||
import PaginationContent from '../ui/pagination/PaginationContent.vue';
|
||||
import PaginationPrevious from '../ui/pagination/PaginationPrevious.vue';
|
||||
import PaginationItem from '../ui/pagination/PaginationItem.vue';
|
||||
import PaginationEllipsis from '../ui/pagination/PaginationEllipsis.vue';
|
||||
import PaginationNext from '../ui/pagination/PaginationNext.vue';
|
||||
import { Lock } from 'lucide-vue-next';
|
||||
|
||||
const requests = ref<DiscussionPost<ModRequest>[]>([]);
|
||||
const loading = ref(true);
|
||||
|
||||
async function loadRequests() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await getModRequests(pageNum.value, pageSize.value);
|
||||
|
||||
requests.value = response.data;
|
||||
pageData.value = response.pagination;
|
||||
} catch (error) {
|
||||
console.error("Failed to load requests:", error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRequests();
|
||||
});
|
||||
|
||||
const pageNum = ref<number>(1);
|
||||
const pageData = ref<pagination>();
|
||||
|
||||
const pageSize = ref<number>(15)
|
||||
const pageSizeOptions = [10, 15, 30]
|
||||
|
||||
function setPageSize(size: number) {
|
||||
pageSize.value = size;
|
||||
pageNum.value = 1;
|
||||
loadRequests();
|
||||
}
|
||||
|
||||
function setPage(pagenum: number) {
|
||||
pageNum.value = pagenum;
|
||||
loadRequests();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mod-request-list">
|
||||
<Table v-if="!loading">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="w-1/2">Title</TableHead>
|
||||
<TableHead class="w-1/4">Creator</TableHead>
|
||||
<TableHead class="w-1/4">Date</TableHead>
|
||||
<TableHead class="w-1/5 text-right">Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="post in requests" :key="post.id"
|
||||
class="hover:bg-muted/50 cursor-pointer transition-colors"
|
||||
@click="$router.push(`/discussions/mod-requests/${post.id}`)">
|
||||
<TableCell class="font-medium">{{ post.title }}</TableCell>
|
||||
<TableCell>{{ post.poster_name }}</TableCell>
|
||||
<TableCell class="text-muted-foreground">{{ new Date(post.created_at).toLocaleDateString() }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right">
|
||||
<Badge :variant="post.is_open ? 'secondary' : 'outline'">
|
||||
{{ post.is_open ? 'Open' : 'Locked' }}
|
||||
<Lock v-if="!post.is_open" />
|
||||
</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div v-else class="flex items-center justify-center py-12">
|
||||
<p class="text-muted-foreground">Loading requests...</p>
|
||||
</div>
|
||||
<div class="mt-5 flex justify-between">
|
||||
<div></div>
|
||||
<Pagination v-slot="{ page }" :items-per-page="pageData?.pageSize || 10" :total="pageData?.total || 10"
|
||||
:default-page="2" :page="pageNum" @update:page="setPage">
|
||||
<PaginationContent v-slot="{ items }">
|
||||
<PaginationPrevious />
|
||||
<template v-for="(item, index) in items" :key="index">
|
||||
<PaginationItem v-if="item.type === 'page'" :value="item.value"
|
||||
:is-active="item.value === page">
|
||||
{{ item.value }}
|
||||
</PaginationItem>
|
||||
</template>
|
||||
<PaginationEllipsis :index="4" />
|
||||
<PaginationNext />
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
<div class="flex items-center gap-3 text-sm">
|
||||
<p class="text-muted-foreground text-nowrap">Per page:</p>
|
||||
|
||||
<button v-for="size in pageSizeOptions" :key="size" @click="setPageSize(size)"
|
||||
class="px-2 py-1 rounded transition-colors" :class="{
|
||||
'bg-muted font-semibold': pageSize === size,
|
||||
'hover:bg-muted/50': pageSize !== size
|
||||
}">
|
||||
{{ size }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
107
ui/src/components/modRequests/ViewModRequest.vue
Normal file
107
ui/src/components/modRequests/ViewModRequest.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import type { DiscussionPost } from '@shared/types/discussion';
|
||||
import type { ModRequest } from '@shared/schemas/modRequest';
|
||||
import { getModRequest } from '@/api/modRequests';
|
||||
import Button from '@/components/ui/button/Button.vue';
|
||||
import Badge from '@/components/ui/badge/Badge.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const post = ref<DiscussionPost<ModRequest> | null>(null);
|
||||
const loading = ref(true);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
async function loadPost() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const id = Number(route.params.id);
|
||||
if (isNaN(id)) {
|
||||
error.value = 'Invalid request ID';
|
||||
return;
|
||||
}
|
||||
post.value = await getModRequest(id);
|
||||
} catch (err: any) {
|
||||
console.error('Failed to fetch mod request', err);
|
||||
error.value = 'Failed to load mod request';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPost();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="max-w-[80rem] mt-5 mx-auto px-2 lg:px-20 w-full">
|
||||
<div class="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="scroll-m-20 text-3xl font-semibold tracking-tight mb-2">Mod Request: {{ post.title }}</h1>
|
||||
<p class="text-muted-foreground" v-if="post">
|
||||
Requested by {{ post.poster_name || 'Unknown' }} on {{ new
|
||||
Date(post.created_at).toLocaleDateString() }}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" @click="router.push('/discussions/mod-requests')">
|
||||
Back to posts
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||
<p class="text-muted-foreground">Loading...</p>
|
||||
</div>
|
||||
<div v-else-if="error" class="text-red-500">{{ error }}</div>
|
||||
<div v-else-if="post" class="space-y-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<Badge :variant="post.is_open ? 'secondary' : 'outline'">
|
||||
{{ post.is_open ? 'Open' : 'Locked' }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="border rounded-lg p-6 bg-muted/50">
|
||||
<h2 class="text-2xl font-semibold mb-2">{{ post.content.mod_title }}</h2>
|
||||
<p class="text-sm text-muted-foreground mb-4"><a :href="post.content.mod_link" target="_blank"
|
||||
class="text-primary underline">{{ post.content.mod_link }}</a></p>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h3 class="font-medium">Description</h3>
|
||||
<p>{{ post.content.description }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium">Reason</h3>
|
||||
<p>{{ post.content.reason }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium">Tested?</h3>
|
||||
<p>{{ post.content.confirmed_tested ? 'Yes' : 'No' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium">Detrimental Effects</h3>
|
||||
<p>{{ post.content.detrimental_effects }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium">Keybind Conflicts</h3>
|
||||
<p>{{ post.content.keybind_conflicts || 'None' }}</p>
|
||||
</div>
|
||||
<div v-if="post.content.special_considerations">
|
||||
<h3 class="font-medium">Additional Notes</h3>
|
||||
<p>{{ post.content.special_considerations }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- discussion placeholder -->
|
||||
<div class="mt-10">
|
||||
<h3 class="text-xl font-semibold">Discussion</h3>
|
||||
<p class="text-muted-foreground">Comments and thread will appear here once implemented.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p class="text-muted-foreground">Unable to locate this mod request.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user