Initial commit

This commit is contained in:
2026-02-03 10:38:46 +01:00
commit eb304f4b14
144 changed files with 22605 additions and 0 deletions

View 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>