Skip to content

Commit 27f3083

Browse files
committed
feat: revamp offer pages with dynamic slots, timeline infographic and copy updates
- Rewrite offer copy for sharper messaging across all 4 pages - Add hash-based slot decrement and count-up animation in offer-dates.ts - Replace step-based process infographic with horizontal timeline bars - Add availability urgency states (low/critical) with fade-in animation - Increase slot count from 5 to 10 per month
1 parent 5d2a9dc commit 27f3083

6 files changed

Lines changed: 181 additions & 145 deletions

File tree

src/pages/offer/1.astro

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ import OfferFooter from '../../components/offer/OfferFooter.astro';
1515
ctaHref="/offer/2"
1616
eagerPhoto={true}
1717
>
18-
<p>Вы решили сделать сайт. Открыли поиск — и начались варианты, каждый из которых чем-то да не устраивает.</p>
19-
<p><strong>Студия.</strong> От 100 тысяч рублей. Несколько недель только на согласование макетов. Бесконечная цепочка менеджеров, дизайнеров, верстальщиков. Результат может быть хорошим, но путь к нему — долгий и дорогой.</p>
20-
<p><strong>Фрилансер на Тильде.</strong> Дешевле — от 15–20 тысяч (обычный ценник новичков). Но сайт выглядит как сайт на Тильде: шаблонная структура, медленная загрузка, ежемесячная подписка за платформу. А если фрилансер использует зероблоки, чтобы сделать красивее — вы потом, скорее всего, не сможете ничего поменять сами, придётся снова обращаться к дизайнеру.</p>
21-
<p><strong>Собрать самому.</strong> Убить несколько недель. Получить что-то, за что немного неловко перед клиентами. Потому что вы не дизайнер — и это нормально.</p>
22-
<p><strong>ИИ-агенты и онлайн-генераторы.</strong> Страница за один промпт. Без понимания вашего бизнеса, без структуры, без внятных текстов. Экономия на спичках.</p>
18+
<p>Вам нужен сайт. Не «когда-нибудь потом», а сейчас. Вы открываете поиск — и понимаете, что ни один вариант не подходит целиком.</p>
19+
<p><strong>Студия.</strong> От 100 тысяч. Недели на согласование макетов. Менеджеры, дизайнеры, верстальщики — цепочка, в которой теряется время и деньги. Результат может быть хорошим, но вы заплатите за процесс, а не за итог.</p>
20+
<p><strong>Фрилансер на Тильде.</strong> От 15–20 тысяч. Но сайт выглядит как Тильда: шаблонная структура, медленная загрузка, подписка за платформу. А если фрилансер уходит в зероблоки ради красоты — менять что-либо потом вы не сможете без него.</p>
21+
<p><strong>Собрать самому.</strong> Несколько недель вечерами. Результат, за который неловко перед клиентами. Вы не дизайнер — и не должны им быть.</p>
22+
<p><strong>ИИ-генераторы.</strong> Страница за минуту. Без понимания бизнеса, без структуры, без текстов, которые продают. Выглядит как сайт, но не работает как сайт.</p>
2323
<p>В итоге вы либо переплачиваете, либо получаете компромисс, с которым потом живёте. Но есть и другой путь.</p>
2424

2525
<OfferFooter slot="footer" />

src/pages/offer/2.astro

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,46 +13,38 @@ import OfferFooter from '../../components/offer/OfferFooter.astro';
1313
ctaText="Как устроен процесс?"
1414
ctaHref="/offer/3"
1515
>
16-
<p>Меня зовут Роман Пуртов. Я делаю сайты с 2018 года — начинал на Тильде и конструкторах, прошёл через десятки проектов в разных нишах. Параллельно занимался маркетингом: запускал рекламу, строил воронки продаж, помогал бизнесам выходить на обороты в миллионы рублей в месяц.</p>
16+
<p>Меня зовут Роман Пуртов. Я делаю сайты с 2013 года — начинал с HTML-вёрстки, продолжил на конструкторах и Тильде, прошёл через десятки проектов в разных нишах. Параллельно занимался маркетингом: запускал рекламу, строил воронки продаж, помогал бизнесам выходить на обороты в миллионы рублей в месяц.</p>
1717
<p>Классический процесс создания сайта устроен избыточно. Сначала рисуется макет в Figma — это дни или недели. Потом этот макет «переводится» в код — ещё столько же. Два этапа, которые по сути делают одно и то же.</p>
1818
<p>Я объединил их в один. Проектирую сайт сразу как готовый продукт — то, что вы видите на экране, уже является рабочим сайтом, а не картинкой в дизайн-программе.</p>
1919

