Дело было вечером или Создаем веб-приложение за 5 часов

tzuvjv89aoumvzomjhe0grtz0ze.png

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

В этой небольшой заметке я хочу рассказать вам о том, как я разработал игру с вопросами по 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(
  
    
      
    
  ,
)

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

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;

Я создавал эту таблицу с помощью графического интерфейса.


kf8dmrurgggyxrn5esmiiv__1kg.png

Обратите внимание: для таблицы должна быть отключена безопасность на уровне строк (значок 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 умеет анализировать документы. На моей машине установлено это замечательное приложение:


ilr12gvvxktpeoxts6eksqrjwus.png

Доступ к 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-канале

u9vgio3hxj12h5u7j3un0wx_zpk.png

© Habrahabr.ru