World Vibe Web
The distributed app store for vibe-coded projects. Browse, discover, and install open-source apps from across GitHub.
翻译准备中
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
Каталог и система бронирования услуг для русскоязычной аудитории в UK. Next.js (App Router) + Supabase + Tailwind, деплой на Vercel. Требования, интерфейс и порядок работ описаны в DESIGN.md и PROMPTS.md. Правила разработки — в CLAUDE.md. • Next.js 16 (App Router, TypeScript, ESLint) • Supabase — Postgres, Auth, Storage (@supabase/supabase-js, @supabase/ssr) • Tailwind CSS 4 .env.local в git не попадает.查看完整 README ↗
Каталог и система бронирования услуг для русскоязычной аудитории в UK. Next.js (App Router) + Supabase + Tailwind, деплой на Vercel.
Требования, интерфейс и порядок работ описаны в DESIGN.md и PROMPTS.md.
Правила разработки — в CLAUDE.md.
@supabase/supabase-js, @supabase/ssr)/app — роуты Next.js
/components — переиспользуемые UI-компоненты
/lib — работа с данными, клиенты Supabase, утилиты
/supabase — server- и browser-клиенты Supabase
/supabase — SQL-миграции
/types — общие типы TypeScript
Скопируй .env.local.example в .env.local и заполни:
| Переменная | Назначение |
|---|---|
NEXT_PUBLIC_SUPABASE_URL | URL проекта Supabase |
NEXT_PUBLIC_SUPABASE_ANON_KEY | anon-ключ (безопасен для браузера) |
SUPABASE_SERVICE_ROLE_KEY | сервисный ключ, только сервер, обходит RLS |
NEXT_PUBLIC_SITE_URL | базовый URL сайта |
CRON_SECRET | секрет для защищённого роута планировщика волн (любая длинная случайная строка) |
NOMINATIM_CONTACT_EMAIL | необязательно: контактный email для User-Agent геокодера OpenStreetMap (их правила просят идентифицировать приложение) |
.env.local в git не попадает.
npm install # установка зависимостей
npm run dev # локальный запуск на http://localhost:3000
npm run build # production-сборка
npm run lint # ESLint
npm run seed # залить демо-данные из seed-data.json (нужен .env.local)
npm run seed:dry # проверить данные без записи в БД
npm run brand:assets # перерисовать иконки, favicon и OG-картинку из лампы и цветов палитры
npm run seed:test-master # создать тестового мастера, привязанного к твоему аккаунту
npm run requests:advance # прогнать одну итерацию волн вручную (нужен запущенный dev)
npm run seed идемпотентный: повторный запуск обновляет данные, а не дублирует.
Запускается через нативный TypeScript-стриппинг Node (без ts-node/tsx). Нужны
NEXT_PUBLIC_SUPABASE_URL и SUPABASE_SERVICE_ROLE_KEY в .env.local.
Схема — в /supabase (пронумерованные миграции), типы — в /types.
Подробности и порядок применения — в supabase/README.md.
next-intl, база en, префикс в URL всегда явный (/en, /ru). Локали и флаг
enabled — в i18n/locales.ts, переводы — в /messages/{locale}.json.
Включённые: en, ru. Готовы к переводу (файлы с английскими значениями,
enabled: false): uk, kk, ka, hy. Отсутствующий ключ падает на английский.
generateMetadata в layout и на
страницах); hreflang для включённых локалей + x-default.sitemap.xml (динамический, из опубликованных провайдеров и событий, со всеми
локалями) и robots.txt (закрывает /admin).LocalBusiness на страницах провайдеров, Event на страницах событий.SUPABASE_SERVICE_ROLE_KEY используется только на
сервере; в клиентском бандле — лишь NEXT_PUBLIC_*.NEXT_PUBLIC_SITE_URL — продовый домен (влияет на canonical,
hreflang, sitemap, OG).supabase link --project-ref <ref> и
supabase db push. Локально — supabase start + supabase db reset. Детали —
в supabase/README.md.app_metadata.is_admin = true), включи Email-провайдер
(magic link), задай Site URL и Redirect URLs и вставь шаблоны писем — см.
«Письма входа» ниже.npm run seed (нужен
SUPABASE_SERVICE_ROLE_KEY).CRON_SECRET и настрой планировщик волн (см. «Планировщик волн» ниже).NEXT_PUBLIC_SITE_URL должен совпадать с реальным.Шаблоны писем Supabase Auth лежат в supabase/templates/ (собираются из
scripts/build-email-templates.mts: npm run email:templates). Локально их
использует Mailpit через supabase/config.toml; в hosted-проект их вставляют
руками в дашборде.
Ссылки в письмах ведут прямо на маршруты приложения с ?token_hash=…&type=…
(/[locale]/cabinet/auth/confirm, /admin/auth/confirm, оба вызывают
verifyOtp). Стандартный {{ .ConfirmationURL }} не подходит: клиент работает
по PKCE, и Supabase вернул бы ?code=, который маршруты не читают.
| Письмо | Ссылка |
|---|---|
| Magic link, Confirm signup | {{ .RedirectTo }}?token_hash={{ .TokenHash }}&type=email — назад на confirm-маршрут, который передало приложение (с локалью или админский) |
| Invite, Reset password, Change email | {{ .SiteURL }}/ru/cabinet/auth/confirm?token_hash={{ .TokenHash }}&type=invite / recovery / email_change — их может отправить дашборд, без адреса возврата |
Поэтому emailRedirectTo в приложении всегда без query-строки (админский next
передаётся в cookie admin_next).
В дашборде Supabase (Authentication → URL Configuration):
/ в конце.<домен>/** (и маска preview-доменов Vercel, если входить
на preview). Если адреса нет в списке, Supabase подставит Site URL и ссылка из
письма никуда не приведёт.Заявка доходит до мастера через NotificationChannel (lib/notifications/).
Сейчас единственная реализация — ConsoleChannel: печатает письмо в консоль
сервера и ничего не отправляет. Точки вызова расставлены как при настоящей
отправке — волны 1/2/3 (lib/requests/advance.ts), матч (accept и выбор отклика),
отмена (lib/requests/guest.ts). Факт отправки пишется в request_targets
(notified_at, channel), повторный прогон волны не шлёт дважды.
Подключить Resend, когда будет свой домен:
RESEND_API_KEY в env.npm i resend (единственная новая зависимость — согласуй).lib/notifications/resend-channel.ts c классом ResendChannel implements NotificationChannel, где send() вызывает Resend с message.subject,
message.body, message.emails (адрес «от» — на твоём домене).getNotificationChannel() (lib/notifications/channel.ts) верни
ResendChannel вместо ConsoleChannel. Точки вызова НЕ трогаются.ctaPath), а не саму заявку; контакты
клиента и адрес в письмо не попадают (раскрываются победителю после матча).SMS и push (когда появится приложение) добавляются так же — новой реализацией
того же интерфейса; request_targets.channel уже хранит фактический канал.
advanceRequests() продвигает заявки по волнам и истечению. Её нужно вызывать
раз в несколько минут. Vercel Cron на бесплатном тарифе запускается редко
(≈раз в сутки) — 15-минутные волны так не обслужить. Поэтому расписание держим
в Supabase: pg_cron внутри Postgres дёргает pg_net, который POST-ит на
защищённый роут /api/requests/advance (секрет CRON_SECRET в заголовке).
Логика волн остаётся одна, в TypeScript, дублировать её в SQL не нужно.
Настроить один раз (SQL-редактор Supabase, права владельца БД):
-- 1. расширения
create extension if not exists pg_cron;
create extension if not exists pg_net;
-- 2. секрет и URL держим в Vault, а не в открытом расписании
select vault.create_secret('https://<домен>/api/requests/advance', 'advance_url');
select vault.create_secret('<тот же CRON_SECRET, что в env Vercel>', 'advance_secret');
-- 3. каждые 3 минуты: прочитать секреты и вызвать роут
select cron.schedule('advance-requests', '*/3 * * * *', $$
select net.http_post(
url := (select decrypted_secret from vault.decrypted_secrets where name = 'advance_url'),
headers := jsonb_build_object(
'Content-Type', 'application/json',
'Authorization', 'Bearer ' || (select decrypted_secret from vault.decrypted_secrets where name = 'advance_secret')
),
body := '{}'::jsonb
);
$$);
Снять расписание: select cron.unschedule('advance-requests');. Проверить
запуски: select * from cron.job_run_details order by start_time desc limit 10;.
Роут без верного Bearer $CRON_SECRET отвечает 401, так что дёрнуть его снаружи
нельзя.
Фаза 1 — каталог, расписание, бронирование. Порядок реализации по шагам — в PROMPTS.md.
Готово: 0–7, 9. Перед запуском (SEO), 10. Фундамент под платное
продвижение, и 8. Участники занятия (Фаза 2) в объёме, возможном без
пользовательских аккаунтов: тумблер is_visible_to_group при бронировании и
безопасный показ участников группового слота (только имя + инициал, без
контактов, через SECURITY DEFINER функцию). Персональный кабинет с тумблером и
счётчик «вы были вместе N раз» требуют аккаунтов и появятся с ними.
Все 10 шагов PROMPTS.md пройдены.
maplibre-gl 5.24.0 — GHSA-jrc7-96c5-q579 (critical): обход XSS-фильтра в
DOM.sanitize(). Уязвимы все версии ≤ 6.4.0; исправление есть только в 6.x
(сейчас 6.11.2) — это major-обновление с breaking changes, отдельная задача.
Реальный риск у нас низкий: HTML в карту попадает только из нашей строки
атрибуции «© OpenStreetMap contributors» (lib/maps/style.ts), Popup /
setHTML не используются, стиль задан в коде. План: перейти на 6.x в
отдельной ветке и проверить ProvidersMap, StaticMiniMap, AdminPickerMap.Закрытый раздел /admin (не локализован, не индексируется). Вход — magic link
через Supabase Auth. Доступ и запись требуют app_metadata.is_admin = true у
пользователя (см. supabase/README.md). CRUD провайдеров (все поля, услуги,
расписание, языки, переводы) и событий; загрузка фото в Storage.