Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ import AddAnnouncementPage from './pages/AddAnnouncement';
import EditAnnouncementPage from './pages/EditAnnouncement';
import MessagesPage from './pages/Messages';
import NotificationSettingsPage from './pages/NotificationSettingsPage';
import PublicProfile from './pages/PublicProfile';

// Route configuration for better maintainability
const ROUTES = [
{ path: '/', element: HomePage, index: true },
{ path: '/feed', element: FeedPage },
{ path: '/items/:id', element: ItemDetailPage },
{ path: '/profile', element: ProfilePage },
{ path: '/profile/:userId', element: PublicProfile },
{ path: '/login', element: LoginPage },
{ path: '/signup', element: SignUpPage },
{ path: '/items/new', element: ReportPage },
Expand Down
11 changes: 7 additions & 4 deletions frontend/src/firebase/firestore.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@ export async function getUserPosts(userId) {
const querySnapshot = await getDocs(q);

const posts = [];
querySnapshot.forEach((doc) => {
const itemData = doc.data();
querySnapshot.forEach((docSnapshot) => {
const itemData = docSnapshot.data();

// Only include items that are not resolved (exclude closed posts)
if (itemData.status?.toLowerCase() !== 'resolved') {
posts.push({
id: doc.id,
...itemData
id: docSnapshot.id,
...itemData,
// Normalize: if kind is missing, use status field as fallback
kind: itemData.kind || (itemData.status?.toLowerCase() === 'found' || itemData.status?.toLowerCase() === 'lost' ? itemData.status : undefined)
Comment on lines 20 to +25
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] status?.toLowerCase() is computed multiple times. Cache it once to simplify and avoid repeated calls, e.g., const status = itemData.status?.toLowerCase(); then compare against status in both the filter and normalization.

Copilot uses AI. Check for mistakes.
});
}
});
Expand Down
13 changes: 11 additions & 2 deletions frontend/src/pages/ItemDetail.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,9 +288,18 @@ const ItemDetailPage = () => {
<span className="font-medium">Posted:</span>
{` ${new Date(item.date).toLocaleString()}`}
</p>
<p>
<p className="flex items-center gap-1">
<span className="font-medium">Reporter:</span>
{` ${userInfo?.name || 'Unknown User'}`}
{userInfo?.id ? (
<Link
to={`/profile/${userInfo.id}`}
className="text-emerald-600 hover:text-emerald-700 hover:underline transition-colors font-medium"
>
{userInfo.name}
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If userInfo.id exists but userInfo.name is undefined or empty, this renders an empty link. Preserve the previous fallback by rendering {userInfo?.name || 'Unknown User'} here.

Suggested change
{userInfo.name}
{userInfo?.name || 'Unknown User'}

Copilot uses AI. Check for mistakes.
</Link>
) : (
<span>{userInfo?.name || 'Unknown User'}</span>
)}
</p>
</div>
<div className="mt-5 flex flex-wrap gap-3">
Expand Down
177 changes: 177 additions & 0 deletions frontend/src/pages/PublicProfile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import React, { useState, useEffect } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { doc, getDoc, collection, query, where, onSnapshot, orderBy } from 'firebase/firestore'
import { db } from '../firebase/config'
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card'
import { ArrowLeft } from 'lucide-react'
import ItemCard from '../components/ItemCard'
import { normalizeFirestoreItem } from '../lib/utils'

const PublicProfile = () => {
const { userId } = useParams()
const navigate = useNavigate()
const [userData, setUserData] = useState(null)
const [userPosts, setUserPosts] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)

// Fetch user data
useEffect(() => {
if (!userId) {
setError('No user ID provided')
setLoading(false)
return
}

const fetchUserData = async () => {
try {
const userDocRef = doc(db, 'users', userId)
const userDocSnap = await getDoc(userDocRef)

if (!userDocSnap.exists()) {
setError('User not found')
setLoading(false)
return
}

const data = userDocSnap.data()
setUserData(data)
} catch (err) {
console.error('Error fetching user data:', err)
setError('Failed to load user profile')
}
}

fetchUserData()
}, [userId])

// Fetch user posts with real-time listener (exactly like Feed page)
useEffect(() => {
if (!userId) return

setLoading(true)
setError(null)

try {
const itemsRef = collection(db, 'items')
const userDocRef = doc(db, 'users', userId)
const itemsQuery = query(
itemsRef,
where('postedBy', '==', userDocRef),
orderBy('date', 'desc')
)
Comment on lines +56 to +62
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] This duplicates user-posts query logic that also exists in getUserPosts (with slightly different filtering). Consider centralizing the query/normalization in a shared helper (e.g., userPostsQuery or subscribeToUserPosts) to keep behavior consistent (especially the 'resolved' filter) and reduce divergence.

Copilot uses AI. Check for mistakes.

const unsubscribe = onSnapshot(
itemsQuery,
(snapshot) => {
const fetchedItems = snapshot.docs.map((doc) =>
normalizeFirestoreItem(doc.data() || {}, doc.id)
)
setUserPosts(fetchedItems)
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This includes posts with status 'resolved', whereas getUserPosts excludes them. Align behavior by filtering out resolved items before updating state, e.g., filter on the raw snapshot data or normalized item status.

Suggested change
setUserPosts(fetchedItems)
const filteredItems = fetchedItems.filter(item => item.status !== 'resolved')
setUserPosts(filteredItems)

Copilot uses AI. Check for mistakes.
setLoading(false)
},
(err) => {
console.error('Error fetching user posts:', err)
setError('Failed to load posts')
setLoading(false)
}
)

return () => unsubscribe()
} catch (err) {
console.error('Error setting up posts listener:', err)
setError('Failed to connect to database')
setLoading(false)
}
}, [userId])

if (loading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-emerald-600 mx-auto mb-4"></div>
<p className="text-gray-600">Loading profile...</p>
</div>
</div>
)
}

if (error) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<p className="text-red-700 mb-4">{error}</p>
<button
onClick={() => navigate(-1)}
className="text-emerald-600 hover:text-emerald-700"
>
Go Back
</button>
</div>
</div>
)
}

