Создание некоммерческого приложения с помощью Next.js и Cosmic

В настоящее время существует множество местных и глобальных проблем, и чаще всего кажется, что мы мало чем можем помочь. Но всегда есть что-то, что мы можем сделать!

Именно поэтому мы собираемся создать простое некоммерческое приложение, которое будет показывать потенциальных студентов и их истории, а также позволит всем желающим сделать пожертвование, используя Stripe. К тому времени, когда вы закончите этот урок, у вас будет базовый шаблон для сайта для студентов-доноров, который использует современные инструменты для быстрого создания и легкого масштабирования.

Инструменты, которые мы будем использовать

Для создания этого приложения мы будем использовать следующие технологии:

  • Next.js — фреймворк React, позволяющий легко создать полнофункциональное приложение.
  • Cosmic — безголовая CMS, которая дает нам возможность быстро управлять данными об учениках и пожертвованиях.
  • Stripe — платежная система, которая позволит нам принимать пожертвования.
  • Tailwind CSS — фреймворк стилей, позволяющий нам создавать отзывчивые макеты.

TL;DR

Установите шаблон приложения

Посмотрите живую демонстрацию

Проверьте код

Создание учетной записи Cosmic

Первое, что вам нужно будет создать, это бесплатный аккаунт Cosmic. Затем вам будет предложено создать новый проект. Убедитесь, что вы выбрали опцию «Начать с нуля». Проект будет называться non-profit-cms, но вы можете назвать его как угодно. Вы можете оставить окружение bucket как «Production».

Далее нам нужно будет создать несколько типов объектов для наших доноров и студентов. На приборной панели Cosmic перейдите в раздел «Добавить тип объекта». Вы увидите следующее окно.

Убедитесь, что вы выбрали опцию «Множественный» объект. Вам нужно только заполнить «Единственное имя» Donor, а остальные два поля сгенерируются автоматически. Далее нам нужно определить метаполя в «Модели содержимого».

У нас будет несколько различных полей для наших доноров: имя студента, сумма пожертвования, идентификатор сессии из Stripe, и, по желанию, имя донора и сообщение. После завершения работы у вас должны быть следующие метаполя.

Мы будем добавлять новые объекты доноров при каждом пожертвовании через Stripe, а затем сможем показывать пожертвования для каждого студента, когда начнем создавать приложение Next. Перед этим давайте закончим с типами объектов, которые нам понадобятся, добавив еще один тип под названием Student.

Вернитесь в панель Cosmic и создайте «Новый тип объекта». Он также будет иметь тип «Multiple», и на этот раз «Singular Name» будет Student. И снова нам нужно создать несколько метаполей для этого типа объекта. Поэтому прокрутите вниз до раздела «Модель содержимого» и добавьте эти метаполя: имя студента, специальность, университет, его историю и снимок головы. Вот как должны выглядеть все метаполя, когда вы закончите.

Теперь, когда вы получите данные о своих студентах и донорах, вы должны увидеть что-то похожее на это для студентов на вашей приборной панели.

И что-то похожее на это для доноров на вашей приборной панели.

Это все, что нам нужно, чтобы все настроить в Cosmic.

Получение некоторых значений для приложения Next

Теперь, когда Cosmic настроен так, как нам нужно, давайте получим несколько переменных окружения, которые понадобятся для приложения Next, которое мы собираемся создать. Зайдите в панель управления Cosmic и перейдите в Bucket > Settings > API Access. Это даст вам возможность доступа, чтения и записи в ваш проект Cosmic. Мы будем работать со студентами и донорами, чтобы иметь возможность вести хороший учет того, кому отправлять соответствующие обновления для студентов.

Прежде чем мы создадим проект Next, есть еще одна служба, которую нам нужно правильно настроить. Нам нужен Stripe, чтобы мы могли принимать пожертвования.

Настройка учетной записи Stripe

Вам нужно перейти на сайт Stripe, чтобы создать бесплатную учетную запись. Главное, в чем вам нужно будет убедиться, это то, что ваша приборная панель оставлена в тестовом режиме и что вы добавили «Публичное название бизнеса» в Settings > Account Details.

