Commit 3c1cab51 by Farhaan Khan

Initial commit

parent 919c218f
...@@ -8,12 +8,16 @@ ...@@ -8,12 +8,16 @@
"name": "frontend", "name": "frontend",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"axios": "^1.9.0",
"canvas": "^3.1.0",
"next": "15.3.3", "next": "15.3.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0",
"tesseract.js": "^6.0.1"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@types/axios": "^0.9.36",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
...@@ -945,6 +949,13 @@ ...@@ -945,6 +949,13 @@
"tailwindcss": "4.1.8" "tailwindcss": "4.1.8"
} }
}, },
"node_modules/@types/axios": {
"version": "0.9.36",
"resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.9.36.tgz",
"integrity": "sha512-NLOpedx9o+rxo/X5ChbdiX6mS1atE4WHmEEIcR9NLenRVa5HoVjAvjafwU3FPTqnZEstpoqCaW7fagqSoTDNeg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.17.57", "version": "20.17.57",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz",
...@@ -975,6 +986,84 @@ ...@@ -975,6 +986,84 @@
"@types/react": "^19.0.0" "@types/react": "^19.0.0"
} }
}, },
"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.9.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"license": "MIT",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/bmp-js": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz",
"integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==",
"license": "MIT"
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/busboy": { "node_modules/busboy": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
...@@ -986,6 +1075,19 @@ ...@@ -986,6 +1075,19 @@
"node": ">=10.16.0" "node": ">=10.16.0"
} }
}, },
"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": { "node_modules/caniuse-lite": {
"version": "1.0.30001720", "version": "1.0.30001720",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001720.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001720.tgz",
...@@ -1006,6 +1108,20 @@ ...@@ -1006,6 +1108,20 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/canvas": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/canvas/-/canvas-3.1.0.tgz",
"integrity": "sha512-tTj3CqqukVJ9NgSahykNwtGda7V33VLObwrHfzT0vqJXu7J4d4C/7kQQW3fOEGDfZZoILPut5H00gOjyttPGyg==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-addon-api": "^7.0.0",
"prebuild-install": "^7.1.1"
},
"engines": {
"node": "^18.12.0 || >= 20.9.0"
}
},
"node_modules/chownr": { "node_modules/chownr": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
...@@ -1067,6 +1183,18 @@ ...@@ -1067,6 +1183,18 @@
"simple-swizzle": "^0.2.2" "simple-swizzle": "^0.2.2"
} }
}, },
"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/csstype": { "node_modules/csstype": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
...@@ -1074,16 +1202,71 @@ ...@@ -1074,16 +1202,71 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"license": "MIT",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"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/detect-libc": { "node_modules/detect-libc": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
"devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
}, },
"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/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/enhanced-resolve": { "node_modules/enhanced-resolve": {
"version": "5.18.1", "version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
...@@ -1098,6 +1281,165 @@ ...@@ -1098,6 +1281,165 @@
"node": ">=10.13.0" "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/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"license": "(MIT OR WTFPL)",
"engines": {
"node": ">=6"
}
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"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.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"license": "MIT"
},
"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-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/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"license": "MIT"
},
"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": { "node_modules/graceful-fs": {
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
...@@ -1105,6 +1447,83 @@ ...@@ -1105,6 +1447,83 @@
"dev": true, "dev": true,
"license": "ISC" "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/idb-keyval": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz",
"integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==",
"license": "Apache-2.0"
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"license": "ISC"
},
"node_modules/is-arrayish": { "node_modules/is-arrayish": {
"version": "0.3.2", "version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
...@@ -1112,6 +1531,12 @@ ...@@ -1112,6 +1531,12 @@
"license": "MIT", "license": "MIT",
"optional": true "optional": true
}, },
"node_modules/is-url": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
"license": "MIT"
},
"node_modules/jiti": { "node_modules/jiti": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
...@@ -1371,6 +1796,57 @@ ...@@ -1371,6 +1796,57 @@
"@jridgewell/sourcemap-codec": "^1.5.0" "@jridgewell/sourcemap-codec": "^1.5.0"
} }
}, },
"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/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/minipass": { "node_modules/minipass": {
"version": "7.1.2", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
...@@ -1410,6 +1886,12 @@ ...@@ -1410,6 +1886,12 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT"
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
...@@ -1428,6 +1910,12 @@ ...@@ -1428,6 +1910,12 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
} }
}, },
"node_modules/napi-build-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
"license": "MIT"
},
"node_modules/next": { "node_modules/next": {
"version": "15.3.3", "version": "15.3.3",
"resolved": "https://registry.npmjs.org/next/-/next-15.3.3.tgz", "resolved": "https://registry.npmjs.org/next/-/next-15.3.3.tgz",
...@@ -1510,6 +1998,62 @@ ...@@ -1510,6 +1998,62 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/node-abi": {
"version": "3.75.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz",
"integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==",
"license": "MIT",
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": ">=10"
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/opencollective-postinstall": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
"integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==",
"license": "MIT",
"bin": {
"opencollective-postinstall": "index.js"
}
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
...@@ -1545,6 +2089,63 @@ ...@@ -1545,6 +2089,63 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^2.0.0",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
"bin": {
"prebuild-install": "bin.js"
},
"engines": {
"node": ">=10"
}
},
"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/pump": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"bin": {
"rc": "cli.js"
}
},
"node_modules/react": { "node_modules/react": {
"version": "19.1.0", "version": "19.1.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
...@@ -1566,6 +2167,46 @@ ...@@ -1566,6 +2167,46 @@
"react": "^19.1.0" "react": "^19.1.0"
} }
}, },
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT"
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/scheduler": { "node_modules/scheduler": {
"version": "0.26.0", "version": "0.26.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
...@@ -1577,7 +2218,6 @@ ...@@ -1577,7 +2218,6 @@
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"license": "ISC", "license": "ISC",
"optional": true,
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
}, },
...@@ -1627,6 +2267,51 @@ ...@@ -1627,6 +2267,51 @@
"@img/sharp-win32-x64": "0.34.2" "@img/sharp-win32-x64": "0.34.2"
} }
}, },
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/simple-swizzle": { "node_modules/simple-swizzle": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
...@@ -1654,6 +2339,24 @@ ...@@ -1654,6 +2339,24 @@
"node": ">=10.0.0" "node": ">=10.0.0"
} }
}, },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/styled-jsx": { "node_modules/styled-jsx": {
"version": "5.1.6", "version": "5.1.6",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
...@@ -1712,12 +2415,88 @@ ...@@ -1712,12 +2415,88 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/tar-fs": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
"integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==",
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"node_modules/tar-fs/node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"license": "ISC"
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"license": "MIT",
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tesseract.js": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-6.0.1.tgz",
"integrity": "sha512-/sPvMvrCtgxnNRCjbTYbr7BRu0yfWDsMZQ2a/T5aN/L1t8wUQN6tTWv6p6FwzpoEBA0jrN2UD2SX4QQFRdoDbA==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"bmp-js": "^0.1.0",
"idb-keyval": "^6.2.0",
"is-url": "^1.2.4",
"node-fetch": "^2.6.9",
"opencollective-postinstall": "^2.0.3",
"regenerator-runtime": "^0.13.3",
"tesseract.js-core": "^6.0.0",
"wasm-feature-detect": "^1.2.11",
"zlibjs": "^0.3.1"
}
},
"node_modules/tesseract.js-core": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/tesseract.js-core/-/tesseract.js-core-6.0.0.tgz",
"integrity": "sha512-1Qncm/9oKM7xgrQXZXNB+NRh19qiXGhxlrR8EwFbK5SaUbPZnS5OMtP/ghtqfd23hsr1ZvZbZjeuAGcMxd/ooA==",
"license": "Apache-2.0"
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/tslib": { "node_modules/tslib": {
"version": "2.8.1", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.8.3", "version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
...@@ -1739,6 +2518,40 @@ ...@@ -1739,6 +2518,40 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/wasm-feature-detect": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz",
"integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==",
"license": "Apache-2.0"
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/yallist": { "node_modules/yallist": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
...@@ -1748,6 +2561,15 @@ ...@@ -1748,6 +2561,15 @@
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
},
"node_modules/zlibjs": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz",
"integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==",
"license": "MIT",
"engines": {
"node": "*"
}
} }
} }
} }
...@@ -9,16 +9,20 @@ ...@@ -9,16 +9,20 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"axios": "^1.9.0",
"canvas": "^3.1.0",
"next": "15.3.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"next": "15.3.3" "tesseract.js": "^6.0.1"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5", "@tailwindcss/postcss": "^4",
"@types/axios": "^0.9.36",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@tailwindcss/postcss": "^4", "tailwindcss": "^4",
"tailwindcss": "^4" "typescript": "^5"
} }
} }
// frontend/pages/components/quizmodal.tsx
import React, { useEffect, useState } from 'react';
import Tesseract from 'tesseract.js';
interface QuizModalProps {
slideImageUrl: string;
onClose: (result: 'correct' | 'fail') => void;
attempt: number;
}
export default function QuizModal({ slideImageUrl, onClose, attempt }: QuizModalProps) {
const [question, setQuestion] = useState('');
const [options, setOptions] = useState<string[]>([]);
const [selectedOption, setSelectedOption] = useState('');
const [correctLetter, setCorrectLetter] = useState('');
const [feedback, setFeedback] = useState('');
const [isCompleted, setIsCompleted] = useState(false);
useEffect(() => {
Tesseract.recognize(slideImageUrl, 'eng').then(({ data: { text } }) => {
const lines = text.split('\n').filter(l => l.trim());
const quizIndex = lines.findIndex(l => l.toLowerCase().includes('quiz'));
if (quizIndex >= 0) {
setQuestion(lines[quizIndex + 1]?.trim() || '');
setOptions(lines.slice(quizIndex + 2, quizIndex + 5));
}
const answerLine = lines.find(l => l.toLowerCase().startsWith('correct answer'));
if (answerLine) {
const match = answerLine.match(/[:\-]\s*([A-Z])/i);
if (match) {
setCorrectLetter(match[1].toUpperCase());
}
}
});
}, [slideImageUrl]);
const handleSubmit = () => {
if (isCompleted) return;
const selectedIndex = options.findIndex(opt => opt === selectedOption);
const selectedLetter = String.fromCharCode(65 + selectedIndex); // 65 = A
if (selectedLetter === correctLetter) {
setIsCompleted(true);
setFeedback('✅ Correct answer!');
setTimeout(() => onClose('correct'), 1000);
} else if (attempt >= 2) {
setFeedback('❌ Max attempts reached. Restarting...');
setTimeout(() => onClose('fail'), 1500);
} else {
setFeedback('❌ Wrong answer. Try again.');
setTimeout(() => onClose('fail'), 1500);
}
};
if (!question && options.length === 0) {
return (
<div style={styles.overlay}>
<div style={styles.modal}>
<p>🔍 Reading quiz from slide... Please wait.</p>
</div>
</div>
);
}
return (
<div style={styles.overlay}>
<div style={styles.modal}>
<h2>Quiz (Attempt {attempt + 1}/3)</h2>
<p style={{ fontWeight: 'bold' }}>{question}</p>
{options.map((option, index) => (
<label key={index} style={styles.option}>
<input
type="radio"
value={option}
checked={selectedOption === option}
onChange={() => !isCompleted && setSelectedOption(option)}
disabled={isCompleted}
/>
{option}
{isCompleted && option === options[correctLetter.charCodeAt(0) - 65] && ' ✔️'}
</label>
))}
<button
style={styles.submitButton}
onClick={handleSubmit}
disabled={!selectedOption || isCompleted}
>
{isCompleted ? 'Completed' : 'Submit Answer'}
</button>
{feedback && <p style={{ marginTop: 10 }}>{feedback}</p>}
</div>
</div>
);
}
const styles: { [key: string]: React.CSSProperties } = {
overlay: {
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
backgroundColor: 'rgba(0,0,0,0.85)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000,
},
modal: {
background: '#fff',
padding: '2rem',
borderRadius: 10,
width: '80%',
maxWidth: '500px',
textAlign: 'left',
zIndex: 1001,
},
option: {
display: 'block',
margin: '0.5rem 0',
},
submitButton: {
padding: '0.5rem 1rem',
backgroundColor: '#0070f3',
color: '#fff',
border: 'none',
borderRadius: 5,
cursor: 'pointer',
marginTop: 10,
},
};
// frontend/pages/components/slideviewer.tsx
import React, { useEffect, useRef, useState } from 'react';
interface Slide {
slideNumber: number;
imageUrl: string;
audioUrl: string | null;
videoUrl: string | null;
}
interface Props {
slide: Slide;
currentIndex: number;
totalSlides: number;
isNextEnabled: boolean;
onAudioEnd: () => void;
onNext: () => void;
onPrev: () => void;
onRestart: () => void;
resetAudio: () => void;
}
export default function SlideViewer({
slide,
currentIndex,
totalSlides,
isNextEnabled,
onAudioEnd,
onNext,
onPrev,
onRestart,
resetAudio,
}: Props) {
const audioRef = useRef<HTMLAudioElement>(null);
const [showVideo, setShowVideo] = useState(false);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && showVideo) {
setShowVideo(false);
const videoEl = document.getElementById('video-player') as HTMLVideoElement;
if (videoEl) {
videoEl.pause();
}
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [showVideo]);
useEffect(() => {
resetAudio();
setShowVideo(false); // Reset video when slide changes
if (audioRef.current) {
audioRef.current.load();
audioRef.current.play().catch(() => {});
}
}, [slide]);
return (
<div style={styles.container}>
<button style={styles.prevButton} onClick={onPrev} disabled={currentIndex === 0}>
</button>
<div style={styles.content}>
<img
src={`http://localhost:3001${slide.imageUrl}`}
alt="Slide"
style={styles.image}
/>
{slide.videoUrl && !showVideo && (
<button
style={styles.videoButton}
onClick={() => setShowVideo(true)}
title="Play Video"
>
</button>
)}
{showVideo && slide.videoUrl && (
<video
controls
autoPlay
style={styles.videoOverlay}
onEnded={() => setShowVideo(false)}
>
<source src={`http://localhost:3001${slide.videoUrl}`} type="video/mp4" />
Your browser does not support the video tag.
</video>
)}
{slide.audioUrl && (
<audio
ref={audioRef}
controls
onEnded={onAudioEnd}
style={styles.audio}
>
<source src={`http://localhost:3001${slide.audioUrl}`} type="audio/mpeg" />
</audio>
)}
</div>
<button
style={styles.nextButton}
onClick={onNext}
disabled={!isNextEnabled || currentIndex === totalSlides - 1}
>
</button>
</div>
);
}
const styles: { [key: string]: React.CSSProperties } = {
container: {
display: 'flex',
width: '100vw',
height: '100vh',
alignItems: 'center',
justifyContent: 'space-between',
padding: 20,
boxSizing: 'border-box',
},
prevButton: {
fontSize: '2rem',
cursor: 'pointer',
background: 'none',
border: 'none',
color: '#333',
},
nextButton: {
fontSize: '2rem',
cursor: 'pointer',
background: 'none',
border: 'none',
color: '#333',
},
content: {
flex: 1,
textAlign: 'center',
height: '100%',
position: 'relative',
},
image: {
maxHeight: 'calc(100vh - 120px)',
width: '100%',
maxWidth: '100%',
objectFit: 'contain',
},
audio: {
position: 'absolute',
bottom: 20,
left: '50%',
transform: 'translateX(-50%)',
},
videoButton: {
position: 'absolute',
right: '25%',
top: '50%',
transform: 'translateY(-50%)',
backgroundColor: '#0070f3',
color: 'white',
border: 'none',
borderRadius: '50%',
width: 50,
height: 50,
fontSize: '1.5rem',
cursor: 'pointer',
zIndex: 2,
},
videoOverlay: {
position: 'absolute',
top: '10%',
left: '50%',
transform: 'translateX(-50%)',
maxWidth: '80%',
maxHeight: '80%',
zIndex: 10,
backgroundColor: '#000',
borderRadius: 8,
},
};
// pages/editor.tsx
import React, { useEffect, useState } from 'react';
import axios from 'axios';
interface SlideFormData {
slideNumber: number;
imageUrl: string;
audioUrl: string | null;
videoUrl: string | null;
newImageFile?: File;
newAudioFile?: File;
newVideoFile?: File;
}
const BACKEND_URL = "http://localhost:3001";
const isAbsoluteUrl = (url: string) => /^(https?:|blob:)/.test(url);
const EditorPage = () => {
const [slides, setSlides] = useState<SlideFormData[]>([]);
const [imagePreview, setImagePreview] = useState<string | null>(null);
const [videoPreview, setVideoPreview] = useState<string | null>(null);
useEffect(() => {
const fetchSlides = async () => {
try {
const stored = localStorage.getItem("slides");
if (stored) {
const parsed = JSON.parse(stored);
if (Array.isArray(parsed)) setSlides(parsed);
else if (parsed.slides && Array.isArray(parsed.slides)) setSlides(parsed.slides);
}
} catch (err) {
console.error("Error loading slides", err);
}
};
fetchSlides();
}, []);
const getFileName = (url: string | null): string =>
url ? url.split('/').pop() || '' : 'No File';
const getImageSrc = (slide: SlideFormData) => {
if (slide.newImageFile) {
return URL.createObjectURL(slide.newImageFile);
}
if (slide.imageUrl) {
return isAbsoluteUrl(slide.imageUrl) ? slide.imageUrl : BACKEND_URL + slide.imageUrl;
}
return undefined;
};
const getAudioSrc = (slide: SlideFormData) => {
if (slide.newAudioFile) {
return URL.createObjectURL(slide.newAudioFile);
}
if (slide.audioUrl) {
return isAbsoluteUrl(slide.audioUrl) ? slide.audioUrl : BACKEND_URL + slide.audioUrl;
}
return undefined;
};
const getVideoSrc = (slide: SlideFormData) => {
if (slide.newVideoFile) {
return URL.createObjectURL(slide.newVideoFile);
}
if (slide.videoUrl) {
return isAbsoluteUrl(slide.videoUrl) ? slide.videoUrl : BACKEND_URL + slide.videoUrl;
}
return undefined;
};
const handleFileChange = (index: number, type: 'audio' | 'video' | 'image', file: File) => {
const updatedSlides = [...slides];
if (type === 'audio') {
updatedSlides[index].newAudioFile = file;
} else if (type === 'video') {
updatedSlides[index].newVideoFile = file;
} else if (type === 'image') {
updatedSlides[index].newImageFile = file;
}
setSlides(updatedSlides);
};
const handleInsert = (index: number, before = false) => {
const newSlide: SlideFormData = {
slideNumber: 0, // will be reset
imageUrl: '',
audioUrl: null,
videoUrl: null
};
const updatedSlides = [...slides];
updatedSlides.splice(before ? index : index + 1, 0, newSlide);
updatedSlides.forEach((s, i) => (s.slideNumber = i + 1));
setSlides(updatedSlides);
};
const handleDelete = (index: number) => {
const updatedSlides = [...slides];
updatedSlides.splice(index, 1);
updatedSlides.forEach((s, i) => (s.slideNumber = i + 1));
setSlides(updatedSlides);
};
const handleSave = async () => {
const formData = new FormData();
const slideNumbers = slides.map((_, i) => (i + 1).toString());
formData.append('slideNumbers', slideNumbers.join(','));
await Promise.all(slides.map(async (slide, index) => {
const slideIndex = index + 1;
// Slide image
if (slide.newImageFile) {
formData.append('slide', new File(
[slide.newImageFile],
`slide${slideIndex}${slide.imageUrl?.includes('_quiz') ? '_quiz' : ''}.jpg`,
{ type: slide.newImageFile.type }
));
} else if (slide.imageUrl) {
const response = await fetch(isAbsoluteUrl(slide.imageUrl) ? slide.imageUrl : BACKEND_URL + slide.imageUrl);
const blob = await response.blob();
formData.append('slide', new File(
[blob],
`slide${slideIndex}${slide.imageUrl?.includes('_quiz') ? '_quiz' : ''}.jpg`,
{ type: blob.type }
));
}
// Audio
if (slide.newAudioFile) {
formData.append('audio', new File(
[slide.newAudioFile],
`slide${slideIndex}.mp3`,
{ type: slide.newAudioFile.type }
));
} else if (slide.audioUrl) {
const response = await fetch(isAbsoluteUrl(slide.audioUrl) ? slide.audioUrl : BACKEND_URL + slide.audioUrl);
const blob = await response.blob();
formData.append('audio', new File(
[blob],
`slide${slideIndex}.mp3`,
{ type: blob.type }
));
}
// Video
if (slide.newVideoFile) {
formData.append('video', new File(
[slide.newVideoFile],
`slide${slideIndex}.mp4`,
{ type: slide.newVideoFile.type }
));
} else if (slide.videoUrl) {
const response = await fetch(isAbsoluteUrl(slide.videoUrl) ? slide.videoUrl : BACKEND_URL + slide.videoUrl);
const blob = await response.blob();
formData.append('video', new File(
[blob],
`slide${slideIndex}.mp4`,
{ type: blob.type }
));
}
}));
try {
const res = await axios.post(`${BACKEND_URL}/editor/save`, formData);
if ((res.data as any).success) {
alert("Slides saved successfully");
// ✅ Update localStorage after successful save
const updatedSlides = slides.map((slide, index) => {
const slideNumber = index + 1;
const baseName = `slide${slideNumber}`;
return {
slideNumber,
imageUrl: `/slides/${baseName}${slide.imageUrl?.includes('_quiz') ? '_quiz' : ''}.jpg`,
audioUrl: `/media/${baseName}.mp3`,
videoUrl: slide.newVideoFile || slide.videoUrl ? `/media/${baseName}.mp4` : null,
};
});
localStorage.setItem('slides', JSON.stringify({ slides: updatedSlides }));
} else {
alert("Failed to save slides");
}
} catch (err) {
console.error("Save failed", err);
alert("Failed to save slides");
}
};
return (
<div className="p-4">
<h1 className="text-xl font-bold mb-4">Slide Editor</h1>
<table className="w-full border text-center">
<thead>
<tr>
<th className="border p-2">Slide #</th>
<th className="border p-2">Image</th>
<th className="border p-2">Audio</th>
<th className="border p-2">Video</th>
<th className="border p-2">Upload Slide</th>
<th className="border p-2">Upload Audio</th>
<th className="border p-2">Upload Video</th>
<th className="border p-2">Actions</th>
</tr>
</thead>
<tbody>
{slides.map((slide, index) => (
<tr key={index}>
<td className="border p-2">{slide.slideNumber}</td>
<td className="border p-2">
{getImageSrc(slide) ? (
<img
src={getImageSrc(slide)}
alt={`Slide ${slide.slideNumber}`}
className="w-20 h-20 object-contain cursor-pointer mx-auto"
onClick={() => setImagePreview(getImageSrc(slide)!)}
/>
) : (
'No Image'
)}
</td>
<td className="border p-2">
{getAudioSrc(slide) ? (
<>
<div className="text-xs truncate">
{slide.newAudioFile?.name || getFileName(slide.audioUrl)}
</div>
<audio controls src={getAudioSrc(slide)} />
</>
) : (
'No Audio'
)}
</td>
<td className="border p-2">
{getVideoSrc(slide) ? (
<>
<div className="text-xs truncate">
{slide.newVideoFile?.name || getFileName(slide.videoUrl)}
</div>
<video
className="w-24 h-16 cursor-pointer"
controls
onClick={() => setVideoPreview(getVideoSrc(slide)!)}
>
<source src={getVideoSrc(slide)} />
Your browser does not support video.
</video>
</>
) : (
'No Video'
)}
</td>
<td className="border p-2">
<input
type="file"
accept="image/*"
onChange={(e) =>
e.target.files && handleFileChange(index, 'image', e.target.files[0])
}
/>
</td>
<td className="border p-2">
<input
type="file"
accept="audio/*"
onChange={(e) =>
e.target.files && handleFileChange(index, 'audio', e.target.files[0])
}
/>
</td>
<td className="border p-2">
<input
type="file"
accept="video/*"
onChange={(e) =>
e.target.files && handleFileChange(index, 'video', e.target.files[0])
}
/>
</td>
<td className="border p-2 text-sm space-y-1">
<button
onClick={() => handleInsert(index, true)}
className="text-blue-600 hover:underline block"
>
Insert Before
</button>
<button
onClick={() => handleInsert(index, false)}
className="text-blue-600 hover:underline block"
>
Insert After
</button>
<button
onClick={() => handleDelete(index)}
className="text-red-600 hover:underline block"
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
<button onClick={handleSave} className="btn btn-primary mt-4">
Save Changes
</button>
{imagePreview && (
<div
className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50"
onClick={() => setImagePreview(null)}
>
<img src={imagePreview} className="max-w-full max-h-full" />
</div>
)}
{videoPreview && (
<div
className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50"
onClick={() => setVideoPreview(null)}
>
<video controls autoPlay className="max-w-full max-h-full">
<source src={videoPreview} />
</video>
</div>
)}
</div>
);
};
export default EditorPage;
import Image from "next/image"; import React, { useState } from 'react';
import { Geist, Geist_Mono } from "next/font/google"; import axios from 'axios';
import { useRouter } from 'next/router';
const geistSans = Geist({ interface Slide {
variable: "--font-geist-sans", slideNumber: number;
subsets: ["latin"], imageUrl: string;
}); audioUrl: string | null;
videoUrl: string | null;
const geistMono = Geist_Mono({ }
variable: "--font-geist-mono",
subsets: ["latin"],
});
export default function Home() { export default function Home() {
const [pdfFile, setPdfFile] = useState<File | null>(null);
const [zipFile, setZipFile] = useState<File | null>(null);
const [loading, setLoading] = useState(false);
const router = useRouter();
const handleUpload = async () => {
if (!pdfFile || !zipFile) {
alert('Please select both PDF and ZIP files!');
return;
}
const formData = new FormData();
formData.append('pdf', pdfFile);
formData.append('zip', zipFile);
try {
setLoading(true);
const res = await axios.post<Slide[]>('http://localhost:3001/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 60000, // 60 seconds for large files
});
console.log(res.data,'res.data')
localStorage.setItem('slides', JSON.stringify(res.data));
router.push('/player');
} catch (err) {
console.error('Upload failed:', err);
alert('Upload failed. Please try again.');
} finally {
setLoading(false);
}
};
return ( return (
<div <div style={styles.container}>
className={`${geistSans.className} ${geistMono.className} grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]`} <h1 style={styles.heading}>Upload Slides & Media</h1>
>
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start"> <div style={styles.formGroup}>
<Image <label style={styles.label}>Upload PDF file:</label>
className="dark:invert" <input type="file" accept=".pdf" onChange={(e) => setPdfFile(e.target.files?.[0] || null)} />
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
pages/index.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div> </div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a <div style={styles.formGroup}>
className="flex items-center gap-2 hover:underline hover:underline-offset-4" <label style={styles.label}>Upload Media ZIP (mp3/mp4):</label>
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app" <input type="file" accept=".zip" onChange={(e) => setZipFile(e.target.files?.[0] || null)} />
target="_blank" </div>
rel="noopener noreferrer"
> <button onClick={handleUpload} style={styles.uploadButton} disabled={loading}>
<Image {loading ? 'Uploading...' : 'Start Upload'}
aria-hidden </button>
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org →
</a>
</footer>
</div> </div>
); );
} }
const styles: { [key: string]: React.CSSProperties } = {
container: {
padding: '2rem',
background: '#f9fafb',
minHeight: '100vh',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
fontFamily: 'Segoe UI, sans-serif',
},
heading: {
fontSize: '2.5rem',
color: '#1f2937',
marginBottom: '2rem',
textAlign: 'center',
},
formGroup: {
marginBottom: '1.5rem',
display: 'flex',
flexDirection: 'column',
width: '100%',
maxWidth: '400px',
},
label: {
marginBottom: '0.5rem',
fontSize: '1rem',
color: '#374151',
},
uploadButton: {
marginTop: '1rem',
padding: '0.75rem 2rem',
fontSize: '1rem',
backgroundColor: '#2563eb',
color: '#ffffff',
border: 'none',
borderRadius: 8,
cursor: 'pointer',
fontWeight: 'bold',
transition: 'background-color 0.2s ease-in-out',
},
};
// frontend/pages/player.tsx
import React, { useEffect, useState } from 'react';
import QuizModal from './components/quizmodal';
import SlideViewer from './components/slideviewer';
interface Slide {
slideNumber: number;
imageUrl: string;
audioUrl: string | null;
videoUrl: string | null;
}
const backendUrl = 'http://localhost:3001';
export default function Player() {
const [slides, setSlides] = useState<Slide[]>([]);
const [currentIndex, setCurrentIndex] = useState(0);
const [audioEnded, setAudioEnded] = useState(false);
const [completedSlides, setCompletedSlides] = useState<number[]>([]);
const [showQuiz, setShowQuiz] = useState(false);
const [quizImageUrl, setQuizImageUrl] = useState('');
const [quizAttempts, setQuizAttempts] = useState(0);
const [quizRedirectMap, setQuizRedirectMap] = useState<{ [key: number]: number }>({});
useEffect(() => {
const stored = localStorage.getItem('slides');
if (!stored) {
alert('No slide data found. Please upload again.');
return;
}
try {
const parsed = JSON.parse(stored);
const parsedSlides = parsed.slides;
if (Array.isArray(parsedSlides) && parsedSlides.length > 0) {
setSlides(parsedSlides);
// Extract quiz slide numbers
const quizSlides: number[] = parsedSlides
.filter((s: Slide) => s.imageUrl.toLowerCase().includes('quiz'))
.map((s: Slide) => s.slideNumber)
.sort((a, b) => a - b);
// Build redirect map
const redirectMap: { [key: number]: number } = {};
for (let i = 0; i < quizSlides.length; i++) {
const currentQuiz = quizSlides[i];
const previousQuiz = quizSlides[i - 1] ?? 0; // 0 if first quiz
const fallback = parsedSlides.find(s => s.slideNumber > previousQuiz)?.slideNumber ?? 0;
redirectMap[currentQuiz] = fallback;
}
setQuizRedirectMap(redirectMap);
} else {
alert('Slide data is empty or invalid. Please re-upload.');
}
} catch (e) {
alert('Slide data is corrupted. Please upload again.');
}
}, []);
useEffect(() => {
setAudioEnded(false);
setShowQuiz(false);
const currentSlide = slides[currentIndex];
if (!currentSlide) return;
const filename = currentSlide.imageUrl.toLowerCase();
if (filename.includes('quiz')) {
setQuizImageUrl(`${backendUrl}${currentSlide.imageUrl}`);
setShowQuiz(true);
}
}, [currentIndex, slides]);
const handleQuizClose = (result: 'correct' | 'fail') => {
setShowQuiz(false);
if (result === 'correct') {
setCompletedSlides((prev) => [...prev, slides[currentIndex].slideNumber]);
setCurrentIndex((prev) => prev + 1);
setQuizAttempts(0);
} else if (quizAttempts < 2) {
setQuizAttempts((prev) => prev + 1);
const failedSlideNumber = slides[currentIndex].slideNumber;
const redirectTo = quizRedirectMap[failedSlideNumber] ?? 0;
const newIndex = slides.findIndex(s => s.slideNumber === redirectTo);
setCurrentIndex(newIndex >= 0 ? newIndex : 0);
} else {
setQuizAttempts(0); // Max attempts used, move forward
setCurrentIndex((prev) => prev + 1);
}
};
const currentSlide = slides[currentIndex];
const isCompleted = completedSlides.includes(currentSlide?.slideNumber ?? -1);
const isNextEnabled = audioEnded || isCompleted || !currentSlide?.audioUrl;
const handleAudioEnd = () => {
setAudioEnded(true);
setCompletedSlides((prev) => [...new Set([...prev, currentSlide.slideNumber])]);
};
if (!currentSlide) {
return <div style={{ padding: 20 }}><h2>No slides to display</h2></div>;
}
return (
<>
<div style={{ textAlign: 'center', margin: '10px 0', fontSize: '18px' }}>
Slide {currentIndex + 1} of {slides.length}
</div>
{!showQuiz && (
<SlideViewer
slide={currentSlide}
currentIndex={currentIndex}
totalSlides={slides.length}
isNextEnabled={isNextEnabled}
onAudioEnd={handleAudioEnd}
onNext={() => setCurrentIndex(prev => prev + 1)}
onPrev={() => setCurrentIndex(prev => Math.max(prev - 1, 0))}
onRestart={() => setCurrentIndex(0)}
resetAudio={() => setAudioEnded(false)}
/>
)}
{showQuiz && (
<QuizModal
slideImageUrl={quizImageUrl}
onClose={handleQuizClose}
attempt={quizAttempts}
/>
)}
</>
);
}
// utils/api.ts
import axios from 'axios';
const API = axios.create({
baseURL: 'http://localhost:3001', // <-- Your backend URL here
timeout: 80000,
});
export default API;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment