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
|