Теперь, когда ваша приборная панель настроена, вы можете получить две последние переменные окружения, которые понадобятся нам для приложения. Перейдите в раздел [Developers > API keys] (https://dashboard.stripe.com/test/apikeys) и получите Publishable key и Secret key.

С этими значениями мы готовы к созданию приложения Next.

Настройка приложения Next.js

К счастью для нас, существует команда yarn для создания нового приложения Next с готовыми конфигурациями. Таким образом, мы можем просто перейти к написанию кода. Чтобы создать этот проект, выполните в терминале следующую команду:

$ yarn create next-app --typescript
Войти в полноэкранный режим Выйти из полноэкранного режима

Затем мы можем добавить пакеты, с которыми будем работать, выполнив следующую команду:

$ yarn add cosmicjs tailwindcss stripe postcss @heroicons/react
Enter fullscreen mode Выйти из полноэкранного режима

Осталась последняя часть настройки, которую нужно сделать, прежде чем мы сможем погрузиться в код.

Добавление файла .env

Помните те значения, которые мы взяли из приборной панели Cosmic и приборной панели Stripe? Мы собираемся добавить их в проект в файл .env. В корне проекта добавьте новый файл .env. Внутри этого файла добавьте следующие значения:

# .env
READ_KEY=your_cosmic_read_key
WRITE_KEY=your_cosmic_write_key
BUCKET_SLUG=your_cosmic_bucket_slug

STRIPE_SECRET_KEY=your_stripe_secret_key
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=your_stripe_publishable_key
Войти в полноэкранный режим Выйти из полноэкранного режима

Когда все эти значения наконец-то установлены, мы можем приступить к самой интересной части создания приложения.

Настройка Tailwind CSS

Для того чтобы воспользоваться преимуществами установленного нами пакета Tailwind CSS, необходимо добавить несколько настроек. В корне вашего проекта должен быть файл tailwind.config.js. Откройте этот файл и замените существующий код на следующий.

// tailwind.config.js

module.exports = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    container: {
      center: true,
    },
    fontFamily: {
      "sans": ["Helvetica", "Arial", "sans-serif"],
    }
  },
  plugins: [],
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь загляните в папку styles и вы должны увидеть файл global.css. Именно так мы включим TailwindCSS в наш проект. Откройте этот файл и замените существующий код на следующий.

// global.css
@tailwind base;
@tailwind components;
@tailwind utilities;
Войти в полноэкранный режим Выйти из полноэкранного режима

Это все, что нам нужно для работы наших стилей. Конечно, вы можете написать CSS самостоятельно, но иногда лучше воспользоваться уже существующим пакетом стилей.

Пара полезных компонентов

Теперь, когда мы можем создать стиль приложения, давайте добавим несколько компонентов, которые помогут связать страницы воедино. Мы добавим навигационную панель, чтобы постоянно возвращаться на главную страницу, и нижний колонтитул для брендинга, чтобы всегда показывать название вашей организации. В корне проекта добавьте новую папку components.

Мы начнем с создания навигации, поэтому внутри папки components добавьте новый файл Navigation.tsx. Он будет отображать ссылку домой. Добавьте следующий код для создания этого компонента.

// Navigation.tsx

import Link from 'next/link'
import { HomeIcon } from '@heroicons/react/solid'

export default function Navigation() {
  return (
    <header className="p-4 border-b-2">
      <Link passHref href={'/'}>
        <div className="flex hover:cursor-pointer gap-2">
          <HomeIcon className="h-6 w-6 text-blue-300" />
          <div>Home</div>
        </div>
      </Link>
    </header>
  )
}
Вход в полноэкранный режим Выход из полноэкранного режима

Последний маленький компонент, который нам нужно добавить, это нижний колонтитул. В папку components добавьте новый файл Footer.tsx. Он будет отображать текст и изображение иконки внизу каждой страницы. В этот новый файл добавьте следующий код.

// Footer.tsx

