@bagelink/vue 0.0.1298 → 0.0.1303

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.
Files changed (36) hide show
  1. package/dist/components/AddressSearch.vue.d.ts +0 -3
  2. package/dist/components/AddressSearch.vue.d.ts.map +1 -1
  3. package/dist/components/DataPreview.vue.d.ts +2 -2
  4. package/dist/components/DataPreview.vue.d.ts.map +1 -1
  5. package/dist/components/DropDown.vue.d.ts +0 -1
  6. package/dist/components/DropDown.vue.d.ts.map +1 -1
  7. package/dist/components/form/inputs/ColorInput.vue.d.ts +21 -0
  8. package/dist/components/form/inputs/ColorInput.vue.d.ts.map +1 -0
  9. package/dist/components/form/inputs/SelectInput.vue.d.ts +0 -6
  10. package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
  11. package/dist/components/form/inputs/SignaturePad.vue.d.ts +3 -0
  12. package/dist/components/form/inputs/SignaturePad.vue.d.ts.map +1 -1
  13. package/dist/components/form/inputs/TelInput.vue.d.ts +1 -618
  14. package/dist/components/form/inputs/TelInput.vue.d.ts.map +1 -1
  15. package/dist/components/form/inputs/index.d.ts +1 -1
  16. package/dist/components/form/inputs/index.d.ts.map +1 -1
  17. package/dist/composables/useSchemaField.d.ts.map +1 -1
  18. package/dist/index.cjs +347 -224
  19. package/dist/index.mjs +348 -225
  20. package/dist/style.css +32 -32
  21. package/dist/utils/BagelFormUtils.d.ts +5 -0
  22. package/dist/utils/BagelFormUtils.d.ts.map +1 -1
  23. package/dist/utils/timeAgo.d.ts +1 -0
  24. package/dist/utils/timeAgo.d.ts.map +1 -1
  25. package/package.json +1 -1
  26. package/src/components/DataPreview.vue +2 -2
  27. package/src/components/Dropdown.vue +20 -3
  28. package/src/components/Spreadsheet/Index.vue +2 -2
  29. package/src/components/form/FieldArray.vue +1 -1
  30. package/src/components/form/inputs/{ColorPicker.vue → ColorInput.vue} +1 -1
  31. package/src/components/form/inputs/SignaturePad.vue +52 -1
  32. package/src/components/form/inputs/TelInput.vue +198 -172
  33. package/src/components/form/inputs/index.ts +1 -1
  34. package/src/composables/useSchemaField.ts +6 -1
  35. package/src/utils/BagelFormUtils.ts +17 -3
  36. package/src/utils/timeAgo.ts +36 -0
@@ -1,9 +1,8 @@
1
1
  <script lang="ts" setup>
2
2
  import type { Country } from '@bagelink/vue'
3
3
  import type { CountryCode, NumberFormat } from 'libphonenumber-js'
