feat: refine portfolio content, contact, and supporting media

This commit is contained in:
2026-09-05 20:08:57 +03:00
parent 15bcdf7cec
commit 103427f95b
53 changed files with 824 additions and 204 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, nextTick, onUnmounted, ref, shallowRef, watch } from 'vue';
import { computed, nextTick, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
import { useLazyMedia } from '@/composables/useLazyMedia';
import { isTestFault, recordTestFaultEvent } from '@/testing/fault-hooks';
@@ -28,8 +28,12 @@ const playing = shallowRef(false);
const unavailable = shallowRef(false);
const status = shallowRef('Video ready to play.');
const savedTime = shallowRef(0);
const automatic = computed(() => props.allowAutomatic && !unavailable.value);
const lazy = useLazyMedia(figure, automatic);
const userPaused = ref(false);
const pageVisible = ref(true);
const enhanced = ref(false);
const automatic = computed(() => props.allowAutomatic && !unavailable.value && !userPaused.value && pageVisible.value);
const lazy = useLazyMedia(figure, automatic, '0px');
const fallbackSource = computed(() => props.sources.find(({ type }) => type === 'video/mp4') ?? props.sources[0]);
let failureTimer: number | undefined;
const clearFailureTimer = (): void => {
@@ -51,7 +55,10 @@ const detach = async (): Promise<void> => {
watch(() => lazy.shouldAttach.value, (attached, previous) => {
if (!attached && previous) void detach();
if (attached) void nextTick(() => video.value?.load());
if (attached) void nextTick(() => {
// Explicit Play loads its sources before play(); a later load would abort it.
if (!lazy.explicitRequested.value) video.value?.load();
});
}, { flush: 'post' });
watch(() => props.allowAutomatic, (allowed) => {
@@ -92,6 +99,7 @@ const play = async (): Promise<void> => {
};
const pause = (): void => {
userPaused.value = true;
video.value?.pause();
playing.value = false;
status.value = 'Video paused.';
@@ -100,9 +108,25 @@ const pause = (): void => {
const togglePlayback = (): void => {
if (playing.value) pause();
else void play();
else {
userPaused.value = false;
void play();
}
};
const handleVisibility = (): void => {
pageVisible.value = !document.hidden;
if (!pageVisible.value) {
video.value?.pause();
lazy.clearExplicit();
}
};
onMounted(() => {
enhanced.value = true;
handleVisibility();
document.addEventListener('visibilitychange', handleVisibility);
});
const restoreTime = (): void => {
const element = video.value;
if (element !== null && Number.isFinite(savedTime.value) && savedTime.value > 0) {
@@ -111,13 +135,14 @@ const restoreTime = (): void => {
};
onUnmounted(() => {
document.removeEventListener('visibilitychange', handleVisibility);
clearFailureTimer();
video.value?.pause();
});
</script>
<template>
<figure ref="figure" class="project-media project-media--video" data-project-media>
<figure ref="figure" class="project-media project-media--video" data-project-media :data-enhanced="enhanced">
<video
ref="video"
:poster="poster"
@@ -125,7 +150,12 @@ onUnmounted(() => {
:height="height"
:aria-label="accessibleLabel"
preload="none"
muted
loop
:autoplay="automatic"
playsinline
@play="playing = true; status = 'Video playing.'"
@pause="playing = false"
@loadedmetadata="restoreTime"
@error="markUnavailable"
@ended="playing = false; status = 'Video ended.'"
@@ -145,5 +175,6 @@ onUnmounted(() => {
</button>
<p data-testid="video-status">{{ status }}</p>
</div>
<a v-if="fallbackSource" class="video-fallback" :href="fallbackSource.src" target="_blank" rel="noopener noreferrer">Open gameplay video</a>
</figure>
</template>