From 5ec47e51906a77f7b1530cfc361d701e21d77580 Mon Sep 17 00:00:00 2001 From: "ALMAZROUEI Shamma (2021) WKIS203" <shamma.almazrouei.2021@live.rhul.ac.uk> Date: Fri, 11 Apr 2025 14:14:02 +0530 Subject: [PATCH] Add authors, messages & profile page --- skillswap/package-lock.json | 612 +++++++++++++++++++++++ skillswap/package.json | 2 + skillswap/src/components/header.jsx | 14 + skillswap/src/components/ui/avatar.jsx | 33 ++ skillswap/src/components/ui/tabs.jsx | 41 ++ skillswap/src/components/ui/textarea.jsx | 18 + skillswap/src/index.css | 4 + skillswap/src/main.jsx | 15 + skillswap/src/pages/Authors.jsx | 163 ++++++ skillswap/src/pages/Messages.jsx | 282 +++++++++++ skillswap/src/pages/Profile.jsx | 424 ++++++++++++++++ 11 files changed, 1608 insertions(+) create mode 100644 skillswap/src/components/ui/avatar.jsx create mode 100644 skillswap/src/components/ui/tabs.jsx create mode 100644 skillswap/src/components/ui/textarea.jsx create mode 100644 skillswap/src/pages/Authors.jsx create mode 100644 skillswap/src/pages/Messages.jsx create mode 100644 skillswap/src/pages/Profile.jsx diff --git a/skillswap/package-lock.json b/skillswap/package-lock.json index 34febcd..664f6f6 100644 --- a/skillswap/package-lock.json +++ b/skillswap/package-lock.json @@ -8,9 +8,11 @@ "name": "skillswap", "version": "0.0.0", "dependencies": { + "@radix-ui/react-avatar": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.475.0", @@ -1092,6 +1094,230 @@ "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", "license": "MIT" }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.4.tgz", + "integrity": "sha512-+kBesLBzwqyDiYCtYFK+6Ktf+N7+Y6QOTUueLGLIbLZ/YeyFW6bsBGDsN+5HxHpM55C90u5fxsg0ErxzXTcwKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.0.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.3.tgz", + "integrity": "sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.3.tgz", + "integrity": "sha512-mM2pxoQw5HJ49rkzwOs7Y6J4oYH22wS8BfK2/bBxROlI4xuR0c4jEenQP63LlTlDkO6Buj2Vt+QYAYcOgqtrXA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.0.3", + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-primitive": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.3.tgz", + "integrity": "sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", @@ -1158,6 +1384,21 @@ } } }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-dismissable-layer": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", @@ -1337,6 +1578,180 @@ } } }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.3.tgz", + "integrity": "sha512-ufbpLUjZiOg4iYgb2hQrWXEPYX6jOLBbR27bDyAff5GYMRrCzcze8lukjuXVUQvJ6HZe8+oL+hhswDcjmcgVyg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.0.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.3.tgz", + "integrity": "sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.1.tgz", + "integrity": "sha512-YnEXIy8/ga01Y1PN0VfaNH//MhA91JlEGVBDxDzROqwrAtG5Yr2QGEPz8A/rJA3C7ZAHryOYGaUv8fLSW2H/mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", @@ -1355,6 +1770,203 @@ } } }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.4.tgz", + "integrity": "sha512-fuHMHWSf5SRhXke+DbHXj2wVMo+ghVH30vhX3XVacdXqDl+J4XWafMIGOOER861QpBx1jxgwKXL2dQnfrsd8MQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.3", + "@radix-ui/react-primitive": "2.0.3", + "@radix-ui/react-roving-focus": "1.1.3", + "@radix-ui/react-use-controllable-state": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-presence": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.3.tgz", + "integrity": "sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.3.tgz", + "integrity": "sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.1.tgz", + "integrity": "sha512-YnEXIy8/ga01Y1PN0VfaNH//MhA91JlEGVBDxDzROqwrAtG5Yr2QGEPz8A/rJA3C7ZAHryOYGaUv8fLSW2H/mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", diff --git a/skillswap/package.json b/skillswap/package.json index 18c5681..2200ff6 100644 --- a/skillswap/package.json +++ b/skillswap/package.json @@ -10,9 +10,11 @@ "preview": "vite preview" }, "dependencies": { + "@radix-ui/react-avatar": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.475.0", diff --git a/skillswap/src/components/header.jsx b/skillswap/src/components/header.jsx index 90cec16..2f9ac42 100644 --- a/skillswap/src/components/header.jsx +++ b/skillswap/src/components/header.jsx @@ -11,6 +11,20 @@ export default function Header() { <img src={Logo} className='h-16 w-auto' alt='SkillSwap Logo' /> </Link> + <ul className='flex items-center gap-8 font-medium'> + <li> + <Link to='/authors' className='hover:underline underline-offset-4'> + Authors + </Link> + </li> + + <li> + <Link to='/profile' className='hover:underline underline-offset-4'> + Profile + </Link> + </li> + </ul> + <div className='flex items-center gap-5'> <Button asChild> <Link to='/login'>Login</Link> diff --git a/skillswap/src/components/ui/avatar.jsx b/skillswap/src/components/ui/avatar.jsx new file mode 100644 index 0000000..9a2f853 --- /dev/null +++ b/skillswap/src/components/ui/avatar.jsx @@ -0,0 +1,33 @@ +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef(({ className, ...props }, ref) => ( + <AvatarPrimitive.Root + ref={ref} + className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)} + {...props} /> +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef(({ className, ...props }, ref) => ( + <AvatarPrimitive.Image + ref={ref} + className={cn("aspect-square h-full w-full", className)} + {...props} /> +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef(({ className, ...props }, ref) => ( + <AvatarPrimitive.Fallback + ref={ref} + className={cn( + "flex h-full w-full items-center justify-center rounded-full bg-muted", + className + )} + {...props} /> +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/skillswap/src/components/ui/tabs.jsx b/skillswap/src/components/ui/tabs.jsx new file mode 100644 index 0000000..b674eb9 --- /dev/null +++ b/skillswap/src/components/ui/tabs.jsx @@ -0,0 +1,41 @@ +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef(({ className, ...props }, ref) => ( + <TabsPrimitive.List + ref={ref} + className={cn( + "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground", + className + )} + {...props} /> +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => ( + <TabsPrimitive.Trigger + ref={ref} + className={cn( + "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow", + className + )} + {...props} /> +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef(({ className, ...props }, ref) => ( + <TabsPrimitive.Content + ref={ref} + className={cn( + "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", + className + )} + {...props} /> +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/skillswap/src/components/ui/textarea.jsx b/skillswap/src/components/ui/textarea.jsx new file mode 100644 index 0000000..9a3276c --- /dev/null +++ b/skillswap/src/components/ui/textarea.jsx @@ -0,0 +1,18 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Textarea = React.forwardRef(({ className, ...props }, ref) => { + return ( + <textarea + className={cn( + "flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", + className + )} + ref={ref} + {...props} /> + ); +}) +Textarea.displayName = "Textarea" + +export { Textarea } diff --git a/skillswap/src/index.css b/skillswap/src/index.css index 2f23020..7a9dab2 100644 --- a/skillswap/src/index.css +++ b/skillswap/src/index.css @@ -2,6 +2,10 @@ @tailwind components; @tailwind utilities; +body { + font-family: Verdana, Geneva, Tahoma, sans-serif; +} + @layer base { :root { --background: 0 0% 100%; diff --git a/skillswap/src/main.jsx b/skillswap/src/main.jsx index 20cca83..6a2acf9 100644 --- a/skillswap/src/main.jsx +++ b/skillswap/src/main.jsx @@ -14,6 +14,9 @@ import Painting from './pages/Painting'; import Photography from './pages/Photography'; import Music from './pages/Music'; import Programming from './pages/Programming'; +import Authors from './pages/Authors'; +import Messages from './pages/Messages'; +import Profile from './pages/Profile'; const router = createBrowserRouter([ { @@ -50,6 +53,18 @@ const router = createBrowserRouter([ path: '/photography', element: <Photography />, }, + { + path: '/authors', + element: <Authors />, + }, + { + path: '/messages/:authorId', + element: <Messages />, + }, + { + path: '/profile', + element: <Profile />, + }, ], }, ]); diff --git a/skillswap/src/pages/Authors.jsx b/skillswap/src/pages/Authors.jsx new file mode 100644 index 0000000..573e269 --- /dev/null +++ b/skillswap/src/pages/Authors.jsx @@ -0,0 +1,163 @@ +import { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; + +const authors = [ + { + id: 1, + name: 'Alex Johnson', + skills: ['Photography', 'Graphic Design', 'Video Editing'], + featured: ['Advanced Photography Course', 'Portrait Composition Guide'], + bio: 'Professional photographer with 8 years of experience in portrait and landscape photography.', + }, + { + id: 2, + name: 'Maya Patel', + skills: ['Web Development', 'UI/UX Design', 'JavaScript'], + featured: ['React Fundamentals', 'Responsive Design Workshop'], + bio: 'Full-stack developer specializing in modern web technologies and user experience design.', + }, + { + id: 3, + name: 'Carlos Rodriguez', + skills: ['Music Production', 'Piano', 'Sound Engineering'], + featured: ['Music Theory Basics', 'Home Studio Setup Guide'], + bio: 'Music producer and pianist with a passion for teaching audio production techniques.', + }, + { + id: 4, + name: 'Sarah Kim', + skills: ['Digital Marketing', 'SEO', 'Content Creation'], + featured: ['Social Media Strategy', 'SEO Optimization Tips'], + bio: 'Digital marketing specialist helping creators and small businesses grow their online presence.', + }, + { + id: 5, + name: 'David Chen', + skills: ['Data Science', 'Python', 'Machine Learning'], + featured: ['Intro to Data Analysis', 'Python for Beginners'], + bio: 'Data scientist with experience in building predictive models and data visualization.', + }, +]; + +const SkillFilter = ({ skills, selectedSkills, onSkillToggle }) => { + const allSkills = [...new Set(skills.flat())]; + + return ( + <div className='mb-6'> + <h3 className='text-lg font-medium mb-4 mt-8'>Filter by Skills</h3> + <div className='flex flex-wrap gap-2'> + {allSkills.map((skill) => ( + <Badge + key={skill} + variant={selectedSkills.includes(skill) ? 'default' : 'outline'} + className='cursor-pointer' + onClick={() => onSkillToggle(skill)} + > + {skill} + </Badge> + ))} + </div> + </div> + ); +}; + +export default function Authors() { + const [selectedSkills, setSelectedSkills] = useState([]); + + const handleSkillToggle = (skill) => { + setSelectedSkills((prev) => + prev.includes(skill) ? prev.filter((s) => s !== skill) : [...prev, skill] + ); + }; + + const filteredAuthors = + selectedSkills.length > 0 + ? authors.filter((author) => + author.skills.some((skill) => selectedSkills.includes(skill)) + ) + : authors; + + return ( + <div className='container mx-auto py-8 px-4 md:py-16 lg:py-24'> + <h1 className='text-3xl font-bold mb-2'>Discover Skilled Authors</h1> + <p className='text-gray-600 mb-6'> + Connect with experts and learn new skills through direct collaboration + </p> + + <SkillFilter + skills={authors.map((author) => author.skills)} + selectedSkills={selectedSkills} + onSkillToggle={handleSkillToggle} + /> + + <div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mt-10'> + {filteredAuthors.map((author) => ( + <Card key={author.id} className='h-full flex flex-col'> + <CardHeader> + <div className='flex items-center gap-4'> + <Avatar> + <AvatarImage src={author.avatar} alt={author.name} /> + <AvatarFallback> + {author.name + .split(' ') + .map((n) => n[0]) + .join('')} + </AvatarFallback> + </Avatar> + <div> + <CardTitle>{author.name}</CardTitle> + <CardDescription className='line-clamp-2 mt-2'> + {author.bio} + </CardDescription> + </div> + </div> + </CardHeader> + <CardContent className='flex-grow'> + <div className='mb-4'> + <h3 className='text-sm font-medium text-gray-500 mb-2'> + Skills + </h3> + <div className='flex flex-wrap gap-1.5'> + {author.skills.map((skill) => ( + <Badge key={skill} variant='secondary' className='text-xs'> + {skill} + </Badge> + ))} + </div> + </div> + <div> + <h3 className='text-sm font-medium text-gray-500 mb-2'> + Featured Content + </h3> + <ul className='space-y-1 text-sm'> + {author.featured.map((item) => ( + <li key={item} className='flex items-center'> + <span className='w-1.5 h-1.5 bg-primary rounded-full mr-2'></span> + {item} + </li> + ))} + </ul> + </div> + </CardContent> + <CardFooter> + <Link to={`/messages/${author.id}`} className='w-full'> + <Button className='w-full'>Connect</Button> + </Link> + </CardFooter> + </Card> + ))} + </div> + </div> + ); +} diff --git a/skillswap/src/pages/Messages.jsx b/skillswap/src/pages/Messages.jsx new file mode 100644 index 0000000..de55930 --- /dev/null +++ b/skillswap/src/pages/Messages.jsx @@ -0,0 +1,282 @@ +import { useState, useEffect, useRef } from 'react'; +import { useParams, Link } from 'react-router-dom'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Input } from '@/components/ui/input'; +import { ArrowLeft, Send } from 'lucide-react'; + +// Sample data for authors +const authors = [ + { + id: 1, + name: 'Alex Johnson', + skills: ['Photography', 'Graphic Design', 'Video Editing'], + }, + { + id: 2, + name: 'Maya Patel', + skills: ['Web Development', 'UI/UX Design', 'JavaScript'], + }, + { + id: 3, + name: 'Carlos Rodriguez', + skills: ['Music Production', 'Piano', 'Sound Engineering'], + }, + { + id: 4, + name: 'Sarah Kim', + skills: ['Digital Marketing', 'SEO', 'Content Creation'], + }, + { + id: 5, + name: 'David Chen', + skills: ['Data Science', 'Python', 'Machine Learning'], + }, +]; + +// Sample initial messages for each author +const initialMessages = { + 1: [ + { + id: 1, + sender: 'author', + text: 'Hi there! Interested in learning photography?', + timestamp: '2025-06-10T14:30:00', + }, + { + id: 2, + sender: 'user', + text: "Yes! I'd love to improve my portrait photography skills.", + timestamp: '2025-06-10T14:32:00', + }, + { + id: 3, + sender: 'author', + text: 'Great! I specialize in portrait photography. What specific aspects are you looking to improve?', + timestamp: '2025-06-10T14:35:00', + }, + ], + 2: [ + { + id: 1, + sender: 'author', + text: 'Hello! Looking to learn web development?', + timestamp: '2025-06-11T10:15:00', + }, + { + id: 2, + sender: 'user', + text: "Hi Maya! Yes, I'm particularly interested in React.", + timestamp: '2025-06-11T10:18:00', + }, + { + id: 3, + sender: 'author', + text: 'Perfect! React is my specialty. Are you a complete beginner or do you have some experience?', + timestamp: '2025-06-11T10:20:00', + }, + ], + 3: [ + { + id: 1, + sender: 'author', + text: 'Hola! Want to learn music production?', + timestamp: '2025-06-12T16:45:00', + }, + { + id: 2, + sender: 'user', + text: 'Hi Carlos! Yes, I play guitar but want to learn how to record and mix my own songs.', + timestamp: '2025-06-12T16:48:00', + }, + { + id: 3, + sender: 'author', + text: "That's awesome! Home recording is a great skill for guitarists. What DAW are you using?", + timestamp: '2025-06-12T16:50:00', + }, + ], + 4: [ + { + id: 1, + sender: 'author', + text: 'Hi there! Need help with digital marketing?', + timestamp: '2025-06-13T09:20:00', + }, + { + id: 2, + sender: 'user', + text: "Yes! I'm trying to grow my small business online but don't know where to start.", + timestamp: '2025-06-13T09:23:00', + }, + { + id: 3, + sender: 'author', + text: "I can definitely help with that! Let's start by discussing your target audience and goals.", + timestamp: '2025-06-13T09:25:00', + }, + ], + 5: [ + { + id: 1, + sender: 'author', + text: 'Hello! Interested in data science?', + timestamp: '2025-06-14T13:10:00', + }, + { + id: 2, + sender: 'user', + text: 'Hi David! Yes, I want to learn Python for data analysis.', + timestamp: '2025-06-14T13:12:00', + }, + { + id: 3, + sender: 'author', + text: 'Great choice! Python is perfect for data analysis. Have you installed Python and any libraries yet?', + timestamp: '2025-06-14T13:15:00', + }, + ], +}; + +const formatTime = (timestamp) => { + const date = new Date(timestamp); + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); +}; + +export default function MessagesPage() { + const { authorId } = useParams(); + const id = Number.parseInt(authorId); + const author = authors.find((a) => a.id === id); + + const getInitialMessages = () => { + const storedMessages = localStorage.getItem(`messages-${id}`); + return storedMessages + ? JSON.parse(storedMessages) + : initialMessages[id] || []; + }; + + const [messages, setMessages] = useState(getInitialMessages); + const [newMessage, setNewMessage] = useState(''); + const messagesEndRef = useRef(null); + + useEffect(() => { + localStorage.setItem(`messages-${id}`, JSON.stringify(messages)); + }, [messages, id]); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages]); + + const handleSendMessage = (e) => { + e.preventDefault(); + if (!newMessage.trim()) return; + + const message = { + id: messages.length + 1, + sender: 'user', + text: newMessage, + timestamp: new Date().toISOString(), + }; + + setMessages([...messages, message]); + setNewMessage(''); + + setTimeout(() => { + const responses = [ + "That's a great question! Let me explain...", + "I'd be happy to help you with that!", + 'Interesting point! Have you considered...', + "I've worked on similar projects before. My approach would be...", + "Let's schedule a skill swap session to discuss this in detail!", + ]; + + const responseMessage = { + id: messages.length + 2, + sender: 'author', + text: responses[Math.floor(Math.random() * responses.length)], + timestamp: new Date().toISOString(), + }; + + setMessages((prev) => [...prev, responseMessage]); + }, 1000); + }; + + if (!author) { + return <div className='container mx-auto py-8 px-4'>Author not found</div>; + } + + return ( + <div className='container mx-auto py-8 md:py-12 lg:py-20 px-4 max-w-3xl'> + <Card className='h-[calc(100vh-2rem)]'> + <CardHeader className='border-b'> + <div className='flex items-center'> + <Link to='/authors' className='mr-4'> + <Button variant='ghost' size='icon'> + <ArrowLeft className='h-5 w-5' /> + </Button> + </Link> + <Avatar className='h-10 w-10 mr-3'> + <AvatarImage src={author.avatar} alt={author.name} /> + <AvatarFallback> + {author.name + .split(' ') + .map((n) => n[0]) + .join('')} + </AvatarFallback> + </Avatar> + <div> + <CardTitle className='text-lg'>{author.name}</CardTitle> + <div className='text-xs text-gray-500'> + {author.skills.join(' • ')} + </div> + </div> + </div> + </CardHeader> + <CardContent className='flex flex-col h-[calc(100%-8rem)]'> + <div className='flex-grow overflow-y-auto py-4 space-y-4'> + {messages.map((message) => ( + <div + key={message.id} + className={`flex ${ + message.sender === 'user' ? 'justify-end' : 'justify-start' + }`} + > + <div + className={`max-w-[80%] rounded-lg px-4 py-2 ${ + message.sender === 'user' + ? 'bg-primary text-primary-foreground' + : 'bg-muted' + }`} + > + <div>{message.text}</div> + <div + className={`text-xs mt-1 ${ + message.sender === 'user' + ? 'text-primary-foreground/80' + : 'text-muted-foreground' + }`} + > + {formatTime(message.timestamp)} + </div> + </div> + </div> + ))} + <div ref={messagesEndRef} /> + </div> + <form onSubmit={handleSendMessage} className='mt-4 flex gap-2'> + <Input + value={newMessage} + onChange={(e) => setNewMessage(e.target.value)} + placeholder='Type your message...' + className='flex-grow' + /> + <Button type='submit' size='icon'> + <Send className='h-5 w-5' /> + </Button> + </form> + </CardContent> + </Card> + </div> + ); +} diff --git a/skillswap/src/pages/Profile.jsx b/skillswap/src/pages/Profile.jsx new file mode 100644 index 0000000..0a339c5 --- /dev/null +++ b/skillswap/src/pages/Profile.jsx @@ -0,0 +1,424 @@ +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Badge } from '@/components/ui/badge'; +import { Label } from '@/components/ui/label'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { PlusCircle, X, Save, Edit } from 'lucide-react'; + +const initialUserData = { + name: 'Jamie Smith', + bio: 'Passionate learner interested in photography, coding, and music. Always looking to expand my skills and connect with experts.', + email: 'jamie.smith@example.com', + location: 'San Francisco, CA', + joinDate: 'June 2025', + skills: ['JavaScript', 'Photography Basics', 'Content Writing'], + learning: ['Advanced Photography', 'React Development', 'Piano'], + connections: 12, + skillSwaps: 8, +}; + +export default function ProfilePage() { + const [userData, setUserData] = useState(initialUserData); + const [isEditing, setIsEditing] = useState(false); + const [editedData, setEditedData] = useState(userData); + const [newSkill, setNewSkill] = useState(''); + const [newLearning, setNewLearning] = useState(''); + + const handleSaveProfile = () => { + setUserData(editedData); + setIsEditing(false); + localStorage.setItem('userData', JSON.stringify(editedData)); + }; + + const handleAddSkill = () => { + if (newSkill.trim() && !editedData.skills.includes(newSkill.trim())) { + setEditedData({ + ...editedData, + skills: [...editedData.skills, newSkill.trim()], + }); + setNewSkill(''); + } + }; + + const handleRemoveSkill = (skillToRemove) => { + setEditedData({ + ...editedData, + skills: editedData.skills.filter((skill) => skill !== skillToRemove), + }); + }; + + const handleAddLearning = () => { + if ( + newLearning.trim() && + !editedData.learning.includes(newLearning.trim()) + ) { + setEditedData({ + ...editedData, + learning: [...editedData.learning, newLearning.trim()], + }); + setNewLearning(''); + } + }; + + const handleRemoveLearning = (learningToRemove) => { + setEditedData({ + ...editedData, + learning: editedData.learning.filter((item) => item !== learningToRemove), + }); + }; + + return ( + <div className='container mx-auto py-8 md:py-16 lg:py-24 px-4 max-w-4xl'> + <Tabs defaultValue='profile' className='w-full'> + <TabsList className='grid w-full grid-cols-2 mb-8'> + <TabsTrigger value='profile'>Profile</TabsTrigger> + <TabsTrigger value='activity'>Activity & Stats</TabsTrigger> + </TabsList> + + <TabsContent value='profile'> + <div className='grid grid-cols-1 md:grid-cols-3 gap-6'> + {/* Profile Card */} + <Card className='md:col-span-1'> + <CardHeader className='text-center'> + <div className='flex justify-center mb-4'> + <Avatar className='h-24 w-24'> + <AvatarImage src={userData.avatar} alt={userData.name} /> + <AvatarFallback> + {userData.name + .split(' ') + .map((n) => n[0]) + .join('')} + </AvatarFallback> + </Avatar> + </div> + <CardTitle>{userData.name}</CardTitle> + <CardDescription>{userData.location}</CardDescription> + </CardHeader> + <CardContent className='space-y-4 text-center'> + <div> + <p className='text-sm text-gray-500'>Member since</p> + <p>{userData.joinDate}</p> + </div> + <div className='flex justify-center space-x-6'> + <div> + <p className='text-sm text-gray-500'>Connections</p> + <p className='font-medium'>{userData.connections}</p> + </div> + <div> + <p className='text-sm text-gray-500'>Skill Swaps</p> + <p className='font-medium'>{userData.skillSwaps}</p> + </div> + </div> + </CardContent> + <CardFooter> + <Button + variant={isEditing ? 'outline' : 'default'} + className='w-full' + onClick={() => + isEditing ? setIsEditing(false) : setIsEditing(true) + } + > + {isEditing ? ( + <>Cancel Editing</> + ) : ( + <> + <Edit className='mr-2 h-4 w-4' /> + Edit Profile + </> + )} + </Button> + </CardFooter> + </Card> + + {/* Profile Details */} + <Card className='md:col-span-2'> + <CardHeader> + <CardTitle>Profile Details</CardTitle> + {isEditing && ( + <Button onClick={handleSaveProfile} className='self-end'> + <Save className='mr-2 h-4 w-4' /> + Save Changes + </Button> + )} + </CardHeader> + <CardContent className='space-y-6'> + {isEditing ? ( + <> + <div className='space-y-2'> + <Label htmlFor='name'>Name</Label> + <Input + id='name' + value={editedData.name} + onChange={(e) => + setEditedData({ ...editedData, name: e.target.value }) + } + /> + </div> + <div className='space-y-2'> + <Label htmlFor='email'>Email</Label> + <Input + id='email' + type='email' + value={editedData.email} + onChange={(e) => + setEditedData({ + ...editedData, + email: e.target.value, + }) + } + /> + </div> + <div className='space-y-2'> + <Label htmlFor='location'>Location</Label> + <Input + id='location' + value={editedData.location} + onChange={(e) => + setEditedData({ + ...editedData, + location: e.target.value, + }) + } + /> + </div> + <div className='space-y-2'> + <Label htmlFor='bio'>Bio</Label> + <Textarea + id='bio' + rows={4} + value={editedData.bio} + onChange={(e) => + setEditedData({ ...editedData, bio: e.target.value }) + } + /> + </div> + + {/* Skills section */} + <div className='space-y-2'> + <Label>Skills I Can Teach</Label> + <div className='flex flex-wrap gap-2 mb-2'> + {editedData.skills.map((skill) => ( + <Badge + key={skill} + variant='secondary' + className='pl-2 pr-1 py-1 flex items-center gap-1' + > + {skill} + <button + onClick={() => handleRemoveSkill(skill)} + className='ml-1 rounded-full hover:bg-gray-200 p-0.5' + > + <X className='h-3 w-3' /> + </button> + </Badge> + ))} + </div> + <div className='flex gap-2'> + <Input + placeholder='Add a skill...' + value={newSkill} + onChange={(e) => setNewSkill(e.target.value)} + onKeyDown={(e) => + e.key === 'Enter' && handleAddSkill() + } + /> + <Button + variant='outline' + size='icon' + onClick={handleAddSkill} + > + <PlusCircle className='h-4 w-4' /> + </Button> + </div> + </div> + + {/* Learning section */} + <div className='space-y-2'> + <Label>Skills I Want to Learn</Label> + <div className='flex flex-wrap gap-2 mb-2'> + {editedData.learning.map((item) => ( + <Badge + key={item} + variant='outline' + className='pl-2 pr-1 py-1 flex items-center gap-1' + > + {item} + <button + onClick={() => handleRemoveLearning(item)} + className='ml-1 rounded-full hover:bg-gray-200 p-0.5' + > + <X className='h-3 w-3' /> + </button> + </Badge> + ))} + </div> + <div className='flex gap-2'> + <Input + placeholder='Add a learning goal...' + value={newLearning} + onChange={(e) => setNewLearning(e.target.value)} + onKeyDown={(e) => + e.key === 'Enter' && handleAddLearning() + } + /> + <Button + variant='outline' + size='icon' + onClick={handleAddLearning} + > + <PlusCircle className='h-4 w-4' /> + </Button> + </div> + </div> + </> + ) : ( + <> + <div> + <h3 className='text-sm font-medium text-gray-500'> + Email + </h3> + <p>{userData.email}</p> + </div> + <div> + <h3 className='text-sm font-medium text-gray-500'>Bio</h3> + <p>{userData.bio}</p> + </div> + <div> + <h3 className='text-sm font-medium text-gray-500'> + Skills I Can Teach + </h3> + <div className='flex flex-wrap gap-2 mt-2'> + {userData.skills.map((skill) => ( + <Badge key={skill} variant='secondary'> + {skill} + </Badge> + ))} + </div> + </div> + <div> + <h3 className='text-sm font-medium text-gray-500'> + Skills I Want to Learn + </h3> + <div className='flex flex-wrap gap-2 mt-2'> + {userData.learning.map((item) => ( + <Badge key={item} variant='outline'> + {item} + </Badge> + ))} + </div> + </div> + </> + )} + </CardContent> + </Card> + </div> + </TabsContent> + + <TabsContent value='activity'> + <Card> + <CardHeader> + <CardTitle>Activity & Statistics</CardTitle> + <CardDescription> + Track your progress and engagement on SkillSwap + </CardDescription> + </CardHeader> + <CardContent> + <div className='space-y-8'> + <div> + <h3 className='text-lg font-medium mb-4'> + Recent Skill Swaps + </h3> + <div className='space-y-4'> + {[ + { + name: 'Alex Johnson', + skill: 'Photography Basics', + date: 'June 15, 2025', + }, + { + name: 'Maya Patel', + skill: 'React Development', + date: 'May 28, 2025', + }, + { + name: 'Carlos Rodriguez', + skill: 'Music Theory', + date: 'May 10, 2025', + }, + ].map((swap, index) => ( + <div + key={index} + className='flex items-center justify-between border-b pb-3' + > + <div className='flex items-center gap-3'> + <Avatar className='h-10 w-10'> + <AvatarFallback> + {swap.name + .split(' ') + .map((n) => n[0]) + .join('')} + </AvatarFallback> + </Avatar> + <div> + <p className='font-medium'>{swap.name}</p> + <p className='text-sm text-gray-500'> + {swap.skill} + </p> + </div> + </div> + <span className='text-sm text-gray-500'> + {swap.date} + </span> + </div> + ))} + </div> + </div> + + <div> + <h3 className='text-lg font-medium mb-4'> + Learning Progress + </h3> + <div className='grid grid-cols-1 md:grid-cols-3 gap-4'> + {userData.learning.map((skill, index) => ( + <Card key={index}> + <CardHeader className='pb-2'> + <CardTitle className='text-base'>{skill}</CardTitle> + </CardHeader> + + <CardContent> + <div className='space-y-2'> + <div className='flex justify-between text-sm'> + <span>Progress</span> + <span>{[30, 45, 15][index % 3]}%</span> + </div> + <div className='w-full bg-gray-200 rounded-full h-2.5'> + <div + className='bg-primary h-2.5 rounded-full' + style={{ width: `${[30, 45, 15][index % 3]}%` }} + ></div> + </div> + </div> + </CardContent> + </Card> + ))} + </div> + </div> + </div> + </CardContent> + </Card> + </TabsContent> + </Tabs> + </div> + ); +} -- GitLab