From aa40fac43ba602120a2e72f41e42c0f54e2fe47e Mon Sep 17 00:00:00 2001 From: Web-serfer Date: Tue, 31 Mar 2026 18:45:18 +0500 Subject: [PATCH] =?UTF-8?q?=D0=9D=D0=BE=D0=B2=D0=BE=D0=B5=20=D0=B8=D0=B7?= =?UTF-8?q?=D0=BC=D0=BD=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=B2=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/about/Achievements.astro | 132 +++--- .../src/components/single-post/Comments.tsx | 392 ++++++++++-------- frontend/src/lib/auth.ts | 28 +- frontend/src/pages/auth/verify.astro | 59 +-- 4 files changed, 337 insertions(+), 274 deletions(-) diff --git a/frontend/src/components/about/Achievements.astro b/frontend/src/components/about/Achievements.astro index abf9acf..8789593 100644 --- a/frontend/src/components/about/Achievements.astro +++ b/frontend/src/components/about/Achievements.astro @@ -5,7 +5,25 @@ import { CONTACT_CONSTANTS } from '@constants/constants.ts'; const dynamicStats = getDynamicStats(); const yearsOfPractice = getYearsOfPractice(); -const achievements = [ +// Отдельный тип для числовых статов (исключаем "24/7") +interface NumericStat { + number: number; + suffix: string; + text: string; + type: 'years' | 'cases' | 'clients'; + icon: 'calendar' | 'briefcase' | 'users'; +} + +// Специальный тип для support +interface SupportStat { + number: "24/7"; + suffix: ''; + text: 'На связи'; + type: 'support'; + icon: 'clock'; +} + +const numericAchievements: NumericStat[] = [ { number: yearsOfPractice, suffix: '', @@ -26,15 +44,16 @@ const achievements = [ text: 'довольных клиентов', type: 'clients', icon: 'users' - }, - { - number: "24/7", - suffix: '', - text: 'На связи', - type: 'support', - icon: 'clock' } -] as const; +]; + +const supportStat: SupportStat = { + number: "24/7", + suffix: '', + text: 'На связи', + type: 'support', + icon: 'clock' +}; const iconPaths = { calendar: "M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z", @@ -43,16 +62,17 @@ const iconPaths = { clock: "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" } as const; -type StatType = typeof achievements[number]['type']; - -const getStatText = (stat: typeof achievements[number]): string => { - const texts: Record = { - years: `${stat.number} ${getYearDeclension(stat.number)} практики`, - cases: 'Успешных дел', - clients: `${stat.number} ${getClientDeclension(stat.number)}`, - support: 'Поддержка клиентов' - }; - return texts[stat.type]; +const getStatText = (stat: NumericStat | SupportStat): string => { + if (stat.type === 'years') { + return `${stat.number} ${getYearDeclension(stat.number)} практики`; + } + if (stat.type === 'cases') { + return 'Успешных дел'; + } + if (stat.type === 'clients') { + return `${stat.number} ${getClientDeclension(stat.number as number)}`; + } + return 'Поддержка клиентов'; }; --- @@ -68,38 +88,51 @@ const getStatText = (stat: typeof achievements[number]): string => {
- {achievements.map((stat) => ( -
- -
+ {numericAchievements.map((stat) => ( +
+
-
- -
- - - -
+
+
+ + + +
- -
- {stat.type !== 'support' ? ( - - 0 - - ) : ( - {stat.number} - )} - {stat.suffix} -
+
+ + 0 + + {stat.suffix} +
- - + {getStatText(stat)} +
-
))} + + +
+
+ +
+
+ + + +
+ +
+ {supportStat.number} +
+ + + {getStatText(supportStat)} + +
+
@@ -112,8 +145,8 @@ const getStatText = (stat: typeof achievements[number]): string => { Получите профессиональную консультацию адвоката уже сегодня

@@ -133,7 +166,7 @@ const getStatText = (stat: typeof achievements[number]): string => { entries.forEach(entry => { if (!entry.isIntersecting) return; - const counter = entry.target; + const counter = entry.target as HTMLElement; // <-- Исправление: явное приведение к HTMLElement const target = parseInt(counter.dataset.target || '0', 10); let startTime: number | null = null; @@ -141,7 +174,7 @@ const getStatText = (stat: typeof achievements[number]): string => { if (!startTime) startTime = timestamp; const progress = Math.min((timestamp - startTime) / speed, 1); const ease = 1 - Math.pow(1 - progress, 3); - + counter.textContent = Math.floor(ease * target).toString(); if (progress < 1) { @@ -164,10 +197,7 @@ const getStatText = (stat: typeof achievements[number]): string => { counters.forEach(counter => observer.observe(counter)); }; - // Запускаем анимацию initStatsAnimation(); - - // Для Astro View Transitions document.addEventListener('astro:after-swap', initStatsAnimation); document.addEventListener('astro:page-load', initStatsAnimation); \ No newline at end of file diff --git a/frontend/src/components/single-post/Comments.tsx b/frontend/src/components/single-post/Comments.tsx index 52e8ae4..23ea94d 100644 --- a/frontend/src/components/single-post/Comments.tsx +++ b/frontend/src/components/single-post/Comments.tsx @@ -27,6 +27,12 @@ interface ApiComment { }; } +// Тип для toast-уведомлений +interface ToastMessage { + type: "success" | "error"; + message: string; +} + export default function Comments(props: CommentsProps) { const [isAuthenticated, setIsAuthenticated] = createSignal(false); const [currentUser, setCurrentUser] = createSignal<{ @@ -39,6 +45,15 @@ export default function Comments(props: CommentsProps) { const [replyTo, setReplyTo] = createSignal(null); const [commentAuthors, setCommentAuthors] = createSignal>({}); const [toastVisible, setToastVisible] = createSignal(null); + // Добавляем сигнал для toast-уведомлений + const [toast, setToast] = createSignal(null); + + // Функция показа toast-уведомлений + const showToast = (message: ToastMessage): void => { + setToast(message); + // Автоматически скрываем через 3 секунды + setTimeout(() => setToast(null), 3000); + }; onMount(async () => { console.log("[Comments] Начало проверки авторизации..."); @@ -68,8 +83,8 @@ export default function Comments(props: CommentsProps) { } finally { setIsLoading(false); console.log( - "[Comments] Загрузка завершена, isAuthenticated:", - isAuthenticated() + "[Comments] Загрузка завершена, isAuthenticated:", + isAuthenticated() ); } }); @@ -77,11 +92,11 @@ export default function Comments(props: CommentsProps) { const loadComments = async () => { try { const response = await fetch( - `/api/comments?post_slug=${encodeURIComponent(props.postSlug)}&parent=null`, - { - method: "GET", - credentials: "include", - } + `/api/comments?post_slug=${encodeURIComponent(props.postSlug)}&parent=null`, + { + method: "GET", + credentials: "include", + } ); if (!response.ok) { @@ -101,20 +116,20 @@ export default function Comments(props: CommentsProps) { setCommentAuthors(authors); const commentsWithReplies: CommentWithReplies[] = await Promise.all( - data.items.map(async (comment: ApiComment) => { - const repliesResponse = await fetch( - `/api/comments?post_slug=${encodeURIComponent(props.postSlug)}&parent=${comment.id}`, - { - method: "GET", - credentials: "include", - } - ); - const repliesData = await repliesResponse.json(); - return { - ...comment, - replies: repliesData.items || [], - }; - }) + data.items.map(async (comment: ApiComment) => { + const repliesResponse = await fetch( + `/api/comments?post_slug=${encodeURIComponent(props.postSlug)}&parent=${comment.id}`, + { + method: "GET", + credentials: "include", + } + ); + const repliesData = await repliesResponse.json(); + return { + ...comment, + replies: repliesData.items || [], + }; + }) ); setComments(commentsWithReplies); @@ -157,8 +172,8 @@ export default function Comments(props: CommentsProps) { }; const handleReply = async ( - commentId: string, - data: { content: string } + commentId: string, + data: { content: string } ) => { try { const response = await fetch("/api/comments", { @@ -228,22 +243,22 @@ export default function Comments(props: CommentsProps) { const sizeClasses = { sm: "w-8 h-8 text-sm", md: "w-12 h-12 text-lg" }; return ( - - {props.author.charAt(0)} -
- } - > - {props.author} - + + {props.author.charAt(0)} + + } + > + {props.author} + ); }; @@ -259,164 +274,179 @@ export default function Comments(props: CommentsProps) { }; return ( - <> - {isLoading() ? ( -
-
-

Комментарии

- + <> + {/* Глобальное toast-уведомление */} + + {(t) => ( +
+ {t().message} +
+ )} +
+ + {isLoading() ? ( +
+
+

Комментарии

+ 0 -
-
-
-

Загрузка...

-
-
- ) : ( - }> -
-
-

Комментарии

- +
+
+
+

Загрузка...

+
+
+ ) : ( + }> +
+
+

Комментарии

+ {comments().length} -
- - 0} - fallback={ -
-

Пока нет комментариев. Будьте первым!

- } - > -
- - {(comment) => ( -
-
- -
-
+ + 0} + fallback={ +
+

Пока нет комментариев. Будьте первым!

+
+ } + > +
+ + {(comment) => ( +
+
+ +
+
{comment.expand?.user?.name || "Аноним"} - - - - - - - {formatDate(comment.created)} - -
-

- {comment.content} -

-
- - - - ⚠️ Нельзя ответить на свой комментарий - - -
-
-
- - -
- handleReply(comment.id, data)} - onCancel={() => setReplyTo(null)} - user={currentUser()} - /> -
-
- - 0}> -
- - {(reply) => ( -
- -
-
- - {reply.expand?.user?.name || "Аноним"} - - - + - + - - - - {formatDate(reply.created)} - -
-

- {reply.content} -

+ /> + + + + {formatDate(comment.created)} + +
+

+ {comment.content} +

+
+ + + + ⚠️ Нельзя ответить на свой комментарий + +
- )} -
-
-
-
- )} -
+
+ + +
+ handleReply(comment.id, data)} + onCancel={() => setReplyTo(null)} + user={currentUser()} + /> +
+
+ + 0}> +
+ + {(reply) => ( +
+ +
+
+ + {reply.expand?.user?.name || "Аноним"} + + + + + + + + {formatDate(reply.created)} + +
+

+ {reply.content} +

+
+
+ )} +
+
+
+
+ )} + +
+ + + {/* Основная форма с передачей данных пользователя */} +
- - {/* Основная форма с передачей данных пользователя */} - -
- - )} - + )} + ); } diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts index 7380ac7..4ec3e1f 100644 --- a/frontend/src/lib/auth.ts +++ b/frontend/src/lib/auth.ts @@ -1,4 +1,5 @@ import PocketBase from 'pocketbase'; +import type { RecordModel } from 'pocketbase'; // URL PocketBase сервера const POCKETBASE_URL = import.meta.env.PUBLIC_POCKETBASE_URL || 'http://localhost:8090'; @@ -22,39 +23,35 @@ export function saveAuthToCookie(): void { // Автоматическое восстановление сессии при загрузке страницы if (typeof document !== 'undefined') { - // Загружаем из cookies при инициализации pb.authStore.loadFromCookie(document.cookie); - - // Подписываемся на изменения + pb.authStore.onChange(() => { saveAuthToCookie(); }, true); } -// Типы для аутентификации -export interface AuthRecord { - id: string; +// Типы для аутентификации — расширяем RecordModel вместо дублирования +export interface AuthRecord extends RecordModel { email: string; name: string; avatar?: string; emailVisibility: boolean; verified: boolean; - created: string; - updated: string; } // Функция входа пользователя export async function login(email: string, password: string): Promise { const authData = await pb.collection('users').authWithPassword(email, password); + // Теперь приведение типа безопасно, так как AuthRecord extends RecordModel return authData.record as AuthRecord; } // Функция регистрации пользователя export async function register( - email: string, - password: string, - passwordConfirm: string, - name?: string + email: string, + password: string, + passwordConfirm: string, + name?: string ): Promise { const record = await pb.collection('users').create({ email, @@ -63,10 +60,9 @@ export async function register( name: name || email.split('@')[0], emailVisibility: true, }); - - // Автоматический вход после регистрации + await login(email, password); - + return record as AuthRecord; } @@ -103,4 +99,4 @@ export async function resendVerificationEmail(email: string): Promise { // Функция подтверждения email export async function confirmVerification(token: string): Promise { await pb.collection('users').confirmVerification(token); -} +} \ No newline at end of file diff --git a/frontend/src/pages/auth/verify.astro b/frontend/src/pages/auth/verify.astro index 7c36329..2d69a00 100644 --- a/frontend/src/pages/auth/verify.astro +++ b/frontend/src/pages/auth/verify.astro @@ -9,10 +9,10 @@ const seo = { ---
@@ -39,7 +39,7 @@ const seo = {

Проверьте вашу почту

- +

Мы отправили ссылку для подтверждения на

@@ -54,9 +54,9 @@ const seo = { @@ -71,7 +71,7 @@ const seo = { Уже подтвердили email? Войти + >

@@ -82,19 +82,19 @@ const seo = { + \ No newline at end of file