mirror of
https://github.com/ershisan99/www.git
synced 2026-01-17 21:02:06 +00:00
add dropzone for releases page
This commit is contained in:
@@ -19,6 +19,15 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import {
|
||||
Dropzone,
|
||||
DropzoneDescription,
|
||||
DropzoneGroup,
|
||||
DropzoneInput,
|
||||
DropzoneTitle,
|
||||
DropzoneUploadIcon,
|
||||
DropzoneZone,
|
||||
} from '@/components/ui/dropzone'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
@@ -80,10 +89,77 @@ export function ReleasesClient() {
|
||||
const [smodsVersions, setSmodsVersions] = useState<string[]>(['latest'])
|
||||
const [lovelyVersions, setLovelyVersions] = useState<string[]>(['latest'])
|
||||
const [newBranch, setNewBranch] = useState<string>('')
|
||||
const [isUploading, setIsUploading] = useState(false)
|
||||
const [uploadError, setUploadError] = useState<string | null>(null)
|
||||
|
||||
// Fetch branches from the database
|
||||
const [branches] = api.branches.getBranches.useSuspenseQuery()
|
||||
console.log(branches)
|
||||
|
||||
// Handle file upload
|
||||
const [editIsUploading, setEditIsUploading] = useState(false)
|
||||
const [editUploadError, setEditUploadError] = useState<string | null>(null)
|
||||
|
||||
const handleFileUpload = async (files: File[], isEdit = false) => {
|
||||
if (files.length === 0) return
|
||||
|
||||
const file = files[0]
|
||||
if (!file.name.endsWith('.zip')) {
|
||||
toast.error('Only zip files are allowed')
|
||||
return
|
||||
}
|
||||
|
||||
if (isEdit) {
|
||||
setEditIsUploading(true)
|
||||
setEditUploadError(null)
|
||||
} else {
|
||||
setIsUploading(true)
|
||||
setUploadError(null)
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
const response = await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || 'Failed to upload file')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Update the URL field in the form
|
||||
if (isEdit) {
|
||||
editForm.setValue('url', data.url)
|
||||
} else {
|
||||
form.setValue('url', data.url)
|
||||
}
|
||||
toast.success('File uploaded successfully')
|
||||
} catch (error) {
|
||||
console.error('Error uploading file:', error)
|
||||
if (isEdit) {
|
||||
setEditUploadError(
|
||||
error instanceof Error ? error.message : 'Failed to upload file'
|
||||
)
|
||||
} else {
|
||||
setUploadError(
|
||||
error instanceof Error ? error.message : 'Failed to upload file'
|
||||
)
|
||||
}
|
||||
toast.error('Failed to upload file')
|
||||
} finally {
|
||||
if (isEdit) {
|
||||
setEditIsUploading(false)
|
||||
} else {
|
||||
setIsUploading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add branch mutation
|
||||
const addBranch = api.branches.addBranch.useMutation({
|
||||
onSuccess: () => {
|
||||
@@ -283,7 +359,51 @@ export function ReleasesClient() {
|
||||
</div>
|
||||
<div className='grid grid-cols-1 gap-2'>
|
||||
<Label htmlFor='url'>URL</Label>
|
||||
<Input id='url' {...form.register('url')} />
|
||||
<div className='flex flex-col gap-2'>
|
||||
<Input
|
||||
id='url'
|
||||
{...form.register('url')}
|
||||
placeholder='URL will be automatically filled after upload'
|
||||
/>
|
||||
<div className='rounded-md border'>
|
||||
<Dropzone
|
||||
onDropAccepted={(files) => handleFileUpload(files, false)}
|
||||
accept={{
|
||||
'application/zip': ['.zip'],
|
||||
}}
|
||||
maxFiles={1}
|
||||
disabled={isUploading}
|
||||
>
|
||||
<DropzoneZone className='w-full p-4'>
|
||||
<DropzoneInput />
|
||||
<DropzoneGroup className='gap-2'>
|
||||
{isUploading ? (
|
||||
<div className='flex items-center justify-center'>
|
||||
<div className='h-6 w-6 animate-spin rounded-full border-primary border-b-2' />
|
||||
</div>
|
||||
) : (
|
||||
<DropzoneUploadIcon />
|
||||
)}
|
||||
<DropzoneGroup>
|
||||
<DropzoneTitle>
|
||||
{isUploading
|
||||
? 'Uploading...'
|
||||
: 'Drop zip file here or click to upload'}
|
||||
</DropzoneTitle>
|
||||
<DropzoneDescription>
|
||||
Upload a zip archive to automatically generate a URL
|
||||
</DropzoneDescription>
|
||||
{uploadError && (
|
||||
<p className='mt-1 text-destructive text-sm'>
|
||||
{uploadError}
|
||||
</p>
|
||||
)}
|
||||
</DropzoneGroup>
|
||||
</DropzoneGroup>
|
||||
</DropzoneZone>
|
||||
</Dropzone>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='grid grid-cols-3 gap-4'>
|
||||
<div className='grid grid-cols-1 gap-2'>
|
||||
@@ -353,11 +473,11 @@ export function ReleasesClient() {
|
||||
</div>
|
||||
</div>
|
||||
<div className='grid grid-cols-1 gap-2'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Label htmlFor='branch-management'>Branch Management</Label>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => setBranchManagementOpen(true)}
|
||||
>
|
||||
@@ -404,7 +524,52 @@ export function ReleasesClient() {
|
||||
</div>
|
||||
<div className='grid grid-cols-1 gap-2'>
|
||||
<Label htmlFor='edit-url'>URL</Label>
|
||||
<Input id='edit-url' {...editForm.register('url')} />
|
||||
<div className='flex flex-col gap-2'>
|
||||
<Input
|
||||
id='edit-url'
|
||||
{...editForm.register('url')}
|
||||
placeholder='URL will be automatically filled after upload'
|
||||
/>
|
||||
<div className='rounded-md border'>
|
||||
<Dropzone
|
||||
onDropAccepted={(files) => handleFileUpload(files, true)}
|
||||
accept={{
|
||||
'application/zip': ['.zip'],
|
||||
}}
|
||||
maxFiles={1}
|
||||
disabled={editIsUploading}
|
||||
>
|
||||
<DropzoneZone className='w-full p-4'>
|
||||
<DropzoneInput />
|
||||
<DropzoneGroup className='gap-2'>
|
||||
{editIsUploading ? (
|
||||
<div className='flex items-center justify-center'>
|
||||
<div className='h-6 w-6 animate-spin rounded-full border-primary border-b-2'></div>
|
||||
</div>
|
||||
) : (
|
||||
<DropzoneUploadIcon />
|
||||
)}
|
||||
<DropzoneGroup>
|
||||
<DropzoneTitle>
|
||||
{editIsUploading
|
||||
? 'Uploading...'
|
||||
: 'Drop zip file here or click to upload'}
|
||||
</DropzoneTitle>
|
||||
<DropzoneDescription>
|
||||
Upload a zip archive to automatically generate a
|
||||
URL
|
||||
</DropzoneDescription>
|
||||
{editUploadError && (
|
||||
<p className='mt-1 text-destructive text-sm'>
|
||||
{editUploadError}
|
||||
</p>
|
||||
)}
|
||||
</DropzoneGroup>
|
||||
</DropzoneGroup>
|
||||
</DropzoneZone>
|
||||
</Dropzone>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='grid grid-cols-3 gap-4'>
|
||||
<div className='grid grid-cols-1 gap-2'>
|
||||
@@ -515,7 +680,10 @@ export function ReleasesClient() {
|
||||
</AlertDialog>
|
||||
|
||||
{/* Branch Management Modal */}
|
||||
<Dialog open={branchManagementOpen} onOpenChange={setBranchManagementOpen}>
|
||||
<Dialog
|
||||
open={branchManagementOpen}
|
||||
onOpenChange={setBranchManagementOpen}
|
||||
>
|
||||
<DialogContent className='sm:max-w-[500px]'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Manage Branches</DialogTitle>
|
||||
@@ -581,7 +749,10 @@ export function ReleasesClient() {
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type='button' onClick={() => setBranchManagementOpen(false)}>
|
||||
<Button
|
||||
type='button'
|
||||
onClick={() => setBranchManagementOpen(false)}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
50
src/app/api/upload/route.ts
Normal file
50
src/app/api/upload/route.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { auth } from '@/server/auth'
|
||||
import { uploadFile } from '@/server/minio'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
// Check if user is authenticated and is an admin
|
||||
const session = await auth()
|
||||
if (!session || session.user.role !== 'admin') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Parse the multipart form data
|
||||
const formData = await req.formData()
|
||||
const file = formData.get('file') as File | null
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No file provided' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if the file is a zip file
|
||||
if (!file.name.endsWith('.zip')) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Only zip files are allowed' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Convert the file to a buffer
|
||||
const buffer = Buffer.from(await file.arrayBuffer())
|
||||
|
||||
// Upload the file to MinIO
|
||||
const fileUrl = await uploadFile(buffer, file.name, file.type)
|
||||
|
||||
// Return the URL of the uploaded file
|
||||
return NextResponse.json({ url: fileUrl })
|
||||
} catch (error) {
|
||||
console.error('Error uploading file:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to upload file' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
10
src/env.js
10
src/env.js
@@ -18,6 +18,11 @@ export const env = createEnv({
|
||||
DATABASE_URL: z.string().url(),
|
||||
REDIS_URL: z.string().url(),
|
||||
WEBHOOK_QUERY_SECRET: z.string(),
|
||||
MINIO_ENDPOINT: z.string(),
|
||||
MINIO_ACCESS_KEY: z.string(),
|
||||
MINIO_SECRET_KEY: z.string(),
|
||||
MINIO_BUCKET_NAME: z.string(),
|
||||
MINIO_USE_SSL: z.enum(['true', 'false']).default('false'),
|
||||
NODE_ENV: z
|
||||
.enum(['development', 'test', 'production'])
|
||||
.default('development'),
|
||||
@@ -46,6 +51,11 @@ export const env = createEnv({
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
CRON_SECRET: process.env.CRON_SECRET,
|
||||
WEBHOOK_QUERY_SECRET: process.env.WEBHOOK_QUERY_SECRET,
|
||||
MINIO_ENDPOINT: process.env.MINIO_ENDPOINT,
|
||||
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
||||
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
||||
MINIO_BUCKET_NAME: process.env.MINIO_BUCKET_NAME,
|
||||
MINIO_USE_SSL: process.env.MINIO_USE_SSL,
|
||||
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
|
||||
},
|
||||
/**
|
||||
|
||||
43
src/server/minio.ts
Normal file
43
src/server/minio.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { env } from '@/env'
|
||||
import { Client } from 'minio'
|
||||
|
||||
// Create and configure the MinIO client
|
||||
export const minioClient = new Client({
|
||||
endPoint: env.MINIO_ENDPOINT,
|
||||
useSSL: env.MINIO_USE_SSL === 'true',
|
||||
accessKey: env.MINIO_ACCESS_KEY,
|
||||
secretKey: env.MINIO_SECRET_KEY,
|
||||
})
|
||||
|
||||
// Function to check if bucket exists and create it if it doesn't
|
||||
export async function ensureBucketExists() {
|
||||
const bucketExists = await minioClient.bucketExists(env.MINIO_BUCKET_NAME)
|
||||
if (!bucketExists) {
|
||||
await minioClient.makeBucket(env.MINIO_BUCKET_NAME, 'us-east-1')
|
||||
}
|
||||
}
|
||||
|
||||
// Function to upload a file to MinIO and return the URL
|
||||
export async function uploadFile(
|
||||
file: Buffer,
|
||||
fileName: string,
|
||||
contentType: string
|
||||
) {
|
||||
await ensureBucketExists()
|
||||
|
||||
// Generate a unique object name to avoid collisions
|
||||
const objectName = `${Date.now()}-${fileName}`
|
||||
|
||||
// Upload the file to MinIO
|
||||
await minioClient.putObject(
|
||||
env.MINIO_BUCKET_NAME,
|
||||
objectName,
|
||||
file,
|
||||
file.length,
|
||||
{ 'Content-Type': contentType }
|
||||
)
|
||||
|
||||
// Construct and return the URL to the uploaded file
|
||||
const protocol = env.MINIO_USE_SSL === 'true' ? 'https' : 'http'
|
||||
return `${protocol}://${env.MINIO_ENDPOINT}/${env.MINIO_BUCKET_NAME}/${objectName}`
|
||||
}
|
||||
Reference in New Issue
Block a user