From 26ac8858ecd58f5df1b7b1c49140171a8aaf06c2 Mon Sep 17 00:00:00 2001 From: Duncan Murchison Date: Wed, 21 Jan 2026 11:43:19 -0500 Subject: [PATCH 1/4] feat: add ProfileSettingsModal for user profile management - Implemented ProfileSettingsModal component for updating username, changing password, and managing two-factor authentication. - Integrated modal into ProfilePage for user settings access. - Added functionality to fetch and display user's voted and commented posts. - Updated API service to include endpoints for fetching voted and commented posts. - Enhanced BlogListPage to show comment counts for each post. - Refactored ProfilePage to streamline user interaction and settings management. --- backend/.env.example | 3 + backend/models/post.py | 3 +- backend/routes/user.py | 49 ++ .../auth/pages/ForgotPasswordPage.tsx | 255 ++++---- .../blog/components/ProfileSettingsModal.tsx | 411 ++++++++++++ .../features/blog/pages/BlogListPage.tsx | 5 + .../features/blog/pages/ProfilePage.tsx | 612 ++++++++---------- frontend/src/services/api.ts | 12 +- frontend/src/types/index.ts | 14 + 9 files changed, 890 insertions(+), 474 deletions(-) create mode 100644 frontend/src/components/features/blog/components/ProfileSettingsModal.tsx diff --git a/backend/.env.example b/backend/.env.example index 2bf470f..eea0486 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -32,6 +32,9 @@ REDIS_PASSWORD= REDIS_URL=redis://:your_redis_password_here@localhost:6379/0 # Frontend URL +# Staging: http://localhost:3000 FRONTEND_URL=http://localhost:5173 +# Backend Port +# Staging: 8000 PORT=5050 diff --git a/backend/models/post.py b/backend/models/post.py index 5126cea..b81b950 100644 --- a/backend/models/post.py +++ b/backend/models/post.py @@ -29,7 +29,8 @@ def to_dict(self): "downvotes": self.downvotes, "created_at": self.created_at.isoformat() if self.created_at else None, "user_id": self.user_id, - "author": self.user.username if hasattr(self, 'user') and self.user else None + "author": self.user.username if hasattr(self, 'user') and self.user else None, + "comment_count": len(self.comments) if self.comments else 0 } def __repr__(self): diff --git a/backend/routes/user.py b/backend/routes/user.py index 1118573..4ee7bcb 100755 --- a/backend/routes/user.py +++ b/backend/routes/user.py @@ -176,3 +176,52 @@ def get_user_comments_count(username): count = Comment.query.filter_by(user_id=user.id).count() return jsonify({"count": count}), 200 +# GET POSTS USER HAS VOTED ON +@user_bp.route('/users//voted-posts', methods=['GET']) +def get_user_voted_posts(username): + """Get all posts that a user has voted on""" + user = User.query.filter_by(username=username).first() + if not user: + return jsonify({"msg": "User not found"}), 404 + + # Get all votes by this user with their associated posts + votes = Vote.query.filter_by(user_id=user.id).all() + + # Get the posts and include user's vote type + posts_with_votes = [] + for vote in votes: + post = BlogPost.query.get(vote.post_id) + if post: + post_dict = post.to_dict() + post_dict['user_vote'] = vote.vote_type + posts_with_votes.append(post_dict) + + return jsonify(posts_with_votes), 200 + +# GET POSTS USER HAS COMMENTED ON +@user_bp.route('/users//commented-posts', methods=['GET']) +def get_user_commented_posts(username): + """Get all posts that a user has commented on, with their comment preview""" + user = User.query.filter_by(username=username).first() + if not user: + return jsonify({"msg": "User not found"}), 404 + + # Get distinct posts user has commented on + # For each post, include user's most recent comment as a preview + comments = Comment.query.filter_by(user_id=user.id).order_by(Comment.created_at.desc()).all() + + # Track unique posts and their most recent comment from this user + seen_posts = {} + for comment in comments: + if comment.post_id not in seen_posts: + post = BlogPost.query.get(comment.post_id) + if post: + post_dict = post.to_dict() + post_dict['user_comment'] = { + 'content': comment.content, + 'created_at': comment.created_at.isoformat() if comment.created_at else None + } + seen_posts[comment.post_id] = post_dict + + return jsonify(list(seen_posts.values())), 200 + diff --git a/frontend/src/components/features/auth/pages/ForgotPasswordPage.tsx b/frontend/src/components/features/auth/pages/ForgotPasswordPage.tsx index 1e7b50f..cabf468 100644 --- a/frontend/src/components/features/auth/pages/ForgotPasswordPage.tsx +++ b/frontend/src/components/features/auth/pages/ForgotPasswordPage.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import React, { useState } from 'react' import { Container, Form, Card } from 'react-bootstrap' import { Link } from 'react-router-dom' import styled from 'styled-components' @@ -7,32 +7,23 @@ import { useTurnstile } from '../../../../hooks/useTurnstile' import StyledAlert from '../../../common/StyledAlert' import logger from '../../../../utils/logger' import { getErrorMessage } from '../../../../utils/errors' -import { PrimaryButton } from '../../../common/StyledButton' -import { colors, gradients, shadows } from '../../../../theme/colors' -import Footer from '../../../layout/Footer' +import { SubmitButton } from '../../../common/StyledButton' +import { colors, gradients } from '../../../../theme/colors' const PageWrapper = styled.div` min-height: 100vh; background: linear-gradient(135deg, ${colors.backgroundDark} 0%, ${colors.background} 100%); display: flex; - flex-direction: column; - padding-top: 70px; /* Account for navbar */ -` - -const ContentWrapper = styled.div` - flex: 1; - display: flex; align-items: center; padding: 40px 0; ` const StyledCard = styled(Card)` - background: ${colors.backgroundAlt}; - border: 1px solid ${colors.borderLight}; - box-shadow: ${shadows.large}; + background: ${colors.background}; + border: 1px solid ${colors.primary}; + box-shadow: 0 8px 32px rgba(40, 167, 69, 0.15); max-width: 500px; margin: 0 auto; - border-radius: 12px; .card-header { background: ${gradients.primary}; @@ -40,20 +31,17 @@ const StyledCard = styled(Card)` color: ${colors.text.primary}; padding: 1.5rem; text-align: center; - border-radius: 12px 12px 0 0; h2 { margin: 0; font-size: 1.75rem; font-weight: 600; - color: #000; } .subtitle { margin-top: 0.5rem; font-size: 0.95rem; opacity: 0.9; - color: #000; } } @@ -68,16 +56,16 @@ const StyledCard = styled(Card)` } .form-control { - background: ${colors.backgroundDark}; - border: 1px solid ${colors.borderLight}; + background: ${colors.backgroundLight}; + border: 1px solid ${colors.borderInput}; color: ${colors.text.primary}; padding: 0.75rem; border-radius: 8px; &:focus { - background: ${colors.backgroundDark}; + background: ${colors.backgroundLight}; border-color: ${colors.primary}; - box-shadow: 0 0 0 0.2rem rgba(2, 196, 60, 0.25); + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); color: ${colors.text.primary}; } @@ -86,13 +74,6 @@ const StyledCard = styled(Card)` } } - .form-text { - color: ${colors.text.muted}; - display: block; - margin-top: 0.5rem; - font-size: 0.875rem; - } - .back-link { color: ${colors.primary}; text-decoration: none; @@ -109,7 +90,7 @@ const StyledCard = styled(Card)` } ` -const ForgotPasswordPage = () => { +const ForgotPasswordPage: React.FC = () => { const [email, setEmail] = useState('') const [firstName, setFirstName] = useState('') const [lastName, setLastName] = useState('') @@ -153,114 +134,112 @@ const ForgotPasswordPage = () => { } return ( - <> - - - - - -

Forgot Password

-
We'll send you a reset link
-
- - {error && ( - - Unable to Process Request -
{error}
-
- )} - - {success ? ( - - Email Sent! -

- If an account with that email exists, a password reset link has been sent. - Please check your email and follow the instructions. The link will expire in 2 hours. -

-
- - - Back to Home - -
-
- ) : ( -
- - Email Address - setEmail(e.target.value)} - placeholder="Enter your email" - required - /> - - - - First Name - setFirstName(e.target.value)} - placeholder="Enter your first name" - required - /> - - - - Last Name - setLastName(e.target.value)} - placeholder="Enter your last name" - required - /> - - For security, please enter your full name as registered - - - - {/* Turnstile CAPTCHA */} -
-
-
- -
- - {loading ? ( - <> - - Sending... - - ) : ( - <> - - Send Reset Link - - )} - -
- -
- - - Back to Home - -
-
- )} -
-
-
-
-
-