How to create a diary application in ReactPy

reactpy diary
Updated: August 1, 2023

A. Introduction

We are building a basic Diary Application that lets users write and save their diary entries. The app will organize each entry with a date/time and description displayed in a user-friendly Bootstrap card format. Also, the application will save the entries to a CSV file, so that old entries persist and can be accessed even after refreshing the app.

B. Build outline

We will present a form with label, textarea and two buttons to save the entries and to clear the textarea. All entries are saved in memory and backed up in a CSV file diary.csv. The diary entries are presented as a bootstrap card which are built from the saved records.

In the beginning, a CSV file is opened, it will be created if it does not exist. The CSV file has two columns the date_time and description. All entries in this CSV file are read and save to records in memory. This record is updated whenever an entry is saved. All cards are created from this records. Before building the cards, the records are saved to a CSV file.

The final Application will look like this.

C. Module Requirements

requirements.txt
reactpy[fastapi]
pandas
uvicorn[standard]
reactpy-flake8

Pandas library helps in processing CSV file. reactpy-flake8 checks errors/warnings in our code.

D. Code Snippets

The code will be saved in diary.py file.

1. Docstring and Imports

"""Diary Application

Records date and description. Entries are displayed as a card.
It uses bootstrap 5.2.3 to style the elements.

pip install reactpy[fastapi]
pip install pandas
pip install uvicorn[standard]
pip install reactpy-flake8
"""


from typing import Union
from datetime import datetime
from reactpy import component, html, event, hooks
from reactpy.backend.fastapi import configure, Options
from fastapi import FastAPI
import pandas as pd

...

app = FastAPI()
configure(app, Diary)

We use the fastapi backend to run our app with the following command.

uvicorn diary:app --reload

The --reload flag will set the server to reload the latest code and update the rendered page.

2. Define Bootstrap for CSS

...

BOOTSTRAP_CSS = html.link(
    {
        'href': 'https://cdn.jsdelivr.net/npm/'
                'bootstrap@5.2.3/dist/css/bootstrap.min.css',
        'integrity': 'sha384-rbsA2VBKQhggwzxH7pPCaAq'
                     'O46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65',
        'rel': 'stylesheet',
        'crossorigin': 'anonymous'
    }
)

...

Use the link tag to define the bootstrap CSS.

3. Define other constants

PAGE_TITLE = 'ReactPy-Diary'
CSV_FILENAME = 'diary.csv'
COLUMN_HEADER = ['Date', 'Description']

We will store diary entries in diary.csv file. The CSV file has two columns, the Date and Description.

4. Utility Function get_df

def get_df(fn: str) -> tuple[bool, Union[pd.DataFrame, str]]:
    """Converts csv file to dataframe."""
    try:
        df = pd.read_csv(fn)
    except FileNotFoundError:
        df = pd.DataFrame(columns=COLUMN_HEADER)
        df.to_csv(fn, index=False)
    except Exception as err:
        return False, repr(err)
    return True, df

Used to read existing CSV and converts it to a pandas dataframe. The file diary.csv will be created if it does not exist.

5. Utility Function get_date

def get_date() -> str:
    """Gets date and time."""
    now = datetime.now()
    return now.strftime("%Y-%m-%d %H:%M:%S")

Before we save an entry, we need the current date and time. Note In every diary entry, we will save the current date and time and the description.

6. The Card component

@component
def Card(text: list):
    return html.div(
        html.div(
            {'class': 'card text-dark bg-light border-secondary mb-2'},
            html.div(
                {'class': 'card-body text-secondary'},
                html.div(
                    {'class': 'card-title text-secondary'},
                    html.h6(text[0])
                ),
                html.div(
                    {'class': 'card-text text-secondary'},
                    html.span(f'{text[1]}'),
                ),
            ),
        ),
    )

This is our card builder. It only returns a single card. It has a parameter text which is a list with values.

text = [date_time_value, description]

The date_time_value text[0] is used in a class card-title. While the description text[1] is used in a class card-text.

The typical bootstrap 5 card is this.

<div class="card" style="width: 18rem;">
  <img src="..." class="card-img-top" alt="...">
  <div class="card-body">
    <h5 class="card-title">Card title</h5>
    <p class="card-text">Some quick example text ...</p>
    <a href="#" class="btn btn-primary">Go somewhere</a>
  </div>
