223 lines
4.5 KiB
Vue
223 lines
4.5 KiB
Vue
<script setup lang="ts">
|
|
import { useTitle, useUrlSearchParams } from "@vueuse/core"
|
|
import { computed, ref, watch } from "vue"
|
|
import { useTimeUntil } from "../hooks/useTimeUntil.hooks"
|
|
|
|
const props = defineProps<{ project?: string; target?: string }>()
|
|
const searchParams = useUrlSearchParams<{ project?: string; target?: string }>(
|
|
"history",
|
|
)
|
|
|
|
const projectTitle = ref(props.project)
|
|
const targetInput = ref(props.target)
|
|
|
|
watch(
|
|
projectTitle,
|
|
() => {
|
|
searchParams.project = projectTitle.value
|
|
},
|
|
{
|
|
immediate: true,
|
|
},
|
|
)
|
|
|
|
watch(
|
|
targetInput,
|
|
() => {
|
|
searchParams.target = targetInput.value
|
|
},
|
|
{
|
|
immediate: true,
|
|
},
|
|
)
|
|
|
|
const target = computed(() => targetInput.value)
|
|
|
|
if (projectTitle.value) {
|
|
useTitle(projectTitle.value)
|
|
}
|
|
|
|
const targetDate = computed(() =>
|
|
targetInput.value
|
|
? new Date(targetInput.value).toLocaleDateString(undefined, {
|
|
dateStyle: "long",
|
|
})
|
|
: null,
|
|
)
|
|
|
|
const {
|
|
timeUntilTarget,
|
|
hasTargetPassed,
|
|
isYearsDisplayed,
|
|
isMonthsDisplayed,
|
|
isDaysDisplayed,
|
|
isHoursDisplayed,
|
|
isMinutesDisplayed,
|
|
isSecondsDisplayed,
|
|
yearsUntil,
|
|
monthsUntil,
|
|
daysUntil,
|
|
hoursUntil,
|
|
minutesUntil,
|
|
secondsUntil,
|
|
} = useTimeUntil(target)
|
|
|
|
const url = computed(() => {
|
|
const newUrl = new URL(document.location.toString())
|
|
if (projectTitle.value) {
|
|
newUrl.searchParams.set("project", projectTitle.value)
|
|
}
|
|
if (targetInput.value) {
|
|
newUrl.searchParams.set("target", targetInput.value)
|
|
}
|
|
|
|
return newUrl.toString()
|
|
})
|
|
|
|
const copyUrl = () => {
|
|
if (navigator.clipboard) {
|
|
navigator.clipboard.writeText(url.value)
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="responsive-time-until">
|
|
<h1 v-if="projectTitle">{{ projectTitle }}</h1>
|
|
<section class="time" v-if="timeUntilTarget">
|
|
<div v-if="isYearsDisplayed" class="count">
|
|
<span class="number">{{ yearsUntil }}</span>
|
|
<span class="moment">years</span>
|
|
</div>
|
|
<div v-if="isMonthsDisplayed" class="count">
|
|
<span class="number">{{ monthsUntil }}</span>
|
|
<span class="moment">months</span>
|
|
</div>
|
|
<div v-if="isDaysDisplayed" class="count">
|
|
<span class="number">{{ daysUntil }}</span>
|
|
<span class="moment">days</span>
|
|
</div>
|
|
<div v-if="isHoursDisplayed" class="count">
|
|
<span class="number">{{ hoursUntil }}</span>
|
|
<span class="moment">hours</span>
|
|
</div>
|
|
<div v-if="isMinutesDisplayed" class="count">
|
|
<span class="number">{{ minutesUntil }}</span>
|
|
<span class="moment">minutes</span>
|
|
</div>
|
|
<div v-if="isSecondsDisplayed" class="count">
|
|
<span class="number">{{ secondsUntil }}</span>
|
|
<span class="moment">seconds</span>
|
|
</div>
|
|
</section>
|
|
<section v-else class="no-target">
|
|
No target set. Expand window to set a target.
|
|
</section>
|
|
<section v-if="targetDate" class="target-date">
|
|
<div v-if="hasTargetPassed" class="has-target-passed">🎊</div>
|
|
<hr v-else />
|
|
{{ targetDate }}
|
|
</section>
|
|
<form @submit.prevent>
|
|
<div>
|
|
<label for="title">Title:</label>
|
|
<input
|
|
type="text"
|
|
id="title"
|
|
v-model="projectTitle"
|
|
autocomplete="false"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label for="target">Milestone:</label>
|
|
<input
|
|
type="date"
|
|
id="target"
|
|
v-model="targetInput"
|
|
autocomplete="false"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<a :href="url" target="_blank" rel="noopener noreferrer"
|
|
><button type="button">open url</button></a
|
|
>
|
|
<button @click="copyUrl">copy url</button>
|
|
</div>
|
|
<p class="hint">Shrink the window to hide this config.</p>
|
|
</form>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.responsive-time-until {
|
|
display: flex;
|
|
flex: 1;
|
|
flex-direction: column;
|
|
justify-content: space-around;
|
|
align-items: center;
|
|
height: 100vh;
|
|
width: 100vw;
|
|
}
|
|
|
|
.time {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.count {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.number {
|
|
font-size: 36px;
|
|
}
|
|
|
|
.target-date {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.no-target {
|
|
padding: 1rem;
|
|
}
|
|
|
|
.has-target-passed {
|
|
font-size: 72px;
|
|
}
|
|
|
|
form {
|
|
display: none;
|
|
padding: 1rem;
|
|
justify-content: center;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
max-width: 800px;
|
|
margin: auto;
|
|
padding: 1rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
label {
|
|
margin-right: 0.3rem;
|
|
}
|
|
|
|
input {
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
@media (min-width: 600px) {
|
|
form {
|
|
display: flex;
|
|
}
|
|
}
|
|
|
|
.hint {
|
|
font-size: 0.75rem;
|
|
opacity: 0.5;
|
|
width: 100%;
|
|
text-align: center;
|
|
margin: 0;
|
|
}
|
|
</style>
|