@meeovi/layer-departments 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/categories/adultstore.vue +123 -0
- package/app/components/categories/chart/[id].vue +254 -0
- package/app/components/categories/chart/add-chart.vue +142 -0
- package/app/components/categories/chart/chart.vue +82 -0
- package/app/components/categories/chart/monthlyChart.vue +46 -0
- package/app/components/categories/chart/musicchart.vue +35 -0
- package/app/components/categories/chart/update-chart.vue +278 -0
- package/app/components/categories/chart/weeklyChart.vue +46 -0
- package/app/components/categories/chart/yearlyChart.vue +46 -0
- package/app/components/categories/charts.vue +118 -0
- package/app/components/categories/deals.vue +101 -0
- package/app/components/categories/eats.vue +49 -0
- package/app/components/categories/restaurants.vue +77 -0
- package/app/components/categories/station/[id].vue +72 -0
- package/app/components/categories/stations.vue +124 -0
- package/app/components/categories/time/features/alarms.vue +276 -0
- package/app/components/categories/time/features/bedtime.vue +12 -0
- package/app/components/categories/time/features/stopwatch.vue +109 -0
- package/app/components/categories/time/features/timer.vue +191 -0
- package/app/components/categories/time/time.vue +63 -0
- package/app/components/categories/travel.vue +75 -0
- package/app/components/categories/weather/weather.vue +44 -0
- package/app/components/related/relatedcharts.vue +39 -0
- package/app/components/related/short.vue +207 -0
- package/app/components/related/space.vue +79 -0
- package/app/pages/departments/[...slug].vue +385 -0
- package/app/pages/departments/category/[...slug].vue +135 -0
- package/dist/nuxt.config.d.ts +2 -0
- package/dist/nuxt.config.js +7 -0
- package/nuxt.config.ts +11 -0
- package/package.json +35 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<v-row>
|
|
4
|
+
<v-col>
|
|
5
|
+
<v-card :disabled="loading" :loading="loading" class="mx-auto my-12" max-width="374">
|
|
6
|
+
<template v-slot:loader="{ isActive }">
|
|
7
|
+
<v-progress-linear :active="isActive" color="deep-purple" height="4"
|
|
8
|
+
indeterminate></v-progress-linear>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<v-img height="250" src="https://cdn.vuetifyjs.com/images/cards/cooking.png" cover></v-img>
|
|
12
|
+
|
|
13
|
+
<v-card-item>
|
|
14
|
+
<v-card-title>Cafe Badilico</v-card-title>
|
|
15
|
+
|
|
16
|
+
<v-card-subtitle>
|
|
17
|
+
<span class="me-1">Local Favorite</span>
|
|
18
|
+
|
|
19
|
+
<v-icon color="error" icon="mdi-fire-circle" size="small"></v-icon>
|
|
20
|
+
</v-card-subtitle>
|
|
21
|
+
</v-card-item>
|
|
22
|
+
|
|
23
|
+
<v-card-text>
|
|
24
|
+
<v-row align="center" class="mx-0">
|
|
25
|
+
<v-rating :model-value="4.5" color="amber" density="compact" size="small" half-increments
|
|
26
|
+
readonly></v-rating>
|
|
27
|
+
|
|
28
|
+
<div class="text-grey ms-4">
|
|
29
|
+
4.5 (413)
|
|
30
|
+
</div>
|
|
31
|
+
</v-row>
|
|
32
|
+
|
|
33
|
+
<div class="my-4 text-subtitle-1">
|
|
34
|
+
$ • Italian, Cafe
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div>Small plates, salads & sandwiches - an intimate setting with 12 indoor seats plus patio
|
|
38
|
+
seating.</div>
|
|
39
|
+
</v-card-text>
|
|
40
|
+
|
|
41
|
+
<v-divider class="mx-4 mb-1"></v-divider>
|
|
42
|
+
|
|
43
|
+
<v-card-title>Tonight's availability</v-card-title>
|
|
44
|
+
|
|
45
|
+
<div class="px-4 mb-2">
|
|
46
|
+
<v-chip-group v-model="selection" selected-class="bg-deep-purple-lighten-2">
|
|
47
|
+
<v-chip>5:30PM</v-chip>
|
|
48
|
+
|
|
49
|
+
<v-chip>7:30PM</v-chip>
|
|
50
|
+
|
|
51
|
+
<v-chip>8:00PM</v-chip>
|
|
52
|
+
|
|
53
|
+
<v-chip>9:00PM</v-chip>
|
|
54
|
+
</v-chip-group>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<v-card-actions>
|
|
58
|
+
<v-btn color="deep-purple-lighten-2" text="Reserve" block border @click="reserve"></v-btn>
|
|
59
|
+
</v-card-actions>
|
|
60
|
+
</v-card>
|
|
61
|
+
</v-col>
|
|
62
|
+
</v-row>
|
|
63
|
+
</div>
|
|
64
|
+
</template>
|
|
65
|
+
|
|
66
|
+
<script setup>
|
|
67
|
+
import { ref } from 'vue'
|
|
68
|
+
|
|
69
|
+
const loading = ref(false)
|
|
70
|
+
const selection = ref(1)
|
|
71
|
+
|
|
72
|
+
function reserve () {
|
|
73
|
+
loading.value = true
|
|
74
|
+
|
|
75
|
+
setTimeout(() => (loading.value = false), 2000)
|
|
76
|
+
}
|
|
77
|
+
</script>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="contentPage">
|
|
3
|
+
<v-row>
|
|
4
|
+
<v-col cols="12">
|
|
5
|
+
<v-card color="#b02564">
|
|
6
|
+
<div class="d-flex flex-no-wrap justify-space-between">
|
|
7
|
+
<div>
|
|
8
|
+
<v-card-title class="text-h5">
|
|
9
|
+
{{ station?.name }} <share style="display: inline-block; font-size: 15px;" />
|
|
10
|
+
</v-card-title>
|
|
11
|
+
|
|
12
|
+
<v-card-subtitle>Format: {{ station?.format }}</v-card-subtitle>
|
|
13
|
+
|
|
14
|
+
<v-card-subtitle>Categories:
|
|
15
|
+
<div v-for="category in station?.categories" :key="category">
|
|
16
|
+
{{ category?.categories_id?.name }}
|
|
17
|
+
</div>
|
|
18
|
+
</v-card-subtitle>
|
|
19
|
+
|
|
20
|
+
<v-card-subtitle v-dompurify-html="station?.description"></v-card-subtitle>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<v-avatar class="ma-3" rounded="0" size="125">
|
|
24
|
+
<NuxtImg :src="`${$directus.url}/assets/${station?.image?.filename_disk}`" />
|
|
25
|
+
</v-avatar>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<audio :src="`${$directus.url}/assets/${station?.file?.filename_disk}`" controls loop class="radioStation"></audio>
|
|
29
|
+
</v-card>
|
|
30
|
+
</v-col>
|
|
31
|
+
|
|
32
|
+
<v-col cols="12">
|
|
33
|
+
<relatedstations />
|
|
34
|
+
</v-col>
|
|
35
|
+
|
|
36
|
+
<v-col cols="12">
|
|
37
|
+
<comments />
|
|
38
|
+
</v-col>
|
|
39
|
+
</v-row>
|
|
40
|
+
</div>
|
|
41
|
+
</template>
|
|
42
|
+
|
|
43
|
+
<script setup>
|
|
44
|
+
import {
|
|
45
|
+
ref,
|
|
46
|
+
computed
|
|
47
|
+
} from 'vue'
|
|
48
|
+
import relatedstations from '@/components/catalog/product/relatedstations.vue'
|
|
49
|
+
import comments from '~/components/partials/globals/comments.vue'
|
|
50
|
+
import share from '~/components/partials/globals/share.vue'
|
|
51
|
+
|
|
52
|
+
const route = useRoute()
|
|
53
|
+
|
|
54
|
+
const {
|
|
55
|
+
$directus,
|
|
56
|
+
$readItem
|
|
57
|
+
} = useNuxtApp()
|
|
58
|
+
|
|
59
|
+
const {
|
|
60
|
+
data: station
|
|
61
|
+
} = await useAsyncData('station', () => {
|
|
62
|
+
return $directus.request($readItem('radios', route.params.id, {
|
|
63
|
+
fields: ['*', {
|
|
64
|
+
'*': ['*']
|
|
65
|
+
}]
|
|
66
|
+
}))
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
useHead({
|
|
70
|
+
title: computed(() => station?.value?.name || 'Station Page'),
|
|
71
|
+
});
|
|
72
|
+
</script>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<!---->
|
|
4
|
+
<v-card elevation="0">
|
|
5
|
+
<v-toolbar :title="stationbar?.name" color="#b02564">
|
|
6
|
+
<v-dialog min-width="500">
|
|
7
|
+
<template v-slot:activator="{ props: activatorProps }">
|
|
8
|
+
<v-btn v-bind="activatorProps" prepend-icon="fas:fa fa-plus" title="Create a Station"
|
|
9
|
+
variant="flat">Create a Station
|
|
10
|
+
</v-btn>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<template v-slot:default="{ isActive }">
|
|
14
|
+
<createstation />
|
|
15
|
+
</template>
|
|
16
|
+
</v-dialog>
|
|
17
|
+
</v-toolbar>
|
|
18
|
+
|
|
19
|
+
<v-tabs v-model="tab" bg-color="#b02564">
|
|
20
|
+
<div v-for="(menu, index) in stationbar?.menus" :key="index">
|
|
21
|
+
<v-tab :value="menu?.value">{{ menu?.name }}</v-tab>
|
|
22
|
+
</div>
|
|
23
|
+
</v-tabs>
|
|
24
|
+
|
|
25
|
+
<v-card-text>
|
|
26
|
+
<v-tabs-window v-model="tab">
|
|
27
|
+
<v-tabs-window-item value="one">
|
|
28
|
+
<v-row style="padding-top: 15px;">
|
|
29
|
+
<v-col cols="6" v-for="(stations, index) in stations" :key="index">
|
|
30
|
+
<station :radio="stations" />
|
|
31
|
+
</v-col>
|
|
32
|
+
</v-row>
|
|
33
|
+
</v-tabs-window-item>
|
|
34
|
+
|
|
35
|
+
<v-tabs-window-item value="two">
|
|
36
|
+
<v-row style="padding-top: 15px;">
|
|
37
|
+
<v-col cols="6" v-for="(stations, index) in livestations" :key="index">
|
|
38
|
+
<station :radio="stations" />
|
|
39
|
+
</v-col>
|
|
40
|
+
</v-row>
|
|
41
|
+
</v-tabs-window-item>
|
|
42
|
+
|
|
43
|
+
<v-tabs-window-item value="three">
|
|
44
|
+
<v-row style="padding-top: 15px;">
|
|
45
|
+
<v-col cols="6" v-for="(stations, index) in mystations" :key="index">
|
|
46
|
+
<station :radio="stations" />
|
|
47
|
+
</v-col>
|
|
48
|
+
</v-row>
|
|
49
|
+
</v-tabs-window-item>
|
|
50
|
+
</v-tabs-window>
|
|
51
|
+
</v-card-text>
|
|
52
|
+
</v-card>
|
|
53
|
+
|
|
54
|
+
</div>
|
|
55
|
+
</template>
|
|
56
|
+
|
|
57
|
+
<script setup>
|
|
58
|
+
import station from '../../../../commerce/app/components/catalog/product/radiostation.vue'
|
|
59
|
+
import createstation from '../../../../commerce/app/components/catalog/product/add-station.vue'
|
|
60
|
+
import { ref } from 'vue'
|
|
61
|
+
import {
|
|
62
|
+
useUserStore
|
|
63
|
+
} from '../../../../auth/app/stores/user'
|
|
64
|
+
|
|
65
|
+
const userStore = useUserStore()
|
|
66
|
+
|
|
67
|
+
const tab = ref(null);
|
|
68
|
+
const {
|
|
69
|
+
$directus,
|
|
70
|
+
$readItems,
|
|
71
|
+
$readItem
|
|
72
|
+
} = useNuxtApp()
|
|
73
|
+
|
|
74
|
+
const userDisplayName = computed(() => {
|
|
75
|
+
return userStore.user?.name || userStore.user?.username || 'User'
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const {
|
|
79
|
+
data: stations
|
|
80
|
+
} = await useAsyncData('stations', () => {
|
|
81
|
+
return $directus.request($readItems('radios', {
|
|
82
|
+
fields: ['*', { '*': ['*'] }]
|
|
83
|
+
}))
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const {
|
|
87
|
+
data: livestations
|
|
88
|
+
} = await useAsyncData('livestations', () => {
|
|
89
|
+
return $directus.request($readItems('radios', {
|
|
90
|
+
filter: {
|
|
91
|
+
type: {
|
|
92
|
+
_eq: "Live"
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
fields: ['*', { '*': ['*'] }]
|
|
96
|
+
}))
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const {
|
|
100
|
+
data: mystations
|
|
101
|
+
} = await useAsyncData('mystations', () => {
|
|
102
|
+
return $directus.request($readItems('radios', {
|
|
103
|
+
filter: {
|
|
104
|
+
creator: {
|
|
105
|
+
_eq: `${userDisplayName.value}`
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}))
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const {
|
|
112
|
+
data: stationbar
|
|
113
|
+
} = await useAsyncData('stationbar', () => {
|
|
114
|
+
return $directus.request($readItem('navigation', '34'))
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
definePageMeta({
|
|
118
|
+
middleware: ['authenticated'],
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
useHead({
|
|
122
|
+
title: 'Radio Stations',
|
|
123
|
+
})
|
|
124
|
+
</script>
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<v-container>
|
|
4
|
+
<v-row justify="center">
|
|
5
|
+
<v-col cols="12" md="8">
|
|
6
|
+
<v-card class="mb-4">
|
|
7
|
+
<v-card-title class="text-h4 justify-center">
|
|
8
|
+
Alarms
|
|
9
|
+
</v-card-title>
|
|
10
|
+
|
|
11
|
+
<!-- Add New Alarm -->
|
|
12
|
+
<v-card-text>
|
|
13
|
+
<v-row align="center">
|
|
14
|
+
<v-col cols="12" sm="4">
|
|
15
|
+
<v-time-picker v-model="newAlarmTime" format="24hr" class="mt-4"></v-time-picker>
|
|
16
|
+
</v-col>
|
|
17
|
+
<v-col cols="12" sm="8">
|
|
18
|
+
<v-text-field v-model="newAlarmLabel" label="Alarm Label"
|
|
19
|
+
placeholder="Enter alarm description"></v-text-field>
|
|
20
|
+
|
|
21
|
+
<!-- Repeat Days -->
|
|
22
|
+
<v-chip-group v-model="selectedDays" multiple class="mb-4">
|
|
23
|
+
<v-chip v-for="day in weekDays" :key="day.value" :value="day.value" filter>
|
|
24
|
+
{{ day.label }}
|
|
25
|
+
</v-chip>
|
|
26
|
+
</v-chip-group>
|
|
27
|
+
|
|
28
|
+
<v-btn color="primary" @click="addAlarm" :disabled="!newAlarmTime">
|
|
29
|
+
Add Alarm
|
|
30
|
+
</v-btn>
|
|
31
|
+
</v-col>
|
|
32
|
+
</v-row>
|
|
33
|
+
</v-card-text>
|
|
34
|
+
</v-card>
|
|
35
|
+
|
|
36
|
+
<!-- Alarm List -->
|
|
37
|
+
<v-card>
|
|
38
|
+
<v-list>
|
|
39
|
+
<v-list-item v-for="alarm in alarms" :key="alarm.id" class="mb-2">
|
|
40
|
+
<v-list-item-content>
|
|
41
|
+
<v-row align="center">
|
|
42
|
+
<v-col cols="12" sm="4">
|
|
43
|
+
<div class="text-h5">{{ formatTime(alarm.time) }}</div>
|
|
44
|
+
<div class="text-caption">{{ alarm.label }}</div>
|
|
45
|
+
</v-col>
|
|
46
|
+
<v-col cols="12" sm="4">
|
|
47
|
+
<v-chip-group>
|
|
48
|
+
<v-chip v-for="day in alarm.days" :key="day" small label>
|
|
49
|
+
{{ weekDays.find(d => d.value === day).shortLabel }}
|
|
50
|
+
</v-chip>
|
|
51
|
+
</v-chip-group>
|
|
52
|
+
</v-col>
|
|
53
|
+
<v-col cols="12" sm="4" class="text-right">
|
|
54
|
+
<v-switch v-model="alarm.active" @change="toggleAlarm(alarm)"
|
|
55
|
+
color="primary" hide-details inset></v-switch>
|
|
56
|
+
<v-btn icon color="error" @click="deleteAlarm(alarm.id)">
|
|
57
|
+
<v-icon>mdi-delete</v-icon>
|
|
58
|
+
</v-btn>
|
|
59
|
+
</v-col>
|
|
60
|
+
</v-row>
|
|
61
|
+
</v-list-item-content>
|
|
62
|
+
</v-list-item>
|
|
63
|
+
</v-list>
|
|
64
|
+
</v-card>
|
|
65
|
+
</v-col>
|
|
66
|
+
</v-row>
|
|
67
|
+
</v-container>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<script setup>
|
|
72
|
+
import {
|
|
73
|
+
ref,
|
|
74
|
+
onMounted,
|
|
75
|
+
watch
|
|
76
|
+
} from 'vue'
|
|
77
|
+
|
|
78
|
+
useHead({
|
|
79
|
+
title: 'Meeovi Time - Alarms'
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// State
|
|
83
|
+
const alarms = ref([])
|
|
84
|
+
const newAlarmTime = ref('')
|
|
85
|
+
const newAlarmLabel = ref('')
|
|
86
|
+
const selectedDays = ref([])
|
|
87
|
+
|
|
88
|
+
// Constants
|
|
89
|
+
const weekDays = [{
|
|
90
|
+
value: 0,
|
|
91
|
+
label: 'Sunday',
|
|
92
|
+
shortLabel: 'Sun'
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
value: 1,
|
|
96
|
+
label: 'Monday',
|
|
97
|
+
shortLabel: 'Mon'
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
value: 2,
|
|
101
|
+
label: 'Tuesday',
|
|
102
|
+
shortLabel: 'Tue'
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
value: 3,
|
|
106
|
+
label: 'Wednesday',
|
|
107
|
+
shortLabel: 'Wed'
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
value: 4,
|
|
111
|
+
label: 'Thursday',
|
|
112
|
+
shortLabel: 'Thu'
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
value: 5,
|
|
116
|
+
label: 'Friday',
|
|
117
|
+
shortLabel: 'Fri'
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
value: 6,
|
|
121
|
+
label: 'Saturday',
|
|
122
|
+
shortLabel: 'Sat'
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
// Methods
|
|
127
|
+
const loadAlarms = () => {
|
|
128
|
+
const savedAlarms = localStorage.getItem('alarms')
|
|
129
|
+
if (savedAlarms) {
|
|
130
|
+
alarms.value = JSON.parse(savedAlarms)
|
|
131
|
+
setupAlarmChecks()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const saveAlarms = () => {
|
|
136
|
+
localStorage.setItem('alarms', JSON.stringify(alarms.value))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const addAlarm = () => {
|
|
140
|
+
if (!newAlarmTime.value) return
|
|
141
|
+
|
|
142
|
+
const newAlarm = {
|
|
143
|
+
id: Date.now(),
|
|
144
|
+
time: newAlarmTime.value,
|
|
145
|
+
label: newAlarmLabel.value || 'Alarm',
|
|
146
|
+
days: selectedDays.value,
|
|
147
|
+
active: true,
|
|
148
|
+
nextTrigger: calculateNextTrigger(newAlarmTime.value, selectedDays.value)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
alarms.value.push(newAlarm)
|
|
152
|
+
saveAlarms()
|
|
153
|
+
setupAlarmCheck(newAlarm)
|
|
154
|
+
|
|
155
|
+
// Reset form
|
|
156
|
+
newAlarmTime.value = ''
|
|
157
|
+
newAlarmLabel.value = ''
|
|
158
|
+
selectedDays.value = []
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const deleteAlarm = (id) => {
|
|
162
|
+
alarms.value = alarms.value.filter(alarm => alarm.id !== id)
|
|
163
|
+
saveAlarms()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const toggleAlarm = (alarm) => {
|
|
167
|
+
alarm.active = !alarm.active
|
|
168
|
+
if (alarm.active) {
|
|
169
|
+
alarm.nextTrigger = calculateNextTrigger(alarm.time, alarm.days)
|
|
170
|
+
}
|
|
171
|
+
saveAlarms()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const formatTime = (time) => {
|
|
175
|
+
if (!time) return ''
|
|
176
|
+
return time
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const calculateNextTrigger = (time, days) => {
|
|
180
|
+
const [hours, minutes] = time.split(':')
|
|
181
|
+
const now = new Date()
|
|
182
|
+
const today = now.getDay()
|
|
183
|
+
|
|
184
|
+
let nextDate = new Date()
|
|
185
|
+
nextDate.setHours(parseInt(hours), parseInt(minutes), 0, 0)
|
|
186
|
+
|
|
187
|
+
if (days.length === 0) {
|
|
188
|
+
if (nextDate <= now) {
|
|
189
|
+
nextDate.setDate(nextDate.getDate() + 1)
|
|
190
|
+
}
|
|
191
|
+
return nextDate.getTime()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Find the next day that the alarm should trigger
|
|
195
|
+
let daysUntilNext = 7
|
|
196
|
+
for (const day of days) {
|
|
197
|
+
const diff = (day - today + 7) % 7
|
|
198
|
+
if (diff === 0 && nextDate > now) {
|
|
199
|
+
daysUntilNext = 0
|
|
200
|
+
break
|
|
201
|
+
}
|
|
202
|
+
if (diff < daysUntilNext && (diff > 0 || (diff === 0 && nextDate <= now))) {
|
|
203
|
+
daysUntilNext = diff
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
nextDate.setDate(nextDate.getDate() + daysUntilNext)
|
|
208
|
+
return nextDate.getTime()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const setupAlarmChecks = () => {
|
|
212
|
+
alarms.value.forEach(setupAlarmCheck)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const setupAlarmCheck = (alarm) => {
|
|
216
|
+
if (!alarm.active) return
|
|
217
|
+
|
|
218
|
+
const checkAlarm = () => {
|
|
219
|
+
const now = Date.now()
|
|
220
|
+
if (now >= alarm.nextTrigger) {
|
|
221
|
+
triggerAlarm(alarm)
|
|
222
|
+
alarm.nextTrigger = calculateNextTrigger(alarm.time, alarm.days)
|
|
223
|
+
saveAlarms()
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Check every minute
|
|
228
|
+
setInterval(checkAlarm, 60000)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const triggerAlarm = (alarm) => {
|
|
232
|
+
// Play alarm sound
|
|
233
|
+
const audio = new Audio()
|
|
234
|
+
audio.src = '/path/to/alarm-sound.mp3' // Add your alarm sound file
|
|
235
|
+
audio.loop = true
|
|
236
|
+
|
|
237
|
+
// Show notification if supported
|
|
238
|
+
if ('Notification' in window) {
|
|
239
|
+
Notification.requestPermission().then(permission => {
|
|
240
|
+
if (permission === 'granted') {
|
|
241
|
+
const notification = new Notification('Alarm', {
|
|
242
|
+
body: alarm.label,
|
|
243
|
+
icon: '/path/to/alarm-icon.png' // Add your alarm icon
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
// Close notification and stop sound after 1 minute
|
|
247
|
+
setTimeout(() => {
|
|
248
|
+
notification.close()
|
|
249
|
+
audio.pause()
|
|
250
|
+
}, 60000)
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
audio.play().catch(error => console.log('Error playing alarm:', error))
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Watch for changes
|
|
259
|
+
watch(alarms, () => {
|
|
260
|
+
saveAlarms()
|
|
261
|
+
}, {
|
|
262
|
+
deep: true
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
// Initialize
|
|
266
|
+
onMounted(() => {
|
|
267
|
+
loadAlarms()
|
|
268
|
+
})
|
|
269
|
+
</script>
|
|
270
|
+
|
|
271
|
+
<style scoped>
|
|
272
|
+
.v-card {
|
|
273
|
+
max-width: 800px;
|
|
274
|
+
margin: auto;
|
|
275
|
+
}
|
|
276
|
+
</style>
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<v-container>
|
|
4
|
+
<v-row justify="center">
|
|
5
|
+
<v-col cols="12" md="6">
|
|
6
|
+
<v-card class="text-center pa-4">
|
|
7
|
+
<v-card-title class="text-h4 justify-center">
|
|
8
|
+
Stopwatch
|
|
9
|
+
</v-card-title>
|
|
10
|
+
<v-card-text>
|
|
11
|
+
<div class="text-h2 my-4">
|
|
12
|
+
{{ hours | zeroPad }}:{{ minutes | zeroPad }}:{{ seconds | zeroPad }}.{{ milliseconds | zeroPad(3) }}
|
|
13
|
+
</div>
|
|
14
|
+
<v-row justify="center" class="mt-4">
|
|
15
|
+
<v-col cols="auto">
|
|
16
|
+
<v-btn color="primary" @click="startTimer" :disabled="isRunning">
|
|
17
|
+
Start
|
|
18
|
+
</v-btn>
|
|
19
|
+
</v-col>
|
|
20
|
+
<v-col cols="auto">
|
|
21
|
+
<v-btn color="warning" @click="pauseTimer" :disabled="!isRunning">
|
|
22
|
+
Pause
|
|
23
|
+
</v-btn>
|
|
24
|
+
</v-col>
|
|
25
|
+
<v-col cols="auto">
|
|
26
|
+
<v-btn color="error" @click="resetTimer">
|
|
27
|
+
Reset
|
|
28
|
+
</v-btn>
|
|
29
|
+
</v-col>
|
|
30
|
+
</v-row>
|
|
31
|
+
<v-list v-if="laps.length" class="mt-4">
|
|
32
|
+
<v-list-item v-for="(lap, index) in laps" :key="index">
|
|
33
|
+
<v-list-item-content>
|
|
34
|
+
Lap {{ index + 1 }}: {{ formatTime(lap) }}
|
|
35
|
+
</v-list-item-content>
|
|
36
|
+
</v-list-item>
|
|
37
|
+
</v-list>
|
|
38
|
+
</v-card-text>
|
|
39
|
+
</v-card>
|
|
40
|
+
</v-col>
|
|
41
|
+
</v-row>
|
|
42
|
+
</v-container>
|
|
43
|
+
</div>
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<script setup>
|
|
47
|
+
import {
|
|
48
|
+
ref,
|
|
49
|
+
computed
|
|
50
|
+
} from 'vue'
|
|
51
|
+
|
|
52
|
+
useHead({
|
|
53
|
+
title: 'Meeovi Time - Stopwatch'
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const time = ref(0)
|
|
57
|
+
const isRunning = ref(false)
|
|
58
|
+
const timer = ref(null)
|
|
59
|
+
const laps = ref([])
|
|
60
|
+
|
|
61
|
+
const hours = computed(() => Math.floor(time.value / 3600000))
|
|
62
|
+
const minutes = computed(() => Math.floor((time.value % 3600000) / 60000))
|
|
63
|
+
const seconds = computed(() => Math.floor((time.value % 60000) / 1000))
|
|
64
|
+
const milliseconds = computed(() => time.value % 1000)
|
|
65
|
+
|
|
66
|
+
const startTimer = () => {
|
|
67
|
+
if (!isRunning.value) {
|
|
68
|
+
isRunning.value = true
|
|
69
|
+
const startTime = Date.now() - time.value
|
|
70
|
+
timer.value = setInterval(() => {
|
|
71
|
+
time.value = Date.now() - startTime
|
|
72
|
+
}, 10)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const pauseTimer = () => {
|
|
77
|
+
isRunning.value = false
|
|
78
|
+
clearInterval(timer.value)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const resetTimer = () => {
|
|
82
|
+
pauseTimer()
|
|
83
|
+
time.value = 0
|
|
84
|
+
laps.value = []
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const addLap = () => {
|
|
88
|
+
laps.value.push(time.value)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const formatTime = (timeValue) => {
|
|
92
|
+
const h = Math.floor(timeValue / 3600000)
|
|
93
|
+
const m = Math.floor((timeValue % 3600000) / 60000)
|
|
94
|
+
const s = Math.floor((timeValue % 60000) / 1000)
|
|
95
|
+
const ms = timeValue % 1000
|
|
96
|
+
return `${padZero(h)}:${padZero(m)}:${padZero(s)}.${padZero(ms, 3)}`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const padZero = (num, places = 2) => {
|
|
100
|
+
return String(num).padStart(places, '0')
|
|
101
|
+
}
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<style scoped>
|
|
105
|
+
.v-card {
|
|
106
|
+
max-width: 600px;
|
|
107
|
+
margin: auto;
|
|
108
|
+
}
|
|
109
|
+
</style>
|