fix(mobile): stacked Add Friend input, responsive tab bar, mobile header
This commit is contained in:
@@ -13,7 +13,11 @@ import { api } from '../../api/client';
|
|||||||
|
|
||||||
type Tab = 'online' | 'all' | 'pending' | 'add';
|
type Tab = 'online' | 'all' | 'pending' | 'add';
|
||||||
|
|
||||||
export function FriendsPage() {
|
interface FriendsPageProps {
|
||||||
|
mobile?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FriendsPage({ mobile }: FriendsPageProps) {
|
||||||
const [activeTab, setActiveTab] = useState<Tab>('online');
|
const [activeTab, setActiveTab] = useState<Tab>('online');
|
||||||
const [addUsername, setAddUsername] = useState('');
|
const [addUsername, setAddUsername] = useState('');
|
||||||
const [addStatus, setAddStatus] = useState<{ type: 'success' | 'error', message: string } | null>(null);
|
const [addStatus, setAddStatus] = useState<{ type: 'success' | 'error', message: string } | null>(null);
|
||||||
@@ -168,44 +172,78 @@ export function FriendsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const popMobileScreen = useUIStore((s) => s.popMobileScreen);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 flex flex-col bg-surface-chat h-full">
|
<div className="flex-1 flex flex-col bg-surface-chat h-full">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="h-12 px-4 flex items-center shadow-header flex-shrink-0 z-10 bg-surface-chat">
|
{mobile ? (
|
||||||
<div className="flex items-center gap-2 mr-4">
|
<div className="h-12 px-3 flex items-center gap-2 border-b border-border-soft flex-shrink-0 z-10 bg-surface-base">
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" className="text-txt-tertiary">
|
<button onClick={popMobileScreen} className="w-8 h-8 flex items-center justify-center text-txt-secondary hover:text-txt-primary">
|
||||||
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
</svg>
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
<span className="font-bold text-txt-primary">Friends</span>
|
</svg>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-[1px] h-6 bg-surface-elevated mx-2" />
|
|
||||||
|
|
||||||
<div className="flex items-center gap-4 ml-2">
|
|
||||||
<TabButton active={activeTab === 'online'} onClick={() => setActiveTab('online')}>Online</TabButton>
|
|
||||||
<TabButton active={activeTab === 'all'} onClick={() => setActiveTab('all')}>All</TabButton>
|
|
||||||
<TabButton active={activeTab === 'pending'} onClick={() => setActiveTab('pending')}>
|
|
||||||
Pending
|
|
||||||
{(pendingIncoming.length > 0) && (
|
|
||||||
<span className="ml-2 px-1.5 py-0.5 bg-accent-rose text-white text-[10px] rounded-full leading-none">
|
|
||||||
{pendingIncoming.length}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</TabButton>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('add')}
|
|
||||||
className={`px-2 py-0.5 rounded text-[14px] font-medium transition-all ${
|
|
||||||
activeTab === 'add' ? 'text-status-online bg-transparent' : 'bg-status-online text-[#13131a] hover:bg-status-online/90'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Add Friend
|
|
||||||
</button>
|
</button>
|
||||||
|
<span className="font-semibold text-sm text-txt-primary">Friends</span>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-12 px-4 flex items-center shadow-header flex-shrink-0 z-10 bg-surface-chat">
|
||||||
|
<div className="flex items-center gap-2 mr-4">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" className="text-txt-tertiary">
|
||||||
|
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
|
||||||
|
</svg>
|
||||||
|
<span className="font-bold text-txt-primary">Friends</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-[1px] h-6 bg-surface-elevated mx-2" />
|
||||||
|
<div className="flex items-center gap-4 ml-2">
|
||||||
|
<TabButton active={activeTab === 'online'} onClick={() => setActiveTab('online')}>Online</TabButton>
|
||||||
|
<TabButton active={activeTab === 'all'} onClick={() => setActiveTab('all')}>All</TabButton>
|
||||||
|
<TabButton active={activeTab === 'pending'} onClick={() => setActiveTab('pending')}>
|
||||||
|
Pending
|
||||||
|
{(pendingIncoming.length > 0) && (
|
||||||
|
<span className="ml-2 px-1.5 py-0.5 bg-accent-rose text-white text-[10px] rounded-full leading-none">
|
||||||
|
{pendingIncoming.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</TabButton>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('add')}
|
||||||
|
className={`px-2 py-0.5 rounded text-[14px] font-medium transition-all ${
|
||||||
|
activeTab === 'add' ? 'text-status-online bg-transparent' : 'bg-status-online text-[#13131a] hover:bg-status-online/90'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Add Friend
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="ml-auto flex items-center gap-1">
|
||||||
|
<MemberListToggleButton />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="ml-auto flex items-center gap-1">
|
{/* Mobile tab bar */}
|
||||||
<MemberListToggleButton />
|
{mobile && (
|
||||||
|
<div className="flex border-b border-border-soft">
|
||||||
|
{(['online', 'all', 'pending', 'add'] as Tab[]).map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab}
|
||||||
|
onClick={() => setActiveTab(tab)}
|
||||||
|
className={`flex-1 py-2.5 text-sm font-medium text-center border-b-2 transition-colors ${
|
||||||
|
activeTab === tab
|
||||||
|
? 'border-accent-primary text-txt-primary'
|
||||||
|
: 'border-transparent text-txt-secondary hover:text-txt-primary'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tab === 'add' ? 'Add Friend' : tab.charAt(0).toUpperCase() + tab.slice(1)}
|
||||||
|
{tab === 'pending' && pendingIncoming.length > 0 && (
|
||||||
|
<span className="ml-1.5 px-1.5 py-0.5 text-xs bg-notification text-white rounded-full">
|
||||||
|
{pendingIncoming.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{renderTabContent()}
|
{renderTabContent()}
|
||||||
</div>
|
</div>
|
||||||
@@ -262,10 +300,10 @@ function AddFriendTab({
|
|||||||
<div className="p-6 pb-4">
|
<div className="p-6 pb-4">
|
||||||
<h2 className="text-base font-bold text-txt-primary uppercase mb-2">Add Friend</h2>
|
<h2 className="text-base font-bold text-txt-primary uppercase mb-2">Add Friend</h2>
|
||||||
<p className="text-sm text-txt-tertiary mb-4">You can add friends with their Backspace username.</p>
|
<p className="text-sm text-txt-tertiary mb-4">You can add friends with their Backspace username.</p>
|
||||||
<form onSubmit={onSubmit} className="relative mb-4">
|
<form onSubmit={onSubmit} className="flex flex-col gap-2 mb-4">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="You can add a friend with their username"
|
placeholder="Enter a username..."
|
||||||
value={addUsername}
|
value={addUsername}
|
||||||
onChange={(e) => setAddUsername(e.target.value)}
|
onChange={(e) => setAddUsername(e.target.value)}
|
||||||
className="input-search w-full px-4 py-3 rounded-lg"
|
className="input-search w-full px-4 py-3 rounded-lg"
|
||||||
@@ -273,7 +311,7 @@ function AddFriendTab({
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!addUsername.trim() || isLoading}
|
disabled={!addUsername.trim() || isLoading}
|
||||||
className="absolute right-2 top-1/2 -translate-y-1/2 px-4 py-1.5 bg-accent-primary hover:bg-accent-primary-hover disabled:opacity-50 disabled:bg-accent-primary text-white text-sm font-medium rounded transition-colors"
|
className="w-full py-2.5 rounded-lg bg-accent-primary hover:bg-accent-primary-hover disabled:opacity-50 disabled:bg-accent-primary text-white text-sm font-medium transition-colors"
|
||||||
>
|
>
|
||||||
Send Friend Request
|
Send Friend Request
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const PlaceholderScreen = ({ label }: { label: string }) => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
import { MobileSpacesScreen } from './MobileSpacesScreen';
|
import { MobileSpacesScreen } from './MobileSpacesScreen';
|
||||||
|
import { FriendsPage } from '../chat/FriendsPage';
|
||||||
// These will be replaced with real imports as each task is completed:
|
// These will be replaced with real imports as each task is completed:
|
||||||
import { MobileDmsScreen } from './MobileDmsScreen';
|
import { MobileDmsScreen } from './MobileDmsScreen';
|
||||||
import { MobileYouScreen } from './MobileYouScreen';
|
import { MobileYouScreen } from './MobileYouScreen';
|
||||||
@@ -25,7 +26,7 @@ const MobileVoiceFullScreen = () => <PlaceholderScreen label="Voice" />;
|
|||||||
|
|
||||||
const screenMap: Record<string, (params?: Record<string, string>) => React.ReactNode> = {
|
const screenMap: Record<string, (params?: Record<string, string>) => React.ReactNode> = {
|
||||||
'channel-chat': (params) => <MobileChatScreen params={params} />,
|
'channel-chat': (params) => <MobileChatScreen params={params} />,
|
||||||
'friends': () => <PlaceholderScreen label="Friends" />,
|
'friends': () => <FriendsPage mobile />,
|
||||||
'settings': () => <MobileSettingsScreen />,
|
'settings': () => <MobileSettingsScreen />,
|
||||||
'settings-account': () => <MobileSettingsScreen initialPanel="account" />,
|
'settings-account': () => <MobileSettingsScreen initialPanel="account" />,
|
||||||
'settings-voice': () => <MobileSettingsScreen initialPanel="voice" />,
|
'settings-voice': () => <MobileSettingsScreen initialPanel="voice" />,
|
||||||
|
|||||||
Reference in New Issue
Block a user