v2: ID verification by state, 15 photos/5 albums, 20 quick replies, logo, mobile polish

This commit is contained in:
Ryan Salazar
2026-06-26 21:08:57 -04:00
parent c6238c3bcf
commit e03d929977
121 changed files with 6380 additions and 153 deletions

View File

@@ -5,8 +5,9 @@ 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 } from "lucide-react";
import { Send, Image as ImageIcon, Users, Lock, Zap, ChevronDown, ChevronUp } from "lucide-react";
import { timeAgo } from "@/lib/utils";
import { CannedMessagesPanel } from "@/components/CannedMessagesPanel";
interface Message {
id: string;
@@ -41,7 +42,7 @@ const MOCK_CITY_MESSAGES: Message[] = [
{
id: "4",
sender: "PupRex",
content: "Woof! At Ramrod later if anyone wants to meet up 🐾",
content: "Woof! At Ramrod later if anyone wants to meet up",
created_at: new Date(Date.now() - 60000).toISOString(),
},
];
@@ -71,18 +72,20 @@ export function ChatPanel({ dmUserId }: ChatPanelProps) {
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 = () => {
if (!input.trim()) return;
const sendMessage = (text?: string) => {
const content = (text ?? input).trim();
if (!content) return;
const msg: Message = {
id: Date.now().toString(),
sender: "You",
content: input.trim(),
content,
created_at: new Date().toISOString(),
isMe: true,
};
@@ -93,29 +96,31 @@ export function ChatPanel({ dmUserId }: ChatPanelProps) {
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-4 mt-2 grid grid-cols-3">
<TabsTrigger value="city" className="gap-1 text-xs">
<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" />
{CITY}
<span className="truncate">{CITY}</span>
</TabsTrigger>
<TabsTrigger value="dm" className="gap-1 text-xs">
<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">
<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">
<div className="px-4 py-2 border-b border-border">
<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>
@@ -126,17 +131,20 @@ export function ChatPanel({ dmUserId }: ChatPanelProps) {
<MessageList messages={messages} />
</TabsContent>
<TabsContent value="dm" className="flex-1 flex flex-col mt-0 overflow-hidden">
<div className="px-4 py-2 border-b border-border">
<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">
<div className="p-4 space-y-2">
<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} />
@@ -146,21 +154,50 @@ export function ChatPanel({ dmUserId }: ChatPanelProps) {
</div>
</TabsContent>
{(activeTab === "city" || activeTab === "dm") && (
<div className="p-4 border-t border-border flex gap-2 safe-bottom">
<Button variant="outline" size="icon">
<ImageIcon className="h-4 w-4" />
</Button>
<Input
placeholder="Type a message..."
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && sendMessage()}
className="flex-1"
/>
<Button variant="horny" size="icon" onClick={sendMessage}>
<Send className="h-4 w-4" />
</Button>
{showComposer && (
<div className="border-t border-border">
{activeTab === "dm" && (
<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">
<ImageIcon className="h-4 w-4" />
</Button>
<Input
placeholder="Type a message..."
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && sendMessage()}
className="flex-1 h-11 text-base"
/>
<Button
variant="horny"
size="icon"
className="shrink-0 h-11 w-11 touch-manipulation"
onClick={() => sendMessage()}
>
<Send className="h-4 w-4" />
</Button>
</div>
</div>
)}
</Tabs>
@@ -171,7 +208,7 @@ export function ChatPanel({ dmUserId }: ChatPanelProps) {
function MessageList({ messages }: { messages: Message[] }) {
return (
<div className="flex-1 overflow-y-auto px-4 py-3 space-y-3">
<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}
@@ -186,7 +223,7 @@ function MessageList({ messages }: { messages: Message[] }) {
</span>
</div>
<div
className={`max-w-[80%] rounded-xl px-3 py-2 text-sm ${
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"
@@ -202,7 +239,7 @@ function MessageList({ messages }: { messages: Message[] }) {
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">
<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>