Help Those Helping Others

December 06, 2022
Written by
Reviewed by

Helping Those Helping Others

In so many countries, when you think of December, you think of Christmas. And, for as long as I can remember, Christmas has always been about giving, not receiving.

When you think about giving, is your first thought of gifts and presents? For many, that would be true. But what about, instead of giving gifts, you give to those helping others in need instead?

Sure, it's nice to receive presents. But, as studies regularly show, if your focus is only on things, on the material, you'll likely end up depressed and anxious, not blissfully happy as the ads suggest.

So in this tutorial, I'm going to show you how to build an application to help anyone quickly donate to any number of charities and nonprofits.

For the sake of simplicity, I've compiled a small list of six charities that I feel are doing great work, and which are especially dear to my heart. These are:

Feel free to add any others that you feel are worthy too!

The application will be built as a combination of a Vue.js frontend styled by Tailwind CSS, and powered by a simplistic PHP backend centred around the Slim Framework.

Prerequisites

To follow along with this tutorial you will need the following:

Create the PHP API backend

It doesn't matter whether you build the backend or front end first. As I'm more familiar with PHP and backend services than JavaScript and the front end, let's start by building the backend API.

Create the project directory structure

Create the project's directory structure and switch into it, by running the commands below.

mkdir -p the-gift-of-giving-backend/public/
cd the-gift-of-giving-backend

If you're using Microsoft Windows, don't use the -p option.

Install the dependencies

Then, with the project's directory structure created, it's time to install the third-party dependencies that the application requires. These are

DependencyDescription
laminas-diactorosThis package simplifies returning JSON content. Okay, it's not strictly necessary, but I love using it as it's so intuitive.
php-di/slim-bridgeMarketed as "The dependency injection container for humans", PHP-DI is a pretty straightforward and intuitive DI container. We’re using it so that we can instantiate certain application resources once and then make them available to the application.
slim/psr7In addition to Slim, this library is required to integrate PSR-7 into the application. It's also not strictly necessary, but I feel it makes the application more maintainable and portable.
slim/slimNaturally, as the application is based on the Slim framework, then Slim is required.

To install the packages, in the top-level directory of the project, run the command below.

composer require --with-all-dependencies \
    laminas/laminas-diactoros \
    php-di/slim-bridge \
    slim/psr7 \
    slim/slim

