Skip to content
Snippets Groups Projects
Commit d887d36b authored by Hin Fung Tsang's avatar Hin Fung Tsang
Browse files

Portfolio Builder 2nd draft, Added client, server, db error handling, fixed...

Portfolio Builder 2nd draft, Added client, server, db error handling, fixed GET method deletion, csrf token, sorting logic for exp, file upload in dashboard
parent 330bc4a1
No related branches found
No related tags found
No related merge requests found
Showing with 400 additions and 195 deletions
......@@ -7,10 +7,7 @@ CREATE TABLE IF NOT EXiSTS"users"(
"email" TEXT NOT NULL UNIQUE,
"role" TEXT NOT NULL,
"headline" TEXT NOT NULL,
"profile_pic" TEXT,
"resume" TEXT,
"linkedin" TEXT,
"github" TEXT
"resume" TEXT
);
CREATE TABLE IF NOT EXISTS "about"(
......@@ -23,7 +20,7 @@ CREATE TABLE IF NOT EXISTS "skills"(
"skill_id" INTEGER PRIMARY KEY AUTOINCREMENT,
"user_id" INTEGER NOT NULL,
"skill_name" TEXT NOT NULL,
"skill_content" TEXT NOT NULL,
"skill_content" TEXT,
"skill_icon" TEXT NOT NULL,
FOREIGN KEY ("user_id") REFERENCES "users" ("id")
);
......
No preview for this file type
......@@ -2,7 +2,9 @@ from flask import render_template, redirect, url_for, request, flash, session, a
from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.utils import secure_filename
from sqlalchemy import text
from sqlalchemy.exc import IntegrityError
from datetime import datetime
import re
import os
......@@ -19,16 +21,30 @@ def home():
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
#validate submission
username = request.form.get('username')
password = request.form.get('password')
username = request.form.get('username').lower().strip()
password = request.form.get('password').strip()
password_hash = generate_password_hash(password, method='pbkdf2:sha256')
first_name = request.form.get('first-name')
last_name = request.form.get('last-name')
role = request.form.get('role')
email = request.form.get('email')
headline = request.form.get('headline')
first_name = request.form.get('first-name').strip()
last_name = request.form.get('last-name').strip()
role = request.form.get('role').strip()
email = request.form.get('email').strip()
headline = request.form.get('headline').strip()
#validate username, password, and email
if not 3 <= len(username) <= 20:
flash("Username must be 3-20 characters long.", "danger")
return render_template("register.html")
if len(password) < 8:
flash("Password must be at least 8 characters long.", "danger")
return render_template("register.html")
if not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', email):
flash("Invalid email address.", "danger")
return render_template("register.html")
sql = """
INSERT INTO users
......@@ -38,6 +54,8 @@ def register():
"""
sql_q = text(sql)
#Catch UNIQUE constraint in db
try:
#Add users
db.session.execute(
sql_q,
......@@ -54,6 +72,11 @@ def register():
db.session.commit()
except IntegrityError:
db.session.rollback()
flash("Registration failed due to unexpected error.", "danger")
return render_template('register.html')
#get id
sql_id = "SELECT id, username, password_hash FROM users WHERE username = :username"
sql_id = text(sql_id)
......@@ -81,7 +104,7 @@ def register():
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
username = request.form.get('username').lower()
password = request.form.get('password')
remember_me = request.form.get('remember-me')
......@@ -122,17 +145,39 @@ def dashboard():
new_role = request.form.get('role')
new_email = request.form.get('email')
new_headline = request.form.get('headline')
new_resume = request.files.get("resume")
#validate email
if not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', new_email):
flash("Invalid email address.", "danger")
return redirect(url_for('dashboard'))
#Get existing resume file name
sql = "SELECT resume FROM users WHERE id = :id"
sql_q = text(sql)
result = db.engine.connect().execute(sql_q, {'id':user_id}).fetchone()
resume = result.resume
if new_resume:
#Save file
new_resume_filename = f"{user_id}_{secure_filename(new_resume.filename)}"
new_resume.save(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'static/uploads', new_resume_filename))
else:
new_resume_filename = resume
sql = """
UPDATE users
SET
first_name= :first_name, last_name = :last_name, email = :email, role = :role, headline = :headline
first_name= :first_name, last_name = :last_name, email = :email, role = :role, headline = :headline, resume = :resume
WHERE id = :id
"""
sql_q = text(sql)
#Update users
try:
db.session.execute(
sql_q,
{
......@@ -141,13 +186,19 @@ def dashboard():
"email": new_email,
"role": new_role,
"headline": new_headline,
"resume": new_resume_filename,
"id": user_id
}
)
db.session.commit()
flash("Information updated successfully!", 'sucess')
flash("Information updated successfully!", 'success')
return redirect( url_for('dashboard'))
except IntegrityError:
db.session.rollback()
flash("Update failed due to unexpected error.", 'danger')
sql = text("SELECT * FROM users WHERE id = :id")
user = db.engine.connect().execute(sql, {'id': user_id}).fetchone()
......@@ -202,7 +253,7 @@ def about():
}
)
db.session.commit()
flash("Information updated successfully!", "success")
flash("About updated successfully!", "success")
return redirect( url_for('about'))
......@@ -228,9 +279,16 @@ def skills():
skill_content = request.form.get("skill_content")
skill_icon = request.form.get("skill_icon")
#Ensure name is not null
if skill_name == "":
flash("Please enter skill name", "danger")
return redirect( url_for('skills'))
#validate skill_icon
if skill_icon not in icons:
return render_template("skills.html", icons=icons)
flash("Unsupported icon submitted", "danger")
return redirect( url_for('skills'))
sql = """
INSERT INTO skills
......@@ -290,6 +348,18 @@ def edit_skill(id):
new_skill_icon = request.form.get('skill_icon')
new_skill_content = request.form.get('skill_content')
#Ensure name is not null
if new_skill_name == "":
flash("Please enter skill name", "danger")
return redirect(url_for('skills'))
#validate skill_icon
if new_skill_icon not in icons:
flash("Unsupported icon submitted", "danger")
return redirect(url_for('skills'))
sql = """
UPDATE skills
......@@ -299,6 +369,7 @@ def edit_skill(id):
"""
sql_q = text(sql)
#Update skill
db.session.execute(
sql_q,
......@@ -325,8 +396,9 @@ def edit_skill(id):
@app.route('/skills/delete/<int:id>')
@app.route('/skills/delete/<int:id>', methods = ['POST'])
def delete_skill(id):
if request.method == 'POST':
if not session.get("user_id"):
flash("Please login first", "warning")
return redirect(url_for('login'))
......@@ -380,6 +452,20 @@ def experience():
description = request.form.get('description')
tags = request.form.get('exp-tag')
#validate input
if not start_date or not end_date or not employer or not role or not description:
flash("Updated failed due to empty field detected.", "danger")
return redirect(url_for('experience'))
if not re.match(r'^[0-9]{4}-[0-9]{2}$', start_date):
flash("Invalid start date.", "danger")
return redirect(url_for('experience'))
if not re.match(r'^[0-9]{4}-[0-9]{2}$', end_date) and end_date != 'CURRENT':
flash("Invalid end date.", "danger")
return redirect(url_for('experience'))
sql = """
INSERT INTO experience
(user_id, start_date, end_date, employer, role, description, tags)
......@@ -406,7 +492,7 @@ def experience():
return redirect(url_for('experience'))
sql = "SELECT * FROM experience WHERE user_id = :user_id"
sql = "SELECT * FROM experience WHERE user_id = :user_id ORDER BY end_date DESC, start_date DESC"
sql = text(sql)
result = db.engine.connect().execute(sql, {'user_id': user_id}).fetchall()
......@@ -496,8 +582,9 @@ def edit_experience(id):
return render_template("edit_experience.html", user_experience=user_experience)
@app.route('/experience/delete/<int:id>')
@app.route('/experience/delete/<int:id>', methods=['POST'])
def delete_experience(id):
if request.method == 'POST':
if not session.get("user_id"):
flash("Please login first", "warning")
return redirect(url_for('login'))
......@@ -551,12 +638,18 @@ def projects():
project_tags = request.form.get("project-tags")
project_order = request.form.get("project-order")
#validate input
if not project_name or not project_description:
flash("Update failed due to empty field detected", "danger")
return redirect(url_for('projects'))
if project_screenshot:
#Save file
project_screenshot.save(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'static/uploads', secure_filename(project_screenshot.filename)))
project_screenshot_filename = f"{str(user_id)}_{secure_filename(project_screenshot.filename)}"
project_screenshot.save(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'static/uploads', project_screenshot_filename))
#Get file name
project_screenshot_filename = project_screenshot.filename
else:
project_screenshot_filename = "project-placeholder.jpg"
......@@ -628,10 +721,9 @@ def edit_project(id):
if new_project_screenshot:
#Save file
new_project_screenshot.save(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'static/uploads', secure_filename(new_project_screenshot.filename)))
new_project_screenshot_filename = f"{str(user_id)}_{secure_filename(new_project_screenshot.filename)}"
new_project_screenshot.save(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'static/uploads', new_project_screenshot_filename))
#Get file name
new_project_screenshot_filename = new_project_screenshot.filename
else:
new_project_screenshot_filename = project_screenshot
......@@ -676,8 +768,9 @@ def edit_project(id):
return render_template("edit_project.html", user_project=user_project)
@app.route('/projects/delete/<int:id>')
@app.route('/projects/delete/<int:id>', methods = ['POST'])
def delete_project(id):
if request.method == 'POST':
if not session.get("user_id"):
flash("Please login first", "warning")
return redirect(url_for('login'))
......
......@@ -95,7 +95,7 @@ main {
.about-container{
max-width: 800px;
border-radius: 10px;
background-color: white;
background-color: #222831;
padding: 10px;
margin: 16px auto;
}
......@@ -202,7 +202,13 @@ main {
.proj-img{
width: 220px;
max-height: 185px;
}
@media (max-width: 800px) {
.proj-img{
width: 220px;
margin-bottom: 12px;
}
}
.project-container{
......
//Quill
//Modified from Quill.js: https://quilljs.com/docs/quickstart
document.addEventListener("DOMContentLoaded", () => {
const quill = new Quill('#editor', {
modules: {
......@@ -25,7 +25,7 @@ document.addEventListener("DOMContentLoaded", () => {
});
//Experience
//Experience current logic
document.addEventListener("DOMContentLoaded", () =>{
//end date is current
......@@ -69,3 +69,22 @@ document.addEventListener("DOMContentLoaded", () => {
});
//Bootstrap form validation
// Directly form Bootstrap documentation : https://getbootstrap.com/docs/5.0/forms/validation/
document.addEventListener("DOMContentLoaded", () => {
'use strict';
const forms = document.querySelectorAll('.needs-validation');
Array.prototype.slice.call(forms).forEach((form) => {
form.addEventListener('submit', (event) => {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
}, false);
});
});
File added
......@@ -5,15 +5,23 @@
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<p class="alert alert-{{ category }}">{{ message }}</p>
{% endfor %}
{% endif %}
{% endwith %}
<div class="about-container">
<h1 class="h4 mb-2 fw-normal">About</h1>
<h1 class="h4 mb-2 fw-normal text-center">About</h1>
<div class="about-container text-light">
<div class="about-p"><p>{{about|safe}}</p></div>
</div>
<form id ="aboutform" method="POST" action="/about">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-group">
<label class="form-label">Update your "about" section below</label>
<div id="editor">{{ about|safe }}</div>
......@@ -21,7 +29,7 @@
</div>
<div class="about-btn">
<button class="btn btn-primary" type="submit">Confirm</button>
<button class="btn btn-primary" type="submit">Save</button>
</div>
</form>
......
......@@ -4,25 +4,35 @@
<h1 class="text-center">User Dashboard</h1>
<p class="text-muted text-center">Update your information below. Username and password cannot be changed here.</p>
<form action="/dashboard" method="POST" class="row g-2 needs-validation" novalidate>
<form action="/dashboard" method="POST" class="row g-2 needs-validation" enctype="multipart/form-data" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<p class="alert alert-{{ category }}">{{ message }}</p>
{% endfor %}
{% endif %}
{% endwith %}
<!-- Username (read-only) -->
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" id="username" class="form-control" value="{{user.username}}" disabled>
<div class="invalid-feedback">This field cannot be empty.</div>
</div>
<!-- First Name -->
<div class="col-6 mb-3">
<label for="first-name" class="form-label">First Name</label>
<input type="text" id="first-name" name="first-name" class="form-control" value="{{user.first_name}}" required>
<div class="invalid-feedback">This field cannot be empty.</div>
</div>
<!-- Last Name -->
<div class="col-6 mb-3">
<label for="last-name" class="form-label">Last Name</label>
<input type="text" id="last-name" name="last-name" class="form-control" value="{{user.last_name}}" required>
<div class="invalid-feedback">This field cannot be empty.</div>
</div>
......@@ -31,20 +41,34 @@
<div class="col-6 mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" id="email" name="email" class="form-control" value="{{user.email}}" required>
<div class="invalid-feedback">Please provide a valid email.</div>
</div>
<!-- Role -->
<div class="col-6 mb-3">
<label for="Role" class="form-label">Current Role or Aspiration</label>
<input type="text" id="role" name="role" class="form-control" value="{{user.role}}" required>
<div class="invalid-feedback">This field cannot be empty.</div>
</div>
<!-- Headline -->
<div class="mb-3">
<label for="headline" class="form-label">Headline</label>
<input type="text" id="headline" name="headline" class="form-control" value="{{user.headline}}" required>
<div class="invalid-feedback">This field cannot be empty.</div>
</div>
<!-- Resume -->
<div class="mb-3">
<label for="resume" class="form-label">Upload your resume (Optional)</label>
<input type="file" name="resume" class="form-control" accept=".pdf">
<div class="form-text">
Upload your full resume for readers. File must be pdf.
</div>
</div>
<!-- Submit Button -->
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">Update Information</button>
......
......@@ -10,7 +10,7 @@
<div class="container text-left form-container">
<p class="text-center">Edit your experience in the form below</p>
<p class="text-center">Edit your experience in the form below.</p>
<form method="POST" class="row g-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
......
......@@ -3,8 +3,16 @@
<div class="container">
<div class="experience-container">
<div>
<h1 class="h4 mb-2 fw-normal text-center">Experience</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<p class="alert alert-{{ category }}">{{ message }}</p>
{% endfor %}
{% endif %}
{% endwith %}
<div class="container text-left px-3 gy-3">
{% if user_experience_list %}
......@@ -12,8 +20,8 @@
<div class="exp-container">
{% for row in user_experience_list %}
<div class="row text-left position-relative exp-row">
<div class="col-3 date-container text-left text-secondary"><span class="exp-date">{{row['start_date_formatted']}} — {{row['end_date_formatted']}}</span></div>
<div class="col-9 exp-heading text-left ">
<div class="col-md-3 col-12 date-container text-left text-secondary"><span class="exp-date">{{row['start_date_formatted']}} — {{row['end_date_formatted']}}</span></div>
<div class="col-md-9 col-12 exp-heading text-left ">
<div class="exp-employer text-light"><h5>{{row['employer']}}</h5></div>
<div class="exp-role text-secondary"><h6>{{row['role']}}</h6></div>
<div class="exp-description"><p style="color: #EEEE;">{{row['description']}}</p> </div>
......@@ -25,11 +33,14 @@
</div>
<div class="position-absolute top-0 end-0">
<a href="{{url_for('edit_experience', id=row.experience_id)}}"><span class="edit-delete">Edit</span></a>
<a href="{{url_for('delete_experience', id=row.experience_id)}}"><span class="edit-delete">Delete</span></a>
<a href="{{url_for('edit_experience', id=row.experience_id)}}"><button class="btn btn-success edit-delete">Edit</button></a>
<form action="{{ url_for('delete_experience', id=row.experience_id) }}" method="POST" style="display: inline;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-danger edit-delete" onclick="return confirm('Are you sure you want to delete this item?');">Delete</button> </div>
</form>
</div>
</div>
</div>
......@@ -46,7 +57,7 @@
<div class="container text-left form-container">
<p class="text-center">Add your experience below. List your most relevant experience first.</p>
<p class="text-center">Add your experience below. More recent experience will appear first.</p>
<form action="/experience" method="POST" class="row g-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
......
......@@ -5,16 +5,24 @@
<div class="projects-container">
<h1 class="h4 mb-2 fw-normal text-center">Projects</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<p class="alert alert-{{ category }}">{{ message }}</p>
{% endfor %}
{% endif %}
{% endwith %}
<div class="container text-left px-3 gy-3">
{% if user_projects%}
{% for project in user_projects %}
<div class="project-container position-relative">
<div class="row text-left ">
<div class="col-4 pic-container text-left"><img class="proj-img rounded border border-dark-subtle" src="{{url_for('static',filename='uploads/' + project.project_screenshot)}}"></div>
<div class="col-12 col-md-4 pic-container text-left img-fluid"><img class="proj-img rounded border border-dark-subtle" src="{{url_for('static',filename='uploads/' + project.project_screenshot)}}"></div>
<div class="col-8 proj-content text-left">
<div class="stretched-link-container">
<div class="proj-name"><a class="project-link stretched-link" href="{{project.project_url if project.project_url else '#'}}"><h5>{{project.project_name}}</h5></a></div>
<div class="col-12 col-md-8 proj-content text-left">
<div class="">
<div class="proj-name"><a class="project-link" href="{{project.project_url if project.project_url else '#'}}"><h5>{{project.project_name}}</h5></a></div>
<div class="proj-description position-relative"> <p>{{project.project_description}}</p> </div>
</div>
{% if project.tags %}
......@@ -27,8 +35,11 @@
{% endif %}
<div class="position-absolute top-0 end-0">
<a href="{{url_for('edit_project', id=project.project_id)}}"><span class="edit-delete">Edit</span></a>
<a href="{{url_for('delete_project', id=project.project_id)}}"><span class="edit-delete">Delete</span></a>
<a href="{{url_for('edit_project', id=project.project_id)}}"><button class="btn btn-success edit-delete">Edit</button></a>
<form action="{{ url_for('delete_project', id=project.project_id) }}" method="POST" style="display: inline;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-danger edit-delete" onclick="return confirm('Are you sure you want to delete this item?');">Delete</button>
</form>
</div>
</div>
......@@ -63,7 +74,7 @@
<!-- Project screenshot -->
<div class="mb-3 col-md-12">
<label for="project-screenshot" class="form-label">Screenshot (Optional)</label>
<input type="file" name="project-screenshot" class="form-control" accept=".png, .jpg, .jpeg" aria-describedby="screenshothelpblock">
<input type="file" name="project-screenshot" class="form-control" accept="image/*" aria-describedby="screenshothelpblock">
<div id="screenshothelpblock" class="form-text">
Upload a screenshot for your project. Default image will be used if no image is uploaded.
</div>
......
......@@ -4,46 +4,66 @@
<meta charset="UTF-8">
<title>Register</title>
<!-- Latest compiled and minified CSS -->
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Latest compiled JavaScript -->
<!-- Bootstrap JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<link rel="stylesheet" type="text/css"
href="{{url_for('static',filename='css/style.css')}}">
<!--My CSS and JS-->
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/style.css')}}">
<script src="{{url_for('static',filename='js/script.js')}}"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="CMT120 Coursework">
<meta name="author" content="Hin Fung Tsang, Tony">
<link rel="icon" href="{{url_for('static',filename='img/logo.png')}}">
</head>
<body>
<main>
<div class="container">
<div class="text-center">
<img class="mb-4" src="{{url_for('static',filename='img/resume_icon.png')}}" alt="Resume Builder icon" width="72" height="72">
<h1 class="h3 mb-3 fw-normal">Register at Portfolio Builder</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<p class="alert alert-{{ category }}">{{ message }}</p>
{% endfor %}
{% endif %}
{% endwith %}
</div>
<div class="container text-left form-container">
<form class="row g-2 needs-validation" method="POST" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="col-md-6">
<!--Username-->
<div class="col-md-6 col-sm-12">
<label for="username" class="form-label">Username</label>
<div class="input-group has-validation">
<span class="input-group-text" id="username-prepend">@</span>
<input type="text" class="form-control" id="username" name="username" aria-describedby="username-prepend" required>
<div class="invalid-feedback">Please choose a username.</div>
<input type="text" class="form-control" id="username" name="username" aria-describedby="username-prepend" minlength="3" maxlength="20" required>
<div id="usernamehelpblock" class="form-text">
Username must be 3-20 characters long.
</div>
<div class="invalid-feedback">Please provide a valid username.</div>
</div>
</div>
<div class="col-md-6">
<div class="col-md-6 col-sm-12">
<label for="password" class="form-label">Password</label>
<div class="input-group has-validation">
<span class="input-group-text" id="password-prepend">#</span>
<input type="password" class="form-control" id="password" name="password" aria-describedby="password-prepend" required>
<div class="invalid-feedback">Please choose a password.</div>
<input type="password" class="form-control" id="password" name="password" aria-describedby="password-prepend" minlength="8" required>
<div id="usernamehelpblock" class="form-text">
Password must be at least 8 characters long.
</div>
<div class="invalid-feedback">Please provide a valid password.</div>
</div>
</div>
......@@ -53,34 +73,34 @@
The following information will be in your portfolio. Don't worry, you can change it anytime.
</div>
<div class="col-md-6">
<div class="col-md-6 col-sm-12">
<label for="first-name" class="form-label">First Name</label>
<input type="text" class="form-control" id="first-name" name="first-name" placeholder="Jon" required>
<div class="valid-feedback">Looks good!</div>
<input type="text" class="form-control" id="first-name" name="first-name" placeholder="John" required>
<div class="invalid-feedback col-sm-12">This field cannot be empty.</div>
</div>
<div class="col-md-6">
<div class="col-md-6 col-sm-12">
<label for="last-name" class="form-label">Last Name</label>
<input type="text" class="form-control" id="last-name" name="last-name" placeholder="Snow" required>
<div class="valid-feedback">Looks good!</div>
<input type="text" class="form-control" id="last-name" name="last-name" placeholder="Doe" required>
<div class="invalid-feedback col-sm-12">This field cannot be empty.</div>
</div>
<div class="col-md-6">
<div class="col-md-6 col-sm-12">
<label for="email" class="form-label">Email</label>
<input type="text" class="form-control" id="email" name="email" placeholder="JS@gmail.com" required>
<div class="valid-feedback">Looks good!</div>
<input type="email" class="form-control" id="email" name="email" placeholder="JD@gmail.com" required>
<div class="invalid-feedback">Please provide a valid email.</div>
</div>
<div class="col-md-6">
<div class="col-md-6 col-sm-12">
<label for="role" class="form-label">Current Role or Aspiration</label>
<input type="text" class="form-control" id="role" name="role" placeholder="Aspiring Data Analysts" required>
<div class="valid-feedback">Looks good!</div>
<div class="invalid-feedback">This field cannot be empty.</div>
</div>
<div class="col-md-12">
<label for="headline" class="form-label">Headline</label>
<input type="text" class="form-control" id="headline" name="headline" placeholder="I like to make life decisions based on numbers." required>
<div class="valid-feedback">Looks good!</div>
<div class="invalid-feedback">This field cannot be empty.</div>
</div>
<div class="d-grid gap-2 register-btn text-center">
......
......@@ -4,16 +4,30 @@
<div class="container">
<div class="text-center skills-container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<p class="alert alert-{{ category }}">{{ message }}</p>
{% endfor %}
{% endif %}
{% endwith %}
<h1 class="h4 mb-2 fw-normal">Skills</h1>
<div class="container text-center px-3 gy-3">
{% if user_skills%}
<div class="row g-2">
{% for row in user_skills%}
<div class="col-4 skill-container position-relative">
<div class="col-12 col-md-4 skill-container position-relative">
<div class="position-absolute top-0 end-0">
<a href="{{url_for('edit_skill', id=row.skill_id)}}"><span class="edit-delete">Edit</span></a>
<a href="{{url_for('delete_skill', id=row.skill_id)}}"><span class="edit-delete">Delete</span></a>
<a href="{{url_for('edit_skill', id=row.skill_id)}}"><button class="btn btn-success edit-delete">Edit</button></a>
<form action="{{ url_for('delete_skill', id=row.skill_id) }}" method="POST" style="display: inline;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-danger edit-delete" onclick="return confirm('Are you sure you want to delete this item?');">Delete</button>
</form>
</div>
<img class="skill-icon" src="{{url_for('static', filename='img/' + row.skill_icon + '.png')}}">
<p class="skill-name">{{row.skill_name}}</p>
......@@ -37,7 +51,7 @@
<!-- Skill name -->
<div class="mb-3 col-md-6">
<label for="skill_name" class="form-label">Skill name</label>
<input type="text" name="skill_name" class="form-control" placeholder="Python" required>
<input type="text" name="skill_name" class="form-control" placeholder="Programming" required>
</div>
<!-- Skill icon -->
......@@ -55,7 +69,9 @@
<!-- Skill content -->
<div class="mb-3">
<label for="skill_content" class="form-label">Description</label>
<textarea id="skill_content" name="skill_content" class="form-control" rows="3" placeholder="Gained competency in Python through online courses, Leetcode challenges, and modules in MSc." required></textarea>
<textarea id="skill_content" name="skill_content" class="form-control" rows="3" placeholder="Proficient in: Python, R, SQL, C, HTML, CSS, JavaScript
" ></textarea>
</div>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment