logo
Hello World!

BetaHub Web Form

20 czerwca 2025 (Updated: 5 lipca 2025)
typescriptbetahubpackage

Biblioteka TypeScript do tworzenia zewnętrznych formularzy zgłaszania błędów z przesyłaniem plików, drag & drop i walidacją w czasie rzeczywistym.

Projekt powstał na początku 2025 roku jako odpowiedź na konkretne potrzeby platformy BetaHub w obszarze zbierania feedback'u od zewnętrznych testerów. Problem był jasny: zespoły deweloperskie potrzebowały prostego sposobu na integrację formularzy zgłaszania błędów w swoich aplikacjach, bez konieczności implementacji skomplikowanej logiki walidacji, uploadu plików czy integracji z API.

BetaHub Web Form to biblioteka TypeScript, która rozwiązuje ten problem poprzez dostarczenie gotowego, uniwersalnego rozwiązania. Biblioteka oferuje dwa tryby użycia: imperatywny (poprzez atrybuty HTML) oraz deklaratywny (poprzez API programistyczne), co czyni ją dostępną zarówno dla frontend developerów jak i osób bez doświadczenia programistycznego.

Biblioteka została zaprojektowana z myślą o maksymalnej uniwersalności, oferując dwa komplementarne sposoby integracji:

Tryb imperatywny (HTML):

  • Integracja poprzez atrybuty data-bhwf-* w HTML
  • Idealne dla statycznych stron lub szybkich prototypów
  • Pełna funkcjonalność bez programowania

Tryb programistyczny (API):

  • Pełne API TypeScript z typami
  • Maksymalna kontrola nad zachowaniem
  • Integracja z frameworkami (React, Vue, Angular)
  • Zaawansowana konfiguracja i dostosowanie

  • Inteligentne przesyłanie plików: Automatyczne rozpoznawanie typów plików i kierowanie do odpowiednich endpointów
  • Przeciągnij i upuść (drag & drop): Natywna obsługa przeciągania plików z wizualną informacją zwrotną
  • Walidacja w czasie rzeczywistym: Natychmiastowa walidacja z wizualnymi komunikatami błędów
  • Responsywny wygląd: Gotowe style dla motywów ciemnego i jasnego
  • Śledzenie postępu: Wizualne śledzenie postępu przesyłania
  • Obsługa błędów: Kompleksowy system obsługi błędów
  • Dostępność: Pełne wsparcie dla czytników ekranu i nawigacji klawiaturą

TypeScript, Microbundle, Dropzone.js, HTML5 File API

Biblioteka została zaprojektowana jako modułowy system, gdzie każda funkcjonalność jest zaimplementowana jako niezależny moduł. Ta architektura zapewnia:

  • Łatwość testowania: Każdy moduł może być testowany niezależnie
  • Rozszerzalność: Nowe funkcjonalności mogą być dodawane bez wpływu na istniejący kod
  • Łatwość utrzymania: Jasne rozdzielenie odpowiedzialności

typescript
// Struktura modułów biblioteki
src/
├── bhwf.ts              // Główny punkt wejścia
├── Form.ts              // Klasa Form - główna logika
├── api.ts               // Komunikacja z API BetaHub
├── types.ts             // Definicje typów TypeScript
├── functions.ts         // Funkcje pomocnicze
├── inputs/
│   ├── Input.ts         // Obsługa pól tekstowych
│   └── FileInput.ts     // Obsługa pól plikowych
├── bhwf.css             // Style główne
└── dropzone.css         // Style dla dropzone
// Struktura modułów biblioteki
src/
├── bhwf.ts              // Główny punkt wejścia
├── Form.ts              // Klasa Form - główna logika
├── api.ts               // Komunikacja z API BetaHub
├── types.ts             // Definicje typów TypeScript
├── functions.ts         // Funkcje pomocnicze
├── inputs/
│   ├── Input.ts         // Obsługa pól tekstowych
│   └── FileInput.ts     // Obsługa pól plikowych
├── bhwf.css             // Style główne
└── dropzone.css         // Style dla dropzone

Biblioteka wykorzystuje microbundle do optymalizacji procesu budowania:

  • Wiele formatów: ES modules, CommonJS, UMD
  • Tree shaking (optymalizacja kodu): Automatyczne usuwanie nieużywanego kodu
  • Definicje TypeScript: Automatyczne generowanie plików .d.ts
  • Minifikacja: Optymalizacja rozmiaru dla produkcji

Biblioteka wykorzystuje dropzone.js jako fundament dla funkcjonalności drag & drop, ale rozszerza ją o:

  • Własna obsługa zdarzeń: Własne eventy dla lepszej integracji
  • Integracja z systemem śledzenia postępu: Synchronizacja z systemem śledzenia postępu
  • Obsługa błędów: Rozszerzona obsługa błędów specyficznych dla BetaHub
  • Integracja z motywami: Automatyczne dostosowanie do motywów biblioteki