return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="space-y-6">
{/* Back button */}
<button
onClick={() => navigate(-1)}
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Back
</button>

{/* Profile Header */}
<div className="flex items-center gap-6">
{/* Profile Picture */}
<div className="w-24 h-24 rounded-full overflow-hidden border-4 border-emerald-600 bg-gray-200">
{userData?.profilePic ? (
<img
src={userData.profilePic}
alt={`${userData.name}'s profile`}
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If userData.name is missing, the alt text becomes "undefined's profile". Provide a meaningful fallback, e.g., alt={userData?.name ? ${userData.name}'s profile : 'Profile picture'}.

Suggested change
alt={`${userData.name}'s profile`}
alt={userData?.name ? `${userData.name}'s profile` : 'Profile picture'}

Copilot uses AI. Check for mistakes.
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-gray-300 text-gray-600 text-2xl font-bold">
{(userData?.name || 'U')[0].toUpperCase()}
</div>
)}
</div>

{/* User Info */}
<div>
<h1 className="text-3xl font-bold text-gray-900">{userData?.name || 'User'}</h1>
<p className="text-gray-600 text-left pt-2">
{userPosts.length} {userPosts.length === 1 ? 'post' : 'posts'}
</p>
</div>
</div>

{/* User's Posts */}
<Card>
<CardHeader>
<CardTitle>Posts by {userData?.name || 'this user'}</CardTitle>
</CardHeader>
<CardContent>
{userPosts.length === 0 ? (
<p className="text-gray-500 text-center py-8">No posts yet</p>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{userPosts.map((post) => (
<ItemCard key={post.id} item={post} />
))}
</div>
)}
</CardContent>
</Card>
</div>
</div>
</div>
)
}

export default PublicProfile