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/index.html b/frontend/index.html index 835bd6a..2db9969 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -33,6 +33,10 @@ + + + +
diff --git a/frontend/src/components/features/auth/pages/ForgotPasswordPage.tsx b/frontend/src/components/features/auth/pages/ForgotPasswordPage.tsx index 1e7b50f..b4ede3f 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,10 +90,14 @@ const StyledCard = styled(Card)` } ` -const ForgotPasswordPage = () => { +// Email validation helper +const isValidEmail = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + return emailRegex.test(email.trim()) +} + +const ForgotPasswordPage: React.FC = () => { const [email, setEmail] = useState('') - const [firstName, setFirstName] = useState('') - const [lastName, setLastName] = useState('') const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [success, setSuccess] = useState(false) @@ -125,6 +110,19 @@ const ForgotPasswordPage = () => { setError('') setSuccess(false) + // Trim and validate email + const trimmedEmail = email.trim() + + if (!trimmedEmail) { + setError('Please enter your email address.') + return + } + + if (!isValidEmail(trimmedEmail)) { + setError('Please enter a valid email address.') + return + } + // Validate Turnstile const turnstileToken = getToken() @@ -136,11 +134,9 @@ const ForgotPasswordPage = () => { setLoading(true) try { - await authAPI.forgotPassword(email, firstName, lastName, turnstileToken) + await authAPI.forgotPassword(trimmedEmail, turnstileToken) setSuccess(true) setEmail('') - setFirstName('') - setLastName('') } catch (error: unknown) { logger.error('Forgot password error:', error) setError(getErrorMessage(error, 'An error occurred. Please try again.')) @@ -153,114 +149,85 @@ 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 - -
-
- )} -
-
-
-
-
-