mirror of
https://github.com/ershisan99/www.git
synced 2025-12-17 12:34:17 +00:00
Merge branch 'refs/heads/feature/admin/releases'
# Conflicts: # src/server/api/routers/history.ts
This commit is contained in:
3
bun.lock
3
bun.lock
@@ -79,6 +79,7 @@
|
|||||||
"usehooks-ts": "^3.1.1",
|
"usehooks-ts": "^3.1.1",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
|
"zlib": "^1.0.5",
|
||||||
"zod": "^3.24.2",
|
"zod": "^3.24.2",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1137,6 +1138,8 @@
|
|||||||
|
|
||||||
"which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
"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=="],
|
"zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
|
||||||
|
|
||||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||||
|
|||||||
@@ -94,6 +94,7 @@
|
|||||||
"usehooks-ts": "^3.1.1",
|
"usehooks-ts": "^3.1.1",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
|
"zlib": "^1.0.5",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
50
scripts/refresh-history-by-date.ts
Normal file
50
scripts/refresh-history-by-date.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { syncHistoryByDateRange } from '@/server/api/routers/history'
|
||||||
|
|
||||||
|
async function refreshHistoryByDate(startDate?: string, endDate?: string) {
|
||||||
|
try {
|
||||||
|
console.log('Refreshing history by date range...')
|
||||||
|
if (startDate) {
|
||||||
|
console.log(`Start date: ${startDate}`)
|
||||||
|
}
|
||||||
|
if (endDate) {
|
||||||
|
console.log(`End date: ${endDate}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await syncHistoryByDateRange(startDate, endDate)
|
||||||
|
console.log('History refresh completed successfully')
|
||||||
|
} catch (err) {
|
||||||
|
console.error('History refresh failed:', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse command line arguments
|
||||||
|
function parseArgs() {
|
||||||
|
const args = process.argv.slice(2)
|
||||||
|
let startDate: string | undefined
|
||||||
|
let endDate: string | undefined
|
||||||
|
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
if (args[i] === '--start-date' && i + 1 < args.length) {
|
||||||
|
startDate = args[i + 1]
|
||||||
|
i++
|
||||||
|
} else if (args[i] === '--end-date' && i + 1 < args.length) {
|
||||||
|
endDate = args[i + 1]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { startDate, endDate }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run if called directly
|
||||||
|
if (require.main === module) {
|
||||||
|
const { startDate, endDate } = parseArgs()
|
||||||
|
|
||||||
|
refreshHistoryByDate(startDate, endDate)
|
||||||
|
.then(() => process.exit(0))
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Refresh failed:', err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
}
|
||||||
35
src/app/(home)/admin/releases/page.tsx
Normal file
35
src/app/(home)/admin/releases/page.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { ReleasesClient } from '@/app/(home)/admin/releases/releases-client'
|
||||||
|
import { auth } from '@/server/auth'
|
||||||
|
import { HydrateClient, api } from '@/trpc/server'
|
||||||
|
import { Suspense } from 'react'
|
||||||
|
|
||||||
|
export default async function ReleasesPage() {
|
||||||
|
const session = await auth()
|
||||||
|
const isAdmin = session?.user.role === 'admin'
|
||||||
|
console.log(session)
|
||||||
|
if (!isAdmin) {
|
||||||
|
return (
|
||||||
|
<div className={'container mx-auto pt-8'}>
|
||||||
|
<div className={'prose'}>
|
||||||
|
<h1>Forbidden</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.releases.getReleases.prefetch()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense>
|
||||||
|
<HydrateClient>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'mx-auto flex w-[calc(100%-1rem)] max-w-fd-container flex-col gap-4 pt-16'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ReleasesClient />
|
||||||
|
</div>
|
||||||
|
</HydrateClient>
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
419
src/app/(home)/admin/releases/releases-client.tsx
Normal file
419
src/app/(home)/admin/releases/releases-client.tsx
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
'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, 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,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
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({
|
||||||
|
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 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 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} className='hover:bg-muted/50'>
|
||||||
|
<TableCell className='font-medium'>{release.name}</TableCell>
|
||||||
|
<TableCell>{release.version}</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='text-blue-500 hover:underline'
|
||||||
|
title={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>
|
||||||
|
|
||||||
|
<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'
|
||||||
|
onSubmit={form.handleSubmit((values) => addRelease.mutate(values))}
|
||||||
|
>
|
||||||
|
<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 htmlFor='version'>Version</Label>
|
||||||
|
<Input id='version' {...form.register('version')} />
|
||||||
|
</div>
|
||||||
|
<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 htmlFor='url'>URL</Label>
|
||||||
|
<Input id='url' {...form.register('url')} />
|
||||||
|
</div>
|
||||||
|
<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'>
|
||||||
|
Add new release
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
41
src/app/api/refresh-history-by-date/route.ts
Normal file
41
src/app/api/refresh-history-by-date/route.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { env } from '@/env'
|
||||||
|
import { syncHistoryByDateRange } from '@/server/api/routers/history'
|
||||||
|
import { headers } from 'next/headers'
|
||||||
|
|
||||||
|
const SECURE_TOKEN = env.CRON_SECRET
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const headersList = await headers()
|
||||||
|
const authToken = headersList.get('authorization')?.replace('Bearer ', '')
|
||||||
|
|
||||||
|
if (authToken !== SECURE_TOKEN) {
|
||||||
|
return new Response('unauthorized', { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse request body to get date range parameters
|
||||||
|
const body = await request.json().catch(() => ({}))
|
||||||
|
const startDate = body.start_date
|
||||||
|
const endDate = body.end_date
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('refreshing history by date range...')
|
||||||
|
if (startDate) {
|
||||||
|
console.log(`Start date: ${startDate}`)
|
||||||
|
}
|
||||||
|
if (endDate) {
|
||||||
|
console.log(`End date: ${endDate}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await syncHistoryByDateRange(startDate, endDate)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('history refresh by date range failed:', err)
|
||||||
|
return new Response('internal error', { status: 500 })
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({ success: true })
|
||||||
|
} catch (err) {
|
||||||
|
console.error('refresh failed:', err)
|
||||||
|
return new Response('internal error', { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/app/api/releases/route.ts
Normal file
8
src/app/api/releases/route.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { db } from '@/server/db'
|
||||||
|
import { releases } from '@/server/db/schema'
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const res = await db.select().from(releases)
|
||||||
|
|
||||||
|
return Response.json(res)
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { discord_router } from '@/server/api/routers/discord'
|
|||||||
import { history_router } from '@/server/api/routers/history'
|
import { history_router } from '@/server/api/routers/history'
|
||||||
import { leaderboard_router } from '@/server/api/routers/leaderboard'
|
import { leaderboard_router } from '@/server/api/routers/leaderboard'
|
||||||
import { playerStateRouter } from '@/server/api/routers/player-state'
|
import { playerStateRouter } from '@/server/api/routers/player-state'
|
||||||
|
import { releasesRouter } from '@/server/api/routers/releases'
|
||||||
import { createCallerFactory, createTRPCRouter } from '@/server/api/trpc'
|
import { createCallerFactory, createTRPCRouter } from '@/server/api/trpc'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,6 +15,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
discord: discord_router,
|
discord: discord_router,
|
||||||
leaderboard: leaderboard_router,
|
leaderboard: leaderboard_router,
|
||||||
playerState: playerStateRouter,
|
playerState: playerStateRouter,
|
||||||
|
releases: releasesRouter,
|
||||||
})
|
})
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
@@ -91,9 +91,6 @@ export const history_router = createTRPCRouter({
|
|||||||
groupBy,
|
groupBy,
|
||||||
}))
|
}))
|
||||||
}),
|
}),
|
||||||
sync: publicProcedure.mutation(async () => {
|
|
||||||
return syncHistory()
|
|
||||||
}),
|
|
||||||
user_games: publicProcedure
|
user_games: publicProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -107,6 +104,19 @@ export const history_router = createTRPCRouter({
|
|||||||
.where(eq(player_games.playerId, input.user_id))
|
.where(eq(player_games.playerId, input.user_id))
|
||||||
.orderBy(desc(player_games.gameNum))
|
.orderBy(desc(player_games.gameNum))
|
||||||
}),
|
}),
|
||||||
|
sync: publicProcedure.mutation(async () => {
|
||||||
|
return syncHistory()
|
||||||
|
}),
|
||||||
|
syncByDateRange: publicProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
start_date: z.string().optional(),
|
||||||
|
end_date: z.string().optional(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
return syncHistoryByDateRange(input.start_date, input.end_date)
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function syncHistory() {
|
export async function syncHistory() {
|
||||||
@@ -164,6 +174,36 @@ export async function syncHistory() {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function syncHistoryByDateRange(
|
||||||
|
start_date?: string,
|
||||||
|
end_date?: string
|
||||||
|
) {
|
||||||
|
const searchParams: Record<string, string> = {}
|
||||||
|
|
||||||
|
if (start_date) {
|
||||||
|
searchParams.start_date = start_date
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end_date) {
|
||||||
|
searchParams.end_date = end_date
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await ky
|
||||||
|
.get('https://api.neatqueue.com/api/history/1226193436521267223', {
|
||||||
|
searchParams,
|
||||||
|
timeout: 60000,
|
||||||
|
})
|
||||||
|
.json<any>()
|
||||||
|
|
||||||
|
const chunkedData = chunk(data.data, 100)
|
||||||
|
for (const chunk of chunkedData) {
|
||||||
|
await insertGameHistory(chunk).catch((e) => {
|
||||||
|
console.error(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
function processGameEntry(gameId: number, game_num: number, entry: any) {
|
function processGameEntry(gameId: number, game_num: number, entry: any) {
|
||||||
const parsedEntry = typeof entry === 'string' ? JSON.parse(entry) : entry
|
const parsedEntry = typeof entry === 'string' ? JSON.parse(entry) : entry
|
||||||
if (parsedEntry.game === '1v1-attrition') {
|
if (parsedEntry.game === '1v1-attrition') {
|
||||||
|
|||||||
83
src/server/api/routers/releases.ts
Normal file
83
src/server/api/routers/releases.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import {
|
||||||
|
adminProcedure,
|
||||||
|
createTRPCRouter,
|
||||||
|
publicProcedure,
|
||||||
|
} from '@/server/api/trpc'
|
||||||
|
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 () => {
|
||||||
|
const res = await db.select().from(releases)
|
||||||
|
return res
|
||||||
|
}),
|
||||||
|
addRelease: adminProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
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
|
||||||
|
.insert(releases)
|
||||||
|
.values({
|
||||||
|
version: input.version,
|
||||||
|
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,3 +131,17 @@ export const protectedProcedure = t.procedure
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const adminProcedure = t.procedure
|
||||||
|
.use(timingMiddleware)
|
||||||
|
.use(({ ctx, next }) => {
|
||||||
|
if (!ctx.session?.user || ctx.session.user.role !== 'admin') {
|
||||||
|
throw new TRPCError({ code: 'FORBIDDEN' })
|
||||||
|
}
|
||||||
|
return next({
|
||||||
|
ctx: {
|
||||||
|
// infers the `session` as non-nullable
|
||||||
|
session: { ...ctx.session, user: ctx.session.user },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -124,3 +124,18 @@ export const verificationTokens = pgTable(
|
|||||||
}),
|
}),
|
||||||
(t) => [primaryKey({ columns: [t.identifier, t.token] })]
|
(t) => [primaryKey({ columns: [t.identifier, t.token] })]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const releases = pgTable('mod_release', {
|
||||||
|
id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
|
||||||
|
name: text('name').notNull(),
|
||||||
|
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()
|
||||||
|
.defaultNow()
|
||||||
|
.$onUpdate(() => new Date()),
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user