From aa25af028e0a9236173dd4b7353515b3eb5abe9f Mon Sep 17 00:00:00 2001 From: Khairul Hidayat Date: Thu, 17 Sep 2026 06:11:14 +0800 Subject: [PATCH] feat: draft update ui --- src/client/whatsapp.ts | 4 +- ui/src/app/chats.tsx | 58 +++------- ui/src/app/devices.tsx | 91 +++++++++++++++ ui/src/app/messages.tsx | 32 +++-- ui/src/app/page.tsx | 62 +++++----- ui/src/components/ui/avatar.tsx | 54 +++++++++ ui/src/components/ui/select.tsx | 199 ++++++++++++++++++++++++++++++++ ui/src/hooks/useLocalStorage.ts | 23 ++++ ui/src/lib/utils.ts | 6 +- 9 files changed, 450 insertions(+), 79 deletions(-) create mode 100644 ui/src/app/devices.tsx create mode 100644 ui/src/components/ui/avatar.tsx create mode 100644 ui/src/components/ui/select.tsx create mode 100644 ui/src/hooks/useLocalStorage.ts diff --git a/src/client/whatsapp.ts b/src/client/whatsapp.ts index 515a552..422b7f4 100644 --- a/src/client/whatsapp.ts +++ b/src/client/whatsapp.ts @@ -127,7 +127,9 @@ export class WhatsAppClient { } listDevices() { - return this.request<{ id: string; name: string }[]>("listDevices"); + return this.request<{ id: string; name: string; lastAlive: number }[]>( + "listDevices", + ); } listChats(params?: ListChatsParams) { diff --git a/ui/src/app/chats.tsx b/ui/src/app/chats.tsx index 4d1585f..a0b23a6 100644 --- a/ui/src/app/chats.tsx +++ b/ui/src/app/chats.tsx @@ -1,5 +1,6 @@ import { useWAClient } from "./context"; -import { cn, getId, getInitial } from "@/lib/utils"; +import Avatar from "@/components/ui/avatar"; +import { getId } from "@/lib/utils"; import type { ChatDTO } from "@shared/dto"; import { useQuery } from "@tanstack/react-query"; import { SearchIcon, XIcon } from "lucide-react"; @@ -11,7 +12,6 @@ type Props = { }; export default function ChatsSection({ selected, onSelect }: Props) { const [query, setQuery] = useState(""); - const [showSearch, setShowSearch] = useState(false); const { client, isReady } = useWAClient(); const { data: chats } = useQuery({ @@ -41,34 +41,23 @@ export default function ChatsSection({ selected, onSelect }: Props) { return ( <>
- {showSearch ? ( -
- - setQuery(event.target.value)} - placeholder="Search conversations" - className="min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground" - /> - -
- ) : ( +
+ + setQuery(event.target.value)} + placeholder="Search conversations" + className="min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground" + /> - )} +
{/*
@@ -84,19 +73,10 @@ export default function ChatsSection({ selected, onSelect }: Props) { -
- {getInitial(chat.formattedTitle)} -
+ +

{chat.formattedTitle || chat.name} @@ -175,6 +176,13 @@ export default function MessagesSection({

) : null} + {item.type === "call_log" && ( +
+ +

Call log

+
+ )} +
diff --git a/ui/src/app/page.tsx b/ui/src/app/page.tsx index 2ebf68d..6df46da 100644 --- a/ui/src/app/page.tsx +++ b/ui/src/app/page.tsx @@ -1,19 +1,27 @@ -import { useState } from "react"; import ChatsSection from "./chats"; import type { ChatDTO } from "@shared/dto"; import MessagesSection from "./messages"; +import DevicesSection from "./devices"; +import { useLocalStorage } from "@/hooks/useLocalStorage"; export default function Page() { - const [chat, setChat] = useState(null); - const [mobileChat, setMobileChat] = useState(false); + const [chat, setChat] = useLocalStorage( + "app/current-chat", + null, + ); + const [mobileChat, setMobileChat] = useLocalStorage( + "app/mobile-chat", + false, + ); + const [curDeviceId, setDeviceId] = useLocalStorage("app/device", ""); return ( -
-
- + + {chat ? ( + + ) : null} + {/*
*/}
); } diff --git a/ui/src/components/ui/avatar.tsx b/ui/src/components/ui/avatar.tsx new file mode 100644 index 0000000..23e06fc --- /dev/null +++ b/ui/src/components/ui/avatar.tsx @@ -0,0 +1,54 @@ +import { getInitial } from "@/lib/utils"; +import { cn } from "cn"; +import { UserIcon } from "lucide-react"; + +type Props = { + name?: string | null; + className?: string; + iconClassName?: string; +}; + +const colorSchemes = [ + "bg-rose-200 text-rose-800", + "bg-pink-200 text-pink-800", + "bg-fuchsia-200 text-fuchsia-800", + "bg-purple-200 text-purple-800", + "bg-violet-200 text-violet-800", + "bg-indigo-200 text-indigo-800", + "bg-blue-200 text-blue-800", + "bg-sky-200 text-sky-800", + "bg-cyan-200 text-cyan-800", + "bg-teal-200 text-teal-800", + "bg-emerald-200 text-emerald-800", + "bg-green-200 text-green-800", + "bg-lime-200 text-lime-800", + "bg-yellow-200 text-yellow-800", + "bg-amber-200 text-amber-800", + "bg-orange-200 text-orange-800", +]; + +function getColorBasedOnName(name?: string | null) { + if (!name) return colorSchemes[0]; + + return colorSchemes[getInitial(name).charCodeAt(0) % colorSchemes.length]; +} + +export default function Avatar({ name, className, iconClassName }: Props) { + const color = getColorBasedOnName(name); + + return ( +
+ {name ? ( + getInitial(name) + ) : ( + + )} +
+ ); +} diff --git a/ui/src/components/ui/select.tsx b/ui/src/components/ui/select.tsx new file mode 100644 index 0000000..4e7529f --- /dev/null +++ b/ui/src/components/ui/select.tsx @@ -0,0 +1,199 @@ +import * as React from "react"; +import { Select as SelectPrimitive } from "@base-ui/react/select"; +import { cn } from "cn"; +import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"; + +const Select = SelectPrimitive.Root; + +function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) { + return ( + + ); +} + +function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) { + return ( + + ); +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: SelectPrimitive.Trigger.Props & { + size?: "sm" | "default"; +}) { + return ( + + {children} + + } + /> + + ); +} + +function SelectContent({ + className, + children, + side = "bottom", + sideOffset = 4, + align = "center", + alignOffset = 0, + alignItemWithTrigger = true, + ...props +}: SelectPrimitive.Popup.Props & + Pick< + SelectPrimitive.Positioner.Props, + "align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger" + >) { + return ( + + + + + {children} + + + + + ); +} + +function SelectLabel({ + className, + ...props +}: SelectPrimitive.GroupLabel.Props) { + return ( + + ); +} + +function SelectItem({ + className, + children, + ...props +}: SelectPrimitive.Item.Props) { + return ( + + + {children} + + + } + > + + + + ); +} + +function SelectSeparator({ + className, + ...props +}: SelectPrimitive.Separator.Props) { + return ( + + ); +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +}; diff --git a/ui/src/hooks/useLocalStorage.ts b/ui/src/hooks/useLocalStorage.ts new file mode 100644 index 0000000..2b2e3df --- /dev/null +++ b/ui/src/hooks/useLocalStorage.ts @@ -0,0 +1,23 @@ +import { useEffect, useState } from "react"; + +export function useLocalStorage(key: string, initialValue: T) { + const [state, setState] = useState(() => { + try { + const item = window.localStorage.getItem(key); + return item ? JSON.parse(item) : initialValue; + } catch (error) { + console.log(error); + return initialValue; + } + }); + + useEffect(() => { + try { + window.localStorage.setItem(key, JSON.stringify(state)); + } catch (error) { + console.log(error); + } + }, [state]); + + return [state, setState] as const; +} diff --git a/ui/src/lib/utils.ts b/ui/src/lib/utils.ts index 5e6121c..5852ab4 100644 --- a/ui/src/lib/utils.ts +++ b/ui/src/lib/utils.ts @@ -10,7 +10,11 @@ export function getId(id?: string | SerializedId | MessageIdDTO | null) { export function getInitial(name?: string | null) { return ( name - ?.split(" ") + ?.replace("dr.", "") + .replace(/[^a-zA-Z ]/g, "") + .toUpperCase() + .trim() + .split(" ") .map((i) => i[0]) .slice(0, 2) .join("") || ""