2020
<div class="o3-process" aria-label="Сравнение подходов по времени">
2121
<p class="o3-process-title">Где теряются сроки</p>
2222

23-
<div class="o3-process-row">
24-
<div class="o3-process-head">
25-
<span class="o3-process-label">Обычный подход</span>
26-
<span class="o3-process-time">Обычно: недели</span>
27-
</div>
28-
<div class="o3-process-steps">
29-
<div class="o3-process-step">
30-
<span class="o3-process-step-dot">1</span>
31-
<span>Дизайн в Figma</span>
32-
</div>
33-
<span class="o3-process-arrow">+</span>
34-
<div class="o3-process-step">
35-
<span class="o3-process-step-dot">2</span>
36-
<span>Вёрстка в код</span>
23+
<div class="o3-timeline-row">
24+
<span class="o3-timeline-label">Обычный подход</span>
25+
<div class="o3-timeline-track">
26+
<div class="o3-timeline-bar o3-timeline-bar--default" style="width:100%">
27+
<span class="o3-timeline-seg">Дизайн в Figma</span>
28+
<span class="o3-timeline-sep">+</span>
29+
<span class="o3-timeline-seg">Вёрстка в код</span>
3730
</div>
31+
<span class="o3-timeline-time">~недели</span>
3832
</div>
3933
</div>
4034

41-
<div class="o3-process-row o3-process-row--highlight">
42-
<div class="o3-process-head">
43-
<span class="o3-process-label">Мой подход</span>
44-
<span class="o3-process-time">Обычно: дни</span>
45-
</div>
46-
<div class="o3-process-steps">
47-
<div class="o3-process-step o3-process-step--accent">
48-
<span class="o3-process-step-dot o3-process-step-dot--accent">1</span>
49-
<span>Сразу делаю дизайн как рабочий сайт</span>
35+
<div class="o3-timeline-row">
36+
<span class="o3-timeline-label o3-timeline-label--accent">Мой подход</span>
37+
<div class="o3-timeline-track">
38+
<div class="o3-timeline-bar o3-timeline-bar--accent" style="width:45%">
39+
<span class="o3-timeline-seg">Сразу рабочий сайт</span>
5040
</div>
41+
<span class="o3-timeline-time">~дни</span>
5142
</div>
5243
</div>
5344
</div>
5445

55-
<p>Сайт готов за дни, а не недели. Страницы грузятся за секунду. Сайт нормально находится в Яндексе и Google. И главное — он полностью принадлежит вам. Никаких ежемесячных платежей за платформу. При этом стоит это не как студийная разработка.</p>
46+
<p>Сайт готов за дни, а не недели. Страницы грузятся за секунду. Сайт нормально находится в Яндексе и Google. И главное — он полностью принадлежит вам. Никаких ежемесячных платежей за платформу. При этом сто́ит это не как студийная разработка.</p>
47+
<p>«А если нужно что-то поменять на сайте — текст, фото, контакты?» Мелкие правки я делаю бесплатно. Для более крупных задач стоимость обсудим отдельно — но даже несколько платных доработок в год обойдутся дешевле, чем ежемесячная подписка на конструктор.</p>
5648

5749
<OfferFooter slot="footer" />
5850
</Offer03Card>

src/pages/offer/3.astro

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@ import OfferFooter from '../../components/offer/OfferFooter.astro';
88
<Offer03Card
99
photo="/images/offer/03/03.jpg"
1010
photoAlt="Роман Пуртов за работой"
11-
superTitle="Понятные этапы, результат на каждом шаге"
11+
superTitle="Понятные этапы = результат на каждом шаге"
1212
heading="От разговора до готового сайта — 5 шагов"
1313
ctaText="Сколько это стоит?"
1414
ctaHref="/offer/4"
1515
>
16-
<p><strong>1. Разговор.</strong> Созваниваемся на 30–40 минут. Разбираемся, что за бизнес, кто ваши клиенты, какую задачу должен решать сайт.</p>
17-
<p><strong>2. Текстовый скелет.</strong> Составляю структуру: какие блоки, в каком порядке, с какими заголовками и текстами. Вы читаете, вносите правки — фиксируем.</p>
18-
<p><strong>3. Визуальный вайрфрейм.</strong> Превращаю скелет в наглядную схему — уже в браузере. Можно потрогать и покрутить с телефона или компьютера.</p>
16+
<p><strong>1. Разговор.</strong> Созваниваемся на 30–40 минут. Разбираемся, какую задачу должен решать сайт, кто будет его смотреть и какой результат вы хотите получить.</p>
17+
<p><strong>2. Текстовый прототип.</strong> Составляю структуру: какие блоки, в каком порядке, с какими заголовками и текстами. Вы читаете, вносите правки — фиксируем.</p>
18+
<p><strong>3. Визуальный вайрфрейм.</strong> Превращаю прототип в наглядную схему — уже в браузере. Можно потрогать и покрутить с телефона и компьютера.</p>
1919
<p><strong>4. Готовый сайт.</strong> Финальный дизайн: цвета, шрифты, изображения, анимации. Это уже рабочий сайт — адаптивный, быстрый, с формой заявки.</p>
20-
<p><strong>5. Запуск.</strong> Размещаю на хостинге, настраиваю формы, прописываю SEO-основу. Сайт работает и принимает заявки.</p>
20+
<p><strong>5. Запуск.</strong> Размещаю на хостинге, настраиваю формы, закладываю SEO-основу. Сайт работает, принимает и отправляет заявки.</p>
2121
<p>После запуска остаюсь на связи. Если нужно поменять текст, заменить фото или подправить мелочи — сделаю бесплатно. Для более крупных доработок обсудим стоимость отдельно.</p>
2222

2323
<OfferFooter slot="footer" />

src/pages/offer/4.astro

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import OfferFooter from '../../components/offer/OfferFooter.astro';
1212
>
1313
<Fragment slot="heading">Лендинг за 2 рабочих дня — <span class="o3-price-old">50 000 &#8381;</span> <span class="o3-price-new">35 000 &#8381;</span></Fragment>
1414

15-
<p data-offer-date="monthLimit">В апреле я беру только 5 проектов. Не больше — потому что каждым занимаюсь лично, в полном фокусе, без параллельных задач на фоне.</p>
15+
<p data-offer-date="monthLimit">В апреле я беру только 10 проектов. Не больше — потому что каждым занимаюсь лично, в полном фокусе, без параллельных задач на фоне.</p>
1616
<p>Это не урезанный продукт и не «сайт-визитка на одну страничку». Это полноценный лендинг с проработанной структурой, дизайном с нуля и всем, что нужно для запуска.</p>
1717

1818
<div class="o3-checklist-wrap">
@@ -55,7 +55,7 @@ import OfferFooter from '../../components/offer/OfferFooter.astro';
5555

5656
<div class="o3-availability" data-offer-date="availability">
5757
<span class="o3-dot"></span>
58-
Свободно: 5 из 5 мест на апрель
58+
Свободно: 10 из 10 мест на апрель
5959
</div>
6060

6161
<div class="o3-form-wrap">

src/scripts/offer-dates.ts

