Introduction
In this second part of our tutorial series, we'll build a beautiful and responsive frontend for our sustainability tips generator using Next.js and React Bootstrap. We'll focus on creating an intuitive user interface with smooth animations and proper error handling.
Check out Part 1
Project Overview
The frontend of SustainAI Tips features:
- A clean, modern user interface
- Responsive design for all screen sizes
- Beautiful card-based tips display
- Loading states and error handling
- Category-based organization with icons
- Smooth animations and transitions
Prerequisites
Before starting, ensure you have:
- Node.js 14 or higher installed
- The backend server from Part 1 running
- Basic knowledge of React and TypeScript
- Familiarity with CSS and animations
Project Structure
frontend/
├── pages/
│ └── index.tsx # Main application page
├── styles/
│ └── globals.css # Global styles
├── package.json # Project dependencies
└── README.md # Frontend documentation
Step 1: Setting Up the Frontend Project
- Create the Next.js project:
npx create-next-app@latest frontend --typescript
cd frontend
- Install required dependencies:
npm install react-bootstrap bootstrap axios react-markdown
- Update
package.json
with the following scripts:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
}
}
Step 2: Implementing the Main Application
Create pages/index.tsx
with the following code:
/**
* SustainAI Tips Frontend
*
* Main page component for the SustainAI Tips application.
* Provides a user interface for generating personalized sustainability tips
* based on location and habits. Features a responsive design with
* animated cards and category-based organization.
*
* @author Antony Ngemu
* @date March 2025
*/
import { useState } from 'react';
import axios from 'axios';
import { Form, Button, Alert, Container, Row, Col, Card, Spinner } from 'react-bootstrap';
import ReactMarkdown from 'react-markdown';
import 'bootstrap/dist/css/bootstrap.min.css';
/**
* Interface for category tips organization
*/
interface CategoryTips {
[key: string]: string[];
}
/**
* Main page component
*/
const Home = () => {
// State management for form inputs and API response
const [location, setLocation] = useState<string>('');
const [habits, setHabits] = useState<string>('');
const [tips, setTips] = useState<string[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
/**
* Handle form submission and API call to generate tips
* @param e - Form submission event
*/
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
setTips([]);
try {
// Make API request to backend
const response = await axios.post('http://localhost:5000/api/tips', {
location,
habits,
});
setTips(response.data.tips);
} catch (err) {
setError('Failed to generate tips. Please try again.');
console.error('Error:', err);
} finally {
setLoading(false);
}
};
/**
* Get the appropriate icon for each category
* @param category - Category name
* @returns Emoji icon for the category
*/
const getCategoryIcon = (category: string): string => {
const icons: { [key: string]: string } = {
'Quick Wins': '⚡',
'Sustainable Living': '🌿',
'Transportation & Mobility': '🚲',
'Community & Social Impact': '🤝',
'Environmental Protection': '🌍',
};
return icons[category] || '🌱';
};
/**
* Render tips organized in category-based cards
* @returns JSX element containing the organized tips
*/
const renderTipCards = () => {
// Initialize categories for organizing tips
const categories: CategoryTips = {
'Quick Wins': [],
'Sustainable Living': [],
'Transportation & Mobility': [],
'Community & Social Impact': [],
'Environmental Protection': [],
};
// Track current category and its tips
let currentCategory = '';
let currentTips: string[] = [];
// Organize tips into categories
tips.forEach((tip) => {
// Check if this is a category header
if (tip.startsWith('## ')) {
const categoryMatch = tip.match(/## \d+\.\s*(.*?)(?:\s*\(.*\))?$/);
if (categoryMatch) {
currentCategory = categoryMatch[1];
currentTips = [];
}
} else if (currentCategory && Object.keys(categories).includes(currentCategory)) {
// Add the tip to the current category
currentTips.push(tip);
categories[currentCategory] = currentTips;
}
});
// Render category cards
return (
<Row xs={1} md={2} lg={3} className="g-4 tips-container">
{Object.entries(categories).map(([category, categoryTips]) => (
categoryTips.length > 0 && (
<Col key={category}>
<Card className="tip-card h-100">
<Card.Header className="category-header">
<div className="category-icon">
{getCategoryIcon(category)}
</div>
<Card.Title className="m-0">{category}</Card.Title>
</Card.Header>
<Card.Body className="d-flex flex-column">
<div className="tip-content">
<ReactMarkdown>
{categoryTips.join('\n\n')}
</ReactMarkdown>
</div>
<div className="tip-footer">
<small className="text-muted">
{categoryTips.length} sustainable {categoryTips.length === 1 ? 'tip' : 'tips'}
</small>
</div>
</Card.Body>
</Card>
</Col>
)
))}
</Row>
);
};
return (
<div className="app-wrapper">
{/* Header Section */}
<header className="app-header">
<Container>
<Row className="justify-content-center">
<Col xs={12} className="text-center">
<h1 className="main-title">
<span className="eco-icon">🌱</span> SustainAI Tips
</h1>
<p className="main-subtitle">
Personalized sustainability tips powered by AI
</p>
</Col>
</Row>
</Container>
</header>
{/* Main Content */}
<Container className="main-content">
<Row className="justify-content-center">
<Col xs={12} md={10} lg={8}>
{/* Input Form */}
<Card className="form-card">
<Card.Body>
<Form onSubmit={handleSubmit}>
<Form.Group className="mb-4" controlId="location">
<Form.Label className="input-label">Your Location</Form.Label>
<Form.Control
type="text"
value={location}
onChange={(e) => setLocation(e.target.value)}
placeholder="e.g., San Francisco, CA"
required
className="custom-input"
/>
<Form.Text className="text-muted">
Enter your city or region for location-specific tips
</Form.Text>
</Form.Group>
<Form.Group className="mb-4" controlId="habits">
<Form.Label className="input-label">Current Habits</Form.Label>
<Form.Control
as="textarea"
rows={3}
value={habits}
onChange={(e) => setHabits(e.target.value)}
placeholder="Tell us about your daily habits (e.g., 'I drive to work, use single-use plastics')"
required
className="custom-input"
/>
</Form.Group>
<div className="text-center">
<Button
variant="success"
type="submit"
disabled={loading}
className="submit-button"
>
{loading ? (
<>
<Spinner as="span" animation="border" size="sm" className="me-2" />
Generating Tips...
</>
) : (
'Get Personalized Tips'
)}
</Button>
</div>
</Form>
</Card.Body>
</Card>
{/* Error Display */}
{error && (
<Alert variant="danger" className="mt-4 error-alert">
{error}
</Alert>
)}
</Col>
</Row>
{/* Tips Display */}
{tips.length > 0 && (
<section className="tips-section">
<h2 className="section-title">Your Personalized Tips</h2>
{renderTipCards()}
</section>
)}
</Container>
{/* Footer */}
<footer className="app-footer">
<Container>
<p className="text-center mb-0">
Made with 💚 for a sustainable future
</p>
</Container>
</footer>
{/* Global Styles */}
<style jsx global>{`
/* Root Variables */
:root {
--primary-color: #198754;
--primary-dark: #146c43;
--bg-color: #f8f9fa;
--text-color: #2c3e50;
--card-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
--hover-transform: translateY(-5px);
--transition-speed: 0.3s;
}
/* Global Styles */
body {
background-color: var(--bg-color);
color: var(--text-color);
}
/* Header Styles */
.app-header {
padding: 4rem 0;
background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
color: white;
}
/* Card Animations */
.tip-card {
transition: all var(--transition-speed);
animation: cardAppear 0.5s ease-out;
}
.tip-card:hover {
transform: var(--hover-transform);
box-shadow: var(--card-shadow);
}
@keyframes cardAppear {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Responsive Design */
@media (max-width: 768px) {
.app-header {
padding: 2rem 0;
}
.main-title {
font-size: 2rem;
}
.tip-card {
margin-bottom: 1rem;
}
}
`}</style>
</div>
);
};
export default Home;
Step 3: Understanding the Implementation
Let's break down the key components of our frontend:
-
Component Structure
- Single page application using Next.js
- Functional component with React hooks
- TypeScript for type safety
- Modular styling with CSS-in-JS
-
State Management
- Form inputs tracked with useState
- Loading and error states for UX
- Tips data organized by categories
-
UI Components
- React Bootstrap for layout and forms
- Custom card design for tips display
- Loading spinner and error alerts
- Responsive grid system
-
Styling Features
- CSS variables for theming
- Smooth animations and transitions
- Mobile-first responsive design
- Custom card and form styling
-
Error Handling
- Form validation
- API error handling
- User-friendly error messages
- Loading states for feedback
Step 4: Running the Application
- Start the development server:
npm run dev
- Open
http://localhost:3000
in your browser
Enhancing the Application
Here are some potential improvements you could add:
-
State Management
- Add Redux or Context for larger applications
- Implement local storage for tip history
- Add user preferences
-
UI Enhancements
- Add dark mode support
- Implement tip sharing
- Add tip favoriting
- Include progress indicators
-
Performance
- Implement SSR for SEO
- Add image optimization
- Implement code splitting
Top comments (0)