Перейти до вмісту
mp.
Усі тексти
Розробка

Мистецька галерея на React і Supabase

Переклад з англійського оригіналу.

Допомагаю організовувати HOME/HOMELAND, міжнародну студентську мистецьку ініціативу, і створив її сайт для подання, перегляду та експонування робіт.

Написав його на React, TypeScript і Supabase, використовуючи агентів під час реалізації та рев’ю. Митці подають роботи приватно. Команда переглядає їх і вирішує, що публікувати. Відвідувачі переглядають виставку.

Галерея має працювати й після першого завантаження. Люди переходять між залами, поки запити ще виконуються. Термін дії медіапосилань спливає. Невдалому запиту потрібен ще один шанс, але не нескінченні повтори. Нижче описую рішення про те, хто може бачити дані й хто ними керує в цих ситуаціях.

Публікація є окремим рішенням

Приватна заявка

Митець подає відомості й роботуЧитати заявку можуть її власник та адміністратори.
Команда переглядає заявкуПрийняття заявки не публікує її.
Явне рішення про публікацію

Публічна виставка

Повернути опубліковані поля виставкиget_published_artworks обирає, що можуть бачити відвідувачі.
Відвідувачі переглядають галереюВони отримують публічні поля, а не повну приватну заявку.

Цю межу забезпечують дозволи бази даних і сховища. Приховування екрана в React її не забезпечує.

Прийняти заявку не означає опублікувати її. Для роботи, отриманої електронною поштою, також потрібен запис, перш ніж сайт зможе її показати. Якщо твору немає в галереї, потрібно знати, як він потрапив у проєкт і чи був опублікований.

Публічна галерея викликає get_published_artworks, функцію бази даних, яка обирає опубліковані роботи й повертає поля, потрібні для виставки. Приватна заявка містить інформацію, яку відвідувачі не мають отримувати. Публічний доступ до всього рядка розкрив би її, навіть якби React ніколи її не показував.

Напряму читати заявки можуть лише їхні власники й адміністратори. Політики сховища окремо перевіряють, чи належить файл опублікованій роботі. Обидві перевірки мають бути на бекенді, щоб запити поза інтерфейсом теж мусили їх пройти.

Функція використовує SECURITY DEFINER. Її привілеї, вибрані поля та налаштований шлях пошуку потрібно перевіряти, бо вона виконується з правами свого власника. Документація функцій та посібник із захисту на рівні рядків Supabase пояснюють потрібні тут перевірки.

Форма подання також зберігає локальну чернетку з часом завершення дії й відхиляє некоректні або прострочені чернетки. Сховище браузера може не працювати, а збереження імені файлу не відновлює сам завантажений файл. Перевірки типу й розміру файлу дають ранній зворотний зв’язок; бекенд мусить застосовувати власні правила завантаження.

Зберігати колекцію під час переходів між залами

Перехід із вестибюля до зали змінює видимі роботи. Він не має заново завантажувати всю виставку. Вихід із галереї та повернення пізніше є іншою ситуацією: тоді потрібне свіже завантаження.

Effect завантаження даних прив’язаний до часу життя галереї, а зала лише фільтрує вже завантажену колекцію. Відвідувач може зайти до зали до завершення початкового запиту, не створюючи ще один. Це також не дає замінювати завантажені зображення заглушками при кожному переході.

Стан самої галереї забезпечив колекції потрібний час життя без кешу на рівні всього застосунку.

Підписаний медіа-URL дає тимчасовий доступ до постійного шляху файлу. Галерея зберігає цей шлях, щоб пізніше поновити URL. У цій реалізації посилання діють годину, а поновлення заплановане через 55 хвилин.

Кожен окремий шлях має невеликий запис у реєстрі:

Стан За що відповідає
pending Не допускає другого запиту підписання, поки перший виконується.
failures Визначає обмежену затримку повтору.
nextAttemptAt Планує початкове підписання, повтор або поновлення.

