mirror of
https://github.com/ershisan99/www.git
synced 2025-12-17 12:34:17 +00:00
wip
This commit is contained in:
3
bun.lock
3
bun.lock
@@ -77,6 +77,7 @@
|
||||
"tailwind-merge": "^3.1.0",
|
||||
"tw-animate-css": "^1.2.5",
|
||||
"vaul": "^1.1.2",
|
||||
"zlib": "^1.0.5",
|
||||
"zod": "^3.24.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1129,6 +1130,8 @@
|
||||
|
||||
"which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
||||
|
||||
"zlib": ["zlib@1.0.5", "", {}, "sha512-40fpE2II+Cd3k8HWTWONfeKE2jL+P42iWJ1zzps5W51qcTsOUKM5Q5m2PFb0CLxlmFAaUuUdJGc3OfZy947v0w=="],
|
||||
|
||||
"zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
"tailwind-merge": "^3.1.0",
|
||||
"tw-animate-css": "^1.2.5",
|
||||
"vaul": "^1.1.2",
|
||||
"zlib": "^1.0.5",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -22,7 +22,11 @@ export default async function ReleasesPage() {
|
||||
return (
|
||||
<Suspense>
|
||||
<HydrateClient>
|
||||
<div className={'container mx-auto pt-8'}>
|
||||
<div
|
||||
className={
|
||||
'mx-auto flex w-[calc(100%-1rem)] max-w-fd-container flex-col gap-4 pt-16'
|
||||
}
|
||||
>
|
||||
<ReleasesClient />
|
||||
</div>
|
||||
</HydrateClient>
|
||||
|
||||
@@ -1,9 +1,34 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -12,79 +37,386 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { api } from '@/trpc/react'
|
||||
import { Pencil, Trash2 } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
export function ReleasesClient() {
|
||||
const utils = api.useUtils()
|
||||
const [releases] = api.releases.getReleases.useSuspenseQuery()
|
||||
const addRelease = api.releases.addRelease.useMutation()
|
||||
const addRelease = api.releases.addRelease.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.releases.getReleases.invalidate()
|
||||
toast.success('Release added successfully')
|
||||
form.reset()
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(`Error adding release: ${error.message}`)
|
||||
},
|
||||
})
|
||||
const updateRelease = api.releases.updateRelease.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.releases.getReleases.invalidate()
|
||||
toast.success('Release updated successfully')
|
||||
setEditDialogOpen(false)
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(`Error updating release: ${error.message}`)
|
||||
},
|
||||
})
|
||||
const deleteRelease = api.releases.deleteRelease.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.releases.getReleases.invalidate()
|
||||
toast.success('Release deleted successfully')
|
||||
setDeleteDialogOpen(false)
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(`Error deleting release: ${error.message}`)
|
||||
},
|
||||
})
|
||||
|
||||
const [smodsVersions, setSmodsVersions] = useState<string[]>(['latest'])
|
||||
const [lovelyVersions, setLovelyVersions] = useState<string[]>(['latest'])
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false)
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||
const [selectedRelease, setSelectedRelease] = useState<
|
||||
(typeof releases)[0] | null
|
||||
>(null)
|
||||
|
||||
const SMODS_RELEASES_URL =
|
||||
'https://api.github.com/repos/Steamodded/smods/releases'
|
||||
const LOVELY_RELEASES_BASE_URL =
|
||||
'https://github.com/ethangreen-dev/lovely-injector/releases'
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch Steamodded versions
|
||||
fetch(SMODS_RELEASES_URL)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
const versions = data.map((release: any) => release.tag_name)
|
||||
setSmodsVersions(['latest', ...versions])
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error fetching Steamodded versions:', error)
|
||||
})
|
||||
|
||||
// Fetch lovely injector versions
|
||||
// Since we don't have a direct API for lovely injector, we'll use GitHub API
|
||||
fetch(
|
||||
'https://api.github.com/repos/ethangreen-dev/lovely-injector/releases'
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
const versions = data.map((release: any) => release.tag_name)
|
||||
setLovelyVersions(['latest', ...versions])
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error fetching lovely injector versions:', error)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
version: '',
|
||||
description: '',
|
||||
url: '',
|
||||
smods_version: 'latest',
|
||||
lovely_version: 'latest',
|
||||
},
|
||||
})
|
||||
|
||||
const editForm = useForm({
|
||||
defaultValues: {
|
||||
id: 0,
|
||||
name: '',
|
||||
version: '',
|
||||
description: '',
|
||||
url: '',
|
||||
smods_version: 'latest',
|
||||
lovely_version: 'latest',
|
||||
},
|
||||
})
|
||||
|
||||
const handleEditRelease = (release: (typeof releases)[0]) => {
|
||||
setSelectedRelease(release)
|
||||
editForm.reset({
|
||||
id: release.id,
|
||||
name: release.name,
|
||||
version: release.version,
|
||||
description: release.description || '',
|
||||
url: release.url,
|
||||
smods_version: release.smods_version || 'latest',
|
||||
lovely_version: release.lovely_version || 'latest',
|
||||
})
|
||||
setEditDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleDeleteRelease = (release: (typeof releases)[0]) => {
|
||||
setSelectedRelease(release)
|
||||
setDeleteDialogOpen(true)
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h1 className={'text-2xl'}>Releases</h1>
|
||||
<div className={'mt-4'}>
|
||||
<Table className={'w-full table-auto'}>
|
||||
<div className='space-y-8'>
|
||||
<h1 className='font-bold text-3xl'>Releases</h1>
|
||||
<div className='overflow-hidden rounded-md border shadow-sm'>
|
||||
<Table className='w-full table-auto'>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableRow className='bg-muted/50'>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Version</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
<TableHead>URL</TableHead>
|
||||
<TableHead>Steamodded Version</TableHead>
|
||||
<TableHead>Lovely Injector Version</TableHead>
|
||||
<TableHead className='text-right'>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{releases.map((release) => (
|
||||
<TableRow key={release.id}>
|
||||
<TableCell>{release.name}</TableCell>
|
||||
<TableRow key={release.id} className='hover:bg-muted/50'>
|
||||
<TableCell className='font-medium'>{release.name}</TableCell>
|
||||
<TableCell>{release.version}</TableCell>
|
||||
<TableCell>{release.description}</TableCell>
|
||||
<TableCell>
|
||||
<TableCell className='max-w-xs'>
|
||||
<div className='truncate' title={release.description || ''}>
|
||||
{release.description}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className='max-w-xs'>
|
||||
<a
|
||||
href={release.url}
|
||||
target={'_blank'}
|
||||
rel={'noopener noreferrer'}
|
||||
className={'hover:underline'}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-blue-500 hover:underline'
|
||||
title={release.url}
|
||||
>
|
||||
{release.url}
|
||||
<div className='truncate'>{release.url}</div>
|
||||
</a>
|
||||
</TableCell>
|
||||
<TableCell>{release.smods_version || 'latest'}</TableCell>
|
||||
<TableCell>{release.lovely_version || 'latest'}</TableCell>
|
||||
<TableCell className='space-x-2 text-right'>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => handleEditRelease(release)}
|
||||
className='inline-flex items-center'
|
||||
>
|
||||
<Pencil className='mr-1 h-4 w-4' />
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant='destructive'
|
||||
size='sm'
|
||||
onClick={() => handleDeleteRelease(release)}
|
||||
className='inline-flex items-center text-white'
|
||||
>
|
||||
<Trash2 className='mr-1 h-4 w-4' />
|
||||
Delete
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<Card className={'mt-8 max-w-xl'}>
|
||||
<CardContent>
|
||||
|
||||
<div className='mt-8 overflow-hidden rounded-md border bg-card shadow-sm'>
|
||||
<div className='p-6'>
|
||||
<h2 className='mb-4 font-semibold text-2xl'>Add New Release</h2>
|
||||
<form
|
||||
className={'space-y-4'}
|
||||
className='space-y-4'
|
||||
onSubmit={form.handleSubmit((values) => addRelease.mutate(values))}
|
||||
>
|
||||
<div className={'grid grid-cols-1 gap-2'}>
|
||||
<Label>Title</Label>
|
||||
<Input {...form.register('name')} />
|
||||
<div className='grid grid-cols-1 gap-2'>
|
||||
<Label htmlFor='name'>Title</Label>
|
||||
<Input id='name' {...form.register('name')} />
|
||||
</div>
|
||||
<div className={'grid grid-cols-1 gap-2'}>
|
||||
<Label>Version</Label>
|
||||
<Input {...form.register('version')} />
|
||||
<div className='grid grid-cols-1 gap-2'>
|
||||
<Label htmlFor='version'>Version</Label>
|
||||
<Input id='version' {...form.register('version')} />
|
||||
</div>
|
||||
<div className={'grid grid-cols-1 gap-2'}>
|
||||
<Label>Description</Label>
|
||||
<Input {...form.register('description')} />
|
||||
<div className='grid grid-cols-1 gap-2'>
|
||||
<Label htmlFor='description'>Description</Label>
|
||||
<Textarea id='description' {...form.register('description')} />
|
||||
</div>
|
||||
<div className={'grid grid-cols-1 gap-2'}>
|
||||
<Label>URL</Label>
|
||||
<Input {...form.register('url')} />
|
||||
<div className='grid grid-cols-1 gap-2'>
|
||||
<Label htmlFor='url'>URL</Label>
|
||||
<Input id='url' {...form.register('url')} />
|
||||
</div>
|
||||
<Button type={'submit'}>Add new release</Button>
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
<div className='grid grid-cols-1 gap-2'>
|
||||
<Label htmlFor='smods_version'>Steamodded Version</Label>
|
||||
<Select
|
||||
defaultValue='latest'
|
||||
onValueChange={(value) =>
|
||||
form.setValue('smods_version', value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger id='smods_version'>
|
||||
<SelectValue placeholder='Select Steamodded version' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{smodsVersions.map((version) => (
|
||||
<SelectItem key={version} value={version}>
|
||||
{version}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className='grid grid-cols-1 gap-2'>
|
||||
<Label htmlFor='lovely_version'>Lovely Injector Version</Label>
|
||||
<Select
|
||||
defaultValue='latest'
|
||||
onValueChange={(value) =>
|
||||
form.setValue('lovely_version', value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger id='lovely_version'>
|
||||
<SelectValue placeholder='Select Lovely Injector version' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{lovelyVersions.map((version) => (
|
||||
<SelectItem key={version} value={version}>
|
||||
{version}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type='submit'
|
||||
className='w-full bg-primary text-white hover:bg-primary/90'
|
||||
>
|
||||
Add new release
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Edit Release Dialog */}
|
||||
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
|
||||
<DialogContent className='sm:max-w-[600px]'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Release</DialogTitle>
|
||||
<DialogDescription>
|
||||
Make changes to the release. Click save when you're done.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form
|
||||
onSubmit={editForm.handleSubmit((values) =>
|
||||
updateRelease.mutate(values)
|
||||
)}
|
||||
>
|
||||
<div className='grid gap-4 py-4'>
|
||||
<div className='grid grid-cols-1 gap-2'>
|
||||
<Label htmlFor='edit-name'>Title</Label>
|
||||
<Input id='edit-name' {...editForm.register('name')} />
|
||||
</div>
|
||||
<div className='grid grid-cols-1 gap-2'>
|
||||
<Label htmlFor='edit-version'>Version</Label>
|
||||
<Input id='edit-version' {...editForm.register('version')} />
|
||||
</div>
|
||||
<div className='grid grid-cols-1 gap-2'>
|
||||
<Label htmlFor='edit-description'>Description</Label>
|
||||
<Textarea
|
||||
id='edit-description'
|
||||
{...editForm.register('description')}
|
||||
/>
|
||||
</div>
|
||||
<div className='grid grid-cols-1 gap-2'>
|
||||
<Label htmlFor='edit-url'>URL</Label>
|
||||
<Input id='edit-url' {...editForm.register('url')} />
|
||||
</div>
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
<div className='grid grid-cols-1 gap-2'>
|
||||
<Label htmlFor='edit-smods_version'>Steamodded Version</Label>
|
||||
<Select
|
||||
value={editForm.watch('smods_version')}
|
||||
onValueChange={(value) =>
|
||||
editForm.setValue('smods_version', value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger id='edit-smods_version'>
|
||||
<SelectValue placeholder='Select Steamodded version' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{smodsVersions.map((version) => (
|
||||
<SelectItem key={version} value={version}>
|
||||
{version}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className='grid grid-cols-1 gap-2'>
|
||||
<Label htmlFor='edit-lovely_version'>
|
||||
Lovely Injector Version
|
||||
</Label>
|
||||
<Select
|
||||
value={editForm.watch('lovely_version')}
|
||||
onValueChange={(value) =>
|
||||
editForm.setValue('lovely_version', value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger id='edit-lovely_version'>
|
||||
<SelectValue placeholder='Select Lovely Injector version' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{lovelyVersions.map((version) => (
|
||||
<SelectItem key={version} value={version}>
|
||||
{version}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
onClick={() => setEditDialogOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type='submit'>Save changes</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
release
|
||||
{selectedRelease && <strong> "{selectedRelease.name}"</strong>}.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className='bg-red-600 text-white hover:bg-red-700'
|
||||
onClick={() =>
|
||||
selectedRelease &&
|
||||
deleteRelease.mutate({ id: selectedRelease.id })
|
||||
}
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import { db } from '@/server/db'
|
||||
import { releases } from '@/server/db/schema'
|
||||
import { z } from 'zod'
|
||||
import { eq } from 'drizzle-orm'
|
||||
|
||||
export const releasesRouter = createTRPCRouter({
|
||||
getReleases: publicProcedure.query(async () => {
|
||||
@@ -19,6 +20,8 @@ export const releasesRouter = createTRPCRouter({
|
||||
url: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
smods_version: z.string().default('latest'),
|
||||
lovely_version: z.string().default('latest'),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
@@ -29,9 +32,52 @@ export const releasesRouter = createTRPCRouter({
|
||||
url: input.url,
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
smods_version: input.smods_version,
|
||||
lovely_version: input.lovely_version,
|
||||
})
|
||||
.returning()
|
||||
|
||||
return res[0]
|
||||
}),
|
||||
updateRelease: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
version: z.string(),
|
||||
url: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
smods_version: z.string().default('latest'),
|
||||
lovely_version: z.string().default('latest'),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const res = await db
|
||||
.update(releases)
|
||||
.set({
|
||||
version: input.version,
|
||||
url: input.url,
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
smods_version: input.smods_version,
|
||||
lovely_version: input.lovely_version,
|
||||
})
|
||||
.where(eq(releases.id, input.id))
|
||||
.returning()
|
||||
|
||||
return res[0]
|
||||
}),
|
||||
deleteRelease: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
await db
|
||||
.delete(releases)
|
||||
.where(eq(releases.id, input.id))
|
||||
|
||||
return { success: true }
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -131,6 +131,8 @@ export const releases = pgTable('mod_release', {
|
||||
description: text('description'),
|
||||
version: text('version').notNull(),
|
||||
url: text('url').notNull(),
|
||||
smods_version: text('smods_version').default('latest'),
|
||||
lovely_version: text('lovely_version').default('latest'),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at')
|
||||
.notNull()
|
||||
|
||||
Reference in New Issue
Block a user