inital commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules/
|
||||
2495
package-lock.json
generated
Normal file
2495
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
package.json
Normal file
19
package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "kitchen-organizer",
|
||||
"version": "1.0.0",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"body-parser": "^2.2.0",
|
||||
"express": "^5.1.0",
|
||||
"socket.io": "^4.8.1",
|
||||
"sqlite3": "^5.1.7"
|
||||
}
|
||||
}
|
||||
BIN
positions.db
Normal file
BIN
positions.db
Normal file
Binary file not shown.
97
public/index.html
Normal file
97
public/index.html
Normal file
@@ -0,0 +1,97 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||
<title>Cuberoo</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="logo">
|
||||
<img src="logo.png" alt="Logo" /> <!-- put it in public -->
|
||||
<span>Cuberoo</span>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container">
|
||||
<!-- wrapper for the draggable map class -->
|
||||
<div id="mapWrapper" class="map-wrapper">
|
||||
<!-- class for the squares -->
|
||||
<div id="map" class="map"></div>
|
||||
<!-- zoom controls -->
|
||||
<div id="zoomControls">
|
||||
<button id="zoomIn">+</button>
|
||||
<button id="zoomOut">-</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- sidebar with the categories -->
|
||||
<div class="sidebar">
|
||||
<div class="categories">
|
||||
<h4>Ted's Team</h4>
|
||||
<div class="fruit" draggable="true" data-fruit="Brandon Brunson">Brandon Brunson</div>
|
||||
<div class="fruit" draggable="true" data-fruit="Eric Smithson">Eric Smithson</div>
|
||||
<div class="fruit" draggable="true" data-fruit="John Hammer">John Hammer</div>
|
||||
<div class="fruit" draggable="true" data-fruit="Seth Lima">Seth Lima</div>
|
||||
<div class="fruit" draggable="true" data-fruit="Ed Edington">Ed Edington</div>
|
||||
<div class="fruit" draggable="true" data-fruit="Rick Sanchez">Rick Sanchez</div>
|
||||
</div>
|
||||
<div class="categories">
|
||||
<h4>Ariel's Team</h4>
|
||||
<div class="fruit" draggable="true" data-fruit="Jerry Smith">Jerry Smith</div>
|
||||
<div class="fruit" draggable="true" data-fruit="Charles Carmichael">Charles Carmichael</div>
|
||||
<div class="fruit" draggable="true" data-fruit="Michael Westen">Michael Westen</div>
|
||||
<div class="fruit" draggable="true" data-fruit="Shawn Spencer">Shawn Spencer</div>
|
||||
<div class="fruit" draggable="true" data-fruit="Eliot Alderson">Eliot Alderson</div>
|
||||
<div class="fruit" draggable="true" data-fruit="Brian D">Brian D</div>
|
||||
</div>
|
||||
<div class="categories">
|
||||
<h4>Elsa's Team</h4>
|
||||
<div class="fruit" draggable="true" data-fruit="John Dorian">John Dorian</div>
|
||||
<div class="fruit" draggable="true" data-fruit="Harvey Spectre">Harvey Spectre</div>
|
||||
<div class="fruit" draggable="true" data-fruit="Juliet O'Hara">Juliet O'Hara</div>
|
||||
<div class="fruit" draggable="true" data-fruit="Fiona Glenanne">Fiona Glenanne</div>
|
||||
</div>
|
||||
<div class="categories">
|
||||
<h4>Ana's Team</h4>
|
||||
<div class="fruit" draggable="true" data-fruit="Neal Caffrey">Neal Caffrey</div>
|
||||
<div class="fruit" draggable="true" data-fruit="Chuck Bartowski">Chuck Bartowski</div>
|
||||
<div class="fruit" draggable="true" data-fruit="Gus Burton">Gus Burton</div>
|
||||
<div class="fruit" draggable="true" data-fruit="Mike Ross">Mike Ross</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// map layout. put numbers for the square IDs. null will be empty i guess.
|
||||
const mapData = [
|
||||
['1', '2', null, '13', '14'],
|
||||
['3', '4', null, '15', '16'],
|
||||
['5', '6', null, '17', '18'],
|
||||
['7', '8', null, '19', '20'],
|
||||
['9', '10', null, '21', '22'],
|
||||
['11', '12', null, '23', '24']
|
||||
];
|
||||
const mapEl = document.getElementById('map');
|
||||
mapData.forEach(row => {
|
||||
const rowEl = document.createElement('div');
|
||||
rowEl.classList.add('map-row');
|
||||
row.forEach(cell => {
|
||||
if (cell) {
|
||||
const squareEl = document.createElement('div');
|
||||
squareEl.classList.add('square');
|
||||
squareEl.dataset.squareId = cell;
|
||||
rowEl.appendChild(squareEl);
|
||||
} else {
|
||||
const emptyEl = document.createElement('div');
|
||||
emptyEl.classList.add('empty');
|
||||
rowEl.appendChild(emptyEl);
|
||||
}
|
||||
});
|
||||
mapEl.appendChild(rowEl);
|
||||
});
|
||||
</script>
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
375
public/script.js
Normal file
375
public/script.js
Normal file
@@ -0,0 +1,375 @@
|
||||
// for some reason it isn't showing that you are dragging a fruit when dragging
|
||||
// from one cell to another. it actually is dragging but it doesn't show it
|
||||
// this is a helper to create a drag image so it shows the fruit being dragged
|
||||
function createDragImage(el) {
|
||||
const dragImg = el.cloneNode(true);
|
||||
dragImg.style.position = 'absolute';
|
||||
dragImg.style.top = '-1000px';
|
||||
dragImg.style.left = '-1000px';
|
||||
document.body.appendChild(dragImg);
|
||||
return dragImg;
|
||||
}
|
||||
|
||||
// helper to place a fruit div inside a square div
|
||||
function placeFruitInSquare(squareEl, fruitName) {
|
||||
// clear existing content
|
||||
squareEl.innerHTML = '';
|
||||
// record the fruit in the square for tracking
|
||||
squareEl.dataset.fruit = fruitName;
|
||||
// find the template fruit in the palette
|
||||
const template = document.querySelector(`.fruit[data-fruit="${fruitName}"]`);
|
||||
if (!template) return;
|
||||
// clone and drop
|
||||
const clone = template.cloneNode(true);
|
||||
clone.classList.remove('fruit'); // remove extra styling if you like
|
||||
clone.classList.add('dropped-fruit');
|
||||
clone.setAttribute('draggable', true); // allow repositioning if desired
|
||||
|
||||
// add drag start for dragging from a square
|
||||
clone.addEventListener('dragstart', e => {
|
||||
e.dataTransfer.setData('text/plain', fruitName);
|
||||
const dragImg = createDragImage(clone);
|
||||
e.dataTransfer.setDragImage(dragImg, dragImg.offsetWidth / 2, dragImg.offsetHeight / 2);
|
||||
clone.addEventListener('dragend', () => {
|
||||
dragImg.remove();
|
||||
}, { once: true });
|
||||
});
|
||||
|
||||
squareEl.appendChild(clone);
|
||||
|
||||
// add X clear button
|
||||
const clearBtn = document.createElement('button');
|
||||
clearBtn.classList.add('clear-btn');
|
||||
clearBtn.textContent = 'X';
|
||||
clearBtn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation(); // prevent event bubbling
|
||||
squareEl.innerHTML = '';
|
||||
delete squareEl.dataset.fruit;
|
||||
const squareId = squareEl.dataset.squareId;
|
||||
await fetch('/api/positions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ squareId, fruit: '' }),
|
||||
});
|
||||
});
|
||||
squareEl.appendChild(clearBtn);
|
||||
}
|
||||
|
||||
// initialize drag & drop on fruits
|
||||
document.querySelectorAll('.fruit').forEach(el => {
|
||||
el.addEventListener('dragstart', e => {
|
||||
e.dataTransfer.setData('text/plain', el.dataset.fruit);
|
||||
const dragImg = createDragImage(el);
|
||||
e.dataTransfer.setDragImage(dragImg, dragImg.offsetWidth / 2, dragImg.offsetHeight / 2);
|
||||
// cleanup after drag
|
||||
el.addEventListener('dragend', () => {
|
||||
dragImg.remove();
|
||||
}, { once: true });
|
||||
});
|
||||
|
||||
// updated mouseover event
|
||||
el.addEventListener('mouseover', e => {
|
||||
const parentCategory = el.closest('.categories');
|
||||
if (parentCategory) {
|
||||
// Unhighlight all squares for fruits in this category first
|
||||
parentCategory.querySelectorAll('.fruit').forEach(sib => {
|
||||
const sq = document.querySelector(`.square[data-fruit="${sib.dataset.fruit}"]`);
|
||||
if (sq) sq.classList.remove('highlight');
|
||||
});
|
||||
}
|
||||
// Now highlight only the hovered fruit's square
|
||||
const square = document.querySelector(`.square[data-fruit="${el.dataset.fruit}"]`);
|
||||
if (square) square.classList.add('highlight');
|
||||
updateHighlightedBorders();
|
||||
});
|
||||
|
||||
// updated mouseout event
|
||||
el.addEventListener('mouseout', e => {
|
||||
// Remove highlight from the hovered fruit's square
|
||||
const square = document.querySelector(`.square[data-fruit="${el.dataset.fruit}"]`);
|
||||
if (square) square.classList.remove('highlight');
|
||||
// If still inside the category container, restore highlighting to all fruits there
|
||||
const parentCategory = el.closest('.categories');
|
||||
if (parentCategory && parentCategory.contains(e.relatedTarget)) {
|
||||
parentCategory.querySelectorAll('.fruit').forEach(sib => {
|
||||
const sq = document.querySelector(`.square[data-fruit="${sib.dataset.fruit}"]`);
|
||||
if (sq) sq.classList.add('highlight');
|
||||
});
|
||||
}
|
||||
updateHighlightedBorders();
|
||||
});
|
||||
});
|
||||
|
||||
// make each square a drop target
|
||||
document.querySelectorAll('.square').forEach(sq => {
|
||||
sq.addEventListener('dragover', e => e.preventDefault());
|
||||
sq.addEventListener('drop', async e => {
|
||||
e.preventDefault();
|
||||
const fruit = e.dataTransfer.getData('text/plain');
|
||||
|
||||
// check if the fruit is already placed in another square
|
||||
const existingSquare = document.querySelector(`.square[data-fruit="${fruit}"]`);
|
||||
if (existingSquare && existingSquare !== sq) {
|
||||
existingSquare.innerHTML = '';
|
||||
delete existingSquare.dataset.fruit;
|
||||
const existingSquareId = existingSquare.dataset.squareId;
|
||||
await fetch('/api/positions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ squareId: existingSquareId, fruit: '' }),
|
||||
});
|
||||
}
|
||||
|
||||
placeFruitInSquare(sq, fruit);
|
||||
|
||||
// save to server
|
||||
const squareId = sq.dataset.squareId;
|
||||
await fetch('/api/positions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ squareId, fruit }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// on load, fetch saved positions and render
|
||||
window.addEventListener('DOMContentLoaded', async () => {
|
||||
try {
|
||||
const res = await fetch('/api/positions');
|
||||
const mapping = await res.json();
|
||||
for (const [squareId, fruit] of Object.entries(mapping)) {
|
||||
const sq = document.querySelector(
|
||||
`.square[data-square-id="${squareId}"]`
|
||||
);
|
||||
if (sq && fruit) {
|
||||
placeFruitInSquare(sq, fruit);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Could not load saved positions', err);
|
||||
}
|
||||
});
|
||||
|
||||
// this is the stuff that let's you drag the map around and zoom in and out
|
||||
(() => {
|
||||
const mapWrapper = document.getElementById('mapWrapper');
|
||||
const map = document.getElementById('map');
|
||||
let isDragging = false;
|
||||
let startX, startY, startPanX, startPanY;
|
||||
let panX = 0, panY = 0;
|
||||
let currentScale = 1;
|
||||
const minScale = 0.5, maxScale = 3;
|
||||
|
||||
function updateTransform() {
|
||||
map.style.transform = `translate(${panX}px, ${panY}px) scale(${currentScale})`;
|
||||
}
|
||||
|
||||
mapWrapper.addEventListener('mousedown', (e) => {
|
||||
if (e.target.closest('.fruit') || e.target.closest('.dropped-fruit')) return;
|
||||
isDragging = true;
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
startPanX = panX;
|
||||
startPanY = panY;
|
||||
map.style.cursor = 'grabbing';
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!isDragging) return;
|
||||
const deltaX = e.clientX - startX;
|
||||
const deltaY = e.clientY - startY;
|
||||
panX = startPanX + deltaX;
|
||||
panY = startPanY + deltaY;
|
||||
updateTransform();
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
map.style.cursor = 'grab';
|
||||
}
|
||||
});
|
||||
|
||||
// mouse wheel zoom and smooth zoom
|
||||
mapWrapper.addEventListener('wheel', (e) => {
|
||||
e.preventDefault();
|
||||
const sensitivity = 0.001; // adjust sensitivity of zoom
|
||||
// calculate new scale smoothly using exponential curve
|
||||
let newScale = currentScale * Math.exp(-e.deltaY * sensitivity);
|
||||
// clamp to allowed values
|
||||
newScale = Math.min(maxScale, Math.max(minScale, newScale));
|
||||
const zoomFactor = newScale / currentScale;
|
||||
|
||||
// Get the mouse position relative to the mapWrapper
|
||||
const rect = mapWrapper.getBoundingClientRect();
|
||||
const offsetX = e.clientX - rect.left;
|
||||
const offsetY = e.clientY - rect.top;
|
||||
|
||||
// Adjust panX and panY so the zoom is centered on the pointer
|
||||
panX = offsetX - zoomFactor * (offsetX - panX);
|
||||
panY = offsetY - zoomFactor * (offsetY - panY);
|
||||
|
||||
currentScale = newScale;
|
||||
updateTransform();
|
||||
});
|
||||
|
||||
// new function for smooth zoom animation
|
||||
function animateZoom(targetScale, targetPanX, targetPanY, duration = 300) {
|
||||
const startScale = currentScale;
|
||||
const startPanX = panX;
|
||||
const startPanY = panY;
|
||||
const startTime = performance.now();
|
||||
|
||||
function step(now) {
|
||||
const elapsed = now - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1); // progress from 0 to 1 (linear easing)
|
||||
|
||||
currentScale = startScale + (targetScale - startScale) * progress;
|
||||
panX = startPanX + (targetPanX - startPanX) * progress;
|
||||
panY = startPanY + (targetPanY - startPanY) * progress;
|
||||
updateTransform();
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(step);
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
// zoom button handlers using smooth animation
|
||||
const zoomInBtn = document.getElementById('zoomIn');
|
||||
const zoomOutBtn = document.getElementById('zoomOut');
|
||||
|
||||
const zoomButtonHandler = (zoomFactor) => {
|
||||
let targetScale = currentScale * zoomFactor;
|
||||
targetScale = Math.min(maxScale, Math.max(minScale, targetScale));
|
||||
// calculate the actual zoom factor applied.
|
||||
const actualFactor = targetScale / currentScale;
|
||||
|
||||
// use the center of the mapWrapper as the zoom origin.
|
||||
const rect = mapWrapper.getBoundingClientRect();
|
||||
const centerX = rect.width / 2;
|
||||
const centerY = rect.height / 2;
|
||||
const targetPanX = centerX - actualFactor * (centerX - panX);
|
||||
const targetPanY = centerY - actualFactor * (centerY - panY);
|
||||
|
||||
animateZoom(targetScale, targetPanX, targetPanY, 300); // change this for the zoom animation time
|
||||
};
|
||||
|
||||
zoomInBtn.addEventListener('click', () => {
|
||||
zoomButtonHandler(1.2);
|
||||
});
|
||||
|
||||
zoomOutBtn.addEventListener('click', () => {
|
||||
zoomButtonHandler(0.8);
|
||||
});
|
||||
})();
|
||||
|
||||
// add highlighting for squares based on category hover
|
||||
document.querySelectorAll('.categories').forEach(category => {
|
||||
category.addEventListener('mouseenter', () => {
|
||||
category.querySelectorAll('.fruit').forEach(fruitEl => {
|
||||
const fruitName = fruitEl.dataset.fruit;
|
||||
const square = document.querySelector(`.square[data-fruit="${fruitName}"]`);
|
||||
if (square) {
|
||||
square.classList.add('highlight');
|
||||
}
|
||||
});
|
||||
updateHighlightedBorders();
|
||||
});
|
||||
category.addEventListener('mouseleave', () => {
|
||||
category.querySelectorAll('.fruit').forEach(fruitEl => {
|
||||
const fruitName = fruitEl.dataset.fruit;
|
||||
const square = document.querySelector(`.square[data-fruit="${fruitName}"]`);
|
||||
if (square) {
|
||||
square.classList.remove('highlight');
|
||||
}
|
||||
});
|
||||
updateHighlightedBorders();
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
this is a helper to update highlighted square borders.
|
||||
for each highlighted square, if a neighboring square is also highlighted,
|
||||
set the border color on that touching side to transparent.
|
||||
this makes it look like the squares are connected.
|
||||
*/
|
||||
function updateHighlightedBorders() {
|
||||
const squares = document.querySelectorAll('.square');
|
||||
squares.forEach(sq => {
|
||||
// Reset any inline style if not highlighted
|
||||
if (!sq.classList.contains('highlight')) {
|
||||
sq.style.borderLeftColor = '';
|
||||
sq.style.borderRightColor = '';
|
||||
sq.style.borderTopColor = '';
|
||||
sq.style.borderBottomColor = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// set all borders to the highlight color. change this to change the color
|
||||
sq.style.borderLeftColor = '#d103f9';
|
||||
sq.style.borderRightColor = '#d103f9';
|
||||
sq.style.borderTopColor = '#d103f9';
|
||||
sq.style.borderBottomColor = '#d103f9';
|
||||
|
||||
// find the parent row and index
|
||||
const parentRow = sq.parentElement; // .map-row
|
||||
const cells = Array.from(parentRow.children);
|
||||
const idx = cells.indexOf(sq);
|
||||
|
||||
// check left neighbor in the same row
|
||||
if (idx > 0 && cells[idx - 1].classList.contains('square') && cells[idx - 1].classList.contains('highlight')) {
|
||||
sq.style.borderLeftColor = 'transparent';
|
||||
}
|
||||
// check right neighbor in the same row
|
||||
if (idx < cells.length - 1 && cells[idx + 1].classList.contains('square') && cells[idx + 1].classList.contains('highlight')) {
|
||||
sq.style.borderRightColor = 'transparent';
|
||||
}
|
||||
|
||||
// for top and bottom, get the row index from map (#map container)
|
||||
const map = parentRow.parentElement;
|
||||
const rows = Array.from(map.children);
|
||||
const rowIndex = rows.indexOf(parentRow);
|
||||
// check top neighbor (like same cell index in previous row)
|
||||
if (rowIndex > 0) {
|
||||
const topRow = rows[rowIndex - 1];
|
||||
const topCells = Array.from(topRow.children);
|
||||
if (topCells[idx] && topCells[idx].classList.contains('square') && topCells[idx].classList.contains('highlight')) {
|
||||
sq.style.borderTopColor = 'transparent';
|
||||
}
|
||||
}
|
||||
// check bottom neighbor (like same cell index in next row)
|
||||
if (rowIndex < rows.length - 1) {
|
||||
const bottomRow = rows[rowIndex + 1];
|
||||
const bottomCells = Array.from(bottomRow.children);
|
||||
if (bottomCells[idx] && bottomCells[idx].classList.contains('square') && bottomCells[idx].classList.contains('highlight')) {
|
||||
sq.style.borderBottomColor = 'transparent';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// add Socket.io client side for real time updates
|
||||
const socket = io();
|
||||
|
||||
socket.on('update', data => {
|
||||
// find the square with the given squareId
|
||||
const sq = document.querySelector(`.square[data-square-id="${data.squareId}"]`);
|
||||
if (sq) {
|
||||
if (data.fruit) {
|
||||
if (sq.dataset.fruit !== data.fruit) {
|
||||
placeFruitInSquare(sq, data.fruit);
|
||||
}
|
||||
} else {
|
||||
sq.innerHTML = '';
|
||||
delete sq.dataset.fruit;
|
||||
}
|
||||
// add highlight and remove after 3 seconds
|
||||
sq.classList.add('highlight-update');
|
||||
setTimeout(() => {
|
||||
sq.classList.remove('highlight-update');
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
206
public/style.css
Normal file
206
public/style.css
Normal file
@@ -0,0 +1,206 @@
|
||||
/* fullscreen flex container */
|
||||
body, html {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
overflow: hidden; /* disable scrollbar */
|
||||
background: black;
|
||||
color: rgb(166, 166, 166); /* text color */
|
||||
}
|
||||
.container {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
font-family: sans-serif;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* sidebar with categories */
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
margin-left: 15px;
|
||||
height: 100vh; /* take up entire vertical height */
|
||||
background: rgb(19, 19, 19);
|
||||
overflow-y: auto; /* make it scrollable */
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Map layout */
|
||||
.map-wrapper {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
border: 0px solid white; /* disbale map border */
|
||||
position: relative;
|
||||
margin: 0; /* margins for map (disbaled) */
|
||||
}
|
||||
|
||||
#map {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: grab;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.map {
|
||||
display: table;
|
||||
margin-right: 30px;
|
||||
}
|
||||
.map-row {
|
||||
display: table-row;
|
||||
}
|
||||
.map-row > .square,
|
||||
.map-row > .empty {
|
||||
display: table-cell;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* what the squares look like */
|
||||
.square {
|
||||
position: relative;
|
||||
border: 1px dashed white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 1.2rem;
|
||||
transition: border-color 0.5s ease, box-shadow 0.5s ease
|
||||
}
|
||||
|
||||
.square.highlight {
|
||||
border: 1px solid purple; /* highlight color when hovered over in sidebar */
|
||||
}
|
||||
|
||||
.square.highlight-update {
|
||||
border: 1px solid purple;
|
||||
box-shadow: 0 0 10px 2px purple;
|
||||
}
|
||||
|
||||
/* the x button to clear that square (temp, make it look better later) */
|
||||
.clear-btn {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
background: red;
|
||||
border: none;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.square:hover .clear-btn {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* empty for when it's null */
|
||||
.empty {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* for the categories */
|
||||
.categories {
|
||||
/* make sidebar wider */
|
||||
padding: 10px;
|
||||
border: 1px solid white; /* sidebar border color */
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.fruit {
|
||||
cursor: grab;
|
||||
margin-bottom: 10px;
|
||||
padding: 5px 10px;
|
||||
border: 1px solid white; /* cells inside sidebar border color */
|
||||
border-radius: 4px;
|
||||
user-select: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.fruit:active {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* what the titles of the categories */
|
||||
.categories h4 {
|
||||
text-align: center;
|
||||
margin-top: 5px; /* change this to make the titles closer to the top of the cell */
|
||||
margin-bottom: 10px;
|
||||
padding: 0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* "cuberoo" logo and text at top */
|
||||
.header {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.logo span {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: rgb(191, 93, 245); /* color of logo text */
|
||||
}
|
||||
|
||||
#zoomControls {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0px;
|
||||
z-index: 100; /* this makes it appear on top of the map*/
|
||||
}
|
||||
|
||||
#zoomControls button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 1.5rem;
|
||||
background: rgb(0, 0, 0);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#zoomControls button:hover {
|
||||
background: rgb(0, 0, 0);
|
||||
}
|
||||
|
||||
.dropped-fruit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
70
server.js
Normal file
70
server.js
Normal file
@@ -0,0 +1,70 @@
|
||||
// server.js
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require('path');
|
||||
const http = require('http');
|
||||
const { Server } = require('socket.io');
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const io = new Server(server);
|
||||
|
||||
app.use(bodyParser.json());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// open (or create) the SQLite database file
|
||||
const db = new sqlite3.Database('positions.db', err => {
|
||||
if (err) {
|
||||
console.error('Could not open DB', err);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// create table if not exists
|
||||
db.run(
|
||||
`CREATE TABLE IF NOT EXISTS positions (
|
||||
square_id TEXT PRIMARY KEY,
|
||||
fruit TEXT
|
||||
)`,
|
||||
(err) => {
|
||||
if (err) console.error('Could not ensure table', err);
|
||||
}
|
||||
);
|
||||
|
||||
// get all saved positions
|
||||
app.get('/api/positions', (req, res) => {
|
||||
db.all('SELECT square_id, fruit FROM positions', (err, rows) => {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
// convert to an object. should look like this: { "1": "Apple", "2": "Banana", … }
|
||||
const mapping = {};
|
||||
rows.forEach(r => (mapping[r.square_id] = r.fruit));
|
||||
res.json(mapping);
|
||||
});
|
||||
});
|
||||
|
||||
// save (or update) a single square’s item
|
||||
app.post('/api/positions', (req, res) => {
|
||||
const { squareId, fruit } = req.body;
|
||||
if (!squareId || typeof fruit !== 'string') {
|
||||
return res.status(400).json({ error: 'squareId and fruit required' });
|
||||
}
|
||||
db.run(
|
||||
`INSERT INTO positions (square_id, fruit)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT(square_id) DO UPDATE SET fruit=excluded.fruit`,
|
||||
[squareId, fruit],
|
||||
function (err) {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
// broadcast update via Socket.io
|
||||
io.emit('update', { squareId, fruit });
|
||||
res.json({ success: true });
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// start server
|
||||
const PORT = process.env.PORT || 3000;
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Listening on http://localhost:${PORT}`);
|
||||
});
|
||||
Reference in New Issue
Block a user