Skip to content

Commit

Permalink
Merge pull request #283 from ymaheshwari1/#139
Browse files Browse the repository at this point in the history
Implemented: support for time zone switcher component(#139)
  • Loading branch information
ravilodhi authored Mar 27, 2024
2 parents 5cd44ef + 3185d86 commit 3c06763
Show file tree
Hide file tree
Showing 8 changed files with 315 additions and 4 deletions.
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,39 @@ If you have any questions or ideas feel free to join our <a href="https://discor
# The license

DXP Components is completely free and released under the Apache v2.0 License. Check <a href="https://github.com/hotwax/dxp-components/blob/main/LICENSE" target="_blank">LICENSE</a> for more details.

# Components
## DxpTimeZoneSwitcher

TimeZoneSwitcher provides support to select the timeZone for the application. The component uses luxon for managing the dateTime.

### Usage
```js
<DxpTimeZoneSwitcher />
```
![DxpTimeZoneSwitcher](/src/assets/images/DxpTimeZoneSwitcher.png)

### Change Date-Time format
You can pass a specific token string in the `dateTimeFormat` to display the timeZone as per the app. For the possible values of dateTimeFormat, check <a target="_blank" rel="noopener noreferrer" href="https://moment.github.io/luxon/#/formatting?id=table-of-tokens">here</a>.
```js
<DxpTimeZoneSwitcher dateTimeFormat="TTT" />
```
![DxpTimeZoneSwitcher with custom dateTime format](/src/assets/images/DxpTimeZoneSwticherCustomFormat.png)

### Slots
No slots are available for this component.

### Props
| Name | Description | Default Value |
| --- | --- | --- |
| showBrowserTimeZone | When `true` displays the timeZone of the browser in the timeZone selector | `true` |
| showDateTime | When `true` will display the current dateTime as per the timeZone option in the format provided in `dateTimeFormat` | `true` |
| dateTimeFormat | Pass the specific format in which you want to display the dateTime for the timeZone options. Honored only when `showDateTime` is `true`. | `t ZZZZ` |

### Methods
No methods are available for this component

### Events
| Name | Description |
| --- | --- |
| timeZoneUpdated | Emitted when timeZone is changed |
Binary file added src/assets/images/DxpTimeZoneSwitcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
216 changes: 216 additions & 0 deletions src/components/DxpTimeZoneSwitcher.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
<template>
<ion-card>
<ion-card-header>
<ion-card-title>
{{ $t('Timezone') }}
</ion-card-title>
</ion-card-header>
<ion-card-content>
{{ $t('The timezone you select is used to ensure automations you schedule are always accurate to the time you select.') }}
</ion-card-content>
<ion-item v-if="showBrowserTimeZone">
<ion-label>
<p class="overline">{{ $t("Browser TimeZone") }}</p>
{{ browserTimeZone.id }}
<p v-if="showDateTime">{{ getCurrentTime(browserTimeZone.id, dateTimeFormat) }}</p>
</ion-label>
</ion-item>
<ion-item lines="none">
<ion-label>
<p class="overline">{{ $t("Selected TimeZone") }}</p>
{{ currentTimeZoneId }}
<p v-if="showDateTime">{{ getCurrentTime(currentTimeZoneId, dateTimeFormat) }}</p>
</ion-label>
<ion-button id="time-zone-modal" slot="end" fill="outline" color="dark">{{ $t("Change") }}</ion-button>
</ion-item>
</ion-card>
<!-- Using inline modal(as recommended by ionic), also using it inline as the component inside modal is not getting mounted when using modalController -->
<ion-modal ref="timeZoneModal" trigger="time-zone-modal" @didPresent="search()" @didDismiss="clearSearch()">
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="closeModal">
<ion-icon :icon="closeOutline" />
</ion-button>
</ion-buttons>
<ion-title>{{ $t("Select time zone") }}</ion-title>
</ion-toolbar>
<ion-toolbar>
<ion-searchbar @ionFocus="selectSearchBarText($event)" :placeholder="$t('Search time zones')" v-model="queryString" @keyup.enter="queryString = $event.target.value; findTimeZone()" @keydown="preventSpecialCharacters($event)" />
</ion-toolbar>
</ion-header>

<ion-content class="ion-padding">
<div>
<ion-radio-group value="rd" v-model="timeZoneId">
<ion-list v-if="showBrowserTimeZone">
<ion-list-header>{{ $t("Browser time zone") }}</ion-list-header>
<ion-item>
<ion-radio label-placement="end" justify="start" :value="browserTimeZone.id">
<ion-label>
{{ browserTimeZone.label }} ({{ browserTimeZone.id }})
<p v-if="showDateTime">{{ getCurrentTime(browserTimeZone.id, dateTimeFormat) }}</p>
</ion-label>
</ion-radio>
</ion-item>
</ion-list>
<ion-list>
<ion-list-header v-if="showBrowserTimeZone">{{ $t("Select a different time zone") }}</ion-list-header>
<!-- Loading state -->
<div class="empty-state" v-if="isLoading">
<ion-item lines="none">
<ion-spinner color="secondary" name="crescent" slot="start" />
{{ $t("Fetching time zones") }}
</ion-item>
</div>
<!-- Empty state -->
<div class="empty-state" v-else-if="filteredTimeZones.length === 0">
<p>{{ $t("No time zone found") }}</p>
</div>
<div v-else>
<ion-item :key="timeZone.id" v-for="timeZone in filteredTimeZones">
<ion-radio label-placement="end" justify="start" :value="timeZone.id">
<ion-label>
{{ timeZone.label }} ({{ timeZone.id }})
<p v-if="showDateTime">{{ getCurrentTime(timeZone.id, dateTimeFormat) }}</p>
</ion-label>
</ion-radio>
</ion-item>
</div>
</ion-list>
</ion-radio-group>
</div>

<ion-fab vertical="bottom" horizontal="end" slot="fixed">
<ion-fab-button :disabled="!currentTimeZoneId" @click="setUserTimeZone">
<ion-icon :icon="saveOutline" />
</ion-fab-button>
</ion-fab>
</ion-content>
</ion-modal>
</template>

<script setup lang="ts">
import {
IonButton,
IonButtons,
IonCard,
IonCardHeader,
IonCardTitle,
IonCardContent,
IonContent,
IonFab,
IonFabButton,
IonHeader,
IonIcon,
IonItem,
IonLabel,
IonList,
IonListHeader,
IonModal,
IonRadio,
IonRadioGroup,
IonSearchbar,
IonSpinner,
IonTitle,
IonToolbar
} from '@ionic/vue';
import { closeOutline, saveOutline } from "ionicons/icons";
import { appContext, useUserStore } from '../index';
import { computed, onBeforeMount, ref } from "vue";
import { getCurrentTime } from '../utils'
const appState = appContext.config.globalProperties.$store;
const userStore = useUserStore();
const userProfile: any = computed(() => appState.getters['user/getUserProfile'])
const timeZones = computed(() => userStore.getTimeZones)
const currentTimeZoneId = computed(() => userStore.getCurrentTimeZone)
const isLoading = ref(false);
const timeZoneModal = ref();
const queryString = ref('');
const filteredTimeZones = ref([])
const timeZoneId = ref('')
// Fetching timeZone of the browser
const browserTimeZone = ref({
label: '',
id: Intl.DateTimeFormat().resolvedOptions().timeZone
})
const emit = defineEmits(["timeZoneUpdated"])
const props = defineProps({
showBrowserTimeZone: {
type: Boolean,
default: true
},
showDateTime: {
type: Boolean,
default: true
},
dateTimeFormat: {
type: String,
default: 't ZZZZ'
}
})
const closeModal = () => {
timeZoneModal.value.$el.dismiss(null, 'cancel');
}
onBeforeMount(async () => {
isLoading.value = true;
await userStore.getAvailableTimeZones();
if(userProfile.value && userProfile.value.userTimeZone) {
userStore.currentTimeZoneId = userProfile.value.userTimeZone
timeZoneId.value = userProfile.value.userTimeZone
}
if(props.showBrowserTimeZone) {
browserTimeZone.value.label = timeZones.value.find((timeZone: any) => timeZone.id.toLowerCase().match(browserTimeZone.value.id.toLowerCase()))?.label
}
findTimeZone();
isLoading.value = false;
})
async function setUserTimeZone() {
await userStore.setUserTimeZone(timeZoneId.value).then((tzId) => {
emit('timeZoneUpdated', tzId);
}).catch(err => err)
closeModal();
}
function findTimeZone() {
const searchedString = queryString.value.toLowerCase();
filteredTimeZones.value = timeZones.value.filter((timeZone: any) => timeZone.id.toLowerCase().match(searchedString) || timeZone.label.toLowerCase().match(searchedString));
if(props.showBrowserTimeZone) {
filteredTimeZones.value = filteredTimeZones.value.filter((timeZone: any) => !timeZone.id.toLowerCase().match(browserTimeZone.value.id.toLowerCase()));
}
}
async function selectSearchBarText(event: any) {
const element = await event.target.getInputElement()
element.select();
}
function preventSpecialCharacters($event: any) {
// Searching special characters fails the API, hence, they must be omitted
if(/[`!@#$%^&*()_+\-=\\|,.<>?~]/.test($event.key)) $event.preventDefault();
}
function search() {
isLoading.value = true;
findTimeZone();
isLoading.value = false;
}
// clearing the data explicitely as the modal is mounted due to the component being mounted always
function clearSearch() {
queryString.value = ''
filteredTimeZones.value = []
}
</script>
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ export { default as DxpOmsInstanceNavigator } from './DxpOmsInstanceNavigator.vu
export { default as DxpProductIdentifier } from "./DxpProductIdentifier.vue";
export { default as DxpShopifyImg } from './DxpShopifyImg.vue';
export { default as DxpUserProfile } from './DxpUserProfile.vue'
export { default as DxpTimeZoneSwitcher } from './DxpTimeZoneSwitcher.vue'
12 changes: 11 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ declare var process: any;
import { createPinia } from "pinia";
import { useProductIdentificationStore } from "./store/productIdentification";
import { useAuthStore } from "./store/auth";
import { DxpAppVersionInfo, DxpImage, DxpLanguageSwitcher, DxpLogin, DxpMenuFooterNavigation, DxpOmsInstanceNavigator, DxpProductIdentifier, DxpShopifyImg, DxpUserProfile } from "./components";
import { DxpAppVersionInfo, DxpImage, DxpLanguageSwitcher, DxpLogin, DxpMenuFooterNavigation, DxpOmsInstanceNavigator, DxpProductIdentifier, DxpShopifyImg, DxpTimeZoneSwitcher, DxpUserProfile } from "./components";
import { goToOms, getProductIdentificationValue } from "./utils";
import { initialiseFirebaseApp } from "./utils/firebase"
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import { createI18n } from 'vue-i18n'
import { useUserStore } from "./store/user";
import { IonicVue } from '@ionic/vue';

import "./service-worker"

Expand Down Expand Up @@ -60,6 +61,9 @@ export let dxpComponents = {
// registering pinia in the app
app.use(pinia);
app.use(i18n);
app.use(IonicVue, {
mode: 'md'
})

app.component('DxpAppVersionInfo', DxpAppVersionInfo)
app.component('DxpImage', DxpImage)
Expand All @@ -69,6 +73,7 @@ export let dxpComponents = {
app.component('DxpOmsInstanceNavigator', DxpOmsInstanceNavigator)
app.component('DxpProductIdentifier', DxpProductIdentifier)
app.component('DxpShopifyImg', DxpShopifyImg)
app.component('DxpTimeZoneSwitcher', DxpTimeZoneSwitcher)
app.component('DxpUserProfile', DxpUserProfile)

showToast = options.showToast
Expand All @@ -83,6 +88,10 @@ export let dxpComponents = {

userContext.setUserLocale = options.setUserLocale

// TimeZone specific api from oms-api package exposed by the app
userContext.setUserTimeZone = options.setUserTimeZone
userContext.getAvailableTimeZones = options.getAvailableTimeZones

productIdentificationContext.getProductIdentificationPref = options.getProductIdentificationPref
productIdentificationContext.setProductIdentificationPref = options.setProductIdentificationPref

Expand All @@ -109,6 +118,7 @@ export {
DxpOmsInstanceNavigator,
DxpProductIdentifier,
DxpShopifyImg,
DxpTimeZoneSwitcher,
DxpUserProfile,
getProductIdentificationValue,
goToOms,
Expand Down
46 changes: 43 additions & 3 deletions src/store/user.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { defineStore } from "pinia";
import { i18n, userContext } from "../../src";
import { appContext, i18n, translate, userContext } from "../../src";
import { hasError } from "@hotwax/oms-api";
import { DateTime } from "luxon";
import { showToast } from "src/utils";

declare let process: any;

export const useUserStore = defineStore('user', {
state: () => {
return {
localeOptions: process.env.VUE_APP_LOCALES ? JSON.parse(process.env.VUE_APP_LOCALES) : { "en-US": "English" },
locale: 'en-US'
locale: 'en-US',
currentTimeZoneId: '',
timeZones: []
}
},
getters: {
getLocale: (state) => state.locale,
getLocaleOptions: (state) => state.localeOptions
getLocaleOptions: (state) => state.localeOptions,
getTimeZones: (state) => state.timeZones,
getCurrentTimeZone: (state) => state.currentTimeZoneId
},
actions: {
async setLocale(locale: string) {
Expand All @@ -34,6 +41,39 @@ export const useUserStore = defineStore('user', {
i18n.global.locale.value = newLocale
this.locale = newLocale
}
},
async setUserTimeZone(tzId: string) {
// Do not make any api call if the user clicks the same timeZone again that is already selected
if(this.currentTimeZoneId === tzId) {
return;
}

try {
await userContext.setUserTimeZone({ tzId })
this.currentTimeZoneId = tzId

showToast(translate("Time zone updated successfully"));
return Promise.resolve(tzId)
} catch(err) {
console.error('Error', err)
return Promise.reject('')
}
},
async getAvailableTimeZones() {
// Do not fetch timeZones information, if already available
if(this.timeZones.length) {
return;
}

try {
const resp = await userContext.getAvailableTimeZones()
this.timeZones = resp.filter((timeZone: any) => DateTime.local().setZone(timeZone.id).isValid);
} catch(err) {
console.error('Error', err)
}
},
updateTimeZone(tzId: string) {
this.currentTimeZoneId = tzId
}
},
persist: true
Expand Down
Loading

0 comments on commit 3c06763

Please sign in to comment.