Автор: Мэри Окосун

Безголовые системы управления контентом эффективны во многих отношениях; они дают нам возможность делать все, что мы хотим, с предпочитаемой нами внешней технологией. Strapi — одна из самых популярных безголовых CMS, и она упрощает работу с серверной частью. В этом руководстве мы обсудим, как создать фотогалерею с помощью Strapi и Next.js, используя Cloudinary для хранения наших изображений.

Предпосылки

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

  • Аккаунт на гитхабе
  • Node.js v12 и выше
  • Yarn 1.22+ или npm (только версия 6) для запуска сценариев установки CLI.
  • Облачный аккаунт

Настройка Cloudinary

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

  • Имя облака
  • Ключ API
  • Секрет API

Держите эти данные в секрете и никому их не сообщайте.

Установка экземпляра Strapi

После создания учетной записи Cloudinary пришло время установить экземпляр Strapi. Выполните следующую команду:

yarn create strapi-app strapi-photo --quickstart 
    #OR
    npm create strapi-app strapi-photo --quickstart

This command will create a folder named strapi-photo and install the Strapi instance to it.

После установки Strapi автоматически запустит команду сборки в http://localhost:1337/admin, немедленно перенаправив вас http://localhost:1337/admin/auth/register-admin, потому что вы запускаете ее впервые. Вам нужно будет зарегистрироваться как superuser.

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

Введите Фото в качестве отображаемого имени и нажмите кнопку Продолжить, чтобы добавить поля.

Добавление полей

Мы добавим четыре поля: имя, дату, местоположение и изображение. Следуйте инструкциям ниже:

  • Щелкните текстовое поле.
  • Введите имя в поле Имя.
  • Перейдите на вкладку Дополнительные настройки и установите флажок в поле Обязательно.
  • Нажмите Добавить другое поле.
  • Щелкните поле Дата.
  • Введите Дата в поле Имя.
  • Выберите дату в раскрывающемся списке типов.
  • Перейдите на вкладку Дополнительные настройки и установите флажок в поле Обязательно.
  • Нажмите Добавить другое поле.
  • Щелкните текстовое поле.
  • Введите местоположение в поле Имя.
  • Перейдите на вкладку Дополнительные настройки и установите флажок в поле Обязательно.
  • Нажмите Добавить другое поле.
  • Щелкните поле «Медиа».
  • Введите img в поле «Имя» и выберите Single Media под флажком типа,
  • Перейдите на вкладку Дополнительные настройки и установите флажок в поле Обязательно.
  • Выберите Изображения только в разделе Выберите разрешенные типы мультимедиа.
  • Нажмите Готово.

  • Нажмите Сохранить. При нажатии на кнопку "Сохранить" ваш сервер будет перезапущен. Ваш экземпляр Strapi должен выглядеть так:

Подключение Cloudinary

Прежде чем добавлять данные в созданную нами коллекцию фотографий, нам необходимо подключить нашу учетную запись Cloudinary к экземпляру Strapi. Было бы полезно, если бы вы остановили сервер перед выполнением приведенной ниже команды.

Запустите эту команду в корневой папке вашего приложения:

npm install @strapi/provider-upload-cloudinary
    #OR    
    yarn add @strapi/provider-upload-cloudinary

После добавления пакета Cloudinary вы можете перезапустить сервер, запустив.

npm run develop
    #OR
    yarn run develop

Создайте файл с именем plugins.js внутри папки config и вставьте в него следующий код:

module.exports = ({ env }) => ({
      upload: {
        config: {
          provider: 'cloudinary',
          providerOptions: {
            cloud_name: env('CLOUDINARY_NAME'),
            api_key: env('CLOUDINARY_KEY'),
            api_secret: env('CLOUDINARY_SECRET'),
          },
          actionOptions: {
            upload: {},
            delete: {},
          },
        },
      },
    });

Добавьте следующие переменные в файл .env. Заполните отсутствующие значения соответствующими значениями, найденными на панели управления Cloudinary в разделе Данные учетной записи, и обязательно перезапустите сервер.

CLOUDINARY_NAME=xxxxxxxxxxxxxxxxxxxxxx
    CLOUDINARY_KEY=xxxxxxxxxxxxxxxxxx
    CLOUDINARY_SECRET=xxxxxxxxxxxxxxxx

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

