initialized web page (React + Tailwind CSS)
962
package-lock.json
generated
Normal file
@@ -0,0 +1,962 @@
|
||||
{
|
||||
"name": "final-project",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "final-project",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^5.2.1",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"yamljs": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@scarf/scarf": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
|
||||
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "^3.0.0",
|
||||
"negotiator": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
|
||||
"integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "^3.1.2",
|
||||
"content-type": "^1.0.5",
|
||||
"debug": "^4.4.3",
|
||||
"http-errors": "^2.0.0",
|
||||
"iconv-lite": "^0.7.0",
|
||||
"on-finished": "^2.4.1",
|
||||
"qs": "^6.14.0",
|
||||
"raw-body": "^3.0.1",
|
||||
"type-is": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"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/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
|
||||
"integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.6.0"
|
||||
}
|
||||
},
|
||||
"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/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.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/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"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/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
"content-disposition": "^1.0.0",
|
||||
"content-type": "^1.0.5",
|
||||
"cookie": "^0.7.1",
|
||||
"cookie-signature": "^1.2.1",
|
||||
"debug": "^4.4.0",
|
||||
"depd": "^2.0.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
"finalhandler": "^2.1.0",
|
||||
"fresh": "^2.0.0",
|
||||
"http-errors": "^2.0.0",
|
||||
"merge-descriptors": "^2.0.0",
|
||||
"mime-types": "^3.0.0",
|
||||
"on-finished": "^2.4.1",
|
||||
"once": "^1.4.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"proxy-addr": "^2.0.7",
|
||||
"qs": "^6.14.0",
|
||||
"range-parser": "^1.2.1",
|
||||
"router": "^2.2.0",
|
||||
"send": "^1.1.0",
|
||||
"serve-static": "^2.2.0",
|
||||
"statuses": "^2.0.1",
|
||||
"type-is": "^2.0.1",
|
||||
"vary": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
|
||||
"integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"parseurl": "^1.3.3",
|
||||
"statuses": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"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/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"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/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/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/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "~2.0.0",
|
||||
"inherits": "~2.0.4",
|
||||
"setprototypeof": "~1.2.0",
|
||||
"statuses": "~2.0.2",
|
||||
"toidentifier": "~1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
|
||||
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"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/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"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/media-typer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
||||
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
||||
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.54.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
||||
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "^1.54.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"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/negotiator": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"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/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
|
||||
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"forwarded": "0.2.0",
|
||||
"ipaddr.js": "1.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
|
||||
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.7.0",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/router": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"depd": "^2.0.0",
|
||||
"is-promise": "^4.0.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"path-to-regexp": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
|
||||
"integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.5",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
"fresh": "^2.0.0",
|
||||
"http-errors": "^2.0.0",
|
||||
"mime-types": "^3.0.1",
|
||||
"ms": "^2.1.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"range-parser": "^1.2.1",
|
||||
"statuses": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
|
||||
"integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"parseurl": "^1.3.3",
|
||||
"send": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-list": "^1.0.0",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/swagger-ui-dist": {
|
||||
"version": "5.30.3",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.3.tgz",
|
||||
"integrity": "sha512-giQl7/ToPxCqnUAx2wpnSnDNGZtGzw1LyUw6ZitIpTmdrvpxKFY/94v1hihm0zYNpgp1/VY0jTDk//R0BBgnRQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scarf/scarf": "=1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/swagger-ui-express": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
|
||||
"integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"swagger-ui-dist": ">=5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= v0.10.32"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": ">=4.0.0 || >=5.0.0-beta"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
||||
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"content-type": "^1.0.5",
|
||||
"media-typer": "^1.1.0",
|
||||
"mime-types": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"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/yamljs": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz",
|
||||
"integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.7",
|
||||
"glob": "^7.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"json2yaml": "bin/json2yaml",
|
||||
"yaml2json": "bin/yaml2json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "final-project",
|
||||
"version": "1.0.0",
|
||||
"description": "2025 Golang Final Project",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Penguin-71630/meme-bot-frontend.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Penguin-71630/meme-bot-frontend/issues"
|
||||
},
|
||||
"homepage": "https://github.com/Penguin-71630/meme-bot-frontend#readme",
|
||||
"dependencies": {
|
||||
"express": "^5.2.1",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"yamljs": "^0.3.0"
|
||||
}
|
||||
}
|
||||
24
webpage/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
73
webpage/README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
23
webpage/eslint.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
13
webpage/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>webpage</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
4129
webpage/package-lock.json
generated
Normal file
33
webpage/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "webpage",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"autoprefixer": "^10.4.22",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.46.4",
|
||||
"vite": "^7.2.4"
|
||||
}
|
||||
}
|
||||
7
webpage/postcss.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
plugins: {
|
||||
'tailwindcss/nesting': {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
1
webpage/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
100
webpage/spec.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Webpage for Memebot
|
||||
|
||||
This webpage is used to manage the images and aliases used by our Discord memebot.
|
||||
|
||||
Relationship between images and aliases: many to many.
|
||||
|
||||
|
||||
|
||||
## Layout
|
||||
|
||||
React + TypeScript (with Tailwind CSS)
|
||||
|
||||
The webpage layout is roughly as follows:
|
||||
```
|
||||
Memebot [Upload-Image] [Login]
|
||||
-----------------------------------------------------------
|
||||
alias_01 | ( Search Bar )
|
||||
alias_02 |
|
||||
alias_03 | Alias: alias_01
|
||||
alias_04 | [ Img Box1 ] [ Img Box2 ] [ Img Box3 ]
|
||||
alias_05 |
|
||||
alias_06 | Alias: alias_02
|
||||
alias_07 | [ Img Box1 ] [ Img Box2 ]
|
||||
alias_08 |
|
||||
alias_09 | Alias: alias_03
|
||||
alias_10 | [ Img Box1 ] [ Img Box2 ] [ Img Box3 ]
|
||||
alias_11 | [ Img Box4 ]
|
||||
Img w/o alias |
|
||||
```
|
||||
|
||||
The top is the Navbar:
|
||||
- The left side of the Navbar shows our project name.
|
||||
- The right side of the Navbar contains function buttons, currently only Upload Image and Login.
|
||||
|
||||
|
||||
### Find
|
||||
|
||||
Find (Viewing and Searching)
|
||||
- The left sidebar serves as a table of content for all aliases, similar to the navigation feature in HackMD or mdBook.
|
||||
- The last entry is "Images without aliases" (Img w/o alias).
|
||||
- The search bar is located directly below the Navbar.
|
||||
- The search bar provides instant feedback; the system automatically lists matching aliases and corresponding images without the user needing to press enter.
|
||||
- The content below the search bar displays images grouped by their respective alias.
|
||||
- Clicking an image brings up a floating window (modal) showing the image and related information (including who uploaded the image, when the image was uploaded, and what aliases it has). Within this modal, the user can edit aliases and delete the image.
|
||||
```
|
||||
======================================
|
||||
| |--------------------------------| |
|
||||
| | | |
|
||||
| | | |
|
||||
| | Show Image Here | |
|
||||
| | | |
|
||||
| | | |
|
||||
| |--------------------------------| |
|
||||
| |
|
||||
| Uploaded Time: 2023-10-20.12:00 |
|
||||
| Uploaded By: konchin.shih |
|
||||
| |
|
||||
| Aliases of this image: |
|
||||
| [-] [ alias_01 ] |
|
||||
| [-] [ alias_02 ] |
|
||||
| [+] [ (Add a new alias) ] |
|
||||
| |
|
||||
| <Save> |
|
||||
======================================
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Upload Image
|
||||
|
||||
When the user clicks this button, a floating window (modal) appears:
|
||||
|
||||
```
|
||||
======================================
|
||||
| |--------------------------------| |
|
||||
| | | |
|
||||
| | | |
|
||||
| | Drag your image here | |
|
||||
| | | |
|
||||
| | | |
|
||||
| |--------------------------------| |
|
||||
| |
|
||||
| Aliases of this image: |
|
||||
| [-] [ alias_01 ] |
|
||||
| [-] [ alias_02 ] |
|
||||
| [+] [ (Add a new alias) ] |
|
||||
| |
|
||||
| <Upload> |
|
||||
======================================
|
||||
```
|
||||
|
||||
## Login (Authentication)
|
||||
|
||||
Authentication is used to verify the current Discord user operating the frontend. Users who are not logged in can only perform viewing-related operations; they cannot edit, upload, or delete.
|
||||
|
||||
|
||||
<!-- Modify the webpage: We don't want one page shows only an alias.
|
||||
We want a paging feature on aliases, so that each page shows a fixed number of aliases and their corresponding images (the fixed number can be stored in a variable in api.ts). And the "next page" and "previous page" buttons should be located at both the top (below search bar) and the bottom of the page.
|
||||
The sidebar is also modified to show the current page number, the aliases on the current page, and the total number of pages. -->
|
||||
|
||||
268
webpage/src/App.tsx
Normal file
@@ -0,0 +1,268 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import Navbar from './components/Navbar';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import SearchBar from './components/SearchBar';
|
||||
import ImageGrid from './components/ImageGrid';
|
||||
import ImageModal from './components/ImageModal';
|
||||
import UploadModal from './components/UploadModal';
|
||||
import Pagination from './components/Pagination';
|
||||
import { api, ALIASES_PER_PAGE } from './api-mock';
|
||||
import type { Image } from './types';
|
||||
|
||||
function App() {
|
||||
const [images, setImages] = useState<Image[]>([]);
|
||||
const [allAliases, setAllAliases] = useState<string[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [selectedImage, setSelectedImage] = useState<Image | null>(null);
|
||||
const [showUploadModal, setShowUploadModal] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [searchQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
// Reset to page 1 when search query changes
|
||||
setCurrentPage(1);
|
||||
}, [searchQuery]);
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [imagesData, aliasesData] = await Promise.all([
|
||||
api.getImages({
|
||||
search: searchQuery || undefined,
|
||||
}),
|
||||
api.getAllAliases(),
|
||||
]);
|
||||
|
||||
setImages(imagesData);
|
||||
setAllAliases(aliasesData);
|
||||
} catch (error) {
|
||||
console.error('Failed to load data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpload = async (file: File, aliases: string[]) => {
|
||||
await api.uploadImage(file, aliases);
|
||||
await loadData();
|
||||
};
|
||||
|
||||
const handleSaveImage = async (imageId: string, aliases: string[]) => {
|
||||
await api.updateImageAliases(imageId, aliases);
|
||||
await loadData();
|
||||
};
|
||||
|
||||
const handleDeleteImage = async (imageId: string) => {
|
||||
await api.deleteImage(imageId);
|
||||
await loadData();
|
||||
};
|
||||
|
||||
// Filter aliases based on search
|
||||
const filteredAliases = searchQuery
|
||||
? allAliases.filter(alias =>
|
||||
alias.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
: allAliases;
|
||||
|
||||
// Pagination calculations
|
||||
const totalPages = Math.max(1, Math.ceil(filteredAliases.length / ALIASES_PER_PAGE));
|
||||
const startIndex = (currentPage - 1) * ALIASES_PER_PAGE;
|
||||
const endIndex = startIndex + ALIASES_PER_PAGE;
|
||||
const currentPageAliases = filteredAliases.slice(startIndex, endIndex);
|
||||
|
||||
// Filter images to only show those with current page aliases
|
||||
const currentPageImages = images.filter(img =>
|
||||
img.aliases.some(alias => currentPageAliases.includes(alias)) ||
|
||||
(img.aliases.length === 0 && currentPageAliases.length > 0)
|
||||
);
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
if (newPage >= 1 && newPage <= totalPages) {
|
||||
setCurrentPage(newPage);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col">
|
||||
<Navbar onUploadClick={() => setShowUploadModal(true)} />
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
<Sidebar
|
||||
aliases={currentPageAliases}
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
<SearchBar value={searchQuery} onChange={setSearchQuery} />
|
||||
|
||||
{/* Top Pagination */}
|
||||
{!loading && totalPages > 1 && (
|
||||
<div className="border-b border-gray-200">
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-gray-500">Loading...</div>
|
||||
</div>
|
||||
) : (
|
||||
<ImageGrid
|
||||
images={currentPageImages}
|
||||
aliases={currentPageAliases}
|
||||
onImageClick={setSelectedImage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Bottom Pagination */}
|
||||
{!loading && totalPages > 1 && (
|
||||
<div className="border-t border-gray-200">
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{selectedImage && (
|
||||
<ImageModal
|
||||
image={selectedImage}
|
||||
onClose={() => setSelectedImage(null)}
|
||||
onSave={handleSaveImage}
|
||||
onDelete={handleDeleteImage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showUploadModal && (
|
||||
<UploadModal
|
||||
onClose={() => setShowUploadModal(false)}
|
||||
onUpload={handleUpload}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
// import { useState, useEffect } from 'react';
|
||||
// import Navbar from './components/Navbar';
|
||||
// import Sidebar from './components/Sidebar';
|
||||
// import SearchBar from './components/SearchBar';
|
||||
// import ImageGrid from './components/ImageGrid';
|
||||
// import ImageModal from './components/ImageModal';
|
||||
// import UploadModal from './components/UploadModal';
|
||||
// import { api } from './api-mock';
|
||||
// import type { Image } from './types';
|
||||
|
||||
// function App() {
|
||||
// const [images, setImages] = useState<Image[]>([]);
|
||||
// const [allAliases, setAllAliases] = useState<string[]>([]);
|
||||
// const [searchQuery, setSearchQuery] = useState('');
|
||||
// const [selectedAlias, setSelectedAlias] = useState<string | null>(null);
|
||||
// const [selectedImage, setSelectedImage] = useState<Image | null>(null);
|
||||
// const [showUploadModal, setShowUploadModal] = useState(false);
|
||||
// const [loading, setLoading] = useState(true);
|
||||
|
||||
// useEffect(() => {
|
||||
// loadData();
|
||||
// }, [searchQuery, selectedAlias]);
|
||||
|
||||
// const loadData = async () => {
|
||||
// setLoading(true);
|
||||
// try {
|
||||
// const [imagesData, aliasesData] = await Promise.all([
|
||||
// api.getImages({
|
||||
// search: searchQuery || undefined,
|
||||
// null_alias: selectedAlias === null ? true : undefined,
|
||||
// }),
|
||||
// api.getAllAliases(),
|
||||
// ]);
|
||||
|
||||
// let filteredImages = imagesData;
|
||||
// if (selectedAlias) {
|
||||
// filteredImages = imagesData.filter((img) =>
|
||||
// img.aliases.includes(selectedAlias)
|
||||
// );
|
||||
// } else if (selectedAlias === null) {
|
||||
// filteredImages = imagesData.filter((img) => img.aliases.length === 0);
|
||||
// }
|
||||
|
||||
// setImages(filteredImages);
|
||||
// setAllAliases(aliasesData);
|
||||
// } catch (error) {
|
||||
// console.error('Failed to load data:', error);
|
||||
// } finally {
|
||||
// setLoading(false);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const handleUpload = async (file: File, aliases: string[]) => {
|
||||
// await api.uploadImage(file, aliases);
|
||||
// await loadData();
|
||||
// };
|
||||
|
||||
// const handleSaveImage = async (imageId: string, aliases: string[]) => {
|
||||
// await api.updateImageAliases(imageId, aliases);
|
||||
// await loadData();
|
||||
// };
|
||||
|
||||
// const handleDeleteImage = async (imageId: string) => {
|
||||
// await api.deleteImage(imageId);
|
||||
// await loadData();
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <div className="h-screen flex flex-col">
|
||||
// <Navbar
|
||||
// onUploadClick={() => setShowUploadModal(true)}
|
||||
// />
|
||||
// <div className="flex-1 flex overflow-hidden">
|
||||
// <Sidebar
|
||||
// aliases={allAliases}
|
||||
// selectedAlias={selectedAlias}
|
||||
// onAliasClick={setSelectedAlias}
|
||||
// />
|
||||
// <main className="flex-1 overflow-y-auto">
|
||||
// <SearchBar value={searchQuery} onChange={setSearchQuery} />
|
||||
// {loading ? (
|
||||
// <div className="flex items-center justify-center h-64">
|
||||
// <div className="text-gray-500">Loading...</div>
|
||||
// </div>
|
||||
// ) : (
|
||||
// <ImageGrid images={images} onImageClick={setSelectedImage} />
|
||||
// )}
|
||||
// </main>
|
||||
// </div>
|
||||
|
||||
// {selectedImage && (
|
||||
// <ImageModal
|
||||
// image={selectedImage}
|
||||
// onClose={() => setSelectedImage(null)}
|
||||
// onSave={handleSaveImage}
|
||||
// onDelete={handleDeleteImage}
|
||||
// />
|
||||
// )}
|
||||
|
||||
// {showUploadModal && (
|
||||
// <UploadModal
|
||||
// onClose={() => setShowUploadModal(false)}
|
||||
// onUpload={handleUpload}
|
||||
// />
|
||||
// )}
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
// export default App;
|
||||
141
webpage/src/api-mock.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import type { Image } from './types';
|
||||
import fakeDb from './assets/tests/fakedb.json';
|
||||
|
||||
export const ALIASES_PER_PAGE = 5;
|
||||
|
||||
class MockApiService {
|
||||
private images: Image[] = [];
|
||||
private nextId: number = 18;
|
||||
|
||||
constructor() {
|
||||
// Load initial data from fakedb.json
|
||||
this.images = JSON.parse(JSON.stringify(fakeDb.images));
|
||||
}
|
||||
|
||||
// Helper to simulate network delay
|
||||
private delay(ms: number = 300): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// Images
|
||||
async getImages(params?: {
|
||||
search?: string;
|
||||
null_alias?: boolean;
|
||||
limit?: number;
|
||||
page?: number;
|
||||
}): Promise<Image[]> {
|
||||
await this.delay();
|
||||
|
||||
let filtered = [...this.images];
|
||||
|
||||
// Filter by search query (search in aliases)
|
||||
if (params?.search) {
|
||||
const searchLower = params.search.toLowerCase();
|
||||
filtered = filtered.filter(img =>
|
||||
img.aliases.some(alias => alias.toLowerCase().includes(searchLower))
|
||||
);
|
||||
}
|
||||
|
||||
// Filter images without aliases
|
||||
if (params?.null_alias) {
|
||||
filtered = filtered.filter(img => img.aliases.length === 0);
|
||||
}
|
||||
|
||||
// Pagination
|
||||
const page = params?.page || 1;
|
||||
const limit = params?.limit || 20;
|
||||
const start = (page - 1) * limit;
|
||||
const end = start + limit;
|
||||
|
||||
return filtered.slice(start, end);
|
||||
}
|
||||
|
||||
async getImage(id: string): Promise<Image> {
|
||||
await this.delay();
|
||||
|
||||
const image = this.images.find(img => img.id === id);
|
||||
if (!image) {
|
||||
throw new Error('Image not found');
|
||||
}
|
||||
return { ...image };
|
||||
}
|
||||
|
||||
async uploadImage(file: File, aliases: string[]): Promise<Image> {
|
||||
await this.delay(500);
|
||||
|
||||
// Create a new image object
|
||||
const newImage: Image = {
|
||||
id: String(this.nextId++),
|
||||
uploaded_user_id: 'konchin.shih',
|
||||
uploaded_at: new Date().toISOString(),
|
||||
aliases: aliases,
|
||||
url: `/api/images/${this.nextId - 1}/file`,
|
||||
};
|
||||
|
||||
this.images.push(newImage);
|
||||
return { ...newImage };
|
||||
}
|
||||
|
||||
async deleteImage(id: string): Promise<void> {
|
||||
await this.delay();
|
||||
|
||||
const index = this.images.findIndex(img => img.id === id);
|
||||
if (index === -1) {
|
||||
throw new Error('Image not found');
|
||||
}
|
||||
this.images.splice(index, 1);
|
||||
}
|
||||
|
||||
// Aliases
|
||||
async getAllAliases(): Promise<string[]> {
|
||||
await this.delay();
|
||||
|
||||
// Get unique aliases from all images
|
||||
const aliasSet = new Set<string>();
|
||||
this.images.forEach(img => {
|
||||
img.aliases.forEach(alias => aliasSet.add(alias));
|
||||
});
|
||||
return Array.from(aliasSet).sort();
|
||||
}
|
||||
|
||||
async updateImageAliases(id: string, aliases: string[]): Promise<Image> {
|
||||
await this.delay();
|
||||
|
||||
const image = this.images.find(img => img.id === id);
|
||||
if (!image) {
|
||||
throw new Error('Image not found');
|
||||
}
|
||||
image.aliases = [...aliases];
|
||||
return { ...image };
|
||||
}
|
||||
|
||||
async addImageAlias(id: string, alias: string): Promise<Image> {
|
||||
await this.delay();
|
||||
|
||||
const image = this.images.find(img => img.id === id);
|
||||
if (!image) {
|
||||
throw new Error('Image not found');
|
||||
}
|
||||
if (!image.aliases.includes(alias)) {
|
||||
image.aliases.push(alias);
|
||||
}
|
||||
return { ...image };
|
||||
}
|
||||
|
||||
async removeImageAlias(id: string, alias: string): Promise<void> {
|
||||
await this.delay();
|
||||
|
||||
const image = this.images.find(img => img.id === id);
|
||||
if (!image) {
|
||||
throw new Error('Image not found');
|
||||
}
|
||||
image.aliases = image.aliases.filter(a => a !== alias);
|
||||
}
|
||||
|
||||
getImageUrl(id: string): string {
|
||||
// For mock, return a placeholder image URL
|
||||
return `https://picsum.photos/seed/${id}/400/400`;
|
||||
}
|
||||
}
|
||||
|
||||
export const api = new MockApiService();
|
||||
103
webpage/src/api.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import type { Image } from './types';
|
||||
|
||||
const API_BASE_URL = 'http://localhost:8080';
|
||||
|
||||
// Pagination configuration
|
||||
export const ALIASES_PER_PAGE = 5; // Number of aliases to show per page
|
||||
|
||||
class ApiService {
|
||||
// Images
|
||||
async getImages(params?: {
|
||||
search?: string;
|
||||
null_alias?: boolean;
|
||||
limit?: number;
|
||||
page?: number;
|
||||
}): Promise<Image[]> {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params?.search) queryParams.append('search', params.search);
|
||||
if (params?.null_alias) queryParams.append('null_alias', 'true');
|
||||
if (params?.limit) queryParams.append('limit', params.limit.toString());
|
||||
if (params?.page) queryParams.append('page', params.page.toString());
|
||||
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/api/images?${queryParams}`
|
||||
);
|
||||
if (!response.ok) throw new Error('Failed to fetch images');
|
||||
const data = await response.json();
|
||||
return data.images || [];
|
||||
}
|
||||
|
||||
async getImage(id: string): Promise<Image> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/images/${id}`);
|
||||
if (!response.ok) throw new Error('Failed to fetch image');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async uploadImage(file: File, aliases: string[]): Promise<Image> {
|
||||
const formData = new FormData();
|
||||
formData.append('imgfile', file);
|
||||
aliases.forEach((alias) => formData.append('aliases', alias));
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/images`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to upload image');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async deleteImage(id: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/images/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to delete image');
|
||||
}
|
||||
|
||||
// Aliases
|
||||
async getAllAliases(): Promise<string[]> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/aliases`);
|
||||
if (!response.ok) throw new Error('Failed to fetch aliases');
|
||||
const data = await response.json();
|
||||
return data.aliases || [];
|
||||
}
|
||||
|
||||
async updateImageAliases(id: string, aliases: string[]): Promise<Image> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/images/${id}/aliases`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ aliases }),
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to update aliases');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async addImageAlias(id: string, alias: string): Promise<Image> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/images/${id}/aliases`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ alias }),
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to add alias');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async removeImageAlias(id: string, alias: string): Promise<void> {
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/api/images/${id}/aliases?alias=${encodeURIComponent(alias)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
}
|
||||
);
|
||||
if (!response.ok) throw new Error('Failed to remove alias');
|
||||
}
|
||||
|
||||
getImageUrl(id: string): string {
|
||||
return `${API_BASE_URL}/api/images/${id}/file`;
|
||||
}
|
||||
}
|
||||
|
||||
export const api = new ApiService();
|
||||
1
webpage/src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
BIN
webpage/src/assets/tests/daisuke.gif
Normal file
|
After Width: | Height: | Size: 3.6 MiB |
137
webpage/src/assets/tests/fakedb.json
Normal file
@@ -0,0 +1,137 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"id": "1",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-20T12:00:00Z",
|
||||
"aliases": ["daisuke"],
|
||||
"url": "/api/images/1/file"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-20T12:05:00Z",
|
||||
"aliases": ["huh"],
|
||||
"url": "/api/images/2/file"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-21T09:00:00Z",
|
||||
"aliases": ["killme"],
|
||||
"url": "/api/images/3/file"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-21T09:30:00Z",
|
||||
"aliases": ["nofriend"],
|
||||
"url": "/api/images/4/file"
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-21T10:00:00Z",
|
||||
"aliases": ["orz"],
|
||||
"url": "/api/images/5/file"
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-22T11:20:00Z",
|
||||
"aliases": ["ramen"],
|
||||
"url": "/api/images/6/file"
|
||||
},
|
||||
{
|
||||
"id": "7",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-22T14:00:00Z",
|
||||
"aliases": ["rrrr"],
|
||||
"url": "/api/images/7/file"
|
||||
},
|
||||
{
|
||||
"id": "8",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-23T08:00:00Z",
|
||||
"aliases": ["sleep"],
|
||||
"url": "/api/images/8/file"
|
||||
},
|
||||
{
|
||||
"id": "9",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-23T08:05:00Z",
|
||||
"aliases": ["sleep"],
|
||||
"url": "/api/images/9/file"
|
||||
},
|
||||
{
|
||||
"id": "10",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-24T15:00:00Z",
|
||||
"aliases": ["你要出多少"],
|
||||
"url": "/api/images/10/file"
|
||||
},
|
||||
{
|
||||
"id": "11",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-24T16:00:00Z",
|
||||
"aliases": ["好ㄘ"],
|
||||
"url": "/api/images/11/file"
|
||||
},
|
||||
{
|
||||
"id": "12",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-25T10:00:00Z",
|
||||
"aliases": ["宅斃了"],
|
||||
"url": "/api/images/12/file"
|
||||
},
|
||||
{
|
||||
"id": "13",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-25T10:10:00Z",
|
||||
"aliases": ["宅斃了"],
|
||||
"url": "/api/images/13/file"
|
||||
},
|
||||
{
|
||||
"id": "14",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-25T10:20:00Z",
|
||||
"aliases": ["宅斃了"],
|
||||
"url": "/api/images/14/file"
|
||||
},
|
||||
{
|
||||
"id": "15",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-26T12:00:00Z",
|
||||
"aliases": ["幹波大的"],
|
||||
"url": "/api/images/15/file"
|
||||
},
|
||||
{
|
||||
"id": "16",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-26T13:00:00Z",
|
||||
"aliases": ["我什麼都沒有"],
|
||||
"url": "/api/images/16/file"
|
||||
},
|
||||
{
|
||||
"id": "17",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-27T09:00:00Z",
|
||||
"aliases": ["欸嘿"],
|
||||
"url": "/api/images/17/file"
|
||||
},
|
||||
{
|
||||
"id": "18",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-25T10:20:00Z",
|
||||
"aliases": ["宅斃了"],
|
||||
"url": "/api/images/18/file"
|
||||
},
|
||||
{
|
||||
"id": "19",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-25T10:20:00Z",
|
||||
"aliases": ["宅斃了"],
|
||||
"url": "/api/images/19/file"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
webpage/src/assets/tests/huh.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
webpage/src/assets/tests/killme.gif
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
webpage/src/assets/tests/nofriend.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
webpage/src/assets/tests/orz.gif
Normal file
|
After Width: | Height: | Size: 448 KiB |
BIN
webpage/src/assets/tests/ramen.jpg
Normal file
|
After Width: | Height: | Size: 450 KiB |
BIN
webpage/src/assets/tests/rrrr.jpg
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
webpage/src/assets/tests/sleep.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
webpage/src/assets/tests/sleep^2.jpg
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
webpage/src/assets/tests/你要出多少.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
webpage/src/assets/tests/好ㄘ.png
Normal file
|
After Width: | Height: | Size: 3.6 MiB |
BIN
webpage/src/assets/tests/宅斃了.jpg
Normal file
|
After Width: | Height: | Size: 423 KiB |
BIN
webpage/src/assets/tests/宅斃了^2.png
Normal file
|
After Width: | Height: | Size: 758 KiB |
BIN
webpage/src/assets/tests/宅斃了^3.png
Normal file
|
After Width: | Height: | Size: 408 KiB |
BIN
webpage/src/assets/tests/宅斃了^4.png
Normal file
|
After Width: | Height: | Size: 758 KiB |
BIN
webpage/src/assets/tests/宅斃了^5.png
Normal file
|
After Width: | Height: | Size: 758 KiB |
BIN
webpage/src/assets/tests/幹波大的.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
webpage/src/assets/tests/我什麼都沒有.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
webpage/src/assets/tests/欸嘿.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
115
webpage/src/components/ImageGrid.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import type { Image } from '../types';
|
||||
|
||||
interface ImageGridProps {
|
||||
images: Image[];
|
||||
aliases: string[];
|
||||
onImageClick: (image: Image) => void;
|
||||
}
|
||||
|
||||
export default function ImageGrid({ images, aliases, onImageClick }: ImageGridProps) {
|
||||
// Group images by alias
|
||||
const groupedImages = images.reduce((acc, image) => {
|
||||
if (image.aliases.length === 0) {
|
||||
if (!acc['__no_alias__']) acc['__no_alias__'] = [];
|
||||
acc['__no_alias__'].push(image);
|
||||
} else {
|
||||
image.aliases.forEach((alias) => {
|
||||
if (!acc[alias]) acc[alias] = [];
|
||||
acc[alias].push(image);
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, Image[]>);
|
||||
|
||||
// Filter to only show aliases on current page
|
||||
const filteredGroups = Object.entries(groupedImages).filter(([alias]) =>
|
||||
aliases.includes(alias) || alias === '__no_alias__'
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-8">
|
||||
{filteredGroups.map(([alias, imgs]) => (
|
||||
<div key={alias}>
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
||||
{alias === '__no_alias__' ? 'Images without aliases' : `Alias: ${alias}`}
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
||||
{imgs.map((image) => (
|
||||
<button
|
||||
key={image.id}
|
||||
onClick={() => onImageClick(image)}
|
||||
className="aspect-square rounded-lg overflow-hidden bg-gray-100 hover:ring-2 hover:ring-blue-500 transition-all"
|
||||
>
|
||||
<img
|
||||
src={`http://localhost:8080${image.url}`}
|
||||
alt={image.aliases.join(', ')}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{filteredGroups.length === 0 && (
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
No images found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// import type { Image } from '../types';
|
||||
|
||||
// interface ImageGridProps {
|
||||
// images: Image[];
|
||||
// onImageClick: (image: Image) => void;
|
||||
// }
|
||||
|
||||
// export default function ImageGrid({ images, onImageClick }: ImageGridProps) {
|
||||
// // Group images by alias
|
||||
// const groupedImages = images.reduce((acc, image) => {
|
||||
// if (image.aliases.length === 0) {
|
||||
// if (!acc['__no_alias__']) acc['__no_alias__'] = [];
|
||||
// acc['__no_alias__'].push(image);
|
||||
// } else {
|
||||
// image.aliases.forEach((alias) => {
|
||||
// if (!acc[alias]) acc[alias] = [];
|
||||
// acc[alias].push(image);
|
||||
// });
|
||||
// }
|
||||
// return acc;
|
||||
// }, {} as Record<string, Image[]>);
|
||||
|
||||
// return (
|
||||
// <div className="p-6 space-y-8">
|
||||
// {Object.entries(groupedImages).map(([alias, imgs]) => (
|
||||
// <div key={alias}>
|
||||
// <h3 className="text-lg font-semibold text-gray-800 mb-4">
|
||||
// {alias === '__no_alias__' ? 'Images without aliases' : `Alias: ${alias}`}
|
||||
// </h3>
|
||||
// <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
||||
// {imgs.map((image) => (
|
||||
// <button
|
||||
// key={image.id}
|
||||
// onClick={() => onImageClick(image)}
|
||||
// className="aspect-square rounded-lg overflow-hidden bg-gray-100 hover:ring-2 hover:ring-blue-500 transition-all"
|
||||
// >
|
||||
// <img
|
||||
// src={`http://localhost:8080${image.url}`}
|
||||
// alt={image.aliases.join(', ')}
|
||||
// className="w-full h-full object-cover"
|
||||
// />
|
||||
// </button>
|
||||
// ))}
|
||||
// </div>
|
||||
// </div>
|
||||
// ))}
|
||||
// {Object.keys(groupedImages).length === 0 && (
|
||||
// <div className="text-center py-12 text-gray-500">
|
||||
// No images found
|
||||
// </div>
|
||||
// )}
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
191
webpage/src/components/ImageModal.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { Image } from '../types';
|
||||
|
||||
interface ImageModalProps {
|
||||
image: Image;
|
||||
onClose: () => void;
|
||||
onSave: (imageId: string, aliases: string[]) => Promise<void>;
|
||||
onDelete: (imageId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export default function ImageModal({
|
||||
image,
|
||||
onClose,
|
||||
onSave,
|
||||
onDelete,
|
||||
}: ImageModalProps) {
|
||||
const [aliases, setAliases] = useState<string[]>(image.aliases);
|
||||
const [newAlias, setNewAlias] = useState('');
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setAliases(image.aliases);
|
||||
}, [image]);
|
||||
|
||||
const handleAddAlias = () => {
|
||||
if (newAlias.trim() && !aliases.includes(newAlias.trim())) {
|
||||
setAliases([...aliases, newAlias.trim()]);
|
||||
setNewAlias('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveAlias = (alias: string) => {
|
||||
setAliases(aliases.filter((a) => a !== alias));
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await onSave(image.id, aliases);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
alert('Failed to save changes');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (confirm('Are you sure you want to delete this image?')) {
|
||||
try {
|
||||
await onDelete(image.id);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
alert('Failed to delete image');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="p-6">
|
||||
<div className="mb-6 rounded-lg overflow-hidden bg-gray-100">
|
||||
<img
|
||||
src={`http://localhost:8080${image.url}`}
|
||||
alt={image.aliases.join(', ')}
|
||||
className="w-full h-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
<div className="text-sm text-gray-600">
|
||||
<p>
|
||||
<span className="font-medium">Uploaded Time:</span>{' '}
|
||||
{new Date(image.uploaded_at).toLocaleString()}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium">Uploaded By:</span>{' '}
|
||||
{image.uploaded_user_id}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900 mb-3">
|
||||
Aliases of this image:
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{aliases.map((alias) => (
|
||||
<div key={alias} className="flex items-center gap-2">
|
||||
{(
|
||||
<button
|
||||
onClick={() => handleRemoveAlias(alias)}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
value={alias}
|
||||
readOnly
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{(
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleAddAlias}
|
||||
className="text-green-600 hover:text-green-700"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Add a new alias"
|
||||
value={newAlias}
|
||||
onChange={(e) => setNewAlias(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleAddAlias()}
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center pt-4 border-t border-gray-200">
|
||||
<div>
|
||||
{(
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="px-4 py-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors font-medium"
|
||||
>
|
||||
Delete Image
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-lg transition-colors font-medium"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
{(
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium disabled:opacity-50"
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
27
webpage/src/components/Navbar.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
interface NavbarProps {
|
||||
onUploadClick: () => void;
|
||||
}
|
||||
|
||||
export default function Navbar({
|
||||
onUploadClick,
|
||||
}: NavbarProps) {
|
||||
return (
|
||||
<nav className="bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<h1 className="text-2xl font-bold text-gray-800">Memebot</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{(
|
||||
<button
|
||||
onClick={onUploadClick}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium"
|
||||
>
|
||||
Upload Image
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
33
webpage/src/components/Pagination.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
interface PaginationProps {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
}
|
||||
|
||||
export default function Pagination({
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPageChange,
|
||||
}: PaginationProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-4 py-4">
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<span className="text-gray-700 font-medium">
|
||||
Page {currentPage} of {totalPages}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
33
webpage/src/components/SearchBar.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
interface SearchBarProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export default function SearchBar({ value, onChange }: SearchBarProps) {
|
||||
return (
|
||||
<div className="px-6 py-4 bg-white border-b border-gray-200">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search aliases..."
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="w-full px-4 py-3 pl-11 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
<svg
|
||||
className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
86
webpage/src/components/Sidebar.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
interface SidebarProps {
|
||||
aliases: string[];
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export default function Sidebar({
|
||||
aliases,
|
||||
currentPage,
|
||||
totalPages,
|
||||
}: SidebarProps) {
|
||||
return (
|
||||
<aside className="w-64 bg-gray-50 border-r border-gray-200 overflow-y-auto">
|
||||
<div className="p-4">
|
||||
<div className="mb-4 pb-3 border-b border-gray-200">
|
||||
<h2 className="text-sm font-semibold text-gray-500 uppercase tracking-wider mb-2">
|
||||
Aliases
|
||||
</h2>
|
||||
<div className="text-xs text-gray-600">
|
||||
<p>Page {currentPage} of {totalPages}</p>
|
||||
<p className="mt-1">{aliases.length} aliases on this page</p>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="space-y-1">
|
||||
{aliases.map((alias) => (
|
||||
<li key={alias}>
|
||||
<div className="w-full text-left px-3 py-2 rounded-lg bg-blue-50 text-blue-700">
|
||||
{alias}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
// interface SidebarProps {
|
||||
// aliases: string[];
|
||||
// selectedAlias: string | null;
|
||||
// onAliasClick: (alias: string | null) => void;
|
||||
// }
|
||||
|
||||
// export default function Sidebar({
|
||||
// aliases,
|
||||
// selectedAlias,
|
||||
// onAliasClick,
|
||||
// }: SidebarProps) {
|
||||
// return (
|
||||
// <aside className="w-64 bg-gray-50 border-r border-gray-200 overflow-y-auto">
|
||||
// <div className="p-4">
|
||||
// <h2 className="text-sm font-semibold text-gray-500 uppercase tracking-wider mb-3">
|
||||
// Aliases
|
||||
// </h2>
|
||||
// <ul className="space-y-1">
|
||||
// {aliases.map((alias) => (
|
||||
// <li key={alias}>
|
||||
// <button
|
||||
// onClick={() => onAliasClick(alias)}
|
||||
// className={`w-full text-left px-3 py-2 rounded-lg transition-colors ${
|
||||
// selectedAlias === alias
|
||||
// ? 'bg-blue-100 text-blue-700 font-medium'
|
||||
// : 'text-gray-700 hover:bg-gray-100'
|
||||
// }`}
|
||||
// >
|
||||
// {alias}
|
||||
// </button>
|
||||
// </li>
|
||||
// ))}
|
||||
// <li className="pt-2 border-t border-gray-200 mt-2">
|
||||
// <button
|
||||
// onClick={() => onAliasClick(null)}
|
||||
// className={`w-full text-left px-3 py-2 rounded-lg transition-colors ${
|
||||
// selectedAlias === null
|
||||
// ? 'bg-blue-100 text-blue-700 font-medium'
|
||||
// : 'text-gray-700 hover:bg-gray-100'
|
||||
// }`}
|
||||
// >
|
||||
// Images without aliases
|
||||
// </button>
|
||||
// </li>
|
||||
// </ul>
|
||||
// </div>
|
||||
// </aside>
|
||||
// );
|
||||
// }
|
||||
245
webpage/src/components/UploadModal.tsx
Normal file
@@ -0,0 +1,245 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
interface UploadModalProps {
|
||||
onClose: () => void;
|
||||
onUpload: (file: File, aliases: string[]) => Promise<void>;
|
||||
}
|
||||
|
||||
export default function UploadModal({ onClose, onUpload }: UploadModalProps) {
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [preview, setPreview] = useState<string | null>(null);
|
||||
const [aliases, setAliases] = useState<string[]>([]);
|
||||
const [newAlias, setNewAlias] = useState('');
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
|
||||
const handleFileChange = (selectedFile: File) => {
|
||||
setFile(selectedFile);
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setPreview(reader.result as string);
|
||||
};
|
||||
reader.readAsDataURL(selectedFile);
|
||||
};
|
||||
|
||||
const handleDrag = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.type === 'dragenter' || e.type === 'dragover') {
|
||||
setDragActive(true);
|
||||
} else if (e.type === 'dragleave') {
|
||||
setDragActive(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDragActive(false);
|
||||
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
||||
handleFileChange(e.dataTransfer.files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddAlias = () => {
|
||||
if (newAlias.trim() && !aliases.includes(newAlias.trim())) {
|
||||
setAliases([...aliases, newAlias.trim()]);
|
||||
setNewAlias('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveAlias = (alias: string) => {
|
||||
setAliases(aliases.filter((a) => a !== alias));
|
||||
};
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!file) {
|
||||
alert('Please select a file');
|
||||
return;
|
||||
}
|
||||
setIsUploading(true);
|
||||
try {
|
||||
await onUpload(file, aliases);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
alert('Failed to upload image');
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="p-6">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-6">
|
||||
Upload Image
|
||||
</h2>
|
||||
|
||||
<div
|
||||
className={`mb-6 border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
|
||||
dragActive
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-300 bg-gray-50'
|
||||
}`}
|
||||
onDragEnter={handleDrag}
|
||||
onDragLeave={handleDrag}
|
||||
onDragOver={handleDrag}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{preview ? (
|
||||
<div className="relative">
|
||||
<img
|
||||
src={preview}
|
||||
alt="Preview"
|
||||
className="max-h-64 mx-auto rounded-lg"
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
setFile(null);
|
||||
setPreview(null);
|
||||
}}
|
||||
className="absolute top-2 right-2 bg-red-600 text-white p-2 rounded-full hover:bg-red-700"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<svg
|
||||
className="mx-auto h-12 w-12 text-gray-400 mb-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||||
/>
|
||||
</svg>
|
||||
<p className="text-gray-600 mb-2">
|
||||
Drag your image here or click to browse
|
||||
</p>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={(e) =>
|
||||
e.target.files && handleFileChange(e.target.files[0])
|
||||
}
|
||||
className="hidden"
|
||||
id="file-upload"
|
||||
/>
|
||||
<label
|
||||
htmlFor="file-upload"
|
||||
className="inline-block px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 cursor-pointer transition-colors"
|
||||
>
|
||||
Choose File
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<h3 className="font-medium text-gray-900 mb-3">
|
||||
Aliases of this image:
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{aliases.map((alias) => (
|
||||
<div key={alias} className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleRemoveAlias(alias)}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
value={alias}
|
||||
readOnly
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleAddAlias}
|
||||
className="text-green-600 hover:text-green-700"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Add a new alias"
|
||||
value={newAlias}
|
||||
onChange={(e) => setNewAlias(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleAddAlias()}
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4 border-t border-gray-200">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-lg transition-colors font-medium"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleUpload}
|
||||
disabled={!file || isUploading}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium disabled:opacity-50"
|
||||
>
|
||||
{isUploading ? 'Uploading...' : 'Upload'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
4
webpage/src/index.css
Normal file
@@ -0,0 +1,4 @@
|
||||
/* File: webpage/src/index.css */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
10
webpage/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
12
webpage/src/types.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export interface Image {
|
||||
id: string;
|
||||
uploaded_user_id: string;
|
||||
uploaded_at: string;
|
||||
aliases: string[];
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
}
|
||||
12
webpage/tailwind.config.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// File: webpage/tailwind.config.js
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}", // 確保掃描所有 src 裡面的元件檔案
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
28
webpage/tsconfig.app.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
webpage/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
26
webpage/tsconfig.node.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
9
webpage/vite.config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
],
|
||||
})
|
||||