manolis-ui 1.1.1 → 1.1.3

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/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "manolis-ui",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "configKey": "manolisUI",
5
5
  "compatibility": {
6
6
  "nuxt": ">=3.10.0"
package/dist/module.mjs CHANGED
@@ -13,7 +13,7 @@ const __filename = __cjs_url__.fileURLToPath(import.meta.url);
13
13
  const __dirname = __cjs_path__.dirname(__filename);
14
14
  const require = __cjs_mod__.createRequire(import.meta.url);
15
15
  const name = "manolis-ui";
16
- const version = "1.1.1";
16
+ const version = "1.1.3";
17
17
 
18
18
  function installTailwind(moduleOptions, nuxt = useNuxt(), resolve = createResolver(import.meta.url).resolve) {
19
19
  const runtimeDir = resolve("./runtime");
@@ -110,7 +110,6 @@ const module = defineNuxtModule({
110
110
  "data-input",
111
111
  "feedback",
112
112
  "layout",
113
- "misc",
114
113
  "navigation"
115
114
  ];
116
115
  for (const dir of componentDirs) {
@@ -1,39 +1,185 @@
1
1
  <script setup lang="ts">
2
+ import { ref, defineProps, defineAsyncComponent, nextTick } from 'vue';
2
3
  import { Search } from 'lucide-vue-next';
3
- import { ref } from 'vue';
4
4
 
5
+ interface Tab {
6
+ name: string;
7
+ description: string;
8
+ type: "date" | "time" | "datetime";
9
+ range: boolean;
10
+ props?: Object;
11
+ value?: any;
12
+ }
5
13
 
6
14
  interface Props {
7
- categories: Array<string>;
15
+ searchOptions: Array<{ category: string; tabs: Tab[] }>;
16
+ currentCategory: string;
8
17
  }
18
+
9
19
  const props = defineProps<Props>();
10
- const currentCategory = ref(props.categories[0]);
20
+ const emit = defineEmits<{
21
+ (e: 'search'): void;
22
+ (e: 'update:search-data', payload: { tab: string, data: any }): void;
23
+ }>();
24
+ const activeTab = ref<Tab | null>(null);
25
+
26
+ // Dynamic component loader
27
+ const componentMap = {
28
+ datetime: defineAsyncComponent(() => import('./datetimePicker.vue')),
29
+ };
30
+
31
+ // Refs for tabs and popup positioning
32
+ const tabRefs = ref<Record<string, HTMLElement | null>>({});
33
+ const popupStyle = ref({ left: '0px', top: '0px' });
34
+ const searchContainer = ref<HTMLElement | null>(null);
35
+
36
+ // made this function compatible with mobile and with desktop
37
+ function componentValueUpdated(data: any, currenTab: number) {
38
+
39
+ if (activeTab.value?.name) {
40
+ c('update:search-data', {
41
+ tab: activeTab.name,
42
+ data: data
43
+ })
44
+ return;
45
+ }
46
+
47
+ let _currentTab = props.searchOptions.find((option: { category: any; }) => option.category === props.currentCategory).tabs[currenTab]
48
+ _currentTab.value = data
49
+ emit('update:search-data', {
50
+ tab: _currentTab.name,
51
+ data: _currentTab.value
52
+ })
53
+
54
+ }
55
+
56
+
57
+ function searchClicked() {
58
+ emit('search');
59
+ }
60
+
61
+ async function handleOutsideClick(event: MouseEvent) {
62
+ if (searchContainer.value && !searchContainer.value.contains(event.target as Node)) {
63
+ await nextTick();
64
+ activeTab.value = null;
65
+ }
66
+ }
67
+
68
+
69
+ async function openMobileView() {
70
+ if (typeof window !== 'undefined' && window.innerWidth <= 768) {
71
+ const modal = document.getElementById('advancedSearchMobile') as HTMLDialogElement | null;
72
+ if (modal) {
73
+ modal.showModal();
74
+ } else {
75
+ console.warn('Modal element not found.');
76
+ }
77
+ // return; // Early return to avoid running desktop logic
78
+ }
79
+ }
80
+
81
+ async function openTab(tab: Tab) {
82
+ activeTab.value = tab;
83
+ await nextTick();
11
84
 
12
- const emit = defineEmits(['category-changed']);
85
+ const tabElement = tabRefs.value[tab.name];
86
+ if (tabElement) {
87
+ const rect = tabElement.getBoundingClientRect();
88
+ const parentRect = searchContainer.value?.getBoundingClientRect() || { left: 0, top: 0 };
13
89
 
14
- function setCurrentCategory(category: string) {
15
- currentCategory.value = category;
16
- emit('category-changed', category);
90
+ // Calculate the popup position for centering on larger screens
91
+ if (window.innerWidth > 768) { // Desktop or tablet screen
92
+ popupStyle.value = {
93
+ left: `${rect.left + (rect.width / 2) - (parentRect.left)}px`,
94
+ top: `${rect.bottom - parentRect.top}px`,
95
+ transform: `translateX(-50%)`,
96
+ };
97
+ } else { // Mobile screen
98
+ popupStyle.value = {
99
+ left: '50%',
100
+ top: `${rect.bottom - parentRect.top}px`,
101
+ transform: `translateX(-50%)`, // Center the popup horizontally on mobile
102
+ };
103
+ }
104
+ } else {
105
+ console.error('Tab element not found for:', tab.name);
106
+ }
17
107
  }
18
108
 
109
+
110
+
111
+ onMounted(() => {
112
+ document.addEventListener('click', handleOutsideClick);
113
+ });
114
+
115
+ onBeforeUnmount(() => {
116
+ document.removeEventListener('click', handleOutsideClick);
117
+ });
19
118
  </script>
20
119
 
21
120
  <template>
22
- <div class="flex gap-4 place-content-center" :class="categories.length > 1 ? 'flex' : 'hidden'">
23
- <button type="button" v-for="category in categories" :class="[{['font-medium']: category == currentCategory }]" @click="setCurrentCategory(category)">{{ category }}</button>
24
- </div>
25
-
26
- <div class="my-8 rounded border-2 border-opacity-25 group/search flex shadow-md hover:shadow-lg transition-all p-2 place-items-center">
27
- <!-- search fields -->
28
- <div class="flex-auto flex gap-4">
29
- <button type="button" class="group/searchitem text-start relative p-1 hover:bg-base-200 rounded after:h-10 after:bg-base-200 after:absolute after:-right-2 after:w-[1px] after:top-0 last-of-type:after:content-none last-of-type:flex-auto first-of-type:flex-auto">
30
- <p class="text-sm">Wie</p>
31
- <p class="text-xs opacity-35 group-hover/searchitem:text-base-500">Aantal personen</p>
121
+ <div ref="searchContainer" class="w-full">
122
+ <!-- desktop/tablet -->
123
+ <div
124
+ class="relative rounded border-2 border-opacity-25 group/search flex shadow-md transition-all p-2 place-items-center w-full cursor-pointer md:cursor-auto"
125
+ @click="openMobileView">
126
+ <div class="tabs tabs-boxed bg-base-100 flex gap-4 w-full">
127
+ <button v-for="tab in props.searchOptions.find(opt => opt.category === props.currentCategory)?.tabs || []"
128
+ :key="tab.name" @click="openTab(tab)" :class="{ 'tab-active': activeTab?.name === tab.name }"
129
+ class="group/searchitem pointer-events-none md:pointer-events-auto w-auto overflow-x-hidden text-start relative p-1 hover:bg-base-200 rounded after:h-10 after:bg-base-200 after:absolute after:-right-2 after:w-[1px] after:top-0 last-of-type:after:content-none last-of-type:flex-auto first-of-type:flex-auto after:content-none after:md:content-['']"
130
+ :ref="(el) => tabRefs[tab.name] = el">
131
+ <p class="text-sm">{{ tab.name }}</p>
132
+ <p class="text-xs opacity-35 truncate ... overflow-hidden md:block hidden">{{ tab.description }}</p>
32
133
  </button>
33
- <!-- <div class="divider divider-horizontal h-10"></div> -->
34
-
134
+ </div>
135
+
136
+ <div class="tab-content absolute w-fit max-w-full flex mt-4 transition-all" v-if="activeTab" :style="popupStyle">
137
+ <component :is="componentMap[activeTab.type]" v-bind="activeTab.props ? activeTab.props : null"
138
+ @updated="componentValueUpdated" />
139
+ </div>
140
+
141
+ <button title="search" type="submit" class="btn btn-primary btn-square ml-1" @click="searchClicked">
142
+ <Search :size="24" color="white" />
143
+ </button>
144
+ </div>
145
+ <div class="hidden">
146
+ <button title="search" @click="">
147
+ <Search :size="24" color="white" />
148
+ </button>
35
149
  </div>
36
- <!-- search button -->
37
- <button title="search" type="submit" class="btn btn-primary btn-square ml-1"><Search :size="24" color="white" /></button>
38
150
  </div>
151
+
152
+
153
+ <dialog id="advancedSearchMobile" class="modal">
154
+ <div class="modal-box">
155
+ <slot name="additionalForMobile">
156
+ <h3 class="text-lg font-bold">{{ currentCategory }}</h3>
157
+ </slot>
158
+
159
+ <div class="collapse bg-base-200 my-4"
160
+ v-for="(tab, index) in props.searchOptions.find(opt => opt.category === props.currentCategory)?.tabs || []"
161
+ :key="tab.name">
162
+ <input type="radio" name="my-accordion-1" :checked="index === 0" />
163
+ <div class="collapse-title text-xl font-medium flex justify-between pr-4 items-center w-full">{{ tab.name }} <p class="text-sm">{{ tab.description }}</p></div>
164
+ <div class="collapse-content flex place-content-center p-0">
165
+ <br>
166
+
167
+ <component :is="componentMap[tab.type]" v-bind="tab.props || {}"
168
+ @updated="(data: any) => componentValueUpdated(data, index)" />
169
+ </div>
170
+ </div>
171
+
172
+ <div class="modal-action">
173
+ <form method="dialog">
174
+ <!-- if there is a button in form, it will close the modal -->
175
+ <button class="btn btn-primary" @click="searchClicked">Close and Search</button>
176
+ </form>
177
+ </div>
178
+ </div>
179
+ </dialog>
180
+
39
181
  </template>
182
+
183
+ <style scoped>
184
+ .tabs button{cursor:pointer}.tab-content{position:absolute;transform-origin:top center;transition:opacity .3s ease;z-index:10}.tabs-boxed :is(.tab-active,[aria-selected=true]):not(.tab-disabled):not([disabled]),.tabs-boxed :is(input:checked){@apply bg-base-300 text-base-content}@media (max-width:768px){.tab-content{bottom:0;left:50%;top:auto;transform:translateX(-50%);width:100%}}@media (min-width:769px){.tab-content{left:unset;transform:unset;width:auto}}
185
+ </style>
@@ -1,14 +1,14 @@
1
1
  <template>
2
- <div class="relative inline-block" ref="pickerContainer">
2
+ <div class="relative inline-block w-full lg:w-[360px]" ref="pickerContainer">
3
3
  <div class="flex items-center gap-2 cursor-pointer" @click="togglePopup">
4
- <slot>
4
+ <slot v-if="popup">
5
5
  <input
6
6
  type="text"
7
7
  class="input input-bordered w-full cursor-pointer"
8
8
  :placeholder="placeholder"
9
9
  :value="formattedValue"
10
10
  readonly
11
- :id="inputId"
11
+ :id="inputId"
12
12
  />
13
13
  <button class="btn btn-ghost">
14
14
  📅
@@ -16,23 +16,23 @@
16
16
  </slot>
17
17
  </div>
18
18
 
19
- <div v-if="showPopup" class="absolute z-50 bg-base-100 shadow-xl rounded-md p-4 mt-2 w-[360px]" :id="popupId">
19
+ <div v-if="showPopup || !popup" class="z-50 rounded-md px-4 md:p-4 md:mt-2 w-full lg:bg-base-100" :class="[{ absolute: popup }]" :id="popupId">
20
20
  <div v-if="showDate" class="flex items-center justify-between mb-4 place-content-center">
21
- <button class="btn btn-sm btn-primary btn-outline" @click="previousMonth">
21
+ <button class="btn hidden md:block md:btn-sm btn-primary btn-outline" @click="previousMonth">
22
22
  <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
23
23
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
24
24
  </svg>
25
25
  </button>
26
26
 
27
- <select class="select border-none w-fit" v-model="currentMonth" @change="emitDateTimeChanged">
27
+ <select class="select border-none w-fit" v-model="currentMonth" @change="emitDateTimeChanged">
28
28
  <option v-for="(month, index) in months" :key="index" :value="month">{{ month }}</option>
29
29
  </select>
30
30
 
31
- <select class="select w-24 border-none" v-model="currentYear" @change="emitDateTimeChanged">
31
+ <select class="select w-24 border-none" v-model="currentYear" @change="emitDateTimeChanged">
32
32
  <option v-for="year in years" :key="year" :value="year">{{ year }}</option>
33
33
  </select>
34
34
 
35
- <button class="btn btn-sm btn-primary btn-outline" @click="nextMonth">
35
+ <button class="btn hidden md:block md:btn-sm btn-primary btn-outline" @click="nextMonth">
36
36
  <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
37
37
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
38
38
  </svg>
@@ -42,33 +42,27 @@
42
42
  <div v-if="showDate" class="grid grid-cols-7 gap-2">
43
43
  <div v-for="day in daysOfWeek" :key="day" class="text-center">{{ day }}</div>
44
44
  <div v-for="emptyDay in emptyDays" :key="emptyDay" class="text-center"></div>
45
- <div v-for="day in daysInMonth" :key="day" class="text-center cursor-pointer py-1 rounded-full hover:bg-primary/10"
46
- :class="{
47
- 'bg-primary text-primary-content': isDateSelected(day),
48
- 'today': isToday(day),
49
- 'range-start': isRangeStart(day),
50
- 'range-end': isRangeEnd(day),
51
- 'in-range': isInDateRange(day),
52
- }"
53
- @click="selectDate(day)">
45
+ <div v-for="day in daysInMonth" :key="day" class="text-center cursor-pointer py-1 rounded-full hover:bg-primary/10" :class="{
46
+ 'bg-primary text-primary-content': isDateSelected(day),
47
+ 'today': isToday(day),
48
+ 'range-start': isRangeStart(day),
49
+ 'range-end': isRangeEnd(day),
50
+ 'in-range': isInDateRange(day),
51
+ }" @click="selectDate(day)">
54
52
  {{ day.getDate() }}
55
53
  </div>
56
54
  </div>
57
55
 
58
- <div v-if="showTime" class="mt-4">
59
- <h3 class="text-lg font-bold mb-2">Select Time</h3>
56
+ <div v-if="showTime" class="mt-4 lg:w-80">
57
+ <h3 class="text-lg font-bold mb-2" v-if="!props.range">Select Time</h3>
60
58
  <div v-if="!props.range">
61
59
  <input v-if="isMobile" type="time" class="input input-bordered w-full" v-model="selectedTime" @change="emitDateTimeChanged" />
62
60
  <div v-else class="flex gap-4">
63
- <select class="select select-bordered w-full" v-model="selectedHour" @change="emitDateTimeChanged">
64
- <option v-for="hour in hours" :key="hour" :value="hour">{{ hour }}</option>
61
+ <select class="select select-bordered w-full" v-model.number="selectedHour" @change="emitDateTimeChanged">
62
+ <option v-for="hour in hours" :key="hour" :value="hour">{{ hour.toString().padStart(2, '0') }}</option>
65
63
  </select>
66
- <select class="select select-bordered w-full" v-model="selectedMinute" @change="emitDateTimeChanged">
67
- <option v-for="minute in minutes" :key="minute" :value="minute">{{ minute }}</option>
68
- </select>
69
- <select class="select select-bordered w-full" v-model="selectedPeriod" @change="emitDateTimeChanged">
70
- <option value="AM">AM</option>
71
- <option value="PM">PM</option>
64
+ <select class="select select-bordered w-full" v-model.number="selectedMinute" @change="emitDateTimeChanged">
65
+ <option v-for="minute in minutes" :key="minute" :value="minute">{{ minute.toString().padStart(2, '0') }}</option>
72
66
  </select>
73
67
  </div>
74
68
  </div>
@@ -77,15 +71,11 @@
77
71
  <p class="font-bold">Start Time</p>
78
72
  <input v-if="isMobile" type="time" class="input input-bordered w-full" v-model="selectedTime.start" @change="emitDateTimeChanged" />
79
73
  <div v-else class="flex gap-2">
80
- <select class="select select-bordered w-full" v-model="selectedHour.start" @change="emitDateTimeChanged">
81
- <option v-for="hour in hours" :key="hour" :value="hour">{{ hour }}</option>
82
- </select>
83
- <select class="select select-bordered w-full" v-model="selectedMinute.start" @change="emitDateTimeChanged">
84
- <option v-for="minute in minutes" :key="minute" :value="minute">{{ minute }}</option>
74
+ <select class="select select-bordered w-full" v-model.number="selectedHour.start" @change="emitDateTimeChanged">
75
+ <option v-for="hour in hours" :key="hour" :value="hour">{{ hour.toString().padStart(2, '0') }}</option>
85
76
  </select>
86
- <select class="select select-bordered w-full" v-model="selectedPeriod.start" @change="emitDateTimeChanged">
87
- <option value="AM">AM</option>
88
- <option value="PM">PM</option>
77
+ <select class="select select-bordered w-full" v-model.number="selectedMinute.start" @change="emitDateTimeChanged">
78
+ <option v-for="minute in minutes" :key="minute" :value="minute">{{ minute.toString().padStart(2, '0') }}</option>
89
79
  </select>
90
80
  </div>
91
81
  </div>
@@ -93,15 +83,11 @@
93
83
  <p class="font-bold">End Time</p>
94
84
  <input v-if="isMobile" type="time" class="input input-bordered w-full" v-model="selectedTime.end" @change="emitDateTimeChanged" />
95
85
  <div v-else class="flex gap-2">
96
- <select class="select select-bordered w-full" v-model="selectedHour.end" @change="emitDateTimeChanged">
97
- <option v-for="hour in hours" :key="hour" :value="hour">{{ hour }}</option>
86
+ <select class="select select-bordered w-full" v-model.number="selectedHour.end" @change="emitDateTimeChanged">
87
+ <option v-for="hour in hours" :key="hour" :value="hour">{{ hour.toString().padStart(2, '0') }}</option>
98
88
  </select>
99
- <select class="select select-bordered w-full" v-model="selectedMinute.end" @change="emitDateTimeChanged">
100
- <option v-for="minute in minutes" :key="minute" :value="minute">{{ minute }}</option>
101
- </select>
102
- <select class="select select-bordered w-full" v-model="selectedPeriod.end" @change="emitDateTimeChanged">
103
- <option value="AM">AM</option>
104
- <option value="PM">PM</option>
89
+ <select class="select select-bordered w-full" v-model.number="selectedMinute.end" @change="emitDateTimeChanged">
90
+ <option v-for="minute in minutes" :key="minute" :value="minute">{{ minute.toString().padStart(2, '0') }}</option>
105
91
  </select>
106
92
  </div>
107
93
  </div>
@@ -109,8 +95,8 @@
109
95
  </div>
110
96
 
111
97
  <div class="mt-4 flex gap-2">
112
- <button class="btn btn-secondary w-fit" @click="clearSelection">Clear</button>
113
- <button class="btn btn-primary btn-wide" @click="closeAndEmit">Close</button>
98
+ <button class="btn btn-secondary" :class="popup ? 'w-fit' : 'w-full'" @click="clearSelection">Clear</button>
99
+ <button v-if="popup" class="btn btn-primary btn-wide" @click="closeAndEmit">Close</button>
114
100
  </div>
115
101
  </div>
116
102
  </div>
@@ -134,14 +120,33 @@ const props = defineProps({
134
120
  type: String,
135
121
  default: 'Select date and time',
136
122
  },
123
+ popup: {
124
+ type: Boolean,
125
+ default: false,
126
+ },
137
127
  id: {
138
128
  type: String,
139
- default: 'datetimepicker',
129
+ default: 'datetimepicker',
130
+ },
131
+ initialDate: {
132
+ type: Object,
133
+ default: () => null
140
134
  }
141
135
  })
142
136
 
137
+ // Watchers
138
+ watch(() => props.range, (newRange) => {
139
+ if (newRange) {
140
+ selectedDate.value = { start: null, end: null };
141
+ selectedTime.value = { start: null, end: null };
142
+ } else {
143
+ selectedDate.value = null;
144
+ selectedTime.value = null;
145
+ }
146
+ });
147
+
143
148
  // Emits
144
- const emit = defineEmits(['update:datetimeChanged'])
149
+ const emit = defineEmits(['updated'])
145
150
 
146
151
  // State
147
152
  const showPopup = ref(false)
@@ -149,9 +154,8 @@ const selectedDate = ref(props.range ? { start: null, end: null } : null)
149
154
  const selectedTime = ref(props.range ? { start: null, end: null } : null)
150
155
  const currentMonth = ref(new Date().toLocaleString('default', { month: 'long' }))
151
156
  const currentYear = ref(new Date().getFullYear())
152
- const selectedHour = ref(props.range ? { start: new Date().getHours() % 12 || 12, end: new Date().getHours() % 12 || 12 } : new Date().getHours() % 12 || 12)
153
- const selectedMinute = ref(props.range ? { start: new Date().getMinutes().toString().padStart(2, '0'), end: new Date().getMinutes().toString().padStart(2, '0') } : new Date().getMinutes().toString().padStart(2, '0'))
154
- const selectedPeriod = ref(props.range ? { start: new Date().getHours() < 12 ? 'AM' : 'PM', end: new Date().getHours() < 12 ? 'AM' : 'PM' } : new Date().getHours() < 12 ? 'AM' : 'PM')
157
+ const selectedHour = ref(props.range ? { start: 0, end: 0 } : 0);
158
+ const selectedMinute = ref(props.range ? { start: 0, end: 0 } : 0);
155
159
 
156
160
  const pickerContainer = ref(null)
157
161
 
@@ -163,9 +167,7 @@ const popupId = computed(() => `${props.id}-popup`);
163
167
  const showDate = computed(() => props.mode === 'date' || props.mode === 'datetime')
164
168
  const showTime = computed(() => props.mode === 'time' || props.mode === 'datetime')
165
169
  const currentMonthYear = computed(() => new Date(currentYear.value, monthIndex.value))
166
- const monthIndex = computed(() => {
167
- return months.indexOf(currentMonth.value)
168
- })
170
+ const monthIndex = computed(() => months.indexOf(currentMonth.value))
169
171
  const daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
170
172
  const firstDay = computed(() => new Date(currentYear.value, monthIndex.value).getDay())
171
173
  const daysInMonth = computed(() => {
@@ -173,9 +175,9 @@ const daysInMonth = computed(() => {
173
175
  return Array.from({ length: days }, (_, i) => new Date(currentYear.value, monthIndex.value, i + 1));
174
176
  });
175
177
  const emptyDays = computed(() => Array.from({ length: firstDay.value }, (_, i) => i))
176
- const hours = Array.from({ length: 12 }, (_, i) => (i + 1).toString().padStart(2, '0'))
177
- const minutes = Array.from({ length: 60 }, (_, i) => i.toString().padStart(2, '0'))
178
- const isMobile = computed(() => /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent))
178
+ const hours = Array.from({ length: 24 }, (_, i) => i)
179
+ const minutes = Array.from({ length: 60 }, (_, i) => i)
180
+ const isMobile = computed(() => /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(process.client ? navigator.userAgent : 'nope'))
179
181
  const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
180
182
  const years = computed(() => {
181
183
  const currentYear = new Date().getFullYear()
@@ -186,52 +188,45 @@ const formattedDate = computed(() => {
186
188
  if (!showDate.value || !selectedDate.value) return '';
187
189
 
188
190
  if (props.range && selectedDate.value.start && selectedDate.value.end) {
189
- const startDate = selectedDate.value.start.toLocaleDateString('en-GB');
190
- const endDate = selectedDate.value.end.toLocaleDateString('en-GB');
191
+ const startDate = selectedDate.value.start.toLocaleDateString('en-US');
192
+ const endDate = selectedDate.value.end.toLocaleDateString('en-US');
191
193
  return `${startDate} - ${endDate}`;
192
- }
194
+ }
193
195
 
194
196
  if (!props.range && selectedDate.value) {
195
- return selectedDate.value.toLocaleDateString('en-GB');
197
+ return selectedDate.value.toLocaleDateString('en-US');
196
198
  }
197
199
 
198
200
  return '';
199
201
  });
200
202
 
203
+ const ensureDate = (d) => d instanceof Date ? d : new Date(d);
204
+ const isValidDate = (d) => d instanceof Date && !isNaN(d);
205
+
201
206
  const formattedTime = computed(() => {
202
207
  if (!showTime.value) return '';
203
208
 
204
- if (props.range && selectedTime.value.start && selectedTime.value.end) {
205
- const formattedStartTime = isMobile.value
206
- ? selectedTime.value.start
207
- : `${selectedHour.value.start}:${selectedMinute.value.start} ${selectedPeriod.value.start}`;
208
- const formattedEndTime = isMobile.value
209
- ? selectedTime.value.end
210
- : `<span class="math-inline">\{selectedHour\.value\.end\}\:</span>{selectedMinute.value.end} ${selectedPeriod.value.end}`;
211
- return `${formattedStartTime} - ${formattedEndTime}`;
212
- }
213
-
214
- if (!props.range && selectedTime.value) {
209
+ const formatTime = (time) => {
210
+ const hour = selectedHour.value?.[time] ?? selectedHour.value;
211
+ const minute = selectedMinute.value?.[time] ?? selectedMinute.value;
215
212
  return isMobile.value
216
- ? selectedTime.value
217
- : `<span class="math-inline">\{selectedHour\.value\}\:</span>{selectedMinute.value} ${selectedPeriod.value}`;
213
+ ? selectedTime.value?.[time] ?? selectedTime.value
214
+ : `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
215
+ };
216
+
217
+ if (props.range) {
218
+ return `${formatTime('start')} - ${formatTime('end')}`;
218
219
  }
219
220
 
220
- return '';
221
+ return formatTime();
221
222
  });
222
223
 
223
224
  const formattedValue = computed(() => {
224
225
  const date = formattedDate.value;
225
226
  const time = formattedTime.value;
226
-
227
- if (date && time) return `${date} ${time}`;
228
- if (date) return date;
229
- if (time) return time;
230
-
231
- return '';
227
+ return `${date} ${time}`.trim();
232
228
  });
233
229
 
234
-
235
230
  // Methods
236
231
  const togglePopup = () => (showPopup.value = !showPopup.value)
237
232
 
@@ -243,10 +238,10 @@ const closeAndEmit = () => {
243
238
  const closePopup = () => (showPopup.value = false)
244
239
 
245
240
  const clearSelection = () => {
246
- selectedDate.value = props.range ? { start: null, end: null } : null
247
- selectedTime.value = props.range ? { start: null, end: null } : null
248
- emitDateTimeChanged()
249
- }
241
+ selectedDate.value = props.range ? { start: null, end: null } : null;
242
+ selectedTime.value = props.range ? { start: null, end: null } : null;
243
+ emitDateTimeChanged();
244
+ };
250
245
 
251
246
  const selectDate = (date) => {
252
247
  if (props.range) {
@@ -260,42 +255,46 @@ const selectDate = (date) => {
260
255
  } else {
261
256
  selectedDate.value = date
262
257
  }
263
- emitDateTimeChanged()
258
+ emitDateTimeChanged()
264
259
  }
265
260
 
266
261
  const isDateSelected = (date) => {
262
+ date = ensureDate(date);
267
263
  if (props.range) {
268
- return (selectedDate.value.start && date.getTime() === selectedDate.value.start.getTime()) || (selectedDate.value.end && date.getTime() === selectedDate.value.end.getTime())
269
- } else {
270
- return selectedDate.value && date.getTime() === selectedDate.value.getTime()
271
- }
272
- }
264
+ return (selectedDate.value.start && isValidDate(selectedDate.value.start) && date.getTime() === selectedDate.value.start.getTime()) ||
265
+ (selectedDate.value.end && isValidDate(selectedDate.value.end) && date.getTime() === selectedDate.value.end.getTime());
266
+ }
267
+ return isValidDate(selectedDate.value) && date.getTime() === selectedDate.value.getTime();
268
+ };
273
269
 
274
270
  const isToday = (date) => {
275
- const today = new Date()
271
+ date = ensureDate(date);
272
+ const today = new Date();
276
273
  return date.getDate() === today.getDate() &&
277
274
  date.getMonth() === today.getMonth() &&
278
- date.getFullYear() === today.getFullYear()
279
- }
275
+ date.getFullYear() === today.getFullYear();
276
+ };
280
277
 
281
278
  const isRangeStart = (date) => {
282
- return props.range && selectedDate.value.start && date.getTime() === selectedDate.value.start.getTime()
283
- }
279
+ date = ensureDate(date);
280
+ return props.range && isValidDate(selectedDate.value.start) && date.getTime() === selectedDate.value.start.getTime();
281
+ };
284
282
 
285
283
  const isRangeEnd = (date) => {
286
- return props.range && selectedDate.value.end && date.getTime() === selectedDate.value.end.getTime()
287
- }
284
+ date = ensureDate(date);
285
+ return props.range && isValidDate(selectedDate.value.end) && date.getTime() === selectedDate.value.end.getTime();
286
+ };
288
287
 
289
288
  const isInDateRange = (date) => {
290
- if (props.range && selectedDate.value.start && selectedDate.value.end) {
291
- const start = selectedDate.value.start.getTime()
292
- const end = selectedDate.value.end.getTime()
293
- const current = date.getTime()
294
- return current > start && current < end
289
+ date = ensureDate(date);
290
+ if (props.range && isValidDate(selectedDate.value.start) && isValidDate(selectedDate.value.end)) {
291
+ const start = selectedDate.value.start.getTime();
292
+ const end = selectedDate.value.end.getTime();
293
+ const current = date.getTime();
294
+ return current > start && current < end;
295
295
  }
296
- return false
297
- }
298
-
296
+ return false;
297
+ };
299
298
 
300
299
  // Month navigation
301
300
  const previousMonth = () => {
@@ -305,6 +304,7 @@ const previousMonth = () => {
305
304
  currentYear.value--
306
305
  }
307
306
  currentMonth.value = months[newMonth]
307
+ emitDateTimeChanged()
308
308
  }
309
309
 
310
310
  const nextMonth = () => {
@@ -314,6 +314,7 @@ const nextMonth = () => {
314
314
  currentYear.value++
315
315
  }
316
316
  currentMonth.value = months[newMonth]
317
+ emitDateTimeChanged()
317
318
  }
318
319
 
319
320
  // Close on outside click
@@ -324,49 +325,71 @@ const handleClickOutside = (event) => {
324
325
  }
325
326
 
326
327
  const emitDateTimeChanged = () => {
327
- let dateStr = '';
328
- let timeStr = '';
329
-
330
- // Handle date formatting
331
- if (selectedDate.value) {
332
- if (props.range) {
333
- const start = selectedDate.value.start?.toLocaleDateString('en-GB') || '';
334
- const end = selectedDate.value.end?.toLocaleDateString('en-GB') || '';
335
- dateStr = `${start} - ${end}`;
336
- } else {
337
- dateStr = selectedDate.value.toLocaleDateString('en-GB');
328
+ let result = {};
329
+
330
+ const formatUTCDate = (date, hour, minute) => {
331
+ if (!(date instanceof Date && !isNaN(date))) {
332
+ // If no date is provided, use today's date
333
+ date = new Date();
338
334
  }
335
+ const newDate = new Date(date);
336
+ newDate.setUTCHours(hour, minute, 0, 0);
337
+ return newDate.toISOString();
338
+ };
339
+
340
+ if (props.range) {
341
+ result.from = formatUTCDate(
342
+ selectedDate.value.start,
343
+ selectedHour.value.start,
344
+ selectedMinute.value.start
345
+ );
346
+ result.to = formatUTCDate(
347
+ selectedDate.value.end,
348
+ selectedHour.value.end,
349
+ selectedMinute.value.end
350
+ );
351
+ } else {
352
+ result = formatUTCDate(
353
+ selectedDate.value,
354
+ selectedHour.value,
355
+ selectedMinute.value
356
+ );
339
357
  }
340
358
 
341
- // Handle time formatting
342
- if (showTime.value) {
343
- if (props.range) {
344
- const start = selectedTime.value.start || `${selectedHour.value.start}:${selectedMinute.value.start} ${selectedPeriod.value.start}`;
345
- const end = selectedTime.value.end || `${selectedHour.value.end}:${selectedMinute.value.end} ${selectedPeriod.value.end}`;
346
- timeStr = `${start} - ${end}`;
347
- } else {
348
- timeStr = `${selectedHour.value}:${selectedMinute.value} ${selectedPeriod.value}`;
359
+ emit('updated', result);
360
+ };
361
+
362
+ onMounted(() => {
363
+ // Initialize selectedDate from props.initialDate
364
+ if (props.initialDate) {
365
+ if (props.range && props.initialDate.start && props.initialDate.end) {
366
+ selectedDate.value = {
367
+ start: new Date(props.initialDate.start),
368
+ end: new Date(props.initialDate.end)
369
+ };
370
+ } else if (props.initialDate.start){
371
+ selectedDate.value = { start: new Date(props.initialDate.start) };
349
372
  }
350
373
  }
351
374
 
352
- // Combine date and time based on mode
353
- let result = '';
354
- if (props.mode === 'datetime') {
355
- result = props.range ? `${dateStr} ${timeStr}` : `${dateStr} ${timeStr}`;
356
- } else if (props.mode === 'date') {
357
- result = dateStr;
358
- } else if (props.mode === 'time') {
359
- result = timeStr;
375
+ // Initialize selectedHour and selectedMinute if they are null in range mode
376
+ if (props.range) {
377
+ selectedHour.value.start = selectedHour.value.start ?? new Date().getHours();
378
+ selectedHour.value.end = selectedHour.value.end ?? new Date().getHours();
379
+ selectedMinute.value.start = selectedMinute.value.start ?? new Date().getMinutes();
380
+ selectedMinute.value.end = selectedMinute.value.end ?? new Date().getMinutes();
381
+ } else {
382
+ selectedHour.value = selectedHour.value ?? new Date().getHours();
383
+ selectedMinute.value = selectedMinute.value ?? new Date().getMinutes();
360
384
  }
385
+ });
361
386
 
362
- // Emit the formatted string
363
- emit('update:datetimeChanged', result.trim());
364
- };
365
-
366
- onMounted(() => document.addEventListener('click', handleClickOutside))
387
+ if(props.popup) {
388
+ onMounted(() => document.addEventListener('click', handleClickOutside))
389
+ }
367
390
  onUnmounted(() => document.removeEventListener('click', handleClickOutside))
368
391
  </script>
369
392
 
370
393
  <style scoped>
371
394
  .input{cursor:pointer}.in-range{@apply bg-primary/20}.in-range:hover{@apply bg-primary/10}.range-end,.range-start{@apply bg-primary text-primary-content}
372
- </style>
395
+ </style>
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ import { defineProps, defineEmits } from 'vue';
3
+
4
+ interface Category {
5
+ category: string;
6
+ tabs: Tab[];
7
+ }
8
+
9
+ interface Props {
10
+ searchOptions: Array<Category>;
11
+ currentCategory: string;
12
+ }
13
+
14
+ const props = defineProps<Props>();
15
+ const emit = defineEmits<{ (e: 'update:currentCategory', category: string): void }>();
16
+
17
+ function changeCategory(category: string) {
18
+ emit('update:currentCategory', category);
19
+ }
20
+ </script>
21
+
22
+ <template>
23
+ <div class="categories flex flex-row gap-4 place-content-center">
24
+ <div v-for="category in props.searchOptions" :key="category.category">
25
+ <button @click="changeCategory(category.category)" class="truncate" :class="{ 'font-semibold': props.currentCategory === category.category }">
26
+ {{ category.category }}
27
+ </button>
28
+ </div>
29
+ </div>
30
+ </template>
@@ -1,19 +1,26 @@
1
1
  <script lang="ts" setup></script>
2
2
 
3
3
  <template>
4
- <nav class="navbar border-b-4 border-primary px-5 py-5 place-items-start">
5
- <div class="navbar-start flex-1">
4
+ <nav class="navbar border-b-4 border-primary px-5 py-5 place-items-start transition-all">
5
+ <div class="navbar-start hidden md:flex">
6
6
  <slot name="start"></slot>
7
7
  </div>
8
-
9
- <div class="navbar-center flex-auto hidden md:block">
8
+
9
+ <div class="navbar-center hidden md:flex justify-center items-center">
10
10
  <slot name="center"></slot>
11
11
  </div>
12
12
 
13
- <div class="navbar-end flex-1">
13
+ <div class="navbar-end md:flex hidden">
14
14
  <slot name="end"></slot>
15
15
  </div>
16
+ <div class="navbar-bottom md:flex hidden">
17
+ <slot name="bottom"></slot>
18
+ </div>
16
19
  </nav>
17
20
  </template>
18
21
 
19
22
 
23
+ <style scoped>
24
+ .navbar{display:grid;gap:0;grid-template-areas:"center center center" "bottom bottom bottom";grid-template-columns:auto auto auto;grid-template-rows:auto auto;width:100%}@media (min-width:768px){.navbar{grid-row-gap:4rem}}.navbar-start{grid-area:start;height:100%}.navbar-center{grid-area:center;height:100%;width:100%}.navbar-end{grid-area:end;height:100%;width:100%}.navbar-bottom{align-items:center;display:flex;grid-area:center;justify-content:center;width:100%}@media (min-width:768px){.navbar-bottom{grid-area:bottom}.navbar{grid-template-areas:"start center end" "bottom bottom bottom";grid-template-columns:.7fr 1.6fr .7fr}}
25
+ </style>
26
+
@@ -1 +1 @@
1
- export declare const useLocalStorage: <T>(key: string, initialValue: T) => [T | null] extends [import("vue").Ref<any, any>] ? import("@vue/shared").IfAny<import("vue").Ref<any, any> & T, import("vue").Ref<import("vue").Ref<any, any> & T, import("vue").Ref<any, any> & T>, import("vue").Ref<any, any> & T> : import("vue").Ref<import("vue").UnwrapRef<T> | null, T | import("vue").UnwrapRef<T> | null>;
1
+ export declare const useLocalStorage: <T>(key: string, initialValue: T) => any;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "manolis-ui",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "My new Nuxt module",
5
5
  "repository": "manolis-trading/manolis-ui",
6
6
  "license": "MIT",
@@ -44,6 +44,7 @@
44
44
  "@nuxt/test-utils": "^3.14.4",
45
45
  "@nuxtjs/tailwindcss": "^6.12.2",
46
46
  "@types/node": "latest",
47
+ "@types/vue": "^1.0.31",
47
48
  "autoprefixer": "^10.4.20",
48
49
  "changelogen": "^0.5.7",
49
50
  "eslint": "^9.15.0",