HOST=0.0.0.0
    PORT=1337
    APP_KEYS=xxxxxxxxxxxxx,xxxxxxxxxxxxx
    API_TOKEN_SALT=xxxxxxxxxxxxxx
    ADMIN_JWT_SECRET=xxxxxxxxxxxx
    JWT_SECRET=xxxxxxxxxxxxxxxxxxxxx
    CLOUDINARY_NAME=xxxxxxxxxxxx
    CLOUDINARY_API_KEY=xxxxxxxxxxxxxxxxx
    CLOUDINARY_API_SECRET=xxxxxxxxxxxxxxxx

После добавления недостающих переменных сервер можно перезапустить, запустив npm run develop

Добавить данные в коллекцию фотографий

Вернитесь в свой проект Strapi по адресу http://localhost:1337/admin и нажмите Диспетчер контента. Нажмите Фотографии, затем Создать новую запись**.**

Я решил использовать для этого изображения J Cole и Vector. Вы можете использовать любое изображение, за которым хотите следить. Убедитесь, что вы сохранили и опубликовали.

Я добавил четыре записи.

Войдите в свой Cloudinary, чтобы убедиться, что изображения есть.

Установка ролей и разрешений в Strapi

Чтобы сделать эти данные доступными для использования любой технологией на стороне клиента, нам нужно установить некоторые роли и разрешения — кто имеет доступ к чему и в какой степени. Теперь перейдите в раздел Настройки→(ПЛАГИН ПОЛЬЗОВАТЕЛЯ И РАЗРЕШЕНИЙ)→Роли→Общий.

  1. Прокрутите вниз в разделе Разрешения.
  2. На вкладке Приложение найдите Фото.
  3. Установите флажки рядом с найти и найти.

  1. Нажмите Сохранить.
  2. Перейдите по адресу http://localhost:1337/api/photos?populate=* в своем браузере или любом клиенте API, таком как Postman, и убедитесь, что у вас есть похожий ответ, например:

Установка и настройка Next.js

Да, мы успешно развернули внутреннюю часть нашего приложения. Теперь давайте воспользуемся Next.js для использования его API. Выйдите из папки экземпляра Strapi и выполните следующую команду, чтобы установить Next.js.

yarn create next-app next-photo
    #OR
    npm create next-app next-photo

Эта команда настраивает все автоматически для нас (следующее фото — это имя моей папки, вы можете назвать свою по-другому). Перейдите в next-photo:

cd next-photo

    yarn dev
    #OR
    npm run dev

Одно из основных преимуществ приложений Next.js заключается в том, что все предварительно визуализируется или создается при первой загрузке. По адресу http://localhost:3000 мы должны увидеть экземпляр Next.js по умолчанию:

Поскольку мы будем работать с изображениями из внешнего источника, Cloudinary, нам нужно настроить файл *next.config.js* для оптимизации изображений, который предоставляет NextJS. Не забудьте загрузить изображения большего размера, чем указано ниже, для лучшей оптимизации.

const nextConfig = {
          //..
          images: {
            deviceSizes: [320, 420, 768, 1024, 1200],
            loader: "default",
            domains: ["res.cloudinary.com"],
          },
        }
        module.exports = nextConfig

Теперь мы создадим в ней папку components и файл ImageDetail.js. Вставьте внутрь следующий код:

import Image from "next/image";
            import Link from "next/link";
            export default function Gallery({ thumbnailUrl, title, id }) {
              return (
                <div>
                  <Link as={`/preview/${id}`} href="/preview/[id]">
                    <a>
                      <Image width={250} height={200} src={thumbnailUrl} />
                      <div className="photoid"> {title}</div>
                    </a>
                  </Link>
                </div>
              );
            }

После импорта Image и Link из next gallery-component имеет три реквизита ( thumbnailUrl, title, id) и возвращает link, который будет динамически перенаправлять на preview/$id каждой фотографии в нашем бэкенде. Я решил сделать ширину и высоту 250px и 200px соответственно.

Создайте еще одну папку с именем preview в папке pages и создайте файл с квадратными скобками, например [id].js внутри только что созданной папки.

