From 7f31a68afe15fb0cab5a847dc26e203db35aadcb Mon Sep 17 00:00:00 2001 From: kosyachniy Date: Mon, 23 Feb 2026 01:52:17 +0300 Subject: [PATCH 1/2] fix(web): prevent pnpm prune from running husky in Docker Use 'pnpm prune --prod --ignore-scripts' in the builder stage so prepare scripts do not fail after devDependencies are removed. --- web/Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web/Dockerfile b/web/Dockerfile index 00f36d2..6a558df 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -24,8 +24,10 @@ COPY . . # Set build environment ENV NEXT_TELEMETRY_DISABLED=1 -# Build the application and strip dev dependencies for runtime -RUN pnpm run build && pnpm prune --prod +# Build the application and strip dev dependencies for runtime. +# `pnpm prune` may trigger lifecycle scripts after removing dev deps; +# disable scripts to avoid husky/prepare failures in CI Docker builds. +RUN pnpm run build && pnpm prune --prod --ignore-scripts # ======================================== # Development Stage - Development runtime with hot reload From bbfaf5f9b76c5e53b613bdea46af7e7b2e7f2fcc Mon Sep 17 00:00:00 2001 From: kosyachniy Date: Wed, 25 Mar 2026 18:38:08 +0300 Subject: [PATCH 2/2] Clean up frontend template interactions --- AGENTS.md | 2 + README.md | 6 +- web/messages/ar.json | 52 ++-- web/messages/en.json | 52 ++-- web/messages/es.json | 52 ++-- web/messages/ru.json | 52 ++-- web/messages/zh.json | 52 ++-- web/src/app/[locale]/catalog/page.tsx | 21 +- .../[locale]/hub/_components/HubToastDemo.tsx | 82 ------ web/src/app/[locale]/hub/page.tsx | 241 ++++++++---------- web/src/app/[locale]/layout.tsx | 29 ++- web/src/app/[locale]/page.tsx | 13 +- .../app/[locale]/posts/[categoryUrl]/page.tsx | 3 +- web/src/features/demo/stores/counterSlice.ts | 32 --- web/src/providers/index.tsx | 22 +- web/src/shared/config/app.ts | 4 +- web/src/shared/hooks/index.ts | 1 - web/src/shared/hooks/useApiWithToast.ts | 178 ------------- web/src/shared/hooks/useToast.ts | 4 +- web/src/shared/services/api/index.ts | 9 - web/src/shared/services/api/withToast.ts | 142 ----------- web/src/shared/stores/store.ts | 9 - web/src/shared/stores/toastSlice.ts | 3 +- .../category-management/ui/CategoryForm.tsx | 1 - .../ui/CategoryManagement.tsx | 5 +- .../category/ui/CategoriesHoverPopup.tsx | 3 +- web/src/widgets/contact-form-sidebar/index.ts | 2 - .../ui/ContactFormSidebar.tsx | 95 ------- .../widgets/feedback-system/lib/use-toast.tsx | 32 --- web/src/widgets/feedback-system/ui/Popup.tsx | 17 +- .../feedback-system/ui/PopupProvider.tsx | 84 +++--- web/src/widgets/footer/ui/Footer.tsx | 234 ++++++----------- web/src/widgets/header/ui/Header.tsx | 18 +- .../post-management/ui/PostManagement.tsx | 7 +- web/src/widgets/posts-list/ui/PostCard.tsx | 21 +- web/src/widgets/posts-list/ui/PostsGrid.tsx | 1 - .../widgets/posts-list/ui/PostsWithSearch.tsx | 1 - .../widgets/product-card/ui/ProductCard.tsx | 50 +--- .../ui/ProductManagement.tsx | 6 +- .../widgets/questionnaire-sidebar/index.ts | 2 - .../ui/QuestionnaireSidebar.tsx | 204 --------------- web/src/widgets/settings/ui/SettingsPage.tsx | 15 +- web/src/widgets/social/ui/SocialPage.tsx | 3 +- .../space-management/ui/SpaceManagement.tsx | 5 +- 44 files changed, 526 insertions(+), 1341 deletions(-) delete mode 100644 web/src/app/[locale]/hub/_components/HubToastDemo.tsx delete mode 100644 web/src/features/demo/stores/counterSlice.ts delete mode 100644 web/src/shared/hooks/useApiWithToast.ts delete mode 100644 web/src/shared/services/api/withToast.ts delete mode 100644 web/src/widgets/contact-form-sidebar/index.ts delete mode 100644 web/src/widgets/contact-form-sidebar/ui/ContactFormSidebar.tsx delete mode 100644 web/src/widgets/feedback-system/lib/use-toast.tsx delete mode 100644 web/src/widgets/questionnaire-sidebar/index.ts delete mode 100644 web/src/widgets/questionnaire-sidebar/ui/QuestionnaireSidebar.tsx diff --git a/AGENTS.md b/AGENTS.md index 65753f9..50693f1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -84,6 +84,7 @@ The "tasks" feature is a reward checklist that grants users inner coins after ve - **Cursor pointer everywhere**: All clickable elements/blocks/links/pickers/sliders must explicitly set `cursor-pointer` for clear affordance - **Dialog affordance**: Dialog close buttons AND the dimmed overlay/backdrop used to close dialogs must have `cursor-pointer` - **No duplicate API calls**: guard client-side fetch effects with stable fetch keys/in-flight refs so Strict Mode doesn’t trigger the same request multiple times (one request per dataset) +- **Honest template surfaces**: Never ship fake counters, `#` links, console-only submit handlers, or mock success actions on real routes. If a flow is not wired yet, hide it or render a disabled localized state with a clear reason. - **Documentation**: Write documentation directly in code files as comments and docstrings, not as separated files (No new .md files to describe logic, usage, or implementation details; No example .json files to show data structures or logging formats) - **Required fields UX**: For all forms mark required inputs with `*` and highlight missing/invalid required fields with a red focus/outline when the API returns validation errors (e.g., `detail` value). Keep visual feedback consistent across the app. - **Popups/Toasts**: Emit only one localized toast per error; map backend `detail`/field keys to translated messages and surface the exact field causing the issue (no duplicate global+local toasts). @@ -95,6 +96,7 @@ The "tasks" feature is a reward checklist that grants users inner coins after ve - **Shared translations first**: Use existing `system.*` translation keys for shared labels (loading, refresh, common actions) instead of introducing feature-specific duplicates; migrate simple words from feature scopes to `system.*` when touching those areas. - When adding new locale strings, ensure non-English locales are translated (avoid copy-pasting English into `ru`/`es`/`ar`/`zh`). - **No duplicated UI/i18n**: If multiple screens/forms need the same control or helper text, extract a shared component in `web/src/shared/ui/` and move the strings to `system.*` (delete feature-scoped duplicates). +- **Delete dead frontend layers**: Remove unused demo slices, duplicate toast hooks, unused providers, and speculative wrappers instead of keeping parallel infrastructure in the runtime tree. - **Unit suffixes**: Show measurement units using right-side labels/suffix segments on inputs (e.g., %, kg, cm); keep left labels clean. - **Number inputs**: Hide browser stepper arrows and prevent scroll-wheel value changes; use shared Input defaults or equivalent handlers for any custom number fields. - **Allow clearing inputs**: Form fields (including numeric inputs) must allow users to clear the value with Backspace/Delete before retyping; do not force immediate fallback values while typing. diff --git a/README.md b/README.md index 4a12699..867c5aa 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ -# Template Web App -Modern full-stack web application with Python FastAPI backend, Next.js frontend, Telegram bot, and Telegram / VK / MAX Mini App support. Built with Docker containers and featuring multilingual support, and production-ready flow. +# Launchpad Template +Reusable full-stack launch template with Python FastAPI backend, Next.js frontend, Telegram bot, and Telegram / VK / MAX Mini App support. Built with Docker containers, multilingual routing, and production-ready flows. + +Reference pages and shared template surfaces should stay honest: ship only real links and real actions, or render disabled localized states until the backend contract is wired. ## Background tasks (Taskiq) - Worker: `uv run taskiq worker tasks.broker:broker tasks.registry` diff --git a/web/messages/ar.json b/web/messages/ar.json index 0f3c3b2..1e0bbc8 100644 --- a/web/messages/ar.json +++ b/web/messages/ar.json @@ -40,20 +40,39 @@ "sections": "الأقسام" }, "hub": { - "toastDemo": { - "title": "الإشعارات", - "description": "شغّل جميع أنواع التوست للتحقق من المظهر.", - "actions": { - "default": "افتراضي" + "description": "طبقة التفاعل الحي للغرف والدردشة والمكالمات والعمل التعاوني.", + "intro": { + "title": "ما الذي يمثله هذا القسم", + "body": "استخدم المركز كطبقة التفاعل الفوري في المنتج: الغرف، خيوط الدردشة، مكالمات الفيديو، اللوحات المشتركة، الحضور، وتدفقات التنسيق. يجب أن يبدو حيًا واجتماعيًا، لا تحريريًا ولا موجّهًا للدعم." + }, + "modes": { + "title": "أنماط التفاعل", + "rooms": { + "title": "الغرف والمساحات", + "description": "غرف دائمة فيها أعضاء وأدوار وحضور وحالة غير مقروء وتبديل سريع بين السياقات." + }, + "chat": { + "title": "الدردشة والخيوط", + "description": "تدفقات رسائل للدردشة المباشرة ونقاشات الغرف والردود والتفاعلات والتنسيق السريع." }, - "messages": { - "default": "إشعار افتراضي", - "success": "إشعار نجاح", - "error": "إشعار خطأ", - "warning": "إشعار تحذير", - "info": "إشعار معلومات", - "loading": "جارٍ التحميل..." + "calls": { + "title": "الصوت والفيديو", + "description": "تدفقات المكالمات مع حالة انضمام واضحة ووضع المشاركين والجدولة والسياق بعد المكالمة." + }, + "boards": { + "title": "اللوحات والسبورات", + "description": "مساحات مشتركة للملاحظات والتعليقات والمراجعة والتفكير التعاوني." } + }, + "principles": { + "title": "قواعد المنتج", + "realtime": "ابنِ هذا القسم حول الحضور الفوري وحالة غير المقروء والكتابة وسياق المشاركين بدلًا من كتل المحتوى الثابتة.", + "lowNoise": "اجعل واجهة التفاعل بسيطة وبشرية: قوائم غرف واضحة وكثافة رسائل مريحة ومن دون ضوضاء زخرفية.", + "realStates": "اعرض فقط الحالات الحقيقية من الواجهة الخلفية أو طبقة النقل. لا تزوّر أعداد المتصلين أو نشاط الغرف أو مؤشرات المكالمات." + }, + "activation": { + "title": "قبل إطلاق تدفقات التفاعل", + "body": "اربط الغرف والأعضاء والرسائل والمكالمات والصلاحيات واللوحات المشتركة أولاً بعقود خلفية حقيقية أو بطبقة نقل فورية. إذا لم تكن قدرة ما جاهزة بعد، فأخفها أو عطّلها مع سبب مترجم بوضوح." } }, "tasks": { @@ -381,6 +400,8 @@ "next": "التالي", "download": "تحميل", "cancel": "إلغاء", + "ok": "حسنًا", + "confirm": "تأكيد", "none": "لا يوجد", "yes": "نعم", "no": "لا", @@ -392,6 +413,7 @@ "iconKeyPlaceholder": "home", "iconKeyDescription": "أدخل مفتاح أيقونة FontAwesome (بدون بادئة 'fa-'). اتركه فارغاً لعدم استخدام أيقونة.", "item": "عنصر", + "deleteItemConfirm": "هل تريد حذف \"{item}\"؟", "processing": "جارٍ المعالجة", "errors": { "unsupportedImageFormat": "تنسيق الصورة غير مدعوم. حوّل الملف إلى ‎JPG‎ أو ‎PNG‎ ثم حاول مرة أخرى." @@ -411,9 +433,9 @@ "smz": "عمل حر" }, "brand": { - "name": "web", - "description": "تطبيق ويب نموذجي", - "tagline": "تطبيق ويب نموذجي", + "name": "Launchpad", + "description": "قالب إطلاق متكامل قابل لإعادة الاستخدام", + "tagline": "قالب إطلاق متكامل قابل لإعادة الاستخدام", "address": "سانت بطرسبرغ، روسيا" }, "footer": { diff --git a/web/messages/en.json b/web/messages/en.json index ff625de..108056a 100644 --- a/web/messages/en.json +++ b/web/messages/en.json @@ -40,20 +40,39 @@ "sections": "Sections" }, "hub": { - "toastDemo": { - "title": "Toasts", - "description": "Trigger each toast variant to verify styling.", - "actions": { - "default": "Default" + "description": "Live interaction layer for rooms, chat, calls, and collaborative work.", + "intro": { + "title": "What this area represents", + "body": "Use the hub as the realtime interaction layer of the product: rooms, chat threads, video calls, shared boards, presence, and coordination flows. It should feel live and social, not editorial or support-oriented." + }, + "modes": { + "title": "Interaction modes", + "rooms": { + "title": "Rooms and spaces", + "description": "Persistent rooms with members, roles, presence, unread state, and fast context switching." + }, + "chat": { + "title": "Chat and threads", + "description": "Message flows for direct chat, room discussion, replies, reactions, and lightweight coordination." }, - "messages": { - "default": "This is a default toast", - "success": "This is a success toast", - "error": "This is an error toast", - "warning": "This is a warning toast", - "info": "This is an info toast", - "loading": "Loading..." + "calls": { + "title": "Voice and video", + "description": "Call flows with clear join state, participant status, scheduling, and post-call context." + }, + "boards": { + "title": "Boards and whiteboards", + "description": "Shared planning surfaces for notes, comments, review, and collaborative thinking." } + }, + "principles": { + "title": "Product rules", + "realtime": "Design this section around realtime presence, unread state, typing, and participant context instead of static content blocks.", + "lowNoise": "Keep interaction UI minimal and human: clear room lists, readable message density, and no decorative overload.", + "realStates": "Show only real backend or transport states. Do not fake online counts, room activity, or call metrics." + }, + "activation": { + "title": "Before shipping interaction flows", + "body": "Wire rooms, membership, messaging, calls, permissions, and shared boards to real backend contracts or realtime transport first. If a capability is not ready yet, keep it hidden or disabled with a localized reason." } }, "tasks": { @@ -381,6 +400,8 @@ "next": "Next", "download": "Download", "cancel": "Cancel", + "ok": "OK", + "confirm": "Confirm", "none": "None", "yes": "Yes", "no": "No", @@ -392,6 +413,7 @@ "iconKeyPlaceholder": "home", "iconKeyDescription": "Enter a FontAwesome icon key (without 'fa-' prefix). Leave empty for no icon.", "item": "Item", + "deleteItemConfirm": "Delete \"{item}\"?", "processing": "Processing", "errors": { "unsupportedImageFormat": "Unsupported image format. Please convert the file to JPG or PNG and try again." @@ -411,9 +433,9 @@ "smz": "Self-employed" }, "brand": { - "name": "web", - "description": "Template web app", - "tagline": "Template web app", + "name": "Launchpad", + "description": "Reusable full-stack launch template", + "tagline": "Reusable full-stack launch template", "address": "St. Petersburg, Russia" }, "footer": { diff --git a/web/messages/es.json b/web/messages/es.json index 19730e8..3d13f60 100644 --- a/web/messages/es.json +++ b/web/messages/es.json @@ -40,20 +40,39 @@ "sections": "Secciones" }, "hub": { - "toastDemo": { - "title": "Notificaciones", - "description": "Activa cada variante de toast para comprobar el estilo.", - "actions": { - "default": "Predeterminado" + "description": "Capa de interacción en vivo para salas, chat, llamadas y trabajo colaborativo.", + "intro": { + "title": "Qué representa esta área", + "body": "Usa el hub como la capa de interacción en tiempo real del producto: salas, hilos de chat, videollamadas, pizarras compartidas, presencia y flujos de coordinación. Debe sentirse vivo y social, no editorial ni orientado a soporte." + }, + "modes": { + "title": "Modos de interacción", + "rooms": { + "title": "Salas y espacios", + "description": "Salas persistentes con miembros, roles, presencia, estado de no leído y cambio rápido de contexto." + }, + "chat": { + "title": "Chat e hilos", + "description": "Flujos de mensajes para chat directo, discusión por sala, respuestas, reacciones y coordinación ligera." }, - "messages": { - "default": "Notificación predeterminada", - "success": "Notificación de éxito", - "error": "Notificación de error", - "warning": "Notificación de advertencia", - "info": "Notificación informativa", - "loading": "Cargando..." + "calls": { + "title": "Voz y video", + "description": "Flujos de llamadas con estado de unión claro, estado de participantes, agenda y contexto posterior." + }, + "boards": { + "title": "Tableros y pizarras", + "description": "Superficies compartidas para notas, comentarios, revisión y pensamiento colaborativo." } + }, + "principles": { + "title": "Reglas del producto", + "realtime": "Diseña esta sección alrededor de presencia en tiempo real, no leídos, escritura y contexto de participantes, no como bloques de contenido estático.", + "lowNoise": "Mantén la UI de interacción mínima y humana: listas claras de salas, densidad legible de mensajes y sin sobrecarga decorativa.", + "realStates": "Muestra solo estados reales del backend o del transporte. No simules usuarios en línea, actividad de salas ni métricas de llamadas." + }, + "activation": { + "title": "Antes de lanzar flujos interactivos", + "body": "Conecta primero salas, membresía, mensajería, llamadas, permisos y pizarras compartidas a contratos reales del backend o al transporte en tiempo real. Si una capacidad aún no está lista, mantenla oculta o deshabilitada con una razón localizada." } }, "tasks": { @@ -381,6 +400,8 @@ "next": "Siguiente", "download": "Descargar", "cancel": "Cancelar", + "ok": "Aceptar", + "confirm": "Confirmar", "none": "Ninguna", "yes": "Sí", "no": "No", @@ -392,6 +413,7 @@ "iconKeyPlaceholder": "home", "iconKeyDescription": "Ingresa una clave de icono FontAwesome (sin prefijo 'fa-'). Deja vacío para no usar icono.", "item": "Elemento", + "deleteItemConfirm": "¿Eliminar \"{item}\"?", "processing": "Procesando", "errors": { "unsupportedImageFormat": "Formato de imagen no admitido. Convierte el archivo a JPG o PNG y vuelve a intentarlo." @@ -411,9 +433,9 @@ "smz": "Autoempleado" }, "brand": { - "name": "web", - "description": "Aplicación web de plantilla", - "tagline": "Aplicación web de plantilla", + "name": "Launchpad", + "description": "Plantilla full-stack reutilizable para lanzamientos", + "tagline": "Plantilla full-stack reutilizable para lanzamientos", "address": "San Petersburgo, Rusia" }, "footer": { diff --git a/web/messages/ru.json b/web/messages/ru.json index ab37b12..a5ee9fd 100644 --- a/web/messages/ru.json +++ b/web/messages/ru.json @@ -40,20 +40,39 @@ "sections": "Разделы" }, "hub": { - "toastDemo": { - "title": "Уведомления", - "description": "Показывает все варианты тостов для проверки стилей.", - "actions": { - "default": "Обычный" + "description": "Живой слой взаимодействия для комнат, чатов, звонков и совместной работы.", + "intro": { + "title": "Что представляет этот раздел", + "body": "Используйте хаб как realtime-слой взаимодействия продукта: комнаты, треды чатов, видеозвонки, общие доски, присутствие и координация. Он должен ощущаться живым и социальным, а не редакционным или справочным." + }, + "modes": { + "title": "Режимы взаимодействия", + "rooms": { + "title": "Комнаты и пространства", + "description": "Постоянные комнаты с участниками, ролями, присутствием, непрочитанным состоянием и быстрым переключением контекста." + }, + "chat": { + "title": "Чаты и треды", + "description": "Сообщения для личного общения, дискуссий в комнатах, ответов, реакций и быстрой координации." }, - "messages": { - "default": "Обычное уведомление", - "success": "Успешное уведомление", - "error": "Уведомление об ошибке", - "warning": "Предупреждение", - "info": "Информация", - "loading": "Загрузка..." + "calls": { + "title": "Голос и видео", + "description": "Сценарии звонков с понятным входом, статусом участников, расписанием и контекстом после созвона." + }, + "boards": { + "title": "Доски и whiteboard", + "description": "Общие поверхности для заметок, комментариев, ревью и совместного мышления." } + }, + "principles": { + "title": "Правила продукта", + "realtime": "Стройте этот раздел вокруг realtime-присутствия, непрочитанного состояния, набора текста и контекста участников, а не вокруг статичных контентных блоков.", + "lowNoise": "Делайте интерфейс взаимодействия минималистичным и человеческим: понятные списки комнат, комфортная плотность сообщений и без декоративного шума.", + "realStates": "Показывайте только реальные статусы из backend или транспортного слоя. Не имитируйте онлайн, активность комнат или метрики звонков." + }, + "activation": { + "title": "Перед запуском интерактивных сценариев", + "body": "Сначала подключите комнаты, участников, сообщения, звонки, права и общие доски к реальным backend-контрактам или realtime-транспорту. Если возможность ещё не готова, держите её скрытой или отключённой с локализованной причиной." } }, "tasks": { @@ -381,6 +400,8 @@ "next": "Далее", "download": "Скачать", "cancel": "Отмена", + "ok": "ОК", + "confirm": "Подтвердить", "none": "Нет", "yes": "Да", "no": "Нет", @@ -392,6 +413,7 @@ "iconKeyPlaceholder": "home", "iconKeyDescription": "Введите ключ иконки FontAwesome (без префикса 'fa-'). Оставьте пустым для отсутствия иконки.", "item": "Элемент", + "deleteItemConfirm": "Удалить «{item}»?", "processing": "Обработка", "errors": { "unsupportedImageFormat": "Неподдерживаемый формат изображения. Конвертируйте файл в JPG или PNG и попробуйте снова." @@ -411,9 +433,9 @@ "smz": "Самозанятый" }, "brand": { - "name": "web", - "description": "Шаблонное веб-приложение", - "tagline": "Шаблонное веб-приложение", + "name": "Launchpad", + "description": "Переиспользуемый full-stack шаблон для запуска", + "tagline": "Переиспользуемый full-stack шаблон для запуска", "address": "Россия, Санкт-Петербург" }, "footer": { diff --git a/web/messages/zh.json b/web/messages/zh.json index 541850d..50b3823 100644 --- a/web/messages/zh.json +++ b/web/messages/zh.json @@ -40,20 +40,39 @@ "sections": "版块" }, "hub": { - "toastDemo": { - "title": "通知", - "description": "触发各类 Toast 以检查样式。", - "actions": { - "default": "默认" + "description": "用于房间、聊天、通话和协作工作的实时互动层。", + "intro": { + "title": "这个区域代表什么", + "body": "将 hub 用作产品的实时互动层:房间、聊天线程、视频通话、共享白板、在线状态与协作流程。它应该体现出实时和社交感,而不是内容页或支持中心。" + }, + "modes": { + "title": "互动模式", + "rooms": { + "title": "房间与空间", + "description": "持久房间,包含成员、角色、在线状态、未读状态以及快速上下文切换。" + }, + "chat": { + "title": "聊天与线程", + "description": "支持私聊、房间讨论、回复、反应和轻量协作的消息流。" }, - "messages": { - "default": "默认提示", - "success": "成功提示", - "error": "错误提示", - "warning": "警告提示", - "info": "信息提示", - "loading": "加载中..." + "calls": { + "title": "语音与视频", + "description": "具备清晰加入状态、参与者状态、排期和通话后上下文的通话流程。" + }, + "boards": { + "title": "白板与协作板", + "description": "用于笔记、评论、评审和共同思考的共享协作表面。" } + }, + "principles": { + "title": "产品规则", + "realtime": "围绕实时在线、未读、输入状态和参与者上下文来设计,而不是静态内容模块。", + "lowNoise": "互动界面要保持克制和有人味:清晰的房间列表、舒适的消息密度、不过度装饰。", + "realStates": "只展示来自后端或传输层的真实状态,不要伪造在线人数、房间活跃度或通话指标。" + }, + "activation": { + "title": "上线互动流程之前", + "body": "先把房间、成员、消息、通话、权限和共享白板接到真实后端契约或实时传输层。如果某个能力还没准备好,就先隐藏,或以本地化原因禁用。" } }, "tasks": { @@ -381,6 +400,8 @@ "next": "下一个", "download": "下载", "cancel": "取消", + "ok": "确定", + "confirm": "确认", "none": "无", "yes": "是", "no": "否", @@ -392,6 +413,7 @@ "iconKeyPlaceholder": "home", "iconKeyDescription": "输入 FontAwesome 图标键(不含 'fa-' 前缀)。留空表示不使用图标。", "item": "项目", + "deleteItemConfirm": "删除“{item}”?", "processing": "处理中", "errors": { "unsupportedImageFormat": "不支持的图片格式。请将文件转换为 JPG 或 PNG 后重试。" @@ -411,9 +433,9 @@ "smz": "自由职业" }, "brand": { - "name": "web", - "description": "模板Web应用程序", - "tagline": "模板Web应用程序", + "name": "Launchpad", + "description": "可复用的全栈上线模板", + "tagline": "可复用的全栈上线模板", "address": "俄罗斯圣彼得堡" }, "footer": { diff --git a/web/src/app/[locale]/catalog/page.tsx b/web/src/app/[locale]/catalog/page.tsx index f56b16b..ce21004 100644 --- a/web/src/app/[locale]/catalog/page.tsx +++ b/web/src/app/[locale]/catalog/page.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import Image from 'next/image'; +import { useSearchParams } from 'next/navigation'; import { useTranslations } from 'next-intl'; import { useRouter } from '@/i18n/routing'; import { PageHeader } from '@/shared/ui/page-header'; @@ -26,10 +27,12 @@ export default function CatalogPage() { const tSearch = useTranslations('search'); const tSystem = useTranslations('system'); const router = useRouter(); + const searchParams = useSearchParams(); + const urlQuery = searchParams.get('q')?.trim() ?? ''; - const [query, setQuery] = useState(''); + const [query, setQuery] = useState(urlQuery); const [filters, setFilters] = useState({}); - const [appliedQuery, setAppliedQuery] = useState(''); + const [appliedQuery, setAppliedQuery] = useState(urlQuery); const [appliedFilters, setAppliedFilters] = useState({}); // Redux state @@ -75,14 +78,19 @@ export default function CatalogPage() { // Handle search const handleSearch = useCallback((searchQuery: string, searchFilters: SearchFilters) => { - const nextQuery = searchQuery ?? query; + const nextQuery = (searchQuery ?? query).trim(); const nextFilters = searchFilters ?? filters; setQuery(nextQuery); setFilters(nextFilters); setAppliedQuery(nextQuery); setAppliedFilters(nextFilters); - }, [filters, query]); + router.replace( + nextQuery + ? { pathname: '/catalog', query: { q: nextQuery } } + : { pathname: '/catalog' } + ); + }, [filters, query, router]); // Handle favorites and cart actions const handleOpenFavorites = useCallback(() => { @@ -119,6 +127,11 @@ export default function CatalogPage() { [showError, tSystem] ); + useEffect(() => { + setQuery((currentQuery) => (currentQuery === urlQuery ? currentQuery : urlQuery)); + setAppliedQuery((currentQuery) => (currentQuery === urlQuery ? currentQuery : urlQuery)); + }, [urlQuery]); + useEffect(() => { if (!selectedSpace?.link) { setDealer(null); diff --git a/web/src/app/[locale]/hub/_components/HubToastDemo.tsx b/web/src/app/[locale]/hub/_components/HubToastDemo.tsx deleted file mode 100644 index f7feee6..0000000 --- a/web/src/app/[locale]/hub/_components/HubToastDemo.tsx +++ /dev/null @@ -1,82 +0,0 @@ -'use client'; - -import { useTranslations } from 'next-intl'; -import { useToast } from '@/widgets/feedback-system'; -import { Box } from '@/shared/ui/box'; -import { IconButton } from '@/shared/ui/icon-button'; -import { AlertIcon, CheckIcon, CloseIcon, InfoIcon, LoadingIcon, MessageIcon } from '@/shared/ui/icons'; - -export function HubToastDemo() { - const t = useTranslations('hub.toastDemo'); - const ts = useTranslations('system'); - const { toast, success, error, warning, info, loading } = useToast(); - - return ( - -
-
-

