Skip to content

Commit 88b644c

Browse files
authored
improvement: mobile experience (#1010)
* wip * wip * contain scrolling * more wip * done * Update MobileTab.tsx * Update MobileTab.tsx * Update MobileTab.tsx * biome stuff * ts
1 parent 61308b5 commit 88b644c

5 files changed

Lines changed: 207 additions & 20 deletions

File tree

apps/web/app/(org)/dashboard/_components/DashboardInner.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,12 @@ export default function DashboardInner({
8080
);
8181
const isSharedCapsPage = pathname === "/dashboard/shared-caps";
8282
return (
83-
<div className="flex flex-col min-h-screen">
83+
<div className="flex overflow-hidden w-full flex-col flex-1 md:mt-0 mt-[126px]">
84+
{/* NavBar */}
8485
<div
8586
className={clsx(
86-
"flex sticky z-40 justify-between items-center px-5 mt-10 w-full border-b",
87-
"bg-gray-1 lg:bg-transparent min-h-16 lg:min-h-10 border-gray-3 lg:border-b-0 lg:pl-0 lg:pr-5 lg:top-0 lg:relative top-[64px] lg:mt-5 lg:h-8",
87+
"flex fixed z-40 justify-between items-center py-3 pr-2 pl-5 w-full md:relative mt-[60px] md:mt-0 md:py-[19px] lg:pl-0 lg:pr-5",
88+
"top-0 bg-gray-1 md:mt-0",
8889
)}
8990
>
9091
<div className="flex flex-col gap-0.5">
@@ -170,12 +171,15 @@ export default function DashboardInner({
170171
<User />
171172
</div>
172173
</div>
174+
{/*End of NavBar*/}
173175
<main
174176
className={
175-
"flex flex-col flex-1 p-5 pb-5 mt-5 border border-b-0 min-h-fit bg-gray-2 border-gray-3 lg:rounded-tl-2xl lg:p-8"
177+
"flex overscroll-contain flex-col flex-1 flex-grow p-5 h-full border border-b-0 custom-scroll bg-gray-2 border-gray-3 lg:rounded-tl-2xl lg:p-8"
176178
}
177179
>
178-
<div className="flex flex-col flex-1 gap-4">{children}</div>
180+
<div className="flex flex-col flex-1 flex-grow gap-4 min-h-fit">
181+
{children}
182+
</div>
179183
</main>
180184
{isSharedCapsPage && activeOrganization?.members && (
181185
<MembersDialog
@@ -300,7 +304,7 @@ const User = () => {
300304
.filter((item) => item.showCondition)
301305
.map((item, index) => (
302306
<MenuItem
303-
key={index}
307+
key={index.toString()}
304308
icon={item.icon}
305309
name={item.name}
306310
href={item.href ?? "#"}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
"use client";
2+
3+
import { Avatar } from "@cap/ui";
4+
import { useClickAway } from "@uidotdev/usehooks";
5+
import clsx from "clsx";
6+
import { Check, ChevronDown } from "lucide-react";
7+
import { AnimatePresence, motion } from "motion/react";
8+
import Image from "next/image";
9+
import Link from "next/link";
10+
import { useRouter } from "next/navigation";
11+
import {
12+
type Dispatch,
13+
type LegacyRef,
14+
type MutableRefObject,
15+
type SetStateAction,
16+
useRef,
17+
useState,
18+
} from "react";
19+
import { useDashboardContext } from "../Contexts";
20+
import { CapIcon, CogIcon, LayersIcon } from "./AnimatedIcons";
21+
import { updateActiveOrganization } from "./Navbar/server";
22+
23+
const Tabs = [
24+
{ icon: <LayersIcon size={20} />, href: "/dashboard/spaces/browse" },
25+
{ icon: <CapIcon size={25} />, href: "/dashboard/caps" },
26+
{
27+
icon: <CogIcon size={22} />,
28+
href: "/dashboard/settings/organization",
29+
ownerOnly: true,
30+
},
31+
];
32+
33+
const MobileTab = () => {
34+
const [open, setOpen] = useState(false);
35+
const containerRef = useRef<HTMLDivElement>(null);
36+
const { activeOrganization: activeOrg, user } = useDashboardContext();
37+
const isOwner = activeOrg?.organization.ownerId === user.id;
38+
const menuRef = useClickAway((e) => {
39+
if (
40+
containerRef.current &&
41+
!containerRef.current.contains(e.target as Node)
42+
) {
43+
setOpen(false);
44+
}
45+
});
46+
return (
47+
<div className="flex sticky bottom-0 z-50 flex-1 justify-between items-center px-5 w-screen h-16 border-t lg:hidden border-gray-5 bg-gray-1">
48+
<AnimatePresence>
49+
{open && <OrgsMenu setOpen={setOpen} menuRef={menuRef} />}
50+
</AnimatePresence>
51+
<Orgs open={open} setOpen={setOpen} containerRef={containerRef} />
52+
<div className="flex gap-6 justify-between items-center h-full text-gray-11">
53+
{Tabs.filter((i) => !i.ownerOnly || isOwner).map((tab) => (
54+
<Link href={tab.href} key={tab.href}>
55+
{tab.icon}
56+
</Link>
57+
))}
58+
</div>
59+
</div>
60+
);
61+
};
62+
63+
const Orgs = ({
64+
setOpen,
65+
open,
66+
containerRef,
67+
}: {
68+
setOpen: Dispatch<SetStateAction<boolean>>;
69+
open: boolean;
70+
containerRef: MutableRefObject<HTMLDivElement | null>;
71+
}) => {
72+
const { activeOrganization: activeOrg } = useDashboardContext();
73+
return (
74+
<div
75+
onClick={() => setOpen((p) => !p)}
76+
ref={containerRef}
77+
className="flex gap-1.5 items-center p-2 rounded-full border bg-gray-3 border-gray-5"
78+
>
79+
{activeOrg?.organization.iconUrl ? (
80+
<div className="overflow-hidden relative flex-shrink-0 rounded-full size-[24px]">
81+
<Image
82+
src={activeOrg.organization.iconUrl}
83+
alt={activeOrg.organization.name || "Organization icon"}
84+
fill
85+
className="object-cover"
86+
/>
87+
</div>
88+
) : (
89+
<Avatar
90+
letterClass="text-xs"
91+
className="relative flex-shrink-0 mx-auto size-6"
92+
name={activeOrg?.organization.name ?? "No organization found"}
93+
/>
94+
)}
95+
<p className="text-xs mr-2 text-gray-12 truncate w-fit max-w-[90px]">
96+
{activeOrg?.organization.name}
97+
</p>
98+
<ChevronDown
99+
className={clsx(
100+
"text-gray-11 size-4 transition-transform",
101+
open && "rotate-180",
102+
)}
103+
/>
104+
</div>
105+
);
106+
};
107+
108+
const OrgsMenu = ({
109+
setOpen,
110+
menuRef,
111+
}: {
112+
setOpen: Dispatch<SetStateAction<boolean>>;
113+
menuRef: MutableRefObject<Element>;
114+
}) => {
115+
const { activeOrganization: activeOrg, organizationData: orgData } =
116+
useDashboardContext();
117+
const router = useRouter();
118+
return (
119+
<motion.div
120+
initial={{ scale: 0.98, opacity: 0 }}
121+
animate={{ scale: 1, opacity: 1 }}
122+
exit={{ scale: 0.9, opacity: 0 }}
123+
transition={{ duration: 0.15 }}
124+
ref={menuRef as LegacyRef<HTMLDivElement>}
125+
className={
126+
"isolate absolute overscroll-contain bottom-14 p-2 space-y-1.5 w-full rounded-xl h-fit border bg-gray-3 max-h-[325px] custom-scroll max-w-[200px] border-gray-4"
127+
}
128+
>
129+
{orgData?.map((organization) => {
130+
const isSelected =
131+
activeOrg?.organization.id === organization.organization.id;
132+
return (
133+
<div
134+
className={clsx(
135+
"p-2 rounded-lg transition-colors duration-300 group",
136+
isSelected
137+
? "pointer-events-none"
138+
: "text-gray-10 hover:text-gray-12 hover:bg-gray-6",
139+
)}
140+
key={organization.organization.name + "-organization"}
141+
onClick={async () => {
142+
await updateActiveOrganization(organization.organization.id);
143+
setOpen(false);
144+
router.push("/dashboard/caps");
145+
}}
146+
>
147+
<div className="flex gap-2 items-center w-full">
148+
{organization.organization.iconUrl ? (
149+
<div className="overflow-hidden relative flex-shrink-0 rounded-full size-5">
150+
<Image
151+
src={organization.organization.iconUrl}
152+
alt={organization.organization.name || "Organization icon"}
153+
fill
154+
className="object-cover"
155+
/>
156+
</div>
157+
) : (
158+
<Avatar
159+
letterClass="text-xs"
160+
className="relative flex-shrink-0 size-5"
161+
name={organization.organization.name}
162+
/>
163+
)}
164+
<p
165+
className={clsx(
166+
"flex-1 text-sm transition-colors duration-200 group-hover:text-gray-12",
167+
isSelected ? "text-gray-12" : "text-gray-10",
168+
)}
169+
>
170+
{organization.organization.name}
171+
</p>
172+
{isSelected && (
173+
<Check size={18} className={"ml-auto text-gray-12"} />
174+
)}
175+
</div>
176+
</div>
177+
);
178+
})}
179+
</motion.div>
180+
);
181+
};
182+
183+
export default MobileTab;

apps/web/app/(org)/dashboard/_components/Navbar/Mobile.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { faMoon, faSun } from "@fortawesome/free-solid-svg-icons";
55
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
66
import { useClickAway } from "@uidotdev/usehooks";
77
import { AnimatePresence, motion } from "framer-motion";
8-
import { Menu, X } from "lucide-react";
8+
import { X } from "lucide-react";
99
import Link from "next/link";
1010
import { type MutableRefObject, useState } from "react";
1111
import { useTheme } from "../../Contexts";
@@ -54,7 +54,7 @@ export const AdminMobileNav = () => {
5454
<LogoBadge className="block w-auto h-8" />
5555
</Link>
5656
</div>
57-
<div className="flex gap-4 items-center h-full">
57+
<div className="flex gap-4 items-center px-4 h-full">
5858
<div
5959
onClick={() => {
6060
setThemeHandler(theme === "light" ? "dark" : "light");
@@ -66,12 +66,6 @@ export const AdminMobileNav = () => {
6666
icon={theme === "dark" ? faSun : faMoon}
6767
/>
6868
</div>
69-
<button
70-
className="flex flex-col gap-2 justify-center items-center px-5 h-full text-white border-l border-gray-3 lg:hidden"
71-
onClick={() => setSidebarOpen(true)}
72-
>
73-
<Menu className="text-gray-12 size-7" aria-hidden="true" />
74-
</button>
7569
</div>
7670
</div>
7771
</>

apps/web/app/(org)/dashboard/layout.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { getCurrentUser } from "@cap/database/auth/session";
22
import { cookies } from "next/headers";
33
import { redirect } from "next/navigation";
44
import DashboardInner from "./_components/DashboardInner";
5+
import MobileTab from "./_components/MobileTab";
56
import DesktopNav from "./_components/Navbar/Desktop";
67
import MobileNav from "./_components/Navbar/Mobile";
78
import { DashboardContexts } from "./Contexts";
@@ -78,16 +79,15 @@ export default async function DashboardLayout({
7879
anyNewNotifications={anyNewNotifications}
7980
userPreferences={userPreferences}
8081
>
81-
<div className="grid grid-cols-[auto,1fr] overflow-y-auto bg-gray-1 grid-rows-[auto,1fr] h-dvh min-h-dvh">
82-
<aside className="z-10 col-span-1 row-span-2">
82+
<div className="grid grid-cols-[auto,1fr] bg-gray-1 grid-rows-[auto,1fr] h-dvh min-h-dvh">
83+
<aside className="hidden z-10 col-span-1 row-span-2 lg:flex">
8384
<DesktopNav />
8485
</aside>
85-
<div className="flex col-span-1 row-span-2 h-full custom-scroll focus:outline-none">
86+
<div className="flex col-span-2 row-span-2 h-full md:col-span-1 focus:outline-none">
8687
<MobileNav />
87-
<div className="dashboard-page">
88-
<DashboardInner>{children}</DashboardInner>
89-
</div>
88+
<DashboardInner>{children}</DashboardInner>
9089
</div>
90+
<MobileTab />
9191
</div>
9292
</DashboardContexts>
9393
</UploadingProvider>

apps/web/app/globals.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,12 @@ footer a {
860860
}
861861
}
862862

863+
@media (max-width: 768px) {
864+
.intercom-lightweight-app-launcher {
865+
bottom: 90px !important;
866+
}
867+
}
868+
863869
@layer base {
864870
* {
865871
@apply border-border;

0 commit comments

Comments
 (0)