commit df1c16aa50904cb0d0fe0c011e03217f722ad72a Author: ION606 Date: Fri Feb 14 22:37:52 2025 -0500 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed5888b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.venv +instance +sites diff --git a/app.py b/app.py new file mode 100644 index 0000000..a7a60ed --- /dev/null +++ b/app.py @@ -0,0 +1,312 @@ +from flask import ( + Flask, + render_template, + request, + redirect, + url_for, + send_from_directory, + flash, + abort, +) +from flask_sqlalchemy import SQLAlchemy +from flask_login import ( + LoginManager, + UserMixin, + login_user, + logout_user, + login_required, + current_user, +) +from flask_apscheduler import APScheduler +from werkzeug.security import generate_password_hash, check_password_hash +from werkzeug.exceptions import NotFound +import os +import shutil +from datetime import datetime, timedelta + +app = Flask(__name__) +app.config["SECRET_KEY"] = "your-secret-key" +app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///db.sqlite" +app.config["UPLOAD_FOLDER"] = "sites" +db = SQLAlchemy(app) + + +# Models +class User(UserMixin, db.Model): + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(100), unique=True) + password_hash = db.Column(db.String(128)) # Renamed for clarity + + def set_password(self, password): + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + return check_password_hash(self.password_hash, password) + + +class Site(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer) + name = db.Column(db.String(100)) + last_accessed = db.Column(db.DateTime) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + +# Auth setup +login_manager = LoginManager() +login_manager.init_app(app) +login_manager.login_view = "login" + + +@login_manager.user_loader +def load_user(user_id): + return User.query.get(int(user_id)) + + +# Scheduler for auto-deletion +scheduler = APScheduler() +scheduler.init_app(app) + + +def delete_inactive_sites(): + with app.app_context(): + cutoff = datetime.utcnow() - timedelta(days=30) + sites = Site.query.filter(Site.last_accessed < cutoff).all() + for site in sites: + site_dir = os.path.join( + app.config["UPLOAD_FOLDER"], str(site.user_id), str(site.id) + ) + if os.path.exists(site_dir): + shutil.rmtree(site_dir) + db.session.delete(site) + db.session.commit() + + +if not scheduler.running: + scheduler.start() + scheduler.add_job( + id="delete_job", func=delete_inactive_sites, trigger="interval", days=1 + ) + + +# Routes +@app.route("/login", methods=["GET", "POST"]) +def login(): + if request.method == "POST": + email = request.form.get("email") + password = request.form.get("password") + user = User.query.filter_by(email=email).first() + + if user and user.check_password(password): + login_user(user) + return redirect(url_for("dashboard")) + flash("Invalid email or password") + return render_template("login.html") + + +@app.route("/logout") +def logout(): + logout_user() + return redirect(url_for("home")) + + +@app.route("/register", methods=["GET", "POST"]) +def register(): + if request.method == "POST": + email = request.form.get("email") + password = request.form.get("password") + + if User.query.filter_by(email=email).first(): + flash("Email already exists") + return redirect(url_for("register")) + + new_user = User(email=email) + new_user.set_password(password) + db.session.add(new_user) + db.session.commit() + + login_user(new_user) + return redirect(url_for("dashboard")) + return render_template("register.html") + + +@app.route("/dashboard") +@login_required +def dashboard(): + sites = Site.query.filter_by(user_id=current_user.id).all() + return render_template("dashboard.html", sites=sites) + + +@app.route("/upload", methods=["POST"]) +@login_required +def upload_site(): + site_name = request.form.get("name") + if not site_name: + flash("Site name is required", "error") + return redirect(url_for("dashboard")) + + # Check if index.html is included + files = request.files.getlist("files") + if not any(file.filename == "index.html" for file in files): + flash("You must include an index.html file", "error") + return redirect(url_for("dashboard")) + + # Create site directory + site = Site(user_id=current_user.id, name=site_name) + db.session.add(site) + db.session.commit() + + site_dir = os.path.join( + app.config["UPLOAD_FOLDER"], str(current_user.id), str(site.id) + ) + os.makedirs(site_dir, exist_ok=True) + + # Save uploaded files + for file in files: + if file.filename == "": + continue + file.save(os.path.join(site_dir, file.filename)) + + flash("Site created successfully!", "success") + return redirect(url_for("dashboard")) + + +@app.route("/edit/", methods=["GET", "POST"]) +@login_required +def edit_site(site_id): + site = Site.query.get_or_404(site_id) + if site.user_id != current_user.id: + return "Unauthorized", 403 + + site_dir = os.path.join( + app.config["UPLOAD_FOLDER"], str(current_user.id), str(site.id) + ) + + if request.method == "POST": + site.name = request.form.get("name", site.name) + db.session.commit() + + for filename, content in request.form.items(): + if filename.endswith((".html", ".css", ".js")): + filepath = os.path.join(site_dir, filename) + with open(filepath, "w") as f: + f.write(content) + + return redirect(url_for("dashboard")) + + files = {} + for file in os.listdir(site_dir): + with open(os.path.join(site_dir, file), "r") as f: + files[file] = f.read() + + return render_template("edit.html", site=site, files=files) + + +@app.route("/delete/", methods=["POST"]) +@login_required +def delete_site(site_id): + site = Site.query.get_or_404(site_id) + if site.user_id != current_user.id: + return "Unauthorized", 403 + + site_dir = os.path.join( + app.config["UPLOAD_FOLDER"], str(current_user.id), str(site.id) + ) + if os.path.exists(site_dir): + shutil.rmtree(site_dir) + + db.session.delete(site) + db.session.commit() + return redirect(url_for("dashboard")) + + +@app.route("/delete_file//", methods=["POST"]) +@login_required +def delete_file(site_id, filename): + # Get the site and verify ownership + site = Site.query.get_or_404(site_id) + if site.user_id != current_user.id: + return "Unauthorized", 403 + + # Build the file path + site_dir = os.path.join( + app.config["UPLOAD_FOLDER"], str(current_user.id), str(site.id) + ) + file_path = os.path.join(site_dir, filename) + + # Delete the file if it exists + if os.path.exists(file_path): + os.remove(file_path) + flash(f"File '{filename}' deleted successfully!", "success") + else: + flash(f"File '{filename}' not found!", "error") + + return redirect(url_for("dashboard")) + + +@app.route("/site//") +def redirect_to_slash(user_id, site_id): + """Redirect URLs without trailing slash to the slash version""" + return redirect( + url_for( + "serve_site_content", + user_id=user_id, + site_id=site_id, + filename="index.html", + ), + code=301, + ) + + +@app.route("/site///", defaults={"filename": "index.html"}) +@app.route("/site///") +def serve_site_content(user_id, site_id, filename): + # Update last accessed time + site = Site.query.get_or_404(site_id) + site.last_accessed = datetime.utcnow() + db.session.commit() + + # Get site directory + site_dir = os.path.join(app.config["UPLOAD_FOLDER"], str(user_id), str(site_id)) + + # Security check + if '..' in filename or filename.startswith('/') or not os.path.exists(site_dir): + abort(404) + + try: + # First try to serve requested file + return send_from_directory(site_dir, filename) + except NotFound: + # Handle extensionless URLs and SPA-style routing + if "." not in filename: + # Try with .html extension + try: + return send_from_directory(site_dir, f"{filename}.html") + except NotFound: + # Fallback to index.html for client-side routing + return send_from_directory(site_dir, "index.html") + abort(404) + + +def list_files(directory): + try: + return os.listdir(directory) + except FileNotFoundError: + return [] + + +@app.context_processor +def inject_utilities(): + return dict(list_files=list_files) + + +@app.route("/") +def home(): + return render_template("home.html") + + +if __name__ == "__main__": + os.makedirs(app.config["UPLOAD_FOLDER"], exist_ok=True) + with app.app_context(): + db.create_all() + app.run(debug=True) diff --git a/static/editor.js b/static/editor.js new file mode 100644 index 0000000..f7a0e46 --- /dev/null +++ b/static/editor.js @@ -0,0 +1,87 @@ +function formatOption(option) { + if (!option.id) { + return option.text; + } + const $option = $(` +
+ ${option.text} +
+ `); + return $option; +} + + +document.addEventListener("DOMContentLoaded", () => { + const storedTheme = localStorage.getItem('editortheme'); + + // for each textarea (one per file) replace with Ace Editor + const editors = Array.from(document.querySelectorAll('textarea')).map((textarea) => { + const language = textarea.dataset.language, + themeStyle = document.querySelector('#theme-style'), + isDark = themeStyle.href.includes('dark-styles.css'), + aceTheme = isDark ? "ace/theme/twilight" : "ace/theme/chrome", + editorContainer = document.createElement('div'); + + editorContainer.style.width = "100%"; + editorContainer.style.height = "400px"; + + // insert the container after the textarea + textarea.parentNode.insertBefore(editorContainer, textarea.nextSibling); + // hide the original textarea + textarea.style.display = "none"; + + const editor = ace.edit(editorContainer); + editor.setTheme(storedTheme || aceTheme); + + let aceMode = "ace/mode/html"; // default fallback + if (language === "css") { + aceMode = "ace/mode/css"; + } else if (language === "javascript") { + aceMode = "ace/mode/javascript"; + } else if (language === "htmlmixed") { + aceMode = "ace/mode/html"; + } + editor.session.setMode(aceMode); + + // set initial content from the textarea and add options + editor.session.setValue(textarea.value.trim()); + editor.setOptions({ + fontSize: "14px", + tabSize: 4, + useSoftTabs: true, + wrap: true, + showPrintMargin: false + }); + + // update the hidden textarea whenever the Ace content changes + editor.session.on('change', () => { + textarea.value = editor.getValue(); + }); + + return editor; + }); + + // Retrieve the list of available themes + const ThemeList = ace.require("ace/ext/themelist"), + themes = ThemeList.themes, + themeSelector = document.querySelector('#theme-selector'); + + themes.forEach(function (theme) { + var option = document.createElement('option'); + option.value = theme.theme; + option.textContent = theme.caption; + themeSelector.appendChild(option); + }); + + if (storedTheme) themeSelector.value = storedTheme; + + // Initialize Select2 on the theme selector + $(themeSelector).select2({ + templateResult: formatOption, + placeholder: 'Select a theme', + allowClear: true + }).on('change', function (_) { + localStorage.setItem('editortheme', themeSelector.value) + editors.forEach(editor => editor.setTheme(themeSelector.value)); + }); +}); \ No newline at end of file diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 0000000..f9fcd73 --- /dev/null +++ b/static/styles.css @@ -0,0 +1,302 @@ +:root { + /* Light mode variables */ + --bg-color: #f0f2f5; + --text-color: #333; + --card-bg: #ffffff; + --border-color: #ddd; + --hover-bg: #f5f5f5; + --accent-color: #1abc9c; + --danger-color: #e74c3c; + --success-color: #155724; + --error-color: #721c24; + --nav-bg: #2c3e50; + --nav-text: #ffffff; + --input-bg: #ffffff; + --input-text: #333; + --input-border: #ccc; + --file-icon-brightness: 1; +} + +[data-theme="dark"] { + /* Dark mode variables */ + --bg-color: #121212; + --text-color: #e0e0e0; + --card-bg: #1f1f1f; + --border-color: #333; + --hover-bg: #2d2d2d; + --accent-color: #1abc9c; + --danger-color: #c0392b; + --success-color: #d4edda; + --error-color: #f8d7da; + --nav-bg: #1f1f1f; + --nav-text: #e0e0e0; + --input-bg: #1f1f1f; + --input-text: #e0e0e0; + --input-border: #444; + --file-icon-brightness: 0.8; +} + +/* General Styles */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; + scroll-behavior: smooth; +} + +body { + font-family: 'Arial', sans-serif; + line-height: 1.6; + background-color: var(--bg-color); + color: var(--text-color); + padding: 20px; + transition: background-color 0.3s ease, color 0.3s ease; +} + +.container { + max-width: 1100px; + margin: 0 auto; + padding: 20px; +} + +/* Navigation Bar */ +.navbar { + background-color: var(--nav-bg); + color: var(--nav-text); + padding: 1rem; + margin-bottom: 2rem; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); +} + +.navbar a { + color: var(--nav-text); + text-decoration: none; + margin-right: 1.5rem; + font-weight: bold; + transition: color 0.3s; +} + +.navbar a:hover { + color: var(--accent-color); +} + +/* Forms and Inputs */ +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: bold; + color: var(--text-color); +} + +input[type="text"], +input[type="email"], +input[type="password"], +input[type="file"] { + width: 100%; + padding: 12px; + background-color: var(--input-bg); + color: var(--input-text); + border: 1px solid var(--input-border); + border-radius: 6px; + font-size: 1rem; + transition: all 0.3s ease; +} + +input:focus { + border-color: var(--accent-color); + outline: none; + box-shadow: 0 0 5px rgba(26, 188, 156, 0.5); +} + +/* Buttons */ +button, +.btn { + padding: 10px 18px; + margin: 5px; + text-decoration: none; + color: white; + background-color: var(--accent-color); + border: none; + border-radius: 6px; + display: inline-block; + font-size: 1rem; + cursor: pointer; + transition: all 0.3s ease; +} + +button:hover, +.btn:hover { + background-color: #16a085; + transform: translateY(-2px); +} + +.btn-danger { + background-color: var(--danger-color); +} + +.btn-danger:hover { + filter: brightness(0.9); +} + +/* File Upload and Preview */ +.file-upload-container { + position: relative; + overflow: hidden; + margin: 10px 0; +} + +.file-upload-button { + display: inline-block; + padding: 10px 20px; + background-color: var(--accent-color); + color: white; + border-radius: 5px; + cursor: pointer; + transition: filter 0.3s; + border: none; + font-size: 14px; +} + +.file-upload-button:hover { + filter: brightness(0.9); +} + +.file-preview { + margin-top: 15px; + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 15px; +} + +.file-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 15px; + margin-top: 10px; +} + +.file-card { + position: relative; + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 15px; + text-align: center; + transition: transform 0.2s; +} + +.file-card:hover { + transform: translateY(-2px); + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1); +} + +.file-icon { + width: 48px; + height: 48px; + margin: 0 auto 10px; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + filter: brightness(var(--file-icon-brightness)); +} + +.file-icon.html { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 384 512'%3E%3Cpath fill='%23e34f26' d='M0 32l34.9 395.8L192 480l157.1-52.2L384 32H0zm308.2 127.9H124.4l4.1 49.4h175.6l-13.6 148.4-97.9 27v.3h-1.1l-98.7-27.3-6-75.8h47.7L138 320l53.5 14.5 53.7-14.5 6-62.2H84.3L71.5 112.2h241.1l-4.4 47.7z'/%3E%3C/svg%3E"); +} + +.file-icon.css { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 384 512'%3E%3Cpath fill='%232649ad' d='M0 32l34.9 395.8L192 480l157.1-52.2L384 32H0zm313.1 80l-4.8 47.3L193 208.9l-.3.1h111.5l-12.8 146.6-98.2 28.7-98.8-29.2-6.4-73.9h48.9l3.2 38.3 52.6 13.3 54.7-15.4 3.7-61.6-119.3-.3-2.2-24.7L237.6 138H0l22.8-31.3L64.1 64H320l-6.9 48z'/%3E%3C/svg%3E"); +} + +.file-icon.js { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath fill='%23f7df1e' d='M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zM243.8 381.4c0 43.6-25.6 63.5-62.9 63.5-33.7 0-53.2-17.4-63.2-38.5l34.3-20.7c6.6 11.7 12.6 21.6 27.1 21.6 13.8 0 22.6-5.4 22.6-26.5V237.7h42.1v143.7zm99.6 63.5c-39.1 0-64.4-18.6-76.7-43l34.3-19.8c9 14.7 20.8 25.6 41.5 25.6 17.4 0 28.6-8.7 28.6-20.8 0-14.4-11.4-19.5-30.7-28l-10.5-4.5c-30.4-12.9-50.5-29.2-50.5-63.5 0-31.6 24.1-55.6 61.6-55.6 26.8 0 46 9.3 59.8 33.7L368 290c-7.2-12.9-15-18-27.1-18-12.3 0-20.1 7.8-20.1 18 0 12.6 7.8 17.7 25.9 25.6l10.5 4.5c35.8 15.3 55.9 31 55.9 66.2 0 37.8-29.8 58.6-69.7 58.6z'/%3E%3C/svg%3E"); +} + +.file-name { + display: block; + font-size: 0.8rem; + word-break: break-word; + color: var(--text-color); +} + +.file-close { + position: absolute; + top: 5px; + right: 5px; + background: transparent; + border: none; + color: #999; + font-size: 1.2rem; + line-height: 1; + cursor: pointer; + padding: 2px 5px; +} + +.file-close:hover { + color: var(--danger-color); +} + +/* Site Cards */ +.site-card { + background-color: var(--card-bg); + border: 1px solid var(--border-color); + color: var(--text-color); + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1); + border-radius: 6px; + padding: 15px; + transition: all 0.3s ease; +} + +.site-card:hover { + transform: scale(1.02); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.15); +} + +/* Theme Toggle */ +#theme-toggle { + background-color: var(--card-bg); + color: var(--text-color); + border: 1px solid var(--border-color); + padding: 8px 14px; + cursor: pointer; + font-weight: bold; + border-radius: 6px; + transition: all 0.3s ease; +} + +#theme-toggle:hover { + background-color: var(--hover-bg); + transform: translateY(-2px); +} + +.upload-label { + display: inline-block; + padding: 10px 20px; + background-color: #007bff; + color: white; + font-size: 16px; + border-radius: 5px; + cursor: pointer; + width: 150px !important; + transition: background 0.3s ease; + text-align: center; +} + +.upload-label:hover { + background-color: #0056b3; +} + +@media (max-width: 768px) { + .file-grid { + grid-template-columns: repeat(2, 1fr); + } +} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..fbf5539 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,65 @@ + + + + + + + Static Site Host - {% block title %}{% endblock %} + + + + + + + + + +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+ + + \ No newline at end of file diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 0000000..c598a06 --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,122 @@ +{% extends "base.html" %} + +{% block title %}Dashboard{% endblock %} + +{% block content %} +

Your Sites

+ +

Create New Site

+
+
+ + +
+ +
+ +
+ + +
+
Selected Files:
+
+
+ + +
+ +
+ +

My Sites

+ +
+ {% for site in sites %} +
+

{{ site.name }}

+

Created: {{ site.created_at.strftime('%Y-%m-%d') }}

+ + +
+
Uploaded Files:
+
+ {% set site_dir = 'sites/' + current_user.id|string + '/' + site.id|string %} + {% for file in list_files(site_dir) %} +
+
+
+ {{ file }} +
+ +
+
+ {% endfor %} +
+
+ + +
+ Edit + View Site +
+ +
+
+
+ {% endfor %} +
+ + + + +{% endblock %} \ No newline at end of file diff --git a/templates/edit.html b/templates/edit.html new file mode 100644 index 0000000..6cd4739 --- /dev/null +++ b/templates/edit.html @@ -0,0 +1,59 @@ +{% extends "base.html" %} + +{% block title %}Edit Site{% endblock %} + +{% block content %} +

Edit {{ site.name }}

+View + Site + +
+ + + + + +
+
+ + +
+ + {% for filename, content in files.items() %} +
+ + +
+ {% endfor %} + + +
+ + + + + + + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 0000000..70f26a1 --- /dev/null +++ b/templates/home.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block title %}Home{% endblock %} + +{% block content %} +

Welcome to Static Site Host

+

Upload and manage your static websites easily!

+{% endblock %} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..3fd223d --- /dev/null +++ b/templates/login.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% block title %}Login{% endblock %} + +{% block content %} +

Login

+
+
+ + +
+
+ + +
+ +
+{% endblock %} \ No newline at end of file diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 0000000..ab1250e --- /dev/null +++ b/templates/register.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% block title %}Register{% endblock %} + +{% block content %} +

Register

+
+
+ + +
+
+ + +
+ +
+{% endblock %} \ No newline at end of file diff --git a/test/about.html b/test/about.html new file mode 100644 index 0000000..c923b98 --- /dev/null +++ b/test/about.html @@ -0,0 +1,26 @@ + + + + + + + About Page + + + + +
+

About Us

+ +
+
+

This is the about page. Learn more about us here!

+ +
+ + + + \ No newline at end of file diff --git a/test/index.html b/test/index.html new file mode 100644 index 0000000..dea7b4f --- /dev/null +++ b/test/index.html @@ -0,0 +1,26 @@ + + + + + + + Home Page + + + + +
+

Welcome to My Site

+ +
+
+

This is the home page. Check out the About page!

+ +
+ + + + \ No newline at end of file diff --git a/test/script.js b/test/script.js new file mode 100644 index 0000000..e882c90 --- /dev/null +++ b/test/script.js @@ -0,0 +1,8 @@ +document.addEventListener('DOMContentLoaded', function () { + const button = document.querySelector('#click-me'); + if (button) { + button.addEventListener('click', function () { + alert('Button clicked!'); + }); + } +}); \ No newline at end of file diff --git a/test/styles.css b/test/styles.css new file mode 100644 index 0000000..d5d0a4f --- /dev/null +++ b/test/styles.css @@ -0,0 +1,222 @@ +/* General Styles */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; + scroll-behavior: smooth; +} + +body { + font-family: 'Arial', sans-serif; + line-height: 1.6; + background-color: #0c1522; + color: #cccccc; + padding: 20px; + transition: background-color 0.3s ease, color 0.3s ease; +} + +.container { + max-width: 1100px; + margin: 0 auto; + padding: 20px; +} + +/* Navigation Bar */ +.navbar { + background-color: #325270; + color: white; + padding: 1rem; + margin-bottom: 2rem; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); +} + +.navbar a { + color: white; + text-decoration: none; + margin-right: 1.5rem; + font-weight: bold; + transition: color 0.3s; +} + +.navbar a:hover { + color: #1abc9c; +} + +/* Flash Messages */ +.flash-message { + padding: 12px; + margin-bottom: 1rem; + border-radius: 6px; + font-weight: bold; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.flash-success { + background-color: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.flash-error { + background-color: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +/* Forms */ +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: bold; + color: #2c3e50; +} + +input[type="text"], +input[type="email"], +input[type="password"] { + width: 100%; + padding: 12px; + border: 1px solid #ccc; + border-radius: 6px; + font-size: 1rem; + transition: all 0.3s ease; +} + +input:focus { + border-color: #1abc9c; + outline: none; + box-shadow: 0 0 5px rgba(26, 188, 156, 0.5); +} + +/* Buttons */ +button, +.btn { + padding: 10px 18px; + margin: 5px; + text-decoration: none; + color: white; + background-color: #1abc9c; + border: none; + border-radius: 6px; + display: inline-block; + font-size: 1rem; + cursor: pointer; + transition: all 0.3s ease; +} + +.btn-danger { + background-color: #e74c3c; +} + +.btn-danger:hover { + background-color: #c0392b; +} + +button:hover, +.btn:hover { + background-color: #16a085; + transform: translateY(-2px); +} + +/* Site Cards */ +.site-card { + border: 1px solid #ddd; + padding: 20px; + margin-bottom: 15px; + border-radius: 6px; + background-color: white; + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; +} + +.site-card:hover { + transform: scale(1.02); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.15); +} + +.site-card h4 { + margin-bottom: 10px; + color: #333; +} + +.site-card p { + margin-bottom: 15px; + color: #555; +} + +/* Uploaded Files List */ +.uploaded-files { + margin-top: 10px; + margin-bottom: 15px; +} + +.uploaded-files h5 { + margin-bottom: 5px; + font-size: 1rem; + color: #555; +} + +.uploaded-files ul { + list-style-type: none; + padding-left: 0; +} + +.uploaded-files li { + font-size: 0.9rem; + color: #777; + padding: 5px 0; + border-bottom: 1px solid #eee; +} + +.uploaded-files li:last-child { + border-bottom: none; +} + +/* Site Actions */ +.site-actions { + display: flex; + gap: 10px; + margin-top: 10px; +} + +/* Headings */ +h1, +h2, +h3 { + color: #2c3e50; + margin-bottom: 1rem; +} + +h1 { + font-size: 2.5rem; +} + +h2 { + font-size: 2rem; +} + +h3 { + font-size: 1.5rem; +} + +/* Dark Mode Toggle */ +#theme-toggle { + margin-left: auto; + background-color: #34495e; + color: white; + border: 1px solid #2c3e50; + transition: all 0.3s ease; +} + +#theme-toggle:hover { + background-color: #2c3e50; + transform: translateY(-2px); +} \ No newline at end of file