{t('title')}

-

{t('description')}

-
- -
- } - onClick={() => toast(t('messages.default'))} - > - {t('actions.default')} - - } - onClick={() => success(t('messages.success'))} - > - {ts('success')} - - } - onClick={() => error(t('messages.error'))} - > - {ts('error')} - - } - onClick={() => warning(t('messages.warning'))} - > - {ts('warning')} - - } - onClick={() => info(t('messages.info'))} - > - {ts('info')} - - } - onClick={() => loading(t('messages.loading'))} - > - {ts('loading')} - -
-
-
- ); -} - diff --git a/web/src/app/[locale]/hub/page.tsx b/web/src/app/[locale]/hub/page.tsx index f6ed519..9903ac5 100644 --- a/web/src/app/[locale]/hub/page.tsx +++ b/web/src/app/[locale]/hub/page.tsx @@ -1,160 +1,145 @@ -import { getTranslations } from 'next-intl/server'; import { Metadata } from 'next'; -import { PageHeader } from '@/shared/ui/page-header'; +import { getTranslations } from 'next-intl/server'; import { Box } from '@/shared/ui/box'; -import { HubIcon, MessageIcon, QuestionIcon, BookIcon, PaletteIcon, ConstructionIcon } from '@/shared/ui/icons'; -import { HubToastDemo } from './_components/HubToastDemo'; +import { PageHeader } from '@/shared/ui/page-header'; +import { ConstructionIcon, HubIcon, MessageIcon, UsersIcon, VideoIcon, WhiteboardIcon } from '@/shared/ui/icons'; export async function generateMetadata(): Promise { - const t = await getTranslations('navigation'); + const [tNavigation, tHub] = await Promise.all([ + getTranslations('navigation'), + getTranslations('hub'), + ]); return { - title: `${t('hub')} - Community Forum`, - description: 'Community forum for user-generated content and discussions', + title: tNavigation('hub'), + description: tHub('description'), }; } export default async function HubPage() { - const t = await getTranslations('navigation'); + const [tNavigation, tHub] = await Promise.all([ + getTranslations('navigation'), + getTranslations('hub'), + ]); + + const modes = [ + { + key: 'rooms', + icon: UsersIcon, + title: tHub('modes.rooms.title'), + description: tHub('modes.rooms.description'), + className: 'bg-blue-500/15 text-blue-600 dark:bg-blue-500/20 dark:text-blue-400', + }, + { + key: 'chat', + icon: MessageIcon, + title: tHub('modes.chat.title'), + description: tHub('modes.chat.description'), + className: 'bg-emerald-500/15 text-emerald-600 dark:bg-emerald-500/20 dark:text-emerald-400', + }, + { + key: 'calls', + icon: VideoIcon, + title: tHub('modes.calls.title'), + description: tHub('modes.calls.description'), + className: 'bg-violet-500/15 text-violet-600 dark:bg-violet-500/20 dark:text-violet-400', + }, + { + key: 'boards', + icon: WhiteboardIcon, + title: tHub('modes.boards.title'), + description: tHub('modes.boards.description'), + className: 'bg-amber-500/15 text-amber-600 dark:bg-amber-500/20 dark:text-amber-400', + } + ]; + + const principles = [ + tHub('principles.realtime'), + tHub('principles.lowNoise'), + tHub('principles.realStates'), + ]; return (
-
+
} iconClassName="bg-orange-500/15 text-orange-600 dark:bg-orange-500/20 dark:text-orange-400" - title={t('hub')} - description="Community forum for user-generated content, discussions, and knowledge sharing." + title={tNavigation('hub')} + description={tHub('description')} /> -
- {/* Forum Categories */} -
-

Forum Categories

- -
- {/* General Discussion */} - -
-
-
- -
-
-

General Discussion

-

Open conversations about any topic

-
-
-
-
1,234 posts
-
Last: 2h ago
-
-
-
- - {/* Q&A */} - -
-
-
- -
-
-

Questions & Answers

-

Get help from the community

-
-
-
-
856 posts
-
Last: 1h ago
-
+
+
+ +
+
+
- +

{tHub('intro.title')}

+
+

+ {tHub('intro.body')} +

+
- {/* Tutorials */} - -
-
-
- -
-
-

Tutorials & Guides

-

Share knowledge and learn new skills

-
-
-
-
432 posts
-
Last: 3h ago
-
+ +
+
+
- - - {/* Showcase */} - -
-
-
- +

{tHub('modes.title')}

+
+
+ {modes.map(({ key, icon: Icon, title, description, className }) => ( +
+
+
+ +
+
+
{title}
+

{description}

+
-
-

Showcase

-

Show off your projects and creations

-
-
-
-
298 posts
-
Last: 5h ago
-
- -
+ ))} +
+
- {/* Sidebar */} -
-
- - {/* Community Stats */} - -

Community Stats

-
-
- Members - 15,234 -
-
- Topics - 2,820 -
-
- Posts - 28,567 -
-
- Online - 234 -
+
+ +
+
+
- +

{tHub('principles.title')}

+
+
    + {principles.map((item) => ( +
  • + {item} +
  • + ))} +
+
- {/* Recent Activity */} - -

Recent Activity

-
-
- User123 posted in General Discussion -
-
- DevPro answered a question -
-
- Designer shared a new tutorial -
+ +
+
+
- -
+

{tHub('activation.title')}

+
+

+ {tHub('activation.body')} +

+
diff --git a/web/src/app/[locale]/layout.tsx b/web/src/app/[locale]/layout.tsx index ab7c70d..a5b912b 100644 --- a/web/src/app/[locale]/layout.tsx +++ b/web/src/app/[locale]/layout.tsx @@ -14,6 +14,7 @@ import { ThemeProvider } from '@/providers'; import { PopupProvider } from '@/widgets/feedback-system'; import { ToastProvider } from '@/widgets/feedback-system'; import { StructuredData, ThemeAwareContent, VkBridgeInitializer } from '@/shared/components/layout'; +import { APP_CONFIG } from '@/shared/config/app'; import { getPublicAppEnv, isNonProdAppEnv } from '@/shared/lib/env'; import Script from "next/script"; @@ -31,14 +32,14 @@ const geistMono = Geist_Mono({ export const metadata: Metadata = { title: { - default: "web", - template: "%s | web" + default: APP_CONFIG.name, + template: `%s | ${APP_CONFIG.name}` }, - description: "Template web app", - keywords: ["web development"], - authors: [{ name: "Alex Poloz " }], - creator: "Alex Poloz ", - publisher: "Alex Poloz ", + description: APP_CONFIG.description, + keywords: ['launch template', 'next.js', 'fastapi'], + authors: [{ name: APP_CONFIG.author }], + creator: APP_CONFIG.author, + publisher: APP_CONFIG.author, formatDetection: { email: false, address: false, @@ -56,16 +57,16 @@ export const metadata: Metadata = { }, }, openGraph: { - title: "web", - description: "Template web app", + title: APP_CONFIG.name, + description: APP_CONFIG.description, url: '/', - siteName: 'web', + siteName: APP_CONFIG.name, images: [ { url: '/logo.svg', width: 1200, height: 630, - alt: 'web Logo', + alt: `${APP_CONFIG.name} logo`, }, ], locale: 'en_US', @@ -73,8 +74,8 @@ export const metadata: Metadata = { }, twitter: { card: 'summary_large_image', - title: "web", - description: "Template web app", + title: APP_CONFIG.name, + description: APP_CONFIG.description, images: ['/logo.svg'], }, robots: { @@ -97,7 +98,7 @@ export const metadata: Metadata = { appleWebApp: { capable: true, statusBarStyle: 'default', - title: 'web', + title: APP_CONFIG.name, }, }; diff --git a/web/src/app/[locale]/page.tsx b/web/src/app/[locale]/page.tsx index 5ef17ad..f2613c3 100644 --- a/web/src/app/[locale]/page.tsx +++ b/web/src/app/[locale]/page.tsx @@ -61,6 +61,7 @@ export default function Home() { const tAdminPosts = useTranslations('admin.posts'); const tAdminProducts = useTranslations('admin.products'); const tContact = useTranslations('contact'); + const tSystem = useTranslations('system'); const { success, error: showError } = useToastActions(); const locale = useLocale(); const [formData, setFormData] = useState({ @@ -86,11 +87,11 @@ export default function Home() { setLandingPosts(response.posts.slice(0, 3)); }) .catch((error) => { - console.error('Error loading landing posts:', error); - showError(tAdminPosts('loading')); + const message = error instanceof Error ? error.message : tSystem('error'); + showError(message); }) .finally(() => setPostsLoading(false)); - }, [locale, showError, tAdminPosts]); + }, [locale, showError, tSystem]); useEffect(() => { const fetchKey = `${locale}-landing-products`; @@ -103,11 +104,11 @@ export default function Home() { setLandingProducts(response.products.slice(0, 3)); }) .catch((error) => { - console.error('Error loading landing products:', error); - showError(tAdminProducts('loading')); + const message = error instanceof Error ? error.message : tSystem('error'); + showError(message); }) .finally(() => setProductsLoading(false)); - }, [locale, showError, tAdminProducts]); + }, [locale, showError, tSystem]); const heroHighlights: Array = useMemo( () => diff --git a/web/src/app/[locale]/posts/[categoryUrl]/page.tsx b/web/src/app/[locale]/posts/[categoryUrl]/page.tsx index 37ab7e5..05f9847 100644 --- a/web/src/app/[locale]/posts/[categoryUrl]/page.tsx +++ b/web/src/app/[locale]/posts/[categoryUrl]/page.tsx @@ -152,8 +152,7 @@ async function loadRelatedPosts(post: Post, locale: string) { }); return posts.filter((item) => item.id !== post.id).slice(0, 3); - } catch (error) { - console.error('Failed to load related posts', error); + } catch { return []; } } diff --git a/web/src/features/demo/stores/counterSlice.ts b/web/src/features/demo/stores/counterSlice.ts deleted file mode 100644 index 0313235..0000000 --- a/web/src/features/demo/stores/counterSlice.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit' - -interface CounterState { - value: number -} - -const initialState: CounterState = { - value: 0, -} - -export const counterSlice = createSlice({ - name: 'counter', - initialState, - reducers: { - increment: (state) => { - state.value += 1 - }, - decrement: (state) => { - state.value -= 1 - }, - incrementByAmount: (state, action: PayloadAction) => { - state.value += action.payload - }, - reset: (state) => { - state.value = 0 - }, - }, -}) - -export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions - -export default counterSlice.reducer diff --git a/web/src/providers/index.tsx b/web/src/providers/index.tsx index be4ef25..b30b4fc 100644 --- a/web/src/providers/index.tsx +++ b/web/src/providers/index.tsx @@ -1,22 +1,2 @@ -'use client'; - -import { PropsWithChildren } from 'react'; -import { ReduxProvider } from './provider'; -import { ThemeProvider } from './ThemeProvider'; -import { ToastProvider } from '@/widgets/feedback-system'; -import { PopupProvider } from '@/widgets/feedback-system'; - -export const AppProviders = ({ children }: PropsWithChildren) => ( - - - - - {children} - - - -); - -// Export individual providers for flexibility export { ReduxProvider } from './provider'; -export { ThemeProvider } from './ThemeProvider'; \ No newline at end of file +export { ThemeProvider } from './ThemeProvider'; diff --git a/web/src/shared/config/app.ts b/web/src/shared/config/app.ts index 12e3700..f548b15 100644 --- a/web/src/shared/config/app.ts +++ b/web/src/shared/config/app.ts @@ -5,9 +5,9 @@ const isProdLikeEnv = isProdLikeAppEnv(appEnv); // Application configuration export const APP_CONFIG = { - name: 'Web App', + name: 'Launchpad', version: '1.0.0', - description: 'Modern web application', + description: 'Reusable full-stack launch template', author: 'Alex Poloz', // API Configuration diff --git a/web/src/shared/hooks/index.ts b/web/src/shared/hooks/index.ts index 451a3e8..c4991f7 100644 --- a/web/src/shared/hooks/index.ts +++ b/web/src/shared/hooks/index.ts @@ -1,4 +1,3 @@ // Shared hooks public API export * from './useToast'; -export * from './useApiWithToast'; export * from './useApiErrorMessage'; diff --git a/web/src/shared/hooks/useApiWithToast.ts b/web/src/shared/hooks/useApiWithToast.ts deleted file mode 100644 index a2e498e..0000000 --- a/web/src/shared/hooks/useApiWithToast.ts +++ /dev/null @@ -1,178 +0,0 @@ -'use client'; - -import { useCallback, useMemo } from 'react'; -import { api, ApiError } from '@/shared/services/api/client'; -import { useToast } from './useToast'; - -export interface ApiCallOptions { - showSuccessToast?: boolean; - showErrorToast?: boolean; - successMessage?: string; - errorMessage?: string; - suppressDefaultErrors?: boolean; -} - -/** - * Hook that provides an API client with automatic toast notifications - * Usage: const apiClient = useApiWithToast(); - */ -export function useApiWithToast() { - const { success, error } = useToast(); - - const getErrorMessage = useCallback((err: unknown, customMessage?: string): string => { - if (customMessage) return customMessage; - - if (err instanceof ApiError) { - return err.message; - } - - if (err instanceof Error) { - return err.message; - } - - return 'An unexpected error occurred'; - }, []); - - const handleRequest = useCallback(async ( - requestFn: () => Promise, - options: ApiCallOptions = {} - ): Promise => { - const { - showSuccessToast = false, - showErrorToast = true, - successMessage, - errorMessage, - suppressDefaultErrors = false - } = options; - - try { - const result = await requestFn(); - - if (showSuccessToast && successMessage) { - success(successMessage); - } - - return result; - } catch (err) { - if (showErrorToast && !suppressDefaultErrors) { - const message = getErrorMessage(err, errorMessage); - error(message); - } - - throw err; // Re-throw so components can still handle errors if needed - } - }, [success, error, getErrorMessage]); - - const get = useCallback(( - endpoint: string, - params?: Record, - options?: ApiCallOptions - ): Promise => { - return handleRequest( - () => api.get(endpoint, params), - options - ); - }, [handleRequest]); - - const post = useCallback(( - endpoint: string, - body?: unknown, - options?: ApiCallOptions - ): Promise => { - return handleRequest( - () => api.post(endpoint, body), - options - ); - }, [handleRequest]); - - const put = useCallback(( - endpoint: string, - body?: unknown, - options?: ApiCallOptions - ): Promise => { - return handleRequest( - () => api.put(endpoint, body), - options - ); - }, [handleRequest]); - - const patch = useCallback(( - endpoint: string, - body?: unknown, - options?: ApiCallOptions - ): Promise => { - return handleRequest( - () => api.patch(endpoint, body), - options - ); - }, [handleRequest]); - - const del = useCallback(( - endpoint: string, - options?: ApiCallOptions - ): Promise => { - return handleRequest( - () => api.delete(endpoint), - options - ); - }, [handleRequest]); - - // Wrapper for existing API functions - const wrap = useCallback(( - apiFunction: (...args: TArgs) => Promise, - options?: ApiCallOptions - ) => { - return (...args: TArgs): Promise => { - return handleRequest( - () => apiFunction(...args), - options - ); - }; - }, [handleRequest]); - - return useMemo(() => ({ - get, - post, - put, - patch, - delete: del, - wrap, - // Direct access to the request handler for custom use - handleRequest - }), [get, post, put, patch, del, wrap, handleRequest]); -} - -// Convenience hook for specific operations -export function useApiActions() { - const apiClient = useApiWithToast(); - - return useMemo(() => ({ - ...apiClient, - // Pre-configured common actions - create: (endpoint: string, data: unknown) => - apiClient.post(endpoint, data, { - showSuccessToast: true, - successMessage: 'Created successfully!', - errorMessage: 'Failed to create' - }), - - update: (endpoint: string, data: unknown) => - apiClient.put(endpoint, data, { - showSuccessToast: true, - successMessage: 'Updated successfully!', - errorMessage: 'Failed to update' - }), - - remove: (endpoint: string) => - apiClient.delete(endpoint, { - showSuccessToast: true, - successMessage: 'Deleted successfully!', - errorMessage: 'Failed to delete' - }), - - load: (endpoint: string, params?: Record) => - apiClient.get(endpoint, params, { - errorMessage: 'Failed to load data' - }) - }), [apiClient]); -} \ No newline at end of file diff --git a/web/src/shared/hooks/useToast.ts b/web/src/shared/hooks/useToast.ts index 3343c7d..a2337c7 100644 --- a/web/src/shared/hooks/useToast.ts +++ b/web/src/shared/hooks/useToast.ts @@ -68,13 +68,13 @@ export function useToast(): UseToastReturn { message: string, options: Partial = {} ): string => { - const toastId = `toast_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const toastId = `toast_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`; // Separate action function from options to avoid storing in Redux const { action, ...reduxOptions } = options; // Add to Redux store (without the non-serializable action function) - dispatch(addToast({ message, ...reduxOptions })); + dispatch(addToast({ id: toastId, message, ...reduxOptions })); // Show with Sonner (with the action function) const toastConfig = { diff --git a/web/src/shared/services/api/index.ts b/web/src/shared/services/api/index.ts index 62d8a41..ead4d21 100644 --- a/web/src/shared/services/api/index.ts +++ b/web/src/shared/services/api/index.ts @@ -9,12 +9,3 @@ export type { ApiRequestOptions, ApiResponse } from './client'; // Export authentication functions export * from './auth'; - -// Common API utilities -/** - * Health check endpoint - */ -export async function healthCheck(): Promise<{ status: string; timestamp: string }> { - const { api } = await import('./client'); - return api.get('/health/'); -} diff --git a/web/src/shared/services/api/withToast.ts b/web/src/shared/services/api/withToast.ts deleted file mode 100644 index d0db6a3..0000000 --- a/web/src/shared/services/api/withToast.ts +++ /dev/null @@ -1,142 +0,0 @@ -'use client'; - -import { api, ApiError } from './client'; -import { toast } from 'sonner'; - -export interface ApiCallOptions { - showSuccessToast?: boolean; - showErrorToast?: boolean; - successMessage?: string; - errorMessage?: string; - suppressDefaultErrors?: boolean; -} - -/** - * Enhanced API wrapper that automatically shows toast notifications - * for errors and optionally for success messages - */ -class ApiWithToast { - private getErrorMessage(error: unknown, customMessage?: string): string { - if (customMessage) return customMessage; - - if (error instanceof ApiError) { - return error.message; - } - - if (error instanceof Error) { - return error.message; - } - - return 'An unexpected error occurred'; - } - - private async handleRequest( - requestFn: () => Promise, - options: ApiCallOptions = {} - ): Promise { - const { - showSuccessToast = false, - showErrorToast = true, - successMessage, - errorMessage, - suppressDefaultErrors = false - } = options; - - try { - const result = await requestFn(); - - if (showSuccessToast && successMessage) { - toast.success(successMessage); - } - - return result; - } catch (error) { - if (showErrorToast && !suppressDefaultErrors) { - const message = this.getErrorMessage(error, errorMessage); - toast.error(message); - } - - throw error; // Re-throw so components can still handle errors if needed - } - } - - async get( - endpoint: string, - params?: Record, - options?: ApiCallOptions - ): Promise { - return this.handleRequest( - () => api.get(endpoint, params), - options - ); - } - - async post( - endpoint: string, - body?: unknown, - options?: ApiCallOptions - ): Promise { - return this.handleRequest( - () => api.post(endpoint, body), - options - ); - } - - async put( - endpoint: string, - body?: unknown, - options?: ApiCallOptions - ): Promise { - return this.handleRequest( - () => api.put(endpoint, body), - options - ); - } - - async patch( - endpoint: string, - body?: unknown, - options?: ApiCallOptions - ): Promise { - return this.handleRequest( - () => api.patch(endpoint, body), - options - ); - } - - async delete( - endpoint: string, - options?: ApiCallOptions - ): Promise { - return this.handleRequest( - () => api.delete(endpoint), - options - ); - } -} - -// Export singleton instance -export const apiWithToast = new ApiWithToast(); - -// Convenience function for one-off requests with custom options -export function createApiCall( - requestFn: () => Promise, - options?: ApiCallOptions -): Promise { - const apiInstance = new ApiWithToast(); - return apiInstance['handleRequest'](requestFn, options); -} - -// Convenience wrapper for existing API calls -export function withAutoToast( - apiFunction: (...args: TArgs) => Promise, - defaultOptions?: ApiCallOptions -) { - return async (...args: TArgs): Promise => { - const apiInstance = new ApiWithToast(); - return apiInstance['handleRequest']( - () => apiFunction(...args), - defaultOptions - ); - }; -} \ No newline at end of file diff --git a/web/src/shared/stores/store.ts b/web/src/shared/stores/store.ts index 563193f..86eee3d 100644 --- a/web/src/shared/stores/store.ts +++ b/web/src/shared/stores/store.ts @@ -3,7 +3,6 @@ import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' import { persistStore, persistReducer } from 'redux-persist' import storage from 'redux-persist/lib/storage' import { combineReducers } from '@reduxjs/toolkit' -import { counterSlice } from '@/features/demo/stores/counterSlice' import { userSettingsSlice } from '@/features/user/stores/userSettingsSlice' import { toastSlice } from '@/shared/stores/toastSlice' import { cartSlice } from '@/features/cart/stores/cartSlice' @@ -13,12 +12,6 @@ import { authSlice } from '@/features/auth/stores/authSlice' import { spaceSelectionSlice } from '@/features/spaces/stores/spaceSelectionSlice' import { layoutSlice } from '@/shared/stores/layoutSlice' -// Persist configuration for counter -const counterPersistConfig = { - key: 'counter', - storage, -} - // Persist configuration for user settings const userSettingsPersistConfig = { key: 'userSettings', @@ -56,7 +49,6 @@ const layoutPersistConfig = { } // Create persisted reducers -const persistedCounterReducer = persistReducer(counterPersistConfig, counterSlice.reducer) const persistedUserSettingsReducer = persistReducer(userSettingsPersistConfig, userSettingsSlice.reducer) const persistedCartReducer = persistReducer(cartPersistConfig, cartSlice.reducer) const persistedFavoritesReducer = persistReducer(favoritesPersistConfig, favoritesSlice.reducer) @@ -66,7 +58,6 @@ const persistedLayoutReducer = persistReducer(layoutPersistConfig, layoutSlice.r // Root reducer const rootReducer = combineReducers({ - counter: persistedCounterReducer, userSettings: persistedUserSettingsReducer, cart: persistedCartReducer, favorites: persistedFavoritesReducer, diff --git a/web/src/shared/stores/toastSlice.ts b/web/src/shared/stores/toastSlice.ts index 3567e23..0fb3cbf 100644 --- a/web/src/shared/stores/toastSlice.ts +++ b/web/src/shared/stores/toastSlice.ts @@ -29,6 +29,7 @@ const initialState: ToastState = { }; export interface AddToastPayload { + id?: string; type?: Toast['type']; title?: string; message: string; @@ -50,7 +51,7 @@ const toastSlice = createSlice({ reducers: { addToast: (state, action: PayloadAction) => { const toast: Toast = { - id: `toast_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + id: action.payload.id ?? `toast_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`, type: action.payload.type || 'default', title: action.payload.title, message: action.payload.message, diff --git a/web/src/widgets/category-management/ui/CategoryForm.tsx b/web/src/widgets/category-management/ui/CategoryForm.tsx index fbc2f2a..80ca5cf 100644 --- a/web/src/widgets/category-management/ui/CategoryForm.tsx +++ b/web/src/widgets/category-management/ui/CategoryForm.tsx @@ -245,7 +245,6 @@ export function CategoryForm({ // TODO: Handle image upload // For now, we'll skip image upload as it requires a separate endpoint if (categoryFileData?.file) { - console.warn('Image upload not yet implemented'); info(t('form.imageUploadNotice'), { title: t('form.noteTitle') }); diff --git a/web/src/widgets/category-management/ui/CategoryManagement.tsx b/web/src/widgets/category-management/ui/CategoryManagement.tsx index 88e7299..e334a3c 100644 --- a/web/src/widgets/category-management/ui/CategoryManagement.tsx +++ b/web/src/widgets/category-management/ui/CategoryManagement.tsx @@ -5,6 +5,7 @@ import { useTranslations } from 'next-intl'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/shared/ui/dialog'; import { EntityManagement } from '@/shared/ui'; import { useToastActions } from '@/shared/hooks/useToast'; +import { usePopupActions } from '@/widgets/feedback-system'; import { getCategories, deleteCategory } from '@/entities/category/api/categoryApi'; import type { Category } from '@/entities/category/model/category'; import { CategoryForm } from './CategoryForm'; @@ -28,6 +29,7 @@ export function CategoryManagement({ const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [editingCategory, setEditingCategory] = useState(null); + const { confirmDelete } = usePopupActions(); const loadCategories = useCallback(async () => { try { @@ -69,7 +71,8 @@ export function CategoryManagement({ }, [triggerRefresh, loadCategories]); const handleDeleteCategory = async (category: Category) => { - if (!confirm(t('deleteConfirm', { title: category.title }))) { + const isConfirmed = await confirmDelete(t('deleteConfirm', { title: category.title })); + if (!isConfirmed) { return; } diff --git a/web/src/widgets/category/ui/CategoriesHoverPopup.tsx b/web/src/widgets/category/ui/CategoriesHoverPopup.tsx index 077061e..1507907 100644 --- a/web/src/widgets/category/ui/CategoriesHoverPopup.tsx +++ b/web/src/widgets/category/ui/CategoriesHoverPopup.tsx @@ -67,8 +67,7 @@ export function CategoriesHoverPopup({ setIsLoading(true); getCategories({ parent: 0, locale, status: 1, include_tree: true }) .then(setCategories) - .catch((error) => { - console.warn('Failed to load categories for hover popup:', error); + .catch(() => { setCategories([]); }) .finally(() => setIsLoading(false)); diff --git a/web/src/widgets/contact-form-sidebar/index.ts b/web/src/widgets/contact-form-sidebar/index.ts deleted file mode 100644 index c876e52..0000000 --- a/web/src/widgets/contact-form-sidebar/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Contact form sidebar widget public API -export { default as ContactFormSidebar } from './ui/ContactFormSidebar'; \ No newline at end of file diff --git a/web/src/widgets/contact-form-sidebar/ui/ContactFormSidebar.tsx b/web/src/widgets/contact-form-sidebar/ui/ContactFormSidebar.tsx deleted file mode 100644 index c0d0c75..0000000 --- a/web/src/widgets/contact-form-sidebar/ui/ContactFormSidebar.tsx +++ /dev/null @@ -1,95 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { useTranslations } from 'next-intl'; -import { SidebarCard } from '@/shared/ui/sidebar-card'; -import { Input } from '@/shared/ui/input'; -import { Textarea } from '@/shared/ui/textarea'; -import { IconButton } from '@/shared/ui/icon-button'; -import { - MailIcon, - SendIcon -} from '@/shared/ui/icons'; - -interface ContactFormSidebarProps { - className?: string; -} - -export default function ContactFormSidebar({ className }: ContactFormSidebarProps) { - const t = useTranslations('contact'); - const [email, setEmail] = useState(''); - const [message, setMessage] = useState(''); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - // Handle form submission - console.log('Contact form submitted:', { email, message }); - // Reset form - setEmail(''); - setMessage(''); - }; - - return ( - } - className={className} - contentSpacing="sm" - > -
- -
-
- - setEmail(e.target.value)} - required - /> -
- -
- -