Implement AvailabilityGrid component
parent
280e02c15a
commit
a11b6f454f
|
@ -40,7 +40,10 @@
|
|||
--green: #a6e3a1;
|
||||
--lavender: #7287fd;
|
||||
--accent: var(--lavender);
|
||||
--accent-transparent: color-mix(in srgb, var(--accent), transparent 80%);
|
||||
--accent-transparent-80: color-mix(in srgb, var(--accent), transparent 80%);
|
||||
--accent-transparent-50: color-mix(in srgb, var(--accent), transparent 50%);
|
||||
--accent-transparent: var(--accent-transparent-80);
|
||||
--shadow: color-mix(in srgb, var(--text), transparent 50%);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -25,10 +25,23 @@ a,
|
|||
button {
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
background-color: var(--surface-0);
|
||||
background-color: var(--crust);
|
||||
border: none;
|
||||
padding: 8px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
button.accent {
|
||||
|
@ -37,6 +50,10 @@ button.accent {
|
|||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
button.accent {
|
||||
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: 800;
|
||||
font-size: 200%;
|
||||
|
@ -46,3 +63,7 @@ h1 {
|
|||
em.aside {
|
||||
color: var(--overlay-0);
|
||||
}
|
||||
|
||||
select {
|
||||
appearance: none;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, defineModel, defineProps, ref } from "vue";
|
||||
|
||||
const model = defineModel();
|
||||
|
||||
const props = defineProps({
|
||||
options: Array<String>,
|
||||
});
|
||||
|
||||
const isOpen = ref(false);
|
||||
const selectedOption = computed(() => props.options[model.value]);
|
||||
|
||||
function selectOption(index) {
|
||||
model.value = index;
|
||||
isOpen.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{ 'dropdown-container': true, 'is-open': isOpen }">
|
||||
<button @click="isOpen = !isOpen">
|
||||
{{ selectedOption }}
|
||||
</button>
|
||||
<ul class="dropdown" v-if="isOpen" @blur="isOpen = false">
|
||||
<li v-for="(option, i) in options" :key="i" @click="selectOption(i)">
|
||||
<button :class="{ 'is-selected': i == model }">
|
||||
{{ option }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dropdown-container {
|
||||
display: inline-block;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.dropdown-container button {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
padding: 4px;
|
||||
transition-duration: 200ms;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown-container button:hover {
|
||||
background-color: var(--crust);
|
||||
}
|
||||
|
||||
.dropdown-container.is-open ul.dropdown {
|
||||
box-shadow: 1px 1px 8px var(--shadow);
|
||||
}
|
||||
|
||||
ul.dropdown {
|
||||
display: block;
|
||||
background-color: var(--base);
|
||||
position: absolute;
|
||||
margin-top: 8px;
|
||||
padding: 0;
|
||||
z-index: 2;
|
||||
border-radius: 8px;
|
||||
overflow: none;
|
||||
}
|
||||
|
||||
ul.dropdown > li {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.dropdown li > button {
|
||||
padding: 8px 16px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.dropdown li > button.is-selected {
|
||||
background-color: var(--accent-transparent);
|
||||
color: var(--accent);
|
||||
}
|
||||
</style>
|
|
@ -1,11 +1,268 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, defineModel, defineProps, reactive, ref, onMounted, onUnmounted } from "vue";
|
||||
|
||||
const model = defineModel();
|
||||
const firstHour = 15;
|
||||
const lastHour = 23;
|
||||
|
||||
const windowStart = new Date(2024, 9, 21);
|
||||
|
||||
const props = defineProps({
|
||||
selectionMode: Number,
|
||||
});
|
||||
|
||||
const selectionStart = reactive({ x: undefined, y: undefined });
|
||||
const selectionEnd = reactive({ x: undefined, y: undefined });
|
||||
const isCtrlDown = ref(false);
|
||||
const isShiftDown = ref(false);
|
||||
|
||||
const lowerBoundX = computed(() => {
|
||||
return isShiftDown.value ? 0 :
|
||||
Math.min(selectionStart.x, selectionEnd.x)
|
||||
});
|
||||
const upperBoundX = computed(() => {
|
||||
return isShiftDown.value ? 7 :
|
||||
Math.max(selectionStart.x, selectionEnd.x)
|
||||
});
|
||||
const lowerBoundY = computed(() => {
|
||||
return isCtrlDown.value ? firstHour :
|
||||
Math.min(selectionStart.y, selectionEnd.y)
|
||||
});
|
||||
const upperBoundY = computed(() => {
|
||||
return isCtrlDown.value ? lastHour :
|
||||
Math.max(selectionStart.y, selectionEnd.y)
|
||||
});
|
||||
|
||||
function selectionInside(dayIndex, hour) {
|
||||
if (selectionStart.x != undefined) {
|
||||
return (dayIndex >= lowerBoundX.value && dayIndex <= upperBoundX.value) &&
|
||||
(hour >= lowerBoundY.value && hour <= upperBoundY.value);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const days = computed(() => {
|
||||
let ret = [];
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const date = new Date(windowStart);
|
||||
date.setDate(windowStart.getDate() + i);
|
||||
ret.push(date);
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
|
||||
const hours = computed(() => {
|
||||
return Array.from(Array(lastHour - firstHour + 1).keys())
|
||||
.map(x => x + firstHour);
|
||||
});
|
||||
|
||||
const daysOfWeek = [
|
||||
"Sun",
|
||||
"Mon",
|
||||
"Tue",
|
||||
"Wed",
|
||||
"Thu",
|
||||
"Fri",
|
||||
"Sat"
|
||||
];
|
||||
|
||||
const isMouseDown = ref(false);
|
||||
const selectionValue = ref(0);
|
||||
|
||||
function onSlotMouseDown($event, x, y) {
|
||||
selectionValue.value = model.value[24 * x + y] == props.selectionMode ?
|
||||
0 : props.selectionMode;
|
||||
|
||||
selectionStart.x = x;
|
||||
selectionStart.y = y;
|
||||
selectionEnd.x = x;
|
||||
selectionEnd.y = y;
|
||||
console.log("selected " + x + " " + y);
|
||||
}
|
||||
|
||||
function onSlotMouseOver($event, x, y) {
|
||||
if ($event.buttons & 1 == 1) {
|
||||
//if (isAdding.value) {
|
||||
// model.value[24 * x + y] = props.selectionMode;
|
||||
//} else {
|
||||
// model.value[24 * x + y] = 0;
|
||||
//}
|
||||
console.log("selected " + (24 * x + y));
|
||||
selectionEnd.x = x;
|
||||
selectionEnd.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
function onSlotMouseUp($event) {
|
||||
console.log("mouseup");
|
||||
console.log(selectionStart);
|
||||
if (selectionStart.x == undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let x = lowerBoundX.value; x <= upperBoundX.value; x++) {
|
||||
for (let y = lowerBoundY.value; y <= upperBoundY.value; y++) {
|
||||
model.value[24 * x + y] = selectionValue.value;
|
||||
}
|
||||
}
|
||||
|
||||
selectionStart.x = undefined;
|
||||
}
|
||||
|
||||
function onKeyUp($event) {
|
||||
switch ($event.key) {
|
||||
case "Shift":
|
||||
isShiftDown.value = false;
|
||||
break;
|
||||
case "Control":
|
||||
isCtrlDown.value = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyDown($event) {
|
||||
switch ($event.key) {
|
||||
case "Shift":
|
||||
isShiftDown.value = true;
|
||||
break;
|
||||
case "Control":
|
||||
isCtrlDown.value = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
window.addEventListener("mouseup", onSlotMouseUp);
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
window.addEventListener("keyup", onKeyUp);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
console.log("removing");
|
||||
window.removeEventListener("mouseup", onSlotMouseUp);
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
window.removeEventListener("keyup", onKeyUp);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid">
|
||||
|
||||
<div>
|
||||
<div class="height-48px"></div>
|
||||
<div class="height-24px hour-marker-container" v-for="hour, i in hours" :key="i">
|
||||
<span class="hour-marker" v-if="i % 2 == 0 || i == hours.length">
|
||||
{{ hour % 24 }}:30 ({{ (hour + 3) % 24 }}:30 EST)
|
||||
</span>
|
||||
</div>
|
||||
<div class="height-24px hour-marker-container">
|
||||
<span class="hour-marker">
|
||||
{{ (lastHour + 1) % 24 }}:30 ({{ (lastHour + 4) % 24 }}:30 EST)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(day, dayIndex) in days" :key="dayIndex" class="column">
|
||||
<div class="date">
|
||||
<div class="day-of-week">{{ daysOfWeek[day.getDay()] }}</div>
|
||||
<div class="day">{{ day.getDate() }}</div>
|
||||
</div>
|
||||
<div class="column-time-slots">
|
||||
<div
|
||||
:class="{
|
||||
'time-slot': true,
|
||||
'height-24px': true,
|
||||
}"
|
||||
:selection="
|
||||
selectionInside(dayIndex, hour) ? selectionValue
|
||||
: model[24 * dayIndex + hour]
|
||||
"
|
||||
v-for="hour in hours"
|
||||
@mousedown="onSlotMouseDown($event, dayIndex, hour)"
|
||||
@mouseover="onSlotMouseOver($event, dayIndex, hour)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.height-24px {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.height-32px {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.height-48px {
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.hour-marker-container {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.hour-marker {
|
||||
font-size: 12px;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
top: -0.75rem;
|
||||
margin-right: 8px;
|
||||
color: var(--subtext-0);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.grid > .column > .column-time-slots {
|
||||
width: 72px;
|
||||
border: 4px;
|
||||
border: 1px solid var(--text);
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.grid > .column:nth-child(2) > .column-time-slots {
|
||||
border-left: 1px solid var(--text);
|
||||
}
|
||||
|
||||
.date {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: end;
|
||||
text-align: center;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.date .day-of-week {
|
||||
color: var(--subtext-0);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
line-height: 0;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.date .day {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.time-slot:hover {
|
||||
background-color: var(--crust);
|
||||
outline: 2px inset var(--subtext-0);
|
||||
}
|
||||
|
||||
.time-slot:nth-child(2n):not(:last-child) {
|
||||
border-bottom: 1px dashed var(--text);
|
||||
}
|
||||
|
||||
.time-slot[selection="1"] {
|
||||
background-color: var(--accent-transparent-80);
|
||||
}
|
||||
|
||||
.time-slot[selection="2"] {
|
||||
background-color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -52,6 +52,11 @@ function onClick() {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
const playtime = computed(() => {
|
||||
let hours = props.player?.playtime / 3600 ?? 0;
|
||||
return hours.toFixed(1);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -67,19 +72,26 @@ function onClick() {
|
|||
<div v-if="player" class="role-info">
|
||||
<span>
|
||||
<h4 class="player-name">{{ player.name }}</h4>
|
||||
<span v-if="roleTitle != player.role">
|
||||
Subbing in as
|
||||
</span>
|
||||
{{ player.role }}
|
||||
<span v-if="!player.main && isRoster">
|
||||
(alternate)
|
||||
</span>
|
||||
<div class="subtitle">
|
||||
<span>
|
||||
{{ player.role }}
|
||||
<span v-if="!player.main && isRoster">
|
||||
(alternate)
|
||||
</span>
|
||||
</span>
|
||||
<span v-if="playtime > 0">
|
||||
{{ playtime }} hours
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<div v-else-if="isRinger" class="role-info">
|
||||
<span>
|
||||
<h4 class="player-name">Ringer</h4>
|
||||
{{ roleTitle }}
|
||||
<div class="subtitle">
|
||||
<span>{{ roleTitle }}</span>
|
||||
<span>nobody likes to play {{ roleTitle }}</span>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="role-info">
|
||||
|
@ -116,6 +128,13 @@ function onClick() {
|
|||
|
||||
.player-card .role-info {
|
||||
text-align: left;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.role-info .subtitle {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.player-card:hover {
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
import { defineModel } from "vue";
|
||||
|
||||
const model = defineModel();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="scroll-box">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.scroll-box {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
|
@ -65,6 +65,14 @@ export const useRosterStore = defineStore("roster", () => {
|
|||
availability: 2,
|
||||
playtime: 47324,
|
||||
},
|
||||
{
|
||||
steamId: 2083,
|
||||
name: "IF_YOU_READ_THIS_",
|
||||
role: "Demoman",
|
||||
main: true,
|
||||
availability: 1,
|
||||
playtime: 32812,
|
||||
},
|
||||
{
|
||||
steamId: 2842,
|
||||
name: "BossOfThisGym",
|
||||
|
@ -142,14 +150,24 @@ export const useRosterStore = defineStore("roster", () => {
|
|||
return availablePlayerRoles.value.filter((player) => player.availability == 1);
|
||||
});
|
||||
|
||||
function comparator(p1: PlayerTeamRole, p2: PlayerTeamRole) {
|
||||
// definitely available > can be available
|
||||
let availabilityDiff = p1.availability - p2.availability;
|
||||
|
||||
// less playtime is preferred
|
||||
let playtimeDiff = p1.playtime - p2.playtime;
|
||||
|
||||
return availabilityDiff || playtimeDiff;
|
||||
}
|
||||
|
||||
const mainRoles = computed(() => {
|
||||
return availablePlayerRoles.value.filter((player) => player.main)
|
||||
.sort((a, b) => b.playtime - a.playtime);
|
||||
.sort(comparator);
|
||||
});
|
||||
|
||||
const alternateRoles = computed(() => {
|
||||
return availablePlayerRoles.value.filter((player) => !player.main)
|
||||
.sort((a, b) => b.playtime - a.playtime);
|
||||
.sort(comparator);
|
||||
});
|
||||
|
||||
const roleIcons = reactive({
|
||||
|
|
|
@ -24,11 +24,8 @@ const hasAlternates = computed(() => {
|
|||
<em class="aside date">Aug. 13, 2036 @ 11:30 PM EST</em>
|
||||
</h1>
|
||||
<div class="button-group">
|
||||
<button>
|
||||
<i class="bi bi-box-arrow-left"></i>
|
||||
Back
|
||||
</button>
|
||||
<button class="accent">Submit</button>
|
||||
<button>Cancel</button>
|
||||
<button class="accent">Save Roster</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
|
|
|
@ -1,8 +1,75 @@
|
|||
<script setup lang="ts">
|
||||
import AvailabilityGrid from "../components/AvailabilityGrid.vue";
|
||||
import AvailabilityComboBox from "../components/AvailabilityComboBox.vue";
|
||||
import WeekSelectionBox from "../components/WeekSelectionBox.vue";
|
||||
import { reactive, ref } from "vue";
|
||||
|
||||
const options = reactive([
|
||||
"Team pepeja",
|
||||
"Snus Brotherhood",
|
||||
]);
|
||||
|
||||
const comboBoxIndex = ref(0);
|
||||
|
||||
const availability = reactive(new Array(168));
|
||||
|
||||
const selectionMode = ref(1);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<h1>Master Schedule</h1>
|
||||
<div v-if="options.length > 0">
|
||||
<div>
|
||||
Availability for
|
||||
<AvailabilityComboBox :options="options" v-model="comboBoxIndex" />
|
||||
<WeekSelectionBox />
|
||||
</div>
|
||||
<AvailabilityGrid v-model="availability" :selection-mode="selectionMode" />
|
||||
<div class="radio-group">
|
||||
<button
|
||||
:class="{ 'radio': true, 'selected': selectionMode == 1, 'left': true }"
|
||||
@click="selectionMode = 1"
|
||||
>
|
||||
Available if needed
|
||||
</button>
|
||||
<button
|
||||
:class="{ 'radio': true, 'selected': selectionMode == 2, 'right': true }"
|
||||
@click="selectionMode = 2"
|
||||
>
|
||||
Definitely available
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
You currently are not in any team to schedule for.
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.radio-group {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
button.radio {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
button.radio:hover {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
button.radio.selected {
|
||||
color: var(--accent);
|
||||
background-color: var(--accent-transparent);
|
||||
}
|
||||
|
||||
button.left {
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
button.right {
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Reference in New Issue