diff --git a/client/src/api/client.ts b/client/src/api/client.ts index c02c843..4908b31 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -45,8 +45,8 @@ class ApiClient { return this.request(path, { method: 'PATCH', body: body ? JSON.stringify(body) : undefined }); } - delete(path: string) { - return this.request(path, { method: 'DELETE' }); + delete(path: string, body?: unknown) { + return this.request(path, { method: 'DELETE', body: body ? JSON.stringify(body) : undefined }); } async upload(path: string, formData: FormData): Promise { diff --git a/client/src/components/layout/Footer.tsx b/client/src/components/layout/Footer.tsx index e350be6..828330f 100644 --- a/client/src/components/layout/Footer.tsx +++ b/client/src/components/layout/Footer.tsx @@ -68,9 +68,9 @@ export function Footer() {

Support

@@ -78,17 +78,17 @@ export function Footer() {

About

Follow Us

- - - - + + + +
@@ -97,8 +97,8 @@ export function Footer() {

© 2024 Marketplace. All Rights Reserved.

- Terms of Service - Privacy Policy + Terms of Service + Privacy Policy
diff --git a/client/src/pages/ChatPage.tsx b/client/src/pages/ChatPage.tsx index e0265ad..03dd044 100644 --- a/client/src/pages/ChatPage.tsx +++ b/client/src/pages/ChatPage.tsx @@ -1,5 +1,6 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { Send } from 'lucide-react'; +import { useLocation } from 'react-router-dom'; import { Avatar } from '../components/ui/Avatar'; import { api } from '../api/client'; import { useAuth } from '../context/AuthContext'; @@ -8,17 +9,25 @@ import type { Conversation, Message } from '../types'; export function ChatPage() { const { user } = useAuth(); + const location = useLocation(); const [conversations, setConversations] = useState([]); const [selectedConv, setSelectedConv] = useState(); const [messages, setMessages] = useState([]); const [newMessage, setNewMessage] = useState(''); const [loading, setLoading] = useState(true); + const [sending, setSending] = useState(false); + const messagesEndRef = useRef(null); useEffect(() => { api.get('/chat/conversations') .then(convs => { setConversations(convs); - if (convs.length > 0 && !selectedConv) setSelectedConv(convs[0].id); + const stateConvId = (location.state as { conversationId?: string })?.conversationId; + if (stateConvId && convs.find(c => c.id === stateConvId)) { + setSelectedConv(stateConvId); + } else if (convs.length > 0 && !selectedConv) { + setSelectedConv(convs[0].id); + } }) .catch(() => {}) .finally(() => setLoading(false)); @@ -27,7 +36,10 @@ export function ChatPage() { useEffect(() => { if (!selectedConv) return; api.get<{ data: Message[] }>(`/chat/conversations/${selectedConv}/messages`) - .then(res => setMessages(res.data)) + .then(res => { + setMessages(res.data); + setTimeout(() => messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }), 100); + }) .catch(() => setMessages([])); }, [selectedConv]); @@ -38,7 +50,8 @@ export function ChatPage() { const handleSend = async (e: React.FormEvent) => { e.preventDefault(); - if (!newMessage.trim() || !selectedConv || !activeConv) return; + if (!newMessage.trim() || !selectedConv || !activeConv || sending) return; + setSending(true); const recipientId = activeConv.user1.id === user?.id ? activeConv.user2.id : activeConv.user1.id; try { await api.post('/chat/conversations', { @@ -47,10 +60,11 @@ export function ChatPage() { message: newMessage, }); setNewMessage(''); - // Refresh messages const res = await api.get<{ data: Message[] }>(`/chat/conversations/${selectedConv}/messages`); setMessages(res.data); + setTimeout(() => messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }), 100); } catch {} + setSending(false); }; if (loading) return
Loading conversations...
; @@ -116,13 +130,15 @@ export function ChatPage() { ); })} +
setNewMessage(e.target.value)} placeholder="Type a message..." className="flex-1 px-4 py-2.5 rounded-xl border border-gray-200 bg-gray-50 text-sm focus:border-primary-400 focus:outline-none" /> -
diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx index 8a53f61..691de2a 100644 --- a/client/src/pages/LoginPage.tsx +++ b/client/src/pages/LoginPage.tsx @@ -4,7 +4,9 @@ import { Mail, Lock, Eye, EyeOff } from 'lucide-react'; import { Input } from '../components/ui/Input'; import { GradientButton } from '../components/ui/GradientButton'; import { Button } from '../components/ui/Button'; +import { Modal } from '../components/ui/Modal'; import { useAuth } from '../context/AuthContext'; +import { api } from '../api/client'; export function LoginPage() { const navigate = useNavigate(); @@ -15,6 +17,12 @@ export function LoginPage() { const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(false); + // Forgot password + const [showForgot, setShowForgot] = useState(false); + const [forgotEmail, setForgotEmail] = useState(''); + const [forgotMessage, setForgotMessage] = useState(''); + const [forgotSending, setForgotSending] = useState(false); + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(''); @@ -29,6 +37,23 @@ export function LoginPage() { } }; + const handleForgotPassword = async () => { + if (!forgotEmail) return; + setForgotSending(true); + setForgotMessage(''); + try { + const res = await api.post<{ message: string }>('/auth/forgot-password', { email: forgotEmail }); + setForgotMessage(res.message); + } catch (err) { + setForgotMessage(err instanceof Error ? err.message : 'Failed to send reset link'); + } + setForgotSending(false); + }; + + const handleSocialLogin = () => { + setError('Social login is not yet available. Please use email and password.'); + }; + return (
@@ -40,11 +65,11 @@ export function LoginPage() { {/* Social Login */}
- - @@ -71,7 +96,8 @@ export function LoginPage() {
- Forgot password? +
Log In @@ -84,6 +110,21 @@ export function LoginPage() {

+ + {/* Forgot Password Modal */} + setShowForgot(false)} title="Reset Password" size="sm"> +

Enter your email address and we'll send you a password reset link.

+ {forgotMessage &&

{forgotMessage}

} +
+ setForgotEmail(e.target.value)} placeholder="Enter your email" /> +
+
+ + + {forgotSending ? 'Sending...' : 'Send Reset Link'} + +
+
); } diff --git a/client/src/pages/NotificationsPage.tsx b/client/src/pages/NotificationsPage.tsx index 213ceb2..2ca3c34 100644 --- a/client/src/pages/NotificationsPage.tsx +++ b/client/src/pages/NotificationsPage.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; import { Bell, Check, Heart, Star, MessageSquare, Tag } from 'lucide-react'; import { Button } from '../components/ui/Button'; import { api } from '../api/client'; @@ -24,6 +25,7 @@ const iconColorMap: Record = { }; export function NotificationsPage() { + const navigate = useNavigate(); const [notifications, setNotifications] = useState([]); const [loading, setLoading] = useState(true); @@ -41,6 +43,17 @@ export function NotificationsPage() { fetchNotifications(); }; + const handleNotificationClick = (notif: Notification) => { + if (notif.type === 'NEW_OFFER' || notif.type === 'OFFER_ACCEPTED' || notif.type === 'OFFER_DECLINED') { + navigate('/dashboard/offers'); + } else if (notif.type === 'NEW_MESSAGE') { + navigate('/dashboard/messages', { state: { conversationId: (notif.data as { conversationId?: string })?.conversationId } }); + } else if (notif.type === 'ITEM_FAVORITED' || notif.type === 'ITEM_SOLD') { + const listingId = (notif.data as { listingId?: string })?.listingId; + if (listingId) navigate(`/listings/${listingId}`); + } + }; + if (loading) return
Loading notifications...
; return ( @@ -61,7 +74,8 @@ export function NotificationsPage() { const colorClass = iconColorMap[notif.type] || 'text-gray-500 bg-gray-50'; return ( -
+
{(notif.type === 'NEW_OFFER' || notif.type === 'OFFER_ACCEPTED') && ( - + )} {!notif.isRead &&
} -
+ ); })} diff --git a/client/src/pages/ProductDetailPage.tsx b/client/src/pages/ProductDetailPage.tsx index 8fcc1af..430b930 100644 --- a/client/src/pages/ProductDetailPage.tsx +++ b/client/src/pages/ProductDetailPage.tsx @@ -225,8 +225,10 @@ export function ProductDetailPage() {
- - + +
diff --git a/client/src/pages/SettingsPage.tsx b/client/src/pages/SettingsPage.tsx index 73bd749..06ca49e 100644 --- a/client/src/pages/SettingsPage.tsx +++ b/client/src/pages/SettingsPage.tsx @@ -1,10 +1,14 @@ import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; import { Shield, Lock, Eye, Trash2 } from 'lucide-react'; import { Card } from '../components/ui/Card'; import { Toggle } from '../components/ui/Toggle'; import { Button } from '../components/ui/Button'; import { GradientButton } from '../components/ui/GradientButton'; +import { Modal } from '../components/ui/Modal'; +import { Input } from '../components/ui/Input'; import { api } from '../api/client'; +import { useAuth } from '../context/AuthContext'; interface Settings { showOnline: boolean; @@ -14,7 +18,16 @@ interface Settings { marketingEmail: boolean; } +interface Session { + id: string; + userAgent: string | null; + ipAddress: string | null; + createdAt: string; +} + export function SettingsPage() { + const navigate = useNavigate(); + const { logout } = useAuth(); const [settings, setSettings] = useState({ showOnline: true, showRating: true, @@ -25,6 +38,27 @@ export function SettingsPage() { const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); + // Password modal + const [showPassword, setShowPassword] = useState(false); + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [passwordError, setPasswordError] = useState(''); + const [passwordSuccess, setPasswordSuccess] = useState(''); + const [changingPassword, setChangingPassword] = useState(false); + + // Sessions modal + const [showSessions, setShowSessions] = useState(false); + const [sessions, setSessions] = useState([]); + + // Delete account modal + const [showDelete, setShowDelete] = useState(false); + const [deletePassword, setDeletePassword] = useState(''); + const [deleteError, setDeleteError] = useState(''); + const [deleting, setDeleting] = useState(false); + + // Blocked users modal + const [showBlocked, setShowBlocked] = useState(false); + useEffect(() => { api.get('/users/settings') .then(setSettings) @@ -50,6 +84,46 @@ export function SettingsPage() { setSettings(prev => ({ ...prev, [key]: value })); }; + const handleChangePassword = async () => { + setPasswordError(''); + setPasswordSuccess(''); + if (newPassword.length < 8) { + setPasswordError('New password must be at least 8 characters'); + return; + } + setChangingPassword(true); + try { + await api.put('/users/password', { currentPassword, newPassword }); + setPasswordSuccess('Password updated successfully'); + setCurrentPassword(''); + setNewPassword(''); + } catch (err) { + setPasswordError(err instanceof Error ? err.message : 'Failed to change password'); + } + setChangingPassword(false); + }; + + const handleViewSessions = async () => { + try { + const data = await api.get('/users/sessions'); + setSessions(data); + setShowSessions(true); + } catch {} + }; + + const handleDeleteAccount = async () => { + setDeleteError(''); + setDeleting(true); + try { + await api.delete('/users/account', { password: deletePassword }); + logout(); + navigate('/'); + } catch (err) { + setDeleteError(err instanceof Error ? err.message : 'Failed to delete account'); + } + setDeleting(false); + }; + if (loading) return
Loading settings...
; return ( @@ -74,7 +148,7 @@ export function SettingsPage() {
Blocked Users
Manage your blocked users list
- + @@ -93,14 +167,14 @@ export function SettingsPage() {
Password Reset
Change your account password
- +
Login Activity
View your recent login activity
- +
@@ -120,26 +194,81 @@ export function SettingsPage() {
Privacy Policy
Read our privacy policy
- + View
Terms of Service
Read our terms of service
- + View
- {saving ? 'Saving...' : 'Save Settings'}
+ + {/* Change Password Modal */} + setShowPassword(false)} title="Change Password" size="sm"> + {passwordError &&

{passwordError}

} + {passwordSuccess &&

{passwordSuccess}

} +
+ setCurrentPassword(e.target.value)} placeholder="Enter current password" /> + setNewPassword(e.target.value)} placeholder="Enter new password (min 8 chars)" /> +
+
+ + + {changingPassword ? 'Changing...' : 'Change Password'} + +
+
+ + {/* Login Activity Modal */} + setShowSessions(false)} title="Login Activity" size="md"> + {sessions.length === 0 ? ( +

No sessions found.

+ ) : ( +
+ {sessions.map(s => ( +
+

{s.userAgent || 'Unknown device'}

+
+ IP: {s.ipAddress || 'Unknown'} + {new Date(s.createdAt).toLocaleString()} +
+
+ ))} +
+ )} +
+ + {/* Delete Account Modal */} + setShowDelete(false)} title="Delete Account" size="sm"> +

This action cannot be undone. Enter your password to confirm.

+ {deleteError &&

{deleteError}

} +
+ setDeletePassword(e.target.value)} placeholder="Enter your password" /> +
+
+ + +
+
+ + {/* Blocked Users Modal */} + setShowBlocked(false)} title="Blocked Users" size="sm"> +

You can block users from their profile page or listing page. Blocked users cannot see your listings or message you.

+
); } diff --git a/client/src/pages/SignUpPage.tsx b/client/src/pages/SignUpPage.tsx index 773de56..e310a2e 100644 --- a/client/src/pages/SignUpPage.tsx +++ b/client/src/pages/SignUpPage.tsx @@ -35,6 +35,10 @@ export function SignUpPage() { } }; + const handleSocialSignup = () => { + setError('Social sign up is not yet available. Please use email and password.'); + }; + return (
@@ -45,11 +49,11 @@ export function SignUpPage() {
- - @@ -79,7 +83,7 @@ export function SignUpPage() { icon={} required /> Sign Up diff --git a/client/src/pages/StaticPages.tsx b/client/src/pages/StaticPages.tsx new file mode 100644 index 0000000..ec77696 --- /dev/null +++ b/client/src/pages/StaticPages.tsx @@ -0,0 +1,147 @@ +import { Card } from '../components/ui/Card'; + +export function AboutPage() { + return ( +
+ +

About Us

+
+

Marketplace is a peer-to-peer platform connecting buyers and sellers of second-hand goods. Our mission is to make buying and selling pre-loved items simple, safe, and enjoyable.

+

Founded in 2024, we believe in giving items a second life. Whether you're decluttering your home or hunting for a great deal, Marketplace is the place to be.

+

Our Values

+
    +
  • Trust & Safety — We verify users and provide secure messaging and payment options.
  • +
  • Sustainability — Every item resold is one less in a landfill.
  • +
  • Community — We foster a friendly community of buyers and sellers.
  • +
+
+
+
+ ); +} + +export function PrivacyPage() { + return ( +
+ +

Privacy Policy

+
+

Last updated: February 2026

+

1. Information We Collect

+

We collect information you provide directly: name, email, phone number, location, and profile information. We also collect usage data, device information, and cookies.

+

2. How We Use Your Information

+

We use your information to provide and improve our services, process transactions, communicate with you, and ensure platform safety.

+

3. Information Sharing

+

We do not sell your personal information. We share data only with service providers who help us operate the platform, or when required by law.

+

4. Data Security

+

We implement industry-standard security measures to protect your data, including encryption, secure servers, and regular security audits.

+

5. Your Rights

+

You can access, update, or delete your personal information at any time through your account settings. You may also contact us to exercise your data rights.

+

6. Contact Us

+

For privacy-related inquiries, email us at privacy@marketplace.com.

+
+
+
+ ); +} + +export function TermsPage() { + return ( +
+ +

Terms of Service

+
+

Last updated: February 2026

+

1. Acceptance of Terms

+

By using Marketplace, you agree to these Terms of Service. If you do not agree, please do not use our platform.

+

2. User Accounts

+

You must provide accurate information when creating an account. You are responsible for maintaining the security of your account and password.

+

3. Listings & Transactions

+

Sellers are responsible for the accuracy of their listings. All transactions are between buyers and sellers directly. Marketplace facilitates but does not guarantee transactions.

+

4. Prohibited Items

+

You may not list illegal items, weapons, drugs, stolen property, or any items that violate local laws or our community guidelines.

+

5. User Conduct

+

Users must treat each other with respect. Harassment, fraud, and spam are strictly prohibited and may result in account termination.

+

6. Limitation of Liability

+

Marketplace is provided "as is" without warranties. We are not liable for losses arising from transactions between users.

+
+
+
+ ); +} + +export function HelpPage() { + return ( +
+ +

Help & Support

+
+

Frequently Asked Questions

+
+
+

How do I create a listing?

+

Click "Sell" in the navigation bar, fill in your item details, upload photos, and submit. Your listing will be visible to all users.

+
+
+

How do I make an offer?

+

Visit a listing page and click "Make Offer". Enter your offer amount and an optional message to the seller.

+
+
+

How do I message a seller?

+

Click the "Message" button on any listing page to start a conversation with the seller.

+
+
+

How do I delete my account?

+

Go to Dashboard > Settings > Delete Account. You'll need to confirm with your password.

+
+
+

How do I report a listing?

+

Click the "Report" button on any listing page. Our team will review the report promptly.

+
+
+
+
+
+ ); +} + +export function ContactPage() { + return ( +
+ +

Contact Us

+
+

We'd love to hear from you. Whether you have a question, feedback, or need assistance, our team is here to help.

+
+

Email: support@marketplace.com

+

Response Time: We aim to respond within 24 hours.

+
+

For urgent matters related to account security, please include "URGENT" in your email subject line.

+
+
+
+ ); +} + +export function ReturnsPage() { + return ( +
+ +

Returns & Conditions

+
+

As a peer-to-peer marketplace, return policies are set by individual sellers. However, we have general guidelines to ensure fair transactions.

+

Item Conditions

+
    +
  • New — Unused, in original packaging
  • +
  • Like New — Barely used, no visible wear
  • +
  • Gently Used — Minor signs of use, fully functional
  • +
  • Used — Normal wear and tear, fully functional
  • +
  • Fair — Visible wear, may have cosmetic issues
  • +
+

Disputes

+

If an item significantly differs from its description, contact the seller first. If you cannot resolve the issue, reach out to our support team.

+
+
+
+ ); +} diff --git a/client/src/router.tsx b/client/src/router.tsx index ea8236d..b0e297a 100644 --- a/client/src/router.tsx +++ b/client/src/router.tsx @@ -16,6 +16,7 @@ import { SoldItemsPage } from './pages/SoldItemsPage'; import { SettingsPage } from './pages/SettingsPage'; import { MyListingsPage } from './pages/MyListingsPage'; import { SavedItemsPage } from './pages/SavedItemsPage'; +import { AboutPage, PrivacyPage, TermsPage, HelpPage, ContactPage, ReturnsPage } from './pages/StaticPages'; export const router = createBrowserRouter([ { @@ -29,6 +30,12 @@ export const router = createBrowserRouter([ { path: 'listings/:id', element: }, { path: 'profile/create', element: }, { path: 'profile/edit', element: }, + { path: 'about', element: }, + { path: 'privacy', element: }, + { path: 'terms', element: }, + { path: 'help', element: }, + { path: 'contact', element: }, + { path: 'returns', element: }, { path: 'dashboard', element: ,