Skip to content

Commit d103480

Browse files
authored
Merge pull request #47 from Kasper24/ui-refactor
refactor: redo the ui app
2 parents b957c6a + c789a86 commit d103480

28 files changed

Lines changed: 1961 additions & 358 deletions

apps/web/src/api/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import {
66
} from "@repo/typiclient";
77
import type rootRouter from "@repo/api/router";
88

9-
const api = createTypiClient<typeof rootRouter>({
9+
const api = createTypiClient<
10+
typeof rootRouter,
11+
{
12+
credentials: "include";
13+
}
14+
>({
1015
baseUrl: "http://localhost:5000/api/v1",
1116
options: {
1217
credentials: "include",
@@ -45,7 +50,7 @@ const api = createTypiClient<typeof rootRouter>({
4550
!config._retry
4651
) {
4752
config._retry = true;
48-
await api.auth["refresh-token"].get();
53+
await api.auth["refresh-token"].post();
4954
return await retry();
5055
}
5156
},

apps/web/src/app/(private)/(chats)/page.tsx

Lines changed: 0 additions & 13 deletions
This file was deleted.
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
"use client";
2+
3+
import {
4+
Phone,
5+
Video,
6+
PhoneIncoming,
7+
PhoneMissed,
8+
Clock,
9+
Users,
10+
} from "lucide-react";
11+
import { useQuery } from "@tanstack/react-query";
12+
import { formatDate } from "date-fns";
13+
import { ScrollArea } from "@repo/ui/components/scroll-area";
14+
import api, { ApiOutputs } from "@repo/web/api";
15+
import {
16+
Avatar,
17+
AvatarFallback,
18+
AvatarImage,
19+
} from "@repo/ui/components/avatar";
20+
import { cn } from "@repo/ui/lib/utils";
21+
import { Button } from "@repo/ui/components/button";
22+
import { useSearch } from "../_providers/search-provider";
23+
24+
type Call = ApiOutputs["/call"]["/"]["get"]["calls"][number];
25+
26+
const CallList = () => {
27+
const { searchQuery } = useSearch();
28+
const { isPending, isError, data } = useQuery({
29+
queryKey: ["calls"],
30+
queryFn: async () => {
31+
const { data } = await api.call[""].get({
32+
options: {
33+
throwOnErrorStatus: true,
34+
},
35+
});
36+
return data;
37+
},
38+
});
39+
40+
if (isPending) {
41+
return <p>Loading...</p>;
42+
}
43+
44+
if (isError) {
45+
return <p>Error</p>;
46+
}
47+
48+
const filteredCalls = searchQuery.trim()
49+
? data.calls.filter((call) =>
50+
call.participants.some((p) =>
51+
p.user.name.toLowerCase().includes(searchQuery.toLowerCase())
52+
)
53+
)
54+
: data.calls;
55+
56+
return (
57+
<ScrollArea className="h-[calc(100vh-190px)]">
58+
{filteredCalls.map((call, index) => {
59+
return (
60+
<div key={call.call.id} className="p-4">
61+
<DateDivider call={call} prevCall={data.calls[index - 1]} />
62+
<Call call={call} />
63+
</div>
64+
);
65+
})}
66+
</ScrollArea>
67+
);
68+
};
69+
70+
const Call = ({ call }: { call: Call }) => {
71+
return (
72+
<div className="mt-2">
73+
<div className="flex items-center gap-2 justify-between">
74+
<CallParticipants call={call} />
75+
</div>
76+
<div className="flex items-center justify-between">
77+
<CallStatus call={call} />
78+
<CallButtons call={call} />
79+
</div>
80+
</div>
81+
);
82+
};
83+
84+
const DateDivider = ({
85+
call,
86+
prevCall,
87+
}: {
88+
call: Call;
89+
prevCall: Call | undefined;
90+
}) => {
91+
const showDate =
92+
prevCall?.self.joinedAt?.toDateString() !==
93+
call.self.joinedAt!.toDateString();
94+
95+
if (!showDate) return null;
96+
97+
return (
98+
<span className="font-medium text-muted-foreground text-sm">
99+
{formatDate(call.self.joinedAt!, "PPP")}
100+
</span>
101+
);
102+
};
103+
104+
const CallParticipants = ({ call }: { call: Call }) => {
105+
const displayParticipants = call.participants.slice(0, 3);
106+
const remainingCount = call.participants.length - 3;
107+
108+
return call.participants && call.participants.length > 1 ? (
109+
<div className="flex items-center gap-2">
110+
<div className="flex -space-x-2">
111+
{displayParticipants.map((participant) => {
112+
return (
113+
<Avatar className="size-8" key={participant.user.id}>
114+
<AvatarImage src={participant.user.picture ?? undefined} />
115+
<AvatarFallback>{participant.user.name}</AvatarFallback>
116+
</Avatar>
117+
);
118+
})}
119+
{remainingCount > 0 && (
120+
<div className="h-8 w-8 rounded-full bg-muted border-2 border-card flex items-center justify-center">
121+
<span className="text-xs text-muted-foreground font-medium">
122+
+{remainingCount}
123+
</span>
124+
</div>
125+
)}
126+
</div>
127+
<div className="flex items-center gap-1">
128+
<Users className="h-3 w-3 text-muted-foreground" />
129+
<span className="font-medium">
130+
{call.participants.length === 2
131+
? call.participants.map((p) => p.user.name).join(", ")
132+
: `${call.participants[0].user.name} and ${call.participants.length - 1} others`}
133+
</span>
134+
</div>
135+
</div>
136+
) : (
137+
<div className="flex items-center gap-2">
138+
<Avatar className="size-8">
139+
<AvatarImage src={call.participants[0].user.picture ?? undefined} />
140+
<AvatarFallback>{call.participants[0].user.name}</AvatarFallback>
141+
</Avatar>
142+
<span className="font-medium">{call.participants[0].user.name}</span>
143+
</div>
144+
);
145+
};
146+
147+
const CallStatus = ({ call }: { call: Call }) => {
148+
return (
149+
<div className="flex items-center gap-2">
150+
<CallIcon call={call} />
151+
<span
152+
className={cn(
153+
"text-sm capitalize",
154+
call.call.status === "missed"
155+
? "text-red-600"
156+
: "text-muted-foreground"
157+
)}
158+
>
159+
{call.call.status}
160+
</span>
161+
{call.call.duration && (
162+
<>
163+
<span className="text-muted-foreground"></span>
164+
<div className="flex items-center gap-1">
165+
<Clock className="h-3 w-3 text-muted-foreground" />
166+
<span className="text-sm text-muted-foreground">
167+
{call.call.duration}
168+
</span>
169+
</div>
170+
</>
171+
)}
172+
</div>
173+
);
174+
};
175+
176+
const CallIcon = ({ call }: { call: Call }) => {
177+
if (call.call.status === "ringing")
178+
return <PhoneIncoming className="h-4 w-4 text-green-500" />;
179+
180+
if (call.call.status === "missed" || call.call.status === "declined")
181+
return <PhoneMissed className="h-4 w-4 text-red-500" />;
182+
183+
return <Phone className="h-4 w-4 text-gray-500" />;
184+
};
185+
186+
const CallButtons = ({ call }: { call: Call }) => {
187+
return (
188+
<div className="flex gap-1">
189+
<Button variant="ghost" size="icon" className="h-7 w-7">
190+
<Phone className="h-3.5 w-3.5" />
191+
</Button>
192+
<Button variant="ghost" size="icon" className="h-7 w-7">
193+
<Video className="h-3.5 w-3.5" />
194+
</Button>
195+
</div>
196+
);
197+
};
198+
199+
export default CallList;

apps/web/src/app/(private)/(chats)/_components/chat-list.tsx renamed to apps/web/src/app/(private)/(main)/_components/chat-list.tsx

Lines changed: 28 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,20 @@
11
"use client";
22

33
import React from "react";
4-
import { Plus, Search } from "lucide-react";
5-
import { Button } from "@repo/ui/components/button";
64
import { ScrollArea } from "@repo/ui/components/scroll-area";
7-
import { Input } from "@repo/ui/components/input";
85
import { Badge } from "@repo/ui/components/badge";
96
import {
107
Avatar,
118
AvatarFallback,
129
AvatarImage,
1310
} from "@repo/ui/components/avatar";
1411
import api from "@repo/web/api";
15-
import { SearchProvider, useSearch } from "../_providers/search-context";
12+
import { useSearch } from "../_providers/search-provider";
1613
import { useQuery } from "@tanstack/react-query";
17-
import { useCurrentChat } from "@repo/web/app/(private)/_providers/current-chat-provider";
14+
import { useCurrentChat } from "@repo/web/app/(private)/(main)/_providers/current-chat-provider";
1815
import { cn } from "@repo/ui/lib/utils";
1916
import { format, isSameWeek } from "date-fns";
2017

21-
const SearchChats = () => {
22-
const { searchQuery, setSearchQuery } = useSearch();
23-
24-
return (
25-
<div className="relative">
26-
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
27-
<Input
28-
placeholder="Search for Chats"
29-
className="pl-9"
30-
value={searchQuery}
31-
onChange={(e) => setSearchQuery(e.target.value)}
32-
/>
33-
</div>
34-
);
35-
};
36-
37-
const Header = () => {
38-
return (
39-
<div className="border-b p-4">
40-
<div className="mb-4 flex items-center justify-between">
41-
<h1 className="text-xl font-semibold">Chats</h1>
42-
<Button variant="ghost" size="icon">
43-
<Plus className="h-5 w-5" />
44-
</Button>
45-
</div>
46-
<SearchChats />
47-
</div>
48-
);
49-
};
50-
5118
const Chat = ({
5219
id,
5320
picture,
@@ -65,17 +32,17 @@ const Chat = ({
6532
}) => {
6633
const { chatId, setChatId } = useCurrentChat();
6734

68-
const messageFormatDate = (date: Date) => {
69-
return isSameWeek(new Date(), date)
70-
? format(date, "iiii")
71-
: format(date, "dd/MM/yyyy");
72-
};
35+
const messageDate = lastMessageTime
36+
? isSameWeek(new Date(), lastMessageTime)
37+
? format(lastMessageTime, "iiii")
38+
: format(lastMessageTime, "dd/MM/yyyy")
39+
: "";
7340

7441
return (
7542
<div
7643
key={id}
7744
className={cn(
78-
"flex items-center space-x-4 rounded-md p-3 cursor-pointer",
45+
"flex items-center space-x-4 rounded-md p-3 cursor-pointer space-y-1",
7946
{
8047
"bg-muted": chatId === id,
8148
"hover:bg-muted/50": chatId !== id,
@@ -91,9 +58,7 @@ const Chat = ({
9158
<div className="flex justify-between gap-10">
9259
<p className="truncate font-medium max-w-[150px]">{name}</p>
9360
{lastMessageTime && (
94-
<span className="text-muted-foreground text-xs">
95-
{messageFormatDate(lastMessageTime)}
96-
</span>
61+
<span className="text-muted-foreground text-xs">{messageDate}</span>
9762
)}
9863
</div>
9964
<div className="flex justify-between gap-10">
@@ -109,14 +74,17 @@ const Chat = ({
10974
);
11075
};
11176

112-
const Chats = () => {
77+
const ChatList = () => {
11378
const { searchQuery } = useSearch();
11479
const { isPending, isError, data } = useQuery({
11580
queryKey: ["chats"],
11681
queryFn: async () => {
117-
const { status, data } = await api.chat[""].get();
118-
if (status === "OK") return data;
119-
throw new Error(data.error.message);
82+
const { data } = await api.chat[""].get({
83+
options: {
84+
throwOnErrorStatus: true,
85+
},
86+
});
87+
return data;
12088
},
12189
});
12290

@@ -131,35 +99,20 @@ const Chats = () => {
13199
: data.chats;
132100

133101
return (
134-
<ScrollArea className="flex-1 min-h-0">
135-
<div className="p-4">
136-
<div className="space-y-1">
137-
{filteredChats.map((chat) => (
138-
<Chat
139-
key={chat.id}
140-
id={chat.id}
141-
picture={chat.picture ?? undefined}
142-
name={chat.name}
143-
lastMessage={chat.latestMessage?.content}
144-
lastMessageTime={chat.latestMessage?.createdAt}
145-
unreadMessagesCount={chat.unreadMessagesCount}
146-
/>
147-
))}
148-
</div>
149-
</div>
102+
<ScrollArea className="h-[calc(100vh-190px)]">
103+
{filteredChats.map((chat) => (
104+
<Chat
105+
key={chat.id}
106+
id={chat.id}
107+
picture={chat.picture ?? undefined}
108+
name={chat.name}
109+
lastMessage={chat.latestMessage?.content}
110+
lastMessageTime={chat.latestMessage?.createdAt}
111+
unreadMessagesCount={chat.unreadMessagesCount}
112+
/>
113+
))}
150114
</ScrollArea>
151115
);
152116
};
153117

154-
const ChatList = () => {
155-
return (
156-
<div className="flex flex-col border-r">
157-
<SearchProvider>
158-
<Header></Header>
159-
<Chats></Chats>
160-
</SearchProvider>
161-
</div>
162-
);
163-
};
164-
165118
export default ChatList;

0 commit comments

Comments
 (0)