274 lines
9.6 KiB
TypeScript
274 lines
9.6 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useRef } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
|
import { Send, Image as ImageIcon, Users, Lock, Zap, ChevronDown, ChevronUp } from "lucide-react";
|
|
import { timeAgo } from "@/lib/utils";
|
|
import { CannedMessagesPanel } from "@/components/CannedMessagesPanel";
|
|
import { LocalGuidePanel } from "@/components/LocalGuidePanel";
|
|
import { GuestUpgradeBanner } from "@/components/GuestUpgradeBanner";
|
|
import { useAccess } from "@/components/Providers";
|
|
|
|
interface Message {
|
|
id: string;
|
|
sender: string;
|
|
content: string;
|
|
image_url?: string;
|
|
created_at: string;
|
|
isMe?: boolean;
|
|
}
|
|
|
|
const CITY = process.env.NEXT_PUBLIC_DEFAULT_CITY || "Fort Lauderdale";
|
|
|
|
const MOCK_CITY_MESSAGES: Message[] = [
|
|
{
|
|
id: "1",
|
|
sender: "Mike_Top",
|
|
content: "Anyone at the beach right now?",
|
|
created_at: new Date(Date.now() - 300000).toISOString(),
|
|
},
|
|
{
|
|
id: "2",
|
|
sender: "Anonymous",
|
|
content: "Yeah, south end. Busy tonight.",
|
|
created_at: new Date(Date.now() - 240000).toISOString(),
|
|
},
|
|
{
|
|
id: "3",
|
|
sender: "VersBear",
|
|
content: "Hosting near Wilton. Door code 1234. Clean only.",
|
|
created_at: new Date(Date.now() - 120000).toISOString(),
|
|
},
|
|
{
|
|
id: "4",
|
|
sender: "PupRex",
|
|
content: "Woof! At Ramrod later if anyone wants to meet up",
|
|
created_at: new Date(Date.now() - 60000).toISOString(),
|
|
},
|
|
];
|
|
|
|
const MOCK_DM_MESSAGES: Message[] = [
|
|
{
|
|
id: "dm1",
|
|
sender: "You",
|
|
content: "Hey, saw you on the map. What's up?",
|
|
created_at: new Date(Date.now() - 600000).toISOString(),
|
|
isMe: true,
|
|
},
|
|
{
|
|
id: "dm2",
|
|
sender: "Mike_Top",
|
|
content: "Not much, hosting. You?",
|
|
created_at: new Date(Date.now() - 540000).toISOString(),
|
|
},
|
|
];
|
|
|
|
interface ChatPanelProps {
|
|
dmUserId?: string | null;
|
|
}
|
|
|
|
export function ChatPanel({ dmUserId }: ChatPanelProps) {
|
|
const { capabilities } = useAccess();
|
|
const [cityMessages, setCityMessages] = useState<Message[]>(MOCK_CITY_MESSAGES);
|
|
const [dmMessages, setDmMessages] = useState<Message[]>(MOCK_DM_MESSAGES);
|
|
const [input, setInput] = useState("");
|
|
const [activeTab, setActiveTab] = useState(dmUserId ? "dm" : "city");
|
|
const [showQuickReplies, setShowQuickReplies] = useState(false);
|
|
const bottomRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
}, [cityMessages, dmMessages, activeTab]);
|
|
|
|
const sendMessage = (text?: string) => {
|
|
if (!capabilities.canSendMessages) return;
|
|
const content = (text ?? input).trim();
|
|
if (!content) return;
|
|
const msg: Message = {
|
|
id: Date.now().toString(),
|
|
sender: "You",
|
|
content,
|
|
created_at: new Date().toISOString(),
|
|
isMe: true,
|
|
};
|
|
|
|
if (activeTab === "city") {
|
|
setCityMessages((prev) => [...prev, msg]);
|
|
} else {
|
|
setDmMessages((prev) => [...prev, msg]);
|
|
}
|
|
setInput("");
|
|
setShowQuickReplies(false);
|
|
};
|
|
|
|
const messages = activeTab === "city" ? cityMessages : dmMessages;
|
|
const showComposer = activeTab === "city" || activeTab === "dm";
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex flex-col h-full">
|
|
<TabsList className="mx-3 sm:mx-4 mt-2 grid grid-cols-3">
|
|
<TabsTrigger value="city" className="gap-1 text-xs min-h-[40px]">
|
|
<Users className="h-3 w-3" />
|
|
<span className="truncate">{CITY}</span>
|
|
</TabsTrigger>
|
|
<TabsTrigger value="dm" className="gap-1 text-xs min-h-[40px]">
|
|
<Lock className="h-3 w-3" />
|
|
DMs
|
|
</TabsTrigger>
|
|
<TabsTrigger value="groups" className="gap-1 text-xs min-h-[40px]">
|
|
Groups
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="city" className="flex-1 flex flex-col mt-0 overflow-hidden data-[state=inactive]:hidden">
|
|
<div className="px-3 sm:px-4 py-2 border-b border-border">
|
|
<Badge variant="online" className="text-[10px]">
|
|
{cityMessages.length + 12} in room
|
|
</Badge>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
Auto-joined based on your location. Public city chat.
|
|
</p>
|
|
</div>
|
|
<MessageList messages={messages} />
|
|
</TabsContent>
|
|
|
|
<TabsContent value="dm" className="flex-1 flex flex-col mt-0 overflow-hidden data-[state=inactive]:hidden">
|
|
<div className="px-3 sm:px-4 py-2 border-b border-border">
|
|
<p className="text-sm font-medium">
|
|
{dmUserId ? `Chat with User #${dmUserId}` : "Private Messages"}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
Use quick replies for address, directions & hosting notes
|
|
</p>
|
|
</div>
|
|
<MessageList messages={dmMessages} />
|
|
</TabsContent>
|
|
|
|
<TabsContent value="groups" className="flex-1 flex flex-col mt-0 overflow-hidden data-[state=inactive]:hidden">
|
|
<div className="p-3 sm:p-4 space-y-2 overflow-y-auto">
|
|
<GroupRoom name="Leather Night FTL" members={8} />
|
|
<GroupRoom name="Beach Cruisers" members={15} />
|
|
<GroupRoom name="Gym Sauna Squad" members={4} />
|
|
<p className="text-xs text-muted-foreground text-center pt-2">
|
|
Create a group room — free, no limits.
|
|
</p>
|
|
<LocalGuidePanel placement="chat_groups" variant="card" className="mt-4" />
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{showComposer && (
|
|
<div className="border-t border-border">
|
|
{!capabilities.canSendMessages && (
|
|
<div className="px-3 sm:px-4 pt-3">
|
|
<GuestUpgradeBanner reason="chat" />
|
|
</div>
|
|
)}
|
|
{activeTab === "dm" && capabilities.canSendMessages && (
|
|
<div className="px-3 sm:px-4 pt-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowQuickReplies(!showQuickReplies)}
|
|
className="flex items-center gap-1.5 text-xs text-horny-pink font-medium mb-2 touch-manipulation"
|
|
>
|
|
<Zap className="h-3.5 w-3.5" />
|
|
Quick Replies
|
|
{showQuickReplies ? (
|
|
<ChevronUp className="h-3 w-3" />
|
|
) : (
|
|
<ChevronDown className="h-3 w-3" />
|
|
)}
|
|
</button>
|
|
{showQuickReplies && (
|
|
<div className="mb-2">
|
|
<CannedMessagesPanel compact onSelect={(c) => sendMessage(c)} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
<div className="p-3 sm:p-4 flex gap-2 safe-bottom">
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
className="shrink-0 h-11 w-11 touch-manipulation"
|
|
disabled={!capabilities.canSendMessages}
|
|
>
|
|
<ImageIcon className="h-4 w-4" />
|
|
</Button>
|
|
<Input
|
|
placeholder={
|
|
capabilities.canSendMessages
|
|
? "Type a message..."
|
|
: "Sign up free to message..."
|
|
}
|
|
value={input}
|
|
onChange={(e) => setInput(e.target.value)}
|
|
onKeyDown={(e) => e.key === "Enter" && sendMessage()}
|
|
className="flex-1 h-11 text-base"
|
|
disabled={!capabilities.canSendMessages}
|
|
/>
|
|
<Button
|
|
variant="horny"
|
|
size="icon"
|
|
className="shrink-0 h-11 w-11 touch-manipulation"
|
|
onClick={() => sendMessage()}
|
|
disabled={!capabilities.canSendMessages}
|
|
>
|
|
<Send className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Tabs>
|
|
<div ref={bottomRef} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function MessageList({ messages }: { messages: Message[] }) {
|
|
return (
|
|
<div className="flex-1 overflow-y-auto px-3 sm:px-4 py-3 space-y-3 overscroll-contain">
|
|
{messages.map((msg) => (
|
|
<div
|
|
key={msg.id}
|
|
className={`flex flex-col ${msg.isMe ? "items-end" : "items-start"}`}
|
|
>
|
|
<div className="flex items-center gap-2 mb-0.5">
|
|
<span className="text-xs font-semibold text-horny-pink">
|
|
{msg.sender}
|
|
</span>
|
|
<span className="text-[10px] text-muted-foreground">
|
|
{timeAgo(msg.created_at)}
|
|
</span>
|
|
</div>
|
|
<div
|
|
className={`max-w-[85%] sm:max-w-[80%] rounded-xl px-3 py-2.5 text-sm ${
|
|
msg.isMe
|
|
? "bg-horny-pink/20 text-foreground"
|
|
: "bg-secondary text-foreground"
|
|
}`}
|
|
>
|
|
{msg.content}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function GroupRoom({ name, members }: { name: string; members: number }) {
|
|
return (
|
|
<div className="flex items-center justify-between rounded-lg border border-border p-3 hover:border-horny-pink/30 cursor-pointer transition-colors min-h-[56px] touch-manipulation active:bg-secondary/50">
|
|
<div>
|
|
<p className="text-sm font-medium">{name}</p>
|
|
<p className="text-xs text-muted-foreground">{members} members</p>
|
|
</div>
|
|
<Badge variant="online" className="text-[10px]">
|
|
Live
|
|
</Badge>
|
|
</div>
|
|
);
|
|
} |