Najprostszy sposób integracji - wystarczy dodać odpowiednie atrybuty do istniejącej struktury HTML:

html
<head>
  <script src="https://unpkg.com/betahub-web-form@1.*.*/dist/bhwf.js"></script>
  <link rel="stylesheet" href="https://unpkg.com/betahub-web-form@1.*.*/dist/bhwf.min.css" />
  <link rel="stylesheet" href="https://unpkg.com/betahub-web-form@1.*.*/dist/dropzone.min.css" />
</head>

<form
  data-bhwf-form
  data-bhwf-project-id="pr-123"
  data-bhwf-api-key="tkn-abc"
>
  <h1>Submit your issue</h1>

  <label>Description</label>
  <div class="bhwf-input-container">
    <textarea
      data-bhwf-input="description"
      data-bhwf-counter
      placeholder="Describe your issue as detailed as possible"
    ></textarea>
  </div>
  <span data-bhwf-error-msg="description"></span>

  <label>Steps to Reproduce</label>
  <textarea
    data-bhwf-input="stepsToReproduce"
    placeholder="Describe the steps leading to the issue"
  ></textarea>

  <label>Attach Media</label>
  <input type="file" data-bhwf-input="media" data-bhwf-dropzone multiple />

  <div data-bhwf-button="submit">Submit</div>

  <!-- Modals for different states -->
  <div data-bhwf-modal="loading">
    <div class="bhwf-loader"></div>
    <span data-bhwf-progress></span>
    <span data-bhwf-progress-msg></span>
  </div>

  <div data-bhwf-modal="error">
    <h2>Something went wrong!</h2>
    <span data-bhwf-error-msg="api"></span>
    <button data-bhwf-button="tryAgain">Try again</button>
  </div>

  <div data-bhwf-modal="success">
    <h2>All done!</h2>
    <a data-bhwf-button="viewIssue" href="" target="_blank">View Your Issue</a>
  </div>
</form>
<head>
  <script src="https://unpkg.com/betahub-web-form@1.*.*/dist/bhwf.js"></script>
  <link rel="stylesheet" href="https://unpkg.com/betahub-web-form@1.*.*/dist/bhwf.min.css" />
  <link rel="stylesheet" href="https://unpkg.com/betahub-web-form@1.*.*/dist/dropzone.min.css" />
</head>

<form
  data-bhwf-form
  data-bhwf-project-id="pr-123"
  data-bhwf-api-key="tkn-abc"
>
  <h1>Submit your issue</h1>

  <label>Description</label>
  <div class="bhwf-input-container">
    <textarea
      data-bhwf-input="description"
      data-bhwf-counter
      placeholder="Describe your issue as detailed as possible"
    ></textarea>
  </div>
  <span data-bhwf-error-msg="description"></span>

  <label>Steps to Reproduce</label>
  <textarea
    data-bhwf-input="stepsToReproduce"
    placeholder="Describe the steps leading to the issue"
  ></textarea>

  <label>Attach Media</label>
  <input type="file" data-bhwf-input="media" data-bhwf-dropzone multiple />

  <div data-bhwf-button="submit">Submit</div>

  <!-- Modals for different states -->
  <div data-bhwf-modal="loading">
    <div class="bhwf-loader"></div>
    <span data-bhwf-progress></span>
    <span data-bhwf-progress-msg></span>
  </div>

  <div data-bhwf-modal="error">
    <h2>Something went wrong!</h2>
    <span data-bhwf-error-msg="api"></span>
    <button data-bhwf-button="tryAgain">Try again</button>
  </div>

  <div data-bhwf-modal="success">
    <h2>All done!</h2>
    <a data-bhwf-button="viewIssue" href="" target="_blank">View Your Issue</a>
  </div>
</form>

Dla zaawansowanych przypadków użycia, biblioteka oferuje pełne API programistyczne:

typescript
import BHWF from 'betahub-web-form';

const form = new BHWF.Form({
  projectId: 'pr-123',
  apiKey: 'tkn-abc',
  formElement: document.querySelector('form')
});

// Event handlers
form.on('loading', () => {
  console.log('Form is loading...');
});

form.on('success', (data) => {
  console.log('Issue created:', data.issueId);
  console.log('Issue URL:', data.issueUrl);
});

form.on('apiError', (data) => {
  console.error('API Error:', data.message);
});

form.on('progress', (data) => {
  console.log(`Progress: ${data.progress}% - ${data.message}`);
});

