feat(contact): notifications system + action
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
BIN
app/assets/images/contact/bottom-screen/notification.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
app/assets/images/contact/bottom-screen/ok-button.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -1,20 +1,31 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
okLabel: "Copy" | "Open";
|
||||||
|
}>();
|
||||||
|
|
||||||
const store = useContactStore();
|
const store = useContactStore();
|
||||||
|
|
||||||
const topBarImage = useTemplateRef("topBarImage");
|
const topBarImage = useTemplateRef("topBarImage");
|
||||||
const bottomBarImage = useTemplateRef("bottomBarImage");
|
const bottomBarImage = useTemplateRef("bottomBarImage");
|
||||||
|
const bottomBarOkImage = useTemplateRef("bottomBarOkImage");
|
||||||
|
|
||||||
useRender((ctx) => {
|
useRender((ctx) => {
|
||||||
if (!topBarImage.value || !bottomBarImage.value) return;
|
if (!topBarImage.value || !bottomBarImage.value || !bottomBarOkImage.value)
|
||||||
|
return;
|
||||||
|
|
||||||
ctx.globalAlpha = store.isIntro ? store.intro.stage3Opacity : 1;
|
ctx.globalAlpha = store.isIntro ? store.intro.stage3Opacity : 1;
|
||||||
|
|
||||||
|
// top bar
|
||||||
ctx.drawImage(topBarImage.value, 0, store.isIntro ? store.intro.topBarY : 0);
|
ctx.drawImage(topBarImage.value, 0, store.isIntro ? store.intro.topBarY : 0);
|
||||||
ctx.drawImage(
|
|
||||||
bottomBarImage.value,
|
// bottom bar
|
||||||
0,
|
ctx.translate(0, store.isIntro ? store.intro.bottomBarY : SCREEN_HEIGHT - 24);
|
||||||
store.isIntro ? store.intro.bottomBarY : SCREEN_HEIGHT - 24,
|
ctx.drawImage(bottomBarImage.value, 0, 0);
|
||||||
);
|
|
||||||
|
ctx.drawImage(bottomBarOkImage.value, 144, 4);
|
||||||
|
ctx.font = "10px NDS10";
|
||||||
|
ctx.fillStyle = "#000000";
|
||||||
|
ctx.fillText(props.okLabel, 144 + 35, 4 + 13);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -29,4 +40,9 @@ useRender((ctx) => {
|
|||||||
src="/assets/images/contact/bottom-screen/bottom-bar.png"
|
src="/assets/images/contact/bottom-screen/bottom-bar.png"
|
||||||
hidden
|
hidden
|
||||||
/>
|
/>
|
||||||
|
<img
|
||||||
|
ref="bottomBarOkImage"
|
||||||
|
src="/assets/images/contact/bottom-screen/ok-button.png"
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -6,7 +6,17 @@ import Bars from "./Bars.vue";
|
|||||||
|
|
||||||
const store = useContactStore();
|
const store = useContactStore();
|
||||||
|
|
||||||
const { selectorPosition } = useButtonNavigation({
|
const ACTIONS = {
|
||||||
|
github: ["Open", "Github profile", "https://github.com/pihkaal"],
|
||||||
|
email: ["Copy", "Email", "hello@pihkaal.me"],
|
||||||
|
website: ["Copy", "Website link", "https://pihkaal.me"],
|
||||||
|
cv: ["Open", "CV", "https://pihkaal.me/cv"],
|
||||||
|
} as const satisfies Record<
|
||||||
|
string,
|
||||||
|
[action: "Copy" | "Open", verb: string, content: string]
|
||||||
|
>;
|
||||||
|
|
||||||
|
const { selectedButton, selectorPosition } = useButtonNavigation({
|
||||||
buttons: {
|
buttons: {
|
||||||
github: [26, 27, 202, 42],
|
github: [26, 27, 202, 42],
|
||||||
email: [26, 59, 202, 42],
|
email: [26, 59, 202, 42],
|
||||||
@@ -30,8 +40,19 @@ const { selectorPosition } = useButtonNavigation({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
initialButton: "github",
|
initialButton: "github",
|
||||||
onButtonClick: (buttonName) => {
|
onButtonClick: async (button) => {
|
||||||
console.log("Clicked on selected button:", buttonName);
|
const [action, verb, content] = ACTIONS[button];
|
||||||
|
if (action === "Copy") {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(content);
|
||||||
|
store.pushNotification(`${verb} copied to clipboard`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to copy to clipboard:", error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await navigateTo(content, { open: { target: "_blank " } });
|
||||||
|
store.pushNotification(`${verb} opened`);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -45,5 +66,5 @@ const { selectorPosition } = useButtonNavigation({
|
|||||||
:opacity="store.isIntro ? store.intro.stage3Opacity : 1"
|
:opacity="store.isIntro ? store.intro.stage3Opacity : 1"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Bars />
|
<Bars :ok-label="ACTIONS[selectedButton][0]" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
83
app/components/Contact/TopScreen/Notifications.vue
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// text color:
|
||||||
|
const store = useContactStore();
|
||||||
|
|
||||||
|
const notificationImage = useTemplateRef("notificationImage");
|
||||||
|
const titleImage = useTemplateRef("titleImage");
|
||||||
|
|
||||||
|
useRender((ctx) => {
|
||||||
|
if (!notificationImage.value || !titleImage.value) return;
|
||||||
|
|
||||||
|
ctx.font = "10px NDS10";
|
||||||
|
|
||||||
|
// notifications
|
||||||
|
for (let i = store.notifications.length - 1; i >= 0; i--) {
|
||||||
|
const index = store.notifications.length - 1 - i;
|
||||||
|
const y = 169 - 24 * index + store.notificationsYOffset;
|
||||||
|
if (y < -24) break;
|
||||||
|
|
||||||
|
ctx.drawImage(notificationImage.value, 21, y);
|
||||||
|
|
||||||
|
const content = store.notifications[i]!;
|
||||||
|
ctx.fillStyle = content.includes("opened") ? "#00fbba" : "#e3f300";
|
||||||
|
ctx.fillText(store.notifications[i]!, 27, y + 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
// title
|
||||||
|
ctx.globalAlpha = store.isIntro ? store.intro.stage1Opacity : 1;
|
||||||
|
ctx.drawImage(
|
||||||
|
titleImage.value,
|
||||||
|
21,
|
||||||
|
store.isIntro
|
||||||
|
? store.intro.titleY
|
||||||
|
: 169 - 24 * store.notifications.length + store.notificationsYOffset,
|
||||||
|
);
|
||||||
|
|
||||||
|
// notifications count (left bar)
|
||||||
|
const MAX = 36;
|
||||||
|
const MAX_VISIBLE = 8;
|
||||||
|
|
||||||
|
let visibleNotifications = Math.min(store.notifications.length, MAX_VISIBLE);
|
||||||
|
const extraActive =
|
||||||
|
store.notificationsYOffset > 0 && store.notifications.length > MAX_VISIBLE;
|
||||||
|
|
||||||
|
if (extraActive) {
|
||||||
|
visibleNotifications += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fillStyle = "#415969";
|
||||||
|
for (let i = 0; i < visibleNotifications; i++) {
|
||||||
|
ctx.fillRect(3, 161 - i * 4, 12, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fillStyle = "#b2c3db";
|
||||||
|
const startY = 161 - visibleNotifications * 4;
|
||||||
|
const top = MAX - MAX_VISIBLE - (extraActive ? 1 : 0);
|
||||||
|
for (
|
||||||
|
let i = 0;
|
||||||
|
i < store.notifications.length - visibleNotifications && i < top;
|
||||||
|
i++
|
||||||
|
) {
|
||||||
|
if (i === top - 1) {
|
||||||
|
ctx.fillRect(7, startY - i * 4, 4, 2);
|
||||||
|
} else if (i === top - 2) {
|
||||||
|
ctx.fillRect(5, startY - i * 4, 8, 2);
|
||||||
|
} else {
|
||||||
|
ctx.fillRect(3, startY - i * 4, 12, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<img
|
||||||
|
ref="notificationImage"
|
||||||
|
src="/assets/images/contact/bottom-screen/notification.png"
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
ref="titleImage"
|
||||||
|
src="/assets/images/contact/top-screen/title.png"
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
const store = useContactStore();
|
|
||||||
|
|
||||||
const titleImage = useTemplateRef("titleImage");
|
|
||||||
|
|
||||||
useRender((ctx) => {
|
|
||||||
if (!titleImage.value) return;
|
|
||||||
|
|
||||||
ctx.globalAlpha = store.isIntro ? store.intro.stage1Opacity : 1;
|
|
||||||
ctx.drawImage(
|
|
||||||
titleImage.value,
|
|
||||||
21,
|
|
||||||
store.isIntro ? store.intro.titleY : SCREEN_HEIGHT - 23,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<img
|
|
||||||
ref="titleImage"
|
|
||||||
src="/assets/images/contact/top-screen/title.png"
|
|
||||||
hidden
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Background from "./Background.vue";
|
import Background from "./Background.vue";
|
||||||
import LeftBar from "./LeftBar.vue";
|
import LeftBar from "./LeftBar.vue";
|
||||||
import Title from "./Title.vue";
|
import Notifications from "./Notifications.vue";
|
||||||
|
|
||||||
const store = useContactStore();
|
const store = useContactStore();
|
||||||
|
|
||||||
@@ -15,5 +15,6 @@ onMounted(() => {
|
|||||||
<Background />
|
<Background />
|
||||||
|
|
||||||
<LeftBar />
|
<LeftBar />
|
||||||
<Title />
|
|
||||||
|
<Notifications />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ export const useContactStore = defineStore("contact", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
isIntro: true,
|
isIntro: true,
|
||||||
|
|
||||||
|
notifications: [] as string[],
|
||||||
|
notificationsYOffset: 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
@@ -70,5 +73,15 @@ export const useContactStore = defineStore("contact", {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
pushNotification(content: string) {
|
||||||
|
this.notifications.push(content);
|
||||||
|
|
||||||
|
gsap.fromTo(
|
||||||
|
this,
|
||||||
|
{ notificationsYOffset: 20 },
|
||||||
|
{ notificationsYOffset: 0, duration: 0.075 },
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||