pp-planer/resources/js/Components/FlashMessage.vue
Thorsten Bus 57d54ec06b feat: Wave 1 Foundation - Database, OAuth, Sync, Files, Layout, Email (T2-T7)
T2: Database Schema + All Migrations
- 10 migrations: users extension, services, songs, song_groups, song_slides,
  song_arrangements, song_arrangement_groups, service_songs, slides, cts_sync_log
- 9 Eloquent models with relationships and casts
- 9 factory classes for testing
- Tests: DatabaseSchemaTest (2 tests, 26 assertions) 

T3: ChurchTools OAuth Provider
- Custom Socialite provider for ChurchTools OAuth2
- AuthController with redirect/callback/logout
- Replaced Breeze login with OAuth-only (German UI)
- Removed all Breeze register/password-reset pages
- Tests: OAuthTest (9 tests, 54 assertions) 

T4: CTS API Service + Sync Command
- ChurchToolsService wrapping 5pm-HDH/churchtools-api
- SyncChurchToolsCommand (php artisan cts:sync)
- SyncController for refresh button
- CCLI-based song matching
- Tests: ChurchToolsSyncTest (2 tests) 

T5: File Conversion Service
- FileConversionService with letterbox/pillarbox to 1920×1080
- ConvertPowerPointJob (queued) with LibreOffice + spatie/pdf-to-image
- ZIP extraction and recursive processing
- Thumbnail generation (320×180)
- Tests: FileConversionTest (2 tests, 21 assertions) 

T6: Shared Vue Components
- AuthenticatedLayout with nav, user info, refresh button
- useAutoSave composable (500ms debounce)
- FlashMessage, ConfirmDialog, LoadingSpinner components
- HandleInertiaRequests middleware with shared props
- Tests: SharedPropsTest (7 tests) 

T7: Email Configuration
- MissingSongRequest mailable (German)
- Email template with song info and service link
- SONG_REQUEST_EMAIL config
- Tests: MissingSongMailTest (2 tests, 10 assertions) 

All tests passing: 30/30 (233 assertions)
All UI text in German with 'Du' form
Wave 1 complete: 7/7 tasks 
2026-03-01 19:39:26 +01:00

101 lines
3.2 KiB
Vue

<script setup>
import { ref, watch, onMounted } from 'vue'
import { usePage } from '@inertiajs/vue3'
const page = usePage()
const visible = ref(false)
const message = ref('')
const type = ref('success')
let hideTimeout = null
function show(msg, msgType) {
message.value = msg
type.value = msgType
visible.value = true
if (hideTimeout) clearTimeout(hideTimeout)
hideTimeout = setTimeout(() => {
visible.value = false
}, 4000)
}
function dismiss() {
visible.value = false
if (hideTimeout) clearTimeout(hideTimeout)
}
function checkFlash() {
const flash = page.props.flash
if (flash?.success) {
show(flash.success, 'success')
} else if (flash?.error) {
show(flash.error, 'error')
}
}
onMounted(checkFlash)
watch(() => page.props.flash, checkFlash, { deep: true })
</script>
<template>
<Transition
enter-active-class="transition duration-300 ease-out"
enter-from-class="translate-y-[-100%] opacity-0"
enter-to-class="translate-y-0 opacity-100"
leave-active-class="transition duration-200 ease-in"
leave-from-class="translate-y-0 opacity-100"
leave-to-class="translate-y-[-100%] opacity-0"
>
<div
v-if="visible"
class="fixed left-1/2 top-4 z-[100] w-auto max-w-lg -translate-x-1/2"
role="alert"
>
<div
class="flex items-center gap-3 rounded-xl px-5 py-3 shadow-lg backdrop-blur-sm"
:class="{
'bg-emerald-600/90 text-white': type === 'success',
'bg-red-600/90 text-white': type === 'error',
}"
>
<!-- Erfolg-Icon -->
<svg
v-if="type === 'success'"
class="h-5 w-5 shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
<!-- Fehler-Icon -->
<svg
v-if="type === 'error'"
class="h-5 w-5 shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
</svg>
<span class="text-sm font-medium">{{ message }}</span>
<button
@click="dismiss"
class="ml-2 shrink-0 rounded-lg p-1 opacity-70 transition hover:opacity-100"
aria-label="Schließen"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</Transition>
</template>