From 6e4a9ac9672e0acb16d1592469738f90a13925a1 Mon Sep 17 00:00:00 2001 From: BinarySandia04 Date: Thu, 11 Jun 2026 23:47:37 +0200 Subject: [PATCH] Place --- backend/package-lock.json | 229 ++++++++++++++++- backend/package.json | 3 +- backend/src/index.js | 53 +++- backend/src/routes/gridCells.js | 67 +++++ backend/src/schemas/GridCell.js | 21 ++ .../components/parts/AnimatedBackground.vue | 4 +- .../app/components/parts/ServerStatus.vue | 24 +- frontend/app/components/widgets/Place.vue | 237 ++++++++++++++++++ .../app/composables/{api.js => useApi.ts} | 0 frontend/app/composables/useSocket.ts | 63 +++++ frontend/app/pages/contact/index.vue | 4 +- frontend/app/pages/index.vue | 138 ++-------- frontend/package-lock.json | 1 + frontend/package.json | 1 + nginx.conf | 2 + 15 files changed, 689 insertions(+), 158 deletions(-) create mode 100644 backend/src/routes/gridCells.js create mode 100644 backend/src/schemas/GridCell.js create mode 100644 frontend/app/components/widgets/Place.vue rename frontend/app/composables/{api.js => useApi.ts} (100%) create mode 100644 frontend/app/composables/useSocket.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index 2fe8161..59c23aa 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,7 +13,8 @@ "dotenv": "^17.3.1", "express": "^5.2.1", "mongoose": "^9.3.0", - "nodemon": "^3.1.14" + "nodemon": "^3.1.14", + "socket.io": "^4.8.3" } }, "node_modules/@mongodb-js/saslprep": { @@ -25,6 +26,30 @@ "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": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -40,6 +65,15 @@ "@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": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -75,6 +109,15 @@ "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": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -329,6 +372,79 @@ "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": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1251,6 +1367,90 @@ "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": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -1343,6 +1543,12 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "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": { "version": "1.0.0", "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", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "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 + } + } } } } diff --git a/backend/package.json b/backend/package.json index 66339b2..411a23b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,6 +14,7 @@ "dotenv": "^17.3.1", "express": "^5.2.1", "mongoose": "^9.3.0", - "nodemon": "^3.1.14" + "nodemon": "^3.1.14", + "socket.io": "^4.8.3" } } diff --git a/backend/src/index.js b/backend/src/index.js index 234d2c5..7a4678c 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -1,39 +1,67 @@ const express = require("express"); -const cors = require('cors'); -const mongoose = require('mongoose'); +const cors = require("cors"); +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({ - path: `.env.${process.env.NODE_ENV}` + path: `.env.${process.env.NODE_ENV}`, }); } else { dotenv.config(); } const app = express(); +const httpServer = createServer(app); +const io = new Server(httpServer, { + cors: { + origin: true, + credentials: true, + }, +}); + const connectDB = require("./db"); +const gridCellsRouter = require("./routes/gridCells"); + +gridCellsRouter.setIO(io); // connect database connectDB(); -app.use(cors({ - origin: 'http://localhost:3000', - credentials: true, // if using cookies/auth -})); +app.use( + cors({ + 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) => { 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) => { const mem = process.memoryUsage(); const uptime = Math.floor(process.uptime()); const hours = Math.floor(uptime / 3600); const minutes = Math.floor((uptime % 3600) / 60); - + mongoose.connection.readyState === 1 ? res.json({ status: "online", @@ -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"); }); \ No newline at end of file diff --git a/backend/src/routes/gridCells.js b/backend/src/routes/gridCells.js new file mode 100644 index 0000000..31633bb --- /dev/null +++ b/backend/src/routes/gridCells.js @@ -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; diff --git a/backend/src/schemas/GridCell.js b/backend/src/schemas/GridCell.js new file mode 100644 index 0000000..fad601f --- /dev/null +++ b/backend/src/schemas/GridCell.js @@ -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; diff --git a/frontend/app/components/parts/AnimatedBackground.vue b/frontend/app/components/parts/AnimatedBackground.vue index 94290a1..ba2e791 100644 --- a/frontend/app/components/parts/AnimatedBackground.vue +++ b/frontend/app/components/parts/AnimatedBackground.vue @@ -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) { diff --git a/frontend/app/components/parts/ServerStatus.vue b/frontend/app/components/parts/ServerStatus.vue index 9c94d55..b29f2f9 100644 --- a/frontend/app/components/parts/ServerStatus.vue +++ b/frontend/app/components/parts/ServerStatus.vue @@ -1,5 +1,5 @@ + + + + diff --git a/frontend/app/composables/api.js b/frontend/app/composables/useApi.ts similarity index 100% rename from frontend/app/composables/api.js rename to frontend/app/composables/useApi.ts diff --git a/frontend/app/composables/useSocket.ts b/frontend/app/composables/useSocket.ts new file mode 100644 index 0000000..a6fc4fb --- /dev/null +++ b/frontend/app/composables/useSocket.ts @@ -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, + } +} diff --git a/frontend/app/pages/contact/index.vue b/frontend/app/pages/contact/index.vue index c1e07d6..38dc5b4 100644 --- a/frontend/app/pages/contact/index.vue +++ b/frontend/app/pages/contact/index.vue @@ -3,7 +3,7 @@ import TableHeader from '~/components/parts/TableHeader.vue'; import FixedLayout from '~/components/layouts/FixedLayout.vue'; import { useSeo } from '~/composables/seo'; -const { get, post } = api(); +const { get, post } = useApi(); const { locale } = useI18n(); useSeo({ @@ -79,4 +79,4 @@ p { .no-sprite .undertable-wrapper { display: none; } - \ No newline at end of file + diff --git a/frontend/app/pages/index.vue b/frontend/app/pages/index.vue index 713ee5c..2a384b3 100644 --- a/frontend/app/pages/index.vue +++ b/frontend/app/pages/index.vue @@ -2,10 +2,12 @@ import FixedLayout from '~/components/layouts/FixedLayout.vue'; import TableHeader from '~/components/parts/TableHeader.vue'; import ServerStatus from '~/components/parts/ServerStatus.vue'; -import api from '~/composables/api' + import { useSeo } from '~/composables/seo' -const { get, post } = api(); +import Place from '~/components/widgets/Place.vue'; + +const { get, post } = useApi(); const { locale } = useI18n(); const { t } = useI18n(); @@ -180,7 +182,14 @@ const sectionTargets = { -
+
+ +

Place things!

+ +
+
+ +

{{ t('pages.projects_heading') }}

@@ -217,101 +226,7 @@ const sectionTargets = {
- - -
+

{{ t('pages.blog_heading') }}

    @@ -328,7 +243,7 @@ const sectionTargets = {
-
+

{{ t('pages.art_heading') }}

@@ -351,7 +266,7 @@ const sectionTargets = {
-
+

{{ t('pages.contact_heading') }}

@@ -422,12 +337,8 @@ const sectionTargets = { } } -.projects-section, -.stats-section, -.blog-section, -.contact-section, -.art-section { - margin-top: 16px; +.normal-section { + margin-top: 24px; .tui-frame:last-child { margin-bottom: 0; @@ -713,11 +624,6 @@ const sectionTargets = { opacity: 0.8; } -/* Blog section */ -.blog-section { - margin-top: 32px; -} - .tui-list { list-style: none; margin: 0; @@ -779,16 +685,6 @@ const sectionTargets = { } } -/* Contact section */ -.contact-section { - margin-top: 32px; -} - -/* Art section */ -.art-section { - margin-top: 32px; -} - .grid { display: grid; grid-template-columns: repeat(3, minmax(250px, 1fr)); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f026569..affbdb9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,6 +13,7 @@ "better-sqlite3": "^12.10.0", "nuxt": "^4.3.1", "sass": "^1.98.0", + "socket.io-client": "^4.8.3", "vue": "^3.5.30", "vue-router": "^4.6.4" }, diff --git a/frontend/package.json b/frontend/package.json index 696042d..b015b32 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,7 @@ "better-sqlite3": "^12.10.0", "nuxt": "^4.3.1", "sass": "^1.98.0", + "socket.io-client": "^4.8.3", "vue": "^3.5.30", "vue-router": "^4.6.4" }, diff --git a/nginx.conf b/nginx.conf index cbcc960..cee22be 100644 --- a/nginx.conf +++ b/nginx.conf @@ -14,6 +14,8 @@ http { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; } # Normal requests