Skip to content

Image Viewer Tab

Projects Involved: GCS

Concepts: React, Typescript, CSS, HTTP Handling

Abstract:

Create a new tab in the React frontend that fetches and displays an image from the existing Go backend. This task introduces essential React hooks and state management, basic TypeScript typing, CSS styling, and a simple HTTP request flow. Use StatusPage and PlaygroundPage as references for structure, routing, and styling conventions.

Goals:

  • Learn some basic React
  • Be able to create new Frontend components for GCS features

Files Involved:

GCS

Reference files:

  • src/pages/StatusPage.tsx (HTTP Handling)
  • src/pages/PlaygroundPage.tsx (Clean examples of hooks and state management)

Files to create/work on:

  • src/pages/ImagePage.tsx (new page)
  • src/styles/ImagePage.css (optional; you may do the css inline as well)
  • src/App.tsx (tabs are registered here)

Base Steps:

1. Explore StatusPage and PlaygroundPage

These existing pages are written so you can understand how tabs are structured, routed, and styled. Scaffold a new tab that follows similar conventions.

2. Write a new tab

Try copying the existing logic by adding a new Tab and verifying the tab works on click. This step should be trivial just by looking at how other tabs are wired.

3. Add React state

There a couple react states that are required for each page. One for loading, error, and image data. Use an effect to trigger a fetch when the page mounts or when a refresh action occurs.

Refer to hints if you are stuck

4. Implement a GET request

Recall the overview of the API lifecycle from task 7. You need to implement the /capture GET request to the Go endpoint you implemented, which should provide an image in base64. Parse the response appropriately and display the image. Show loading and error states.

5. Define TypeScript types

Define TypeScript types for the response you expect. Keep props and component state typed and avoid any.

6. Add some basic CSS

Add some basic CSS that aligns with the existing design and make sure the image scales well and has sensible max dimensions.

7. Run the code

Manually verify the full flow: loading indicator, image renders, errors handled gracefully, and retry/refresh works.

Tips:

1: If you are new to react...
2: To display the image... You don't need to do any fancy magic to convert the base64 string into an actual image. Simply using the img tag like below is enough.
<img src={`data:image/png;base64,${image_b64_string}`} />
3: If you are completely lost... Use the boilerplate below as a starting point, and fill out the TODOs. Look at StatusPage.tsx, there are two API calls there that might be relevant.
import { useState } from "react";
import "./ImagePage.css";

const ImagePage = () => {
    const [imageB64, setImageB64] = useState<string | null>(null);
    const [error, setError] = useState<string | null>(null);
    const [isLoading, setIsLoading] = useState<boolean>(false);

    const fetchImage = () => {
        setIsLoading(true);
        setError(null);
        setImageB64(null);

        fetch(`/api/v1/obc/capture`)
            .then(async (response) => {
                if (!response.ok) {
                    // TODO
                }
                return response.text();
            })
            .then((text: string) => {
                // TODO
            })
            .catch((err: Error) => {
                // TODO
            })
            .finally(() => {
                // TODO
            });
    };

    const handleRefresh = () => {
        fetchImage();
    };

    const dataUrl = imageB64 ? `data:image/png;base64,${imageB64}` : null;

    return (
        <div style={{ fontFamily: "sans-serif", padding: "20px" }}>
            <h2>Image Viewer</h2>

            <div style={{ display: "flex", gap: 10, alignItems: "center" }}>
                <button onClick={handleRefresh} disabled={isLoading}>
                    {isLoading ? "Loading..." : "Load / Reload Image"}
                </button>
            </div>

            <section
                style={{
                    marginTop: "20px",
                    padding: "12px",
                    border: "1px solid #ccc",
                    borderRadius: 6,
                    backgroundColor: "#f9f9f9",
                    minHeight: 200,
                }}
            >
                <h3 style={{ marginTop: 0 }}>Preview</h3>

                {isLoading && <p>Fetching image from backend...</p>}

                {error && (
                    <div className="error-box">
                        <h4>Error</h4>
                        <pre style={{ whiteSpace: "pre-wrap" }}>{error}</pre>
                        <button onClick={handleRefresh} disabled={isLoading}>
                            Retry
                        </button>
                    </div>
                )}

                {!isLoading && !error && !dataUrl && (
                    <p>No image yet. Click Refresh Image to fetch.</p>
                )}

                {dataUrl && !error && (
                    <div className="image-container">
                        <img
                            src={dataUrl}
                            alt="Fetched from backend"
                            className="image-preview"
                        />
                    </div>
                )}
            </section>
        </div>
    );
};

export default ImagePage;
Here's a CSS template you can just use
.image-container {
    display: grid;
    place-items: center;
    width: 100%;
    overflow: hidden;
}

.image-preview {
max-width: 100%;
height: auto;
max-height: 480px;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
}

.error-box {
color: red;
}