Part 2: Configure Frontend with Amplify
Step 2.1: Install Dependencies
cd Treat-Dispenser-Frontend
npm install aws-amplify @aws-amplify/ui-react Expected output:
added 244 packages, changed 30 packages, and audited 1046 packages in 22s Note: You may see security vulnerability warnings - this is normal. Run npm audit fix to resolve most issues.
Step 2.2: Create Environment Variables
Create .env.local file (or update existing):
# Create/edit .env.local
vim .env.local Add these values (replace with your actual values from Step 1.8):
# Existing AWS IoT config
NEXT_PUBLIC_AWS_REGION=us-east-1
NEXT_PUBLIC_AWS_IOT_ENDPOINT=xxxxx-ats.iot.us-east-1.amazonaws.com
AWS_ACCESS_KEY_ID=your-key-id
AWS_SECRET_ACCESS_KEY=your-secret-key
# NEW: Cognito configuration
NEXT_PUBLIC_USER_POOL_ID=us-east-1_XXXXXXXXX
NEXT_PUBLIC_USER_POOL_CLIENT_ID=1234567890abcdefghijklmnop ⚠️ Important:
- Values prefixed with
NEXT_PUBLIC_are accessible in the browser - Never prefix secrets like
AWS_SECRET_ACCESS_KEYwithNEXT_PUBLIC_
For this project I am using this homepage. This will work for any Amplify configuration that follows a similar architecture
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { getCurrentUser, signOut } from "aws-amplify/auth";
export default function Home() {
const router = useRouter();
const [user, setUser] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [testStatus, setTestStatus] = useState<string>("");
const [testLoading, setTestLoading] = useState(false);
const [dispenseStatus, setDispenseStatus] = useState<string>("");
const [dispenseLoading, setDispenseLoading] = useState(false);
useEffect(() => {
checkUser();
}, []);
async function checkUser() {
try {
const currentUser = await getCurrentUser();
setUser(currentUser);
setLoading(false);
} catch (error) {
// Not logged in, redirect to login page
router.push("/login");
}
}
async function handleSignOut() {
try {
await signOut();
router.push("/login");
} catch (error) {
console.error("Error signing out:", error);
}
}
if (loading) {
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "100vh",
}}
>
<p>Loading...</p>
</div>
);
}
const testPublish = async () => {
setTestLoading(true);
setTestStatus("Publishing test message...");
try {
const response = await fetch("/api/iot/publish", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
topic: "TreatDispenser/commands",
message: { action: "test", timestamp: new Date().toISOString() },
}),
});
const data = await response.json();
if (response.ok) {
setTestStatus(`✓ Success! Published to ${data.topic}`);
} else {
setTestStatus(`✗ Error: ${data.error}`);
}
} catch (error) {
setTestStatus(`✗ Failed to connect: ${error}`);
} finally {
setTestLoading(false);
}
};
const dispensePublish = async () => {
setDispenseLoading(true);
setDispenseStatus("Publishing dispense treat message...");
try {
const response = await fetch("/api/iot/publish", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
topic: "TreatDispenser/commands",
message: {
action: "DISPENSE",
timestamp: new Date().toISOString(),
requestedBy: "Web_UI",
},
}),
});
const data = await response.json();
if (response.ok) {
setDispenseStatus(
`✓ Success! Published dispense message to ${data.topic}`,
);
} else {
setDispenseStatus(`✗ Error: ${data.error}`);
}
} catch (error) {
setDispenseStatus(`✗ Failed to connect: ${error}`);
} finally {
setDispenseLoading(false);
}
};
return (
<main
style={{
minHeight: "100vh",
background: "linear-gradient(to bottom, #f0f4f8, #e2e8f0)",
padding: "2rem",
}}
>
<div style={{ maxWidth: "1200px", margin: "0 auto" }}>
{/* Header with user info and sign out button */}
<header
style={{
marginBottom: "2rem",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<div>
<h1
style={{
fontSize: "2.5rem",
fontWeight: "bold",
color: "#1a202c",
marginBottom: "0.5rem",
}}
>
🐱 Cat Treat Dispenser
</h1>
<p style={{ color: "#718096", fontSize: "1.1rem" }}>
Welcome, {user?.username || "Guest"}!
</p>
</div>
<div style={{ display: "flex", gap: "1rem" }}>
<button
onClick={() => router.push("/devices")}
style={{
padding: "0.5rem 1rem",
background: "#4299e1",
color: "white",
border: "none",
borderRadius: "4px",
cursor: "pointer",
fontSize: "1rem",
}}
>
My Devices
</button>
<button
onClick={handleSignOut}
style={{
padding: "0.5rem 1rem",
background: "#e53e3e",
color: "white",
border: "none",
borderRadius: "4px",
cursor: "pointer",
fontSize: "1rem",
}}
>
Sign Out
</button>
</div>
</header>
{/* Test Connection Button */}
<div style={{ marginTop: "1.5rem" }}>
<button
onClick={testPublish}
disabled={testLoading}
style={{
background: testLoading ? "#cbd5e0" : "#4299e1",
color: "white",
padding: "0.75rem 1.5rem",
borderRadius: "8px",
border: "none",
fontSize: "1rem",
fontWeight: "600",
cursor: testLoading ? "not-allowed" : "pointer",
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
}}
>
{testLoading ? "Testing..." : "Test AWS IoT Connection"}
</button>
{testStatus && (
<p
style={{
marginTop: "0.75rem",
color: testStatus.startsWith("✓") ? "#38a169" : "#e53e3e",
fontWeight: "500",
}}
>
{testStatus}
</p>
)}
</div>
{/* Test Connection Button */}
<div style={{ marginTop: "1.5rem" }}>
<button
onClick={dispensePublish}
disabled={dispenseLoading}
style={{
background: dispenseLoading ? "#cbd5e0" : "#4299e1",
color: "white",
padding: "0.75rem 1.5rem",
borderRadius: "8px",
border: "none",
fontSize: "1rem",
fontWeight: "600",
cursor: dispenseLoading ? "not-allowed" : "pointer",
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
}}
>
{dispenseLoading
? "Publishing dispense message to MQTT broker..."
: "Publishes dispense message to MQTT broker"}
</button>
{dispenseStatus && (
<p
style={{
marginTop: "0.75rem",
color: dispenseStatus.startsWith("✓") ? "#38a169" : "#e53e3e",
fontWeight: "500",
}}
>
{dispenseStatus}
</p>
)}
</div>
{/* Dashboard Grid */}
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
gap: "3rem",
}}
>
{/* Cat Profiles Card */}
<div
style={{
background: "white",
borderRadius: "12px",
padding: "1.5rem",
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
}}
>
<h2
style={{
fontSize: "1.5rem",
fontWeight: "600",
marginBottom: "1rem",
color: "#2d3748",
}}
>
Cat Profiles
</h2>
<p style={{ color: "#718096" }}>No cats registered yet.</p>
</div>
{/* Recent Activity Card */}
<div
style={{
background: "white",
borderRadius: "12px",
padding: "1.5rem",
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
}}
>
<h2
style={{
fontSize: "1.5rem",
fontWeight: "600",
marginBottom: "1rem",
color: "#2d3748",
}}
>
Recent Activity
</h2>
<p style={{ color: "#718096" }}>No recent treats dispensed.</p>
</div>
</div>
{/*Where you cat is at*/}
<div
style={{
background: "white",
borderRadius: "12px",
padding: "1.5rem",
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
marginTop: "2.5rem",
}}
>
<h2
style={{
fontSize: "1.5rem",
fontWeight: "600",
marginBottom: "1rem",
color: "#2d3748",
}}
>
Where your Cat is at
</h2>
<p style={{ color: "#718096" }}>The cat is located:</p>
</div>
</div>
</main>
);
} Step 2.3: Configure Amplify
Create a new file for Amplify configuration:
Treat-Dispenser-Frontend/
├── app/
├── docs/
├── lib/ ← Create this folder
│ └── amplify-config.ts
├── package.json
└── ... // lib/amplify-config.ts
import { Amplify } from "aws-amplify";
export function configureAmplify() {
Amplify.configure({
Auth: {
Cognito: {
userPoolId: process.env.NEXT_PUBLIC_USER_POOL_ID!,
userPoolClientId: process.env.NEXT_PUBLIC_USER_POOL_CLIENT_ID!,
loginWith: {
email: true,
},
signUpVerificationMethod: "code",
userAttributes: {
email: {
required: true,
},
name: {
required: false,
},
},
passwordFormat: {
minLength: 8,
requireLowercase: true,
requireUppercase: true,
requireNumbers: true,
requireSpecialCharacters: true,
},
},
},
});
} Step 2.4: Update Layout to Initialize Amplify
Update your root layout:
(TODO: Update file tree)
Treat-Dispenser-Frontend/
├── app/
|
├── docs/
├── lib/ ← Create this folder
│ └── amplify-config.ts
├── package.json
└── ... "use client";
import { useEffect } from "react";
import { configureAmplify } from "@/lib/amplify-config";
import "./globals.css";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
useEffect(() => {
configureAmplify();
}, []);
return (
<html lang="en">
<body>{children}</body>
</html>
);
} Step 2.5: Create Login Page with Authenticator
Create a new login page:
(TODO: Update file tree)
Treat-Dispenser-Frontend/
├── app/
|
├── docs/
├── lib/ ← Create this folder
│ └── amplify-config.ts
├── package.json
└── ... "use client";
import { Authenticator } from "@aws-amplify/ui-react";
import "@aws-amplify/ui-react/styles.css";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
function RedirectIfAuthenticated({ user }: { user: any }) {
const router = useRouter();
useEffect(() => {
if (user) {
router.push("/");
}
}, [user, router]);
return null;
}
export default function LoginPage() {
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "100vh",
background: "linear-gradient(to bottom, #f0f4f8, #e2e8f0)",
}}
>
<div
style={{
background: "white",
padding: "2rem",
borderRadius: "8px",
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
}}
>
<h1 style={{ textAlign: "center", marginBottom: "2rem" }}>
🐱 Cat Treat Dispenser
</h1>
<Authenticator
signUpAttributes={["email", "name"]}
socialProviders={[]}
>
{({ user }) => <RedirectIfAuthenticated user={user} />}
</Authenticator>
</div>
</div>
);
}