Place
All checks were successful
Build and Deploy Nuxt / build (push) Successful in 2m1s

This commit is contained in:
2026-06-11 23:47:37 +02:00
parent 887e8c80af
commit 6e4a9ac967
15 changed files with 689 additions and 158 deletions

View File

@@ -260,8 +260,8 @@ onMounted(() => {
canvas.value.style.height = `${h}px`
ctx.setTransform(1, 0, 0, 1, 0, 0)
ctx.scale(dpr, dpr)
particles = createParticles(w, h)
bgParticles = createBgParticles(w, h)
// particles = createParticles(w, h)
// bgParticles = createBgParticles(w, h)
})
if (ctx && canvas.value) {

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
const { get } = api();
const { get } = useApi();
const { t } = useI18n();
const status = ref<{
@@ -27,31 +27,20 @@ const online = computed(() => status.value?.mongo === 'connected');
<template>
<div class="server-status-card">
<div class="monitor-bar">
<span class="monitor-label">SERVER STATUS</span>
<span class="monitor-label">STATUS</span>
<span v-if="loading" class="monitor-blink"></span>
<span v-else class="monitor-dot" :class="online ? 'green' : 'red'"></span>
</div>
<div class="monitor-screen">
<div class="screen-grid">
<div class="screen-line">
<span>Everything OK!</span>
</div>
<div class="screen-line">
<span class="screen-key">UPTIME</span>
<span class="screen-val">{{ loading ? '.......' : status?.uptime || '--' }}</span>
</div>
<div class="screen-line">
<span class="screen-key">MEM_RSS</span>
<span class="screen-val">{{ loading ? '......' : status?.memory.rss || '--' }}</span>
</div>
<div class="screen-line">
<span class="screen-key">HEAP</span>
<span class="screen-val">{{ loading ? '......' : status?.memory.heapUsed || '--' }}</span>
</div>
<div class="screen-line">
<span class="screen-key">MONGO</span>
<span class="screen-val" :class="status?.mongo === 'connected' ? 'ok' : status?.mongo ? 'err' : ''">
[{{ loading ? '.+.' : status?.mongo || '--' }}]
</span>
</div>
</div>
</div>
</div>
@@ -77,7 +66,6 @@ const online = computed(() => status.value?.mongo === 'connected');
background: #0e0e0e;
color: #d4d4d4;
font-family: 'Hurmit', monospace;
font-size: 0.5rem;
letter-spacing: 2px;
text-transform: uppercase;
border-bottom: 2px solid #3a3a3a;
@@ -128,7 +116,6 @@ const online = computed(() => status.value?.mongo === 'connected');
.screen-key {
font-family: 'Hurmit', monospace;
font-size: 0.55rem;
color: var(--color-text);
opacity: 0.4;
letter-spacing: 1px;
@@ -137,7 +124,6 @@ const online = computed(() => status.value?.mongo === 'connected');
.screen-val {
font-family: 'Hurmit', monospace;
font-size: 0.65rem;
color: var(--color-link);
letter-spacing: 0.5px;

View File

@@ -0,0 +1,237 @@
<script setup>
const GRID_COLS = 25
const GRID_ROWS = 80
// Palette of swatches
const palette = [
"#172038",
"#253a5e",
"#3c5e8b",
"#4f8fba",
"#73bed3",
"#a4dddb",
"#193024",
"#245938",
"#2b8435",
"#62ac4c",
"#a2dc6e",
"#c5e49b",
"#19332d",
"#25562e",
"#468232",
"#75a743",
"#a8ca58",
"#d0da91",
"#5f6d43",
"#97933a",
"#a9b74c",
"#cfd467",
"#d5dc97",
"#d6dea6",
"#382a28",
"#43322f",
"#564238",
"#715a42",
"#867150",
"#b1a282",
"#4d2b32",
"#7a4841",
"#ad7757",
"#c09473",
"#d7b594",
"#e7d5b3",
"#341c27",
"#602c2c",
"#884b2b",
"#be772b",
"#de9e41",
"#e8c170",
"#241527",
"#411d31",
"#752438",
"#a53030",
"#cf573c",
"#da863e",
"#1e1d39",
"#402751",
"#7a367b",
"#a23e8c",
"#c65197",
"#df84a5",
"#090a14",
"#10141f",
"#151d28",
"#202e37",
"#394a50",
"#577277",
"#819796",
"#a8b5b2",
"#c7cfcc",
"#ebede9"
]
const colorMap = ref({})
const selectedColor = ref(palette[0])
const selectedColorId = ref(0)
const { get, post } = useApi();
const { initSocket, onGridCellPaint, removeGridCellPaintListener } = useSocket();
onMounted(async () => {
initSocket()
try {
const cells = await get(`/grid-cells?minx=${1}&miny=${1}&maxx=${GRID_ROWS}&maxy=${GRID_COLS}`);
cells.forEach(cell => {
let key = `${cell.x},${cell.y}`;
if(cell.color > 0 && cell.color <= palette.length)
colorMap.value[key] = palette[cell.color];
else
colorMap.value[key] = "#00000000"
; });
} catch(e) {
console.error(e);
}
onGridCellPaint((data) => {
const key = `${data.x},${data.y}`;
if (data.color > 0 && data.color <= palette.length) {
colorMap.value[key] = palette[data.color];
} else {
delete colorMap.value[key];
}
});
});
onUnmounted(() => {
removeGridCellPaintListener();
});
function onCellClick(r, c) {
if (!selectedColor.value) return
const key = `${r},${c}`
colorMap.value[key] = selectedColor.value
// Send color paint
post('/grid-cells/paint', {
x: r,
y: c,
color: selectedColorId.value
})
}
function onCellRightClick(r, c, e) {
e.preventDefault()
const key = `${r},${c}`
delete colorMap.value[key]
// Send color delete
post('/grid-cells/paint', {
x: r,
y: c,
color: 0
})
}
function onSwatchClick(color) {
selectedColor.value = color;
selectedColorId.value = palette.indexOf(color);
}
const gridStyle = computed(() => ({
display: 'grid',
gridTemplateColumns: `repeat(${GRID_ROWS}, 1fr)`,
}))
</script>
<template>
<div class="place-container">
<div class="palette-row" role="toolbar" aria-label="Color palette">
<button
v-for="(color, i) in palette"
:key="i"
type="button"
class="swatch"
:class="{ active: selectedColor === color }"
:style="{ backgroundColor: color }"
:aria-label="`Select color ${color}`"
@click="onSwatchClick(color)"
/>
</div>
<div class="grid-wrapper" :style="gridStyle">
<div
v-for="r in GRID_ROWS"
:key="`row-${r}`"
class="grid-row"
>
<div
v-for="c in GRID_COLS"
:key="`${r}-${c}`"
class="cell"
:class="{ filled: colorMap[`${r},${c}`] }"
:style="colorMap[`${r},${c}`] ? { backgroundColor: colorMap[`${r},${c}`] } : {}"
@click="onCellClick(r, c)"
@contextmenu="onCellRightClick(r, c, $event)"
/>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.place-container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
}
.palette-row {
display: flex;
gap: 6px;
padding: 12px 0;
flex-wrap: wrap;
}
.swatch {
width: 28px;
height: 28px;
border: none;
cursor: pointer;
transition: border-color 0.05s, transform 0.05s;
&:hover {
transform: scale(1.15);
}
&.active {
border: 2px solid var(--color-link, #a4dddb);
box-shadow: 0 0 0 1px var(--color-link, #a4dddb);
}
@media (max-width: 600px) {
width: 28px;
height: 28px;
}
}
.grid-wrapper {
background-color: var(--color-background-line);
margin-bottom: 20px;
}
.cell {
aspect-ratio: 1 / 1;
cursor: pointer;
background-color: transparent;
transition: background-color 0.05s steps(1);
&:hover {
outline: 1px solid #fff;
}
}
.filled {
opacity: 1;
}
</style>