This commit is contained in:
229
backend/package-lock.json
generated
229
backend/package-lock.json
generated
@@ -13,7 +13,8 @@
|
|||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"mongoose": "^9.3.0",
|
"mongoose": "^9.3.0",
|
||||||
"nodemon": "^3.1.14"
|
"nodemon": "^3.1.14",
|
||||||
|
"socket.io": "^4.8.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mongodb-js/saslprep": {
|
"node_modules/@mongodb-js/saslprep": {
|
||||||
@@ -25,6 +26,30 @@
|
|||||||
"sparse-bitfield": "^3.0.3"
|
"sparse-bitfield": "^3.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@socket.io/component-emitter": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/cors": {
|
||||||
|
"version": "2.8.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
|
||||||
|
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "25.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz",
|
||||||
|
"integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": ">=7.24.0 <7.24.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/webidl-conversions": {
|
"node_modules/@types/webidl-conversions": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
|
||||||
@@ -40,6 +65,15 @@
|
|||||||
"@types/webidl-conversions": "*"
|
"@types/webidl-conversions": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
|
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||||
@@ -75,6 +109,15 @@
|
|||||||
"node": "18 || 20 || >=22"
|
"node": "18 || 20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/base64id": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^4.5.0 || >= 5.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
@@ -329,6 +372,79 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/engine.io": {
|
||||||
|
"version": "6.6.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.8.tgz",
|
||||||
|
"integrity": "sha512-2agL3ueZhqxoVrfmntO8yuVj+uNSlIOnhykYHk3Cq0ShYPdUjjUiSJrQvXjq01I9jAuI0Zl2YO8Evv5Mqytm5g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/cors": "^2.8.12",
|
||||||
|
"@types/node": ">=10.0.0",
|
||||||
|
"@types/ws": "^8.5.12",
|
||||||
|
"accepts": "~1.3.4",
|
||||||
|
"base64id": "2.0.0",
|
||||||
|
"cookie": "~0.7.2",
|
||||||
|
"cors": "~2.8.5",
|
||||||
|
"debug": "~4.4.1",
|
||||||
|
"engine.io-parser": "~5.2.1",
|
||||||
|
"ws": "~8.20.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-parser": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io/node_modules/accepts": {
|
||||||
|
"version": "1.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
|
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-types": "~2.1.34",
|
||||||
|
"negotiator": "0.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io/node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io/node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io/node_modules/negotiator": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-define-property": {
|
"node_modules/es-define-property": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
@@ -1251,6 +1367,90 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/socket.io": {
|
||||||
|
"version": "4.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz",
|
||||||
|
"integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"accepts": "~1.3.4",
|
||||||
|
"base64id": "~2.0.0",
|
||||||
|
"cors": "~2.8.5",
|
||||||
|
"debug": "~4.4.1",
|
||||||
|
"engine.io": "~6.6.0",
|
||||||
|
"socket.io-adapter": "~2.5.2",
|
||||||
|
"socket.io-parser": "~4.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-adapter": {
|
||||||
|
"version": "2.5.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.7.tgz",
|
||||||
|
"integrity": "sha512-e0LyK91f3cUxTmv95/KzoLg47+zF+s/sbxRGDNsyG4dmIP8ZSX8ax6byOxfJXeNNtS/8AZlfD+uP7gBeR7DLlg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "~4.4.1",
|
||||||
|
"ws": "~8.20.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser": {
|
||||||
|
"version": "4.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz",
|
||||||
|
"integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.4.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io/node_modules/accepts": {
|
||||||
|
"version": "1.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
|
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-types": "~2.1.34",
|
||||||
|
"negotiator": "0.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io/node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io/node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io/node_modules/negotiator": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sparse-bitfield": {
|
"node_modules/sparse-bitfield": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
|
||||||
@@ -1343,6 +1543,12 @@
|
|||||||
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
|
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "7.24.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
|
||||||
|
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/unpipe": {
|
"node_modules/unpipe": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
@@ -1388,6 +1594,27 @@
|
|||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.20.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
|
||||||
|
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"mongoose": "^9.3.0",
|
"mongoose": "^9.3.0",
|
||||||
"nodemon": "^3.1.14"
|
"nodemon": "^3.1.14",
|
||||||
|
"socket.io": "^4.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,61 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const cors = require('cors');
|
const cors = require("cors");
|
||||||
const mongoose = require('mongoose');
|
const mongoose = require("mongoose");
|
||||||
|
const { createServer } = require("http");
|
||||||
|
const { Server } = require("socket.io");
|
||||||
|
|
||||||
const dotenv = require('dotenv');
|
const dotenv = require("dotenv");
|
||||||
|
|
||||||
if(process.env.NODE_ENV) {
|
if (process.env.NODE_ENV) {
|
||||||
dotenv.config({
|
dotenv.config({
|
||||||
path: `.env.${process.env.NODE_ENV}`
|
path: `.env.${process.env.NODE_ENV}`,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
const httpServer = createServer(app);
|
||||||
|
const io = new Server(httpServer, {
|
||||||
|
cors: {
|
||||||
|
origin: true,
|
||||||
|
credentials: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const connectDB = require("./db");
|
const connectDB = require("./db");
|
||||||
|
const gridCellsRouter = require("./routes/gridCells");
|
||||||
|
|
||||||
|
gridCellsRouter.setIO(io);
|
||||||
|
|
||||||
// connect database
|
// connect database
|
||||||
connectDB();
|
connectDB();
|
||||||
|
|
||||||
app.use(cors({
|
app.use(
|
||||||
origin: 'http://localhost:3000',
|
cors({
|
||||||
credentials: true, // if using cookies/auth
|
origin: true,
|
||||||
}));
|
credentials: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Socket.IO
|
||||||
|
io.on("connection", (socket) => {
|
||||||
|
console.log(`Socket connected: ${socket.id}`);
|
||||||
|
|
||||||
|
socket.on("disconnect", () => {
|
||||||
|
console.log(`Socket disconnected: ${socket.id}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
app.get("/api/test", (req, res) => {
|
app.get("/api/test", (req, res) => {
|
||||||
console.log("Hey");
|
console.log("Hey");
|
||||||
res.json({"message": "Hello from backend!"});
|
res.json({ message: "Hello from backend!" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.use("/api/grid-cells", gridCellsRouter);
|
||||||
|
|
||||||
app.get("/api/status", (req, res) => {
|
app.get("/api/status", (req, res) => {
|
||||||
const mem = process.memoryUsage();
|
const mem = process.memoryUsage();
|
||||||
const uptime = Math.floor(process.uptime());
|
const uptime = Math.floor(process.uptime());
|
||||||
@@ -55,6 +83,7 @@ app.get("/api/status", (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.listen(5000, () => {
|
const PORT = process.env.PORT || 5000;
|
||||||
|
httpServer.listen(PORT, () => {
|
||||||
console.log("Server running on port 5000");
|
console.log("Server running on port 5000");
|
||||||
});
|
});
|
||||||
67
backend/src/routes/gridCells.js
Normal file
67
backend/src/routes/gridCells.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const GridCell = require("../schemas/GridCell");
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
let io;
|
||||||
|
router.setIO = (_io) => {
|
||||||
|
io = _io;
|
||||||
|
};
|
||||||
|
|
||||||
|
// GET /api/grid-cells
|
||||||
|
router.get("/", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { minx, miny, maxx, maxy } = req.query;
|
||||||
|
if (minx != null && miny != null && maxx != null && maxy != null) {
|
||||||
|
const query = {};
|
||||||
|
if (minx !== "" && !isNaN(parseInt(minx))) query.x = { $gte: parseInt(minx) };
|
||||||
|
if (miny !== "" && !isNaN(parseInt(miny))) query.y = { $gte: parseInt(miny) };
|
||||||
|
if (maxx !== "" && !isNaN(parseInt(maxx))) {
|
||||||
|
query.x = query.x || {};
|
||||||
|
query.x.$lte = parseInt(maxx);
|
||||||
|
}
|
||||||
|
if (maxy !== "" && !isNaN(parseInt(maxy))) {
|
||||||
|
query.y = query.y || {};
|
||||||
|
query.y.$lte = parseInt(maxy);
|
||||||
|
}
|
||||||
|
const cells = await GridCell.find(query)
|
||||||
|
.sort({ x: 1, y: 1 })
|
||||||
|
.limit(10000)
|
||||||
|
.lean();
|
||||||
|
return res.json(cells);
|
||||||
|
}
|
||||||
|
const limit = Math.min(parseInt(req.query.limit) || 1000, 1000);
|
||||||
|
const cells = await GridCell.find()
|
||||||
|
.sort({ x: 1, y: 1 })
|
||||||
|
.limit(limit)
|
||||||
|
.lean();
|
||||||
|
res.json(cells);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).json({ error: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/grid-cells/paint — upsert (create or update a grid cell)
|
||||||
|
router.post("/paint", async (req, res) => {
|
||||||
|
console.log("Hola")
|
||||||
|
try {
|
||||||
|
const { x, y, color } = req.body;
|
||||||
|
if (x == null || y == null || color == null)
|
||||||
|
return res.status(400).json({ error: "x, y, and color are required" });
|
||||||
|
const validated = await GridCell.findOneAndUpdate(
|
||||||
|
{ x, y },
|
||||||
|
{ x, y, color },
|
||||||
|
{ returnDocument: 'after', upsert: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (io) {
|
||||||
|
io.emit("grid-cell-paint", { x, y, color });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(validated);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).json({ error: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
21
backend/src/schemas/GridCell.js
Normal file
21
backend/src/schemas/GridCell.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const mongoose = require("mongoose");
|
||||||
|
|
||||||
|
const gridCellSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
x: { type: Number, required: true, min: 1, max: 90 },
|
||||||
|
y: { type: Number, required: true, min: 1, max: 90},
|
||||||
|
color: { type: Number, required: true },
|
||||||
|
},
|
||||||
|
{ timestamps: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
gridCellSchema.index({ x: 1, y: 1 }, { unique: true });
|
||||||
|
|
||||||
|
gridCellSchema.methods.toDisplayColor = function () {
|
||||||
|
if (this.color.startsWith("#")) return this.color;
|
||||||
|
return `#${this.color}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const GridCell = mongoose.model("GridCell", gridCellSchema);
|
||||||
|
|
||||||
|
module.exports = GridCell;
|
||||||
@@ -260,8 +260,8 @@ onMounted(() => {
|
|||||||
canvas.value.style.height = `${h}px`
|
canvas.value.style.height = `${h}px`
|
||||||
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
||||||
ctx.scale(dpr, dpr)
|
ctx.scale(dpr, dpr)
|
||||||
particles = createParticles(w, h)
|
// particles = createParticles(w, h)
|
||||||
bgParticles = createBgParticles(w, h)
|
// bgParticles = createBgParticles(w, h)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (ctx && canvas.value) {
|
if (ctx && canvas.value) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const { get } = api();
|
const { get } = useApi();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const status = ref<{
|
const status = ref<{
|
||||||
@@ -27,31 +27,20 @@ const online = computed(() => status.value?.mongo === 'connected');
|
|||||||
<template>
|
<template>
|
||||||
<div class="server-status-card">
|
<div class="server-status-card">
|
||||||
<div class="monitor-bar">
|
<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-if="loading" class="monitor-blink">▚</span>
|
||||||
<span v-else class="monitor-dot" :class="online ? 'green' : 'red'"></span>
|
<span v-else class="monitor-dot" :class="online ? 'green' : 'red'"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="monitor-screen">
|
<div class="monitor-screen">
|
||||||
<div class="screen-grid">
|
<div class="screen-grid">
|
||||||
|
<div class="screen-line">
|
||||||
|
<span>Everything OK!</span>
|
||||||
|
</div>
|
||||||
<div class="screen-line">
|
<div class="screen-line">
|
||||||
<span class="screen-key">UPTIME</span>
|
<span class="screen-key">UPTIME</span>
|
||||||
<span class="screen-val">{{ loading ? '.......' : status?.uptime || '--' }}</span>
|
<span class="screen-val">{{ loading ? '.......' : status?.uptime || '--' }}</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,7 +66,6 @@ const online = computed(() => status.value?.mongo === 'connected');
|
|||||||
background: #0e0e0e;
|
background: #0e0e0e;
|
||||||
color: #d4d4d4;
|
color: #d4d4d4;
|
||||||
font-family: 'Hurmit', monospace;
|
font-family: 'Hurmit', monospace;
|
||||||
font-size: 0.5rem;
|
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
border-bottom: 2px solid #3a3a3a;
|
border-bottom: 2px solid #3a3a3a;
|
||||||
@@ -128,7 +116,6 @@ const online = computed(() => status.value?.mongo === 'connected');
|
|||||||
|
|
||||||
.screen-key {
|
.screen-key {
|
||||||
font-family: 'Hurmit', monospace;
|
font-family: 'Hurmit', monospace;
|
||||||
font-size: 0.55rem;
|
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
@@ -137,7 +124,6 @@ const online = computed(() => status.value?.mongo === 'connected');
|
|||||||
|
|
||||||
.screen-val {
|
.screen-val {
|
||||||
font-family: 'Hurmit', monospace;
|
font-family: 'Hurmit', monospace;
|
||||||
font-size: 0.65rem;
|
|
||||||
color: var(--color-link);
|
color: var(--color-link);
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
|
|||||||
237
frontend/app/components/widgets/Place.vue
Normal file
237
frontend/app/components/widgets/Place.vue
Normal 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>
|
||||||
63
frontend/app/composables/useSocket.ts
Normal file
63
frontend/app/composables/useSocket.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { ref, onUnmounted } from 'vue'
|
||||||
|
import { io } from 'socket.io-client'
|
||||||
|
|
||||||
|
export default function useSocket() {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const socketUrl = `${config.public.apiBaseUrl.replace('/api', '')}`
|
||||||
|
|
||||||
|
const socket = ref(null)
|
||||||
|
const isConnected = ref(false)
|
||||||
|
|
||||||
|
function initSocket() {
|
||||||
|
if (socket.value) return
|
||||||
|
|
||||||
|
const s = io(socketUrl, {
|
||||||
|
transports: ['websocket', 'polling'],
|
||||||
|
reconnection: true,
|
||||||
|
reconnectionAttempts: 10,
|
||||||
|
reconnectionDelay: 1000,
|
||||||
|
})
|
||||||
|
|
||||||
|
s.on('connect', () => {
|
||||||
|
console.log('[socket.io] connected')
|
||||||
|
isConnected.value = true
|
||||||
|
})
|
||||||
|
|
||||||
|
s.on('disconnect', () => {
|
||||||
|
console.log('[socket.io] disconnected')
|
||||||
|
isConnected.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
s.on('connect_error', (err) => {
|
||||||
|
console.error('[socket.io] connection error:', err.message)
|
||||||
|
isConnected.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.value = s
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGridCellPaint(callback) {
|
||||||
|
if (!socket.value) initSocket()
|
||||||
|
socket.value.on('grid-cell-paint', callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeGridCellPaintListener() {
|
||||||
|
if (socket.value) {
|
||||||
|
socket.value.off('grid-cell-paint')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (socket.value) {
|
||||||
|
socket.value.disconnect()
|
||||||
|
socket.value = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
isConnected,
|
||||||
|
initSocket,
|
||||||
|
onGridCellPaint,
|
||||||
|
removeGridCellPaintListener,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import TableHeader from '~/components/parts/TableHeader.vue';
|
|||||||
import FixedLayout from '~/components/layouts/FixedLayout.vue';
|
import FixedLayout from '~/components/layouts/FixedLayout.vue';
|
||||||
import { useSeo } from '~/composables/seo';
|
import { useSeo } from '~/composables/seo';
|
||||||
|
|
||||||
const { get, post } = api();
|
const { get, post } = useApi();
|
||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
|
|
||||||
useSeo({
|
useSeo({
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
import FixedLayout from '~/components/layouts/FixedLayout.vue';
|
import FixedLayout from '~/components/layouts/FixedLayout.vue';
|
||||||
import TableHeader from '~/components/parts/TableHeader.vue';
|
import TableHeader from '~/components/parts/TableHeader.vue';
|
||||||
import ServerStatus from '~/components/parts/ServerStatus.vue';
|
import ServerStatus from '~/components/parts/ServerStatus.vue';
|
||||||
import api from '~/composables/api'
|
|
||||||
import { useSeo } from '~/composables/seo'
|
import { useSeo } from '~/composables/seo'
|
||||||
|
|
||||||
const { get, post } = api();
|
import Place from '~/components/widgets/Place.vue';
|
||||||
|
|
||||||
|
const { get, post } = useApi();
|
||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -180,7 +182,14 @@ const sectionTargets = {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="projects-section" id="scroll-projects" v-if="projects && projects.length > 0">
|
<section class="normal-section" id="place">
|
||||||
|
<Container>
|
||||||
|
<h2 class="section-title">Place things!</h2>
|
||||||
|
<Place></Place>
|
||||||
|
</Container>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="normal-section projects-section" id="scroll-projects" v-if="projects && projects.length > 0">
|
||||||
<Container>
|
<Container>
|
||||||
<h2 class="section-title">{{ t('pages.projects_heading') }}</h2>
|
<h2 class="section-title">{{ t('pages.projects_heading') }}</h2>
|
||||||
|
|
||||||
@@ -217,101 +226,7 @@ const sectionTargets = {
|
|||||||
</Container>
|
</Container>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!--
|
<section class="normal-section" id="scroll-blog">
|
||||||
<section class="stats-section" id="scroll-stats">
|
|
||||||
<Container>
|
|
||||||
<h2 class="section-title">{{ t('pages.stats_heading') }}</h2>
|
|
||||||
<div class="stats-grid">
|
|
||||||
<div class="stat-card">
|
|
||||||
<span class="stat-card-corner tl"></span>
|
|
||||||
<span class="stat-card-corner tr"></span>
|
|
||||||
<span class="stat-card-corner bl"></span>
|
|
||||||
<span class="stat-card-corner br"></span>
|
|
||||||
<span class="stat-card-frame-top"></span>
|
|
||||||
<span class="stat-card-frame-bottom"></span>
|
|
||||||
<div class="stat-card-header">CODE</div>
|
|
||||||
<div class="stat-card-content">
|
|
||||||
<svg class="stat-pixel-art" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 3h2v2H3V3zm4 0h2v2H7V3zm4 0h2v2h-2V3zM3 7h2v2H3V7zm6 0h2v2H9V7zm4 0h2v2h-2V7zM3 11h2v2H3v-2zm4 0h2v2H7v-2zm4 0h2v2h-2v-2z"/></svg>
|
|
||||||
<span class="stat-number">50K+</span>
|
|
||||||
<span class="stat-label">{{ t('pages.stat_lines_of_code') }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<span class="stat-card-corner tl"></span>
|
|
||||||
<span class="stat-card-corner tr"></span>
|
|
||||||
<span class="stat-card-corner bl"></span>
|
|
||||||
<span class="stat-card-corner br"></span>
|
|
||||||
<span class="stat-card-frame-top"></span>
|
|
||||||
<span class="stat-card-frame-bottom"></span>
|
|
||||||
<div class="stat-card-header">WORK</div>
|
|
||||||
<div class="stat-card-content">
|
|
||||||
<svg class="stat-pixel-art" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 3h8v2H6v2h6v2H8v2h4v2H4V3z"/></svg>
|
|
||||||
<span class="stat-number">12+</span>
|
|
||||||
<span class="stat-label">{{ t('pages.stat_projects') }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<span class="stat-card-corner tl"></span>
|
|
||||||
<span class="stat-card-corner tr"></span>
|
|
||||||
<span class="stat-card-corner bl"></span>
|
|
||||||
<span class="stat-card-corner br"></span>
|
|
||||||
<span class="stat-card-frame-top"></span>
|
|
||||||
<span class="stat-card-frame-bottom"></span>
|
|
||||||
<div class="stat-card-header">CAFFE</div>
|
|
||||||
<div class="stat-card-content">
|
|
||||||
<svg class="stat-pixel-art" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 5h8v2H4V5zm-2 2h2v1H2V7zm10 0h2v1h-2V7zM3 9h1v3h1v-1h3v1h1V9h1v3h1v-1h2v1h1v-4H3z"/></svg>
|
|
||||||
<span class="stat-number">∞</span>
|
|
||||||
<span class="stat-label">{{ t('pages.stat_coffee') }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<span class="stat-card-corner tl"></span>
|
|
||||||
<span class="stat-card-corner tr"></span>
|
|
||||||
<span class="stat-card-corner bl"></span>
|
|
||||||
<span class="stat-card-corner br"></span>
|
|
||||||
<span class="stat-card-frame-top"></span>
|
|
||||||
<span class="stat-card-frame-bottom"></span>
|
|
||||||
<div class="stat-card-header">PLAY</div>
|
|
||||||
<div class="stat-card-content">
|
|
||||||
<svg class="stat-pixel-art" viewBox="0 0 16 16" aria-hidden="true"><path d="M2 4h3v2H3v1h2v2H8v-2h2V6h-1V4h3v2h-2v3H5V6H4V4zm7 2h2v2h-2V6z"/></svg>
|
|
||||||
<span class="stat-number">28</span>
|
|
||||||
<span class="stat-label">{{ t('pages.stat_board_games') }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<span class="stat-card-corner tl"></span>
|
|
||||||
<span class="stat-card-corner tr"></span>
|
|
||||||
<span class="stat-card-corner bl"></span>
|
|
||||||
<span class="stat-card-corner br"></span>
|
|
||||||
<span class="stat-card-frame-top"></span>
|
|
||||||
<span class="stat-card-frame-bottom"></span>
|
|
||||||
<div class="stat-card-header">TIME</div>
|
|
||||||
<div class="stat-card-content">
|
|
||||||
<svg class="stat-pixel-art" viewBox="0 0 16 16" aria-hidden="true"><path d="M5 2h6v2H5V2zm-2 4h10v1H3V6zm1 2h8v1H4V8zm2 2h4v1H6v-1zM6 2h4v12H6V2z"/></svg>
|
|
||||||
<span class="stat-number">8+</span>
|
|
||||||
<span class="stat-label">{{ t('pages.stat_years_programming') }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<span class="stat-card-corner tl"></span>
|
|
||||||
<span class="stat-card-corner tr"></span>
|
|
||||||
<span class="stat-card-corner bl"></span>
|
|
||||||
<span class="stat-card-corner br"></span>
|
|
||||||
<span class="stat-card-frame-top"></span>
|
|
||||||
<span class="stat-card-frame-bottom"></span>
|
|
||||||
<div class="stat-card-header">DEPLOY</div>
|
|
||||||
<div class="stat-card-content">
|
|
||||||
<svg class="stat-pixel-art" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 3h8v2H9v2h2v2h-2v2h-2v2h6v2H2v-2h2v-2H2V7h2V5H2V3z"/></svg>
|
|
||||||
<span class="stat-number">150+</span>
|
|
||||||
<span class="stat-label">{{ t('pages.stat_deployments') }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
</section>
|
|
||||||
-->
|
|
||||||
|
|
||||||
<section class="blog-section" id="scroll-blog">
|
|
||||||
<Container>
|
<Container>
|
||||||
<h2 class="section-title">{{ t('pages.blog_heading') }}</h2>
|
<h2 class="section-title">{{ t('pages.blog_heading') }}</h2>
|
||||||
<ul class="tui-list">
|
<ul class="tui-list">
|
||||||
@@ -328,7 +243,7 @@ const sectionTargets = {
|
|||||||
</Container>
|
</Container>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="art-section" id="scroll-art">
|
<section class="normal-section" id="scroll-art">
|
||||||
<Container>
|
<Container>
|
||||||
<h2 class="section-title">{{ t('pages.art_heading') }}</h2>
|
<h2 class="section-title">{{ t('pages.art_heading') }}</h2>
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
@@ -351,7 +266,7 @@ const sectionTargets = {
|
|||||||
</Container>
|
</Container>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="contact-section" id="scroll-contact">
|
<section class="normal-section" id="scroll-contact">
|
||||||
<Container>
|
<Container>
|
||||||
<h2 class="section-title">{{ t('pages.contact_heading') }}</h2>
|
<h2 class="section-title">{{ t('pages.contact_heading') }}</h2>
|
||||||
<ContentRenderer v-if="contactMarkdown" :value="contactMarkdown"></ContentRenderer>
|
<ContentRenderer v-if="contactMarkdown" :value="contactMarkdown"></ContentRenderer>
|
||||||
@@ -422,12 +337,8 @@ const sectionTargets = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.projects-section,
|
.normal-section {
|
||||||
.stats-section,
|
margin-top: 24px;
|
||||||
.blog-section,
|
|
||||||
.contact-section,
|
|
||||||
.art-section {
|
|
||||||
margin-top: 16px;
|
|
||||||
|
|
||||||
.tui-frame:last-child {
|
.tui-frame:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
@@ -713,11 +624,6 @@ const sectionTargets = {
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Blog section */
|
|
||||||
.blog-section {
|
|
||||||
margin-top: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tui-list {
|
.tui-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -779,16 +685,6 @@ const sectionTargets = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Contact section */
|
|
||||||
.contact-section {
|
|
||||||
margin-top: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Art section */
|
|
||||||
.art-section {
|
|
||||||
margin-top: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(250px, 1fr));
|
grid-template-columns: repeat(3, minmax(250px, 1fr));
|
||||||
|
|||||||
1
frontend/package-lock.json
generated
1
frontend/package-lock.json
generated
@@ -13,6 +13,7 @@
|
|||||||
"better-sqlite3": "^12.10.0",
|
"better-sqlite3": "^12.10.0",
|
||||||
"nuxt": "^4.3.1",
|
"nuxt": "^4.3.1",
|
||||||
"sass": "^1.98.0",
|
"sass": "^1.98.0",
|
||||||
|
"socket.io-client": "^4.8.3",
|
||||||
"vue": "^3.5.30",
|
"vue": "^3.5.30",
|
||||||
"vue-router": "^4.6.4"
|
"vue-router": "^4.6.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"better-sqlite3": "^12.10.0",
|
"better-sqlite3": "^12.10.0",
|
||||||
"nuxt": "^4.3.1",
|
"nuxt": "^4.3.1",
|
||||||
"sass": "^1.98.0",
|
"sass": "^1.98.0",
|
||||||
|
"socket.io-client": "^4.8.3",
|
||||||
"vue": "^3.5.30",
|
"vue": "^3.5.30",
|
||||||
"vue-router": "^4.6.4"
|
"vue-router": "^4.6.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ http {
|
|||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
}
|
}
|
||||||
|
|
||||||
# Normal requests
|
# Normal requests
|
||||||
|
|||||||
Reference in New Issue
Block a user