update
This commit is contained in:
Binary file not shown.
@@ -1,14 +1,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
// Eine Einladungskarte zum Verschicken: Datum, Uhrzeit, Ort, der gezogene
|
// Eine Einladungskarte zum Verschicken: Titel, Datum, Uhrzeit, Genre, Ort,
|
||||||
// Film — als Bild fürs Handy. Zwei Designs: Horror und kein Horror. Das
|
// Adresse der Seite — als Bild fürs Handy. Zwei Designs: Horror und kein
|
||||||
// Bild entsteht hier im Browser auf einem Canvas, der Host lädt es
|
// Horror. Jede Zeile ist ein Textfeld. Das Bild entsteht hier im Browser
|
||||||
// herunter und schickt es selbst herum.
|
// auf einem Canvas, der Host lädt es herunter und schickt es selbst herum.
|
||||||
import { onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
import { onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
||||||
import { posterUrl } from '../images'
|
|
||||||
|
|
||||||
const emit = defineEmits(['zu'])
|
const emit = defineEmits(['zu'])
|
||||||
|
|
||||||
const GEMERKT = 'filmabend_einladung'
|
const GEMERKT = 'filmabend_karte'
|
||||||
const B = 1080
|
const B = 1080
|
||||||
const H = 1350
|
const H = 1350
|
||||||
|
|
||||||
@@ -23,9 +22,9 @@ const form = reactive({
|
|||||||
titel: 'Filmabend',
|
titel: 'Filmabend',
|
||||||
datum: naechsterMittwoch(),
|
datum: naechsterMittwoch(),
|
||||||
uhrzeit: '20:00',
|
uhrzeit: '20:00',
|
||||||
ort: '',
|
genre: 'Horror',
|
||||||
satz: 'Bring was zu knabbern mit.',
|
ort: 'Bananarama - Wohnzimmer',
|
||||||
filmZeigen: true,
|
url: 'horror.marha.de',
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -34,35 +33,11 @@ try {
|
|||||||
// Kaputte Erinnerung — dann eben die Vorgaben
|
// Kaputte Erinnerung — dann eben die Vorgaben
|
||||||
}
|
}
|
||||||
|
|
||||||
// Der gezogene Film vom Rad, falls es einen gibt
|
|
||||||
const film = ref(null)
|
|
||||||
let poster = null
|
|
||||||
const leinwand = ref(null)
|
const leinwand = ref(null)
|
||||||
|
|
||||||
async function filmLaden() {
|
function stilWaehlen(stil) {
|
||||||
try {
|
form.stil = stil
|
||||||
const resp = await fetch('/api/spin')
|
form.genre = stil === 'horror' ? 'Horror' : 'Kein Horror'
|
||||||
const daten = resp.ok ? await resp.json() : null
|
|
||||||
film.value = daten?.result?.gewinner || null
|
|
||||||
} catch {
|
|
||||||
film.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function posterLaden() {
|
|
||||||
poster = null
|
|
||||||
const pfad = film.value?.poster_path
|
|
||||||
if (!pfad) return
|
|
||||||
const bild = new Image()
|
|
||||||
// Sonst darf das Canvas nicht als Bild gespeichert werden
|
|
||||||
bild.crossOrigin = 'anonymous'
|
|
||||||
bild.src = posterUrl(pfad, 'w500')
|
|
||||||
try {
|
|
||||||
await bild.decode()
|
|
||||||
poster = bild
|
|
||||||
} catch {
|
|
||||||
poster = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Malen ───────────────────────────────────────────────────────────
|
// ── Malen ───────────────────────────────────────────────────────────
|
||||||
@@ -100,117 +75,242 @@ function rundeck(ctx, x, y, b, h, r) {
|
|||||||
ctx.closePath()
|
ctx.closePath()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Feste Zufallsfolge: Körnung und Spritzer bleiben bei jedem Tastendruck gleich
|
||||||
|
function zufall(saat) {
|
||||||
|
let z = saat
|
||||||
|
return () => {
|
||||||
|
z = (z * 1664525 + 1013904223) % 4294967296
|
||||||
|
return z / 4294967296
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SCHRIFT = 'system-ui, -apple-system, "Segoe UI", sans-serif'
|
||||||
|
|
||||||
|
const FARBEN = {
|
||||||
|
horror: { text: '#f1ecec', leise: '#9a8f92', akzent: '#e50914' },
|
||||||
|
nett: { text: '#1b2a41', leise: '#7d6b50', akzent: '#e0532f' },
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Horror: Dunkelheit, Körnung, Spinnennetz, Blut ──────────────────
|
||||||
|
function spinnennetz(ctx, cx, cy, radius, von, bis) {
|
||||||
|
ctx.save()
|
||||||
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.26)'
|
||||||
|
ctx.lineWidth = 2
|
||||||
|
const speichen = 7
|
||||||
|
const winkel = []
|
||||||
|
for (let i = 0; i <= speichen; i++) winkel.push(von + ((bis - von) * i) / speichen)
|
||||||
|
for (const w of winkel) {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(cx, cy)
|
||||||
|
ctx.lineTo(cx + Math.cos(w) * radius, cy + Math.sin(w) * radius)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
// Die Querfäden hängen leicht durch
|
||||||
|
for (let r = radius / 6; r <= radius; r += radius / 6) {
|
||||||
|
ctx.beginPath()
|
||||||
|
winkel.forEach((w, i) => {
|
||||||
|
const x = cx + Math.cos(w) * r
|
||||||
|
const y = cy + Math.sin(w) * r
|
||||||
|
if (i === 0) return ctx.moveTo(x, y)
|
||||||
|
const m = (winkel[i - 1] + w) / 2
|
||||||
|
ctx.quadraticCurveTo(cx + Math.cos(m) * r * 0.9, cy + Math.sin(m) * r * 0.9, x, y)
|
||||||
|
})
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
function blut(ctx) {
|
||||||
|
ctx.fillStyle = '#b3060f'
|
||||||
|
ctx.fillRect(0, 0, B, 30)
|
||||||
|
const tropfen = [
|
||||||
|
[50, 70, 16],
|
||||||
|
[150, 140, 22],
|
||||||
|
[230, 50, 14],
|
||||||
|
[330, 180, 26],
|
||||||
|
[430, 90, 18],
|
||||||
|
[520, 220, 24],
|
||||||
|
[640, 60, 16],
|
||||||
|
[720, 150, 20],
|
||||||
|
[820, 110, 22],
|
||||||
|
[910, 200, 26],
|
||||||
|
[1000, 80, 18],
|
||||||
|
]
|
||||||
|
for (const [x, laenge, breite] of tropfen) {
|
||||||
|
rundeck(ctx, x - breite / 2, 10, breite, laenge, breite / 2)
|
||||||
|
ctx.fill()
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(x, 10 + laenge, breite * 0.7, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
const r = zufall(3)
|
||||||
|
for (let i = 0; i < 40; i++) {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(r() * B, 30 + r() * 260, 2 + r() * 5, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function horrorHintergrund(ctx) {
|
||||||
|
ctx.fillStyle = '#07060a'
|
||||||
|
ctx.fillRect(0, 0, B, H)
|
||||||
|
const glut = ctx.createRadialGradient(B / 2, H * 0.55, 60, B / 2, H * 0.55, H * 0.75)
|
||||||
|
glut.addColorStop(0, 'rgba(150, 10, 20, 0.35)')
|
||||||
|
glut.addColorStop(1, 'rgba(0, 0, 0, 0)')
|
||||||
|
ctx.fillStyle = glut
|
||||||
|
ctx.fillRect(0, 0, B, H)
|
||||||
|
const r = zufall(7)
|
||||||
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.05)'
|
||||||
|
for (let i = 0; i < 6000; i++) ctx.fillRect(r() * B, r() * H, 2, 2)
|
||||||
|
const rand = ctx.createRadialGradient(B / 2, H / 2, H * 0.35, B / 2, H / 2, H * 0.85)
|
||||||
|
rand.addColorStop(0, 'rgba(0, 0, 0, 0)')
|
||||||
|
rand.addColorStop(1, 'rgba(0, 0, 0, 0.85)')
|
||||||
|
ctx.fillStyle = rand
|
||||||
|
ctx.fillRect(0, 0, B, H)
|
||||||
|
spinnennetz(ctx, B, 0, 380, Math.PI / 2, Math.PI)
|
||||||
|
spinnennetz(ctx, 0, H, 300, -Math.PI / 2, 0)
|
||||||
|
blut(ctx)
|
||||||
|
// Ein dünner Strich zwischen Wann und Was
|
||||||
|
ctx.fillStyle = 'rgba(229, 9, 20, 0.5)'
|
||||||
|
ctx.fillRect(300, 670, B - 600, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Kein Horror: ein Kinoticket auf einer Markise ───────────────────
|
||||||
|
const TICKET = { x: 70, y: 90, b: B - 140, h: H - 180, r: 36 }
|
||||||
|
|
||||||
|
function filmstreifen(ctx, y) {
|
||||||
|
ctx.fillStyle = FARBEN.nett.text
|
||||||
|
ctx.fillRect(TICKET.x, y, TICKET.b, 70)
|
||||||
|
ctx.fillStyle = '#fff4dc'
|
||||||
|
for (let x = TICKET.x + 30; x < TICKET.x + TICKET.b - 30; x += 70) {
|
||||||
|
rundeck(ctx, x, y + 20, 36, 30, 6)
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function nettHintergrund(ctx) {
|
||||||
|
// Markise: schräge Streifen
|
||||||
|
ctx.fillStyle = '#f2bd3a'
|
||||||
|
ctx.fillRect(0, 0, B, H)
|
||||||
|
ctx.save()
|
||||||
|
ctx.translate(B / 2, H / 2)
|
||||||
|
ctx.rotate(-Math.PI / 4)
|
||||||
|
ctx.fillStyle = '#e9b02c'
|
||||||
|
for (let x = -H; x < H; x += 80) ctx.fillRect(x, -H, 40, 2 * H)
|
||||||
|
ctx.restore()
|
||||||
|
|
||||||
|
// Das Ticket mit Schatten
|
||||||
|
ctx.save()
|
||||||
|
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)'
|
||||||
|
ctx.shadowBlur = 40
|
||||||
|
ctx.shadowOffsetY = 16
|
||||||
|
ctx.fillStyle = '#fff4dc'
|
||||||
|
rundeck(ctx, TICKET.x, TICKET.y, TICKET.b, TICKET.h, TICKET.r)
|
||||||
|
ctx.fill()
|
||||||
|
ctx.restore()
|
||||||
|
|
||||||
|
ctx.save()
|
||||||
|
rundeck(ctx, TICKET.x, TICKET.y, TICKET.b, TICKET.h, TICKET.r)
|
||||||
|
ctx.clip()
|
||||||
|
filmstreifen(ctx, TICKET.y)
|
||||||
|
filmstreifen(ctx, TICKET.y + TICKET.h - 70)
|
||||||
|
ctx.restore()
|
||||||
|
|
||||||
|
// Abrisskante mit Kerben
|
||||||
|
const ky = 670
|
||||||
|
ctx.fillStyle = '#f2bd3a'
|
||||||
|
for (const x of [TICKET.x, TICKET.x + TICKET.b]) {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(x, ky, 36, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
ctx.save()
|
||||||
|
ctx.strokeStyle = FARBEN.nett.text
|
||||||
|
ctx.globalAlpha = 0.35
|
||||||
|
ctx.lineWidth = 4
|
||||||
|
ctx.setLineDash([18, 14])
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(TICKET.x + 60, ky)
|
||||||
|
ctx.lineTo(TICKET.x + TICKET.b - 60, ky)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Text ────────────────────────────────────────────────────────────
|
||||||
|
function stempel(ctx, text, y, farbe) {
|
||||||
|
ctx.save()
|
||||||
|
ctx.translate(B / 2, y)
|
||||||
|
ctx.rotate(-0.06)
|
||||||
|
ctx.font = `bold 58px ${SCHRIFT}`
|
||||||
|
ctx.letterSpacing = '6px'
|
||||||
|
ctx.textBaseline = 'middle'
|
||||||
|
const breite = Math.min(ctx.measureText(text).width + 70, B - 160)
|
||||||
|
ctx.strokeStyle = farbe
|
||||||
|
ctx.lineWidth = 6
|
||||||
|
rundeck(ctx, -breite / 2, -52, breite, 96, 12)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.fillStyle = farbe
|
||||||
|
ctx.fillText(text, 3, 2, breite - 40)
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
function malen() {
|
function malen() {
|
||||||
const ctx = leinwand.value?.getContext('2d')
|
const ctx = leinwand.value?.getContext('2d')
|
||||||
if (!ctx) return
|
if (!ctx) return
|
||||||
const horror = form.stil === 'horror'
|
const horror = form.stil === 'horror'
|
||||||
const farbe = {
|
const farbe = FARBEN[form.stil]
|
||||||
hintergrund: horror ? '#0b0b0d' : '#ffe9cf',
|
if (horror) horrorHintergrund(ctx)
|
||||||
text: horror ? '#f1f1f3' : '#3b2a1a',
|
else nettHintergrund(ctx)
|
||||||
leise: horror ? '#9a9aa3' : '#8a6d52',
|
|
||||||
akzent: horror ? '#e50914' : '#e8891d',
|
|
||||||
}
|
|
||||||
const schrift = 'system-ui, -apple-system, "Segoe UI", sans-serif'
|
|
||||||
|
|
||||||
// Hintergrund mit sanftem Lichtfleck
|
|
||||||
ctx.fillStyle = farbe.hintergrund
|
|
||||||
ctx.fillRect(0, 0, B, H)
|
|
||||||
const licht = ctx.createRadialGradient(B / 2, H * 0.45, 80, B / 2, H * 0.45, H * 0.8)
|
|
||||||
licht.addColorStop(0, horror ? 'rgba(229, 9, 20, 0.14)' : 'rgba(255, 250, 240, 0.9)')
|
|
||||||
licht.addColorStop(1, 'rgba(0, 0, 0, 0)')
|
|
||||||
ctx.fillStyle = licht
|
|
||||||
ctx.fillRect(0, 0, B, H)
|
|
||||||
|
|
||||||
if (horror) {
|
|
||||||
// Blut läuft von oben herunter
|
|
||||||
ctx.fillStyle = farbe.akzent
|
|
||||||
ctx.fillRect(0, 0, B, 26)
|
|
||||||
const tropfen = [60, 180, 250, 390, 520, 610, 760, 840, 960, 1030]
|
|
||||||
tropfen.forEach((x, i) => {
|
|
||||||
const laenge = [60, 100, 40, 120, 70, 130, 50, 110, 90, 60][i]
|
|
||||||
rundeck(ctx, x - 9, 10, 18, laenge, 9)
|
|
||||||
ctx.fill()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Ein Rahmen wie eine Kinokarte
|
|
||||||
ctx.strokeStyle = farbe.akzent
|
|
||||||
ctx.lineWidth = 6
|
|
||||||
ctx.setLineDash([22, 14])
|
|
||||||
rundeck(ctx, 40, 40, B - 80, H - 80, 28)
|
|
||||||
ctx.stroke()
|
|
||||||
ctx.setLineDash([])
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.textAlign = 'center'
|
ctx.textAlign = 'center'
|
||||||
ctx.textBaseline = 'alphabetic'
|
ctx.textBaseline = 'alphabetic'
|
||||||
|
|
||||||
// Überschrift
|
// Überschrift
|
||||||
|
ctx.save()
|
||||||
|
if (horror) {
|
||||||
|
ctx.shadowColor = 'rgba(229, 9, 20, 0.7)'
|
||||||
|
ctx.shadowBlur = 40
|
||||||
|
}
|
||||||
ctx.fillStyle = horror ? farbe.akzent : farbe.text
|
ctx.fillStyle = horror ? farbe.akzent : farbe.text
|
||||||
ctx.font = `bold 104px ${schrift}`
|
ctx.font = `bold 104px ${SCHRIFT}`
|
||||||
ctx.letterSpacing = horror ? '8px' : '2px'
|
ctx.letterSpacing = horror ? '10px' : '2px'
|
||||||
const titel = horror ? form.titel.toUpperCase() : form.titel
|
const titel = horror ? form.titel.toUpperCase() : form.titel
|
||||||
ctx.fillText(titel, B / 2, 265, B - 120)
|
ctx.fillText(titel, B / 2, 440, B - 160)
|
||||||
ctx.letterSpacing = '0px'
|
ctx.restore()
|
||||||
|
|
||||||
// Wann und wo
|
// Wann
|
||||||
ctx.fillStyle = farbe.text
|
ctx.fillStyle = farbe.text
|
||||||
ctx.font = `600 46px ${schrift}`
|
ctx.font = `600 50px ${SCHRIFT}`
|
||||||
ctx.fillText(datumText(), B / 2, 345, B - 120)
|
ctx.fillText(datumText(), B / 2, 540, B - 160)
|
||||||
ctx.fillStyle = farbe.leise
|
ctx.fillStyle = farbe.leise
|
||||||
ctx.font = `40px ${schrift}`
|
ctx.font = `42px ${SCHRIFT}`
|
||||||
const wo = [form.uhrzeit && `${form.uhrzeit} Uhr`, form.ort].filter(Boolean).join(' · ')
|
if (form.uhrzeit) ctx.fillText(`${form.uhrzeit} Uhr`, B / 2, 600, B - 160)
|
||||||
ctx.fillText(wo, B / 2, 400, B - 120)
|
|
||||||
|
|
||||||
// Der Film: Poster oder Überraschung
|
// Genre als Stempel
|
||||||
const pb = 420
|
stempel(ctx, form.genre.toUpperCase(), 770, farbe.akzent)
|
||||||
const ph = 630
|
|
||||||
const px = (B - pb) / 2
|
// Kleine Titel über Discord und Webseite
|
||||||
const py = 460
|
const titelZeile = (text, y) => {
|
||||||
if (form.filmZeigen && film.value) {
|
|
||||||
if (poster) {
|
|
||||||
ctx.save()
|
|
||||||
rundeck(ctx, px, py, pb, ph, horror ? 6 : 24)
|
|
||||||
ctx.clip()
|
|
||||||
ctx.drawImage(poster, px, py, pb, ph)
|
|
||||||
if (horror) {
|
|
||||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.25)'
|
|
||||||
ctx.fillRect(px, py, pb, ph)
|
|
||||||
}
|
|
||||||
ctx.restore()
|
|
||||||
ctx.strokeStyle = farbe.akzent
|
|
||||||
ctx.lineWidth = horror ? 4 : 8
|
|
||||||
rundeck(ctx, px, py, pb, ph, horror ? 6 : 24)
|
|
||||||
ctx.stroke()
|
|
||||||
}
|
|
||||||
ctx.fillStyle = farbe.text
|
|
||||||
ctx.font = `bold 54px ${schrift}`
|
|
||||||
const name = film.value.year ? `${film.value.title} (${film.value.year})` : film.value.title
|
|
||||||
let y = poster ? py + ph + 80 : 700
|
|
||||||
for (const z of zeilen(ctx, name, B - 160)) {
|
|
||||||
ctx.fillText(z, B / 2, y)
|
|
||||||
y += 64
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ctx.fillStyle = farbe.leise
|
ctx.fillStyle = farbe.leise
|
||||||
ctx.font = `600 54px ${schrift}`
|
ctx.font = `600 30px ${SCHRIFT}`
|
||||||
ctx.fillText('Film wird ausgelost', B / 2, 760)
|
ctx.letterSpacing = '4px'
|
||||||
ctx.font = `44px ${schrift}`
|
ctx.fillText(text, B / 2, y)
|
||||||
ctx.fillText('Vorschläge aufs Rad!', B / 2, 830)
|
ctx.letterSpacing = '0px'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Der freie Satz
|
titelZeile('DISCORD', 890)
|
||||||
ctx.fillStyle = farbe.text
|
ctx.fillStyle = farbe.text
|
||||||
ctx.font = `44px ${schrift}`
|
ctx.font = `46px ${SCHRIFT}`
|
||||||
let y = 1215
|
let y = 950
|
||||||
for (const z of zeilen(ctx, form.satz, B - 160)) {
|
for (const z of zeilen(ctx, form.ort, B - 200)) {
|
||||||
ctx.fillText(z, B / 2, y)
|
ctx.fillText(z, B / 2, y)
|
||||||
y += 54
|
y += 56
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wo man die Seite findet
|
titelZeile('WEBSEITE', y + 50)
|
||||||
ctx.fillStyle = farbe.leise
|
ctx.fillStyle = farbe.text
|
||||||
ctx.font = `34px ${schrift}`
|
ctx.font = `48px ${SCHRIFT}`
|
||||||
ctx.fillText(location.host, B / 2, H - 50)
|
ctx.fillText(form.url, B / 2, y + 110, B - 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
function speichern() {
|
function speichern() {
|
||||||
@@ -233,12 +333,9 @@ function escape(e) {
|
|||||||
if (e.key === 'Escape') emit('zu')
|
if (e.key === 'Escape') emit('zu')
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(() => {
|
||||||
window.addEventListener('keydown', escape)
|
window.addEventListener('keydown', escape)
|
||||||
malen()
|
malen()
|
||||||
await filmLaden()
|
|
||||||
await posterLaden()
|
|
||||||
malen()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => window.removeEventListener('keydown', escape))
|
onUnmounted(() => window.removeEventListener('keydown', escape))
|
||||||
@@ -254,11 +351,15 @@ onUnmounted(() => window.removeEventListener('keydown', escape))
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
:class="{ an: form.stil === 'horror' }"
|
:class="{ an: form.stil === 'horror' }"
|
||||||
@click="form.stil = 'horror'"
|
@click="stilWaehlen('horror')"
|
||||||
>
|
>
|
||||||
Horror
|
Horror
|
||||||
</button>
|
</button>
|
||||||
<button type="button" :class="{ an: form.stil === 'nett' }" @click="form.stil = 'nett'">
|
<button
|
||||||
|
type="button"
|
||||||
|
:class="{ an: form.stil === 'nett' }"
|
||||||
|
@click="stilWaehlen('nett')"
|
||||||
|
>
|
||||||
Kein Horror
|
Kein Horror
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -278,20 +379,17 @@ onUnmounted(() => window.removeEventListener('keydown', escape))
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<label>
|
<label>
|
||||||
Ort
|
Genre
|
||||||
<input v-model="form.ort" type="text" maxlength="60" placeholder="Bei Marek" />
|
<input v-model="form.genre" type="text" maxlength="24" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Ein Satz
|
Discord
|
||||||
<input v-model="form.satz" type="text" maxlength="90" />
|
<input v-model="form.ort" type="text" maxlength="60" />
|
||||||
</label>
|
</label>
|
||||||
<label v-if="film" class="haken">
|
<label>
|
||||||
<input v-model="form.filmZeigen" type="checkbox" />
|
Webseite
|
||||||
Film zeigen: {{ film.title }}
|
<input v-model="form.url" type="text" maxlength="60" />
|
||||||
</label>
|
</label>
|
||||||
<p v-else class="hinweis">
|
|
||||||
Noch kein Film gezogen — die Karte sagt „Film wird ausgelost".
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="fuss">
|
<div class="fuss">
|
||||||
<button type="button" class="speichern" @click="speichern">Als Bild speichern</button>
|
<button type="button" class="speichern" @click="speichern">Als Bild speichern</button>
|
||||||
@@ -388,19 +486,6 @@ input[type='time'] {
|
|||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
.haken {
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 0.5rem;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--text);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hinweis {
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fuss {
|
.fuss {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|||||||
@@ -236,70 +236,72 @@ async function entfernen(user) {
|
|||||||
<p v-else class="leer">Noch niemand in der Runde.</p>
|
<p v-else class="leer">Noch niemand in der Runde.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-if="host" class="einladung">
|
<div v-if="host" class="rechts">
|
||||||
<h3>Einladung</h3>
|
<section class="einladung">
|
||||||
<p class="erklaerung">
|
<h3>Einladung</h3>
|
||||||
Eine Karte zum Verschicken: Datum, Ort, der gezogene Film. Als Bild fürs Handy.
|
<p class="erklaerung">
|
||||||
</p>
|
Eine Karte zum Verschicken: Datum, Genre, Discord, Webseite. Als Bild fürs Handy.
|
||||||
<button type="button" class="klein" @click="einladung = true">Einladung erstellen</button>
|
</p>
|
||||||
</section>
|
<button type="button" class="klein" @click="einladung = true">Einladung erstellen</button>
|
||||||
<Einladung v-if="einladung" @zu="einladung = false" />
|
</section>
|
||||||
|
<Einladung v-if="einladung" @zu="einladung = false" />
|
||||||
|
|
||||||
<section v-if="host" class="hostfilm">
|
<section class="hostfilm">
|
||||||
<h3>Host-Film</h3>
|
<h3>Host-Film</h3>
|
||||||
<p class="erklaerung">
|
<p class="erklaerung">
|
||||||
Die Tastenkombination fragt ohne Freischaltung nach diesem Film. Wer ihn anklickt, ist Host
|
Die Tastenkombination fragt ohne Freischaltung nach diesem Film. Wer ihn anklickt, ist
|
||||||
— ohne Link.
|
Host — ohne Link.
|
||||||
</p>
|
</p>
|
||||||
<div class="zeile">
|
<div class="zeile">
|
||||||
<button type="button" class="klein" @click="hostFilmDialog = true">Film setzen</button>
|
<button type="button" class="klein" @click="hostFilmDialog = true">Film setzen</button>
|
||||||
<button type="button" class="klein" title="Anzeigen" @click="hostFilmZeigen">👁</button>
|
<button type="button" class="klein" title="Anzeigen" @click="hostFilmZeigen">👁</button>
|
||||||
<button type="button" class="klein" @click="hostFilmSetzen(null)">Aufheben</button>
|
<button type="button" class="klein" @click="hostFilmSetzen(null)">Aufheben</button>
|
||||||
<span v-if="hostFilm" class="film-anzeige">
|
<span v-if="hostFilm" class="film-anzeige">
|
||||||
<template v-if="hostFilm.laden">…</template>
|
<template v-if="hostFilm.laden">…</template>
|
||||||
<template v-else-if="hostFilm.fehler">nicht lesbar</template>
|
<template v-else-if="hostFilm.fehler">nicht lesbar</template>
|
||||||
<template v-else-if="hostFilm.film"
|
<template v-else-if="hostFilm.film"
|
||||||
>{{ hostFilm.film.title }} ({{ hostFilm.film.year }})</template
|
>{{ hostFilm.film.title }} ({{ hostFilm.film.year }})</template
|
||||||
>
|
>
|
||||||
<template v-else>keiner gesetzt</template>
|
<template v-else>keiner gesetzt</template>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-if="host" class="aufnahmen">
|
<section class="aufnahmen">
|
||||||
<h3>Aufnahmen</h3>
|
<h3>Aufnahmen</h3>
|
||||||
<p v-if="aufnahmenFehler" class="fehler">{{ aufnahmenFehler }}</p>
|
<p v-if="aufnahmenFehler" class="fehler">{{ aufnahmenFehler }}</p>
|
||||||
<p v-if="aufnahmen === null" class="leer">Suche Aufnahmen …</p>
|
<p v-if="aufnahmen === null" class="leer">Suche Aufnahmen …</p>
|
||||||
<p v-else-if="!aufnahmen.length" class="leer">Keine Aufnahmen.</p>
|
<p v-else-if="!aufnahmen.length" class="leer">Keine Aufnahmen.</p>
|
||||||
<ul v-else>
|
<ul v-else>
|
||||||
<li v-for="s in aufnahmen" :key="s.start" class="zeile">
|
<li v-for="s in aufnahmen" :key="s.start" class="zeile">
|
||||||
<span>
|
<span>
|
||||||
{{ aufnahmeText(s) }} ·
|
{{ aufnahmeText(s) }} ·
|
||||||
<strong v-if="s.film">{{ s.film.title }}</strong>
|
<strong v-if="s.film">{{ s.film.title }}</strong>
|
||||||
<em v-else>kein Film</em>
|
<em v-else>kein Film</em>
|
||||||
</span>
|
</span>
|
||||||
<span class="knoepfe">
|
<span class="knoepfe">
|
||||||
<button type="button" class="klein" @click="filmDialogFuer = s.start">Film</button>
|
<button type="button" class="klein" @click="filmDialogFuer = s.start">Film</button>
|
||||||
<button
|
<button
|
||||||
v-if="s.zugewiesen"
|
v-if="s.zugewiesen"
|
||||||
type="button"
|
type="button"
|
||||||
class="klein"
|
class="klein"
|
||||||
@click="filmZuweisen(s.start, null)"
|
@click="filmZuweisen(s.start, null)"
|
||||||
>
|
>
|
||||||
Aufheben
|
Aufheben
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="klein"
|
class="klein"
|
||||||
:class="{ warnung: loeschFrage === s.start }"
|
:class="{ warnung: loeschFrage === s.start }"
|
||||||
@click="aufnahmeLoeschen(s)"
|
@click="aufnahmeLoeschen(s)"
|
||||||
>
|
>
|
||||||
{{ loeschFrage === s.start ? 'Wirklich löschen?' : 'Löschen' }}
|
{{ loeschFrage === s.start ? 'Wirklich löschen?' : 'Löschen' }}
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<div v-if="filmDialogFuer" class="hintergrund" @click.self="filmDialogFuer = null">
|
<div v-if="filmDialogFuer" class="hintergrund" @click.self="filmDialogFuer = null">
|
||||||
@@ -361,7 +363,28 @@ async function entfernen(user) {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.verwaltung-tab {
|
.verwaltung-tab {
|
||||||
max-width: 34rem;
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
gap: 2.5rem 3.5rem;
|
||||||
|
align-items: start;
|
||||||
|
max-width: 72rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mit Host-Rechten: Benutzer links, der Rest rechts */
|
||||||
|
.verwaltung-tab:has(.rechts) {
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rechts {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.verwaltung-tab:has(.rechts) {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
@@ -399,20 +422,21 @@ h3 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.liste {
|
.liste {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.3rem;
|
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
background: var(--bg-soft);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.liste li {
|
.liste li {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.4rem;
|
||||||
padding: 0.55rem 0.85rem;
|
padding: 0.4rem 0.7rem;
|
||||||
background: var(--bg-soft);
|
}
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 8px;
|
.liste li + li {
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
|
|
||||||
.zeile {
|
.zeile {
|
||||||
@@ -491,7 +515,7 @@ h3 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.erklaerung {
|
.erklaerung {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 0.8rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user