open-navigator / web_app /src /pages /Profile.tsx
jcbowyer's picture
Clean HuggingFace deployment without binary files
e59d91d
Raw
History Blame Contribute Delete
16 kB
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Tab } from '@headlessui/react'
import api from '../lib/api'
import { formatCityState } from '../utils/formatters'
import {
UserIcon,
BuildingLibraryIcon,
HeartIcon,
Cog6ToothIcon
} from '@heroicons/react/24/outline'
import { useAuth } from '../contexts/AuthContext'
import SocialStats from '../components/SocialStats'
import FollowButton from '../components/FollowButton'
interface Leader {
id: number
name: string
slug: string
title?: string
photo_url?: string
office?: string
city?: string
state?: string
follower_count: number
is_verified: boolean
}
interface Organization {
master_org_id: string
name: string
description?: string
logo_url?: string
org_type?: string
city?: string
state?: string
follower_count: number
is_verified?: boolean
}
interface Tag {
tag_id: string
label?: string
description?: string
category?: string
breadcrumb?: string
icon?: string
follower_count: number
}
function classNames(...classes: string[]) {
return classes.filter(Boolean).join(' ')
}
export default function Profile() {
const { user } = useAuth()
const [selectedTab, setSelectedTab] = useState(0)
const { data: leadersFollowing } = useQuery<Leader[]>({
queryKey: ['following', 'leaders'],
queryFn: async () => {
const response = await api.get('/social/following/leaders')
return response.data
},
enabled: !!user
})
const { data: orgsFollowing } = useQuery<Organization[]>({
queryKey: ['following', 'organizations'],
queryFn: async () => {
const response = await api.get('/social/following/organizations')
return response.data
},
enabled: !!user
})
const { data: tagsFollowing } = useQuery<Tag[]>({
queryKey: ['following', 'tags'],
queryFn: async () => {
const response = await api.get('/social/following/tags')
return response.data
},
enabled: !!user
})
if (!user) {
return (
<div className="min-h-screen flex items-center justify-center" style={{ backgroundColor: '#F1F5F9' }}>
<div className="text-center">
<h2 className="text-2xl font-bold mb-4" style={{ color: '#354F52' }}>
Please sign in
</h2>
<p className="text-gray-600">You need to be signed in to view your profile</p>
</div>
</div>
)
}
return (
<div className="min-h-screen p-8" style={{ backgroundColor: '#F1F5F9' }}>
<div className="max-w-6xl mx-auto">
{/* Profile Header */}
<div className="bg-white rounded-lg shadow-md p-8 mb-6">
<div className="flex items-start gap-6">
{/* Avatar */}
<div className="flex-shrink-0">
{user.avatar_url ? (
<img
src={user.avatar_url}
alt={user.full_name || user.email}
className="h-24 w-24 rounded-full object-cover border-4 border-gray-200"
/>
) : (
<div
className="h-24 w-24 rounded-full flex items-center justify-center text-white text-3xl font-bold"
style={{ backgroundColor: '#354F52' }}
>
{(user.full_name || user.email).charAt(0).toUpperCase()}
</div>
)}
</div>
{/* Profile Info */}
<div className="flex-1">
<h1 className="text-3xl font-bold mb-2" style={{ color: '#354F52' }}>
{user.full_name || 'Community Member'}
</h1>
<p className="text-gray-600 mb-4">
{user.email}
</p>
{user.city && user.state && (
<p className="text-sm text-gray-500 mb-4">
📍 {user.city}, {user.state}
</p>
)}
{/* Social Stats */}
<SocialStats showBreakdown={true} clickable={false} />
</div>
{/* Edit Profile Button */}
<a
href="/settings"
className="flex items-center gap-2 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
<Cog6ToothIcon className="h-5 w-5" />
<span>Edit Profile</span>
</a>
</div>
</div>
{/* Following Tabs */}
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-2xl font-bold mb-6" style={{ color: '#354F52' }}>
Following
</h2>
<Tab.Group selectedIndex={selectedTab} onChange={setSelectedTab}>
<Tab.List className="flex gap-2 border-b border-gray-200 mb-6">
<Tab
className={({ selected }) =>
classNames(
'px-4 py-2 font-medium text-sm border-b-2 transition-colors',
selected
? 'border-[#354F52] text-[#354F52]'
: 'border-transparent text-gray-500 hover:text-gray-700'
)
}
>
<div className="flex items-center gap-2">
<UserIcon className="h-5 w-5" />
Leaders ({leadersFollowing?.length || 0})
</div>
</Tab>
<Tab
className={({ selected }) =>
classNames(
'px-4 py-2 font-medium text-sm border-b-2 transition-colors',
selected
? 'border-[#354F52] text-[#354F52]'
: 'border-transparent text-gray-500 hover:text-gray-700'
)
}
>
<div className="flex items-center gap-2">
<BuildingLibraryIcon className="h-5 w-5" />
Charities ({orgsFollowing?.length || 0})
</div>
</Tab>
<Tab
className={({ selected }) =>
classNames(
'px-4 py-2 font-medium text-sm border-b-2 transition-colors',
selected
? 'border-[#354F52] text-[#354F52]'
: 'border-transparent text-gray-500 hover:text-gray-700'
)
}
>
<div className="flex items-center gap-2">
<HeartIcon className="h-5 w-5" />
Tags ({tagsFollowing?.length || 0})
</div>
</Tab>
</Tab.List>
<Tab.Panels>
{/* Leaders */}
<Tab.Panel>
{!leadersFollowing || leadersFollowing.length === 0 ? (
<div className="text-center py-12">
<UserIcon className="h-16 w-16 mx-auto text-gray-300 mb-4" />
<p className="text-gray-500">You're not following any leaders yet</p>
<a
href="/people"
className="inline-block mt-4 text-blue-600 hover:text-blue-700 font-medium"
>
Find leaders to follow →
</a>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{leadersFollowing.map((leader) => (
<div
key={leader.id}
className="flex items-start gap-4 p-4 border border-gray-200 rounded-lg hover:border-gray-300 transition-colors"
>
{leader.photo_url ? (
<img
src={leader.photo_url}
alt={leader.name}
className="h-16 w-16 rounded-full object-cover"
/>
) : (
<div
className="h-16 w-16 rounded-full flex items-center justify-center text-white text-xl font-bold"
style={{ backgroundColor: '#354F52' }}
>
{leader.name.charAt(0)}
</div>
)}
<div className="flex-1">
<h3 className="font-semibold" style={{ color: '#354F52' }}>
{leader.name}
{leader.is_verified && <span className="ml-1 text-blue-500"></span>}
</h3>
{leader.title && <p className="text-sm text-gray-600">{leader.title}</p>}
{leader.office && <p className="text-sm text-gray-500">{leader.office}</p>}
{leader.city && leader.state && (
<p className="text-xs text-gray-400 mt-1">
{formatCityState(leader.city, leader.state)}
</p>
)}
<p className="text-xs text-gray-500 mt-1">
{leader.follower_count.toLocaleString()} followers
</p>
</div>
<FollowButton
type="leader"
id={leader.id}
initialFollowing={true}
initialCount={leader.follower_count}
showCount={false}
compact={true}
/>
</div>
))}
</div>
)}
</Tab.Panel>
{/* Organizations */}
<Tab.Panel>
{!orgsFollowing || orgsFollowing.length === 0 ? (
<div className="text-center py-12">
<BuildingLibraryIcon className="h-16 w-16 mx-auto text-gray-300 mb-4" />
<p className="text-gray-500">You're not following any charities yet</p>
<a
href="/nonprofits"
className="inline-block mt-4 text-blue-600 hover:text-blue-700 font-medium"
>
Find charities to follow →
</a>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{orgsFollowing.map((org) => (
<div
key={org.master_org_id}
className="flex items-start gap-4 p-4 border border-gray-200 rounded-lg hover:border-gray-300 transition-colors"
>
{org.logo_url ? (
<img
src={org.logo_url}
alt={org.name}
className="h-16 w-16 rounded object-contain"
/>
) : (
<div
className="h-16 w-16 rounded flex items-center justify-center text-white text-xl font-bold"
style={{ backgroundColor: '#52796F' }}
>
{org.name.charAt(0)}
</div>
)}
<div className="flex-1">
<h3 className="font-semibold" style={{ color: '#354F52' }}>
{org.name}
{org.is_verified && <span className="ml-1 text-blue-500"></span>}
</h3>
{org.org_type && (
<span className="inline-block px-2 py-0.5 text-xs rounded-full bg-green-100 text-green-800 capitalize mb-1">
{org.org_type}
</span>
)}
{org.description && (
<p className="text-sm text-gray-600 line-clamp-2">{org.description}</p>
)}
{org.city && org.state && (
<p className="text-xs text-gray-400 mt-1">
{formatCityState(org.city, org.state)}
</p>
)}
<p className="text-xs text-gray-500 mt-1">
{org.follower_count.toLocaleString()} followers
</p>
</div>
<FollowButton
type="organization"
id={org.master_org_id}
initialFollowing={true}
initialCount={org.follower_count}
showCount={false}
compact={true}
/>
</div>
))}
</div>
)}
</Tab.Panel>
{/* Tags */}
<Tab.Panel>
{!tagsFollowing || tagsFollowing.length === 0 ? (
<div className="text-center py-12">
<HeartIcon className="h-16 w-16 mx-auto text-gray-300 mb-4" />
<p className="text-gray-500">You're not following any tags yet</p>
<a
href="/"
className="inline-block mt-4 text-blue-600 hover:text-blue-700 font-medium"
>
Explore tags →
</a>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{tagsFollowing.map((tag) => (
<div
key={tag.tag_id}
className="p-4 border border-gray-200 rounded-lg hover:border-gray-300 transition-colors"
>
<div className="flex items-center gap-3 mb-2">
<div className="h-8 w-8 rounded-full" style={{ backgroundColor: '#354F52' }} />
<h3 className="font-semibold flex-1" style={{ color: '#354F52' }}>
{tag.label || tag.tag_id}
</h3>
</div>
{tag.breadcrumb && (
<p className="text-xs text-gray-400 mb-2">{tag.breadcrumb}</p>
)}
{tag.description && (
<p className="text-sm text-gray-600 mb-3">{tag.description}</p>
)}
{tag.category && (
<span className="inline-block px-2 py-0.5 text-xs rounded-full bg-purple-100 text-purple-800 capitalize mb-2">
{tag.category}
</span>
)}
<div className="flex items-center justify-between">
<p className="text-xs text-gray-500">
{tag.follower_count.toLocaleString()} followers
</p>
<FollowButton
type="tag"
id={tag.tag_id}
initialFollowing={true}
initialCount={tag.follower_count}
showCount={false}
compact={true}
/>
</div>
</div>
))}
</div>
)}
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
</div>
</div>
)
}