diff --git a/apps/web/src/components/allies/allies-page.tsx b/apps/web/src/components/allies/allies-page.tsx index 89ed59e..33170d1 100644 --- a/apps/web/src/components/allies/allies-page.tsx +++ b/apps/web/src/components/allies/allies-page.tsx @@ -13,10 +13,12 @@ import { Button } from "@repo/ui/components/button" import { Input } from "@repo/ui/components/input" import { ScrollArea } from "@repo/ui/components/scroll-area" import { Skeleton } from "@repo/ui/components/skeleton" +import { useIsMobile } from "@repo/ui/hooks/use-mobile" import { cn } from "@repo/ui/lib/utils" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { Check, + Menu, MessageCircle, Search, ShieldOff, @@ -28,6 +30,7 @@ import { import { useState } from "react" import { toast } from "sonner" import { UserAvatar } from "@/components/ui/user-avatar" +import { useMobileSidebar } from "@/context/mobile-sidebar-context" import { useBlockedUsers } from "@/hooks/use-blocked-users" import { useCreateDM } from "@/hooks/use-create-dm" import { apiClient } from "@/lib/api-client" @@ -205,6 +208,8 @@ function BlockedUserRow({ export function AlliesPage() { const queryClient = useQueryClient() const createDM = useCreateDM() + const isMobile = useIsMobile() + const { setOpen: openMobileSidebar } = useMobileSidebar() const [tab, setTab] = useState("all") const [search, setSearch] = useState("") const [addUsername, setAddUsername] = useState("") @@ -391,6 +396,15 @@ export function AlliesPage() {
{/* Header */}
+ {isMobile && ( + + )}

Allies

diff --git a/apps/web/src/components/chat/header.tsx b/apps/web/src/components/chat/header.tsx index b7f19a1..b9ae2b0 100644 --- a/apps/web/src/components/chat/header.tsx +++ b/apps/web/src/components/chat/header.tsx @@ -4,8 +4,11 @@ import { TooltipContent, TooltipTrigger, } from "@repo/ui/components/tooltip" -import { Hash, PanelRight, Pin } from "lucide-react" +import { useIsMobile } from "@repo/ui/hooks/use-mobile" +import { useParams } from "@tanstack/react-router" +import { Hash, Menu, PanelRight, Pin } from "lucide-react" import { useRightSidebar } from "@/components/sidebar/right-panel/right-sidebar-context" +import { useMobileSidebar } from "@/context/mobile-sidebar-context" import { HeaderSearch } from "./header-search" export type ChatContext = @@ -27,10 +30,23 @@ export function ChatHeader({ channelId: string onTogglePinnedMessages?: () => void }) { - const { isCollapsed, toggleCollapsed } = useRightSidebar() + const { view, setView, clearView, isCollapsed, toggleCollapsed } = + useRightSidebar() + const isMobile = useIsMobile() + const { setOpen: openMobileSidebar } = useMobileSidebar() + const { guildSlug } = useParams({ strict: false }) return (
+ {isMobile && ( + + )} {context.type === "channel" && ( )} @@ -77,20 +93,46 @@ export function ChatHeader({ Pinned Messages )} - {isCollapsed && context.type === "channel" && ( - - - - - Show Panel - - )} + {context.type === "channel" && + (isMobile ? ( + + + + + Members + + ) : ( + isCollapsed && ( + + + + + Show Panel + + ) + ))}
) diff --git a/apps/web/src/components/onboarding/onboarding-dialog.tsx b/apps/web/src/components/onboarding/onboarding-dialog.tsx index 1b74188..287e2d3 100644 --- a/apps/web/src/components/onboarding/onboarding-dialog.tsx +++ b/apps/web/src/components/onboarding/onboarding-dialog.tsx @@ -152,11 +152,11 @@ export function OnboardingDialog({ open }: { open: boolean }) { if (!channelsRes.ok) return null const channels = await channelsRes.json() - return ( - channels.uncategorized[0]?.id ?? - channels.categories[0]?.channels[0]?.id ?? - null - ) + if (channels.uncategorized[0]?.id) return channels.uncategorized[0].id + for (const cat of channels.categories) { + if (cat.channels[0]?.id) return cat.channels[0].id + } + return null } const handleCreate = async (e: React.FormEvent) => { @@ -477,7 +477,7 @@ export function OnboardingDialog({ open }: { open: boolean }) { setInviteLink(e.target.value)} disabled={loading} diff --git a/apps/web/src/components/sidebar/channel-panel/channel-list.tsx b/apps/web/src/components/sidebar/channel-panel/channel-list.tsx index d47a543..19708ee 100644 --- a/apps/web/src/components/sidebar/channel-panel/channel-list.tsx +++ b/apps/web/src/components/sidebar/channel-panel/channel-list.tsx @@ -38,6 +38,7 @@ import { } from "lucide-react" import { AnimatePresence, motion } from "motion/react" import { useCallback, useState } from "react" +import { useMobileSidebar } from "@/context/mobile-sidebar-context" import { useUnread } from "@/context/unread-context" import { apiClient } from "@/lib/api-client" import type { Channel, ListChannelsResponse } from "@/lib/api-types" @@ -109,6 +110,7 @@ export function ChannelList() { const { guildSlug, channelId: activeChannelId } = useParams({ strict: false }) const navigate = useNavigate() const queryClient = useQueryClient() + const { setOpen: closeMobileSidebar } = useMobileSidebar() const { data, isPending } = useQuery({ queryKey: ["channels", guildSlug], @@ -386,7 +388,7 @@ export function ChannelList() { active={activeChannelId === ch.id} canManage={canManage} canDelete={canDelete} - onClick={() => + onClick={() => { navigate({ to: "/$guildSlug/$channelId", params: { @@ -394,7 +396,8 @@ export function ChannelList() { channelId: ch.id, }, }) - } + closeMobileSidebar(false) + }} /> ))}
@@ -417,12 +420,13 @@ export function ChannelList() { activeChannelId={activeChannelId} canManage={canManage} canDelete={canDelete} - onChannelClick={(channelId) => + onChannelClick={(channelId) => { navigate({ to: "/$guildSlug/$channelId", params: { guildSlug: guildSlug as string, channelId }, }) - } + closeMobileSidebar(false) + }} /> ))} diff --git a/apps/web/src/components/sidebar/dm-panel/dm-list.tsx b/apps/web/src/components/sidebar/dm-panel/dm-list.tsx index 5fb2f78..6c16382 100644 --- a/apps/web/src/components/sidebar/dm-panel/dm-list.tsx +++ b/apps/web/src/components/sidebar/dm-panel/dm-list.tsx @@ -8,6 +8,7 @@ import { import { cn } from "@repo/ui/lib/utils" import { useQuery } from "@tanstack/react-query" import { useNavigate, useParams } from "@tanstack/react-router" +import { useMobileSidebar } from "@/context/mobile-sidebar-context" import { useUnread } from "@/context/unread-context" import { apiClient } from "@/lib/api-client" import type { DMember } from "@/lib/api-types" @@ -16,6 +17,7 @@ import { UserAvatar } from "../../ui/user-avatar" export function DMList() { const navigate = useNavigate() const { dmId } = useParams({ strict: false }) + const { setOpen: closeMobileSidebar } = useMobileSidebar() const { data } = useQuery({ queryKey: ["dms"], @@ -52,9 +54,10 @@ export function DMList() { lastMessageAuthor={dm.lastMessage?.author.name ?? null} isGroupDM={dm.type === "group_dm"} active={dmId === dm.id} - onClick={() => + onClick={() => { navigate({ to: "/dms/$dmId", params: { dmId: dm.id } }) - } + closeMobileSidebar(false) + }} /> ) })} diff --git a/apps/web/src/components/sidebar/dm-panel/dm-panel.tsx b/apps/web/src/components/sidebar/dm-panel/dm-panel.tsx index 06626a2..07b6cb4 100644 --- a/apps/web/src/components/sidebar/dm-panel/dm-panel.tsx +++ b/apps/web/src/components/sidebar/dm-panel/dm-panel.tsx @@ -4,6 +4,7 @@ import { cn } from "@repo/ui/lib/utils" import { useNavigate, useParams } from "@tanstack/react-router" import { Plus, Users } from "lucide-react" import { useState } from "react" +import { useMobileSidebar } from "@/context/mobile-sidebar-context" import { SearchBar } from "../channel-panel/search-bar" import { UserBar } from "../channel-panel/user-bar" import { DMList } from "./dm-list" @@ -12,6 +13,7 @@ import { NewDMDialog } from "./new-dm-dialog" export function DMPanel() { const navigate = useNavigate() const { dmId } = useParams({ strict: false }) + const { setOpen: closeMobileSidebar } = useMobileSidebar() const [newDMOpen, setNewDMOpen] = useState(false) return ( @@ -20,7 +22,10 @@ export function DMPanel() {
+ +
+ + )} + + {step === "create" && ( + <> + + + Create a Guild + + Give your community a name and a unique URL. + + +
+
+ + setName(e.target.value)} + disabled={loading} + autoFocus + /> +
+
+ +
+ + townhall.chat/ + + { + setSlugEdited(true) + setSlug(normalizeSlugInput(e.target.value)) + }} + onBlur={() => + setSlug((currentSlug) => sluggify(currentSlug)) + } + disabled={loading} + /> +
+
+ {error &&

{error}

} + +
+ + )} + + {step === "join" && ( + <> + + + Join a Guild + + Paste an invite link or code to join an existing guild. + + +
+
+ + setInviteLink(e.target.value)} + disabled={loading} + autoFocus + /> +
+ {error &&

{error}

} + +
+ + )} + + + ) +} diff --git a/apps/web/src/components/sidebar/guild-bar/guild-bar.tsx b/apps/web/src/components/sidebar/guild-bar/guild-bar.tsx index 4feca09..9241810 100644 --- a/apps/web/src/components/sidebar/guild-bar/guild-bar.tsx +++ b/apps/web/src/components/sidebar/guild-bar/guild-bar.tsx @@ -3,6 +3,8 @@ import { cn } from "@repo/ui/lib/utils" import { useQuery } from "@tanstack/react-query" import { useNavigate, useParams } from "@tanstack/react-router" import { MessageCircle, Plus } from "lucide-react" +import { useState } from "react" +import { CreateGuildDialog } from "./create-guild-dialog" function GuildIcon({ name, @@ -55,6 +57,7 @@ function GuildIcon({ export function GuildBar() { const navigate = useNavigate() + const [createDialogOpen, setCreateDialogOpen] = useState(false) const { guildSlug } = useParams({ strict: false }) @@ -120,11 +123,20 @@ export function GuildBar() {
{/* Add guild button */} -
+
+ + +
) } diff --git a/apps/web/src/components/sidebar/index.tsx b/apps/web/src/components/sidebar/index.tsx index 993c523..595541b 100644 --- a/apps/web/src/components/sidebar/index.tsx +++ b/apps/web/src/components/sidebar/index.tsx @@ -4,10 +4,13 @@ import { ResizablePanelGroup, useDefaultLayout, } from "@repo/ui/components/resizable" +import { Sheet, SheetContent } from "@repo/ui/components/sheet" +import { useIsMobile } from "@repo/ui/hooks/use-mobile" import { cn } from "@repo/ui/lib/utils" import { useParams } from "@tanstack/react-router" import { AnimatePresence, motion } from "motion/react" -import { useCallback, useRef, useState } from "react" +import { useCallback, useEffect, useRef, useState } from "react" +import { useMobileSidebar } from "@/context/mobile-sidebar-context" import { ChannelPanel } from "./channel-panel/channel-panel" import { DMPanel } from "./dm-panel/dm-panel" import { GuildBar } from "./guild-bar/guild-bar" @@ -17,7 +20,36 @@ import { } from "./right-panel/right-sidebar-context" import { RightSidebarPanel } from "./right-panel/right-sidebar-panel" -function SidebarLayout({ children }: { children: React.ReactNode }) { +function LeftSidebarContent() { + const { guildSlug } = useParams({ strict: false }) + + return ( +
+ +
+ {guildSlug ? : } +
+
+ ) +} + +function MobileSidebar() { + const { open, setOpen } = useMobileSidebar() + + return ( + + + + + + ) +} + +function DesktopSidebarLayout({ children }: { children: React.ReactNode }) { const { guildSlug } = useParams({ strict: false }) const { view, isCollapsed, panelWidth, setPanelWidth, isHydrated } = useRightSidebar() @@ -48,9 +80,7 @@ function SidebarLayout({ children }: { children: React.ReactNode }) { } const handleMouseUp = () => { - // Commit width first so the next render has the correct value setPanelWidth(widthRef.current) - // Use rAF to clear resizing after React has committed the new width requestAnimationFrame(() => setIsResizing(false)) document.removeEventListener("mousemove", handleMouseMove) document.removeEventListener("mouseup", handleMouseUp) @@ -117,6 +147,50 @@ function SidebarLayout({ children }: { children: React.ReactNode }) { ) } +function MobileRightPanel() { + const { view, clearView } = useRightSidebar() + const { guildSlug } = useParams({ strict: false }) + const open = !!view && !!guildSlug + + useEffect(() => { + if (!guildSlug && view) clearView() + }, [guildSlug, view, clearView]) + + return ( + { + if (!isOpen) clearView() + }} + modal + > + + {view && } + + + ) +} + +function SidebarLayout({ children }: { children: React.ReactNode }) { + const isMobile = useIsMobile() + + if (isMobile) { + return ( +
+ + + {children} +
+ ) + } + + return {children} +} + export function Sidebar({ children }: { children: React.ReactNode }) { return ( diff --git a/apps/web/src/components/sidebar/right-panel/guild-members-panel.tsx b/apps/web/src/components/sidebar/right-panel/guild-members-panel.tsx index 579156f..30cf56f 100644 --- a/apps/web/src/components/sidebar/right-panel/guild-members-panel.tsx +++ b/apps/web/src/components/sidebar/right-panel/guild-members-panel.tsx @@ -34,6 +34,7 @@ import { } from "@repo/ui/components/dropdown-menu" import { ScrollArea } from "@repo/ui/components/scroll-area" import { Skeleton } from "@repo/ui/components/skeleton" +import { useIsMobile } from "@repo/ui/hooks/use-mobile" import { cn } from "@repo/ui/lib/utils" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { MoreHorizontal, PanelRight } from "lucide-react" @@ -304,7 +305,8 @@ export function GuildMembersPanel({ view }: { view: GuildMembersSidebarView }) { const socket = useSocket() const queryClient = useQueryClient() const { data: session } = authClient.useSession() - const { toggleCollapsed } = useRightSidebar() + const { toggleCollapsed, clearView } = useRightSidebar() + const isMobile = useIsMobile() const [moderationDialog, setModerationDialog] = useState(null) const queryKey = useMemo( @@ -639,7 +641,7 @@ export function GuildMembersPanel({ view }: { view: GuildMembersSidebarView }) {