Дело было вечером или Создаем веб-приложение за 5 часов
Привет, друзья!
В этой небольшой заметке я хочу рассказать вам о том, как я разработал игру с вопросами по JavaScript за один вечер, потому что, во-первых, мне было скучно : D, во-вторых, мне стало интересно, как быстро я смогу «запилить» подобный MVP.
Вот что мы имеем на сегодняшний день.
Интересно? Тогда прошу под кат.
Приложение представляет собой классическое SPA и состоит из двух страниц:
- Экран приветствия или список вопросов.
- Таблица с рекордами.
В приложении реализован механизм аутентификации/авторизации по email или аккаунтам Google/GitHub. Авторизованный пользователь может записать свой результат в базу данных, когда его результат лучше худшего рекорда.
Есть БД PostgreSQL для хранения рекордов (лучших результатов) в количестве 100 штук.
Далее я кратко опишу алгоритм создания приложения. Вот репозиторий с кодом проекта.
❯ Создание и настройка проекта
Создаем шаблон React + TypeScript приложения с помощью Vite:
npm create vite@latest javascript-questions -- --template react-ts
Устанавливаем дополнительные зависимости:
npm i @mui/material @mui/icons-material @mui/x-date-pickers @emotion/react @emotion/styled @fontsource/roboto material-react-table react-router-dom react-syntax-highlighter react-toastify react-use
npm i -D @types/react-syntax-highlighter
@mui...
,@emotion...
и@fontsource/roboto
нужны для MUI — библиотеки компонентов UI- material-react-table — библиотека для работы с таблицами TanStack Table на основе компонентов MUI
- react-router-dom — библиотека клиентской маршрутизации
- react-syntax-highlighter — компонент для подсветки синтаксиса
- react-toastify — компонент для уведомлений
- react-use — кастомные хуки
❯ Аутентификация/авторизация
Идем на платформу управления пользователями Clerk и создаем там проект. Находим Publishable key
в разделе API Keys
и создаем в корне проекта файл .env
следующего содержания:
VITE_CLERK_PUBLISHABLE_KEY=pk_test_...
Устанавливаем два пакета:
npm i @clerk/clerk-react @clerk/localizations
Оборачиваем корневой компонент приложения в провайдер:
import { ClerkProvider } from '@clerk/clerk-react'
// Локализация неполная, к сожалению
import { ruRU } from '@clerk/localizations'
const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
if (!PUBLISHABLE_KEY) {
throw new Error('Отсутствует ключ Clerk')
}
ReactDOM.createRoot(document.getElementById('root')!).render(
,
)
И рендерим в шапке сайта соответствующие компоненты:
import {
SignedIn,
SignedOut,
SignInButton,
UserButton,
} from '@clerk/clerk-react'
import { Button } from '@mui/material'
export default function Nav() {
return (
<>
>
)
}
Верите или нет, но это все, что нужно для реализации полноценного механизма аутентификации/авторизации (magic! : D).
❯ База данных
Идем на платформу BaaS Supabase и создаем там проект. Идем в раздел Project Settings
, затем в раздел API
, находим там Project URL
и anon public key
в Project API keys
и добавляем их в .env
:
VITE_SUPABASE_URL=https://....supabase.co
VITE_SUPABASE_ANON_KEY=eyJ...
Идем в раздел Table Editor
и создаем такую таблицу results
:
create table
public.results (
id uuid not null default gen_random_uuid (),
created_at timestamp with time zone not null default now(),
user_id text not null,
user_name text not null,
question_count bigint not null,
correct_answer_percent bigint not null,
correct_answer_count bigint not null,
constraint results_pkey primary key (id)
) tablespace pg_default;
Я создавал эту таблицу с помощью графического интерфейса.
Обратите внимание: для таблицы должна быть отключена безопасность на уровне строк (значок RLS disabled
).
Устанавливаем пакет:
npm i @supabase/supabase-js
Инициализируем и экспортируем клиента:
import { createClient } from '@supabase/supabase-js'
const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL
const SUPABASE_ANON_KEY = import.meta.env.VITE_SUPABASE_ANON_KEY
if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
throw new Error('Отсутствует URL или ключ Supabase')
}
export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
Верите или нет, но это все, что нужно для создания и настройки Postres (magic! : D).
Обратите внимание: Supabase предоставляет собственный механизм аутентификации/авторизации, но Clerk мне больше нравится.
В качестве альтернативы можно рассмотреть такие варианты БД:
- Vercel Postgres (пробовал, понравилось, но без Prisma работать с базой не очень удобно, а для
prisma
нужен сервер) - Convex (не пробовал, но знаю, что в тренде, планирую потестить в ближайшее время)
❯ Вопросы
Честное слово, я не хотел прибегать к помощи ИИ, но пришлось : D У меня был файл с вопросами в количестве 231 штуки в формате Markdown следующего содержания:
## ❯ Вопрос № 1
\`\`\`javascript
function sayHi() {
console.log(name)
console.log(age)
var name = "John"
let age = 30
}
sayHi()
\`\`\`
- A: `John` и `undefined`
- B: `John` и `Error`
- C: `Error`
- D: `undefined` и `Error`
Ответ
Правильный ответ: D
В функции `sayHi` мы сначала определяем переменную `name` с помощью ключевого слова `var`. Это означает, что `name` поднимается в начало функции. `name` будет иметь значение `undefined` до тех пор, пока выполнение кода не дойдет до строки, где ей присваивается значение `John`. Мы еще не определили значение `name`, когда пытаемся вывести ее значение в консоль, поэтому получаем `undefined`. Переменные, объявленные с помощью ключевых слов `let` и `const`, также поднимаются в начало области видимости, но в отличие от переменных, объявленных с помощью `var`, не инициализируются, т.е. такие переменные поднимаются без значения. Доступ к ним до инициализации невозможен. Это называется `временной мертвой зоной`. Когда мы пытаемся обратиться к переменным до их определения, `JavaScript` выбрасывает исключение `ReferenceError`.
...
Кстати, все вопросы, а также много другого интересного и полезного контента можно найти на моем сайте.
Мне нужно было преобразовать этот текст в такой массив объектов:
export default [
{
question:
'function sayHi() {\n console.log(name)\n console.log(age)\n var name = "John"\n let age = 30\n}\n\nsayHi()',
answers: ['John и undefined', 'John и Error', 'Error', 'undefined и Error'],
correctAnswerIndex: 3,
explanation:
'В функции `sayHi` мы сначала определяем переменную `name` с помощью ключевого слова `var`. Это означает, что `name` поднимается в начало функции. `name` будет иметь значение `undefined` до тех пор, пока выполнение кода не дойдет до строки, где ей присваивается значение `John`. Мы еще не определили значение `name`, когда пытаемся вывести ее значение в консоль, поэтому получаем `undefined`. Переменные, объявленные с помощью ключевых слов `let` и `const`, также поднимаются в начало области видимости, но в отличие от переменных, объявленных с помощью `var`, не инициализируются, т.е. такие переменные поднимаются без значения. Доступ к ним до инициализации невозможен. Это называется `временной мертвой зоной`. Когда мы пытаемся обратиться к переменным до их определения, `JavaScript` выбрасывает исключение `ReferenceError`.',
},
...
]
Как вы понимаете, делать это вручную, мягко говоря, немного утомительно. И тут я вспомнил про то, что ChatGPT умеет анализировать документы. На моей машине установлено это замечательное приложение:
Доступ к ChatGPT из России я получил так: купил сервер в Нидерландах и развернул там VPN по инструкции из этой замечательной статьи. Затем нашел эту замечательную статью, откуда перешел на этот замечательный сайт и купил там нидерландский номер телефона (рублей за 50, если мне память не изменяет), на который пришел код подтверждения от OpenAI (ваша локация должна совпадать с «родиной» номера телефона, если я правильно понял схему валидации OpenAI).
Итак, я скормил ChatGPT файл с вопросами и составил примерно такой запрос: «Многоуважаемый ИИ, не соблаговолите ли вы проанализировать этот документ и преобразовать вопросы в такие объекты: … Буду очень признателен, если результат вы оформите в виде файла JavaScript» : D
Подумав минуту, ChatGPT сгенерировал почти идеальный JS-файл, содержащий все вопросы в виде массива объектов (некоторые вопросы слиплись, на редактирование файла ушло около часа).
❯ Деплой
Для деплоя своих приложений я использую либо Netlify (для SPA), либо Vercel (для приложений, разработанных с помощью Next.js). Для деплоя на Netlify я использую Netlify CLI:
# Устанавливаем пакет глобально
npm i -g netlify-cli
# Авторизуемся (разумеется, у вас должен быть аккаунт)
netlify login
# Подключаем проект (репозиторий должен находится в GitHub)
netlify init
Верите или нет, но это все, что нужно для деплоя приложения и повторной сборки приложения при отправке изменений в репозиторий с помощью git push
(continuos deployment во всей красе: D)
Пожалуй, это все, чем я хотел поделиться с вами в этой заметке.
Из ближайших планов:
- расширить функционал (есть парочка идей)
- сделать PWA (есть плагин, который пока не хочет работать)
- сделать мобильное приложение (скорее всего, будет только Android) с помощью React Native и Expo
- возможно, сделать десктопное приложение с помощью Electron или Tauri
Буду рад любым замечаниям и предложениям. Happy coding!
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