Ключ за шляхом файлу дозволяє двом роботам із посиланням на той самий файл використовувати один запит підписання. Незалежне підписання різних шляхів означає, що один повільний запит не затримує всі інші зображення.

Ця незалежність створює іншу проблему: callbacks можуть завершуватися на різних тактах. Кожен успішний callback має оновити поточну колекцію, а не замінити її знімком на момент початку свого запиту. Відповідне оновлення в робочому коді використовує функціональне оновлення стану React:

setArt((current) =>
  current.map((piece) => {
    if (piece.filePath !== path) return piece;
    return piece.mediaType === 'image'
      ? { ...piece, image: url }
      : { ...piece, mediaUrl: url };
  })
);

Тут path і url походять із завершеного запиту підписання; setArt оновлює стан галереї. Цей уривок має бути всередині відповідного callback. Оновлення від current зберігає попередній успіх іншого запиту. Решта даних кожної роботи залишається незмінною, зокрема вихідний шлях для наступного поновлення.

Приклад: два медіазапити завершуються не по черзі
Запит A починаєтьсяПідписання URL роботи A триває довше.
Запит B починаєтьсяІнший шлях можна підписати незалежно.
B завершується першим
Оновити B в поточній колекціїA ще чекає. B вже має новий URL.
A завершується пізніше
Оновити A в поточній колекціїЗберегти завершене оновлення B. Обидві роботи тепер мають нові URL.

Заміна колекції старим знімком A могла б стерти успіх B.

Функціональне оновлення вище читає актуальний стан, коли React його застосовує. Callbacks від уже очищеного екземпляра галереї ігноруються.

Відновленню потрібні межа й можливість повернутися

Після помилки підписання є повтори через одну й три секунди, потім автоматичні спроби припиняються. Вичерпавши затримки, реалізація встановлює час наступної спроби в нескінченність. Це не дає недоступному сервісу створити нескінченний цикл таймерів.

Назавжди припинити спроби було б іншою помилкою. Повернення до зали, відновлення видимості сторінки, фокус вікна або повернення мережі можуть знову дозволити спробу для невдалого запису. Запис із pending не чіпаємо, тому ці сигнали не множать уже запущений запит.

Під час поновлення старе зображення залишається, поки отримуємо новий URL. Якби ми спочатку його прибрали, звичайна фонова операція виглядала б як миготіння чи порожній переглядач. Це не гарантує, що прострочений URL працюватиме далі; зате ми не викидаємо придатний стан до появи заміни.

Очищення прибирає таймери й обробники подій та позначає цей екземпляр effect як скасований. Уже запущені мережеві запити можуть завершитися, але їхні callbacks ігноруються. Це не те саме, що перервати сам запит.

Реєстр створюється всередині effect. У режимі розробки React Strict Mode може виконати налаштування, очищення й знову налаштування. Повторне використання реєстру, чиї старі запити логічно скасовані, могло б назавжди залишити записи в pending. Новий реєстр дає новому екземпляру почати власну роботу. React описує цю додаткову перевірку під час розробки.

Керувати порядком у тесті

Набір навігаційних тестів керує завершенням promises підписання й переводить тестовий годинник для поновлень і повторів. Він перевіряє, що запити, які завершуються на різних тактах, зберігають свої зображення, що Strict Mode не блокує підписання і що запізнілий callback старого екземпляра галереї не оновлює новий.

Один тест повторів переводить час до трьох дозволених спроб, потім ще раз, щоб підтвердити, що їхня кількість залишилася рівною трьом. Далі змінює залу й підтверджує відновлення. Інший зберігає останнє зображення після невдалого поновлення й перевіряє, що повернення на сторінку не дублює запит, який уже виконується.

Тестове оточення навмисно залишає неочікуваний другий запит виставки незавершеним. Мок, який одразу повертав би ту саму колекцію, приховав би випадкове повторне завантаження. Я хочу, щоб тест виявив цей другий запит.

