Привет, друзья!


В этой небольшой заметке я хочу рассказать вам о том, как я разработал игру с вопросами по JavaScript за один вечер, потому что, во-первых, мне было скучно :D, во-вторых, мне стало интересно, как быстро я смогу "запилить" подобный MVP.


Вот что мы имеем на сегодняшний день.


Интересно? Тогда прошу под кат.


Приложение представляет собой классическое SPA и состоит из двух страниц:


  1. Экран приветствия или список вопросов.
  2. Таблица с рекордами.

В приложении реализован механизм аутентификации/авторизации по 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(
  <React.StrictMode>
    <ClerkProvider publishableKey={PUBLISHABLE_KEY} localization={ruRU}>
      <App />
    </ClerkProvider>
  </React.StrictMode>,
)

И рендерим в шапке сайта соответствующие компоненты:


import {
  SignedIn,
  SignedOut,
  SignInButton,
  UserButton,
} from '@clerk/clerk-react'
import { Button } from '@mui/material'

export default function Nav() {
  return (
    <>
      <SignedOut>
        <SignInButton>
          <Button variant='contained' color='success'>
            Войти
          </Button>
        </SignInButton>
      </SignedOut>
      <SignedIn>
        <UserButton />
      </SignedIn>
    </>
  )
}

Верите или нет, но это все, что нужно для реализации полноценного механизма аутентификации/авторизации (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`

<details>
<summary>Ответ</summary>
<div>
<h4>Правильный ответ: D</h4>

В функции `sayHi` мы сначала определяем переменную `name` с помощью ключевого слова `var`. Это означает, что `name` поднимается в начало функции. `name` будет иметь значение `undefined` до тех пор, пока выполнение кода не дойдет до строки, где ей присваивается значение `John`. Мы еще не определили значение `name`, когда пытаемся вывести ее значение в консоль, поэтому получаем `undefined`. Переменные, объявленные с помощью ключевых слов `let` и `const`, также поднимаются в начало области видимости, но в отличие от переменных, объявленных с помощью `var`, не инициализируются, т.е. такие переменные поднимаются без значения. Доступ к ним до инициализации невозможен. Это называется `временной мертвой зоной`. Когда мы пытаемся обратиться к переменным до их определения, `JavaScript` выбрасывает исключение `ReferenceError`.

</div>
</details>
...

Кстати, все вопросы, а также много другого интересного и полезного контента можно найти на моем сайте.


Мне нужно было преобразовать этот текст в такой массив объектов:


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-канале

Комментарии (6)


  1. ChizhM
    19.09.2024 08:37
    +4

    Напомнило статью, в которой для "hello world" использовали десяток фреймворков и ещё черте-что.


    1. ExternalWayfarer
      19.09.2024 08:37

      Звучит забавно. Не найдется ссылочки на нее?



  1. Calium
    19.09.2024 08:37
    +2

    Спасибо за статью! Полезно в плане авторизации и базы данных, как раз хотел при пилить бд. Возьму на заметку для своих будущих проектов.


    1. ChizhM
      19.09.2024 08:37

      Ну как сказать... из-за каждой ерунды подключать сторонний сервис - такое себе.

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

      На практике даже jquery лучше хранить локально, вместо использования cdn.


  1. razornd
    19.09.2024 08:37
    +4

    Давать изменять данные в БД с фронта не лучшая идея. У вас проблемы с безопасностью:

    Кто угодно может добавлять/изменять данные в таблице рекордов.