Sliding Cards Part 2

HTML/CSS, Javascript, programming, react

With the basics down, the next step in this little adventure w/ Javascript is to make it work in a React.js environment (Next.js to be more precise).

There were some iterations. What emerged was (using a Next.js framework):

  • app/models.ts — the model types (Card) I’m working with.
  • app/cards/page.tsx — the component displaying the card “carousel” and a scroller.
  • app/cards/ui/CardCarousel.tsx — component displaying the card “carousel.”
  • app/cards/ui/cardcarousel.module.css — styling for the CardCarousel.
  • app/cards/ui/Scroller.tsx — component displaying the scroller (I will omit this in the post since it’s not really relevant).
  • app/lib/ui/renderers.tsx — a renderer function that renders a Card.

The hierarchy of files will differ from time to time and from person to person. What I consider the main trick to get this working with React.js is in the app/cards/ui/CardCarousel.tsx::Carousel component:

The Trick

To avoid the component immediately rendering the center card, I instead render the card passed into the component immediately in its left and right cards that are initially hidden, leaving the center card blank (or whatever it was before). This means that the component should render the center card using the model stored in a useRef value which starts out being undefined.

Then, in a useEffect, call a function that adds a slide CSS class to the left (or right, depending on the direction I want to slide) card. This should kick off the translation animation. At the same time, start a timer (just like the raw JS example) matching the animation duration to:

  • update the useRef.current to now point to the card passed in.
  • remove the slide CSS class so that the cards snap back into place.
  • trigger a refresh so that the center card gets rendered using the updated useRef.current (which should now be the card passed into the carousel).

app/models.ts

export type Card = {
    ...
    title: string
    copy: string
    ...
}

....

app/cards/ui/cardcarousel.module.css

.slide-left {
    transition: transform 0.3s;
    transform: translate(-600px)
}

.slide-right {
    transition: transform 0.3s;
    transform: translate(600px)
}

app/cards/page.tsx

'use client'

...
import { renderCard } from "@/app/lib/ui/renderers";
import { Carousel, Direction } from "../ui/CardCarousel"
...

export default function Page() {
    ...
    // Tracks which Card (index into an array) is being displayed and how it
    // should slide into the view
    const [cardDisplayState, dispatch] = useReducer(
        cardDisplayStateReducer, {index: 0, slideDirection: Direction.LEFT}
    )

    ...
        const { cards } = stack

        return (
            <div className="...">
                ...
                <Carousel 
                    t={cards[cardDisplayState.index]} 
                    slideDirection={cardDisplayState.slideDirection} 
                    renderer={renderCard} 
                />
                <Scroller ... /> 
            </div>
        )
}

app/cards/ui/CardCarousel.tsx

import styles from "@/app/cards/ui/cardcarousel.module.css"
...

export enum Direction {
    LEFT,
    RIGHT
}

const SLIDE_CSS_CLASSNAME: Record<Direction, string> = {
    [Direction.LEFT]: "slide-left",
    [Direction.RIGHT]: "slide-right"
}

/**
 * Parameters to the Carousel component.
 * 
 * @param t the item model being rendered into a "card" to be displayed
 * @param slideDirection the direction the new card will slide in to replace the center card
 * @param renderer a function that will render the card for the item model
 */
type Parameters<T> = {
    t?: T
    slideDirection: Direction
    renderer: (c: T|undefined) => JSX.Element
}

