Spaces:
Sleeping
Sleeping
Upload 531 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +28 -0
- LICENSE +21 -0
- app.py +0 -0
- assets/124852522.jpeg +0 -0
- assets/logo.jpg +0 -0
- config/__pycache__/courses.cpython-311.pyc +0 -0
- config/__pycache__/database.cpython-311.pyc +0 -0
- config/__pycache__/job_roles.cpython-311.pyc +0 -0
- config/courses.py +181 -0
- config/database.py +532 -0
- config/job_roles.py +167 -0
- dashboard/__init__.py +1 -0
- dashboard/__pycache__/__init__.cpython-311.pyc +0 -0
- dashboard/__pycache__/dashboard.cpython-311.pyc +0 -0
- dashboard/components.py +175 -0
- dashboard/dashboard.py +1155 -0
- feedback/__pycache__/feedback.cpython-311.pyc +0 -0
- feedback/feedback.db +0 -0
- feedback/feedback.py +295 -0
- feedback/schema.sql +10 -0
- jobs/__pycache__/companies.cpython-311.pyc +0 -0
- jobs/__pycache__/job_portals.cpython-311.pyc +0 -0
- jobs/__pycache__/job_search.cpython-311.pyc +0 -0
- jobs/__pycache__/linkedin_scraper.cpython-311.pyc +0 -0
- jobs/__pycache__/suggestions.cpython-311.pyc +0 -0
- jobs/__pycache__/webdriver_utils.cpython-311.pyc +0 -0
- jobs/companies.py +187 -0
- jobs/job_portals.py +288 -0
- jobs/job_search.py +510 -0
- jobs/linkedin_scraper.py +662 -0
- jobs/suggestions.py +225 -0
- jobs/webdriver_utils.py +219 -0
- packages.txt +9 -0
- poppler/poppler-24.08.0/Library/bin/Lerc.dll +3 -0
- poppler/poppler-24.08.0/Library/bin/cairo.dll +3 -0
- poppler/poppler-24.08.0/Library/bin/charset.dll +0 -0
- poppler/poppler-24.08.0/Library/bin/deflate.dll +3 -0
- poppler/poppler-24.08.0/Library/bin/expat.dll +3 -0
- poppler/poppler-24.08.0/Library/bin/fontconfig-1.dll +3 -0
- poppler/poppler-24.08.0/Library/bin/freetype.dll +3 -0
- poppler/poppler-24.08.0/Library/bin/iconv.dll +3 -0
- poppler/poppler-24.08.0/Library/bin/jpeg8.dll +3 -0
- poppler/poppler-24.08.0/Library/bin/lcms2.dll +3 -0
- poppler/poppler-24.08.0/Library/bin/libcrypto-3-x64.dll +3 -0
- poppler/poppler-24.08.0/Library/bin/libcurl.dll +3 -0
- poppler/poppler-24.08.0/Library/bin/libexpat.dll +3 -0
- poppler/poppler-24.08.0/Library/bin/liblzma.dll +3 -0
- poppler/poppler-24.08.0/Library/bin/libpng16.dll +3 -0
- poppler/poppler-24.08.0/Library/bin/libssh2.dll +3 -0
- poppler/poppler-24.08.0/Library/bin/libtiff.dll +3 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,31 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
poppler/poppler-24.08.0/Library/bin/cairo.dll filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
poppler/poppler-24.08.0/Library/bin/deflate.dll filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
poppler/poppler-24.08.0/Library/bin/expat.dll filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
poppler/poppler-24.08.0/Library/bin/fontconfig-1.dll filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
poppler/poppler-24.08.0/Library/bin/freetype.dll filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
poppler/poppler-24.08.0/Library/bin/iconv.dll filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
poppler/poppler-24.08.0/Library/bin/jpeg8.dll filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
poppler/poppler-24.08.0/Library/bin/lcms2.dll filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
poppler/poppler-24.08.0/Library/bin/Lerc.dll filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
poppler/poppler-24.08.0/Library/bin/libcrypto-3-x64.dll filter=lfs diff=lfs merge=lfs -text
|
| 46 |
+
poppler/poppler-24.08.0/Library/bin/libcurl.dll filter=lfs diff=lfs merge=lfs -text
|
| 47 |
+
poppler/poppler-24.08.0/Library/bin/libexpat.dll filter=lfs diff=lfs merge=lfs -text
|
| 48 |
+
poppler/poppler-24.08.0/Library/bin/liblzma.dll filter=lfs diff=lfs merge=lfs -text
|
| 49 |
+
poppler/poppler-24.08.0/Library/bin/libpng16.dll filter=lfs diff=lfs merge=lfs -text
|
| 50 |
+
poppler/poppler-24.08.0/Library/bin/libssh2.dll filter=lfs diff=lfs merge=lfs -text
|
| 51 |
+
poppler/poppler-24.08.0/Library/bin/libtiff.dll filter=lfs diff=lfs merge=lfs -text
|
| 52 |
+
poppler/poppler-24.08.0/Library/bin/libzstd.dll filter=lfs diff=lfs merge=lfs -text
|
| 53 |
+
poppler/poppler-24.08.0/Library/bin/openjp2.dll filter=lfs diff=lfs merge=lfs -text
|
| 54 |
+
poppler/poppler-24.08.0/Library/bin/pdftocairo.exe filter=lfs diff=lfs merge=lfs -text
|
| 55 |
+
poppler/poppler-24.08.0/Library/bin/pdftohtml.exe filter=lfs diff=lfs merge=lfs -text
|
| 56 |
+
poppler/poppler-24.08.0/Library/bin/pixman-1-0.dll filter=lfs diff=lfs merge=lfs -text
|
| 57 |
+
poppler/poppler-24.08.0/Library/bin/poppler-cpp.dll filter=lfs diff=lfs merge=lfs -text
|
| 58 |
+
poppler/poppler-24.08.0/Library/bin/poppler-glib.dll filter=lfs diff=lfs merge=lfs -text
|
| 59 |
+
poppler/poppler-24.08.0/Library/bin/poppler.dll filter=lfs diff=lfs merge=lfs -text
|
| 60 |
+
poppler/poppler-24.08.0/Library/bin/tiff.dll filter=lfs diff=lfs merge=lfs -text
|
| 61 |
+
poppler/poppler-24.08.0/Library/bin/zstd.dll filter=lfs diff=lfs merge=lfs -text
|
| 62 |
+
poppler/poppler-24.08.0/Library/bin/zstd.exe filter=lfs diff=lfs merge=lfs -text
|
| 63 |
+
resume_data.db filter=lfs diff=lfs merge=lfs -text
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 Parthib karak
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
app.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
assets/124852522.jpeg
ADDED
|
assets/logo.jpg
ADDED
|
config/__pycache__/courses.cpython-311.pyc
ADDED
|
Binary file (12.1 kB). View file
|
|
|
config/__pycache__/database.cpython-311.pyc
ADDED
|
Binary file (24.1 kB). View file
|
|
|
config/__pycache__/job_roles.cpython-311.pyc
ADDED
|
Binary file (6.23 kB). View file
|
|
|
config/courses.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Course recommendations organized by job categories
|
| 2 |
+
COURSES_BY_CATEGORY = {
|
| 3 |
+
"Software Development and Engineering": {
|
| 4 |
+
"Frontend Developer": [
|
| 5 |
+
["Frontend Web Development Bootcamp [Free]", "https://youtu.be/zJSY8tbf_ys"],
|
| 6 |
+
["React Complete Course 2024 [Free]", "https://youtu.be/bMknfKXIFA8"],
|
| 7 |
+
["The Web Developer Bootcamp", "https://www.udemy.com/course/the-web-developer-bootcamp/"],
|
| 8 |
+
["Frontend Masters Complete Path", "https://frontendmasters.com/learn/beginner/"],
|
| 9 |
+
["Advanced CSS and Sass", "https://www.udemy.com/course/advanced-css-and-sass/"]
|
| 10 |
+
],
|
| 11 |
+
"Backend Developer": [
|
| 12 |
+
["Node.js Tutorial for Beginners [Free]", "https://youtu.be/TlB_eWDSMt4"],
|
| 13 |
+
["Python Django Full Course [Free]", "https://youtu.be/o0XbHvKxw7Y"],
|
| 14 |
+
["Complete Python Developer in 2024", "https://www.udemy.com/course/complete-python-developer-zero-to-mastery/"],
|
| 15 |
+
["Java Spring Boot Complete Course", "https://www.udemy.com/course/spring-hibernate-tutorial/"],
|
| 16 |
+
["The Complete Node.js Developer Course", "https://www.udemy.com/course/the-complete-nodejs-developer-course-2/"]
|
| 17 |
+
],
|
| 18 |
+
"Full Stack Developer": [
|
| 19 |
+
["Full Stack Development Course [Free]", "https://youtu.be/nu_pCVPKzTk"],
|
| 20 |
+
["The Complete 2024 Web Development Bootcamp", "https://www.udemy.com/course/the-complete-web-development-bootcamp/"],
|
| 21 |
+
["Full Stack Engineer Career Path", "https://www.codecademy.com/learn/paths/full-stack-engineer-career-path"],
|
| 22 |
+
["MERN Stack Front To Back", "https://www.udemy.com/course/mern-stack-front-to-back/"],
|
| 23 |
+
["Full Stack Development with React & Node.js", "https://www.udemy.com/course/full-stack-react-node/"]
|
| 24 |
+
],
|
| 25 |
+
"Mobile App Developer": [
|
| 26 |
+
["Flutter & Dart Complete Course [Free]", "https://youtu.be/VPvVD8t02U8"],
|
| 27 |
+
["iOS & Swift Complete iOS App Development", "https://www.udemy.com/course/ios-13-app-development-bootcamp/"],
|
| 28 |
+
["Android Development with Kotlin", "https://www.udacity.com/course/android-kotlin-developer-nanodegree--nd940"],
|
| 29 |
+
["React Native - The Practical Guide", "https://www.udemy.com/course/react-native-the-practical-guide/"],
|
| 30 |
+
["Flutter & Firebase: Build a Complete App", "https://www.udemy.com/course/flutter-firebase-tutorial-build-5-social-media-apps/"]
|
| 31 |
+
],
|
| 32 |
+
"Game Developer": [
|
| 33 |
+
["Unity Game Development [Free]", "https://youtu.be/gB1F9G0JXOo"],
|
| 34 |
+
["Unreal Engine 5 C++ Developer", "https://www.udemy.com/course/unrealcourse/"],
|
| 35 |
+
["Complete C# Unity Game Developer 2D", "https://www.udemy.com/course/unitycourse/"],
|
| 36 |
+
["Unity Certified Programmer Exam Preparation", "https://www.udemy.com/course/unity-advance-essentials-n/"],
|
| 37 |
+
["Game Design and Development Specialization", "https://www.coursera.org/specializations/game-design-and-development"]
|
| 38 |
+
]
|
| 39 |
+
},
|
| 40 |
+
"Data Science and Analytics": {
|
| 41 |
+
"Data Scientist": [
|
| 42 |
+
["Data Science Full Course [Free]", "https://youtu.be/_V8eKsto3Ug"],
|
| 43 |
+
["IBM Data Science Professional Certificate", "https://www.coursera.org/professional-certificates/ibm-data-science"],
|
| 44 |
+
["Data Science Career Path", "https://www.codecademy.com/learn/paths/data-science"],
|
| 45 |
+
["Applied Data Science with Python", "https://www.coursera.org/specializations/data-science-python"],
|
| 46 |
+
["Complete Data Science Bootcamp", "https://www.udemy.com/course/the-data-science-course-complete-data-science-bootcamp/"]
|
| 47 |
+
],
|
| 48 |
+
"Data Analyst": [
|
| 49 |
+
["Data Analytics Full Course [Free]", "https://youtu.be/ua-CiDNNj30"],
|
| 50 |
+
["Google Data Analytics Professional Certificate", "https://www.coursera.org/professional-certificates/google-data-analytics"],
|
| 51 |
+
["Data Analyst with Python", "https://www.datacamp.com/tracks/data-analyst-with-python"],
|
| 52 |
+
["Business Analytics Specialization", "https://www.coursera.org/specializations/business-analytics"],
|
| 53 |
+
["Data Analysis with Pandas and Python", "https://www.udemy.com/course/data-analysis-with-pandas/"]
|
| 54 |
+
],
|
| 55 |
+
"Machine Learning Engineer": [
|
| 56 |
+
["Machine Learning Full Course [Free]", "https://youtu.be/jGwO_UgTS7I"],
|
| 57 |
+
["Deep Learning Specialization", "https://www.coursera.org/specializations/deep-learning"],
|
| 58 |
+
["Machine Learning Engineer Nanodegree", "https://www.udacity.com/course/machine-learning-engineer-nanodegree--nd009t"],
|
| 59 |
+
["TensorFlow Developer Certificate", "https://www.tensorflow.org/certificate"],
|
| 60 |
+
["Complete Machine Learning & Data Science Bootcamp", "https://www.udemy.com/course/complete-machine-learning-and-data-science-zero-to-mastery/"]
|
| 61 |
+
]
|
| 62 |
+
},
|
| 63 |
+
"Cloud Computing and DevOps": {
|
| 64 |
+
"Cloud Architect": [
|
| 65 |
+
["AWS Cloud Practitioner [Free]", "https://youtu.be/3hLmDS179YE"],
|
| 66 |
+
["AWS Solutions Architect Professional", "https://www.udemy.com/course/aws-solutions-architect-professional/"],
|
| 67 |
+
["Google Cloud Architect Professional Certificate", "https://www.coursera.org/professional-certificates/gcp-cloud-architect"],
|
| 68 |
+
["Microsoft Azure Architect Technologies", "https://learn.microsoft.com/en-us/certifications/azure-solutions-architect/"],
|
| 69 |
+
["Cloud Architecture with Google Cloud", "https://www.coursera.org/professional-certificates/gcp-cloud-architect"]
|
| 70 |
+
],
|
| 71 |
+
"DevOps Engineer": [
|
| 72 |
+
["DevOps Engineering Course [Free]", "https://youtu.be/j5Zsa_eOXeY"],
|
| 73 |
+
["DevOps Engineer Masters Program", "https://www.simplilearn.com/cloud-computing/devops-engineer-masters-program-training"],
|
| 74 |
+
["Docker and Kubernetes: The Complete Guide", "https://www.udemy.com/course/docker-and-kubernetes-the-complete-guide/"],
|
| 75 |
+
["GitLab CI: The Complete Guide", "https://www.udemy.com/course/gitlab-ci-pipelines-ci-cd-and-devops-for-beginners/"],
|
| 76 |
+
["Jenkins: The Complete Guide", "https://www.udemy.com/course/jenkins-from-zero-to-hero/"]
|
| 77 |
+
],
|
| 78 |
+
"Site Reliability Engineer": [
|
| 79 |
+
["SRE Course [Free]", "https://youtu.be/uTEL8Ff1Zvk"],
|
| 80 |
+
["Site Reliability Engineering: Measuring and Managing Reliability", "https://www.coursera.org/learn/site-reliability-engineering-slos"],
|
| 81 |
+
["Linux System Administration", "https://www.udemy.com/course/linux-administration-bootcamp/"],
|
| 82 |
+
["Monitoring and Alerting with Prometheus", "https://www.udemy.com/course/monitoring-and-alerting-with-prometheus/"],
|
| 83 |
+
["Advanced System Administration", "https://www.linkedin.com/learning/paths/advance-your-skills-as-a-linux-system-administrator"]
|
| 84 |
+
]
|
| 85 |
+
},
|
| 86 |
+
"Cybersecurity": {
|
| 87 |
+
"Security Analyst": [
|
| 88 |
+
["Cyber Security Full Course [Free]", "https://youtu.be/nzZkKoREEGo"],
|
| 89 |
+
["CompTIA Security+ Certification", "https://www.comptia.org/certifications/security"],
|
| 90 |
+
["Certified Information Systems Security Professional (CISSP)", "https://www.isc2.org/Certifications/CISSP"],
|
| 91 |
+
["IBM Cybersecurity Analyst Professional Certificate", "https://www.coursera.org/professional-certificates/ibm-cybersecurity-analyst"],
|
| 92 |
+
["The Complete Cyber Security Course", "https://www.udemy.com/course/the-complete-internet-security-privacy-course-volume-1/"]
|
| 93 |
+
],
|
| 94 |
+
"Penetration Tester": [
|
| 95 |
+
["Ethical Hacking Course [Free]", "https://youtu.be/3Kq1MIfTWCE"],
|
| 96 |
+
["Certified Ethical Hacker (CEH)", "https://www.eccouncil.org/programs/certified-ethical-hacker-ceh/"],
|
| 97 |
+
["Complete Ethical Hacking Bootcamp", "https://www.udemy.com/course/complete-ethical-hacking-bootcamp-zero-to-mastery/"],
|
| 98 |
+
["Web Security & Bug Bounty", "https://www.udemy.com/course/web-security-bug-bounty-learn-penetration-testing/"],
|
| 99 |
+
["Advanced Penetration Testing", "https://www.offensive-security.com/pwk-oscp/"]
|
| 100 |
+
]
|
| 101 |
+
},
|
| 102 |
+
"UI/UX Design": {
|
| 103 |
+
"UI Designer": [
|
| 104 |
+
["UI Design Course [Free]", "https://youtu.be/c9Wg6Cb_YlU"],
|
| 105 |
+
["Google UX Design Professional Certificate", "https://www.coursera.org/professional-certificates/google-ux-design"],
|
| 106 |
+
["UI Design Bootcamp", "https://www.udemy.com/course/ui-design-bootcamp/"],
|
| 107 |
+
["Advanced UI Design Course", "https://www.udacity.com/course/ui-design--ud511"],
|
| 108 |
+
["Design System Course", "https://www.designsystems.com/"]
|
| 109 |
+
],
|
| 110 |
+
"UX Designer": [
|
| 111 |
+
["UX Design Course [Free]", "https://youtu.be/uL2aArZGqzk"],
|
| 112 |
+
["UX Design Professional Certificate", "https://www.coursera.org/professional-certificates/google-ux-design"],
|
| 113 |
+
["User Experience Design Bootcamp", "https://www.udemy.com/course/user-experience-design-fundamentals/"],
|
| 114 |
+
["UX Research & Strategy", "https://www.interaction-design.org/courses"],
|
| 115 |
+
["Advanced UX Methods", "https://www.nngroup.com/courses/"]
|
| 116 |
+
]
|
| 117 |
+
},
|
| 118 |
+
"Project Management": {
|
| 119 |
+
"Project Manager": [
|
| 120 |
+
["Project Management Basics [Free]", "https://youtu.be/H0_yKBitO8M"],
|
| 121 |
+
["PMP Certification Prep", "https://www.udemy.com/course/pmp-pmbok6-35-pdus/"],
|
| 122 |
+
["Google Project Management Certificate", "https://www.coursera.org/professional-certificates/google-project-management"],
|
| 123 |
+
["Agile with Atlassian Jira", "https://www.coursera.org/learn/agile-atlassian-jira"],
|
| 124 |
+
["Scrum Master Certification", "https://www.scrum.org/professional-scrum-certifications"]
|
| 125 |
+
],
|
| 126 |
+
"Product Manager": [
|
| 127 |
+
["Product Management Course [Free]", "https://youtu.be/lYZYB9VWaeI"],
|
| 128 |
+
["Product Management Certification", "https://www.udemy.com/course/become-a-product-manager-learn-the-skills-get-a-job/"],
|
| 129 |
+
["Digital Product Management", "https://www.coursera.org/specializations/uva-darden-digital-product-management"],
|
| 130 |
+
["Product Analytics", "https://www.udacity.com/course/product-manager-nanodegree--nd036"],
|
| 131 |
+
["Agile Product Management", "https://www.scrum.org/professional-scrum-product-owner-certifications"]
|
| 132 |
+
]
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
# Helper videos for resume and interview preparation
|
| 137 |
+
RESUME_VIDEOS = {
|
| 138 |
+
"Resume Writing": [
|
| 139 |
+
["Resume Writing Masterclass [Free]", "https://youtu.be/Tt08KmFfIYQ"],
|
| 140 |
+
["How to Write a Professional Resume in 2024", "https://youtu.be/y8YH0Qbu5h4"],
|
| 141 |
+
["Resume Tips from a Hiring Manager", "https://youtu.be/u75hUSShvnc"],
|
| 142 |
+
["ATS-Friendly Resume Guide", "https://youtu.be/BYUy1yvjHxE"]
|
| 143 |
+
],
|
| 144 |
+
"Resume Design": [
|
| 145 |
+
["Create a Modern Resume in Word", "https://youtu.be/3agP4x8LYFM"],
|
| 146 |
+
["Professional Resume Design Tips", "https://youtu.be/KFaugkGVeNQ"],
|
| 147 |
+
["Resume Templates and Formatting", "https://youtu.be/GyjzOKdaioU"]
|
| 148 |
+
]
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
INTERVIEW_VIDEOS = {
|
| 152 |
+
"Technical Interviews": [
|
| 153 |
+
["Coding Interview Preparation [Free]", "https://youtu.be/HG68Ymazo18"],
|
| 154 |
+
["System Design Interview Guide", "https://youtu.be/BOvAAoxM4vg"],
|
| 155 |
+
["Data Structures & Algorithms Interview", "https://youtu.be/KukmClH1KoA"]
|
| 156 |
+
],
|
| 157 |
+
"Behavioral Interviews": [
|
| 158 |
+
["STAR Method Explained", "https://youtu.be/7_aAicmPB3A"],
|
| 159 |
+
["Common Behavioral Questions", "https://youtu.be/1mHjMNZZvFo"],
|
| 160 |
+
["Interview Body Language Tips", "https://youtu.be/WfdtKbAJOmE"]
|
| 161 |
+
],
|
| 162 |
+
"Interview Tips": [
|
| 163 |
+
["Salary Negotiation Tips", "https://youtu.be/IBjM-F56qS0"],
|
| 164 |
+
["Questions to Ask Interviewers", "https://youtu.be/4tYoVx0QoN0"],
|
| 165 |
+
["Remote Interview Best Practices", "https://youtu.be/Ge0Udbws1kc"]
|
| 166 |
+
]
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
def get_courses_for_role(role_name):
|
| 170 |
+
"""Helper function to get courses for a specific role"""
|
| 171 |
+
for category, roles in COURSES_BY_CATEGORY.items():
|
| 172 |
+
if role_name in roles:
|
| 173 |
+
return roles[role_name]
|
| 174 |
+
return None
|
| 175 |
+
|
| 176 |
+
def get_category_for_role(role_name):
|
| 177 |
+
"""Helper function to get the category for a specific role"""
|
| 178 |
+
for category, roles in COURSES_BY_CATEGORY.items():
|
| 179 |
+
if role_name in roles:
|
| 180 |
+
return category
|
| 181 |
+
return None
|
config/database.py
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
|
| 4 |
+
def get_database_connection():
|
| 5 |
+
"""Create and return a database connection"""
|
| 6 |
+
conn = sqlite3.connect('resume_data.db')
|
| 7 |
+
return conn
|
| 8 |
+
|
| 9 |
+
def init_database():
|
| 10 |
+
"""Initialize database tables"""
|
| 11 |
+
conn = get_database_connection()
|
| 12 |
+
cursor = conn.cursor()
|
| 13 |
+
|
| 14 |
+
# Create resume_data table
|
| 15 |
+
cursor.execute('''
|
| 16 |
+
CREATE TABLE IF NOT EXISTS resume_data (
|
| 17 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 18 |
+
name TEXT NOT NULL,
|
| 19 |
+
email TEXT NOT NULL,
|
| 20 |
+
phone TEXT NOT NULL,
|
| 21 |
+
linkedin TEXT,
|
| 22 |
+
github TEXT,
|
| 23 |
+
portfolio TEXT,
|
| 24 |
+
summary TEXT,
|
| 25 |
+
target_role TEXT,
|
| 26 |
+
target_category TEXT,
|
| 27 |
+
education TEXT,
|
| 28 |
+
experience TEXT,
|
| 29 |
+
projects TEXT,
|
| 30 |
+
skills TEXT,
|
| 31 |
+
template TEXT,
|
| 32 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 33 |
+
)
|
| 34 |
+
''')
|
| 35 |
+
|
| 36 |
+
# Create resume_skills table
|
| 37 |
+
cursor.execute('''
|
| 38 |
+
CREATE TABLE IF NOT EXISTS resume_skills (
|
| 39 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 40 |
+
resume_id INTEGER,
|
| 41 |
+
skill_name TEXT NOT NULL,
|
| 42 |
+
skill_category TEXT NOT NULL,
|
| 43 |
+
proficiency_score REAL,
|
| 44 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 45 |
+
FOREIGN KEY (resume_id) REFERENCES resume_data (id)
|
| 46 |
+
)
|
| 47 |
+
''')
|
| 48 |
+
|
| 49 |
+
# Create resume_analysis table
|
| 50 |
+
cursor.execute('''
|
| 51 |
+
CREATE TABLE IF NOT EXISTS resume_analysis (
|
| 52 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 53 |
+
resume_id INTEGER,
|
| 54 |
+
ats_score REAL,
|
| 55 |
+
keyword_match_score REAL,
|
| 56 |
+
format_score REAL,
|
| 57 |
+
section_score REAL,
|
| 58 |
+
missing_skills TEXT,
|
| 59 |
+
recommendations TEXT,
|
| 60 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 61 |
+
FOREIGN KEY (resume_id) REFERENCES resume_data (id)
|
| 62 |
+
)
|
| 63 |
+
''')
|
| 64 |
+
|
| 65 |
+
# Create admin_logs table
|
| 66 |
+
cursor.execute('''
|
| 67 |
+
CREATE TABLE IF NOT EXISTS admin_logs (
|
| 68 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 69 |
+
admin_email TEXT NOT NULL,
|
| 70 |
+
action TEXT NOT NULL,
|
| 71 |
+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 72 |
+
)
|
| 73 |
+
''')
|
| 74 |
+
|
| 75 |
+
# Create admin table
|
| 76 |
+
cursor.execute('''
|
| 77 |
+
CREATE TABLE IF NOT EXISTS admin (
|
| 78 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 79 |
+
email TEXT NOT NULL UNIQUE,
|
| 80 |
+
password TEXT NOT NULL,
|
| 81 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 82 |
+
)
|
| 83 |
+
''')
|
| 84 |
+
|
| 85 |
+
conn.commit()
|
| 86 |
+
conn.close()
|
| 87 |
+
|
| 88 |
+
def save_resume_data(data):
|
| 89 |
+
"""Save resume data to database"""
|
| 90 |
+
conn = get_database_connection()
|
| 91 |
+
cursor = conn.cursor()
|
| 92 |
+
|
| 93 |
+
try:
|
| 94 |
+
personal_info = data.get('personal_info', {})
|
| 95 |
+
|
| 96 |
+
cursor.execute('''
|
| 97 |
+
INSERT INTO resume_data (
|
| 98 |
+
name, email, phone, linkedin, github, portfolio,
|
| 99 |
+
summary, target_role, target_category, education,
|
| 100 |
+
experience, projects, skills, template
|
| 101 |
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 102 |
+
''', (
|
| 103 |
+
personal_info.get('full_name', ''),
|
| 104 |
+
personal_info.get('email', ''),
|
| 105 |
+
personal_info.get('phone', ''),
|
| 106 |
+
personal_info.get('linkedin', ''),
|
| 107 |
+
personal_info.get('github', ''),
|
| 108 |
+
personal_info.get('portfolio', ''),
|
| 109 |
+
data.get('summary', ''),
|
| 110 |
+
data.get('target_role', ''),
|
| 111 |
+
data.get('target_category', ''),
|
| 112 |
+
str(data.get('education', [])),
|
| 113 |
+
str(data.get('experience', [])),
|
| 114 |
+
str(data.get('projects', [])),
|
| 115 |
+
str(data.get('skills', [])),
|
| 116 |
+
data.get('template', '')
|
| 117 |
+
))
|
| 118 |
+
|
| 119 |
+
conn.commit()
|
| 120 |
+
return cursor.lastrowid
|
| 121 |
+
except Exception as e:
|
| 122 |
+
print(f"Error saving resume data: {str(e)}")
|
| 123 |
+
conn.rollback()
|
| 124 |
+
return None
|
| 125 |
+
finally:
|
| 126 |
+
conn.close()
|
| 127 |
+
|
| 128 |
+
def save_analysis_data(resume_id, analysis):
|
| 129 |
+
"""Save resume analysis data"""
|
| 130 |
+
conn = get_database_connection()
|
| 131 |
+
cursor = conn.cursor()
|
| 132 |
+
|
| 133 |
+
try:
|
| 134 |
+
cursor.execute('''
|
| 135 |
+
INSERT INTO resume_analysis (
|
| 136 |
+
resume_id, ats_score, keyword_match_score,
|
| 137 |
+
format_score, section_score, missing_skills,
|
| 138 |
+
recommendations
|
| 139 |
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
| 140 |
+
''', (
|
| 141 |
+
resume_id,
|
| 142 |
+
float(analysis.get('ats_score', 0)),
|
| 143 |
+
float(analysis.get('keyword_match_score', 0)),
|
| 144 |
+
float(analysis.get('format_score', 0)),
|
| 145 |
+
float(analysis.get('section_score', 0)),
|
| 146 |
+
analysis.get('missing_skills', ''),
|
| 147 |
+
analysis.get('recommendations', '')
|
| 148 |
+
))
|
| 149 |
+
|
| 150 |
+
conn.commit()
|
| 151 |
+
except Exception as e:
|
| 152 |
+
print(f"Error saving analysis data: {str(e)}")
|
| 153 |
+
conn.rollback()
|
| 154 |
+
finally:
|
| 155 |
+
conn.close()
|
| 156 |
+
|
| 157 |
+
def get_resume_stats():
|
| 158 |
+
"""Get statistics about resumes"""
|
| 159 |
+
conn = get_database_connection()
|
| 160 |
+
cursor = conn.cursor()
|
| 161 |
+
|
| 162 |
+
try:
|
| 163 |
+
# Get total resumes
|
| 164 |
+
cursor.execute('SELECT COUNT(*) FROM resume_data')
|
| 165 |
+
total_resumes = cursor.fetchone()[0]
|
| 166 |
+
|
| 167 |
+
# Get average ATS score
|
| 168 |
+
cursor.execute('SELECT AVG(ats_score) FROM resume_analysis')
|
| 169 |
+
avg_ats_score = cursor.fetchone()[0] or 0
|
| 170 |
+
|
| 171 |
+
# Get recent activity
|
| 172 |
+
cursor.execute('''
|
| 173 |
+
SELECT name, target_role, created_at
|
| 174 |
+
FROM resume_data
|
| 175 |
+
ORDER BY created_at DESC
|
| 176 |
+
LIMIT 5
|
| 177 |
+
''')
|
| 178 |
+
recent_activity = cursor.fetchall()
|
| 179 |
+
|
| 180 |
+
return {
|
| 181 |
+
'total_resumes': total_resumes,
|
| 182 |
+
'avg_ats_score': round(avg_ats_score, 2),
|
| 183 |
+
'recent_activity': recent_activity
|
| 184 |
+
}
|
| 185 |
+
except Exception as e:
|
| 186 |
+
print(f"Error getting resume stats: {str(e)}")
|
| 187 |
+
return None
|
| 188 |
+
finally:
|
| 189 |
+
conn.close()
|
| 190 |
+
|
| 191 |
+
def log_admin_action(admin_email, action):
|
| 192 |
+
"""Log admin login/logout actions"""
|
| 193 |
+
conn = get_database_connection()
|
| 194 |
+
cursor = conn.cursor()
|
| 195 |
+
|
| 196 |
+
try:
|
| 197 |
+
cursor.execute('''
|
| 198 |
+
INSERT INTO admin_logs (admin_email, action)
|
| 199 |
+
VALUES (?, ?)
|
| 200 |
+
''', (admin_email, action))
|
| 201 |
+
conn.commit()
|
| 202 |
+
except Exception as e:
|
| 203 |
+
print(f"Error logging admin action: {str(e)}")
|
| 204 |
+
finally:
|
| 205 |
+
conn.close()
|
| 206 |
+
|
| 207 |
+
def get_admin_logs():
|
| 208 |
+
"""Get all admin login/logout logs"""
|
| 209 |
+
conn = get_database_connection()
|
| 210 |
+
cursor = conn.cursor()
|
| 211 |
+
|
| 212 |
+
try:
|
| 213 |
+
cursor.execute('''
|
| 214 |
+
SELECT admin_email, action, timestamp
|
| 215 |
+
FROM admin_logs
|
| 216 |
+
ORDER BY timestamp DESC
|
| 217 |
+
''')
|
| 218 |
+
return cursor.fetchall()
|
| 219 |
+
except Exception as e:
|
| 220 |
+
print(f"Error getting admin logs: {str(e)}")
|
| 221 |
+
return []
|
| 222 |
+
finally:
|
| 223 |
+
conn.close()
|
| 224 |
+
|
| 225 |
+
def get_all_resume_data():
|
| 226 |
+
"""Get all resume data for admin dashboard"""
|
| 227 |
+
conn = get_database_connection()
|
| 228 |
+
cursor = conn.cursor()
|
| 229 |
+
|
| 230 |
+
try:
|
| 231 |
+
# Get resume data joined with analysis data
|
| 232 |
+
cursor.execute('''
|
| 233 |
+
SELECT
|
| 234 |
+
r.id,
|
| 235 |
+
r.name,
|
| 236 |
+
r.email,
|
| 237 |
+
r.phone,
|
| 238 |
+
r.linkedin,
|
| 239 |
+
r.github,
|
| 240 |
+
r.portfolio,
|
| 241 |
+
r.target_role,
|
| 242 |
+
r.target_category,
|
| 243 |
+
r.created_at,
|
| 244 |
+
a.ats_score,
|
| 245 |
+
a.keyword_match_score,
|
| 246 |
+
a.format_score,
|
| 247 |
+
a.section_score
|
| 248 |
+
FROM resume_data r
|
| 249 |
+
LEFT JOIN resume_analysis a ON r.id = a.resume_id
|
| 250 |
+
ORDER BY r.created_at DESC
|
| 251 |
+
''')
|
| 252 |
+
return cursor.fetchall()
|
| 253 |
+
except Exception as e:
|
| 254 |
+
print(f"Error getting resume data: {str(e)}")
|
| 255 |
+
return []
|
| 256 |
+
finally:
|
| 257 |
+
conn.close()
|
| 258 |
+
|
| 259 |
+
def verify_admin(email, password):
|
| 260 |
+
"""Verify admin credentials"""
|
| 261 |
+
conn = get_database_connection()
|
| 262 |
+
cursor = conn.cursor()
|
| 263 |
+
|
| 264 |
+
try:
|
| 265 |
+
cursor.execute('SELECT * FROM admin WHERE email = ? AND password = ?', (email, password))
|
| 266 |
+
result = cursor.fetchone()
|
| 267 |
+
return bool(result)
|
| 268 |
+
except Exception as e:
|
| 269 |
+
print(f"Error verifying admin: {str(e)}")
|
| 270 |
+
return False
|
| 271 |
+
finally:
|
| 272 |
+
conn.close()
|
| 273 |
+
|
| 274 |
+
def add_admin(email, password):
|
| 275 |
+
"""Add a new admin"""
|
| 276 |
+
conn = get_database_connection()
|
| 277 |
+
cursor = conn.cursor()
|
| 278 |
+
|
| 279 |
+
try:
|
| 280 |
+
cursor.execute('INSERT INTO admin (email, password) VALUES (?, ?)', (email, password))
|
| 281 |
+
conn.commit()
|
| 282 |
+
return True
|
| 283 |
+
except Exception as e:
|
| 284 |
+
print(f"Error adding admin: {str(e)}")
|
| 285 |
+
return False
|
| 286 |
+
finally:
|
| 287 |
+
conn.close()
|
| 288 |
+
|
| 289 |
+
def save_ai_analysis_data(resume_id, analysis_data):
|
| 290 |
+
"""Save AI analysis data to the database"""
|
| 291 |
+
conn = get_database_connection()
|
| 292 |
+
cursor = conn.cursor()
|
| 293 |
+
|
| 294 |
+
try:
|
| 295 |
+
# Check if the ai_analysis table exists
|
| 296 |
+
cursor.execute("""
|
| 297 |
+
CREATE TABLE IF NOT EXISTS ai_analysis (
|
| 298 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 299 |
+
resume_id INTEGER,
|
| 300 |
+
model_used TEXT,
|
| 301 |
+
resume_score INTEGER,
|
| 302 |
+
job_role TEXT,
|
| 303 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 304 |
+
FOREIGN KEY (resume_id) REFERENCES resume_data (id)
|
| 305 |
+
)
|
| 306 |
+
""")
|
| 307 |
+
|
| 308 |
+
# Insert the analysis data
|
| 309 |
+
cursor.execute("""
|
| 310 |
+
INSERT INTO ai_analysis (
|
| 311 |
+
resume_id, model_used, resume_score, job_role
|
| 312 |
+
) VALUES (?, ?, ?, ?)
|
| 313 |
+
""", (
|
| 314 |
+
resume_id,
|
| 315 |
+
analysis_data.get('model_used', ''),
|
| 316 |
+
analysis_data.get('resume_score', 0),
|
| 317 |
+
analysis_data.get('job_role', '')
|
| 318 |
+
))
|
| 319 |
+
|
| 320 |
+
conn.commit()
|
| 321 |
+
return cursor.lastrowid
|
| 322 |
+
except Exception as e:
|
| 323 |
+
print(f"Error saving AI analysis data: {e}")
|
| 324 |
+
conn.rollback()
|
| 325 |
+
raise
|
| 326 |
+
finally:
|
| 327 |
+
conn.close()
|
| 328 |
+
|
| 329 |
+
def get_ai_analysis_stats():
|
| 330 |
+
"""Get statistics about AI analyzer usage"""
|
| 331 |
+
conn = get_database_connection()
|
| 332 |
+
cursor = conn.cursor()
|
| 333 |
+
|
| 334 |
+
try:
|
| 335 |
+
# Check if the ai_analysis table exists
|
| 336 |
+
cursor.execute("""
|
| 337 |
+
SELECT name FROM sqlite_master WHERE type='table' AND name='ai_analysis'
|
| 338 |
+
""")
|
| 339 |
+
|
| 340 |
+
if not cursor.fetchone():
|
| 341 |
+
return {
|
| 342 |
+
"total_analyses": 0,
|
| 343 |
+
"model_usage": [],
|
| 344 |
+
"average_score": 0,
|
| 345 |
+
"top_job_roles": []
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
# Get total number of analyses
|
| 349 |
+
cursor.execute("SELECT COUNT(*) FROM ai_analysis")
|
| 350 |
+
total_analyses = cursor.fetchone()[0]
|
| 351 |
+
|
| 352 |
+
# Get model usage statistics
|
| 353 |
+
cursor.execute("""
|
| 354 |
+
SELECT model_used, COUNT(*) as count
|
| 355 |
+
FROM ai_analysis
|
| 356 |
+
GROUP BY model_used
|
| 357 |
+
ORDER BY count DESC
|
| 358 |
+
""")
|
| 359 |
+
model_usage = [{"model": row[0], "count": row[1]} for row in cursor.fetchall()]
|
| 360 |
+
|
| 361 |
+
# Get average resume score
|
| 362 |
+
cursor.execute("SELECT AVG(resume_score) FROM ai_analysis")
|
| 363 |
+
average_score = cursor.fetchone()[0] or 0
|
| 364 |
+
|
| 365 |
+
# Get top job roles
|
| 366 |
+
cursor.execute("""
|
| 367 |
+
SELECT job_role, COUNT(*) as count
|
| 368 |
+
FROM ai_analysis
|
| 369 |
+
GROUP BY job_role
|
| 370 |
+
ORDER BY count DESC
|
| 371 |
+
LIMIT 5
|
| 372 |
+
""")
|
| 373 |
+
top_job_roles = [{"role": row[0], "count": row[1]} for row in cursor.fetchall()]
|
| 374 |
+
|
| 375 |
+
return {
|
| 376 |
+
"total_analyses": total_analyses,
|
| 377 |
+
"model_usage": model_usage,
|
| 378 |
+
"average_score": round(average_score, 1),
|
| 379 |
+
"top_job_roles": top_job_roles
|
| 380 |
+
}
|
| 381 |
+
except Exception as e:
|
| 382 |
+
print(f"Error getting AI analysis stats: {e}")
|
| 383 |
+
return {
|
| 384 |
+
"total_analyses": 0,
|
| 385 |
+
"model_usage": [],
|
| 386 |
+
"average_score": 0,
|
| 387 |
+
"top_job_roles": []
|
| 388 |
+
}
|
| 389 |
+
finally:
|
| 390 |
+
conn.close()
|
| 391 |
+
|
| 392 |
+
def get_detailed_ai_analysis_stats():
|
| 393 |
+
"""Get detailed statistics about AI analyzer usage including daily trends"""
|
| 394 |
+
conn = get_database_connection()
|
| 395 |
+
cursor = conn.cursor()
|
| 396 |
+
|
| 397 |
+
try:
|
| 398 |
+
# Check if the ai_analysis table exists
|
| 399 |
+
cursor.execute("""
|
| 400 |
+
SELECT name FROM sqlite_master WHERE type='table' AND name='ai_analysis'
|
| 401 |
+
""")
|
| 402 |
+
|
| 403 |
+
if not cursor.fetchone():
|
| 404 |
+
return {
|
| 405 |
+
"total_analyses": 0,
|
| 406 |
+
"model_usage": [],
|
| 407 |
+
"average_score": 0,
|
| 408 |
+
"top_job_roles": [],
|
| 409 |
+
"daily_trend": [],
|
| 410 |
+
"score_distribution": [],
|
| 411 |
+
"recent_analyses": []
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
# Get total number of analyses
|
| 415 |
+
cursor.execute("SELECT COUNT(*) FROM ai_analysis")
|
| 416 |
+
total_analyses = cursor.fetchone()[0]
|
| 417 |
+
|
| 418 |
+
# Get model usage statistics
|
| 419 |
+
cursor.execute("""
|
| 420 |
+
SELECT model_used, COUNT(*) as count
|
| 421 |
+
FROM ai_analysis
|
| 422 |
+
GROUP BY model_used
|
| 423 |
+
ORDER BY count DESC
|
| 424 |
+
""")
|
| 425 |
+
model_usage = [{"model": row[0], "count": row[1]} for row in cursor.fetchall()]
|
| 426 |
+
|
| 427 |
+
# Get average resume score
|
| 428 |
+
cursor.execute("SELECT AVG(resume_score) FROM ai_analysis")
|
| 429 |
+
average_score = cursor.fetchone()[0] or 0
|
| 430 |
+
|
| 431 |
+
# Get top job roles
|
| 432 |
+
cursor.execute("""
|
| 433 |
+
SELECT job_role, COUNT(*) as count
|
| 434 |
+
FROM ai_analysis
|
| 435 |
+
GROUP BY job_role
|
| 436 |
+
ORDER BY count DESC
|
| 437 |
+
LIMIT 5
|
| 438 |
+
""")
|
| 439 |
+
top_job_roles = [{"role": row[0], "count": row[1]} for row in cursor.fetchall()]
|
| 440 |
+
|
| 441 |
+
# Get daily trend for the last 7 days
|
| 442 |
+
cursor.execute("""
|
| 443 |
+
SELECT DATE(created_at) as date, COUNT(*) as count
|
| 444 |
+
FROM ai_analysis
|
| 445 |
+
WHERE created_at >= date('now', '-7 days')
|
| 446 |
+
GROUP BY DATE(created_at)
|
| 447 |
+
ORDER BY date
|
| 448 |
+
""")
|
| 449 |
+
daily_trend = [{"date": row[0], "count": row[1]} for row in cursor.fetchall()]
|
| 450 |
+
|
| 451 |
+
# Get score distribution
|
| 452 |
+
score_ranges = [
|
| 453 |
+
{"min": 0, "max": 20, "range": "0-20"},
|
| 454 |
+
{"min": 21, "max": 40, "range": "21-40"},
|
| 455 |
+
{"min": 41, "max": 60, "range": "41-60"},
|
| 456 |
+
{"min": 61, "max": 80, "range": "61-80"},
|
| 457 |
+
{"min": 81, "max": 100, "range": "81-100"}
|
| 458 |
+
]
|
| 459 |
+
|
| 460 |
+
score_distribution = []
|
| 461 |
+
for range_info in score_ranges:
|
| 462 |
+
cursor.execute("""
|
| 463 |
+
SELECT COUNT(*) FROM ai_analysis
|
| 464 |
+
WHERE resume_score >= ? AND resume_score <= ?
|
| 465 |
+
""", (range_info["min"], range_info["max"]))
|
| 466 |
+
count = cursor.fetchone()[0]
|
| 467 |
+
score_distribution.append({"range": range_info["range"], "count": count})
|
| 468 |
+
|
| 469 |
+
# Get recent analyses
|
| 470 |
+
cursor.execute("""
|
| 471 |
+
SELECT model_used, resume_score, job_role, datetime(created_at) as date
|
| 472 |
+
FROM ai_analysis
|
| 473 |
+
ORDER BY created_at DESC
|
| 474 |
+
LIMIT 5
|
| 475 |
+
""")
|
| 476 |
+
recent_analyses = [
|
| 477 |
+
{
|
| 478 |
+
"model": row[0],
|
| 479 |
+
"score": row[1],
|
| 480 |
+
"job_role": row[2],
|
| 481 |
+
"date": row[3]
|
| 482 |
+
} for row in cursor.fetchall()
|
| 483 |
+
]
|
| 484 |
+
|
| 485 |
+
return {
|
| 486 |
+
"total_analyses": total_analyses,
|
| 487 |
+
"model_usage": model_usage,
|
| 488 |
+
"average_score": round(average_score, 1),
|
| 489 |
+
"top_job_roles": top_job_roles,
|
| 490 |
+
"daily_trend": daily_trend,
|
| 491 |
+
"score_distribution": score_distribution,
|
| 492 |
+
"recent_analyses": recent_analyses
|
| 493 |
+
}
|
| 494 |
+
except Exception as e:
|
| 495 |
+
print(f"Error getting detailed AI analysis stats: {e}")
|
| 496 |
+
return {
|
| 497 |
+
"total_analyses": 0,
|
| 498 |
+
"model_usage": [],
|
| 499 |
+
"average_score": 0,
|
| 500 |
+
"top_job_roles": [],
|
| 501 |
+
"daily_trend": [],
|
| 502 |
+
"score_distribution": [],
|
| 503 |
+
"recent_analyses": []
|
| 504 |
+
}
|
| 505 |
+
finally:
|
| 506 |
+
conn.close()
|
| 507 |
+
|
| 508 |
+
def reset_ai_analysis_stats():
|
| 509 |
+
"""Reset AI analysis statistics by truncating the ai_analysis table"""
|
| 510 |
+
conn = get_database_connection()
|
| 511 |
+
cursor = conn.cursor()
|
| 512 |
+
|
| 513 |
+
try:
|
| 514 |
+
# Check if the ai_analysis table exists
|
| 515 |
+
cursor.execute("""
|
| 516 |
+
SELECT name FROM sqlite_master WHERE type='table' AND name='ai_analysis'
|
| 517 |
+
""")
|
| 518 |
+
|
| 519 |
+
if not cursor.fetchone():
|
| 520 |
+
return {"success": False, "message": "AI analysis table does not exist"}
|
| 521 |
+
|
| 522 |
+
# Delete all records from the ai_analysis table
|
| 523 |
+
cursor.execute("DELETE FROM ai_analysis")
|
| 524 |
+
conn.commit()
|
| 525 |
+
|
| 526 |
+
return {"success": True, "message": "AI analysis statistics have been reset successfully"}
|
| 527 |
+
except Exception as e:
|
| 528 |
+
conn.rollback()
|
| 529 |
+
print(f"Error resetting AI analysis stats: {e}")
|
| 530 |
+
return {"success": False, "message": f"Error resetting AI analysis statistics: {str(e)}"}
|
| 531 |
+
finally:
|
| 532 |
+
conn.close()
|
config/job_roles.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
JOB_ROLES = {
|
| 2 |
+
"Software Development and Engineering": {
|
| 3 |
+
"Frontend Developer": {
|
| 4 |
+
"required_skills": ["HTML", "CSS", "JavaScript", "React", "Angular", "Vue.js", "UI/UX", "Responsive Design"],
|
| 5 |
+
"description": "Create user interfaces and implement visual elements",
|
| 6 |
+
"sections": ["Technical Skills", "Projects", "Work Experience", "Education"],
|
| 7 |
+
"recommended_skills": {
|
| 8 |
+
"technical": ["HTML5", "CSS3", "JavaScript", "React/Angular/Vue", "TypeScript", "Git"],
|
| 9 |
+
"soft": ["Communication", "Problem-solving", "Attention to detail", "Creativity"]
|
| 10 |
+
}
|
| 11 |
+
},
|
| 12 |
+
"Backend Developer": {
|
| 13 |
+
"required_skills": ["Python", "Java", "Node.js", "SQL", "APIs", "Django", "Flask", "Database Design"],
|
| 14 |
+
"description": "Build server-side logic and databases",
|
| 15 |
+
"sections": ["Technical Skills", "System Architecture", "Work Experience", "Education"],
|
| 16 |
+
"recommended_skills": {
|
| 17 |
+
"technical": ["Python/Java/Node.js", "SQL", "RESTful APIs", "Microservices", "Docker"],
|
| 18 |
+
"soft": ["Analytical thinking", "Problem-solving", "Team collaboration"]
|
| 19 |
+
}
|
| 20 |
+
},
|
| 21 |
+
"Full Stack Developer": {
|
| 22 |
+
"required_skills": ["Frontend Tech", "Backend Tech", "Databases", "DevOps", "System Design", "APIs"],
|
| 23 |
+
"description": "Handle both client and server-side development",
|
| 24 |
+
"sections": ["Technical Skills", "Full Stack Projects", "Work Experience", "Education"],
|
| 25 |
+
"recommended_skills": {
|
| 26 |
+
"technical": ["Frontend & Backend technologies", "Database design", "API development", "DevOps"],
|
| 27 |
+
"soft": ["Versatility", "Project management", "Communication"]
|
| 28 |
+
}
|
| 29 |
+
},
|
| 30 |
+
"Mobile App Developer": {
|
| 31 |
+
"required_skills": ["Swift", "Kotlin", "React Native", "Flutter", "Mobile UI/UX", "App Store Deployment"],
|
| 32 |
+
"description": "Develop mobile applications for iOS and Android platforms",
|
| 33 |
+
"sections": ["Technical Skills", "Mobile Projects", "Work Experience", "Education"],
|
| 34 |
+
"recommended_skills": {
|
| 35 |
+
"technical": ["iOS/Android Development", "Cross-platform frameworks", "Mobile UI/UX", "App Performance"],
|
| 36 |
+
"soft": ["User-centric thinking", "Problem-solving", "Attention to detail"]
|
| 37 |
+
}
|
| 38 |
+
},
|
| 39 |
+
"Game Developer": {
|
| 40 |
+
"required_skills": ["Unity", "Unreal Engine", "C++", "C#", "3D Graphics", "Game Physics"],
|
| 41 |
+
"description": "Create engaging and interactive games",
|
| 42 |
+
"sections": ["Technical Skills", "Game Projects", "Work Experience", "Education"],
|
| 43 |
+
"recommended_skills": {
|
| 44 |
+
"technical": ["Game Engines", "Graphics Programming", "Physics Simulation", "Multiplayer"],
|
| 45 |
+
"soft": ["Creativity", "Problem-solving", "Team collaboration"]
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
},
|
| 49 |
+
"Data Science and Analytics": {
|
| 50 |
+
"Data Scientist": {
|
| 51 |
+
"required_skills": ["Python", "R", "Machine Learning", "Statistics", "SQL", "Deep Learning"],
|
| 52 |
+
"description": "Analyze complex data sets to find patterns",
|
| 53 |
+
"sections": ["Technical Skills", "Projects", "Research", "Education"],
|
| 54 |
+
"recommended_skills": {
|
| 55 |
+
"technical": ["Python", "R", "Machine Learning", "Statistical Analysis", "Big Data"],
|
| 56 |
+
"soft": ["Analytical thinking", "Research", "Problem-solving"]
|
| 57 |
+
}
|
| 58 |
+
},
|
| 59 |
+
"Data Analyst": {
|
| 60 |
+
"required_skills": ["SQL", "Excel", "Python", "Data Visualization", "Statistics"],
|
| 61 |
+
"description": "Transform data into insights",
|
| 62 |
+
"sections": ["Technical Skills", "Analysis Projects", "Work Experience", "Education"],
|
| 63 |
+
"recommended_skills": {
|
| 64 |
+
"technical": ["SQL", "Excel", "Python", "Tableau/Power BI", "Statistical Analysis"],
|
| 65 |
+
"soft": ["Data interpretation", "Communication", "Attention to detail"]
|
| 66 |
+
}
|
| 67 |
+
},
|
| 68 |
+
"Machine Learning Engineer": {
|
| 69 |
+
"required_skills": ["Python", "TensorFlow", "PyTorch", "MLOps", "Deep Learning"],
|
| 70 |
+
"description": "Build and deploy machine learning models",
|
| 71 |
+
"sections": ["Technical Skills", "ML Projects", "Work Experience", "Education"],
|
| 72 |
+
"recommended_skills": {
|
| 73 |
+
"technical": ["Machine Learning", "Deep Learning", "MLOps", "Model Deployment"],
|
| 74 |
+
"soft": ["Research", "Problem-solving", "Critical thinking"]
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
},
|
| 78 |
+
"Cloud Computing and DevOps": {
|
| 79 |
+
"Cloud Architect": {
|
| 80 |
+
"required_skills": ["AWS", "Azure", "GCP", "Infrastructure as Code", "Security"],
|
| 81 |
+
"description": "Design and manage cloud infrastructure",
|
| 82 |
+
"sections": ["Technical Skills", "Cloud Projects", "Work Experience", "Certifications"],
|
| 83 |
+
"recommended_skills": {
|
| 84 |
+
"technical": ["Cloud Platforms", "Security", "Networking", "Cost Optimization"],
|
| 85 |
+
"soft": ["Strategic thinking", "Problem-solving", "Communication"]
|
| 86 |
+
}
|
| 87 |
+
},
|
| 88 |
+
"DevOps Engineer": {
|
| 89 |
+
"required_skills": ["Docker", "Kubernetes", "CI/CD", "Automation", "Monitoring"],
|
| 90 |
+
"description": "Implement DevOps practices and tools",
|
| 91 |
+
"sections": ["Technical Skills", "DevOps Projects", "Work Experience", "Education"],
|
| 92 |
+
"recommended_skills": {
|
| 93 |
+
"technical": ["Containerization", "Orchestration", "CI/CD", "Infrastructure as Code"],
|
| 94 |
+
"soft": ["Automation mindset", "Problem-solving", "Team collaboration"]
|
| 95 |
+
}
|
| 96 |
+
},
|
| 97 |
+
"Site Reliability Engineer": {
|
| 98 |
+
"required_skills": ["Linux", "Monitoring", "Automation", "Performance Tuning", "Incident Response"],
|
| 99 |
+
"description": "Ensure system reliability and performance",
|
| 100 |
+
"sections": ["Technical Skills", "SRE Projects", "Work Experience", "Education"],
|
| 101 |
+
"recommended_skills": {
|
| 102 |
+
"technical": ["System Administration", "Monitoring", "Automation", "Incident Management"],
|
| 103 |
+
"soft": ["Problem-solving", "Communication", "Critical thinking"]
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
},
|
| 107 |
+
"Cybersecurity": {
|
| 108 |
+
"Security Analyst": {
|
| 109 |
+
"required_skills": ["Network Security", "Threat Detection", "Security Tools", "Incident Response"],
|
| 110 |
+
"description": "Monitor and protect against security threats",
|
| 111 |
+
"sections": ["Technical Skills", "Security Projects", "Work Experience", "Certifications"],
|
| 112 |
+
"recommended_skills": {
|
| 113 |
+
"technical": ["Security Tools", "Threat Analysis", "Incident Response", "Compliance"],
|
| 114 |
+
"soft": ["Analytical thinking", "Attention to detail", "Communication"]
|
| 115 |
+
}
|
| 116 |
+
},
|
| 117 |
+
"Penetration Tester": {
|
| 118 |
+
"required_skills": ["Ethical Hacking", "Security Tools", "Network Security", "Web Security"],
|
| 119 |
+
"description": "Test systems for security vulnerabilities",
|
| 120 |
+
"sections": ["Technical Skills", "Security Projects", "Work Experience", "Certifications"],
|
| 121 |
+
"recommended_skills": {
|
| 122 |
+
"technical": ["Penetration Testing", "Security Tools", "Vulnerability Assessment"],
|
| 123 |
+
"soft": ["Ethical mindset", "Problem-solving", "Report writing"]
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
},
|
| 127 |
+
"UI/UX Design": {
|
| 128 |
+
"UI Designer": {
|
| 129 |
+
"required_skills": ["Figma", "Adobe XD", "Visual Design", "Typography", "Color Theory"],
|
| 130 |
+
"description": "Create beautiful user interfaces",
|
| 131 |
+
"sections": ["Design Skills", "Portfolio", "Work Experience", "Education"],
|
| 132 |
+
"recommended_skills": {
|
| 133 |
+
"technical": ["Design Tools", "Visual Design", "Prototyping", "Design Systems"],
|
| 134 |
+
"soft": ["Creativity", "Attention to detail", "User empathy"]
|
| 135 |
+
}
|
| 136 |
+
},
|
| 137 |
+
"UX Designer": {
|
| 138 |
+
"required_skills": ["User Research", "Wireframing", "Prototyping", "Usability Testing"],
|
| 139 |
+
"description": "Design user experiences and flows",
|
| 140 |
+
"sections": ["Design Skills", "Case Studies", "Work Experience", "Education"],
|
| 141 |
+
"recommended_skills": {
|
| 142 |
+
"technical": ["Research Methods", "Information Architecture", "User Testing"],
|
| 143 |
+
"soft": ["Empathy", "Communication", "Problem-solving"]
|
| 144 |
+
}
|
| 145 |
+
}
|
| 146 |
+
},
|
| 147 |
+
"Project Management": {
|
| 148 |
+
"Project Manager": {
|
| 149 |
+
"required_skills": ["Project Planning", "Agile", "Scrum", "Risk Management", "Stakeholder Management"],
|
| 150 |
+
"description": "Lead and manage project delivery",
|
| 151 |
+
"sections": ["Management Skills", "Project History", "Work Experience", "Certifications"],
|
| 152 |
+
"recommended_skills": {
|
| 153 |
+
"technical": ["Project Management Tools", "Agile Methodologies", "Budgeting"],
|
| 154 |
+
"soft": ["Leadership", "Communication", "Problem-solving"]
|
| 155 |
+
}
|
| 156 |
+
},
|
| 157 |
+
"Product Manager": {
|
| 158 |
+
"required_skills": ["Product Strategy", "Market Research", "User Stories", "Roadmapping"],
|
| 159 |
+
"description": "Define and drive product vision",
|
| 160 |
+
"sections": ["Product Skills", "Product Launches", "Work Experience", "Education"],
|
| 161 |
+
"recommended_skills": {
|
| 162 |
+
"technical": ["Product Management Tools", "Analytics", "Market Research"],
|
| 163 |
+
"soft": ["Strategic thinking", "Communication", "Leadership"]
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
}
|
dashboard/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from .dashboard import DashboardManager
|
dashboard/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (245 Bytes). View file
|
|
|
dashboard/__pycache__/dashboard.cpython-311.pyc
ADDED
|
Binary file (52.2 kB). View file
|
|
|
dashboard/components.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import plotly.graph_objects as go
|
| 3 |
+
from plotly.subplots import make_subplots
|
| 4 |
+
import pandas as pd
|
| 5 |
+
from datetime import datetime, timedelta
|
| 6 |
+
|
| 7 |
+
class DashboardComponents:
|
| 8 |
+
def __init__(self, colors):
|
| 9 |
+
self.colors = colors
|
| 10 |
+
|
| 11 |
+
def render_metric_card(self, title, value, subtitle=None, trend=None, trend_value=None):
|
| 12 |
+
"""Render a metric card with optional trend indicator"""
|
| 13 |
+
trend_html = ""
|
| 14 |
+
if trend and trend_value:
|
| 15 |
+
trend_color = self.colors['success'] if trend == 'up' else self.colors['danger']
|
| 16 |
+
trend_arrow = '↑' if trend == 'up' else '↓'
|
| 17 |
+
trend_html = f"""
|
| 18 |
+
<div style="color: {trend_color}; font-size: 0.9rem; margin-top: 5px;">
|
| 19 |
+
{trend_arrow} {trend_value}%
|
| 20 |
+
</div>
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
st.markdown(f"""
|
| 24 |
+
<div class="metric-card">
|
| 25 |
+
<div style="color: {self.colors['subtext']}; font-size: 0.9rem;">{title}</div>
|
| 26 |
+
<div style="color: {self.colors['text']}; font-size: 2rem; font-weight: bold; margin: 10px 0;">
|
| 27 |
+
{value}
|
| 28 |
+
</div>
|
| 29 |
+
{f'<div style="color: {self.colors["subtext"]}; font-size: 0.8rem;">{subtitle}</div>' if subtitle else ''}
|
| 30 |
+
{trend_html}
|
| 31 |
+
</div>
|
| 32 |
+
""", unsafe_allow_html=True)
|
| 33 |
+
|
| 34 |
+
def create_gauge_chart(self, value, title):
|
| 35 |
+
"""Create a gauge chart for metrics like ATS score"""
|
| 36 |
+
fig = go.Figure(go.Indicator(
|
| 37 |
+
mode="gauge+number",
|
| 38 |
+
value=value,
|
| 39 |
+
title={'text': title, 'font': {'size': 24, 'color': self.colors['text']}},
|
| 40 |
+
gauge={
|
| 41 |
+
'axis': {'range': [0, 100], 'tickwidth': 1, 'tickcolor': self.colors['text']},
|
| 42 |
+
'bar': {'color': self.colors['primary']},
|
| 43 |
+
'bgcolor': "white",
|
| 44 |
+
'borderwidth': 2,
|
| 45 |
+
'bordercolor': "gray",
|
| 46 |
+
'steps': [
|
| 47 |
+
{'range': [0, 40], 'color': self.colors['danger']},
|
| 48 |
+
{'range': [40, 70], 'color': self.colors['warning']},
|
| 49 |
+
{'range': [70, 100], 'color': self.colors['success']}
|
| 50 |
+
],
|
| 51 |
+
}
|
| 52 |
+
))
|
| 53 |
+
|
| 54 |
+
fig.update_layout(
|
| 55 |
+
paper_bgcolor=self.colors['card'],
|
| 56 |
+
plot_bgcolor=self.colors['card'],
|
| 57 |
+
font={'color': self.colors['text']},
|
| 58 |
+
height=300,
|
| 59 |
+
margin=dict(l=20, r=20, t=50, b=20)
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
return fig
|
| 63 |
+
|
| 64 |
+
def create_trend_chart(self, dates, values, title):
|
| 65 |
+
"""Create a trend line chart"""
|
| 66 |
+
fig = go.Figure()
|
| 67 |
+
fig.add_trace(go.Scatter(
|
| 68 |
+
x=dates,
|
| 69 |
+
y=values,
|
| 70 |
+
mode='lines+markers',
|
| 71 |
+
line=dict(color=self.colors['info'], width=3),
|
| 72 |
+
marker=dict(size=8, color=self.colors['info'])
|
| 73 |
+
))
|
| 74 |
+
|
| 75 |
+
fig.update_layout(
|
| 76 |
+
title=title,
|
| 77 |
+
paper_bgcolor=self.colors['card'],
|
| 78 |
+
plot_bgcolor=self.colors['card'],
|
| 79 |
+
font={'color': self.colors['text']},
|
| 80 |
+
height=300,
|
| 81 |
+
margin=dict(l=20, r=20, t=50, b=20),
|
| 82 |
+
xaxis=dict(
|
| 83 |
+
showgrid=True,
|
| 84 |
+
gridwidth=1,
|
| 85 |
+
gridcolor=self.colors['background']
|
| 86 |
+
),
|
| 87 |
+
yaxis=dict(
|
| 88 |
+
showgrid=True,
|
| 89 |
+
gridwidth=1,
|
| 90 |
+
gridcolor=self.colors['background']
|
| 91 |
+
)
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
return fig
|
| 95 |
+
|
| 96 |
+
def create_bar_chart(self, categories, values, title):
|
| 97 |
+
"""Create a bar chart"""
|
| 98 |
+
fig = go.Figure(go.Bar(
|
| 99 |
+
x=categories,
|
| 100 |
+
y=values,
|
| 101 |
+
marker_color=self.colors['primary'],
|
| 102 |
+
text=values,
|
| 103 |
+
textposition='auto',
|
| 104 |
+
))
|
| 105 |
+
|
| 106 |
+
fig.update_layout(
|
| 107 |
+
title=title,
|
| 108 |
+
paper_bgcolor=self.colors['card'],
|
| 109 |
+
plot_bgcolor=self.colors['card'],
|
| 110 |
+
font={'color': self.colors['text']},
|
| 111 |
+
height=300,
|
| 112 |
+
margin=dict(l=20, r=20, t=50, b=20),
|
| 113 |
+
xaxis=dict(
|
| 114 |
+
showgrid=False,
|
| 115 |
+
title_text="Categories",
|
| 116 |
+
color=self.colors['text']
|
| 117 |
+
),
|
| 118 |
+
yaxis=dict(
|
| 119 |
+
showgrid=True,
|
| 120 |
+
gridwidth=1,
|
| 121 |
+
gridcolor=self.colors['background'],
|
| 122 |
+
title_text="Values",
|
| 123 |
+
color=self.colors['text']
|
| 124 |
+
)
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
return fig
|
| 128 |
+
|
| 129 |
+
def create_dual_axis_chart(self, categories, values1, values2, title):
|
| 130 |
+
"""Create a chart with dual y-axes"""
|
| 131 |
+
fig = make_subplots(specs=[[{"secondary_y": True}]])
|
| 132 |
+
|
| 133 |
+
fig.add_trace(
|
| 134 |
+
go.Bar(
|
| 135 |
+
x=categories,
|
| 136 |
+
y=values1,
|
| 137 |
+
name="Count",
|
| 138 |
+
marker_color=self.colors['secondary']
|
| 139 |
+
),
|
| 140 |
+
secondary_y=False
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
fig.add_trace(
|
| 144 |
+
go.Scatter(
|
| 145 |
+
x=categories,
|
| 146 |
+
y=values2,
|
| 147 |
+
name="Score",
|
| 148 |
+
line=dict(color=self.colors['warning'], width=3),
|
| 149 |
+
mode='lines+markers'
|
| 150 |
+
),
|
| 151 |
+
secondary_y=True
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
fig.update_layout(
|
| 155 |
+
title=title,
|
| 156 |
+
paper_bgcolor=self.colors['card'],
|
| 157 |
+
plot_bgcolor=self.colors['card'],
|
| 158 |
+
font={'color': self.colors['text']},
|
| 159 |
+
height=300,
|
| 160 |
+
margin=dict(l=20, r=20, t=50, b=20),
|
| 161 |
+
showlegend=True,
|
| 162 |
+
legend=dict(
|
| 163 |
+
orientation="h",
|
| 164 |
+
yanchor="bottom",
|
| 165 |
+
y=1.02,
|
| 166 |
+
xanchor="right",
|
| 167 |
+
x=1
|
| 168 |
+
)
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
fig.update_xaxes(title_text="Categories", color=self.colors['text'])
|
| 172 |
+
fig.update_yaxes(title_text="Count", color=self.colors['text'], secondary_y=False)
|
| 173 |
+
fig.update_yaxes(title_text="Score", color=self.colors['text'], secondary_y=True)
|
| 174 |
+
|
| 175 |
+
return fig
|
dashboard/dashboard.py
ADDED
|
@@ -0,0 +1,1155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import plotly.express as px
|
| 4 |
+
import plotly.graph_objects as go
|
| 5 |
+
from datetime import datetime, timedelta
|
| 6 |
+
from config.database import get_database_connection
|
| 7 |
+
import io
|
| 8 |
+
import uuid
|
| 9 |
+
from plotly.subplots import make_subplots
|
| 10 |
+
from io import BytesIO
|
| 11 |
+
|
| 12 |
+
class DashboardManager:
|
| 13 |
+
def __init__(self):
|
| 14 |
+
self.conn = get_database_connection()
|
| 15 |
+
self.colors = {
|
| 16 |
+
'primary': '#4CAF50',
|
| 17 |
+
'secondary': '#2196F3',
|
| 18 |
+
'warning': '#FFA726',
|
| 19 |
+
'danger': '#F44336',
|
| 20 |
+
'info': '#00BCD4',
|
| 21 |
+
'success': '#66BB6A',
|
| 22 |
+
'purple': '#9C27B0',
|
| 23 |
+
'background': '#1E1E1E',
|
| 24 |
+
'card': '#2D2D2D',
|
| 25 |
+
'text': '#FFFFFF',
|
| 26 |
+
'subtext': '#B0B0B0'
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
def apply_dashboard_style(self):
|
| 30 |
+
"""Apply custom styling for dashboard"""
|
| 31 |
+
st.markdown("""
|
| 32 |
+
<style>
|
| 33 |
+
.dashboard-title {
|
| 34 |
+
font-size: 2.5rem;
|
| 35 |
+
font-weight: bold;
|
| 36 |
+
margin-bottom: 2rem;
|
| 37 |
+
color: white;
|
| 38 |
+
text-align: center;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.metric-card {
|
| 42 |
+
background-color: #2D2D2D;
|
| 43 |
+
border-radius: 15px;
|
| 44 |
+
padding: 1.5rem;
|
| 45 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 46 |
+
transition: transform 0.3s ease;
|
| 47 |
+
height: 100%;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.metric-card:hover {
|
| 51 |
+
transform: translateY(-5px);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
.metric-value {
|
| 55 |
+
font-size: 2.5rem;
|
| 56 |
+
font-weight: bold;
|
| 57 |
+
color: #4CAF50;
|
| 58 |
+
margin: 0.5rem 0;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.metric-label {
|
| 62 |
+
font-size: 1rem;
|
| 63 |
+
color: #B0B0B0;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.trend-up {
|
| 67 |
+
color: #4CAF50;
|
| 68 |
+
font-size: 1.2rem;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.trend-down {
|
| 72 |
+
color: #F44336;
|
| 73 |
+
font-size: 1.2rem;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.chart-container {
|
| 77 |
+
background-color: #2D2D2D;
|
| 78 |
+
border-radius: 15px;
|
| 79 |
+
padding: 1.5rem;
|
| 80 |
+
margin: 1rem 0;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.section-title {
|
| 84 |
+
font-size: 1.5rem;
|
| 85 |
+
color: white;
|
| 86 |
+
margin: 2rem 0 1rem 0;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.stPlotlyChart {
|
| 90 |
+
background-color: #2D2D2D;
|
| 91 |
+
border-radius: 15px;
|
| 92 |
+
padding: 1rem;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
div[data-testid="stHorizontalBlock"] > div {
|
| 96 |
+
background-color: #2D2D2D;
|
| 97 |
+
border-radius: 15px;
|
| 98 |
+
padding: 1rem;
|
| 99 |
+
margin: 0.5rem;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
[data-testid="stMetricValue"] {
|
| 103 |
+
font-size: 2rem !important;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
[data-testid="stMetricLabel"] {
|
| 107 |
+
font-size: 1rem !important;
|
| 108 |
+
}
|
| 109 |
+
</style>
|
| 110 |
+
""", unsafe_allow_html=True)
|
| 111 |
+
|
| 112 |
+
def get_resume_metrics(self):
|
| 113 |
+
"""Get resume-related metrics from database"""
|
| 114 |
+
cursor = self.conn.cursor()
|
| 115 |
+
|
| 116 |
+
# Get current date
|
| 117 |
+
now = datetime.now()
|
| 118 |
+
start_of_day = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
| 119 |
+
start_of_week = now - timedelta(days=now.weekday())
|
| 120 |
+
start_of_month = now.replace(day=1)
|
| 121 |
+
|
| 122 |
+
# Fetch metrics for different time periods
|
| 123 |
+
metrics = {}
|
| 124 |
+
for period, start_date in [
|
| 125 |
+
('Today', start_of_day),
|
| 126 |
+
('This Week', start_of_week),
|
| 127 |
+
('This Month', start_of_month),
|
| 128 |
+
('All Time', datetime(2000, 1, 1))
|
| 129 |
+
]:
|
| 130 |
+
cursor.execute("""
|
| 131 |
+
SELECT
|
| 132 |
+
COUNT(DISTINCT rd.id) as total_resumes,
|
| 133 |
+
ROUND(AVG(ra.ats_score), 1) as avg_ats_score,
|
| 134 |
+
ROUND(AVG(ra.keyword_match_score), 1) as avg_keyword_score,
|
| 135 |
+
COUNT(DISTINCT CASE WHEN ra.ats_score >= 70 THEN rd.id END) as high_scoring
|
| 136 |
+
FROM resume_data rd
|
| 137 |
+
LEFT JOIN resume_analysis ra ON rd.id = ra.resume_id
|
| 138 |
+
WHERE rd.created_at >= ?
|
| 139 |
+
""", (start_date.strftime('%Y-%m-%d %H:%M:%S'),))
|
| 140 |
+
|
| 141 |
+
row = cursor.fetchone()
|
| 142 |
+
if row:
|
| 143 |
+
metrics[period] = {
|
| 144 |
+
'total': row[0] or 0,
|
| 145 |
+
'ats_score': row[1] or 0,
|
| 146 |
+
'keyword_score': row[2] or 0,
|
| 147 |
+
'high_scoring': row[3] or 0
|
| 148 |
+
}
|
| 149 |
+
else:
|
| 150 |
+
metrics[period] = {
|
| 151 |
+
'total': 0,
|
| 152 |
+
'ats_score': 0,
|
| 153 |
+
'keyword_score': 0,
|
| 154 |
+
'high_scoring': 0
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
return metrics
|
| 158 |
+
|
| 159 |
+
def get_skill_distribution(self):
|
| 160 |
+
"""Get skill distribution data"""
|
| 161 |
+
cursor = self.conn.cursor()
|
| 162 |
+
cursor.execute("""
|
| 163 |
+
WITH RECURSIVE split(skill, rest) AS (
|
| 164 |
+
SELECT '', skills || ','
|
| 165 |
+
FROM resume_data
|
| 166 |
+
UNION ALL
|
| 167 |
+
SELECT
|
| 168 |
+
substr(rest, 0, instr(rest, ',')),
|
| 169 |
+
substr(rest, instr(rest, ',') + 1)
|
| 170 |
+
FROM split
|
| 171 |
+
WHERE rest <> ''
|
| 172 |
+
),
|
| 173 |
+
SkillCategories AS (
|
| 174 |
+
SELECT
|
| 175 |
+
CASE
|
| 176 |
+
WHEN LOWER(TRIM(skill, '[]" ')) LIKE '%python%' OR LOWER(TRIM(skill, '[]" ')) LIKE '%java%' OR
|
| 177 |
+
LOWER(TRIM(skill, '[]" ')) LIKE '%javascript%' OR LOWER(TRIM(skill, '[]" ')) LIKE '%c++%' OR
|
| 178 |
+
LOWER(TRIM(skill, '[]" ')) LIKE '%programming%' THEN 'Programming'
|
| 179 |
+
WHEN LOWER(TRIM(skill, '[]" ')) LIKE '%sql%' OR LOWER(TRIM(skill, '[]" ')) LIKE '%database%' OR
|
| 180 |
+
LOWER(TRIM(skill, '[]" ')) LIKE '%mongodb%' THEN 'Database'
|
| 181 |
+
WHEN LOWER(TRIM(skill, '[]" ')) LIKE '%aws%' OR LOWER(TRIM(skill, '[]" ')) LIKE '%cloud%' OR
|
| 182 |
+
LOWER(TRIM(skill, '[]" ')) LIKE '%azure%' THEN 'Cloud'
|
| 183 |
+
WHEN LOWER(TRIM(skill, '[]" ')) LIKE '%agile%' OR LOWER(TRIM(skill, '[]" ')) LIKE '%scrum%' OR
|
| 184 |
+
LOWER(TRIM(skill, '[]" ')) LIKE '%management%' THEN 'Management'
|
| 185 |
+
ELSE 'Other'
|
| 186 |
+
END as category,
|
| 187 |
+
COUNT(*) as count
|
| 188 |
+
FROM split
|
| 189 |
+
WHERE skill <> ''
|
| 190 |
+
GROUP BY category
|
| 191 |
+
)
|
| 192 |
+
SELECT category, count
|
| 193 |
+
FROM SkillCategories
|
| 194 |
+
ORDER BY count DESC
|
| 195 |
+
""")
|
| 196 |
+
|
| 197 |
+
categories, counts = [], []
|
| 198 |
+
for row in cursor.fetchall():
|
| 199 |
+
categories.append(row[0])
|
| 200 |
+
counts.append(row[1])
|
| 201 |
+
|
| 202 |
+
return categories, counts
|
| 203 |
+
|
| 204 |
+
def get_weekly_trends(self):
|
| 205 |
+
"""Get weekly submission trends"""
|
| 206 |
+
cursor = self.conn.cursor()
|
| 207 |
+
now = datetime.now()
|
| 208 |
+
dates = [(now - timedelta(days=x)).strftime('%Y-%m-%d') for x in range(6, -1, -1)]
|
| 209 |
+
|
| 210 |
+
submissions = []
|
| 211 |
+
for date in dates:
|
| 212 |
+
cursor.execute("""
|
| 213 |
+
SELECT COUNT(*)
|
| 214 |
+
FROM resume_data
|
| 215 |
+
WHERE DATE(created_at) = DATE(?)
|
| 216 |
+
""", (date,))
|
| 217 |
+
submissions.append(cursor.fetchone()[0])
|
| 218 |
+
|
| 219 |
+
return [d[-3:] for d in dates], submissions # Return shortened date format (e.g., 'Mon', 'Tue')
|
| 220 |
+
|
| 221 |
+
def get_job_category_stats(self):
|
| 222 |
+
"""Get statistics by job category"""
|
| 223 |
+
cursor = self.conn.cursor()
|
| 224 |
+
cursor.execute("""
|
| 225 |
+
SELECT
|
| 226 |
+
COALESCE(target_category, 'Other') as category,
|
| 227 |
+
COUNT(*) as count,
|
| 228 |
+
ROUND(AVG(CASE WHEN ra.ats_score >= 70 THEN 1 ELSE 0 END) * 100, 1) as success_rate
|
| 229 |
+
FROM resume_data rd
|
| 230 |
+
LEFT JOIN resume_analysis ra ON rd.id = ra.resume_id
|
| 231 |
+
GROUP BY category
|
| 232 |
+
ORDER BY count DESC
|
| 233 |
+
LIMIT 5
|
| 234 |
+
""")
|
| 235 |
+
|
| 236 |
+
categories, success_rates = [], []
|
| 237 |
+
for row in cursor.fetchall():
|
| 238 |
+
categories.append(row[0])
|
| 239 |
+
success_rates.append(row[2] or 0)
|
| 240 |
+
|
| 241 |
+
return categories, success_rates
|
| 242 |
+
|
| 243 |
+
def render_admin_panel(self):
|
| 244 |
+
"""Render admin panel with data management tools"""
|
| 245 |
+
st.sidebar.markdown("### 👋 Welcome Admin!")
|
| 246 |
+
st.sidebar.markdown("---")
|
| 247 |
+
|
| 248 |
+
if st.sidebar.button("🚪 Logout"):
|
| 249 |
+
st.session_state.is_admin = False
|
| 250 |
+
st.rerun()
|
| 251 |
+
|
| 252 |
+
st.sidebar.markdown("### 🛠️ Admin Tools")
|
| 253 |
+
|
| 254 |
+
# Data Export Options
|
| 255 |
+
export_format = st.sidebar.selectbox(
|
| 256 |
+
"Export Format",
|
| 257 |
+
["Excel", "CSV", "JSON"],
|
| 258 |
+
key="export_format"
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
if st.sidebar.button("📥 Export Data"):
|
| 262 |
+
if export_format == "Excel":
|
| 263 |
+
excel_data = self.export_to_excel()
|
| 264 |
+
if excel_data:
|
| 265 |
+
st.sidebar.download_button(
|
| 266 |
+
"⬇️ Download Excel",
|
| 267 |
+
data=excel_data,
|
| 268 |
+
file_name=f"resume_data_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx",
|
| 269 |
+
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
| 270 |
+
)
|
| 271 |
+
elif export_format == "CSV":
|
| 272 |
+
csv_data = self.export_to_csv()
|
| 273 |
+
if csv_data:
|
| 274 |
+
st.sidebar.download_button(
|
| 275 |
+
"⬇️ Download CSV",
|
| 276 |
+
data=csv_data,
|
| 277 |
+
file_name=f"resume_data_{datetime.now().strftime('%Y%m%d_%H%M')}.csv",
|
| 278 |
+
mime="text/csv"
|
| 279 |
+
)
|
| 280 |
+
else:
|
| 281 |
+
json_data = self.export_to_json()
|
| 282 |
+
if json_data:
|
| 283 |
+
st.sidebar.download_button(
|
| 284 |
+
"⬇️ Download JSON",
|
| 285 |
+
data=json_data,
|
| 286 |
+
file_name=f"resume_data_{datetime.now().strftime('%Y%m%d_%H%M')}.json",
|
| 287 |
+
mime="application/json"
|
| 288 |
+
)
|
| 289 |
+
|
| 290 |
+
# Database Stats
|
| 291 |
+
st.sidebar.markdown("### 📊 Database Stats")
|
| 292 |
+
stats = self.get_database_stats()
|
| 293 |
+
st.sidebar.markdown(f"""
|
| 294 |
+
- Total Resumes: {stats['total_resumes']}
|
| 295 |
+
- Today's Submissions: {stats['today_submissions']}
|
| 296 |
+
- Storage Used: {stats['storage_size']}
|
| 297 |
+
""")
|
| 298 |
+
|
| 299 |
+
def get_resume_data(self):
|
| 300 |
+
"""Get all resume data"""
|
| 301 |
+
cursor = self.conn.cursor()
|
| 302 |
+
try:
|
| 303 |
+
cursor.execute('''
|
| 304 |
+
SELECT
|
| 305 |
+
r.id,
|
| 306 |
+
r.name,
|
| 307 |
+
r.email,
|
| 308 |
+
r.phone,
|
| 309 |
+
r.linkedin,
|
| 310 |
+
r.github,
|
| 311 |
+
r.portfolio,
|
| 312 |
+
r.target_role,
|
| 313 |
+
r.target_category,
|
| 314 |
+
r.created_at,
|
| 315 |
+
a.ats_score,
|
| 316 |
+
a.keyword_match_score,
|
| 317 |
+
a.format_score,
|
| 318 |
+
a.section_score
|
| 319 |
+
FROM resume_data r
|
| 320 |
+
LEFT JOIN resume_analysis a ON r.id = a.resume_id
|
| 321 |
+
ORDER BY r.created_at DESC
|
| 322 |
+
''')
|
| 323 |
+
return cursor.fetchall()
|
| 324 |
+
except Exception as e:
|
| 325 |
+
print(f"Error fetching resume data: {str(e)}")
|
| 326 |
+
return []
|
| 327 |
+
|
| 328 |
+
def render_resume_data_section(self):
|
| 329 |
+
"""Render resume data section with Excel download"""
|
| 330 |
+
st.markdown("<h2 class='section-title'>Resume Submissions</h2>", unsafe_allow_html=True)
|
| 331 |
+
|
| 332 |
+
# Get resume data
|
| 333 |
+
resume_data = self.get_resume_data()
|
| 334 |
+
|
| 335 |
+
if resume_data:
|
| 336 |
+
# Convert to DataFrame
|
| 337 |
+
columns = [
|
| 338 |
+
'ID', 'Name', 'Email', 'Phone', 'LinkedIn', 'GitHub',
|
| 339 |
+
'Portfolio', 'Target Role', 'Target Category', 'Submission Date',
|
| 340 |
+
'ATS Score', 'Keyword Match', 'Format Score', 'Section Score'
|
| 341 |
+
]
|
| 342 |
+
df = pd.DataFrame(resume_data, columns=columns)
|
| 343 |
+
|
| 344 |
+
# Format scores as percentages
|
| 345 |
+
score_columns = ['ATS Score', 'Keyword Match', 'Format Score', 'Section Score']
|
| 346 |
+
for col in score_columns:
|
| 347 |
+
df[col] = df[col].apply(lambda x: f"{x*100:.1f}%" if pd.notnull(x) else "N/A")
|
| 348 |
+
|
| 349 |
+
# Style the dataframe
|
| 350 |
+
st.markdown("""
|
| 351 |
+
<style>
|
| 352 |
+
.resume-data {
|
| 353 |
+
background-color: #2D2D2D;
|
| 354 |
+
border-radius: 10px;
|
| 355 |
+
padding: 1rem;
|
| 356 |
+
margin-bottom: 1rem;
|
| 357 |
+
}
|
| 358 |
+
</style>
|
| 359 |
+
""", unsafe_allow_html=True)
|
| 360 |
+
|
| 361 |
+
with st.container():
|
| 362 |
+
st.markdown('<div class="resume-data">', unsafe_allow_html=True)
|
| 363 |
+
|
| 364 |
+
# Add filters
|
| 365 |
+
col1, col2 = st.columns(2)
|
| 366 |
+
with col1:
|
| 367 |
+
target_role = st.selectbox(
|
| 368 |
+
"Filter by Target Role",
|
| 369 |
+
options=["All"] + list(df['Target Role'].unique()),
|
| 370 |
+
key="role_filter"
|
| 371 |
+
)
|
| 372 |
+
with col2:
|
| 373 |
+
target_category = st.selectbox(
|
| 374 |
+
"Filter by Category",
|
| 375 |
+
options=["All"] + list(df['Target Category'].unique()),
|
| 376 |
+
key="category_filter"
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
# Apply filters
|
| 380 |
+
filtered_df = df.copy()
|
| 381 |
+
if target_role != "All":
|
| 382 |
+
filtered_df = filtered_df[filtered_df['Target Role'] == target_role]
|
| 383 |
+
if target_category != "All":
|
| 384 |
+
filtered_df = filtered_df[filtered_df['Target Category'] == target_category]
|
| 385 |
+
|
| 386 |
+
# Display filtered data
|
| 387 |
+
st.dataframe(
|
| 388 |
+
filtered_df,
|
| 389 |
+
use_container_width=True,
|
| 390 |
+
hide_index=True
|
| 391 |
+
)
|
| 392 |
+
|
| 393 |
+
# Add download buttons
|
| 394 |
+
col1, col2 = st.columns(2)
|
| 395 |
+
with col1:
|
| 396 |
+
# Download filtered data
|
| 397 |
+
excel_buffer = BytesIO()
|
| 398 |
+
filtered_df.to_excel(excel_buffer, index=False, engine='openpyxl')
|
| 399 |
+
excel_buffer.seek(0)
|
| 400 |
+
|
| 401 |
+
st.download_button(
|
| 402 |
+
label="📥 Download Filtered Data",
|
| 403 |
+
data=excel_buffer,
|
| 404 |
+
file_name=f"resume_data_filtered_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx",
|
| 405 |
+
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 406 |
+
key="download_filtered_data"
|
| 407 |
+
)
|
| 408 |
+
|
| 409 |
+
with col2:
|
| 410 |
+
# Download all data
|
| 411 |
+
excel_buffer_all = BytesIO()
|
| 412 |
+
df.to_excel(excel_buffer_all, index=False, engine='openpyxl')
|
| 413 |
+
excel_buffer_all.seek(0)
|
| 414 |
+
|
| 415 |
+
st.download_button(
|
| 416 |
+
label="📥 Download All Data",
|
| 417 |
+
data=excel_buffer_all,
|
| 418 |
+
file_name=f"resume_data_all_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx",
|
| 419 |
+
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 420 |
+
key="download_all_data"
|
| 421 |
+
)
|
| 422 |
+
|
| 423 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 424 |
+
else:
|
| 425 |
+
st.info("No resume submissions available")
|
| 426 |
+
|
| 427 |
+
def render_admin_section(self):
|
| 428 |
+
"""Render admin section with logs and Excel download"""
|
| 429 |
+
# Render resume data section
|
| 430 |
+
self.render_resume_data_section()
|
| 431 |
+
|
| 432 |
+
# Render admin logs section
|
| 433 |
+
st.markdown("<h2 class='section-title'>Admin Activity Logs</h2>", unsafe_allow_html=True)
|
| 434 |
+
|
| 435 |
+
# Get admin logs
|
| 436 |
+
admin_logs = self.get_admin_logs()
|
| 437 |
+
|
| 438 |
+
if admin_logs:
|
| 439 |
+
# Convert to DataFrame
|
| 440 |
+
df = pd.DataFrame(admin_logs, columns=['Admin Email', 'Action', 'Timestamp'])
|
| 441 |
+
|
| 442 |
+
# Style the dataframe
|
| 443 |
+
st.markdown("""
|
| 444 |
+
<style>
|
| 445 |
+
.admin-logs {
|
| 446 |
+
background-color: #2D2D2D;
|
| 447 |
+
border-radius: 10px;
|
| 448 |
+
padding: 1rem;
|
| 449 |
+
}
|
| 450 |
+
</style>
|
| 451 |
+
""", unsafe_allow_html=True)
|
| 452 |
+
|
| 453 |
+
with st.container():
|
| 454 |
+
st.markdown('<div class="admin-logs">', unsafe_allow_html=True)
|
| 455 |
+
st.dataframe(
|
| 456 |
+
df,
|
| 457 |
+
use_container_width=True,
|
| 458 |
+
hide_index=True
|
| 459 |
+
)
|
| 460 |
+
|
| 461 |
+
# Add download button
|
| 462 |
+
excel_buffer = BytesIO()
|
| 463 |
+
df.to_excel(excel_buffer, index=False, engine='openpyxl')
|
| 464 |
+
excel_buffer.seek(0)
|
| 465 |
+
|
| 466 |
+
st.download_button(
|
| 467 |
+
label="📥 Download Admin Logs as Excel",
|
| 468 |
+
data=excel_buffer,
|
| 469 |
+
file_name=f"admin_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx",
|
| 470 |
+
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 471 |
+
key="download_admin_logs"
|
| 472 |
+
)
|
| 473 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 474 |
+
else:
|
| 475 |
+
st.info("No admin activity logs available")
|
| 476 |
+
|
| 477 |
+
def export_to_excel(self):
|
| 478 |
+
"""Export data to Excel format"""
|
| 479 |
+
query = """
|
| 480 |
+
SELECT
|
| 481 |
+
rd.name, rd.email, rd.phone, rd.linkedin, rd.github, rd.portfolio,
|
| 482 |
+
rd.summary, rd.target_role, rd.target_category,
|
| 483 |
+
rd.education, rd.experience, rd.projects, rd.skills,
|
| 484 |
+
ra.ats_score, ra.keyword_match_score, ra.format_score, ra.section_score,
|
| 485 |
+
ra.missing_skills, ra.recommendations,
|
| 486 |
+
rd.created_at
|
| 487 |
+
FROM resume_data rd
|
| 488 |
+
LEFT JOIN resume_analysis ra ON rd.id = ra.resume_id
|
| 489 |
+
"""
|
| 490 |
+
try:
|
| 491 |
+
df = pd.read_sql_query(query, self.conn)
|
| 492 |
+
|
| 493 |
+
# Create Excel writer object
|
| 494 |
+
output = BytesIO()
|
| 495 |
+
with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
|
| 496 |
+
# Write main data
|
| 497 |
+
df.to_excel(writer, sheet_name='Resume Data', index=False)
|
| 498 |
+
|
| 499 |
+
# Get the workbook and the worksheet
|
| 500 |
+
workbook = writer.book
|
| 501 |
+
worksheet = writer.sheets['Resume Data']
|
| 502 |
+
|
| 503 |
+
# Add formatting
|
| 504 |
+
header_format = workbook.add_format({
|
| 505 |
+
'bold': True,
|
| 506 |
+
'text_wrap': True,
|
| 507 |
+
'valign': 'top',
|
| 508 |
+
'fg_color': '#D7E4BC',
|
| 509 |
+
'border': 1
|
| 510 |
+
})
|
| 511 |
+
|
| 512 |
+
# Write headers with formatting
|
| 513 |
+
for col_num, value in enumerate(df.columns.values):
|
| 514 |
+
worksheet.write(0, col_num, value, header_format)
|
| 515 |
+
|
| 516 |
+
# Auto-adjust columns' width
|
| 517 |
+
for i, col in enumerate(df.columns):
|
| 518 |
+
max_length = max(
|
| 519 |
+
df[col].astype(str).apply(len).max(),
|
| 520 |
+
len(str(col))
|
| 521 |
+
) + 2
|
| 522 |
+
worksheet.set_column(i, i, min(max_length, 50))
|
| 523 |
+
|
| 524 |
+
# Return the Excel file
|
| 525 |
+
output.seek(0)
|
| 526 |
+
return output.getvalue()
|
| 527 |
+
|
| 528 |
+
except Exception as e:
|
| 529 |
+
st.error(f"Error exporting to Excel: {str(e)}")
|
| 530 |
+
return None
|
| 531 |
+
|
| 532 |
+
def export_to_csv(self):
|
| 533 |
+
"""Export data to CSV format"""
|
| 534 |
+
query = """
|
| 535 |
+
SELECT
|
| 536 |
+
rd.name, rd.email, rd.phone, rd.linkedin, rd.github, rd.portfolio,
|
| 537 |
+
rd.summary, rd.target_role, rd.target_category,
|
| 538 |
+
rd.education, rd.experience, rd.projects, rd.skills,
|
| 539 |
+
ra.ats_score, ra.keyword_match_score, ra.format_score, ra.section_score,
|
| 540 |
+
ra.missing_skills, ra.recommendations,
|
| 541 |
+
rd.created_at
|
| 542 |
+
FROM resume_data rd
|
| 543 |
+
LEFT JOIN resume_analysis ra ON rd.id = ra.resume_id
|
| 544 |
+
"""
|
| 545 |
+
try:
|
| 546 |
+
df = pd.read_sql_query(query, self.conn)
|
| 547 |
+
return df.to_csv(index=False).encode('utf-8')
|
| 548 |
+
except Exception as e:
|
| 549 |
+
st.error(f"Error exporting to CSV: {str(e)}")
|
| 550 |
+
return None
|
| 551 |
+
|
| 552 |
+
def export_to_json(self):
|
| 553 |
+
"""Export data to JSON format"""
|
| 554 |
+
query = """
|
| 555 |
+
SELECT
|
| 556 |
+
rd.*, ra.*
|
| 557 |
+
FROM resume_data rd
|
| 558 |
+
LEFT JOIN resume_analysis ra ON rd.id = ra.resume_id
|
| 559 |
+
"""
|
| 560 |
+
try:
|
| 561 |
+
df = pd.read_sql_query(query, self.conn)
|
| 562 |
+
return df.to_json(orient='records', date_format='iso')
|
| 563 |
+
except Exception as e:
|
| 564 |
+
st.error(f"Error exporting to JSON: {str(e)}")
|
| 565 |
+
return None
|
| 566 |
+
|
| 567 |
+
def get_database_stats(self):
|
| 568 |
+
"""Get database statistics"""
|
| 569 |
+
cursor = self.conn.cursor()
|
| 570 |
+
stats = {}
|
| 571 |
+
|
| 572 |
+
# Total resumes
|
| 573 |
+
cursor.execute("SELECT COUNT(*) FROM resume_data")
|
| 574 |
+
stats['total_resumes'] = cursor.fetchone()[0]
|
| 575 |
+
|
| 576 |
+
# Today's submissions
|
| 577 |
+
cursor.execute("""
|
| 578 |
+
SELECT COUNT(*)
|
| 579 |
+
FROM resume_data
|
| 580 |
+
WHERE DATE(created_at) = DATE('now')
|
| 581 |
+
""")
|
| 582 |
+
stats['today_submissions'] = cursor.fetchone()[0]
|
| 583 |
+
|
| 584 |
+
# Database size (approximate)
|
| 585 |
+
cursor.execute("PRAGMA page_count")
|
| 586 |
+
page_count = cursor.fetchone()[0]
|
| 587 |
+
cursor.execute("PRAGMA page_size")
|
| 588 |
+
page_size = cursor.fetchone()[0]
|
| 589 |
+
size_bytes = page_count * page_size
|
| 590 |
+
|
| 591 |
+
if size_bytes < 1024:
|
| 592 |
+
stats['storage_size'] = f"{size_bytes} bytes"
|
| 593 |
+
elif size_bytes < 1024 * 1024:
|
| 594 |
+
stats['storage_size'] = f"{size_bytes/1024:.1f} KB"
|
| 595 |
+
else:
|
| 596 |
+
stats['storage_size'] = f"{size_bytes/(1024*1024):.1f} MB"
|
| 597 |
+
|
| 598 |
+
return stats
|
| 599 |
+
|
| 600 |
+
def get_admin_logs(self):
|
| 601 |
+
"""Get admin logs"""
|
| 602 |
+
cursor = self.conn.cursor()
|
| 603 |
+
try:
|
| 604 |
+
cursor.execute('''
|
| 605 |
+
SELECT admin_email, action, timestamp
|
| 606 |
+
FROM admin_logs
|
| 607 |
+
ORDER BY timestamp DESC
|
| 608 |
+
''')
|
| 609 |
+
return cursor.fetchall()
|
| 610 |
+
except Exception as e:
|
| 611 |
+
print(f"Error fetching admin logs: {str(e)}")
|
| 612 |
+
return []
|
| 613 |
+
|
| 614 |
+
def render_dashboard(self):
|
| 615 |
+
"""Main dashboard rendering function"""
|
| 616 |
+
# Apply styling
|
| 617 |
+
st.markdown("""
|
| 618 |
+
<style>
|
| 619 |
+
.dashboard-container {
|
| 620 |
+
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
| 621 |
+
padding: 2rem;
|
| 622 |
+
border-radius: 20px;
|
| 623 |
+
margin: -1rem -1rem 2rem -1rem;
|
| 624 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
| 625 |
+
}
|
| 626 |
+
.dashboard-title {
|
| 627 |
+
color: #4FD1C5;
|
| 628 |
+
font-size: 2.5rem;
|
| 629 |
+
margin-bottom: 0.5rem;
|
| 630 |
+
display: flex;
|
| 631 |
+
align-items: center;
|
| 632 |
+
gap: 1rem;
|
| 633 |
+
}
|
| 634 |
+
.dashboard-icon {
|
| 635 |
+
background: rgba(79, 209, 197, 0.2);
|
| 636 |
+
padding: 0.5rem;
|
| 637 |
+
border-radius: 12px;
|
| 638 |
+
}
|
| 639 |
+
.stats-grid {
|
| 640 |
+
display: grid;
|
| 641 |
+
grid-template-columns: repeat(4, 1fr);
|
| 642 |
+
gap: 1.5rem;
|
| 643 |
+
margin-top: 2rem;
|
| 644 |
+
}
|
| 645 |
+
.stat-card {
|
| 646 |
+
background: rgba(255, 255, 255, 0.05);
|
| 647 |
+
backdrop-filter: blur(10px);
|
| 648 |
+
padding: 1.5rem;
|
| 649 |
+
border-radius: 16px;
|
| 650 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 651 |
+
transition: all 0.3s ease;
|
| 652 |
+
}
|
| 653 |
+
.stat-card:hover {
|
| 654 |
+
transform: translateY(-5px);
|
| 655 |
+
background: rgba(255, 255, 255, 0.1);
|
| 656 |
+
}
|
| 657 |
+
.stat-value {
|
| 658 |
+
font-size: 2.5rem;
|
| 659 |
+
font-weight: bold;
|
| 660 |
+
margin: 0;
|
| 661 |
+
color: #4FD1C5;
|
| 662 |
+
}
|
| 663 |
+
.stat-label {
|
| 664 |
+
font-size: 1rem;
|
| 665 |
+
color: rgba(255, 255, 255, 0.7);
|
| 666 |
+
margin: 0.5rem 0 0 0;
|
| 667 |
+
}
|
| 668 |
+
.section-title {
|
| 669 |
+
color: #4FD1C5;
|
| 670 |
+
font-size: 1.5rem;
|
| 671 |
+
margin: 1rem 0 0.5rem 0;
|
| 672 |
+
padding-bottom: 0.5rem;
|
| 673 |
+
border-bottom: 2px solid rgba(79, 209, 197, 0.2);
|
| 674 |
+
}
|
| 675 |
+
.chart-container {
|
| 676 |
+
background: rgba(255, 255, 255, 0.05);
|
| 677 |
+
border-radius: 16px;
|
| 678 |
+
padding: 1rem;
|
| 679 |
+
margin-bottom: 1rem;
|
| 680 |
+
}
|
| 681 |
+
.insights-grid {
|
| 682 |
+
display: grid;
|
| 683 |
+
grid-template-columns: repeat(3, 1fr);
|
| 684 |
+
gap: 1.5rem;
|
| 685 |
+
margin-top: 1rem;
|
| 686 |
+
}
|
| 687 |
+
.insight-card {
|
| 688 |
+
background: rgba(255, 255, 255, 0.05);
|
| 689 |
+
padding: 1.5rem;
|
| 690 |
+
border-radius: 16px;
|
| 691 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 692 |
+
}
|
| 693 |
+
.trend-indicator {
|
| 694 |
+
display: inline-flex;
|
| 695 |
+
align-items: center;
|
| 696 |
+
padding: 0.25rem 0.5rem;
|
| 697 |
+
border-radius: 12px;
|
| 698 |
+
font-size: 0.875rem;
|
| 699 |
+
margin-left: 0.5rem;
|
| 700 |
+
}
|
| 701 |
+
.trend-up {
|
| 702 |
+
background: rgba(46, 204, 113, 0.2);
|
| 703 |
+
color: #2ecc71;
|
| 704 |
+
}
|
| 705 |
+
.trend-down {
|
| 706 |
+
background: rgba(231, 76, 60, 0.2);
|
| 707 |
+
color: #e74c3c;
|
| 708 |
+
}
|
| 709 |
+
@keyframes fadeInUp {
|
| 710 |
+
from {
|
| 711 |
+
opacity: 0;
|
| 712 |
+
transform: translateY(20px);
|
| 713 |
+
}
|
| 714 |
+
to {
|
| 715 |
+
opacity: 1;
|
| 716 |
+
transform: translateY(0);
|
| 717 |
+
}
|
| 718 |
+
}
|
| 719 |
+
.animate-fade-in {
|
| 720 |
+
animation: fadeInUp 0.5s ease-out forwards;
|
| 721 |
+
}
|
| 722 |
+
</style>
|
| 723 |
+
""", unsafe_allow_html=True)
|
| 724 |
+
|
| 725 |
+
# Dashboard Header
|
| 726 |
+
st.markdown("""
|
| 727 |
+
<div class="dashboard-container animate-fade-in">
|
| 728 |
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
| 729 |
+
<div class="dashboard-title">
|
| 730 |
+
<span class="dashboard-icon">📊</span>
|
| 731 |
+
Resume Analytics Dashboard
|
| 732 |
+
</div>
|
| 733 |
+
<div style="color: rgba(255, 255, 255, 0.7);">
|
| 734 |
+
Last updated: {}
|
| 735 |
+
</div>
|
| 736 |
+
</div>
|
| 737 |
+
""".format(datetime.now().strftime('%B %d, %Y %I:%M %p')), unsafe_allow_html=True)
|
| 738 |
+
|
| 739 |
+
# Quick Stats
|
| 740 |
+
stats = self.get_quick_stats()
|
| 741 |
+
trend_indicators = self.get_trend_indicators()
|
| 742 |
+
|
| 743 |
+
st.markdown("""
|
| 744 |
+
<div class="stats-grid">
|
| 745 |
+
<div class="stat-card">
|
| 746 |
+
<p class="stat-value">{}</p>
|
| 747 |
+
<p class="stat-label">Total Resumes</p>
|
| 748 |
+
<span class="trend-indicator {}">
|
| 749 |
+
{} {}%
|
| 750 |
+
</span>
|
| 751 |
+
</div>
|
| 752 |
+
<div class="stat-card">
|
| 753 |
+
<p class="stat-value">{}</p>
|
| 754 |
+
<p class="stat-label">Avg ATS Score</p>
|
| 755 |
+
<span class="trend-indicator {}">
|
| 756 |
+
{} {}%
|
| 757 |
+
</span>
|
| 758 |
+
</div>
|
| 759 |
+
<div class="stat-card">
|
| 760 |
+
<p class="stat-value">{}</p>
|
| 761 |
+
<p class="stat-label">High Performing</p>
|
| 762 |
+
<span class="trend-indicator {}">
|
| 763 |
+
{} {}%
|
| 764 |
+
</span>
|
| 765 |
+
</div>
|
| 766 |
+
<div class="stat-card">
|
| 767 |
+
<p class="stat-value">{}</p>
|
| 768 |
+
<p class="stat-label">Success Rate</p>
|
| 769 |
+
<span class="trend-indicator {}">
|
| 770 |
+
{} {}%
|
| 771 |
+
</span>
|
| 772 |
+
</div>
|
| 773 |
+
</div>
|
| 774 |
+
</div>
|
| 775 |
+
""".format(
|
| 776 |
+
stats['Total Resumes'],
|
| 777 |
+
trend_indicators['resumes']['class'], trend_indicators['resumes']['icon'], trend_indicators['resumes']['value'],
|
| 778 |
+
stats['Avg ATS Score'],
|
| 779 |
+
trend_indicators['ats']['class'], trend_indicators['ats']['icon'], trend_indicators['ats']['value'],
|
| 780 |
+
stats['High Performing'],
|
| 781 |
+
trend_indicators['high_performing']['class'], trend_indicators['high_performing']['icon'], trend_indicators['high_performing']['value'],
|
| 782 |
+
stats['Success Rate'],
|
| 783 |
+
trend_indicators['success_rate']['class'], trend_indicators['success_rate']['icon'], trend_indicators['success_rate']['value']
|
| 784 |
+
), unsafe_allow_html=True)
|
| 785 |
+
|
| 786 |
+
# Performance Analytics Section
|
| 787 |
+
st.markdown('<div class="section-title">📈 Performance Analytics</div>', unsafe_allow_html=True)
|
| 788 |
+
|
| 789 |
+
col1, col2 = st.columns(2)
|
| 790 |
+
|
| 791 |
+
with col1:
|
| 792 |
+
st.markdown('<div class="chart-container">', unsafe_allow_html=True)
|
| 793 |
+
fig = self.create_enhanced_ats_gauge(float(stats['Avg ATS Score'].rstrip('%')))
|
| 794 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 795 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 796 |
+
|
| 797 |
+
with col2:
|
| 798 |
+
st.markdown('<div class="chart-container">', unsafe_allow_html=True)
|
| 799 |
+
fig = self.create_skill_distribution_chart()
|
| 800 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 801 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 802 |
+
|
| 803 |
+
# Additional Analytics
|
| 804 |
+
col1, col2 = st.columns(2)
|
| 805 |
+
|
| 806 |
+
with col1:
|
| 807 |
+
st.markdown('<div class="chart-container">', unsafe_allow_html=True)
|
| 808 |
+
fig = self.create_submission_trends_chart()
|
| 809 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 810 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 811 |
+
|
| 812 |
+
with col2:
|
| 813 |
+
st.markdown('<div class="chart-container">', unsafe_allow_html=True)
|
| 814 |
+
fig = self.create_job_category_chart()
|
| 815 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 816 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 817 |
+
|
| 818 |
+
# Key Insights Section
|
| 819 |
+
st.markdown('<div class="section-title">🎯 Key Insights</div>', unsafe_allow_html=True)
|
| 820 |
+
insights = self.get_detailed_insights()
|
| 821 |
+
|
| 822 |
+
st.markdown('<div class="insights-grid">', unsafe_allow_html=True)
|
| 823 |
+
for insight in insights:
|
| 824 |
+
st.markdown(f"""
|
| 825 |
+
<div class="insight-card">
|
| 826 |
+
<h3 style="color: #4FD1C5; margin-bottom: 1rem;">
|
| 827 |
+
{insight['icon']} {insight['title']}
|
| 828 |
+
</h3>
|
| 829 |
+
<p style="color: rgba(255, 255, 255, 0.7); margin: 0;">
|
| 830 |
+
{insight['description']}
|
| 831 |
+
</p>
|
| 832 |
+
<div style="margin-top: 1rem;">
|
| 833 |
+
<span class="trend-indicator {insight['trend_class']}">
|
| 834 |
+
{insight['trend_icon']} {insight['trend_value']}
|
| 835 |
+
</span>
|
| 836 |
+
</div>
|
| 837 |
+
</div>
|
| 838 |
+
""", unsafe_allow_html=True)
|
| 839 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 840 |
+
|
| 841 |
+
# Admin logs section with Excel download functionality
|
| 842 |
+
if st.session_state.get('is_admin', False):
|
| 843 |
+
self.render_admin_section()
|
| 844 |
+
|
| 845 |
+
def get_trend_indicators(self):
|
| 846 |
+
"""Get trend indicators for stats"""
|
| 847 |
+
cursor = self.conn.cursor()
|
| 848 |
+
indicators = {}
|
| 849 |
+
|
| 850 |
+
# Compare with last week's data
|
| 851 |
+
for metric in ['resumes', 'ats', 'high_performing', 'success_rate']:
|
| 852 |
+
try:
|
| 853 |
+
if metric == 'resumes':
|
| 854 |
+
cursor.execute("""
|
| 855 |
+
SELECT
|
| 856 |
+
(COUNT(*) - (
|
| 857 |
+
SELECT COUNT(*)
|
| 858 |
+
FROM resume_data
|
| 859 |
+
WHERE created_at < date('now', '-7 days')
|
| 860 |
+
)) * 100.0 /
|
| 861 |
+
NULLIF((
|
| 862 |
+
SELECT COUNT(*)
|
| 863 |
+
FROM resume_data
|
| 864 |
+
WHERE created_at < date('now', '-7 days')
|
| 865 |
+
), 0)
|
| 866 |
+
FROM resume_data
|
| 867 |
+
""")
|
| 868 |
+
elif metric == 'ats':
|
| 869 |
+
cursor.execute("""
|
| 870 |
+
SELECT
|
| 871 |
+
(AVG(ats_score) - (
|
| 872 |
+
SELECT AVG(ats_score)
|
| 873 |
+
FROM resume_analysis
|
| 874 |
+
WHERE created_at < date('now', '-7 days')
|
| 875 |
+
)) * 100.0 /
|
| 876 |
+
NULLIF((
|
| 877 |
+
SELECT AVG(ats_score)
|
| 878 |
+
FROM resume_analysis
|
| 879 |
+
WHERE created_at < date('now', '-7 days')
|
| 880 |
+
), 0)
|
| 881 |
+
FROM resume_analysis
|
| 882 |
+
""")
|
| 883 |
+
|
| 884 |
+
change = cursor.fetchone()[0] or 0
|
| 885 |
+
indicators[metric] = {
|
| 886 |
+
'value': abs(round(change, 1)),
|
| 887 |
+
'icon': '↑' if change >= 0 else '↓',
|
| 888 |
+
'class': 'trend-up' if change >= 0 else 'trend-down'
|
| 889 |
+
}
|
| 890 |
+
except Exception:
|
| 891 |
+
indicators[metric] = {
|
| 892 |
+
'value': 0,
|
| 893 |
+
'icon': '→',
|
| 894 |
+
'class': 'trend-neutral'
|
| 895 |
+
}
|
| 896 |
+
|
| 897 |
+
return indicators
|
| 898 |
+
|
| 899 |
+
def get_detailed_insights(self):
|
| 900 |
+
"""Get detailed insights from the database"""
|
| 901 |
+
cursor = self.conn.cursor()
|
| 902 |
+
insights = []
|
| 903 |
+
|
| 904 |
+
# Most Successful Job Category
|
| 905 |
+
cursor.execute("""
|
| 906 |
+
SELECT target_category, AVG(ats_score) as avg_score,
|
| 907 |
+
COUNT(*) as submission_count
|
| 908 |
+
FROM resume_data rd
|
| 909 |
+
JOIN resume_analysis ra ON rd.id = ra.resume_id
|
| 910 |
+
GROUP BY target_category
|
| 911 |
+
ORDER BY avg_score DESC
|
| 912 |
+
LIMIT 1
|
| 913 |
+
""")
|
| 914 |
+
top_category = cursor.fetchone()
|
| 915 |
+
if top_category:
|
| 916 |
+
insights.append({
|
| 917 |
+
'title': 'Top Performing Category',
|
| 918 |
+
'icon': '🏆',
|
| 919 |
+
'description': f"{top_category[0]} leads with {top_category[1]:.1f}% average ATS score across {top_category[2]} submissions",
|
| 920 |
+
'trend_class': 'trend-up',
|
| 921 |
+
'trend_icon': '↑',
|
| 922 |
+
'trend_value': f"{top_category[1]:.1f}%"
|
| 923 |
+
})
|
| 924 |
+
|
| 925 |
+
# Recent Improvement
|
| 926 |
+
cursor.execute("""
|
| 927 |
+
SELECT
|
| 928 |
+
(SELECT AVG(ats_score) FROM resume_analysis
|
| 929 |
+
WHERE created_at >= date('now', '-7 days')) as recent_score,
|
| 930 |
+
(SELECT AVG(ats_score) FROM resume_analysis
|
| 931 |
+
WHERE created_at < date('now', '-7 days')) as old_score
|
| 932 |
+
""")
|
| 933 |
+
scores = cursor.fetchone()
|
| 934 |
+
if scores and scores[0] and scores[1]:
|
| 935 |
+
change = scores[0] - scores[1]
|
| 936 |
+
insights.append({
|
| 937 |
+
'title': 'Weekly Trend',
|
| 938 |
+
'icon': '📈',
|
| 939 |
+
'description': f"ATS scores have {'improved' if change >= 0 else 'decreased'} by {abs(change):.1f}% in the last week",
|
| 940 |
+
'trend_class': 'trend-up' if change >= 0 else 'trend-down',
|
| 941 |
+
'trend_icon': '↑' if change >= 0 else '↓',
|
| 942 |
+
'trend_value': f"{abs(change):.1f}%"
|
| 943 |
+
})
|
| 944 |
+
|
| 945 |
+
# Most Common Skills
|
| 946 |
+
cursor.execute("""
|
| 947 |
+
WITH RECURSIVE
|
| 948 |
+
split(skill, rest) AS (
|
| 949 |
+
SELECT '', skills || ','
|
| 950 |
+
FROM resume_data
|
| 951 |
+
WHERE skills IS NOT NULL
|
| 952 |
+
UNION ALL
|
| 953 |
+
SELECT
|
| 954 |
+
substr(rest, 0, instr(rest, ',')),
|
| 955 |
+
substr(rest, instr(rest, ',') + 1)
|
| 956 |
+
FROM split
|
| 957 |
+
WHERE rest <> ''
|
| 958 |
+
),
|
| 959 |
+
cleaned_skills AS (
|
| 960 |
+
SELECT TRIM(REPLACE(REPLACE(skill, '[', ''), ']', '')) as skill
|
| 961 |
+
FROM split
|
| 962 |
+
WHERE skill <> ''
|
| 963 |
+
)
|
| 964 |
+
SELECT skill, COUNT(*) as count
|
| 965 |
+
FROM cleaned_skills
|
| 966 |
+
GROUP BY skill
|
| 967 |
+
ORDER BY count DESC
|
| 968 |
+
LIMIT 3
|
| 969 |
+
""")
|
| 970 |
+
top_skills = cursor.fetchall()
|
| 971 |
+
if top_skills:
|
| 972 |
+
skills_text = f"Most in-demand skills: Python ({top_skills[0][1]} resumes), Java ({top_skills[1][1]} resumes), Express ({top_skills[2][1]} resumes)"
|
| 973 |
+
insights.append({
|
| 974 |
+
'title': 'Top Skills',
|
| 975 |
+
'icon': '💡',
|
| 976 |
+
'description': f"Most in-demand skills: {skills_text}",
|
| 977 |
+
'trend_class': 'trend-up',
|
| 978 |
+
'trend_icon': '🔝',
|
| 979 |
+
'trend_value': f"Top {len(top_skills)}"
|
| 980 |
+
})
|
| 981 |
+
|
| 982 |
+
return insights
|
| 983 |
+
|
| 984 |
+
def get_quick_stats(self):
|
| 985 |
+
"""Get quick statistics for the dashboard"""
|
| 986 |
+
cursor = self.conn.cursor()
|
| 987 |
+
|
| 988 |
+
# Total Resumes
|
| 989 |
+
cursor.execute("SELECT COUNT(*) FROM resume_data")
|
| 990 |
+
total_resumes = cursor.fetchone()[0]
|
| 991 |
+
|
| 992 |
+
# Average ATS Score
|
| 993 |
+
cursor.execute("SELECT AVG(ats_score) FROM resume_analysis")
|
| 994 |
+
avg_ats = cursor.fetchone()[0] or 0
|
| 995 |
+
|
| 996 |
+
# High Performing Resumes
|
| 997 |
+
cursor.execute("SELECT COUNT(*) FROM resume_analysis WHERE ats_score >= 70")
|
| 998 |
+
high_performing = cursor.fetchone()[0]
|
| 999 |
+
|
| 1000 |
+
# Success Rate
|
| 1001 |
+
success_rate = (high_performing / total_resumes * 100) if total_resumes > 0 else 0
|
| 1002 |
+
|
| 1003 |
+
return {
|
| 1004 |
+
"Total Resumes": f"{total_resumes:,}",
|
| 1005 |
+
"Avg ATS Score": f"{avg_ats:.1f}%",
|
| 1006 |
+
"High Performing": f"{high_performing:,}",
|
| 1007 |
+
"Success Rate": f"{success_rate:.1f}%"
|
| 1008 |
+
}
|
| 1009 |
+
|
| 1010 |
+
def create_enhanced_ats_gauge(self, value):
|
| 1011 |
+
"""Create an enhanced ATS score gauge chart"""
|
| 1012 |
+
reference = 70 # Target score
|
| 1013 |
+
delta = value - reference
|
| 1014 |
+
|
| 1015 |
+
fig = go.Figure(go.Indicator(
|
| 1016 |
+
mode="gauge+number+delta",
|
| 1017 |
+
value=value,
|
| 1018 |
+
delta={
|
| 1019 |
+
'reference': reference,
|
| 1020 |
+
'valueformat': '.1f',
|
| 1021 |
+
'increasing': {'color': '#2ecc71'},
|
| 1022 |
+
'decreasing': {'color': '#e74c3c'}
|
| 1023 |
+
},
|
| 1024 |
+
number={'font': {'size': 40, 'color': 'white'}},
|
| 1025 |
+
gauge={
|
| 1026 |
+
'axis': {
|
| 1027 |
+
'range': [0, 100],
|
| 1028 |
+
'tickwidth': 1,
|
| 1029 |
+
'tickcolor': 'white',
|
| 1030 |
+
'tickfont': {'color': 'white'}
|
| 1031 |
+
},
|
| 1032 |
+
'bar': {'color': '#3498db'},
|
| 1033 |
+
'bgcolor': 'rgba(0,0,0,0)',
|
| 1034 |
+
'borderwidth': 2,
|
| 1035 |
+
'bordercolor': 'white',
|
| 1036 |
+
'steps': [
|
| 1037 |
+
{'range': [0, 40], 'color': '#e74c3c'},
|
| 1038 |
+
{'range': [40, 70], 'color': '#f1c40f'},
|
| 1039 |
+
{'range': [70, 100], 'color': '#2ecc71'}
|
| 1040 |
+
],
|
| 1041 |
+
'threshold': {
|
| 1042 |
+
'line': {'color': 'white', 'width': 4},
|
| 1043 |
+
'thickness': 0.75,
|
| 1044 |
+
'value': reference
|
| 1045 |
+
}
|
| 1046 |
+
}
|
| 1047 |
+
))
|
| 1048 |
+
|
| 1049 |
+
fig.update_layout(
|
| 1050 |
+
title={
|
| 1051 |
+
'text': 'ATS Score Performance',
|
| 1052 |
+
'font': {'size': 24, 'color': 'white'},
|
| 1053 |
+
'y': 0.85
|
| 1054 |
+
},
|
| 1055 |
+
paper_bgcolor='rgba(0,0,0,0)',
|
| 1056 |
+
plot_bgcolor='rgba(0,0,0,0)',
|
| 1057 |
+
font={'color': 'white'},
|
| 1058 |
+
height=350,
|
| 1059 |
+
margin=dict(l=20, r=20, t=80, b=20)
|
| 1060 |
+
)
|
| 1061 |
+
|
| 1062 |
+
return fig
|
| 1063 |
+
|
| 1064 |
+
def create_skill_distribution_chart(self):
|
| 1065 |
+
"""Create a skill distribution chart"""
|
| 1066 |
+
categories, counts = self.get_skill_distribution()
|
| 1067 |
+
|
| 1068 |
+
fig = go.Figure(data=[
|
| 1069 |
+
go.Bar(
|
| 1070 |
+
x=categories,
|
| 1071 |
+
y=counts,
|
| 1072 |
+
marker_color=self.colors['info'],
|
| 1073 |
+
text=counts,
|
| 1074 |
+
textposition='auto',
|
| 1075 |
+
)
|
| 1076 |
+
])
|
| 1077 |
+
|
| 1078 |
+
fig.update_layout(
|
| 1079 |
+
title={
|
| 1080 |
+
'text': 'Skill Distribution',
|
| 1081 |
+
'y':0.95,
|
| 1082 |
+
'x':0.5,
|
| 1083 |
+
'xanchor': 'center',
|
| 1084 |
+
'yanchor': 'top'
|
| 1085 |
+
},
|
| 1086 |
+
height=350,
|
| 1087 |
+
plot_bgcolor='rgba(0,0,0,0)',
|
| 1088 |
+
paper_bgcolor='rgba(0,0,0,0)',
|
| 1089 |
+
font=dict(color=self.colors['text']),
|
| 1090 |
+
margin=dict(l=40, r=40, t=60, b=40),
|
| 1091 |
+
xaxis=dict(
|
| 1092 |
+
showgrid=False,
|
| 1093 |
+
showline=True,
|
| 1094 |
+
linecolor='rgba(255,255,255,0.2)',
|
| 1095 |
+
tickfont=dict(size=12)
|
| 1096 |
+
),
|
| 1097 |
+
yaxis=dict(
|
| 1098 |
+
showgrid=True,
|
| 1099 |
+
gridcolor='rgba(255,255,255,0.1)',
|
| 1100 |
+
zeroline=False
|
| 1101 |
+
),
|
| 1102 |
+
bargap=0.3
|
| 1103 |
+
)
|
| 1104 |
+
return fig
|
| 1105 |
+
|
| 1106 |
+
def create_submission_trends_chart(self):
|
| 1107 |
+
"""Create a weekly submission trend chart"""
|
| 1108 |
+
dates, submissions = self.get_weekly_trends()
|
| 1109 |
+
fig = go.Figure()
|
| 1110 |
+
fig.add_trace(go.Scatter(
|
| 1111 |
+
x=dates,
|
| 1112 |
+
y=submissions,
|
| 1113 |
+
mode='lines+markers',
|
| 1114 |
+
line=dict(color=self.colors['info'], width=3),
|
| 1115 |
+
marker=dict(size=8, color=self.colors['info'])
|
| 1116 |
+
))
|
| 1117 |
+
|
| 1118 |
+
fig.update_layout(
|
| 1119 |
+
title="Weekly Submission Pattern",
|
| 1120 |
+
paper_bgcolor=self.colors['card'],
|
| 1121 |
+
plot_bgcolor=self.colors['card'],
|
| 1122 |
+
font={'color': self.colors['text']},
|
| 1123 |
+
height=300,
|
| 1124 |
+
margin=dict(l=20, r=20, t=50, b=20)
|
| 1125 |
+
)
|
| 1126 |
+
fig.update_xaxes(title_text="Day of Week", color=self.colors['text'])
|
| 1127 |
+
fig.update_yaxes(title_text="Number of Submissions", color=self.colors['text'])
|
| 1128 |
+
|
| 1129 |
+
return fig
|
| 1130 |
+
|
| 1131 |
+
def create_job_category_chart(self):
|
| 1132 |
+
"""Create a success rate by category chart"""
|
| 1133 |
+
categories, rates = self.get_job_category_stats()
|
| 1134 |
+
fig = go.Figure(go.Bar(
|
| 1135 |
+
x=categories,
|
| 1136 |
+
y=rates,
|
| 1137 |
+
marker_color=[self.colors['success'], self.colors['info'],
|
| 1138 |
+
self.colors['warning'], self.colors['purple'],
|
| 1139 |
+
self.colors['secondary']],
|
| 1140 |
+
text=[f"{rate}%" for rate in rates],
|
| 1141 |
+
textposition='auto',
|
| 1142 |
+
))
|
| 1143 |
+
|
| 1144 |
+
fig.update_layout(
|
| 1145 |
+
title="Success Rate by Job Category",
|
| 1146 |
+
paper_bgcolor=self.colors['card'],
|
| 1147 |
+
plot_bgcolor=self.colors['card'],
|
| 1148 |
+
font={'color': self.colors['text']},
|
| 1149 |
+
height=300,
|
| 1150 |
+
margin=dict(l=20, r=20, t=50, b=20)
|
| 1151 |
+
)
|
| 1152 |
+
fig.update_xaxes(title_text="Job Category", color=self.colors['text'])
|
| 1153 |
+
fig.update_yaxes(title_text="Success Rate (%)", color=self.colors['text'])
|
| 1154 |
+
|
| 1155 |
+
return fig
|
feedback/__pycache__/feedback.cpython-311.pyc
ADDED
|
Binary file (14.6 kB). View file
|
|
|
feedback/feedback.db
ADDED
|
Binary file (12.3 kB). View file
|
|
|
feedback/feedback.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import sqlite3
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import time
|
| 6 |
+
|
| 7 |
+
class FeedbackManager:
|
| 8 |
+
def __init__(self):
|
| 9 |
+
self.db_path = "feedback/feedback.db"
|
| 10 |
+
self.setup_database()
|
| 11 |
+
|
| 12 |
+
def setup_database(self):
|
| 13 |
+
"""Create feedback table if it doesn't exist"""
|
| 14 |
+
conn = sqlite3.connect(self.db_path)
|
| 15 |
+
c = conn.cursor()
|
| 16 |
+
c.execute('''
|
| 17 |
+
CREATE TABLE IF NOT EXISTS feedback (
|
| 18 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 19 |
+
rating INTEGER,
|
| 20 |
+
usability_score INTEGER,
|
| 21 |
+
feature_satisfaction INTEGER,
|
| 22 |
+
missing_features TEXT,
|
| 23 |
+
improvement_suggestions TEXT,
|
| 24 |
+
user_experience TEXT,
|
| 25 |
+
timestamp DATETIME
|
| 26 |
+
)
|
| 27 |
+
''')
|
| 28 |
+
conn.commit()
|
| 29 |
+
conn.close()
|
| 30 |
+
|
| 31 |
+
def save_feedback(self, feedback_data):
|
| 32 |
+
"""Save feedback to database"""
|
| 33 |
+
conn = sqlite3.connect(self.db_path)
|
| 34 |
+
c = conn.cursor()
|
| 35 |
+
c.execute('''
|
| 36 |
+
INSERT INTO feedback (
|
| 37 |
+
rating, usability_score, feature_satisfaction,
|
| 38 |
+
missing_features, improvement_suggestions,
|
| 39 |
+
user_experience, timestamp
|
| 40 |
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
| 41 |
+
''', (
|
| 42 |
+
feedback_data['rating'],
|
| 43 |
+
feedback_data['usability_score'],
|
| 44 |
+
feedback_data['feature_satisfaction'],
|
| 45 |
+
feedback_data['missing_features'],
|
| 46 |
+
feedback_data['improvement_suggestions'],
|
| 47 |
+
feedback_data['user_experience'],
|
| 48 |
+
datetime.now()
|
| 49 |
+
))
|
| 50 |
+
conn.commit()
|
| 51 |
+
conn.close()
|
| 52 |
+
|
| 53 |
+
def get_feedback_stats(self):
|
| 54 |
+
"""Get feedback statistics"""
|
| 55 |
+
conn = sqlite3.connect(self.db_path)
|
| 56 |
+
df = pd.read_sql_query("SELECT * FROM feedback", conn)
|
| 57 |
+
conn.close()
|
| 58 |
+
|
| 59 |
+
if df.empty:
|
| 60 |
+
return {
|
| 61 |
+
'avg_rating': 0,
|
| 62 |
+
'avg_usability': 0,
|
| 63 |
+
'avg_satisfaction': 0,
|
| 64 |
+
'total_responses': 0
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
return {
|
| 68 |
+
'avg_rating': df['rating'].mean(),
|
| 69 |
+
'avg_usability': df['usability_score'].mean(),
|
| 70 |
+
'avg_satisfaction': df['feature_satisfaction'].mean(),
|
| 71 |
+
'total_responses': len(df)
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
def render_feedback_form(self):
|
| 75 |
+
"""Render the feedback form"""
|
| 76 |
+
st.markdown("""
|
| 77 |
+
<style>
|
| 78 |
+
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css');
|
| 79 |
+
|
| 80 |
+
.feedback-container {
|
| 81 |
+
background: rgba(255, 255, 255, 0.05);
|
| 82 |
+
backdrop-filter: blur(10px);
|
| 83 |
+
padding: 30px;
|
| 84 |
+
border-radius: 20px;
|
| 85 |
+
margin: 20px 0;
|
| 86 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 87 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.feedback-header {
|
| 91 |
+
color: #E0E0E0;
|
| 92 |
+
font-size: 1.5em;
|
| 93 |
+
font-weight: 600;
|
| 94 |
+
margin-bottom: 25px;
|
| 95 |
+
text-align: center;
|
| 96 |
+
padding: 15px;
|
| 97 |
+
background: linear-gradient(135deg, #4CAF50, #2196F3);
|
| 98 |
+
border-radius: 12px;
|
| 99 |
+
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.2);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.feedback-section {
|
| 103 |
+
margin: 20px 0;
|
| 104 |
+
padding: 20px;
|
| 105 |
+
border-radius: 15px;
|
| 106 |
+
background: rgba(255, 255, 255, 0.03);
|
| 107 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 108 |
+
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.feedback-section:hover {
|
| 112 |
+
transform: translateY(-5px);
|
| 113 |
+
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
.feedback-label {
|
| 117 |
+
color: #E0E0E0;
|
| 118 |
+
font-size: 1.1em;
|
| 119 |
+
font-weight: 500;
|
| 120 |
+
margin-bottom: 10px;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.star-rating {
|
| 124 |
+
font-size: 24px;
|
| 125 |
+
color: #FFD700;
|
| 126 |
+
cursor: pointer;
|
| 127 |
+
transition: transform 0.2s ease;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.star-rating:hover {
|
| 131 |
+
transform: scale(1.1);
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.rating-container {
|
| 135 |
+
display: flex;
|
| 136 |
+
align-items: center;
|
| 137 |
+
gap: 10px;
|
| 138 |
+
margin: 15px 0;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.submit-button {
|
| 142 |
+
background: linear-gradient(135deg, #4CAF50, #2196F3);
|
| 143 |
+
color: white;
|
| 144 |
+
padding: 12px 25px;
|
| 145 |
+
border: none;
|
| 146 |
+
border-radius: 8px;
|
| 147 |
+
font-weight: 600;
|
| 148 |
+
cursor: pointer;
|
| 149 |
+
transition: all 0.3s ease;
|
| 150 |
+
text-transform: uppercase;
|
| 151 |
+
letter-spacing: 1px;
|
| 152 |
+
width: 100%;
|
| 153 |
+
margin-top: 20px;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
.submit-button:hover {
|
| 157 |
+
transform: translateY(-2px);
|
| 158 |
+
box-shadow: 0 5px 15px rgba(33, 150, 243, 0.3);
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
.textarea-container {
|
| 162 |
+
background: rgba(255, 255, 255, 0.03);
|
| 163 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 164 |
+
border-radius: 8px;
|
| 165 |
+
padding: 10px;
|
| 166 |
+
margin-top: 10px;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.textarea-container textarea {
|
| 170 |
+
width: 100%;
|
| 171 |
+
min-height: 100px;
|
| 172 |
+
background: transparent;
|
| 173 |
+
border: none;
|
| 174 |
+
color: #E0E0E0;
|
| 175 |
+
font-size: 1em;
|
| 176 |
+
resize: vertical;
|
| 177 |
+
}
|
| 178 |
+
</style>
|
| 179 |
+
""", unsafe_allow_html=True)
|
| 180 |
+
|
| 181 |
+
st.markdown('<div class="feedback-container">', unsafe_allow_html=True)
|
| 182 |
+
st.markdown('<h2 class="feedback-header">📝 Share Your Feedback</h2>', unsafe_allow_html=True)
|
| 183 |
+
|
| 184 |
+
# Overall Rating
|
| 185 |
+
st.markdown('<div class="feedback-section">', unsafe_allow_html=True)
|
| 186 |
+
st.markdown('<label class="feedback-label">Overall Experience Rating</label>', unsafe_allow_html=True)
|
| 187 |
+
rating = st.slider("Overall Rating", 1, 5, 5, help="Rate your overall experience with the app", label_visibility="collapsed")
|
| 188 |
+
st.markdown(f'<div class="rating-container">{"⭐" * rating}</div>', unsafe_allow_html=True)
|
| 189 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 190 |
+
|
| 191 |
+
# Usability Score
|
| 192 |
+
st.markdown('<div class="feedback-section">', unsafe_allow_html=True)
|
| 193 |
+
st.markdown('<label class="feedback-label">How easy was it to use our app?</label>', unsafe_allow_html=True)
|
| 194 |
+
usability_score = st.slider("Usability Score", 1, 5, 5, help="Rate the app's ease of use", label_visibility="collapsed")
|
| 195 |
+
st.markdown(f'<div class="rating-container">{"⭐" * usability_score}</div>', unsafe_allow_html=True)
|
| 196 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 197 |
+
|
| 198 |
+
# Feature Satisfaction
|
| 199 |
+
st.markdown('<div class="feedback-section">', unsafe_allow_html=True)
|
| 200 |
+
st.markdown('<label class="feedback-label">How satisfied are you with our features?</label>', unsafe_allow_html=True)
|
| 201 |
+
feature_satisfaction = st.slider("Feature Satisfaction", 1, 5, 5, help="Rate your satisfaction with the app's features", label_visibility="collapsed")
|
| 202 |
+
st.markdown(f'<div class="rating-container">{"⭐" * feature_satisfaction}</div>', unsafe_allow_html=True)
|
| 203 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 204 |
+
|
| 205 |
+
# Text Feedback
|
| 206 |
+
st.markdown('<div class="feedback-section">', unsafe_allow_html=True)
|
| 207 |
+
st.markdown('<label class="feedback-label">What features would you like to see added?</label>', unsafe_allow_html=True)
|
| 208 |
+
missing_features = st.text_area("Missing Features", placeholder="Share your feature requests...", label_visibility="collapsed")
|
| 209 |
+
|
| 210 |
+
st.markdown('<label class="feedback-label">How can we improve?</label>', unsafe_allow_html=True)
|
| 211 |
+
improvement_suggestions = st.text_area("Improvement Suggestions", placeholder="Your suggestions for improvement...", label_visibility="collapsed")
|
| 212 |
+
|
| 213 |
+
st.markdown('<label class="feedback-label">Tell us about your experience</label>', unsafe_allow_html=True)
|
| 214 |
+
user_experience = st.text_area("User Experience", placeholder="Share your experience with us...", label_visibility="collapsed")
|
| 215 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 216 |
+
|
| 217 |
+
# Submit Button
|
| 218 |
+
if st.button("Submit Feedback", key="submit_feedback"):
|
| 219 |
+
try:
|
| 220 |
+
# Create progress bar
|
| 221 |
+
progress_bar = st.progress(0)
|
| 222 |
+
status_text = st.empty()
|
| 223 |
+
|
| 224 |
+
# Simulate processing with animation
|
| 225 |
+
for i in range(100):
|
| 226 |
+
progress_bar.progress(i + 1)
|
| 227 |
+
if i < 30:
|
| 228 |
+
status_text.text("Processing feedback... 📝")
|
| 229 |
+
elif i < 60:
|
| 230 |
+
status_text.text("Analyzing responses... 🔍")
|
| 231 |
+
elif i < 90:
|
| 232 |
+
status_text.text("Saving to database... 💾")
|
| 233 |
+
else:
|
| 234 |
+
status_text.text("Finalizing... ✨")
|
| 235 |
+
time.sleep(0.01)
|
| 236 |
+
|
| 237 |
+
# Save feedback
|
| 238 |
+
feedback_data = {
|
| 239 |
+
'rating': rating,
|
| 240 |
+
'usability_score': usability_score,
|
| 241 |
+
'feature_satisfaction': feature_satisfaction,
|
| 242 |
+
'missing_features': missing_features,
|
| 243 |
+
'improvement_suggestions': improvement_suggestions,
|
| 244 |
+
'user_experience': user_experience
|
| 245 |
+
}
|
| 246 |
+
self.save_feedback(feedback_data)
|
| 247 |
+
|
| 248 |
+
# Clear progress elements
|
| 249 |
+
progress_bar.empty()
|
| 250 |
+
status_text.empty()
|
| 251 |
+
|
| 252 |
+
# Show success message with animation
|
| 253 |
+
success_container = st.empty()
|
| 254 |
+
success_container.markdown("""
|
| 255 |
+
<div style="text-align: center; padding: 20px; background: linear-gradient(90deg, rgba(76, 175, 80, 0.1), rgba(33, 150, 243, 0.1)); border-radius: 10px;">
|
| 256 |
+
<h2 style="color: #4CAF50;">Thank You! 🎉</h2>
|
| 257 |
+
<p style="color: #E0E0E0;">Your feedback helps us improve Smart Resume AI</p>
|
| 258 |
+
</div>
|
| 259 |
+
""", unsafe_allow_html=True)
|
| 260 |
+
|
| 261 |
+
# Show balloons animation
|
| 262 |
+
st.balloons()
|
| 263 |
+
|
| 264 |
+
# Keep success message visible
|
| 265 |
+
time.sleep(2)
|
| 266 |
+
|
| 267 |
+
except Exception as e:
|
| 268 |
+
st.error(f"Error submitting feedback: {str(e)}")
|
| 269 |
+
|
| 270 |
+
def render_feedback_stats(self):
|
| 271 |
+
"""Render feedback statistics"""
|
| 272 |
+
stats = self.get_feedback_stats()
|
| 273 |
+
|
| 274 |
+
st.markdown("""
|
| 275 |
+
<div style="text-align: center; padding: 15px; background: linear-gradient(90deg, rgba(76, 175, 80, 0.1), rgba(33, 150, 243, 0.1)); border-radius: 10px; margin-bottom: 20px;">
|
| 276 |
+
<h3 style="color: #E0E0E0;">Feedback Overview 📊</h3>
|
| 277 |
+
</div>
|
| 278 |
+
""", unsafe_allow_html=True)
|
| 279 |
+
|
| 280 |
+
cols = st.columns(4)
|
| 281 |
+
metrics = [
|
| 282 |
+
{"label": "Total Responses", "value": f"{stats['total_responses']:,}", "delta": "↗"},
|
| 283 |
+
{"label": "Avg Rating", "value": f"{stats['avg_rating']:.1f}/5.0", "delta": "⭐"},
|
| 284 |
+
{"label": "Usability Score", "value": f"{stats['avg_usability']:.1f}/5.0", "delta": "🎯"},
|
| 285 |
+
{"label": "Satisfaction", "value": f"{stats['avg_satisfaction']:.1f}/5.0", "delta": "😊"}
|
| 286 |
+
]
|
| 287 |
+
|
| 288 |
+
for col, metric in zip(cols, metrics):
|
| 289 |
+
col.markdown(f"""
|
| 290 |
+
<div style="background: rgba(255, 255, 255, 0.05); padding: 15px; border-radius: 8px; text-align: center;">
|
| 291 |
+
<div style="color: #B0B0B0; font-size: 0.9em;">{metric['label']}</div>
|
| 292 |
+
<div style="font-size: 1.5em; color: #4CAF50; margin: 5px 0;">{metric['value']}</div>
|
| 293 |
+
<div style="color: #E0E0E0; font-size: 1.2em;">{metric['delta']}</div>
|
| 294 |
+
</div>
|
| 295 |
+
""", unsafe_allow_html=True)
|
feedback/schema.sql
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
CREATE TABLE IF NOT EXISTS feedback (
|
| 2 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 3 |
+
rating INTEGER NOT NULL,
|
| 4 |
+
usability_score INTEGER NOT NULL,
|
| 5 |
+
feature_satisfaction INTEGER NOT NULL,
|
| 6 |
+
missing_features TEXT,
|
| 7 |
+
improvement_suggestions TEXT,
|
| 8 |
+
user_experience TEXT,
|
| 9 |
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 10 |
+
);
|
jobs/__pycache__/companies.cpython-311.pyc
ADDED
|
Binary file (6.53 kB). View file
|
|
|
jobs/__pycache__/job_portals.cpython-311.pyc
ADDED
|
Binary file (10.1 kB). View file
|
|
|
jobs/__pycache__/job_search.cpython-311.pyc
ADDED
|
Binary file (26.4 kB). View file
|
|
|
jobs/__pycache__/linkedin_scraper.cpython-311.pyc
ADDED
|
Binary file (32.2 kB). View file
|
|
|
jobs/__pycache__/suggestions.cpython-311.pyc
ADDED
|
Binary file (9.63 kB). View file
|
|
|
jobs/__pycache__/webdriver_utils.cpython-311.pyc
ADDED
|
Binary file (10.2 kB). View file
|
|
|
jobs/companies.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Company data and market insights for job search"""
|
| 2 |
+
|
| 3 |
+
FEATURED_COMPANIES = {
|
| 4 |
+
"tech": [
|
| 5 |
+
{
|
| 6 |
+
"name": "Google",
|
| 7 |
+
"icon": "fab fa-google",
|
| 8 |
+
"color": "#4285F4",
|
| 9 |
+
"careers_url": "https://careers.google.com",
|
| 10 |
+
"description": "Leading technology company known for search, cloud, and innovation",
|
| 11 |
+
"categories": ["Software", "AI/ML", "Cloud", "Data Science"]
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
"name": "Microsoft",
|
| 15 |
+
"icon": "fab fa-microsoft",
|
| 16 |
+
"color": "#00A4EF",
|
| 17 |
+
"careers_url": "https://careers.microsoft.com",
|
| 18 |
+
"description": "Global leader in software, cloud, and enterprise solutions",
|
| 19 |
+
"categories": ["Software", "Cloud", "Gaming", "Enterprise"]
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"name": "Amazon",
|
| 23 |
+
"icon": "fab fa-amazon",
|
| 24 |
+
"color": "#FF9900",
|
| 25 |
+
"careers_url": "https://www.amazon.jobs",
|
| 26 |
+
"description": "E-commerce and cloud computing giant",
|
| 27 |
+
"categories": ["Software", "Operations", "Cloud", "Retail"]
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"name": "Apple",
|
| 31 |
+
"icon": "fab fa-apple",
|
| 32 |
+
"color": "#555555",
|
| 33 |
+
"careers_url": "https://www.apple.com/careers",
|
| 34 |
+
"description": "Innovation leader in consumer technology",
|
| 35 |
+
"categories": ["Software", "Hardware", "Design", "AI/ML"]
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"name": "Facebook",
|
| 39 |
+
"icon": "fab fa-facebook",
|
| 40 |
+
"color": "#1877F2",
|
| 41 |
+
"careers_url": "https://www.metacareers.com/",
|
| 42 |
+
"description": "Social media and technology company",
|
| 43 |
+
"categories": ["Software", "Marketing", "Networking", "AI/ML"]
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"name": "Netflix",
|
| 47 |
+
"icon": "fas fa-play-circle",
|
| 48 |
+
"color": "#E50914",
|
| 49 |
+
"careers_url": "https://explore.jobs.netflix.net/careers",
|
| 50 |
+
"description": "Streaming media company",
|
| 51 |
+
"categories": ["Software", "Marketing", "Design", "Service"],
|
| 52 |
+
"logo_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/08/Netflix_2015_logo.svg/1920px-Netflix_2015_logo.svg.png",
|
| 53 |
+
"website": "https://jobs.netflix.com/",
|
| 54 |
+
"industry": "Entertainment & Technology"
|
| 55 |
+
}
|
| 56 |
+
],
|
| 57 |
+
"indian_tech": [
|
| 58 |
+
{
|
| 59 |
+
"name": "TCS",
|
| 60 |
+
"icon": "fas fa-building",
|
| 61 |
+
"color": "#0070C0",
|
| 62 |
+
"careers_url": "https://www.tcs.com/careers",
|
| 63 |
+
"description": "India's largest IT services company",
|
| 64 |
+
"categories": ["IT Services", "Consulting", "Digital"]
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
"name": "Infosys",
|
| 68 |
+
"icon": "fas fa-building",
|
| 69 |
+
"color": "#007CC3",
|
| 70 |
+
"careers_url": "https://www.infosys.com/careers",
|
| 71 |
+
"description": "Global leader in digital services and consulting",
|
| 72 |
+
"categories": ["IT Services", "Consulting", "Digital"]
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"name": "Wipro",
|
| 76 |
+
"icon": "fas fa-building",
|
| 77 |
+
"color": "#341F65",
|
| 78 |
+
"careers_url": "https://careers.wipro.com",
|
| 79 |
+
"description": "Leading global information technology company",
|
| 80 |
+
"categories": ["IT Services", "Consulting", "Digital"]
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
"name": "HCL",
|
| 84 |
+
"icon": "fas fa-building",
|
| 85 |
+
"color": "#0075C9",
|
| 86 |
+
"careers_url": "https://www.hcltech.com/careers",
|
| 87 |
+
"description": "Global technology company",
|
| 88 |
+
"categories": ["IT Services", "Engineering", "Digital"]
|
| 89 |
+
}
|
| 90 |
+
],
|
| 91 |
+
"global_corps": [
|
| 92 |
+
{
|
| 93 |
+
"name": "IBM",
|
| 94 |
+
"icon": "fas fa-server",
|
| 95 |
+
"color": "#1F70C1",
|
| 96 |
+
"careers_url": "https://www.ibm.com/careers",
|
| 97 |
+
"description": "Global leader in technology and consulting",
|
| 98 |
+
"categories": ["Software", "Consulting", "AI/ML", "Cloud"],
|
| 99 |
+
"logo_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/51/IBM_logo.svg/1920px-IBM_logo.svg.png",
|
| 100 |
+
"website": "https://www.ibm.com/careers/",
|
| 101 |
+
"industry": "Technology & Consulting"
|
| 102 |
+
},
|
| 103 |
+
{
|
| 104 |
+
"name": "Accenture",
|
| 105 |
+
"icon": "fas fa-building",
|
| 106 |
+
"color": "#A100FF",
|
| 107 |
+
"careers_url": "https://www.accenture.com/careers",
|
| 108 |
+
"description": "Global professional services company",
|
| 109 |
+
"categories": ["Consulting", "Technology", "Digital"]
|
| 110 |
+
},
|
| 111 |
+
{
|
| 112 |
+
"name": "Cognizant",
|
| 113 |
+
"icon": "fas fa-building",
|
| 114 |
+
"color": "#1299D8",
|
| 115 |
+
"careers_url": "https://careers.cognizant.com",
|
| 116 |
+
"description": "Leading professional services company",
|
| 117 |
+
"categories": ["IT Services", "Consulting", "Digital"]
|
| 118 |
+
}
|
| 119 |
+
]
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
JOB_MARKET_INSIGHTS = {
|
| 123 |
+
"trending_skills": [
|
| 124 |
+
{"name": "Artificial Intelligence", "growth": "+45%", "icon": "fas fa-brain"},
|
| 125 |
+
{"name": "Cloud Computing", "growth": "+38%", "icon": "fas fa-cloud"},
|
| 126 |
+
{"name": "Data Science", "growth": "+35%", "icon": "fas fa-chart-line"},
|
| 127 |
+
{"name": "Cybersecurity", "growth": "+32%", "icon": "fas fa-shield-alt"},
|
| 128 |
+
{"name": "DevOps", "growth": "+30%", "icon": "fas fa-code-branch"},
|
| 129 |
+
{"name": "Machine Learning", "growth": "+28%", "icon": "fas fa-robot"},
|
| 130 |
+
{"name": "Blockchain", "growth": "+25%", "icon": "fas fa-lock"},
|
| 131 |
+
{"name": "Big Data", "growth": "+23%", "icon": "fas fa-database"},
|
| 132 |
+
{"name": "Internet of Things", "growth": "+21%", "icon": "fas fa-wifi"}
|
| 133 |
+
],
|
| 134 |
+
"top_locations": [
|
| 135 |
+
{"name": "Bangalore", "jobs": "50,000+", "icon": "fas fa-city"},
|
| 136 |
+
{"name": "Mumbai", "jobs": "35,000+", "icon": "fas fa-city"},
|
| 137 |
+
{"name": "Delhi NCR", "jobs": "30,000+", "icon": "fas fa-city"},
|
| 138 |
+
{"name": "Hyderabad", "jobs": "25,000+", "icon": "fas fa-city"},
|
| 139 |
+
{"name": "Pune", "jobs": "20,000+", "icon": "fas fa-city"},
|
| 140 |
+
{"name": "Chennai", "jobs": "15,000+", "icon": "fas fa-city"},
|
| 141 |
+
{"name": "Noida", "jobs": "10,000+", "icon": "fas fa-city"},
|
| 142 |
+
{"name": "Vadodara", "jobs": "7,000+", "icon": "fas fa-city"},
|
| 143 |
+
{"name": "Ahmedabad", "jobs": "6,000+", "icon": "fas fa-city"},
|
| 144 |
+
{"name": "Remote", "jobs": "3,000+", "icon": "fas fa-globe-americas"},
|
| 145 |
+
],
|
| 146 |
+
"salary_insights": [
|
| 147 |
+
{"role": "Machine Learning Engineer", "range": "10-35 LPA", "experience": "0-5 years"},
|
| 148 |
+
{"role": "Big Data Engineer", "range": "8-30 LPA", "experience": "0-5 years"},
|
| 149 |
+
{"role": "Software Engineer", "range": "5-25 LPA", "experience": "0-5 years"},
|
| 150 |
+
{"role": "Data Scientist", "range": "8-30 LPA", "experience": "0-5 years"},
|
| 151 |
+
{"role": "DevOps Engineer", "range": "6-28 LPA", "experience": "0-5 years"},
|
| 152 |
+
{"role": "UI/UX Designer", "range": "5-25 LPA", "experience": "0-5 years"},
|
| 153 |
+
{"role": "Full Stack Developer", "range": "8-30 LPA", "experience": "0-5 years"},
|
| 154 |
+
{"role": "C++/C#/Python/Java Developer", "range": "6-26 LPA", "experience": "0-5 years"},
|
| 155 |
+
{"role": "Django Developer", "range": "7-27 LPA", "experience": "0-5 years"},
|
| 156 |
+
{"role": "Cloud Engineer", "range": "6-26 LPA", "experience": "0-5 years"},
|
| 157 |
+
{"role": "Google Cloud/AWS/Azure Engineer", "range": "6-26 LPA", "experience": "0-5 years"},
|
| 158 |
+
{"role": "Salesforce Engineer", "range": "6-26 LPA", "experience": "0-5 years"},
|
| 159 |
+
]
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
def get_featured_companies(category=None):
|
| 163 |
+
"""Get featured companies, optionally filtered by category"""
|
| 164 |
+
if category and category in FEATURED_COMPANIES:
|
| 165 |
+
return FEATURED_COMPANIES[category]
|
| 166 |
+
return [company for companies in FEATURED_COMPANIES.values() for company in companies]
|
| 167 |
+
|
| 168 |
+
def get_market_insights():
|
| 169 |
+
"""Get job market insights"""
|
| 170 |
+
return JOB_MARKET_INSIGHTS
|
| 171 |
+
|
| 172 |
+
def get_company_info(company_name):
|
| 173 |
+
"""Get company information by name"""
|
| 174 |
+
for companies in FEATURED_COMPANIES.values():
|
| 175 |
+
for company in companies:
|
| 176 |
+
if company["name"] == company_name:
|
| 177 |
+
return company
|
| 178 |
+
return None
|
| 179 |
+
|
| 180 |
+
def get_companies_by_industry(industry):
|
| 181 |
+
"""Get list of companies by industry"""
|
| 182 |
+
companies = []
|
| 183 |
+
for companies_list in FEATURED_COMPANIES.values():
|
| 184 |
+
for company in companies_list:
|
| 185 |
+
if "industry" in company and company["industry"] == industry:
|
| 186 |
+
companies.append(company)
|
| 187 |
+
return companies
|
jobs/job_portals.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Module for handling job portal integrations"""
|
| 2 |
+
import urllib.parse
|
| 3 |
+
from typing import Dict, List
|
| 4 |
+
from .suggestions import LOCATION_SUGGESTIONS, get_cities_by_state
|
| 5 |
+
|
| 6 |
+
class JobPortal:
|
| 7 |
+
"""Class for searching jobs across multiple job portals"""
|
| 8 |
+
|
| 9 |
+
def __init__(self):
|
| 10 |
+
"""Initialize job portal URLs and parameters"""
|
| 11 |
+
self.portals = [
|
| 12 |
+
{
|
| 13 |
+
"name": "LinkedIn",
|
| 14 |
+
"icon": "fab fa-linkedin",
|
| 15 |
+
"color": "#0A66C2",
|
| 16 |
+
"url": "https://www.linkedin.com/jobs/search/?keywords={}&location={}&f_E={}",
|
| 17 |
+
"experience_param": ""
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"name": "Naukri",
|
| 21 |
+
"icon": "fas fa-building",
|
| 22 |
+
"color": "#FF7555",
|
| 23 |
+
"url": "https://www.naukri.com/{}-jobs-in-{}?experience={}",
|
| 24 |
+
"experience_param": ""
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"name": "Foundit (Monster)",
|
| 28 |
+
"icon": "fas fa-globe",
|
| 29 |
+
"color": "#5D3FD3",
|
| 30 |
+
"url": "https://www.foundit.in/srp/results?query={}&locations={}",
|
| 31 |
+
"experience_param": ""
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"name": "FreshersWorld",
|
| 35 |
+
"icon": "fas fa-graduation-cap",
|
| 36 |
+
"color": "#003A9B",
|
| 37 |
+
"url": "https://www.freshersworld.com/jobs/jobsearch/{}-jobs-in-{}",
|
| 38 |
+
"experience_param": ""
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"name": "TimesJobs",
|
| 42 |
+
"icon": "fas fa-briefcase",
|
| 43 |
+
"color": "#003A9B",
|
| 44 |
+
"url": "https://www.timesjobs.com/candidate/job-search.html?searchType=personalizedSearch&from=submit&txtKeywords={}&txtLocation={}",
|
| 45 |
+
"experience_param": ""
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"name": "Instahyre",
|
| 49 |
+
"icon": "fas fa-user-tie",
|
| 50 |
+
"color": "#003A9B",
|
| 51 |
+
"url": "https://www.instahyre.com/{}-jobs-in-{}",
|
| 52 |
+
"experience_param": ""
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"name": "Indeed",
|
| 56 |
+
"icon": "fas fa-search-dollar",
|
| 57 |
+
"color": "#003A9B",
|
| 58 |
+
"url": "https://in.indeed.com/jobs?q={}&l={}&explvl={}",
|
| 59 |
+
"experience_param": ""
|
| 60 |
+
}
|
| 61 |
+
]
|
| 62 |
+
|
| 63 |
+
def get_portal_list(self) -> List[Dict]:
|
| 64 |
+
"""Get list of available job portals"""
|
| 65 |
+
return self.portals
|
| 66 |
+
|
| 67 |
+
def format_query(self, query: str) -> str:
|
| 68 |
+
"""Format query string for URLs"""
|
| 69 |
+
# Replace spaces with appropriate characters based on portal
|
| 70 |
+
return query.replace(" ", "+")
|
| 71 |
+
|
| 72 |
+
def format_location(self, location: str) -> str:
|
| 73 |
+
"""Format location string for URLs"""
|
| 74 |
+
if not location:
|
| 75 |
+
return ""
|
| 76 |
+
|
| 77 |
+
# Check if location is a state
|
| 78 |
+
location = location.strip()
|
| 79 |
+
is_state = False
|
| 80 |
+
|
| 81 |
+
# Check if the location is a state
|
| 82 |
+
for loc in LOCATION_SUGGESTIONS:
|
| 83 |
+
if loc.get("type") == "state" and loc.get("text").lower() == location.lower():
|
| 84 |
+
is_state = True
|
| 85 |
+
break
|
| 86 |
+
|
| 87 |
+
# If it's a state, get the major city in that state for better job results
|
| 88 |
+
if is_state:
|
| 89 |
+
cities = get_cities_by_state(location)
|
| 90 |
+
if cities:
|
| 91 |
+
# Use the first city in the state (usually the capital or major city)
|
| 92 |
+
location = cities[0]["text"]
|
| 93 |
+
|
| 94 |
+
# Convert to lowercase and replace spaces with hyphens
|
| 95 |
+
return location.lower().replace(" ", "-")
|
| 96 |
+
|
| 97 |
+
def format_job_title(self, title: str) -> str:
|
| 98 |
+
"""Format job title for URLs"""
|
| 99 |
+
# Remove common words and special characters
|
| 100 |
+
title = title.lower()
|
| 101 |
+
title = title.replace("developer", "").replace("engineer", "").strip()
|
| 102 |
+
title = title.replace(" ", "-")
|
| 103 |
+
return title.strip("-")
|
| 104 |
+
|
| 105 |
+
def format_experience(self, experience: str) -> tuple:
|
| 106 |
+
"""Format experience for different job portals"""
|
| 107 |
+
if not experience or experience == "all":
|
| 108 |
+
return "", "0", "0", "entry"
|
| 109 |
+
|
| 110 |
+
try:
|
| 111 |
+
# Handle dictionary input
|
| 112 |
+
if isinstance(experience, dict):
|
| 113 |
+
exp_id = experience.get('id', 'all')
|
| 114 |
+
if exp_id == 'all':
|
| 115 |
+
return "", "0", "0", "entry"
|
| 116 |
+
|
| 117 |
+
# Split experience range (e.g., "1-3" -> ["1", "3"])
|
| 118 |
+
if "-" in exp_id:
|
| 119 |
+
exp_min, exp_max = exp_id.split('-')
|
| 120 |
+
if exp_max == "+":
|
| 121 |
+
exp_max = "15" # Set a reasonable maximum for 10+ years
|
| 122 |
+
else:
|
| 123 |
+
# Handle "fresher" or other non-range values
|
| 124 |
+
exp_min = "0"
|
| 125 |
+
exp_max = "1"
|
| 126 |
+
|
| 127 |
+
# Map to portal-specific format
|
| 128 |
+
exp_level = {
|
| 129 |
+
"fresher": "0",
|
| 130 |
+
"0-1": "0",
|
| 131 |
+
"1-3": "1",
|
| 132 |
+
"3-5": "2",
|
| 133 |
+
"5-7": "3",
|
| 134 |
+
"7-10": "4",
|
| 135 |
+
"10+": "5"
|
| 136 |
+
}.get(exp_id, "0")
|
| 137 |
+
|
| 138 |
+
return exp_level, exp_min, exp_max, "entry" if exp_min == "0" else "experienced"
|
| 139 |
+
|
| 140 |
+
return "", "0", "0", "entry"
|
| 141 |
+
|
| 142 |
+
except Exception as e:
|
| 143 |
+
print(f"Error formatting experience: {str(e)}")
|
| 144 |
+
return "", "0", "0", "entry"
|
| 145 |
+
|
| 146 |
+
def get_experience_param(self, portal_name, experience):
|
| 147 |
+
"""Get experience parameter for specific portal"""
|
| 148 |
+
experience_id = experience.get("id", "all")
|
| 149 |
+
|
| 150 |
+
if experience_id == "all":
|
| 151 |
+
if portal_name == "Foundit (Monster)":
|
| 152 |
+
return ""
|
| 153 |
+
elif portal_name == "Naukri":
|
| 154 |
+
return ""
|
| 155 |
+
elif portal_name == "LinkedIn":
|
| 156 |
+
return ""
|
| 157 |
+
elif portal_name == "Indeed":
|
| 158 |
+
return "entry_level"
|
| 159 |
+
|
| 160 |
+
if portal_name == "Foundit (Monster)":
|
| 161 |
+
if experience_id == "fresher":
|
| 162 |
+
return "&experienceRanges=0~0"
|
| 163 |
+
elif experience_id == "0-1":
|
| 164 |
+
return "&experienceRanges=0~1"
|
| 165 |
+
elif experience_id == "1-3":
|
| 166 |
+
return "&experienceRanges=1~3"
|
| 167 |
+
elif experience_id == "3-5":
|
| 168 |
+
return "&experienceRanges=3~5"
|
| 169 |
+
elif experience_id == "5-7":
|
| 170 |
+
return "&experienceRanges=5~7"
|
| 171 |
+
elif experience_id == "7-10":
|
| 172 |
+
return "&experienceRanges=7~10"
|
| 173 |
+
elif experience_id == "10+":
|
| 174 |
+
return "&experienceRanges=10~50"
|
| 175 |
+
|
| 176 |
+
elif portal_name == "Naukri":
|
| 177 |
+
if experience_id == "fresher":
|
| 178 |
+
return "0"
|
| 179 |
+
elif experience_id == "0-1":
|
| 180 |
+
return "0-1"
|
| 181 |
+
elif experience_id == "1-3":
|
| 182 |
+
return "1-3"
|
| 183 |
+
elif experience_id == "3-5":
|
| 184 |
+
return "3-5"
|
| 185 |
+
elif experience_id == "5-7":
|
| 186 |
+
return "5-7"
|
| 187 |
+
elif experience_id == "7-10":
|
| 188 |
+
return "7-10"
|
| 189 |
+
elif experience_id == "10+":
|
| 190 |
+
return "10-50"
|
| 191 |
+
|
| 192 |
+
elif portal_name == "LinkedIn":
|
| 193 |
+
if experience_id == "fresher" or experience_id == "0-1":
|
| 194 |
+
return "1" # Entry level
|
| 195 |
+
elif experience_id == "1-3" or experience_id == "3-5":
|
| 196 |
+
return "2" # Associate
|
| 197 |
+
elif experience_id == "5-7" or experience_id == "7-10":
|
| 198 |
+
return "3" # Mid-Senior level
|
| 199 |
+
elif experience_id == "10+":
|
| 200 |
+
return "4" # Director
|
| 201 |
+
|
| 202 |
+
elif portal_name == "Indeed":
|
| 203 |
+
if experience_id == "fresher" or experience_id == "0-1":
|
| 204 |
+
return "entry_level"
|
| 205 |
+
elif experience_id == "1-3" or experience_id == "3-5":
|
| 206 |
+
return "mid_level"
|
| 207 |
+
elif experience_id == "5-7" or experience_id == "7-10" or experience_id == "10+":
|
| 208 |
+
return "senior_level"
|
| 209 |
+
|
| 210 |
+
return ""
|
| 211 |
+
|
| 212 |
+
def search_jobs(self, job_title, location, experience=None):
|
| 213 |
+
"""Search jobs across multiple portals"""
|
| 214 |
+
if not experience:
|
| 215 |
+
experience = {"id": "all", "text": "All Levels"}
|
| 216 |
+
|
| 217 |
+
results = []
|
| 218 |
+
|
| 219 |
+
for portal in self.portals:
|
| 220 |
+
portal_name = portal["name"]
|
| 221 |
+
|
| 222 |
+
# Format job title based on portal
|
| 223 |
+
if portal_name == "Foundit (Monster)":
|
| 224 |
+
formatted_job = job_title.replace(' ', '+')
|
| 225 |
+
elif portal_name == "Naukri":
|
| 226 |
+
formatted_job = self.format_job_title(job_title)
|
| 227 |
+
elif portal_name == "Glassdoor":
|
| 228 |
+
# For Glassdoor, format job title with + signs
|
| 229 |
+
formatted_job = job_title.replace(' ', '+')
|
| 230 |
+
elif portal_name in ["LinkedIn", "Indeed", "TimesJobs"]:
|
| 231 |
+
formatted_job = job_title.replace(' ', '%20')
|
| 232 |
+
elif portal_name in ["FreshersWorld", "Instahyre"]:
|
| 233 |
+
formatted_job = job_title.lower().replace(' ', '-')
|
| 234 |
+
else:
|
| 235 |
+
formatted_job = job_title
|
| 236 |
+
|
| 237 |
+
# Format location based on portal
|
| 238 |
+
if portal_name == "Foundit (Monster)":
|
| 239 |
+
formatted_location = location.replace(' ', '+') if location else "India"
|
| 240 |
+
elif portal_name == "Naukri":
|
| 241 |
+
formatted_location = self.format_location(location) if location else "india"
|
| 242 |
+
elif portal_name == "Glassdoor":
|
| 243 |
+
# For Glassdoor, keep spaces in location name
|
| 244 |
+
formatted_location = location if location else "India"
|
| 245 |
+
elif portal_name in ["LinkedIn", "Indeed", "TimesJobs"]:
|
| 246 |
+
formatted_location = location.replace(' ', '%20') if location else "India"
|
| 247 |
+
elif portal_name in ["FreshersWorld", "Instahyre"]:
|
| 248 |
+
formatted_location = location.lower().replace(' ', '-') if location else "india"
|
| 249 |
+
else:
|
| 250 |
+
formatted_location = location if location else "India"
|
| 251 |
+
|
| 252 |
+
# Get experience parameter
|
| 253 |
+
exp_param = self.get_experience_param(portal_name, experience)
|
| 254 |
+
|
| 255 |
+
# Build URL based on portal
|
| 256 |
+
try:
|
| 257 |
+
if portal_name == "Foundit (Monster)":
|
| 258 |
+
url = portal["url"].format(formatted_job, formatted_location)
|
| 259 |
+
if exp_param:
|
| 260 |
+
url += exp_param
|
| 261 |
+
elif portal_name == "Naukri":
|
| 262 |
+
url = portal["url"].format(formatted_job, formatted_location, exp_param)
|
| 263 |
+
elif portal_name == "LinkedIn":
|
| 264 |
+
url = portal["url"].format(formatted_job, formatted_location, exp_param)
|
| 265 |
+
elif portal_name == "Indeed":
|
| 266 |
+
url = portal["url"].format(formatted_job, formatted_location, exp_param)
|
| 267 |
+
elif portal_name == "Glassdoor":
|
| 268 |
+
# For Glassdoor, location comes first, then job title (occ parameter)
|
| 269 |
+
url = portal["url"].format(formatted_location, formatted_job)
|
| 270 |
+
elif portal_name in ["TimesJobs"]:
|
| 271 |
+
url = portal["url"].format(formatted_job, formatted_location)
|
| 272 |
+
elif portal_name in ["FreshersWorld", "Instahyre"]:
|
| 273 |
+
url = portal["url"].format(formatted_job, formatted_location)
|
| 274 |
+
else:
|
| 275 |
+
url = portal["url"]
|
| 276 |
+
|
| 277 |
+
results.append({
|
| 278 |
+
"portal": portal_name,
|
| 279 |
+
"icon": portal["icon"],
|
| 280 |
+
"color": portal["color"],
|
| 281 |
+
"title": f"{job_title} jobs in {location if location else 'India'}",
|
| 282 |
+
"url": url
|
| 283 |
+
})
|
| 284 |
+
except Exception as e:
|
| 285 |
+
print(f"Error creating URL for {portal_name}: {str(e)}")
|
| 286 |
+
continue
|
| 287 |
+
|
| 288 |
+
return results
|
jobs/job_search.py
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from typing import List, Dict
|
| 3 |
+
from .job_portals import JobPortal
|
| 4 |
+
from .suggestions import (
|
| 5 |
+
JOB_SUGGESTIONS,
|
| 6 |
+
LOCATION_SUGGESTIONS,
|
| 7 |
+
EXPERIENCE_RANGES,
|
| 8 |
+
SALARY_RANGES,
|
| 9 |
+
JOB_TYPES,
|
| 10 |
+
get_cities_by_state,
|
| 11 |
+
get_all_states
|
| 12 |
+
)
|
| 13 |
+
from .companies import get_featured_companies, get_market_insights
|
| 14 |
+
from .linkedin_scraper import render_linkedin_scraper
|
| 15 |
+
from streamlit_extras.add_vertical_space import add_vertical_space
|
| 16 |
+
from streamlit_option_menu import option_menu
|
| 17 |
+
|
| 18 |
+
def filter_suggestions(query: str, suggestions: List[Dict]) -> List[Dict]:
|
| 19 |
+
"""Filter suggestions based on user input"""
|
| 20 |
+
if not query:
|
| 21 |
+
return []
|
| 22 |
+
return [
|
| 23 |
+
s for s in suggestions
|
| 24 |
+
if query.lower() in s["text"].lower()
|
| 25 |
+
][:5]
|
| 26 |
+
|
| 27 |
+
def filter_location_suggestions(query: str, suggestions: List[Dict]) -> List[Dict]:
|
| 28 |
+
"""Filter location suggestions based on user input with smart categorization"""
|
| 29 |
+
if not query or len(query) < 2:
|
| 30 |
+
return []
|
| 31 |
+
|
| 32 |
+
# First check if query matches any state
|
| 33 |
+
matching_states = [s for s in suggestions if s.get("type") == "state" and query.lower() in s["text"].lower()]
|
| 34 |
+
|
| 35 |
+
# Then check cities
|
| 36 |
+
matching_cities = [s for s in suggestions if s.get("type") == "city" and query.lower() in s["text"].lower()]
|
| 37 |
+
|
| 38 |
+
# Then check work modes
|
| 39 |
+
matching_work_modes = [s for s in suggestions if s.get("type") == "work_mode" and query.lower() in s["text"].lower()]
|
| 40 |
+
|
| 41 |
+
# Combine results with states first, then major cities, then other matches
|
| 42 |
+
results = matching_states + matching_cities + matching_work_modes
|
| 43 |
+
return results[:7] # Return top 7 matches
|
| 44 |
+
|
| 45 |
+
def get_filter_options():
|
| 46 |
+
"""Get filter options for job search"""
|
| 47 |
+
return {
|
| 48 |
+
"experience_levels": [
|
| 49 |
+
{"id": "all", "text": "All Levels"},
|
| 50 |
+
{"id": "fresher", "text": "Fresher"},
|
| 51 |
+
{"id": "0-1", "text": "0-1 years"},
|
| 52 |
+
{"id": "1-3", "text": "1-3 years"},
|
| 53 |
+
{"id": "3-5", "text": "3-5 years"},
|
| 54 |
+
{"id": "5-7", "text": "5-7 years"},
|
| 55 |
+
{"id": "7-10", "text": "7-10 years"},
|
| 56 |
+
{"id": "10+", "text": "10+ years"}
|
| 57 |
+
],
|
| 58 |
+
"salary_ranges": [
|
| 59 |
+
{"id": "all", "text": "All Ranges"},
|
| 60 |
+
{"id": "0-3", "text": "0-3 LPA"},
|
| 61 |
+
{"id": "3-6", "text": "3-6 LPA"},
|
| 62 |
+
{"id": "6-10", "text": "6-10 LPA"},
|
| 63 |
+
{"id": "10-15", "text": "10-15 LPA"},
|
| 64 |
+
{"id": "15+", "text": "15+ LPA"}
|
| 65 |
+
],
|
| 66 |
+
"job_types": [
|
| 67 |
+
{"id": "all", "text": "All Types"},
|
| 68 |
+
{"id": "full-time", "text": "Full Time"},
|
| 69 |
+
{"id": "part-time", "text": "Part Time"},
|
| 70 |
+
{"id": "contract", "text": "Contract"},
|
| 71 |
+
{"id": "remote", "text": "Remote"}
|
| 72 |
+
]
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
def render_company_section():
|
| 76 |
+
"""Render the featured companies section"""
|
| 77 |
+
st.markdown("""
|
| 78 |
+
<style>
|
| 79 |
+
.company-grid {
|
| 80 |
+
display: grid;
|
| 81 |
+
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
| 82 |
+
gap: 1rem;
|
| 83 |
+
padding: 1rem 0;
|
| 84 |
+
}
|
| 85 |
+
.company-card {
|
| 86 |
+
background: rgba(255, 255, 255, 0.05);
|
| 87 |
+
border-radius: 10px;
|
| 88 |
+
padding: 1rem;
|
| 89 |
+
transition: transform 0.2s;
|
| 90 |
+
cursor: pointer;
|
| 91 |
+
}
|
| 92 |
+
.company-card:hover {
|
| 93 |
+
transform: translateY(-5px);
|
| 94 |
+
background: rgba(255, 255, 255, 0.08);
|
| 95 |
+
}
|
| 96 |
+
.company-header {
|
| 97 |
+
display: flex;
|
| 98 |
+
align-items: center;
|
| 99 |
+
margin-bottom: 0.5rem;
|
| 100 |
+
}
|
| 101 |
+
.company-icon {
|
| 102 |
+
font-size: 1.5rem;
|
| 103 |
+
margin-right: 0.5rem;
|
| 104 |
+
}
|
| 105 |
+
.company-categories {
|
| 106 |
+
display: flex;
|
| 107 |
+
flex-wrap: wrap;
|
| 108 |
+
gap: 0.5rem;
|
| 109 |
+
margin-top: 0.5rem;
|
| 110 |
+
}
|
| 111 |
+
.company-category {
|
| 112 |
+
background: rgba(255, 255, 255, 0.1);
|
| 113 |
+
padding: 0.2rem 0.5rem;
|
| 114 |
+
border-radius: 15px;
|
| 115 |
+
font-size: 0.8rem;
|
| 116 |
+
}
|
| 117 |
+
</style>
|
| 118 |
+
""", unsafe_allow_html=True)
|
| 119 |
+
|
| 120 |
+
# Featured Companies
|
| 121 |
+
st.markdown("### 🏢 Featured Companies")
|
| 122 |
+
|
| 123 |
+
tabs = st.tabs(["All Companies", "Tech Giants", "Indian Tech", "Global Corps"])
|
| 124 |
+
|
| 125 |
+
categories = [None, "tech", "indian_tech", "global_corps"]
|
| 126 |
+
for tab, category in zip(tabs, categories):
|
| 127 |
+
with tab:
|
| 128 |
+
companies = get_featured_companies(category)
|
| 129 |
+
st.markdown('<div class="company-grid">', unsafe_allow_html=True)
|
| 130 |
+
|
| 131 |
+
for company in companies:
|
| 132 |
+
st.markdown(f"""
|
| 133 |
+
<a href="{company['careers_url']}" target="_blank" style="text-decoration: none; color: inherit;">
|
| 134 |
+
<div class="company-card">
|
| 135 |
+
<div class="company-header">
|
| 136 |
+
<i class="{company['icon']} company-icon" style="color: {company['color']}"></i>
|
| 137 |
+
<h3 style="margin: 0;">{company['name']}</h3>
|
| 138 |
+
</div>
|
| 139 |
+
<p style="margin: 0.5rem 0; color: #888;">{company['description']}</p>
|
| 140 |
+
<div class="company-categories">
|
| 141 |
+
{' '.join(f'<span class="company-category">{cat}</span>' for cat in company['categories'])}
|
| 142 |
+
</div>
|
| 143 |
+
</div>
|
| 144 |
+
</a>
|
| 145 |
+
""", unsafe_allow_html=True)
|
| 146 |
+
|
| 147 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 148 |
+
|
| 149 |
+
def render_market_insights():
|
| 150 |
+
"""Render job market insights section"""
|
| 151 |
+
insights = get_market_insights()
|
| 152 |
+
|
| 153 |
+
st.markdown("""
|
| 154 |
+
<style>
|
| 155 |
+
.insights-grid {
|
| 156 |
+
display: grid;
|
| 157 |
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
| 158 |
+
gap: 1rem;
|
| 159 |
+
padding: 1rem 0;
|
| 160 |
+
}
|
| 161 |
+
.insight-card {
|
| 162 |
+
background: rgba(255, 255, 255, 0.05);
|
| 163 |
+
border-radius: 10px;
|
| 164 |
+
padding: 1rem;
|
| 165 |
+
text-align: center;
|
| 166 |
+
transition: transform 0.3s ease, background 0.3s ease;
|
| 167 |
+
}
|
| 168 |
+
.insight-card:hover {
|
| 169 |
+
transform: translateY(-5px);
|
| 170 |
+
background: rgba(255, 255, 255, 0.08);
|
| 171 |
+
}
|
| 172 |
+
.insight-icon {
|
| 173 |
+
font-size: 2rem;
|
| 174 |
+
margin-bottom: 0.5rem;
|
| 175 |
+
color: #00bfa5;
|
| 176 |
+
}
|
| 177 |
+
.growth-text {
|
| 178 |
+
color: #00c853;
|
| 179 |
+
font-weight: bold;
|
| 180 |
+
}
|
| 181 |
+
.salary-card {
|
| 182 |
+
background: rgba(255, 255, 255, 0.05);
|
| 183 |
+
border-radius: 15px;
|
| 184 |
+
padding: 1.5rem;
|
| 185 |
+
margin-bottom: 1rem;
|
| 186 |
+
transition: all 0.3s ease;
|
| 187 |
+
border-left: 4px solid #00bfa5;
|
| 188 |
+
}
|
| 189 |
+
.salary-card:hover {
|
| 190 |
+
transform: translateX(10px);
|
| 191 |
+
background: rgba(255, 255, 255, 0.08);
|
| 192 |
+
}
|
| 193 |
+
.salary-header {
|
| 194 |
+
display: flex;
|
| 195 |
+
align-items: center;
|
| 196 |
+
margin-bottom: 1rem;
|
| 197 |
+
}
|
| 198 |
+
.role-icon {
|
| 199 |
+
font-size: 1.5rem;
|
| 200 |
+
margin-right: 1rem;
|
| 201 |
+
color: #00bfa5;
|
| 202 |
+
}
|
| 203 |
+
.salary-details {
|
| 204 |
+
display: flex;
|
| 205 |
+
justify-content: space-between;
|
| 206 |
+
align-items: center;
|
| 207 |
+
margin-top: 0.5rem;
|
| 208 |
+
}
|
| 209 |
+
.salary-tag {
|
| 210 |
+
background: rgba(0, 191, 165, 0.1);
|
| 211 |
+
color: #00bfa5;
|
| 212 |
+
padding: 0.3rem 0.8rem;
|
| 213 |
+
border-radius: 20px;
|
| 214 |
+
font-size: 0.9rem;
|
| 215 |
+
}
|
| 216 |
+
.experience-tag {
|
| 217 |
+
background: rgba(255, 255, 255, 0.1);
|
| 218 |
+
padding: 0.3rem 0.8rem;
|
| 219 |
+
border-radius: 20px;
|
| 220 |
+
font-size: 0.9rem;
|
| 221 |
+
}
|
| 222 |
+
.role-title {
|
| 223 |
+
font-size: 1.2rem;
|
| 224 |
+
font-weight: bold;
|
| 225 |
+
margin: 0;
|
| 226 |
+
}
|
| 227 |
+
.salary-range {
|
| 228 |
+
font-size: 1.1rem;
|
| 229 |
+
color: #00bfa5;
|
| 230 |
+
font-weight: bold;
|
| 231 |
+
}
|
| 232 |
+
.role-icons {
|
| 233 |
+
font-family: "Font Awesome 5 Free";
|
| 234 |
+
}
|
| 235 |
+
</style>
|
| 236 |
+
""", unsafe_allow_html=True)
|
| 237 |
+
|
| 238 |
+
st.markdown("### 📊 Job Market Insights")
|
| 239 |
+
|
| 240 |
+
tabs = st.tabs(["Trending Skills", "Top Locations", "Salary Insights"])
|
| 241 |
+
|
| 242 |
+
with tabs[0]:
|
| 243 |
+
st.markdown('<div class="insights-grid">', unsafe_allow_html=True)
|
| 244 |
+
for skill in insights["trending_skills"]:
|
| 245 |
+
st.markdown(f"""
|
| 246 |
+
<div class="insight-card">
|
| 247 |
+
<i class="{skill['icon']} insight-icon"></i>
|
| 248 |
+
<h4>{skill['name']}</h4>
|
| 249 |
+
<p class="growth-text">Growth: {skill['growth']}</p>
|
| 250 |
+
</div>
|
| 251 |
+
""", unsafe_allow_html=True)
|
| 252 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 253 |
+
|
| 254 |
+
with tabs[1]:
|
| 255 |
+
st.markdown('<div class="insights-grid">', unsafe_allow_html=True)
|
| 256 |
+
for location in insights["top_locations"]:
|
| 257 |
+
st.markdown(f"""
|
| 258 |
+
<div class="insight-card">
|
| 259 |
+
<i class="{location['icon']} insight-icon"></i>
|
| 260 |
+
<h4>{location['name']}</h4>
|
| 261 |
+
<p>Available Jobs: {location['jobs']}</p>
|
| 262 |
+
</div>
|
| 263 |
+
""", unsafe_allow_html=True)
|
| 264 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 265 |
+
|
| 266 |
+
with tabs[2]:
|
| 267 |
+
# Role-specific icons
|
| 268 |
+
role_icons = {
|
| 269 |
+
"Software Engineer": "fas fa-code",
|
| 270 |
+
"Data Scientist": "fas fa-brain",
|
| 271 |
+
"Product Manager": "fas fa-tasks",
|
| 272 |
+
"DevOps Engineer": "fas fa-server",
|
| 273 |
+
"UI/UX Designer": "fas fa-paint-brush"
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
for insight in insights["salary_insights"]:
|
| 277 |
+
role = insight['role']
|
| 278 |
+
icon = role_icons.get(role, "fas fa-briefcase")
|
| 279 |
+
|
| 280 |
+
st.markdown(f"""
|
| 281 |
+
<div class="salary-card">
|
| 282 |
+
<div class="salary-header">
|
| 283 |
+
<i class="{icon} role-icon"></i>
|
| 284 |
+
<div>
|
| 285 |
+
<h3 class="role-title">{role}</h3>
|
| 286 |
+
<div class="salary-details">
|
| 287 |
+
<span class="salary-tag">₹ {insight['range']}</span>
|
| 288 |
+
<span class="experience-tag">
|
| 289 |
+
<i class="fas fa-history"></i> {insight['experience']}
|
| 290 |
+
</span>
|
| 291 |
+
</div>
|
| 292 |
+
</div>
|
| 293 |
+
</div>
|
| 294 |
+
</div>
|
| 295 |
+
""", unsafe_allow_html=True)
|
| 296 |
+
|
| 297 |
+
def render_job_search():
|
| 298 |
+
"""Render job search page with enhanced features"""
|
| 299 |
+
st.title("🔍 Smart Job Search")
|
| 300 |
+
st.markdown("Find Your Dream Job Across Multiple Platforms")
|
| 301 |
+
|
| 302 |
+
# Market Insights Section (Above Search)
|
| 303 |
+
render_market_insights()
|
| 304 |
+
|
| 305 |
+
# Job Search Section
|
| 306 |
+
with st.container():
|
| 307 |
+
st.markdown("""
|
| 308 |
+
<style>
|
| 309 |
+
.search-container {
|
| 310 |
+
background: rgba(255, 255, 255, 0.05);
|
| 311 |
+
border-radius: 10px;
|
| 312 |
+
padding: 20px;
|
| 313 |
+
margin-bottom: 20px;
|
| 314 |
+
}
|
| 315 |
+
.search-title {
|
| 316 |
+
color: #00bfa5;
|
| 317 |
+
font-weight: bold;
|
| 318 |
+
margin-bottom: 5px;
|
| 319 |
+
}
|
| 320 |
+
.search-description {
|
| 321 |
+
color: #888;
|
| 322 |
+
font-size: 0.9rem;
|
| 323 |
+
margin-bottom: 20px;
|
| 324 |
+
}
|
| 325 |
+
</style>
|
| 326 |
+
""", unsafe_allow_html=True)
|
| 327 |
+
|
| 328 |
+
st.markdown('<div class="search-container">', unsafe_allow_html=True)
|
| 329 |
+
|
| 330 |
+
# Create tabs with icons
|
| 331 |
+
tabs = option_menu(
|
| 332 |
+
menu_title=None,
|
| 333 |
+
options=["Job Portal", "LinkedIn"],
|
| 334 |
+
icons=["search", "linkedin"],
|
| 335 |
+
menu_icon="cast",
|
| 336 |
+
default_index=0,
|
| 337 |
+
orientation="horizontal",
|
| 338 |
+
styles={
|
| 339 |
+
"container": {"padding": "0px", "margin-bottom": "20px"},
|
| 340 |
+
"icon": {"font-size": "18px"},
|
| 341 |
+
"nav-link": {"font-size": "16px", "text-align": "center", "padding": "10px", "border-radius": "5px"},
|
| 342 |
+
"nav-link-selected": {"background-color": "#00bfa5", "font-weight": "bold"},
|
| 343 |
+
}
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
+
# Display content based on selected tab
|
| 347 |
+
if tabs == "Job Portal":
|
| 348 |
+
st.markdown('<h3 class="search-title"><i class="fas fa-search-dollar" style="color: #00bfa5;"></i> Search Jobs Across Multiple Platforms</h3>', unsafe_allow_html=True)
|
| 349 |
+
st.markdown('<p class="search-description">Find job opportunities from top job portals like LinkedIn, Indeed, Naukri, and Foundit</p>', unsafe_allow_html=True)
|
| 350 |
+
|
| 351 |
+
# Search inputs
|
| 352 |
+
col1, col2 = st.columns([2, 1])
|
| 353 |
+
|
| 354 |
+
with col1:
|
| 355 |
+
job_query = st.text_input("Job Title / Skills",
|
| 356 |
+
value="",
|
| 357 |
+
placeholder="e.g. Software Engineer, Data Scientist")
|
| 358 |
+
|
| 359 |
+
if job_query and len(job_query) >= 2:
|
| 360 |
+
filtered_jobs = [s["text"] for s in JOB_SUGGESTIONS if job_query.lower() in s["text"].lower()]
|
| 361 |
+
if filtered_jobs:
|
| 362 |
+
job_query = st.selectbox("Select Job Title", filtered_jobs)
|
| 363 |
+
|
| 364 |
+
with col2:
|
| 365 |
+
location = st.text_input("Location",
|
| 366 |
+
value="",
|
| 367 |
+
placeholder="e.g. Bangalore, Karnataka")
|
| 368 |
+
|
| 369 |
+
if location and len(location) >= 2:
|
| 370 |
+
# Use enhanced location filtering
|
| 371 |
+
filtered_locations = filter_location_suggestions(location, LOCATION_SUGGESTIONS)
|
| 372 |
+
|
| 373 |
+
if filtered_locations:
|
| 374 |
+
# Format the display text to show location type
|
| 375 |
+
location_options = []
|
| 376 |
+
location_display = {}
|
| 377 |
+
|
| 378 |
+
for loc in filtered_locations:
|
| 379 |
+
display_text = loc["text"]
|
| 380 |
+
if loc.get("type") == "state":
|
| 381 |
+
display_text = f"{loc['text']} (State)"
|
| 382 |
+
elif loc.get("type") == "city":
|
| 383 |
+
display_text = f"{loc['text']}, {loc.get('state', '')}"
|
| 384 |
+
elif loc.get("type") == "work_mode":
|
| 385 |
+
display_text = f"{loc['text']} (Work Mode)"
|
| 386 |
+
|
| 387 |
+
location_options.append(loc["text"])
|
| 388 |
+
location_display[loc["text"]] = display_text
|
| 389 |
+
|
| 390 |
+
# Create a selectbox with formatted display
|
| 391 |
+
selected_location = st.selectbox(
|
| 392 |
+
"Select Location",
|
| 393 |
+
options=location_options,
|
| 394 |
+
format_func=lambda x: location_display.get(x, x)
|
| 395 |
+
)
|
| 396 |
+
|
| 397 |
+
location = selected_location
|
| 398 |
+
|
| 399 |
+
# If a state is selected, show cities in that state
|
| 400 |
+
selected_loc_type = next((loc.get("type") for loc in filtered_locations if loc["text"] == selected_location), None)
|
| 401 |
+
|
| 402 |
+
if selected_loc_type == "state":
|
| 403 |
+
st.markdown(f"**Cities in {selected_location}:**")
|
| 404 |
+
cities = get_cities_by_state(selected_location)
|
| 405 |
+
|
| 406 |
+
# Display cities as clickable buttons
|
| 407 |
+
city_cols = st.columns(3)
|
| 408 |
+
for i, city in enumerate(cities):
|
| 409 |
+
with city_cols[i % 3]:
|
| 410 |
+
if st.button(f"{city['icon']} {city['text']}", key=f"city_{i}"):
|
| 411 |
+
location = city['text']
|
| 412 |
+
|
| 413 |
+
# Advanced Filters
|
| 414 |
+
with st.expander("🎯 Advanced Filters"):
|
| 415 |
+
st.markdown('<div class="filter-section">', unsafe_allow_html=True)
|
| 416 |
+
filter_cols = st.columns(3)
|
| 417 |
+
|
| 418 |
+
with filter_cols[0]:
|
| 419 |
+
experience = st.selectbox("Experience Level",
|
| 420 |
+
options=get_filter_options()["experience_levels"],
|
| 421 |
+
format_func=lambda x: x["text"])
|
| 422 |
+
|
| 423 |
+
with filter_cols[1]:
|
| 424 |
+
salary_range = st.selectbox("Salary Range",
|
| 425 |
+
options=get_filter_options()["salary_ranges"],
|
| 426 |
+
format_func=lambda x: x["text"])
|
| 427 |
+
|
| 428 |
+
with filter_cols[2]:
|
| 429 |
+
job_type = st.selectbox("Job Type",
|
| 430 |
+
options=get_filter_options()["job_types"],
|
| 431 |
+
format_func=lambda x: x["text"])
|
| 432 |
+
|
| 433 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 434 |
+
|
| 435 |
+
# Search button
|
| 436 |
+
if st.button("SEARCH JOBS", type="primary", use_container_width=True):
|
| 437 |
+
if job_query:
|
| 438 |
+
job_portal = JobPortal()
|
| 439 |
+
results = job_portal.search_jobs(job_query, location, experience)
|
| 440 |
+
|
| 441 |
+
if results:
|
| 442 |
+
st.markdown("""
|
| 443 |
+
<style>
|
| 444 |
+
.result-card {
|
| 445 |
+
background: rgba(255, 255, 255, 0.05);
|
| 446 |
+
border-radius: 10px;
|
| 447 |
+
padding: 15px;
|
| 448 |
+
margin-bottom: 10px;
|
| 449 |
+
border-left: 4px solid #00bfa5;
|
| 450 |
+
transition: transform 0.2s;
|
| 451 |
+
}
|
| 452 |
+
.result-card:hover {
|
| 453 |
+
transform: translateX(5px);
|
| 454 |
+
background: rgba(255, 255, 255, 0.08);
|
| 455 |
+
}
|
| 456 |
+
.portal-name {
|
| 457 |
+
color: #00bfa5;
|
| 458 |
+
font-weight: bold;
|
| 459 |
+
font-size: 1.2rem;
|
| 460 |
+
}
|
| 461 |
+
.portal-link {
|
| 462 |
+
display: inline-block;
|
| 463 |
+
background: #00bfa5;
|
| 464 |
+
color: white !important;
|
| 465 |
+
padding: 5px 15px;
|
| 466 |
+
border-radius: 5px;
|
| 467 |
+
text-decoration: none;
|
| 468 |
+
margin-top: 10px;
|
| 469 |
+
font-weight: bold;
|
| 470 |
+
}
|
| 471 |
+
.portal-link:hover {
|
| 472 |
+
background: #00a589;
|
| 473 |
+
}
|
| 474 |
+
</style>
|
| 475 |
+
""", unsafe_allow_html=True)
|
| 476 |
+
|
| 477 |
+
st.markdown("### 🎯 Job Search Results")
|
| 478 |
+
for result in results:
|
| 479 |
+
with st.container():
|
| 480 |
+
st.markdown(f"""
|
| 481 |
+
<div class="result-card">
|
| 482 |
+
<div class="portal-name">
|
| 483 |
+
<i class="{result["icon"]}" style="color: {result["color"]}"></i>
|
| 484 |
+
{result["portal"]}
|
| 485 |
+
</div>
|
| 486 |
+
<p>{result["title"]}</p>
|
| 487 |
+
<a href="{result["url"]}" target="_blank" class="portal-link">
|
| 488 |
+
View Jobs on {result["portal"]} →
|
| 489 |
+
</a>
|
| 490 |
+
</div>
|
| 491 |
+
""", unsafe_allow_html=True)
|
| 492 |
+
else:
|
| 493 |
+
st.warning("No results found. Try different search terms or filters.")
|
| 494 |
+
else:
|
| 495 |
+
st.warning("Please enter a job title or skills to search.")
|
| 496 |
+
|
| 497 |
+
else:
|
| 498 |
+
# LinkedIn Job Scraper - only show the title once
|
| 499 |
+
st.markdown('<h3 class="search-title"><i class="fab fa-linkedin" style="color: #0A66C2;"></i> LinkedIn Job Scraper</h3>', unsafe_allow_html=True)
|
| 500 |
+
st.markdown('<p class="search-description">Find real-time job listings directly from LinkedIn</p>', unsafe_allow_html=True)
|
| 501 |
+
|
| 502 |
+
# Render LinkedIn scraper without showing the title again
|
| 503 |
+
render_linkedin_scraper()
|
| 504 |
+
|
| 505 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 506 |
+
|
| 507 |
+
# Featured Companies Section (Below Search)
|
| 508 |
+
render_company_section()
|
| 509 |
+
|
| 510 |
+
# Removed render_job_search() call to prevent automatic rendering
|
jobs/linkedin_scraper.py
ADDED
|
@@ -0,0 +1,662 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
import numpy as np
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import streamlit as st
|
| 5 |
+
from streamlit_extras.add_vertical_space import add_vertical_space
|
| 6 |
+
from selenium import webdriver
|
| 7 |
+
from selenium.webdriver.common.by import By
|
| 8 |
+
from selenium.webdriver.common.keys import Keys
|
| 9 |
+
from selenium.common.exceptions import NoSuchElementException
|
| 10 |
+
import warnings
|
| 11 |
+
warnings.filterwarnings('ignore')
|
| 12 |
+
|
| 13 |
+
# Import our custom webdriver utility
|
| 14 |
+
from .webdriver_utils import setup_webdriver
|
| 15 |
+
|
| 16 |
+
class LinkedInScraper:
|
| 17 |
+
"""Class for scraping job listings from LinkedIn"""
|
| 18 |
+
|
| 19 |
+
@staticmethod
|
| 20 |
+
def webdriver_setup():
|
| 21 |
+
"""Set up and configure the Chrome webdriver"""
|
| 22 |
+
# Use our custom webdriver setup utility with multiple fallback options
|
| 23 |
+
return setup_webdriver()
|
| 24 |
+
|
| 25 |
+
@staticmethod
|
| 26 |
+
def get_user_input(show_title=True):
|
| 27 |
+
"""Get user input for job search parameters"""
|
| 28 |
+
add_vertical_space(1)
|
| 29 |
+
|
| 30 |
+
# Apply custom styling for the form
|
| 31 |
+
if show_title:
|
| 32 |
+
st.markdown("""
|
| 33 |
+
<style>
|
| 34 |
+
.linkedin-form {
|
| 35 |
+
background: rgba(10, 102, 194, 0.05);
|
| 36 |
+
border-radius: 10px;
|
| 37 |
+
padding: 20px;
|
| 38 |
+
border-left: 4px solid #0A66C2;
|
| 39 |
+
margin-bottom: 20px;
|
| 40 |
+
}
|
| 41 |
+
.linkedin-title {
|
| 42 |
+
color: #0A66C2;
|
| 43 |
+
font-weight: bold;
|
| 44 |
+
}
|
| 45 |
+
.linkedin-subtitle {
|
| 46 |
+
color: #666;
|
| 47 |
+
font-size: 0.9rem;
|
| 48 |
+
margin-bottom: 15px;
|
| 49 |
+
}
|
| 50 |
+
</style>
|
| 51 |
+
""", unsafe_allow_html=True)
|
| 52 |
+
|
| 53 |
+
st.markdown('<div class="linkedin-form">', unsafe_allow_html=True)
|
| 54 |
+
st.markdown('<h3 class="linkedin-title"><i class="fab fa-linkedin"></i> LinkedIn Job Scraper</h3>', unsafe_allow_html=True)
|
| 55 |
+
st.markdown('<p class="linkedin-subtitle">Find real-time job listings directly from LinkedIn</p>', unsafe_allow_html=True)
|
| 56 |
+
|
| 57 |
+
with st.form(key='linkedin_scrape'):
|
| 58 |
+
col1, col2, col3 = st.columns([0.5, 0.3, 0.2], gap='medium')
|
| 59 |
+
|
| 60 |
+
with col1:
|
| 61 |
+
job_title_input = st.text_input(
|
| 62 |
+
label='Job Title',
|
| 63 |
+
placeholder='e.g. Data Scientist, Software Engineer',
|
| 64 |
+
help="Enter job titles separated by commas"
|
| 65 |
+
)
|
| 66 |
+
job_title_input = job_title_input.split(',')
|
| 67 |
+
|
| 68 |
+
with col2:
|
| 69 |
+
job_location = st.text_input(
|
| 70 |
+
label='Job Location',
|
| 71 |
+
value='India',
|
| 72 |
+
placeholder='e.g. Bangalore, Mumbai, Remote',
|
| 73 |
+
help="Enter a location or 'India' for nationwide search"
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
with col3:
|
| 77 |
+
job_count = st.number_input(
|
| 78 |
+
label='Number of Jobs',
|
| 79 |
+
min_value=1,
|
| 80 |
+
max_value=10,
|
| 81 |
+
value=3,
|
| 82 |
+
step=1,
|
| 83 |
+
help="Number of job listings to scrape (max 10)"
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
# Submit Button
|
| 87 |
+
add_vertical_space(1)
|
| 88 |
+
submit = st.form_submit_button(
|
| 89 |
+
label='Search LinkedIn Jobs',
|
| 90 |
+
type='primary',
|
| 91 |
+
use_container_width=True
|
| 92 |
+
)
|
| 93 |
+
add_vertical_space(1)
|
| 94 |
+
|
| 95 |
+
if show_title:
|
| 96 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 97 |
+
|
| 98 |
+
return job_title_input, job_location, job_count, submit
|
| 99 |
+
|
| 100 |
+
@staticmethod
|
| 101 |
+
def build_url(job_title, job_location):
|
| 102 |
+
"""Build LinkedIn search URL from job title and location"""
|
| 103 |
+
# Format job titles
|
| 104 |
+
formatted_titles = []
|
| 105 |
+
for title in job_title:
|
| 106 |
+
if title.strip(): # Skip empty titles
|
| 107 |
+
words = title.strip().split()
|
| 108 |
+
formatted_title = '%20'.join(words)
|
| 109 |
+
formatted_titles.append(formatted_title)
|
| 110 |
+
|
| 111 |
+
# If no valid titles, use a default
|
| 112 |
+
if not formatted_titles:
|
| 113 |
+
formatted_titles = ["jobs"]
|
| 114 |
+
|
| 115 |
+
# Join multiple job titles
|
| 116 |
+
job_title_param = '%2C%20'.join(formatted_titles)
|
| 117 |
+
|
| 118 |
+
# Format location
|
| 119 |
+
location_param = job_location.replace(' ', '%20')
|
| 120 |
+
|
| 121 |
+
# Build the LinkedIn search URL
|
| 122 |
+
link = f"https://in.linkedin.com/jobs/search?keywords={job_title_param}&location={location_param}&geoId=102713980&f_TPR=r604800&position=1&pageNum=0"
|
| 123 |
+
|
| 124 |
+
return link
|
| 125 |
+
|
| 126 |
+
@staticmethod
|
| 127 |
+
def open_link(driver, link):
|
| 128 |
+
"""Open LinkedIn link and wait for page to load"""
|
| 129 |
+
max_attempts = 3
|
| 130 |
+
attempts = 0
|
| 131 |
+
|
| 132 |
+
while attempts < max_attempts:
|
| 133 |
+
try:
|
| 134 |
+
driver.get(link)
|
| 135 |
+
driver.implicitly_wait(5)
|
| 136 |
+
time.sleep(3)
|
| 137 |
+
|
| 138 |
+
# Check if page loaded correctly
|
| 139 |
+
if "LinkedIn" in driver.title:
|
| 140 |
+
return True
|
| 141 |
+
|
| 142 |
+
# Alternative check for elements
|
| 143 |
+
try:
|
| 144 |
+
driver.find_element(by=By.CSS_SELECTOR, value='.jobs-search-results')
|
| 145 |
+
return True
|
| 146 |
+
except:
|
| 147 |
+
pass
|
| 148 |
+
|
| 149 |
+
try:
|
| 150 |
+
driver.find_element(by=By.CSS_SELECTOR, value='.jobs-search-results-list')
|
| 151 |
+
return True
|
| 152 |
+
except:
|
| 153 |
+
pass
|
| 154 |
+
|
| 155 |
+
# One more attempt with a different selector
|
| 156 |
+
try:
|
| 157 |
+
driver.find_element(by=By.CSS_SELECTOR, value='.base-search-card')
|
| 158 |
+
return True
|
| 159 |
+
except:
|
| 160 |
+
pass
|
| 161 |
+
|
| 162 |
+
attempts += 1
|
| 163 |
+
if attempts >= max_attempts:
|
| 164 |
+
st.warning("Could not load LinkedIn jobs page. Please try again.")
|
| 165 |
+
return False
|
| 166 |
+
|
| 167 |
+
time.sleep(2)
|
| 168 |
+
|
| 169 |
+
except Exception as e:
|
| 170 |
+
attempts += 1
|
| 171 |
+
if attempts >= max_attempts:
|
| 172 |
+
st.warning(f"Error loading LinkedIn page: {str(e)}")
|
| 173 |
+
return False
|
| 174 |
+
time.sleep(2)
|
| 175 |
+
|
| 176 |
+
return False
|
| 177 |
+
|
| 178 |
+
@staticmethod
|
| 179 |
+
def link_open_scrolldown(driver, link, job_count):
|
| 180 |
+
"""Open LinkedIn link and scroll down to load more jobs"""
|
| 181 |
+
# Open the link
|
| 182 |
+
if not LinkedInScraper.open_link(driver, link):
|
| 183 |
+
return False
|
| 184 |
+
|
| 185 |
+
# Scroll down to load more jobs
|
| 186 |
+
scroll_attempts = min(job_count + 5, 15) # Add extra scrolls to ensure we get enough jobs
|
| 187 |
+
|
| 188 |
+
for i in range(scroll_attempts):
|
| 189 |
+
try:
|
| 190 |
+
# Handle sign-in modal if it appears
|
| 191 |
+
try:
|
| 192 |
+
dismiss_buttons = driver.find_elements(
|
| 193 |
+
by=By.CSS_SELECTOR,
|
| 194 |
+
value="button[data-tracking-control-name='public_jobs_contextual-sign-in-modal_modal_dismiss']"
|
| 195 |
+
)
|
| 196 |
+
if dismiss_buttons:
|
| 197 |
+
dismiss_buttons[0].click()
|
| 198 |
+
except:
|
| 199 |
+
pass
|
| 200 |
+
|
| 201 |
+
# Scroll down to load more content
|
| 202 |
+
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
|
| 203 |
+
time.sleep(1.5)
|
| 204 |
+
|
| 205 |
+
# Try to click "See more jobs" button if present
|
| 206 |
+
try:
|
| 207 |
+
see_more_buttons = driver.find_elements(
|
| 208 |
+
by=By.CSS_SELECTOR,
|
| 209 |
+
value="button[aria-label='See more jobs']"
|
| 210 |
+
)
|
| 211 |
+
if see_more_buttons:
|
| 212 |
+
see_more_buttons[0].click()
|
| 213 |
+
time.sleep(2)
|
| 214 |
+
except:
|
| 215 |
+
pass
|
| 216 |
+
|
| 217 |
+
except Exception as e:
|
| 218 |
+
continue
|
| 219 |
+
|
| 220 |
+
return True
|
| 221 |
+
|
| 222 |
+
@staticmethod
|
| 223 |
+
def job_title_filter(scrap_job_title, user_job_title_input):
|
| 224 |
+
"""Filter job titles based on user input"""
|
| 225 |
+
# Skip filtering if job title input is empty or contains only empty strings
|
| 226 |
+
if not user_job_title_input or all(not title.strip() for title in user_job_title_input):
|
| 227 |
+
return scrap_job_title
|
| 228 |
+
|
| 229 |
+
# User job titles converted to lowercase
|
| 230 |
+
user_input = [title.lower().strip() for title in user_job_title_input if title.strip()]
|
| 231 |
+
|
| 232 |
+
# If no valid user input after cleaning, return the original title
|
| 233 |
+
if not user_input:
|
| 234 |
+
return scrap_job_title
|
| 235 |
+
|
| 236 |
+
# Scraped job title converted to lowercase
|
| 237 |
+
scrap_title = scrap_job_title.lower().strip()
|
| 238 |
+
|
| 239 |
+
# Check if any user job title matches the scraped job title
|
| 240 |
+
for user_title in user_input:
|
| 241 |
+
# Check if all words in user title are in scraped title
|
| 242 |
+
if all(word in scrap_title for word in user_title.split()):
|
| 243 |
+
return scrap_job_title
|
| 244 |
+
|
| 245 |
+
# No match found
|
| 246 |
+
return np.nan
|
| 247 |
+
|
| 248 |
+
@staticmethod
|
| 249 |
+
def scrap_company_data(driver, job_title_input, job_location):
|
| 250 |
+
"""Scrape company data from LinkedIn job listings"""
|
| 251 |
+
try:
|
| 252 |
+
# Scrape company names
|
| 253 |
+
company_elements = driver.find_elements(
|
| 254 |
+
by=By.CSS_SELECTOR,
|
| 255 |
+
value='h4.base-search-card__subtitle'
|
| 256 |
+
)
|
| 257 |
+
company_names = [element.text for element in company_elements if element.text.strip()]
|
| 258 |
+
|
| 259 |
+
# Scrape job locations
|
| 260 |
+
location_elements = driver.find_elements(
|
| 261 |
+
by=By.CSS_SELECTOR,
|
| 262 |
+
value='span.job-search-card__location'
|
| 263 |
+
)
|
| 264 |
+
company_locations = [element.text for element in location_elements if element.text.strip()]
|
| 265 |
+
|
| 266 |
+
# Scrape job titles
|
| 267 |
+
title_elements = driver.find_elements(
|
| 268 |
+
by=By.CSS_SELECTOR,
|
| 269 |
+
value='h3.base-search-card__title'
|
| 270 |
+
)
|
| 271 |
+
job_titles = [element.text for element in title_elements if element.text.strip()]
|
| 272 |
+
|
| 273 |
+
# Scrape job URLs
|
| 274 |
+
url_elements = driver.find_elements(
|
| 275 |
+
by=By.XPATH,
|
| 276 |
+
value='//a[contains(@href, "/jobs/view/")]'
|
| 277 |
+
)
|
| 278 |
+
job_urls = [element.get_attribute('href') for element in url_elements if element.get_attribute('href')]
|
| 279 |
+
|
| 280 |
+
# Check if we have any data
|
| 281 |
+
if not company_names or not job_titles or not company_locations or not job_urls:
|
| 282 |
+
st.warning("No job listings found on LinkedIn. Try different search terms.")
|
| 283 |
+
return pd.DataFrame()
|
| 284 |
+
|
| 285 |
+
# Ensure all arrays have the same length by truncating to the shortest length
|
| 286 |
+
min_length = min(len(company_names), len(job_titles), len(company_locations), len(job_urls))
|
| 287 |
+
|
| 288 |
+
if min_length == 0:
|
| 289 |
+
st.warning("No job listings found on LinkedIn. Try different search terms.")
|
| 290 |
+
return pd.DataFrame()
|
| 291 |
+
|
| 292 |
+
company_names = company_names[:min_length]
|
| 293 |
+
job_titles = job_titles[:min_length]
|
| 294 |
+
company_locations = company_locations[:min_length]
|
| 295 |
+
job_urls = job_urls[:min_length]
|
| 296 |
+
|
| 297 |
+
# Create DataFrame
|
| 298 |
+
df = pd.DataFrame({
|
| 299 |
+
'Company Name': company_names,
|
| 300 |
+
'Job Title': job_titles,
|
| 301 |
+
'Location': company_locations,
|
| 302 |
+
'Website URL': job_urls
|
| 303 |
+
})
|
| 304 |
+
|
| 305 |
+
# Filter job titles based on user input if provided
|
| 306 |
+
if job_title_input and job_title_input != ['']:
|
| 307 |
+
filtered_titles = []
|
| 308 |
+
for title in df['Job Title']:
|
| 309 |
+
if any(user_title.lower().strip() in title.lower() for user_title in job_title_input if user_title.strip()):
|
| 310 |
+
filtered_titles.append(title)
|
| 311 |
+
else:
|
| 312 |
+
filtered_titles.append(np.nan)
|
| 313 |
+
df['Job Title'] = filtered_titles
|
| 314 |
+
|
| 315 |
+
# Filter locations based on user input if provided and not "India"
|
| 316 |
+
if job_location and job_location.lower() != "india":
|
| 317 |
+
filtered_locations = []
|
| 318 |
+
for loc in df['Location']:
|
| 319 |
+
if job_location.lower() in loc.lower():
|
| 320 |
+
filtered_locations.append(loc)
|
| 321 |
+
else:
|
| 322 |
+
filtered_locations.append(np.nan)
|
| 323 |
+
df['Location'] = filtered_locations
|
| 324 |
+
|
| 325 |
+
# Drop rows with NaN values and reset index
|
| 326 |
+
df = df.dropna()
|
| 327 |
+
df = df.reset_index(drop=True)
|
| 328 |
+
|
| 329 |
+
return df
|
| 330 |
+
|
| 331 |
+
except Exception as e:
|
| 332 |
+
st.error(f"Error scraping company data: {str(e)}")
|
| 333 |
+
st.info("Try refreshing the page or using different search terms.")
|
| 334 |
+
return pd.DataFrame()
|
| 335 |
+
|
| 336 |
+
@staticmethod
|
| 337 |
+
def scrap_job_description(driver, df, job_count):
|
| 338 |
+
"""Scrape job descriptions for each job listing"""
|
| 339 |
+
if df.empty:
|
| 340 |
+
return df
|
| 341 |
+
|
| 342 |
+
# Get job URLs
|
| 343 |
+
job_urls = df['Website URL'].tolist()
|
| 344 |
+
|
| 345 |
+
# Limit to requested job count
|
| 346 |
+
job_urls = job_urls[:min(len(job_urls), job_count)]
|
| 347 |
+
|
| 348 |
+
# Initialize list for job descriptions
|
| 349 |
+
job_descriptions = []
|
| 350 |
+
|
| 351 |
+
# Progress bar for scraping job descriptions
|
| 352 |
+
progress_bar = st.progress(0)
|
| 353 |
+
status_text = st.empty()
|
| 354 |
+
|
| 355 |
+
for i, url in enumerate(job_urls):
|
| 356 |
+
try:
|
| 357 |
+
# Update progress
|
| 358 |
+
progress = int((i + 1) / len(job_urls) * 100)
|
| 359 |
+
progress_bar.progress(progress)
|
| 360 |
+
status_text.text(f"Scraping job {i+1} of {len(job_urls)}...")
|
| 361 |
+
|
| 362 |
+
# Open job listing page
|
| 363 |
+
driver.get(url)
|
| 364 |
+
driver.implicitly_wait(5)
|
| 365 |
+
time.sleep(2)
|
| 366 |
+
|
| 367 |
+
# Try to click "Show more" button to expand job description
|
| 368 |
+
try:
|
| 369 |
+
show_more_buttons = driver.find_elements(
|
| 370 |
+
by=By.CSS_SELECTOR,
|
| 371 |
+
value='button[data-tracking-control-name="public_jobs_show-more-html-btn"]'
|
| 372 |
+
)
|
| 373 |
+
if show_more_buttons:
|
| 374 |
+
show_more_buttons[0].click()
|
| 375 |
+
time.sleep(1)
|
| 376 |
+
except:
|
| 377 |
+
pass
|
| 378 |
+
|
| 379 |
+
# Get job description
|
| 380 |
+
description_elements = driver.find_elements(
|
| 381 |
+
by=By.CSS_SELECTOR,
|
| 382 |
+
value='div.show-more-less-html__markup'
|
| 383 |
+
)
|
| 384 |
+
|
| 385 |
+
if description_elements and description_elements[0].text.strip():
|
| 386 |
+
description_text = description_elements[0].text
|
| 387 |
+
|
| 388 |
+
# Process and structure the job description
|
| 389 |
+
processed_description = LinkedInScraper.process_job_description(description_text)
|
| 390 |
+
job_descriptions.append(processed_description)
|
| 391 |
+
else:
|
| 392 |
+
# Try alternative selectors
|
| 393 |
+
alt_description = driver.find_elements(
|
| 394 |
+
by=By.CSS_SELECTOR,
|
| 395 |
+
value='div.description__text'
|
| 396 |
+
)
|
| 397 |
+
if alt_description and alt_description[0].text.strip():
|
| 398 |
+
description_text = alt_description[0].text
|
| 399 |
+
processed_description = LinkedInScraper.process_job_description(description_text)
|
| 400 |
+
job_descriptions.append(processed_description)
|
| 401 |
+
else:
|
| 402 |
+
job_descriptions.append("Description not available")
|
| 403 |
+
|
| 404 |
+
except Exception as e:
|
| 405 |
+
job_descriptions.append("Description not available")
|
| 406 |
+
st.warning(f"Error scraping job description {i+1}: {str(e)}")
|
| 407 |
+
|
| 408 |
+
# Clear progress indicators
|
| 409 |
+
progress_bar.empty()
|
| 410 |
+
status_text.empty()
|
| 411 |
+
|
| 412 |
+
# Filter DataFrame to include only rows with descriptions
|
| 413 |
+
df = df.iloc[:len(job_descriptions), :]
|
| 414 |
+
|
| 415 |
+
# Add job descriptions to DataFrame
|
| 416 |
+
df['Job Description'] = job_descriptions
|
| 417 |
+
|
| 418 |
+
# Filter out rows with unavailable descriptions
|
| 419 |
+
df['Job Description'] = df['Job Description'].apply(
|
| 420 |
+
lambda x: np.nan if x == "Description not available" else x
|
| 421 |
+
)
|
| 422 |
+
df = df.dropna()
|
| 423 |
+
df = df.reset_index(drop=True)
|
| 424 |
+
|
| 425 |
+
return df
|
| 426 |
+
|
| 427 |
+
@staticmethod
|
| 428 |
+
def process_job_description(text):
|
| 429 |
+
"""Process and structure job description text"""
|
| 430 |
+
if not text or text == "Description not available":
|
| 431 |
+
return text
|
| 432 |
+
|
| 433 |
+
# Split into sections
|
| 434 |
+
sections = text.split('\n\n')
|
| 435 |
+
processed_sections = []
|
| 436 |
+
|
| 437 |
+
# Common section headers to identify
|
| 438 |
+
section_headers = [
|
| 439 |
+
"responsibilities", "requirements", "qualifications", "skills",
|
| 440 |
+
"about the job", "about the role", "what you'll do", "what you'll need",
|
| 441 |
+
"about us", "about the company", "who we are", "benefits", "perks",
|
| 442 |
+
"job description", "role description", "experience", "education",
|
| 443 |
+
"job summary", "job overview", "job requirements", "job responsibilities",
|
| 444 |
+
"job qualifications", "job skills", "job benefits", "job perks",
|
| 445 |
+
"job description", "role description", "experience", "education",
|
| 446 |
+
"job summary", "job overview", "job requirements", "job responsibilities",
|
| 447 |
+
"job qualifications", "job skills", "job benefits", "job perks",
|
| 448 |
+
"Education Qualification and Experience", "Required Skills", "Preferred Qualifications", "Key Responsibilities",
|
| 449 |
+
"About Us", "About the Company", "About the Role", "About the Job",
|
| 450 |
+
"About the Team", "About the Organization", "About the Industry", "About the Location",
|
| 451 |
+
"Position", "Job Description", "Job Summary", "Job Overview"
|
| 452 |
+
]
|
| 453 |
+
|
| 454 |
+
# Process each section
|
| 455 |
+
current_section = ""
|
| 456 |
+
for section in sections:
|
| 457 |
+
if not section.strip():
|
| 458 |
+
continue
|
| 459 |
+
|
| 460 |
+
# Check if this is a new section header
|
| 461 |
+
is_header = False
|
| 462 |
+
section_lower = section.lower().strip()
|
| 463 |
+
|
| 464 |
+
# Check if section starts with a header
|
| 465 |
+
for header in section_headers:
|
| 466 |
+
if section_lower.startswith(header) or section_lower.startswith("• " + header) or section_lower.startswith("- " + header):
|
| 467 |
+
# Format as a header
|
| 468 |
+
current_section = section.strip()
|
| 469 |
+
is_header = True
|
| 470 |
+
processed_sections.append(f"\n**{current_section}**\n")
|
| 471 |
+
break
|
| 472 |
+
|
| 473 |
+
if not is_header:
|
| 474 |
+
# Check if it's a bullet point list
|
| 475 |
+
if section.strip().startswith('•') or section.strip().startswith('-') or section.strip().startswith('*'):
|
| 476 |
+
lines = section.split('\n')
|
| 477 |
+
formatted_lines = []
|
| 478 |
+
|
| 479 |
+
for line in lines:
|
| 480 |
+
line = line.strip()
|
| 481 |
+
if line:
|
| 482 |
+
if line.startswith('•') or line.startswith('-') or line.startswith('*'):
|
| 483 |
+
# Format as bullet point
|
| 484 |
+
formatted_lines.append(f"• {line.lstrip('•').lstrip('-').lstrip('*').strip()}")
|
| 485 |
+
else:
|
| 486 |
+
formatted_lines.append(line)
|
| 487 |
+
|
| 488 |
+
processed_sections.append('\n'.join(formatted_lines))
|
| 489 |
+
else:
|
| 490 |
+
# Regular paragraph
|
| 491 |
+
processed_sections.append(section.strip())
|
| 492 |
+
|
| 493 |
+
# Join all processed sections
|
| 494 |
+
return '\n\n'.join(processed_sections)
|
| 495 |
+
|
| 496 |
+
@staticmethod
|
| 497 |
+
def display_data_userinterface(df_final):
|
| 498 |
+
"""Display scraped job data in the user interface"""
|
| 499 |
+
if df_final.empty:
|
| 500 |
+
st.warning("No matching jobs found. Try different search terms or location.")
|
| 501 |
+
return
|
| 502 |
+
|
| 503 |
+
# Apply custom styling for job cards
|
| 504 |
+
st.markdown("""
|
| 505 |
+
<style>
|
| 506 |
+
.job-card {
|
| 507 |
+
background: rgba(255, 255, 255, 0.05);
|
| 508 |
+
border-radius: 10px;
|
| 509 |
+
padding: 1.5rem;
|
| 510 |
+
margin-bottom: 1rem;
|
| 511 |
+
border-left: 4px solid #0A66C2;
|
| 512 |
+
transition: transform 0.2s;
|
| 513 |
+
}
|
| 514 |
+
.job-card:hover {
|
| 515 |
+
background: rgba(255, 255, 255, 0.08);
|
| 516 |
+
}
|
| 517 |
+
.job-title {
|
| 518 |
+
color: #0A66C2;
|
| 519 |
+
font-size: 1.3rem;
|
| 520 |
+
margin-bottom: 0.5rem;
|
| 521 |
+
}
|
| 522 |
+
.company-name {
|
| 523 |
+
font-weight: bold;
|
| 524 |
+
font-size: 1.1rem;
|
| 525 |
+
}
|
| 526 |
+
.job-location {
|
| 527 |
+
color: #888;
|
| 528 |
+
margin-bottom: 1rem;
|
| 529 |
+
}
|
| 530 |
+
.job-url-button {
|
| 531 |
+
display: inline-block;
|
| 532 |
+
background: #0A66C2;
|
| 533 |
+
color: white;
|
| 534 |
+
padding: 0.5rem 1rem;
|
| 535 |
+
border-radius: 5px;
|
| 536 |
+
text-decoration: none;
|
| 537 |
+
margin-top: 1rem;
|
| 538 |
+
font-weight: bold;
|
| 539 |
+
}
|
| 540 |
+
.job-url-button:hover {
|
| 541 |
+
background: #084d8e;
|
| 542 |
+
}
|
| 543 |
+
.job-count {
|
| 544 |
+
background: rgba(10, 102, 194, 0.1);
|
| 545 |
+
color: #0A66C2;
|
| 546 |
+
padding: 0.5rem 1rem;
|
| 547 |
+
border-radius: 5px;
|
| 548 |
+
margin-bottom: 1rem;
|
| 549 |
+
font-weight: bold;
|
| 550 |
+
}
|
| 551 |
+
.job-section {
|
| 552 |
+
margin-top: 1rem;
|
| 553 |
+
padding-top: 0.5rem;
|
| 554 |
+
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
| 555 |
+
}
|
| 556 |
+
.job-section-title {
|
| 557 |
+
font-weight: bold;
|
| 558 |
+
color: #0A66C2;
|
| 559 |
+
margin-bottom: 0.5rem;
|
| 560 |
+
}
|
| 561 |
+
</style>
|
| 562 |
+
""", unsafe_allow_html=True)
|
| 563 |
+
|
| 564 |
+
# Display job count
|
| 565 |
+
st.markdown(f'<div class="job-count">🎯 Found {len(df_final)} matching jobs on LinkedIn</div>', unsafe_allow_html=True)
|
| 566 |
+
|
| 567 |
+
# Display each job
|
| 568 |
+
for i in range(len(df_final)):
|
| 569 |
+
company_name = df_final.iloc[i, 0]
|
| 570 |
+
job_title = df_final.iloc[i, 1]
|
| 571 |
+
location = df_final.iloc[i, 2]
|
| 572 |
+
url = df_final.iloc[i, 3]
|
| 573 |
+
description = df_final.iloc[i, 4]
|
| 574 |
+
|
| 575 |
+
# Create job card
|
| 576 |
+
st.markdown(f"""
|
| 577 |
+
<div class="job-card">
|
| 578 |
+
<div class="job-title">{job_title}</div>
|
| 579 |
+
<div class="company-name">{company_name}</div>
|
| 580 |
+
<div class="job-location">📍 {location}</div>
|
| 581 |
+
</div>
|
| 582 |
+
""", unsafe_allow_html=True)
|
| 583 |
+
|
| 584 |
+
# Job description in expander with better formatting
|
| 585 |
+
with st.expander("View Job Description"):
|
| 586 |
+
st.markdown(description)
|
| 587 |
+
st.markdown(f"<a href='{url}' target='_blank' class='job-url-button'>Apply on LinkedIn</a>", unsafe_allow_html=True)
|
| 588 |
+
|
| 589 |
+
st.markdown("<hr>", unsafe_allow_html=True)
|
| 590 |
+
|
| 591 |
+
@staticmethod
|
| 592 |
+
def main(show_title=True):
|
| 593 |
+
"""Main function to run the LinkedIn job scraper"""
|
| 594 |
+
# Initialize driver to None
|
| 595 |
+
driver = None
|
| 596 |
+
|
| 597 |
+
try:
|
| 598 |
+
# Get user input
|
| 599 |
+
job_title_input, job_location, job_count, submit = LinkedInScraper.get_user_input(show_title)
|
| 600 |
+
|
| 601 |
+
if submit:
|
| 602 |
+
if job_title_input != [''] and job_location:
|
| 603 |
+
try:
|
| 604 |
+
# Set up Chrome webdriver
|
| 605 |
+
with st.spinner('Setting up Chrome webdriver...'):
|
| 606 |
+
driver = LinkedInScraper.webdriver_setup()
|
| 607 |
+
|
| 608 |
+
if not driver:
|
| 609 |
+
st.error("Failed to initialize Chrome webdriver. Please make sure Chrome is installed.")
|
| 610 |
+
return
|
| 611 |
+
|
| 612 |
+
# Build URL and open LinkedIn
|
| 613 |
+
with st.spinner('Loading LinkedIn jobs page...'):
|
| 614 |
+
link = LinkedInScraper.build_url(job_title_input, job_location)
|
| 615 |
+
st.info(f"Searching for: {', '.join([t for t in job_title_input if t.strip()])} in {job_location}")
|
| 616 |
+
success = LinkedInScraper.link_open_scrolldown(driver, link, job_count)
|
| 617 |
+
|
| 618 |
+
if not success:
|
| 619 |
+
st.error("Failed to load LinkedIn jobs page. Please try again.")
|
| 620 |
+
return
|
| 621 |
+
|
| 622 |
+
# Scrape job data
|
| 623 |
+
with st.spinner('Scraping job listings...'):
|
| 624 |
+
df = LinkedInScraper.scrap_company_data(driver, job_title_input, job_location)
|
| 625 |
+
|
| 626 |
+
if df.empty:
|
| 627 |
+
st.warning("No jobs found matching your criteria. Try different search terms.")
|
| 628 |
+
return
|
| 629 |
+
|
| 630 |
+
# Scrape job descriptions
|
| 631 |
+
with st.spinner('Fetching job descriptions...'):
|
| 632 |
+
df_final = LinkedInScraper.scrap_job_description(driver, df, job_count)
|
| 633 |
+
|
| 634 |
+
if df_final.empty:
|
| 635 |
+
st.warning("Could not retrieve job descriptions. Try different search terms.")
|
| 636 |
+
return
|
| 637 |
+
|
| 638 |
+
# Display results
|
| 639 |
+
LinkedInScraper.display_data_userinterface(df_final)
|
| 640 |
+
|
| 641 |
+
except Exception as e:
|
| 642 |
+
st.error(f"An error occurred: {str(e)}")
|
| 643 |
+
st.info("Try refreshing the page or using different search terms.")
|
| 644 |
+
|
| 645 |
+
elif job_title_input == ['']:
|
| 646 |
+
st.warning("Please enter a job title to search.")
|
| 647 |
+
|
| 648 |
+
elif not job_location:
|
| 649 |
+
st.warning("Please enter a job location to search.")
|
| 650 |
+
|
| 651 |
+
except Exception as e:
|
| 652 |
+
st.error(f"An unexpected error occurred: {str(e)}")
|
| 653 |
+
|
| 654 |
+
finally:
|
| 655 |
+
# Close the webdriver
|
| 656 |
+
if driver:
|
| 657 |
+
driver.quit()
|
| 658 |
+
|
| 659 |
+
def render_linkedin_scraper():
|
| 660 |
+
"""Render the LinkedIn job scraper interface"""
|
| 661 |
+
# Don't show the title again, as it's already shown in the job_search.py file
|
| 662 |
+
LinkedInScraper.main(show_title=False)
|
jobs/suggestions.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Module containing job-related data and configurations"""
|
| 2 |
+
|
| 3 |
+
# Job titles and skills suggestions
|
| 4 |
+
JOB_SUGGESTIONS = [
|
| 5 |
+
{"text": "Software Engineer", "icon": "💻"},
|
| 6 |
+
{"text": "Full Stack Developer", "icon": "🔧"},
|
| 7 |
+
{"text": "Data Scientist", "icon": "📊"},
|
| 8 |
+
{"text": "Product Manager", "icon": "📱"},
|
| 9 |
+
{"text": "DevOps Engineer", "icon": "⚙️"},
|
| 10 |
+
{"text": "UI/UX Designer", "icon": "🎨"},
|
| 11 |
+
{"text": "Python Developer", "icon": "🐍"},
|
| 12 |
+
{"text": "Java Developer", "icon": "☕"},
|
| 13 |
+
{"text": "React Developer", "icon": "⚛️"},
|
| 14 |
+
{"text": "Machine Learning Engineer", "icon": "🤖"},
|
| 15 |
+
{"text": "Backend Developer", "icon": "🖧"},
|
| 16 |
+
{"text": "Frontend Developer", "icon": "🎨"},
|
| 17 |
+
{"text": "Node.js Developer", "icon": "🌿"},
|
| 18 |
+
{"text": "Angular Developer", "icon": "📐"},
|
| 19 |
+
{"text": "PHP Developer", "icon": "🐘"},
|
| 20 |
+
{"text": "Ruby Developer", "icon": "💎"},
|
| 21 |
+
{"text": "Go Developer", "icon": "🚀"},
|
| 22 |
+
{"text": "C++ Developer", "icon": "🖥️"},
|
| 23 |
+
{"text": "C# Developer", "icon": "🎮"},
|
| 24 |
+
{"text": "Django Developer", "icon": "🛠️"},
|
| 25 |
+
{"text": "Data Analyst", "icon": "📈"},
|
| 26 |
+
{"text": "Big Data Engineer", "icon": "📡"},
|
| 27 |
+
{"text": "Database Administrator", "icon": "🗄️"},
|
| 28 |
+
{"text": "Business Intelligence Analyst", "icon": "📊"},
|
| 29 |
+
{"text": "Cloud Engineer", "icon": "☁️"},
|
| 30 |
+
{"text": "AWS Engineer", "icon": "☁️🔧"},
|
| 31 |
+
{"text": "Azure Engineer", "icon": "☁️🖥️"},
|
| 32 |
+
{"text": "Google Cloud Engineer", "icon": "☁️📡"},
|
| 33 |
+
{"text": "Network Engineer", "icon": "🔌"},
|
| 34 |
+
{"text": "AI Researcher", "icon": "🧠"},
|
| 35 |
+
{"text": "NLP Engineer", "icon": "🗣️"},
|
| 36 |
+
{"text": "Computer Vision Engineer", "icon": "👁️"},
|
| 37 |
+
{"text": "Deep Learning Engineer", "icon": "🧠📚"},
|
| 38 |
+
{"text": "Cybersecurity Analyst", "icon": "🔒"},
|
| 39 |
+
{"text": "Ethical Hacker", "icon": "🕵️♂️"},
|
| 40 |
+
{"text": "Security Engineer", "icon": "🛡️"},
|
| 41 |
+
{"text": "Penetration Tester", "icon": "🔍"},
|
| 42 |
+
{"text": "Cryptography Engineer", "icon": "🔑"},
|
| 43 |
+
{"text": "Game Developer", "icon": "🎮"},
|
| 44 |
+
{"text": "Embedded Systems Engineer", "icon": "🖧⚙️"},
|
| 45 |
+
{"text": "Mobile App Developer", "icon": "📱"},
|
| 46 |
+
{"text": "iOS Developer", "icon": "🍏"},
|
| 47 |
+
{"text": "Android Developer", "icon": "🤖"},
|
| 48 |
+
{"text": "Blockchain Developer", "icon": "🔗"},
|
| 49 |
+
{"text": "IoT Developer", "icon": "🌐"},
|
| 50 |
+
{"text": "AR/VR Developer", "icon": "🕶️"},
|
| 51 |
+
{"text": "Project Manager", "icon": "📋"},
|
| 52 |
+
{"text": "Technical Writer", "icon": "✍️"},
|
| 53 |
+
{"text": "QA Engineer", "icon": "✅"},
|
| 54 |
+
{"text": "Scrum Master", "icon": "🔄"},
|
| 55 |
+
{"text": "Support Engineer", "icon": "📞"},
|
| 56 |
+
{"text": "IT Consultant", "icon": "🧑💼"},
|
| 57 |
+
{"text": "Technical Support Specialist", "icon": "🎧"}
|
| 58 |
+
]
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
# Location suggestions - organized by states and major cities
|
| 62 |
+
LOCATION_SUGGESTIONS = [
|
| 63 |
+
# Work modes
|
| 64 |
+
{"text": "Remote", "icon": "🏠", "type": "work_mode"},
|
| 65 |
+
{"text": "Work from Home", "icon": "🏠", "type": "work_mode"},
|
| 66 |
+
{"text": "Hybrid", "icon": "🏢", "type": "work_mode"},
|
| 67 |
+
|
| 68 |
+
# Major tech hubs
|
| 69 |
+
{"text": "Bangalore", "icon": "📍", "type": "city", "state": "Karnataka"},
|
| 70 |
+
{"text": "Mumbai", "icon": "📍", "type": "city", "state": "Maharashtra"},
|
| 71 |
+
{"text": "Delhi", "icon": "📍", "type": "city", "state": "Delhi"},
|
| 72 |
+
{"text": "Hyderabad", "icon": "📍", "type": "city", "state": "Telangana"},
|
| 73 |
+
{"text": "Pune", "icon": "📍", "type": "city", "state": "Maharashtra"},
|
| 74 |
+
{"text": "Chennai", "icon": "📍", "type": "city", "state": "Tamil Nadu"},
|
| 75 |
+
{"text": "Noida", "icon": "📍", "type": "city", "state": "Uttar Pradesh"},
|
| 76 |
+
{"text": "Gurgaon", "icon": "📍", "type": "city", "state": "Haryana"},
|
| 77 |
+
|
| 78 |
+
# States
|
| 79 |
+
{"text": "Karnataka", "icon": "🗺️", "type": "state"},
|
| 80 |
+
{"text": "Maharashtra", "icon": "🗺️", "type": "state"},
|
| 81 |
+
{"text": "Tamil Nadu", "icon": "🗺️", "type": "state"},
|
| 82 |
+
{"text": "Telangana", "icon": "🗺️", "type": "state"},
|
| 83 |
+
{"text": "Delhi", "icon": "🗺️", "type": "state"},
|
| 84 |
+
{"text": "Uttar Pradesh", "icon": "🗺️", "type": "state"},
|
| 85 |
+
{"text": "Gujarat", "icon": "🗺️", "type": "state"},
|
| 86 |
+
{"text": "Rajasthan", "icon": "🗺️", "type": "state"},
|
| 87 |
+
{"text": "Kerala", "icon": "🗺️", "type": "state"},
|
| 88 |
+
{"text": "West Bengal", "icon": "🗺️", "type": "state"},
|
| 89 |
+
{"text": "Punjab", "icon": "🗺️", "type": "state"},
|
| 90 |
+
{"text": "Haryana", "icon": "🗺️", "type": "state"},
|
| 91 |
+
{"text": "Andhra Pradesh", "icon": "🗺️", "type": "state"},
|
| 92 |
+
{"text": "Madhya Pradesh", "icon": "🗺️", "type": "state"},
|
| 93 |
+
{"text": "Bihar", "icon": "🗺️", "type": "state"},
|
| 94 |
+
|
| 95 |
+
# Karnataka cities
|
| 96 |
+
{"text": "Mysore", "icon": "📍", "type": "city", "state": "Karnataka"},
|
| 97 |
+
{"text": "Hubli", "icon": "📍", "type": "city", "state": "Karnataka"},
|
| 98 |
+
{"text": "Mangalore", "icon": "📍", "type": "city", "state": "Karnataka"},
|
| 99 |
+
{"text": "Belgaum", "icon": "📍", "type": "city", "state": "Karnataka"},
|
| 100 |
+
{"text": "Davangere", "icon": "📍", "type": "city", "state": "Karnataka"},
|
| 101 |
+
|
| 102 |
+
# Maharashtra cities
|
| 103 |
+
{"text": "Nagpur", "icon": "📍", "type": "city", "state": "Maharashtra"},
|
| 104 |
+
{"text": "Nashik", "icon": "📍", "type": "city", "state": "Maharashtra"},
|
| 105 |
+
{"text": "Aurangabad", "icon": "📍", "type": "city", "state": "Maharashtra"},
|
| 106 |
+
{"text": "Kolhapur", "icon": "📍", "type": "city", "state": "Maharashtra"},
|
| 107 |
+
{"text": "Solapur", "icon": "📍", "type": "city", "state": "Maharashtra"},
|
| 108 |
+
|
| 109 |
+
# Tamil Nadu cities
|
| 110 |
+
{"text": "Coimbatore", "icon": "📍", "type": "city", "state": "Tamil Nadu"},
|
| 111 |
+
{"text": "Madurai", "icon": "📍", "type": "city", "state": "Tamil Nadu"},
|
| 112 |
+
{"text": "Salem", "icon": "📍", "type": "city", "state": "Tamil Nadu"},
|
| 113 |
+
{"text": "Tiruchirappalli", "icon": "📍", "type": "city", "state": "Tamil Nadu"},
|
| 114 |
+
{"text": "Vellore", "icon": "📍", "type": "city", "state": "Tamil Nadu"},
|
| 115 |
+
|
| 116 |
+
# Uttar Pradesh cities
|
| 117 |
+
{"text": "Lucknow", "icon": "📍", "type": "city", "state": "Uttar Pradesh"},
|
| 118 |
+
{"text": "Kanpur", "icon": "📍", "type": "city", "state": "Uttar Pradesh"},
|
| 119 |
+
{"text": "Agra", "icon": "📍", "type": "city", "state": "Uttar Pradesh"},
|
| 120 |
+
{"text": "Varanasi", "icon": "📍", "type": "city", "state": "Uttar Pradesh"},
|
| 121 |
+
{"text": "Meerut", "icon": "📍", "type": "city", "state": "Uttar Pradesh"},
|
| 122 |
+
|
| 123 |
+
# Andhra Pradesh cities
|
| 124 |
+
{"text": "Vijayawada", "icon": "📍", "type": "city", "state": "Andhra Pradesh"},
|
| 125 |
+
{"text": "Visakhapatnam", "icon": "📍", "type": "city", "state": "Andhra Pradesh"},
|
| 126 |
+
{"text": "Tirupati", "icon": "📍", "type": "city", "state": "Andhra Pradesh"},
|
| 127 |
+
{"text": "Guntur", "icon": "📍", "type": "city", "state": "Andhra Pradesh"},
|
| 128 |
+
{"text": "Nellore", "icon": "📍", "type": "city", "state": "Andhra Pradesh"},
|
| 129 |
+
|
| 130 |
+
# West Bengal cities
|
| 131 |
+
{"text": "Kolkata", "icon": "📍", "type": "city", "state": "West Bengal"},
|
| 132 |
+
{"text": "Darjeeling", "icon": "📍", "type": "city", "state": "West Bengal"},
|
| 133 |
+
{"text": "Siliguri", "icon": "📍", "type": "city", "state": "West Bengal"},
|
| 134 |
+
{"text": "Durgapur", "icon": "📍", "type": "city", "state": "West Bengal"},
|
| 135 |
+
{"text": "Asansol", "icon": "📍", "type": "city", "state": "West Bengal"},
|
| 136 |
+
|
| 137 |
+
# Gujarat cities
|
| 138 |
+
{"text": "Ahmedabad", "icon": "📍", "type": "city", "state": "Gujarat"},
|
| 139 |
+
{"text": "Surat", "icon": "📍", "type": "city", "state": "Gujarat"},
|
| 140 |
+
{"text": "Vadodara", "icon": "📍", "type": "city", "state": "Gujarat"},
|
| 141 |
+
{"text": "Rajkot", "icon": "📍", "type": "city", "state": "Gujarat"},
|
| 142 |
+
{"text": "Bhavnagar", "icon": "📍", "type": "city", "state": "Gujarat"},
|
| 143 |
+
|
| 144 |
+
# Rajasthan cities
|
| 145 |
+
{"text": "Jaipur", "icon": "📍", "type": "city", "state": "Rajasthan"},
|
| 146 |
+
{"text": "Jodhpur", "icon": "📍", "type": "city", "state": "Rajasthan"},
|
| 147 |
+
{"text": "Udaipur", "icon": "📍", "type": "city", "state": "Rajasthan"},
|
| 148 |
+
{"text": "Kota", "icon": "📍", "type": "city", "state": "Rajasthan"},
|
| 149 |
+
{"text": "Ajmer", "icon": "📍", "type": "city", "state": "Rajasthan"},
|
| 150 |
+
|
| 151 |
+
# Kerala cities
|
| 152 |
+
{"text": "Kochi", "icon": "📍", "type": "city", "state": "Kerala"},
|
| 153 |
+
{"text": "Thiruvananthapuram", "icon": "📍", "type": "city", "state": "Kerala"},
|
| 154 |
+
{"text": "Kozhikode", "icon": "📍", "type": "city", "state": "Kerala"},
|
| 155 |
+
{"text": "Thrissur", "icon": "📍", "type": "city", "state": "Kerala"},
|
| 156 |
+
{"text": "Alappuzha", "icon": "📍", "type": "city", "state": "Kerala"},
|
| 157 |
+
|
| 158 |
+
# Punjab cities
|
| 159 |
+
{"text": "Amritsar", "icon": "📍", "type": "city", "state": "Punjab"},
|
| 160 |
+
{"text": "Ludhiana", "icon": "📍", "type": "city", "state": "Punjab"},
|
| 161 |
+
{"text": "Jalandhar", "icon": "📍", "type": "city", "state": "Punjab"},
|
| 162 |
+
{"text": "Patiala", "icon": "📍", "type": "city", "state": "Punjab"},
|
| 163 |
+
{"text": "Bathinda", "icon": "📍", "type": "city", "state": "Punjab"},
|
| 164 |
+
|
| 165 |
+
# Haryana cities
|
| 166 |
+
{"text": "Faridabad", "icon": "📍", "type": "city", "state": "Haryana"},
|
| 167 |
+
{"text": "Panipat", "icon": "📍", "type": "city", "state": "Haryana"},
|
| 168 |
+
{"text": "Ambala", "icon": "📍", "type": "city", "state": "Haryana"},
|
| 169 |
+
{"text": "Karnal", "icon": "📍", "type": "city", "state": "Haryana"},
|
| 170 |
+
{"text": "Hisar", "icon": "📍", "type": "city", "state": "Haryana"},
|
| 171 |
+
|
| 172 |
+
# Northeast cities
|
| 173 |
+
{"text": "Guwahati", "icon": "📍", "type": "city", "state": "Assam"},
|
| 174 |
+
{"text": "Shillong", "icon": "📍", "type": "city", "state": "Meghalaya"},
|
| 175 |
+
{"text": "Imphal", "icon": "📍", "type": "city", "state": "Manipur"},
|
| 176 |
+
{"text": "Aizawl", "icon": "📍", "type": "city", "state": "Mizoram"},
|
| 177 |
+
{"text": "Gangtok", "icon": "📍", "type": "city", "state": "Sikkim"},
|
| 178 |
+
|
| 179 |
+
# Union Territories
|
| 180 |
+
{"text": "Chandigarh", "icon": "📍", "type": "city", "state": "Chandigarh"},
|
| 181 |
+
{"text": "Port Blair", "icon": "📍", "type": "city", "state": "Andaman and Nicobar Islands"},
|
| 182 |
+
{"text": "Shimla", "icon": "📍", "type": "city", "state": "Himachal Pradesh"},
|
| 183 |
+
{"text": "Dehradun", "icon": "📍", "type": "city", "state": "Uttarakhand"},
|
| 184 |
+
{"text": "Itanagar", "icon": "📍", "type": "city", "state": "Arunachal Pradesh"}
|
| 185 |
+
]
|
| 186 |
+
|
| 187 |
+
# Function to get cities by state
|
| 188 |
+
def get_cities_by_state(state_name):
|
| 189 |
+
"""Get list of cities for a specific state"""
|
| 190 |
+
return [loc for loc in LOCATION_SUGGESTIONS if loc.get("type") == "city" and loc.get("state") == state_name]
|
| 191 |
+
|
| 192 |
+
# Function to get all states
|
| 193 |
+
def get_all_states():
|
| 194 |
+
"""Get list of all states"""
|
| 195 |
+
return [loc for loc in LOCATION_SUGGESTIONS if loc.get("type") == "state"]
|
| 196 |
+
|
| 197 |
+
# Job types
|
| 198 |
+
JOB_TYPES = [
|
| 199 |
+
{"id": "all", "text": "All Types"},
|
| 200 |
+
{"id": "full-time", "text": "Full Time"},
|
| 201 |
+
{"id": "part-time", "text": "Part Time"},
|
| 202 |
+
{"id": "contract", "text": "Contract"},
|
| 203 |
+
{"id": "internship", "text": "Internship"},
|
| 204 |
+
{"id": "remote", "text": "Remote"}
|
| 205 |
+
]
|
| 206 |
+
|
| 207 |
+
# Experience levels
|
| 208 |
+
EXPERIENCE_RANGES = [
|
| 209 |
+
{"id": "all", "text": "All Levels"},
|
| 210 |
+
{"id": "fresher", "text": "Fresher"},
|
| 211 |
+
{"id": "1-3", "text": "1-3 years"},
|
| 212 |
+
{"id": "3-5", "text": "3-5 years"},
|
| 213 |
+
{"id": "5-7", "text": "5-7 years"},
|
| 214 |
+
{"id": "7+", "text": "7+ years"}
|
| 215 |
+
]
|
| 216 |
+
|
| 217 |
+
# Salary ranges
|
| 218 |
+
SALARY_RANGES = [
|
| 219 |
+
{"id": "all", "text": "All Ranges"},
|
| 220 |
+
{"id": "0-3", "text": "0-3 LPA"},
|
| 221 |
+
{"id": "3-6", "text": "3-6 LPA"},
|
| 222 |
+
{"id": "6-10", "text": "6-10 LPA"},
|
| 223 |
+
{"id": "10-15", "text": "10-15 LPA"},
|
| 224 |
+
{"id": "15+", "text": "15+ LPA"}
|
| 225 |
+
]
|
jobs/webdriver_utils.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Utility functions for webdriver setup and management"""
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
import platform
|
| 5 |
+
import tempfile
|
| 6 |
+
import subprocess
|
| 7 |
+
import streamlit as st
|
| 8 |
+
from selenium import webdriver
|
| 9 |
+
from selenium.webdriver.chrome.service import Service
|
| 10 |
+
from selenium.webdriver.chrome.options import Options
|
| 11 |
+
|
| 12 |
+
# Try to import various webdriver managers with fallbacks
|
| 13 |
+
try:
|
| 14 |
+
from webdriver_manager.chrome import ChromeDriverManager
|
| 15 |
+
from webdriver_manager.core.utils import ChromeType
|
| 16 |
+
webdriver_manager_available = True
|
| 17 |
+
except ImportError:
|
| 18 |
+
webdriver_manager_available = False
|
| 19 |
+
|
| 20 |
+
try:
|
| 21 |
+
import chromedriver_autoinstaller
|
| 22 |
+
autoinstaller_available = True
|
| 23 |
+
except ImportError:
|
| 24 |
+
autoinstaller_available = False
|
| 25 |
+
|
| 26 |
+
def get_chrome_version():
|
| 27 |
+
"""Get the installed Chrome/Chromium version"""
|
| 28 |
+
system = platform.system()
|
| 29 |
+
|
| 30 |
+
if system == "Windows":
|
| 31 |
+
# Windows-specific Chrome detection
|
| 32 |
+
try:
|
| 33 |
+
# Try to find Chrome in Program Files
|
| 34 |
+
chrome_paths = [
|
| 35 |
+
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
| 36 |
+
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
|
| 37 |
+
os.path.expandvars(r"%LOCALAPPDATA%\Google\Chrome\Application\chrome.exe")
|
| 38 |
+
]
|
| 39 |
+
|
| 40 |
+
for path in chrome_paths:
|
| 41 |
+
if os.path.exists(path):
|
| 42 |
+
try:
|
| 43 |
+
# Try using registry/wmic to get version
|
| 44 |
+
escaped_path = path.replace("\\", "\\\\")
|
| 45 |
+
output = subprocess.check_output(
|
| 46 |
+
['wmic', 'datafile', 'where', f'name="{escaped_path}"', 'get', 'Version', '/value'],
|
| 47 |
+
stderr=subprocess.STDOUT
|
| 48 |
+
)
|
| 49 |
+
version_str = output.decode('utf-8').strip()
|
| 50 |
+
if "Version=" in version_str:
|
| 51 |
+
version = version_str.split('=')[1].split('.')[0]
|
| 52 |
+
return version
|
| 53 |
+
except:
|
| 54 |
+
# Try alternative method
|
| 55 |
+
try:
|
| 56 |
+
output = subprocess.check_output([path, '--version'], stderr=subprocess.STDOUT)
|
| 57 |
+
version = output.decode('utf-8').strip().split()[-1].split('.')[0]
|
| 58 |
+
return version
|
| 59 |
+
except:
|
| 60 |
+
pass
|
| 61 |
+
except Exception:
|
| 62 |
+
# Silently fail and continue with default
|
| 63 |
+
pass
|
| 64 |
+
else:
|
| 65 |
+
# Linux/Mac detection
|
| 66 |
+
try:
|
| 67 |
+
# Try different Chrome/Chromium binaries
|
| 68 |
+
for binary in ['/usr/bin/google-chrome', '/usr/bin/chromium', '/usr/bin/chromium-browser']:
|
| 69 |
+
if os.path.exists(binary):
|
| 70 |
+
version = subprocess.check_output([binary, '--version'], stderr=subprocess.STDOUT)
|
| 71 |
+
version = version.decode('utf-8').strip().split()[-1].split('.')[0]
|
| 72 |
+
return version
|
| 73 |
+
except Exception:
|
| 74 |
+
# Silently fail and continue with default
|
| 75 |
+
pass
|
| 76 |
+
|
| 77 |
+
# Default to latest if all else fails
|
| 78 |
+
return "120"
|
| 79 |
+
|
| 80 |
+
def run_setup_script():
|
| 81 |
+
"""Run the setup_chromedriver.py script to install the correct chromedriver"""
|
| 82 |
+
try:
|
| 83 |
+
# Get the path to the setup script
|
| 84 |
+
script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 85 |
+
setup_script = os.path.join(script_dir, "setup_chromedriver.py")
|
| 86 |
+
|
| 87 |
+
if os.path.exists(setup_script):
|
| 88 |
+
st.info("Running chromedriver setup script...")
|
| 89 |
+
result = subprocess.run([sys.executable, setup_script],
|
| 90 |
+
capture_output=True, text=True)
|
| 91 |
+
|
| 92 |
+
if result.returncode == 0:
|
| 93 |
+
st.success("Chromedriver setup completed successfully!")
|
| 94 |
+
# Extract the chromedriver path from the output
|
| 95 |
+
for line in result.stdout.split('\n'):
|
| 96 |
+
if "Chromedriver path:" in line:
|
| 97 |
+
chromedriver_path = line.split("Chromedriver path:")[1].strip()
|
| 98 |
+
return chromedriver_path
|
| 99 |
+
else:
|
| 100 |
+
st.warning(f"Chromedriver setup failed: {result.stderr}")
|
| 101 |
+
else:
|
| 102 |
+
st.warning(f"Setup script not found at {setup_script}")
|
| 103 |
+
except Exception as e:
|
| 104 |
+
st.warning(f"Error running setup script: {str(e)}")
|
| 105 |
+
|
| 106 |
+
return None
|
| 107 |
+
|
| 108 |
+
def get_chromedriver_path():
|
| 109 |
+
"""Get the path to the chromedriver executable based on the platform"""
|
| 110 |
+
system = platform.system()
|
| 111 |
+
|
| 112 |
+
if system == "Windows":
|
| 113 |
+
# Check in LocalAppData
|
| 114 |
+
local_app_data = os.environ.get('LOCALAPPDATA', '')
|
| 115 |
+
if local_app_data:
|
| 116 |
+
chromedriver_path = os.path.join(local_app_data, "ChromeDriver", "chromedriver.exe")
|
| 117 |
+
if os.path.exists(chromedriver_path):
|
| 118 |
+
return chromedriver_path
|
| 119 |
+
else:
|
| 120 |
+
# Check in ~/.chromedriver
|
| 121 |
+
home_dir = os.path.expanduser("~")
|
| 122 |
+
chromedriver_path = os.path.join(home_dir, ".chromedriver", "chromedriver")
|
| 123 |
+
if os.path.exists(chromedriver_path):
|
| 124 |
+
return chromedriver_path
|
| 125 |
+
|
| 126 |
+
return None
|
| 127 |
+
|
| 128 |
+
def setup_webdriver():
|
| 129 |
+
"""
|
| 130 |
+
Set up and configure Chrome webdriver with multiple fallback options
|
| 131 |
+
|
| 132 |
+
Returns:
|
| 133 |
+
webdriver.Chrome or None: Configured Chrome webdriver or None if setup fails
|
| 134 |
+
"""
|
| 135 |
+
options = Options()
|
| 136 |
+
options.add_argument('--headless')
|
| 137 |
+
options.add_argument('--no-sandbox')
|
| 138 |
+
options.add_argument('--disable-dev-shm-usage')
|
| 139 |
+
options.add_argument('--disable-gpu')
|
| 140 |
+
options.add_argument('--window-size=1920,1080')
|
| 141 |
+
options.add_argument('--disable-blink-features=AutomationControlled')
|
| 142 |
+
options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36')
|
| 143 |
+
|
| 144 |
+
# Method 1: Try direct initialization first since it's working
|
| 145 |
+
try:
|
| 146 |
+
driver = webdriver.Chrome(options=options)
|
| 147 |
+
st.success("Chrome webdriver initialized successfully!")
|
| 148 |
+
return driver
|
| 149 |
+
except Exception:
|
| 150 |
+
# If direct initialization fails, try other methods
|
| 151 |
+
pass
|
| 152 |
+
|
| 153 |
+
# Method 2: Check if we already have a chromedriver installed
|
| 154 |
+
chromedriver_path = get_chromedriver_path()
|
| 155 |
+
if chromedriver_path:
|
| 156 |
+
try:
|
| 157 |
+
service = Service(executable_path=chromedriver_path)
|
| 158 |
+
driver = webdriver.Chrome(service=service, options=options)
|
| 159 |
+
return driver
|
| 160 |
+
except Exception:
|
| 161 |
+
# Silently fail and continue with other methods
|
| 162 |
+
pass
|
| 163 |
+
|
| 164 |
+
# Method 3: Try using webdriver-manager
|
| 165 |
+
if webdriver_manager_available:
|
| 166 |
+
try:
|
| 167 |
+
service = Service(ChromeDriverManager().install())
|
| 168 |
+
driver = webdriver.Chrome(service=service, options=options)
|
| 169 |
+
return driver
|
| 170 |
+
except Exception:
|
| 171 |
+
# Silently fail and continue with other methods
|
| 172 |
+
pass
|
| 173 |
+
|
| 174 |
+
# Method 4: Try platform-specific approaches
|
| 175 |
+
system = platform.system()
|
| 176 |
+
if system == "Windows":
|
| 177 |
+
try:
|
| 178 |
+
# Try with Chrome binary path
|
| 179 |
+
chrome_paths = [
|
| 180 |
+
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
| 181 |
+
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
|
| 182 |
+
os.path.expandvars(r"%LOCALAPPDATA%\Google\Chrome\Application\chrome.exe")
|
| 183 |
+
]
|
| 184 |
+
|
| 185 |
+
for path in chrome_paths:
|
| 186 |
+
if os.path.exists(path):
|
| 187 |
+
options.binary_location = path
|
| 188 |
+
try:
|
| 189 |
+
driver = webdriver.Chrome(options=options)
|
| 190 |
+
return driver
|
| 191 |
+
except Exception:
|
| 192 |
+
continue
|
| 193 |
+
except Exception:
|
| 194 |
+
# Silently fail and continue with other methods
|
| 195 |
+
pass
|
| 196 |
+
elif system == "Linux":
|
| 197 |
+
try:
|
| 198 |
+
# Try with Chromium binary path
|
| 199 |
+
options.binary_location = "/usr/bin/chromium"
|
| 200 |
+
try:
|
| 201 |
+
driver = webdriver.Chrome(options=options)
|
| 202 |
+
return driver
|
| 203 |
+
except Exception:
|
| 204 |
+
pass
|
| 205 |
+
|
| 206 |
+
# Try with Google Chrome binary path
|
| 207 |
+
options.binary_location = "/usr/bin/google-chrome"
|
| 208 |
+
try:
|
| 209 |
+
driver = webdriver.Chrome(options=options)
|
| 210 |
+
return driver
|
| 211 |
+
except Exception:
|
| 212 |
+
pass
|
| 213 |
+
except Exception:
|
| 214 |
+
# Silently fail and continue with other methods
|
| 215 |
+
pass
|
| 216 |
+
|
| 217 |
+
# All methods failed
|
| 218 |
+
st.error("Failed to initialize Chrome webdriver. Please make sure Chrome is installed.")
|
| 219 |
+
return None
|
packages.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
chromium
|
| 2 |
+
chromium-driver
|
| 3 |
+
libglib2.0-0
|
| 4 |
+
libnss3
|
| 5 |
+
libgconf-2-4
|
| 6 |
+
libfontconfig1
|
| 7 |
+
xvfb
|
| 8 |
+
wget
|
| 9 |
+
unzip
|
poppler/poppler-24.08.0/Library/bin/Lerc.dll
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3383b3f5df29c5b556ccbff8b5c1cbcdcaa423a68cb22bc3bf84c4c36a1bcdbc
|
| 3 |
+
size 519680
|
poppler/poppler-24.08.0/Library/bin/cairo.dll
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c690978c5be8486d06e4363cbc802fe800a3c454d66b64dd19f172292bd20a3f
|
| 3 |
+
size 1016320
|
poppler/poppler-24.08.0/Library/bin/charset.dll
ADDED
|
Binary file (11.8 kB). View file
|
|
|
poppler/poppler-24.08.0/Library/bin/deflate.dll
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b0c144c1bcb58b16d25f43cfb36e5e7fb03f39414fbb2f5b2e14b1874f6c7f27
|
| 3 |
+
size 177152
|
poppler/poppler-24.08.0/Library/bin/expat.dll
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1eef16676ab7cabca520ed0b809bb7f38b23250a7d051942cf1b288aebdb9bfe
|
| 3 |
+
size 402432
|
poppler/poppler-24.08.0/Library/bin/fontconfig-1.dll
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cfce15422bcf0fce28278b2e517741ef718de4e9f86ee8e9234805695005d018
|
| 3 |
+
size 282624
|
poppler/poppler-24.08.0/Library/bin/freetype.dll
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f878121d560f43c398671bc1d0d7a61bf719fa7894eac5a1db665414bcd4096f
|
| 3 |
+
size 670720
|
poppler/poppler-24.08.0/Library/bin/iconv.dll
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cb108fa99613c60b732221c6edf9a7ee0484c2758c63252708969616912c256c
|
| 3 |
+
size 937472
|
poppler/poppler-24.08.0/Library/bin/jpeg8.dll
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b98a020fe2161efb9f8cfe991f8f77c5d497b290422328fe351948d2234838e9
|
| 3 |
+
size 803328
|
poppler/poppler-24.08.0/Library/bin/lcms2.dll
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2f864b9f1bf5839fbc963265a0c9c94a4709124439e5ab5bfcae5f522483a1c4
|
| 3 |
+
size 560128
|
poppler/poppler-24.08.0/Library/bin/libcrypto-3-x64.dll
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:124c5f40a6cf0e642f9d92784dd66314fd548b4fdf93c543bf478e71e1209f9d
|
| 3 |
+
size 6459392
|
poppler/poppler-24.08.0/Library/bin/libcurl.dll
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a70c1cee2d55cf029eedc1a304e087d9276995d7caa0970832faf856d3bd4ae7
|
| 3 |
+
size 617984
|
poppler/poppler-24.08.0/Library/bin/libexpat.dll
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1eef16676ab7cabca520ed0b809bb7f38b23250a7d051942cf1b288aebdb9bfe
|
| 3 |
+
size 402432
|
poppler/poppler-24.08.0/Library/bin/liblzma.dll
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:07a4148f1972c843ddb31dbf1d058a7f73cb375f74473c9220f1476b8b973518
|
| 3 |
+
size 154624
|
poppler/poppler-24.08.0/Library/bin/libpng16.dll
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:54789e18aceb43b5ad8a1c6a3a496b9379dd63d3ce46aefeddbeec87b8da3a08
|
| 3 |
+
size 196608
|
poppler/poppler-24.08.0/Library/bin/libssh2.dll
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4a37f850e18c32d9606a9c5b38039bc4079b9d9f0346a3aab4c34ad7e2e4b5d9
|
| 3 |
+
size 244224
|
poppler/poppler-24.08.0/Library/bin/libtiff.dll
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:58498f3e9e5f8444c9e315de6f35dae090e630978326838770187b5946303bda
|
| 3 |
+
size 493056
|