</div>

I have not included the img and anchor tag a. The class styles the card, with boostrap you can design in anyway you want it to look like.

Sample Card

The class card text-dark sets the card color to black. The bg is the background color of the card, its value is bg-light. See the Card component code above.

7. The Card Generator

@component
def BuildCards(fn: str, records: list):
    """Save record to csv and build a list of cards."""
    dfr = pd.DataFrame(records, columns=COLUMN_HEADER)
    dfr.to_csv(fn, index=False)

    return html.div(
        {
            'style': {
                'height': '600px',
                'overflow-y': 'auto',
                'white-space': 'pre-wrap'
            }
        },
        [Card(rec) for rec in records[::-1]]
    )

The BuildCards function/component has two parameters. They are used to save the entries to CSV file and for each entry, creates an equivalent bootstrap card. We just use a list comprehension to generate cards from the records. We also have some styling to contain these cards under a 600px height allowing to scroll only along the y-axis. The white-space attribute with value pre-wrap sets a new line from the input element to a new line in the card.

8. The root component Diary

@component
def Diary():
    csvfn = CSV_FILENAME
    description, set_description = hooks.use_state('')

    # Open the existing csv file. It will be created if it does not exist.
    # If there is error, we will send the error message and exit.
    okdf, df = get_df(csvfn)
    if not okdf:
        return html.h4(
            {'style': {'color': 'red'}},
            f'There is error {df} in opening the {csvfn} file.'
        )

    # Initialize our records from existing csv file.
    records, set_records = hooks.use_state(df.values.tolist())

    def update_textvalue(event):
        set_description(event['target']['value'])

    @event(prevent_default=True)
    def submit(event):
        """Updates records."""
        set_records(records + [[get_date(), description]])

    return html.div(
        BOOTSTRAP_CSS,
        html.div(
            {'class': 'container'},
            html.div(
                html.h2('My Diary'),
                html.form(
                    {'on_submit': submit},

                    html.div(
                        {'class': 'form-group'},
                        html.label(
                            {
                                'html_for': 'description',
                                'class': 'text-primary fs-5'
                            },
                            'Description'
                        ),
                        html.textarea(
                            {
                                'class': 'form-control border-primary',
                                'id': 'description',
                                'type': 'textarea',
                                'rows': '4',
                                'on_change': update_textvalue,
                            }
                        ),
                    ),

                    html.p(),
                    html.input(
                        {'class': 'btn btn-success',
                         'type': 'submit', 'value': 'Save'}
                    ),
                    html.input(
                        {'class': 'btn btn-danger mx-1',
                         'type': 'reset', 'value': 'Clear'}
                    ),
                ),
                html.p(),
                BuildCards(csvfn, records),
            ),
        ),
    )

Our form has four elements, label, textarea, and two input buttons submit and reset. The submit type has a value of Save which is the text displayed in the button. The reset button has a value of Clear. Bootstrap 5 styled the elements to look it more appealing.

At the top we have a system of loading existing csv file into the memory of the application.

def Diary():
    csvfn = CSV_FILENAME
    description, set_description = hooks.use_state('')

    # Open the existing csv file. It will be created if it does not exist.
    # If there is error, we will send the error message and exit.
    okdf, df = get_df(csvfn)
    if not okdf:
        return html.h4(
            {'style': {'color': 'red'}},
            f'There is error {df} in opening the {csvfn} file.'
        )

    # Initialize our records from existing csv file.
    records, set_records = hooks.use_state(df.values.tolist())
    
    ...

E. Full Code

diary.py
"""Diary Application

Records date and description. Entries are displayed as a card.
It uses bootstrap 5.2.3 to style the elements.

pip install reactpy[fastapi]
pip install pandas
pip install uvicorn[standard]
pip install reactpy-flake8
"""


from typing import Union
from datetime import datetime
from reactpy import component, html, event, hooks
from reactpy.backend.fastapi import configure, Options
from fastapi import FastAPI
import pandas as pd


BOOTSTRAP_CSS = html.link(
    {
        'href': 'https://cdn.jsdelivr.net/npm/'
                'bootstrap@5.2.3/dist/css/bootstrap.min.css',
        'integrity': 'sha384-rbsA2VBKQhggwzxH7pPCaAq'
                     'O46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65',
        'rel': 'stylesheet',
        'crossorigin': 'anonymous'
    }
)


