Сайти та застосунки

Сайти та мобільні застосунки під задачу

Проєктуємо й пишемо швидкі сайти та застосунки для iOS і Android. Без шаблонів і без прив’язки: код, домен і всі акаунти оформлені на вас.

Що робимо

Сайти

Корпоративні сайти та посадкові сторінки, які відкриваються миттєво, готові до просування в Google і працюють кількома мовами з першого дня.

Мобільні застосунки

iOS і Android з однієї кодової бази: функція пишеться один раз і працює в обох системах. Публікацію в App Store і Google Play беремо на себе.

Бекенд та інтеграції

База даних, особисті кабінети, ролі й права доступу, приймання оплати через Stripe, розсилки, push-сповіщення та зв’язок із зовнішніми сервісами через API.

Супровід

Проєкт не закінчується в день запуску. Залишаємось поруч: виправляємо, вимірюємо, додаємо функції та стежимо за інфраструктурою.

Як працюємо

  1. Спочатку задача

    До першого рядка коду розбираємось, який процес з’їдає час або гроші. Іноді відповідь — не застосунок, і ми кажемо про це прямо.

  2. Готові перевірені частини

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

  3. Усе оформлено на вас

    Код, домен, хостинг і база даних — ваші. Будь-коли можете забрати все й піти до іншої команди, не питаючи нас.

Що ми вже збудували

Сайти, вебсистеми та застосунки для iOS і Android — роботи для клієнтів і наші власні продукти. До кожної — докладний розбір: задача, що всередині, екрани й технології.

Проєкти

Інженерія

Код, на якому працює цей сайт

Уривки в тому вигляді, в якому вони лежать у репозиторії: форма заявки, підписання договорів, статуси ремонту і години роботи майстерні. Усе вирізане позначено «// …».

Cloudflare Workerssrc/worker.js
if (form.company) return json({ ok: true });

const name = String(form.name ?? '').trim().slice(0, 200);
const contact = String(form.contact ?? '').trim().slice(0, 200);
const about = String(form.about ?? '').trim().slice(0, MAX_LEN);

if (!name || !contact || !about || form.consent !== true) {
  return json({ error: 'validation' }, 400);
}

if (env.TURNSTILE_SECRET) {
  const ip = request.headers.get('cf-connecting-ip');
  const ok = await verifyTurnstile(env.TURNSTILE_SECRET, form.turnstile, ip);
  if (!ok) return json({ error: 'captcha' }, 400);
}

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

Cloudflare Workerssrc/contracts.js
export async function sha256Hex(text) {
  return toHex(await crypto.subtle.digest('SHA-256', enc.encode(text)));
}
// …
const now = new Date().toISOString();
await env.DB.prepare(
  `update contracts
      set status = 'signed', signed_at = ?, signer_name = ?, signer_image = ?,
          signer_ip = ?, signer_ua = ?, signed_hash = ?,
          signer_phone = ?, signer_address = ?
    where id = ? and status <> 'signed'`,
)
  .bind(
    // …
    await sha256Hex(row.body),
    // …
  )
  .run();

Фіксуємо час, IP, пристрій і SHA-256 тексту, який людина бачила під час підписання: підпис можна довести, а документ — не підмінити.

Cloudflare Workerssrc/orders.js
const status = ALL_STATUSES.includes(b.status) ? b.status : row.status;
const now = new Date().toISOString();
// …
if (status !== row.status || clean(b.comment, 500)) {
  await addEvent(env, id, status, clean(b.comment, 500), now);
}

let mailed = false;
if (status === 'ready' && row.status !== 'ready') {
  mailed = await mailOrder(env, 'ready', {
    email: row.client_email,
    lang: row.lang,
    number: row.number,
    device: row.device,
    url: new URL(`/o/${id}`, SITE.url).href,
  });
}

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

браузерsrc/scripts/shop-status.js
function canary(d) {
  const f = new Intl.DateTimeFormat('en-CA', {
    timeZone: 'Atlantic/Canary', year: 'numeric', month: '2-digit', day: '2-digit',
    hour: '2-digit', minute: '2-digit', hourCycle: 'h23', weekday: 'short',
  });
  const p = Object.fromEntries(f.formatToParts(d).map((x) => [x.type, x.value]));
  const dow = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 }[p.weekday];
  return { date: `${p.year}-${p.month}-${p.day}`, dow, mins: Number(p.hour) * 60 + Number(p.minute) };
}
const toMins = (hhmm) => Number(hhmm.slice(0, 2)) * 60 + Number(hhmm.slice(3, 5));
const workday = (c) => (HOURS[c.dow] && !HOLIDAYS.has(c.date) ? HOURS[c.dow] : null);

export function shopStatus(now = new Date()) {
  const c = canary(now);
  const h = workday(c);
  if (h && c.mins >= toMins(h[0]) && c.mins < toMins(h[1])) return { open: true, closes: h[1] };
  // …
}

Сайт сам знає, чи відчинена майстерня: час Тенерифе з переходом на літній і святами — з того самого джерела, що й дані для Google.

Стек наших продуктів

Лише те, на чому справді працюють HostBase, Hourgrid, Mansaria і цей сайт. Нічого «для солідності».

  • Astro, Веб
  • Next.js, Веб
  • TypeScript, Веб
  • Tailwind CSS, Веб
  • Canvas, Веб
  • React Native, Мобільні
  • Expo, Мобільні
  • PostgreSQL, Дані
  • Supabase, Дані
  • Cloudflare D1, Дані
  • Cloudflare R2, Дані
  • Workers KV, Дані
  • Cloudflare Workers, Сервер
  • Cron Triggers, Сервер
  • Netlify, Сервер
  • Row Level Security, Захист
  • Turnstile, Захист
  • Web Crypto, Захист
  • Stripe, Платежі
  • Verifactu, Податки
  • Brevo, Пошта
  • Anthropic, ШІ
  • Groq, ШІ
  • Sentry, Моніторинг

Уже знаєте, що потрібно? Позначте задачі, стек і формат роботи — заявка збереться сама.

Зібрати проєкт у конструкторі

Є ідея або процес, який з’їдає день?

Розкажіть у WhatsApp. Перша розмова безкоштовна й потрібна, щоб зрозуміти, чи варто це взагалі робити.

WhatsApp