DEV Community

Cover image for Building a Sustainable Living Tips Generator with Next.js, Flask, and Google Gemini AI - Part 2: Frontend Development
ngemuantony
ngemuantony

Posted on

Building a Sustainable Living Tips Generator with Next.js, Flask, and Google Gemini AI - Part 2: Frontend Development

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
Enter fullscreen mode Exit fullscreen mode

Step 1: Setting Up the Frontend Project

  1. Create the Next.js project:
npx create-next-app@latest frontend --typescript
cd frontend
Enter fullscreen mode Exit fullscreen mode
  1. Install required dependencies:
npm install react-bootstrap bootstrap axios react-markdown
Enter fullscreen mode Exit fullscreen mode
  1. Update package.json with the following scripts:
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Step 3: Understanding the Implementation

Let's break down the key components of our frontend:

  1. Component Structure

    • Single page application using Next.js
    • Functional component with React hooks
    • TypeScript for type safety
    • Modular styling with CSS-in-JS
  2. State Management

    • Form inputs tracked with useState
    • Loading and error states for UX
    • Tips data organized by categories
  3. UI Components

    • React Bootstrap for layout and forms
    • Custom card design for tips display
    • Loading spinner and error alerts
    • Responsive grid system
  4. Styling Features

    • CSS variables for theming
    • Smooth animations and transitions
    • Mobile-first responsive design
    • Custom card and form styling
  5. Error Handling

    • Form validation
    • API error handling
    • User-friendly error messages
    • Loading states for feedback

Step 4: Running the Application

  1. Start the development server:
npm run dev
Enter fullscreen mode Exit fullscreen mode
  1. Open http://localhost:3000 in your browser

Enhancing the Application

Here are some potential improvements you could add:

  1. State Management

    • Add Redux or Context for larger applications
    • Implement local storage for tip history
    • Add user preferences
  2. UI Enhancements

    • Add dark mode support
    • Implement tip sharing
    • Add tip favoriting
    • Include progress indicators
  3. Performance

    • Implement SSR for SEO
    • Add image optimization
    • Implement code splitting

Resources


Heroku

Amplify your impact where it matters most — building exceptional apps.

Leave the infrastructure headaches to us, while you focus on pushing boundaries, realizing your vision, and making a lasting impression on your users.

Get Started

Top comments (0)

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay