В этой статье объясняется, как создать фотогалерею с помощью Strapi, Next.js и Cloudinary.

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

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


Предпосылки

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

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


Настройка Cloudinary

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

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

Обязательно сохраните эти данные секрет и не делитесь ими ни с кем.

Облачная информационная панель


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

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

    yarn create strapi-app strapi-photo --quickstart 
    #OR
    npm create strapi-app strapi-photo --quickstart 
Войти в полноэкранный режим

Выйти из полноэкранного режима

Эта команда создаст папку с именем strapi-photo и установите на него экземпляр Strapi.

Запуск терминала Strapi Instance

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

Страница регистрации администратора Strapi

Панель администратора Strapi

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

Strapi Content-Types-Builder

Тип Фото для вашего отображаемого имени и нажмите кнопку Продолжать кнопка добавления полей.

Конфигурация типа коллекции Strapi


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

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

  • Щелкните текстовое поле.
  • Тип имя в Имя поле.
  • Переключиться на Расширенные настройки вкладку и проверьте Необходимый поле.
  • Нажмите на Добавить еще одно поле.
  • Щелкните поле Дата.
  • Тип Свидание в поле Имя.
  • Выбирать свидание раскрывающийся список под типом
  • Переключиться на Расширенные настройки вкладку и проверьте Необходимый поле.
  • Нажмите на Добавить еще одно поле.
  • Щелкните текстовое поле.
  • Тип расположение в поле Имя.
  • Переключиться на Расширенные настройки вкладку и проверьте Необходимый поле.
  • Нажмите на Добавить еще одно поле.
  • Щелкните поле «Медиа».
  • Тип изображение в поле Имя и выберите Одно СМИ под флажком типа,
  • Переключиться на Расширенные настройки вкладку и проверьте Необходимый поле.
  • Выбирать Картинки только под Выберите разрешенные типы носителей.
  • Нажмите на Заканчивать.

Страница Strapi Content-Types-Builder

  • Нажмите на Сохранять. Нажатие кнопки «Сохранить» перезапустит ваш сервер. Ваш экземпляр Strapi должен выглядеть так:
    Структура проекта 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_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 по адресу и нажмите на Контент менеджер. Нажмите на Фототогда Создать новую запись.

Страница управления контентом Strapi

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

Конструктор контента Strapi Photos

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

Конструктор контента Strapi Photos

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


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

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

Панель администратора Strapi — настройки

  1. Прокрутите вниз под Разрешения.
  2. в Заявление вкладка, найти Фото.
  3. Установите флажки рядом с найти а также найти.
    Панель администратора Strapi — флажок «Разрешения»

  4. Нажмите Сохранять.

  5. Перейти к в вашем браузере или любом клиенте API, таком как Postman, и убедитесь, что у вас есть похожий ответ, например:
    ПОЛУЧИТЬ конечную точку (http://localhost:1337/api/photos)


Установка и настройка 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://локальный: 3000мы должны увидеть экземпляр Next.js по умолчанию:

Макет Nextjs

Так как мы будем работать с изображениями из внешнего источника, 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 внутри только что созданной папки.

Структура проекта Nextjs

Мы еще вернемся к этому файлу, а пока зайдите в свой 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("");
          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(
                      `&filters\[name\][$eq]=${search}`
                    );
                    const details = await results.json();
                    setPhotos(await details);
                  }}
                >
                  Find
                </button>
Войти в полноэкранный режим

Выйти из полноэкранного режима

Линия 1 to 6 заботится о вводе. Он нацелен на значение в поле ввода. Обратите внимание на то, что извлекается в Line 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(
                      `&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("");
          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("");
            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, нашем фотокомпоненте.

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

Получить фото по ID


Вывод

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

Репозиторий для реализация внешнего интерфейса а также бэкэнд реализация можно найти на Github.