// Programmatic form control
form.submit();
import BHWF from 'betahub-web-form';

const form = new BHWF.Form({
  projectId: 'pr-123',
  apiKey: 'tkn-abc',
  formElement: document.querySelector('form')
});

// Event handlers
form.on('loading', () => {
  console.log('Form is loading...');
});

form.on('success', (data) => {
  console.log('Issue created:', data.issueId);
  console.log('Issue URL:', data.issueUrl);
});

form.on('apiError', (data) => {
  console.error('API Error:', data.message);
});

form.on('progress', (data) => {
  console.log(`Progress: ${data.progress}% - ${data.message}`);
});

// Programmatic form control
form.submit();

typescript
import BHWF from 'betahub-web-form';

const form = new BHWF.Form({
  projectId: 'pr-123',
  apiKey: 'tkn-abc',
  formElement: document.querySelector('form'),
  customElements: {
    description: {
      validator: (value: string): [boolean, string | undefined] => {
        if (!value) return [false, 'Description is required'];
        if (value.length < 50) return [false, 'Description must be at least 50 characters'];
        return [true, undefined];
      }
    },
    media: {
      validator: (files: FileList | undefined): [boolean, string | undefined] => {
        if (files && files.length > 5) return [false, 'Maximum 5 files allowed'];
        return [true, undefined];
      }
    }
  }
});
import BHWF from 'betahub-web-form';

const form = new BHWF.Form({
  projectId: 'pr-123',
  apiKey: 'tkn-abc',
  formElement: document.querySelector('form'),
  customElements: {
    description: {
      validator: (value: string): [boolean, string | undefined] => {
        if (!value) return [false, 'Description is required'];
        if (value.length < 50) return [false, 'Description must be at least 50 characters'];
        return [true, undefined];
      }
    },
    media: {
      validator: (files: FileList | undefined): [boolean, string | undefined] => {
        if (files && files.length > 5) return [false, 'Maximum 5 files allowed'];
        return [true, undefined];
      }
    }
  }
});

Problem: Integracja biblioteki Dropzone.js z natywnymi elementami HTML <input type="file"> wymagała rozwiązania problemu synchronizacji danych między dwoma różnymi systemami zarządzania plikami.

Rozwiązanie:

  • Implementacja funkcji transformIntoDropzone() która ukrywa oryginalny input i tworzy element dropzone
  • Synchronizacja plików poprzez DataTransfer API
  • Automatyczne aktualizowanie input.files przy każdej zmianie w dropzone
typescript
const _syncFileInputFiles = () => {
  if (inputElement) {
    const fileList = new DataTransfer();
    dropzone.files.forEach((file) => fileList.items.add(file));
    inputElement.files = fileList.files;
    inputElement.dispatchEvent(new Event("input", { bubbles: true }));
  }
};
const _syncFileInputFiles = () => {
  if (inputElement) {
    const fileList = new DataTransfer();
    dropzone.files.forEach((file) => fileList.items.add(file));
    inputElement.files = fileList.files;
    inputElement.dispatchEvent(new Event("input", { bubbles: true }));
  }
};

Problem: Implementacja precyzyjnego śledzenia postępu podczas uploadu wielu plików różnych typów (screenshots, videos, logs) do różnych endpointów API.

Rozwiązanie:

  • Kalkulacja całkowitego rozmiaru wszystkich plików przed rozpoczęciem uploadu
  • Śledzenie postępu każdego pliku osobno z aktualizacją globalnego postępu
  • Obsługa różnych typów plików z automatycznym kierowaniem do odpowiednich endpointów
typescript
const totalFileSize: number = allFiles.reduce(
  (acc, file) => acc + (file.size || 0),
  0
);
let uploadedFileSize: number = 0;

// W funkcji uploadFile:
const progress = (uploadedFileSize / totalFileSize) * 100;
this.emit("progress", {
  progress: progress > 100 ? 100 : progress,
  message: `Uploading files: ${uploadedFiles + 1}/${totalFiles}`,
});
const totalFileSize: number = allFiles.reduce(
  (acc, file) => acc + (file.size || 0),
  0
);
let uploadedFileSize: number = 0;

// W funkcji uploadFile:
const progress = (uploadedFileSize / totalFileSize) * 100;
this.emit("progress", {
  progress: progress > 100 ? 100 : progress,
  message: `Uploading files: ${uploadedFiles + 1}/${totalFiles}`,
});

Problem: Implementacja inteligentnego systemu rozpoznawania typów plików w polu media i kierowania ich do odpowiednich endpointów API.

Rozwiązanie:

  • Analiza file.type (MIME type) dla automatycznej klasyfikacji
  • Routing plików do odpowiednich funkcji uploadu na podstawie typu
  • Obsługa fallback dla nieznanych typów plików
