Added in Logo's and worked on Avatars and Chats
This commit is contained in:
Generated
+403
-1
@@ -8,6 +8,8 @@
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
"@radix-ui/react-avatar": "^1.1.3",
|
||||
@@ -27,6 +29,7 @@
|
||||
"autoprefixer": "^10.4.20",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"emoji-mart": "^5.6.0",
|
||||
"framer-motion": "^11.15.0",
|
||||
"lucide-react": "^0.468.0",
|
||||
"next": "15.0.4",
|
||||
@@ -86,7 +89,6 @@
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
@@ -102,6 +104,22 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emoji-mart/data": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emoji-mart/data/-/data-1.2.1.tgz",
|
||||
"integrity": "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@emoji-mart/react": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@emoji-mart/react/-/react-1.1.1.tgz",
|
||||
"integrity": "sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"emoji-mart": "^5.2",
|
||||
"react": "^16.8 || ^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
|
||||
@@ -339,6 +357,384 @@
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
|
||||
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
|
||||
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
|
||||
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
|
||||
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
|
||||
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
|
||||
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
|
||||
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
|
||||
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
|
||||
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
|
||||
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
|
||||
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
|
||||
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
|
||||
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
|
||||
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
|
||||
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
|
||||
@@ -3208,6 +3604,12 @@
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz",
|
||||
"integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA=="
|
||||
},
|
||||
"node_modules/emoji-mart": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.6.0.tgz",
|
||||
"integrity": "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
|
||||
+4
-1
@@ -9,8 +9,9 @@
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
"@radix-ui/react-avatar": "^1.1.3",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
@@ -26,8 +27,10 @@
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@tanstack/react-table": "^8.20.6",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"emoji-mart": "^5.6.0",
|
||||
"framer-motion": "^11.15.0",
|
||||
"lucide-react": "^0.468.0",
|
||||
"next": "15.0.4",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1,401 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useRef, useCallback, useEffect } from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { conversations as conversationsData } from "@/data/chats"
|
||||
import {
|
||||
Search, Send, Phone, Video, MoreHorizontal, Paperclip,
|
||||
Smile, Flag, Ban, Trash2, Image, File, X,
|
||||
} from "lucide-react"
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription,
|
||||
DialogFooter, DialogClose,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { useTheme } from "next-themes"
|
||||
import { useUser } from "@/providers/user-provider"
|
||||
import { toast } from "sonner"
|
||||
import data from "@emoji-mart/data"
|
||||
import Picker from "@emoji-mart/react"
|
||||
|
||||
export default function ChatsPage() {
|
||||
const { theme } = useTheme()
|
||||
const { user } = useUser()
|
||||
const [activeChat, setActiveChat] = useState(conversationsData[0]?.id ?? null)
|
||||
const [messageInput, setMessageInput] = useState("")
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
|
||||
const [attachments, setAttachments] = useState<File[]>([])
|
||||
const [panelWidth, setPanelWidth] = useState(320)
|
||||
const [isResizing, setIsResizing] = useState(false)
|
||||
const [reportDialogOpen, setReportDialogOpen] = useState(false)
|
||||
const [reportReason, setReportReason] = useState("")
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const emojiPickerRef = useRef<HTMLDivElement>(null)
|
||||
const resizeStartRef = useRef({ x: 0, width: 0 })
|
||||
|
||||
const [conversations, setConversations] = useState(conversationsData)
|
||||
const conversation = conversations.find((c) => c.id === activeChat)
|
||||
const otherParticipant = (conv: typeof conversationsData[0]) =>
|
||||
conv.participants.find((p) => p.id !== "user1") ?? conv.participants[0]
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (emojiPickerRef.current && !emojiPickerRef.current.contains(e.target as Node)) {
|
||||
setShowEmojiPicker(false)
|
||||
}
|
||||
}
|
||||
document.addEventListener("mousedown", handleClickOutside)
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside)
|
||||
}, [])
|
||||
|
||||
const handleResizeStart = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
setIsResizing(true)
|
||||
resizeStartRef.current = { x: e.clientX, width: panelWidth }
|
||||
}, [panelWidth])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isResizing) return
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const delta = e.clientX - resizeStartRef.current.x
|
||||
const newWidth = Math.max(240, Math.min(560, resizeStartRef.current.width + delta))
|
||||
setPanelWidth(newWidth)
|
||||
}
|
||||
const handleMouseUp = () => setIsResizing(false)
|
||||
document.addEventListener("mousemove", handleMouseMove)
|
||||
document.addEventListener("mouseup", handleMouseUp)
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", handleMouseMove)
|
||||
document.removeEventListener("mouseup", handleMouseUp)
|
||||
}
|
||||
}, [isResizing])
|
||||
|
||||
const handleEmojiSelect = (emoji: { native: string }) => {
|
||||
setMessageInput((prev) => prev + emoji.native)
|
||||
setShowEmojiPicker(false)
|
||||
}
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files ?? [])
|
||||
setAttachments((prev) => [...prev, ...files])
|
||||
if (e.target) e.target.value = ""
|
||||
}
|
||||
|
||||
const removeAttachment = (index: number) => {
|
||||
setAttachments((prev) => prev.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
const handleSend = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!messageInput.trim() && attachments.length === 0) return
|
||||
const newMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
conversationId: activeChat!,
|
||||
senderId: "user1",
|
||||
senderName: "Sarah Chen",
|
||||
senderAvatar: "SC",
|
||||
content: messageInput.trim(),
|
||||
timestamp: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
|
||||
}
|
||||
setConversations((prev) =>
|
||||
prev.map((conv) =>
|
||||
conv.id === activeChat
|
||||
? {
|
||||
...conv,
|
||||
messages: [...conv.messages, newMessage],
|
||||
lastMessage: newMessage.content,
|
||||
lastMessageTime: "Just now",
|
||||
unread: 0,
|
||||
}
|
||||
: conv
|
||||
)
|
||||
)
|
||||
setMessageInput("")
|
||||
setAttachments([])
|
||||
}
|
||||
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes < 1024) return bytes + " B"
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB"
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + " MB"
|
||||
}
|
||||
|
||||
const isImageFile = (file: File) => file.type.startsWith("image/")
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-8rem)] -m-4 lg:-m-6 rounded-lg border bg-card overflow-hidden">
|
||||
{/* Conversations list - left panel */}
|
||||
<div
|
||||
className="flex flex-col border-r shrink-0 overflow-hidden"
|
||||
style={{ width: panelWidth }}
|
||||
>
|
||||
<div className="p-4 border-b space-y-3">
|
||||
<h2 className="text-lg font-semibold">Chats</h2>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input placeholder="Search conversations..." className="h-9 pl-9" />
|
||||
</div>
|
||||
</div>
|
||||
<ScrollArea className="flex-1">
|
||||
{conversations.map((conv) => {
|
||||
const person = otherParticipant(conv)
|
||||
const isActive = conv.id === activeChat
|
||||
return (
|
||||
<button
|
||||
key={conv.id}
|
||||
onClick={() => setActiveChat(conv.id)}
|
||||
className={cn(
|
||||
"w-full flex items-start gap-3 p-4 text-left transition-colors hover:bg-muted/50",
|
||||
isActive && "bg-muted"
|
||||
)}
|
||||
>
|
||||
<Avatar className="h-10 w-10 shrink-0">
|
||||
<AvatarFallback>{person.avatar}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-sm font-medium truncate">{person.name}</span>
|
||||
<span className="text-xs text-muted-foreground shrink-0">{conv.lastMessageTime}</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground truncate mt-0.5">{conv.lastMessage}</p>
|
||||
</div>
|
||||
{conv.unread > 0 && (
|
||||
<span className="shrink-0 flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1.5 text-[10px] font-medium text-primary-foreground">
|
||||
{conv.unread}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
{/* Resize handle */}
|
||||
<div
|
||||
className="w-1.5 cursor-col-resize shrink-0 relative group hover:bg-primary/20 transition-colors"
|
||||
onMouseDown={handleResizeStart}
|
||||
>
|
||||
<div className="absolute inset-y-0 -left-1 -right-1" />
|
||||
</div>
|
||||
|
||||
{/* Chat area - right panel */}
|
||||
{conversation ? (
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
{/* Chat header */}
|
||||
<div className="flex items-center justify-between gap-4 px-6 h-16 border-b shrink-0">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<Avatar className="h-9 w-9 shrink-0">
|
||||
<AvatarFallback>{otherParticipant(conversation).avatar}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium truncate">{otherParticipant(conversation).name}</p>
|
||||
<p className="text-xs text-muted-foreground truncate">{otherParticipant(conversation).role}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<Button
|
||||
variant="ghost" size="icon" className="h-8 w-8"
|
||||
onClick={() => toast.info("Voice calling coming soon")}
|
||||
>
|
||||
<Phone className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost" size="icon" className="h-8 w-8"
|
||||
onClick={() => toast.info("Video calling coming soon")}
|
||||
>
|
||||
<Video className="h-4 w-4" />
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-44">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => setReportDialogOpen(true)}>
|
||||
<Flag className="mr-2 h-4 w-4" /> Report
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => toast.info("Blocked")}>
|
||||
<Ban className="mr-2 h-4 w-4" /> Block
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-destructive"
|
||||
onClick={() => toast.info("Conversation deleted")}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" /> Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<ScrollArea className="flex-1 p-6">
|
||||
<div className="space-y-4">
|
||||
{conversation.messages.map((msg) => {
|
||||
const isMe = msg.senderId === "user1"
|
||||
return (
|
||||
<div key={msg.id} className={cn("flex gap-3", isMe && "flex-row-reverse")}>
|
||||
<Avatar className="h-8 w-8 mt-0.5 shrink-0">
|
||||
{isMe ? <AvatarImage src={user.avatar} /> : null}
|
||||
<AvatarFallback className={cn("text-xs", isMe && "bg-primary text-primary-foreground")}>
|
||||
{msg.senderAvatar}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className={cn("max-w-[70%]", isMe && "items-end flex flex-col")}>
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-2xl px-4 py-2.5 text-sm",
|
||||
isMe ? "bg-primary text-primary-foreground rounded-tr-sm" : "bg-muted rounded-tl-sm"
|
||||
)}
|
||||
>
|
||||
{msg.content}
|
||||
</div>
|
||||
<span className="text-[10px] text-muted-foreground mt-1 px-1">{msg.timestamp}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Input */}
|
||||
<div className="p-4 border-t shrink-0 space-y-2">
|
||||
{attachments.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{attachments.map((file, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center gap-2 rounded-lg border bg-muted/50 px-3 py-1.5 text-sm max-w-[200px]"
|
||||
>
|
||||
{isImageFile(file) ? (
|
||||
<Image className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
) : (
|
||||
<File className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
)}
|
||||
<span className="truncate">{file.name}</span>
|
||||
<span className="text-xs text-muted-foreground shrink-0">{formatFileSize(file.size)}</span>
|
||||
<Button
|
||||
variant="ghost" size="icon" className="h-5 w-5 -mr-1 shrink-0"
|
||||
onClick={() => removeAttachment(i)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleSend} className="flex items-center gap-2">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.txt"
|
||||
className="hidden"
|
||||
onChange={handleFileSelect}
|
||||
/>
|
||||
<Button
|
||||
type="button" variant="ghost" size="icon" className="h-9 w-9 shrink-0"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<Paperclip className="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
value={messageInput}
|
||||
onChange={(e) => setMessageInput(e.target.value)}
|
||||
placeholder="Type a message..."
|
||||
className="h-9 w-full pr-9"
|
||||
/>
|
||||
<div className="absolute right-0 top-0 bottom-0 flex items-center pr-1" ref={emojiPickerRef}>
|
||||
<Button
|
||||
type="button" variant="ghost" size="icon" className="h-7 w-7 shrink-0"
|
||||
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
|
||||
>
|
||||
<Smile className="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
{showEmojiPicker && (
|
||||
<div className="absolute bottom-full right-0 mb-2 z-50">
|
||||
<Picker
|
||||
data={data}
|
||||
onEmojiSelect={handleEmojiSelect}
|
||||
theme={theme === "dark" ? "dark" : "light"}
|
||||
previewPosition="none"
|
||||
skinTonePosition="none"
|
||||
set="native"
|
||||
maxFrequentRows={2}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit" size="icon" className="h-9 w-9 shrink-0" disabled={!messageInput.trim() && attachments.length === 0}>
|
||||
<Send className="h-4 w-4" />
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center text-muted-foreground">
|
||||
<p>Select a conversation to start chatting</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Resize overlay */}
|
||||
{isResizing && <div className="fixed inset-0 z-50 cursor-col-resize" />}
|
||||
|
||||
{/* Report dialog */}
|
||||
<Dialog open={reportDialogOpen} onOpenChange={setReportDialogOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Report {conversation ? otherParticipant(conversation).name : "User"}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Let us know why you're reporting this conversation.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 py-2">
|
||||
<Label htmlFor="reason">Reason</Label>
|
||||
<Textarea
|
||||
id="reason"
|
||||
placeholder="Describe the issue..."
|
||||
value={reportReason}
|
||||
onChange={(e) => setReportReason(e.target.value)}
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
onClick={() => {
|
||||
toast.success("Report submitted")
|
||||
setReportReason("")
|
||||
setReportDialogOpen(false)
|
||||
}}
|
||||
disabled={!reportReason.trim()}
|
||||
>
|
||||
Submit Report
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
"use client"
|
||||
|
||||
import { AppShell } from "@/components/layout/app-shell"
|
||||
import { UserProvider } from "@/providers/user-provider"
|
||||
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return <AppShell>{children}</AppShell>
|
||||
return (
|
||||
<UserProvider>
|
||||
<AppShell>{children}</AppShell>
|
||||
</UserProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
"use client"
|
||||
|
||||
import { useRef } from "react"
|
||||
import { PageHeader } from "@/components/shared/page-header"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { useUser } from "@/providers/user-provider"
|
||||
import { Mail, Calendar, Shield, Activity, Camera } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { user, updateAvatar } = useUser()
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
const validTypes = ["image/png", "image/jpeg"]
|
||||
if (!validTypes.includes(file.type)) {
|
||||
toast.error("Only PNG and JPEG files are allowed")
|
||||
return
|
||||
}
|
||||
const url = URL.createObjectURL(file)
|
||||
updateAvatar(url)
|
||||
toast.success("Avatar updated")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader title="Profile" description="Your account information" />
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
<Card className="lg:col-span-1">
|
||||
<CardContent className="flex flex-col items-center pt-8">
|
||||
<div className="relative">
|
||||
<Avatar className="h-24 w-24">
|
||||
<AvatarImage src={user.avatar} />
|
||||
<AvatarFallback className="text-2xl">{user.name.split(" ").map((n) => n[0]).join("")}</AvatarFallback>
|
||||
</Avatar>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="absolute inset-0 flex items-center justify-center rounded-full bg-black/40 opacity-0 transition-opacity hover:opacity-100"
|
||||
>
|
||||
<Camera className="h-6 w-6 text-white" />
|
||||
</button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".png,.jpeg,.jpg"
|
||||
className="hidden"
|
||||
onChange={handleAvatarChange}
|
||||
/>
|
||||
</div>
|
||||
<h2 className="mt-4 text-xl font-semibold">{user.name}</h2>
|
||||
<Badge variant="secondary" className="mt-1 capitalize">
|
||||
{user.role}
|
||||
</Badge>
|
||||
<div className="mt-6 flex w-full flex-col gap-3 text-sm">
|
||||
<div className="flex items-center gap-3 rounded-lg bg-muted/50 px-4 py-3">
|
||||
<Mail className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">{user.email}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 rounded-lg bg-muted/50 px-4 py-3">
|
||||
<Calendar className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">
|
||||
Joined {new Date(user.createdAt).toLocaleDateString("en-US", { month: "long", year: "numeric" })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 rounded-lg bg-muted/50 px-4 py-3">
|
||||
<Shield className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground capitalize">{user.role} access</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 rounded-lg bg-muted/50 px-4 py-3">
|
||||
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">{user.active ? "Active" : "Inactive"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Account Details</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs text-muted-foreground">Full Name</span>
|
||||
<p className="text-sm font-medium">{user.name}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs text-muted-foreground">Email</span>
|
||||
<p className="text-sm font-medium">{user.email}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs text-muted-foreground">Role</span>
|
||||
<p className="text-sm font-medium capitalize">{user.role}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs text-muted-foreground">Status</span>
|
||||
<p className="text-sm font-medium">{user.active ? "Active" : "Inactive"}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs text-muted-foreground">Member Since</span>
|
||||
<p className="text-sm font-medium">
|
||||
{new Date(user.createdAt).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs text-muted-foreground">User ID</span>
|
||||
<p className="text-sm font-medium font-mono">{user.id}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+4
-1
@@ -10,8 +10,11 @@ const inter = Inter({
|
||||
})
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Coastal IT - CRM",
|
||||
title: "Coast IT - CRM",
|
||||
description: "Customer Relationship Management System",
|
||||
icons: {
|
||||
icon: "/logo/CompanyMiniLogo.png",
|
||||
},
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
+11
-7
@@ -35,9 +35,11 @@ export default function LoginPage() {
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-white/20 backdrop-blur-sm">
|
||||
<span className="text-lg font-bold">C</span>
|
||||
</div>
|
||||
<img
|
||||
src="/logo/CompanyLogo.png"
|
||||
alt={COMPANY_NAME}
|
||||
className="h-10 w-10 rounded-xl object-contain"
|
||||
/>
|
||||
<span className="text-xl font-semibold">{COMPANY_NAME}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,7 +74,7 @@ export default function LoginPage() {
|
||||
<div className="h-10 w-10 rounded-full bg-white/20" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">Marcus Johnson</p>
|
||||
<p className="text-xs text-white/60">Sales Lead, Coastal IT</p>
|
||||
<p className="text-xs text-white/60">Sales Lead, Coast IT</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
@@ -93,9 +95,11 @@ export default function LoginPage() {
|
||||
>
|
||||
{/* Mobile logo */}
|
||||
<div className="flex flex-col items-center gap-3 lg:hidden">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary">
|
||||
<span className="text-xl font-bold text-primary-foreground">C</span>
|
||||
</div>
|
||||
<img
|
||||
src="/logo/CompanyLogo.png"
|
||||
alt={COMPANY_NAME}
|
||||
className="h-12 w-12 rounded-xl object-contain"
|
||||
/>
|
||||
<span className="text-xl font-semibold">{COMPANY_NAME}</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { usePathname } from "next/navigation"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import {
|
||||
LayoutDashboard,
|
||||
@@ -15,12 +16,18 @@ import {
|
||||
ChevronRight,
|
||||
Building2,
|
||||
PanelLeftClose,
|
||||
MessageSquare,
|
||||
} from "lucide-react"
|
||||
import { COMPANY_NAME } from "@/lib/constants"
|
||||
import { useUser } from "@/providers/user-provider"
|
||||
import { conversations as conversationsData } from "@/data/chats"
|
||||
|
||||
const totalUnread = conversationsData.reduce((sum, c) => sum + c.unread, 0)
|
||||
|
||||
const navItems = [
|
||||
{ href: "/dashboard", label: "Dashboard", icon: LayoutDashboard },
|
||||
{ href: "/leads", label: "Leads", icon: Users },
|
||||
{ href: "/chats", label: "Chats", icon: MessageSquare },
|
||||
{ href: "/users", label: "Users", icon: Building2 },
|
||||
{ href: "/settings", label: "Settings", icon: Settings },
|
||||
]
|
||||
@@ -34,6 +41,8 @@ interface SidebarProps {
|
||||
|
||||
export function Sidebar({ collapsed, onToggle, mobileOpen, onMobileClose }: SidebarProps) {
|
||||
const pathname = usePathname()
|
||||
const { user } = useUser()
|
||||
const initials = user.name.split(" ").map((n) => n[0]).join("")
|
||||
|
||||
const sidebarContent = (
|
||||
<div
|
||||
@@ -45,9 +54,11 @@ export function Sidebar({ collapsed, onToggle, mobileOpen, onMobileClose }: Side
|
||||
{/* Logo */}
|
||||
<div className={cn("flex h-16 items-center border-b border-sidebar-border px-4", collapsed ? "justify-center" : "justify-between")}>
|
||||
<Link href="/" className="flex items-center gap-3 overflow-hidden">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary">
|
||||
<span className="text-sm font-bold text-primary-foreground">C</span>
|
||||
</div>
|
||||
<img
|
||||
src="/logo/CompanyLogo.png"
|
||||
alt={COMPANY_NAME}
|
||||
className="h-8 w-8 shrink-0 rounded-lg object-contain"
|
||||
/>
|
||||
<AnimatePresence mode="wait">
|
||||
{!collapsed && (
|
||||
<motion.span
|
||||
@@ -85,13 +96,18 @@ export function Sidebar({ collapsed, onToggle, mobileOpen, onMobileClose }: Side
|
||||
<Link
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"flex h-10 w-10 items-center justify-center rounded-lg transition-colors",
|
||||
"relative flex h-10 w-10 items-center justify-center rounded-lg transition-colors",
|
||||
isActive
|
||||
? "bg-sidebar-primary text-sidebar-primary-foreground"
|
||||
: "text-sidebar-foreground/60 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||
)}
|
||||
>
|
||||
<item.icon className="h-5 w-5" />
|
||||
{item.label === "Chats" && totalUnread > 0 && (
|
||||
<span className="absolute -right-0.5 -top-0.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-destructive px-1 text-[10px] font-medium text-destructive-foreground">
|
||||
{totalUnread}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="ml-2">
|
||||
@@ -141,17 +157,19 @@ export function Sidebar({ collapsed, onToggle, mobileOpen, onMobileClose }: Side
|
||||
{/* User info */}
|
||||
<div className={cn("border-t border-sidebar-border p-3", collapsed && "flex justify-center")}>
|
||||
{collapsed ? (
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-sidebar-accent">
|
||||
<span className="text-sm font-medium text-sidebar-accent-foreground">SC</span>
|
||||
</div>
|
||||
<Avatar className="h-10 w-10">
|
||||
<AvatarImage src={user.avatar} />
|
||||
<AvatarFallback className="text-sm font-medium">{initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
) : (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-sidebar-accent">
|
||||
<span className="text-sm font-medium text-sidebar-accent-foreground">SC</span>
|
||||
</div>
|
||||
<Avatar className="h-9 w-9">
|
||||
<AvatarImage src={user.avatar} />
|
||||
<AvatarFallback className="text-sm font-medium">{initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<p className="text-sm font-medium truncate">Sarah Chen</p>
|
||||
<p className="text-xs text-sidebar-foreground/60 truncate">Admin</p>
|
||||
<p className="text-sm font-medium truncate">{user.name}</p>
|
||||
<p className="text-xs text-sidebar-foreground/60 truncate capitalize">{user.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useTheme } from "next-themes"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { useUser } from "@/providers/user-provider"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -31,8 +33,11 @@ interface TopbarProps {
|
||||
}
|
||||
|
||||
export function Topbar({ onMenuClick }: TopbarProps) {
|
||||
const router = useRouter()
|
||||
const { theme, setTheme } = useTheme()
|
||||
const { user } = useUser()
|
||||
const [searchOpen, setSearchOpen] = useState(false)
|
||||
const initials = user.name.split(" ").map((n) => n[0]).join("")
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-20 flex h-16 items-center gap-4 border-b bg-background px-4 lg:px-6">
|
||||
@@ -107,26 +112,26 @@ export function Topbar({ onMenuClick }: TopbarProps) {
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="relative h-9 gap-2 pl-2 pr-3">
|
||||
<Avatar className="h-7 w-7">
|
||||
<AvatarImage src="https://ui-avatars.com/api/?name=Sarah+Chen&background=1d4ed8&color=fff&size=64" />
|
||||
<AvatarFallback>SC</AvatarFallback>
|
||||
<AvatarImage src={user.avatar} />
|
||||
<AvatarFallback>{initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="hidden text-sm font-medium md:inline-block">Sarah Chen</span>
|
||||
<span className="hidden text-sm font-medium md:inline-block">{user.name}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm font-medium">Sarah Chen</p>
|
||||
<p className="text-xs text-muted-foreground">sarah@coastalit.com</p>
|
||||
<Badge variant="secondary" className="mt-1 w-fit text-xs">Admin</Badge>
|
||||
<p className="text-sm font-medium">{user.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{user.email}</p>
|
||||
<Badge variant="secondary" className="mt-1 w-fit text-xs capitalize">{user.role}</Badge>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => router.push("/profile")}>
|
||||
<User className="mr-2 h-4 w-4" />
|
||||
Profile
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => router.push("/settings")}>
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
Settings
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { COMPANY_NAME } from "@/lib/constants"
|
||||
import { toast } from "sonner"
|
||||
|
||||
export function CompanySettingsForm() {
|
||||
return (
|
||||
@@ -39,7 +40,7 @@ export function CompanySettingsForm() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end pt-4">
|
||||
<Button>Save Changes</Button>
|
||||
<Button onClick={() => toast.success("Company settings saved")}>Save Changes</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { toast } from "sonner"
|
||||
|
||||
const notifications = [
|
||||
{
|
||||
@@ -63,7 +64,7 @@ export function NotificationSettings() {
|
||||
</div>
|
||||
))}
|
||||
<div className="flex justify-end pt-4">
|
||||
<Button>Save Preferences</Button>
|
||||
<Button onClick={() => toast.success("Notification preferences saved")}>Save Preferences</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { toast } from "sonner"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -65,7 +66,7 @@ export function UserPreferencesForm() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end pt-4">
|
||||
<Button>Save Preferences</Button>
|
||||
<Button onClick={() => toast.success("Preferences saved")}>Save Preferences</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Conversation } from "@/types"
|
||||
|
||||
export const conversations: Conversation[] = [
|
||||
{
|
||||
id: "1",
|
||||
participants: [
|
||||
{ id: "user1", name: "Sarah Chen", avatar: "SC", role: "Admin" },
|
||||
{ id: "user2", name: "Mike Johnson", avatar: "MJ", role: "Sales" },
|
||||
],
|
||||
lastMessage: "Sure, I'll send over the proposal by EOD",
|
||||
lastMessageTime: "2m ago",
|
||||
unread: 2,
|
||||
messages: [
|
||||
{ id: "m1", conversationId: "1", senderId: "user2", senderName: "Mike Johnson", senderAvatar: "MJ", content: "Hey Sarah, have you reviewed the Brightwave leads?", timestamp: "10:32 AM" },
|
||||
{ id: "m2", conversationId: "1", senderId: "user1", senderName: "Sarah Chen", senderAvatar: "SC", content: "Yes, I went through them this morning. Looks promising!", timestamp: "10:33 AM" },
|
||||
{ id: "m3", conversationId: "1", senderId: "user2", senderName: "Mike Johnson", senderAvatar: "MJ", content: "Great! Should I prepare a proposal for the top 3?", timestamp: "10:34 AM" },
|
||||
{ id: "m4", conversationId: "1", senderId: "user1", senderName: "Sarah Chen", senderAvatar: "SC", content: "Absolutely. Focus on Brightwave and Nexus Digital first.", timestamp: "10:35 AM" },
|
||||
{ id: "m5", conversationId: "1", senderId: "user2", senderName: "Mike Johnson", senderAvatar: "MJ", content: "Sure, I'll send over the proposal by EOD", timestamp: "10:36 AM" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
participants: [
|
||||
{ id: "user1", name: "Sarah Chen", avatar: "SC", role: "Admin" },
|
||||
{ id: "user3", name: "Emily Davis", avatar: "ED", role: "Sales" },
|
||||
],
|
||||
lastMessage: "Can you review the contract terms?",
|
||||
lastMessageTime: "1h ago",
|
||||
unread: 0,
|
||||
messages: [
|
||||
{ id: "m6", conversationId: "2", senderId: "user3", senderName: "Emily Davis", senderAvatar: "ED", content: "Hi Sarah, I'm finalizing the deal with Pinnacle Web Solutions.", timestamp: "9:15 AM" },
|
||||
{ id: "m7", conversationId: "2", senderId: "user1", senderName: "Sarah Chen", senderAvatar: "SC", content: "That's great news! What's the status?", timestamp: "9:16 AM" },
|
||||
{ id: "m8", conversationId: "2", senderId: "user3", senderName: "Emily Davis", senderAvatar: "ED", content: "They're ready to sign, just need approval on the discount.", timestamp: "9:17 AM" },
|
||||
{ id: "m9", conversationId: "2", senderId: "user1", senderName: "Sarah Chen", senderAvatar: "SC", content: "Can you review the contract terms?", timestamp: "9:18 AM" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
participants: [
|
||||
{ id: "user1", name: "Sarah Chen", avatar: "SC", role: "Admin" },
|
||||
{ id: "user4", name: "Alex Turner", avatar: "AT", role: "Sales" },
|
||||
],
|
||||
lastMessage: "Updated the lead status for Vertex Media",
|
||||
lastMessageTime: "3h ago",
|
||||
unread: 1,
|
||||
messages: [
|
||||
{ id: "m10", conversationId: "3", senderId: "user4", senderName: "Alex Turner", senderAvatar: "AT", content: "Just a heads up, Vertex Media called back.", timestamp: "7:45 AM" },
|
||||
{ id: "m11", conversationId: "3", senderId: "user1", senderName: "Sarah Chen", senderAvatar: "SC", content: "Oh nice! What did they say?", timestamp: "7:46 AM" },
|
||||
{ id: "m12", conversationId: "3", senderId: "user4", senderName: "Alex Turner", senderAvatar: "AT", content: "They're interested in the enterprise plan. Set up a meeting for next week.", timestamp: "7:48 AM" },
|
||||
{ id: "m13", conversationId: "3", senderId: "user4", senderName: "Alex Turner", senderAvatar: "AT", content: "Updated the lead status for Vertex Media", timestamp: "7:50 AM" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
participants: [
|
||||
{ id: "user1", name: "Sarah Chen", avatar: "SC", role: "Admin" },
|
||||
{ id: "user5", name: "Lisa Wong", avatar: "LW", role: "Sales" },
|
||||
],
|
||||
lastMessage: "Thanks for the update!",
|
||||
lastMessageTime: "Yesterday",
|
||||
unread: 0,
|
||||
messages: [
|
||||
{ id: "m14", conversationId: "4", senderId: "user5", senderName: "Lisa Wong", senderAvatar: "LW", content: "Sarah, I just closed the deal with Crafted Web Agency!", timestamp: "4:20 PM" },
|
||||
{ id: "m15", conversationId: "4", senderId: "user1", senderName: "Sarah Chen", senderAvatar: "SC", content: "Congratulations Lisa! That's fantastic!", timestamp: "4:21 PM" },
|
||||
{ id: "m16", conversationId: "4", senderId: "user5", senderName: "Lisa Wong", senderAvatar: "LW", content: "Thanks! Contract is signed and onboarding starts Monday.", timestamp: "4:22 PM" },
|
||||
{ id: "m17", conversationId: "4", senderId: "user1", senderName: "Sarah Chen", senderAvatar: "SC", content: "Thanks for the update!", timestamp: "4:23 PM" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
participants: [
|
||||
{ id: "user1", name: "Sarah Chen", avatar: "SC", role: "Admin" },
|
||||
{ id: "user6", name: "James Wilson", avatar: "JW", role: "Sales" },
|
||||
],
|
||||
lastMessage: "Can you assign me the new leads?",
|
||||
lastMessageTime: "Yesterday",
|
||||
unread: 0,
|
||||
messages: [
|
||||
{ id: "m18", conversationId: "5", senderId: "user6", senderName: "James Wilson", senderAvatar: "JW", content: "I've got capacity for more leads this quarter.", timestamp: "2:00 PM" },
|
||||
{ id: "m19", conversationId: "5", senderId: "user1", senderName: "Sarah Chen", senderAvatar: "SC", content: "Good to know James. I'll assign you some from the new batch.", timestamp: "2:01 PM" },
|
||||
{ id: "m20", conversationId: "5", senderId: "user6", senderName: "James Wilson", senderAvatar: "JW", content: "Can you assign me the new leads?", timestamp: "2:02 PM" },
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -19,4 +19,4 @@ export const LEAD_SOURCES = [
|
||||
|
||||
export const ITEMS_PER_PAGE = 10
|
||||
|
||||
export const COMPANY_NAME = "Coastal IT"
|
||||
export const COMPANY_NAME = "Coast IT"
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"use client"
|
||||
|
||||
import { createContext, useContext, useState, ReactNode } from "react"
|
||||
import { currentUser } from "@/data/users"
|
||||
import type { User } from "@/types"
|
||||
|
||||
interface UserContextValue {
|
||||
user: User
|
||||
updateAvatar: (url: string) => void
|
||||
}
|
||||
|
||||
const UserContext = createContext<UserContextValue | null>(null)
|
||||
|
||||
export function UserProvider({ children }: { children: ReactNode }) {
|
||||
const [avatar, setAvatar] = useState(currentUser.avatar)
|
||||
|
||||
const user: User = { ...currentUser, avatar }
|
||||
|
||||
return (
|
||||
<UserContext.Provider value={{ user, updateAvatar: setAvatar }}>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useUser() {
|
||||
const ctx = useContext(UserContext)
|
||||
if (!ctx) throw new Error("useUser must be used within UserProvider")
|
||||
return ctx
|
||||
}
|
||||
@@ -55,3 +55,22 @@ export interface ColumnFilter {
|
||||
id: string
|
||||
value: unknown
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string
|
||||
conversationId: string
|
||||
senderId: string
|
||||
senderName: string
|
||||
senderAvatar: string
|
||||
content: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface Conversation {
|
||||
id: string
|
||||
participants: { id: string; name: string; avatar: string; role: string }[]
|
||||
lastMessage: string
|
||||
lastMessageTime: string
|
||||
unread: number
|
||||
messages: ChatMessage[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user