PAGE_TITLE = 'ReactPy-Diary'
CSV_FILENAME = 'diary.csv'
COLUMN_HEADER = ['Date', 'Description']


def get_df(fn: str) -&gt; tuple[bool, Union[pd.DataFrame, str]]:
    """Converts csv file to dataframe."""
    try:
        df = pd.read_csv(fn)
    except FileNotFoundError:
        df = pd.DataFrame(columns=COLUMN_HEADER)
        df.to_csv(fn, index=False)
    except Exception as err:
        return False, repr(err)
    return True, df


def get_date() -&gt; str:
    """Gets date and time."""
    now = datetime.now()
    return now.strftime("%Y-%m-%d %H:%M:%S")


@component
def Card(text: list):
    return html.div(
        html.div(
            {'class': 'card text-dark bg-light border-secondary mb-2'},
            html.div(
                {'class': 'card-body text-secondary'},
                html.div(
                    {'class': 'card-title text-secondary'},
                    html.h6(text[0])
                ),
                html.div(
                    {'class': 'card-text text-secondary'},
                    html.span(f'{text[1]}'),
                ),
            ),
        ),
    )


@component
def BuildCards(fn: str, records: list):
    """Save record to csv and build a list of cards."""
    dfr = pd.DataFrame(records, columns=COLUMN_HEADER)
    dfr.to_csv(fn, index=False)

    return html.div(
        {
            'style': {
                'height': '600px',
                'overflow-y': 'auto',
                'white-space': 'pre-wrap'
            }
        },
        [Card(rec) for rec in records[::-1]]
    )


@component
def Diary():
    csvfn = CSV_FILENAME
    description, set_description = hooks.use_state('')

    # Open the existing csv file. It will be created if it does not exist.
    # If there is error, we will send the error message and exit.
    okdf, df = get_df(csvfn)
    if not okdf:
        return html.h4(
            {'style': {'color': 'red'}},
            f'There is error {df} in opening the {csvfn} file.'
        )

    # Initialize our records from existing csv file.
    records, set_records = hooks.use_state(df.values.tolist())

    def update_textvalue(event):
        set_description(event['target']['value'])

    @event(prevent_default=True)
    def submit(event):
        """Updates records."""
        set_records(records + [[get_date(), description]])

    return html.div(
        BOOTSTRAP_CSS,
        html.div(
            {'class': 'container'},
            html.div(
                html.h2('My Diary'),
                html.form(
                    {'on_submit': submit},

                    html.div(
                        {'class': 'form-group'},
                        html.label(
                            {
                                'html_for': 'description',
                                'class': 'text-primary fs-5'
                            },
                            'Description'
                        ),
                        html.textarea(
                            {
                                'class': 'form-control border-primary',
                                'id': 'description',
                                'type': 'textarea',
                                'rows': '4',
                                'on_change': update_textvalue,
                            }
                        ),
                    ),

                    html.p(),
                    html.input(
                        {'class': 'btn btn-success',
                         'type': 'submit', 'value': 'Save'}
                    ),
                    html.input(
                        {'class': 'btn btn-danger mx-1',
                         'type': 'reset', 'value': 'Clear'}
                    ),
                ),
                html.p(),
                BuildCards(csvfn, records),
            ),
        ),
    )


app = FastAPI()
configure(app, Diary, options=Options(head=html.head(html.title(PAGE_TITLE))))

The page title is changed from the options parameter in the configure class.

The command line is:

uvicorn diary:app

The diary.csv and the diary.py files must be located in same folder. If diary.csv does not exist, the app will create it the folder where diary.py is located.

The source code can also be found from my ReactPy-Diary github repository.

F. Check the code with flake8

Flake8 is a Python linting tool that checks the code for errors, and style issues. Do the following to check a reactpy code.

flake8 diary.py

G. Summary

The diary app is created through the use of ReactPy styled by Bootstrap 5. It uses a form with a description label, input textarea, and two buttons to save the records and to clear the input.

Each diary entry has two fields, the date_time and description. They are managed both from memory and CSV file. Refreshing the page is not an issue as the app loads the diary data from the CSV file. In memory, it utilizes a use_state hook to handle the description and entry records. We use pandas library to handle CSV file operations.

H. References