@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,191 @@
|
|
|
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
|
+
Timer
|
|
9
|
+
</v-card-title>
|
|
10
|
+
|
|
11
|
+
<v-card-text>
|
|
12
|
+
<!-- Timer Display -->
|
|
13
|
+
<div class="text-h2 my-4">
|
|
14
|
+
{{ displayTime }}
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<!-- Time Input Fields (shown when timer is not running) -->
|
|
18
|
+
<v-row justify="center" v-if="!isRunning && !isPaused">
|
|
19
|
+
<v-col cols="auto">
|
|
20
|
+
<v-text-field v-model="hours" label="Hours" type="number" min="0" max="99"
|
|
21
|
+
style="width: 100px" @input="validateInput('hours')"></v-text-field>
|
|
22
|
+
</v-col>
|
|
23
|
+
<v-col cols="auto">
|
|
24
|
+
<v-text-field v-model="minutes" label="Minutes" type="number" min="0" max="59"
|
|
25
|
+
style="width: 100px" @input="validateInput('minutes')"></v-text-field>
|
|
26
|
+
</v-col>
|
|
27
|
+
<v-col cols="auto">
|
|
28
|
+
<v-text-field v-model="seconds" label="Seconds" type="number" min="0" max="59"
|
|
29
|
+
style="width: 100px" @input="validateInput('seconds')"></v-text-field>
|
|
30
|
+
</v-col>
|
|
31
|
+
</v-row>
|
|
32
|
+
|
|
33
|
+
<!-- Control Buttons -->
|
|
34
|
+
<v-row justify="center" class="mt-4">
|
|
35
|
+
<v-col cols="auto">
|
|
36
|
+
<v-btn color="primary" @click="startTimer" :disabled="isRunning || !isValidInput">
|
|
37
|
+
Start
|
|
38
|
+
</v-btn>
|
|
39
|
+
</v-col>
|
|
40
|
+
<v-col cols="auto">
|
|
41
|
+
<v-btn color="warning" @click="pauseTimer" :disabled="!isRunning">
|
|
42
|
+
Pause
|
|
43
|
+
</v-btn>
|
|
44
|
+
</v-col>
|
|
45
|
+
<v-col cols="auto">
|
|
46
|
+
<v-btn color="error" @click="resetTimer"
|
|
47
|
+
:disabled="!isRunning && !isPaused && totalSeconds === 0">
|
|
48
|
+
Reset
|
|
49
|
+
</v-btn>
|
|
50
|
+
</v-col>
|
|
51
|
+
</v-row>
|
|
52
|
+
</v-card-text>
|
|
53
|
+
</v-card>
|
|
54
|
+
</v-col>
|
|
55
|
+
</v-row>
|
|
56
|
+
</v-container>
|
|
57
|
+
</div>
|
|
58
|
+
</template>
|
|
59
|
+
|
|
60
|
+
<script setup>
|
|
61
|
+
import {
|
|
62
|
+
ref,
|
|
63
|
+
computed,
|
|
64
|
+
onBeforeUnmount
|
|
65
|
+
} from 'vue'
|
|
66
|
+
|
|
67
|
+
useHead({
|
|
68
|
+
title: 'Meeovi Time - Timer'
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// State variables
|
|
72
|
+
const hours = ref('0')
|
|
73
|
+
const minutes = ref('0')
|
|
74
|
+
const seconds = ref('0')
|
|
75
|
+
const remainingTime = ref(0)
|
|
76
|
+
const isRunning = ref(false)
|
|
77
|
+
const isPaused = ref(false)
|
|
78
|
+
const timer = ref(null)
|
|
79
|
+
|
|
80
|
+
// Computed properties
|
|
81
|
+
const isValidInput = computed(() => {
|
|
82
|
+
const h = parseInt(hours.value) || 0
|
|
83
|
+
const m = parseInt(minutes.value) || 0
|
|
84
|
+
const s = parseInt(seconds.value) || 0
|
|
85
|
+
return h > 0 || m > 0 || s > 0
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const totalSeconds = computed(() => {
|
|
89
|
+
return remainingTime.value
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const displayTime = computed(() => {
|
|
93
|
+
const h = Math.floor(totalSeconds.value / 3600)
|
|
94
|
+
const m = Math.floor((totalSeconds.value % 3600) / 60)
|
|
95
|
+
const s = totalSeconds.value % 60
|
|
96
|
+
return `${padZero(h)}:${padZero(m)}:${padZero(s)}`
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// Methods
|
|
100
|
+
const validateInput = (field) => {
|
|
101
|
+
switch (field) {
|
|
102
|
+
case 'hours':
|
|
103
|
+
hours.value = Math.min(Math.max(parseInt(hours.value) || 0, 0), 99).toString()
|
|
104
|
+
break
|
|
105
|
+
case 'minutes':
|
|
106
|
+
minutes.value = Math.min(Math.max(parseInt(minutes.value) || 0, 0), 59).toString()
|
|
107
|
+
break
|
|
108
|
+
case 'seconds':
|
|
109
|
+
seconds.value = Math.min(Math.max(parseInt(seconds.value) || 0, 0), 59).toString()
|
|
110
|
+
break
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const startTimer = () => {
|
|
115
|
+
if (!isRunning.value) {
|
|
116
|
+
if (!isPaused.value) {
|
|
117
|
+
// Calculate total seconds from input
|
|
118
|
+
remainingTime.value = (
|
|
119
|
+
parseInt(hours.value) * 3600 +
|
|
120
|
+
parseInt(minutes.value) * 60 +
|
|
121
|
+
parseInt(seconds.value)
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
isRunning.value = true
|
|
125
|
+
isPaused.value = false
|
|
126
|
+
|
|
127
|
+
timer.value = setInterval(() => {
|
|
128
|
+
if (remainingTime.value > 0) {
|
|
129
|
+
remainingTime.value--
|
|
130
|
+
} else {
|
|
131
|
+
playAlarm()
|
|
132
|
+
resetTimer()
|
|
133
|
+
}
|
|
134
|
+
}, 1000)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const pauseTimer = () => {
|
|
139
|
+
if (isRunning.value) {
|
|
140
|
+
clearInterval(timer.value)
|
|
141
|
+
isRunning.value = false
|
|
142
|
+
isPaused.value = true
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const resetTimer = () => {
|
|
147
|
+
clearInterval(timer.value)
|
|
148
|
+
isRunning.value = false
|
|
149
|
+
isPaused.value = false
|
|
150
|
+
remainingTime.value = 0
|
|
151
|
+
hours.value = '0'
|
|
152
|
+
minutes.value = '0'
|
|
153
|
+
seconds.value = '0'
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const playAlarm = () => {
|
|
157
|
+
// Play sound when timer ends
|
|
158
|
+
const audio = new Audio()
|
|
159
|
+
audio.src = '/path/to/alarm-sound.mp3' // Add your alarm sound file
|
|
160
|
+
audio.play().catch(error => console.log('Error playing alarm:', error))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const padZero = (num) => {
|
|
164
|
+
return String(num).padStart(2, '0')
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Cleanup on component unmount
|
|
168
|
+
onBeforeUnmount(() => {
|
|
169
|
+
if (timer.value) {
|
|
170
|
+
clearInterval(timer.value)
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
</script>
|
|
174
|
+
|
|
175
|
+
<style scoped>
|
|
176
|
+
.v-card {
|
|
177
|
+
max-width: 600px;
|
|
178
|
+
margin: auto;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* Remove spinner buttons from number inputs */
|
|
182
|
+
input::-webkit-outer-spin-button,
|
|
183
|
+
input::-webkit-inner-spin-button {
|
|
184
|
+
-webkit-appearance: none;
|
|
185
|
+
margin: 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
input[type=number] {
|
|
189
|
+
-moz-appearance: textfield;
|
|
190
|
+
}
|
|
191
|
+
</style>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<h1 class="mbr-section-title mbr-fonts-style mbr-white mb-3 display-1" id="dateTime">
|
|
4
|
+
</h1>
|
|
5
|
+
</div>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script setup>
|
|
9
|
+
import {
|
|
10
|
+
onMounted,
|
|
11
|
+
onUnmounted
|
|
12
|
+
} from 'vue';
|
|
13
|
+
const {
|
|
14
|
+
$directus,
|
|
15
|
+
$readItem
|
|
16
|
+
} = useNuxtApp()
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
data: department
|
|
20
|
+
} = await useAsyncData('department', () => {
|
|
21
|
+
return $directus.request($readItem('departments', '68', {
|
|
22
|
+
fields: ['*', '*', '*']
|
|
23
|
+
}))
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
let intervalId;
|
|
27
|
+
|
|
28
|
+
const updateDateTime = () => {
|
|
29
|
+
const months = [
|
|
30
|
+
"January", "February", "March", "April", "May", "June", "July",
|
|
31
|
+
"August", "September", "October", "November", "December"
|
|
32
|
+
];
|
|
33
|
+
const date = new Date();
|
|
34
|
+
const monthName = months[date.getMonth()];
|
|
35
|
+
const formattedDate = date.toLocaleString();
|
|
36
|
+
|
|
37
|
+
const dateTimeElement = document.getElementById("dateTime");
|
|
38
|
+
if (dateTimeElement) {
|
|
39
|
+
dateTimeElement.innerHTML = `${monthName} ${formattedDate}`;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
onMounted(() => {
|
|
44
|
+
updateDateTime(); // Initial update
|
|
45
|
+
intervalId = setInterval(updateDateTime, 1000);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
onUnmounted(() => {
|
|
49
|
+
if (intervalId) {
|
|
50
|
+
clearInterval(intervalId);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const props = defineProps({
|
|
55
|
+
category: {
|
|
56
|
+
type: String,
|
|
57
|
+
required: true,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
const {
|
|
61
|
+
category
|
|
62
|
+
} = props;
|
|
63
|
+
</script>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div id="map" style="height: 400px;"></div>
|
|
4
|
+
<p v-if="locationError">{{ locationError }}</p>
|
|
5
|
+
</div>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script setup>
|
|
9
|
+
import {
|
|
10
|
+
onMounted,
|
|
11
|
+
ref
|
|
12
|
+
} from 'vue';
|
|
13
|
+
|
|
14
|
+
const props = defineProps({
|
|
15
|
+
category: {
|
|
16
|
+
type: String,
|
|
17
|
+
required: true,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const mapInstance = ref(null);
|
|
22
|
+
const locationError = ref(null);
|
|
23
|
+
|
|
24
|
+
onMounted(() => {
|
|
25
|
+
import('leaflet').then((L) => {
|
|
26
|
+
// Create the map with a default view
|
|
27
|
+
mapInstance.value = L.map('map').setView([0, 0], 2);
|
|
28
|
+
|
|
29
|
+
// Add the tile layer
|
|
30
|
+
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
31
|
+
attribution: '© <NuxtLink to="https://www.openstreetmap.org/">OpenStreetMap</NuxtLink> contributors',
|
|
32
|
+
maxZoom: 18,
|
|
33
|
+
}).addTo(mapInstance.value);
|
|
34
|
+
|
|
35
|
+
// Try to get the user's location
|
|
36
|
+
if ("geolocation" in navigator) {
|
|
37
|
+
navigator.geolocation.getCurrentPosition(
|
|
38
|
+
(position) => {
|
|
39
|
+
const {
|
|
40
|
+
latitude,
|
|
41
|
+
longitude
|
|
42
|
+
} = position.coords;
|
|
43
|
+
mapInstance.value.setView([latitude, longitude], 13);
|
|
44
|
+
|
|
45
|
+
// Add a marker at the user's location
|
|
46
|
+
L.marker([latitude, longitude])
|
|
47
|
+
.addTo(mapInstance.value)
|
|
48
|
+
.bindPopup('You are here!')
|
|
49
|
+
.openPopup();
|
|
50
|
+
},
|
|
51
|
+
(error) => {
|
|
52
|
+
console.error("Error getting location:", error.message);
|
|
53
|
+
locationError.value =
|
|
54
|
+
`Unable to get your location: ${error.message}. Using default view.`;
|
|
55
|
+
// Fallback to a default location (e.g., New York City)
|
|
56
|
+
mapInstance.value.setView([40.7128, -74.0060], 13);
|
|
57
|
+
}, {
|
|
58
|
+
enableHighAccuracy: true,
|
|
59
|
+
timeout: 10000, // Increased timeout to 10 seconds
|
|
60
|
+
maximumAge: 0
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
} else {
|
|
64
|
+
locationError.value =
|
|
65
|
+
"Geolocation is not supported by this browser. Using default view.";
|
|
66
|
+
// Fallback to a default location
|
|
67
|
+
mapInstance.value.setView([40.7128, -74.0060], 13);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
<style>
|
|
74
|
+
@import 'leaflet/dist/leaflet.css';
|
|
75
|
+
</style>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div id="ww_5134399f73857" v='1.3' loc='auto' a='{"t":"responsive","lang":"en","sl_lpl":1,"ids":[],"font":"Arial","sl_ics":"one_a","sl_sot":"celsius","cl_bkg":"image","cl_font":"#FFFFFF","cl_cloud":"#FFFFFF","cl_persp":"#81D4FA","cl_sun":"#FFC107","cl_moon":"#FFC107","cl_thund":"#FF5722","cl_odd":"#0000000a"}'>Weather Data Source: <NuxtLink href="https://wetterlang.de" id="ww_5134399f73857_u" target="_blank">Wetter vorhersage 30 tage</NuxtLink></div>
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup>
|
|
8
|
+
import {
|
|
9
|
+
onMounted,
|
|
10
|
+
ref
|
|
11
|
+
} from 'vue'
|
|
12
|
+
|
|
13
|
+
const widgetId = 'ww_5134399f73857'
|
|
14
|
+
|
|
15
|
+
const loadExternalScript = (src) => {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
const script = document.createElement('script')
|
|
18
|
+
script.type = 'text/javascript';
|
|
19
|
+
script.src = src
|
|
20
|
+
script.async = true
|
|
21
|
+
script.onload = resolve
|
|
22
|
+
document.head.appendChild(script)
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
onMounted(async () => {
|
|
27
|
+
try {
|
|
28
|
+
await loadExternalScript(`https://app3.weatherwidget.org/js/?id=${widgetId}`)
|
|
29
|
+
// You may need a delay to allow the widget script to initialize
|
|
30
|
+
setTimeout(() => {
|
|
31
|
+
// Check if additional initialization is needed here
|
|
32
|
+
}, 100)
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error('Failed to load weather widget script:', error)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const props = defineProps({
|
|
39
|
+
category: {
|
|
40
|
+
type: String,
|
|
41
|
+
required: true,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
</script>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<v-sheet class="mx-auto row align-items-stretch items-row">
|
|
4
|
+
<v-toolbar title="Charts within the community" color="transparent">
|
|
5
|
+
<div><NuxtLink to="/departments/categories/charts/">All Charts</NuxtLink></div>
|
|
6
|
+
</v-toolbar>
|
|
7
|
+
<v-slide-group v-model="model" class="pa-4" selected-class="bg-success" show-arrows>
|
|
8
|
+
<v-slide-group-item v-slot="{ isSelected, toggle, selectedClass }" v-for="(result, index) in chart" :key="index">
|
|
9
|
+
<charts style="margin: 10px;" :chart="result" />
|
|
10
|
+
</v-slide-group-item>
|
|
11
|
+
</v-slide-group>
|
|
12
|
+
</v-sheet>
|
|
13
|
+
</div>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup>
|
|
17
|
+
import {
|
|
18
|
+
ref,
|
|
19
|
+
} from 'vue';
|
|
20
|
+
import charts from '../categories/chart/musicchart.vue'
|
|
21
|
+
import createchart from '../categories/chart/add-chart.vue'
|
|
22
|
+
//import {groups} from '~/graphql/cms/queries/groups'
|
|
23
|
+
//import { getGroups } from '~/composables/social/getGroups.js'; // Import the composable function
|
|
24
|
+
|
|
25
|
+
const tab = ref(null);
|
|
26
|
+
const model = ref(null);
|
|
27
|
+
const {
|
|
28
|
+
$directus,
|
|
29
|
+
$readItems
|
|
30
|
+
} = useNuxtApp()
|
|
31
|
+
|
|
32
|
+
const {
|
|
33
|
+
data: chart
|
|
34
|
+
} = await useAsyncData('chart', () => {
|
|
35
|
+
return $directus.request($readItems('musicchart', {
|
|
36
|
+
fields: ['*', { '*': ['*'] }]
|
|
37
|
+
}))
|
|
38
|
+
})
|
|
39
|
+
</script>
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card class="mb-4" elevation="2">
|
|
3
|
+
<v-card-title class="d-flex align-center">
|
|
4
|
+
<v-avatar size="40" class="mr-3">
|
|
5
|
+
<v-img :src="short?.creator_avatar || '/images/display-2.png'" :alt="short?.creator" />
|
|
6
|
+
</v-avatar>
|
|
7
|
+
<div>
|
|
8
|
+
<div class="font-weight-bold">{{ short?.creator || 'Anonymous' }}</div>
|
|
9
|
+
<div class="text-caption text-grey">{{ formatDate(short?.date_created) }}</div>
|
|
10
|
+
</div>
|
|
11
|
+
<v-spacer />
|
|
12
|
+
<v-menu>
|
|
13
|
+
<template v-slot:activator="{ props }">
|
|
14
|
+
<v-btn icon="mdi-dots-vertical" variant="text" v-bind="props" />
|
|
15
|
+
</template>
|
|
16
|
+
<v-list>
|
|
17
|
+
<v-list-item @click="shareVibe">
|
|
18
|
+
<v-list-item-title>Share Vibe</v-list-item-title>
|
|
19
|
+
</v-list-item>
|
|
20
|
+
<v-list-item @click="reportVibe">
|
|
21
|
+
<v-list-item-title>Report</v-list-item-title>
|
|
22
|
+
</v-list-item>
|
|
23
|
+
</v-list>
|
|
24
|
+
</v-menu>
|
|
25
|
+
</v-card-title>
|
|
26
|
+
|
|
27
|
+
<video
|
|
28
|
+
ref="videoRef"
|
|
29
|
+
class="vibe-video"
|
|
30
|
+
:src="`${$directus.url}/assets/${short?.video?.filename_disk}`"
|
|
31
|
+
controls
|
|
32
|
+
preload="metadata"
|
|
33
|
+
@click="togglePlay"
|
|
34
|
+
/>
|
|
35
|
+
|
|
36
|
+
<v-card-text>
|
|
37
|
+
<h4 v-if="short?.name" class="mb-2">{{ short.name }}</h4>
|
|
38
|
+
<p v-if="short?.description">{{ short.description }}</p>
|
|
39
|
+
|
|
40
|
+
<div v-if="hashtags.length" class="mt-2">
|
|
41
|
+
<v-chip
|
|
42
|
+
v-for="tag in hashtags"
|
|
43
|
+
:key="tag"
|
|
44
|
+
size="small"
|
|
45
|
+
color="primary"
|
|
46
|
+
variant="outlined"
|
|
47
|
+
class="mr-1 mb-1"
|
|
48
|
+
@click="$emit('hashtag-click', tag)"
|
|
49
|
+
>
|
|
50
|
+
#{{ tag }}
|
|
51
|
+
</v-chip>
|
|
52
|
+
</div>
|
|
53
|
+
</v-card-text>
|
|
54
|
+
|
|
55
|
+
<v-card-actions>
|
|
56
|
+
<v-btn icon="mdi-heart" variant="text" @click="toggleLike" :color="isLiked ? 'red' : 'grey'" />
|
|
57
|
+
<span class="text-caption">{{ short?.likes_count || 0 }}</span>
|
|
58
|
+
|
|
59
|
+
<v-btn icon="mdi-comment" variant="text" @click="toggleComments" />
|
|
60
|
+
<span class="text-caption">{{ short?.comments_count || 0 }}</span>
|
|
61
|
+
|
|
62
|
+
<v-btn icon="fas:fa:fa share-nodes" variant="text" @click="shareVibe" />
|
|
63
|
+
<span class="text-caption">{{ short?.shares_count || 0 }}</span>
|
|
64
|
+
|
|
65
|
+
<v-spacer />
|
|
66
|
+
<v-btn :to="`/social/vibe/${short?.id}`" variant="text" size="small">View Vibe</v-btn>
|
|
67
|
+
</v-card-actions>
|
|
68
|
+
|
|
69
|
+
<!-- Comments Section -->
|
|
70
|
+
<v-expand-transition>
|
|
71
|
+
<div v-show="showComments">
|
|
72
|
+
<v-divider />
|
|
73
|
+
<v-card-text>
|
|
74
|
+
<v-text-field
|
|
75
|
+
v-model="newComment"
|
|
76
|
+
label="Add a comment..."
|
|
77
|
+
variant="outlined"
|
|
78
|
+
density="compact"
|
|
79
|
+
append-inner-icon="mdi-send"
|
|
80
|
+
@click:append-inner="addComment"
|
|
81
|
+
@keyup.enter="addComment"
|
|
82
|
+
/>
|
|
83
|
+
|
|
84
|
+
<div v-for="comment in vibeComments" :key="comment.id" class="mb-2">
|
|
85
|
+
<div class="d-flex align-start">
|
|
86
|
+
<v-avatar size="32" class="mr-2">
|
|
87
|
+
<v-img :src="comment.user_avatar || '/default-avatar.png'" />
|
|
88
|
+
</v-avatar>
|
|
89
|
+
<div>
|
|
90
|
+
<div class="font-weight-bold text-caption">{{ comment.username }}</div>
|
|
91
|
+
<div class="text-body-2">{{ comment.content }}</div>
|
|
92
|
+
<div class="text-caption text-grey">{{ formatDate(comment.date_created) }}</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</v-card-text>
|
|
97
|
+
</div>
|
|
98
|
+
</v-expand-transition>
|
|
99
|
+
</v-card>
|
|
100
|
+
</template>
|
|
101
|
+
|
|
102
|
+
<script setup>
|
|
103
|
+
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
104
|
+
|
|
105
|
+
const props = defineProps({
|
|
106
|
+
short: {
|
|
107
|
+
type: Object,
|
|
108
|
+
required: true,
|
|
109
|
+
},
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const emit = defineEmits(['hashtag-click', 'comment-click', 'share'])
|
|
113
|
+
|
|
114
|
+
const { short } = props
|
|
115
|
+
const videoRef = ref(null)
|
|
116
|
+
const isLiked = ref(false)
|
|
117
|
+
const showComments = ref(false)
|
|
118
|
+
const newComment = ref('')
|
|
119
|
+
const vibeComments = ref([])
|
|
120
|
+
|
|
121
|
+
const hashtags = computed(() => {
|
|
122
|
+
if (!short?.description) return []
|
|
123
|
+
const matches = short.description.match(/#(\w+)/g)
|
|
124
|
+
return matches ? matches.map(tag => tag.slice(1)) : []
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const formatDate = (dateString) => {
|
|
128
|
+
if (!dateString) return 'Unknown date'
|
|
129
|
+
return new Date(dateString).toLocaleDateString('en-US', {
|
|
130
|
+
year: 'numeric',
|
|
131
|
+
month: 'short',
|
|
132
|
+
day: 'numeric',
|
|
133
|
+
hour: '2-digit',
|
|
134
|
+
minute: '2-digit'
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const togglePlay = () => {
|
|
139
|
+
if (videoRef.value) {
|
|
140
|
+
if (videoRef.value.paused) {
|
|
141
|
+
videoRef.value.play()
|
|
142
|
+
} else {
|
|
143
|
+
videoRef.value.pause()
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const toggleLike = async () => {
|
|
149
|
+
isLiked.value = !isLiked.value
|
|
150
|
+
// TODO: Implement like functionality with Directus
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const toggleComments = () => {
|
|
154
|
+
showComments.value = !showComments.value
|
|
155
|
+
if (showComments.value && vibeComments.value.length === 0) {
|
|
156
|
+
loadComments()
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const addComment = async () => {
|
|
161
|
+
if (!newComment.value.trim()) return
|
|
162
|
+
|
|
163
|
+
// TODO: Implement comment creation with Directus
|
|
164
|
+
const comment = {
|
|
165
|
+
id: Date.now(),
|
|
166
|
+
username: 'Current User',
|
|
167
|
+
content: newComment.value,
|
|
168
|
+
date_created: new Date().toISOString()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
vibeComments.value.unshift(comment)
|
|
172
|
+
newComment.value = ''
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const loadComments = async () => {
|
|
176
|
+
// TODO: Load comments from Directus
|
|
177
|
+
vibeComments.value = [
|
|
178
|
+
{
|
|
179
|
+
id: 1,
|
|
180
|
+
username: 'User1',
|
|
181
|
+
content: 'Great vibe!',
|
|
182
|
+
date_created: new Date().toISOString()
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const shareVibe = () => {
|
|
188
|
+
emit('share', short)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const reportVibe = () => {
|
|
192
|
+
console.log('Reporting vibe:', short.id)
|
|
193
|
+
}
|
|
194
|
+
</script>
|
|
195
|
+
|
|
196
|
+
<style scoped>
|
|
197
|
+
.vibe-video {
|
|
198
|
+
width: 100%;
|
|
199
|
+
max-height: 400px;
|
|
200
|
+
object-fit: cover;
|
|
201
|
+
cursor: pointer;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.vibe-video:hover {
|
|
205
|
+
opacity: 0.9;
|
|
206
|
+
}
|
|
207
|
+
</style>
|