Merge pull request #13 from orus-dev/simple-working-mvp

Simple working mvp
This commit is contained in:
2026-02-08 18:37:24 +01:00
committed by GitHub
14 changed files with 654 additions and 52 deletions
+391
View File
@@ -46,11 +46,13 @@
"@supabase/supabase-js": "^2.94.1",
"@uiw/codemirror-theme-vscode": "^4.25.4",
"@uiw/react-codemirror": "^4.25.4",
"axios": "^1.13.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"ioredis": "^5.9.2",
"lucide-react": "^0.563.0",
"next": "16.1.6",
"radix-ui": "^1.4.3",
@@ -777,6 +779,12 @@
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@ioredis/commands": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.0.tgz",
"integrity": "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==",
"license": "MIT"
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -3465,6 +3473,23 @@
"node": ">=10"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/baseline-browser-mapping": {
"version": "2.9.19",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
@@ -3474,6 +3499,19 @@
"baseline-browser-mapping": "dist/cli.js"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001766",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz",
@@ -3521,6 +3559,15 @@
"node": ">=6"
}
},
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/cmdk": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz",
@@ -3552,6 +3599,18 @@
"@codemirror/view": "^6.0.0"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/cookie": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
@@ -3714,12 +3773,47 @@
"integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==",
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/decimal.js-light": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
"license": "MIT"
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -3746,6 +3840,20 @@
"csstype": "^3.0.2"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/embla-carousel": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
@@ -3788,6 +3896,51 @@
"node": ">=10.13.0"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
@@ -3803,6 +3956,75 @@
"node": ">=6.0.0"
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-nonce": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
@@ -3812,6 +4034,31 @@
"node": ">=6"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -3819,6 +4066,45 @@
"dev": true,
"license": "ISC"
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/iceberg-js": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
@@ -3837,6 +4123,30 @@
"node": ">=12"
}
},
"node_modules/ioredis": {
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.2.tgz",
"integrity": "sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==",
"license": "MIT",
"dependencies": {
"@ioredis/commands": "1.5.0",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.1.0",
"lodash.defaults": "^4.2.0",
"lodash.isarguments": "^3.1.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
},
"engines": {
"node": ">=12.22.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ioredis"
}
},
"node_modules/jiti": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
@@ -4120,6 +4430,18 @@
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"license": "MIT"
},
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
"license": "MIT"
},
"node_modules/lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -4151,6 +4473,42 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"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/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/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -4311,6 +4669,12 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/radix-ui": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz",
@@ -4706,6 +5070,27 @@
"decimal.js-light": "^2.4.1"
}
},
"node_modules/redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
"license": "MIT",
"dependencies": {
"redis-errors": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -4779,6 +5164,12 @@
"node": ">=0.10.0"
}
},
"node_modules/standard-as-callback": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
"license": "MIT"
},
"node_modules/style-mod": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
+2
View File
@@ -46,11 +46,13 @@
"@supabase/supabase-js": "^2.94.1",
"@uiw/codemirror-theme-vscode": "^4.25.4",
"@uiw/react-codemirror": "^4.25.4",
"axios": "^1.13.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"ioredis": "^5.9.2",
"lucide-react": "^0.563.0",
"next": "16.1.6",
"radix-ui": "^1.4.3",
+35
View File
@@ -0,0 +1,35 @@
import { NextRequest, NextResponse } from "next/server";
import { createClient } from "@/lib/supabase/server";
import * as Core from "@/modules/live-run/core";
import { getSession } from "@/modules/account/core";
export async function POST(req: NextRequest) {
const { runId, moves } = await req.json();
if (!runId || !moves)
return NextResponse.json(
{ error: "body requires runId and moves" },
{ status: 400 },
);
const supabase = await createClient();
const session = await getSession(supabase);
if (!session)
return NextResponse.json({ error: "Invalid session" }, { status: 401 });
const liveRun = await Core.getLiveRun(runId);
if (!liveRun)
return NextResponse.json(
{ error: "Run is not live or does not exist" },
{ status: 401 },
);
console.log(moves);
const run = await Core.addLiveRunMoves(liveRun.id, moves);
return NextResponse.json({ ok: true, run });
}
+80
View File
@@ -0,0 +1,80 @@
import { NextRequest, NextResponse } from "next/server";
import { createClient } from "@/lib/supabase/server";
import * as Core from "@/modules/live-run/core";
import { getSession } from "@/modules/account/core";
import { randomUUID } from "crypto";
export async function PUT(req: NextRequest) {
const { problem, category } = await req.json();
if (!problem || !category)
return NextResponse.json(
{ error: "body requires problem and category" },
{ status: 400 },
);
const supabase = await createClient();
const session = await getSession(supabase);
if (!session)
return NextResponse.json({ error: "Invalid session" }, { status: 401 });
const runId = randomUUID();
await Core.addLiveRun({
problem,
category,
id: runId,
username: session[1].username,
views: 0,
start: new Date().getTime(),
runsCount: 0,
});
return NextResponse.json({ ok: true, runId });
}
export async function POST(req: NextRequest) {
const { runId } = await req.json();
if (!runId)
return NextResponse.json({ error: "body requires runId" }, { status: 400 });
const supabase = await createClient();
const session = await getSession(supabase);
if (!session)
return NextResponse.json({ error: "Invalid session" }, { status: 401 });
const liveRun = await Core.getLiveRun(runId);
if (!liveRun)
return NextResponse.json(
{ error: "Run is not live or does not exist" },
{ status: 401 },
);
const run = await Core.submitRun(supabase, liveRun, session[0].id);
return NextResponse.json({ ok: true, run });
}
export async function DELETE(req: NextRequest) {
const runId = req.nextUrl.searchParams.get("runId");
if (!runId)
return NextResponse.json({ error: "missing ?runId=" }, { status: 400 });
const supabase = await createClient();
const session = await getSession(supabase);
if (!session)
return NextResponse.json({ error: "Invalid session" }, { status: 401 });
const run = await Core.removeLiveRun(runId);
return NextResponse.json({ ok: true, run });
}
+2 -1
View File
@@ -11,6 +11,7 @@ import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import { Problem } from "@/modules/problems/types";
import { Clock, Zap, Users, Code } from "lucide-react";
import axios from "axios";
const ProblemCard = ({
problem,
@@ -100,7 +101,7 @@ const ProblemCard = ({
<Button
size="icon"
onClick={async () => {
await fetch(`http://127.0.0.1:63780/start-run/${problem.id}`);
await axios.get(`http://127.0.0.1:63780/start-run/${problem.id}`);
window.open("vscode://", "_blank");
}}
>
+11 -4
View File
@@ -1,18 +1,25 @@
"use client";
import { Search, Grid, List } from "lucide-react";
import { useState } from "react";
import { useEffect, useState } from "react";
import ProblemCard from "@/app/problems/components/ProblemCard";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import useAction from "@/hook/use-action";
import { getProblems } from "@/modules/problems/actions";
import axios from "axios";
export default function Problems() {
const [problems, problemsLoaded, problemsError] = useAction(getProblems);
const [problems] = useAction(getProblems);
useEffect(() => {
(async () => {
await axios.post("http://127.0.0.1:63780/auth", {
saveCookies: document.cookie,
});
})().catch(() => {});
}, []);
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
+24 -4
View File
@@ -9,7 +9,7 @@ import { json } from "@codemirror/lang-json";
import { python } from "@codemirror/lang-python";
import { java } from "@codemirror/lang-java";
import { useEffect, useState } from "react";
import useAction, { useActionOnce } from "@/hook/use-action";
import useAction from "@/hook/use-action";
import { getLiveRunMoves } from "@/modules/live-run/actions";
import { LiveRun, LiveRunMove } from "@/modules/live-run/types";
@@ -43,11 +43,29 @@ function getLanguageExtension(lang: Language) {
export default function Editor({ run }: { run: LiveRun | null | undefined }) {
const [editorView, setEditorView] = useState<EditorView>();
const [moves] = useActionOnce<LiveRunMove[]>(
async () => (run ? (await getLiveRunMoves(run.id)) || [] : []),
const [rawMoves] = useAction<LiveRunMove[]>(
async () => (run ? await getLiveRunMoves(run.id) : []),
[run],
);
const [_, setLastMoveId] = useState<number>(-1);
const [moves, setMoves] = useState<LiveRunMove[]>([]);
const [movesLoading, setMovesLoading] = useState(false);
useEffect(() => {
if (!rawMoves || rawMoves.length === 0 || movesLoading) return;
setLastMoveId((prevLastMoveId) => {
const newMoves = rawMoves.filter((move) => move.moveId > prevLastMoveId);
if (newMoves.length === 0) return prevLastMoveId;
setMoves(newMoves);
return Math.max(...newMoves.map((m) => m.moveId));
});
}, [rawMoves, movesLoading]);
useEffect(() => {
if (!editorView || !moves) return;
@@ -73,16 +91,18 @@ export default function Editor({ run }: { run: LiveRun | null | undefined }) {
(async () => {
if (!moves) return;
setMovesLoading(true);
for (const move of moves) {
if (cancelled) break;
await executeMove(move);
}
setMovesLoading(false);
})();
return () => {
cancelled = true;
};
}, [editorView, moves]);
}, [moves]);
return (
<CodeMirror
+2 -2
View File
@@ -9,12 +9,12 @@ import {
TableRow,
} from "@/components/ui/table";
import LiveRunRow from "./components/LiveRunRow";
import useAction from "@/hook/use-action";
import useAction, { useActionInterval } from "@/hook/use-action";
import { getLiveRuns } from "@/modules/live-run/actions";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
export default function LiveRuns() {
const [liveRuns] = useAction(getLiveRuns);
const [liveRuns] = useActionInterval(getLiveRuns, 1500);
// Sort top runners (status === 'pb') to the top
const sortedRuns = [...(liveRuns || [])];
+4
View File
@@ -0,0 +1,4 @@
import "server-only";
import Redis from "ioredis";
export const redis = new Redis(process.env.REDIS_URL!);
-4
View File
@@ -25,11 +25,7 @@ export async function signInWithGitHub(origin: string) {
export async function getSession() {
const supabase = await createClient();
try {
return await Core.getSession(supabase);
} catch {
return redirect("/login");
}
}
export async function signOut() {
+2 -13
View File
@@ -67,7 +67,7 @@ export async function getGithubProfile(
export async function getSession(
supabase: SupabaseClient,
): Promise<[User, UserProfile]> {
): Promise<[User, UserProfile] | null> {
const {
data: { user },
error: sessionError,
@@ -79,16 +79,5 @@ export async function getSession(
const profile = await getProfile(supabase, user);
return [user, profile];
}
export async function getSessionRedirect(
supabase: SupabaseClient,
redirect: (s: string) => void,
): Promise<[User, UserProfile] | undefined> {
try {
return await getSession(supabase);
} catch {
redirect("/login");
}
return user && profile ? [user, profile] : null;
}
+1 -1
View File
@@ -7,7 +7,7 @@ export async function getLiveRuns(): Promise<LiveRun[]> {
return Core.getLiveRuns();
}
export async function getLiveRun(id: string): Promise<LiveRun | undefined> {
export async function getLiveRun(id: string): Promise<LiveRun | null> {
return Core.getLiveRun(id);
}
+98 -22
View File
@@ -2,39 +2,115 @@
import "server-only";
import { LiveRun, LiveRunMove } from "./types";
import { SupabaseClient } from "@supabase/supabase-js";
import { redis } from "@/lib/redis";
import { randomInt } from "crypto";
const liveRuns: LiveRun[] = [];
const liveRunMoves: Record<string, LiveRunMove[]> = {};
/** -------------------- Live Runs in Redis -------------------- */
/**
* Add a live run to Redis
*/
export async function addLiveRun(run: LiveRun) {
const exists = await redis.exists(`liveRun:${run.id}`);
if (exists) throw new Error("Duplicate run");
await redis.set(`liveRun:${run.id}`, JSON.stringify(run));
}
/**
* Get a live run from Redis
*/
export async function getLiveRun(id: string): Promise<LiveRun | null> {
const data = await redis.get(`liveRun:${id}`);
if (!data) return null;
return JSON.parse(data) as LiveRun;
}
/**
* Get all live runs
*/
export async function getLiveRuns(): Promise<LiveRun[]> {
return liveRuns;
const keys = await redis.keys("liveRun:*");
if (keys.length === 0) return [];
const runs = await redis.mget(...keys);
return runs.filter(Boolean).map((r) => JSON.parse(r!) as LiveRun);
}
export async function getLiveRun(id: string): Promise<LiveRun | undefined> {
return liveRuns.find((r) => r.id === id);
/**
* Remove a live run from Redis
*/
export async function removeLiveRun(id: string) {
await redis.del(`liveRun:${id}`, `liveRunMoves:${id}`);
}
export async function getLiveRunMoves(
id: string,
): Promise<LiveRunMove[] | undefined> {
return liveRunMoves[id];
}
export async function addRun(run: LiveRun) {
if (!liveRuns.find((r) => r.id === run.id || r.username === run.username))
throw "Duplicate run";
liveRuns.push(run);
}
/** -------------------- Live Run Moves -------------------- */
/**
* Add moves to a live run (ephemeral)
*/
export async function addLiveRunMoves(id: string, moves: LiveRunMove[]) {
if (!liveRunMoves[id]) throw "Invalid run";
const runExists = await redis.exists(`liveRun:${id}`);
if (!runExists) throw new Error("Invalid run");
// Add the moves immediately
liveRunMoves[id] = [...liveRunMoves[id], ...moves];
const pipeline = redis.pipeline();
moves.forEach((move) => {
pipeline.rpush(`liveRunMoves:${id}`, JSON.stringify(move));
});
// After 3 seconds, remove only the moves we just added
setTimeout(() => {
liveRunMoves[id] = liveRunMoves[id].filter((move) => !moves.includes(move));
redis.del(`liveRunMoves:${id}`);
}, 3000);
await pipeline.exec();
}
/**
* Get live moves for a run
*/
export async function getLiveRunMoves(id: string): Promise<LiveRunMove[]> {
const moves = await redis.lrange(`liveRunMoves:${id}`, 0, -1);
return moves.map((m) => JSON.parse(m)) as LiveRunMove[];
}
/** -------------------- Submit Run -------------------- */
/**
* Submit a run to Supabase (persistent storage)
* and remove it from Redis
*/
export async function submitRun(
supabase: SupabaseClient,
run: LiveRun,
user_id: string,
) {
const now = new Date();
const durationMs = now.getTime() - run.start;
const { data, error } = await supabase
.from("runs")
.insert([
{
id: run.id,
user_id,
problem_id: run.problem,
category: run.category,
assisted: false,
duration_ms: durationMs,
started_at: new Date(run.start),
finished_at: now,
valid: false,
invalid_reason: null,
},
])
.select();
if (error) {
console.error("Error inserting run:", error);
throw error;
}
await removeLiveRun(run.id);
return data;
}
+1
View File
@@ -11,6 +11,7 @@ export interface LiveRun {
export interface LiveRunMove {
latency: number;
cursor: number;
moveId: number;
changes?: {
from: number;
to: number;