add branches to releases

This commit is contained in:
2025-06-15 14:07:20 +02:00
parent e2dee56c15
commit ff4b07debd
7 changed files with 298 additions and 12 deletions

View File

@@ -17,7 +17,10 @@ export default async function ReleasesPage() {
) )
} }
await api.releases.getReleases.prefetch() await Promise.all([
api.releases.getReleases.prefetch(),
api.branches.getBranches.prefetch(),
])
return ( return (
<Suspense> <Suspense>

View File

@@ -11,7 +11,6 @@ import {
AlertDialogTitle, AlertDialogTitle,
} from '@/components/ui/alert-dialog' } from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -80,8 +79,42 @@ export function ReleasesClient() {
const [smodsVersions, setSmodsVersions] = useState<string[]>(['latest']) const [smodsVersions, setSmodsVersions] = useState<string[]>(['latest'])
const [lovelyVersions, setLovelyVersions] = useState<string[]>(['latest']) const [lovelyVersions, setLovelyVersions] = useState<string[]>(['latest'])
const [newBranch, setNewBranch] = useState<string>('')
// Fetch branches from the database
const [branches] = api.branches.getBranches.useSuspenseQuery()
console.log(branches)
// Add branch mutation
const addBranch = api.branches.addBranch.useMutation({
onSuccess: () => {
utils.branches.getBranches.invalidate()
toast.success('Branch added successfully')
setNewBranch('')
},
onError: (error) => {
toast.error(`Error adding branch: ${error.message}`)
},
})
// Delete branch mutation
const deleteBranch = api.branches.deleteBranch.useMutation({
onSuccess: () => {
utils.branches.getBranches.invalidate()
toast.success('Branch deleted successfully')
},
onError: (error) => {
toast.error(`Error deleting branch: ${error.message}`)
},
})
const handleAddBranch = () => {
if (newBranch && !branches.some((branch) => branch.name === newBranch)) {
addBranch.mutate({ name: newBranch })
}
}
const [editDialogOpen, setEditDialogOpen] = useState(false) const [editDialogOpen, setEditDialogOpen] = useState(false)
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
const [branchManagementOpen, setBranchManagementOpen] = useState(false)
const [selectedRelease, setSelectedRelease] = useState< const [selectedRelease, setSelectedRelease] = useState<
(typeof releases)[0] | null (typeof releases)[0] | null
>(null) >(null)
@@ -126,6 +159,7 @@ export function ReleasesClient() {
url: '', url: '',
smods_version: 'latest', smods_version: 'latest',
lovely_version: 'latest', lovely_version: 'latest',
branchId: 1,
}, },
}) })
@@ -138,6 +172,7 @@ export function ReleasesClient() {
url: '', url: '',
smods_version: 'latest', smods_version: 'latest',
lovely_version: 'latest', lovely_version: 'latest',
branchId: 1,
}, },
}) })
@@ -151,6 +186,7 @@ export function ReleasesClient() {
url: release.url, url: release.url,
smods_version: release.smods_version || 'latest', smods_version: release.smods_version || 'latest',
lovely_version: release.lovely_version || 'latest', lovely_version: release.lovely_version || 'latest',
branchId: release.branchId,
}) })
setEditDialogOpen(true) setEditDialogOpen(true)
} }
@@ -170,6 +206,7 @@ export function ReleasesClient() {
<TableHead>Version</TableHead> <TableHead>Version</TableHead>
<TableHead>Description</TableHead> <TableHead>Description</TableHead>
<TableHead>URL</TableHead> <TableHead>URL</TableHead>
<TableHead>Branch</TableHead>
<TableHead>Steamodded Version</TableHead> <TableHead>Steamodded Version</TableHead>
<TableHead>Lovely Injector Version</TableHead> <TableHead>Lovely Injector Version</TableHead>
<TableHead className='text-right'>Actions</TableHead> <TableHead className='text-right'>Actions</TableHead>
@@ -196,6 +233,7 @@ export function ReleasesClient() {
<div className='truncate'>{release.url}</div> <div className='truncate'>{release.url}</div>
</a> </a>
</TableCell> </TableCell>
<TableCell>{release.branchName || 'main'}</TableCell>
<TableCell>{release.smods_version || 'latest'}</TableCell> <TableCell>{release.smods_version || 'latest'}</TableCell>
<TableCell>{release.lovely_version || 'latest'}</TableCell> <TableCell>{release.lovely_version || 'latest'}</TableCell>
<TableCell className='space-x-2 text-right'> <TableCell className='space-x-2 text-right'>
@@ -247,7 +285,7 @@ export function ReleasesClient() {
<Label htmlFor='url'>URL</Label> <Label htmlFor='url'>URL</Label>
<Input id='url' {...form.register('url')} /> <Input id='url' {...form.register('url')} />
</div> </div>
<div className='grid grid-cols-2 gap-4'> <div className='grid grid-cols-3 gap-4'>
<div className='grid grid-cols-1 gap-2'> <div className='grid grid-cols-1 gap-2'>
<Label htmlFor='smods_version'>Steamodded Version</Label> <Label htmlFor='smods_version'>Steamodded Version</Label>
<Select <Select
@@ -288,6 +326,44 @@ export function ReleasesClient() {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className='grid grid-cols-1 gap-2'>
<Label htmlFor='branch'>Branch</Label>
<div className='flex gap-2'>
<Select
defaultValue={'1'}
onValueChange={(value) =>
form.setValue('branchId', Number(value))
}
>
<SelectTrigger id='branch'>
<SelectValue placeholder='Select branch' />
</SelectTrigger>
<SelectContent>
{branches.map((branch) => (
<SelectItem
key={branch.id}
value={branch.id.toString()}
>
{branch.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
<div className='grid grid-cols-1 gap-2'>
<div className='flex justify-between items-center'>
<Label htmlFor='branch-management'>Branch Management</Label>
<Button
type='button'
variant='outline'
size='sm'
onClick={() => setBranchManagementOpen(true)}
>
Manage Branches
</Button>
</div>
</div> </div>
<Button type='submit' className='w-full'> <Button type='submit' className='w-full'>
Add new release Add new release
@@ -330,7 +406,7 @@ export function ReleasesClient() {
<Label htmlFor='edit-url'>URL</Label> <Label htmlFor='edit-url'>URL</Label>
<Input id='edit-url' {...editForm.register('url')} /> <Input id='edit-url' {...editForm.register('url')} />
</div> </div>
<div className='grid grid-cols-2 gap-4'> <div className='grid grid-cols-3 gap-4'>
<div className='grid grid-cols-1 gap-2'> <div className='grid grid-cols-1 gap-2'>
<Label htmlFor='edit-smods_version'>Steamodded Version</Label> <Label htmlFor='edit-smods_version'>Steamodded Version</Label>
<Select <Select
@@ -373,6 +449,29 @@ export function ReleasesClient() {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className='grid grid-cols-1 gap-2'>
<Label htmlFor='edit-branch'>Branch</Label>
<Select
value={editForm.watch('branchId').toString()}
onValueChange={(value) =>
editForm.setValue('branchId', Number(value))
}
>
<SelectTrigger id='edit-branch'>
<SelectValue placeholder='Select branch' />
</SelectTrigger>
<SelectContent>
{branches.map((branch) => (
<SelectItem
key={branch.id}
value={branch.id.toString()}
>
{branch.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
@@ -414,6 +513,80 @@ export function ReleasesClient() {
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
{/* Branch Management Modal */}
<Dialog open={branchManagementOpen} onOpenChange={setBranchManagementOpen}>
<DialogContent className='sm:max-w-[500px]'>
<DialogHeader>
<DialogTitle>Manage Branches</DialogTitle>
<DialogDescription>
Add new branches or remove existing ones.
</DialogDescription>
</DialogHeader>
<div className='grid gap-4 py-4'>
<div className='grid grid-cols-1 gap-2'>
<Label htmlFor='modal-new-branch'>Add New Branch</Label>
<div className='flex gap-2'>
<Input
id='modal-new-branch'
value={newBranch}
onChange={(e) => setNewBranch(e.target.value)}
placeholder='Enter new branch name'
/>
<Button
type='button'
onClick={handleAddBranch}
disabled={
!newBranch ||
branches.some((branch) => branch.name === newBranch)
}
>
Add
</Button>
</div>
</div>
<div className='grid grid-cols-1 gap-2'>
<Label>Existing Branches</Label>
<div className='max-h-60 overflow-y-auto rounded-md border p-2'>
{branches.length === 0 ? (
<p className='text-muted-foreground text-sm'>
No branches found
</p>
) : (
<ul className='space-y-1'>
{branches.map((branch) => (
<li
key={branch.id}
className='flex items-center justify-between rounded-md px-2 py-1 hover:bg-muted'
>
<span>{branch.name}</span>
{branch.name !== 'main' && (
<Button
variant='ghost'
size='sm'
className='h-7 w-7 p-0 text-destructive hover:bg-destructive/10 hover:text-destructive'
onClick={() =>
deleteBranch.mutate({ id: branch.id })
}
>
<Trash2 className='h-4 w-4' />
<span className='sr-only'>Delete branch</span>
</Button>
)}
</li>
))}
</ul>
)}
</div>
</div>
</div>
<DialogFooter>
<Button type='button' onClick={() => setBranchManagementOpen(false)}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div> </div>
) )
} }

View File

@@ -1,8 +1,24 @@
import { db } from '@/server/db' import { db } from '@/server/db'
import { releases } from '@/server/db/schema' import { branches, releases } from '@/server/db/schema'
import { eq } from 'drizzle-orm'
export async function GET() { export async function GET() {
const res = await db.select().from(releases) const res = await db
.select({
id: releases.id,
name: releases.name,
description: releases.description,
version: releases.version,
url: releases.url,
smods_version: releases.smods_version,
lovely_version: releases.lovely_version,
branchId: releases.branchId,
branchName: branches.name,
createdAt: releases.createdAt,
updatedAt: releases.updatedAt,
})
.from(releases)
.leftJoin(branches, eq(releases.branchId, branches.id))
return Response.json(res) return Response.json(res)
} }

View File

@@ -1,3 +1,4 @@
import { branchesRouter } from '@/server/api/routers/branches'
import { discord_router } from '@/server/api/routers/discord' 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'
@@ -11,6 +12,7 @@ import { createCallerFactory, createTRPCRouter } from '@/server/api/trpc'
* All routers added in /api/routers should be manually added here. * All routers added in /api/routers should be manually added here.
*/ */
export const appRouter = createTRPCRouter({ export const appRouter = createTRPCRouter({
branches: branchesRouter,
history: history_router, history: history_router,
discord: discord_router, discord: discord_router,
leaderboard: leaderboard_router, leaderboard: leaderboard_router,

View File

@@ -0,0 +1,53 @@
import {
adminProcedure,
createTRPCRouter,
publicProcedure,
} from '@/server/api/trpc'
import { db } from '@/server/db'
import { branches } from '@/server/db/schema'
import { z } from 'zod'
import { eq } from 'drizzle-orm'
export const branchesRouter = createTRPCRouter({
getBranches: publicProcedure.query(async () => {
const res = await db.select().from(branches)
return res
}),
addBranch: adminProcedure
.input(
z.object({
name: z.string(),
})
)
.mutation(async ({ input }) => {
try {
const res = await db
.insert(branches)
.values({
name: input.name,
})
.returning()
return res[0]
} catch (error) {
// Handle unique constraint violation
if (error instanceof Error && error.message.includes('unique constraint')) {
throw new Error('Branch with this name already exists')
}
throw error
}
}),
deleteBranch: adminProcedure
.input(
z.object({
id: z.number(),
})
)
.mutation(async ({ input }) => {
await db
.delete(branches)
.where(eq(branches.id, input.id))
return { success: true }
}),
})

View File

@@ -4,13 +4,28 @@ import {
publicProcedure, publicProcedure,
} from '@/server/api/trpc' } from '@/server/api/trpc'
import { db } from '@/server/db' import { db } from '@/server/db'
import { releases } from '@/server/db/schema' import { branches, releases } from '@/server/db/schema'
import { z } from 'zod'
import { eq } from 'drizzle-orm' import { eq } from 'drizzle-orm'
import { z } from 'zod'
export const releasesRouter = createTRPCRouter({ export const releasesRouter = createTRPCRouter({
getReleases: publicProcedure.query(async () => { getReleases: publicProcedure.query(async () => {
const res = await db.select().from(releases) const res = await db
.select({
id: releases.id,
name: releases.name,
description: releases.description,
version: releases.version,
url: releases.url,
smods_version: releases.smods_version,
lovely_version: releases.lovely_version,
branchId: releases.branchId,
branchName: branches.name,
createdAt: releases.createdAt,
updatedAt: releases.updatedAt,
})
.from(releases)
.leftJoin(branches, eq(releases.branchId, branches.id))
return res return res
}), }),
addRelease: adminProcedure addRelease: adminProcedure
@@ -22,6 +37,7 @@ export const releasesRouter = createTRPCRouter({
description: z.string(), description: z.string(),
smods_version: z.string().default('latest'), smods_version: z.string().default('latest'),
lovely_version: z.string().default('latest'), lovely_version: z.string().default('latest'),
branchId: z.number().default(1),
}) })
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
@@ -34,6 +50,7 @@ export const releasesRouter = createTRPCRouter({
description: input.description, description: input.description,
smods_version: input.smods_version, smods_version: input.smods_version,
lovely_version: input.lovely_version, lovely_version: input.lovely_version,
branchId: input.branchId,
}) })
.returning() .returning()
@@ -49,6 +66,7 @@ export const releasesRouter = createTRPCRouter({
description: z.string(), description: z.string(),
smods_version: z.string().default('latest'), smods_version: z.string().default('latest'),
lovely_version: z.string().default('latest'), lovely_version: z.string().default('latest'),
branchId: z.number().default(1),
}) })
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
@@ -61,6 +79,7 @@ export const releasesRouter = createTRPCRouter({
description: input.description, description: input.description,
smods_version: input.smods_version, smods_version: input.smods_version,
lovely_version: input.lovely_version, lovely_version: input.lovely_version,
branchId: input.branchId,
}) })
.where(eq(releases.id, input.id)) .where(eq(releases.id, input.id))
.returning() .returning()
@@ -74,9 +93,7 @@ export const releasesRouter = createTRPCRouter({
}) })
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
await db await db.delete(releases).where(eq(releases.id, input.id))
.delete(releases)
.where(eq(releases.id, input.id))
return { success: true } return { success: true }
}), }),

View File

@@ -125,6 +125,13 @@ export const verificationTokens = pgTable(
(t) => [primaryKey({ columns: [t.identifier, t.token] })] (t) => [primaryKey({ columns: [t.identifier, t.token] })]
) )
export const branches = pgTable('mod_branches', {
id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
name: text('name').notNull().unique(),
description: text('description'),
createdAt: timestamp('created_at').notNull().defaultNow(),
})
export const releases = pgTable('mod_release', { export const releases = pgTable('mod_release', {
id: integer('id').primaryKey().generatedByDefaultAsIdentity(), id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
name: text('name').notNull(), name: text('name').notNull(),
@@ -133,9 +140,24 @@ export const releases = pgTable('mod_release', {
url: text('url').notNull(), url: text('url').notNull(),
smods_version: text('smods_version').default('latest'), smods_version: text('smods_version').default('latest'),
lovely_version: text('lovely_version').default('latest'), lovely_version: text('lovely_version').default('latest'),
branchId: integer('branch_id')
.references(() => branches.id)
.notNull()
.default(1),
createdAt: timestamp('created_at').notNull().defaultNow(), createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at') updatedAt: timestamp('updated_at')
.notNull() .notNull()
.defaultNow() .defaultNow()
.$onUpdate(() => new Date()), .$onUpdate(() => new Date()),
}) })
export const branchesRelations = relations(branches, ({ many }) => ({
releases: many(releases),
}))
export const releasesRelations = relations(releases, ({ one }) => ({
branch: one(branches, {
fields: [releases.branchId],
references: [branches.id],
}),
}))