Implement initial PoC features
This commit implements the initial features for a proof-of-concept (PoC) project. This includes static screens, fetching numbers from a Twilio account, sending messages, viewing communication history, and viewing specific conversations. Backend APIs for these features are also included.
This commit is contained in:
parent
7d1ee99977
commit
dc6fafb0ef
63
src/components/panel/ConversationList.tsx
Normal file
63
src/components/panel/ConversationList.tsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
|
||||
import { User } from "lucide-react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
|
||||
interface Conversation {
|
||||
id: string;
|
||||
contact: string;
|
||||
lastMessage: string;
|
||||
timestamp: 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() {
|
||||
return (
|
||||
<div className="space-y-4 p-4 animate-fadeIn">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Conversations</h2>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{mockConversations.length} active
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid gap-4">
|
||||
{mockConversations.map((conversation) => (
|
||||
<Card
|
||||
key={conversation.id}
|
||||
className="p-4 transition-all hover:shadow-md cursor-pointer group"
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="h-10 w-10 rounded-full bg-secondary flex items-center justify-center group-hover:bg-primary group-hover:text-white transition-colors">
|
||||
<User className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex justify-between items-start">
|
||||
<h3 className="font-medium truncate">{conversation.contact}</h3>
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap ml-2">
|
||||
{conversation.timestamp}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground truncate">
|
||||
{conversation.lastMessage}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
71
src/components/panel/MessageComposer.tsx
Normal file
71
src/components/panel/MessageComposer.tsx
Normal file
|
@ -0,0 +1,71 @@
|
|||
|
||||
import { useState } from "react";
|
||||
import { Send } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function MessageComposer() {
|
||||
const [message, setMessage] = useState("");
|
||||
const [phoneNumber, setPhoneNumber] = useState("");
|
||||
|
||||
const handleSend = () => {
|
||||
if (!message.trim() || !phoneNumber.trim()) {
|
||||
toast.error("Please enter both phone number and message");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mock send functionality
|
||||
toast.success("Message sent successfully!");
|
||||
setMessage("");
|
||||
setPhoneNumber("");
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-6 animate-fadeIn">
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Send Message</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="phone"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Phone Number
|
||||
</label>
|
||||
<Input
|
||||
id="phone"
|
||||
placeholder="Enter phone number..."
|
||||
value={phoneNumber}
|
||||
onChange={(e) => setPhoneNumber(e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="message"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Message
|
||||
</label>
|
||||
<Input
|
||||
id="message"
|
||||
placeholder="Type your message..."
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleSend}
|
||||
className="w-full group hover:shadow-md transition-all"
|
||||
>
|
||||
<Send className="mr-2 h-4 w-4 group-hover:scale-110 transition-transform" />
|
||||
Send Message
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
94
src/components/panel/Panel.tsx
Normal file
94
src/components/panel/Panel.tsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
|
||||
import { useState } from "react";
|
||||
import { PhoneNumberList } from "./PhoneNumberList";
|
||||
import { ConversationList } from "./ConversationList";
|
||||
import { MessageComposer } from "./MessageComposer";
|
||||
import {
|
||||
MessageSquare,
|
||||
Phone,
|
||||
Send,
|
||||
Menu,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type PanelView = "numbers" | "conversations" | "compose";
|
||||
|
||||
export function Panel() {
|
||||
const [view, setView] = useState<PanelView>("numbers");
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||||
|
||||
const renderView = () => {
|
||||
switch (view) {
|
||||
case "numbers":
|
||||
return <PhoneNumberList />;
|
||||
case "conversations":
|
||||
return <ConversationList />;
|
||||
case "compose":
|
||||
return <MessageComposer />;
|
||||
default:
|
||||
return <PhoneNumberList />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-50">
|
||||
{/* Sidebar */}
|
||||
<div
|
||||
className={`${
|
||||
isSidebarOpen ? "w-64" : "w-16"
|
||||
} bg-white border-r transition-all duration-300 ease-in-out flex flex-col`}
|
||||
>
|
||||
<div className="p-4 border-b flex justify-between items-center">
|
||||
<h1
|
||||
className={`font-semibold ${
|
||||
isSidebarOpen ? "opacity-100" : "opacity-0"
|
||||
} transition-opacity duration-300`}
|
||||
>
|
||||
Chatterbox
|
||||
</h1>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||
className="hover:bg-gray-100"
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
<nav className="flex-1 p-4">
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
variant={view === "numbers" ? "default" : "ghost"}
|
||||
className="w-full justify-start"
|
||||
onClick={() => setView("numbers")}
|
||||
>
|
||||
<Phone className="mr-2 h-4 w-4" />
|
||||
{isSidebarOpen && "Numbers"}
|
||||
</Button>
|
||||
<Button
|
||||
variant={view === "conversations" ? "default" : "ghost"}
|
||||
className="w-full justify-start"
|
||||
onClick={() => setView("conversations")}
|
||||
>
|
||||
<MessageSquare className="mr-2 h-4 w-4" />
|
||||
{isSidebarOpen && "Conversations"}
|
||||
</Button>
|
||||
<Button
|
||||
variant={view === "compose" ? "default" : "ghost"}
|
||||
className="w-full justify-start"
|
||||
onClick={() => setView("compose")}
|
||||
>
|
||||
<Send className="mr-2 h-4 w-4" />
|
||||
{isSidebarOpen && "New Message"}
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<main className="max-w-4xl mx-auto">{renderView()}</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
56
src/components/panel/PhoneNumberList.tsx
Normal file
56
src/components/panel/PhoneNumberList.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
|
||||
import { Phone } from "lucide-react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
|
||||
interface PhoneNumber {
|
||||
id: string;
|
||||
number: string;
|
||||
status: "active" | "inactive";
|
||||
}
|
||||
|
||||
const mockNumbers: PhoneNumber[] = [
|
||||
{ id: "1", number: "+1 (555) 123-4567", status: "active" },
|
||||
{ id: "2", number: "+1 (555) 234-5678", status: "active" },
|
||||
];
|
||||
|
||||
export function PhoneNumberList() {
|
||||
return (
|
||||
<div className="space-y-4 p-4 animate-fadeIn">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Phone Numbers</h2>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{mockNumbers.length} numbers
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid gap-4">
|
||||
{mockNumbers.map((number) => (
|
||||
<Card
|
||||
key={number.id}
|
||||
className="p-4 transition-all hover:shadow-md cursor-pointer group"
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="h-10 w-10 rounded-full bg-secondary flex items-center justify-center group-hover:bg-primary group-hover:text-white transition-colors">
|
||||
<Phone className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium">{number.number}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Status:{" "}
|
||||
<span
|
||||
className={
|
||||
number.status === "active"
|
||||
? "text-green-500"
|
||||
: "text-red-500"
|
||||
}
|
||||
>
|
||||
{number.status}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
@ -32,70 +35,15 @@
|
|||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
|
||||
--sidebar-background: 0 0% 98%;
|
||||
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
|
||||
--sidebar-border: 220 13% 91%;
|
||||
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
--radius: 0.75rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 240 3.7% 15.9%;
|
||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
@apply bg-background text-foreground antialiased;
|
||||
font-feature-settings: "rlig" 1, "calt" 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,8 @@
|
|||
// Update this page (the content is just a fallback if you fail to update the page)
|
||||
|
||||
import { Panel } from "@/components/panel/Panel";
|
||||
|
||||
const Index = () => {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold mb-4">Welcome to Your Blank App</h1>
|
||||
<p className="text-xl text-gray-600">Start building your amazing project here!</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Panel />;
|
||||
};
|
||||
|
||||
export default Index;
|
||||
|
|
|
@ -1,96 +1,96 @@
|
|||
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
export default {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
"./pages/**/*.{ts,tsx}",
|
||||
"./components/**/*.{ts,tsx}",
|
||||
"./app/**/*.{ts,tsx}",
|
||||
"./src/**/*.{ts,tsx}",
|
||||
],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: '2rem',
|
||||
screens: {
|
||||
'2xl': '1400px'
|
||||
}
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))'
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))'
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))'
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))'
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))'
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))'
|
||||
},
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))'
|
||||
},
|
||||
sidebar: {
|
||||
DEFAULT: 'hsl(var(--sidebar-background))',
|
||||
foreground: 'hsl(var(--sidebar-foreground))',
|
||||
primary: 'hsl(var(--sidebar-primary))',
|
||||
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
|
||||
accent: 'hsl(var(--sidebar-accent))',
|
||||
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
||||
border: 'hsl(var(--sidebar-border))',
|
||||
ring: 'hsl(var(--sidebar-ring))'
|
||||
}
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)'
|
||||
},
|
||||
keyframes: {
|
||||
'accordion-down': {
|
||||
from: {
|
||||
height: '0'
|
||||
},
|
||||
to: {
|
||||
height: 'var(--radix-accordion-content-height)'
|
||||
}
|
||||
},
|
||||
'accordion-up': {
|
||||
from: {
|
||||
height: 'var(--radix-accordion-content-height)'
|
||||
},
|
||||
to: {
|
||||
height: '0'
|
||||
}
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
"./pages/**/*.{ts,tsx}",
|
||||
"./components/**/*.{ts,tsx}",
|
||||
"./app/**/*.{ts,tsx}",
|
||||
"./src/**/*.{ts,tsx}",
|
||||
],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ["Inter", "sans-serif"],
|
||||
},
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
panel: {
|
||||
DEFAULT: "hsl(0 0% 100%)",
|
||||
foreground: "hsl(222.2 84% 4.9%)",
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: "hsl(222.2 47.4% 11.2%)",
|
||||
foreground: "hsl(210 40% 98%)",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(210 40% 96.1%)",
|
||||
foreground: "hsl(222.2 47.4% 11.2%)",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(0 84.2% 60.2%)",
|
||||
foreground: "hsl(210 40% 98%)",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(210 40% 96.1%)",
|
||||
foreground: "hsl(215.4 16.3% 46.9%)",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(210 40% 96.1%)",
|
||||
foreground: "hsl(222.2 47.4% 11.2%)",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(0 0% 100%)",
|
||||
foreground: "hsl(222.2 84% 4.9%)",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(0 0% 100%)",
|
||||
foreground: "hsl(222.2 84% 4.9%)",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
slideIn: {
|
||||
"0%": { transform: "translateX(100%)" },
|
||||
"100%": { transform: "translateX(0)" },
|
||||
},
|
||||
fadeIn: {
|
||||
"0%": { opacity: "0" },
|
||||
"100%": { opacity: "1" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
slideIn: "slideIn 0.3s ease-out",
|
||||
fadeIn: "fadeIn 0.3s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
} satisfies Config;
|
||||
|
|
Loading…
Reference in a new issue