export default function Footer() {
  return (
    <footer className="p-4 border-t-2">
      <a
        href="https://www.cosmicjs.com?ref=non-profit-cms"
        target="_blank"
        rel="noopener noreferrer"
      >
        <div className="flex gap-2">
          <div>Powered by</div>
          {/* eslint-disable-next-line @next/next/no-img-element */}
          <img
            alt="Cosmic logo"
            src="https://cdn.cosmicjs.com/049dabb0-8e19-11ea-81c6-b3a804bfff46-cosmic-dark.png"
            width="100"
            height="100"
          />
        </div>
      </a>
    </footer>
  )
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Это только два компонента, которые нам нужно было сделать. Теперь вам нужно обновить файл _app.tsx, чтобы включить в него компонент Footer. Таким образом, он будет отображаться на каждой странице приложения. Поэтому откройте этот файл и обновите существующий компонент, чтобы он соответствовал этому:

// _app.tsx
...
import Footer from '../components/Footer'

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <div className="flex flex-col h-screen justify-between">
      <Component {...pageProps} />
      <Footer />
    </div>
  )
}
...
Вход в полноэкранный режим Выйти из полноэкранного режима

Обратите внимание, что здесь есть новый оператор импорта и что все приложение теперь обернуто в стилизованный div, который также содержит этот элемент колонтитула. Мы добавляем элемент Navigation только на отдельные страницы студентов, к чему мы перейдем позже.

Отображение всех студентов

Мы можем начать работу над приложением Next, чтобы отображать всех студентов всем, кто заходит на сайт. Мы начнем с обновления существующего файла index.tsx, чтобы импортировать и использовать Cosmic для получения данных об учениках. Поэтому добавьте следующий код прямо под существующими импортами в файле.

// index.tsx
...
import Cosmic from 'cosmicjs'

const api = Cosmic()

const bucket = api.bucket({
  slug: process.env.BUCKET_SLUG,
  read_key: process.env.READ_KEY,
})
...
Войти в полноэкранный режим Выйти из полноэкранного режима

Затем нам нужно добавить функцию getStaticProps для получения данных о студентах из Cosmic:

// index.tsx
...
export async function getStaticProps() {
  const query = {
    type: 'students',
  }
  const studentsReq = await bucket.getObjects({ query })
  const students: Student[] = studentsReq.objects

  return {
    props: {
      students,
    },
  }
}
...
Войти в полноэкранный режим Выход из полноэкранного режима

Эта функция запускается только во время сборки страницы, поэтому вы не будете делать запрос каждый раз. Внутри этой функции мы определяем query, который мы отправим в запросе Cosmic. Затем мы делаем запрос к bucket, который мы определили ранее, и получаем все возвращенные объекты студентов. Наконец, мы отправляем массив students в props компонента страницы.

Теперь, когда у нас есть эти данные, мы можем вывести некоторые элементы на главную страницу. Вы можете удалить весь текущий код, который находится внутри компонента Home, и заменить его следующим:

// index.tsx
...
const Home: NextPage = ({ students }) => {
  if (!students) {
    return <div>Loading our incredible students...</div>
  }

  return (
    <div>
      <Head>
        <title>Student Raiser</title>
        <meta
          name="description"
          content="A website dedicated to helping students receive the funding they need for college"
        />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main>
        <h1 className="px-11 pt-11 text-2xl">Students in your area</h1>
        <div className="flex flex-wrap gap-4 p-11">
          {students.map((student: Student) => (
            <div
              className="hover:cursor-pointer w-64"
              key={student.metadata.name}
            >
              <Link
                passHref
                href={`/student/${encodeURIComponent(student.slug)}`}
              >
                <div
                  key={student.slug}
                  className="border-2 rounded max-w-sm rounded overflow-hidden shadow-lg"
                >
                  {/* eslint-disable-next-line @next/next/no-img-element */}
                  <img
                    src={`${student.metadata.student_headshot.imgix_url}?w=400`}
                    alt={student.metadata.name}
                    className="w-full"
                    style={{ backgroundPosition: 'cover' }}
                  />
                  <div className="p-4">
                    <div className="text-amber-800 p-1">
                      {student.metadata.name}
                    </div>
                    <div className="border-b-2 p-1">
                      {student.metadata.major}
                    </div>
                    <div className="p-1">{student.metadata.university}</div>
                  </div>
                </div>
              </Link>
            </div>
          ))}
        </div>
      </main>
    </div>
  )
}
...
Вход в полноэкранный режим Выход из полноэкранного режима

