Compare commits

...

19 Commits

12 changed files with 2093 additions and 36 deletions

View File

@ -19,7 +19,12 @@ steps:
- yarn --frozen-lockfile --network-timeout 100000
- yarn run lint
- yarn run build
- name: test
image: node:13
pull: true
group: build-static
commands:
- yarn test
---
kind: pipeline
name: release-latest

View File

@ -5,7 +5,8 @@
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build --modern",
"lint": "vue-cli-service lint"
"lint": "vue-cli-service lint --ignore-pattern '*.test.*'",
"test": "jest"
},
"dependencies": {
"bulma": "0.9.1",
@ -40,6 +41,7 @@
"babel-eslint": "10.1.0",
"eslint": "7.14.0",
"eslint-plugin-vue": "7.1.0",
"jest": "^26.6.3",
"node-sass": "5.0.0",
"sass-loader": "10.1.0",
"vue-flatpickr-component": "8.1.6",

View File

@ -0,0 +1,186 @@
<template>
<div class="datepicker">
<a @click.stop="show = !show">
<template v-if="date === null">
{{ chooseDateLabel }}
</template>
<template v-else>
{{ formatDateShort(date) }}
</template>
</a>
<transition name="fade">
<div v-if="show" class="datepicker-popup">
<a @click.stop="() => setDate('today')">
<span class="icon">
<icon :icon="['far', 'calendar-alt']"/>
</span>
<span class="text">
<span>
Today
</span>
<span class="weekday">
{{ getWeekdayFromStringInterval('today') }}
</span>
</span>
</a>
<a @click.stop="() => setDate('tomorrow')">
<span class="icon">
<icon :icon="['far', 'sun']"/>
</span>
<span class="text">
<span>
Tomorrow
</span>
<span class="weekday">
{{ getWeekdayFromStringInterval('tomorrow') }}
</span>
</span>
</a>
<a @click.stop="() => setDate('nextMonday')">
<span class="icon">
<icon icon="coffee"/>
</span>
<span class="text">
<span>
Next Monday
</span>
<span class="weekday">
{{ getWeekdayFromStringInterval('nextMonday') }}
</span>
</span>
</a>
<a @click.stop="() => setDate('thisWeekend')">
<span class="icon">
<icon icon="cocktail"/>
</span>
<span class="text">
<span>
This Weekend
</span>
<span class="weekday">
{{ getWeekdayFromStringInterval('thisWeekend') }}
</span>
</span>
</a>
<a @click.stop="() => setDate('laterThisWeek')">
<span class="icon">
<icon icon="chess-knight"/>
</span>
<span class="text">
<span>
Later This Week
</span>
<span class="weekday">
{{ getWeekdayFromStringInterval('laterThisWeek') }}
</span>
</span>
</a>
<a @click.stop="() => setDate('nextWeek')">
<span class="icon">
<icon icon="forward"/>
</span>
<span class="text">
<span>
Next Week
</span>
<span class="weekday">
{{ getWeekdayFromStringInterval('nextWeek') }}
</span>
</span>
</a>
<flat-pickr
:config="flatPickerConfig"
class="input"
v-model="date"
/>
</div>
</transition>
</div>
</template>
<script>
import flatPickr from 'vue-flatpickr-component'
import 'flatpickr/dist/flatpickr.css'
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
import {format} from 'date-fns'
import {calculateNearestHours} from '@/helpers/time/calculateNearestHours'
export default {
name: 'datepicker',
data() {
return {
date: null,
show: false,
flatPickerConfig: {
altFormat: 'j M Y H:i',
altInput: true,
dateFormat: 'Y-m-d H:i',
enableTime: true,
time_24hr: true,
inline: true,
},
}
},
components: {
flatPickr,
},
props: {
value: {
validator: prop => prop instanceof Date || prop === null
},
chooseDateLabel: {
type: String,
default: 'Choose a date'
},
},
mounted() {
this.date = this.value
document.addEventListener('click', this.hideDatePopup)
},
beforeDestroy() {
document.removeEventListener('click', this.hideDatePopup)
},
watch: {
value(newVal) {
this.date = newVal
},
},
methods: {
updateData() {
this.$emit('input', this.date)
this.$emit('change')
},
hideDatePopup() {
this.show = false
},
setDate(date) {
if (this.date === null) {
this.date = new Date()
}
const interval = calculateDayInterval(date)
const newDate = new Date()
newDate.setDate(newDate.getDate() + interval)
newDate.setHours(calculateNearestHours(newDate))
newDate.setMinutes(0)
newDate.setSeconds(0)
this.date = newDate
this.updateData()
},
getDayIntervalFromString(date) {
return calculateDayInterval(date)
},
getWeekdayFromStringInterval(date) {
const interval = calculateDayInterval(date)
const newDate = new Date()
newDate.setDate(newDate.getDate() + interval)
return format(newDate, 'E')
},
},
}
</script>

View File

@ -23,17 +23,20 @@
placeholder="Add a new reminder..."
/>
</div>
<datepicker v-model="n" />
</div>
</template>
<script>
import flatPickr from 'vue-flatpickr-component'
import 'flatpickr/dist/flatpickr.css'
import datepicker from '@/components/input/datepicker'
export default {
name: 'reminders',
data() {
return {
n: null,
reminders: [],
lastReminder: 0,
nowUnix: new Date(),
@ -43,6 +46,7 @@ export default {
altInput: true,
dateFormat: 'Y-m-d H:i',
enableTime: true,
time_24hr: true,
onOpen: this.updateLastReminderDate,
onClose: this.addReminderDate,
},
@ -59,6 +63,7 @@ export default {
},
components: {
flatPickr,
datepicker,
},
mounted() {
this.reminders = this.value

View File

@ -0,0 +1,26 @@
export function calculateDayInterval(date, currentDay = (new Date().getDay())) {
switch (date) {
case 'today':
return 0
case 'tomorrow':
return 1
case 'nextMonday':
// Monday is 1, so we calculate the distance to the next 1
return (currentDay + (8 - currentDay * 2)) % 7
case 'thisWeekend':
// Saturday is 6 so we calculate the distance to the next 6
return (6 - currentDay) % 6
case 'laterThisWeek':
if (currentDay === 5 || currentDay === 6 || currentDay === 0) {
return 0
}
return 2
case 'laterNextWeek':
return calculateDayInterval('laterThisWeek', currentDay) + 7
case 'nextWeek':
return 7
default:
return 0
}
}

View File

@ -0,0 +1,93 @@
import {calculateDayInterval} from './calculateDayInterval'
const days = {
monday: 1,
tuesday: 2,
wednesday: 3,
thursday: 4,
friday: 5,
saturday: 6,
sunday: 0,
}
for (const n in days) {
test(`today on a ${n}`, () => {
expect(calculateDayInterval('today', days[n])).toBe(0)
})
}
for (const n in days) {
test(`tomorrow on a ${n}`, () => {
expect(calculateDayInterval('tomorrow', days[n])).toBe(1)
})
}
const nextMonday = {
monday: 0,
tuesday: 6,
wednesday: 5,
thursday: 4,
friday: 3,
saturday: 2,
sunday: 1,
}
for (const n in nextMonday) {
test(`next monday on a ${n}`, () => {
expect(calculateDayInterval('nextMonday', days[n])).toBe(nextMonday[n])
})
}
const thisWeekend = {
monday: 5,
tuesday: 4,
wednesday: 3,
thursday: 2,
friday: 1,
saturday: 0,
sunday: 0,
}
for (const n in thisWeekend) {
test(`this weekend on a ${n}`, () => {
expect(calculateDayInterval('thisWeekend', days[n])).toBe(thisWeekend[n])
})
}
const laterThisWeek = {
monday: 2,
tuesday: 2,
wednesday: 2,
thursday: 2,
friday: 0,
saturday: 0,
sunday: 0,
}
for (const n in laterThisWeek) {
test(`later this week on a ${n}`, () => {
expect(calculateDayInterval('laterThisWeek', days[n])).toBe(laterThisWeek[n])
})
}
const laterNextWeek = {
monday: 7 + 2,
tuesday: 7 + 2,
wednesday: 7 + 2,
thursday: 7 + 2,
friday: 7 + 0,
saturday: 7 + 0,
sunday: 7 + 0,
}
for (const n in laterNextWeek) {
test(`later next week on a ${n} (this week)`, () => {
expect(calculateDayInterval('laterNextWeek', days[n])).toBe(laterNextWeek[n])
})
}
for (const n in days) {
test(`next week on a ${n}`, () => {
expect(calculateDayInterval('nextWeek', days[n])).toBe(7)
})
}

View File

@ -0,0 +1,21 @@
export function calculateNearestHours(currentDate = new Date()) {
if (currentDate.getHours() <= 9 || currentDate.getHours() > 21) {
return 9
}
if (currentDate.getHours() <= 12) {
return 12
}
if (currentDate.getHours() <= 15) {
return 15
}
if (currentDate.getHours() <= 18) {
return 18
}
if (currentDate.getHours() <= 21) {
return 21
}
}

View File

@ -0,0 +1,90 @@
import {calculateNearestHours} from './calculateNearestHours'
test('5:00', () => {
const date = new Date()
date.setHours(5)
expect(calculateNearestHours(date)).toBe(9)
})
test('7:00', () => {
const date = new Date()
date.setHours(7)
expect(calculateNearestHours(date)).toBe(9)
})
test('7:41', () => {
const date = new Date()
date.setHours(7)
date.setMinutes(41)
expect(calculateNearestHours(date)).toBe(9)
})
test('9:00', () => {
const date = new Date()
date.setHours(9)
date.setMinutes(0)
expect(calculateNearestHours(date)).toBe(9)
})
test('10:00', () => {
const date = new Date()
date.setHours(10)
date.setMinutes(0)
expect(calculateNearestHours(date)).toBe(12)
})
test('12:00', () => {
const date = new Date()
date.setHours(12)
date.setMinutes(0)
expect(calculateNearestHours(date)).toBe(12)
})
test('13:00', () => {
const date = new Date()
date.setHours(13)
date.setMinutes(0)
expect(calculateNearestHours(date)).toBe(15)
})
test('15:00', () => {
const date = new Date()
date.setHours(15)
date.setMinutes(0)
expect(calculateNearestHours(date)).toBe(15)
})
test('16:00', () => {
const date = new Date()
date.setHours(16)
date.setMinutes(0)
expect(calculateNearestHours(date)).toBe(18)
})
test('18:00', () => {
const date = new Date()
date.setHours(18)
date.setMinutes(0)
expect(calculateNearestHours(date)).toBe(18)
})
test('19:00', () => {
const date = new Date()
date.setHours(19)
date.setMinutes(0)
expect(calculateNearestHours(date)).toBe(21)
})
test('22:00', () => {
const date = new Date()
date.setHours(22)
date.setMinutes(0)
expect(calculateNearestHours(date)).toBe(9)
})
test('22:40', () => {
const date = new Date()
date.setHours(22)
date.setMinutes(0)
expect(calculateNearestHours(date)).toBe(9)
})

View File

@ -53,8 +53,12 @@ import {
faTrashAlt,
faUser,
faUsers,
faForward,
faChessKnight,
faCoffee,
faCocktail,
} from '@fortawesome/free-solid-svg-icons'
import {faCalendarAlt, faClock, faComments, faSave, faStar, faTimesCircle} from '@fortawesome/free-regular-svg-icons'
import {faCalendarAlt, faClock, faComments, faSave, faStar, faTimesCircle, faSun} from '@fortawesome/free-regular-svg-icons'
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
// PWA
import './registerServiceWorker'
@ -135,6 +139,11 @@ library.add(faFillDrip)
library.add(faKeyboard)
library.add(faSave)
library.add(faStarSolid)
library.add(faForward)
library.add(faSun)
library.add(faChessKnight)
library.add(faCoffee)
library.add(faCocktail)
Vue.component('icon', FontAwesomeIcon)
@ -146,6 +155,13 @@ Vue.directive('focus', focus)
import tooltip from '@/directives/tooltip'
Vue.directive('tooltip', tooltip)
const formatDate = (date, f) => {
if (typeof date === 'string') {
date = new Date(date)
}
return date ? format(date, f) : ''
}
Vue.mixin({
methods: {
formatDateSince: date => {
@ -170,6 +186,9 @@ Vue.mixin({
}
return date ? format(date, 'PPPPpppp') : ''
},
formatDateShort: date => {
return formatDate(date, 'PPpp')
},
error: (e, context, actions = []) => message.error(e, context, actions),
success: (s, context, actions = []) => message.success(s, context, actions),
colorIsDark: colorIsDark,

View File

@ -22,3 +22,4 @@
@import 'legal';
@import 'keyboard-shortcuts';
@import 'api-config';
@import 'datepicker'

View File

@ -0,0 +1,55 @@
.datepicker {
input.input {
display: none;
}
.datepicker-popup {
position: absolute;
z-index: 99;
width: 320px;
background: $white;
border-radius: $radius;
box-shadow: $card-shadow;
a {
display: flex;
align-items: center;
padding: 0 .5rem;
width: 100%;
height: 2.25rem;
color: $text;
transition: all $transition;
&:first-child {
border-radius: $radius $radius 0 0;
}
&:hover {
background: $light;
}
.text {
width: 100%;
font-size: .85rem;
display: flex;
justify-content: space-between;
padding-right: .25rem;
.weekday {
color: $text-light;
text-transform: capitalize;
}
}
.icon {
width: 2rem;
text-align: center;
}
}
.flatpickr-calendar {
margin: 0 auto;
box-shadow: none;
}
}
}

1620
yarn.lock

File diff suppressed because it is too large Load Diff