Compare commits
10 commits
bd29eefcef
...
e360003061
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e360003061 | ||
![]() |
c4d3c70f8c | ||
![]() |
777409e6cc | ||
![]() |
4dbee045fc | ||
![]() |
6b844ceb1b | ||
![]() |
4d033228b1 | ||
![]() |
ce57fdd5c6 | ||
![]() |
add47667af | ||
![]() |
151e179300 | ||
![]() |
134e7d12e3 |
8
package-lock.json
generated
8
package-lock.json
generated
|
@ -41,7 +41,7 @@
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^4.1.0",
|
||||||
"embla-carousel-react": "^8.3.0",
|
"embla-carousel-react": "^8.3.0",
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
|
@ -4196,9 +4196,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/date-fns": {
|
"node_modules/date-fns": {
|
||||||
"version": "3.6.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||||
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
|
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^4.1.0",
|
||||||
"embla-carousel-react": "^8.3.0",
|
"embla-carousel-react": "^8.3.0",
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
|
|
|
@ -1,42 +1,80 @@
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
import { User } from "lucide-react";
|
import { User } from "lucide-react";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { supabase } from "@/lib/supabase";
|
||||||
|
import { formatDistanceToNow } from "date-fns";
|
||||||
|
|
||||||
interface Conversation {
|
interface Message {
|
||||||
id: string;
|
id: string;
|
||||||
contact: string;
|
phone_number: string;
|
||||||
lastMessage: string;
|
message: string;
|
||||||
timestamp: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockConversations: Conversation[] = [
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
contact: "+1 (555) 987-6543",
|
|
||||||
lastMessage: "Thanks for your message",
|
|
||||||
timestamp: "2 min ago",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
contact: "+1 (555) 876-5432",
|
|
||||||
lastMessage: "I'll get back to you shortly",
|
|
||||||
timestamp: "1 hour ago",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function ConversationList() {
|
export function ConversationList() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const { data: messages, isLoading } = useQuery({
|
||||||
|
queryKey: ['messages'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('messages')
|
||||||
|
.select('*')
|
||||||
|
.order('created_at', { ascending: false });
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error fetching messages:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data as Message[];
|
||||||
|
},
|
||||||
|
refetchInterval: 5000 // Refetch every 5 seconds
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subscribe to new messages
|
||||||
|
React.useEffect(() => {
|
||||||
|
const subscription = supabase
|
||||||
|
.channel('messages')
|
||||||
|
.on('postgres_changes', {
|
||||||
|
event: '*',
|
||||||
|
schema: 'public',
|
||||||
|
table: 'messages'
|
||||||
|
}, () => {
|
||||||
|
// Trigger a refetch when new messages arrive
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['messages'] });
|
||||||
|
})
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
};
|
||||||
|
}, [queryClient]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<p className="text-muted-foreground">Loading conversations...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversations = messages || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 p-4 animate-fadeIn">
|
<div className="space-y-4 p-4 animate-fadeIn">
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<h2 className="text-2xl font-semibold tracking-tight">Conversations</h2>
|
<h2 className="text-2xl font-semibold tracking-tight">Conversations</h2>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{mockConversations.length} active
|
{conversations.length} active
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
{mockConversations.map((conversation) => (
|
{conversations.map((message) => (
|
||||||
<Card
|
<Card
|
||||||
key={conversation.id}
|
key={message.id}
|
||||||
className="p-4 transition-all hover:shadow-md cursor-pointer group"
|
className="p-4 transition-all hover:shadow-md cursor-pointer group"
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
|
@ -45,13 +83,13 @@ export function ConversationList() {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<h3 className="font-medium truncate">{conversation.contact}</h3>
|
<h3 className="font-medium truncate">{message.phone_number}</h3>
|
||||||
<span className="text-xs text-muted-foreground whitespace-nowrap ml-2">
|
<span className="text-xs text-muted-foreground whitespace-nowrap ml-2">
|
||||||
{conversation.timestamp}
|
{formatDistanceToNow(new Date(message.created_at), { addSuffix: true })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground truncate">
|
<p className="text-sm text-muted-foreground truncate">
|
||||||
{conversation.lastMessage}
|
{message.message}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,17 +5,7 @@ import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { createClient } from "@supabase/supabase-js";
|
import { supabase } from "@/lib/supabase";
|
||||||
|
|
||||||
// Initialize Supabase client with proper error handling
|
|
||||||
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
|
|
||||||
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
|
|
||||||
|
|
||||||
if (!supabaseUrl || !supabaseAnonKey) {
|
|
||||||
throw new Error('Supabase URL and Anon Key are required');
|
|
||||||
}
|
|
||||||
|
|
||||||
const supabase = createClient(supabaseUrl, supabaseAnonKey);
|
|
||||||
|
|
||||||
export function MessageComposer() {
|
export function MessageComposer() {
|
||||||
const [message, setMessage] = useState("");
|
const [message, setMessage] = useState("");
|
||||||
|
@ -28,28 +18,35 @@ export function MessageComposer() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Attempting to send message:', { phoneNumber, message });
|
const formattedPhoneNumber = phoneNumber.startsWith('+1') ? phoneNumber : `+1${phoneNumber}`;
|
||||||
|
|
||||||
|
console.log('Attempting to store message:', { phoneNumber: formattedPhoneNumber, message });
|
||||||
setIsSending(true);
|
setIsSending(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Invoking send-sms function...');
|
// Store the message in Supabase
|
||||||
const { data, error } = await supabase.functions.invoke('send-sms', {
|
const { data, error } = await supabase
|
||||||
body: { phoneNumber, message }
|
.from('messages')
|
||||||
});
|
.insert([
|
||||||
|
{
|
||||||
console.log('Response from send-sms:', { data, error });
|
phone_number: formattedPhoneNumber,
|
||||||
|
message: message
|
||||||
|
}
|
||||||
|
])
|
||||||
|
.select();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('Supabase function error:', error);
|
console.error('Supabase insert error:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success("Message sent successfully!");
|
console.log('Message stored successfully:', data);
|
||||||
|
toast.success("Message stored successfully!");
|
||||||
setMessage("");
|
setMessage("");
|
||||||
setPhoneNumber("");
|
setPhoneNumber("");
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('Detailed error:', error);
|
console.error('Detailed error:', error);
|
||||||
toast.error("Failed to send message. Please try again.");
|
toast.error(`Failed to store message: ${error.message || 'Please try again'}`);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSending(false);
|
setIsSending(false);
|
||||||
}
|
}
|
||||||
|
@ -69,7 +66,7 @@ export function MessageComposer() {
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
id="phone"
|
id="phone"
|
||||||
placeholder="Enter phone number..."
|
placeholder="Enter phone number... (e.g. +13109228324)"
|
||||||
value={phoneNumber}
|
value={phoneNumber}
|
||||||
onChange={(e) => setPhoneNumber(e.target.value)}
|
onChange={(e) => setPhoneNumber(e.target.value)}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
|
11
src/lib/supabase.ts
Normal file
11
src/lib/supabase.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
import { createClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
const supabaseUrl = "https://ikmbnngahzcellthaysf.supabase.co";
|
||||||
|
const supabaseAnonKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImlrbWJubmdhaHpjZWxsdGhheXNmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDA1ODY5NjUsImV4cCI6MjA1NjE2Mjk2NX0.puDSeLAaTirOPyj6ndF2Mzzu8CKxlwJsoFP6gK8cWuA";
|
||||||
|
|
||||||
|
if (!supabaseUrl || !supabaseAnonKey) {
|
||||||
|
throw new Error('Supabase URL and Anon Key are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
|
Loading…
Reference in a new issue