Phase 3: Polish & PWA
Backend: - DB: reactions table, invites table, reply_to column on messages - gateway/events.go: added REACTION_ADD, REACTION_REMOVE events - internal/reaction/handlers.go: reaction CRUD with WebSocket broadcast - internal/invite/handlers.go: invite creation, info, join with code - gateway/hub.go: presence tracking with idle detection - gateway/client.go: idle timeout support Frontend - Social: - TypingIndicator: real-time 'user is typing...' display - ReactionBar: emoji reactions on messages with counts - EmojiPicker: searchable emoji grid for reactions - ReplyBar: quoted reply display above messages - MentionPopup: @mention autocomplete with user list Frontend - PWA: - manifest.json: PWA manifest with theme color and icons - sw.js: service worker with cache-first strategy and push support - stores/push.ts: push notification subscription management - InstallPrompt: 'Add to Home Screen' banner Frontend - Mobile: - MobileNav: bottom nav bar for mobile (servers/channels/chat/members) - MobileDrawer: slide-out drawer with server bar + channel list - index.html: PWA meta tags, safe area viewport Frontend - Polish: - ThemeToggle: dark/light mode switch with localStorage persistence - InviteModal: generate invite links with expiry and max uses - JoinServer: /invite/:code join flow - stores/typing.ts: typing indicator state management - stores/presence.ts: real-time presence tracking - tailwind.config.js: darkMode: 'class', light mode color tokens - styles/index.css: light mode CSS variable overrides
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import { useVoiceStore } from '../stores/voice.ts';
|
||||
import { ThemeToggle } from './ThemeToggle.tsx';
|
||||
|
||||
interface MobileNavProps {
|
||||
activeTab: 'servers' | 'channels' | 'chat' | 'members';
|
||||
onTabChange: (tab: 'servers' | 'channels' | 'chat' | 'members') => void;
|
||||
onToggleDrawer: () => void;
|
||||
}
|
||||
|
||||
export function MobileNav({ activeTab, onTabChange, onToggleDrawer }: MobileNavProps) {
|
||||
const isConnected = useVoiceStore((state: any) => state.isConnected);
|
||||
|
||||
const tabs = [
|
||||
{ id: 'servers' as const, label: '[=]', title: 'Servers' },
|
||||
{ id: 'channels' as const, label: '[#]', title: 'Channels' },
|
||||
{ id: 'chat' as const, label: '[_]', title: 'Chat' },
|
||||
{ id: 'members' as const, label: '[@]', title: 'Members' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="md:hidden fixed bottom-0 left-0 right-0 bg-gb-bg-h border-t border-gb-bg-t z-40">
|
||||
<div className="flex items-center justify-around py-2">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => {
|
||||
if (tab.id === 'servers' || tab.id === 'channels') {
|
||||
onToggleDrawer();
|
||||
}
|
||||
onTabChange(tab.id);
|
||||
}}
|
||||
className={`
|
||||
px-3 py-1 text-xs font-mono transition-colors
|
||||
${activeTab === tab.id
|
||||
? 'text-gb-orange'
|
||||
: 'text-gb-fg-f hover:text-gb-fg'
|
||||
}
|
||||
`}
|
||||
title={tab.title}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
{isConnected && (
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-gb-green animate-pulse" title="In voice" />
|
||||
)}
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
{/* Safe area for phones with bottom notch */}
|
||||
<div className="h-[env(safe-area-inset-bottom)]" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user