Skip to content

Commit 8ff79f6

Browse files
committed
feat: global modal
1 parent 0674f74 commit 8ff79f6

9 files changed

Lines changed: 180 additions & 48 deletions

File tree

resources/js/App.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { RouterView } from 'vue-router';
99
1010
import ToastController from '@/components/pinesUI/ToastController.vue';
1111
import ContextMenu from '@/components/pinesUI/ContextMenu.vue';
12+
import GlobalModal from '@/components/modals/GlobalModal.vue';
1213
1314
const toastPosition = ref<ToastPostion>();
1415
@@ -38,5 +39,6 @@ watch(isPlaylist, setIsPlaylist, { immediate: false });
3839
<template>
3940
<ToastController v-if="toastPosition" :position="toastPosition" />
4041
<RouterView />
42+
<GlobalModal />
4143
<ContextMenu ref="contextMenu" :items="contextMenuItems" :style="contextMenuStyle" :itemStyle="contextMenuItemStyle ?? 'hover:bg-purple-600 hover:text-white'" />
4244
</template>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<script setup lang="ts">
2+
import { useModalStore } from '@/stores/ModalStore';
3+
4+
import ButtonCorner from '@/components/inputs/ButtonCorner.vue';
5+
6+
const modalStore = useModalStore();
7+
</script>
8+
9+
<template>
10+
<section class="flex flex-wrap gap-2 items-center">
11+
<h3 ref="modalTitle" id="modalTitle" class="text-xl font-semibold scroll-mt-16 sm:scroll-mt-12 flex-1">
12+
<slot name="title">
13+
{{ modalStore.props?.title ?? 'Modal Title' }}
14+
</slot>
15+
</h3>
16+
<ButtonCorner @click="modalStore.close" class="!m-0 !static" />
17+
<p class="text-neutral-500 dark:text-neutral-400 text-sm w-full" v-if="$slots.description" id="modalDescription">
18+
<slot name="description"> </slot>
19+
</p>
20+
</section>
21+
<slot> </slot>
22+
</template>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<script setup lang="ts">
2+
import { deleteAccount } from '@/service/authAPI';
3+
import { useModalStore } from '@/stores/ModalStore';
4+
import { useRouter } from 'vue-router';
5+
6+
import PasswordConfirm from '@/components/forms/PasswordConfirm.vue';
7+
import BaseModal from '@/components/modals/BaseModal.vue';
8+
9+
const modalStore = useModalStore();
10+
const router = useRouter();
11+
12+
function handleSuccess() {
13+
modalStore.close();
14+
router.push('/');
15+
}
16+
</script>
17+
18+
<template>
19+
<BaseModal>
20+
<template #title>Are you sure you want to delete your account?</template>
21+
<template #description>
22+
Once your account is deleted, all of its resources and data will also be permanently deleted. Please enter your password to confirm you would like to permanently delete
23+
your account.
24+
</template>
25+
26+
<PasswordConfirm :action="deleteAccount" success-message="Account Deleted..." @confirm="handleSuccess" @cancel="modalStore.close" />
27+
</BaseModal>
28+
</template>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<script setup lang="ts">
2+
import { OnClickOutside } from '@vueuse/components';
3+
import { useModalStore } from '@/stores/ModalStore';
4+
import { UseFocusTrap } from '@vueuse/integrations/useFocusTrap/component';
5+
6+
const modalStore = useModalStore();
7+
</script>
8+
<template>
9+
<Teleport to="body">
10+
<dialog
11+
v-show="modalStore.isOpen || modalStore.isAnimating"
12+
class="modal fixed top-0 left-0 z-[300] flex items-center justify-center w-screen h-screen text-gray-900 dark:text-neutral-200 bg-transparent"
13+
v-cloak
14+
aria-modal="true"
15+
aria-labelledby="modalTitle"
16+
aria-describedby="modalDescription"
17+
>
18+
<Transition
19+
enter-active-class="ease-out duration-300"
20+
enter-from-class="opacity-0"
21+
enter-to-class="opacity-100"
22+
leave-active-class="ease-in duration-300"
23+
leave-from-class="opacity-100"
24+
leave-to-class="opacity-0"
25+
>
26+
<div v-if="modalStore.isOpen" class="absolute inset-0 w-full h-full backdrop-blur-sm bg-opacity-70"></div>
27+
</Transition>
28+
<Transition
29+
enter-active-class="ease-out duration-300"
30+
enter-from-class="opacity-0 sm:scale-95"
31+
enter-to-class="opacity-100 sm:scale-100"
32+
leave-active-class="ease-in duration-200"
33+
leave-from-class="opacity-100 sm:scale-100"
34+
leave-to-class="opacity-0 sm:scale-95"
35+
>
36+
<UseFocusTrap v-if="modalStore.isOpen" class="relative w-full px-6 py-10 sm:py-6 max-h-screen h-full overflow-y-scroll scrollbar-hide flex items-center">
37+
<OnClickOutside
38+
@trigger="modalStore.close"
39+
@keydown.esc="modalStore.close"
40+
class="gap-4 flex flex-col drop-shadow-md m-auto w-full p-6 bg-white dark:bg-neutral-800/90 backdrop-blur-lg border shadow-lg border-neutral-200 dark:border-neutral-700 sm:max-w-lg xl:max-w-xl 3xl:max-w-2xl rounded-md sm:rounded-lg"
41+
tabindex="-1"
42+
>
43+
<component :is="modalStore.component" v-bind="modalStore.props" @close="modalStore.close" />
44+
</OnClickOutside>
45+
</UseFocusTrap>
46+
</Transition>
47+
</dialog>
48+
</Teleport>
49+
</template>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<script setup lang="ts">
2+
import { signOutOtherSessions } from '@/service/authAPI';
3+
import { useModalStore } from '@/stores/ModalStore';
4+
5+
import PasswordConfirm from '@/components/forms/PasswordConfirm.vue';
6+
import BaseModal from '@/components/modals/BaseModal.vue';
7+
8+
const props = defineProps<{ onSuccess?: () => void }>();
9+
const modalStore = useModalStore();
10+
11+
function handleSuccess() {
12+
modalStore.close();
13+
props.onSuccess?.();
14+
}
15+
</script>
16+
17+
<template>
18+
<BaseModal>
19+
<template #description>Are you sure you want to sign out of other devices? You won't be able to undo this action.</template>
20+
<PasswordConfirm :action="signOutOtherSessions" success-message="Other Sessions Logged Out Successfully" @confirm="handleSuccess" @cancel="modalStore.close" />
21+
</BaseModal>
22+
</template>

