various display fixes
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue";
|
import { computed, useTemplateRef } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useTitle } from "@vueuse/core";
|
import { useElementSize, useTitle } from "@vueuse/core";
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
import { eventPlanningQuery } from "@/queries";
|
import { eventPlanningQuery } from "@/queries";
|
||||||
import type { PlanningSlotOut } from "./api/types.gen";
|
import type { PlanningSlotOut } from "./api/types.gen";
|
||||||
@@ -20,12 +20,19 @@ useTitle(computed(() => t("planning.title")));
|
|||||||
const { data: slots, isPending: loading } = useQuery(eventPlanningQuery(props.eventPath));
|
const { data: slots, isPending: loading } = useQuery(eventPlanningQuery(props.eventPath));
|
||||||
|
|
||||||
const HOUR = 3600e3;
|
const HOUR = 3600e3;
|
||||||
const HOUR_PX = 104; // a 2h slot ends up close to the 2:3 cover ratio
|
// Hour height per px of column width: a 2h slot ends up close to the 2:3 cover ratio
|
||||||
|
const HOUR_RATIO = 104 / 150;
|
||||||
const BREAK_PX = 34;
|
const BREAK_PX = 34;
|
||||||
const DAY_PX = 32;
|
const DAY_PX = 32;
|
||||||
const AXIS_PX = 56;
|
const AXIS_PX = 56;
|
||||||
const COL_PX = 150;
|
// Columns shrink to fit the page, the planning only scrolls sideways below the min
|
||||||
|
const MIN_COL_PX = 80;
|
||||||
|
const MAX_COL_PX = 150;
|
||||||
const GAP_PX = 10;
|
const GAP_PX = 10;
|
||||||
|
// The multi-anims column is the big one, its covers 1.75x the size of the others
|
||||||
|
const WIDE_RATIO = 1.75;
|
||||||
|
// Room on the left for the day names, they stick out of the time axis
|
||||||
|
const DAY_LABEL_ROOM_PX = 120;
|
||||||
// Empty runs at least this long (nights) get squashed into a thin break
|
// Empty runs at least this long (nights) get squashed into a thin break
|
||||||
const MIN_BREAK_HOURS = 3;
|
const MIN_BREAK_HOURS = 3;
|
||||||
|
|
||||||
@@ -48,6 +55,9 @@ interface Placed {
|
|||||||
when: string;
|
when: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scroller = useTemplateRef<HTMLElement>("scroller");
|
||||||
|
const { width: available } = useElementSize(scroller);
|
||||||
|
|
||||||
const layout = computed(() => {
|
const layout = computed(() => {
|
||||||
const list = (slots.value ?? []).map((slot) => ({
|
const list = (slots.value ?? []).map((slot) => ({
|
||||||
slot,
|
slot,
|
||||||
@@ -56,6 +66,16 @@ const layout = computed(() => {
|
|||||||
}));
|
}));
|
||||||
if (!list.length) return null;
|
if (!list.length) return null;
|
||||||
|
|
||||||
|
// Unused columns are dropped, the multi-anims ones are wider
|
||||||
|
const columnIds = [...new Set(list.map(({ slot }) => slot.column))].sort((a, b) => a - b);
|
||||||
|
const isWide = (id: number) => list.some(({ slot }) => slot.column === id && slot.is_multi_anims);
|
||||||
|
const wideCount = columnIds.filter(isWide).length;
|
||||||
|
const fit =
|
||||||
|
(available.value - DAY_LABEL_ROOM_PX - AXIS_PX - (columnIds.length - 1) * GAP_PX) /
|
||||||
|
(columnIds.length + (WIDE_RATIO - 1) * wideCount);
|
||||||
|
const colPx = available.value ? Math.min(MAX_COL_PX, Math.max(MIN_COL_PX, fit)) : MAX_COL_PX;
|
||||||
|
const hourPx = colPx * HOUR_RATIO;
|
||||||
|
|
||||||
// Multi-anims run the whole event, only the other slots decide what's busy
|
// Multi-anims run the whole event, only the other slots decide what's busy
|
||||||
const singles = list.filter(({ slot }) => !slot.is_multi_anims);
|
const singles = list.filter(({ slot }) => !slot.is_multi_anims);
|
||||||
const busy = (t: number) => singles.some(({ b, e }) => b < t + HOUR && e > t);
|
const busy = (t: number) => singles.some(({ b, e }) => b < t + HOUR && e > t);
|
||||||
@@ -75,8 +95,8 @@ const layout = computed(() => {
|
|||||||
y += DAY_PX;
|
y += DAY_PX;
|
||||||
lastDay = day;
|
lastDay = day;
|
||||||
}
|
}
|
||||||
segments.push({ from: t, to: t + HOUR, y, h: HOUR_PX, kind: "hour" });
|
segments.push({ from: t, to: t + HOUR, y, h: hourPx, kind: "hour" });
|
||||||
y += HOUR_PX;
|
y += hourPx;
|
||||||
t += HOUR;
|
t += HOUR;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -84,7 +104,7 @@ const layout = computed(() => {
|
|||||||
while (u < end && !busy(u)) u += HOUR;
|
while (u < end && !busy(u)) u += HOUR;
|
||||||
const hours = (u - t) / HOUR;
|
const hours = (u - t) / HOUR;
|
||||||
const isBreak = hours >= MIN_BREAK_HOURS;
|
const isBreak = hours >= MIN_BREAK_HOURS;
|
||||||
const h = isBreak ? BREAK_PX : HOUR_PX * hours;
|
const h = isBreak ? BREAK_PX : hourPx * hours;
|
||||||
segments.push({ from: t, to: u, y, h, kind: isBreak ? "break" : "idle" });
|
segments.push({ from: t, to: u, y, h, kind: isBreak ? "break" : "idle" });
|
||||||
y += h;
|
y += h;
|
||||||
t = u;
|
t = u;
|
||||||
@@ -116,8 +136,6 @@ const layout = computed(() => {
|
|||||||
.filter((s) => s.kind === "break")
|
.filter((s) => s.kind === "break")
|
||||||
.map((s) => ({ y: s.y, h: s.h, label: `${fmtTime.format(s.from)} – ${fmtTime.format(s.to)}` }));
|
.map((s) => ({ y: s.y, h: s.h, label: `${fmtTime.format(s.from)} – ${fmtTime.format(s.to)}` }));
|
||||||
|
|
||||||
// Unused columns are dropped
|
|
||||||
const columnIds = [...new Set(list.map(({ slot }) => slot.column))].sort((a, b) => a - b);
|
|
||||||
let left = AXIS_PX;
|
let left = AXIS_PX;
|
||||||
const columns = columnIds.map((id) => {
|
const columns = columnIds.map((id) => {
|
||||||
const placed = list
|
const placed = list
|
||||||
@@ -128,8 +146,7 @@ const layout = computed(() => {
|
|||||||
height: yOf(e) - yOf(b) - 6,
|
height: yOf(e) - yOf(b) - 6,
|
||||||
when: `${fmtTime.format(b)} – ${fmtTime.format(e)}`,
|
when: `${fmtTime.format(b)} – ${fmtTime.format(e)}`,
|
||||||
}));
|
}));
|
||||||
// The multi-anims column is the big one, twice as wide
|
const width = isWide(id) ? WIDE_RATIO * colPx : colPx;
|
||||||
const width = placed.some(({ slot }) => slot.is_multi_anims) ? 2 * COL_PX + GAP_PX : COL_PX;
|
|
||||||
const column = { left, width, placed };
|
const column = { left, width, placed };
|
||||||
left += width + GAP_PX;
|
left += width + GAP_PX;
|
||||||
return column;
|
return column;
|
||||||
@@ -148,7 +165,8 @@ const layout = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex-1 bg-base-300 flex justify-center px-4 py-12">
|
<div class="flex-1 bg-base-300 flex justify-center px-4 py-12">
|
||||||
<div class="w-full max-w-7xl">
|
<!-- min-w-0: lets the planning scroll sideways instead of overflowing the page -->
|
||||||
|
<div class="w-full max-w-7xl min-w-0">
|
||||||
<div class="text-center mb-12">
|
<div class="text-center mb-12">
|
||||||
<h1 class="text-5xl font-bold mb-4">{{ t("planning.title") }}</h1>
|
<h1 class="text-5xl font-bold mb-4">{{ t("planning.title") }}</h1>
|
||||||
</div>
|
</div>
|
||||||
@@ -161,8 +179,13 @@ const layout = computed(() => {
|
|||||||
{{ t("planning.empty") }}
|
{{ t("planning.empty") }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div v-else class="overflow-x-auto pb-4">
|
<!-- pr/pt/pb: room for the Fils rouges glow, the scroll box would clip it -->
|
||||||
<div class="relative mx-auto" :style="{ width: `${layout.width}px`, height: `${layout.height}px` }">
|
<div v-else ref="scroller" class="overflow-x-auto pr-10 pt-4 pb-10">
|
||||||
|
<div class="relative" :style="{
|
||||||
|
width: `${layout.width}px`,
|
||||||
|
height: `${layout.height}px`,
|
||||||
|
marginLeft: `max(${DAY_LABEL_ROOM_PX}px, (100% - ${layout.width}px) / 2)`,
|
||||||
|
}">
|
||||||
<!-- Time axis: a header row per day, hour ticks on the side -->
|
<!-- Time axis: a header row per day, hour ticks on the side -->
|
||||||
<!-- Above the cards: slots running past midnight go over the header row -->
|
<!-- Above the cards: slots running past midnight go over the header row -->
|
||||||
<div v-for="day in layout.days" :key="`day-${day.y}`"
|
<div v-for="day in layout.days" :key="`day-${day.y}`"
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const offset = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref="track" class="relative flex-1">
|
<div ref="track" class="relative flex-1">
|
||||||
<div ref="stack" class="absolute inset-x-0 grid grid-cols-2 gap-3"
|
<div ref="stack" class="absolute inset-x-0 flex flex-col gap-3"
|
||||||
:style="{ transform: `translateY(${offset}px)` }">
|
:style="{ transform: `translateY(${offset}px)` }">
|
||||||
<component :is="game.store_url ? 'a' : 'div'" v-for="game in entry.games" :key="game.name"
|
<component :is="game.store_url ? 'a' : 'div'" v-for="game in entry.games" :key="game.name"
|
||||||
:href="game.store_url || undefined" target="_blank" rel="noopener noreferrer"
|
:href="game.store_url || undefined" target="_blank" rel="noopener noreferrer"
|
||||||
|
|||||||
Reference in New Issue
Block a user