Это отображение на массив students для создания элемента для выделения каждого студента. На каждый из этих элементов можно нажать, чтобы узнать больше о конкретном студенте, и именно на этой странице мы сейчас и будем работать. Мы вернемся и добавим больше стилей, но если вы запустите приложение с помощью yarn dev, вы должны увидеть что-то похожее на это.

Создание страницы для отдельных студентов

Теперь мы воспользуемся встроенной в Next динамической маршрутизацией для создания страниц для каждого ученика. Добавьте новую папку в каталог pages под названием student. Внутри этой папки добавьте новый файл [имя].tsx.

Давайте начнем с добавления импортов, которые понадобятся для работы этой страницы. В верхней части файла [name].tsx добавьте следующие строки.

// [name].tsx

import { useEffect, useState } from 'react'
import Cosmic from 'cosmicjs'
import { Donor, Student } from '../../types'
import Navigation from '../../components/Navigation'
import {
  BadgeCheckIcon,
  ExclamationIcon,
  UserCircleIcon,
  UserIcon,
} from '@heroicons/react/solid'
...
Войти в полноэкранный режим Выход из полноэкранного режима

Пока не беспокойтесь о файле types. Мы добавим его в ближайшее время. А пока давайте добавим скелет для компонента Student под нашими импортами.

// [name].tsx
...
function Student({ student, donors }) {
  return (
    <>
      <h2 className="container text-3xl py-8">{student.metadata.name}</h2>
    </>
  )
}

export default Student
Вход в полноэкранный режим Выход из полноэкранного режима

Мы еще многое добавим в этот компонент, но сначала нам нужно получить данные student и donors. Мы будем использовать функцию getServerSideProps для получения данных о конкретном студенте из Cosmic при каждом вызове этого маршрута. Все это не происходит в браузере, поэтому данные остаются в безопасности.