Мы еще вернемся к этому файлу, а пока перейдите к вашему файлу index.js в папке pages и замените существующий код следующим:

import Head from "next/head";
        import { useState } from "react";
        import Gallery from "../components/ImageDetail";
        import styles from "../styles/Home.module.css";
        export default function Home({ stuff }) {
          const [photos, setPhotos] = useState(stuff);
          const [search, setSearch] = useState("");
          return (
            <div className={styles.container}>
              <Head>
                <title>Photo Gallery</title>
                <link rel="icon" href="/favicon.ico" />
              </Head>
              <main className={styles.main}>
                <div className={styles.fade}>
                  <div className={styles.gridContainer}>
                    {photos &&
                      photos.data.map((detail) => (
                        <Gallery
                          key={detail.id}
                          thumbnailUrl={detail.attributes.img.data.attributes.formats.thumbnail.url}
                          title={detail.attributes.name}
                          id={detail.id}
                        />
                      ))}
                  </div>
                </div>
              </main>
            </div>
          );
        }
        export async function getStaticProps() {
          const results = await fetch("http://localhost:1337/api/photos?populate=*");
          const stuff = await results.json();
          return {
            props: { stuff },
          };
        }

Мы импортировали и использовали Gallery из ImageDetail.js в нашей папке components. Мы сопоставили каждый экземпляр созданных нами состояний фотографий. Строка 32 здесь важна, потому что она использует Next.js, getStaticProps, который извлекает данные во время сборки из нашего экземпляра Strapi по адресу http://localhost:1337/api/photos. Ваше приложение должно выглядеть так:

Ответная реакция

Давайте сделаем все отзывчивым, выполнив следующие шаги.

  • Скопируйте и замените следующий код css здесь на Home.module.css в папке styles.
  • Скопируйте и замените следующий код css здесь на global.css в папке styles.

Теперь ваше приложение должно выглядеть так:

Добавление функции поиска

Мы запустили домашнюю страницу. Было бы неплохо иметь поле ввода для поиска, где пользователи могли бы найти конкретное изображение по его названию. Это будет наиболее полезно, когда фотографии будут заполнены. В файле index.js добавьте следующий код сразу после открытия тега <main>:

<input
                  onChange={(e) => setSearch(e.target.value)}
                  className={styles.searchInput}
                  type="text"
                  placeholder="Search for an image"
                ></input>
                <button
                  className="button"
                  disabled={search === ""}
                  onClick={async () => {
                    const results = await fetch(
                      `http://localhost:1337/api/photos?populate=*&filters\[name\][$eq]=${search}`
                    );
                    const details = await results.json();
                    setPhotos(await details);
                  }}
                >
                  Find
                </button>

Строка 1 to 6 отвечает за ввод. Он нацелен на значение в поле ввода. Обратите внимание на то, что извлекается на линии 12. Он использует методы фильтрации. Вы можете прочитать больше об этом здесь". Убедитесь, что вы установили состояние поиска. Ваш окончательный файл index.js должен выглядеть так:

import Head from "next/head";
        import { useState } from "react";
        import Gallery from "../components/ImageDetail";
        import styles from "../styles/Home.module.css";
        export default function Home({ stuff }) {
          const [photos, setPhotos] = useState(stuff);
          const [search, setSearch] = useState("");
          return (
            <div className={styles.container}>
              <Head>
                <title>Photo Gallery</title>
                <link rel="icon" href="/favicon.ico" />
              </Head>
              <main className={styles.main}>
                <input
                  onChange={(e) => setSearch(e.target.value)}
                  className={styles.searchInput}
                  type="text"
                  placeholder="Search for an image"
                ></input>
                <button
                  className="button"
                  disabled={search === ""}
                  onClick={async () => {
                    const results = await fetch(
                      `http://localhost:1337/api/photos?populate=*&filters\[name\][$eq]=${search}`
                    );
                    const details = await results.json();
                    setPhotos(await details);
                  }}
                >
                  Find
                </button>
                <div className={styles.fade}>
                  <div className={styles.gridContainer}>
                    {photos &&
                      photos.data.map((detail) => (
                        <Gallery
                          key={detail.id}
                          thumbnailUrl={detail.attributes.img.data.attributes.formats.thumbnail.url}
                          title={detail.attributes.name}
                          id={detail.id}
                        />
                      ))}
                  </div>
                </div>
              </main>
            </div>
          );
        }
        export async function getStaticProps() {
          const results = await fetch("http://localhost:1337/api/photos?populate=*");
          const stuff = await results.json();
          return {
            props: { stuff },
          };
        }