If you're using cmd.exe on Microsoft Windows, replace the backslash character () with a caret (^). If you're using PowerShell, replace the backslash character () with a backtick (`).

Create the bootstrap file

Next, in the public directory, create a new file named index.php. In the new file, paste the code below.

<?php

use DI\Container;
use Laminas\Diactoros\Response\EmptyResponse;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
use Slim\Middleware\ContentLengthMiddleware;
use Slim\Views\Twig;
use Slim\Views\TwigMiddleware;
use Twig\Extra\Markdown\DefaultMarkdown;
use Twig\Extra\Markdown\MarkdownRuntime;
use Twig\RuntimeLoader\RuntimeLoaderInterface;

require __DIR__ . '/../vendor/autoload.php';

$container = new Container;

$container->set('charities', function(): array {
    $asrcDescription = <<<EOF
[The Asylum Seeker Resource Centre (ASRC)](https://asrc.org.au) is Australia's largest human rights organisation providing support to people seeking asylum.

They are an independent not-for-profit organisation whose programs support and empower people seeking asylum to maximise their own physical, mental and social well-being.

They champion the rights of people seeking asylum and mobilise a community of compassion to create lasting social and policy change.
EOF;

    $mjfDescription = <<<EOF
[The Michael J. Fox Foundation for Parkinson's Research](https://www.michaeljfox.org/) exists for one reason: to accelerate the next generation of Parkinson's disease (PD) treatments.

In practice, that means identifying and funding projects most vital to patients; spearheading solutions around seemingly intractable field-wide challenges; coordinating and streamlining the efforts of multiple, often disparate, teams; and doing whatever it takes to drive faster knowledge turns for the benefit of every life touched by PD.
EOF;

    $mecfsDescription = <<<EOF
[The ME-CFS Portal](https://www.me-cfs.net/) is the largest self-help group in German-speaking countries for people with [Myalgic Encephalomyelitis / Chronic Fatigue Syndrome (ME/CFS)](https://www.nhs.uk/conditions/chronic-fatigue-syndrome-cfs/) and [Long-COVID](https://www.cdc.gov/coronavirus/2019-ncov/long-term-effects/index.html).

The ME-CFS Portal provides comprehensive information and support to people with ME/CF, those who suspect that they may have it, and those wanting to know more about it.

Have you been diagnosed with ME/CF or think that you may suffer from it but don't know what to do? Do you know someone with ME/CF and want to learn more about it? The ME-CFS Portal is the resource you need!
EOF;

    $pcrfDescription = <<<EOF
The core mission of [the Australian Pancreatic Cancer Foundation (PanKind)](https://pankind.org.au/) as a cancer support in Australia is to improve the survival rates and quality of life for pancreatic cancer patients and their families.

It takes a strategic and collaborative approach to address the challenges of pancreatic cancer. We focus on raising awareness, providing support and investing in ground-breaking research.
EOF;

    $rrDescription = <<<EOF
[Reuben's Retreat](https://www.reubensretreat.org/) walks side-by-side, offering emotional and practical help through family support charity to families of child loss or those that have a child who is complexly poorly and may face an uncertain future.

It enables families to create memories cocooned in the sanctuary of Reuben's Retreat underpinned by their army of love and compassionate hearts.
EOF;

$tbbDescription = <<<EOF
[The Birthday Bank](https://thebirthdaybank.org.uk/) provides you with a birthday celebration pack, containing gifts, a cake, and the other essentials that you need to celebrate your child's special day, such as cards, wrap, candles, decorations and the all-important badge! They show families in need that they are supported and valued by friends in their community
EOF;

    return [
        'asrc' => [
            'image' => 'asrc-background.jpg',
            'name' => 'The Asylum Seeker Resource Center',
            'description' => $asrcDescription,
            'actions' => [
                'Legal aid to those in refugee detention',
                'Advocacy to people in onshore and offshore detention',
                'Train young advocates to tell their stories',
                'Education and training including english language skills and training and professional development courses',
                'Employment programs which build skills, confidence and agency',
                'Paid internships for people seeking asylum who want to develop skills in program evaluation',
                ],
            'email' => 'admin@asrc.org.au',
            'website' => 'https://asrc.org.au',
            'donation_link' => 'https://donate.asrc.org.au/noonebehind',
            'social' => [
                'instagram' => 'asrc1',
                'linkedin' => 'company/asylum-seeker-resource-centre',
                'twitter' => 'ASRC1',
            ]
        ],
        'mjfpr' => [
            'image' => 'michael-j-fox.jpg',
            'name' => "The Michael J. Fox Foundation",
            'description' => $mjfDescription,
            'email' => 'info@michaeljfox.org',
            'website' => 'https://www.michaeljfox.org/',
            'donation_link' => 'https://give.michaeljfox.org/give/421686/#!/donation/checkout',
            'actions' => [
                "Build improved knowledge about the lived experience of Parkinson's disease",
                "Find an objective test for Parkinson's",
                "Engage patients in research",
                "Support the development of new treatments and a cure",
            ],
            'social' => [
                'instagram' => 'michaeljfoxorg',
                'linkedin' => 'company/michaeljfoxorg',
                'twitter' => 'MichaelJFoxOrg',
            ]
        ],
        'mecfs' => [
            'image' => 'me-cfs-portal.jpg',
            'name' => 'ME-CFS Portal (German)',
            'description' => $mecfsDescription,
            'email' => 'program_inquiries@newyork.msf.org',
            'website' => 'https://www.me-cfs.net/',
            'donation_link' => '',
            'actions' => [
                "Provides details about symptoms, diagnosis, important research, available therapies",
                "Support when dealing with government agencies and departments",
                "A forum to talk with, support, and receive support from others",
                "A regularly updated blog on everything related to ME/CF",
                "A database of knowledge about ME/CF as shared by others"
            ],
            'social' => [
                'instagram' => 'me_cfs_portal',
                'twitter' => 'MECFS_Portal',
                'youtube' => 'UCRhCLjPGVo1ZlpsU94xu-8g'
            ]
        ],
        'pcrf' => [
            'image' => 'pancreatic-cancer-foundation.jpg',
            'name' => 'The Australian Pancreatic Cancer Research Foundation',
            'description' => $pcrfDescription,
            'email' => ' info@pankind.org.au',
            'website' => 'https://pankind.org.au/',
            'donation_link' => 'https://pankind.org.au/donate/',
            'actions' => [
                "Invest in research to accelerate treatments and improve survival",
                "Advocate on behalf of the pancreatic cancer community for equitable optimal and earlier access to diagnosis, treatment, and care",
                "Increase awareness of pancreatic cancer to support earlier diagnosis, and raise funds for research",
                "Support patients and carers through comprehensive information, resources, and links to support services"
            ],
            'social' => [
                'instagram' => 'pankind_apcf',
                'twitter' => 'PanKind_APCF',
            ]
        ],
        'rr' => [
            'image' => 'ruebens-retreat.jpg',
            'name' => "Reuben's Retreat",
            'description' => $rrDescription,
            'email' => 'contact@reubensretreat.org',
            'website' => 'https://www.reubensretreat.org/',
            'donation_link' => 'https://www.reubensretreat.org/get-involved/donate/',
            'actions' => [
                "Provide practical and emotional support and promise to walk side by side with a family on their journey",
                "Deliver a bespoke and tailor-made support package for each family and guidance to help them navigate their journey",
                "Peer groups which enable parents of loss to come together, share their story and gain peer support",
                "Counselling and talking therapies can help families to navigate the raw and painful emotions",
            ],
            'social' => [
                'instagram' => 'reubensretreat',
                'linkedin' => 'company/3342334',
                'twitter' => 'ReubensRetreat',
            ]
        ],
        'tbb' => [
            'image' => 'the-birthday-bank.jpg',
            'name' => 'The Birthday Bank',
            'description' => $tbbDescription,
            'email' => 'laura@thebirthdaybank.org.uk',
            'website' => 'https://thebirthdaybank.org.uk/',
            'donation_link' => 'https://thebirthdaybank.org.uk/#features',
            'social' => [
                   'instagram' => 'thebirthdaybank',
                'linkedin' => 'in/lauracunningham3',
            ]
        ],
    ];
});

AppFactory::setContainer($container);
$app = AppFactory::create();

$app->get('/charities', function (Request $request, Response $response, array $args): Response {
    return new JsonResponse(
        $this->get('charities'), 
        200, 
        ['Access-Control-Allow-Origin' => '*']
    );
})->add(new ContentLengthMiddleware());

$app->run();

Despite the amount of code in the file, there isn't a lot going on in index.php.

It starts off by initialising a DI (Dependency Injection) container ($container). Then, it registers a single service with it, named charities, which returns an associative array of charities.

Each element's key is a short code for the charity, and the value is an associative array of the charity's properties. These include the charity's name, description, some of the key actions that it undertakes, its email address, website, and a direct donation link.

Following that, a new Slim application ($app) is initialised with the container, and the application's sole route is added. The route returns the charities array in JSON format.

Two things are worth noting about the route:

  • Responses from the endpoint include the Access-Control-Allow-Origin header so that XHR (XMLHttpRequest) requests can be successfully sent to it.
  • The ContentLengthMiddleware is added to the request so that the response contains the length of the response's content body, should requests require it.

Finally, the app is started.

Test that the backend works

Before you move on and create the front end, test that the backend works. Start it by running the command below.

php -S localhost:8080 -t public

Then, in a separate terminal, use curl to make a request to the charities endpoint by running the following command.

curl http://localhost:8080/charities

You should see output similar to the example below.

{"asrc":{"image":"asrc-background.jpg","name":"The Asylum Seeker Resource Center","description":"[The Asylum Seeker Resource Centre (ASRC)](https://asrc.org.au) is Australia\u0027s largest human rights organisation providing support to people seeking asylum. \n\nThey are an independent not-for-profit organisation whose programs support and empower people seeking asylum to maximise their own physical, mental and social well-being. \n\nThey champion the rights of people seeking asylum and mobilise a community of compassion to create lasting social and policy change.","actions":["Legal aid to those in refugee detention","Advocacy to people in onshore and offshore detention","Train young advocates to tell their stories","Education and training including english language skills and training and professional development courses","Employment programs which build skills, confidence and agency","Paid internships for people seeking asylum who want to develop skills in program evaluation"],"email":"admin@asrc.org.au","website":"https://asrc.org.au","donation_link":"https://donate.asrc.org.au/noonebehind","social":{"instagram":"asrc1","linkedin":"company/asylum-seeker-resource-centre","twitter":"ASRC1"}},

If you'd like to format the JSON output so that it's easier to read, pipe it to jq or paste it into JSONLint.com.

If you saw the charities list output to the terminal in JSON format, then you're ready to build the front end.

Create the Vue.js frontend

The front end will be built with a combination of JavaScript, Vue.js, and Tailwind CSS. If you're new to any of these, please don't be concerned. This isn't an overly complicated application. Here's a small design layout to help you visualise what it will look like.

The design layout of the frontend application. A header block spans the entire screen. Below the header is a great of 6 charity blocks, 3 charity blocks per row.

Bootstrap a new Vue.js application

Now it's time to create the front end. Navigate up a directory and scaffold a new Vue.js application by running the commands below.

cd ../
npm init vue@latest

Respond to the prompts as follows.

QuestionResponse
Need to install the following packages:  create-vue@3.4.1Ok to proceed? (y)y
Project namethe-gift-of-giving-frontend
Package namethe-gift-of-giving-frontend
Add TypeScript? No
Add JSX Support?No
Add Vue Router for Single Page for Application development?No
Add Pinia for state management?No
Add Vitest for Unit Testing?No
Add an End-to-End Testing Solution? No
Add ESLint for code quality?No

After the command completes, the application will be available in a new directory named the-gift-of-giving-frontend in the current directory. Change into the newly created directory, install the dependencies, and start the application in development mode by running the commands below.

cd the-gift-of-giving-frontend
npm install
npm run dev

After running the second npm command, open your browser to http://localhost:5173/ where you should see it look like the screenshot below.

The default, scaffolded Vue.js application

Add extra dependencies

The scaffolded application already provides a lot of functionality, however, it will need a few more dependencies for it to function properly. These are

DependencyDescription
AutoPrefixerAutoPrefixer automatically manages vendor prefixes in the generated CSS.
PostCSSPostCSS helps Tailwind CSS build faster and avoid any quirks or workarounds.
Tailwind CSSTailwind CSS is a utility-first CSS framework which I love using as it greatly simplifies creating original and professional designs.
Vue3 Markdown ITThis provides Markdown support to convert each charity description, written in Markdown, into HTML.

To install them, in the-gift-of-giving-frontend directory in a new terminal session, run the following commands.

npm install -D autoprefixer postcss tailwindcss vue3-markdown-it
npx tailwindcss init -p

Then, update tailwind.config.js, in the top-level directory of the project, to match the following configuration.


/** @type {import('tailwindcss').Config} */
module.exports = {
    content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx,vue}"],
    theme: {
        extend: {},
    },
    plugins: [],
};

Next, download additional.css from the project's GitHub repository to src/assets. Then, update src/assets/main.css to match the following — make sure you remove the custom CSS at the end of the file after these two import statements.

@import './base.css';
@import './additional.css';

For what it's worth, while the classes in additional.css could be included directly in the Vue.js Component templates, I felt that doing so would be too distracting and that it was better to extract them.

Remove the unnecessary files

With the frontend application working, it's time to remove some of the auto-generated files which, while potentially helpful, aren't necessary.

In src/components remove HelloWorld.vue, TheWelcome.vue, and WelcomeItem.vue, as well as the icons directory. Then, remove references to them by updating src/App.vue to match the code below.

<script setup>
</script>

<template>
    <main>
    </main>
</template>

<style scoped></style>

Download the frontend's images

The application uses two types of images, a header image for each charity, along with three social media icons; one for Instagram, LinkedIn, and Twitter. Download an archive of the images from the project's GitHub repository, then extract the archive to the public directory.

Add the core application logic

Now, it's time to add the core application logic. There's not that much to it, just a single component named CharitiesList. It is responsible for retrieving the list of charities and beautifully rendering them on the page.

To build it, create a new file named CharitiesList.vue in src/components. In that new file, paste the HTML and JavaScript code below.

<template>
    <div class="mt-4">
        <div id="pick-a-charity" class="mt-6 lg:mt-12">
            <h2 class="mb-3">Our Charities</h2>
            <div id="charities-list">
                <div
                    class="charity-item flex flex-col hover:drop-shadow-md"
                    v-for="(charity, code) in charities"
                    :key="code"
                >
                    <div id="charity-header" class="drop-shadow-md">
                        <img
                            :src="'/images/backgrounds/' + charity.image"
                            :alt="`Background header image for ${charity.name}`"
                            class="rounded-t-md"
                        />
                    </div>
                    <div id="charity-details" class="px-4 pt-4">
                        <h3 class="mb-4">{{ charity.name }}</h3>
                        <Markdown :source="charity.description" />
                    </div>
                    <div id="key-actions" v-if="charity.actions">
                        <h4>Key Actions</h4>
                        <ul>
                            <li v-for="action in charity.actions" :key="action">
                                {{ action }}
                            </li>
                        </ul>
                    </div>
                    <div id="social-connections" v-if="charity.social">
                        <h4>Connect Socially</h4>
                        <div class="flex">
                            <a
                                v-for="(username, platform) in charity.social"
                                :key="platform"
                                :href="`https://${platform}.com/${username}`"
                                :title="`Follow ${charity.name} on ${ucfirst(platform)}`"
                                class="mr-2"
                                target="_blank"
                            >
                                <img
                                    :src="`/images/social-media-icons/${platform}.png`"
                                    width="24"
                                    class="contrast-50 grayscale hover:contrast-100 hover:grayscale-0 ease-in-out duration-200"
                                    :alt="`Follow ${charity.name} on ${ucfirst(platform)}`"
                            /></a>
                        </div>
                    </div>

                    <div id="donate-wrapper" class="pt-4">
                        <a
                            :href="`${charity.donation_link}`"
                            class="donate block text-center no-underline rounded-b-md w-full py-8 font-bold text-white uppercase bg-purple-900 hover:bg-purple-800 ease-in-out duration-200"
                            target="_blank"
                        >
                            Donate &rarr;
                        </a>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
import Markdown from "vue3-markdown-it";

export default {
    data() {
        return {
            api: "http://localhost:8080",
            charities: [],
    };
},
components: {
    Markdown,
},
name: "CharitiesList",
methods: {
    async fetchData() {
        const res = await fetch(`${this.api}/charities`, {
            mode: "cors",
        });
        this.charities = await res.json();
        console.log(this.charities);
    },
    ucfirst: function(str) {
        if (!str) return str;
            return str[0].toUpperCase() + str.slice(1);
        },
    },
    mounted() {
        this.fetchData();
    },
};
</script>

<style scoped></style>

Like all Vue.js Components, the file consists of a template and a script section. The template section renders the list of charities retrieved in the script section rendering the respective details of each one. For each charity, its name, description, image, actions, respective social media accounts, and direct donation link are rendered.

I've done my best to keep it as succinct as possible, making use of the Vue.js elements that make the most sense, such as conditional rendering, list rendering, and class and style bindings.

In the script section's fetchData() method, which is run when the component is mounted, the /charities endpoint of the backend application is requested and the response is set in the charities element.

In addition, a Markdown component from the Vue3 Markdown IT package is added, so that it can be used to convert charity descriptions from Markdown to HTML.

Update App.vue

Next, it's time to enable the CharitiesList component. Do that by importing it, and then adding it to the template section of App.vue, as in the code below.

hl_lines="2,7"
<script setup>
import CharitiesList from "./components/CharitiesList.vue";
</script>

<template>
    <main>
        <CharitiesList />
    </main>
</template>

<style scoped></style>

Update index.html

Next, you need to make a small update to index.html, so that the header of the page will be for the project, and not for the original scaffolded application. They also add two paragraphs explaining what the application is about.

To do that, update index.html to match the code below.

hl_lines="7,10,11,12,13,14,15,16,17,18,19,20,21,22,23"
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Help Those Helping Others</title>
</head>
<body>
    <header class="bg-gradient-to-r from-violet-500 to-violet-900">
        <div class="app-content pb-4">
            <h1>Help Those Helping Others</h1>
            <p class="text-xl mb-6 mt-8 text-white">
                At Christmas time, we typically think about giving gifts to friends, family, and loved ones.
                But what about those who don't have anyone to receive a gift from, or aren't able to give gifts to others?
                What about those who are desperately in need and a present is the last thing on their mind?
            </p>
            <p class="text-xl text-white">
                If you want to help others in need, but don't know how, here are six charities from around the world who care for and support people every day of the year, people in need for so many reasons.
                By donating to one or more of them, you can help them to help those less fortunate, less privileged than yourself?
            </p>
        </div>
</header>

<div id="app"></div>

<script type="module" src="/src/main.js"></script>
</body>
</html>

Test the application

Now that all of the code has been written, it's time to test the application. If you still have your browser open from before, when you first tested the front end, switch to it, otherwise, open a new window or tab in your browser of choice to http://localhost:5173/. It should look like the screenshot below.

Example screenshot of the application, showing a charity card with details, social media links, and a donate button.

Then, click on any of the donate links under the respective charities to open the charity's donation link. If you have some funds to spare, please consider making a donation. They're all doing very worthwhile work in their respective communities.

The application's finished

It wasn't the most complicated of applications, but rather a fun exercise in building a Vue.js-powered frontend backed by a simplistic PHP API backend. Feel free to play with the code, change the charities, and update it to your heart's content. The code for the front end and back end are available on GitHub.

Are any of these charities ones you're familiar with? If not, which worthwhile charity do you want to draw attention to? Share it with me on Twitter.

Matthew Setter is the PHP Editor in the Twilio Voices team and a PHP and Go developer. He’s also the author of Deploy With Docker Compose and Mezzio Essentials. When he’s not writing PHP code, he’s editing great PHP articles here at Twilio. You can find him at msetter@twilio.com, Twitter, GitHub, and LinkedIn.