Added mobile
This commit is contained in:
parent
82a14f65fa
commit
80114d1d66
125 changed files with 8200 additions and 542 deletions
|
|
@ -6,7 +6,7 @@
|
|||
"scripts": {
|
||||
"format": "bunx prettier --write .",
|
||||
"lint": "eslint src",
|
||||
"dev": "vite --port 3000",
|
||||
"dev": "vite --port 3000 --host 0.0.0.0",
|
||||
"test": "bun test --pass-with-no-tests",
|
||||
"build": "bun run test && tsc -b && vite build",
|
||||
"preview": "cd dist && nix-shell -p python3 --run 'python3 -m http.server 3000' && cd .."
|
||||
|
|
@ -25,6 +25,7 @@
|
|||
"@tensamin/ttp": "workspace:*",
|
||||
"@tensamin/ui": "workspace:*",
|
||||
"@tensamin/user": "workspace:*",
|
||||
"@tensamin/mobile": "workspace:*",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"comlink": "^4.4.2",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Button } from "@tensamin/ui/cmp/button";
|
||||
import { House, Phone } from "lucide-react";
|
||||
import { House, Phone, User } from "lucide-react";
|
||||
import { useLocation, useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { useCall } from "@tensamin/call/context";
|
||||
import Wrapper from "@tensamin/user/wrapper";
|
||||
|
|
@ -13,6 +13,7 @@ import {
|
|||
} from "@tensamin/ui/cmp/select";
|
||||
import { displayCallId } from "@tensamin/call/utils";
|
||||
import { useState } from "react";
|
||||
import { SidebarTrigger, useSidebar } from "@tensamin/ui/cmp/sidebar";
|
||||
|
||||
export default function Navbar() {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -28,7 +29,7 @@ export default function Navbar() {
|
|||
const [selectOpen, setSelectOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="w-full h-13.5 flex items-center justify-center">
|
||||
<div className="w-full gap-2 h-13.5 flex items-center justify-center">
|
||||
<Button
|
||||
onClick={() => navigate({ to: "/" })}
|
||||
className="w-9 h-9 aspect-square rounded-lg"
|
||||
|
|
@ -40,7 +41,7 @@ export default function Navbar() {
|
|||
<Wrapper
|
||||
userId={id}
|
||||
component={(user) => (
|
||||
<p className="font-medium pl-3 text-md">{user?.display}</p>
|
||||
<p className="font-medium text-md">{user?.display}</p>
|
||||
)}
|
||||
loading={<Skeleton className="ml-3 w-40 h-5" />}
|
||||
/>
|
||||
|
|
@ -100,3 +101,29 @@ export default function Navbar() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MobileNavbar() {
|
||||
const navigate = useNavigate();
|
||||
const { setOpenMobile } = useSidebar();
|
||||
|
||||
return (
|
||||
<div className="z-100 w-full h-16 flex justify-center items-center gap-8 bg-card border-t">
|
||||
<SidebarTrigger
|
||||
className="text-foreground w-12! h-12! aspect-square rounded-xl"
|
||||
variant="link"
|
||||
>
|
||||
<User className="size-5.5" />
|
||||
</SidebarTrigger>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigate({ to: "/" });
|
||||
setOpenMobile(false);
|
||||
}}
|
||||
className="text-foreground w-12 h-12 aspect-square rounded-xl"
|
||||
variant="link"
|
||||
>
|
||||
<House className="size-5.5" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@ import * as React from "react";
|
|||
import { Basic, Loading } from "./modals/basic";
|
||||
import List from "@/features/conversation/list/body";
|
||||
|
||||
import {
|
||||
Sidebar as SidebarRoot,
|
||||
SidebarContent,
|
||||
} from "@tensamin/ui/cmp/sidebar";
|
||||
|
||||
/**
|
||||
* Renders the conversation sidebar with account summary and conversation list.
|
||||
* @returns Sidebar JSX.
|
||||
|
|
@ -17,17 +22,21 @@ export default function Sidebar() {
|
|||
}, [load]);
|
||||
|
||||
return (
|
||||
<div className="w-65 h-full flex flex-col gap-3 p-2">
|
||||
<div>
|
||||
<Wrapper
|
||||
loading={<Loading />}
|
||||
userId={userId}
|
||||
component={(user) => <Basic user={user} />}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full">
|
||||
<List />
|
||||
</div>
|
||||
</div>
|
||||
<SidebarRoot>
|
||||
<SidebarContent>
|
||||
<div className="h-full w-full flex flex-col gap-3 p-2">
|
||||
<div>
|
||||
<Wrapper
|
||||
loading={<Loading />}
|
||||
userId={userId}
|
||||
component={(user) => <Basic user={user} />}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full">
|
||||
<List />
|
||||
</div>
|
||||
</div>
|
||||
</SidebarContent>
|
||||
</SidebarRoot>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import * as React from "react";
|
||||
|
||||
type Category = "conversations" | "communities";
|
||||
|
||||
/**
|
||||
|
|
@ -11,28 +9,6 @@ export default function Switch(props: {
|
|||
category: Category;
|
||||
setCategory: (category: Category) => void;
|
||||
}) {
|
||||
const conversationsRef = React.useRef<HTMLButtonElement | null>(null);
|
||||
const communitiesRef = React.useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
const [indicator, setIndicator] = React.useState({ left: 0, width: 0 });
|
||||
|
||||
const updateIndicator = React.useCallback(() => {
|
||||
const active =
|
||||
props.category === "conversations"
|
||||
? conversationsRef.current
|
||||
: communitiesRef.current;
|
||||
if (!active) return;
|
||||
|
||||
setIndicator({
|
||||
left: active.offsetLeft,
|
||||
width: active.offsetWidth,
|
||||
});
|
||||
}, [props.category]);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
updateIndicator();
|
||||
}, [updateIndicator]);
|
||||
|
||||
/**
|
||||
* Executes toggleCategory.
|
||||
* @param none This function has no parameters.
|
||||
|
|
@ -47,20 +23,19 @@ export default function Switch(props: {
|
|||
return (
|
||||
<div
|
||||
role="tablist"
|
||||
className="border relative inline-flex rounded-full bg-card p-1 select-none"
|
||||
className="border relative grid grid-cols-2 rounded-full bg-card p-1 select-none"
|
||||
>
|
||||
<div
|
||||
className="absolute top-1 bottom-1 rounded-full bg-input shadow-sm transition-all duration-300 ease-in-out"
|
||||
style={{
|
||||
left: `${indicator.left}px`,
|
||||
width: `${indicator.width}px`,
|
||||
left: props.category === "conversations" ? "0.25rem" : "50%",
|
||||
width: "calc(50% - 0.25rem)",
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
ref={conversationsRef}
|
||||
role="tab"
|
||||
aria-selected={props.category === "conversations"}
|
||||
className={`relative z-10 cursor-pointer px-2.5 py-1.5 rounded-full text-sm font-medium transition-colors duration-300 ${
|
||||
className={`relative z-10 w-full cursor-pointer rounded-full px-2.5 py-1.5 text-center text-sm font-medium transition-colors duration-300 ${
|
||||
props.category === "conversations"
|
||||
? "text-foreground"
|
||||
: "text-ring/50 hover:text-ring"
|
||||
|
|
@ -70,10 +45,9 @@ export default function Switch(props: {
|
|||
Conversations
|
||||
</button>
|
||||
<button
|
||||
ref={communitiesRef}
|
||||
role="tab"
|
||||
aria-selected={props.category === "communities"}
|
||||
className={`relative z-10 cursor-pointer px-2.5 py-1.5 rounded-full text-sm font-medium transition-colors duration-300 ${
|
||||
className={`relative z-10 w-full cursor-pointer rounded-full px-2.5 py-1.5 text-center text-sm font-medium transition-colors duration-300 ${
|
||||
props.category === "communities"
|
||||
? "text-foreground"
|
||||
: "text-ring/50 hover:text-ring"
|
||||
|
|
|
|||
|
|
@ -14,18 +14,20 @@ export default function ConversationModal({ userId }: { userId: number }) {
|
|||
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>
|
||||
<div
|
||||
className="select-none cursor-pointer"
|
||||
onClick={() => navigate({ to: "/chat", search: { id: userId } })}
|
||||
>
|
||||
<Wrapper
|
||||
loading={<Loading />}
|
||||
userId={userId}
|
||||
component={(user) => <Basic user={user} />}
|
||||
/>
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuTrigger
|
||||
render={
|
||||
<div
|
||||
className="select-none cursor-pointer"
|
||||
onClick={() => navigate({ to: "/chat", search: { id: userId } })}
|
||||
>
|
||||
<Wrapper
|
||||
loading={<Loading />}
|
||||
userId={userId}
|
||||
component={(user) => <Basic user={user} />}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuGroup>
|
||||
<ContextMenuItem>Test</ContextMenuItem>
|
||||
|
|
|
|||
|
|
@ -70,9 +70,7 @@ function AddConversationButton() {
|
|||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Add Conversation</Button>
|
||||
</DialogTrigger>
|
||||
<DialogTrigger render={<Button>Add Conversation</Button>} />
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>New Conversation</DialogTitle>
|
||||
|
|
@ -98,9 +96,7 @@ function AddConversationButton() {
|
|||
placeholder="Enter username..."
|
||||
/>
|
||||
<div className="flex w-full justify-end gap-1">
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<DialogClose render={<Button variant="outline">Cancel</Button>} />
|
||||
<Button disabled={loading} type="submit">
|
||||
{loading && <Loader2 className="animate-spin" />} Continue
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { type ReactNode } from "react";
|
||||
|
||||
import Sidebar from "@/components/sidebar";
|
||||
import Navbar from "@/components/navbar";
|
||||
import Navbar, { MobileNavbar } from "@/components/navbar";
|
||||
|
||||
import { SidebarProvider } from "@tensamin/ui/cmp/sidebar";
|
||||
import { useIsMobile, cn } from "@tensamin/ui/utils";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
|
|
@ -11,6 +14,8 @@ import { motion } from "framer-motion";
|
|||
* @returns unknown.
|
||||
*/
|
||||
export default function Layout(props: { children: ReactNode }) {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="w-full h-full flex bg-sidebar"
|
||||
|
|
@ -26,13 +31,21 @@ export default function Layout(props: { children: ReactNode }) {
|
|||
},
|
||||
}}
|
||||
>
|
||||
<Sidebar />
|
||||
<div className="w-full h-full flex flex-col">
|
||||
<Navbar />
|
||||
<div className="bg-background h-full w-full rounded-tl-3xl border-t border-l">
|
||||
{props.children}
|
||||
<SidebarProvider>
|
||||
<Sidebar />
|
||||
<div className="w-full h-full flex flex-col">
|
||||
{!isMobile && <Navbar />}
|
||||
<div
|
||||
className={cn(
|
||||
"bg-background h-full w-full",
|
||||
!isMobile && "rounded-tl-3xl border-t border-l",
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
{isMobile && <MobileNavbar />}
|
||||
</div>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useEffect, useState, type ReactNode } from "react";
|
|||
|
||||
import Storage from "@tensamin/storage/context";
|
||||
import Crypto from "@tensamin/crypto/context";
|
||||
import Mobile from "@tensamin/mobile/context";
|
||||
|
||||
import LegalWrapper from "@/features/legal/screen";
|
||||
|
||||
|
|
@ -10,6 +11,7 @@ import { TooltipProvider } from "@tensamin/ui/cmp/tooltip";
|
|||
|
||||
import { useStorage } from "@tensamin/storage/context";
|
||||
import { useLocation, useNavigate } from "@tanstack/react-router";
|
||||
import { useIsMobile } from "@tensamin/ui/utils";
|
||||
|
||||
/**
|
||||
* Executes Layout.
|
||||
|
|
@ -20,6 +22,8 @@ export default function Layout(props: { children: ReactNode }) {
|
|||
const [pixelRatio, setPixelRatio] = useState(devicePixelRatio);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setPixelRatio(devicePixelRatio);
|
||||
|
|
@ -35,7 +39,9 @@ export default function Layout(props: { children: ReactNode }) {
|
|||
<>
|
||||
<div className="fixed top-0 left-0 z-100 w-screen h-screen flex flex-col justify-center items-center pointer-events-none select-none">
|
||||
{visible && (
|
||||
<div className="bg-card border p-1 text-xs">{pixelRatio}</div>
|
||||
<div className="bg-card border p-1 text-xs">
|
||||
{Math.round(pixelRatio)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Todo: Make this nicer */}
|
||||
|
|
@ -45,12 +51,14 @@ export default function Layout(props: { children: ReactNode }) {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Toaster />
|
||||
<Toaster position={isMobile ? "top-center" : "bottom-right"} />
|
||||
<TooltipProvider>
|
||||
<Storage>
|
||||
<LoginWrapper>
|
||||
<LegalWrapper>
|
||||
<Crypto>{props.children}</Crypto>
|
||||
<Crypto>
|
||||
<Mobile>{props.children}</Mobile>
|
||||
</Crypto>
|
||||
</LegalWrapper>
|
||||
</LoginWrapper>
|
||||
</Storage>
|
||||
|
|
|
|||
Loading…
Reference in a new issue