First Commit
This commit is contained in:
320
app.py
Normal file
320
app.py
Normal file
@@ -0,0 +1,320 @@
|
||||
from flask import Flask, request, redirect, url_for, render_template, session, flash
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy import func
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
from werkzeug.utils import secure_filename
|
||||
from flask_login import LoginManager, login_required, logout_user, UserMixin, login_user
|
||||
from flask_mail import Mail, Message
|
||||
import json
|
||||
from slugify import slugify
|
||||
|
||||
load_dotenv()
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///posts.db'
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app.config['UPLOAD_FOLDER'] = 'static/uploads'
|
||||
app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY')
|
||||
|
||||
app.config['MAIL_SERVER'] = 'mail.bennyshouse.net'
|
||||
app.config['MAIL_PORT'] = 587
|
||||
app.config['MAIL_USE_TLS'] = True
|
||||
app.config['MAIL_USERNAME'] = os.getenv("MAIL_USERNAME")
|
||||
app.config['MAIL_PASSWORD'] = os.getenv("MAIL_PASSWORD")
|
||||
app.config['MAIL_DEFAULT_SENDER'] = os.getenv("MAIL_DEFAULT_SENDER")
|
||||
|
||||
mail = Mail(app)
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
login_manager = LoginManager(app)
|
||||
login_manager.login_view = 'login'
|
||||
|
||||
USERNAME = os.getenv('FLASK_LOGIN_USER')
|
||||
PASSWORD = os.getenv('FLASK_LOGIN_PASSWORD')
|
||||
|
||||
if not os.path.exists(app.config['UPLOAD_FOLDER']):
|
||||
os.makedirs(app.config['UPLOAD_FOLDER'])
|
||||
|
||||
class User(UserMixin):
|
||||
id = 1
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
if user_id == "1":
|
||||
return User()
|
||||
return None
|
||||
|
||||
class BlogPost(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.String(255), nullable=False)
|
||||
slug = db.Column(db.String(255), nullable=False)
|
||||
content = db.Column(db.Text, nullable=False)
|
||||
images = db.Column(db.Text, nullable=True)
|
||||
category = db.Column(db.String(100), nullable=True)
|
||||
tags = db.Column(db.Text, nullable=True)
|
||||
pinned = db.Column(db.Boolean, default=False)
|
||||
|
||||
class ProjectPost(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.String(255), nullable=False)
|
||||
slug = db.Column(db.String(255), nullable=False)
|
||||
content = db.Column(db.Text, nullable=False)
|
||||
images = db.Column(db.Text, nullable=True)
|
||||
category = db.Column(db.String(100), nullable=True)
|
||||
tags = db.Column(db.Text, nullable=True)
|
||||
pinned = db.Column(db.Boolean, default=False)
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
@app.template_filter('from_json')
|
||||
def from_json(value):
|
||||
try:
|
||||
return json.loads(value)
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
return []
|
||||
|
||||
@app.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
if request.method == "POST":
|
||||
username = request.form["username"]
|
||||
password = request.form["password"]
|
||||
|
||||
if username == USERNAME and password == PASSWORD:
|
||||
user = User()
|
||||
login_user(user)
|
||||
session.permanent = True
|
||||
return redirect(url_for("admin_panel"))
|
||||
|
||||
flash("Invalid email or password!", "danger")
|
||||
return render_template("login.html")
|
||||
|
||||
@app.route("/logout")
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
return redirect(url_for("index"))
|
||||
|
||||
@app.route('/admin')
|
||||
@login_required
|
||||
def admin_panel():
|
||||
blogpost = BlogPost.query.order_by(BlogPost.id.desc()).all()
|
||||
projectpost = ProjectPost.query.order_by(ProjectPost.id.desc()).all()
|
||||
return render_template('admin.html', blogpost=blogpost, projectpost=projectpost)
|
||||
|
||||
@app.route('/newblog', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def new_blog():
|
||||
if request.method == 'POST':
|
||||
title = request.form['title']
|
||||
content = request.form['content'].replace("\n", "<br>")
|
||||
category = request.form['category']
|
||||
tags = request.form['tags']
|
||||
pinned = 'pinned' in request.form
|
||||
images = request.files.getlist('images')
|
||||
|
||||
slug = slugify(title)
|
||||
image_filenames = [secure_filename(image.filename) for image in images if image.filename]
|
||||
for image, filename in zip(images, image_filenames):
|
||||
image.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
|
||||
|
||||
new_post = BlogPost(
|
||||
title=title,
|
||||
slug=slug,
|
||||
content=content,
|
||||
category=category,
|
||||
tags=tags,
|
||||
pinned=pinned,
|
||||
images=json.dumps(image_filenames)
|
||||
)
|
||||
db.session.add(new_post)
|
||||
db.session.commit()
|
||||
return redirect(url_for('blog'))
|
||||
|
||||
return render_template('new_blog.html')
|
||||
|
||||
@app.route('/newproject', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def new_project():
|
||||
if request.method == 'POST':
|
||||
title = request.form['title']
|
||||
content = request.form['content'].replace("\n", "<br>")
|
||||
category = request.form['category']
|
||||
tags = request.form['tags']
|
||||
pinned = 'pinned' in request.form
|
||||
images = request.files.getlist('images')
|
||||
|
||||
slug = slugify(title)
|
||||
image_filenames = [secure_filename(image.filename) for image in images if image.filename]
|
||||
for image, filename in zip(images, image_filenames):
|
||||
image.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
|
||||
|
||||
new_post = ProjectPost(
|
||||
title=title,
|
||||
slug=slug,
|
||||
content=content,
|
||||
category=category,
|
||||
tags=tags,
|
||||
pinned=pinned,
|
||||
images=json.dumps(image_filenames)
|
||||
)
|
||||
db.session.add(new_post)
|
||||
db.session.commit()
|
||||
return redirect(url_for('projects'))
|
||||
|
||||
return render_template('new_project.html')
|
||||
|
||||
@app.route('/edit-blog/<slug>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_blog(slug):
|
||||
blogpost = BlogPost.query.filter_by(slug=slug).first_or_404()
|
||||
|
||||
if request.method == 'POST':
|
||||
blogpost.title = request.form['title']
|
||||
blogpost.slug = slugify(blogpost.title)
|
||||
blogpost.content = request.form['content'].replace("\n", "<br>")
|
||||
blogpost.category = request.form['category']
|
||||
blogpost.tags = request.form['tags']
|
||||
blogpost.pinned = 'pinned' in request.form
|
||||
|
||||
images = request.files.getlist('images')
|
||||
image_filenames = json.loads(blogpost.images) if blogpost.images else []
|
||||
|
||||
for image in images:
|
||||
if image.filename:
|
||||
filename = secure_filename(image.filename)
|
||||
image.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
|
||||
image_filenames.append(filename)
|
||||
|
||||
blogpost.images = json.dumps(image_filenames)
|
||||
db.session.commit()
|
||||
return redirect(url_for('view_blog', slug=blogpost.slug))
|
||||
|
||||
return render_template('edit_blog.html', blogpost=blogpost)
|
||||
|
||||
@app.route('/edit-project/<slug>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_project(slug):
|
||||
projectpost = ProjectPost.query.filter_by(slug=slug).first_or_404()
|
||||
|
||||
if request.method == 'POST':
|
||||
projectpost.title = request.form['title']
|
||||
projectpost.slug = slugify(projectpost.title)
|
||||
projectpost.content = request.form['content'].replace("\n", "<br>")
|
||||
projectpost.category = request.form['category']
|
||||
projectpost.tags = request.form['tags']
|
||||
projectpost.pinned = 'pinned' in request.form
|
||||
|
||||
images = request.files.getlist('images')
|
||||
image_filenames = json.loads(projectpost.images) if projectpost.images else []
|
||||
|
||||
for image in images:
|
||||
if image.filename:
|
||||
filename = secure_filename(image.filename)
|
||||
image.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
|
||||
image_filenames.append(filename)
|
||||
|
||||
projectpost.images = json.dumps(image_filenames)
|
||||
db.session.commit()
|
||||
return redirect(url_for('view_project', slug=projectpost.slug))
|
||||
|
||||
return render_template('edit_project.html', projectpost=projectpost)
|
||||
|
||||
@app.route('/delete-blog/<slug>', methods=['POST'])
|
||||
@login_required
|
||||
def delete_blog(slug):
|
||||
blogpost = BlogPost.query.filter_by(slug=slug).first_or_404()
|
||||
if blogpost.images:
|
||||
for filename in json.loads(blogpost.images):
|
||||
image_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
||||
if os.path.exists(image_path):
|
||||
os.remove(image_path)
|
||||
db.session.delete(blogpost)
|
||||
db.session.commit()
|
||||
return redirect(url_for('admin_panel'))
|
||||
|
||||
@app.route('/delete-project/<slug>', methods=['POST'])
|
||||
@login_required
|
||||
def delete_project(slug):
|
||||
projectpost = ProjectPost.query.filter_by(slug=slug).first_or_404()
|
||||
if projectpost.images:
|
||||
for filename in json.loads(projectpost.images):
|
||||
image_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
||||
if os.path.exists(image_path):
|
||||
os.remove(image_path)
|
||||
db.session.delete(projectpost)
|
||||
db.session.commit()
|
||||
return redirect(url_for('admin_panel'))
|
||||
|
||||
@app.route('/blogtag/<tag>')
|
||||
def view_tag(tag):
|
||||
tag_lower = tag.lower() # Normalize case
|
||||
blogpost = BlogPost.query.filter(func.lower(BlogPost.tags).contains(tag_lower)).order_by(BlogPost.id.desc()).all()
|
||||
return render_template('blog_tag_results.html', blogpost=blogpost, tag=tag)
|
||||
|
||||
@app.route('/projecttag/<tag>')
|
||||
def view_project_tag(tag):
|
||||
tag_lower = tag.lower() # Normalize case
|
||||
projectpost = ProjectPost.query.filter(func.lower(ProjectPost.tags).contains(tag_lower)).order_by(ProjectPost.id.desc()).all()
|
||||
return render_template('project_tag_results.html', projectpost=projectpost, tag=tag)
|
||||
|
||||
@app.route('/blog')
|
||||
def blog():
|
||||
blogpost = BlogPost.query.order_by(BlogPost.id.desc()).all()
|
||||
return render_template('blog.html', blogpost=blogpost)
|
||||
|
||||
@app.route('/projects')
|
||||
def projects():
|
||||
projectpost = ProjectPost.query.order_by(ProjectPost.id.desc()).all()
|
||||
return render_template('project.html', projectpost=projectpost)
|
||||
|
||||
@app.route('/blog/<slug>')
|
||||
def view_blog(slug):
|
||||
blogpost = BlogPost.query.filter_by(slug=slug).first_or_404()
|
||||
return render_template('view_blog.html', blogpost=blogpost)
|
||||
|
||||
@app.route('/project/<slug>')
|
||||
def view_project(slug):
|
||||
projectpost = ProjectPost.query.filter_by(slug=slug).first_or_404()
|
||||
return render_template('view_project.html', projectpost=projectpost)
|
||||
|
||||
@app.route("/contact", methods=["GET", "POST"])
|
||||
def contact():
|
||||
if request.method == "POST":
|
||||
name = request.form.get("name")
|
||||
email = request.form.get("email")
|
||||
message = request.form.get("message")
|
||||
|
||||
# Compose the email
|
||||
msg = Message(subject=f"New Contact Message from {name}",
|
||||
sender=("Portfolio Updates", "ben@bennyshouse.net"),
|
||||
recipients=["ben@bennyshouse.net"],
|
||||
body=f"Name: {name}\nEmail: {email}\n\n{message}")
|
||||
|
||||
try:
|
||||
mail.send(msg)
|
||||
flash("Your message has been sent!", "success")
|
||||
except Exception as e:
|
||||
flash("Something went wrong. Please try again later.", "error")
|
||||
print(f"Email error: {e}")
|
||||
|
||||
return redirect(url_for("contact"))
|
||||
|
||||
return render_template("contact.html")
|
||||
|
||||
|
||||
@app.route('/resume')
|
||||
def resume():
|
||||
return render_template('resume.html')
|
||||
|
||||
@app.route('/about')
|
||||
def about():
|
||||
return render_template('about.html')
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
||||
52
templates/about.html
Normal file
52
templates/about.html
Normal file
@@ -0,0 +1,52 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}About{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="text-center py-20 bg-gradient-to-br from-red-800 to-orange-400 text-white">
|
||||
<h1 class="text-5xl font-extrabold mb-4">About Me</h1>
|
||||
<p class="text-xl mb-6">Learn more about who I am, what I do, and what drives me.</p>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="py-16 bg-black text-white">
|
||||
<div class="max-w-4xl mx-auto px-4 space-y-12">
|
||||
|
||||
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold text-orange-600 mb-4">Who Am I?</h2>
|
||||
<p class="text-lg leading-relaxed">
|
||||
I'm a passionate developer and tech enthusiast focused on building useful, secure, and accessible digital experiences.
|
||||
Whether it’s creating full-stack web applications, designing clean UIs, or managing network infrastructure — I love what I do.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold text-orange-600 mb-4">Skills & Tools</h2>
|
||||
<ul class="grid sm:grid-cols-2 gap-4 text-lg list-disc pl-6">
|
||||
<li>Python & Flask</li>
|
||||
<li>HTML CSS and Javascript</li>
|
||||
<li>SQL (MYSQL, SQLite)</li>
|
||||
<li>Linux system administration</li>
|
||||
<li>Networking & Security (WireGuard, Samba, DNS-Server)</li>
|
||||
<li>Git & Deployment </li>
|
||||
<li>API integrations</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Philosophy or Interests -->
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold text-orange-600 mb-4">What I'm About</h2>
|
||||
<p class="text-lg leading-relaxed">
|
||||
I believe technology should empower people — not confuse them. I enjoy creating tools that are intuitive, helpful, and secure.
|
||||
I'm always learning, whether it’s diving into cybersecurity, mastering backend frameworks, or exploring automation and DevOps.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
100
templates/admin.html
Normal file
100
templates/admin.html
Normal file
@@ -0,0 +1,100 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<section class="py-20 bg-gradient-to-br from-red-800 to-orange-400 text-white text-center">
|
||||
<h1 class="text-5xl font-extrabold mb-4">Admin Dashboard</h1>
|
||||
<p class="text-xl">Manage your blog posts and projects here.</p>
|
||||
</section>
|
||||
|
||||
<div class="max-w-6xl mx-auto py-12 px-4 space-y-16">
|
||||
|
||||
<!-- Projects Section -->
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-3xl font-bold text-orange-600">Project Options</h2>
|
||||
<a href="{{ url_for('new_project') }}"
|
||||
class="bg-orange-600 text-white px-4 py-2 rounded hover:bg-orange-700 transition shadow">+ New Project</a>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full table-auto border border-gray-300 bg-white rounded-lg shadow">
|
||||
<thead class="bg-gray-100">
|
||||
<tr class="text-left text-gray-700 uppercase text-sm">
|
||||
<th class="px-4 py-3">Title</th>
|
||||
<th class="px-4 py-3">Category</th>
|
||||
<th class="px-4 py-3">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for projectpost in projectpost %}
|
||||
<tr class="border-t border-gray-200">
|
||||
<td class="text-black px-4 py-3 font-medium">{{ projectpost.title }}</td>
|
||||
<td class="text-black px-4 py-3">{{ projectpost.category }}</td>
|
||||
<td class="px-4 py-3 space-x-2">
|
||||
<a href="{{ url_for('view_project', slug=projectpost.slug) }}"
|
||||
class="inline-block bg-blue-500 text-white px-3 py-1 rounded hover:bg-blue-600">View</a>
|
||||
<a href="{{ url_for('edit_project', slug=projectpost.slug) }}"
|
||||
class="inline-block bg-yellow-500 text-white px-3 py-1 rounded hover:bg-yellow-600">Edit</a>
|
||||
<form action="{{ url_for('delete_project', slug=projectpost.slug) }}" method="POST" class="inline">
|
||||
<button type="submit"
|
||||
onclick="return confirm('Delete this Project?');"
|
||||
class="bg-red-500 text-white px-3 py-1 rounded hover:bg-red-600">
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blog Section -->
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-3xl font-bold text-orange-600">Blog Options</h2>
|
||||
<a href="{{ url_for('new_blog') }}"
|
||||
class="bg-orange-600 text-white px-4 py-2 rounded hover:bg-orange-700 transition shadow">+ New Blog Post</a>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full table-auto border border-gray-300 bg-white rounded-lg shadow">
|
||||
<thead class="bg-gray-100">
|
||||
<tr class="text-left text-gray-700 uppercase text-sm">
|
||||
<th class="px-4 py-3">Title</th>
|
||||
<th class="px-4 py-3">Category</th>
|
||||
<th class="px-4 py-3">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for blogpost in blogpost %}
|
||||
<tr class="border-t border-gray-200">
|
||||
<td class="text-black px-4 py-3 font-medium">{{ blogpost.title }}</td>
|
||||
<td class="text-black px-4 py-3">{{ blogpost.category }}</td>
|
||||
<td class="px-4 py-3 space-x-2">
|
||||
<a href="{{ url_for('view_blog', slug=blogpost.slug) }}"
|
||||
class="inline-block bg-blue-500 text-white px-3 py-1 rounded hover:bg-blue-600">View</a>
|
||||
<a href="{{ url_for('edit_blog', slug=blogpost.slug) }}"
|
||||
class="inline-block bg-yellow-500 text-white px-3 py-1 rounded hover:bg-yellow-600">Edit</a>
|
||||
<form action="{{ url_for('delete_blog', slug=blogpost.slug) }}" method="POST" class="inline">
|
||||
<button type="submit"
|
||||
onclick="return confirm('Delete this post?');"
|
||||
class="bg-red-500 text-white px-3 py-1 rounded hover:bg-red-600">
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
85
templates/base.html
Normal file
85
templates/base.html
Normal file
@@ -0,0 +1,85 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" integrity="sha512-...snip..." crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
|
||||
<title>{% block title %}Benjamin Mosley{% endblock %}</title>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body class="bg-black text-white font-sans min-h-screen flex flex-col">
|
||||
|
||||
|
||||
<header class="bg-black text-white shadow" x-data="{ open: false }">
|
||||
<div class="max-w-7xl mx-auto flex justify-between items-center p-6">
|
||||
|
||||
<h1>
|
||||
<a href="/" class="text-xl font-bold">Benjamin Mosley</a>
|
||||
</h1>
|
||||
|
||||
<!-- Hamburger button (shown on small screens) -->
|
||||
<button @click="open = !open" class="sm:hidden focus:outline-none">
|
||||
<svg x-show="!open" x-cloak xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg x-show="open" x-cloak xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Desktop nav -->
|
||||
<nav class="hidden sm:flex space-x-4">
|
||||
<a href="/blog" class="p-3 hover:text-blue-400">Blog</a>
|
||||
<a href="/resume" class="p-3 hover:text-blue-400">Resume</a>
|
||||
<a href="/projects" class="p-3 hover:text-blue-400">Projects</a>
|
||||
<a href="/contact" class="p-3 hover:text-blue-400">Contact</a>
|
||||
<a href="/about" class="p-3 hover:text-blue-400">About</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Mobile nav -->
|
||||
<div x-show="open" x-cloak class="sm:hidden bg-black border-t border-gray-800">
|
||||
<nav class="flex flex-col p-4 space-y-2">
|
||||
<a href="/blog" class="hover:text-blue-400">Blog</a>
|
||||
<a href="/resume" class="hover:text-blue-400">Resume</a>
|
||||
<a href="/projects" class="hover:text-blue-400">Projects</a>
|
||||
<a href="/contact" class="hover:text-blue-400">Contact</a>
|
||||
<a href="/about" class="hover:text-blue-400">About</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<main class="flex-grow">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="bg-black text-white text-center py-6 border-t border-gray-800">
|
||||
<div class="max-w-4xl mx-auto px-4">
|
||||
<p class="text-sm">2025 Benjamin Mosley.</p>
|
||||
<div class="flex justify-center gap-4 mt-2 text-sm">
|
||||
<a href="https://github.com/Benny-ui-ux" target="_blank" class="hover:text-white transition">GitHub</a>
|
||||
<a href="https://bennyshouse.net" class="hover:text-white transition">Business Site</a>
|
||||
<a href="mailto:ben@bennyshouse.net" class="hover:text-white transition">Email</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
38
templates/blog.html
Normal file
38
templates/blog.html
Normal file
@@ -0,0 +1,38 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Blog{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="text-center py-20 bg-gradient-to-br from-red-800 to-orange-400 text-white">
|
||||
<h1 class="text-5xl font-extrabold mb-4">Blog</h1>
|
||||
<p class="text-xl mb-6">Insights, tutorials, and personal thoughts — straight from my mind to the page.</p>
|
||||
</section>
|
||||
|
||||
<!-- Blog Posts Grid -->
|
||||
<section id="posts" class="py-16 bg-black text-white">
|
||||
<div class="max-w-6xl mx-auto px-4">
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{% for blogpost in blogpost %}
|
||||
<div class="bg-white rounded-lg shadow p-6 hover:shadow-lg transition">
|
||||
<h2 class="text-2xl font-bold text-red-800 mb-2">
|
||||
<a href="{{ url_for('view_blog', slug=blogpost.slug) }}" class="hover:underline">{{ blogpost.title }}</a>
|
||||
</h2>
|
||||
<p class="text-sm text-orange-500 font-medium mb-4">{{ blogpost.category }}</p>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% for tag in blogpost.tags.split(',') %}
|
||||
<a href="{{ url_for('view_tag', tag=tag.strip()) }}"
|
||||
class="inline-block bg-gray-800 text-white text-sm px-3 py-1 rounded-full hover:bg-gray-700 transition">
|
||||
#{{ tag.strip() }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
35
templates/blog_tag_results.html
Normal file
35
templates/blog_tag_results.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Tag Results{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="text-center py-20 bg-gradient-to-br from-red-800 to-orange-400 text-white">
|
||||
<h1 class="text-5xl font-extrabold mb-4">Posts Tagged: <span class="text-white">#{{ tag }}</span></h1>
|
||||
<p class="text-lg">Here are all blog posts with this tag.</p>
|
||||
</section>
|
||||
|
||||
<!-- Tag Results Content -->
|
||||
<section class="py-16 bg-black text-white">
|
||||
<div class="max-w-5xl mx-auto px-4">
|
||||
{% if blogpost %}
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{% for blogpost in blogpost %}
|
||||
<div class="bg-white rounded-lg shadow p-6 hover:shadow-lg transition">
|
||||
<h2 class="text-2xl font-bold text-red-800 mb-2">
|
||||
<a href="{{ url_for('view_blog', slug=blogpost.slug) }}" class="hover:underline">
|
||||
{{ blogpost.title }}
|
||||
</a>
|
||||
</h2>
|
||||
<p class="text-sm text-orange-500 font-medium">{{ blogpost.category }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-center text-lg text-gray-600">No posts found with this tag.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
58
templates/contact.html
Normal file
58
templates/contact.html
Normal file
@@ -0,0 +1,58 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Contact{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<section class="text-center py-20 bg-gradient-to-br from-red-800 to-orange-400 text-white">
|
||||
<h1 class="text-5xl font-extrabold mb-4">Contact Me</h1>
|
||||
<p class="text-xl mb-6">Looking to connect? Here's where you can reach me!</p>
|
||||
</section>
|
||||
|
||||
<section class="bg-black text-white py-16 px-6 md:px-20">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="grid md:grid-cols-2 gap-12">
|
||||
|
||||
<!-- Contact Info -->
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold mb-4 text-orange-600">Get in Touch</h2>
|
||||
<p class="mb-4">Feel free to drop me a message via the form or through any of the channels below:</p>
|
||||
<ul class="space-y-3 text-lg">
|
||||
<li><strong>Email:</strong> <a href="mailto:you@example.com" class="text-blue-600 hover:underline">ben@bennyshouse.net</a></li>
|
||||
<li><strong>Business Website:</strong> <a href="https://bennyshouse.net" class="text-blue-600 hover:underline">bennyshouse.net</a></li>
|
||||
<li><strong>Location:</strong> Canyon, Texas</li>
|
||||
</ul>
|
||||
<div class="mt-6 flex space-x-4">
|
||||
|
||||
<a href="https://www.instagram.com/bennyshousefr/" class="text-orange-600 hover:text-orange-800 transition"><i class="fab fa-instagram fa-lg"></i></a>
|
||||
<a href="https://www.linkedin.com/in/benjamin-mosley-849643329/" class="text-orange-600 hover:text-orange-800 transition"><i class="fab fa-linkedin fa-lg"></i></a>
|
||||
<a href="https://github.com/Benny-ui-ux" class="text-orange-600 hover:text-orange-800 transition"><i class="fab fa-github fa-lg"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<form method="POST" action="/contact" class="space-y-4">
|
||||
<div>
|
||||
<label for="name" class="block font-semibold mb-1">Name</label>
|
||||
<input type="text" id="name" name="name" required class="text-black w-full border border-gray-300 rounded px-4 py-2 focus:ring-2 focus:ring-orange-400 focus:outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label for="email" class="block font-semibold mb-1">Email</label>
|
||||
<input type="email" id="email" name="email" required class="text-black w-full border border-gray-300 rounded px-4 py-2 focus:ring-2 focus:ring-orange-400 focus:outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label for="message" class="block font-semibold mb-1">Message</label>
|
||||
<textarea id="message" name="message" rows="5" required class="text-black w-full border border-gray-300 rounded px-4 py-2 focus:ring-2 focus:ring-orange-400 focus:outline-none"></textarea>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button type="submit" class="bg-orange-600 text-white px-6 py-2 rounded hover:bg-orange-700 transition">Send</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
29
templates/edit_blog.html
Normal file
29
templates/edit_blog.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Edit Blog{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<label>Title:</label>
|
||||
<input type="text" name="title" value="{{ blogpost.title }}" required>
|
||||
|
||||
<label>Content:</label>
|
||||
<textarea name="content">{{ blogpost.content|replace('<br>', '\n') }}</textarea>
|
||||
|
||||
<label>Category:</label>
|
||||
<input type="text" name="category" value="{{ blogpost.category }}">
|
||||
|
||||
<label>Tags:</label>
|
||||
<input type="text" name="tags" value="{{ blogpost.tags }}">
|
||||
|
||||
|
||||
<label>Upload New Images:</label>
|
||||
<input type="file" name="images" multiple>
|
||||
|
||||
<button type="submit">Save Changes</button>
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
||||
26
templates/edit_project.html
Normal file
26
templates/edit_project.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Edit Project{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<label>Title:</label>
|
||||
<input type="text" name="title" value="{{ projectpost.title }}" required>
|
||||
|
||||
<label>Content:</label>
|
||||
<textarea name="content">{{ projectpost.content|replace('<br>', '\n') }}</textarea>
|
||||
|
||||
<label>Category:</label>
|
||||
<input type="text" name="category" value="{{ projectpost.category }}">
|
||||
|
||||
<label>Tags:</label>
|
||||
<input type="text" name="tags" value="{{ projectpost.tags }}">
|
||||
|
||||
|
||||
<label>Upload New Images:</label>
|
||||
<input type="file" name="images" multiple>
|
||||
|
||||
<button type="submit">Save Changes</button>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
46
templates/index.html
Normal file
46
templates/index.html
Normal file
@@ -0,0 +1,46 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Benjamin Mosley{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<section class="text-center py-16 bg-gradient-to-br from-red-800 to-orange-400 text-white">
|
||||
<h1 class="mb-3 text-5xl font-extrabold mb-4">Benjamin Mosley's Personal Portfolio</h1>
|
||||
<img src="{{ url_for('static', filename='Ben.png') }}" alt="Benjamin Mosley"
|
||||
class="h-32 w-32 mx-auto shadow-lg border-4 border-white mb-6">
|
||||
<p class="text-xlmb-6">I am Benjamin Mosley, a Computer Information Systems student with
|
||||
a passion for building, creating, and learning.</p>
|
||||
|
||||
</section>
|
||||
|
||||
<section class="grid grid-cols-1 md:grid-cols-3 gap-6 p-10 text-white">
|
||||
<div class="bg-gradient-to-br from-red-600 to-orange-300 p-6 rounded shadow text-center">
|
||||
<h3 class="text-lg font-bold mb-2">Contact Me</h3>
|
||||
<p>Have any questions? Reach out to me here</p>
|
||||
|
||||
<br>
|
||||
|
||||
<a href="/contact" class="bg-blue-400 inline-block text-white font-semibold px-4 py-2 rounded shadpw hpver:bg-gray-100 transition">Learn More</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-gradient-to-br from-red-600 to-orange-300 p-6 rounded shadow text-center">
|
||||
<h3 class="text-lg font-bold mb-2">My Projects</h3>
|
||||
<p>Check out my latest projects!</p>
|
||||
|
||||
<br>
|
||||
|
||||
<a href="/projects" class="bg-blue-400 inline-block text-white font-semibold px-4 py-2 rounded shadpw hpver:bg-gray-100 transition">Learn More</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-gradient-to-br from-red-600 to-orange-300 p-6 rounded shadow text-center">
|
||||
<h3 class="text-lg font-bold mb-2">My Blog</h3>
|
||||
<p>Read my latest Blog-Posts!</p>
|
||||
|
||||
<br>
|
||||
|
||||
<a href="/blog" class="bg-blue-400 inline-block text-white font-semibold px-4 py-2 rounded shadpw hpver:bg-gray-100 transition">Learn More</a>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
66
templates/login.html
Normal file
66
templates/login.html
Normal file
@@ -0,0 +1,66 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
|
||||
<title>Login</title>
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(to right, #667eea, #764ba2);
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.login-container {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
.logo-placeholder {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: #ddd;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0 auto 1rem;
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 100px; /* Adjust size as needed */
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 0 auto 1rem; /* Centers the logo */
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<img src="{{ url_for('static', filename='Netdeploy.jpg') }}" alt="Logo" class="logo">
|
||||
|
||||
|
||||
<h2 class="mb-3">Login</h2>
|
||||
<form action="/login" method="POST">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
78
templates/new_blog.html
Normal file
78
templates/new_blog.html
Normal file
@@ -0,0 +1,78 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}New Blog Post{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<section class="py-20 bg-gradient-to-br from-red-800 to-orange-400 text-white text-center">
|
||||
<h1 class="text-5xl font-extrabold mb-2">Create a New Blog Post</h1>
|
||||
<p class="text-lg">Share your thoughts, tutorials, or experiences with the world.</p>
|
||||
</section>
|
||||
|
||||
<section class="py-16 bg-black text-white">
|
||||
<div class="max-w-3xl mx-auto px-6">
|
||||
<form action="{{ url_for('new_blog') }}" method="post" enctype="multipart/form-data" class="space-y-6">
|
||||
|
||||
|
||||
<div>
|
||||
<label for="title" class="block font-semibold mb-1">Post Title</label>
|
||||
<input type="text" id="title" name="title" required
|
||||
class="text-black w-full border border-gray-300 rounded px-4 py-2 focus:ring-2 focus:ring-orange-400 focus:outline-none">
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label for="content" class="block font-semibold mb-1">Post Content</label>
|
||||
<textarea id="content" name="content" required rows="6"
|
||||
class="text-black w-full border border-gray-300 rounded px-4 py-2 focus:ring-2 focus:ring-orange-400 focus:outline-none resize-none"></textarea>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label for="category" class="block font-semibold mb-1">Category</label>
|
||||
<input type="text" id="category" name="category" placeholder="What's This Post About?"
|
||||
class="text-black w-full border border-gray-300 rounded px-4 py-2 focus:ring-2 focus:ring-orange-400 focus:outline-none">
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label for="tags" class="block font-semibold mb-1">Tags (comma-separated)</label>
|
||||
<input type="text" id="tags" name="tags" placeholder="e.g., Flask, SQLite, API"
|
||||
class="text-black w-full border border-gray-300 rounded px-4 py-2 focus:ring-2 focus:ring-orange-400 focus:outline-none">
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label for="images" class="block font-semibold mb-1">Upload Images</label>
|
||||
<input type="file" name="images" id="images" multiple
|
||||
class="text-black w-full border border-gray-300 rounded px-3 py-2 text-gray-700 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:bg-orange-600 file:text-white hover:file:bg-orange-700">
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="pt-4">
|
||||
<button type="submit"
|
||||
class="bg-orange-600 text-white px-6 py-2 rounded hover:bg-orange-700 transition">
|
||||
Submit Post
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-8 text-center">
|
||||
<a href="{{ url_for('blog') }}" class="text-blue-600 hover:underline">← Back to Blog</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Textarea autoresize -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const textarea = document.getElementById('content');
|
||||
textarea.addEventListener('input', function () {
|
||||
this.style.height = 'auto';
|
||||
this.style.height = this.scrollHeight + 'px';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
78
templates/new_project.html
Normal file
78
templates/new_project.html
Normal file
@@ -0,0 +1,78 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}New Project Post{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<section class="py-20 bg-gradient-to-br from-red-800 to-orange-400 text-white text-center">
|
||||
<h1 class="text-5xl font-extrabold mb-2">Create a New Project Post</h1>
|
||||
<p class="text-lg">Share your projects with the world.</p>
|
||||
</section>
|
||||
|
||||
<section class="py-16 bg-black text-white">
|
||||
<div class="max-w-3xl mx-auto px-6">
|
||||
<form action="{{ url_for('new_project') }}" method="post" enctype="multipart/form-data" class="space-y-6">
|
||||
|
||||
|
||||
<div>
|
||||
<label for="title" class="block font-semibold mb-1">Post Title</label>
|
||||
<input type="text" id="title" name="title" required
|
||||
class="text-black w-full border border-gray-300 rounded px-4 py-2 focus:ring-2 focus:ring-orange-400 focus:outline-none">
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label for="content" class="block font-semibold mb-1">Post Content</label>
|
||||
<textarea id="content" name="content" required rows="6"
|
||||
class="text-black w-full border border-gray-300 rounded px-4 py-2 focus:ring-2 focus:ring-orange-400 focus:outline-none resize-none"></textarea>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label for="category" class="block font-semibold mb-1">Category</label>
|
||||
<input type="text" id="category" name="category" placeholder="What's This Post About?"
|
||||
class="text-black w-full border border-gray-300 rounded px-4 py-2 focus:ring-2 focus:ring-orange-400 focus:outline-none">
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label for="tags" class="block font-semibold mb-1">Tags (comma-separated)</label>
|
||||
<input type="text" id="tags" name="tags" placeholder="e.g., Flask, SQLite, API"
|
||||
class="text-black w-full border border-gray-300 rounded px-4 py-2 focus:ring-2 focus:ring-orange-400 focus:outline-none">
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label for="images" class="block font-semibold mb-1">Upload Images</label>
|
||||
<input type="file" name="images" id="images" multiple
|
||||
class="text-black w-full border border-gray-300 rounded px-3 py-2 text-gray-700 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:bg-orange-600 file:text-white hover:file:bg-orange-700">
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="pt-4">
|
||||
<button type="submit"
|
||||
class="bg-orange-600 text-white px-6 py-2 rounded hover:bg-orange-700 transition">
|
||||
Submit Post
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-8 text-center">
|
||||
<a href="{{ url_for('blog') }}" class="text-blue-600 hover:underline">← Back to Blog</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Textarea autoresize -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const textarea = document.getElementById('content');
|
||||
textarea.addEventListener('input', function () {
|
||||
this.style.height = 'auto';
|
||||
this.style.height = this.scrollHeight + 'px';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
38
templates/project.html
Normal file
38
templates/project.html
Normal file
@@ -0,0 +1,38 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Projects{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="text-center py-20 bg-gradient-to-br from-red-800 to-orange-400 text-white">
|
||||
<h1 class="text-5xl font-extrabold mb-4">My Projects</h1>
|
||||
<p class="text-xl mb-6">Here’s a showcase of the things I’ve built or contributed to.</p>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- Projects Grid -->
|
||||
<section id="projects" class="py-16 bg-black text-white">
|
||||
<div class="max-w-6xl mx-auto px-4">
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{% for projectpost in projectpost %}
|
||||
<div class="bg-white rounded-lg shadow p-6 hover:shadow-lg transition">
|
||||
<h2 class="text-2xl font-bold text-red-800 mb-2">
|
||||
<a href="{{ url_for('view_project', slug=projectpost.slug) }}" class="hover:underline">{{ projectpost.title }}</a>
|
||||
</h2>
|
||||
<p class="text-sm text-orange-500 font-medium mb-4">{{ projectpost.category }}</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% for tag in projectpost.tags.split(',') %}
|
||||
<a href="{{ url_for('view_project_tag', tag=tag.strip()) }}"
|
||||
class="inline-block bg-orange-200 text-orange-900 text-sm px-3 py-1 rounded-full hover:bg-orange-300 transition">
|
||||
#{{ tag.strip() }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
35
templates/project_tag_results.html
Normal file
35
templates/project_tag_results.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Tag Results{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="text-center py-20 bg-gradient-to-br from-red-800 to-orange-400 text-white">
|
||||
<h1 class="text-5xl font-extrabold mb-4">Posts Tagged: <span class="text-white">#{{ tag }}</span></h1>
|
||||
<p class="text-lg">Here are all Project Posts with this tag.</p>
|
||||
</section>
|
||||
|
||||
<!-- Tag Results Content -->
|
||||
<section class="py-16 bg-black text-white">
|
||||
<div class="max-w-5xl mx-auto px-4">
|
||||
{% if projectpost %}
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{% for projectpost in projectpost %}
|
||||
<div class="bg-white rounded-lg shadow p-6 hover:shadow-lg transition">
|
||||
<h2 class="text-2xl font-bold text-red-800 mb-2">
|
||||
<a href="{{ url_for('view_project', slug=projectpost.slug) }}" class="hover:underline">
|
||||
{{ projectpost.title }}
|
||||
</a>
|
||||
</h2>
|
||||
<p class="text-sm text-orange-500 font-medium">{{ projectpost.category }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-center text-lg text-gray-600">No posts found with this tag.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
95
templates/resume.html
Normal file
95
templates/resume.html
Normal file
@@ -0,0 +1,95 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}About Me{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
<section class="text-center py-20 bg-gradient-to-br from-red-800 to-orange-400 text-white">
|
||||
<h1 class="text-5xl font-extrabold mb-4">Benjamin Mosley</h1>
|
||||
<p class="text-xl">Aspiring IT Professional & Web Developer</p>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="py-16 bg-black text-white">
|
||||
<div class="max-w-4xl mx-auto px-6 space-y-12">
|
||||
|
||||
|
||||
<div class="text-center space-y-2">
|
||||
<p><strong>Email:</strong> <a href="mailto:ben@bennyshouse.net" class="text-blue-600 hover:underline">ben@bennyshouse.net</a></p>
|
||||
<p><strong>Portfolio:</strong> <a href="https://benjaminmosley.com" class="text-blue-600 hover:underline">benjaminmosley.com</a></p>
|
||||
<p><strong>Business Website:</strong> <a href="https://bennyshouse.net" class="text-blue-600 hover:underline">bennyshouse.net</a></p>
|
||||
<p><strong>LinkedIn:</strong> <a href="https://www.linkedin.com/in/benjamin-mosley-849643329/" class="text-blue-600 hover:underline">Benjamin Mosley</a></p>
|
||||
<p><strong>GitHub:</strong> <a href="https://github.com/Benny-ui-ux" class="text-blue-600 hover:underline">github.com/Benny-ui-ux</a></p>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold text-orange-600 mb-4">Education</h2>
|
||||
<ul class="space-y-3 text-lg">
|
||||
<li><strong>High School Graduation:</strong> Mother of Divine Grace – June 2022</li>
|
||||
<li><strong>College:</strong> West Texas A&M University<br>
|
||||
<span class="text-sm text-white italic">Currently pursuing B.S. in Computer Information Systems, graduating Spring 2026</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold text-orange-600 mb-4">Background</h2>
|
||||
<p class="text-lg leading-relaxed">
|
||||
I’ve been working with computers for as long as I can remember — what started as curiosity has grown into a passion. My technical journey continues to evolve through real-world experience and ongoing education.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold text-orange-600 mb-4">Skills</h2>
|
||||
<ul class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-lg list-disc pl-6">
|
||||
<li>Python Programming</li>
|
||||
<li>Linux Operating Systems</li>
|
||||
<li>Web Design & Hosting</li>
|
||||
<li>SQL & Database Management</li>
|
||||
<li>Leadership & Teamwork</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold text-orange-600 mb-4">Work Experience</h2>
|
||||
<div class="space-y-6 text-lg">
|
||||
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold">United Supermarkets – Borger, TX</h3>
|
||||
<p class="italic text-white">Grocery Team Lead | Nov 2019 – Aug 2022</p>
|
||||
<ul class="list-disc pl-5 mt-2">
|
||||
<li>Stocked shelves and maintained floor organization</li>
|
||||
<li>Assisted customers and team members</li>
|
||||
<li>Managed ordering and inventory processes</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold">The Pergola Shop – Canyon, TX</h3>
|
||||
<p class="italic text-white">Wood Stainer / Cutter | Aug 2022 – Aug 2023</p>
|
||||
<ul class="list-disc pl-5 mt-2">
|
||||
<li>Prepped and treated wood materials for construction</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold">United Supermarkets – Canyon, TX</h3>
|
||||
<p class="italic text-white">Service Center / Bookkeeper | Aug 2023 – Present</p>
|
||||
<ul class="list-disc pl-5 mt-2">
|
||||
<li>Nightly accounting duties and managment of my team</li>
|
||||
<li>Provided high-quality customer service</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
78
templates/view_blog.html
Normal file
78
templates/view_blog.html
Normal file
@@ -0,0 +1,78 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ blogpost.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<!-- Hero Title Section -->
|
||||
<section class="py-20 bg-gradient-to-br from-red-800 to-orange-400 text-white text-center">
|
||||
<h1 class="text-5xl font-extrabold mb-4">{{ blogpost.title }}</h1>
|
||||
<p class="text-lg italic">Category: {{ blogpost.category }}</p>
|
||||
<div class="mt-4 flex justify-center flex-wrap gap-2">
|
||||
{% for tag in blogpost.tags.split(',') %}
|
||||
<a href="{{ url_for('view_tag', tag=tag.strip()) }}"
|
||||
class="bg-white text-gray-800 text-sm px-3 py-1 rounded-full shadow hover:bg-gray-100 transition">
|
||||
#{{ tag.strip() }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Content Section -->
|
||||
<section class="py-16 bg-black text-white">
|
||||
<div class="max-w-4xl mx-auto px-4 space-y-8">
|
||||
|
||||
{% if blogpost.images %}
|
||||
<div class="mb-5">
|
||||
<h3 class="fs-3 fw-semibold text-orange mb-4">Gallery</h3>
|
||||
|
||||
<div id="blogImageCarousel" class="carousel slide shadow rounded overflow-hidden" data-bs-ride="carousel">
|
||||
<div class="carousel-inner">
|
||||
{% for image in blogpost.images|from_json %}
|
||||
<div class="carousel-item {% if loop.first %}active{% endif %}">
|
||||
<img src="{{ url_for('static', filename='uploads/' + image) }}" class="d-block w-100" alt="Post Image"
|
||||
style="max-height: 500px; object-fit: cover;">
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<button class="carousel-control-prev" type="button" data-bs-target="#blogImageCarousel" data-bs-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
<span class="visually-hidden">Previous</span>
|
||||
</button>
|
||||
<button class="carousel-control-next" type="button" data-bs-target="#blogImageCarousel" data-bs-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
<span class="visually-hidden">Next</span>
|
||||
</button>
|
||||
|
||||
<!-- Optional indicators -->
|
||||
<div class="carousel-indicators">
|
||||
{% for image in blogpost.images|from_json %}
|
||||
<button type="button" data-bs-target="#blogImageCarousel" data-bs-slide-to="{{ loop.index0 }}"
|
||||
{% if loop.first %}class="active" aria-current="true"{% endif %} aria-label="Slide {{ loop.index }}"></button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
<!-- Blog Content -->
|
||||
<div class="prose lg:prose-lg max-w-none">
|
||||
{{ blogpost.content | safe }}
|
||||
</div>
|
||||
|
||||
<!-- Back Button -->
|
||||
<div class="pt-10 text-center">
|
||||
<a href="{{ url_for('blog') }}"
|
||||
class="inline-block bg-orange-600 text-white px-6 py-2 rounded hover:bg-orange-700 transition">
|
||||
← Back to Blog
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
77
templates/view_project.html
Normal file
77
templates/view_project.html
Normal file
@@ -0,0 +1,77 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ projectpost.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<!-- Hero Title Section -->
|
||||
<section class="py-20 bg-gradient-to-br from-red-800 to-orange-400 text-white text-center">
|
||||
<h1 class="text-5xl font-extrabold mb-4">{{ projectpost.title }}</h1>
|
||||
<p class="text-lg italic">Category: {{ projectpost.category }}</p>
|
||||
<div class="mt-4 flex justify-center flex-wrap gap-2">
|
||||
{% for tag in projectpost.tags.split(',') %}
|
||||
<a href="{{ url_for('view_tag', tag=tag.strip()) }}"
|
||||
class="bg-white text-gray-800 text-sm px-3 py-1 rounded-full shadow hover:bg-gray-100 transition">
|
||||
#{{ tag.strip() }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Content Section -->
|
||||
<section class="py-16 bg-black text-white">
|
||||
<div class="max-w-4xl mx-auto px-4 space-y-8">
|
||||
|
||||
{% if projectpost.images %}
|
||||
<div class="mb-5">
|
||||
<h3 class="fs-3 fw-semibold text-orange mb-4">Gallery</h3>
|
||||
|
||||
<div id="blogImageCarousel" class="carousel slide shadow rounded overflow-hidden" data-bs-ride="carousel">
|
||||
<div class="carousel-inner">
|
||||
{% for image in projectpost.images|from_json %}
|
||||
<div class="carousel-item {% if loop.first %}active{% endif %}">
|
||||
<img src="{{ url_for('static', filename='uploads/' + image) }}" class="d-block w-100" alt="Post Image"
|
||||
style="max-height: 500px; object-fit: cover;">
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<button class="carousel-control-prev" type="button" data-bs-target="#blogImageCarousel" data-bs-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
<span class="visually-hidden">Previous</span>
|
||||
</button>
|
||||
<button class="carousel-control-next" type="button" data-bs-target="#blogImageCarousel" data-bs-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
<span class="visually-hidden">Next</span>
|
||||
</button>
|
||||
|
||||
<!-- Optional indicators -->
|
||||
<div class="carousel-indicators">
|
||||
{% for image in projectpost.images|from_json %}
|
||||
<button type="button" data-bs-target="#blogImageCarousel" data-bs-slide-to="{{ loop.index0 }}"
|
||||
{% if loop.first %}class="active" aria-current="true"{% endif %} aria-label="Slide {{ loop.index }}"></button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<!-- Blog Content -->
|
||||
<div class="prose lg:prose-lg max-w-none">
|
||||
{{ projectpost.content | safe }}
|
||||
</div>
|
||||
|
||||
<!-- Back Button -->
|
||||
<div class="pt-10 text-center">
|
||||
<a href="{{ url_for('projects') }}"
|
||||
class="inline-block bg-orange-600 text-white px-6 py-2 rounded hover:bg-orange-700 transition">
|
||||
← Back to Projects
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user