У статті про роботу з агентами є окремий приклад на Bun із завершенням оновлень не по черзі. Можна запустити робочу версію, а потім підставити застарілий знімок і побачити, як перевірка падає.

Перевіряти досвід користування разом зі станом

Переглядач робіт має діалогове вікно з доступною назвою, закриття через Escape і шлях до коментарів із клавіатури.

Компонентні тести можуть перевірити доступні назви й надіслані клавіатурні події. Для справжнього переміщення фокуса, прокручування й відображення потрібна перевірка в браузері. Шаблон діалогового вікна ARIA є корисним орієнтиром для його поведінки.

Той самий принцип стосується мови. Галерея використовує мовні префікси в маршрутах, тому посилання зберігає мову, якою читала людина. Збережене налаштування не має непомітно перекривати URL, який хтось явно відкрив.

У цьому проєкті довелося перевіряти, чи збігаються вичитані німецькі переклади з файлами, які відправляємо на сайт. Для цього звіряв наданий документ, репозиторій та опубліковану сторінку.

Назви й описи, написані митцями, потребують особливої уваги. Браузерні тести перевіряють, що зміна мови інтерфейсу не перекладає авторський текст машинно. Також перевіряють арабську версію й переповнення на мобільних екранах. Авторський текст залишається в поданому вигляді, коли інтерфейс навколо змінює мову.

Зробити повторні перевірки доступними

GitHub Actions галереї запускає лінтинг, перевірку типів, продакшен-збірку, модульні тести й браузерні тести Chromium. Версія Bun закріплена в конфігурації пакета й використовується в командах Vercel, тому локальне оновлення середовища має дійти й до сервісу збірки.

Push у ту саму гілку скасовує старіший запуск CI. Застарілий коміт не займає чергу перевірок, поки його заміна чекає. Окремі гілки все ще можуть працювати незалежно. Керування паралельністю GitHub визначає це групування й скасування. Завантажений браузер кешується за ОС runner та точною версією Playwright; навіть при влучанні в кеш встановлюються потрібні системні залежності. Прогрітий кеш має економити завантаження, не підмінюючи браузер невідповідною версією. Це вибір проєкту, а не універсальна рекомендація: посібник Playwright для CI зазначає, що відновлення кешу браузера може тривати стільки ж, скільки завантаження.

Браузерний звіт і матеріали збоїв завантажуються навіть після падіння тестів, якщо запуск не скасовано. Невдала перевірка залишає матеріал для дослідження, а не лише червоний статус.

Vercel розгортає сайт із репозиторію окремо від цього CI. Успішні локальні тести не доводять, що деплой завершився, а сам запуск CI на push не блокує Vercel від розгортання цього push. Обов’язкові перевірки перед злиттям і огляд отриманого деплою є окремими частинами релізу.

У браузерних тестах живий бекенд вимкнений. Вони можуть перевіряти навігацію, фокус та інтерфейс подання, не вдаючи, що перевіряють продакшен-базу.

Що встановлюють ці перевірки

Проєкт використовує Vitest для компонентних тестів і Playwright для браузерних. Набори тестів подання, навігації галереї та переглядача перевіряють описані тут зміни стану. Репозиторій приватний, тож ця стаття є розбором проєкту; уривку коду вище потрібен навколишній callback галереї.

Компонентні тести підміняють бекенд-виклики, а браузерні вимикають живий бекенд. Вони перевіряють клієнтську поведінку. Щоб перевірити опубліковані дозволи, я також перевірив би доступ як відвідувач, автор заявки, інший митець і адміністратор. Успішний компонентний тест не може встановити, що сховище відхиляє чужого користувача або що лист доставлено.

В іншому проєкті я повторно використав би саме життєвий цикл запиту: зберігати постійну ідентичність файлу, оновлювати поточну колекцію, зупиняти callbacks старого екземпляра й давати невдалим запитам обмежену можливість відновлення. Кожне з цих рішень має сценарій збою, який можна відтворити до зміни реалізації.

Усі тексти: розробка, есеї та оповідання.