frontend/app/components/AboutMeWindow.vue

181 lines
4.2 KiB
Vue
Raw Normal View History

2025-09-10 23:43:41 +00:00
<template>
<div
v-if="windowData.isVisible && !windowData.isMinimized"
class="window"
:style="windowStyle"
@mousedown="bringToFront"
>
<div class="title-bar" @mousedown="startDrag">
<div class="title-bar-text">{{ t(windowData.title) }}</div>
<div class="title-bar-controls">
<button aria-label="Minimize" @click.stop="minimizeWindow"></button>
<button aria-label="Close" @click.stop="closeWindow"></button>
</div>
</div>
<div class="window-body">
<p>{{ t('hello_developer') }}</p>
<p>{{ t('demo_ui') }}</p>
<p>{{ t('coming_soon') }}</p>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits, ref, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
windowData: {
type: Object,
required: true,
default: () => ({}),
},
});
const emit = defineEmits([
'close',
'minimize',
'restore',
'bring-to-front',
'update-position',
]);
// Local refs for dragging, updated by watch for initial position from props
const x = ref(props.windowData.x);
const y = ref(props.windowData.y);
let isDragging = false;
let dragOffsetX = 0;
let dragOffsetY = 0;
let animationFrameId = null; // To store requestAnimationFrame ID
// Watch for changes in windowData.x and windowData.y (e.g., from localStorage load or app.vue updates)
watch(() => props.windowData.x, (newX) => { x.value = newX; });
watch(() => props.windowData.y, (newY) => { y.value = newY; });
const windowStyle = computed(() => ({
left: `${x.value}px`,
top: `${y.value}px`,
width: `${props.windowData.width}px`,
height: `${props.windowData.height}px`,
zIndex: props.windowData.zIndex,
}));
const startDrag = (event) => {
isDragging = true;
dragOffsetX = event.clientX - x.value;
dragOffsetY = event.clientY - y.value;
window.addEventListener('mousemove', doDrag);
window.addEventListener('mouseup', stopDrag);
bringToFront(); // Bring to front when dragging starts
};
const doDrag = (event) => {
if (isDragging) {
const newX = event.clientX - dragOffsetX;
const newY = event.clientY - dragOffsetY;
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
animationFrameId = requestAnimationFrame(() => {
x.value = newX;
y.value = newY;
});
}
};
const stopDrag = () => {
isDragging = false;
window.removeEventListener('mousemove', doDrag);
window.removeEventListener('mouseup', stopDrag);
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
// Emit the final updated position
emit('update-position', { x: x.value, y: y.value });
};
const closeWindow = () => {
emit('close');
};
const minimizeWindow = () => {
emit('minimize');
};
const bringToFront = () => {
emit('bring-to-front');
};
</script>
<style scoped>
.window {
position: absolute;
background-color: #c0c0c0; /* Classic Windows gray */
border: 2px solid;
border-color: #ffffff #808080 #808080 #ffffff; /* 3D effect */
box-shadow: 4px 4px 0px #000; /* Simple black shadow */
display: flex;
flex-direction: column;
/* z-index, top, left, width, height are set dynamically via style binding */
}
.title-bar {
background: linear-gradient(to right, #0a246a, #a6caf0); /* Classic blue gradient */
padding: 3px 2px 3px 3px;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: bold;
color: white;
font-size: 0.9rem;
cursor: grab;
}
.title-bar-text {
margin-left: 3px;
}
.title-bar-controls button {
background-color: #c0c0c0;
border: 2px solid;
border-color: #ffffff #000 #000 #ffffff;
width: 16px;
height: 14px;
font-size: 0.8rem;
line-height: 1;
padding: 0;
cursor: pointer;
}
.title-bar-controls button:active {
border-color: #000 #ffffff #ffffff #000;
}
.title-bar-controls button[aria-label="Minimize"]::before {
content: '_';
display: block;
position: relative;
top: -2px;
}
.title-bar-controls button[aria-label="Close"]::before {
content: 'X';
display: block;
position: relative;
top: -2px;
}
.window-body {
padding: 1rem;
flex-grow: 1;
overflow-y: auto;
border-top: 2px solid #000;
background-color: #c0c0c0; /* Ensure classic gray background for body */
}
</style>