Ваше приложение должно выглядеть так с полем поиска и кнопкой Найти:

Когда вы выполняете Поиск и нажимаете Найти, это должно выглядеть так:

Теперь пришло время позаботиться о том, что происходит при нажатии на фотографию. Помните, что наш компонент Галерея в ImageDetail.js внутри папки component имеет ссылку. При нажатии на любую фотографию прямо сейчас появится эта страница с ошибкой:

Это потому, что ничего не было сделано внутри [id].js, который мы создали внутри папки preview. Давайте исправим это. Чтобы исправить ошибку, вставьте следующий код внутрь [id].js.

import { useRouter } from "next/router";
        import Image from "next/image";
        import Link from "next/link";
        export default function photo({ photo, location, name, date }) {
            const router = useRouter();
            if (!router.isFallback && !photo) {
                return <ErrorPage statusCode={404} />;
            }
            return (
                <div>
                    <div className="Imagecontainer">
                        <Link className="homeButton" href="/">
                            <a className="homeButton">
                                <button className="button"> Home </button>
                            </a>
                        </Link>
                    </div>
                    <div className="Imagecontainer">
                        {router.isFallback ? (
                            <div>Loading…</div>
                        ) : (
                            <>
                                <Image width={960} priority height={540} src={photo} />
                            </>
                        )}
                    </div>
                    <div className="Imagecontainer">Name : {name}</div>
                    <div className="Imagecontainer">Location {location}</div>
                    <div className="Imagecontainer">Date: {date}</div>
                    <div className="Imagecontainer">
                        <Link className="homeButton" href="/">
                            <a className="homeButton">
                                <button className="button"> Back </button>
                            </a>
                        </Link>
                    </div>
                </div>
            );
        }
        export async function getStaticProps({ params }) {
            const photoid = params.id;
            const results = await fetch(`http://localhost:1337/api/photos/${photoid}?populate=*`);
            const previews = await results.json();
            const photo = await previews.data.attributes.img.data.attributes.url;
            const name = await previews.data.attributes.name;
            const location = previews.data.attributes.location;
            const date = await previews.data.attributes.createdAt.toString();
            return {
                props: { photo, name, location, date },
            };
        }
        export async function getStaticPaths() {
            const results = await fetch("http://localhost:1337/api/photos?populate=*");
            const previews = await results.json();
            return {
                paths:
                    previews?.data.map((pic) => ({
                        params: { id: pic.id.toString() },
                    })) || [],
                fallback: true,
            };
        }

Я объясню, что делает большая часть этого кода.

  • getStaticPaths в строке 52 — это основной метод выборки данных Next.js, необходимый из-за динамических маршрутов нашего приложения. Подробнее об этом статическая генерация.
  • getStaticProps извлечет params.id, определенный в getStaticPaths. Поскольку это доступно, мы затем динамически извлекаем каждый идентификатор из JSON в строке 43 перед доступом к каждой из вещей, которые нам нужны.
  • Строка 27 to 29 отображает все остальные поля (местоположение, имя, дата) прямо под компонентом изображения, показывающим каждую деталь изображения в размере 960 x 540 пикселей. Обратите внимание, что мы уже определили их как свойства в строке 4, нашем фотокомпоненте.

Если вы все сделали правильно, у вас должно получиться что-то подобное у себя при клике на любое фото.

Заключение

Мы настроили и подключили нашу учетную запись Cloudinary к экземпляру Strapi. Кроме того, мы поиграли со Strapi, его разрешениями и ролями, тем самым создав нашу коллекцию в соответствии с тем, что мы задумали. Мы говорили о Next.js и некоторых его готовых методах, таких как getStaticProps и getStaticPaths. Наконец, мы смогли собрать все это вместе, чтобы создать наше приложение для фотогалереи.

Репозиторий для frontend-реализации и backend-реализации можно найти на Github.