Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions apps/web/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import {
} from "@repo/typiclient";
import type rootRouter from "@repo/api/router";

const api = createTypiClient<typeof rootRouter>({
const api = createTypiClient<
typeof rootRouter,
{
credentials: "include";
}
>({
baseUrl: "http://localhost:5000/api/v1",
options: {
credentials: "include",
Expand Down Expand Up @@ -45,7 +50,7 @@ const api = createTypiClient<typeof rootRouter>({
!config._retry
) {
config._retry = true;
await api.auth["refresh-token"].get();
await api.auth["refresh-token"].post();
return await retry();
}
},
Expand Down
13 changes: 0 additions & 13 deletions apps/web/src/app/(private)/(chats)/page.tsx

This file was deleted.

199 changes: 199 additions & 0 deletions apps/web/src/app/(private)/(main)/_components/call-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
"use client";

import {
Phone,
Video,
PhoneIncoming,
PhoneMissed,
Clock,
Users,
} from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { formatDate } from "date-fns";
import { ScrollArea } from "@repo/ui/components/scroll-area";
import api, { ApiOutputs } from "@repo/web/api";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@repo/ui/components/avatar";
import { cn } from "@repo/ui/lib/utils";
import { Button } from "@repo/ui/components/button";
import { useSearch } from "../_providers/search-provider";

type Call = ApiOutputs["/call"]["/"]["get"]["calls"][number];

const CallList = () => {
const { searchQuery } = useSearch();
const { isPending, isError, data } = useQuery({
queryKey: ["calls"],
queryFn: async () => {
const { data } = await api.call[""].get({
options: {
throwOnErrorStatus: true,
},
});
return data;
},
});

if (isPending) {
return <p>Loading...</p>;
}

if (isError) {
return <p>Error</p>;
}

const filteredCalls = searchQuery.trim()
? data.calls.filter((call) =>
call.participants.some((p) =>
p.user.name.toLowerCase().includes(searchQuery.toLowerCase())
)
)
: data.calls;

return (
<ScrollArea className="h-[calc(100vh-190px)]">
{filteredCalls.map((call, index) => {
return (
<div key={call.call.id} className="p-4">
<DateDivider call={call} prevCall={data.calls[index - 1]} />
<Call call={call} />
</div>
);
})}
</ScrollArea>
);
};

const Call = ({ call }: { call: Call }) => {
return (
<div className="mt-2">
<div className="flex items-center gap-2 justify-between">
<CallParticipants call={call} />
</div>
<div className="flex items-center justify-between">
<CallStatus call={call} />
<CallButtons call={call} />
</div>
</div>
);
};

const DateDivider = ({
call,
prevCall,
}: {
call: Call;
prevCall: Call | undefined;
}) => {
const showDate =
prevCall?.self.joinedAt?.toDateString() !==
call.self.joinedAt!.toDateString();

if (!showDate) return null;

return (
<span className="font-medium text-muted-foreground text-sm">
{formatDate(call.self.joinedAt!, "PPP")}
</span>
);
};

const CallParticipants = ({ call }: { call: Call }) => {
const displayParticipants = call.participants.slice(0, 3);
const remainingCount = call.participants.length - 3;

return call.participants && call.participants.length > 1 ? (
<div className="flex items-center gap-2">
<div className="flex -space-x-2">
{displayParticipants.map((participant) => {
return (
<Avatar className="size-8" key={participant.user.id}>
<AvatarImage src={participant.user.picture ?? undefined} />
<AvatarFallback>{participant.user.name}</AvatarFallback>
</Avatar>
);
})}
{remainingCount > 0 && (
<div className="h-8 w-8 rounded-full bg-muted border-2 border-card flex items-center justify-center">
<span className="text-xs text-muted-foreground font-medium">
+{remainingCount}
</span>
</div>
)}
</div>
<div className="flex items-center gap-1">
<Users className="h-3 w-3 text-muted-foreground" />
<span className="font-medium">
{call.participants.length === 2
? call.participants.map((p) => p.user.name).join(", ")
: `${call.participants[0].user.name} and ${call.participants.length - 1} others`}
</span>
</div>
</div>
) : (
<div className="flex items-center gap-2">
<Avatar className="size-8">
<AvatarImage src={call.participants[0].user.picture ?? undefined} />
<AvatarFallback>{call.participants[0].user.name}</AvatarFallback>
</Avatar>
<span className="font-medium">{call.participants[0].user.name}</span>
</div>
);
};

const CallStatus = ({ call }: { call: Call }) => {
return (
<div className="flex items-center gap-2">
<CallIcon call={call} />
<span
className={cn(
"text-sm capitalize",
call.call.status === "missed"
? "text-red-600"
: "text-muted-foreground"
)}
>
{call.call.status}
</span>
{call.call.duration && (
<>
<span className="text-muted-foreground">•</span>
<div className="flex items-center gap-1">
<Clock className="h-3 w-3 text-muted-foreground" />
<span className="text-sm text-muted-foreground">
{call.call.duration}
</span>
</div>
</>
)}
</div>
);
};

const CallIcon = ({ call }: { call: Call }) => {
if (call.call.status === "ringing")
return <PhoneIncoming className="h-4 w-4 text-green-500" />;

if (call.call.status === "missed" || call.call.status === "declined")
return <PhoneMissed className="h-4 w-4 text-red-500" />;

return <Phone className="h-4 w-4 text-gray-500" />;
};

const CallButtons = ({ call }: { call: Call }) => {
return (
<div className="flex gap-1">
<Button variant="ghost" size="icon" className="h-7 w-7">
<Phone className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7">
<Video className="h-3.5 w-3.5" />
</Button>
</div>
);
};

export default CallList;
Original file line number Diff line number Diff line change
@@ -1,53 +1,20 @@
"use client";

import React from "react";
import { Plus, Search } from "lucide-react";
import { Button } from "@repo/ui/components/button";
import { ScrollArea } from "@repo/ui/components/scroll-area";
import { Input } from "@repo/ui/components/input";
import { Badge } from "@repo/ui/components/badge";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@repo/ui/components/avatar";
import api from "@repo/web/api";
import { SearchProvider, useSearch } from "../_providers/search-context";
import { useSearch } from "../_providers/search-provider";
import { useQuery } from "@tanstack/react-query";
import { useCurrentChat } from "@repo/web/app/(private)/_providers/current-chat-provider";
import { useCurrentChat } from "@repo/web/app/(private)/(main)/_providers/current-chat-provider";
import { cn } from "@repo/ui/lib/utils";
import { format, isSameWeek } from "date-fns";

const SearchChats = () => {
const { searchQuery, setSearchQuery } = useSearch();

return (
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search for Chats"
className="pl-9"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
);
};

const Header = () => {
return (
<div className="border-b p-4">
<div className="mb-4 flex items-center justify-between">
<h1 className="text-xl font-semibold">Chats</h1>
<Button variant="ghost" size="icon">
<Plus className="h-5 w-5" />
</Button>
</div>
<SearchChats />
</div>
);
};

const Chat = ({
id,
picture,
Expand All @@ -65,17 +32,17 @@ const Chat = ({
}) => {
const { chatId, setChatId } = useCurrentChat();

const messageFormatDate = (date: Date) => {
return isSameWeek(new Date(), date)
? format(date, "iiii")
: format(date, "dd/MM/yyyy");
};
const messageDate = lastMessageTime
? isSameWeek(new Date(), lastMessageTime)
? format(lastMessageTime, "iiii")
: format(lastMessageTime, "dd/MM/yyyy")
: "";

return (
<div
key={id}
className={cn(
"flex items-center space-x-4 rounded-md p-3 cursor-pointer",
"flex items-center space-x-4 rounded-md p-3 cursor-pointer space-y-1",
{
"bg-muted": chatId === id,
"hover:bg-muted/50": chatId !== id,
Expand All @@ -91,9 +58,7 @@ const Chat = ({
<div className="flex justify-between gap-10">
<p className="truncate font-medium max-w-[150px]">{name}</p>
{lastMessageTime && (
<span className="text-muted-foreground text-xs">
{messageFormatDate(lastMessageTime)}
</span>
<span className="text-muted-foreground text-xs">{messageDate}</span>
)}
</div>
<div className="flex justify-between gap-10">
Expand All @@ -109,14 +74,17 @@ const Chat = ({
);
};

const Chats = () => {
const ChatList = () => {
const { searchQuery } = useSearch();
const { isPending, isError, data } = useQuery({
queryKey: ["chats"],
queryFn: async () => {
const { status, data } = await api.chat[""].get();
if (status === "OK") return data;
throw new Error(data.error.message);
const { data } = await api.chat[""].get({
options: {
throwOnErrorStatus: true,
},
});
return data;
},
});

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

return (
<ScrollArea className="flex-1 min-h-0">
<div className="p-4">
<div className="space-y-1">
{filteredChats.map((chat) => (
<Chat
key={chat.id}
id={chat.id}
picture={chat.picture ?? undefined}
name={chat.name}
lastMessage={chat.latestMessage?.content}
lastMessageTime={chat.latestMessage?.createdAt}
unreadMessagesCount={chat.unreadMessagesCount}
/>
))}
</div>
</div>
<ScrollArea className="h-[calc(100vh-190px)]">
{filteredChats.map((chat) => (
<Chat
key={chat.id}
id={chat.id}
picture={chat.picture ?? undefined}
name={chat.name}
lastMessage={chat.latestMessage?.content}
lastMessageTime={chat.latestMessage?.createdAt}
unreadMessagesCount={chat.unreadMessagesCount}
/>
))}
</ScrollArea>
);
};

const ChatList = () => {
return (
<div className="flex flex-col border-r">
<SearchProvider>
<Header></Header>
<Chats></Chats>
</SearchProvider>
</div>
);
};

export default ChatList;
Loading
Loading