It Was in the Evening or Creating a Web Application in 5 Hours

Hello, friends!

In this short note I want to tell you how I developed a game with JavaScript questions in one evening, because, firstly, I was bored :D, secondly, I was interested in how quickly I could “saw off” something like this MVP.

Here what we have today.

Interesting? Then please read on.

The application is a classic SPA and consists of two pages:

  1. Welcome screen or list of questions.
  2. Table of records.

The application implements an authentication/authorization mechanism via email or Google/GitHub accounts. An authorized user can record their result in the database when their result is better than the worst record.

There is a PostgreSQL database for storing records (best results) in the amount of 100 pieces.

Next I will briefly describe the algorithm for creating an application. Here repository with project code.

❯ Creating and configuring a project

Create a React + TypeScript application template using Vite:

npm create vite@latest javascript-questions -- --template react-ts

Install additional dependencies:

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

❯ Authentication/authorization

Let's go to the user management platform Clerk and create a project there. Find Publishable key in the section API Keys and create a file in the root of the project .env the following content:

VITE_CLERK_PUBLISHABLE_KEY=pk_test_...

We install two packages:

npm i @clerk/clerk-react @clerk/localizations

We wrap the root component of the application in a provider:

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>,
)

And we render the corresponding components in the site header:

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>
    </>
  )
}

Believe it or not, this is all that is needed to implement a full-fledged authentication/authorization mechanism (magic! :D).

❯ Database

Let's go to the BaaS platform Supabase and create a project there. Go to the section Project Settingsthen to the section APIwe find it there Project URL And anon public key V Project API keys and add them to .env:

VITE_SUPABASE_URL=https://....supabase.co
VITE_SUPABASE_ANON_KEY=eyJ...

Let's go to the section Table Editor and create such a table 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;

I created this table using a graphical interface.

Please note: for the table must be disabled row level security (icon RLS disabled).

Install the package:

npm i @supabase/supabase-js

Initialize and export the client:

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)

Believe it or not, that's all you need to create and configure Postres (magic! :D).

Please note: Supabase provides its own authentication/authorization mechanism, but I like Clerk better.

As an alternative, you can consider the following database options:

  • Vercel Postgres (I tried it, I liked it, but without Prisma It is not very convenient to work with the database, and for prisma need a server)
  • Convex (I haven't tried it, but I know it's trending, I plan to test it soon)

❯ Questions

Honestly, I didn't want to resort to AI help, but I had to 😀 I had file with 231 questions in Markdown format the following content:

## ❯ Вопрос № 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>
...

By the way, all questions, as well as a lot of other interesting and useful content can be found on my site.

I needed to convert this text into an array of objects like this:

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`.',
  },
  ...
]

As you can imagine, doing this manually is, to put it mildly, a bit tedious. And then I remembered that ChatGPT can analyze documents. I have it installed on my machine this is a great app:

I got access to ChatGPT from Russia like this: I bought a server in the Netherlands and deployed a VPN there according to the instructions in this great article. Then I found this great article, from where I switched to this is a wonderful site and bought a Dutch phone number there (for about 50 rubles, if my memory serves me right), to which a confirmation code from OpenAI was sent (your location should match the “homeland” of the phone number, if I understood the OpenAI validation scheme correctly).

So, I fed ChatGPT a file with questions and composed a request something like this: “Dear AI, would you be so kind as to analyze this document and transform the questions into objects like this:… I would be very grateful if you could format the result as a JavaScript file” 😀

After thinking for a minute, ChatGPT generated a nearly perfect JS file containing all questions as an array of objects (some questions stuck together, it took about an hour to edit the file).

❯ Deploy

To deploy my applications I use either Netlify (for SPA), or Vercel (for applications developed using Next.js). For deployment on Netlify I use Netlify CLI:

# Устанавливаем пакет глобально
npm i -g netlify-cli

# Авторизуемся (разумеется, у вас должен быть аккаунт)
netlify login

# Подключаем проект (репозиторий должен находится в GitHub)
netlify init

Believe it or not, that's all it takes to deploy your app and rebuild your app when you push changes to the repository using git push (continuos deployment in all its glory :D)

I guess that's all I wanted to share with you in this post.

Our immediate plans include:

  • expand functionality (there are a couple of ideas)
  • do PWA (There is pluginwho doesn't want to work yet)
  • make a mobile app (most likely it will be Android only) using React Native And Expo
  • maybe make a desktop app using Electron or Tauri

I will be glad to any comments and suggestions. Happy coding!


News, product reviews and contests from the Timeweb.Cloud team — in our Telegram channel

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *