feat(common): move confirmation modal to his own store, and add 'onClosed' event

This commit is contained in:
2025-12-30 00:37:39 +01:00
parent c0fd13cd26
commit 408abb695d
5 changed files with 116 additions and 109 deletions

View File

@@ -4,7 +4,7 @@ import Buttons from "./Buttons.vue";
const { onRender } = useScreen(); const { onRender } = useScreen();
const { assets } = useAssets(); const { assets } = useAssets();
const { close, state } = useConfirmationModal(); const confirmationModal = useConfirmationModal();
const BG_WIDTH = assets.common.confirmationModal.width; const BG_WIDTH = assets.common.confirmationModal.width;
const BG_HEIGHT = assets.common.confirmationModal.height; const BG_HEIGHT = assets.common.confirmationModal.height;
@@ -17,22 +17,22 @@ const BOTTOM_BAR_HEIGHT = 24;
const CLIP_HEIGHT = SCREEN_HEIGHT - BOTTOM_BAR_HEIGHT; const CLIP_HEIGHT = SCREEN_HEIGHT - BOTTOM_BAR_HEIGHT;
const handleActivateA = () => { const handleActivateA = () => {
state.value.onConfirm?.(); confirmationModal.onConfirm?.();
close(); confirmationModal.close();
}; };
const handleActivateB = () => { const handleActivateB = () => {
close(); confirmationModal.close();
}; };
onRender((ctx) => { onRender((ctx) => {
if (!state.value.isVisible) return; if (!confirmationModal.isVisible) return;
ctx.beginPath(); ctx.beginPath();
ctx.rect(0, 0, SCREEN_WIDTH, CLIP_HEIGHT); ctx.rect(0, 0, SCREEN_WIDTH, CLIP_HEIGHT);
ctx.clip(); ctx.clip();
ctx.translate(0, state.value.offsetY); ctx.translate(0, confirmationModal.offsetY);
ctx.drawImage(assets.common.confirmationModal, BG_X, BG_Y); ctx.drawImage(assets.common.confirmationModal, BG_X, BG_Y);
@@ -40,18 +40,18 @@ onRender((ctx) => {
ctx.textBaseline = "top"; ctx.textBaseline = "top";
ctx.fillStyle = "#ffffff"; ctx.fillStyle = "#ffffff";
fillTextCentered(ctx, state.value.text, BG_X, TEXT_Y, BG_WIDTH); fillTextCentered(ctx, confirmationModal.text, BG_X, TEXT_Y, BG_WIDTH);
}, 100); }, 100);
onUnmounted(() => { onUnmounted(() => {
close(); confirmationModal.close();
}); });
</script> </script>
<template> <template>
<Buttons <Buttons
v-if="state.onConfirm" v-if="confirmationModal.onConfirm"
:y-offset="state.modalButtonsYOffset" :y-offset="confirmationModal.modalButtonsYOffset"
b-label="Cancel" b-label="Cancel"
a-label="Confirm" a-label="Confirm"
@activate-a="handleActivateA" @activate-a="handleActivateA"

View File

@@ -6,7 +6,7 @@ import Buttons from "./Buttons.vue";
import ButtonSelector from "~/components/Common/ButtonSelector.vue"; import ButtonSelector from "~/components/Common/ButtonSelector.vue";
const store = useContactStore(); const store = useContactStore();
const { open: openModal, state: modalState } = useConfirmationModal(); const confirmationModal = useConfirmationModal();
const ACTIONS = { const ACTIONS = {
github: ["Open", "Github profile", "https://github.com/pihkaal"], github: ["Open", "Github profile", "https://github.com/pihkaal"],
@@ -59,7 +59,7 @@ const actionateButton = async (button: (typeof selectedButton)["value"]) => {
} }
} else { } else {
const url = content.replace(/^https?:\/\//, ""); const url = content.replace(/^https?:\/\//, "");
openModal({ confirmationModal.open({
text: `Open ${url}?`, text: `Open ${url}?`,
onConfirm: async () => { onConfirm: async () => {
store.pushNotification(`${verb} opened`); store.pushNotification(`${verb} opened`);
@@ -92,7 +92,7 @@ const actionateButton = async (button: (typeof selectedButton)["value"]) => {
<CommonConfirmationModal /> <CommonConfirmationModal />
<CommonButtons <CommonButtons
:y-offset=" :y-offset="
store.isIntro ? store.intro.barOffsetY : modalState.buttonsYOffset store.isIntro ? store.intro.barOffsetY : confirmationModal.buttonsYOffset
" "
:opacity=" :opacity="
store.isIntro store.isIntro

View File

@@ -22,7 +22,7 @@ export const useButtonNavigation = <T extends Record<string, ButtonConfig>>({
>; >;
disabled?: Ref<boolean>; disabled?: Ref<boolean>;
}) => { }) => {
const { state: modalState } = useConfirmationModal(); const confirmationModal = useConfirmationModal();
const selectedButton = ref(initialButton); const selectedButton = ref(initialButton);
const selectorPosition = computed(() => buttons[selectedButton.value]!); const selectorPosition = computed(() => buttons[selectedButton.value]!);
@@ -32,7 +32,7 @@ export const useButtonNavigation = <T extends Record<string, ButtonConfig>>({
const { onClick } = useScreen(); const { onClick } = useScreen();
onClick((x: number, y: number) => { onClick((x: number, y: number) => {
if (modalState.value.isOpen || disabled?.value) return; if (confirmationModal.isOpen || disabled?.value) return;
for (const [buttonName, config] of Object.entries(buttons) as [ for (const [buttonName, config] of Object.entries(buttons) as [
keyof T, keyof T,
@@ -60,7 +60,7 @@ export const useButtonNavigation = <T extends Record<string, ButtonConfig>>({
}); });
useKeyDown((key) => { useKeyDown((key) => {
if (modalState.value.isOpen || disabled?.value) return; if (confirmationModal.isOpen || disabled?.value) return;
const currentButton = selectedButton.value as keyof T; const currentButton = selectedButton.value as keyof T;
const currentNav = navigation[currentButton]; const currentNav = navigation[currentButton];

View File

@@ -1,93 +0,0 @@
import gsap from "gsap";
const MODAL_MAX_Y_OFFSET = 106;
const BUTTONS_MAX_Y_OFFSET = 20;
const BUTTONS_TIME = 0.1;
const MODAL_TIME = 0.225;
const state = useState("confirmationModal", () =>
reactive({
isOpen: false,
text: "",
onConfirm: null as (() => void) | null,
offsetY: MODAL_MAX_Y_OFFSET,
buttonsYOffset: 0,
modalButtonsYOffset: BUTTONS_MAX_Y_OFFSET,
isVisible: false,
isClosing: false,
}),
);
const open = (options: { text: string; onConfirm?: () => void }) => {
gsap.killTweensOf(state.value);
state.value.text = options.text;
state.value.onConfirm = options.onConfirm ?? null;
state.value.isVisible = true;
state.value.isClosing = false;
state.value.isOpen = true;
gsap
.timeline()
// standard buttons down
.fromTo(
state.value,
{ buttonsYOffset: 0 },
{
buttonsYOffset: BUTTONS_MAX_Y_OFFSET,
duration: BUTTONS_TIME,
ease: "none",
},
)
// modal up
.fromTo(
state.value,
{ offsetY: MODAL_MAX_Y_OFFSET },
{ offsetY: 0, duration: MODAL_TIME, ease: "none" },
)
// modal buttons up
.fromTo(
state.value,
{ modalButtonsYOffset: BUTTONS_MAX_Y_OFFSET },
{ modalButtonsYOffset: 0, duration: BUTTONS_TIME, ease: "none" },
);
};
const close = () => {
if (!state.value.isVisible || state.value.isClosing) return;
state.value.isClosing = true;
gsap
.timeline()
// modal buttons down
.to(state.value, {
modalButtonsYOffset: BUTTONS_MAX_Y_OFFSET,
duration: BUTTONS_TIME,
ease: "none",
})
// modal down
.to(state.value, {
offsetY: MODAL_MAX_Y_OFFSET,
duration: MODAL_TIME,
ease: "none",
})
// standard buttons up
.to(state.value, {
buttonsYOffset: 0,
duration: BUTTONS_TIME,
ease: "none",
})
.call(() => {
state.value.isVisible = false;
state.value.isClosing = false;
state.value.isOpen = false;
state.value.text = "";
state.value.onConfirm = null;
});
};
export const useConfirmationModal = () => ({
open,
close,
state: readonly(state),
});

View File

@@ -0,0 +1,100 @@
import gsap from "gsap";
const MODAL_MAX_Y_OFFSET = 106;
const BUTTONS_MAX_Y_OFFSET = 20;
const BUTTONS_TIME = 0.1;
const MODAL_TIME = 0.225;
export const useConfirmationModal = defineStore("confirmationModal", {
state: () => ({
isOpen: false,
text: "",
onConfirm: null as (() => void) | null,
onClosed: null as (() => void) | null,
offsetY: MODAL_MAX_Y_OFFSET,
buttonsYOffset: 0,
modalButtonsYOffset: BUTTONS_MAX_Y_OFFSET,
isVisible: false,
isClosing: false,
}),
actions: {
open(options: {
text: string;
onConfirm?: () => void;
onClosed?: () => void;
}) {
gsap.killTweensOf(this);
this.text = options.text;
this.onConfirm = options.onConfirm ?? null;
this.onClosed = options.onClosed ?? null;
this.isVisible = true;
this.isClosing = false;
this.isOpen = true;
gsap
.timeline()
// standard buttons down
.fromTo(
this,
{ buttonsYOffset: 0 },
{
buttonsYOffset: BUTTONS_MAX_Y_OFFSET,
duration: BUTTONS_TIME,
ease: "none",
},
)
// modal up
.fromTo(
this,
{ offsetY: MODAL_MAX_Y_OFFSET },
{ offsetY: 0, duration: MODAL_TIME, ease: "none" },
)
// modal buttons up
.fromTo(
this,
{ modalButtonsYOffset: BUTTONS_MAX_Y_OFFSET },
{ modalButtonsYOffset: 0, duration: BUTTONS_TIME, ease: "none" },
);
},
close() {
if (!this.isVisible || this.isClosing) return;
this.isClosing = true;
gsap
.timeline()
// modal buttons down
.to(this, {
modalButtonsYOffset: BUTTONS_MAX_Y_OFFSET,
duration: BUTTONS_TIME,
ease: "none",
})
// modal down
.to(this, {
offsetY: MODAL_MAX_Y_OFFSET,
duration: MODAL_TIME,
ease: "none",
})
// standard buttons up
.to(this, {
buttonsYOffset: 0,
duration: BUTTONS_TIME,
ease: "none",
})
.call(() => {
const closedCallback = this.onClosed;
this.isVisible = false;
this.isClosing = false;
this.isOpen = false;
this.text = "";
this.onConfirm = null;
this.onClosed = null;
closedCallback?.();
});
},
},
});