4
- import type { Raw, StyleValue } from 'vue'
4
+ import type { Raw, StyleValue, Ref } from 'vue'
5
5
  import {
6
-
7
6
  Dropdown,
8
7
  Flag,
9
8
  Icon,
@@ -12,11 +11,8 @@ import {
12
11
  useDebounceFn
13
12
  } from '@bagelink/vue'
14
13
  import axios from 'axios'
15
- import {
16
-
17
- parsePhoneNumberFromString
18
- } from 'libphonenumber-js'
19
- import { onMounted, watch } from 'vue'
14
+ import { parsePhoneNumberFromString } from 'libphonenumber-js'
15
+ import { onMounted, watch, ref, computed } from 'vue'
20
16
 
21
17
  export interface Props {
22
18
  label?: string
@@ -40,7 +36,6 @@ export interface Props {
40
36
  onlyCountries?: string[]
41
37
  preferredCountries?: string[]
42
38
  parseArg?: { extract?: boolean }
43
- debounceDelay?: number
44
39
  }
45
40
 
46
41
  const props = withDefaults(defineProps<Props>(), {
@@ -60,7 +55,6 @@ const props = withDefaults(defineProps<Props>(), {
60
55
  onlyCountries: () => [],
61
56
  preferredCountries: () => [],
62
57
  showDropdown: true,
63
- debounceDelay: 300,
64
58
  })
65
59
 
66
60
  const emit = defineEmits([
@@ -75,13 +69,8 @@ const emit = defineEmits([
75
69
  'debounce',
76
70
  ])
77
71
 
78
- async function getIp() {
79
- const apiData = sessionStorage.getItem('ipapi')
80
- if (apiData) return JSON.parse(apiData)
81
- const { data } = await axios.get('https://ipapi.co/json/')
82
- sessionStorage.setItem('ipapi', JSON.stringify(data))
83
- return data
84
- }
72
+ const open = ref(false)
73
+ const dropdownOpenDirection = ref('below')
85
74
 
86
75
  const defaultDropdownOptions = {
87
76
  hide: false,
@@ -117,192 +106,225 @@ const computedInputOptions = $computed(() => ({
117
106
  ...props.inputOptions,
118
107
  }))
119
108
 
120
- let activeCountryCode = $ref<CountryCode>()
121
- let open = $ref(false)
122
- let selectedIndex = $ref<number>()
123
- const dropdownOpenDirection = $ref('below')
124
- const searchQuery = $ref('')
125
-
126
- // const refInput = $ref<HTMLInputElement | undefined>()
109
+ // Composables
110
+ function useCountrySelection(props: Props, emit: any) {
111
+ const activeCountryCode = ref<CountryCode>()
112
+ const selectedIndex = ref<number>()
113
+ const searchQuery = ref('')
127
114
 
128
- // Computed: activeCountry
129
- const activeCountry = $computed(() => props.allCountries.find(
130
- country => country.iso2 === activeCountryCode?.toUpperCase(),
131
- ),
132
- )
115
+ const activeCountry = computed(() => props.allCountries?.find(
116
+ country => country.iso2 === activeCountryCode.value?.toUpperCase(),
117
+ ))
133
118
 
134
- const isPreferred = (country?: Country) => props.preferredCountries.includes(country?.iso2 as CountryCode)
119
+ const filteredCountries = computed(() => {
120
+ const countries = props.allCountries || []
121
+ const onlyCountries = props.onlyCountries || []
122
+ const excludeCountries = props.excludeCountries || []
135
123
 
136
- const filteredCountries = $computed(() => {
137
- if (props.onlyCountries.length > 0) {
138
- return props.allCountries.filter(({ iso2 }) => props.onlyCountries.some(c => c.toUpperCase() === iso2),
139
- )
140
- }
141
- if (props.excludeCountries.length > 0) {
142
- return props.allCountries.filter(
143
- ({ iso2 }) => !props.excludeCountries.includes(iso2.toUpperCase())
144
- && !props.excludeCountries.includes(iso2.toLowerCase()),
145
- )
146
- }
147
- return props.allCountries
148
- })
124
+ if (onlyCountries.length > 0) {
125
+ return countries.filter(({ iso2 }) => onlyCountries.some(c => c.toUpperCase() === iso2))
126
+ }
127
+ if (excludeCountries.length > 0) {
128
+ return countries.filter(
129
+ ({ iso2 }) => !excludeCountries.includes(iso2.toUpperCase())
130
+ && !excludeCountries.includes(iso2.toLowerCase()),
131
+ )
132
+ }
133
+ return countries
134
+ })
149
135
 
150
- const sortedCountries = $computed(() => {
151
- const preferredCountries = getCountries(props.preferredCountries)
136
+ const sortedCountries = computed(() => {
137
+ const preferredCountries = getCountries(props.preferredCountries || [])
138
+ const countriesList = [...preferredCountries, ...filteredCountries.value]
139
+ const cleanInput = searchQuery.value.replaceAll(
140
+ /[~`!#$%&*()+={};:'"<>.,\\/@-]/g,
141
+ '',
142
+ ).toLowerCase()
143
+ return countriesList
144
+ .filter(
145
+ c => new RegExp(cleanInput, 'i').test(c.name || '')
146
+ || new RegExp(cleanInput, 'i').test(c.iso2 || '')
147
+ || new RegExp(cleanInput, 'i').test(c.dialCode || ''),
148
+ )
149
+ .filter(Boolean)
150
+ })
152
151
 
153
- const countriesList = [...preferredCountries, ...filteredCountries]
154
- const cleanInput = searchQuery.replaceAll(
155
- // eslint-disable-next-line regexp/no-obscure-range
156
- /[~`!#$%&*()+={};:'"<>.,/?-_]/g,
157
- '',
152
+ const findCountry = (iso: string) => filteredCountries.value.find(
153
+ country => country.iso2 === iso.toUpperCase()
158
154
  )
159
- return countriesList
160
- .filter(
161
- c => new RegExp(cleanInput, 'i').test(c.name || '')
162
- || new RegExp(cleanInput, 'i').test(c.iso2 || '')
163
- || new RegExp(cleanInput, 'i').test(c.dialCode || ''),
164
- )
165
- .filter(Boolean)
166
- })
167
-
168
- const parseArgs = $computed(() => ({
169
- ...props.parseArg,
170
- defaultCountry: activeCountryCode,
171
- }))
172
-
173
- const debouncedEmit = useDebounceFn((maybeFormatted: string) => {
174
- emit('debounce', maybeFormatted)
175
- }, props.debounceDelay)
176
-
177
- const phone = defineModel<string>('modelValue', {
178
- default: '',
179
- set: (value) => {
180
- let maybeFormatted = value
181
- if (value.length > 5) {
182
- maybeFormatted = formatPhone(`${value}`)
183
-
184
- if (!maybeFormatted) { emit('update:modelValue', ''); return '' }
185
- }
186
-
187
- emit('update:modelValue', maybeFormatted)
188
- debouncedEmit(maybeFormatted)
189
- return maybeFormatted
190
- },
191
- get: value => value,
192
- })
193
155
 
194
- function formatPhone(val: string): string {
195
- const phoneNumber = parsePhoneNumberFromString(val, parseArgs)
156
+ const findCountryByDialCode = (dialCode: number) => filteredCountries.value.find((country: Country) => Number(country.dialCode) === dialCode)
196
157
 
197
- if (!phoneNumber) {
198
- const dialCode
199
- = sortedCountries.find(c => c.iso2 === activeCountryCode)?.dialCode || ''
200
- if (props.mode === 'INTERNATIONAL') return `+${dialCode}`
201
- return dialCode
158
+ function getCountries(list: string[]): Country[] {
159
+ const countryList: Country[] = []
160
+ list.forEach((countryCode) => {
161
+ const country = findCountry(countryCode)
162
+ if (country) countryList.push(country)
163
+ })
164
+ return countryList
202
165
  }
203
- return phoneNumber.format(props.mode).replaceAll(' ', '') || ''
204
- }
205
166
 
206
- // Watchers
207
- watch(
208
- () => activeCountry,
209
- (value, oldValue) => {
210
- if (!value && oldValue?.iso2) {
211
- activeCountryCode = oldValue.iso2
212
- return
213
- }
214
- // eslint-disable-next-line vue/custom-event-name-casing
215
- if (value?.iso2) emit('country-changed', value)
216
- },
217
- )
218
-
219
- async function initializeCountry() {
220
- // if (phone?.[0] === '+') return;
221
- // 2. Use default country if passed from parent
222
- if (props.defaultCountry) {
223
- if (typeof props.defaultCountry === 'string') {
224
- chooseCountry(props.defaultCountry)
225
- return
226
- }
227
- if (typeof props.defaultCountry === 'number') {
228
- const country = findCountryByDialCode(props.defaultCountry)
229
- if (country) {
230
- chooseCountry(country.iso2)
167
+ async function initializeCountry() {
168
+ if (props.defaultCountry) {
169
+ if (typeof props.defaultCountry === 'string') {
170
+ chooseCountry(props.defaultCountry)
231
171
  return
232
172
  }
173
+ if (typeof props.defaultCountry === 'number') {
174
+ const country = findCountryByDialCode(props.defaultCountry)
175
+ if (country) {
176
+ chooseCountry(country.iso2)
177
+ return
178
+ }
179
+ }
233
180
  }
234
- }
235
181
 
236
- const fallbackCountry = sortedCountries[0]
182
+ const fallbackCountry = sortedCountries.value[0]
237
183
 
238
- if (props.autoDefaultCountry) {
239
- try {
240
- const res = (await getIp()).country
241
- chooseCountry(res || activeCountryCode)
242
- } catch (error) {
243
- console.warn(error)
184
+ if (props.autoDefaultCountry) {
185
+ try {
186
+ const res = (await getIp()).country
187
+ chooseCountry(res || activeCountryCode.value)
188
+ } catch (error) {
189
+ console.warn(error)
190
+ chooseCountry(fallbackCountry.iso2)
191
+ }
192
+ } else {
244
193
  chooseCountry(fallbackCountry.iso2)
245
194
  }
246
- } else {
247
- // 4. Use the first country from preferred list (if available) or all countries list
248
- chooseCountry(fallbackCountry.iso2)
249
195
  }
250
- }
251
196
 
252
- onMounted(initializeCountry)
197
+ function chooseCountry(country?: string) {
198
+ if (!country) return
199
+ const parsedCountry = findCountry(country)
200
+ if (!parsedCountry) return
201
+ activeCountryCode.value = parsedCountry.iso2
202
+ emit('country-changed', parsedCountry)
203
+ open.value = false
204
+ }
253
205
 
254
- const findCountry = (iso: string) => filteredCountries.find(country => country.iso2 === iso.toUpperCase())
206
+ watch(
207
+ () => activeCountry.value,
208
+ (value, oldValue) => {
209
+ if (!value && oldValue?.iso2) {
210
+ activeCountryCode.value = oldValue.iso2
211
+ return
212
+ }
213
+ if (value?.iso2) emit('country-changed', value)
214
+ },
215
+ )
255
216
 
256
- function getCountries(list: string[]): Country[] {
257
- const countryList: Country[] = []
258
- list.forEach((countryCode) => {
259
- const country = findCountry(countryCode)
260
- if (country) countryList.push(country)
261
- })
262
- return countryList
217
+ return {
218
+ activeCountryCode,
219
+ activeCountry,
220
+ selectedIndex,
221
+ searchQuery,
222
+ sortedCountries,
223
+ chooseCountry,
224
+ initializeCountry,
225
+ findCountry,
226
+ findCountryByDialCode
227
+ }
263
228
  }
264
229
 
265
- function findCountryByDialCode(dialCode: number) {
266
- return filteredCountries.find(country => Number(country.dialCode) === dialCode)
267
- }
230
+ function usePhoneNumberFormatting(props: Props, activeCountryCode: Ref<CountryCode | undefined>) {
231
+ const parseArgs = computed(() => ({
232
+ ...props.parseArg,
233
+ defaultCountry: activeCountryCode.value,
234
+ }))
235
+
236
+ function formatPhone(val: string): string {
237
+ // First, try to parse the number as is
238
+ let phoneNumber = parsePhoneNumberFromString(val, parseArgs.value)
239
+
240
+ // If parsing failed and there's a + at the start, try removing any existing country code
241
+ if (!phoneNumber && val.startsWith('+')) {
242
+ const currentCountry = props.allCountries?.find(c => c.iso2 === activeCountryCode.value)
243
+ if (currentCountry) {
244
+ // Remove the current country code if it exists
245
+ const { dialCode } = currentCountry
246
+ const withoutDialCode = val.replace(new RegExp(`^\\+${dialCode}`), '')
247
+ // Try parsing again with the cleaned number
248
+ phoneNumber = parsePhoneNumberFromString(withoutDialCode, parseArgs.value)
249
+ }
250
+ }
251
+
252
+ if (!phoneNumber) {
253
+ const dialCode = props.allCountries?.find(
254
+ c => c.iso2 === activeCountryCode.value
255
+ )?.dialCode || ''
268
256
 
269
- const phoneDropdown = $ref<typeof Dropdown>()
257
+ if (props.mode === 'INTERNATIONAL') return `+${dialCode}`
258
+ return dialCode
259
+ }
260
+
261
+ // Format the number according to the selected mode
262
+ const formattedNumber = phoneNumber.format(props.mode || 'INTERNATIONAL')
270
263
 
271
- function chooseCountry(country?: string) {
272
- if (!country) return
273
- const parsedCountry = findCountry(country)
274
- if (!parsedCountry) return
275
- activeCountryCode = parsedCountry.iso2
264
+ // For international format, ensure proper formatting with country code
265
+ if (props.mode === 'INTERNATIONAL') {
266
+ const countryCode = phoneNumber.countryCallingCode
267
+ const { nationalNumber } = phoneNumber
268
+ return `+${countryCode}${nationalNumber}`
269
+ }
276
270
 
277
- if (props.inputOptions?.showDialCode && parsedCountry) {
278
- phone.value = `+ ${parsedCountry.dialCode}`
279
- activeCountryCode = parsedCountry.iso2 || ''
280
- return
271
+ return formattedNumber.replaceAll(' ', '') || ''
281
272
  }
282
273
 
283
- activeCountryCode = parsedCountry.iso2 || ''
284
- // emitInput(phone);
285
- phoneDropdown?.hide()
274
+ return {
275
+ formatPhone,
276
+ parseArgs
277
+ }
286
278
  }
287
279
 
288
- function onBlur() { emit('blur') }
289
-
290
- function onFocus() { emit('focus') }
280
+ const debouncedEmit = useDebounceFn((maybeFormatted: string) => { emit('debounce', maybeFormatted) })
291
281
 
292
- function onEnter(e: KeyboardEvent) { emit('enter') }
282
+ const {
283
+ activeCountryCode,
284
+ selectedIndex,
285
+ searchQuery,
286
+ sortedCountries,
287
+ chooseCountry,
288
+ initializeCountry
289
+ } = useCountrySelection(props, emit)
293
290
 
294
- function onSpace() { emit('space') }
291
+ const { formatPhone } = usePhoneNumberFormatting(props, activeCountryCode)
295
292
 
296
- // const focus = () => refInput?.focus();
293
+ const isPreferred = (country?: Country) => props.preferredCountries.includes(country?.iso2 as CountryCode) || false
297
294
 
298
295
  // Method: reset
299
296
  function reset() {
300
- selectedIndex = sortedCountries.findIndex(
301
- c => c.iso2 === activeCountryCode,
297
+ if (!sortedCountries.value || !activeCountryCode.value) return
298
+ selectedIndex.value = sortedCountries.value.findIndex(
299
+ (c: Country) => c.iso2 === activeCountryCode.value
302
300
  )
303
- open = false
301
+ open.value = false
304
302
  }
305
303
 
304
+ const phone = defineModel<string>('modelValue', {
305
+ default: '',
306
+ set: (value) => {
307
+ let maybeFormatted = value
308
+ if (value.length > 5) {
309
+ maybeFormatted = formatPhone(`${value}`)
310
+ if (!maybeFormatted) {
311
+ emit('update:modelValue', '')
312
+ return ''
313
+ }
314
+ }
315
+
316
+ emit('update:modelValue', maybeFormatted)
317
+ debouncedEmit(maybeFormatted)
318
+ return maybeFormatted
319
+ },
320
+ get: value => value,
321
+ })
322
+
323
+ function onBlur() { emit('blur') }
324
+ function onFocus() { emit('focus') }
325
+ function onEnter() { emit('enter') }
326
+ function onSpace() { emit('space') }
327
+
306
328
  function handleInput(e: KeyboardEvent) {
307
329
  const keyVal = (e.key as string | undefined) ?? ''
308
330
  if (keyVal.length > 1 || e.metaKey) return
@@ -313,6 +335,16 @@ function handleInput(e: KeyboardEvent) {
313
335
 
314
336
  e.preventDefault()
315
337
  }
338
+
339
+ async function getIp() {
340
+ const apiData = sessionStorage.getItem('ipapi')
341
+ if (apiData) return JSON.parse(apiData)
342
+ const { data } = await axios.get('https://ipapi.co/json/')
343
+ sessionStorage.setItem('ipapi', JSON.stringify(data))
344
+ return data
345
+ }
346
+
347
+ onMounted(initializeCountry)
316
348
  </script>
317
349
 
318
350
  <template>
@@ -333,17 +365,12 @@ function handleInput(e: KeyboardEvent) {
333
365
  @keydown.tab="reset"
334
366
  >
335
367
  <Dropdown
336
- v-if="!computedDropDownOptions.hide"
337
- ref="phoneDropdown"
368
+ v-model:shown="open"
338
369
  placement="bottom-start"
339
370
  :disabled="computedDropDownOptions.disabled"
340
- @hide="open = false"
341
371
  >
342
372
  <template #trigger>
343
- <span
344
- class="flex gap-05"
345
- @click="open = true"
346
- >
373
+ <span class="flex gap-05">
347
374
  <Icon :icon="open ? 'collapse_all' : 'expand_all'" />
348
375
  <Flag
349
376
  v-if="computedDropDownOptions.showFlags && activeCountryCode"
@@ -361,8 +388,7 @@ function handleInput(e: KeyboardEvent) {
361
388
  />
362
389
 
363
390
  <ul
364
- class="overflow-y p-0"
365
- :style="{ 'max-height': '400px' }"
391
+ class="overflow-y p-0 max-h-300px"
366
392
  :class="dropdownOpenDirection"
367
393
  role="listbox"
368
394
  >
@@ -1,7 +1,7 @@
1
1
  export { default as Checkbox } from './Checkbox.vue'
2
2
  export { default as CheckInput } from './CheckInput.vue'
3
3
  export { default as CodeEditor } from './CodeEditor/Index.vue'
4
- export { default as ColorPicker } from './ColorPicker.vue'
4
+ export { default as ColorInput } from './ColorInput.vue'
5
5
  export { default as DateInput } from './DateInput.vue'
6
6
  export { default as DatePick } from './DatePick.vue'
7
7
  export { default as DatePicker } from './DatePicker.vue'
@@ -14,8 +14,11 @@ import {
14
14
  BglForm,
15
15
  bindAttrs,
16
16
  classify,
17
- keyToLabel
17
+ keyToLabel,
18
+ TelInput,
19
+ ColorInput,
18
20
  } from '@bagelink/vue'
21
+
19
22
  import { h, isVNode } from 'vue'
20
23
 
21
24
  const SLOT_VALUE_COMPONENTS = new Set(['div', 'span', 'p'])
@@ -44,6 +47,8 @@ export function useSchemaField<T extends { [key: string]: any }>(optns: UseSchem
44
47
  textarea: TextInput,
45
48
  number: NumberInput,
46
49
  array: FieldArray,
50
+ color: ColorInput,
51
+ tel: TelInput,
47
52
  select: SelectInput,
48
53
  toggle: ToggleInput,
49
54
  check: CheckInput,
@@ -1,7 +1,5 @@
1
1
  import type { BglFormSchemaT, Field, Option, DotNotation } from '@bagelink/vue'
2
2
  import type { UploadInputProps } from '../components/form/inputs/Upload/upload.types'
3
- import { TelInput } from '@bagelink/vue'
4
- import { markRaw } from 'vue'
5
3
 
6
4
  interface InputOptions {
7
5
  required?: boolean
@@ -223,11 +221,27 @@ export function telField<T extends { [key: string]: any }>(
223
221
  options?: { [key: string]: any }
224
222
  ): Field<T> {
225
223
  return {
226
- $el: markRaw(TelInput),
224
+ $el: 'tel',
227
225
  id,
228
226
  label,
229
227
  vIf: options?.vIf,
230
228
  attrs: options,
229
+ class: options?.class,
230
+ }
231
+ }
232
+
233
+ export function colorField<T extends { [key: string]: any }>(
234
+ id: DotNotation<T> | string,
235
+ label?: string,
236
+ options?: { [key: string]: any }
237
+ ): Field<T> {
238
+ return {
239
+ $el: 'color',
240
+ id,
241
+ label,
242
+ vIf: options?.vIf,
243
+ attrs: options,
244
+ class: options?.class,
231
245
  }
232
246
  }
233
247
 
@@ -117,3 +117,39 @@ export function timeAgo(date: string | Date, lang: AvailableTimeLanguages = 'en'
117
117
 
118
118
  return selectedLang.justNow as string
119
119
  }
120
+
121
+ const formatMap = {
122
+ dd: { type: 'day', format: '2-digit' },
123
+ ddd: { type: 'weekday', format: 'short' },
124
+ dddd: { type: 'weekday', format: 'long' },
125
+ mm: { type: 'month', format: '2-digit' },
126
+ mmm: { type: 'month', format: 'short' },
127
+ mmmm: { type: 'month', format: 'long' },
128
+ yy: { type: 'year', format: '2-digit' },
129
+ yyyy: { type: 'year', format: 'numeric' },
130
+ HH: { type: 'hour', format: '2-digit' },
131
+ hh: { type: 'hour', format: '2-digit' },
132
+ MM: { type: 'minute', format: '2-digit' },
133
+ ss: { type: 'second', format: '2-digit' },
134
+ ampm: { type: 'dayPeriod', format: 'short' }
135
+ } as const
136
+
137
+ export function formatDate(date?: string | Date, format: string = 'dd.mm.yy') {
138
+ if (!date) return ''
139
+ try {
140
+ const formatParts = format.split(/[.\s:]/)
141
+ const formatObject: Record<string, string> = {}
142
+
143
+ for (const part of formatParts) {
144
+ const formatInfo = formatMap[part as keyof typeof formatMap]
145
+ if (!formatInfo) continue
146
+ formatObject[formatInfo.type] = formatInfo.format
147
+ }
148
+
149
+ const d = typeof date === 'string' ? new Date(date) : date
150
+
151
+ return d.toLocaleDateString('he-IL', formatObject)
152
+ } catch (error) {
153
+ return ''
154
+ }
155
+ }