Lines changed: 92 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,63 @@ const ACC = ['январь','февраль','март','апрель','май',
22
const PREP = ['январе','феврале','марте','апреле','мае','июне','июле','августе','сентябре','октябре','ноябре','декабре'];
33

44
const FINAL_WEEK_THRESHOLD = 7;
5-
const SLOTS_SINGLE = 5;
6-
const SLOTS_DOUBLE = 10;
5+
const SLOTS_SINGLE = 10;
6+
const SLOTS_DOUBLE = 20;
7+
8+
/* ---------- hash & fake-decrement ---------- */
9+
10+
function splitmix(seed: number): number {
11+
let h = (seed + 0x9e3779b9) >>> 0;
12+
h = ((h ^ (h >>> 16)) * 0x85ebca6b) >>> 0;
13+
h = ((h ^ (h >>> 13)) * 0xc2b2ae35) >>> 0;
14+
return (h ^ (h >>> 16)) >>> 0;
15+
}
16+
17+
function getSlotsTaken(totalSlots: number, year: number, month: number, day: number): number {
18+
const daysInMonth = new Date(year, month + 1, 0).getDate();
19+
20+
// Collect working days starting from day 2
21+
const workingDays: number[] = [];
22+
for (let d = 2; d <= daysInMonth; d++) {
23+
const dow = new Date(year, month, d).getDay();
24+
if (dow !== 0 && dow !== 6) workingDays.push(d);
25+
}
26+
27+
// Pick `totalSlots` days from working days using hash
28+
const takenDays: number[] = [];
29+
const pool = [...workingDays];
30+
31+
for (let i = 0; i < totalSlots && pool.length > 0; i++) {
32+
const h = splitmix(year * 374761 + month * 668265 + i * 119873);
33+
const norm = h / 0xFFFFFFFF;
34+
const biased = Math.pow(norm, 0.7); // slight front-loading
35+
const idx = Math.min(Math.floor(biased * pool.length), pool.length - 1);
36+
takenDays.push(pool[idx]);
37+
pool.splice(idx, 1);
38+
}
39+
40+
// Count how many "taken" days have passed
41+
const taken = takenDays.filter(d => d <= day).length;
42+
43+
// Always keep at least 1 slot free
44+
return Math.min(taken, totalSlots - 1);
45+
}
46+
47+
/* ---------- count-up animation ---------- */
48+
49+
function animateCount(el: HTMLElement, target: number, duration = 600) {
50+
if (target <= 0) { el.textContent = '0'; return; }
51+
const start = performance.now();
52+
const step = (now: number) => {
53+
const progress = Math.min((now - start) / duration, 1);
54+
const eased = 1 - Math.pow(1 - progress, 3); // ease-out cubic
55+
el.textContent = String(Math.round(eased * target));
56+
if (progress < 1) requestAnimationFrame(step);
57+
};
58+
requestAnimationFrame(step);
59+
}
60+
61+
/* ---------- main ---------- */
762

863
const now = new Date();
964
const month = now.getMonth();
@@ -17,36 +72,53 @@ const nextYear = month === 11 ? year + 1 : year;
1772

1873
const isDouble = daysLeft <= FINAL_WEEK_THRESHOLD;
1974
const slots = isDouble ? SLOTS_DOUBLE : SLOTS_SINGLE;
75+
const taken = getSlotsTaken(SLOTS_SINGLE, year, month, day);
76+
const free = slots - taken;
2077

21-
// superTitle: "Специальное предложение на апрель 2026" или "на апрель–май 2026"
78+
// superTitle
2279
const superEl = document.querySelector<HTMLElement>('.o3-super');
2380
if (superEl) {
24-
if (isDouble) {
25-
superEl.textContent = `Специальное предложение на ${ACC[month]}${ACC[nextMonth]} ${nextYear}`;
26-
} else {
27-
superEl.textContent = `Специальное предложение на ${ACC[month]} ${year}`;
28-
}
81+
superEl.textContent = isDouble
82+
? `Специальное предложение на ${ACC[month]}${ACC[nextMonth]} ${nextYear}`
83+
: `Специальное предложение на ${ACC[month]} ${year}`;
2984
}
3085

31-
// monthLimit: "В апреле я беру только 5 проектов" или "В апреле–мае я беру только 10 проектов"
86+
// monthLimit
3287
const limitEl = document.querySelector<HTMLElement>('[data-offer-date="monthLimit"]');
3388
if (limitEl) {
3489
const rest = '. Не больше — потому что каждым занимаюсь лично, в полном фокусе, без параллельных задач на фоне.';
35-
if (isDouble) {
36-
limitEl.textContent = ${PREP[month]}${PREP[nextMonth]} я беру только ${slots} проектов${rest}`;
37-
} else {
38-
limitEl.textContent = ${PREP[month]} я беру только ${slots} проектов${rest}`;
39-
}
90+
limitEl.textContent = isDouble
91+
? ${PREP[month]}${PREP[nextMonth]} я беру только ${slots} проектов${rest}`
92+
: ${PREP[month]} я беру только ${slots} проектов${rest}`;
4093
}
4194

42-
// availability: "Свободно: 5 из 5 мест на апрель" или "10 из 10 мест на апрель–май"
95+
// availability
4396
const availEl = document.querySelector<HTMLElement>('[data-offer-date="availability"]');
4497
if (availEl) {
4598
const dot = availEl.querySelector('.o3-dot');
46-
if (isDouble) {
47-
availEl.textContent = `Свободно: ${slots} из ${slots} мест на ${ACC[month]}${ACC[nextMonth]}`;
48-
} else {
49-
availEl.textContent = `Свободно: ${slots} из ${slots} мест на ${ACC[month]}`;
99+
const monthLabel = isDouble ? `${ACC[month]}${ACC[nextMonth]}` : ACC[month];
100+
101+
// Build content with a span for the animated number
102+
const countSpan = document.createElement('span');
103+
countSpan.className = 'o3-free-count';
104+
countSpan.textContent = '0';
105+
106+
availEl.textContent = '';
107+
if (dot) availEl.appendChild(dot);
108+
availEl.appendChild(document.createTextNode(' Свободно: '));
109+
availEl.appendChild(countSpan);
110+
availEl.appendChild(document.createTextNode(` из ${slots} мест на ${monthLabel}`));
111+
112+
// Urgency classes
113+
if (free <= 1) {
114+
availEl.classList.add('is-critical');
115+
} else if (free <= 3) {
116+
availEl.classList.add('is-low');
50117
}
51-
if (dot) availEl.prepend(dot);
118+
119+
// Fade-in + count-up
120+
requestAnimationFrame(() => {
121+
availEl.classList.add('is-visible');
122+
animateCount(countSpan, free);
123+
});
52124
}

0 commit comments

Comments
 (0)