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.txtreactpy[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) -> 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() -> 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.