📄 Huge intl feature
Some checks failed
Some checks failed
This commit was merged in pull request #10.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import Link from 'next/link'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { Link } from '@/i18n/navigation'
|
||||
import Image from 'next/image'
|
||||
import { Post } from '@/lib/types/frontmatter'
|
||||
import { formatDate } from '@/lib/utils'
|
||||
@@ -9,6 +10,7 @@ interface BlogCardProps {
|
||||
}
|
||||
|
||||
export function BlogCard({ post, variant }: BlogCardProps) {
|
||||
const t = useTranslations('BlogPost')
|
||||
const hasImage = !!post.frontmatter.image
|
||||
|
||||
if (!hasImage || variant === 'text-only') {
|
||||
@@ -38,7 +40,7 @@ export function BlogCard({ post, variant }: BlogCardProps) {
|
||||
))}
|
||||
</div>
|
||||
<span className="inline-flex items-center font-mono text-xs uppercase text-cyan-400 hover:text-cyan-300 transition-colors">
|
||||
> READ [{post.readingTime}MIN]
|
||||
> {t('readingTime', {minutes: post.readingTime})}
|
||||
</span>
|
||||
</article>
|
||||
</Link>
|
||||
@@ -82,7 +84,7 @@ export function BlogCard({ post, variant }: BlogCardProps) {
|
||||
))}
|
||||
</div>
|
||||
<span className="inline-flex items-center font-mono text-xs uppercase text-cyan-400 hover:text-cyan-300 transition-colors">
|
||||
> READ [{post.readingTime}MIN]
|
||||
> {t('readingTime', {minutes: post.readingTime})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -127,7 +129,7 @@ export function BlogCard({ post, variant }: BlogCardProps) {
|
||||
))}
|
||||
</div>
|
||||
<span className="inline-flex items-center font-mono text-xs uppercase text-cyan-400 hover:text-cyan-300 transition-colors">
|
||||
> READ [{post.readingTime}MIN]
|
||||
> {t('readingTime', {minutes: post.readingTime})}
|
||||
</span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -6,7 +6,8 @@ import rehypeSanitize from 'rehype-sanitize'
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
import { OptimizedImage } from './OptimizedImage'
|
||||
import { CodeBlock } from './code-block'
|
||||
import Link from 'next/link'
|
||||
import { useLocale } from 'next-intl'
|
||||
import { Link } from '@/i18n/navigation'
|
||||
|
||||
interface MarkdownRendererProps {
|
||||
content: string
|
||||
@@ -14,6 +15,7 @@ interface MarkdownRendererProps {
|
||||
}
|
||||
|
||||
export default function MarkdownRenderer({ content, className = '' }: MarkdownRendererProps) {
|
||||
const locale = useLocale()
|
||||
return (
|
||||
<div className={`prose prose-invert prose-zinc max-w-none ${className}`}>
|
||||
<ReactMarkdown
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { Link } from '@/i18n/navigation'
|
||||
import { ThemeToggle } from '@/components/theme-toggle'
|
||||
import LanguageSwitcher from '@/components/layout/LanguageSwitcher'
|
||||
|
||||
export function Navbar() {
|
||||
const t = useTranslations('Navigation')
|
||||
const [isVisible, setIsVisible] = useState(true)
|
||||
const [lastScrollY, setLastScrollY] = useState(0)
|
||||
|
||||
@@ -39,10 +42,10 @@ export function Navbar() {
|
||||
className="font-mono text-sm uppercase tracking-wider transition-colors cursor-pointer"
|
||||
style={{ color: 'var(--neon-cyan)' }}
|
||||
>
|
||||
< HOME
|
||||
< {t('home')}
|
||||
</Link>
|
||||
<span className="font-mono text-sm text-zinc-100 dark:text-zinc-300 uppercase tracking-wider">
|
||||
// <span style={{ color: 'var(--neon-pink)' }}>BLOG</span> ARCHIVE
|
||||
// <span style={{ color: 'var(--neon-pink)' }}>{t('blog')}</span> ARCHIVE
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
@@ -50,15 +53,16 @@ export function Navbar() {
|
||||
href="/about"
|
||||
className="font-mono text-sm text-zinc-400 dark:text-zinc-500 uppercase tracking-wider hover:text-cyan-400 dark:hover:text-cyan-300 transition-colors cursor-pointer"
|
||||
>
|
||||
[ABOUT]
|
||||
[{t('about')}]
|
||||
</Link>
|
||||
<Link
|
||||
href="/blog"
|
||||
className="font-mono text-sm text-zinc-400 dark:text-zinc-500 uppercase tracking-wider hover:text-cyan-400 dark:hover:text-cyan-300 transition-colors cursor-pointer"
|
||||
>
|
||||
[BLOG]
|
||||
[{t('blog')}]
|
||||
</Link>
|
||||
<ThemeToggle />
|
||||
<ThemeToggle />
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getPopularTags } from '@/lib/tags'
|
||||
import { TagBadge } from './tag-badge'
|
||||
|
||||
export async function PopularTags({ limit = 5 }: { limit?: number }) {
|
||||
const tags = await getPopularTags(limit)
|
||||
const tags = await getPopularTags("en", limit)
|
||||
|
||||
if (tags.length === 0) return null
|
||||
|
||||
|
||||
@@ -30,9 +30,9 @@ export function ReadingProgress() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="fixed top-4 right-4 z-50 px-3 py-1.5 bg-[rgb(var(--bg-primary))] border-2 border-[var(--neon-cyan)] text-xs font-mono font-bold text-[var(--neon-cyan)] relative">
|
||||
<span className="relative z-10">[{Math.round(progress)}%]</span>
|
||||
</div>
|
||||
{/* <div className="fixed left-13 z-50 m-3 px-3 py-1.5 bg-[rgb(var(--bg-primary))] border-2 border-[var(--neon-cyan)] text-xs font-mono font-bold text-[var(--neon-cyan)]">
|
||||
<span className="left-4 z-10">[{Math.round(progress)}%]</span>
|
||||
</div> */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -43,17 +43,12 @@ export function StickyFooter({ url, title }: StickyFooterProps) {
|
||||
className={`
|
||||
fixed bottom-0 left-0 right-0 z-40
|
||||
bg-black/98 backdrop-blur-sm
|
||||
border-t-4 border-[var(--neon-magenta)]
|
||||
border-t-1 border-[var(--neon-magenta)]
|
||||
transition-transform duration-200 ease-in-out
|
||||
${isVisible ? 'translate-y-0' : 'translate-y-full'}
|
||||
`}
|
||||
style={{
|
||||
boxShadow: isVisible
|
||||
? '0 -8px 30px rgba(155,90,142,0.5), inset 0 4px 20px rgba(155,90,142,0.1)'
|
||||
: 'none',
|
||||
}}
|
||||
>
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-[var(--neon-magenta)] to-transparent opacity-70" />
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent to-transparent opacity-70" />
|
||||
|
||||
<div className="max-w-7xl mx-auto px-6 py-4 relative">
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Link from 'next/link'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { Link } from '@/i18n/navigation'
|
||||
import { TagInfo } from '@/lib/tags'
|
||||
|
||||
interface TagCloudProps {
|
||||
@@ -6,6 +7,7 @@ interface TagCloudProps {
|
||||
}
|
||||
|
||||
export function TagCloud({ tags }: TagCloudProps) {
|
||||
const t = useTranslations('Tags')
|
||||
const sizeClasses = {
|
||||
sm: 'text-xs opacity-70',
|
||||
md: 'text-sm',
|
||||
@@ -26,7 +28,7 @@ export function TagCloud({ tags }: TagCloudProps) {
|
||||
hover:text-cyan-400
|
||||
transition-colors
|
||||
`}
|
||||
title={`${tag.count} ${tag.count === 1 ? 'articol' : 'articole'}`}
|
||||
title={t('postsWithTag', {count: tag.count, tag: tag.name})}
|
||||
>
|
||||
#{tag.name}
|
||||
</Link>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import {Link} from '@/i18n/navigation'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useLocale, useTranslations } from 'next-intl'
|
||||
import { Fragment } from 'react'
|
||||
import { BreadcrumbsSchema } from './breadcrumbs-schema'
|
||||
|
||||
@@ -38,25 +39,33 @@ function ChevronIcon({ className }: { className?: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
function formatSegmentLabel(segment: string): string {
|
||||
const specialCases: { [key: string]: string } = {
|
||||
blog: 'Blog',
|
||||
tags: 'Tag-uri',
|
||||
about: 'Despre',
|
||||
}
|
||||
|
||||
if (specialCases[segment]) {
|
||||
return specialCases[segment]
|
||||
}
|
||||
|
||||
return segment
|
||||
.split('-')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
export function Breadcrumbs({ items }: { items?: BreadcrumbItem[] }) {
|
||||
const pathname = usePathname()
|
||||
const locale = useLocale()
|
||||
const t = useTranslations('Breadcrumbs')
|
||||
|
||||
// Hide breadcrumbs on main page
|
||||
const isMainPage = pathname === `/${locale}` || pathname === '/'
|
||||
if (isMainPage) {
|
||||
return null
|
||||
}
|
||||
|
||||
const formatSegmentLabel = (segment: string): string => {
|
||||
const specialCases: { [key: string]: string } = {
|
||||
blog: t('blog'),
|
||||
tags: t('tags'),
|
||||
about: t('about'),
|
||||
}
|
||||
|
||||
if (specialCases[segment]) {
|
||||
return specialCases[segment]
|
||||
}
|
||||
|
||||
return segment
|
||||
.split('-')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
let breadcrumbs: BreadcrumbItem[] = items || []
|
||||
|
||||
@@ -71,12 +80,8 @@ export function Breadcrumbs({ items }: { items?: BreadcrumbItem[] }) {
|
||||
})
|
||||
}
|
||||
|
||||
if (pathname === '/') {
|
||||
return null
|
||||
}
|
||||
|
||||
const schemaItems = [
|
||||
{ position: 1, name: 'Acasă', item: '/' },
|
||||
{ position: 1, name: t('home'), item: '/' },
|
||||
...breadcrumbs.map((item, index) => ({
|
||||
position: index + 2,
|
||||
name: item.label,
|
||||
@@ -96,7 +101,7 @@ export function Breadcrumbs({ items }: { items?: BreadcrumbItem[] }) {
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center text-gray-500 hover:text-primary-600 transition"
|
||||
aria-label="Acasă"
|
||||
aria-label={t('home')}
|
||||
>
|
||||
<HomeIcon className="w-4 h-4" />
|
||||
</Link>
|
||||
|
||||
59
components/layout/LanguageSwitcher.tsx
Normal file
59
components/layout/LanguageSwitcher.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
'use client';
|
||||
|
||||
import {useLocale} from 'next-intl';
|
||||
import {useRouter, usePathname} from '@/i18n/navigation';
|
||||
import {routing} from '@/i18n/routing';
|
||||
import {useState} from 'react';
|
||||
|
||||
export default function LanguageSwitcher() {
|
||||
const locale = useLocale();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const handleLocaleChange = (newLocale: string) => {
|
||||
router.replace(pathname, {locale: newLocale});
|
||||
router.refresh();
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative z-[100]">
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="px-3 py-1 border-2 border-slate-700 font-mono uppercase text-xs hover:border-cyan-500 transition-colors"
|
||||
aria-label="Switch language"
|
||||
>
|
||||
{locale}
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 top-full mt-2 bg-slate-900 border-2 border-slate-700 min-w-[120px] z-[100]">
|
||||
{routing.locales.map((loc: string) => (
|
||||
<button
|
||||
key={loc}
|
||||
onClick={() => handleLocaleChange(loc)}
|
||||
className={`
|
||||
w-full text-left px-4 py-2 font-mono uppercase text-xs
|
||||
border-b border-slate-700 last:border-b-0
|
||||
${locale === loc
|
||||
? 'bg-cyan-900 text-cyan-300'
|
||||
: 'text-slate-400 hover:bg-slate-800'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{loc === 'en' ? 'English' : 'Română'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-40"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user