Added pretty much everything except discussion forums

This commit is contained in:
2026-03-01 11:00:38 -05:00
parent a239b7e204
commit 5cdbf72328
15 changed files with 917 additions and 1 deletions

View 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>

View 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>

View 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>