resources/js/components/pinesUI/ModalBase.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import ButtonForm from '@/components/inputs/ButtonForm.vue';
1010
const props = withDefaults(
1111
defineProps<{
1212
modalData: any;
13+
// Should not be part of the modal
1314
action?: () => Promise<void>;
1415
useControls?: boolean;
1516
isProcessing?: boolean;
@@ -22,8 +23,10 @@ const props = withDefaults(
2223
2324
const { setScrollLock } = useAppStore();
2425
const title = useTemplateRef('modalTitle');
26+
// Should not be part of the modal
2527
const processing = ref(false);
2628
29+
// Should not be part of the modal
2730
const submitModal = async () => {
2831
processing.value = true;
2932
await props.action?.();
Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
<script setup lang="ts">
2-
import { deleteAccount } from '@/service/authAPI';
3-
import { useRouter } from 'vue-router';
2+
import { useModalStore } from '@/stores/ModalStore';
43
5-
import PasswordConfirm from '@/components/forms/PasswordConfirm.vue';
4+
import DeleteAccountModal from '@/components/modals/DeleteAccountModal.vue';
65
import SettingsHeader from '@/components/settings/SettingsHeader.vue';
76
import SettingsCard from '@/components/cards/SettingsCard.vue';
87
import ButtonForm from '@/components/inputs/ButtonForm.vue';
9-
import ModalBase from '@/components/pinesUI/ModalBase.vue';
10-
import useModal from '@/composables/useModal';
118
12-
const confirmModal = useModal({ title: 'Are you sure you want to delete your account?', submitText: 'Confim' });
13-
const router = useRouter();
9+
const modal = useModalStore();
1410
</script>
1511

1612
<template>
@@ -28,32 +24,12 @@ const router = useRouter();
2824
variant="submit"
2925
class="bg-rose-600 hover:bg-rose-700 dark:hover:bg-rose-500"
3026
title="Delete your account permanently"
31-
@click="confirmModal.toggleModal()"
27+
@click="modal.open(DeleteAccountModal)"
3228
>
3329
Delete Account
3430
</ButtonForm>
3531
</div>
3632
</section>
3733
</template>
3834
</SettingsCard>
39-
40-
<ModalBase :modalData="confirmModal" :useControls="false">
41-
<template #description>
42-
Once your account is deleted, all of its resources and data will also be permanently deleted. Please enter your password to confirm you would like to permanently delete
43-
your account.
44-
</template>
45-
<template #content>
46-
<PasswordConfirm
47-
:action="deleteAccount"
48-
@cancel="confirmModal.toggleModal()"
49-
:success-action="
50-
() => {
51-
confirmModal.toggleModal();
52-
router.push('/');
53-
}
54-
"
55-
success-message="Account Deleted..."
56-
/>
57-
</template>
58-
</ModalBase>
5935
</template>
Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
11
<script setup lang="ts">
2-
import { signOutOtherSessions } from '@/service/authAPI';
32
import { useSettingsStore } from '@/stores/SettingsStore';
3+
import { useModalStore } from '@/stores/ModalStore';
44
import { storeToRefs } from 'pinia';
55
6+
import LogoutSessionsModal from '@/components/modals/LogoutSessionsModal.vue';
67
import SettingsHeader from '@/components/settings/SettingsHeader.vue';
78
import SettingsCard from '@/components/cards/SettingsCard.vue';
89
import SessionCard from '@/components/cards/SessionCard.vue';
910
import ButtonForm from '@/components/inputs/ButtonForm.vue';
1011
1112
import SvgSpinners90RingWithBg from '~icons/svg-spinners/90-ring-with-bg';
1213
13-
import PasswordConfirm from '@/components/forms/PasswordConfirm.vue';
14-
import ModalBase from '@/components/pinesUI/ModalBase.vue';
15-
import useModal from '@/composables/useModal';
16-
17-
const confirmModal = useModal({ title: 'Sign Out of Other Devices', submitText: 'Confim' });
18-
1914
const { stateSessions, isLoadingSessions } = storeToRefs(useSettingsStore());
15+
16+
const modal = useModalStore();
2017
</script>
2118

2219
<template>
@@ -43,21 +40,9 @@ const { stateSessions, isLoadingSessions } = storeToRefs(useSettingsStore());
4340
<SessionCard v-for="(session, index) in stateSessions" :key="index" :session="session" class="flex items-center" />
4441
</div>
4542
<div class="relative flex flex-col-reverse sm:flex-row sm:justify-end gap-2 w-full">
46-
<ButtonForm variant="submit" title="Not Implemented Yet" @click="confirmModal.toggleModal()">Log Out Other Sessions</ButtonForm>
43+
<ButtonForm variant="submit" @click="modal.open(LogoutSessionsModal, { title: 'Sign Out of Other Devices' })">Log Out Other Sessions</ButtonForm>
4744
</div>
4845
</section>
4946
</template>
5047
</SettingsCard>
51-
52-
<ModalBase :modalData="confirmModal" :useControls="false">
53-
<template #description>Are you sure you want to sign out of other devices? You won't be able to undo this action. </template>
54-
<template #content>
55-
<PasswordConfirm
56-
:action="signOutOtherSessions"
57-
success-message="Other Sessions Logged Out Successfully"
58-
@cancel="confirmModal.toggleModal()"
59-
@confirm="confirmModal.toggleModal()"
60-
/>
61-
</template>
62-
</ModalBase>
6348
</template>

resources/js/stores/ModalStore.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { shallowRef, ref, reactive, type Component } from 'vue';
2+
import { defineStore } from 'pinia';
3+
4+
export const useModalStore = defineStore('Modal', () => {
5+
const isAnimating = ref(false);
6+
const isOpen = ref(false);
7+
8+
const props = reactive<Record<string, any>>({});
9+
const component = shallowRef<any>(null);
10+
11+
const animationTime = ref(300);
12+
const timeoutId = ref<number | null>(null);
13+
14+
function open(comp: Component, newProps: Record<string, any> = {}) {
15+
if (timeoutId.value) clearTimeout(timeoutId.value);
16+
component.value = comp;
17+
Object.assign(props, newProps);
18+
19+
isOpen.value = true;
20+
isAnimating.value = true;
21+
22+
timeoutId.value = window.setTimeout(() => {
23+
isAnimating.value = false;
24+
}, animationTime.value);
25+
}
26+
27+
function close() {
28+
if (timeoutId.value) clearTimeout(timeoutId.value);
29+
isOpen.value = false;
30+
isAnimating.value = true;
31+
timeoutId.value = window.setTimeout(() => {
32+
isAnimating.value = false;
33+
}, animationTime.value);
34+
}
35+
36+
return {
37+
props,
38+
isOpen,
39+
isAnimating,
40+
component,
41+
animationTime,
42+
open,
43+
close,
44+
};
45+
});

0 commit comments

Comments
 (0)