Содержание
TL;DR
В этом уроке, состоящем из двух частей, мы создадим полнофункциональное приложение для мгновенного генератора мемов, используя:
- React & NodeJS w/ TypeScript
- OpenAI’s Function Calling API
- ImgFlip.com’s Meme creator API
Вы можете посмотреть развернутую версию приложения, которое мы собираемся создать, здесь: [The Memerator](https://damemerator.netlify.app/../../assets/images/0a0bc9uxabyg8cue2axu.png
Intro
Call Me, Maybe
С помощью OpenAI’s chat completions API разработчики теперь могут делать действительно крутые вещи. По сути, это функциональность ChatGPT, но в виде вызываемого API, который можно интегрировать в любое приложение.
Но при работе с API многие разработчики хотели, чтобы GPT возвращала данные в формате, например JSON, который они могли бы использовать в функциях своего приложения.
К сожалению, если вы просили ChatGPT вернуть данные в определенном формате, он не всегда получал их правильно. Именно поэтому OpenAI выпустила функцию вызова.
По их словам, вызов функций позволяет разработчикам ”…описывать функции в GPT, а модель разумно выбирать для вывода JSON-объект, содержащий аргументы для вызова этих функций”.
Это отличный способ превратить естественный язык в вызов API.
Итак, что может быть лучше для изучения возможностей GPT по вызову функций, чем использовать их для вызова Imgflip.com’s meme creator API!
Let’s Build
В этом уроке, состоящем из двух частей, мы построим полнофункциональное приложение на React/NodeJS:
- Аутентификация
- Генерация мемов с помощью вызова функций OpenAI и API ImgFlip.com
- Ежедневное задание cron для получения новых шаблонов мемов
- Редактирование и удаление мемов
- и многое другое!
Я уже выложил рабочую версию этого приложения, которую вы можете опробовать здесь: https://damemerator.netlify.app - так что попробуйте и давайте… начнем.
В первой части этого руководства мы настроим приложение, сгенерируем и отобразим мемы.
Во второй части мы добавим больше функциональности, например, периодические задания cron для получения новых шаблонов мемов, а также возможность редактировать и удалять мемы.
BTW, два быстрых совета:
- если вам нужно в любой момент обратиться к готовому коду приложения, чтобы помочь вам с этим руководством, вы можете проверить GitHub Repo здесь.
- Если у вас возникнут вопросы, не стесняйтесь зайти на Wasp Discord Server и задать их нам!
Часть 1
Configuration
Мы собираемся сделать это веб-приложение на основе React/NodeJS, поэтому сначала нам нужно настроить его. Но не волнуйтесь, это не займет много времени, потому что в качестве фреймворка мы будем использовать Wasp.
Wasp сделает за нас всю тяжелую работу. Вы увидите, что я имею в виду, через секунду.
Настройте свой проект Wasp
Сначала установите Wasp, выполнив в терминале следующее:
curl -sSL <https://get.wasp-lang.dev/installer.sh> | sh
Вход в полноэкранный режим
Далее давайте клонируем ветку start приложения Memerator, которую я подготовил для вас:
git clone -b start https://github.com/vincanger/memerator.git
Вход в полноэкранный режим
Затем перейдите в каталог Memerator и откройте проект в VS Code:
cd Memerator && code .
Вход в полноэкранный режим
Вы заметите, что Wasp устанавливает ваше полноэкранное приложение с такой структурой файлов:
.
├──── main.wasp # Файл конфигурации wasp.
└──── src
├──── client # Сюда помещается код вашего React-клиента (JS/CSS/HTML).
├──── server # Сюда помещается код вашего сервера (Node JS).
└──── shared # Ваш общий (независимый от времени выполнения) код отправляется сюда.
Вход в полноэкранный режим
Давайте сначала посмотрим на файл main.wasp. Вы можете считать его ”скелетом”, или инструкцией, вашего приложения. Этот файл настраивает большую часть вашего полноэкранного приложения за вас 🤯:
app Memerator {
оса: {
версия: "^0.11.3"
},
title: "Memerator",
клиент: {
rootComponent: import { Layout } из "@client/Layout",
},
db: {
система: PostgreSQL,
prisma: {
clientPreviewFeatures: ["extendedWhereUnique"]
}
},
аутентификация: {
userEntity: Пользователь,
методы: {
usernameAndPassword: {}
},
onAuthFailedRedirectTo: "/login",
onAuthSucceededRedirectTo: "/"
},
зависимости: [
("openai", "4.2.0"),
("axios", "^1.4.0"),
("react-icons", "4.10.1"),
]
}
entity User {=psl
id Int @id @default(autoincrement())
имя пользователя String @unique
пароль String
memes Meme[]
isAdmin Boolean @default(false)
credits Int @default(2)
psl=}
entity Meme {=psl
id String @id @default(uuid())
url String
текст0 Строка
текст1 Строка
темы String
аудитория Строка
шаблон Шаблон @relation(поля: [templateId], ссылки: [id])
templateId String
пользователь Пользователь @relation(fields: [userId], references: [id])
userId Int
createdAt DateTime @default(now())
psl=}
сущность Шаблон {=psl
id String @id @unique
имя Строка
url String
ширина Int
высота Int
boxCount Int
мемы Meme[]
psl=}
маршрут HomePageRoute { path: "/", to: HomePage }
страница HomePage {
компонент: import { HomePage } из "@client/pages/Home",
}
route LoginRoute { path: "/login", to: LoginPage }
страница LoginPage {
компонент: import Login from "@client/pages/auth/Login"
}
route SignupRoute { path: "/signup", to: SignupPage }
страница SignupPage {
компонент: import Signup from "@client/pages/auth/Signup"
}
Вход в полноэкранный режим
Как видите, в нашем файле конфигурации main.wasp появился наш:
- зависимости,
- метод аутентификации,
- тип базы данных и
- модели базы данных (”сущности”)
- страницы и маршруты на стороне клиента
Вы также могли заметить этот синтаксис {=psl psl=} в сущностях выше. Это означает, что все, что находится между этими скобками psl, на самом деле является другим языком, в данном случае Prisma Schema Language. Wasp использует Prisma под капотом, поэтому, если вы уже использовали Prisma, все должно быть просто.
Также не забудьте установить расширение Wasp VS code extension, чтобы получить хорошую подсветку синтаксиса и наилучшее общее впечатление от разработки.
Настройка базы данных
Нам еще нужно настроить базу данных Postgres.
Обычно это может быть довольно хлопотно, но с Wasp это очень просто.
- Просто установите и запустите Docker Deskop,
- откройте отдельную вкладку/окно терминала,.
cdв директориюMemerator, а затем запустить
wasp start db
Вход в полноэкранный режим
Это запустит и подключит ваше приложение к базе данных Postgres. Не нужно ничего делать elсе! 🤯
Просто оставьте эту вкладку терминала вместе с рабочим столом docker открытой и работающей в фоновом режиме.
Теперь в другой вкладке терминала запустите
wasp db migrate-dev
Вход в полноэкранный режим
и не забудьте дать миграции базы данных имя, например init.
Переменные окружения
В корне вашего проекта вы найдете файл .env.server.example, который выглядит следующим образом:
# установите свои собственные учетные данные на https://imgflip.com/signup и переименуйте этот файл в .env.server
# ПРИМЕЧАНИЕ: убедитесь, что вы зарегистрировались с именем пользователя и паролем (не google)
IMGFLIP_USERNAME=
IMGFLIP_PASSWORD=
# получите свой api ключ с https://platform.openai.com/
OPENAI_API_KEY=
JWT_SECRET=засекреченная фраза длиной не менее тридцати двух символов
Вход в полноэкранный режим
Переименуйте этот файл в .env.server и следуйте инструкциям в нем, чтобы получить свой:
так как они нам понадобятся для генерации наших мемов 🤡
Запустите свое приложение
Теперь, когда все настроено правильно, вы должны быть в состоянии запустить
запустить wasp
Вход в полноэкранный режим
При запуске wasp start Wasp установит все необходимые пакеты npm, запустит наш NodeJS-сервер на порт 3001 и наш React-клиент на порт 3000.
Перейдите по адресу localhost:3000 в браузере, чтобы проверить это. Основа нашего приложения должна выглядеть следующим образом:
В коде шаблона уже настроена клиентская форма для генерации мемов:
- темы
- целевая аудитория
Эту информацию мы отправим на бэкенд для вызова OpenAI API с помощью вызовов функций. Затем мы отправим эту информацию в imglfip.com API для генерации мема.
Но конечной точке /caption_image API imgflip нужен идентификатор шаблона мема. И чтобы получить этот идентификатор, нам сначала нужно получить доступные шаблоны мемов из конечной точки /get_memes imgflip.
Так что давайте настроим это сейчас.
Код на стороне сервера
Создайте новый файл в src/server/ с именем utils.ts:
import axios from 'axios';
import { stringify } from 'querystring';
import HttpError from '@wasp/core/HttpError.js';
type GenerateMemeArgs = {
text0: string;
text1: string;
templateId: string;
};
export const fetchMemeTemplates = async () => {
try {
const response = await axios.get('https://api.imgflip.com/get_memes');
return response.data.data.memes;
} catch (error) {
console.error(error);
throw new HttpError(500, 'Error fetching meme templates');
}
};
export const generateMemeImage = async (args: GenerateMemeArgs) => {
console.log('args: ', args);
try {
const data = stringify({
template_id: args.templateId,
имя пользователя: process.env.IMGFLIP_USERNAME,
пароль: process.env.IMGFLIP_PASSWORD,
text0: args.text0,
text1: args.text1,
});
// Реализуем генерацию мемов с помощью Imgflip API
const res = await axios.post('https://api.imgflip.com/caption_image', data, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
const url = res.data.data.url;
console.log('generated meme url: ', url);
возвращает url как строку;
} catch (error) {
console.error(error);
throw new HttpError(500, 'Error generating meme image');
}
};
Вход в полноэкранный режим
Таким образом, мы получаем несколько вспомогательных функций, которые помогут нам получить все шаблоны мемов, с помощью которых мы можем генерировать изображения мемов.
Обратите внимание, что POST-запрос к конечной точке /caption_image принимает следующие данные:
- имя пользователя и пароль нашего imgflip.
- ID шаблона мема, который мы будем использовать
- текст для верхней части мема, т.е. текст0
- текст для нижней части мема, т.е. text1
Аргументы text0 и text1 сгенерирует для нас наш милый друг, ChatGPT. Но для того, чтобы GPT мог это сделать, нам нужно настроить вызов его API.
Для этого создайте новый файл в src/server/ под названием actions.ts.
Затем вернитесь к вашему файлу конфигурации main.wasp и добавьте следующее действие Wasp Action в нижней части файла:
//...
действие createMeme {
fn: import { createMeme } из "@server/actions.js",
entities: [Meme, Template, User]
}
Вход в полноэкранный режим
Действие Action - это тип операции Wasp Operation, которая изменяет некоторое состояние на бэкенде. По сути, это функция NodeJS, которая вызывается на сервере, но Wasp заботится о том, чтобы настроить все это за вас.
Это означает, что вам не нужно беспокоиться о создании HTTP API для Action, управлении обработкой запросов на стороне сервера или даже об обработке и кэшировании ответов на стороне клиента. Вместо этого вы просто пишете бизнес-логику!
Если у вас установлено расширение Wasp VS Code, вы увидите ошибку (вверху). Наведите на нее курсор и нажмите Quick Fix > create function createMeme.
Это создаст для вас функцию createMeme (ниже) в вашем файле actions.ts, если такой файл существует. Очень круто!
import { CreateMeme } from '@wasp/actions/types'
тип CreateMemeInput = void
тип CreateMemeOutput = void
export const createMeme: CreateMeme<CreateMemeInput, CreateMemeOutput> = async (args, context) => {
// Реализация здесь
}
Вход в полноэкранный режим
Вы можете видеть, что он также импортирует типы действий.
Поскольку мы будем отправлять массив topics и предполагаемую строку audience для мема из нашей внешней формы, а в конце вернем только что созданную сущность Meme, именно так мы и должны определить наши типы.
Помните, что сущность Meme - это модель базы данных, которую мы определили в нашем файле конфигурации main.wasp.
Зная это, мы можем изменить содержимое файла actions.ts следующим образом:
import type { CreateMeme } from '@wasp/actions/types'
import type { Meme } from '@wasp/entities';
type CreateMemeArgs = { topics: string[]; audience: string };
export const createMeme: CreateMeme<CreateMemeArgs, Meme> = async ({ topics, audience }, context) => {
// Реализация здесь
}
Вход в полноэкранный режим
Прежде чем реализовать остальную логику, давайте разберемся, как должна работать наша функция createMeme и как будет генерироваться наш Meme:
- получить шаблон мема с imgflip, который мы хотим использовать
- отправляем его название, темы и целевую аудиторию в OpenAI’s chat completions API
- сообщить OpenAI, что мы хотим получить результат в виде аргументов, которые мы можем передать нашей следующей функции в формате JSON, т. е. вызов функции OpenAI.
- передаем эти аргументы в конечную точку imgflip /caption-image и получаем url нашего созданного мема
- сохраняем url мема и другую информацию в нашей БД как сущность
Meme.
Учитывая все это, перейдите к полной замене содержимого нашего файла actions.ts на готовый экшен createMeme:
import HttpError from '@wasp/core/HttpError.js';
import OpenAI from 'openai';
import { fetchMemeTemplates, generateMemeImage } из './utils.js';
import type { CreateMeme } from '@wasp/actions/types';
import type { Meme, Template } from '@wasp/entities';
type CreateMemeArgs = { topics: string[]; audience: string };
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
export const createMeme: CreateMeme<CreateMemeArgs, Meme> = async ({ topics, audience }, context) => {
if (!context.user) {
throw new HttpError(401, 'Вы должны быть авторизованы');
}
if (context.user.credits === 0 && !context.user.isAdmin) {
throw new HttpError(403, 'У вас не осталось кредитов');
}
const topicsStr = topics.join(', ');
let templates: Template[] = await context.entities.Template.findMany({});
if (templates.length === 0) {
const memeTemplates = await fetchMemeTemplates();
templates = await Promise.all(
memeTemplates.map(async (template: any) => {
const addedTemplate = await context.entities.Template.upsert({
where: { id: template.id },
create: {
id: template.id,
имя: template.name,
url: template.url,
width: template.width,
высота: template.height,
boxCount: template.box_count
},
update: {}
});
return addedTemplate;
})
);
}
// отфильтруйте шаблоны с box_count > 2
templates = templates.filter((template) => template.boxCount <= 2);
const randomTemplate = templates[Math.floor(Math.random() * templates.length)];
console.log('случайный шаблон: ', randomTemplate);
const sysPrompt = `Вы являетесь генератором идей для мемов. Вы будете использовать imgflip api для генерации мемов на основе предложенной вами идеи. Получив случайное название шаблона и темы, сгенерируйте мем для целевой аудитории. Используйте только предоставленный шаблон`;
const userPrompt = `Топики: ${topicsStr} \n Предполагаемая аудитория: ${аудитория} \n Шаблон: ${randomTemplate.name} \n`;
let openAIResponse: OpenAI.Chat.Completions.ChatCompletion;
try {
openAIResponse = await openai.chat.completions.create({
сообщения: [
{ роль: 'system', содержимое: sysPrompt }
{ роль: 'user', содержимое: userPrompt }
],
функции: [
{
name: 'generateMemeImage',
description: 'Генерировать мем через API imgflip на основе заданной идеи',
параметры: {
тип: 'object',
свойства: {
text0: { type: 'string', description: 'Текст для верхней надписи мема' },
text1: { type: 'string', description: 'Текст для нижней надписи мема' }
},
required: ['templateName', 'text0', 'text1'],
},
},
],
function_call: {
name: 'generateMemeImage',
},
model: 'gpt-4-0613',
});
} catch (error: any) {
console.error('Ошибка вызова openAI: ', error);
throw new HttpError(500, 'Ошибка вызова openAI');
}
console.log(openAIResponse.choices[0]);
/**
* Вызов функции, возвращаемый openAI, выглядит следующим образом:
*/
// {
// index: 0,
// сообщение: {
// роль: 'помощник',
// содержание: null,
// function_call: {
// name: 'generateMeme',
// arguments: '{\n' +
// `"text0": "CSS, который вы писали весь день",\n` + // ``текст0''.
// ' "text1": "Это выглядит ужасно"\n' + // '
// '}'
// }
// },
// finish_reason: 'stop'
// }
if (!openAIResponse.choices[0].message.function_call) throw new HttpError(500, 'No function call in openAI response');
const gptArgs = JSON.parse(openAIResponse.choices[0].message.function_call.arguments);
console.log('gptArgs: ', gptArgs);
const memeIdeaText0 = gptArgs.text0;
const memeIdeaText1 = gptArgs.text1;
console.log('meme Idea args: ', memeIdeaText0, memeIdeaText1);
const memeUrl = await generateMemeImage({
templateId: randomTemplate.id,
text0: memeIdeaText0,
text1: memeIdeaText1,
});
const newMeme = await context.entities.Meme.create({
data: {
text0: memeIdeaText0,
text1: memeIdeaText1,
темы: topicsStr,
аудитория: аудитория,
url: memeUrl,
шаблон: { connect: { id: randomTemplate.id } },
пользователь: { connect: { id: context.user.id } },
},
});
return newMeme;
};
Вход в полноэкранный режим
На данный момент приведенный выше код не требует пояснений, но я хочу обратить внимание на пару моментов:
- Объект
contextпередается во всех действиях и запросах Wasp. Он содержит клиент Prisma с доступом к сущностям БД, которые вы определили в конфигурационном файлеmain.wasp. - Сначала мы ищем шаблоны мемов imgflip в нашей БД. Если таковые не найдены, мы извлекаем их с помощью нашей функции
fetchTemplates, которую мы определили ранее. Затем мывставляемих в нашу БД. - Есть некоторые шаблоны мемов, которые занимают более 2 текстовых полей, но в этом уроке мы будем использовать только шаблоны мемов с 2 текстовыми полями, чтобы было проще.
- Мы выбираем случайный шаблон из этого списка, чтобы использовать его в качестве основы для нашего мема (на самом деле это отличный способ случайно сгенерировать интересный контент для мемов).
- Мы сообщаем OpenAI API информацию о функциях, для которых он может создавать аргументы, с помощью свойств
functionsиfunction_call, которые указывают ему всегда возвращать JSON-аргументы для нашей функцииgenerateMemeImage.
Отлично! Но как только мы начнем генерировать мемы, нам понадобится способ отобразить их на нашем фронт-энде.
Поэтому давайте создадим запрос Wasp Query. Запрос работает так же, как и действие, за исключением того, что он предназначен только для чтения данных.
Перейдите в src/server и создайте новый файл под названием queries.ts`.
Затем в файле main.wasp добавьте следующий код:
//...
запрос getAllMemes {
fn: import { getAllMemes } from "@server/queries.js",
entities: [Meme]
}
Вход в полноэкранный режим
Затем в файле queries.ts добавьте функцию getAllMemes:
import HttpError from '@wasp/core/HttpError.js';
import type { Meme } from '@wasp/entities';
import type { GetAllMemes } from '@wasp/queries/types';
export const getAllMemes: GetAllMemes<void, Meme[]> = async (_args, context) => {
const memeIdeas = await context.entities.Meme.findMany({
orderBy: { createdAt: 'desc' },
include: { template: true },
});
return memeIdeas;
};
Вход в полноэкранный режим
Код на стороне клиента
Теперь, когда у нас есть код createMeme и getAllMemes, реализованный на стороне сервера, давайте подключим его к нашему клиенту.
Wasp позволяет очень легко импортировать операции, которые мы только что создали, и вызывать их на нашем фронт-энде.
Для этого нужно перейти в src/client/pages/Home.tsx и добавить следующий код в верхнюю часть файла:
//...другие импорты...
import { useQuery } from '@wasp/queries';
import createMeme from '@wasp/actions/createMeme';
import getAllMemes from '@wasp/queries/getAllMemes';
import useAuth from '@wasp/auth/useAuth';
export function HomePage() {
const [topics, setTopics] = useState(['']);
const [audience, setAudience] = useState('');
const [isMemeGenerating, setIsMemeGenerating] = useState(false);
// 😎 😎 😎
const { data: user } = useAuth();
const { данные: memes, isLoading, error } = useQuery(getAllMemes);
const handleGenerateMeme: FormEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault();
if (!user) {
history.push('/login');
return;
}
if (topics.join('').trim().length === 0 || audience.length === 0) {
alert('Пожалуйста, укажите тему и аудиторию');
return;
}
try {
setIsMemeGenerating(true);
await createMeme({ topics, audience }); // <--- 😎 😎 😎 😎
} catch (error: any) {
alert('Ошибка при генерации мема: ' + error.message);
} finally {
setIsMemeGenerating(false);
}
};
//...
Вход в полноэкранный режим
Как видите, мы импортировали createMeme и getAllMemes (😎).
Для getAllMemes мы обернули его в хук useQuery, чтобы мы могли получать и кэшировать данные. С другой стороны, наш экшен createMeme вызывается в handleGenerateMeme, который мы будем вызывать при отправке нашей формы.
Вместо того чтобы добавлять код в файл Home.tsx по частям, вот файл со всем кодом для генерации и отображения мемов. Замените весь файл Home.tsx этим кодом, а ниже я расскажу о нем подробнее:
import { useState, FormEventHandler } from 'react';
import { useQuery } from '@wasp/queries';
import createMeme from '@wasp/actions/createMeme';
import getAllMemes from '@wasp/queries/getAllMemes';
import useAuth from '@wasp/auth/useAuth';
import { useHistory } from 'react-router-dom';
импорт {
AiOutlinePlusCircle,
AiOutlineMinusCircle,
AiOutlineRobot,
} из 'react-icons/ai';
export function HomePage() {
const [topics, setTopics] = useState(['']);
const [audience, setAudience] = useState('');
const [isMemeGenerating, setIsMemeGenerating] = useState(false);
const history = useHistory();
const { data: user } = useAuth();
const { data: memes, isLoading, error } = useQuery(getAllMemes);
const handleGenerateMeme: FormEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault();
if (!user) {
history.push('/login');
return;
}
if (topics.join('').trim().length === 0 || audience.length === 0) {
alert('Пожалуйста, укажите тему и аудиторию');
return;
}
try {
setIsMemeGenerating(true);
await createMeme({ topics, audience });
} catch (error: any) {
alert('Ошибка при генерации мема: ' + error.message);
} finally {
setIsMemeGenerating(false);
}
};
const handleDeleteMeme = async (id: string) => {
//...
};
if (isLoading) return 'Loading...';
if (error) return 'Error: ' + error;
return (
<div className='p-4'>
<h1 className='text-3xl font-bold mb-4'>Добро пожаловать на Memerator!</h1>
<p className='mb-4'>Начните генерировать идеи мемов, указав темы и целевую аудиторию.
<form onSubmit={handleGenerateMeme}>.
<div className='mb-4 max-w-[500px]'>
<label htmlFor='topics' className='block font-bold mb-2'>
Темы:
</label>
{topics.map((topic, index) => (
<input
key={index}
type='text'
id='topics'
value={topic}
onChange={(e) => {
const updatedTopics = [...topics];
updatedTopics[index] = e.target.value;
setTopics(updatedTopics);
}}
className='p-1 mr-1 mb-1 border rounded text-lg focus:outline-none focus:ring-2focus:ring-primary-600 focus:border-transparent'
/>
))}
<div className='flex items-center my-2 gap-1'>
<кнопка
type='button'
onClick={() => topics.length < 3 && setTopics([...topics, ''])}
className='flex items-center gap-1 bg-primary-200 hover:bg-primary-300 border-2 text-black text-xs py-1 px-2 rounded'
>
<AiOutlinePlusCircle /> Добавить тему
</button>
{topics.length > 1 && (
<кнопка
onClick={() => setTopics(topics.slice(0, -1))}
className='flex items-center gap-1 bg-red-500 hover:bg-red-700 border-2 text-white text-xs py-1 px-2 rounded'
>
<AiOutlineMinusCircle /> Удалить тему
</button>
)}
</div>
</div>
<div className='mb-4'>
<label htmlFor='audience' className='block font-bold mb-2'>
Предполагаемая аудитория:
</label>
<input
type='text'
id='audience'
value={audience}
onChange={(e) => setAudience(e.target.value)}
className='p-1 border rounded text-lg focus:outline-none focus:ring-2 focus:ring-primary-600 focus:border-transparent'
/>
</div>
<кнопка
type='submit'
className={`flex items-center gap-1 bg-primary-200 hover:bg-primary-300 border-2 text-black text-sm font-bold py-1 px-2 rounded ${{
isMemeGenerating ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
} $}`}
>
<AiOutlineRobot />
{!isMemeGenerating ? 'Generate Meme' : 'Generating...'}
</button>
</form>
{!!memes && memes.length > 0 ? (
memes.map((memeIdea) => (
<div key={memeIdea.id} className='mt-4 p-4 bg-gray-100 rounded-lg'>
<img src={memeIdea.url} width='500px' />
<div className='flex flex-col items-start mt-2'>
<div>
<span className='text-sm text-gray-700'>Темы: </span>
<span className='text-sm italic text-gray-500'>{memeIdea.topics}</span>.
</div>
<div>
<span className='text-sm text-gray-700'>Аудитория: </span>
<span className='text-sm italic text-gray-500'>{memeIdea.audience}</span>.
</div>
</div>
{/* TODO: реализовать функции редактирования и удаления мемов */}
</div>
))
) : (
<div className='flex justify-center mt-5'> :( мемов не найдено</div>
)}
</div>
);
}
Вход в полноэкранный режим
В этом коде есть две вещи, на которые я хотел бы обратить внимание:
- Хук
useQueryвызывает наш запросgetAllMemes, когда компонент монтируется. Он также кэширует результат для нас, а также автоматически перезагружает страницу каждый раз, когда мы добавляем новый мем в нашу БД с помощьюcreateMeme. Это означает, что наша страница будет автоматически перезагружаться при появлении нового мема. - Хук
useAuthпозволяет нам получать информацию о нашем залогиненном пользователе. Если пользователь не вошел в систему, мы заставим его сделать это, прежде чем он сможет сгенерировать мем.
Это действительно классные функции Wasp, которые значительно облегчают вашу жизнь как разработчика 🙂 .
Итак, давайте попробуем сгенерировать мем. Вот тот, который я только что сгенерировал:
Хаха. Довольно неплохо!
Но было бы здорово, если бы мы могли редактировать и удалять наши мемы? А если бы мы могли расширить набор шаблонов мемов для нашего генератора? Разве это не было бы тоже круто?
Да, было бы. Так что давайте сделаем это.
To Be Continued…
Во второй части этого руководства мы займемся остальной частью приложения, например, добавим повторяющиеся задания cron для получения новых шаблонов мемов, а также возможность редактировать, удалять и делиться мемами.
Если вы нашли это полезным, пожалуйста, покажите нам свою поддержку, дав нам звезду на GitHub! Это поможет нам продолжать делать больше подобных вещей.