Design Rationale - lisannevvliet/so-nuts Wiki

Debriefing

Debriefing SO-NUTS

Probleem-definitie

Hoe kan een digitale tool ervoor zorgen dat gepensioneerden gestimuleerd worden om een duurzaam beweeg- en voedingspatroon aan te houden zodat sarcopenie, obesitas en sarcopene obesitas voorkomen kunnen worden?

Oplossing

Voorafgaand de questionnaire en het opstellen van de doelen, wordt de gebruiker geïnformeerd over wat ze te verwachten staat. Dit is vooral belangrijk vanwege het feit dat dit veel gaat worden. Persoonlijk zouden wij er ook nog voor kiezen om een optie te geven om bijvoorbeeld de questionnaire over te slaan, maar aangezien de content gebaseerd moet zijn op specifieke persoonlijke omstandigheden snappen we dat dit niet de bedoeling is. Na de onboarding is alles op een minimalistische en intuïtieve manier vormgegeven voor een fijne gebruikservaring. De lange vragenlijst is opgesplitst om het minder te laten lijken voor de gebruiker. Eenmaal ingevuld krijgen ze een pagina te zien waarop ze hun doelen kunnen uitkiezen en personaliseren. De meeste stimulatie zit hem in het streak element en een challenge in de SO-NUTS. De challenge is gebaseerd op een antwoord uit de questionnaire. Als je een partner hebt (die ook SO-NUTS gebruikt) kan je tegen hen spelen, maar als je alleenstaand bent kan tegen een andere SO-NUTS gebruiker de uitdaging aangaan. Hierin passen we loss aversion toe, een principe waar verlies heftiger aanvoelt voor iemand dan winst, want je koestert wat je al hebt.

Uitleg van de code

Kort gezegd, met behulp van onze eigen database is het mogelijk dat gebruikers een eigen profiel aanmaken en hierna doelen kunnen uitkiezen. Deze worden voor iedere unieke gebruiker opgeslagen en kunnen afgevinkt worden om een streak bij te houden van de gebruiker zijn/haar uitgekozen doelen. Belangrijk om te benoemen is dat de werking van de code ook in comments te vinden is.

Laten we beginnen bij het begin. Als eerste krijgt de gebruiker een login pagina te zien. Gezien het feit dat Chippr graag een app wil, is het ook mogelijk dat mensen SO-NUTS installeren als Progressive Web App. Op het moment dat de gegevens ingevuld zijn en er op login of registreer wordt geklikt, wordt er een formulier met een POST-request verstuurd naar de server. Dan wordt gecheckt of de gebruiker al bestaat in de Supabase database met users. Als dit niet zo is dan wordt er een nieuwe user aangemaakt en wordt de gebruiker doorgestuurd naar een onboarding met een link waarin hun gegevens staan. Is het een bestaande gebruiker dan wordt er gekeken of ze de questionnaire al hebben ingevuld en worden ze op basis hiervan doorgestuurd naar de voor hen relevante pagina.

database.read_user(req.body.email)
            .then(user => {
                // If not, create a new user in the database.
                if (user == null) {
                    database.insert_user(req.body.email)
                        .then(() => {
                            // Redirect to the onboarding page.
                            res.redirect(`/onboarding?name=${req.body.name}&email=${req.body.email}`)
                        })
                } else {
                    // Check if the questionnaire has already been completed.
                    if (user.questionnaire == false) {
                        // Redirect to the onboarding page.
                        res.redirect(`/onboarding?name=${req.body.name}&email=${req.body.email}`)
                    } else {
                        // Redirect to the goals page.
                        res.redirect(`/goals?name=${req.body.name}&email=${req.body.email}`)
                    }
                }
            })
    })

Als ze de questionnaire nog moeten invullen dan wordt er uit de Chippr API (V1) gefetched naar de vragen en de domains voor de namen van de drie categorieën waarin de vragen vallen. Deze twee API-requests worden gelijktijdig uitgevoerd door de promise all methode. Hiermee wordt een herhaling van promises ingevoerd en teruggegeven als een enkele promise in een array van de resultaten van de promises.

    // Listen to all GET requests on /questionnaire.
    .get("/", (_req, res) => {
        Promise.all([
            api.get("Questionnaires/2"),
            api.get("Domains")
        ])
            .then(([questionnaire, domains]) => {
                // Load the questionnaire page with the domains, questionnaire and questionnaire length.
                res.render("questionnaire", {
                    domains: domains,
                    questionnaire: questionnaire.questions,
                    length: questionnaire.questions.length - 1
                })
            })
    })

Voor de laatste vraag verandert de knop in een dashboard knop en wordt er een POST-request gedaan voor de antwoorden die naar de webserver worden verstuurd.

