Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion frontend/components/blog/BlogCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,24 @@ export default function BlogCard({
disableHoverColor?: boolean;
}) {
const t = useTranslations('blog');

const getCategoryLabel = (categoryName: string): string => {
const key = categoryName.toLowerCase() as
| 'tech'
| 'career'
| 'insights'
| 'news'
| 'growth';
const categoryTranslations: Record<string, string> = {
tech: t('categories.tech'),
career: t('categories.career'),
insights: t('categories.insights'),
news: t('categories.news'),
growth: t('categories.growth'),
};
return categoryTranslations[key] || categoryName;
};

const excerpt =
(post.body ?? [])
.filter((b): b is PortableTextBlock => b._type === 'block')
Expand All @@ -34,8 +52,9 @@ export default function BlogCard({
() => formatBlogDate(post.publishedAt),
[post.publishedAt]
);
const categoryLabel =
const rawCategory =
post.categories?.[0] === 'Growth' ? 'Career' : post.categories?.[0];
const categoryLabel = rawCategory ? getCategoryLabel(rawCategory) : undefined;

return (
<article
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/blog/BlogFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ export default function BlogFilters({
<div className="relative flex flex-col h-full pt-8">
{featuredPost.categories?.[0] && (
<div className="absolute top-0 left-0 text-xs font-bold uppercase tracking-[0.2em] text-[var(--accent-primary)]">
{featuredPost.categories[0]}
{getCategoryLabel(featuredPost.categories[0] === 'Growth' ? 'Career' : featuredPost.categories[0])}
</div>
)}
<div className="my-auto">
Expand Down
3 changes: 2 additions & 1 deletion frontend/components/blog/BlogHeaderSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ function normalizeSearchText(value: string) {

export function BlogHeaderSearch() {
const t = useTranslations('blog');
const tAria = useTranslations('aria');
const locale = useLocale();
const [open, setOpen] = useState(false);
const [value, setValue] = useState('');
Expand Down Expand Up @@ -158,7 +159,7 @@ export function BlogHeaderSearch() {
})
}
className="flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
aria-label="Search blog"
aria-label={tAria('searchBlog')}
>
<Search className="h-4 w-4" aria-hidden="true" />
</button>
Expand Down
21 changes: 20 additions & 1 deletion frontend/components/header/AppMobileMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,25 @@ export function AppMobileMenu({
const tMobileMenu = useTranslations('mobileMenu');
const tCategories = useTranslations('shop.catalog.categories');
const tProducts = useTranslations('shop.products');
const tBlog = useTranslations('blog');
const pathname = usePathname();

const getBlogCategoryLabel = (categoryName: string): string => {
const key = categoryName.toLowerCase() as
| 'tech'
| 'career'
| 'insights'
| 'news'
| 'growth';
const categoryTranslations: Record<string, string> = {
tech: tBlog('categories.tech'),
career: tBlog('categories.career'),
insights: tBlog('categories.insights'),
news: tBlog('categories.news'),
growth: tBlog('categories.growth'),
};
return categoryTranslations[key] || categoryName;
};
const searchParams = useSearchParams();
const currentCategory = searchParams.get('category');
const [open, setOpen] = useState(false);
Expand Down Expand Up @@ -176,14 +194,15 @@ export function AppMobileMenu({
const slug = slugify(category.title || '');
const href = `/blog/category/${slug}`;
const isActive = pathname === href;
const displayTitle = category.title === 'Growth' ? 'Career' : category.title;
return (
<Link
key={category._id}
href={href}
onClick={close}
className={linkClass(isActive)}
>
{category.title}
{getBlogCategoryLabel(displayTitle)}
</Link>
);
})}
Expand Down
5 changes: 3 additions & 2 deletions frontend/components/header/DesktopActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function DesktopActions({
showAdminLink = false,
}: DesktopActionsProps) {
const t = useTranslations('navigation');
const tAria = useTranslations('aria');
const isShop = variant === 'shop';
const isBlog = variant === 'blog';

Expand All @@ -32,7 +33,7 @@ export function DesktopActions({
variant="icon"
href="/dashboard"
icon={User}
label="Dashboard"
label={tAria('dashboard')}
/>
)}

Expand All @@ -41,7 +42,7 @@ export function DesktopActions({
variant="icon"
href="/shop/admin"
icon={Settings}
label="Shop admin"
label={tAria('shopAdmin')}
/>
)}

Expand Down
15 changes: 8 additions & 7 deletions frontend/components/home/InteractiveCTAButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ export function InteractiveCTAButton() {
const [variantIndex, setVariantIndex] = React.useState(1);
const [isHovered, setIsHovered] = React.useState(false);

// Тимчасово для тестування - цифри замість текстів
const textVariants = [
`${t('cta')} 1`,
`${t('cta')} 2`,
`${t('cta')} 3`,
`${t('cta')} 4`,
`${t('cta')} 5`,
t('ctaVariants.1'),
t('ctaVariants.2'),
t('ctaVariants.3'),
t('ctaVariants.4'),
t('ctaVariants.5'),
t('ctaVariants.6'),
t('ctaVariants.7'),
t('ctaVariants.8'),
];

const defaultVariant = t('cta');
Expand All @@ -35,7 +37,6 @@ export function InteractiveCTAButton() {
setVariantIndex(prev => (prev + 1) % textVariants.length);
};

// Орбітальні частинки
const particles = Array.from({ length: 12 }, (_, i) => ({
id: i,
angle: (i * 360) / 8,
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/quiz/QuizContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ export function QuizContainer({
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
Exit Quiz
{tExit('exitButton')}
</Button>
</div>
<QuizProgress
Expand Down
4 changes: 3 additions & 1 deletion frontend/components/shared/GitHubStarButton.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
'use client';

import { Star } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useState, useEffect, useRef } from 'react';

interface GitHubStarButtonProps {
className?: string;
}

export function GitHubStarButton({ className = '' }: GitHubStarButtonProps) {
const t = useTranslations('aria');
const [displayCount, setDisplayCount] = useState(0);
const [finalCount, setFinalCount] = useState<number | null>(null);
const githubUrl = 'https://github.com/DevLoversTeam/devlovers.net';
Expand Down Expand Up @@ -74,7 +76,7 @@ export function GitHubStarButton({ className = '' }: GitHubStarButtonProps) {
href={githubUrl}
target="_blank"
rel="noopener noreferrer"
aria-label={`Star on GitHub - ${displayCount} stars`}
aria-label={t('starOnGithub', { count: displayCount })}
className={`
hidden lg:inline-flex
group relative
Expand Down
5 changes: 4 additions & 1 deletion frontend/components/shop/header/CartButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import { ShoppingBag } from 'lucide-react';
import { useTranslations } from 'next-intl';

import { useMounted } from '@/hooks/use-mounted';
import { HeaderButton } from '@/components/shared/HeaderButton';
Expand All @@ -10,18 +11,20 @@ import { useCart } from '../CartProvider';
export function CartButton() {
const { cart } = useCart();
const mounted = useMounted();
const t = useTranslations('aria');

const itemCount = mounted ? cart.summary.itemCount : 0;
const showCount = itemCount > 0;

const badgeText = itemCount > 99 ? '99+' : itemCount;
const label = showCount ? t('cartWithItems', { count: itemCount }) : t('cart');

return (
<HeaderButton
href="/shop/cart"
variant="icon"
icon={ShoppingBag}
label={showCount ? `Cart, ${itemCount} items` : 'Cart'}
label={label}
badge={showCount ? badgeText : undefined}
badgeClassName="bg-[color:var(--accent-primary)] text-white"
/>
Expand Down
12 changes: 7 additions & 5 deletions frontend/components/theme/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
'use client';

import { useTheme } from 'next-themes';
import { useTranslations } from 'next-intl';
import { Monitor, Sun, Moon } from 'lucide-react';
import { motion } from 'framer-motion';
import { useEffect, useState } from 'react';

const themes = [
{ value: 'system', icon: Monitor, label: 'System theme' },
{ value: 'light', icon: Sun, label: 'Light theme' },
{ value: 'dark', icon: Moon, label: 'Dark theme' },
{ value: 'system', icon: Monitor, labelKey: 'themeSystem' },
{ value: 'light', icon: Sun, labelKey: 'themeLight' },
{ value: 'dark', icon: Moon, labelKey: 'themeDark' },
] as const;

export function ThemeToggle() {
const { theme, setTheme } = useTheme();
const t = useTranslations('aria');
const [mounted, setMounted] = useState(false);

useEffect(() => {
Expand All @@ -36,11 +38,11 @@ export function ThemeToggle() {

return (
<div className="flex h-9 items-center gap-1 rounded-full bg-neutral-100 p-1 dark:border dark:border-neutral-800 dark:bg-neutral-950">
{themes.map(({ value, icon: Icon, label }) => (
{themes.map(({ value, icon: Icon, labelKey }) => (
<button
key={value}
onClick={() => setTheme(value)}
aria-label={label}
aria-label={t(labelKey)}
className="theme-toggle-btn relative flex h-7 w-7 items-center justify-center rounded-full"
>
{theme === value && (
Expand Down
23 changes: 20 additions & 3 deletions frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@
"searchBlog": "Search blog",
"toggleMenu": "Toggle menu",
"closeMenu": "Close menu",
"logout": "Log out"
"logout": "Log out",
"themeSystem": "System theme",
"themeLight": "Light theme",
"themeDark": "Dark theme",
"cart": "Cart",
"cartWithItems": "Cart, {count} items",
"starOnGithub": "Star on GitHub - {count} stars"
},
"mobileMenu": {
"newProduct": "New product",
Expand All @@ -41,7 +47,17 @@
"title": "DevLovers",
"subtitle": "Platform for Technical Interview Preparation",
"description": "Master frontend, backend, and full-stack development with our interview questions, quizzes, and learning materials.",
"cta": "Start Learning"
"cta": "Take the first step",
"ctaVariants": {
"1": "No pressure. Just start.",
"2": "This might actually be fun",
"3": "You're already halfway",
"4": "One click from progress",
"5": "You clicked. That counts.",
"6": "Brave move, by the way",
"7": "Okay, now it's happening",
"8": "Hello, new commit"
}
},
"qa": {
"title": "Questions and Answers",
Expand Down Expand Up @@ -178,7 +194,8 @@
"title": "Exit Quiz?",
"message": "Your progress will not be saved.",
"confirm": "Exit",
"cancel": "Continue"
"cancel": "Continue",
"exitButton": "Exit Quiz"
},
"antiCheat": {
"copy": "Copying is not allowed during the quiz",
Expand Down
23 changes: 20 additions & 3 deletions frontend/messages/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@
"searchBlog": "Szukaj w blogu",
"toggleMenu": "Przełącz menu",
"closeMenu": "Zamknij menu",
"logout": "Wyloguj się"
"logout": "Wyloguj się",
"themeSystem": "Motyw systemowy",
"themeLight": "Jasny motyw",
"themeDark": "Ciemny motyw",
"cart": "Koszyk",
"cartWithItems": "Koszyk, {count} produktów",
"starOnGithub": "Gwiazdka na GitHub - {count} gwiazdek"
},
"mobileMenu": {
"newProduct": "Nowy produkt",
Expand All @@ -41,7 +47,17 @@
"title": "DevLovers",
"subtitle": "Platforma do Przygotowania do Rozmów Kwalifikacyjnych",
"description": "Opanuj frontend, backend i full-stack development dzięki naszym pytaniom do rozmów kwalifikacyjnych, quizom i materiałom edukacyjnym.",
"cta": "Rozpocznij naukę"
"cta": "Rozpocznij naukę",
"ctaVariants": {
"1": "Bez presji. Po prostu zacznij.",
"2": "To może być ciekawsze, niż myślisz.",
"3": "Właśnie tutaj to się zaczyna.",
"4": "Jesteś już w połowie drogi.",
"5": "Jeden klik do postępu.",
"6": "Klik był — to się liczy.",
"7": "Postęp ważniejszy niż perfekcja.",
"8": "Zacząć to też funkcja."
}
},
"qa": {
"title": "Pytania i Odpowiedzi",
Expand Down Expand Up @@ -178,7 +194,8 @@
"title": "Wyjść z Quizu?",
"message": "Twój postęp nie zostanie zapisany.",
"confirm": "Wyjdź",
"cancel": "Kontynuuj"
"cancel": "Kontynuuj",
"exitButton": "Wyjdź z quizu"
},
"antiCheat": {
"copy": "Kopiowanie jest zabronione podczas quizu",
Expand Down
23 changes: 20 additions & 3 deletions frontend/messages/uk.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@
"searchBlog": "Пошук у блозі",
"toggleMenu": "Відкрити/закрити меню",
"closeMenu": "Закрити меню",
"logout": "Вийти"
"logout": "Вийти",
"themeSystem": "Системна тема",
"themeLight": "Світла тема",
"themeDark": "Темна тема",
"cart": "Кошик",
"cartWithItems": "Кошик, {count} товарів",
"starOnGithub": "Зірка на GitHub - {count} зірок"
},
"mobileMenu": {
"newProduct": "Новий товар",
Expand All @@ -41,7 +47,17 @@
"title": "DevLovers",
"subtitle": "Платформа для підготовки до технічних співбесід",
"description": "Опануйте frontend, backend та full-stack-розробку за допомогою наших питань для співбесід, квізів і навчальних матеріалів.",
"cta": "Стартуємо"
"cta": "Зроби перший крок",
"ctaVariants": {
"1": "Без тиску. Просто почни.",
"2": "Це може бути цікавіше, ніж здається.",
"3": "Ось тут усе й починається.",
"4": "Ти вже на пів шляху.",
"5": "Один клік до прогресу.",
"6": "Ну все, процес пішов.",
"7": "Простіше, ніж виглядає",
"8": "Почати — це теж фіча."
}
},
"qa": {
"title": "Питання та Відповіді",
Expand Down Expand Up @@ -178,7 +194,8 @@
"title": "Вийти з квізу?",
"message": "Ваш прогрес не буде збережено.",
"confirm": "Вийти",
"cancel": "Продовжити"
"cancel": "Продовжити",
"exitButton": "Вийти з квізу"
},
"antiCheat": {
"copy": "Копіювання заборонено під час квізу",
Expand Down