export function Carousel<T>({ t, slideDirection, renderer }: Parameters<T>) {
    const currentTRef = useRef<T|undefined>(undefined)

    // This is just to allow us to force an update (got this trick from a search)
    const [, updateState] = useState<{}>();
    const forceUpdate = useCallback(() => updateState({}), []);

    const slideWithCssClassName = useCallback((slideCssClassName: string) => {
        const elems = Array.from(document.getElementsByClassName("card"))

        // Adding the class will trigger the animation
        elems.forEach(elem => {
          elem.classList.add(styles[slideCssClassName])  
        })

        // Set up a timer to reset the elems (hopefully immediately after the animation completes)
        setTimeout(() => {
            // By this time, the slide animation should be complete. We can snap the elems back
            // to the original positions by removing the class. 

            // currentTRef is not sync'ed, so this should not trigger a refresh on its own. We 
            // don't want it to in order to not interfere with the slide.
            currentTRef.current = t

            elems.forEach((elem) => {
                elem.classList.remove(styles[slideCssClassName])
            })
            // Since currentTRef is not sync'ed, force an update manually now.
            forceUpdate()
        }, 300)
    }, [t, currentTRef.current])

    useEffect(
        () => {
            setTimeout(
                () => slideWithCssClassName(SLIDE_CSS_CLASSNAME[slideDirection]),
                150
            )
        },
        [t, slideDirection]
    )

    // Truncated most of the Tailwind CSS classes for brevity
    return (
        <div className="...">
            <div className="...">
                <div className="card ... left-[-600px]" id="previous-card">
                    {renderer(t)}
                </div>                
                <div className="card ..." id="current-card">
                    {renderer(currentTRef.current)}
                </div>
                <div className="card ... left-[600px] " id="next-card">
                    {renderer(t)}
                </div>                  
            </div>       
        </div>
    )

app/lib/ui/renderers.tsx

export function renderCard(card: Card|undefined): JSX.Element {
    // Nothing special; just render the title and copy of the card
    const cardDisplay = card ? (
        <div className="...">
            <div className="flex-none font-bold">{card!.title}</div>
            <div className="grow">{card.copy}</div>
        </div>
    ) : (
        <></>
    )

    return <div className="size-full">{cardDisplay}</div>
}

renderer === Card component?

That renderCard function in renders.tsx looks like just a Card component. Why not just write the component and call it from Carousel?

export function Carousel({ t, slideDirection }: Parameters<T>) {
    const currentTRef = useRef<Card|undefined>(undefined)

    ...

    // Truncated most of the Tailwind CSS classes for brevity
    return (
        <div className="...">
            <div className="...">
                <div className="card ... left-[-600px]" id="previous-card">
                    <Card card={t} ... />
                </div>                
                <div className="card ..." id="current-card">
                    <Card card={currentTRef.current} />
                </div>
                <div className="card ... left-[600px] " id="next-card">
                    <Card card={t} ... />
                </div>                  
            </div>       
        </div>
    )

That’s what I started with. However, doing this will couple the Carousel component to the Card. I thought I’d try to have the Carousel be more reusable for other models/components.

The result is that a Carousel<T> that is bound to a model T and accepts a renderer function that knows how to render a “card” for that T. The Carousel<T> will just delegate the rendering of T to that renderer when needed.

So far I don’t know if there is a better way to accomplish that decoupling. An obvious approach is to have Carousel<T> accept a children as other composite components do. However, I think in that case children would be the rendered “current” card, and there is no way to hold on to the “previous” card without pushing up the logic of maintaining a “previous” vs. “current” card to the parent of the Carousel, and that seems to me like a leakage of the implementation concerns of the Carousel to its parent.

For now, therefore, the Carousel<T> will require a function that renders a “card” for the T. That function will be called with either the current or previous T depending on where in the flow the Carousel is.

And as can be seen with the Page.tsx, the parent (Page.tsx in this case) doesn’t need to worry about which is the previous Card and which is the current (or current vs. next) Card; the parent just passes the T (in this case Card) it wants to slide in, and the Carousel<T> component takes care of maintaining that previous/current state. The cost is that the parent does need to pass in a rendering function for the Card / T.

Do I get points taken off for using a rendering function instead of a “component”? 🤔

Result

I’ve increased the dimensions of the card (from 200×100 in Part 1) to 600×350. Again, I have skipped the whole Scroller component above since there’s nothing special there. However, it’s displaying those two chevrons at the bottom used to scroll between 3 cards.

Infinite Sliding Cards

HTML/CSS, Javascript, programming

Part of a project I’m working on involves some animation of “cards” sliding in and out of existence. This trinity of HTML/CSS/JS serves as a basis for that functionality.

The illusion is that there is an endless (at least until integer overflow anyway) number of rectangles that will scroll either forward or backward.

The truth is that there are only 3 such rectangles. Theoretically only 2 are needed, but that comes with the cost of more complexity in the code. When a scroll is happening, there is one rectangle that is scrolling out, then there is another one scrolling in to take its place. Doing it once is easy with CSS transform. However, to continue to do it over and over again seamlessly requires some trickery.

The trick is that, once the transform animation to replace the center rectangle with the new rectangle completes, we need to immediately shift the rectangles back to be ready for the next scroll. This is done by the reset() function. The sequence looks like this:

  • Render the new rectangle (off screen).
  • Scroll the center rectangle out.
  • Scroll the new rectangle in.
  • Render the center rectangle (off screen) to match the new rectangle.
  • Reset the position of the rectangles so that the center rectangle is again at the center. Because the content of the rerendered center rectangle now matches that of the new rectangle, this should look seamless.

This is done by the functions slideRightWithCount and slideLeftWithCount. Adding the slideRight or slideLeft class to the rectangles will animate the sliding for 0.3s. At the same time, set a timer for 300ms (0.3s) to then call reset() which positions the rectangles to be ready for another scroll. There is some 🤞 going on assuming that the transform animation is done by the time the timer expires and reset() is called (preferrably at the same time). If for some reason the timer expires earlier than the full transform is complete, there can be some flickering (easily tested by either increasing the transition duration and/or decreasing the timer expiry).

HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Rectangle Animation</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div id="rects">
        <div class="container">
            <div class="rectangle" id="rectangle0"></div>
            <div class="rectangle" id="rectangle1">1</div>
            <div class="rectangle" id="rectangle2"></div>
        </div>
    </div>
    <div id="button-panel">
        <div>
            <button onclick="slideRightWithCount()">BACK</button>
        </div>
        <div>
            <button onclick="slideLeftWithCount()">FORWARD</button>
        </div>
    </div>
    <script src="script.js"></script>
</body>
</html>

STYLES.CSS

body {
    height: 100vh;
    margin: 0;
    background-color: #f0f0f0;
}

#rects {
    justify-content: center;
    align-items: center;
    width: 100%;
    height: 200px;
    display: flex;
}

.container {
    position: absolute;
    width: 200px;
    height: 100px;
    overflow: hidden;
}

.rectangle {
    position: absolute;
    width: 100%;
    height: 100%;
    text-align: center;
    padding: 1rem;
    border: 10px solid white;
    box-sizing: border-box;
    background-color: antiquewhite;
}

#rectangle0 {
    left: -200px;
}

#rectangle2 {
    left: 200px;
}

.slide-left {
    transition: transform 0.3s;
    transform: translate(-200px)
}

.slide-right {
    transition: transform 0.3s;
    transform: translate(200px)
}

#button-panel {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: .5rem;
}

#button-panel button {
    width: 7rem;
}

SCRIPT.JS

const RECT_IDS = [
    "rectangle0", "rectangle1", "rectangle2"
]

var COUNTER = 1


function slideLeft() {
    const rects = RECT_IDS.map((id) => document.getElementById(id))

    rects.forEach((elem) => {
        elem.classList.add("slide-left")
        elem.classList.remove("slide-right")
    })
};

function reset() {
    const rects = RECT_IDS.map((id) => document.getElementById(id))

    document.getElementById("rectangle1").innerText = COUNTER
    rects.forEach((elem) => {
        elem.classList.remove("slide-left")
        elem.classList.remove("slide-right")
    })
}

function slideRight() {
    const rects = RECT_IDS.map((id) => document.getElementById(id))

    rects.forEach((elem) => {
        elem.classList.add("slide-right")
        elem.classList.remove("slide-left")
    })
};

function slideLeftWithCount() {
    COUNTER = COUNTER + 1
    document.getElementById("rectangle2").innerText = COUNTER
    slideLeft()
    
    setTimeout(() => {
        reset()
    }, 300)
}

function slideRightWithCount() {
    COUNTER = COUNTER - 1
    document.getElementById("rectangle0").innerText = COUNTER
    slideRight()
    
    setTimeout(() => {
        reset()
    }, 300)
}

The next challenge is to make this work with React.js. 😵‍💫

PyTorch in Docker

docker, programming, Python

I’m getting into the fray that is Generative AI since, according to some, my job as a programmer will soon be taken over by some literal code cranking machine.

There are a few things to set up:

Like most tools and libraries, the instructions assume that I have nothing else happening on my machine, so install just Anaconda globally according to their simplistic assumptions. All the other dependencies like even the Python version on my machine can go to hell.

Dockerizing the environment

Well. Until I have enough $$$ to buy a new machine for each new tool I want to try out, I’ll be using Docker (think goodness I don’t need an actual VM) to isolate an environment to play with.

After some trial and error, this is a template Dockerfile and docker-compose.yml I’m using:

Dockerfile

FROM python:3.11-bookworm
ENV PYTHONUNBUFFERED 1

WORKDIR /code

RUN apt update && apt install -y \
    vim

RUN curl -O https://repo.anaconda.com/miniconda/Miniconda3-py311_24.1.2-0-Linux-x86_64.sh
RUN sh Miniconda3-py311_24.1.2-0-Linux-x86_64.sh -b
ENV PATH="$PATH:/root/miniconda3/bin"

RUN conda install -y pytorch torchvision torchaudio pytorch-cuda=12.1 -c pytorch -c nvidia
RUN conda init

The above environment has:

  • Vim
  • Python 3.11
  • Miniconda3
  • PyTorch with CUDA 12.1

docker-compose.yml

services:
  app:
    build: .
    volumes:
      - ./:/code
    tty: true
    stdin_open: true

The docker-compose.yml is a template one I use for other things. Nothing special here.

Redux Toolkit w/ Vite in Docker

docker, Node.js, programming, react

Notes on setting up a Docker/Vite/React/Redux Toolkit service in Docker. That’s a mouthful.

Docker Files

Start with a name for the project (e.g. “myproject”). Then create these Docker files:

Dockerfile

FROM node:20.10

RUN apt update && apt install -y \
  xdg-utils

EXPOSE 5173

docker-compose.yml

version: '3'
services:
  app:
    build: .
    command: >
      sh -c "npm run dev"
    ports:
      - "5173:5173"
    expose:
      - "5173"
    volumes:
      - .:/app
    tty: true
    stdin_open: true

Now build the working image and shell into the container:

docker-compose build

Redux Toolkit Setup

Shell into a container of the working image and initialize the project:

docker-compose run --rm app /bin/bash
...
npx degit reduxjs/redux-templates/packages/vite-template-redux myproject

Of course, use the name of the project instead of myproject.

This will create the starter files for the project under myproject/.

Exit the shell.

File Updates

Dockerfile

Modify Dockerfile to build the image with the project:

FROM node:20.10

RUN apt update && apt install -y \
  xdg-utils

EXPOSE 5173

WORKDIR /app/myproject

RUN npm install -g npm 

vite.config.ts

Modify the Vite config to allow it to host from a Docker container:

import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  server: {
    host: true,  // <-- ADD THIS
    open: true,
  },
  ....

Start

Finally, to start the server, first build the image again:

docker-compose build

Then bring up the app:

docker-compose up

This should now bring up the app at http://localhost:5173/

To work on the project’s settings (e.g. installing packages, running tests, etc.), shell in with:

docker-compose run --rm --service-ports app /bin/bash

The --service-ports option will allow commands like npm run dev to start the app correctly (i.e. so that http://localhost:5173/ works). Without it, the port 5173 will not be mapped to the host, and docker-compose up will be the only way to run the app.

Celery with RabbitMQ on Docker

celery, django, docker, programming, Python

Picking up from Django app template w/ Docker, here are the steps to add Celery to the Django app.

Add RabbitMQ as the message queue

Modify docker-compose.yml to include:

services:
  ...
  rabbitmq:
    image: rabbitmq:3-management
    ports:
      - "15672:15672"
    expose:
      - "15672"

I use the “3-management” tag so that it includes a management plugin (accessible at http://localhost:15672/). However, simpler tags (e.g. “3” or “latest“) can be used if the management UI is not needed.

Install celery to the Django project

docker-compose run --rm app /bin/bash
...
pip install celery
pip freeze -r requirements.txt > requirements.txt
exit

Rebuild the container with the new packages added by celery.

Add a couple of files to set up Celery in our Django project

The Celery stuff will be added into the myapp Django app.

myapp/celery.py

from celery import Celery


app = Celery(
    'celery_app',
    broker='amqp://rabbitmq',
    # backend='rpc://',

    # This should include modules/files that define tasks. This is a list of strs 
    # to be evaluated later in order to get around circular dependencies, I suspect.
    include=[  
        'myapp.tasks',  # This is our file containing our task
    ]
)

# Optional configuration, see the application user guide.
app.conf.update(result_expires=3600)


if __name__ == '__main__':
    app.start()

myapp/tasks.py

import logging

from myapp.celery import app


logger = logging.getLogger(__name__)


# This is where Celery tasks are defined


@app.task
def add(x: int, y: int) -> int:
    logger.info(f"add({x}, {y}) called.")
    return x + y

Add a Celery service to docker-compose.yml

Modify docker-compose.yml again to add a Celery service. This can be done together with the RabbitMQ service above, but it is shown here separately for readability.

services:
  ...
  rabbitmq:
    ...
  app-celery:
    build: .
    environment:
    - DJANGO_SETTINGS_MODULE=myapp.settings
    command: >
      sh -c "celery -A myapp.celery worker --loglevel=INFO"
    volumes:
      - ./:/code
    depends_on:
      rabbitmq:
        condition: service_started

Things to watch out for

A bunch of things to highlight to show where the connection points are:

  • The broker URL when instantiating the Celery app is amqp://rabbitmq (not amqp://localhost) because that’s how networking in Docker works. The “rabbitmq” in this case the name of the service we use for the RabbitMQ container. So if a different container name is used, this AMQP URL needs to use that corresponding name.
  • The Celery app parameter (-A myapp.celery) is the path to the myapp/celery.py file where the Celery app (app = Celery('celery_app', ...) ) is created.
  • Speaking of which, when defining the Celery app, its include=[ ... ] should include str values that point to files where Celery tasks are defined.
  • And the task files that define the Celery tasks need to import the Celery app and use its @app.task decorator for the task functions.

Complete docker-compose.yml

The entire file looks like:

services:
  app:
    build: .
    command: >
      sh -c "python manage.py migrate &&
        python manage.py runserver 0.0.0.0:8000"
    ports:
      - "8000:8000"
    expose:
      - "8000"
    volumes:
      - ./:/code
    depends_on:
      rabbitmq:
        condition: service_started
    tty: true
    stdin_open: true
  app-celery:
    build: .
    command: >
      sh -c "celery -A myapp.celery worker --loglevel=INFO"
    volumes:
      - ./:/code
    depends_on:
      rabbitmq:
        condition: service_started
  rabbitmq:
    image: rabbitmq:3-management
    ports:
      - "15672:15672"
    expose:
      - "15672"

Sentry on Django

django, programming, Python, sentry

Sentry is one of those things that most companies use, but most people (or maybe just I) don’t really know how to use it. The way I learn these things is to set it up on my own.

Fortunately, they have a way for anyone to start with a free account to play with.

Register for an account

Just go to the https://sentry.io and click their “GET STARTED” button. Fill out the info and create an Account. By default I got a free account this way. I don’t know what limitations it has, but to just test things, I don’t care.

Create a Project

Once the account is set up, log in and create a new Project. There is a collection of SDKs to select from. Fortunately, there is one for Django.

Selecting it and giving the project a name (replacing the default “python-django” name) brings me to a very useful page where I can copy and paste the blurb to paste into my Django settings.py file:

The “dsn” URL is specific to the project and account. Think of that as the “API key” to the project. That’s why I redacted mine from the pic.

As seen in that “Verify” section, this will hook in an exception handler into the app to catch and log to Sentry any uncaught exceptions.

What about Raven?

The code from work also installed the raven package. However, looking at the docs, it seems that the raven is deprecated and replaced by sentry-sdk.

However, if Raven is used in a Django project, then a logger handler can be connected to handle logs.

Hooking Up Sentry to Error Logger

My workplace, for instance, has Raven installed and set it up so that any errors logged will also go into Sentry via a handler from the Raven package. This is done by configuring the LOGGING setting:

LOGGING = {
    ...
    "handlers": {
        ...
        "sentry": {
            "class": "raven.contrib.django.raven_compat.handlers.SentryHandler",
            "level": "ERROR",
            ...
        },
    },
    ...
}

Just make sure that the “sentryhandler is invoked by a logger.

NOTE: This is not necessary when using sentry-sdk.

Extra Data

There is a section in the event where additional “extra” data can be added. The values can be:

  • simple strings
  • “object” values

Adding extra data to logging or errors and exceptions is simple: just include the “extra” property:

import logging

logger = logging.getLogger(__name__)
...

    logger.error(
        "Hello", extra={
            "prop1": "property one",
            "prop2": "property two",
            "prop3": {
                "prop4": "property four",
                "now": datetime.utcnow(),
            }
        }
    )

From the above, the properties prop1 and prop2 are normal strings. prop3 is an object which is serialized (bonus: it works with datetime instances, unlike json.puts()).

One limitation of these extra values is that they are not searchable.

Searching for Events: Custom Tags

The Sentry console allows some searching with a list of default searchable properties. It seems like the only way to work with search for our own data is to use Custom Tags. For Python/Django, the easiest way to do that is to set up a scope, then set the tag(s) before calling Sentry:

import logging

from sentry_sdk import push_scope, set_tag
...

logger = logging.getLogger(__name__)

...

with push_scope() as scope:
    set_tag("VanTag", "Hello1")  # This will add a VanTag:Hello1 to the event 
    logger.error("Test Error")

With the above, the event raised will now have a VanTag tag of Hello1.

As a result, you can now search for issues/events with this tag:

Grouping Control: Custom Fingerprints

An issue that annoys probably every Sentry user one time or another is how it groups (or ungroups) issues. The documentation says a lot of stuff, but the summary is pretty much, “We have a super algorithm. Trust us.”

Well. When it does not do what we want. One way to influence the grouping is to set the scope fingerprints before calling Sentry.

        with push_scope() as scope:

            scope.fingerprint = ["Hello1"]  # list of strings make the fingerprint
            logger.error("Appointment Creation Error")

When the event is sent to Sentry, it will be grouped by the fingerprint (list of strings).

  • Note: PyCharm mistakenly says that scope.fingerprint is a read-only attribute. It’s not; the code above will work as expected.

Django app template w/ Docker

django, docker, programming, Python

Revisiting https://www.pn.therealvan.com/2021/01/24/postgresql-and-mysql-docker-containers/, this post focuses on a plain Django app with minimal dependencies:

  • exclude pipenv
  • using the default SQLite DB

Bootstrapping

Start with these files:

Dockerfile

FROM python:3
ENV PYTHONUNBUFFERED 1

WORKDIR /code
#COPY requirements.txt /code/

#RUN pip install --upgrade pip && pip install -r requirements.txt

docker-compose.yml

version: '3'
services:
  app:
    build: .
    #command: >
    #  sh -c "python manage.py migrate &&
    #    python manage.py runserver 0.0.0.0:8000"
    ports:
      - "8000:8000"
    expose:
      - "8000"
    volumes:
      - ./:/code
    tty: true
    stdin_open: true

Run these to start up a container:

docker-compose build
docker-compose run --rm app /bin/bash

Initializing a Django project

Run these inside the container:

pip install django
pip freeze > requirements.txt

django-admin startproject myproj .
django-admin startapp myapp

exit

Rebuild and start the app

Now uncomment the lines in Dockerfile and docker-compose.yml.

Build the image and restart the container:

docker-compose build

Modify myproj/settings.py to add a line to register myapp to Django:

INSTALLED_APPS = [
    ...
    'myapp.apps.MyappConfig',  # Add this line
]

Now start the app again:

docker-compose up app

This should now bring up the app listening to http://localhost:8000/