// [name].tsx
...
export async function getServerSideProps(context) {
  const slug = context.params.name

  const studentRes = await bucket.getObjects({
    props: 'metadata,id',
    query: {
      slug: slug,
      type: 'students',
    },
  })

  const student: Student = studentRes.objects[0]

  try {
    const donorsRes = await bucket.getObjects({
      props: 'metadata',
      query: {
        type: 'donors',
        'metadata.student': slug,
      },
    })

    let total

    const donors: Donor[] = donorsRes ? donorsRes.objects : null

    if (donors.length === 1) {
      total = donors[0].metadata.amount
    } else {
      total = donors
        .map((donor) => donor.metadata.amount)
        .reduce((prev, curr) => prev + curr, 0)
    }

    return {
      props: {
        student,
        donors,
        total,
      },
    }
  } catch {
    return {
      props: {
        student,
        donors: null,
        total: 0,
      },
    }
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Затем мы передадим эти данные в компонент, чтобы выделить конкретного студента для пользователей и потенциальных доноров. В компоненте Student мы сделаем несколько вещей. Во-первых, мы проверим, была ли страница студента посещена через перенаправление со страницы проверки Stripe. Затем мы отобразим информацию о студенте, которую мы храним в Cosmic. Далее у нас будет форма для заполнения донорами, если они хотят сделать пожертвование для этого конкретного студента. И наконец, у нас будет список всех жертвователей для этого конкретного студента.

Итак, вы можете заменить набросок компонента Student следующим полным кодом.

// [name].tsx
...
function Student({ student, donors, total }) {
  const [query, setQuery] = useState<string>('')

  useEffect(() => {
    // Check to see if this is a redirect back from Checkout
    const query = new URLSearchParams(window.location.search)

    if (query.get('success')) {
      setQuery('success')
      console.log('Donation made! You will receive an email confirmation.')
    }

    if (query.get('canceled')) {
      setQuery('canceled')
      console.log(
        'Donation canceled -- something weird happened but please try again.'
      )
    }
  }, [])

  return (
    <div>
      <Navigation />
      {query === 'success' && (
        <div
          className="bg-green-100 rounded-lg py-5 px-6 mb-3 text-base text-green-700 inline-flex items-center w-full"
          role="alert"
        >
          <BadgeCheckIcon className="w-4 h-4 mr-2 fill-current" />
          Donation made! You will receive an email confirmation.
        </div>
      )}
      {query === 'canceled' && (
        <div
          className="bg-yellow-100 rounded-lg py-5 px-6 mb-3 text-base text-yellow-700 inline-flex items-center w-full"
          role="alert"
        >
          <ExclamationIcon className="w-4 h-4 mr-2 fill-current" />
          Donation canceled -- something weird happened but please try again.
        </div>
      )}
      <h2 className="container text-3xl py-8">{student.metadata.name}</h2>
      <div className="container flex gap-4">
        <div>
          {/* eslint-disable-next-line @next/next/no-img-element */}
          <img
            src={`${student.metadata.student_headshot.imgix_url}?w=800`}
            alt={student.metadata.name}
            style={{ backgroundPosition: 'cover' }}
          />
          <div className="container border-y-2 my-4">
            <p className="font-bold pt-4 px-2">
              Major: {student.metadata.major}
            </p>
            <p className="font-bold border-b-2 pb-4 px-2">
              University: {student.metadata.university}
            </p>
            <p className="py-4 px-2">{student.metadata.story}</p>
          </div>
        </div>
        <div>
          <p className="font-bold text-xl pb-4">Total raised: ${total}</p>
          <form
            action="/api/donation"
            method="POST"
            className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"
          >
            <input name="student_id" type="hidden" value={student.id} />
            <div className="mb-4">
              <label
                className="block text-gray-700 text-sm font-bold mb-2"
                htmlFor="amount"
              >
                Donation Amount
              </label>
              <input
                className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
                name="amount"
                type="number"
                defaultValue={100}
              />
            </div>
            <div className="mb-4">
              <label className="block text-gray-700 text-sm font-bold mb-2">
                Name
              </label>
              <input
                className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
                name="name"
                type="text"
                defaultValue="Anonymous"
              />
            </div>
            <div className="mb-6">
              <label className="block text-gray-700 text-sm font-bold mb-2">
                Message
              </label>
              <input
                className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
                name="message"
                type="text"
                defaultValue="Good Luck!"
              />
            </div>
            <div>
              <button
                type="submit"
                role="link"
                className="hover:bg-lime-400 text-white font-bold py-2 px-4 border-b-12 border-lime-700 hover:border-lime-500 rounded-full text-lg bg-lime-500 w-64 mt-6 mx-8"
              >
                Make a Donation
              </button>
            </div>
          </form>
          <div className="flex flex-col gap-4 pt-4 w-full">
            {donors ? (
              donors.map((donor: Donor) => (
                <div
                  key={donor.slug}
                  className="border-b-2 rounded p-4 w-64 flex gap-4"
                >
                  <UserCircleIcon className="h-12 w-12 text-green-300" />
                  <div>
                    <p>{donor.metadata.name}</p>
                    <p>${donor.metadata.amount}</p>
                  </div>
                </div>
              ))
            ) : (
              <div className="border-2 rounded p-4 w-64 flex gap-4">
                <UserCircleIcon className="h-12 w-12 text-green-300" />
                <p>Be the first donor!</p>
              </div>
            )}
          </div>
        </div>
      </div>
      <div className="flex flex-col gap-4 p-11 w-full">
        <h2 className="text-xl font-bold">Encouragement</h2>
        {donors ? (
          donors.map((donor: Donor) => (
            <div
              key={donor.slug}
              className="flex flex-col border-b-2 rounded p-4 gap-4"
            >
              <div className="w-64 flex gap-4 w-full">
                <UserIcon className="h-12 w-12 text-green-300" />
                <div className="flex flex-col">
                  <p>{donor.metadata.name}</p>
                  <p>${donor.metadata.amount}</p>
                </div>
              </div>
              <p>{donor.metadata.message}</p>
            </div>
          ))
        ) : (
          <div>You can do it!</div>
        )}
      </div>
    </div>
  )
}
...
Вход в полноэкранный режим Выйти из полноэкранного режима

Если вы запустите приложение сейчас с помощью yarn dev, вы должны увидеть что-то похожее на это для одного из студентов.

Теперь, когда мы заполнили всю функциональность, давайте добавим файл types.ts, чтобы не получить никаких ошибок TypeScript.

Добавление файла types

Наличие определенных типов для наших данных поможет нам узнать, когда API изменились, и мы не столкнемся с большим количеством непредвиденных ошибок в производстве. В корне вашего проекта создайте новый файл types.ts и добавьте следующий код:

// types.ts

export interface Student {
  metadata: {
    name: string
    student_headshot: {
      url: string
      imgix_url: string
    }
    major: string
    university: string
    story: string
  }
  slug: string
}

export interface Donor {
  slug: string
  metadata: {
    name: string
    amount: number
    message: string
  }
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Это поможет нам определить данные, которые мы ожидаем использовать от наших вызовов API к Cosmic.

Добавление функциональности оформления заказа Stripe

Последнее, что нам нужно добавить, это API, который будет вызываться при отправке формы пожертвования, и для этого мы будем использовать Stripe. Если вы найдете в каталоге pages > api вашего проекта, вы увидите файл под названием hello.ts. Вы можете удалить этот файл-заполнитель и создать новый файл под названием donation.ts.

Давайте откроем этот новый файл и следующие импорты.

// donation.ts

import type { NextApiRequest, NextApiResponse } from 'next'
import Cosmic from 'cosmicjs'

const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY)
Вход в полноэкранный режим Выйти из полноэкранного режима

Поскольку нам нужно обработать только один POST-запрос, наша функция-обработчик может быть относительно простой. Мы проведем быструю проверку, чтобы убедиться, что выполняется POST-запрос. Если будет сделан запрос другого типа, то мы выдадим ошибку.

После проверки запроса мы создадим оператор try-catch, который сначала проверит, можем ли мы установить соединение с нашим Cosmic bucket, чтобы добавить нового донора. После этого мы создадим сессию оформления заказа в Stripe, используя информацию о форме, переданную из front-end. Затем мы получаем сессию от Stripe, чтобы добавить их данные в Cosmic.

Наконец, мы создаем метаполе для добавления нового донора в нашу приборную панель Cosmic и используем метод addObject, чтобы убедиться, что этот донор будет записан в правильный объект. Добавьте следующий код, чтобы выполнить всю эту работу.

// donation.ts
...
export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method === 'POST') {
    try {
      const api = Cosmic()

      const bucket = api.bucket({
        slug: process.env.BUCKET_SLUG,
        read_key: process.env.READ_KEY,
        write_key: process.env.WRITE_KEY,
      })

      const { student_id, amount, name, message } = req.body

      const student = (
        await bucket.getObject({ id: student_id, props: 'id,title,slug' })
      ).object

      // Create Checkout Sessions from body params.
      const session = await stripe.checkout.sessions.create({
        line_items: [
          {
            amount: amount * 100, // Cents
            currency: 'usd',
            quantity: 1,
            name: `Donation - ${student.title}`,
          },
        ],
        mode: 'payment',
        success_url: `${req.headers.referer}/?success=true`,
        cancel_url: `${req.headers.referer}/?canceled=true`,
      })

      const donorParams = {
        title: name,
        type: 'donors',
        metafields: [
          {
            title: 'Name',
            type: 'text',
            value: name,
            key: 'name',
          },
          {
            title: 'Student',
            type: 'text',
            value: student.slug,
            key: 'student',
          },
          {
            title: 'Amount',
            type: 'number',
            value: Number(amount),
            key: 'amount',
          },
          {
            title: 'Message',
            type: 'text',
            value: message,
            key: 'message',
          },
          {
            title: 'Stripe Id',
            type: 'text',
            value: session.id,
            key: 'stripe_id',
          },
        ],
      }

      await bucket.addObject(donorParams)

      res.redirect(303, session.url)
    } catch (err) {
      res.status(err.statusCode || 500).json(err.message)
    }
  } else {
    res.setHeader('Allow', 'POST')
    res.status(405).end('Method Not Allowed')
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Готовый код

Вы можете найти весь код для этого проекта в этом репозитории.

Развертывание на Vercel

Вы можете развернуть этот шаблон на Vercel, нажав здесь.

Заключение

Теперь у вас есть полностью интегрированный сайт для пожертвований, который вы можете настроить для любого типа некоммерческих организаций, занимающихся сбором средств и пожертвований. Не стесняйтесь клонировать его и изменять стили в соответствии с потребностями вашей организации.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *