diff --git a/client/eslint.config.js b/client/eslint.config.js
index 4fa125d..8f54ce3 100644
--- a/client/eslint.config.js
+++ b/client/eslint.config.js
@@ -5,25 +5,26 @@ import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
- globalIgnores(['dist']),
- {
- files: ['**/*.{js,jsx}'],
- extends: [
- js.configs.recommended,
- reactHooks.configs.flat.recommended,
- reactRefresh.configs.vite,
- ],
- languageOptions: {
- ecmaVersion: 2020,
- globals: globals.browser,
- parserOptions: {
- ecmaVersion: 'latest',
- ecmaFeatures: { jsx: true },
- sourceType: 'module',
- },
- },
- rules: {
- 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
- },
- },
-])
+globalIgnores(['dist']),
+{
+files: ['**/*.{js,jsx}'],
+extends: [
+js.configs.recommended,
+reactHooks.configs.flat.recommended,
+reactRefresh.configs.vite,
+],
+languageOptions: {
+ecmaVersion: 2020,
+globals: globals.browser,
+parserOptions: {
+ecmaVersion: 'latest',
+ecmaFeatures: { jsx: true },
+sourceType: 'module',
+},
+},
+rules: {
+'no-unused-vars': ['warn', { varsIgnorePattern: '^[A-Z_]|set[A-Z]' }],
+'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
+},
+},
+])
\ No newline at end of file
diff --git a/client/package-lock.json b/client/package-lock.json
index 0abc62a..333f2f3 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -8,8 +8,10 @@
"name": "client",
"version": "0.0.0",
"dependencies": {
+ "axios": "^1.13.6",
"react": "^19.2.0",
- "react-dom": "^19.2.0"
+ "react-dom": "^19.2.0",
+ "react-router-dom": "^7.13.1"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
@@ -1529,6 +1531,23 @@
"dev": true,
"license": "Python-2.0"
},
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.13.6",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
+ "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1595,6 +1614,19 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "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/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -1663,6 +1695,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1677,6 +1721,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -1724,6 +1781,29 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/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/electron-to-chromium": {
"version": "1.5.302",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz",
@@ -1731,6 +1811,51 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/esbuild": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
@@ -2071,6 +2196,42 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -2086,6 +2247,15 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "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/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -2096,6 +2266,43 @@
"node": ">=6.9.0"
}
},
+ "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-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -2122,6 +2329,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "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-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -2132,6 +2351,45 @@
"node": ">=8"
}
},
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/hermes-estree": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
@@ -2340,6 +2598,36 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/minimatch": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz",
@@ -2536,6 +2824,12 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -2561,6 +2855,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -2578,6 +2873,44 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-router": {
+ "version": "7.13.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz",
+ "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.13.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz",
+ "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.13.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -2649,6 +2982,12 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
diff --git a/client/package.json b/client/package.json
index 6895186..a3219c2 100644
--- a/client/package.json
+++ b/client/package.json
@@ -10,8 +10,10 @@
"preview": "vite preview"
},
"dependencies": {
+ "axios": "^1.13.6",
"react": "^19.2.0",
- "react-dom": "^19.2.0"
+ "react-dom": "^19.2.0",
+ "react-router-dom": "^7.13.1"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
diff --git a/client/src/App.jsx b/client/src/App.jsx
index f67355a..ec70322 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -1,35 +1,83 @@
-import { useState } from 'react'
-import reactLogo from './assets/react.svg'
-import viteLogo from '/vite.svg'
-import './App.css'
+import { Routes, Route } from "react-router-dom";
-function App() {
- const [count, setCount] = useState(0)
+import Header from "./components/layout/Header";
+import Footer from "./components/layout/Footer";
+
+import Home from "./pages/Home";
+import Login from "./pages/Login";
+import Register from "./pages/Register";
+import Dashboard from "./pages/Dashboard";
+
+import ProtectedRoute from "./components/common/ProtectedRoute";
+import PublicRoute from "./components/common/PublicRoute";
+import { AuthProvider } from "./context/AuthContext";
+
+function App() {
return (
- <>
-
-
-
- Edit src/App.jsx and save to test HMR
-
+
+
+
+
+
+
+
+ {/* Public */}
+ } />
+
+
+
+
+ }
+ />
+
+
+
+
+ }
+ />
+
+ {/* Protected */}
+
+
+
+ }
+ />
+
+ } />
+
+
+
+
+
-
- Click on the Vite and React logos to learn more
-
- >
- )
+
+ );
}
-export default App
+const NotFound = () => (
+
+
404 - Page Not Found
+
+);
+
+const appStyle = {
+ display: "flex",
+ flexDirection: "column",
+ minHeight: "100vh",
+};
+
+const mainStyle = {
+ flex: 1,
+};
+
+export default App;
\ No newline at end of file
diff --git a/client/src/components/common/ConnectionTest.jsx b/client/src/components/common/ConnectionTest.jsx
new file mode 100644
index 0000000..0ed4fbc
--- /dev/null
+++ b/client/src/components/common/ConnectionTest.jsx
@@ -0,0 +1,109 @@
+import { useState } from 'react';
+
+const ConnectionTest = () => {
+ const [message, setMessage] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+
+ const testConnection = async () => {
+ setLoading(true);
+ setError('');
+ setMessage('');
+
+ try {
+ // Using relative URL because of Vite proxy
+ const response = await fetch('/api/health');
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const data = await response.json();
+ setMessage(data.message);
+
+ } catch (err) {
+ setError('Failed to connect to server: ' + err.message);
+ console.error('Connection test error:', err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
Backend Connection Test
+
+
+
+ {message && (
+
+ ✅ Success: {message}
+
+ )}
+
+ {error && (
+
+ ❌ Error: {error}
+
+ )}
+
+
+
Backend URL: http://localhost:5000
+
Frontend URL: http://localhost:5173
+
Using: Vite Proxy
+
+
+ );
+};
+
+const containerStyle = {
+ padding: '2rem',
+ backgroundColor: '#f8f9fa',
+ borderRadius: '8px',
+ margin: '2rem 0',
+};
+
+const buttonStyle = {
+ backgroundColor: '#007bff',
+ color: 'white',
+ padding: '0.75rem 1.5rem',
+ border: 'none',
+ borderRadius: '5px',
+ fontSize: '1rem',
+ cursor: 'pointer',
+ marginTop: '1rem',
+};
+
+const successStyle = {
+ marginTop: '1rem',
+ padding: '1rem',
+ backgroundColor: '#d4edda',
+ color: '#155724',
+ borderRadius: '5px',
+ border: '1px solid #c3e6cb',
+};
+
+const errorStyle = {
+ marginTop: '1rem',
+ padding: '1rem',
+ backgroundColor: '#f8d7da',
+ color: '#721c24',
+ borderRadius: '5px',
+ border: '1px solid #f5c6cb',
+};
+
+const infoStyle = {
+ marginTop: '1.5rem',
+ padding: '1rem',
+ backgroundColor: 'white',
+ borderRadius: '5px',
+ fontSize: '0.9rem',
+};
+
+export default ConnectionTest;
\ No newline at end of file
diff --git a/client/src/components/common/LoadingSpinner.jsx b/client/src/components/common/LoadingSpinner.jsx
new file mode 100644
index 0000000..e190072
--- /dev/null
+++ b/client/src/components/common/LoadingSpinner.jsx
@@ -0,0 +1,33 @@
+const LoadingSpinner = ({ message = 'Loading...' }) => {
+ return (
+
+ );
+};
+
+const containerStyle = {
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'center',
+ minHeight: '80vh',
+};
+
+const spinnerStyle = {
+ width: '50px',
+ height: '50px',
+ border: '4px solid #f3f3f3',
+ borderTop: '4px solid #007bff',
+ borderRadius: '50%',
+ animation: 'spin 1s linear infinite',
+};
+
+const textStyle = {
+ marginTop: '1rem',
+ color: '#666',
+ fontSize: '1.1rem',
+};
+
+export default LoadingSpinner;
\ No newline at end of file
diff --git a/client/src/components/common/ProtectedRoute.jsx b/client/src/components/common/ProtectedRoute.jsx
new file mode 100644
index 0000000..4b8ba2b
--- /dev/null
+++ b/client/src/components/common/ProtectedRoute.jsx
@@ -0,0 +1,23 @@
+import { Navigate, useLocation } from 'react-router-dom';
+import { useAuth } from '../../context/AuthContext';
+import LoadingSpinner from './LoadingSpinner';
+
+const ProtectedRoute = ({ children }) => {
+ const { user, loading } = useAuth();
+ const location = useLocation();
+
+ // Show loading while checking authentication
+ if (loading) {
+ return
;
+ }
+
+ // If not authenticated, redirect to login
+ if (!user) {
+ return
;
+ }
+
+ // If authenticated, render the protected content
+ return children;
+};
+
+export default ProtectedRoute;
\ No newline at end of file
diff --git a/client/src/components/common/PublicRoute.jsx b/client/src/components/common/PublicRoute.jsx
new file mode 100644
index 0000000..15dcdb7
--- /dev/null
+++ b/client/src/components/common/PublicRoute.jsx
@@ -0,0 +1,27 @@
+import { Navigate } from 'react-router-dom';
+import { useAuth } from '../../context/AuthContext';
+
+const PublicRoute = ({ children }) => {
+ const { user, loading } = useAuth();
+
+ if (loading) {
+ return
Loading...
;
+ }
+
+ // If already logged in, redirect to dashboard
+ if (user) {
+ return
;
+ }
+
+ // If not logged in, show the public page
+ return children;
+};
+
+const loadingStyle = {
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ minHeight: '80vh',
+};
+
+export default PublicRoute;
\ No newline at end of file
diff --git a/client/src/components/layout/Footer.jsx b/client/src/components/layout/Footer.jsx
new file mode 100644
index 0000000..51ed6d8
--- /dev/null
+++ b/client/src/components/layout/Footer.jsx
@@ -0,0 +1,26 @@
+const Footer = () => {
+ return (
+
+ );
+};
+
+const footerStyle = {
+ backgroundColor: '#333',
+ color: 'white',
+ padding: '2rem 0',
+ marginTop: 'auto',
+ textAlign: 'center',
+};
+
+const containerStyle = {
+ maxWidth: '1200px',
+ margin: '0 auto',
+ padding: '0 2rem',
+};
+
+export default Footer;
\ No newline at end of file
diff --git a/client/src/components/layout/Header.jsx b/client/src/components/layout/Header.jsx
new file mode 100644
index 0000000..acb3fce
--- /dev/null
+++ b/client/src/components/layout/Header.jsx
@@ -0,0 +1,98 @@
+import { Link } from 'react-router-dom';
+import { useAuth } from '../../context/AuthContext';
+
+const Header = () => {
+ // Grab the auth state from our global context
+ const { user, logout, isAuthenticated } = useAuth();
+
+ return (
+
+
+
+
+ Creators Platform
+
+
+
+
+
+
+ );
+};
+
+// --- STYLES ---
+
+const headerStyle = {
+ backgroundColor: '#212529',
+ padding: '1rem 0',
+ color: 'white',
+ boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
+};
+
+const containerStyle = {
+ maxWidth: '1200px',
+ margin: '0 auto',
+ padding: '0 2rem',
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center'
+};
+
+const logoStyle = {
+ margin: 0,
+ fontSize: '1.5rem'
+};
+
+const linkStyle = {
+ color: 'white',
+ textDecoration: 'none',
+ fontWeight: 'bold'
+};
+
+const navStyle = {
+ display: 'flex',
+ alignItems: 'center',
+ gap: '1.5rem',
+};
+
+const navLinkStyle = {
+ color: '#ced4da',
+ textDecoration: 'none',
+ fontSize: '0.9rem'
+};
+
+const userNameStyle = {
+ color: '#ffc107', // Highlight the name in gold/yellow
+ fontSize: '0.9rem',
+ fontWeight: '500'
+};
+
+const logoutBtnStyle = {
+ padding: '0.4rem 0.8rem',
+ backgroundColor: '#dc3545',
+ color: 'white',
+ border: 'none',
+ borderRadius: '4px',
+ cursor: 'pointer',
+ fontSize: '0.85rem'
+};
+
+export default Header;
\ No newline at end of file
diff --git a/client/src/context/AuthContext.jsx b/client/src/context/AuthContext.jsx
new file mode 100644
index 0000000..51d9c4c
--- /dev/null
+++ b/client/src/context/AuthContext.jsx
@@ -0,0 +1,66 @@
+import { createContext, useState, useEffect, useContext } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+const AuthContext = createContext(null);
+
+export const AuthProvider = ({ children }) => {
+ const [user, setUser] = useState(null);
+ const [token, setToken] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const storedToken = localStorage.getItem('token');
+ const storedUser = localStorage.getItem('user');
+
+ if (storedToken && storedUser) {
+ try {
+ setToken(storedToken);
+ setUser(JSON.parse(storedUser));
+ } catch (error) {
+ console.error('Auth error:', error);
+ localStorage.removeItem('token');
+ localStorage.removeItem('user');
+ }
+ }
+ setLoading(false);
+ }, []);
+
+ const login = (userData, userToken) => {
+ setUser(userData);
+ setToken(userToken);
+ localStorage.setItem('token', userToken);
+ localStorage.setItem('user', JSON.stringify(userData));
+ };
+
+ const logout = () => {
+ setUser(null);
+ setToken(null);
+ localStorage.removeItem('token');
+ localStorage.removeItem('user');
+ navigate('/login');
+ };
+
+ const value = {
+ user,
+ token,
+ loading,
+ login,
+ logout,
+ isAuthenticated: () => !!token && !!user
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error('useAuth must be used within AuthProvider');
+ }
+ return context;
+};
\ No newline at end of file
diff --git a/client/src/index.css b/client/src/index.css
index 08a3ac9..f316def 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -1,68 +1,35 @@
-:root {
- font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
- line-height: 1.5;
- font-weight: 400;
-
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #242424;
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
- font-synthesis: none;
- text-rendering: optimizeLegibility;
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
+ background-color: #f8f9fa;
}
-a {
- font-weight: 500;
- color: #646cff;
- text-decoration: inherit;
-}
-a:hover {
- color: #535bf2;
+h1, h2, h3, h4, h5, h6 {
+ margin-bottom: 0.5rem;
}
-body {
- margin: 0;
- display: flex;
- place-items: center;
- min-width: 320px;
- min-height: 100vh;
-}
-
-h1 {
- font-size: 3.2em;
- line-height: 1.1;
+p {
+ margin-bottom: 1rem;
}
-button {
- border-radius: 8px;
- border: 1px solid transparent;
- padding: 0.6em 1.2em;
- font-size: 1em;
- font-weight: 500;
- font-family: inherit;
- background-color: #1a1a1a;
- cursor: pointer;
- transition: border-color 0.25s;
-}
-button:hover {
- border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
- outline: 4px auto -webkit-focus-ring-color;
+a {
+ color: #007bff;
+ text-decoration: none;
}
-@media (prefers-color-scheme: light) {
- :root {
- color: #213547;
- background-color: #ffffff;
- }
- a:hover {
- color: #747bff;
- }
- button {
- background-color: #f9f9f9;
- }
+a:hover {
+ text-decoration: underline;
}
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
\ No newline at end of file
diff --git a/client/src/main.jsx b/client/src/main.jsx
index b9a1a6d..4a0a381 100644
--- a/client/src/main.jsx
+++ b/client/src/main.jsx
@@ -1,10 +1,13 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import './index.css'
-import App from './App.jsx'
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+import App from './App';
+import './index.css';
-createRoot(document.getElementById('root')).render(
-
-
- ,
-)
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+
+
+
+);
\ No newline at end of file
diff --git a/client/src/pages/Dashboard.jsx b/client/src/pages/Dashboard.jsx
new file mode 100644
index 0000000..2fb04b7
--- /dev/null
+++ b/client/src/pages/Dashboard.jsx
@@ -0,0 +1,146 @@
+import { useNavigate, Navigate } from "react-router-dom";
+// 1. Import the useAuth hook
+import { useAuth } from "../context/AuthContext";
+import api from "../services/api";
+
+function Dashboard() {
+ // 2. Grab user, logout, and loading from our new Context
+ const { user, logout, loading } = useAuth();
+ const navigate = useNavigate();
+
+ // 3. Instead of the manual useEffect, we use the loading state from Context
+ if (loading) {
+ return (
+
+ Loading...
+
+ );
+ }
+
+ // 4. If there is no user, send them to login automatically
+ if (!user) {
+ return
;
+ }
+ const fetchUsers = async () => {
+ try {
+ const response = await api.get("/api/users");
+ const users = response.data;
+ console.log(users);
+ } catch (error) {
+ console.error("Error fetching users:", error);
+ }
+ };
+
+ // Update Profile
+ const updateProfile = async () => {
+ try {
+ const response = await api.put("/api/profile", {
+ name: "Anisha",
+ bio: "Full Stack Developer"
+ });
+
+ console.log("Profile updated:", response.data);
+ } catch (error) {
+ console.error("Update failed:", error);
+ }
+ };
+
+ // Delete Post
+ const deletePost = async (id) => {
+ try {
+ await api.delete(`/api/posts/${id}`);
+ console.log("Post deleted");
+ } catch (error) {
+ console.error("Delete failed:", error);
+ }
+ };
+
+
+ return (
+
+
+
Welcome, {user.name}!
+ {/* 5. Use the logout function from context */}
+
+
+
+
+
+
Your Account
+
+
Name: {user.name}
+
Email: {user.email}
+
+ Member Since:{" "}
+ {user.createdAt
+ ? new Date(user.createdAt).toLocaleDateString()
+ : "N/A"}
+
+
+
+
+
+
Dashboard Features
+
This is your personalized dashboard. Future features will include:
+
+ - Create and manage your content
+ - View your statistics
+ - Edit your profile
+ - See your activity
+
+
+
+
+ );
+}
+
+// --- STYLES (Stay exactly the same) ---
+const containerStyle = {
+ minHeight: "80vh",
+ padding: "2rem",
+ maxWidth: "1200px",
+ margin: "0 auto",
+};
+
+const headerStyle = {
+ display: "flex",
+ justifyContent: "space-between",
+ alignItems: "center",
+ marginBottom: "2rem",
+ padding: "1rem",
+ backgroundColor: "white",
+ borderRadius: "8px",
+ boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
+};
+
+const logoutButtonStyle = {
+ padding: "0.5rem 1.5rem",
+ backgroundColor: "#dc3545",
+ color: "white",
+ border: "none",
+ borderRadius: "5px",
+ cursor: "pointer",
+ fontWeight: "500",
+};
+
+const contentStyle = {
+ display: "grid",
+ gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
+ gap: "2rem",
+};
+
+const cardStyle = {
+ padding: "2rem",
+ backgroundColor: "white",
+ borderRadius: "8px",
+ boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
+};
+
+const infoStyle = {
+ marginTop: "1rem",
+ lineHeight: "2",
+};
+
+export default Dashboard;
\ No newline at end of file
diff --git a/client/src/pages/Home.jsx b/client/src/pages/Home.jsx
new file mode 100644
index 0000000..903708f
--- /dev/null
+++ b/client/src/pages/Home.jsx
@@ -0,0 +1,117 @@
+import { Link } from 'react-router-dom';
+import ConnectionTest from '../components/common/ConnectionTest';
+
+const Home = () => {
+ return (
+
+
+
{/* Platform Name/Tagline */}
+
+ {/* Brief description of what your platform does */}
+
+
+
+ Get Started
+
+
+ Login
+
+
+
+ {/* Backend Connection Test */}
+
+
+
+
+ {/* Features Section (Optional) */}
+
+
Why Choose {/* Platform Name */}?
+
+ {/* Add 3 feature cards based on your theme */}
+
+
Feature 1
+
Description
+
+
+
Feature 2
+
Description
+
+
+
Feature 3
+
Description
+
+
+
+
+ );
+};
+
+const containerStyle = {
+ minHeight: '100vh',
+};
+
+const heroStyle = {
+ textAlign: 'center',
+ padding: '4rem 2rem',
+ backgroundColor: '#f5f5f5',
+};
+
+const titleStyle = {
+ fontSize: '3rem',
+ marginBottom: '1rem',
+ color: '#333',
+};
+
+const subtitleStyle = {
+ fontSize: '1.25rem',
+ color: '#666',
+ marginBottom: '2rem',
+};
+
+const ctaStyle = {
+ display: 'flex',
+ gap: '1rem',
+ justifyContent: 'center',
+};
+
+const buttonStyle = {
+ backgroundColor: '#007bff',
+ color: 'white',
+ padding: '0.75rem 2rem',
+ borderRadius: '5px',
+ textDecoration: 'none',
+ fontWeight: 'bold',
+};
+
+const secondaryButtonStyle = {
+ backgroundColor: 'white',
+ color: '#007bff',
+ padding: '0.75rem 2rem',
+ borderRadius: '5px',
+ textDecoration: 'none',
+ fontWeight: 'bold',
+ border: '2px solid #007bff',
+};
+
+const featuresStyle = {
+ padding: '4rem 2rem',
+ textAlign: 'center',
+};
+
+const featureGridStyle = {
+ display: 'grid',
+ gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
+ gap: '2rem',
+ marginTop: '2rem',
+ maxWidth: '1200px',
+ margin: '2rem auto',
+};
+
+const featureCardStyle = {
+ padding: '2rem',
+ backgroundColor: 'white',
+ borderRadius: '8px',
+ boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
+};
+
+export default Home;
\ No newline at end of file
diff --git a/client/src/pages/Login.jsx b/client/src/pages/Login.jsx
new file mode 100644
index 0000000..fb6a548
--- /dev/null
+++ b/client/src/pages/Login.jsx
@@ -0,0 +1,259 @@
+import { useState } from 'react';
+import { useLocation, useNavigate, Link } from 'react-router-dom';
+import { useAuth } from '../context/AuthContext';
+import api from "../services/api";
+
+const Login = () => {
+ const location = useLocation();
+ const navigate = useNavigate();
+ const { login } = useAuth();
+
+ const [formData, setFormData] = useState({
+ email: '',
+ password: ''
+ });
+
+
+ const [errors, setErrors] = useState({});
+ const [isLoading, setIsLoading] = useState(false);
+ const [apiError, setApiError] = useState('');
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+
+ setFormData(prev => ({
+ ...prev,
+ [name]: value
+ }));
+
+ if (errors[name]) {
+ setErrors(prev => ({
+ ...prev,
+ [name]: ''
+ }));
+ }
+
+ if (apiError) setApiError('');
+ };
+
+ const validateForm = () => {
+ const newErrors = {};
+
+ if (!formData.email.trim()) {
+ newErrors.email = 'Email is required';
+ }
+ else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
+ newErrors.email = 'Please enter a valid email';
+ }
+
+ if (!formData.password) {
+ newErrors.password = 'Password is required';
+ }
+
+ setErrors(newErrors);
+
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setApiError('');
+ if (!validateForm()) return;
+ setIsLoading(true);
+ try {
+ const response = await api.post('/api/auth/login', {
+ email: formData.email,
+ password: formData.password
+ });
+ const data = response.data;
+ if (response.status === 200) {
+ login(data.user, data.token);
+ setFormData({ email: '', password: '' });
+ const from = location.state?.from?.pathname || '/dashboard';
+ navigate(from, { replace: true });
+ }
+ } catch (error) {
+ console.error('Login error:', error);
+ setApiError('Unable to connect to server. Please try again.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+
+ return (
+
+
+
Welcome Back
+
Login to your Creators Platform account
+
+ {apiError &&
{apiError}
}
+
+
+
+
+ Don't have an account?
+ Sign up here
+
+
+
+
+ );
+};
+
+const containerStyle = {
+ minHeight: '80vh',
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: '2rem',
+ backgroundColor: '#f8f9fa'
+};
+
+const formContainerStyle = {
+ maxWidth: '400px',
+ width: '100%',
+ padding: '2.5rem',
+ backgroundColor: 'white',
+ borderRadius: '10px',
+ boxShadow: '0 4px 6px rgba(0,0,0,0.1)'
+};
+
+const titleStyle = {
+ textAlign: 'center',
+ marginBottom: '0.5rem',
+ color: '#333',
+ fontSize: '2rem'
+};
+
+const subtitleStyle = {
+ textAlign: 'center',
+ color: '#666',
+ marginBottom: '2rem',
+ fontSize: '0.95rem'
+};
+
+const formStyle = {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '1.5rem'
+};
+
+const fieldStyle = {
+ display: 'flex',
+ flexDirection: 'column'
+};
+
+const labelStyle = {
+ marginBottom: '0.5rem',
+ fontWeight: '500',
+ color: '#333',
+ fontSize: '0.9rem'
+};
+
+const inputStyle = {
+ padding: '0.75rem',
+ fontSize: '1rem',
+ border: '1px solid #ddd',
+ borderRadius: '5px'
+};
+
+const inputErrorStyle = {
+ ...inputStyle,
+ borderColor: '#dc3545'
+};
+
+const errorTextStyle = {
+ color: '#dc3545',
+ fontSize: '0.85rem',
+ marginTop: '0.25rem'
+};
+
+const buttonStyle = {
+ padding: '0.875rem',
+ fontSize: '1rem',
+ fontWeight: 'bold',
+ color: 'white',
+ backgroundColor: '#007bff',
+ border: 'none',
+ borderRadius: '5px',
+ cursor: 'pointer',
+ marginTop: '0.5rem'
+};
+
+const buttonDisabledStyle = {
+ ...buttonStyle,
+ backgroundColor: '#6c757d',
+ cursor: 'not-allowed'
+};
+
+const errorMessageStyle = {
+ padding: '1rem',
+ backgroundColor: '#f8d7da',
+ color: '#721c24',
+ borderRadius: '5px',
+ marginBottom: '1rem',
+ border: '1px solid #f5c6cb'
+};
+
+const linkTextStyle = {
+ textAlign: 'center',
+ marginTop: '1.5rem',
+ color: '#666'
+};
+
+const linkStyle = {
+ color: '#007bff',
+ textDecoration: 'none',
+ fontWeight: '500'
+};
+
+export default Login;
\ No newline at end of file
diff --git a/client/src/pages/Register.jsx b/client/src/pages/Register.jsx
new file mode 100644
index 0000000..dc70a82
--- /dev/null
+++ b/client/src/pages/Register.jsx
@@ -0,0 +1,395 @@
+import { useState } from 'react';
+import { Link, useNavigate } from 'react-router-dom';
+
+const Register = () => {
+ // Form field states
+ const [formData, setFormData] = useState({
+ name: '',
+ email: '',
+ password: '',
+ confirmPassword: ''
+ });
+
+ // UI states
+ const [errors, setErrors] = useState({});
+ const [isLoading, setIsLoading] = useState(false);
+ const [successMessage, setSuccessMessage] = useState('');
+ const [apiError, setApiError] = useState('');
+
+ // For navigation after successful registration
+ const navigate = useNavigate();
+
+ // Handle input change
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+
+ setFormData(prev => ({
+ ...prev,
+ [name]: value
+ }));
+
+ // Clear error for this field when user starts typing
+ if (errors[name]) {
+ setErrors(prev => ({
+ ...prev,
+ [name]: ''
+ }));
+ }
+ };
+
+ // Validate form fields
+ const validateForm = () => {
+ const newErrors = {};
+
+ // Name validation
+ if (!formData.name.trim()) {
+ newErrors.name = 'Name is required';
+ } else if (formData.name.trim().length < 2) {
+ newErrors.name = 'Name must be at least 2 characters';
+ } else if (formData.name.trim().length > 50) {
+ newErrors.name = 'Name cannot exceed 50 characters';
+ }
+
+ // Email validation
+ if (!formData.email.trim()) {
+ newErrors.email = 'Email is required';
+ } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
+ newErrors.email = 'Please enter a valid email address';
+ }
+
+ // Password validation
+ if (!formData.password) {
+ newErrors.password = 'Password is required';
+ } else if (formData.password.length < 6) {
+ newErrors.password = 'Password must be at least 6 characters';
+ }
+
+ // Confirm password validation
+ if (!formData.confirmPassword) {
+ newErrors.confirmPassword = 'Please confirm your password';
+ } else if (formData.password !== formData.confirmPassword) {
+ newErrors.confirmPassword = 'Passwords do not match';
+ }
+
+ setErrors(newErrors);
+
+ // Return true if no errors
+ return Object.keys(newErrors).length === 0;
+ };
+
+ // Handle form submission
+ const handleSubmit = async (e) => {
+ e.preventDefault(); // Prevent page reload
+
+ // Clear previous messages
+ setSuccessMessage('');
+ setApiError('');
+
+ // Validate form
+ if (!validateForm()) {
+ return; // Stop if validation fails
+ }
+
+ // Start loading
+ setIsLoading(true);
+
+ try {
+ // Prepare data to send (exclude confirmPassword)
+ const registrationData = {
+ name: formData.name.trim(),
+ email: formData.email.trim().toLowerCase(),
+ password: formData.password
+ };
+
+ // Send POST request to backend
+ const response = await fetch('/api/users/register', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(registrationData)
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ // Registration successful
+ setSuccessMessage('Account created successfully! Redirecting to login...');
+
+ // Clear form
+ setFormData({
+ name: '',
+ email: '',
+ password: '',
+ confirmPassword: ''
+ });
+
+ // Redirect to login page after 2 seconds
+ setTimeout(() => {
+ navigate('/login');
+ }, 2000);
+
+ } else {
+ // Registration failed - show error from backend
+ setApiError(data.message || 'Registration failed. Please try again.');
+ }
+
+ } catch (error) {
+ // Network or other error
+ console.error('Registration error:', error);
+ setApiError('Unable to connect to server. Please check your connection and try again.');
+ } finally {
+ // Stop loading regardless of success/failure
+ setIsLoading(false);
+ }
+ };
+
+ // JSX for registration form
+ return (
+
+
+
Create Your Account
+
+ Join Your Platform Name and start creating today
+
+
+ {/* Success Message */}
+ {successMessage && (
+
+ {successMessage}
+
+ )}
+
+ {/* API Error Message */}
+ {apiError && (
+
+ {apiError}
+
+ )}
+
+ {/* Registration Form */}
+
+
+ {/* Login Link */}
+
+ Already have an account?{' '}
+
+ Login here
+
+
+
+
+ );
+};
+
+export default Register;
+
+// ===================== STYLES =====================
+
+const containerStyle = {
+ minHeight: '80vh',
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: '2rem',
+ backgroundColor: '#f8f9fa',
+};
+
+const formContainerStyle = {
+ maxWidth: '450px',
+ width: '100%',
+ padding: '2.5rem',
+ backgroundColor: 'white',
+ borderRadius: '10px',
+ boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
+};
+
+const titleStyle = {
+ textAlign: 'center',
+ marginBottom: '0.5rem',
+ color: '#333',
+ fontSize: '2rem',
+};
+
+const subtitleStyle = {
+ textAlign: 'center',
+ color: '#666',
+ marginBottom: '2rem',
+ fontSize: '0.95rem',
+};
+
+const formStyle = {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '1.5rem',
+};
+
+const fieldStyle = {
+ display: 'flex',
+ flexDirection: 'column',
+};
+
+const labelStyle = {
+ marginBottom: '0.5rem',
+ fontWeight: '500',
+ color: '#333',
+ fontSize: '0.9rem',
+};
+
+const inputStyle = {
+ padding: '0.75rem',
+ fontSize: '1rem',
+ border: '1px solid #ddd',
+ borderRadius: '5px',
+ transition: 'border-color 0.3s',
+};
+
+const inputErrorStyle = {
+ ...inputStyle,
+ borderColor: '#dc3545',
+};
+
+const errorTextStyle = {
+ color: '#dc3545',
+ fontSize: '0.85rem',
+ marginTop: '0.25rem',
+};
+
+const buttonStyle = {
+ padding: '0.875rem',
+ fontSize: '1rem',
+ fontWeight: 'bold',
+ color: 'white',
+ backgroundColor: '#007bff',
+ border: 'none',
+ borderRadius: '5px',
+ cursor: 'pointer',
+ transition: 'background-color 0.3s',
+ marginTop: '0.5rem',
+};
+
+const buttonDisabledStyle = {
+ ...buttonStyle,
+ backgroundColor: '#6c757d',
+ cursor: 'not-allowed',
+};
+
+const successStyle = {
+ padding: '1rem',
+ backgroundColor: '#d4edda',
+ color: '#155724',
+ borderRadius: '5px',
+ marginBottom: '1rem',
+ border: '1px solid #c3e6cb',
+};
+
+const errorMessageStyle = {
+ padding: '1rem',
+ backgroundColor: '#f8d7da',
+ color: '#721c24',
+ borderRadius: '5px',
+ marginBottom: '1rem',
+ border: '1px solid #f5c6cb',
+};
+
+const linkTextStyle = {
+ textAlign: 'center',
+ marginTop: '1.5rem',
+ color: '#666',
+};
+
+const linkStyle = {
+ color: '#007bff',
+ textDecoration: 'none',
+ fontWeight: '500',
+};
\ No newline at end of file
diff --git a/client/src/services/api.js b/client/src/services/api.js
new file mode 100644
index 0000000..b792d93
--- /dev/null
+++ b/client/src/services/api.js
@@ -0,0 +1,41 @@
+import axios from 'axios';
+
+// Create instance
+const api = axios.create({
+ baseURL: import.meta.env.VITE_API_URL || 'http://localhost:5000',
+ timeout: 10000,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
+// Request interceptor - attach token
+api.interceptors.request.use(
+ (config) => {
+ const token = localStorage.getItem('token');
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+ },
+ (error) => {
+ return Promise.reject(error);
+ }
+);
+
+// Response interceptor - handle errors
+api.interceptors.response.use(
+ (response) => {
+ return response;
+ },
+ (error) => {
+ if (error.response?.status === 401) {
+ localStorage.removeItem('token');
+ localStorage.removeItem('user');
+ window.location.href = '/login';
+ }
+ return Promise.reject(error);
+ }
+);
+
+export default api;
\ No newline at end of file
diff --git a/client/vite.config.js b/client/vite.config.js
index 8b0f57b..7cd1ac0 100644
--- a/client/vite.config.js
+++ b/client/vite.config.js
@@ -1,7 +1,15 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
-// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
-})
+ server: {
+ proxy: {
+ '/api': {
+ target: 'http://localhost:5000',
+ changeOrigin: true,
+ secure: false,
+ },
+ },
+ },
+});
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 9bc2fe8..997a1c2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,6 +7,11 @@
"": {
"name": "creators-platform",
"version": "1.0.0",
+ "dependencies": {
+ "cors": "^2.8.6",
+ "prop-types": "^15.8.1",
+ "react-router-dom": "^7.13.1"
+ },
"devDependencies": {
"concurrently": "^8.2.2"
}
@@ -140,6 +145,36 @@
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
}
},
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
@@ -204,6 +239,12 @@
"node": ">=8"
}
},
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
"node_modules/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
@@ -211,6 +252,105 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
+ "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
+ "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.4"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
+ "node_modules/react-router": {
+ "version": "7.13.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz",
+ "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.13.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz",
+ "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.13.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -231,6 +371,18 @@
"tslib": "^2.1.0"
}
},
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
"node_modules/shell-quote": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
@@ -311,6 +463,15 @@
"dev": true,
"license": "0BSD"
},
+ "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/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
diff --git a/package.json b/package.json
index 95496b4..63be358 100644
--- a/package.json
+++ b/package.json
@@ -9,5 +9,10 @@
},
"devDependencies": {
"concurrently": "^8.2.2"
+ },
+ "dependencies": {
+ "cors": "^2.8.6",
+ "prop-types": "^15.8.1",
+ "react-router-dom": "^7.13.1"
}
-}
\ No newline at end of file
+}
diff --git a/server/controllers/authController.js b/server/controllers/authController.js
new file mode 100644
index 0000000..41eb3e6
--- /dev/null
+++ b/server/controllers/authController.js
@@ -0,0 +1,56 @@
+import jwt from "jsonwebtoken";
+import bcrypt from "bcryptjs";
+import User from "../models/User.js";
+
+export const loginUser = async (req, res) => {
+ try {
+ const { email, password } = req.body || {};
+
+ if (!email || !password) {
+ return res.status(400).json({
+ success: false,
+ message: "Please provide email and password"
+ });
+ }
+
+ const user = await User.findOne({ email }).select("+password");
+
+ if (!user) {
+ return res.status(401).json({
+ success: false,
+ message: "Invalid credentials"
+ });
+ }
+
+ const isMatch = await bcrypt.compare(password, user.password);
+ console.log("LOGIN SECRET:", process.env.JWT_SECRET);
+
+ if (!isMatch) {
+ return res.status(401).json({
+ success: false,
+ message: "Invalid credentials"
+ });
+ }
+
+ const token = jwt.sign(
+ { id: user._id, email: user.email },
+ process.env.JWT_SECRET,
+ { expiresIn: process.env.JWT_EXPIRE }
+ );
+
+ user.password = undefined;
+
+ res.status(200).json({
+ success: true,
+ token,
+ data: user
+ });
+
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ message: "Server error during login",
+ error: error.message
+ });
+ }
+};
\ No newline at end of file
diff --git a/server/controllers/postController.js b/server/controllers/postController.js
new file mode 100644
index 0000000..e3e4676
--- /dev/null
+++ b/server/controllers/postController.js
@@ -0,0 +1,89 @@
+import Post from '../models/Post.js';
+
+// @desc Create new post
+// @route POST /api/posts
+// @access Private
+export const createPost = async (req, res) => {
+ try {
+ const { title, content, category, status } = req.body;
+
+ // Validate required fields
+ if (!title || !content) {
+ return res.status(400).json({
+ success: false,
+ message: 'Please provide title and content'
+ });
+ }
+
+ // Create post with authenticated user as author
+ const post = await Post.create({
+ title,
+ content,
+ category,
+ status,
+ author: req.user._id // From protect middleware
+ });
+
+ res.status(201).json({
+ success: true,
+ message: 'Post created successfully',
+ data: post
+ });
+
+ } catch (error) {
+ console.error('Create post error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error creating post',
+ error: error.message
+ });
+ }
+};
+
+// @desc Get posts with pagination
+// @route GET /api/posts?page=1&limit=10
+// @access Private
+export const getPosts = async (req, res) => {
+ try {
+ // Get page and limit from query params (with defaults)
+ const page = parseInt(req.query.page) || 1;
+ const limit = parseInt(req.query.limit) || 10;
+
+ // Calculate skip value
+ const skip = (page - 1) * limit;
+
+ // Get posts for logged-in user only
+ const posts = await Post.find({ author: req.user._id })
+ .sort({ createdAt: -1 }) // Newest first
+ .skip(skip)
+ .limit(limit)
+ .populate('author', 'name email'); // Include author info
+
+ // Get total count for pagination
+ const total = await Post.countDocuments({ author: req.user._id });
+
+ // Calculate total pages
+ const totalPages = Math.ceil(total / limit);
+
+ res.status(200).json({
+ success: true,
+ data: posts,
+ pagination: {
+ page,
+ limit,
+ total,
+ totalPages,
+ hasNextPage: page < totalPages,
+ hasPrevPage: page > 1
+ }
+ });
+
+ } catch (error) {
+ console.error('Get posts error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error fetching posts',
+ error: error.message
+ });
+ }
+};
\ No newline at end of file
diff --git a/server/controllers/userController.js b/server/controllers/userController.js
index 4544257..10467d6 100644
--- a/server/controllers/userController.js
+++ b/server/controllers/userController.js
@@ -1,5 +1,6 @@
import User from '../models/user.js';
import bcrypt from 'bcrypt';
+import jwt from 'jsonwebtoken';
/* ===========================
Register User
@@ -20,7 +21,7 @@ const registerUser = async (req, res) => {
if (existingUser) {
return res.status(400).json({
success: false,
- message: 'User already exists with this email'
+ message: 'User already exists'
});
}
@@ -36,157 +37,99 @@ const registerUser = async (req, res) => {
res.status(201).json({
success: true,
- message: 'User registered successfully',
data: user
});
} catch (error) {
res.status(500).json({
success: false,
- message: 'Server error during registration',
- error: error.message
+ message: error.message
});
}
};
-
/* ===========================
- Get All Users
+ Login User ✅ ONLY ONCE
=========================== */
-const getAllUsers = async (req, res) => {
+const loginUser = async (req, res) => {
try {
- const users = await User.find().select('-password');
-
- res.status(200).json({
- success: true,
- count: users.length,
- data: users
- });
-
- } catch (error) {
- res.status(500).json({
- success: false,
- message: 'Error fetching users',
- error: error.message
- });
- }
-};
-
+ const { email, password } = req.body || {};
-/* ===========================
- Get Single User
-=========================== */
-const getUserById = async (req, res) => {
- try {
- const { id } = req.params;
-
- const user = await User.findById(id).select('-password');
-
- if (!user) {
- return res.status(404).json({
+ if (!email || !password) {
+ return res.status(400).json({
success: false,
- message: 'User not found'
+ message: 'Email and password required'
});
}
- res.status(200).json({
- success: true,
- data: user
- });
-
- } catch (error) {
- res.status(500).json({
- success: false,
- message: 'Error fetching user',
- error: error.message
- });
- }
-};
-
-
-/* ===========================
- Update User
-=========================== */
-const updateUser = async (req, res) => {
- try {
- const { id } = req.params;
- const { name, email } = req.body || {};
+ const user = await User.findOne({ email });
- if (!name && !email) {
- return res.status(400).json({
+ if (!user) {
+ return res.status(401).json({
success: false,
- message: 'Please provide at least one field to update'
+ message: 'Invalid credentials'
});
}
- const user = await User.findById(id);
+ const isMatch = await bcrypt.compare(password, user.password);
- if (!user) {
- return res.status(404).json({
+ if (!isMatch) {
+ return res.status(401).json({
success: false,
- message: 'User not found'
+ message: 'Invalid credentials'
});
}
- if (name) user.name = name;
- if (email) user.email = email;
-
- await user.save();
-
- user.password = undefined;
+ const token = jwt.sign(
+ { id: user._id, name: user.name },
+ process.env.JWT_SECRET,
+ { expiresIn: '2h' }
+ );
res.status(200).json({
success: true,
- message: 'User updated successfully',
- data: user
+ token
});
} catch (error) {
res.status(500).json({
success: false,
- message: 'Error updating user',
- error: error.message
+ message: error.message
});
}
};
-
/* ===========================
- Delete User
+ Other Controllers
=========================== */
-const deleteUser = async (req, res) => {
- try {
- const { id } = req.params;
-
- const user = await User.findByIdAndDelete(id);
-
- if (!user) {
- return res.status(404).json({
- success: false,
- message: 'User not found'
- });
- }
+const getAllUsers = async (req, res) => {
+ const users = await User.find().select('-password');
+ res.json(users);
+};
- res.status(200).json({
- success: true,
- message: 'User deleted successfully'
- });
+const getUserById = async (req, res) => {
+ const user = await User.findById(req.params.id).select('-password');
+ res.json(user);
+};
- } catch (error) {
- res.status(500).json({
- success: false,
- message: 'Error deleting user',
- error: error.message
- });
- }
+const updateUser = async (req, res) => {
+ const user = await User.findByIdAndUpdate(req.params.id, req.body, {
+ new: true
+ });
+ res.json(user);
};
+const deleteUser = async (req, res) => {
+ await User.findByIdAndDelete(req.params.id);
+ res.json({ message: 'User deleted' });
+};
/* ===========================
- EXPORTS
+ EXPORTS (ONLY ONCE)
=========================== */
export {
registerUser,
+ loginUser,
getAllUsers,
getUserById,
updateUser,
diff --git a/server/middleware/auth.js b/server/middleware/auth.js
new file mode 100644
index 0000000..303e714
--- /dev/null
+++ b/server/middleware/auth.js
@@ -0,0 +1,48 @@
+import jwt from 'jsonwebtoken';
+import User from '../models/User.js';
+
+export const protect = async (req, res, next) => {
+ try {
+ // Get token from header
+ let token;
+
+ if (req.headers.authorization &&
+ req.headers.authorization.startsWith('Bearer')) {
+ // Extract token from "Bearer
"
+ token = req.headers.authorization.split(' ')[1];
+ }
+
+ // Check if token exists
+ if (!token) {
+ return res.status(401).json({
+ success: false,
+ message: 'Not authorized, no token'
+ });
+ }
+
+ // Verify token
+ const decoded = jwt.verify(token, process.env.JWT_SECRET);
+
+ // Get user from token (exclude password)
+ req.user = await User.findById(decoded.id).select('-password');
+ console.log("LOGIN SECRET:", process.env.JWT_SECRET);
+
+ if (!req.user) {
+ return res.status(401).json({
+ success: false,
+ message: 'User not found'
+ });
+ }
+
+ // Continue to next middleware/route
+ next();
+
+ }catch (error) {
+ console.error("JWT ERROR:", error.message);
+
+ return res.status(401).json({
+ success: false,
+ message: error.message
+ });
+}
+};
\ No newline at end of file
diff --git a/server/middleware/authMiddleware.js b/server/middleware/authMiddleware.js
new file mode 100644
index 0000000..e69de29
diff --git a/server/models/Post.js b/server/models/Post.js
new file mode 100644
index 0000000..36d0b17
--- /dev/null
+++ b/server/models/Post.js
@@ -0,0 +1,40 @@
+import mongoose from 'mongoose';
+
+const postSchema = new mongoose.Schema(
+ {
+ title: {
+ type: String,
+ required: [true, 'Title is required'],
+ trim: true,
+ maxlength: [100, 'Title cannot exceed 100 characters']
+ },
+ content: {
+ type: String,
+ required: [true, 'Content is required'],
+ minlength: [10, 'Content must be at least 10 characters']
+ },
+ author: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'User',
+ required: true
+ },
+ category: {
+ type: String,
+ enum: ['Technology', 'Lifestyle', 'Travel', 'Food'],
+ default: 'Technology'
+ },
+ status: {
+ type: String,
+ enum: ['draft', 'published'],
+ default: 'draft'
+ }
+ },
+ {
+ timestamps: true // Adds createdAt and updatedAt
+ }
+);
+
+// ✅ Fix for OverwriteModelError and hot reload
+const Post = mongoose.models.Post || mongoose.model('Post', postSchema);
+
+export default Post;
\ No newline at end of file
diff --git a/server/models/user.js b/server/models/user.js
index 6838fda..f781fd7 100644
--- a/server/models/user.js
+++ b/server/models/user.js
@@ -24,14 +24,15 @@ const userSchema = new mongoose.Schema(
type: String,
required: [true, 'Password is required'],
minlength: [6, 'Password must be at least 6 characters'],
- select: false // Don't return password in queries by default
+ select: false
}
},
{
- timestamps: true // Automatically creates createdAt and updatedAt fields
+ timestamps: true
}
);
-const User = mongoose.model('User', userSchema);
+// ✅ Fix for OverwriteModelError
+const User = mongoose.models.User || mongoose.model('User', userSchema);
export default User;
\ No newline at end of file
diff --git a/server/package-lock.json b/server/package-lock.json
index 144a0a2..eb89826 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -10,9 +10,11 @@
"license": "ISC",
"dependencies": {
"bcrypt": "^6.0.0",
+ "bcryptjs": "^3.0.3",
"cors": "^2.8.6",
"dotenv": "^17.3.1",
"express": "^5.2.1",
+ "jsonwebtoken": "^9.0.3",
"mongoose": "^9.2.2"
},
"devDependencies": {
@@ -94,6 +96,15 @@
"node": ">= 18"
}
},
+ "node_modules/bcryptjs": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
+ "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
+ "license": "BSD-3-Clause",
+ "bin": {
+ "bcrypt": "bin/bcrypt"
+ }
+ },
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -166,6 +177,12 @@
"node": ">=20.19.0"
}
},
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -338,6 +355,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -723,6 +749,49 @@
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT"
},
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
+ "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
+ "license": "MIT",
+ "dependencies": {
+ "jws": "^4.0.1",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/jwa": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "^1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/kareem": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/kareem/-/kareem-3.2.0.tgz",
@@ -732,6 +801,48 @@
"node": ">=18.0.0"
}
},
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
+ "license": "MIT"
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -1152,6 +1263,26 @@
"node": ">= 18"
}
},
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -1162,7 +1293,6 @@
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
- "dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
diff --git a/server/package.json b/server/package.json
index 621a417..ea8aa54 100644
--- a/server/package.json
+++ b/server/package.json
@@ -13,9 +13,11 @@
"license": "ISC",
"dependencies": {
"bcrypt": "^6.0.0",
+ "bcryptjs": "^3.0.3",
"cors": "^2.8.6",
"dotenv": "^17.3.1",
"express": "^5.2.1",
+ "jsonwebtoken": "^9.0.3",
"mongoose": "^9.2.2"
},
"devDependencies": {
diff --git a/server/routes/authRoutes.js b/server/routes/authRoutes.js
new file mode 100644
index 0000000..e05582d
--- /dev/null
+++ b/server/routes/authRoutes.js
@@ -0,0 +1,8 @@
+import express from 'express';
+import { loginUser } from '../controllers/authController.js';
+
+const router = express.Router();
+
+router.post('/login', loginUser);
+
+export default router;
\ No newline at end of file
diff --git a/server/routes/postRoutes.js b/server/routes/postRoutes.js
new file mode 100644
index 0000000..e014f6b
--- /dev/null
+++ b/server/routes/postRoutes.js
@@ -0,0 +1,11 @@
+import express from 'express';
+import { protect } from '../middleware/auth.js';
+import { createPost, getPosts } from '../controllers/postController.js';
+
+const router = express.Router();
+
+// Both routes require authentication
+router.post('/', protect, createPost);
+router.get('/', protect, getPosts);
+
+export default router;
\ No newline at end of file
diff --git a/server/routes/userRoutes.js b/server/routes/userRoutes.js
index 736cdf5..9d9768d 100644
--- a/server/routes/userRoutes.js
+++ b/server/routes/userRoutes.js
@@ -1,6 +1,8 @@
import express from 'express';
+import { protect } from '../middleware/auth.js';
import {
registerUser,
+ loginUser,
getAllUsers,
getUserById,
updateUser,
@@ -9,11 +11,14 @@ import {
const router = express.Router();
-// User routes
+// Public routes
router.post('/register', registerUser);
-router.get('/', getAllUsers);
-router.get('/:id', getUserById);
-router.put('/:id', updateUser);
-router.delete('/:id', deleteUser);
+router.post('/login', loginUser);
+
+// Protected routes
+router.get('/', protect, getAllUsers);
+router.get('/:id', protect, getUserById);
+router.put('/:id', protect, updateUser);
+router.delete('/:id', protect, deleteUser);
export default router;
\ No newline at end of file
diff --git a/server/server.js b/server/server.js
index 73a42c3..bfc5028 100644
--- a/server/server.js
+++ b/server/server.js
@@ -3,33 +3,36 @@ import dotenv from 'dotenv';
import cors from 'cors';
import connectDB from './config/database.js';
import userRoutes from './routes/userRoutes.js';
+import authRoutes from './routes/authRoutes.js'; // Add this import
+import postRoutes from './routes/postRoutes.js';
-// Load environment variables
dotenv.config();
-
-// Connect to database
connectDB();
const app = express();
const PORT = process.env.PORT || 5000;
// Middleware
-app.use(cors());
+app.use(cors({
+ origin: process.env.CLIENT_URL || 'http://localhost:5173',
+ credentials: true
+}));
app.use(express.json());
+app.use('/api/posts', postRoutes);
+
// Routes
app.use('/api/users', userRoutes);
+app.use('/api/auth', authRoutes); // Add this line
-// Health check endpoint (keep this for testing)
+// Health check
app.get('/api/health', (req, res) => {
res.json({
message: 'Server is running!',
- timestamp: new Date(),
- database: 'Connected'
+ timestamp: new Date()
});
});
-// Start server
app.listen(PORT, () => {
console.log(`🚀 Server running on http://localhost:${PORT}`);
});
\ No newline at end of file