Wanneer de gebruiker de questionnaire al heeft ingevuld dan belandt hij/zij op de goals pagina. Op deze pagina bestaat een empty state als de gebruiker (nog) geen doelen heeft die hij/zij aan het bijhouden is in de vorm van een streak.

{{#ifCond user_goals.length 0}}
        <section class="empty_state">
            <img src="/images/goals/empty_state.webp" loading="lazy">
            <p>Begin met het aanmaken van doelen! Voor jou samengesteld. Het is aan te raden om er op 2 à 3 te focussen.
            </p>
        </section>
    {{/ifCond}}

Er worden de doelnamen, doel iconen en doel categorieën uit Supabase gehaald, die vervolgens worden gebruikt voor de styling van de lijst met doelen waar men uit kan kiezen.

   read_goals: async () => {
        const response = await supabase
            .from("goals")
            .select("name, icon, category")

        return response.data
    },
.get("/", (req, res) => {
        // Get the goals and user goals from the database.
        Promise.all([
            database.read_goals(),
            database.read_user_goals(req.query.email)
        ])

Als iemand een doel toevoegt dan wordt dit toegevoegd aan de user_goals tabel in de database. Door middel van foreign keys worden het doel gekoppeld aan de gebruiker.

  add_user_goal: async (email, goal) => {
        await supabase
            .from("user_goals")
            .insert([{ email: email, goal: goal }])
    },

In het geval dat een gebruiker meerdere doelen aanklikt is het de bedoeling dat je pas teruggaat naar het gepersonaliseerde doelen overzicht op het moment dat alle aangeklikte doelen toegevoegd zijn en niet alleen de eerste. Daarom bestaat deze if else.

    if (Array.isArray(req.body.goal)) {
            req.body.goal.forEach((goal, index) => {
                database.add_user_goal(req.body.email, goal)
                    .then(() => {
                        // Check if the last goal has been added.
                        if (index == req.body.goal.length - 1) {
                            // Redirect to the goals page.
                            res.redirect(`/goals?name=${req.body.name}&email=${req.body.email}`)
                        }
                    })
            })
        } else {
            database.add_user_goal(req.body.email, req.body.goal)
                .then(() => {
                    // Redirect to the goals page.
                    res.redirect(`/goals?name=${req.body.name}&email=${req.body.email}`)
                })
        }
    })

Op de profielpagina wordt een quotes API aangeroepen en wordt de naam uit de URL gehaald. De hoogste streak wordt uit Supabase gehaald. Hij sorteert de streaks op oplopende volgorde en haalt maar één row op door de limiet. Als iemand een nieuwsgierig aagje is en al naar de profielpagina gaat terwijl hij/zij nog geen doelen heeft gekozen komt er 0 te staan.

    read_highest_streak: async (email) => {
        const response = await supabase
            .from("user_goals")
            .select("streak")
            .eq("email", email)
            .order("streak", { ascending: false })
            .limit(1)
            .single()
 
        // Return zero if the user has no goals.
        if (response.data == null) {
            return 0
        } else {
            return response.data.streak
        }
    }
}

Op de profielpagina wordt ook een GET-request gedaan naar de ingevulde antwoorden van de questionnaire te tonen.

  // Listen to all GET requests on /profile.
    .get("/", (req, res) => {
        // Get ZenQuotes' daily quote.
        Promise.all([
            database.read_user(req.query.email),
            api.quote(),
            database.read_highest_streak(req.query.email),
            api.get("Questionnaires/2")
        ])
            .then(([user, quote, streak, questionnaire]) => {
                api.get(`QuestionnaireResponses/${user.id}`)
                    .then(questionnaire_response => {
                        // Load the profile page with the name, quote, streak, questionnaire and questionnaire response.
                        res.render("profile", {
                            name: req.query.name,
                            quote: quote,
                            streak: streak,
                            questionnaire: questionnaire.questions,
                            questionnaire_response: questionnaire_response.questionResponses
                        })
                    })
            })
    })

Terwijl dit allemaal gebeurt worden de pagina’s gecached. De client/server vraagt eerst uit de cache dan gaat hij het internet aanroepen als dat niet lukt dan serveert hij de cache. Als het wel lukt serveert hij het van het internet. Als je het internet uitzet en je refreshed de pagina dan blijft de pagina hetzelfde. In het geval dat een gebruiker een URL invoert die niet bestaat dan komt hij/zij terecht op een error state.

De reden dat wij zoveel mogelijk server side werken is in verband met veiligheid. De client hoeft niet te communiceren met de database. Bovendien wordt de client side JavaScript hierdoor lichter, waardoor de pagina beter/sneller zal renderen. Iets wat vooral handiger is voor budget telefoons waar onze doelgroep mogelijk van in het bezit is, aangezien het een generatie is die minder geeft om technologie.

⚠️ **GitHub.com Fallback** ⚠️