diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..408b799
Binary files /dev/null and b/.DS_Store differ
diff --git a/.gitignore b/.gitignore
index 3729cd4..baf0908 100644
--- a/.gitignore
+++ b/.gitignore
@@ -138,4 +138,10 @@ dmypy.json
.pytype/
# Cython debug symbols
-cython_debug/
\ No newline at end of file
+cython_debug/
+
+# Old stylesheet backup
+forum/static/old_style.css
+
+.env
+
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..d2b4941
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,13 @@
+FROM python:3.11-slim
+
+WORKDIR /app
+
+COPY requirements.txt .
+
+RUN pip install --no-cache-dir -r requirements.txt
+
+COPY . .
+
+EXPOSE 5000
+
+CMD ["gunicorn", "-b", "0.0.0.0:5000", "forum.app:app"]
\ No newline at end of file
diff --git a/Documents/.DS_Store b/Documents/.DS_Store
new file mode 100644
index 0000000..85d348a
Binary files /dev/null and b/Documents/.DS_Store differ
diff --git a/Documents/CircusCircus_uml.png b/Documents/CircusCircus_uml.png
new file mode 100644
index 0000000..d59149a
Binary files /dev/null and b/Documents/CircusCircus_uml.png differ
diff --git a/Documents/Current_Module_Summary.md b/Documents/Current_Module_Summary.md
new file mode 100644
index 0000000..7233eaf
--- /dev/null
+++ b/Documents/Current_Module_Summary.md
@@ -0,0 +1,34 @@
+# CircusCircusLimes β Module Summary
+
+This is a small Flask forum app ("Schooner"). Modules:
+
+## `config.py`
+Flask config class: secret key, SQLite DB URI (`circuscircus.db`), SQLAlchemy settings. Note: secret key is hardcoded (`'kristofer'`), not loaded from env despite the commented-out `load_dotenv`.
+
+## `forum/__init__.py`
+App factory (`create_app`). Builds the Flask app, loads config, registers the `comments` and `routes` blueprints, initializes SQLAlchemy, and creates tables. Comment notes more blueprints (post/subforum routes) may be split out later.
+
+## `forum/app.py`
+Entrypoint. Creates the app via `create_app()`, sets site name/description, wires up `flask_login`'s `LoginManager` with a `user_loader`, seeds default subforums (Forum, Announcements, Bug Reports, General Discussion, Other) on first run via `init_site()`/`add_subforum()`, and defines the `/` index route listing top-level subforums.
+
+## `forum/models.py`
+SQLAlchemy models and shared helpers:
+
+- **`User`** (UserMixin): username/email/password hash, admin flag, relations to posts/comments, password hashing via werkzeug.
+- **`Post`**: title/content/postdate, tied to a user and subforum, has a relative-time cache (`get_time_string`) recalculated every 30s.
+- **`Subforum`**: title/description, self-referential parent/children, list of posts, `hidden` flag.
+- **`Comment`**: content/postdate tied to a user and post, same relative-time caching pattern as `Post`.
+- **Helpers**: `error()` (renders a red-text HTML error string β not escaped, potential XSS if fed user input), `generateLinkPath()` (builds breadcrumb HTML for a subforum chain), and `valid_title`/`valid_content` length validators.
+
+## `forum/routes.py`
+Main blueprint (`rt`) with the bulk of the app's routes: login/logout, account creation, viewing a subforum, the "add post" form, viewing a post with comments, and submitting a new post. Explicit comment in the file flags it as needing to be split into smaller route modules (`post_routes`, `subforum_routes`, etc.).
+
+## `forum/comments.py`
+`comments` blueprint with a single route, `/action_comment`, for posting a comment on a post (login required).
+
+## `forum/user.py`
+Account utility functions: username/password regex validation, and `username_taken`/`email_taken` lookups against the `User` table.
+
+---
+
+**Worth flagging:** the hardcoded `SECRET_KEY`, the unescaped HTML in `error()`, and the author's own TODO to split `routes.py` into per-feature blueprints.
diff --git a/Documents/circuscircus_future_uml.png b/Documents/circuscircus_future_uml.png
new file mode 100644
index 0000000..93ee507
Binary files /dev/null and b/Documents/circuscircus_future_uml.png differ
diff --git a/TASKS.md b/TASKS.md
new file mode 100644
index 0000000..c2a1824
--- /dev/null
+++ b/TASKS.md
@@ -0,0 +1,9 @@
+# Tasks
+
+## Active
+
+## Waiting On
+
+## Someday
+
+## Done
diff --git a/_to_delete/.env.example b/_to_delete/.env.example
new file mode 100644
index 0000000..1db8a2e
--- /dev/null
+++ b/_to_delete/.env.example
@@ -0,0 +1,13 @@
+# Copy this file to .env (already gitignored) and fill in real values.
+# .env is loaded automatically by config.py via python-dotenv.
+
+# Any private, unpredictable string. Used by Flask-Login to sign session
+# cookies -- do not commit a real value, and do not reuse the same value
+# across developers/environments if you can avoid it.
+SECRET_KEY=
+
+# Shared MySQL database credentials (see CircusCircusLimes_MySQL_Team_Setup_Guide).
+DB_USER=
+DB_PASSWORD=
+DB_HOST=localhost
+DB_NAME=circuscircuslimes
diff --git a/compose.yaml b/compose.yaml
new file mode 100644
index 0000000..afc77ed
--- /dev/null
+++ b/compose.yaml
@@ -0,0 +1,43 @@
+services:
+ web:
+ build: .
+ container_name: dishcourse-web
+
+ ports:
+ - "${APP_PORT}:5000"
+
+ environment:
+ DB_USER: ${MYSQL_USER}
+ DB_PASSWORD: ${MYSQL_PASSWORD}
+ DB_HOST: db
+ DB_NAME: ${MYSQL_DATABASE}
+
+ depends_on:
+ db:
+ condition: service_healthy
+
+ db:
+ image: mysql:8.4
+ container_name: dishcourse-mysql
+
+ environment:
+ MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
+ MYSQL_DATABASE: ${MYSQL_DATABASE}
+ MYSQL_USER: ${MYSQL_USER}
+ MYSQL_PASSWORD: ${MYSQL_PASSWORD}
+
+ ports:
+ - "127.0.0.1:${DB_PORT}:3306"
+
+ volumes:
+ - dishcourse_mysql_data:/var/lib/mysql
+
+ healthcheck:
+ test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
+ interval: 5s
+ timeout: 5s
+ retries: 10
+ start_period: 10s
+
+volumes:
+ dishcourse_mysql_data:
\ No newline at end of file
diff --git a/config.py b/config.py
index da78fea..4880a32 100644
--- a/config.py
+++ b/config.py
@@ -2,17 +2,32 @@
Flask configuration variables.
"""
from os import environ, path
+from sqlalchemy import URL
basedir = path.abspath(path.dirname(__file__))
# load_dotenv(path.join(basedir, '.env'))
class Config:
- """Set Flask configuration from .env file."""
- # General Config
SECRET_KEY = 'kristofer'
FLASK_APP = 'forum.app'
- # Database
- SQLALCHEMY_DATABASE_URI = 'sqlite:///circuscircus.db'
+ # Reject request bodies over 2.5MB outright (avatar uploads are capped at
+ # 2MB in forum/user.py; this is a blunt server-side backstop so an
+ # oversized upload doesn't even get fully read into memory).
+ MAX_CONTENT_LENGTH = int(2.5 * 1024 * 1024)
+
+ DB_USER = environ.get("DB_USER")
+ DB_PASSWORD = environ.get("DB_PASSWORD")
+ DB_HOST = environ.get("DB_HOST", "localhost")
+ DB_NAME = environ.get("DB_NAME", "circuscircuslimes")
+
+ SQLALCHEMY_DATABASE_URI = URL.create(
+ drivername="mysql+pymysql",
+ username=DB_USER,
+ password=DB_PASSWORD,
+ host=DB_HOST,
+ database=DB_NAME
+ )
+
SQLALCHEMY_ECHO = False
SQLALCHEMY_TRACK_MODIFICATIONS = False
\ No newline at end of file
diff --git a/dashboard.html b/dashboard.html
new file mode 100644
index 0000000..e2ca8a7
--- /dev/null
+++ b/dashboard.html
@@ -0,0 +1,3111 @@
+
+
+
+
+
+ Productivity
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Select your TASKS.md file
+
Select TASKS.md
+
+
+
+
+
+
+
+
Select your project folder
+
Select Folder
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/forum/__init__.py b/forum/__init__.py
index c10b0f3..124ec84 100644
--- a/forum/__init__.py
+++ b/forum/__init__.py
@@ -1,5 +1,12 @@
from flask import Flask
from forum.routes import rt
+from forum.comments import comments
+from forum.reactions import reactions
+from forum.comment_reactions import comment_reactions
+from forum.posts import posts
+from forum.messages import messages
+from forum.settings import settings
+from forum.profile import profile
def create_app():
"""Construct the core application."""
@@ -9,6 +16,13 @@ def create_app():
# post_routes
# subforum_routes
# etc
+ app.register_blueprint(posts)
+ app.register_blueprint(comments)
+ app.register_blueprint(reactions)
+ app.register_blueprint(comment_reactions)
+ app.register_blueprint(messages)
+ app.register_blueprint(settings)
+ app.register_blueprint(profile)
app.register_blueprint(rt)
# Set globals
from forum.models import db
diff --git a/forum/app.py b/forum/app.py
index 4d8a828..f367afe 100644
--- a/forum/app.py
+++ b/forum/app.py
@@ -37,6 +37,7 @@ def add_subforum(title, description, parent=None):
login_manager = LoginManager()
login_manager.init_app(app)
+login_manager.login_view = 'routes.loginform'
@login_manager.user_loader
def load_user(userid):
diff --git a/forum/comment_reactions.py b/forum/comment_reactions.py
new file mode 100644
index 0000000..c6b57e6
--- /dev/null
+++ b/forum/comment_reactions.py
@@ -0,0 +1,45 @@
+from flask import Blueprint, request, redirect
+from flask_login import login_required, current_user
+from forum.models import db, Comment, CommentReaction, error
+
+comment_reactions = Blueprint("comment_reactions", __name__)
+
+@comment_reactions.route("/action_comment_reaction", methods=["POST"])
+@login_required
+def react_to_comment():
+
+ comment_id = request.form.get("comment_id")
+
+ reaction_type = request.form.get("reaction_type")
+
+ if reaction_type not in ["like", "dislike", "heart"]:
+ return error("Invalid reaction.")
+
+ comment = Comment.query.filter(Comment.id == comment_id).first()
+
+ if not comment:
+ return error("That comment does not exist.")
+
+ existing_reaction = CommentReaction.query.filter_by(
+ user_id=current_user.id,
+ comment_id=comment.id
+ ).first()
+
+ if existing_reaction:
+
+ if existing_reaction.reaction_type == reaction_type:
+ db.session.delete(existing_reaction)
+
+ else:
+ existing_reaction.reaction_type = reaction_type
+
+ else:
+
+ reaction = CommentReaction(reaction_type)
+ reaction.user_id = current_user.id
+ reaction.comment_id = comment.id
+ db.session.add(reaction)
+
+ db.session.commit()
+
+ return redirect("/viewpost?post=" + str(comment.post_id))
\ No newline at end of file
diff --git a/forum/comments.py b/forum/comments.py
new file mode 100644
index 0000000..b088a70
--- /dev/null
+++ b/forum/comments.py
@@ -0,0 +1,43 @@
+import datetime
+from flask import Blueprint, request, redirect, flash
+from flask_login import current_user, login_required
+from forum.models import Post, Comment, db, error, valid_comment
+
+comments = Blueprint('comments', __name__)
+
+@comments.route('/action_comment', methods=['POST'])
+@login_required
+def comment():
+ post_value = request.args.get("post")
+
+ if not post_value:
+ flash("A post ID is required.", "error")
+ return redirect("/")
+
+ try:
+ post_id = int(post_value)
+ except ValueError:
+ flash("Invalid post ID.", "error")
+ return redirect("/")
+
+ post = Post.query.filter(Post.id == post_id).first()
+
+ if not post:
+ flash("That post does not exist.", "error")
+ return redirect("/")
+
+ content = request.form.get("content", "").strip()
+
+ if not valid_comment(content):
+ flash("Comment must be between 1 and 1000 characters.", "error")
+ return redirect("/viewpost?post=" + str(post_id))
+
+ postdate = datetime.datetime.now()
+ comment = Comment(content, postdate)
+
+ current_user.comments.append(comment)
+ post.comments.append(comment)
+
+ db.session.commit()
+
+ return redirect("/viewpost?post=" + str(post_id))
\ No newline at end of file
diff --git a/forum/messages.py b/forum/messages.py
new file mode 100644
index 0000000..a588a90
--- /dev/null
+++ b/forum/messages.py
@@ -0,0 +1,102 @@
+import datetime
+from flask import Blueprint, render_template, request, redirect, url_for, flash
+from flask_login import current_user, login_required
+from sqlalchemy import or_, and_
+
+from forum.models import User, Message, db, error, valid_message
+
+messages = Blueprint('messages', __name__)
+
+
+@messages.app_context_processor
+def inject_unread_message_count():
+ """Makes the unread DM count available to every template (e.g. for a sidebar badge)."""
+ if current_user.is_authenticated:
+ count = Message.query.filter(
+ Message.recipient_id == current_user.id,
+ Message.is_read == False
+ ).count()
+ return {"unread_message_count": count}
+ return {"unread_message_count": 0}
+
+
+@messages.route('/messages')
+@login_required
+def inbox():
+ thread_messages = Message.query.filter(
+ or_(Message.sender_id == current_user.id, Message.recipient_id == current_user.id)
+ ).order_by(Message.sentdate.desc()).all()
+
+ conversations = {}
+ for m in thread_messages:
+ other = m.recipient if m.sender_id == current_user.id else m.sender
+ if other.id not in conversations:
+ conversations[other.id] = {
+ "user": other,
+ "last_message": m,
+ "unread_count": Message.query.filter(
+ Message.sender_id == other.id,
+ Message.recipient_id == current_user.id,
+ Message.is_read == False
+ ).count(),
+ }
+
+ conversation_list = sorted(
+ conversations.values(),
+ key=lambda c: c["last_message"].sentdate,
+ reverse=True
+ )
+
+ return render_template("messages_inbox.html", conversations=conversation_list)
+
+
+@messages.route('/messages/')
+@login_required
+def conversation(username):
+ other = User.query.filter(User.username == username).first()
+ if not other:
+ return error("That user does not exist!")
+ if other.id == current_user.id:
+ return error("You can't send messages to yourself!")
+
+ thread = Message.query.filter(
+ or_(
+ and_(Message.sender_id == current_user.id, Message.recipient_id == other.id),
+ and_(Message.sender_id == other.id, Message.recipient_id == current_user.id)
+ )
+ ).order_by(Message.sentdate.asc()).all()
+
+ # Mark any messages the other user sent us as read now that we're viewing the thread.
+ unread = [m for m in thread if m.recipient_id == current_user.id and not m.is_read]
+ if unread:
+ for m in unread:
+ m.is_read = True
+ db.session.commit()
+
+ return render_template("conversation.html", other=other, thread_messages=thread)
+
+
+@messages.route('/action_send_message', methods=['POST'])
+@login_required
+def send_message():
+ username = request.form.get('recipient', '').strip()
+ content = request.form.get('content', '').strip()
+
+ recipient = User.query.filter(User.username == username).first()
+ if not recipient:
+ flash("That user does not exist.", "error")
+ return redirect(url_for('messages.inbox'))
+ if recipient.id == current_user.id:
+ flash("You can't send messages to yourself.", "error")
+ return redirect(url_for('messages.inbox'))
+ if not valid_message(content):
+ flash("Message must be between 1 and 2000 characters.", "error")
+ return redirect(url_for('messages.conversation', username=username))
+
+ message = Message(content, datetime.datetime.now())
+ message.sender = current_user
+ message.recipient = recipient
+ db.session.add(message)
+ db.session.commit()
+
+ return redirect(url_for('messages.conversation', username=username))
diff --git a/forum/models.py b/forum/models.py
index 8add9ae..d20102c 100644
--- a/forum/models.py
+++ b/forum/models.py
@@ -9,38 +9,70 @@
db = SQLAlchemy()
#OBJECT MODELS
+# class User(UserMixin, db.Model):
+# id = db.Column(db.Integer, primary_key=True)
+# username = db.Column(db.Text, unique=True)
+# password_hash = db.Column(db.Text)
+# email = db.Column(db.Text, unique=True)
+# admin = db.Column(db.Boolean, default=False)
+# posts = db.relationship("Post", backref="user")
+# comments = db.relationship("Comment", backref="user")
+
+# def __init__(self, email, username, password):
+# self.email = email
+# self.username = username
+# self.password_hash = generate_password_hash(password)
+# def check_password(self, password):
+# return check_password_hash(self.password_hash, password)
+
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
- username = db.Column(db.Text, unique=True)
- password_hash = db.Column(db.Text)
- email = db.Column(db.Text, unique=True)
+ username = db.Column(db.String(40), unique=True)
+ password_hash = db.Column(db.String(255))
+ email = db.Column(db.String(255), unique=True)
admin = db.Column(db.Boolean, default=False)
+ # Filename only (not a path) of the user's uploaded avatar image, stored
+ # under forum/static/images/avatars/. NULL means "no photo uploaded" --
+ # the UI falls back to the colored-initial avatar in that case.
+ avatar_filename = db.Column(db.String(255), nullable=True)
+
posts = db.relationship("Post", backref="user")
comments = db.relationship("Comment", backref="user")
+ reactions = db.relationship("Reaction", backref="user")
+ comment_reactions = db.relationship("CommentReaction", backref="user") #added 8/21 LJD
def __init__(self, email, username, password):
self.email = email
self.username = username
self.password_hash = generate_password_hash(password)
+
def check_password(self, password):
return check_password_hash(self.password_hash, password)
+
+ def set_password(self, password):
+ self.password_hash = generate_password_hash(password)
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.Text)
content = db.Column(db.Text)
comments = db.relationship("Comment", backref="post")
+ reactions = db.relationship("Reaction", backref="post")
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
subforum_id = db.Column(db.Integer, db.ForeignKey('subforum.id'))
postdate = db.Column(db.DateTime)
+ is_public = db.Column(db.Boolean,default=True) #added MCC
+ media_url = db.Column(db.Text, default=None) #added MCC
#cache stuff
lastcheck = None
savedresponce = None
- def __init__(self, title, content, postdate):
+ def __init__(self, title, content, postdate, is_public, media_url=None):
self.title = title
self.content = content
self.postdate = postdate
+ self.is_public = is_public #added MCC
+ self.media_url = media_url #added MCC
def get_time_string(self):
#this only needs to be calculated every so often, not for every request
#this can be a rudamentary chache
@@ -69,7 +101,8 @@ def get_time_string(self):
class Subforum(db.Model):
id = db.Column(db.Integer, primary_key=True)
- title = db.Column(db.Text, unique=True)
+ # title = db.Column(db.Text, unique=True)
+ title = db.Column(db.String(140), unique=True) #string is standard in MYSQL
description = db.Column(db.Text)
subforums = db.relationship("Subforum")
parent_id = db.Column(db.Integer, db.ForeignKey('subforum.id'))
@@ -87,6 +120,12 @@ class Comment(db.Model):
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
post_id = db.Column(db.Integer, db.ForeignKey("post.id"))
+ reactions = db.relationship(
+ "CommentReaction",
+ backref="comment",
+ cascade="all, delete-orphan"
+ )
+
lastcheck = None
savedresponce = None
def __init__(self, content, postdate):
@@ -115,6 +154,89 @@ def get_time_string(self):
self.savedresponce = "Just a moment ago!"
return self.savedresponce
+class Reaction(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ reaction_type = db.Column(db.String(10), nullable=False)
+ user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
+ post_id = db.Column(db.Integer, db.ForeignKey("post.id"), nullable=False)
+
+ # this prevents one user from having multiple reactions to the same comment/post
+ __table_args__ = (
+ db.UniqueConstraint("user_id", "post_id", name="unique_user_post_reaction"),
+ )
+
+ def __init__(self, reaction_type):
+ self.reaction_type = reaction_type
+
+class CommentReaction(db.Model):
+
+ id = db.Column(db.Integer, primary_key=True)
+
+ reaction_type = db.Column(db.String(10), nullable=False)
+
+ user_id = db.Column(
+ db.Integer,
+ db.ForeignKey("user.id"),
+ nullable=False
+ )
+
+ comment_id = db.Column(
+ db.Integer,
+ db.ForeignKey("comment.id"),
+ nullable=False
+ )
+
+ __table_args__ = (
+ db.UniqueConstraint(
+ "user_id",
+ "comment_id",
+ name="unique_user_comment_reaction"
+ ),
+ )
+
+ def __init__(self, reaction_type):
+ self.reaction_type = reaction_type
+
+class Message(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ content = db.Column(db.Text, nullable=False)
+ sentdate = db.Column(db.DateTime)
+ is_read = db.Column(db.Boolean, default=False)
+
+ sender_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
+ recipient_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
+
+ sender = db.relationship("User", foreign_keys=[sender_id], backref="sent_messages")
+ recipient = db.relationship("User", foreign_keys=[recipient_id], backref="received_messages")
+
+ lastcheck = None
+ savedresponce = None
+ def __init__(self, content, sentdate):
+ self.content = content
+ self.sentdate = sentdate
+ def get_time_string(self):
+ #this only needs to be calculated every so often, not for every request
+ #this can be a rudamentary chache
+ now = datetime.datetime.now()
+ if self.lastcheck is None or (now - self.lastcheck).total_seconds() > 30:
+ self.lastcheck = now
+ else:
+ return self.savedresponce
+
+ diff = now - self.sentdate
+ seconds = diff.total_seconds()
+ if seconds / (60 * 60 * 24 * 30) > 1:
+ self.savedresponce = " " + str(int(seconds / (60 * 60 * 24 * 30))) + " months ago"
+ elif seconds / (60 * 60 * 24) > 1:
+ self.savedresponce = " " + str(int(seconds / (60* 60 * 24))) + " days ago"
+ elif seconds / (60 * 60) > 1:
+ self.savedresponce = " " + str(int(seconds / (60 * 60))) + " hours ago"
+ elif seconds / (60) > 1:
+ self.savedresponce = " " + str(int(seconds / 60)) + " minutes ago"
+ else:
+ self.savedresponce = "Just a moment ago!"
+ return self.savedresponce
+
def error(errormessage):
return "" + errormessage + " "
@@ -139,3 +261,11 @@ def valid_title(title):
def valid_content(content):
return len(content) > 10 and len(content) < 5000
+# Comment checks
+def valid_comment(content):
+ return len(content.strip()) > 0 and len(content) <= 1000
+
+# Direct message checks
+def valid_message(content):
+ return len(content.strip()) > 0 and len(content) <= 2000
+
diff --git a/forum/posts.py b/forum/posts.py
new file mode 100644
index 0000000..acf2a95
--- /dev/null
+++ b/forum/posts.py
@@ -0,0 +1,110 @@
+
+from flask import render_template, request, redirect, url_for, Blueprint, flash
+from flask_login import current_user, login_user, logout_user
+from flask_login.utils import login_required
+import datetime
+from forum.models import User, Post, Comment, Subforum, Reaction, valid_content, valid_title, db, generateLinkPath, error
+from forum.user import username_taken, email_taken, valid_username
+
+posts = Blueprint('posts', __name__)
+
+
+@posts.route('/addpost')
+@login_required
+def addpost():
+ subforum_id = int(request.args.get("sub"))
+ subforum = Subforum.query.filter(Subforum.id == subforum_id).first()
+ if not subforum:
+ return error("That subforum does not exist!")
+
+ return render_template("createpost.html", subforum=subforum)
+
+
+@posts.route('/viewpost')
+def viewpost():
+ postid = int(request.args.get("post"))
+ post = Post.query.filter(Post.id == postid).first()
+
+ if not post:
+ return error("That post does not exist!")
+
+ if not post.is_public and not current_user.is_authenticated:
+ flash("This post is private. Log in to view it.", "error")
+ return redirect(f"/subforum?sub={post.subforum_id}")
+
+ subforumpath = post.subforum.path
+ if not subforumpath:
+ subforumpath = generateLinkPath(post.subforum.id)
+
+ comments = Comment.query.filter(
+ Comment.post_id == postid
+ ).order_by(Comment.id.desc())
+
+ like_count = Reaction.query.filter_by(
+ post_id=postid,
+ reaction_type="like"
+ ).count()
+
+ dislike_count = Reaction.query.filter_by(
+ post_id=postid,
+ reaction_type="dislike"
+ ).count()
+
+ heart_count = Reaction.query.filter_by(
+ post_id=postid,
+ reaction_type="heart"
+ ).count()
+
+ user_reaction = None
+
+ if current_user.is_authenticated:
+ reaction = Reaction.query.filter_by(
+ post_id=postid,
+ user_id=current_user.id
+ ).first()
+
+ if reaction:
+ user_reaction = reaction.reaction_type
+
+ return render_template(
+ "viewpost.html",
+ post=post,
+ path=subforumpath,
+ comments=comments,
+ like_count=like_count,
+ dislike_count=dislike_count,
+ heart_count=heart_count,
+ user_reaction=user_reaction
+ )
+
+
+@posts.route('/action_post', methods=['POST'])
+@login_required
+def action_post():
+ subforum_id = int(request.args.get("sub"))
+ subforum = Subforum.query.filter(Subforum.id == subforum_id).first()
+ if not subforum:
+ return redirect(url_for("subforums"))
+
+ user = current_user
+ title = request.form['title']
+ content = request.form['content']
+ is_public = request.form.get('is_public') # added, MCC
+ is_public = (is_public == 'on') # added, MCC
+ media_url = request.form.get('media_url') # added, MCC
+ # check for valid posting
+ errors = []
+ retry = False
+ if not valid_title(title):
+ errors.append("Title must be between 4 and 140 characters long!")
+ retry = True
+ if not valid_content(content):
+ errors.append("Post must be between 10 and 5000 characters long!")
+ retry = True
+ if retry:
+ return render_template("createpost.html", subforum=subforum, errors=errors)
+ post = Post(title, content, datetime.datetime.now(), is_public, media_url) # updated, MCC
+ subforum.posts.append(post)
+ user.posts.append(post)
+ db.session.commit()
+ return redirect("/viewpost?post=" + str(post.id))
\ No newline at end of file
diff --git a/forum/profile.py b/forum/profile.py
new file mode 100644
index 0000000..7860853
--- /dev/null
+++ b/forum/profile.py
@@ -0,0 +1,104 @@
+import os
+import uuid
+
+from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app
+from flask_login import current_user, login_required
+from werkzeug.utils import secure_filename
+
+from forum.models import db
+from forum.user import (
+ valid_username, valid_email, username_taken, email_taken,
+ valid_avatar_filename, avatar_extension, MAX_AVATAR_BYTES,
+)
+
+profile = Blueprint('profile', __name__)
+
+AVATAR_SUBDIR = os.path.join('images', 'avatars')
+
+
+def _avatar_dir():
+ path = os.path.join(current_app.root_path, 'static', AVATAR_SUBDIR)
+ os.makedirs(path, exist_ok=True)
+ return path
+
+
+@profile.route('/profile')
+@login_required
+def profile_page():
+ return render_template("profile.html")
+
+
+@profile.route('/action_update_username', methods=['POST'])
+@login_required
+def update_username():
+ username = request.form.get('username', '').strip()
+
+ if not valid_username(username):
+ flash("Username must be 4-40 characters (letters, numbers, and !@#%& only).", "error")
+ elif username != current_user.username and username_taken(username):
+ flash("That username is already taken.", "error")
+ else:
+ current_user.username = username
+ db.session.commit()
+ flash("Your username has been updated.", "success")
+
+ return redirect(url_for('profile.profile_page'))
+
+
+@profile.route('/action_update_email', methods=['POST'])
+@login_required
+def update_email():
+ email = request.form.get('email', '').strip()
+
+ if not valid_email(email):
+ flash("Please enter a valid email address.", "error")
+ elif email != current_user.email and email_taken(email):
+ flash("That email is already in use by another account.", "error")
+ else:
+ current_user.email = email
+ db.session.commit()
+ flash("Your email has been updated.", "success")
+
+ return redirect(url_for('profile.profile_page'))
+
+
+@profile.route('/action_update_avatar', methods=['POST'])
+@login_required
+def update_avatar():
+ upload = request.files.get('avatar')
+
+ if upload is None or upload.filename == '':
+ flash("Please choose an image to upload.", "error")
+ return redirect(url_for('profile.profile_page'))
+
+ filename = secure_filename(upload.filename)
+
+ if not valid_avatar_filename(filename):
+ flash("Profile pictures must be a PNG, JPG, GIF, or WEBP file.", "error")
+ return redirect(url_for('profile.profile_page'))
+
+ # Peek at the size without trusting Content-Length: seek to the end of
+ # the stream, read the position, then rewind before saving.
+ upload.stream.seek(0, os.SEEK_END)
+ size = upload.stream.tell()
+ upload.stream.seek(0)
+
+ if size > MAX_AVATAR_BYTES:
+ flash("Profile pictures must be 2MB or smaller.", "error")
+ return redirect(url_for('profile.profile_page'))
+
+ new_filename = f"user_{current_user.id}_{uuid.uuid4().hex}.{avatar_extension(filename)}"
+ avatar_dir = _avatar_dir()
+ upload.save(os.path.join(avatar_dir, new_filename))
+
+ old_filename = current_user.avatar_filename
+ current_user.avatar_filename = new_filename
+ db.session.commit()
+
+ if old_filename:
+ old_path = os.path.join(avatar_dir, old_filename)
+ if os.path.exists(old_path):
+ os.remove(old_path)
+
+ flash("Your profile picture has been updated.", "success")
+ return redirect(url_for('profile.profile_page'))
diff --git a/forum/reactions.py b/forum/reactions.py
new file mode 100644
index 0000000..ef2dcbe
--- /dev/null
+++ b/forum/reactions.py
@@ -0,0 +1,39 @@
+from flask import Blueprint, request, redirect
+from flask_login import login_required, current_user
+from forum.models import db, Post, Reaction, error
+
+reactions = Blueprint("reactions", __name__)
+
+@reactions.route("/action_reaction", methods=["POST"])
+@login_required
+def react():
+ post_id = request.form.get("post_id")
+ reaction_type = request.form.get("reaction_type")
+
+ if reaction_type not in ["like", "dislike", "heart"]:
+ return error("Invalid reaction.")
+
+ post = Post.query.filter(Post.id == post_id).first()
+
+ if not post:
+ return error("That post does not exist.")
+
+ existing_reaction = Reaction.query.filter_by(
+ user_id=current_user.id,
+ post_id=post.id
+ ).first()
+
+ if existing_reaction:
+ if existing_reaction.reaction_type == reaction_type:
+ db.session.delete(existing_reaction)
+ else:
+ existing_reaction.reaction_type = reaction_type
+ else:
+ reaction = Reaction(reaction_type)
+ reaction.user = current_user
+ reaction.post = post
+ db.session.add(reaction)
+
+ db.session.commit()
+
+ return redirect("/viewpost?post=" + str(post.id))
\ No newline at end of file
diff --git a/forum/routes.py b/forum/routes.py
index 75993e5..0791e4f 100644
--- a/forum/routes.py
+++ b/forum/routes.py
@@ -1,9 +1,9 @@
-from flask import render_template, request, redirect, url_for
+from flask import Blueprint, render_template, request, redirect, url_for
from flask_login import current_user, login_user, logout_user
from flask_login.utils import login_required
import datetime
-from flask import Blueprint, render_template, request, redirect, url_for
-from forum.models import User, Post, Comment, Subforum, valid_content, valid_title, db, generateLinkPath, error
+
+from forum.models import User, Post, Comment, Reaction, Subforum, valid_content, valid_title, db, generateLinkPath, error
from forum.user import username_taken, email_taken, valid_username
##
@@ -80,68 +80,128 @@ def subforum():
def loginform():
return render_template("login.html")
-
-@login_required
-@rt.route('/addpost')
-def addpost():
- subforum_id = int(request.args.get("sub"))
- subforum = Subforum.query.filter(Subforum.id == subforum_id).first()
- if not subforum:
- return error("That subforum does not exist!")
-
- return render_template("createpost.html", subforum=subforum)
-
-@rt.route('/viewpost')
-def viewpost():
- postid = int(request.args.get("post"))
- post = Post.query.filter(Post.id == postid).first()
- if not post:
- return error("That post does not exist!")
- if not post.subforum.path:
- subforumpath = generateLinkPath(post.subforum.id)
- comments = Comment.query.filter(Comment.post_id == postid).order_by(Comment.id.desc()) # no need for scalability now
- return render_template("viewpost.html", post=post, path=subforumpath, comments=comments)
-
-@login_required
-@rt.route('/action_comment', methods=['POST', 'GET'])
-def comment():
- post_id = int(request.args.get("post"))
- post = Post.query.filter(Post.id == post_id).first()
- if not post:
- return error("That post does not exist!")
- content = request.form['content']
- postdate = datetime.datetime.now()
- comment = Comment(content, postdate)
- current_user.comments.append(comment)
- post.comments.append(comment)
- db.session.commit()
- return redirect("/viewpost?post=" + str(post_id))
-
-@login_required
-@rt.route('/action_post', methods=['POST'])
-def action_post():
- subforum_id = int(request.args.get("sub"))
- subforum = Subforum.query.filter(Subforum.id == subforum_id).first()
- if not subforum:
- return redirect(url_for("subforums"))
-
- user = current_user
- title = request.form['title']
- content = request.form['content']
- #check for valid posting
- errors = []
- retry = False
- if not valid_title(title):
- errors.append("Title must be between 4 and 140 characters long!")
- retry = True
- if not valid_content(content):
- errors.append("Post must be between 10 and 5000 characters long!")
- retry = True
- if retry:
- return render_template("createpost.html",subforum=subforum, errors=errors)
- post = Post(title, content, datetime.datetime.now())
- subforum.posts.append(post)
- user.posts.append(post)
- db.session.commit()
- return redirect("/viewpost?post=" + str(post.id))
-
+@rt.route('/about')
+def about():
+ return render_template("about.html")
+
+@rt.route('/contact')
+def contact():
+ return render_template("contact.html")
+
+@rt.route('/house-rules')
+def house_rules():
+ return render_template("house_rules.html")
+
+
+# THE CODE BELOW WAS MOVED TO VARIOUS .py
+# Moved to forum/posts.py.
+# @login_required
+# @rt.route('/addpost')
+# def addpost():
+# subforum_id = int(request.args.get("sub"))
+# subforum = Subforum.query.filter(Subforum.id == subforum_id).first()
+# if not subforum:
+# return error("That subforum does not exist!")
+#
+# return render_template("createpost.html", subforum=subforum)
+#
+# @rt.route('/viewpost')
+# def viewpost():
+# postid = int(request.args.get("post"))
+# post = Post.query.filter(Post.id == postid).first()
+# if not post:
+# return error("That post does not exist!")
+# if not post.subforum.path:
+# subforumpath = generateLinkPath(post.subforum.id)
+# comments = Comment.query.filter(Comment.post_id == postid).order_by(Comment.id.desc()) # no need for scalability now
+# return render_template("viewpost.html", post=post, path=subforumpath, comments=comments)
+
+ # if not post:
+ # return error("That post does not exist!")
+
+ # if not post.subforum.path:
+ # subforumpath = generateLinkPath(post.subforum.id)
+
+ # comments = Comment.query.filter(
+ # Comment.post_id == postid
+ # ).order_by(Comment.id.desc())
+
+ # like_count = Reaction.query.filter_by(
+ # post_id=postid,
+ # reaction_type="like"
+ # ).count()
+
+ # dislike_count = Reaction.query.filter_by(
+ # post_id=postid,
+ # reaction_type="dislike"
+ # ).count()
+
+ # heart_count = Reaction.query.filter_by(
+ # post_id=postid,
+ # reaction_type="heart"
+ # ).count()
+
+ # user_reaction = None
+
+ # if current_user.is_authenticated:
+ # reaction = Reaction.query.filter_by(
+ # post_id=postid,
+ # user_id=current_user.id
+ # ).first()
+
+ # if reaction:
+ # user_reaction = reaction.reaction_type
+
+ # return render_template(
+ # "viewpost.html",
+ # post=post,
+ # path=subforumpath,
+ # comments=comments,
+ # like_count=like_count,
+ # dislike_count=dislike_count,
+ # heart_count=heart_count,
+ # user_reaction=user_reaction
+ # )
+# @login_required
+# @rt.route('/action_comment', methods=['POST', 'GET'])
+# def comment():
+# post_id = int(request.args.get("post"))
+# post = Post.query.filter(Post.id == post_id).first()
+# if not post:
+# return error("That post does not exist!")
+# content = request.form['content']
+# postdate = datetime.datetime.now()
+# comment = Comment(content, postdate)
+# current_user.comments.append(comment)
+# post.comments.append(comment)
+# db.session.commit()
+# return redirect("/viewpost?post=" + str(post_id))
+
+# Moved to forum/posts.py.
+# @login_required
+# @rt.route('/action_post', methods=['POST'])
+# def action_post():
+# subforum_id = int(request.args.get("sub"))
+# subforum = Subforum.query.filter(Subforum.id == subforum_id).first()
+# if not subforum:
+# return redirect(url_for("subforums"))
+#
+# user = current_user
+# title = request.form['title']
+# content = request.form['content']
+# #check for valid posting
+# errors = []
+# retry = False
+# if not valid_title(title):
+# errors.append("Title must be between 4 and 140 characters long!")
+# retry = True
+# if not valid_content(content):
+# errors.append("Post must be between 10 and 5000 characters long!")
+# retry = True
+# if retry:
+# return render_template("createpost.html",subforum=subforum, errors=errors)
+# post = Post(title, content, datetime.datetime.now())
+# subforum.posts.append(post)
+# user.posts.append(post)
+# db.session.commit()
+# return redirect("/viewpost?post=" + str(post.id))
diff --git a/forum/settings.py b/forum/settings.py
new file mode 100644
index 0000000..b8e6e0b
--- /dev/null
+++ b/forum/settings.py
@@ -0,0 +1,40 @@
+from flask import Blueprint, render_template, request, redirect, url_for, flash
+from flask_login import current_user, login_required
+
+from forum.models import db
+from forum.user import valid_password
+
+settings = Blueprint('settings', __name__)
+
+
+@settings.route('/settings')
+@login_required
+def settings_page():
+ return render_template("settings.html")
+
+
+@settings.route('/action_change_password', methods=['POST'])
+@login_required
+def change_password():
+ current_password = request.form.get('current_password', '')
+ new_password = request.form.get('new_password', '')
+ confirm_password = request.form.get('confirm_password', '')
+
+ errors = []
+
+ if not current_user.check_password(current_password):
+ errors.append("Your current password is incorrect.")
+ if not valid_password(new_password):
+ errors.append("New password must be 6-40 characters (letters, numbers, and !@#%& only).")
+ elif new_password != confirm_password:
+ errors.append("New password and confirmation do not match.")
+
+ if errors:
+ for e in errors:
+ flash(e, "error")
+ return redirect(url_for('settings.settings_page'))
+
+ current_user.set_password(new_password)
+ db.session.commit()
+ flash("Your password has been changed.", "success")
+ return redirect(url_for('settings.settings_page'))
diff --git a/forum/static/images/DishCourseLogo.png b/forum/static/images/DishCourseLogo.png
new file mode 100644
index 0000000..a9dd546
Binary files /dev/null and b/forum/static/images/DishCourseLogo.png differ
diff --git a/forum/static/images/avatars/.gitkeep b/forum/static/images/avatars/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/forum/static/images/avatars/user_5_ab78342e861744ae84b946df6aeca8c8.png b/forum/static/images/avatars/user_5_ab78342e861744ae84b946df6aeca8c8.png
new file mode 100644
index 0000000..5a1aaa2
Binary files /dev/null and b/forum/static/images/avatars/user_5_ab78342e861744ae84b946df6aeca8c8.png differ
diff --git a/forum/static/images/avatars/user_5_bd6274a86c3c4a9d8f43bd4db0293042.png b/forum/static/images/avatars/user_5_bd6274a86c3c4a9d8f43bd4db0293042.png
new file mode 100644
index 0000000..16e7fa7
Binary files /dev/null and b/forum/static/images/avatars/user_5_bd6274a86c3c4a9d8f43bd4db0293042.png differ
diff --git a/forum/static/style.css b/forum/static/style.css
index 2dd2e2e..0f08108 100644
--- a/forum/static/style.css
+++ b/forum/static/style.css
@@ -1,191 +1,2113 @@
-.page{
- font-family: Verdana;
- font-style: normal;
- font-size: 14pt;
+/* =========================================================
+ DISHCOURSE
+ ========================================================= */
- margin-left: auto;
- margin-right: auto;
- margin-top: 1%;
- width: 80%;
-
- padding-left: 3%;
- padding-right: 3%;
+:root {
+ --dc-plum-dark: #26062f;
+ --dc-plum: #3d0a48;
+ --dc-plum-mid: #57125d;
+ --dc-orange: #ff5722;
+ --dc-orange-soft: #ff7a3d;
+ --dc-peach: #ffe6d2;
+ --dc-pink: #f5dceb;
+ --dc-ivory: #fffaf4;
+ --dc-paper: #ffffff;
+ --dc-text: #2d1237;
+ --dc-muted: #786c78;
+ --dc-border: #eee2d7;
+ --dc-shadow: 0 8px 24px rgba(56, 14, 66, 0.08);
+ --dc-radius-lg: 20px;
+ --dc-radius-md: 14px;
+ --dc-radius-sm: 10px;
+}
+
+/* =========================================================
+ GLOBAL
+ ========================================================= */
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ margin: 0;
+ height: 100%;
+ min-height: 100%;
+ overflow: hidden;
+}
+
+html {
+ font-size: 14px;
+}
+
+body {
+ background: var(--dc-ivory);
+ color: var(--dc-text);
+ font-family: "Avenir Next", Avenir, "Helvetica Neue", Arial, sans-serif;
+ font-size: 1rem;
+ line-height: 1.55;
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: none;
+}
+
+input,
+textarea,
+select,
+button {
+ font-family: inherit;
+}
+
+.dc-app-shell {
+ height: 100vh;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+/* =========================================================
+ HEADER
+ ========================================================= */
+
+.dc-top-header {
+ flex-shrink: 0;
+ position: relative;
+ z-index: 1000;
+ background: var(--dc-paper);
+ border-bottom: 1px solid var(--dc-border);
+}
+
+.dc-header-inner {
+ height: 105px;
+ min-height: 105px;
+ display: grid;
+ grid-template-columns: 320px minmax(300px, 520px) minmax(330px, 1fr);
+ align-items: center;
+ gap: 2rem;
+ padding: 0.75rem 2rem;
+}
+
+.dc-logo {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ width: 340px;
+ height: 82px;
+ overflow: hidden;
+}
+
+.dc-logo-image {
+ display: block;
+ width: 320px;
+ height: 78px;
+ object-fit: contain;
+ object-position: left center;
+}
+
+.dc-search {
+ height: 54px;
+ display: flex;
+ align-items: center;
+ gap: 0.8rem;
+ padding: 0 1.25rem;
+ background: var(--dc-paper);
+ border: 1px solid var(--dc-border);
+ border-radius: 28px;
+}
+
+.dc-search-icon {
+ font-size: 1.35rem;
+ color: var(--dc-plum);
+}
+
+.dc-search input {
+ width: 100%;
+ border: none;
+ outline: none;
+ background: transparent;
+ color: var(--dc-text);
+ font-size: 1rem;
+}
+
+.dc-search input::placeholder {
+ color: #84557e;
+}
+
+.dc-header-actions {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 1.8rem;
+}
+
+.dc-start-button {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.7rem;
+ background: linear-gradient(135deg, var(--dc-plum), var(--dc-plum-dark));
+ color: #fff;
+ font-weight: 700;
+ padding: 0.95rem 1.5rem;
+ border-radius: 12px;
+ white-space: nowrap;
+ box-shadow: 0 6px 14px rgba(38, 6, 47, 0.15);
+}
+
+.dc-start-button:hover {
+ color: #fff;
+ transform: translateY(-1px);
+}
+
+.dc-plus {
+ font-size: 1.4rem;
+ font-weight: 300;
+}
+
+.dc-header-user {
+ display: flex;
+ align-items: center;
+ gap: 0.8rem;
+ white-space: nowrap;
+}
+
+.dc-avatar {
+ width: 50px;
+ height: 50px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ background: linear-gradient(135deg, var(--dc-orange), var(--dc-plum));
+ color: #fff;
+ font-size: 1.05rem;
+ font-weight: 900;
+ overflow: hidden;
+}
+
+.dc-avatar-image {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.dc-header-user-info {
+ font-weight: 700;
+}
+
+/* =========================================================
+ APPLICATION LAYOUT
+ ========================================================= */
+
+.dc-body-layout {
+ flex: 1;
+ min-height: 0;
+ display: grid;
+ grid-template-columns: 260px minmax(0, 1fr) 330px;
+ align-items: stretch;
+ overflow: hidden;
+}
+
+.dc-left-sidebar {
+ height: 100%;
+ min-height: 0;
+ padding: 1.5rem;
+ overflow-y: auto;
+ background: linear-gradient(165deg, #290531, #3c0a45 58%, #26042c);
+ color: #fff;
+}
+
+.dc-main-content {
+ height: 100%;
+ min-width: 0;
+ min-height: 0;
+ padding: 2rem;
+ overflow-y: auto;
+ background: var(--dc-ivory);
+}
+
+.dc-main-content > .container {
+ max-width: none;
+}
+
+.dc-right-sidebar {
+ height: 100%;
+ min-height: 0;
+ padding: 1.5rem 1.5rem 2rem 0;
+ overflow-y: auto;
+ background: var(--dc-ivory);
+}
+
+/* =========================================================
+ LEFT SIDEBAR
+ ========================================================= */
+
+.dc-sidebar-nav {
+ display: flex;
+ flex-direction: column;
+ gap: 0.45rem;
+}
+
+.dc-sidebar-item {
+ min-height: 54px;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 0.8rem 1rem;
+ border-radius: 10px;
+ color: #fff;
+ font-size: 0.92rem;
+ font-weight: 700;
+ transition: background 0.2s ease, transform 0.2s ease;
+}
+
+.dc-sidebar-item:hover {
+ background: rgba(255, 255, 255, 0.08);
+ color: #fff;
+}
+
+.dc-sidebar-item.active {
+ background: linear-gradient(135deg, var(--dc-orange), #ff7041);
+ box-shadow: 0 8px 20px rgba(255, 87, 34, 0.25);
+}
+
+.dc-sidebar-icon {
+ width: 24px;
+ text-align: center;
+ font-size: 1.1rem;
+}
+
+.dc-sidebar-divider {
+ height: 1px;
+ margin: 1.2rem 0;
+ background: rgba(255, 255, 255, 0.22);
+}
+
+.dc-sidebar-community {
+ margin-top: 4rem;
+ padding: 1rem;
+ text-align: center;
+}
+
+.dc-food-art {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.6rem;
+ color: var(--dc-orange);
+ font-size: 2rem;
+ margin-bottom: 1rem;
+}
+
+.dc-sidebar-community p {
+ color: rgba(255, 255, 255, 0.9);
+ font-size: 0.9rem;
+ line-height: 1.6;
+}
+
+.dc-sidebar-community strong {
+ display: block;
+ margin-top: 1rem;
+ color: var(--dc-orange);
+ font-size: 0.9rem;
+}
+
+/* =========================================================
+ RIGHT SIDEBAR
+ ========================================================= */
+
+.dc-side-card {
+ background: rgba(255, 255, 255, 0.75);
+ border: 1px solid var(--dc-border);
+ border-radius: var(--dc-radius-lg);
+ padding: 1.4rem;
+ margin-bottom: 1.3rem;
+ box-shadow: 0 4px 16px rgba(67, 20, 70, 0.035);
+}
+
+.dc-side-card h2 {
+ margin: 0 0 1rem;
+ font-family: "Arial Rounded MT Bold", "Avenir Next", Avenir, sans-serif;
+ font-size: 1.1rem;
+ font-weight: 900;
+ letter-spacing: -0.025em;
+ color: var(--dc-plum-dark);
+}
+
+.dc-side-card-header {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.dc-most-discussed {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.dc-discussed-item {
+ display: grid;
+ grid-template-columns: 44px 1fr auto;
+ align-items: center;
+ gap: 0.7rem;
+}
+
+.dc-mini-food {
+ width: 44px;
+ height: 44px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ background: var(--dc-peach);
+ font-size: 1rem;
+}
+
+.dc-discussed-title {
+ font-size: 0.88rem;
+ line-height: 1.35;
+ font-weight: 600;
+}
+
+.dc-discussed-count {
+ display: flex;
+ align-items: center;
+ white-space: nowrap;
+ font-size: 0.88rem;
+}
+
+.dc-view-all {
+ display: inline-block;
+ margin-top: 1.5rem;
+ color: var(--dc-orange);
+ font-size: 1rem;
+ font-weight: 800;
+}
+
+.dc-category-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+}
+
+.dc-category {
+ display: flex;
+ align-items: center;
+ gap: 0.8rem;
+ padding: 0.65rem 0.75rem;
+ border-radius: 9px;
+ color: var(--dc-text);
+ font-size: 0.9rem;
+ font-weight: 600;
+}
+
+.dc-category:hover {
+ background: #fff0e5;
+}
+
+.dc-category.active {
+ background: var(--dc-peach);
+}
+
+/* =========================================================
+ SHARED COMPONENTS
+ ========================================================= */
+
+.section-eyebrow {
+ color: var(--dc-orange);
+ font-size: 0.75rem;
+ font-weight: 900;
+ letter-spacing: 0.15em;
+ text-transform: uppercase;
+}
+
+.dishcourse-primary-btn,
+.dishcourse-login-btn {
+ background: var(--dc-orange);
+ border: none;
+ color: #fff;
+ font-weight: 800;
+ padding: 0.7rem 1rem;
+ border-radius: 9px;
+}
+
+.dishcourse-primary-btn:hover,
+.dishcourse-login-btn:hover {
+ background: #e84412;
+ color: #fff;
+}
+
+/* =========================================================
+ LANDING PAGE
+ ========================================================= */
+
+.dishcourse-hero {
+ background: linear-gradient(135deg, #3a0b44, #5b174e);
+ border-radius: var(--dc-radius-lg);
+ padding: 3rem 2rem;
+ margin-bottom: 2rem;
+ color: #fff;
+ box-shadow: var(--dc-shadow);
+}
+
+.hero-content {
+ max-width: 760px;
+ margin: 0 auto;
+ text-align: center;
+}
+
+.hero-eyebrow {
+ color: var(--dc-orange);
+ font-size: 0.72rem;
+ text-transform: uppercase;
+ letter-spacing: 0.15em;
+ font-weight: 900;
+}
+
+.hero-title {
+ font-family: "Arial Rounded MT Bold", "Avenir Next", Avenir, sans-serif;
+ font-size: 2.4rem;
+ font-weight: 900;
+ line-height: 1.1;
+ letter-spacing: -0.035em;
+}
+
+.hero-text {
+ color: rgba(255, 255, 255, 0.88);
+ font-size: 1.02rem;
+ line-height: 1.7;
+}
+
+.forum-section-heading {
+ display: flex;
+ justify-content: space-between;
+ align-items: end;
+ gap: 2rem;
+ margin-bottom: 1.2rem;
+}
+
+.forum-section-heading h2 {
+ margin: 0;
+ font-family: "Arial Rounded MT Bold", "Avenir Next", Avenir, sans-serif;
+ font-size: 1.75rem;
+ font-weight: 900;
+ letter-spacing: -0.025em;
+ color: var(--dc-plum-dark);
+}
+
+.forum-section-intro {
+ color: var(--dc-muted);
+ font-size: 0.98rem;
+}
+
+.forum-card-link {
+ display: block;
+ height: 100%;
+}
+
+.forum-card {
+ min-height: 185px;
+ height: 100%;
+ background: var(--dc-paper);
+ border: 1px solid var(--dc-border);
+ border-radius: var(--dc-radius-md);
+ padding: 1.45rem;
+ box-shadow: 0 5px 14px rgba(67, 20, 70, 0.04);
+ transition: transform 0.18s ease, box-shadow 0.18s ease;
+}
+
+.forum-card:hover {
+ transform: translateY(-4px);
+ box-shadow: var(--dc-shadow);
+}
+
+.forum-card-icon {
+ width: 50px;
+ height: 50px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ background: var(--dc-peach);
+ margin-bottom: 1rem;
+}
+
+.forum-card-title {
+ color: var(--dc-plum-dark);
+ font-size: 1.05rem;
+ font-weight: 900;
+}
+
+.forum-card-description {
+ color: var(--dc-muted);
+ font-size: 0.98rem;
+ line-height: 1.55;
+}
+
+.forum-card-action {
+ color: var(--dc-orange);
+ font-size: 0.98rem;
+ font-weight: 800;
+}
+
+/* =========================================================
+ SUBFORUM / DISCUSSION PAGE
+ ========================================================= */
+
+.discussion-path {
+ color: var(--dc-muted);
+ font-size: 0.85rem;
+ margin-bottom: 1rem;
+}
+
+.discussion-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 2rem;
+ background: var(--dc-paper);
+ border: 1px solid var(--dc-border);
+ border-radius: var(--dc-radius-lg);
+ padding: 1.8rem;
+ margin-bottom: 1.6rem;
+}
+
+.discussion-title {
+ color: var(--dc-plum-dark);
+ font-family: "Arial Rounded MT Bold", "Avenir Next", Avenir, sans-serif;
+ font-size: 2rem;
+ font-weight: 900;
+ letter-spacing: -0.025em;
+}
+
+.discussion-description {
+ color: var(--dc-muted);
+ font-size: 1rem;
+}
+
+.related-forum-card {
+ height: 100%;
+ background: var(--dc-peach);
+ border-radius: var(--dc-radius-md);
+ padding: 1rem;
+}
+
+.related-forum-card h3 {
+ color: var(--dc-plum-dark);
+ font-size: 1rem;
+ font-weight: 900;
+}
+
+.related-forum-card p {
+ color: var(--dc-muted);
+ font-size: 0.9rem;
+}
+
+.discussion-list-heading h2 {
+ color: var(--dc-plum-dark);
+ font-family: "Arial Rounded MT Bold", "Avenir Next", Avenir, sans-serif;
+ font-size: 1.8rem;
+ font-weight: 900;
+ letter-spacing: -0.025em;
+}
+
+.post-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.8rem;
+}
+
+.discussion-post-card {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background: var(--dc-paper);
+ border: 1px solid var(--dc-border);
+ border-radius: var(--dc-radius-md);
+ padding: 1rem 1.2rem;
+}
+
+.discussion-post-title {
+ color: var(--dc-plum-dark);
+ font-size: 1.05rem;
+ font-weight: 900;
+}
+
+.discussion-post-meta {
+ color: var(--dc-muted);
+ font-size: 0.85rem;
+}
+
+/* =========================================================
+ DISCUSSION FEED CARDS
+ ========================================================= */
+
+.feed-post-card {
+ display: grid;
+ grid-template-columns: 56px minmax(0, 1fr) 120px 95px 36px;
+ align-items: center;
+ gap: 1rem;
+ background: var(--dc-paper);
+ border: 1px solid var(--dc-border);
+ border-radius: 16px;
+ padding: 1.2rem;
+ box-shadow: 0 4px 14px rgba(67, 20, 70, 0.04);
+ transition: transform 0.18s ease, box-shadow 0.18s ease;
+}
+
+.feed-post-card:hover {
+ transform: translateY(-2px);
+ box-shadow: var(--dc-shadow);
+}
+
+.feed-post-avatar {
+ width: 52px;
+ height: 52px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ background: linear-gradient(135deg, var(--dc-orange), var(--dc-plum));
+ color: #fff;
+ font-size: 1rem;
+ font-weight: 900;
+}
+
+.feed-post-title {
+ display: inline-block;
+ color: var(--dc-plum-dark);
+ font-size: 1.1rem;
+ font-weight: 900;
+ margin-bottom: 0.25rem;
+}
+
+.feed-post-title:hover {
+ color: var(--dc-orange);
+}
+
+.feed-post-meta {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ color: var(--dc-muted);
+ font-size: 0.85rem;
+ margin-bottom: 0.7rem;
+}
+
+.feed-meta-dot {
+ color: var(--dc-orange);
+}
+
+.feed-post-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.45rem;
+}
+
+.feed-tag {
+ background: var(--dc-pink);
+ color: var(--dc-plum);
+ padding: 0.3rem 0.75rem;
+ border-radius: 999px;
+ font-size: 0.78rem;
+ font-weight: 700;
+}
+
+.feed-tag-orange {
+ background: var(--dc-peach);
+ color: #c84011;
+}
+
+.feed-post-media-placeholder {
+ width: 110px;
+ height: 72px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ justify-self: center;
+ border-radius: 10px;
+ background: linear-gradient(135deg, #ffe8d5, #f7d6bf);
+ font-size: 1.6rem;
+}
+
+.feed-post-stats {
+ display: flex;
+ flex-direction: column;
+ gap: 0.9rem;
+ color: var(--dc-plum);
+}
+
+.feed-post-stats > div {
+ display: grid;
+ grid-template-columns: 24px auto;
+ column-gap: 0.4rem;
+ align-items: center;
+}
+
+.feed-stat-icon {
+ grid-row: 1 / span 2;
+ font-size: 1.2rem;
+ color: var(--dc-plum);
+}
+
+.feed-post-stats strong {
+ font-size: 0.95rem;
+ font-weight: 900;
+ color: var(--dc-plum-dark);
+}
+
+.feed-post-stats small {
+ font-size: 0.78rem;
+ font-weight: 600;
+ color: var(--dc-muted);
+}
+
+.feed-post-open {
+ color: var(--dc-plum);
+ font-size: 1.6rem;
+ font-weight: 700;
+ text-align: center;
+}
+
+.feed-post-open:hover {
+ color: var(--dc-orange);
+}
+
+/* =========================================================
+ EMPTY STATES
+ ========================================================= */
+
+.empty-discussion-state,
+.empty-comments-state {
+ background: var(--dc-paper);
+ border: 1px dashed var(--dc-border);
+ border-radius: var(--dc-radius-lg);
+ padding: 1.4rem 1rem;
+ text-align: center;
+}
+
+.empty-discussion-state h3,
+.empty-comments-state h3 {
+ color: var(--dc-plum-dark);
+ font-weight: 900;
+}
+
+.empty-discussion-state p,
+.empty-comments-state p {
+ color: var(--dc-muted);
+}
+
+.empty-comments-state {
+ margin-top: 0.5rem;
+}
+
+.empty-comments-state h3 {
+ margin-bottom: 0.4rem;
+}
+
+.empty-comments-state p {
+ margin-bottom: 0;
+}
+
+/* =========================================================
+ VIEW POST
+ ========================================================= */
+
+.post-detail-card {
+ background: var(--dc-paper);
+ border: 1px solid var(--dc-border);
+ border-radius: var(--dc-radius-lg);
+ overflow: hidden;
+}
+
+.post-detail-header {
+ background: linear-gradient(135deg, #fff1e5, #ffe4d7);
+ padding: 1.6rem 1.8rem;
+}
+
+.post-detail-author-row {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.post-detail-avatar,
+.comment-avatar {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ border-radius: 50%;
+ background: linear-gradient(135deg, var(--dc-orange), var(--dc-plum));
+ color: #fff;
+ font-weight: 900;
+}
+
+.post-detail-avatar {
+ width: 58px;
+ height: 58px;
+ font-size: 1.1rem;
+}
+
+.post-detail-title {
+ color: var(--dc-plum-dark);
+ font-family: "Arial Rounded MT Bold", "Avenir Next", Avenir, sans-serif;
+ font-size: 1.8rem;
+ font-weight: 900;
+ letter-spacing: -0.025em;
+}
+
+.post-detail-meta {
+ color: var(--dc-muted);
+ font-size: 0.9rem;
+}
+
+.post-detail-content {
+ padding: 1.8rem;
+ font-size: 1.05rem;
+ line-height: 1.7;
+}
+
+.post-detail-tags {
+ display: flex;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+ padding: 0 1.8rem 1.2rem;
+}
+
+.post-detail-tags .feed-tag {
+ font-size: 0.82rem;
+}
+
+.post-reaction-placeholder {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.9rem 1.8rem;
+ border-top: 1px solid var(--dc-border);
+}
+
+.reaction-placeholder-label {
+ color: var(--dc-muted);
+ font-size: 0.78rem;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ font-weight: 900;
+}
+
+.reaction-placeholder-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.6rem;
+ color: var(--dc-plum);
+ font-weight: 700;
+}
+
+.reaction-button {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.3rem;
+ border: 1px solid var(--dc-border);
+ background: var(--dc-paper);
+ color: var(--dc-plum);
+ border-radius: 999px;
+ padding: 0.35rem 0.65rem;
+ font-size: 0.9rem;
+ font-weight: 700;
+ cursor: pointer;
+ transition: background 0.15s ease, border-color 0.15s ease;
+}
+
+.reaction-button:hover {
+ background: var(--dc-peach);
+ border-color: var(--dc-orange);
+}
+
+.reaction-selected {
+ background: var(--dc-peach);
+ border-color: var(--dc-orange);
+ color: var(--dc-plum-dark);
+ box-shadow: 0 0 0 1px rgba(255, 87, 34, 0.15);
+}
+
+/* =========================================================
+ COMMENTS
+ ========================================================= */
+
+.comments-section-heading {
+ display: flex;
+ justify-content: space-between;
+ align-items: end;
+ margin: 1.5rem 0 1rem;
+}
+
+.comments-section-heading h2 {
+ color: var(--dc-plum-dark);
+ font-family: "Arial Rounded MT Bold", "Avenir Next", Avenir, sans-serif;
+ font-size: 1.6rem;
+ font-weight: 900;
+ letter-spacing: -0.025em;
+}
+
+.addcomment {
+ display: none;
+}
+
+.comment-form {
+ background: var(--dc-paper);
+ border: 1px solid var(--dc-border);
+ border-radius: var(--dc-radius-md);
+ padding: 1.25rem;
+ margin-bottom: 1.2rem;
+}
+
+.comment-textarea {
+ width: 100%;
+ font-size: 1rem;
+}
+
+.comment-card {
+ display: grid;
+ grid-template-columns: 46px minmax(0, 1fr);
+ gap: 0.9rem;
+ align-items: start;
+ background: var(--dc-paper);
+ border: 1px solid var(--dc-border);
+ border-radius: var(--dc-radius-md);
+ padding: 1rem 1.2rem;
+ margin-bottom: 0.8rem;
+}
+
+.comment-avatar {
+ width: 44px;
+ height: 44px;
+ font-size: 0.95rem;
+}
+
+.comment-card-body {
+ min-width: 0;
+}
+
+.comment-card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 0.55rem;
+}
+
+.comment-author {
+ color: var(--dc-plum);
+ font-size: 1rem;
+ font-weight: 900;
+}
+
+.comment-date {
+ color: var(--dc-muted);
+ font-size: 0.85rem;
+}
+
+.comment-card-content {
+ font-size: 1rem;
+ line-height: 1.55;
+}
+
+.comment-form-actions {
+ text-align: right;
+ margin-top: 0.8rem;
+}
+.comment-reactions {
+ margin-top: 0.75rem;
+}
+
+.comment-reaction-form {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+}
+
+.dishcourse-submit-btn {
+ background: var(--dc-plum);
+ color: #fff;
+ border: none;
+ font-weight: 800;
+ padding: 0.7rem 1rem;
+ border-radius: 9px;
+}
+
+.dishcourse-submit-btn:hover {
+ background: var(--dc-plum-dark);
+ color: #fff;
+}
+
+/* =========================================================
+ CREATE POST
+ ========================================================= */
+.create-post-page {
+ max-width: 850px;
+ margin: 0 auto;
}
-.header{
- border: 1px solid black;
- overflow: hidden;
- vertical-align: middle;
- padding: 0.5%;
- margin-bottom: none;
+
+.create-post-header {
+ margin-bottom: 1.4rem;
}
-.content{
- margin-top: 0.5%;
+
+.create-post-header h1 {
+ color: var(--dc-plum-dark);
+ font-family: "Arial Rounded MT Bold", "Avenir Next", Avenir, sans-serif;
+ font-size: 2rem;
+ font-weight: 900;
+ margin-bottom: 0.3rem;
}
-.login{
- width: 25%;
- float: right;
- text-align: center;
+.create-post-header p {
+ color: var(--dc-muted);
+ font-size: 1rem;
}
-.loginbox{
- margin-right: auto;
- margin-left: auto;
+
+.create-post-card {
+ max-width: 900px;
+ background: var(--dc-paper);
+ border: 1px solid var(--dc-border);
+ border-radius: var(--dc-radius-lg);
+ padding: 2rem;
+ box-shadow: var(--dc-shadow);
+}
+
+.create-post-field {
+ margin-bottom: 1.3rem;
+}
+
+.create-post-field label {
+ display: block;
+ color: var(--dc-plum);
+ font-weight: 800;
+ margin-bottom: 0.45rem;
+}
+
+.create-post-field input,
+.create-post-field textarea,
+.auth-field input {
+ width: 100%;
+ border: 1px solid var(--dc-border);
+ border-radius: var(--dc-radius-sm);
+ padding: 0.8rem;
+ background: #fffdfa;
+ font-size: 1rem;
+}
+
+.create-post-field input {
+ min-height: 50px;
+}
+
+.create-post-field input:focus,
+.create-post-field textarea:focus,
+.auth-field input:focus {
+ border-color: var(--dc-orange);
+ outline: none;
+ box-shadow: 0 0 0 3px rgba(255, 87, 34, 0.12);
+}
+
+.create-post-textarea {
+ min-height: 280px;
+ resize: vertical;
+ line-height: 1.6;
+}
+
+.create-post-helper {
+ display: flex;
+ align-items: center;
+ gap: 0.7rem;
+ background: var(--dc-peach);
+ border-radius: 10px;
+ padding: 0.8rem 1rem;
+ margin-top: 0.5rem;
+ color: var(--dc-plum);
+}
+
+.create-post-helper p {
+ margin: 0;
+ font-size: 0.9rem;
+}
+
+.create-post-actions {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 1rem;
+ margin-top: 1.4rem;
+}
+
+.create-post-cancel {
+ color: var(--dc-muted);
+ font-weight: 700;
+}
+
+.create-post-cancel:hover {
+ color: var(--dc-plum);
+}
+
+/* =========================================================
+ LOGIN / REGISTER
+ ========================================================= */
+
+.auth-page {
+ max-width: 1000px;
+ margin: 0 auto;
+}
+
+.auth-header {
+ margin-bottom: 2rem;
+}
+
+.auth-header h1 {
+ color: var(--dc-plum-dark);
+ font-family: "Arial Rounded MT Bold", "Avenir Next", Avenir, sans-serif;
+ font-size: 2rem;
+ font-weight: 900;
+ margin-bottom: 0.4rem;
+}
+
+.auth-header p {
+ color: var(--dc-muted);
+ font-size: 1rem;
+}
+
+.auth-card {
+ position: relative;
+ height: 100%;
+ background: var(--dc-paper);
+ border: 1px solid var(--dc-border);
+ border-radius: var(--dc-radius-lg);
+ padding: 2rem;
+ box-shadow: var(--dc-shadow);
+}
+
+.auth-card h2 {
+ color: var(--dc-plum-dark);
+ font-family: "Arial Rounded MT Bold", "Avenir Next", Avenir, sans-serif;
+ font-size: 1.4rem;
+ font-weight: 900;
+ letter-spacing: -0.025em;
+ margin-bottom: 0.3rem;
+}
+
+.auth-card-intro {
+ color: var(--dc-muted);
+ font-size: 1rem;
+}
+
+.auth-card-icon {
+ width: 50px;
+ height: 50px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--dc-peach);
+ border-radius: 50%;
+ font-size: 1.4rem;
+ margin-bottom: 1rem;
+}
+
+.auth-field {
+ margin-bottom: 1rem;
+}
+
+.auth-field label {
+ display: block;
+ color: var(--dc-plum);
+ font-weight: 800;
+ font-size: 0.85rem;
+ margin-bottom: 0.35rem;
+}
+
+.auth-field input {
+ min-height: 48px;
+}
+
+/* =========================================================
+ FOOTER
+ ========================================================= */
+
+.dc-footer {
+ flex-shrink: 0;
+ background: var(--dc-paper);
+ border-top: 1px solid var(--dc-border);
+ padding: 1.6rem 2rem;
+ font-size: 1rem;
+}
+
+.dc-footer-inner {
+ display: grid;
+ grid-template-columns: 1fr auto 1fr;
+ align-items: center;
+ gap: 2rem;
+}
+
+.dc-footer-links {
+ display: flex;
+ justify-content: center;
+ gap: 2rem;
+}
+
+.dc-footer-links a {
+ font-size: 1rem;
+ font-weight: 600;
+}
+
+.dc-footer-links a:hover {
+ color: var(--dc-orange);
+}
+
+.dc-social-links {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 0.6rem;
+ /* font-size: 1rem;
+ font-weight: 900; */
+}
+
+.dc-social-links i {
+ width: 34px;
+ height: 34px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ line-height: 1;
+}
+/* =========================================================
+ MESSAGE MODAL
+ ========================================================= */
+
+.dc-modal-overlay {
+ position: fixed;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 9999;
+ padding: 1rem;
+ background: rgba(45, 8, 50, 0.55);
+}
+
+.dc-message-modal {
+ width: min(440px, 100%);
+ background: #fffaf5;
+ border: 1px solid var(--dc-border);
+ border-radius: 22px;
+ padding: 2rem;
text-align: center;
- margin-top: 3%;
+ box-shadow: 0 20px 60px rgba(45, 8, 50, 0.25);
+}
+
+.dc-message-icon {
+ font-size: 2rem;
+ margin-bottom: 0.5rem;
+}
+
+.dc-message-modal h2 {
+ margin: 0 0 0.75rem;
+ color: var(--dc-plum-dark);
+ font-weight: 900;
+}
+
+.dc-message-modal p {
+ margin: 0 0 1.5rem;
+ color: var(--dc-text);
+ font-size: 1rem;
+ line-height: 1.5;
+}
+
+.dc-modal-button {
+ min-width: 100px;
+ border: 0;
+ border-radius: 10px;
+ background: var(--dc-orange);
+ color: #fff;
+ padding: 0.7rem 1.5rem;
+ font-weight: 800;
+ cursor: pointer;
+}
+
+.dc-modal-button:hover {
+ opacity: 0.9;
+}
+
+/* =========================================================
+ RESPONSIVE
+ ========================================================= */
+
+@media (max-width: 1250px) {
+ .dc-body-layout {
+ grid-template-columns: 220px minmax(0, 1fr);
+ }
+
+ .dc-right-sidebar {
+ display: none;
+ }
+
+ .dc-header-inner {
+ height: auto;
+ min-height: 100px;
+ grid-template-columns: 280px 1fr auto;
+ }
+
+ .dc-logo {
+ width: 260px;
+ }
+
+ .dc-logo-image {
+ width: 245px;
+ }
+}
+
+@media (max-width: 1050px) {
+ .feed-post-card {
+ grid-template-columns: 52px minmax(0, 1fr) 70px 30px;
+ }
+
+ .feed-post-media-placeholder {
+ display: none;
+ }
+}
+
+@media (max-width: 900px) {
+ .dc-header-inner {
+ grid-template-columns: 1fr auto;
+ min-height: auto;
+ }
+
+ .dc-logo {
+ width: 250px;
+ height: 72px;
+ }
+
+ .dc-logo-image {
+ width: 235px;
+ height: 68px;
+ }
+
+ .dc-search {
+ grid-column: 1 / -1;
+ grid-row: 2;
+ }
+
+ .dc-start-button {
+ display: none;
+ }
+
+ .dc-body-layout {
+ display: block;
+ overflow-y: auto;
+ }
+
+ .dc-left-sidebar {
+ height: auto;
+ min-height: auto;
+ padding: 0.7rem 1rem;
+ overflow: visible;
+ }
+
+ .dc-sidebar-nav {
+ flex-direction: row;
+ overflow-x: visible;
+ justify-content: space-between;
+ gap: 0.25rem;
+ }
+
+ .dc-sidebar-item {
+ flex: 0 1 auto;
+ white-space: nowrap;
+ padding: 0.7rem 0.65rem;
+ font-size: 0.9rem;
+ }
+
+ .dc-sidebar-divider,
+ .dc-sidebar-community {
+ display: none;
+ }
+
+ .dc-main-content {
+ height: auto;
+ padding: 1.3rem;
+ overflow: visible;
+ }
+}
+
+@media (max-width: 700px) {
+ .feed-post-card {
+ grid-template-columns: 48px minmax(0, 1fr) 30px;
+ }
+
+ .feed-post-stats {
+ display: none;
+ }
+
+ .feed-post-avatar {
+ width: 46px;
+ height: 46px;
+ }
+
+ .feed-post-meta {
+ display: block;
+ }
+
+ .feed-meta-dot {
+ display: none;
+ }
+
+ .post-detail-author-row {
+ align-items: flex-start;
+ }
+
+ .post-detail-avatar {
+ width: 48px;
+ height: 48px;
+ }
+
+ .reaction-placeholder-actions {
+ margin-top: 0.7rem;
+ flex-wrap: wrap;
+ }
+
+ .comment-card {
+ grid-template-columns: 40px minmax(0, 1fr);
+ }
+}
+
+@media (max-width: 600px) {
+ html,
+ body {
+ height: auto;
+ min-height: 100%;
+ overflow-y: auto;
+ }
+
+ .dc-app-shell {
+ height: auto;
+ min-height: 100vh;
+ }
+
+ .dc-body-layout {
+ display: block;
+ overflow: visible;
+ }
+
+ .dc-left-sidebar {
+ height: auto;
+ overflow: visible;
+ padding: 0.6rem;
+ }
+
+ .dc-main-content {
+ height: auto;
+ overflow: visible;
+ padding: 1rem;
+ }
+
+ .dc-sidebar-nav {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 0.4rem;
+ }
+
+ .dc-sidebar-item {
+ justify-content: center;
+ padding: 0.65rem 0.4rem;
+ font-size: 0.8rem;
+ white-space: nowrap;
+ }
+
+ .dc-sidebar-icon {
+ width: auto;
+ font-size: 1rem;
+ }
+
+ .dc-footer {
+ margin-top: auto;
+ }
+
+ .dc-header-inner {
+ padding: 1rem;
+ }
+
+ .dc-logo {
+ width: 210px;
+ height: 64px;
+ }
+
+ .dc-logo-image {
+ width: 200px;
+ height: 60px;
+ }
+
+ .dc-header-user-info {
+ display: none;
+ }
+
+ .dc-footer-inner {
+ display: block;
+ text-align: center;
+ }
+
+ .dc-footer-links {
+ flex-wrap: wrap;
+ gap: 0.7rem 1rem;
+ margin: 1rem 0;
+ }
+
+ .dc-social-links {
+ justify-content: center;
+ }
+
+ .forum-section-heading,
+ .discussion-header,
+ .comments-section-heading,
+ .post-reaction-placeholder {
+ display: block;
+ }
+}
+
+/* =========================================================
+ PUBLIC POST TOGGLE
+ ========================================================= */
+
+.create-post-public {
+ margin: 1.2rem 0 1.5rem;
+}
+
+.public-toggle {
+ display: flex;
+ align-items: center;
+ gap: 0.9rem;
+ cursor: pointer;
+}
+
+.public-toggle input {
+ position: absolute;
+ opacity: 0;
+ pointer-events: none;
+}
+
+.public-toggle-slider {
+ position: relative;
+ width: 48px;
+ height: 26px;
+ flex-shrink: 0;
+ background: var(--dc-border);
+ border-radius: 999px;
+ transition: background 0.2s ease;
+}
+
+.public-toggle-slider::after {
+ content: "";
+ position: absolute;
+ top: 3px;
+ left: 3px;
+ width: 20px;
+ height: 20px;
+ background: #fff;
+ border-radius: 50%;
+ box-shadow: 0 2px 6px rgba(38, 6, 47, 0.18);
+ transition: transform 0.2s ease;
+}
+
+.public-toggle input:checked + .public-toggle-slider {
+ background: var(--dc-orange);
+}
+
+.public-toggle input:checked + .public-toggle-slider::after {
+ transform: translateX(22px);
+}
+
+.public-toggle-text {
+ display: flex;
+ flex-direction: column;
+}
+
+.public-toggle-text strong {
+ color: var(--dc-plum-dark);
+ font-size: 1rem;
+ font-weight: 800;
+}
+
+.public-toggle-text small {
+ color: var(--dc-muted);
+ font-size: 0.85rem;
+ margin-top: 0.15rem;
+}
+
+/* =========================================================
+ ABOUT PAGE
+ ========================================================= */
+
+.about-page {
+ max-width: 1000px;
+ margin: 0 auto;
+}
+
+.about-hero {
+ background: linear-gradient(135deg, var(--dc-plum-dark), var(--dc-plum-mid));
+ color: #fff;
+ border-radius: var(--dc-radius-lg);
+ padding: 2.5rem;
+ margin-bottom: 1.5rem;
+ box-shadow: var(--dc-shadow);
+}
+
+.about-hero h1 {
+ font-family: "Arial Rounded MT Bold", "Avenir Next", Avenir, sans-serif;
+ font-size: 2.2rem;
+ font-weight: 900;
+ margin-bottom: 0.8rem;
+}
+
+.about-hero p {
+ max-width: 700px;
+ font-size: 1rem;
+ line-height: 1.7;
+ color: rgba(255, 255, 255, 0.88);
}
-.loginelement{
- margin-top: 0.5%;
+
+.about-card {
+ background: var(--dc-paper);
+ border: 1px solid var(--dc-border);
+ border-radius: var(--dc-radius-lg);
+ padding: 1.8rem;
+ margin-bottom: 1.4rem;
+ box-shadow: 0 5px 14px rgba(67, 20, 70, 0.04);
+}
+
+.about-card h2 {
+ color: var(--dc-plum-dark);
+ font-family: "Arial Rounded MT Bold", "Avenir Next", Avenir, sans-serif;
+ font-size: 1.5rem;
+ font-weight: 900;
+ margin-bottom: 0.8rem;
+}
+
+.about-card p {
+ color: var(--dc-muted);
+ line-height: 1.7;
+}
+
+.about-team-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 1rem;
+}
+
+.about-team-member {
+ background: var(--dc-peach);
+ border-radius: var(--dc-radius-md);
+ padding: 1.2rem;
+}
+
+.about-team-member h3 {
+ color: var(--dc-plum-dark);
+ font-size: 1.1rem;
+ font-weight: 900;
+ margin-bottom: 0.25rem;
+}
+
+.about-team-member p {
+ color: var(--dc-orange);
+ font-weight: 800;
+ margin-bottom: 0.5rem;
+}
+
+.about-team-member span {
+ color: var(--dc-muted);
+ font-size: 0.9rem;
+ line-height: 1.5;
}
-.logoutlink{
- padding-left: 0.5%;
- font-size: 12pt;
+
+@media (max-width: 700px) {
+ .about-team-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .about-hero {
+ padding: 1.8rem;
+ }
}
-.postsubmit{
+/* =========================================================
+ DIRECT MESSAGES
+ ========================================================= */
+
+.dc-sidebar-badge {
margin-left: auto;
- margin-right: auto;
+ background: var(--dc-orange);
+ color: #fff;
+ font-size: 0.72rem;
+ font-weight: 800;
+ line-height: 1;
+ padding: 0.28rem 0.5rem;
+ border-radius: 999px;
+ min-width: 1.4rem;
text-align: center;
+}
+
+.message-user-link {
+ color: var(--dc-orange);
+ font-weight: 700;
+ font-size: 0.88rem;
+ text-decoration: none;
+}
+
+.message-user-link:hover {
+ text-decoration: underline;
+}
+.messages-page,
+.conversation-page {
+ background: var(--dc-paper);
+ border: 1px solid var(--dc-border);
+ border-radius: var(--dc-radius-lg);
+ padding: 1.8rem;
+ margin-bottom: 1.6rem;
}
-.subforumlisting{
- border-bottom: 1px solid gray;
- padding: 1%;
- margin-bottom: 1%;
+
+.messages-heading {
+ margin-bottom: 1.4rem;
+}
+
+.messages-heading h1 {
+ color: var(--dc-plum-dark);
+ font-family: "Arial Rounded MT Bold", "Avenir Next", Avenir, sans-serif;
+ margin: 0.2rem 0 0;
+}
+
+.conversation-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.6rem;
}
-.inputbox{
- padding: 0.25%;
- margin-top: 0.5%;
+
+.conversation-item {
+ display: grid;
+ grid-template-columns: 46px minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 0.9rem;
+ background: var(--dc-ivory);
+ border: 1px solid var(--dc-border);
+ border-radius: var(--dc-radius-md);
+ padding: 0.9rem 1.1rem;
+ text-decoration: none;
+ color: inherit;
+ transition: box-shadow 0.15s ease, transform 0.15s ease;
}
-.errors{
- border-bottom: 1px solid black;
- margin-top: 2%;
+
+.conversation-item:hover {
+ box-shadow: 0 5px 14px rgba(67, 20, 70, 0.06);
+ transform: translateY(-1px);
+ color: inherit;
}
-.error{
- color: red;
- font-weight: bold;
+
+.conversation-item.conversation-unread {
+ border-color: var(--dc-orange-soft);
+ background: #fff;
}
-.subforumtitle{
- color: blue;
- font-size: 15pt;
- font-weight: bold;
+
+.conversation-avatar,
+.message-bubble-row .conversation-avatar {
+ width: 46px;
+ height: 46px;
+ border-radius: 50%;
+ background: var(--dc-plum);
+ color: #fff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 800;
+ flex-shrink: 0;
}
-.subforumdesc{
- font-size: 13pt;
- font-style: italic;
+
+.conversation-info {
+ min-width: 0;
}
-.subforumheader{
- padding: 0.5%;
- padding-left: 1%;
- margin-top: none;
- padding-bottom: none;
- margin-bottom: 0.5%;
- border: 1px solid black;
- font-size: 15pt;
+
+.conversation-top-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ gap: 0.6rem;
}
-.subforumheadertitle{
- display: inline;
- font-weight: bold;
+
+.conversation-top-row strong {
+ color: var(--dc-plum-dark);
}
-.subforumheaderdesc{
- display: inline;
+
+.conversation-time {
+ color: var(--dc-muted);
+ font-size: 0.8rem;
+ white-space: nowrap;
}
-.post{
- padding: 1%;
- border-bottom: 1px solid gray;
+
+.conversation-preview {
+ color: var(--dc-muted);
+ font-size: 0.9rem;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ margin-top: 0.2rem;
}
-.postusername{
- font-style: italic;
- font-size: 12pt;
+
+.conversation-preview-you {
+ color: var(--dc-plum);
+ font-weight: 700;
}
-.subforumheaderlink{
- font-style: italic;
- font-size: 12pt;
- margin-top: 0.5%;
+
+.conversation-badge {
+ background: var(--dc-orange);
+ color: #fff;
+ font-size: 0.78rem;
+ font-weight: 800;
+ line-height: 1;
+ padding: 0.35rem 0.6rem;
+ border-radius: 999px;
+ min-width: 1.6rem;
+ text-align: center;
}
-.postinginfo{
- font-size: 11pt;
- font-style: italic;
+
+.conversation-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ margin-bottom: 1.4rem;
+ flex-wrap: wrap;
}
-#nolinks{
+
+.conversation-back {
+ color: var(--dc-muted);
+ font-weight: 700;
text-decoration: none;
+ font-size: 0.9rem;
}
-.noposts{
- padding: 1%;
+
+.conversation-back:hover {
+ color: var(--dc-plum);
}
-.subsubforums{
- border: 1px solid black;
- padding: 0.5%;
- margin-top: 0.5%;
- padding-left: 1%;
+
+.conversation-header-user {
+ display: flex;
+ align-items: center;
+ gap: 0.8rem;
}
-.subsubforumtitle{
- font-weight: bolder;
- text-decoration: none;
- font-size: 14pt;
+
+.conversation-header-user h1 {
+ color: var(--dc-plum-dark);
+ font-family: "Arial Rounded MT Bold", "Avenir Next", Avenir, sans-serif;
+ margin: 0;
+ font-size: 1.4rem;
+}
+
+.conversation-thread {
+ display: flex;
+ flex-direction: column;
+ gap: 0.7rem;
+ min-height: 200px;
+ margin-bottom: 1.4rem;
+}
+
+.message-bubble-row {
+ display: flex;
+}
+
+.message-bubble-row.message-outgoing {
+ justify-content: flex-end;
+}
+
+.message-bubble-row.message-incoming {
+ justify-content: flex-start;
+}
+
+.message-bubble {
+ max-width: min(70%, 520px);
+ padding: 0.7rem 1rem;
+ border-radius: var(--dc-radius-md);
+}
+
+.message-incoming .message-bubble {
+ background: var(--dc-ivory);
+ border: 1px solid var(--dc-border);
+ color: var(--dc-text);
+}
+
+.message-outgoing .message-bubble {
+ background: var(--dc-plum);
+ color: #fff;
}
-.subsubforumdesc{
- font-style: italic;
- font-size: 12pt;
+
+.message-bubble-content {
+ font-size: 0.95rem;
+ line-height: 1.5;
+ white-space: pre-wrap;
+ word-break: break-word;
}
-.toplevelinfo{
- padding: 0.5%;
- font-weight: bold;
+
+.message-bubble-time {
+ font-size: 0.72rem;
+ opacity: 0.7;
+ margin-top: 0.3rem;
}
-.actualpost{
- padding: 1%;
- border: 1px solid black;
- border-collapse: collapse;
- margin-bottom: 1%;
+
+.message-compose-form {
+ display: flex;
+ gap: 0.8rem;
+ align-items: flex-end;
+ border-top: 1px solid var(--dc-border);
+ padding-top: 1.2rem;
}
-.actualposttitle{
- border-collapse: collapse;
- border-bottom: 1px solid black;
- padding: 1%;
- font-size: 16pt;
- font-weight: bold;
+
+.message-compose-textarea {
+ flex: 1;
+ resize: vertical;
}
-.postcontent{
- padding: 1%;
+
+/* =========================================================
+ PROFILE
+ ========================================================= */
+
+.profile-card {
+ max-width: 640px;
+ margin: 0 auto;
}
-.postposter{
- font-size: 12pt;
- font-style: italic;
- color: black;
+
+.profile-avatar-row {
+ display: flex;
+ align-items: center;
+ gap: 1.5rem;
+ padding-bottom: 1.5rem;
+ margin-bottom: 1.5rem;
+ border-bottom: 1px solid var(--dc-border);
}
-.posttime{
- font-style: italic;
- font-size: 11pt;
+
+.profile-avatar-large {
+ width: 96px;
+ height: 96px;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ background: linear-gradient(135deg, var(--dc-orange), var(--dc-plum));
+ color: #fff;
+ font-size: 2.2rem;
+ font-weight: 900;
+ overflow: hidden;
}
-.whitespace{
- white-space: pre-line;
+
+.profile-avatar-large img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
}
-.comments{
- border: 1px solid gray;
- padding: 1%;
- margin-top: 1%;
+
+.profile-field-row {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 1rem;
+ padding: 1rem 0;
+ border-bottom: 1px solid var(--dc-border);
}
-.comment{
- border-bottom: 1px solid gray;
+
+.profile-field-row:last-of-type {
+ border-bottom: none;
}
-.commentuser{
- display: inline;
- font-weight: bold;
+
+.profile-field-label {
+ color: var(--dc-muted);
+ font-size: 0.78rem;
+ font-weight: 800;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+ margin-bottom: 0.2rem;
}
-.commentcontent{
- display: inline;
+
+.profile-field-value {
+ color: var(--dc-text);
+ font-size: 1.05rem;
+ font-weight: 700;
}
-.commenttime{
- font-size: 11pt;
- font-style: italic;
+
+.profile-change {
+ flex-shrink: 0;
}
+.profile-change > summary {
+ list-style: none;
+ cursor: pointer;
+ color: var(--dc-orange);
+ font-size: 0.88rem;
+ font-weight: 800;
+}
-.addcomment{
+.profile-change > summary::-webkit-details-marker {
display: none;
- margin-right: auto;
- margin-left: auto;
- text-align: center;
}
-.varwidth{
- width: 50%;
+
+.profile-change > summary:hover {
+ color: #e84412;
+ text-decoration: underline;
+}
+
+.profile-change-form {
+ margin-top: 0.8rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.6rem;
+ min-width: 220px;
+}
+
+.profile-change-hint {
+ margin: 0;
+ color: var(--dc-muted);
+ font-size: 0.8rem;
+}
+
+.profile-settings-link {
+ max-width: 640px;
+ margin: 1.25rem auto 0;
+ text-align: right;
+}
+
+.dc-settings-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ background: var(--dc-paper);
+ border: 1px solid var(--dc-border);
+ color: var(--dc-plum-dark);
+ font-weight: 800;
+ padding: 0.6rem 1.1rem;
+ border-radius: 9px;
+}
+
+.dc-settings-btn:hover {
+ background: var(--dc-peach);
+ color: var(--dc-plum-dark);
+}
+
+.profile-back-link {
+ display: inline-block;
+ margin-top: 0.6rem;
+ color: var(--dc-orange);
+ font-weight: 700;
+ font-size: 0.9rem;
+}
+
+.profile-back-link:hover {
+ color: #e84412;
}
diff --git a/forum/templates/about.html b/forum/templates/about.html
new file mode 100644
index 0000000..ada0d40
--- /dev/null
+++ b/forum/templates/about.html
@@ -0,0 +1,64 @@
+{% extends 'layout.html' %}
+{% block body %}
+
+
+
+ About DishCourse
+ Good Food. Better Conversation.
+
+ DishCourse is a social food forum created for everyday cooks, foodies,
+ social media chefs, and anyone who enjoys talking about food.
+
+
+
+
+ Our Persona
+
+ DishCourse was designed as a welcoming space where people can share
+ recipes, ask questions, exchange ideas, critique dishes, and learn
+ from one another.
+
+
+
+ The experience blends the casual energy of a food community with a
+ clean, organized discussion platform that makes conversations easy
+ to follow.
+
+
+
+
+ Meet the Team
+
+
+
+
Leigh
+
Interaction & Presentation
+
Comments, reactions, branding, responsive UI, and integration.
+
+
+
+
Monah
+
Posts & Content
+
Post features, content functionality, and related enhancements.
+
+
+
+
Sloane
+
Users & Communication
+
User-focused functionality, communication features, and related enhancements.
+
+
+
+
+
+ Built By CircusCircusLimes
+
+ DishCourse was developed as a collaborative software engineering
+ project using Python, Flask, MySQL, Bootstrap, HTML, CSS, Jinja,
+ and GitHub.
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/forum/templates/contact.html b/forum/templates/contact.html
new file mode 100644
index 0000000..96e03ef
--- /dev/null
+++ b/forum/templates/contact.html
@@ -0,0 +1,107 @@
+{% extends 'layout.html' %}
+{% block body %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/forum/templates/conversation.html b/forum/templates/conversation.html
new file mode 100644
index 0000000..8e986a9
--- /dev/null
+++ b/forum/templates/conversation.html
@@ -0,0 +1,43 @@
+{% extends 'layout.html' %}
+
+{% block body %}
+
+
+
+
+ {% if thread_messages %}
+ {% for message in thread_messages %}
+
+
+
{{ message.content }}
+
{{ message.get_time_string() }}
+
+
+ {% endfor %}
+ {% else %}
+
+
No messages yet.
+
Send {{ other.username }} a message to start the conversation.
+
+ {% endif %}
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/forum/templates/createpost.html b/forum/templates/createpost.html
index 85947a6..13e5c22 100644
--- a/forum/templates/createpost.html
+++ b/forum/templates/createpost.html
@@ -1,21 +1,93 @@
{% extends 'layout.html' %}
+
{% block body %}
+
+
+
+
START A CONVERSATION
+
Create a Discussion
+
You're posting in {{ subforum.name }}
+
+
+
+
+
+
+
+ Discussion Title
+
+
+
+
+
+
+
+
+
+ Make this post public
+ Public posts can be viewed by everyone.
+
+
+
+
+
+ What would you like to share?
+
+
+
+
+
π‘
+
Tip: Give people enough detail to jump into the conversation.
+
+
+
+ Image or video link (optional)
+
+
+
+
+ Cancel
+
-
- Enter your post here:
-
- You are posting to {{ subforum.title }}
-
+
+
-
+
-
+
-
+
-
-
-
{% endblock %}
\ No newline at end of file
diff --git a/forum/templates/header.html b/forum/templates/header.html
index 9403a0d..fe90b2b 100644
--- a/forum/templates/header.html
+++ b/forum/templates/header.html
@@ -1,8 +1,114 @@
-{{ config.SITE_NAME }} {% if config.SITE_DESCRIPTION %} - {% endif %} {{ config.SITE_DESCRIPTION }}
-
- {% if current_user.is_authenticated %}
-
{{ current_user.username }} (logout)
- {% else %}
-
Click here to login or register!
- {% endif %}
+
\ No newline at end of file
diff --git a/forum/templates/house_rules.html b/forum/templates/house_rules.html
new file mode 100644
index 0000000..652a66c
--- /dev/null
+++ b/forum/templates/house_rules.html
@@ -0,0 +1,67 @@
+{% extends 'layout.html' %}
+{% block body %}
+
+
+
+
+ DishCourse Community
+ House Rules π½οΈ
+
+ Good food brings people together. Good conversation should too.
+ Pull up a chair, share your thoughts, and help us keep DishCourse
+ welcoming for everyone at the table.
+
+
+
+
+ π₯ Critique the Dish, Not the Cook
+
+ Disagree with a recipe, technique, or opinion all you want β
+ just keep it respectful. There's always room for a different
+ taste at the table.
+
+
+
+
+ πΆοΈ Keep It Flavorful, Not Foul
+
+ A little spice makes a conversation interesting. Personal attacks,
+ insults, and disrespect do not.
+
+
+
+
+ π° Give Credit Where It's Due
+
+ Sharing someone else's recipe, technique, or idea? Give them
+ their flowers β and their credit.
+
+
+
+
+ π₯ Don't Spam the Kitchen
+
+ Nobody ordered 37 servings of the same post. Keep discussions
+ relevant and avoid repetitive or promotional spam.
+
+
+
+
+ π΄ Everyone Gets a Seat
+
+ New cooks, seasoned chefs, foodies, recipe experimenters, and
+ people who just really love to eat are all welcome here.
+
+
+
+
+ β€οΈ Enjoy the DishCourse
+
+ Ask questions. Share what you know. Learn something new.
+ Laugh a little. And most importantly, enjoy the conversation.
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/forum/templates/layout.html b/forum/templates/layout.html
index a2a5e14..4d74329 100644
--- a/forum/templates/layout.html
+++ b/forum/templates/layout.html
@@ -1,30 +1,116 @@
-
+
-
-
-
{{ config.SITE_NAME }}
+
+
+
+
+
+
+
+
+
+
DishCourse
+
-
-
- {% if errors %}
-
- {% for error in errors %}
-
- {{ error }}
-
-
- {% endfor %}
-
-{% endif %}
-
- {% block body %}{% endblock %}
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% if errors %}
+
+ {% for error in errors %}
+
{{ error }}
+ {% endfor %}
+
+ {% endif %}
-
+ {% with messages = get_flashed_messages(with_categories=true) %}
+ {% if messages %}
+ {% set ns = namespace(all_success=true) %}
+ {% for category, message in messages %}
+ {% if category != 'success' %}
+ {% set ns.all_success = false %}
+ {% endif %}
+ {% endfor %}
+
+
+
{% if ns.all_success %}β
{% else %}β οΈ{% endif %}
+
{% if ns.all_success %}All Set{% else %}Hold That Thought{% endif %}
+ {% for category, message in messages %}
+
{{ message }}
+ {% endfor %}
+
OK
+
+
+ {% endif %}
+ {% endwith %}
+
+ {% block body %}{% endblock %}
+
+
+
+
+
+
+
+
+
+
+
+
+