typescript
for (const mediaFile of Array.from(fileInputsData.media)) {
  const fileType: string = mediaFile.type;
  if (fileType.startsWith("image/")) {
    await uploadFile(API.uploadScreenshot, { screenshot: mediaFile });
  } else if (fileType.startsWith("video/")) {
    await uploadFile(API.uploadVideoClip, { videoClip: mediaFile });
  } else {
    await uploadFile(API.uploadLogFile, { logFile: mediaFile });
  }
}
for (const mediaFile of Array.from(fileInputsData.media)) {
  const fileType: string = mediaFile.type;
  if (fileType.startsWith("image/")) {
    await uploadFile(API.uploadScreenshot, { screenshot: mediaFile });
  } else if (fileType.startsWith("video/")) {
    await uploadFile(API.uploadVideoClip, { videoClip: mediaFile });
  } else {
    await uploadFile(API.uploadLogFile, { logFile: mediaFile });
  }
}

Problem: Implementacja systemu zarządzania stanem formularza z obsługą różnych stanów (loading, success, error) i automatycznym czyszczeniem błędów.

Rozwiązanie:

  • System event-driven z automatycznym ustawianiem atrybutu data-bhwf-state
  • Automatyczne czyszczenie błędów przy każdej zmianie inputu
  • Centralne zarządzanie stanem poprzez klasę Form
typescript
this.on("loading", () => {
  this.formElement?.setAttribute("data-bhwf-state", "loading");
});

this.on("success", (data) => {
  this.formElement?.setAttribute("data-bhwf-state", "success");
  // Aktualizacja linków do issue
});

this.formElement?.addEventListener("input", this.cleanErrors);
this.on("loading", () => {
  this.formElement?.setAttribute("data-bhwf-state", "loading");
});

this.on("success", (data) => {
  this.formElement?.setAttribute("data-bhwf-state", "success");
  // Aktualizacja linków do issue
});

this.formElement?.addEventListener("input", this.cleanErrors);

W normalnych warunkach umieszczanie kluczy API na frontendzie byłoby problemem bezpieczeństwa. W przypadku BetaHub Web Form jest to świadoma decyzja, uzasadniona specyfiką aplikacji:

  • Anonimowe zgłoszenia: Błędy mogą być zgłaszane przez anonimowych użytkowników
  • Rate limiting: Serwer implementuje zaawansowane ograniczanie liczby żądań
  • Weryfikacja po stronie serwera: Wszystkie zgłoszenia wymagają dodatkowej weryfikacji
  • Zarządzanie po stronie platformy: Klucze można w dowolnym momencie dezaktywować

Biblioteka implementuje podstawową walidację po stronie klienta:

  • Format danych: Sprawdzanie poprawności formatu i długości
  • Typy plików: Walidacja rozszerzeń i typów MIME
  • Rozmiar plików: Sprawdzanie limitów przed przesłaniem

BetaHub Web Form to biblioteka, która znacząco ułatwia tworzenie zewnętrznych formularzy do zgłaszania błędów na platformie BetaHub. Jej uniwersalność i łatwość użycia czyni ją idealnym rozwiązaniem dla zespołów deweloperskich potrzebujących szybkiej integracji systemu zbierania feedback'u.

Dual API Design: Stworzenie dwóch komplementarnych sposobów integracji - imperatywnego dla szybkich wdrożeń i deklaratywnego dla zaawansowanych przypadków użycia.

Modułowa architektura: System zbudowany z niezależnych modułów, zapewniający łatwość testowania, rozszerzania i utrzymania.

Optymalizacja wydajności: Optymalizacja rozmiaru biblioteki poprzez tree shaking i minifikację zasobów.

Ten projekt pokazał mi, jak ważne jest znalezienie równowagi między prostotą użycia a elastycznością. Początkowo myślałem o stworzeniu tylko imperatywnego API z atrybutami HTML, ale szybko zrozumiałem, że programiści potrzebują większej kontroli nad zachowaniem formularza.

Projekt nauczył mnie też, że czasami najlepsze rozwiązania są najprostsze. Zamiast tworzyć skomplikowany system konfiguracji, postawiłem na prosty system bazujący na eventach z możliwością customizacji walidacji. To pozwoliło zachować prostotę dla początkujących użytkowników, jednocześnie dając zaawansowanym programistom pełną kontrolę.

Najbardziej satysfakcjonujące było zobaczenie, jak biblioteka działa w praktyce - od prostego formularza HTML po zaawansowane integracje z React. To potwierdziło, że dobry design API może obsłużyć bardzo różne przypadki użycia bez komplikowania podstawowej funkcjonalności.

©2025 BatStack