From 6fe6ed28272694c8e71d1a9cbf07858e3bea964f Mon Sep 17 00:00:00 2001 From: Wolf Date: Wed, 15 Apr 2026 02:22:02 +0400 Subject: [PATCH] feat(vibesuite): add Knowledge Gaps map, expand skills to 61, fix modals & UX - Add Knowledge Gaps feature: horizontal flow-map of 10 stages (AI SaaS build path), hover tooltips with learned/not-learned skill tiles, click-to-open skill detail - Add 10 new skills across existing categories (EN + RU): AI agents, PWA, i18n, real-time, search, SEO, error monitoring, Slack/Discord bots, Zapier - Fix scrollbar layout shift on modal open (scrollbar-gutter on html, not body) - Fix sidebar not dimmed behind modals (portal ProgressHeader modal to document.body) - Fix SkillDetailPanel vertical jump (pin top with align-items: flex-start) - Add smooth open/close animations to Modal component (all modals project-wide) - Add hover/active states to Settings modal Cancel/Save buttons (light + dark themes) - Add hover/active states to CategoryNav category buttons - Move My Progress button from header bar into RecommendationModal - Add ESC key support for Knowledge Gaps and RecommendationModal - Remove .env.example (secrets now managed separately) Co-Authored-By: Claude Opus 4.6 (1M context) --- .env.example | 21 -- src/api/vibesuite.ts | 8 +- src/components/Modal/Modal.module.scss | 50 +++ src/components/Modal/Modal.tsx | 24 +- .../SettingsModal/SettingsModal.module.scss | 46 ++- .../CategoryNav/CategoryNav.module.scss | 57 ++++ .../vibesuite/CategoryNav/CategoryNav.tsx | 5 + .../CategoryNav/CategoryNav.types.ts | 1 + .../KnowledgeGapsMap.module.scss | 306 ++++++++++++++++++ .../KnowledgeGapsMap/KnowledgeGapsMap.tsx | 166 ++++++++++ .../vibesuite/KnowledgeGapsMap/index.ts | 3 + .../vibesuite/MapClient/MapClient.tsx | 21 +- .../ProgressHeader/ProgressHeader.module.scss | 2 +- .../ProgressHeader/ProgressHeader.tsx | 254 ++++++++------- .../ProgressHeader/ProgressHeader.types.ts | 2 + .../RecommendationModal.module.scss | 50 ++- .../RecommendationModal.tsx | 14 + .../RecommendationModal.types.ts | 1 + .../SkillDetailPanel.module.scss | 3 +- src/data/vibesuite/intl/en.ts | 16 + src/data/vibesuite/intl/ru.ts | 15 + src/data/vibesuite/intl/skills.ru.ts | 84 +++++ src/data/vibesuite/knowledgeGapsStages.ts | 105 ++++++ src/data/vibesuite/skills.ts | 157 ++++++++- src/styles/globals.scss | 2 +- 25 files changed, 1234 insertions(+), 179 deletions(-) delete mode 100644 .env.example create mode 100644 src/components/vibesuite/KnowledgeGapsMap/KnowledgeGapsMap.module.scss create mode 100644 src/components/vibesuite/KnowledgeGapsMap/KnowledgeGapsMap.tsx create mode 100644 src/components/vibesuite/KnowledgeGapsMap/index.ts create mode 100644 src/data/vibesuite/knowledgeGapsStages.ts diff --git a/.env.example b/.env.example deleted file mode 100644 index f0f7bfc..0000000 --- a/.env.example +++ /dev/null @@ -1,21 +0,0 @@ -NEXT_PUBLIC_ENV= -NEXT_PUBLIC_INDEXING= -NEXT_PUBLIC_MIXPANEL_TOKEN= -NEXT_PUBLIC_DOMAIN= -NEXT_PUBLIC_API_KEY= -NEXT_PUBLIC_STRAPI= -NEXTAUTH_SECRET= -NEXT_PUBLIC_UXCAT_API= -NEXTAUTH_URL= -NEXT_PUBLIC_GA_MEASUREMENT_ID= - -STRAPI_URL= - -GOOGLE_CLIENT_ID= -GOOGLE_CLIENT_SECRET= - -LINKEDIN_CLIENT_ID= -LINKEDIN_CLIENT_SECRET= - -DISCORD_CLIENT_ID= -DISCORD_CLIENT_SECRET= diff --git a/src/api/vibesuite.ts b/src/api/vibesuite.ts index 1d475e0..835d4ca 100644 --- a/src/api/vibesuite.ts +++ b/src/api/vibesuite.ts @@ -1,8 +1,12 @@ export const getVibesuite = async (locale: string) => { - const url = `${process.env.NEXT_PUBLIC_STRAPI}/api/vibesuite?populate[pageSeo]=*&populate[OGTags][populate]=ogImage&locale=${locale}`; + const base = process.env.NEXT_PUBLIC_STRAPI; + if (!base) return null; + + const url = `${base}/api/vibesuite?populate[pageSeo]=*&populate[OGTags][populate]=ogImage&locale=${locale}`; return await fetch(url) .then(resp => resp.json()) - .then(json => json?.data?.attributes || null); + .then(json => json?.data?.attributes || null) + .catch(() => null); }; export const updateLearnedSkills = async (learnedSkills: string[]) => { diff --git a/src/components/Modal/Modal.module.scss b/src/components/Modal/Modal.module.scss index 379d92c..2a449e9 100644 --- a/src/components/Modal/Modal.module.scss +++ b/src/components/Modal/Modal.module.scss @@ -11,8 +11,18 @@ align-items: center; justify-content: center; z-index: 200; + animation: overlayFadeIn 0.2s ease-out; + + &.overlayClosing { + animation: overlayFadeOut 0.18s ease-in forwards; + } + + .wrapperClosing { + animation: wrapperOut 0.18s ease-in forwards; + } .wrapper { + animation: wrapperIn 0.2s ease-out; display: flex; width: 100%; background-color: #fff; @@ -236,3 +246,43 @@ } } } + +@keyframes overlayFadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes overlayFadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +@keyframes wrapperIn { + from { + opacity: 0; + transform: scale(0.97) translateY(8px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +@keyframes wrapperOut { + from { + opacity: 1; + transform: scale(1) translateY(0); + } + to { + opacity: 0; + transform: scale(0.97) translateY(8px); + } +} diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 2150907..79b7791 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -1,6 +1,12 @@ import cn from 'classnames'; import Image from 'next/image'; -import React, { FC, KeyboardEvent, ReactNode, useEffect } from 'react'; +import React, { + FC, + KeyboardEvent, + ReactNode, + useEffect, + useState, +} from 'react'; import { createPortal } from 'react-dom'; import styles from './Modal.module.scss'; @@ -52,8 +58,14 @@ const Modal: FC = ({ backgroundImageUrl, hasHr, }) => { + const [closing, setClosing] = useState(false); + const handleClose = () => { - onClick(); + setClosing(true); + setTimeout(() => { + setClosing(false); + onClick(); + }, 180); }; useEffect(() => { @@ -70,10 +82,10 @@ const Modal: FC = ({ if (!close) { document.documentElement.style.overflowY = 'hidden'; - document.body.classList.add('hide-body-move'); + document.documentElement.classList.add('hide-body-move'); } else { document.documentElement.style.overflowY = overflowDefaultValue; - document.body.classList.remove('hide-body-move'); + document.documentElement.classList.remove('hide-body-move'); } // @ts-ignore @@ -81,7 +93,7 @@ const Modal: FC = ({ return () => { document.documentElement.style.overflowY = overflowDefaultValue; - document.body.classList.remove('hide-body-move'); + document.documentElement.classList.remove('hide-body-move'); // @ts-ignore document.removeEventListener('keydown', handleKeyDown); }; @@ -92,6 +104,7 @@ const Modal: FC = ({
@@ -109,6 +122,7 @@ const Modal: FC = ({ [styles.fullSizeMobile]: fullSizeMobile, [styles.isLongevityProtocolModal]: isLongevityProtocolModal, [styles.withBackgroundImage]: backgroundImageUrl, + [styles.wrapperClosing]: closing, })} style={{ backgroundImage: backgroundImageUrl diff --git a/src/components/SettingsModal/SettingsModal.module.scss b/src/components/SettingsModal/SettingsModal.module.scss index 73bfa55..f309469 100644 --- a/src/components/SettingsModal/SettingsModal.module.scss +++ b/src/components/SettingsModal/SettingsModal.module.scss @@ -39,6 +39,19 @@ line-height: 1.4; height: 40px !important; padding: 9px 16px !important; + transition: + background-color 0.15s ease, + color 0.15s ease; + + &:hover { + background-color: #242424 !important; + color: #fff !important; + } + + &:active { + background-color: #111 !important; + color: #fff !important; + } } .SaveBtn { @@ -51,11 +64,18 @@ line-height: 1.4; height: 40px !important; padding: 6px 23px; + transition: + background-color 0.15s ease, + opacity 0.15s ease; &:hover { - background-color: #242424 !important; - border-color: #242424 !important; - color: #fff !important; + background-color: #111 !important; + border-color: #111 !important; + } + + &:active { + background-color: #000 !important; + border-color: #000 !important; } } @@ -98,6 +118,16 @@ .CancelBtn { border-color: #fff !important; color: #fff !important; + + &:hover { + background-color: #fff !important; + color: rgba(0, 0, 0, 0.85) !important; + } + + &:active { + background-color: #ddd !important; + color: rgba(0, 0, 0, 0.85) !important; + } } .SaveBtn { @@ -106,9 +136,13 @@ color: rgba(0, 0, 0, 0.85) !important; &:hover { - background-color: #fff !important; - border-color: #fff !important; - color: rgba(0, 0, 0, 0.85) !important; + background-color: #e8e8e8 !important; + border-color: #e8e8e8 !important; + } + + &:active { + background-color: #ddd !important; + border-color: #ddd !important; } } } diff --git a/src/components/vibesuite/CategoryNav/CategoryNav.module.scss b/src/components/vibesuite/CategoryNav/CategoryNav.module.scss index 56a2ca7..25fdfbf 100644 --- a/src/components/vibesuite/CategoryNav/CategoryNav.module.scss +++ b/src/components/vibesuite/CategoryNav/CategoryNav.module.scss @@ -68,6 +68,45 @@ color: var(--text-tertiary); } +.knowledgeGapsBtn { + display: block; + width: calc(100% - 3.5rem); + margin: 0 1.75rem 1.25rem; + padding: 0.6rem 0.85rem; + font-family: var(--font-ui); + font-size: 12px; + font-weight: 500; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--accent); + background: var(--bg-card); + border: 1px solid var(--accent); + cursor: pointer; + text-align: center; + transition: + background 0.15s ease, + color 0.15s ease; + animation: knowledgeGapsPulse 4s ease-in-out infinite; + + &:hover { + background: var(--accent); + color: #fff; + animation: none; + } +} + +@keyframes knowledgeGapsPulse { + 0%, + 100% { + border-color: var(--accent); + opacity: 1; + } + 50% { + border-color: var(--border); + opacity: 0.65; + } +} + .allCategoriesBtn { display: block; width: 100%; @@ -84,6 +123,15 @@ transition: all 0.15s ease; margin-bottom: 0.25rem; + &:hover:not(.active) { + color: var(--text-primary); + background: var(--bg-card-active); + } + + &:active:not(.active) { + border-left-color: var(--border-strong); + } + &.active { font-weight: 500; color: var(--accent); @@ -120,6 +168,15 @@ transition: all 0.15s ease; margin-bottom: 0.125rem; + &:hover:not(.active) { + color: var(--text-primary); + background: var(--bg-card-active); + } + + &:active:not(.active) { + border-left-color: var(--border-strong); + } + &.active { color: var(--accent); background: var(--bg-card-active); diff --git a/src/components/vibesuite/CategoryNav/CategoryNav.tsx b/src/components/vibesuite/CategoryNav/CategoryNav.tsx index 9176f18..cb6071b 100644 --- a/src/components/vibesuite/CategoryNav/CategoryNav.tsx +++ b/src/components/vibesuite/CategoryNav/CategoryNav.tsx @@ -20,6 +20,7 @@ export default function CategoryNav({ onSelectCategory, onOpenRecommendations, onOpenWhyModal, + onOpenKnowledgeGaps, allCompleted, }: CategoryNavProps) { const { locale } = useRouter() as TRouter; @@ -45,6 +46,10 @@ export default function CategoryNav({ )} + +
  • +
+ +
+ {stages.map((stage, i) => { + const stageName = t[stageKeys[stage.id]] || stage.id; + const hasAny = stage.done > 0; + const isHovered = hoveredStage === stage.id; + + return ( +
setHoveredStage(stage.id)} + onMouseLeave={() => setHoveredStage(null)} + > +
+ {i > 0 &&
} +
+ {hasAny && } +
+ {i < stages.length - 1 && ( +
+ )} +
+ {stageName} + + {isHovered && ( +
+ {stage.skills.map(skill => { + const learned = !!progress[skill.id]?.completed; + return ( + + ); + })} +
+ )} +
+ ); + })} +
+
+
, + document.body, + ); +}; + +export default KnowledgeGapsMap; diff --git a/src/components/vibesuite/KnowledgeGapsMap/index.ts b/src/components/vibesuite/KnowledgeGapsMap/index.ts new file mode 100644 index 0000000..bd5a156 --- /dev/null +++ b/src/components/vibesuite/KnowledgeGapsMap/index.ts @@ -0,0 +1,3 @@ +import KnowledgeGapsMap from './KnowledgeGapsMap'; + +export default KnowledgeGapsMap; diff --git a/src/components/vibesuite/MapClient/MapClient.tsx b/src/components/vibesuite/MapClient/MapClient.tsx index 05173d7..65deef8 100644 --- a/src/components/vibesuite/MapClient/MapClient.tsx +++ b/src/components/vibesuite/MapClient/MapClient.tsx @@ -31,6 +31,7 @@ import Heading from '@components/Heading'; import LogIn from '@components/LogIn'; import CategoryIcon from '@components/vibesuite/CategoryIcons'; import CategoryNav from '@components/vibesuite/CategoryNav'; +import KnowledgeGapsMap from '@components/vibesuite/KnowledgeGapsMap'; import ProgressHeader from '@components/vibesuite/ProgressHeader'; import RecommendationModal from '@components/vibesuite/RecommendationModal'; import SkillCard from '@components/vibesuite/SkillCard'; @@ -56,6 +57,8 @@ export default function MapClient({ const [whyClosing, setWhyClosing] = useState(false); const [showGuideModal, setShowGuideModal] = useState(false); const [guideClosing, setGuideClosing] = useState(false); + const [showKnowledgeGaps, setShowKnowledgeGaps] = useState(false); + const [triggerProgress, setTriggerProgress] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [showFilter, setShowFilter] = useState< 'all' | 'learned' | 'not-learned' @@ -213,7 +216,11 @@ export default function MapClient({ return (
- + setTriggerProgress(false)} + />
setShowKnowledgeGaps(true)} allCompleted={allCompleted} />
@@ -452,6 +460,7 @@ export default function MapClient({ handleSelectSkill(skillId); }} onClose={() => setShowRecommendations(false)} + onOpenProgress={() => setTriggerProgress(true)} /> )} @@ -608,6 +617,16 @@ export default function MapClient({
)} {showLogin && } + {showKnowledgeGaps && ( + setShowKnowledgeGaps(false)} + onSelectSkill={skillId => { + setShowKnowledgeGaps(false); + setSelectedSkillId(skillId); + }} + /> + )} ); } diff --git a/src/components/vibesuite/ProgressHeader/ProgressHeader.module.scss b/src/components/vibesuite/ProgressHeader/ProgressHeader.module.scss index c12a685..36d9543 100644 --- a/src/components/vibesuite/ProgressHeader/ProgressHeader.module.scss +++ b/src/components/vibesuite/ProgressHeader/ProgressHeader.module.scss @@ -141,7 +141,7 @@ .modalBackdrop { position: fixed; inset: 0; - z-index: 60; + z-index: 200; background: rgba(0, 0, 0, 0.3); display: flex; align-items: center; diff --git a/src/components/vibesuite/ProgressHeader/ProgressHeader.tsx b/src/components/vibesuite/ProgressHeader/ProgressHeader.tsx index 944795e..d4dde1d 100644 --- a/src/components/vibesuite/ProgressHeader/ProgressHeader.tsx +++ b/src/components/vibesuite/ProgressHeader/ProgressHeader.tsx @@ -1,6 +1,7 @@ import cn from 'classnames'; import { useRouter } from 'next/router'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; import type { TRouter } from '@local-types/global'; @@ -21,7 +22,11 @@ const MILESTONE_KEYS = [ const HIT_RADIUS = 24; -export default function ProgressHeader({ progress }: ProgressHeaderProps) { +export default function ProgressHeader({ + progress, + externalShowProgress, + onProgressShown, +}: ProgressHeaderProps) { const { locale } = useRouter() as TRouter; const t = vibesuiteIntl[locale]; @@ -41,6 +46,15 @@ export default function ProgressHeader({ progress }: ProgressHeaderProps) { const [showProgressModal, setShowProgressModal] = useState(false); const [progressClosing, setProgressClosing] = useState(false); const [copied, setCopied] = useState(false); + useEffect(() => { + if (externalShowProgress) { + setProgressClosing(false); + setCopied(false); + setShowProgressModal(true); + onProgressShown?.(); + } + }, [externalShowProgress]); + const canvasRef = useRef(null); const animRef = useRef(0); const timeRef = useRef(0); @@ -318,138 +332,130 @@ export default function ProgressHeader({ progress }: ProgressHeaderProps) { }, [hideTooltip]); return ( -
-
- vibecode -
- -
- - {tooltipData && ( + <> +
+
+ vibecode +
+ +
+ + {tooltipData && ( +
+ {tooltipData.text} +
+ )} +
+ +
+ + {completed} + / {total} + ({Math.round(pct)}%) + +
+
+ {showProgressModal && + createPortal(
{ + setProgressClosing(true); + setTimeout(() => { + setShowProgressModal(false); + setProgressClosing(false); + }, 180); }} > - {tooltipData.text} -
- )} -
- -
- - {completed} - / {total} - ({Math.round(pct)}%) - - - -
- - {showProgressModal && ( -
{ - setProgressClosing(true); - setTimeout(() => { - setShowProgressModal(false); - setProgressClosing(false); - }, 180); - }} - > -
e.stopPropagation()} - > -
-

{t.myProgress}

-

{t.progressBody1}

-

{t.progressBody2}

- -
- - + lines.push('---'); + lines.push(t.copyStateEnd); + navigator.clipboard.writeText(lines.join('\n')); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }} + > + {copied ? t.copied : t.copyMyLearningState} + + +
-
- - )} -
+ , + document.body, + )} + ); } diff --git a/src/components/vibesuite/ProgressHeader/ProgressHeader.types.ts b/src/components/vibesuite/ProgressHeader/ProgressHeader.types.ts index f6572fd..8e98be2 100644 --- a/src/components/vibesuite/ProgressHeader/ProgressHeader.types.ts +++ b/src/components/vibesuite/ProgressHeader/ProgressHeader.types.ts @@ -2,4 +2,6 @@ import { UserProgress } from '@local-types/pageTypes/vibesuite'; export type ProgressHeaderProps = { progress: UserProgress; + externalShowProgress?: boolean; + onProgressShown?: () => void; }; diff --git a/src/components/vibesuite/RecommendationModal/RecommendationModal.module.scss b/src/components/vibesuite/RecommendationModal/RecommendationModal.module.scss index eef4946..79d8ca5 100644 --- a/src/components/vibesuite/RecommendationModal/RecommendationModal.module.scss +++ b/src/components/vibesuite/RecommendationModal/RecommendationModal.module.scss @@ -6,10 +6,28 @@ align-items: center; justify-content: center; background: rgba(28, 28, 26, 0.35); - transition: background 0.2s ease; + animation: backdropIn 0.2s ease-out; &.closing { - background: rgba(28, 28, 26, 0); + animation: backdropOut 0.18s ease-in forwards; + } +} + +@keyframes backdropIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes backdropOut { + from { + opacity: 1; + } + to { + opacity: 0; } } @@ -148,3 +166,31 @@ color: var(--accent); padding-left: 1.35rem; } + +.progressBtn { + display: block; + width: 100%; + margin-top: 1.25rem; + padding: 0.6rem 0; + font-family: var(--font-ui); + font-size: 12px; + font-weight: 500; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--text-tertiary); + background: none; + border: 1px solid var(--border); + cursor: pointer; + transition: + border-color 0.15s, + color 0.15s; + + &:hover { + border-color: var(--accent); + color: var(--accent); + } + + &:active { + color: var(--text-primary); + } +} diff --git a/src/components/vibesuite/RecommendationModal/RecommendationModal.tsx b/src/components/vibesuite/RecommendationModal/RecommendationModal.tsx index 03c90cf..e470693 100644 --- a/src/components/vibesuite/RecommendationModal/RecommendationModal.tsx +++ b/src/components/vibesuite/RecommendationModal/RecommendationModal.tsx @@ -58,6 +58,7 @@ export default function RecommendationModal({ recommendations, onSelectSkill, onClose, + onOpenProgress, }: RecommendationModalProps) { const { locale } = useRouter() as TRouter; const t = vibesuiteIntl[locale]; @@ -173,6 +174,19 @@ export default function RecommendationModal({ ); })} + + ); diff --git a/src/components/vibesuite/RecommendationModal/RecommendationModal.types.ts b/src/components/vibesuite/RecommendationModal/RecommendationModal.types.ts index b6a2fcb..4442b88 100644 --- a/src/components/vibesuite/RecommendationModal/RecommendationModal.types.ts +++ b/src/components/vibesuite/RecommendationModal/RecommendationModal.types.ts @@ -4,4 +4,5 @@ export type RecommendationModalProps = { recommendations: Recommendation[]; onSelectSkill: (skillId: string) => void; onClose: () => void; + onOpenProgress: () => void; }; diff --git a/src/components/vibesuite/SkillDetailPanel/SkillDetailPanel.module.scss b/src/components/vibesuite/SkillDetailPanel/SkillDetailPanel.module.scss index 7ce6c1c..9731e66 100644 --- a/src/components/vibesuite/SkillDetailPanel/SkillDetailPanel.module.scss +++ b/src/components/vibesuite/SkillDetailPanel/SkillDetailPanel.module.scss @@ -8,8 +8,9 @@ right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.35); - align-items: center; + align-items: flex-start; justify-content: center; + padding-top: 7.5vh; z-index: 9999; @media (max-width: 1023px) { diff --git a/src/data/vibesuite/intl/en.ts b/src/data/vibesuite/intl/en.ts index 457211d..7df271f 100644 --- a/src/data/vibesuite/intl/en.ts +++ b/src/data/vibesuite/intl/en.ts @@ -98,6 +98,22 @@ const en = { unmarkAsLearned: 'Unmark as Learned', markAsLearned: 'Mark as Learned', + // Knowledge Gaps + knowledgeGaps: 'Your Knowledge Gaps', + knowledgeGapsTitle: 'AI SaaS Platform — Full Build Path', + knowledgeGapsSubtitle: + 'Your knowledge mapped across a real project lifecycle', + stageTooling: 'Tooling Setup', + stagePrototype: 'Prototype', + stageBackend: 'Backend Foundation', + stageAiCore: 'AI Core', + stageFrontendPolish: 'Frontend Polish', + stageInfrastructure: 'Infrastructure', + stageMonetization: 'Monetization & Growth', + stageIntegrations: 'Integrations', + stageSecurity: 'Security Hardening', + stageScale: 'Scale & Optimize', + // RecommendationModal whatToLearnNextTitle: 'What to learn next', diff --git a/src/data/vibesuite/intl/ru.ts b/src/data/vibesuite/intl/ru.ts index 0a2bc43..9e4adeb 100644 --- a/src/data/vibesuite/intl/ru.ts +++ b/src/data/vibesuite/intl/ru.ts @@ -99,6 +99,21 @@ const ru = { unmarkAsLearned: 'Снять отметку', markAsLearned: 'Отметить как изученный', + // Knowledge Gaps + knowledgeGaps: 'Ваши пробелы', + knowledgeGapsTitle: 'AI SaaS платформа — полный путь разработки', + knowledgeGapsSubtitle: 'Ваши знания на карте реального проекта', + stageTooling: 'Настройка инструментов', + stagePrototype: 'Прототип', + stageBackend: 'Основа бэкенда', + stageAiCore: 'AI-ядро', + stageFrontendPolish: 'Финиш фронтенда', + stageInfrastructure: 'Инфраструктура', + stageMonetization: 'Монетизация и рост', + stageIntegrations: 'Интеграции', + stageSecurity: 'Безопасность', + stageScale: 'Масштаб и оптимизация', + // RecommendationModal whatToLearnNextTitle: 'Что изучить дальше', diff --git a/src/data/vibesuite/intl/skills.ru.ts b/src/data/vibesuite/intl/skills.ru.ts index 54ed965..b5753b8 100644 --- a/src/data/vibesuite/intl/skills.ru.ts +++ b/src/data/vibesuite/intl/skills.ru.ts @@ -125,6 +125,15 @@ export const skillsRu: Record< 'Простые вопросы идут к дешёвой модели, сложные — к мощной. Научитесь работать с несколькими AI-провайдерами одновременно, маршрутизации запросов и оптимизации затрат.', timeEstimate: '1–2 дня', }, + 'ai-agents-workflows': { + id: 'ai-agents-workflows', + name: 'AI-агенты — многошаговые воркфлоу', + projectTitle: + 'Создайте AI-агента, который разбивает задачи и выполняет их пошагово', + projectDescription: + 'Вместо «один промпт → один ответ» агент планирует, выполняет шаги, проверяет результат и корректирует. Создайте исследовательского агента, который ищет, резюмирует и составляет отчёт. Научитесь агентным циклам, оркестрации инструментов, памяти между шагами и критериям остановки.', + timeEstimate: '1–2 дня', + }, // ─── Local AI Models ─── 'ollama-local': { @@ -245,6 +254,23 @@ export const skillsRu: Record< 'Например, карту с метками на Mapbox или масштабируемый график на D3. Научитесь canvas/SVG, обработке пользовательских жестов и визуальному отображению данных.', timeEstimate: '1–2 дня', }, + 'pwa-mobile': { + id: 'pwa-mobile', + name: 'PWA — мобильное приложение из сайта', + projectTitle: + 'Превратите сайт в устанавливаемое приложение с офлайн-режимом', + projectDescription: + 'Пользователи смогут установить ваш сайт на домашний экран телефона и пользоваться им без интернета. Добавьте service worker, манифест приложения, push-уведомления и мобильные паттерны. Научитесь стандарту PWA, стратегиям кэширования и тому, как сделать веб похожим на нативное приложение.', + timeEstimate: '3–4 часа', + }, + 'i18n-localization': { + id: 'i18n-localization', + name: 'i18n — мультиязычность', + projectTitle: 'Сделайте сайт доступным на нескольких языках', + projectDescription: + 'Добавьте переключение языков, переведите UI-строки, обработайте RTL-раскладки и форматирование дат и чисел для разных локалей. Научитесь i18n-маршрутизации в Next.js, структуре файлов переводов и доставке контента с учётом локали.', + timeEstimate: '3–4 часа', + }, // ─── Backend & Databases ─── 'api-routes-first': { @@ -303,6 +329,23 @@ export const skillsRu: Record< 'Отправляйте еженедельный дайджест, очищайте старые данные каждую ночь или проверяйте API каждый час — без нажатия кнопки. Научитесь Vercel Cron, GitHub Actions по расписанию и написанию надёжных фоновых задач.', timeEstimate: '2–3 часа', }, + 'realtime-websockets': { + id: 'realtime-websockets', + name: 'Реалтайм — WebSockets и живые обновления', + projectTitle: 'Создайте живой чат или ленту уведомлений в реальном времени', + projectDescription: + 'Сообщения появляются мгновенно без перезагрузки страницы. Создайте чат-комнату, живые комментарии или совместный документ. Научитесь WebSockets, Supabase Realtime или Pusher, индикаторам присутствия и обработке разрывов соединения.', + timeEstimate: '3–4 часа', + }, + 'fulltext-search': { + id: 'fulltext-search', + name: 'Поиск — полнотекстовый и векторный', + projectTitle: + 'Добавьте мгновенный поиск — по ключевым словам и с помощью AI', + projectDescription: + 'Создайте поисковую строку, которая находит результаты по мере набора текста. Начните с полнотекстового поиска в PostgreSQL, затем добавьте AI-семантический поиск с векторными эмбеддингами. Научитесь поисковому индексированию, ранжированию, debounce-паттернам и выбору между keyword- и vector-поиском.', + timeEstimate: '3–4 часа', + }, // ─── Auth & Security ─── 'nextauth-google-login': { @@ -381,6 +424,22 @@ export const skillsRu: Record< 'Купите домен, настройте DNS, включите CDN и SSL. Научитесь работе DNS, что такое CDN и как защитить сайт от DDoS.', timeEstimate: '1 час', }, + 'seo-meta-tags': { + id: 'seo-meta-tags', + name: 'SEO — мета-теги и Open Graph', + projectTitle: 'Сделайте сайт красивым в Google и превью соцсетей', + projectDescription: + 'Добавьте мета-теги, Open Graph изображения, структурированные данные, sitemap и robots.txt. Когда кто-то делится вашей ссылкой в Twitter или LinkedIn — показывается красивое превью вместо пустой карточки. Научитесь техническому SEO, компоненту Head в Next.js и тому, как поисковики индексируют страницы.', + timeEstimate: '2–3 часа', + }, + 'error-monitoring': { + id: 'error-monitoring', + name: 'Мониторинг ошибок — Sentry', + projectTitle: 'Узнавайте о падениях приложения раньше пользователей', + projectDescription: + 'Настройте Sentry для отлова ошибок в продакшене, получайте оповещения в Slack/email и видите, какая именно строка кода упала и почему. Научитесь трекингу ошибок, source maps, мониторингу производительности и триажу багов.', + timeEstimate: '1–2 часа', + }, // ─── Payments & Monetization ─── 'stripe-payments': { @@ -451,6 +510,31 @@ export const skillsRu: Record< 'Пишите в Notion — сайт обновляется автоматически. Идеально для блогов или документации. Научитесь Notion API, маппингу блоков Notion в HTML и ISR.', timeEstimate: '3–4 часа', }, + 'slack-bot': { + id: 'slack-bot', + name: 'Slack-бот', + projectTitle: 'Создайте Slack-бот для команды', + projectDescription: + 'Создайте бота, который отвечает на команды, публикует обновления или выполняет AI-запросы прямо в Slack. Научитесь Slack Bolt SDK, слэш-командам, интерактивным сообщениям и OAuth-установке приложений.', + timeEstimate: '3–4 часа', + }, + 'discord-bot': { + id: 'discord-bot', + name: 'Discord-бот', + projectTitle: 'Создайте Discord-бот для сообщества', + projectDescription: + 'Бот, который модерирует, отвечает на вопросы или выполняет команды в вашем Discord-сервере. Научитесь Discord.js, слэш-командам, эмбедам и хостингу бота.', + timeEstimate: '2–3 часа', + }, + 'zapier-make-automation': { + id: 'zapier-make-automation', + name: 'Zapier / Make — автоматизация без кода', + projectTitle: + 'Подключите приложение к 5000+ сервисам без написания интеграций', + projectDescription: + 'Новый пользователь зарегистрировался → добавить в Mailchimp, написать в Slack, создать страницу в Notion — всё автоматически. Научитесь выставлять вебхуки из приложения, запускать внешние воркфлоу и выбирать между no-code автоматизацией и кастомным кодом.', + timeEstimate: '1–2 часа', + }, // ─── AI Tools for Vibe Coding ─── 'claude-code-tool': { diff --git a/src/data/vibesuite/knowledgeGapsStages.ts b/src/data/vibesuite/knowledgeGapsStages.ts new file mode 100644 index 0000000..b79ffc2 --- /dev/null +++ b/src/data/vibesuite/knowledgeGapsStages.ts @@ -0,0 +1,105 @@ +/** Stages of building an AI SaaS Platform, mapped to skill IDs. */ + +export interface KnowledgeGapsStage { + id: string; + skillIds: string[]; +} + +export const knowledgeGapsStages: KnowledgeGapsStage[] = [ + { + id: 'tooling', + skillIds: [ + 'claude-code-tool', + 'cursor-windsurf', + 'claude-projects', + 'v0-dev-ai-ui', + ], + }, + { + id: 'prototype', + skillIds: [ + 'react-nextjs-portfolio', + 'tailwind-styling', + 'shadcn-ui-dashboard', + ], + }, + { + id: 'backend', + skillIds: [ + 'api-routes-first', + 'supabase-crud', + 'file-storage-uploads', + 'nextauth-google-login', + 'magic-link-auth', + ], + }, + { + id: 'ai-core', + skillIds: [ + 'claude-api-chatbot', + 'openai-api-content', + 'prompt-engineering-advisor', + 'streaming-responses', + 'rag-chat-documents', + 'ai-function-calling', + 'ai-agents-workflows', + ], + }, + { + id: 'frontend-polish', + skillIds: [ + 'framer-motion-animations', + 'i18n-localization', + 'pwa-mobile', + 'interactive-visualizations', + ], + }, + { + id: 'infrastructure', + skillIds: [ + 'vercel-first-deploy', + 'cloudflare-domain-cdn', + 'github-actions-cicd', + 'seo-meta-tags', + 'error-monitoring', + 'analytics-know-users', + ], + }, + { + id: 'monetization', + skillIds: [ + 'stripe-payments', + 'subscriptions', + 'prepaid-credits', + 'coinbase-crypto', + 'email-resend', + ], + }, + { + id: 'integrations', + skillIds: [ + 'webhooks-events', + 'cron-scheduled-tasks', + 'telegram-bot', + 'slack-bot', + 'zapier-make-automation', + 'notion-api', + 'google-sheets-api', + ], + }, + { + id: 'security', + skillIds: ['api-keys-rate-limits', 'row-level-security'], + }, + { + id: 'scale', + skillIds: [ + 'realtime-websockets', + 'fulltext-search', + 'redis-vercel-kv-cache', + 'neon-serverless-pg', + 'multi-model-routing', + 'vibe-coding-method', + ], + }, +]; diff --git a/src/data/vibesuite/skills.ts b/src/data/vibesuite/skills.ts index 3e2ec98..b48dc06 100644 --- a/src/data/vibesuite/skills.ts +++ b/src/data/vibesuite/skills.ts @@ -42,7 +42,8 @@ export const categories: SkillCategory[] = [ { id: 'streaming-responses', name: 'Streaming — Live AI Response', - projectTitle: 'Add streaming to your chatbot — text appears letter by letter like ChatGPT', + projectTitle: + 'Add streaming to your chatbot — text appears letter by letter like ChatGPT', projectDescription: 'Instead of waiting for the full response, text prints in real time. Learn Server-Sent Events (SSE), streaming APIs, and how to handle streamed data in the UI.', difficulty: 'intermediate', @@ -64,12 +65,17 @@ export const categories: SkillCategory[] = [ { id: 'rag-chat-documents', name: 'RAG — Chat With Your Documents', - projectTitle: 'Build a chatbot that answers questions from your uploaded documents', + projectTitle: + 'Build a chatbot that answers questions from your uploaded documents', projectDescription: 'Upload a PDF or text file — the AI reads it and answers questions based on its content. Learn vector embeddings (turning text into numbers AI can search), chunking (splitting documents into pieces), and similarity search.', difficulty: 'intermediate', timeEstimate: '1 day', - tools: ['Anthropic API', 'OpenAI Embeddings', 'Pinecone or Supabase pgvector'], + tools: [ + 'Anthropic API', + 'OpenAI Embeddings', + 'Pinecone or Supabase pgvector', + ], dependsOn: ['claude-api-chatbot'], }, { @@ -95,6 +101,18 @@ export const categories: SkillCategory[] = [ tools: ['Claude API', 'OpenAI API', 'Next.js'], dependsOn: ['claude-api-chatbot', 'openai-api-content'], }, + { + id: 'ai-agents-workflows', + name: 'AI Agents — Multi-Step Workflows', + projectTitle: + 'Build an AI agent that breaks down tasks and executes them step by step', + projectDescription: + 'Instead of one prompt → one answer, the agent plans, executes steps, checks results, and adjusts. Build a research agent that searches, summarizes, and compiles a report. Learn agent loops, tool orchestration, memory between steps, and when to stop.', + difficulty: 'advanced', + timeEstimate: '1-2 days', + tools: ['Anthropic API', 'Next.js'], + dependsOn: ['ai-function-calling'], + }, ], }, @@ -119,7 +137,8 @@ export const categories: SkillCategory[] = [ { id: 'local-ai-privacy', name: 'Local AI for Privacy', - projectTitle: 'Build an app that processes sensitive data with a local AI model', + projectTitle: + 'Build an app that processes sensitive data with a local AI model', projectDescription: 'Medical notes, legal documents, personal journals — some data should never leave your computer. Run a local model to summarize, classify, or extract info from private files. Learn when to use local vs. cloud AI, data privacy patterns, and offline inference.', difficulty: 'intermediate', @@ -130,7 +149,8 @@ export const categories: SkillCategory[] = [ { id: 'local-ai-backend', name: 'Local AI Backend for Projects', - projectTitle: 'Connect your project to a local model instead of a paid API', + projectTitle: + 'Connect your project to a local model instead of a paid API', projectDescription: 'Replace Claude/OpenAI API calls with a local model — same code, but free. Learn the OpenAI-compatible API format and how to switch between local and cloud models.', difficulty: 'intermediate', @@ -225,7 +245,8 @@ export const categories: SkillCategory[] = [ { id: 'tailwind-styling', name: 'Tailwind CSS — Styling', - projectTitle: 'Restyle your site with Tailwind and make it look professional', + projectTitle: + 'Restyle your site with Tailwind and make it look professional', projectDescription: 'Take your site and add professional styling: mobile responsive, dark theme, hover effects. Learn the utility-first CSS approach and responsive design.', difficulty: 'beginner', @@ -235,7 +256,8 @@ export const categories: SkillCategory[] = [ { id: 'shadcn-ui-dashboard', name: 'shadcn/ui — Ready Components', - projectTitle: 'Assemble a dashboard from ready-made components in an hour', + projectTitle: + 'Assemble a dashboard from ready-made components in an hour', projectDescription: 'Use a library of beautiful components (buttons, modals, tables, charts) and build a working dashboard from them. Learn the component approach and UI library customization.', difficulty: 'beginner', @@ -275,6 +297,29 @@ export const categories: SkillCategory[] = [ tools: ['D3.js', 'Mapbox / Leaflet', 'React'], dependsOn: ['react-nextjs-portfolio'], }, + { + id: 'pwa-mobile', + name: 'PWA — Mobile App Experience', + projectTitle: + 'Turn your website into an installable app with offline support', + projectDescription: + 'Users can install your site on their phone home screen and use it offline. Add a service worker, app manifest, push notifications, and responsive mobile patterns. Learn the PWA standard, caching strategies, and how to make web feel native.', + difficulty: 'intermediate', + timeEstimate: '3-4 hours', + tools: ['next-pwa', 'Service Workers', 'Web Push API'], + dependsOn: ['react-nextjs-portfolio'], + }, + { + id: 'i18n-localization', + name: 'i18n — Multi-Language Support', + projectTitle: 'Make your site available in multiple languages', + projectDescription: + 'Add language switching, translate UI strings, handle RTL layouts, and format dates and numbers for different locales. Learn Next.js i18n routing, translation file structure, and locale-aware content delivery.', + difficulty: 'intermediate', + timeEstimate: '3-4 hours', + tools: ['next-intl or next-i18next', 'Next.js'], + dependsOn: ['react-nextjs-portfolio'], + }, ], }, @@ -361,6 +406,28 @@ export const categories: SkillCategory[] = [ tools: ['Vercel Cron', 'GitHub Actions', 'Next.js'], dependsOn: ['api-routes-first'], }, + { + id: 'realtime-websockets', + name: 'Real-Time — WebSockets & Live Updates', + projectTitle: 'Build a live chat or real-time notification feed', + projectDescription: + 'Messages appear instantly without page refresh. Build a chat room, live comments, or a collaborative document. Learn WebSockets, Supabase Realtime or Pusher, presence indicators, and handling connection drops.', + difficulty: 'intermediate', + timeEstimate: '3-4 hours', + tools: ['Supabase Realtime or Pusher', 'Next.js'], + dependsOn: ['api-routes-first'], + }, + { + id: 'fulltext-search', + name: 'Search — Full-Text & Vector', + projectTitle: 'Add instant search to your app — keyword and AI-powered', + projectDescription: + 'Build a search bar that finds results as the user types. Start with full-text search in PostgreSQL, then add AI-powered semantic search with vector embeddings. Learn search indexing, ranking, debounce patterns, and when to use keyword vs. vector search.', + difficulty: 'intermediate', + timeEstimate: '3-4 hours', + tools: ['Supabase or Algolia', 'OpenAI Embeddings', 'Next.js'], + dependsOn: ['supabase-crud'], + }, ], }, @@ -471,13 +538,37 @@ export const categories: SkillCategory[] = [ { id: 'cloudflare-domain-cdn', name: 'Cloudflare — Domain & CDN', - projectTitle: 'Connect your domain and speed up your site via Cloudflare', + projectTitle: + 'Connect your domain and speed up your site via Cloudflare', projectDescription: 'Buy a domain, set up DNS, enable CDN and SSL. Learn how DNS works, what CDN is, and how to protect your site from DDoS.', difficulty: 'beginner', timeEstimate: '1 hour', tools: ['Cloudflare'], }, + { + id: 'seo-meta-tags', + name: 'SEO — Meta Tags & Open Graph', + projectTitle: + 'Make your site look great in Google and social media previews', + projectDescription: + 'Add meta tags, Open Graph images, structured data, sitemap, and robots.txt. When someone shares your link on Twitter or LinkedIn — it shows a beautiful preview instead of a blank card. Learn technical SEO, Next.js Head component, and how search engines index pages.', + difficulty: 'beginner', + timeEstimate: '2-3 hours', + tools: ['Next.js', 'Google Search Console'], + dependsOn: ['vercel-first-deploy'], + }, + { + id: 'error-monitoring', + name: 'Error Monitoring — Sentry', + projectTitle: 'Know when your app breaks before your users tell you', + projectDescription: + 'Set up Sentry to catch errors in production, get Slack/email alerts, and see exactly which line of code crashed and why. Learn error tracking, source maps, performance monitoring, and how to triage bugs.', + difficulty: 'beginner', + timeEstimate: '1-2 hours', + tools: ['Sentry', 'Next.js'], + dependsOn: ['vercel-first-deploy'], + }, ], }, @@ -567,7 +658,8 @@ export const categories: SkillCategory[] = [ { id: 'google-sheets-api', name: 'Google APIs', - projectTitle: 'Connect Google Sheets as a database for a simple project', + projectTitle: + 'Connect Google Sheets as a database for a simple project', projectDescription: 'Read and write data directly from Google Sheets. Great for MVPs and prototypes. Learn Google API, service accounts, and authorization.', difficulty: 'intermediate', @@ -585,6 +677,38 @@ export const categories: SkillCategory[] = [ tools: ['Notion API', 'Next.js'], dependsOn: ['react-nextjs-portfolio'], }, + { + id: 'slack-bot', + name: 'Slack Bot', + projectTitle: 'Build a Slack bot for your team', + projectDescription: + 'Create a bot that responds to commands, posts updates, or runs AI queries inside Slack. Learn Slack Bolt SDK, slash commands, interactive messages, and OAuth app installation.', + difficulty: 'intermediate', + timeEstimate: '3-4 hours', + tools: ['Slack Bolt SDK', 'Next.js'], + }, + { + id: 'discord-bot', + name: 'Discord Bot', + projectTitle: 'Build a Discord bot for your community', + projectDescription: + 'A bot that moderates, answers questions, or runs commands in your Discord server. Learn Discord.js, slash commands, embeds, and bot hosting.', + difficulty: 'beginner', + timeEstimate: '2-3 hours', + tools: ['Discord.js', 'Node.js'], + }, + { + id: 'zapier-make-automation', + name: 'Zapier / Make — No-Code Automation', + projectTitle: + 'Connect your app to 5,000+ services without writing integrations', + projectDescription: + 'New user signs up → add to Mailchimp, post in Slack, create a Notion page — all automatic. Learn how to expose webhooks from your app, trigger external workflows, and when to use no-code automation vs. custom code.', + difficulty: 'beginner', + timeEstimate: '1-2 hours', + tools: ['Zapier or Make', 'Webhooks'], + dependsOn: ['api-routes-first'], + }, ], }, @@ -599,7 +723,8 @@ export const categories: SkillCategory[] = [ { id: 'claude-code-tool', name: 'Claude Code', - projectTitle: 'Create an entire project with a single prompt in the terminal', + projectTitle: + 'Create an entire project with a single prompt in the terminal', projectDescription: 'Type a task in the CLI — Claude Code creates files, installs dependencies, writes code. Learn agentic coding, how to formulate tasks for AI, and terminal workflow.', difficulty: 'beginner', @@ -653,20 +778,22 @@ export const categories: SkillCategory[] = [ // ─── Helpers ───────────────────────────────────────────────────────────────── -export const allSkills: Skill[] = categories.flatMap((c) => c.skills); +export const allSkills: Skill[] = categories.flatMap(c => c.skills); export function getSkillById(id: string): Skill | undefined { - return allSkills.find((s) => s.id === id); + return allSkills.find(s => s.id === id); } -export function getCategoryBySkillId(skillId: string): SkillCategory | undefined { - return categories.find((c) => c.skills.some((s) => s.id === skillId)); +export function getCategoryBySkillId( + skillId: string, +): SkillCategory | undefined { + return categories.find(c => c.skills.some(s => s.id === skillId)); } export function getDependencies(skillId: string): Skill[] { const skill = getSkillById(skillId); if (!skill?.dependsOn) return []; - return skill.dependsOn.map((id) => getSkillById(id)).filter(Boolean) as Skill[]; + return skill.dependsOn.map(id => getSkillById(id)).filter(Boolean) as Skill[]; } export function getTotalSkillCount(): number { diff --git a/src/styles/globals.scss b/src/styles/globals.scss index 2b42159..df5cf0f 100644 --- a/src/styles/globals.scss +++ b/src/styles/globals.scss @@ -46,7 +46,7 @@ html.scroll-style-articles::-webkit-scrollbar-thumb { html.scroll-style-longevity::-webkit-scrollbar-thumb { background: #e2d0b1; } -.hide-body-move { +html.hide-body-move { scrollbar-gutter: stable; }