Refactored frontend directory
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 2m46s
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 2m46s
This commit is contained in:
161
frontend/src/components/shared/TimeSelector.vue
Normal file
161
frontend/src/components/shared/TimeSelector.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<div class="time-selector">
|
||||
<!-- Hour column -->
|
||||
<div class="time-col">
|
||||
<button class="arrow-btn" @click="incrementHour" aria-label="Increase hour">▲</button>
|
||||
<div class="time-value">{{ displayHour }}</div>
|
||||
<button class="arrow-btn" @click="decrementHour" aria-label="Decrease hour">▼</button>
|
||||
</div>
|
||||
|
||||
<div class="time-separator">:</div>
|
||||
|
||||
<!-- Minute column -->
|
||||
<div class="time-col">
|
||||
<button class="arrow-btn" @click="incrementMinute" aria-label="Increase minute">▲</button>
|
||||
<div class="time-value">{{ displayMinute }}</div>
|
||||
<button class="arrow-btn" @click="decrementMinute" aria-label="Decrease minute">▼</button>
|
||||
</div>
|
||||
|
||||
<!-- AM/PM toggle -->
|
||||
<button class="ampm-btn" @click="toggleAmPm">{{ period }}</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface TimeValue {
|
||||
hour: number // 0–23 (24h)
|
||||
minute: number // 0, 15, 30, or 45
|
||||
}
|
||||
|
||||
const props = defineProps<{ modelValue: TimeValue }>()
|
||||
const emit = defineEmits<{ (e: 'update:modelValue', val: TimeValue): void }>()
|
||||
|
||||
const MINUTES = [0, 15, 30, 45]
|
||||
|
||||
// ── display helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
const period = computed(() => (props.modelValue.hour < 12 ? 'AM' : 'PM'))
|
||||
|
||||
const displayHour = computed(() => {
|
||||
const h = props.modelValue.hour % 12
|
||||
return String(h === 0 ? 12 : h)
|
||||
})
|
||||
|
||||
const displayMinute = computed(() => String(props.modelValue.minute).padStart(2, '0'))
|
||||
|
||||
// ── mutations ────────────────────────────────────────────────────────────────
|
||||
|
||||
function incrementHour() {
|
||||
const next = (props.modelValue.hour + 1) % 24
|
||||
emit('update:modelValue', { hour: next, minute: props.modelValue.minute })
|
||||
}
|
||||
|
||||
function decrementHour() {
|
||||
const next = (props.modelValue.hour - 1 + 24) % 24
|
||||
emit('update:modelValue', { hour: next, minute: props.modelValue.minute })
|
||||
}
|
||||
|
||||
function incrementMinute() {
|
||||
const idx = MINUTES.indexOf(props.modelValue.minute)
|
||||
if (idx === MINUTES.length - 1) {
|
||||
// Wrap minute to 0 and carry into next hour
|
||||
const nextHour = (props.modelValue.hour + 1) % 24
|
||||
emit('update:modelValue', { hour: nextHour, minute: 0 })
|
||||
} else {
|
||||
emit('update:modelValue', { hour: props.modelValue.hour, minute: MINUTES[idx + 1] })
|
||||
}
|
||||
}
|
||||
|
||||
function decrementMinute() {
|
||||
const idx = MINUTES.indexOf(props.modelValue.minute)
|
||||
if (idx === 0) {
|
||||
// Wrap minute to 45 and borrow from previous hour
|
||||
const prevHour = (props.modelValue.hour - 1 + 24) % 24
|
||||
emit('update:modelValue', { hour: prevHour, minute: 45 })
|
||||
} else {
|
||||
emit('update:modelValue', { hour: props.modelValue.hour, minute: MINUTES[idx - 1] })
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAmPm() {
|
||||
const next = props.modelValue.hour < 12 ? props.modelValue.hour + 12 : props.modelValue.hour - 12
|
||||
emit('update:modelValue', { hour: next, minute: props.modelValue.minute })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.time-selector {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.time-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.arrow-btn {
|
||||
width: 1.8rem;
|
||||
height: 1.8rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: 1px solid var(--kebab-menu-border, #bcc1c9);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
color: var(--primary, #667eea);
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.arrow-btn:hover {
|
||||
background: var(--menu-item-hover-bg, rgba(102, 126, 234, 0.08));
|
||||
}
|
||||
|
||||
.time-value {
|
||||
width: 1.8rem;
|
||||
height: 1.8rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--secondary, #7257b3);
|
||||
border: 1.5px solid var(--kebab-menu-border, #bcc1c9);
|
||||
border-radius: 6px;
|
||||
background: var(--modal-bg, #fff);
|
||||
}
|
||||
|
||||
.time-separator {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--secondary, #7257b3);
|
||||
padding-bottom: 0.1rem;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.ampm-btn {
|
||||
width: 2.2rem;
|
||||
height: 1.8rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
border: 1.5px solid var(--btn-primary, #667eea);
|
||||
border-radius: 6px;
|
||||
background: var(--btn-primary, #667eea);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
margin-left: 0.35rem;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.ampm-btn:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user