Initial commit
This commit is contained in:
328
admin/src/pages/EventCreatePage.vue
Normal file
328
admin/src/pages/EventCreatePage.vue
Normal file
@@ -0,0 +1,328 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useEvents } from '../composables/useEvents'
|
||||
import { useGoogleDrive } from '../composables/useGoogleDrive'
|
||||
import type { EventFormData } from '../types/event'
|
||||
|
||||
const router = useRouter()
|
||||
const { createEvent, loading: saving, error } = useEvents()
|
||||
const { fetchStatus, getAuthUrl, listSharedDrives, listFolders, createFolder } = useGoogleDrive()
|
||||
|
||||
const driveConnected = ref(false)
|
||||
const sharedDrives = ref<{ id: string; name: string; type: string }[]>([])
|
||||
const folders = ref<{ id: string; name: string }[]>([])
|
||||
const selectedDriveId = ref('')
|
||||
const selectedDriveName = ref('')
|
||||
const selectedFolderId = ref('')
|
||||
const selectedFolderName = ref('')
|
||||
const viewMode = ref<'my-drive' | 'shared-drives'>('my-drive')
|
||||
|
||||
const form = reactive<Partial<EventFormData>>({
|
||||
name: '',
|
||||
description: '',
|
||||
slug: '',
|
||||
google_drive_folder_id: '',
|
||||
google_drive_folder_name: '',
|
||||
is_active: true,
|
||||
upload_start_at: '',
|
||||
upload_end_at: '',
|
||||
max_file_size_mb: 500,
|
||||
allowed_extensions: ['mp4', 'mov', 'avi', 'mkv', 'webm'],
|
||||
require_password: false,
|
||||
upload_password: '',
|
||||
})
|
||||
|
||||
const EXT_OPTIONS = ['mp4', 'mov', 'avi', 'mkv', 'webm', 'jpg', 'jpeg', 'png']
|
||||
|
||||
onMounted(async () => {
|
||||
const s = await fetchStatus().catch(() => ({ connected: false }))
|
||||
driveConnected.value = s?.connected ?? false
|
||||
if (driveConnected.value) {
|
||||
await loadMyDrive()
|
||||
}
|
||||
})
|
||||
|
||||
async function connectDrive() {
|
||||
const url = await getAuthUrl()
|
||||
window.location.href = url
|
||||
}
|
||||
|
||||
async function loadMyDrive() {
|
||||
viewMode.value = 'my-drive'
|
||||
selectedDriveId.value = ''
|
||||
selectedDriveName.value = ''
|
||||
folders.value = await listFolders().catch(() => [])
|
||||
}
|
||||
|
||||
async function loadSharedDrives() {
|
||||
viewMode.value = 'shared-drives'
|
||||
selectedDriveId.value = ''
|
||||
selectedDriveName.value = ''
|
||||
selectedFolderId.value = ''
|
||||
selectedFolderName.value = ''
|
||||
folders.value = []
|
||||
sharedDrives.value = await listSharedDrives().catch(() => [])
|
||||
}
|
||||
|
||||
function selectDrive(id: string, name: string) {
|
||||
selectedDriveId.value = id
|
||||
selectedDriveName.value = name
|
||||
selectedFolderId.value = ''
|
||||
selectedFolderName.value = ''
|
||||
loadFoldersInDrive(id)
|
||||
}
|
||||
|
||||
async function loadFoldersInDrive(driveId: string, parentId?: string) {
|
||||
folders.value = await listFolders(parentId, driveId)
|
||||
}
|
||||
|
||||
async function loadFolders(parentId?: string) {
|
||||
if (selectedDriveId.value) {
|
||||
folders.value = await listFolders(parentId, selectedDriveId.value)
|
||||
} else {
|
||||
folders.value = await listFolders(parentId)
|
||||
}
|
||||
}
|
||||
|
||||
function selectFolder(id: string, name: string) {
|
||||
selectedFolderId.value = id
|
||||
selectedFolderName.value = name
|
||||
form.google_drive_folder_id = id
|
||||
form.google_drive_folder_name = name
|
||||
}
|
||||
|
||||
async function handleCreateFolder() {
|
||||
const name = prompt('Folder name')
|
||||
if (!name) return
|
||||
const created = await createFolder(
|
||||
name,
|
||||
selectedFolderId.value || undefined,
|
||||
selectedDriveId.value || undefined
|
||||
)
|
||||
await loadFolders(selectedFolderId.value || undefined)
|
||||
selectFolder(created.id, created.name)
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
try {
|
||||
const payload = {
|
||||
...form,
|
||||
google_drive_folder_id: form.google_drive_folder_id || null,
|
||||
google_drive_folder_name: form.google_drive_folder_name || null,
|
||||
upload_password: form.require_password ? form.upload_password : null,
|
||||
}
|
||||
const event = await createEvent(payload)
|
||||
router.push({ name: 'event-uploads', params: { id: event.id } })
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="admin-page admin-form-page">
|
||||
<router-link to="/" class="back-link mb-4">
|
||||
← Back to Events
|
||||
</router-link>
|
||||
|
||||
<div class="admin-card admin-card-body mb-4">
|
||||
<h2 class="section-title mb-4">Event details</h2>
|
||||
<form @submit.prevent="onSubmit" class="row g-3">
|
||||
<div v-if="error" class="col-12 alert alert-danger py-2">{{ error }}</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label">Name</label>
|
||||
<input v-model="form.name" type="text" class="form-control" required placeholder="e.g. Summer Conference 2025" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea v-model="form.description" class="form-control" rows="2" placeholder="Optional description for attendees"></textarea>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">URL slug <span class="text-muted">(optional, auto-generated from name)</span></label>
|
||||
<input v-model="form.slug" type="text" class="form-control" placeholder="my-event" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="admin-card admin-card-body mb-4">
|
||||
<h2 class="section-title mb-3">Google Drive folder</h2>
|
||||
<p class="text-muted small mb-3">Uploads will be saved to the selected folder.</p>
|
||||
<div v-if="!driveConnected" class="drive-connect-box p-4 rounded-3 bg-light border">
|
||||
<p class="mb-3 text-muted small">Connect your Google account to choose a destination folder.</p>
|
||||
<button type="button" class="btn btn-primary" @click="connectDrive">
|
||||
Connect Google Drive
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center mb-3">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
:class="viewMode === 'my-drive' ? 'btn-primary' : 'btn-outline-primary'"
|
||||
@click="loadMyDrive"
|
||||
>
|
||||
My Drive
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
:class="viewMode === 'shared-drives' ? 'btn-primary' : 'btn-outline-primary'"
|
||||
@click="loadSharedDrives"
|
||||
>
|
||||
Shared Drives
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" @click="loadFolders()">
|
||||
Refresh
|
||||
</button>
|
||||
<button
|
||||
v-if="viewMode === 'my-drive' || selectedDriveId"
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
@click="handleCreateFolder"
|
||||
>
|
||||
Create folder
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="viewMode === 'shared-drives' && !selectedDriveId">
|
||||
<ul class="folder-list list-group">
|
||||
<li
|
||||
v-for="d in sharedDrives"
|
||||
:key="d.id"
|
||||
class="list-group-item list-group-item-action"
|
||||
@click="selectDrive(d.id, d.name)"
|
||||
>
|
||||
📁 {{ d.name }}
|
||||
</li>
|
||||
</ul>
|
||||
<p v-if="sharedDrives.length === 0" class="text-muted small mt-2 mb-0">No shared drives found</p>
|
||||
</div>
|
||||
|
||||
<div v-if="viewMode === 'my-drive' || selectedDriveId">
|
||||
<p v-if="selectedDriveName" class="text-muted small mb-2">
|
||||
📁 {{ selectedDriveName }}
|
||||
<button type="button" class="btn btn-sm btn-link p-0 ms-2" @click="loadSharedDrives">(change)</button>
|
||||
</p>
|
||||
<ul class="folder-list list-group">
|
||||
<li
|
||||
v-for="f in folders"
|
||||
:key="f.id"
|
||||
class="list-group-item list-group-item-action"
|
||||
:class="{ active: selectedFolderId === f.id }"
|
||||
@click="selectFolder(f.id, f.name)"
|
||||
>
|
||||
{{ f.name }}
|
||||
</li>
|
||||
</ul>
|
||||
<p v-if="selectedFolderName" class="mt-2 text-muted small mb-0">
|
||||
Selected: <strong>{{ selectedFolderName }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-card admin-card-body mb-4">
|
||||
<h2 class="section-title mb-4">Settings</h2>
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="form-check form-switch">
|
||||
<input v-model="form.is_active" type="checkbox" class="form-check-input" id="is_active" />
|
||||
<label class="form-check-label" for="is_active">Event is active (accepting uploads)</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">Max file size (MB)</label>
|
||||
<input v-model.number="form.max_file_size_mb" type="number" class="form-control" min="1" max="2000" />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">Upload window start</label>
|
||||
<input v-model="form.upload_start_at" type="datetime-local" class="form-control" />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">Upload window end</label>
|
||||
<input v-model="form.upload_end_at" type="datetime-local" class="form-control" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Allowed file extensions</label>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<label v-for="ext in EXT_OPTIONS" :key="ext" class="form-check form-check-inline">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="form-check-input"
|
||||
:value="ext"
|
||||
:checked="form.allowed_extensions?.includes(ext)"
|
||||
@change="(e: globalThis.Event) => {
|
||||
const arr = form.allowed_extensions || []
|
||||
const target = (e.target as HTMLInputElement)
|
||||
if (target.checked) form.allowed_extensions = [...arr, ext]
|
||||
else form.allowed_extensions = arr.filter(x => x !== ext)
|
||||
}"
|
||||
/>
|
||||
<span class="form-check-label">.{{ ext }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-check form-switch mb-2">
|
||||
<input v-model="form.require_password" type="checkbox" class="form-check-input" id="require_password" />
|
||||
<label class="form-check-label" for="require_password">Require upload password</label>
|
||||
</div>
|
||||
<input
|
||||
v-if="form.require_password"
|
||||
v-model="form.upload_password"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Password for attendees"
|
||||
minlength="4"
|
||||
style="max-width: 280px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-card admin-card-body">
|
||||
<button type="submit" class="btn btn-primary btn-lg" :disabled="saving" @click="onSubmit">
|
||||
{{ saving ? 'Creating...' : 'Create Event' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-form-page {
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
color: var(--admin-muted);
|
||||
text-decoration: none;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 500;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
color: var(--admin-primary);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--admin-heading);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.folder-list {
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
border-radius: var(--admin-radius);
|
||||
}
|
||||
|
||||
.drive-connect-box {
|
||||
border-color: var(--admin-border) !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user