Skip to content
Draft
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
115 changes: 115 additions & 0 deletions app/api/sendSMS/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { NextRequest, NextResponse } from "next/server";
import { SMSService } from "@/lib/sms-service";
import { SMSRequest, SMSResponse } from "@/lib/types/sms";

export async function POST(request: NextRequest) {
try {
// Parse request body
const body: SMSRequest = await request.json();

// Validate required fields
if (!body.to || !body.message) {
return NextResponse.json(
{
success: false,
message: "Missing required fields: 'to' and 'message' are required",
} satisfies SMSResponse,
{ status: 400 }
);
}

// Initialize SMS service
const smsService = new SMSService();

// Validate phone numbers
const recipients = Array.isArray(body.to) ? body.to : [body.to];
for (const phone of recipients) {
if (!smsService.validatePhoneNumber(phone)) {
return NextResponse.json(
{
success: false,
message: `Invalid phone number format: ${phone}. Use international format (+1234567890)`,
} satisfies SMSResponse,
{ status: 400 }
);
}
}

// Validate message
if (!smsService.validateMessage(body.message)) {
return NextResponse.json(
{
success: false,
message: "Invalid message: must be between 1 and 1600 characters",
} satisfies SMSResponse,
{ status: 400 }
);
}

// Send SMS
const result = await smsService.sendSMS(body.to, body.message, body.from);

// Return success response
return NextResponse.json(
{
success: true,
message: "SMS sent successfully",
data: {
messageId: result.data?.message_id,
status: result.data?.status,
recipients: result.data?.recipients,
cost: result.data?.cost,
},
} satisfies SMSResponse,
{ status: 200 }
);

} catch (error) {
console.error("SMS API error:", error);

// Handle specific error cases
if (error instanceof Error && error.message.includes("MBOA_SMS_API_KEY")) {
return NextResponse.json(
{
success: false,
message: "SMS service not configured",
error: "Missing API configuration",
} satisfies SMSResponse,
{ status: 500 }
);
}

return NextResponse.json(
{
success: false,
message: "Failed to send SMS",
error: error instanceof Error ? error.message : "Unknown error",
} satisfies SMSResponse,
{ status: 500 }
);
}
}

// Handle GET requests - return API documentation
export async function GET() {
return NextResponse.json(
{
message: "SMS API Endpoint",
method: "POST",
endpoint: "/api/sendSMS",
requiredFields: {
to: "string | string[] - Phone number(s) in international format (+1234567890)",
message: "string - SMS content (1-1600 characters)",
},
optionalFields: {
from: "string - Sender ID (if supported by provider)",
},
example: {
to: "+1234567890",
message: "Hello from PlaceRank!",
from: "PlaceRank",
},
},
{ status: 200 }
);
}
15 changes: 1 addition & 14 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,6 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});

const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
Expand All @@ -24,9 +13,7 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<body className="antialiased">
{children}
</body>
</html>
Expand Down
6 changes: 6 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ export default function Home() {
/>
Go to nextjs.org →
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="/sms-test"
>
📱 Test SMS API
</a>
</footer>
</div>
);
Expand Down
8 changes: 8 additions & 0 deletions app/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function ProfilePage() {
return (
<div>
<h1>Profile Page</h1>
<p>Profile functionality coming soon...</p>
</div>
);
}
141 changes: 141 additions & 0 deletions app/sms-test/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
"use client";

import { useState } from "react";
import { SMSResponse } from "@/lib/types/sms";

export default function SMSTestPage() {
const [to, setTo] = useState("");
const [message, setMessage] = useState("");
const [from, setFrom] = useState("");
const [result, setResult] = useState<SMSResponse | null>(null);
const [loading, setLoading] = useState(false);

const sendSMS = async () => {
setLoading(true);
setResult(null);

try {
const response = await fetch("/api/sendSMS", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
to,
message,
...(from && { from }),
}),
});

const data = await response.json();
setResult(data);
} catch (error) {
setResult({
success: false,
message: "Failed to send SMS",
error: error instanceof Error ? error.message : "Unknown error",
});
} finally {
setLoading(false);
}
};

return (
<div className="min-h-screen p-8">
<div className="max-w-2xl mx-auto">
<h1 className="text-3xl font-bold mb-8">SMS API Test</h1>

<div className="space-y-4">
<div>
<label htmlFor="to" className="block text-sm font-medium mb-2">
Phone Number (required)
</label>
<input
id="to"
type="text"
placeholder="+1234567890"
value={to}
onChange={(e) => setTo(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<p className="text-sm text-gray-500 mt-1">
Use international format (+1234567890)
</p>
</div>

<div>
<label htmlFor="message" className="block text-sm font-medium mb-2">
Message (required)
</label>
<textarea
id="message"
placeholder="Your SMS message here..."
value={message}
onChange={(e) => setMessage(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={4}
/>
<p className="text-sm text-gray-500 mt-1">
{message.length}/1600 characters
</p>
</div>

<div>
<label htmlFor="from" className="block text-sm font-medium mb-2">
Sender ID (optional)
</label>
<input
id="from"
type="text"
placeholder="PlaceRank"
value={from}
onChange={(e) => setFrom(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>

<button
onClick={sendSMS}
disabled={loading || !to || !message}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{loading ? "Sending..." : "Send SMS"}
</button>
</div>

{result && (
<div className="mt-8">
<h2 className="text-xl font-bold mb-4">Result</h2>
<div
className={`p-4 rounded-md ${
result.success
? "bg-green-100 border border-green-300"
: "bg-red-100 border border-red-300"
}`}
>
<pre className="whitespace-pre-wrap text-sm">
{JSON.stringify(result, null, 2)}
</pre>
</div>
</div>
)}

<div className="mt-8">
<h2 className="text-xl font-bold mb-4">API Documentation</h2>
<div className="bg-gray-100 p-4 rounded-md">
<p className="text-sm">
This page demonstrates the <code>/api/sendSMS</code> endpoint.
</p>
<p className="text-sm mt-2">
To use with a real API key, update your <code>.env.local</code> file
with your mboasms credentials.
</p>
<p className="text-sm mt-2">
See <code>docs/SMS_API.md</code> for complete documentation.
</p>
</div>
</div>
</div>
</div>
);
}
Loading