Spaces:
Paused
Paused
Commit
·
d8acd61
1
Parent(s):
504df0f
fixed-login
Browse files* fixed-login
* Updated after copilot feedback
- .venv/bin/email_validator +10 -0
- backend/app.py +78 -73
- backend/form/AuthForms.py +17 -0
- backend/instance/codingo.db +0 -0
- backend/models/database.py +0 -1
- backend/models/user.py +19 -0
- backend/routes/auth.py +45 -0
- backend/templates/apply.html +13 -12
- backend/templates/base.html +103 -48
- backend/templates/login.html +122 -0
- backend/templates/signup.html +213 -0
- backend/uploads/resumes/Hussein_El_Saadi_-_CV.pdf +0 -0
- backend/uploads/resumes/Mohamad_MoallemCV-2024.pdf +0 -0
- instance/codingo.db +0 -0
- requirements.txt +2 -1
.venv/bin/email_validator
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/bin/sh
|
2 |
+
'''exec' "/Users/husseinelsaadi/Documents/Data Science USAL/Spring 24-25/FYP - Codingo/Codingo/.venv/bin/python3.12" "$0" "$@"
|
3 |
+
' '''
|
4 |
+
# -*- coding: utf-8 -*-
|
5 |
+
import re
|
6 |
+
import sys
|
7 |
+
from email_validator.__main__ import main
|
8 |
+
if __name__ == '__main__':
|
9 |
+
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
10 |
+
sys.exit(main())
|
backend/app.py
CHANGED
@@ -1,102 +1,111 @@
|
|
1 |
from flask import Flask, render_template, redirect, url_for, flash, request
|
|
|
2 |
from werkzeug.utils import secure_filename
|
3 |
import os
|
4 |
import sys
|
5 |
import json
|
6 |
from datetime import datetime
|
7 |
|
8 |
-
#
|
9 |
current_dir = os.path.dirname(os.path.abspath(__file__))
|
10 |
parent_dir = os.path.dirname(current_dir)
|
11 |
sys.path.append(parent_dir)
|
12 |
sys.path.append(current_dir)
|
13 |
|
14 |
-
#
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
|
|
|
|
|
|
|
|
23 |
|
|
|
24 |
try:
|
25 |
-
from
|
26 |
except ImportError:
|
27 |
-
|
28 |
-
from backend.models.database import db, Job, Application, init_db
|
29 |
-
except ImportError:
|
30 |
-
print("Error importing database models. Check the path.")
|
31 |
-
sys.exit(1)
|
32 |
|
33 |
try:
|
34 |
-
from models.resume_parser.resume_to_features import extract_resume_features
|
35 |
except ImportError:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
try:
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
|
42 |
-
|
43 |
-
|
44 |
-
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///codingo.db'
|
45 |
-
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
46 |
-
app.config['UPLOAD_FOLDER'] = 'uploads/resumes'
|
47 |
|
48 |
-
|
49 |
-
|
|
|
|
|
50 |
|
51 |
-
# Initialize the database with the app
|
52 |
-
init_db(app)
|
53 |
|
54 |
# Routes
|
55 |
@app.route('/')
|
56 |
def index():
|
57 |
return render_template('index.html')
|
58 |
|
59 |
-
|
60 |
@app.route('/jobs')
|
61 |
def jobs():
|
62 |
all_jobs = Job.query.order_by(Job.date_posted.desc()).all()
|
63 |
return render_template('jobs.html', jobs=all_jobs)
|
64 |
|
65 |
-
|
66 |
@app.route('/job/<int:job_id>')
|
67 |
def job_detail(job_id):
|
68 |
job = Job.query.get_or_404(job_id)
|
69 |
return render_template('job_detail.html', job=job)
|
70 |
|
71 |
-
|
72 |
@app.route('/apply/<int:job_id>', methods=['GET', 'POST'])
|
|
|
73 |
def apply(job_id):
|
74 |
job = Job.query.get_or_404(job_id)
|
75 |
form = JobApplicationForm()
|
76 |
form.job_id.data = job_id
|
77 |
|
78 |
if form.validate_on_submit():
|
79 |
-
# Save resume file
|
80 |
resume_file = form.resume.data
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
# Extract features from resume
|
87 |
-
try:
|
88 |
-
features = extract_resume_features(resume_path)
|
89 |
-
features_json = json.dumps(features)
|
90 |
-
except Exception as e:
|
91 |
-
print(f"Error extracting features: {e}")
|
92 |
-
features_json = "{}"
|
93 |
-
|
94 |
-
# Create new application
|
95 |
application = Application(
|
96 |
job_id=job_id,
|
97 |
name=form.name.data,
|
98 |
email=form.email.data,
|
99 |
-
resume_path=
|
100 |
cover_letter=form.cover_letter.data,
|
101 |
extracted_features=features_json
|
102 |
)
|
@@ -109,36 +118,32 @@ def apply(job_id):
|
|
109 |
|
110 |
return render_template('apply.html', form=form, job=job)
|
111 |
|
112 |
-
|
113 |
@app.route('/parse_resume', methods=['POST'])
|
114 |
def parse_resume():
|
115 |
-
|
116 |
-
|
117 |
|
118 |
-
|
119 |
-
|
120 |
-
return {"error": "
|
121 |
|
122 |
-
|
123 |
-
|
124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
125 |
|
126 |
-
# Extract features from resume
|
127 |
-
try:
|
128 |
-
features = extract_resume_features(file_path)
|
129 |
-
response = {
|
130 |
-
"name": features.get('name', ''),
|
131 |
-
"email": features.get('email', ''),
|
132 |
-
"mobile_number": features.get('mobile_number', ''),
|
133 |
-
"skills": features.get('skills', []),
|
134 |
-
"experience": features.get('experience', [])
|
135 |
-
}
|
136 |
-
return response, 200
|
137 |
-
except Exception as e:
|
138 |
-
print(f"Error parsing resume: {e}")
|
139 |
-
return {"error": "Failed to parse resume"}, 500
|
140 |
|
141 |
|
142 |
if __name__ == '__main__':
|
143 |
print("Starting Codingo application...")
|
144 |
-
app.
|
|
|
|
|
|
1 |
from flask import Flask, render_template, redirect, url_for, flash, request
|
2 |
+
from flask_login import LoginManager, login_required, current_user
|
3 |
from werkzeug.utils import secure_filename
|
4 |
import os
|
5 |
import sys
|
6 |
import json
|
7 |
from datetime import datetime
|
8 |
|
9 |
+
# Adjust sys.path for import flexibility
|
10 |
current_dir = os.path.dirname(os.path.abspath(__file__))
|
11 |
parent_dir = os.path.dirname(current_dir)
|
12 |
sys.path.append(parent_dir)
|
13 |
sys.path.append(current_dir)
|
14 |
|
15 |
+
# Initialize Flask app
|
16 |
+
app = Flask(__name__)
|
17 |
+
app.config['SECRET_KEY'] = 'your-secret-key'
|
18 |
+
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///codingo.db'
|
19 |
+
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
20 |
+
|
21 |
+
# Import and initialize DB
|
22 |
+
from backend.models.database import db, Job, Application, init_db
|
23 |
+
init_db(app)
|
24 |
+
|
25 |
+
# Import models/routes AFTER initializing the DB
|
26 |
+
from backend.models.user import User
|
27 |
+
from backend.routes.auth import auth_bp
|
28 |
|
29 |
+
# Import other modules
|
30 |
try:
|
31 |
+
from backend.form.JobApplicationForm import JobApplicationForm
|
32 |
except ImportError:
|
33 |
+
from form.JobApplicationForm import JobApplicationForm
|
|
|
|
|
|
|
|
|
34 |
|
35 |
try:
|
36 |
+
from backend.models.resume_parser.resume_to_features import extract_resume_features
|
37 |
except ImportError:
|
38 |
+
from models.resume_parser.resume_to_features import extract_resume_features
|
39 |
+
|
40 |
+
# Flask-Login setup
|
41 |
+
login_manager = LoginManager()
|
42 |
+
login_manager.login_view = 'auth.login'
|
43 |
+
login_manager.init_app(app)
|
44 |
+
|
45 |
+
@login_manager.user_loader
|
46 |
+
def load_user(user_id):
|
47 |
+
return db.session.get(User, int(user_id))
|
48 |
+
|
49 |
+
# Register auth blueprint
|
50 |
+
app.register_blueprint(auth_bp)
|
51 |
+
|
52 |
+
|
53 |
+
def handle_resume_upload(file):
|
54 |
+
"""
|
55 |
+
Save uploaded file temporarily, extract features, then clean up.
|
56 |
+
Returns (features_dict, error_message, filename)
|
57 |
+
"""
|
58 |
+
if not file or file.filename == '':
|
59 |
+
return None, "No file uploaded", None
|
60 |
+
|
61 |
try:
|
62 |
+
filename = secure_filename(file.filename)
|
63 |
+
filepath = os.path.join(current_dir, 'temp', filename)
|
64 |
+
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
65 |
+
file.save(filepath)
|
66 |
|
67 |
+
features = extract_resume_features(filepath)
|
68 |
+
os.remove(filepath) # Clean up after parsing
|
|
|
|
|
|
|
69 |
|
70 |
+
return features, None, filename
|
71 |
+
except Exception as e:
|
72 |
+
print(f"Error in handle_resume_upload: {e}")
|
73 |
+
return None, str(e), None
|
74 |
|
|
|
|
|
75 |
|
76 |
# Routes
|
77 |
@app.route('/')
|
78 |
def index():
|
79 |
return render_template('index.html')
|
80 |
|
|
|
81 |
@app.route('/jobs')
|
82 |
def jobs():
|
83 |
all_jobs = Job.query.order_by(Job.date_posted.desc()).all()
|
84 |
return render_template('jobs.html', jobs=all_jobs)
|
85 |
|
|
|
86 |
@app.route('/job/<int:job_id>')
|
87 |
def job_detail(job_id):
|
88 |
job = Job.query.get_or_404(job_id)
|
89 |
return render_template('job_detail.html', job=job)
|
90 |
|
|
|
91 |
@app.route('/apply/<int:job_id>', methods=['GET', 'POST'])
|
92 |
+
@login_required
|
93 |
def apply(job_id):
|
94 |
job = Job.query.get_or_404(job_id)
|
95 |
form = JobApplicationForm()
|
96 |
form.job_id.data = job_id
|
97 |
|
98 |
if form.validate_on_submit():
|
|
|
99 |
resume_file = form.resume.data
|
100 |
+
|
101 |
+
features, error, filename = handle_resume_upload(resume_file)
|
102 |
+
features_json = json.dumps(features or {})
|
103 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
104 |
application = Application(
|
105 |
job_id=job_id,
|
106 |
name=form.name.data,
|
107 |
email=form.email.data,
|
108 |
+
resume_path=filename,
|
109 |
cover_letter=form.cover_letter.data,
|
110 |
extracted_features=features_json
|
111 |
)
|
|
|
118 |
|
119 |
return render_template('apply.html', form=form, job=job)
|
120 |
|
|
|
121 |
@app.route('/parse_resume', methods=['POST'])
|
122 |
def parse_resume():
|
123 |
+
file = request.files.get('resume')
|
124 |
+
features, error, _ = handle_resume_upload(file)
|
125 |
|
126 |
+
if error:
|
127 |
+
print(f"[Resume Error] {error}")
|
128 |
+
return {"error": "Error parsing resume. Please try again."}, 400
|
129 |
|
130 |
+
if not features:
|
131 |
+
print("[Resume Error] No features extracted.")
|
132 |
+
return {"error": "Failed to extract resume details."}, 400
|
133 |
+
|
134 |
+
response = {
|
135 |
+
"name": features.get('name', ''),
|
136 |
+
"email": features.get('email', ''),
|
137 |
+
"mobile_number": features.get('mobile_number', ''),
|
138 |
+
"skills": features.get('skills', []),
|
139 |
+
"experience": features.get('experience', [])
|
140 |
+
}
|
141 |
+
return response, 200
|
142 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
143 |
|
144 |
|
145 |
if __name__ == '__main__':
|
146 |
print("Starting Codingo application...")
|
147 |
+
with app.app_context():
|
148 |
+
db.create_all()
|
149 |
+
app.run(debug=True, port=5001)
|
backend/form/AuthForms.py
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
from flask_wtf import FlaskForm
|
3 |
+
from wtforms import StringField, PasswordField, SubmitField, SelectField
|
4 |
+
from wtforms.validators import DataRequired, Email, EqualTo
|
5 |
+
|
6 |
+
class LoginForm(FlaskForm):
|
7 |
+
email = StringField('Email', validators=[DataRequired(), Email()])
|
8 |
+
password = PasswordField('Password', validators=[DataRequired()])
|
9 |
+
submit = SubmitField('Login')
|
10 |
+
|
11 |
+
class SignupForm(FlaskForm):
|
12 |
+
username = StringField('Username', validators=[DataRequired()])
|
13 |
+
email = StringField('Email', validators=[DataRequired(), Email()])
|
14 |
+
password = PasswordField('Password', validators=[DataRequired()])
|
15 |
+
confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
|
16 |
+
role = SelectField('Role', choices=[('unemployed', 'Unemployed'), ('recruiter', 'Recruiter')])
|
17 |
+
submit = SubmitField('Sign Up')
|
backend/instance/codingo.db
CHANGED
Binary files a/backend/instance/codingo.db and b/backend/instance/codingo.db differ
|
|
backend/models/database.py
CHANGED
@@ -7,7 +7,6 @@ from datetime import datetime
|
|
7 |
|
8 |
db = SQLAlchemy()
|
9 |
|
10 |
-
|
11 |
class Job(db.Model):
|
12 |
"""Job model representing a job posting."""
|
13 |
id = db.Column(db.Integer, primary_key=True)
|
|
|
7 |
|
8 |
db = SQLAlchemy()
|
9 |
|
|
|
10 |
class Job(db.Model):
|
11 |
"""Job model representing a job posting."""
|
12 |
id = db.Column(db.Integer, primary_key=True)
|
backend/models/user.py
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask_login import UserMixin
|
2 |
+
from werkzeug.security import generate_password_hash, check_password_hash
|
3 |
+
from backend.models.database import db # ✅ Use shared db instance only
|
4 |
+
|
5 |
+
class User(UserMixin, db.Model):
|
6 |
+
__tablename__ = 'user'
|
7 |
+
__table_args__ = {'extend_existing': True}
|
8 |
+
|
9 |
+
id = db.Column(db.Integer, primary_key=True)
|
10 |
+
username = db.Column(db.String(150), unique=True, nullable=False)
|
11 |
+
email = db.Column(db.String(150), unique=True, nullable=False)
|
12 |
+
password_hash = db.Column(db.String(256), nullable=False)
|
13 |
+
role = db.Column(db.String(50), nullable=False)
|
14 |
+
|
15 |
+
def set_password(self, password):
|
16 |
+
self.password_hash = generate_password_hash(password)
|
17 |
+
|
18 |
+
def check_password(self, password):
|
19 |
+
return check_password_hash(self.password_hash, password)
|
backend/routes/auth.py
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
2 |
+
from flask_login import login_user, logout_user, login_required
|
3 |
+
from backend.form.AuthForms import LoginForm, SignupForm
|
4 |
+
from backend.models.user import User
|
5 |
+
from backend.models.database import db
|
6 |
+
|
7 |
+
auth_bp = Blueprint('auth', __name__)
|
8 |
+
|
9 |
+
@auth_bp.route('/login', methods=['GET', 'POST'])
|
10 |
+
def login():
|
11 |
+
form = LoginForm()
|
12 |
+
if form.validate_on_submit():
|
13 |
+
user = User.query.filter_by(email=form.email.data).first()
|
14 |
+
if user and user.check_password(form.password.data):
|
15 |
+
login_user(user)
|
16 |
+
return redirect(url_for('index'))
|
17 |
+
else:
|
18 |
+
flash('Invalid credentials', 'danger')
|
19 |
+
return render_template('login.html', form=form)
|
20 |
+
|
21 |
+
@auth_bp.route('/signup', methods=['GET', 'POST'])
|
22 |
+
def signup():
|
23 |
+
form = SignupForm()
|
24 |
+
if form.validate_on_submit():
|
25 |
+
existing_user = User.query.filter_by(email=form.email.data).first()
|
26 |
+
if existing_user:
|
27 |
+
flash('Email already registered.', 'warning')
|
28 |
+
else:
|
29 |
+
user = User(
|
30 |
+
username=form.username.data,
|
31 |
+
email=form.email.data,
|
32 |
+
role=form.role.data
|
33 |
+
)
|
34 |
+
user.set_password(form.password.data)
|
35 |
+
db.session.add(user)
|
36 |
+
db.session.commit()
|
37 |
+
flash('Account created successfully!', 'success')
|
38 |
+
return redirect(url_for('auth.login'))
|
39 |
+
return render_template('signup.html', form=form)
|
40 |
+
|
41 |
+
@auth_bp.route('/logout')
|
42 |
+
@login_required
|
43 |
+
def logout():
|
44 |
+
logout_user()
|
45 |
+
return redirect(url_for('auth.login'))
|
backend/templates/apply.html
CHANGED
@@ -21,7 +21,7 @@
|
|
21 |
<li><a href="{{ url_for('job_detail', job_id=job.id) }}">{{ job.title }}</a></li>
|
22 |
<li>Apply</li>
|
23 |
</ul>
|
24 |
-
|
25 |
<div class="card">
|
26 |
<div class="card-header">
|
27 |
<h2>Complete Your Application</h2>
|
@@ -29,17 +29,15 @@
|
|
29 |
</div>
|
30 |
|
31 |
<div class="card-body">
|
32 |
-
<form id="resumeForm" method="POST" enctype="multipart/form-data">
|
33 |
-
<div class="form-group">
|
34 |
-
<label for="resume">Upload Resume</label>
|
35 |
-
<input type="file" id="resume" name="resume" class="form-control" required>
|
36 |
-
</div>
|
37 |
-
</form>
|
38 |
-
|
39 |
<form method="POST" enctype="multipart/form-data">
|
40 |
{{ form.hidden_tag() }}
|
41 |
{{ form.job_id }}
|
42 |
|
|
|
|
|
|
|
|
|
|
|
43 |
<div class="form-group">
|
44 |
{{ form.name.label }}
|
45 |
{{ form.name(class="form-control", id="name", placeholder="Enter your full name") }}
|
@@ -57,12 +55,14 @@
|
|
57 |
|
58 |
<div class="form-group">
|
59 |
<label for="skills">Skills</label>
|
60 |
-
<textarea id="skills" class="form-control"
|
|
|
61 |
</div>
|
62 |
|
63 |
<div class="form-group">
|
64 |
<label for="experience">Previous Experience</label>
|
65 |
-
<textarea id="experience" class="form-control"
|
|
|
66 |
</div>
|
67 |
|
68 |
<div class="form-group">
|
@@ -75,17 +75,18 @@
|
|
75 |
{{ form.submit(class="btn btn-primary") }}
|
76 |
</div>
|
77 |
</form>
|
|
|
78 |
</div>
|
79 |
</div>
|
80 |
</section>
|
81 |
|
82 |
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
83 |
<script>
|
84 |
-
$(document).ready(function() {
|
85 |
$("#resume").on("change", function () {
|
86 |
var formData = new FormData();
|
87 |
formData.append("resume", $("#resume")[0].files[0]);
|
88 |
-
|
89 |
$.ajax({
|
90 |
url: "{{ url_for('parse_resume') }}",
|
91 |
type: "POST",
|
|
|
21 |
<li><a href="{{ url_for('job_detail', job_id=job.id) }}">{{ job.title }}</a></li>
|
22 |
<li>Apply</li>
|
23 |
</ul>
|
24 |
+
|
25 |
<div class="card">
|
26 |
<div class="card-header">
|
27 |
<h2>Complete Your Application</h2>
|
|
|
29 |
</div>
|
30 |
|
31 |
<div class="card-body">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
<form method="POST" enctype="multipart/form-data">
|
33 |
{{ form.hidden_tag() }}
|
34 |
{{ form.job_id }}
|
35 |
|
36 |
+
<div class="form-group">
|
37 |
+
<label for="resume">Upload Resume</label>
|
38 |
+
{{ form.resume(class="form-control", id="resume", required=True) }}
|
39 |
+
</div>
|
40 |
+
|
41 |
<div class="form-group">
|
42 |
{{ form.name.label }}
|
43 |
{{ form.name(class="form-control", id="name", placeholder="Enter your full name") }}
|
|
|
55 |
|
56 |
<div class="form-group">
|
57 |
<label for="skills">Skills</label>
|
58 |
+
<textarea id="skills" class="form-control"
|
59 |
+
placeholder="Skills extracted from your resume..."></textarea>
|
60 |
</div>
|
61 |
|
62 |
<div class="form-group">
|
63 |
<label for="experience">Previous Experience</label>
|
64 |
+
<textarea id="experience" class="form-control"
|
65 |
+
placeholder="Experience extracted from your resume..."></textarea>
|
66 |
</div>
|
67 |
|
68 |
<div class="form-group">
|
|
|
75 |
{{ form.submit(class="btn btn-primary") }}
|
76 |
</div>
|
77 |
</form>
|
78 |
+
|
79 |
</div>
|
80 |
</div>
|
81 |
</section>
|
82 |
|
83 |
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
84 |
<script>
|
85 |
+
$(document).ready(function () {
|
86 |
$("#resume").on("change", function () {
|
87 |
var formData = new FormData();
|
88 |
formData.append("resume", $("#resume")[0].files[0]);
|
89 |
+
|
90 |
$.ajax({
|
91 |
url: "{{ url_for('parse_resume') }}",
|
92 |
type: "POST",
|
backend/templates/base.html
CHANGED
@@ -94,9 +94,46 @@
|
|
94 |
|
95 |
.login-buttons {
|
96 |
display: flex;
|
|
|
97 |
gap: 1rem;
|
98 |
}
|
99 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
100 |
.btn {
|
101 |
padding: 0.5rem 1.5rem;
|
102 |
border-radius: 5px;
|
@@ -248,14 +285,26 @@
|
|
248 |
}
|
249 |
|
250 |
@keyframes pulse {
|
251 |
-
0% {
|
252 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
253 |
}
|
254 |
|
255 |
@keyframes float {
|
256 |
-
0% {
|
257 |
-
|
258 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
259 |
}
|
260 |
|
261 |
.luna-avatar img {
|
@@ -679,53 +728,59 @@
|
|
679 |
</style>
|
680 |
</head>
|
681 |
<body>
|
682 |
-
|
683 |
-
|
684 |
-
|
685 |
-
|
686 |
-
|
687 |
-
|
688 |
-
|
689 |
-
|
690 |
-
</
|
|
|
|
|
|
|
|
|
691 |
</div>
|
692 |
-
|
693 |
-
|
694 |
-
|
695 |
-
|
696 |
-
|
697 |
-
|
698 |
-
|
699 |
-
|
700 |
-
|
701 |
-
|
702 |
-
|
703 |
-
|
704 |
-
|
705 |
-
|
706 |
-
{% endwith %}
|
707 |
-
|
708 |
-
{% block content %}{% endblock %}
|
709 |
</div>
|
710 |
-
|
711 |
-
|
712 |
-
|
713 |
-
|
714 |
-
|
715 |
-
|
716 |
-
|
717 |
-
|
718 |
-
|
719 |
-
|
720 |
-
|
721 |
-
|
722 |
-
|
|
|
|
|
|
|
|
|
723 |
</div>
|
724 |
</div>
|
725 |
-
<div class="copyright">
|
726 |
-
<p>© 2025 Codingo. All rights reserved.</p>
|
727 |
-
</div>
|
728 |
</div>
|
729 |
-
|
|
|
|
|
|
|
|
|
730 |
</body>
|
731 |
</html>
|
|
|
94 |
|
95 |
.login-buttons {
|
96 |
display: flex;
|
97 |
+
align-items: center;
|
98 |
gap: 1rem;
|
99 |
}
|
100 |
|
101 |
+
/* Style for the welcome message */
|
102 |
+
.welcome-message {
|
103 |
+
color: white;
|
104 |
+
font-weight: 500;
|
105 |
+
margin-right: 0.5rem;
|
106 |
+
display: flex;
|
107 |
+
align-items: center;
|
108 |
+
background-color: rgba(255, 255, 255, 0.1);
|
109 |
+
padding: 0.5rem 1rem;
|
110 |
+
border-radius: 5px;
|
111 |
+
transition: all 0.3s ease;
|
112 |
+
}
|
113 |
+
|
114 |
+
.welcome-message:before {
|
115 |
+
content: '👋 ';
|
116 |
+
margin-right: 0.5rem;
|
117 |
+
}
|
118 |
+
|
119 |
+
/* Enhanced logout button */
|
120 |
+
.btn-logout {
|
121 |
+
background-color: transparent;
|
122 |
+
border: 2px solid var(--accent);
|
123 |
+
color: var(--accent);
|
124 |
+
font-weight: 600;
|
125 |
+
padding: 0.5rem 1.5rem;
|
126 |
+
border-radius: 5px;
|
127 |
+
transition: all 0.3s ease;
|
128 |
+
}
|
129 |
+
|
130 |
+
.btn-logout:hover {
|
131 |
+
background-color: var(--accent);
|
132 |
+
color: var(--dark);
|
133 |
+
transform: translateY(-2px);
|
134 |
+
box-shadow: 0 5px 15px rgba(76, 201, 240, 0.3);
|
135 |
+
}
|
136 |
+
|
137 |
.btn {
|
138 |
padding: 0.5rem 1.5rem;
|
139 |
border-radius: 5px;
|
|
|
285 |
}
|
286 |
|
287 |
@keyframes pulse {
|
288 |
+
0% {
|
289 |
+
transform: scale(0.9);
|
290 |
+
opacity: 0.5;
|
291 |
+
}
|
292 |
+
100% {
|
293 |
+
transform: scale(1.1);
|
294 |
+
opacity: 0.7;
|
295 |
+
}
|
296 |
}
|
297 |
|
298 |
@keyframes float {
|
299 |
+
0% {
|
300 |
+
transform: translateY(0px) rotateY(0deg);
|
301 |
+
}
|
302 |
+
50% {
|
303 |
+
transform: translateY(-15px) rotateY(5deg);
|
304 |
+
}
|
305 |
+
100% {
|
306 |
+
transform: translateY(0px) rotateY(0deg);
|
307 |
+
}
|
308 |
}
|
309 |
|
310 |
.luna-avatar img {
|
|
|
728 |
</style>
|
729 |
</head>
|
730 |
<body>
|
731 |
+
<header>
|
732 |
+
<div class="container nav-container">
|
733 |
+
<a href="{{ url_for('index') }}" class="logo">
|
734 |
+
<span class="logo-part1">Cod</span><span class="logo-part2">in</span><span class="logo-part3">go</span>
|
735 |
+
</a>
|
736 |
+
<div class="login-buttons">
|
737 |
+
{% if current_user.is_authenticated %}
|
738 |
+
<span class="welcome-message">Welcome, {{ current_user.username }}</span>
|
739 |
+
<a href="{{ url_for('auth.logout') }}" class="btn btn-logout">Logout</a>
|
740 |
+
{% else %}
|
741 |
+
<a href="{{ url_for('auth.login') }}" class="btn btn-outline">Log In</a>
|
742 |
+
<a href="{{ url_for('auth.signup') }}" class="btn btn-primary">Sign Up</a>
|
743 |
+
{% endif %}
|
744 |
</div>
|
745 |
+
|
746 |
+
</div>
|
747 |
+
</header>
|
748 |
+
|
749 |
+
{% block hero %}{% endblock %}
|
750 |
+
|
751 |
+
<main>
|
752 |
+
<div class="container">
|
753 |
+
{% with messages = get_flashed_messages(with_categories=true) %}
|
754 |
+
{% if messages %}
|
755 |
+
<div class="flash-messages">
|
756 |
+
{% for category, message in messages %}
|
757 |
+
<div class="alert alert-{{ category }}">{{ message }}</div>
|
758 |
+
{% endfor %}
|
|
|
|
|
|
|
759 |
</div>
|
760 |
+
{% endif %}
|
761 |
+
{% endwith %}
|
762 |
+
|
763 |
+
{% block content %}{% endblock %}
|
764 |
+
</div>
|
765 |
+
</main>
|
766 |
+
|
767 |
+
<footer>
|
768 |
+
<div class="container">
|
769 |
+
<div class="footer-grid">
|
770 |
+
<div class="footer-col">
|
771 |
+
<h3>Codingo</h3>
|
772 |
+
<p>AI-powered recruitment platform that revolutionizes how companies hire technical talent.</p>
|
773 |
+
<div class="social-links">
|
774 |
+
<a href="#"><span>f</span></a>
|
775 |
+
<a href="#"><span>t</span></a>
|
776 |
+
<a href="#"><span>in</span></a>
|
777 |
</div>
|
778 |
</div>
|
|
|
|
|
|
|
779 |
</div>
|
780 |
+
<div class="copyright">
|
781 |
+
<p>© 2025 Codingo. All rights reserved.</p>
|
782 |
+
</div>
|
783 |
+
</div>
|
784 |
+
</footer>
|
785 |
</body>
|
786 |
</html>
|
backend/templates/login.html
ADDED
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends "base.html" %}
|
2 |
+
|
3 |
+
{% block title %}Codingo - Login{% endblock %}
|
4 |
+
|
5 |
+
{% block content %}
|
6 |
+
<section class="content-section">
|
7 |
+
<div class="container">
|
8 |
+
<div class="card" style="max-width: 500px; margin: 2rem auto;">
|
9 |
+
<div class="card-header">
|
10 |
+
<h2 style="text-align: center; margin: 0; color: white;">Login to Codingo</h2>
|
11 |
+
</div>
|
12 |
+
<div class="card-body">
|
13 |
+
<form method="POST" class="auth-form">
|
14 |
+
{{ form.hidden_tag() }}
|
15 |
+
|
16 |
+
<div class="form-group">
|
17 |
+
{{ form.email.label(class="form-label") }}
|
18 |
+
{{ form.email(class="form-control", placeholder="Enter your email") }}
|
19 |
+
{% if form.email.errors %}
|
20 |
+
<div class="alert alert-danger">
|
21 |
+
{% for error in form.email.errors %}
|
22 |
+
<span>{{ error }}</span>
|
23 |
+
{% endfor %}
|
24 |
+
</div>
|
25 |
+
{% endif %}
|
26 |
+
</div>
|
27 |
+
|
28 |
+
<div class="form-group">
|
29 |
+
{{ form.password.label(class="form-label") }}
|
30 |
+
{{ form.password(class="form-control", placeholder="Enter your password") }}
|
31 |
+
{% if form.password.errors %}
|
32 |
+
<div class="alert alert-danger">
|
33 |
+
{% for error in form.password.errors %}
|
34 |
+
<span>{{ error }}</span>
|
35 |
+
{% endfor %}
|
36 |
+
</div>
|
37 |
+
{% endif %}
|
38 |
+
</div>
|
39 |
+
|
40 |
+
<div class="form-group" style="margin-top: 2rem;">
|
41 |
+
{{ form.submit(class="btn btn-primary", style="width: 100%;") }}
|
42 |
+
</div>
|
43 |
+
</form>
|
44 |
+
|
45 |
+
<div style="text-align: center; margin-top: 1.5rem;">
|
46 |
+
<p>Don't have an account? <a href="{{ url_for('auth.signup') }}" style="color: var(--primary); font-weight: 500;">Sign up here</a>.</p>
|
47 |
+
</div>
|
48 |
+
</div>
|
49 |
+
</div>
|
50 |
+
</div>
|
51 |
+
</section>
|
52 |
+
|
53 |
+
<style>
|
54 |
+
.auth-form .form-label {
|
55 |
+
display: block;
|
56 |
+
margin-bottom: 0.5rem;
|
57 |
+
font-weight: 500;
|
58 |
+
color: var(--dark);
|
59 |
+
}
|
60 |
+
|
61 |
+
.auth-form .form-control {
|
62 |
+
width: 100%;
|
63 |
+
padding: 0.75rem;
|
64 |
+
border: 1px solid #ddd;
|
65 |
+
border-radius: 5px;
|
66 |
+
font-size: 1rem;
|
67 |
+
transition: all 0.3s ease;
|
68 |
+
}
|
69 |
+
|
70 |
+
.auth-form .form-control:focus {
|
71 |
+
border-color: var(--primary);
|
72 |
+
outline: none;
|
73 |
+
box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.2);
|
74 |
+
}
|
75 |
+
|
76 |
+
.auth-form .btn-primary {
|
77 |
+
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
78 |
+
color: white;
|
79 |
+
padding: 0.75rem;
|
80 |
+
border: none;
|
81 |
+
border-radius: 5px;
|
82 |
+
font-size: 1rem;
|
83 |
+
font-weight: 500;
|
84 |
+
cursor: pointer;
|
85 |
+
transition: all 0.3s ease;
|
86 |
+
position: relative;
|
87 |
+
overflow: hidden;
|
88 |
+
z-index: 1;
|
89 |
+
}
|
90 |
+
|
91 |
+
.auth-form .btn-primary::before {
|
92 |
+
content: '';
|
93 |
+
position: absolute;
|
94 |
+
top: 0;
|
95 |
+
left: 0;
|
96 |
+
width: 0%;
|
97 |
+
height: 100%;
|
98 |
+
background-color: rgba(255, 255, 255, 0.1);
|
99 |
+
transition: all 0.3s ease;
|
100 |
+
z-index: -1;
|
101 |
+
}
|
102 |
+
|
103 |
+
.auth-form .btn-primary:hover::before {
|
104 |
+
width: 100%;
|
105 |
+
}
|
106 |
+
|
107 |
+
.auth-form .btn-primary:hover {
|
108 |
+
transform: translateY(-2px);
|
109 |
+
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
110 |
+
}
|
111 |
+
|
112 |
+
.alert-danger {
|
113 |
+
color: var(--danger);
|
114 |
+
background-color: rgba(231, 76, 60, 0.1);
|
115 |
+
border: 1px solid var(--danger);
|
116 |
+
border-radius: 5px;
|
117 |
+
padding: 0.5rem;
|
118 |
+
margin-top: 0.5rem;
|
119 |
+
font-size: 0.9rem;
|
120 |
+
}
|
121 |
+
</style>
|
122 |
+
{% endblock %}
|
backend/templates/signup.html
ADDED
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends "base.html" %}
|
2 |
+
|
3 |
+
{% block title %}Codingo - Sign Up{% endblock %}
|
4 |
+
|
5 |
+
{% block content %}
|
6 |
+
<section class="content-section">
|
7 |
+
<div class="container">
|
8 |
+
<div class="card" style="max-width: 500px; margin: 2rem auto;">
|
9 |
+
<div class="card-header">
|
10 |
+
<h2 style="text-align: center; margin: 0; color: white;">Sign Up for Codingo</h2>
|
11 |
+
</div>
|
12 |
+
<div class="card-body">
|
13 |
+
<form method="POST" class="auth-form">
|
14 |
+
{{ form.hidden_tag() }}
|
15 |
+
|
16 |
+
<div class="form-group">
|
17 |
+
{{ form.username.label(class="form-label") }}
|
18 |
+
{{ form.username(class="form-control", placeholder="Choose a username") }}
|
19 |
+
{% if form.username.errors %}
|
20 |
+
<div class="alert alert-danger">
|
21 |
+
{% for error in form.username.errors %}
|
22 |
+
<span>{{ error }}</span>
|
23 |
+
{% endfor %}
|
24 |
+
</div>
|
25 |
+
{% endif %}
|
26 |
+
</div>
|
27 |
+
|
28 |
+
<div class="form-group">
|
29 |
+
{{ form.email.label(class="form-label") }}
|
30 |
+
{{ form.email(class="form-control", placeholder="Enter your email") }}
|
31 |
+
{% if form.email.errors %}
|
32 |
+
<div class="alert alert-danger">
|
33 |
+
{% for error in form.email.errors %}
|
34 |
+
<span>{{ error }}</span>
|
35 |
+
{% endfor %}
|
36 |
+
</div>
|
37 |
+
{% endif %}
|
38 |
+
</div>
|
39 |
+
|
40 |
+
<div class="form-group">
|
41 |
+
{{ form.password.label(class="form-label") }}
|
42 |
+
{{ form.password(class="form-control", placeholder="Create a password") }}
|
43 |
+
{% if form.password.errors %}
|
44 |
+
<div class="alert alert-danger">
|
45 |
+
{% for error in form.password.errors %}
|
46 |
+
<span>{{ error }}</span>
|
47 |
+
{% endfor %}
|
48 |
+
</div>
|
49 |
+
{% endif %}
|
50 |
+
<div class="password-strength">
|
51 |
+
<div class="password-strength-bar" id="password-strength-bar"></div>
|
52 |
+
</div>
|
53 |
+
</div>
|
54 |
+
|
55 |
+
<div class="form-group">
|
56 |
+
{{ form.confirm_password.label(class="form-label") }}
|
57 |
+
{{ form.confirm_password(class="form-control", placeholder="Confirm your password") }}
|
58 |
+
{% if form.confirm_password.errors %}
|
59 |
+
<div class="alert alert-danger">
|
60 |
+
{% for error in form.confirm_password.errors %}
|
61 |
+
<span>{{ error }}</span>
|
62 |
+
{% endfor %}
|
63 |
+
</div>
|
64 |
+
{% endif %}
|
65 |
+
</div>
|
66 |
+
|
67 |
+
<div class="form-group">
|
68 |
+
{{ form.role.label(class="form-label") }}
|
69 |
+
{{ form.role(class="form-control") }}
|
70 |
+
{% if form.role.errors %}
|
71 |
+
<div class="alert alert-danger">
|
72 |
+
{% for error in form.role.errors %}
|
73 |
+
<span>{{ error }}</span>
|
74 |
+
{% endfor %}
|
75 |
+
</div>
|
76 |
+
{% endif %}
|
77 |
+
</div>
|
78 |
+
|
79 |
+
<div class="form-group" style="margin-top: 2rem;">
|
80 |
+
{{ form.submit(class="btn btn-primary", style="width: 100%;") }}
|
81 |
+
</div>
|
82 |
+
</form>
|
83 |
+
|
84 |
+
<div style="text-align: center; margin-top: 1.5rem;">
|
85 |
+
<p>Already have an account? <a href="{{ url_for('auth.login') }}" style="color: var(--primary); font-weight: 500;">Login here</a>.</p>
|
86 |
+
</div>
|
87 |
+
</div>
|
88 |
+
</div>
|
89 |
+
</div>
|
90 |
+
</section>
|
91 |
+
|
92 |
+
<style>
|
93 |
+
.auth-form .form-label {
|
94 |
+
display: block;
|
95 |
+
margin-bottom: 0.5rem;
|
96 |
+
font-weight: 500;
|
97 |
+
color: var(--dark);
|
98 |
+
}
|
99 |
+
|
100 |
+
.auth-form .form-control {
|
101 |
+
width: 100%;
|
102 |
+
padding: 0.75rem;
|
103 |
+
border: 1px solid #ddd;
|
104 |
+
border-radius: 5px;
|
105 |
+
font-size: 1rem;
|
106 |
+
transition: all 0.3s ease;
|
107 |
+
}
|
108 |
+
|
109 |
+
.auth-form .form-control:focus {
|
110 |
+
border-color: var(--primary);
|
111 |
+
outline: none;
|
112 |
+
box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.2);
|
113 |
+
}
|
114 |
+
|
115 |
+
.auth-form .btn-primary {
|
116 |
+
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
117 |
+
color: white;
|
118 |
+
padding: 0.75rem;
|
119 |
+
border: none;
|
120 |
+
border-radius: 5px;
|
121 |
+
font-size: 1rem;
|
122 |
+
font-weight: 500;
|
123 |
+
cursor: pointer;
|
124 |
+
transition: all 0.3s ease;
|
125 |
+
position: relative;
|
126 |
+
overflow: hidden;
|
127 |
+
z-index: 1;
|
128 |
+
}
|
129 |
+
|
130 |
+
.auth-form .btn-primary::before {
|
131 |
+
content: '';
|
132 |
+
position: absolute;
|
133 |
+
top: 0;
|
134 |
+
left: 0;
|
135 |
+
width: 0%;
|
136 |
+
height: 100%;
|
137 |
+
background-color: rgba(255, 255, 255, 0.1);
|
138 |
+
transition: all 0.3s ease;
|
139 |
+
z-index: -1;
|
140 |
+
}
|
141 |
+
|
142 |
+
.auth-form .btn-primary:hover::before {
|
143 |
+
width: 100%;
|
144 |
+
}
|
145 |
+
|
146 |
+
.auth-form .btn-primary:hover {
|
147 |
+
transform: translateY(-2px);
|
148 |
+
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
149 |
+
}
|
150 |
+
|
151 |
+
.alert-danger {
|
152 |
+
color: var(--danger);
|
153 |
+
background-color: rgba(231, 76, 60, 0.1);
|
154 |
+
border: 1px solid var(--danger);
|
155 |
+
border-radius: 5px;
|
156 |
+
padding: 0.5rem;
|
157 |
+
margin-top: 0.5rem;
|
158 |
+
font-size: 0.9rem;
|
159 |
+
}
|
160 |
+
|
161 |
+
.password-strength {
|
162 |
+
height: 5px;
|
163 |
+
width: 100%;
|
164 |
+
background-color: #eee;
|
165 |
+
margin-top: 5px;
|
166 |
+
border-radius: 3px;
|
167 |
+
overflow: hidden;
|
168 |
+
}
|
169 |
+
|
170 |
+
.password-strength-bar {
|
171 |
+
height: 100%;
|
172 |
+
width: 0;
|
173 |
+
border-radius: 3px;
|
174 |
+
transition: width 0.3s, background-color 0.3s;
|
175 |
+
}
|
176 |
+
</style>
|
177 |
+
|
178 |
+
<script>
|
179 |
+
document.addEventListener('DOMContentLoaded', function() {
|
180 |
+
const passwordInput = document.querySelector('input[name="password"]');
|
181 |
+
const strengthBar = document.getElementById('password-strength-bar');
|
182 |
+
|
183 |
+
if (passwordInput && strengthBar) {
|
184 |
+
passwordInput.addEventListener('input', function() {
|
185 |
+
const password = this.value;
|
186 |
+
const strength = checkPasswordStrength(password);
|
187 |
+
|
188 |
+
strengthBar.style.width = `${strength.percentage}%`;
|
189 |
+
strengthBar.style.backgroundColor = strength.color;
|
190 |
+
});
|
191 |
+
}
|
192 |
+
|
193 |
+
function checkPasswordStrength(password) {
|
194 |
+
const requirements = {
|
195 |
+
length: password.length >= 8,
|
196 |
+
uppercase: /[A-Z]/.test(password),
|
197 |
+
lowercase: /[a-z]/.test(password),
|
198 |
+
number: /\d/.test(password),
|
199 |
+
special: /[^A-Za-z0-9]/.test(password)
|
200 |
+
};
|
201 |
+
|
202 |
+
const strength = Object.values(requirements).filter(Boolean).length;
|
203 |
+
const percentage = (strength / 5) * 100;
|
204 |
+
|
205 |
+
return {
|
206 |
+
valid: Object.values(requirements).every(Boolean),
|
207 |
+
percentage,
|
208 |
+
color: strength < 2 ? '#e74c3c' : strength < 4 ? '#f39c12' : '#27ae60'
|
209 |
+
};
|
210 |
+
}
|
211 |
+
});
|
212 |
+
</script>
|
213 |
+
{% endblock %}
|
backend/uploads/resumes/Hussein_El_Saadi_-_CV.pdf
DELETED
Binary file (72.9 kB)
|
|
backend/uploads/resumes/Mohamad_MoallemCV-2024.pdf
DELETED
Binary file (58.9 kB)
|
|
instance/codingo.db
ADDED
Binary file (24.6 kB). View file
|
|
requirements.txt
CHANGED
@@ -8,4 +8,5 @@ spacy>=3.0.0
|
|
8 |
nltk
|
9 |
pyresparser
|
10 |
flask_sqlalchemy
|
11 |
-
flask_wtf
|
|
|
|
8 |
nltk
|
9 |
pyresparser
|
10 |
flask_sqlalchemy
|
11 |
+
flask_wtf
|
12 |
+
email-validator
|