localization && navbar fix
This commit is contained in:
@@ -8,7 +8,7 @@ services:
|
||||
- media_volume:/app/storage
|
||||
- static_volume:/app/staticfiles
|
||||
env_file:
|
||||
- .env
|
||||
- .env.local
|
||||
ports:
|
||||
- "8000:8000"
|
||||
depends_on:
|
||||
@@ -22,7 +22,7 @@ services:
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data/
|
||||
env_file:
|
||||
- .env
|
||||
- .env.local
|
||||
environment:
|
||||
- POSTGRES_INITDB_ARGS=--auth-host=scram-sha-256
|
||||
restart: unless-stopped
|
||||
@@ -34,9 +34,9 @@ services:
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NEXT_PUBLIC_API_URL=https://links.shareon.kr
|
||||
- NEXT_PUBLIC_API_URL=http://localhost:8000
|
||||
env_file:
|
||||
- .env
|
||||
- .env.local
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- web
|
||||
|
||||
@@ -3,7 +3,6 @@ node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# Production build
|
||||
@@ -81,7 +80,6 @@ __tests__/
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
tsconfig.json
|
||||
|
||||
# Storybook
|
||||
.storybook/
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
FROM node:20-alpine
|
||||
# Этап 1: Установка зависимостей
|
||||
FROM node:20-alpine as deps
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Копирование package.json и package-lock.json
|
||||
COPY package*.json ./
|
||||
|
||||
# Установка зависимостей
|
||||
RUN npm install
|
||||
# Установка зависимостей с очисткой кеша
|
||||
RUN npm ci --omit=dev && npm cache clean --force
|
||||
|
||||
# Этап 2: Сборка приложения
|
||||
FROM node:20-alpine as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Копирование package.json и package-lock.json
|
||||
COPY package*.json ./
|
||||
|
||||
# Установка всех зависимостей (включая dev)
|
||||
RUN npm ci
|
||||
|
||||
# Копирование исходного кода
|
||||
COPY . .
|
||||
@@ -14,6 +26,17 @@ COPY . .
|
||||
# Сборка приложения
|
||||
RUN npm run build
|
||||
|
||||
# Этап 3: Финальный образ
|
||||
FROM node:20-alpine as runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Копирование зависимостей продакшена
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/.next ./.next
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/package*.json ./
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["npm", "start"]
|
||||
@@ -6,6 +6,8 @@ import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import { ProfileCard } from '../../components/ProfileCard'
|
||||
import { CustomizationPanel } from '../../components/CustomizationPanel'
|
||||
import { Navbar } from '../../components/Navbar'
|
||||
import { useLocale } from '../../contexts/LocaleContext'
|
||||
|
||||
interface UserProfile {
|
||||
id: number
|
||||
@@ -72,6 +74,7 @@ interface DesignSettings {
|
||||
}
|
||||
|
||||
export default function DashboardClient() {
|
||||
const { t } = useLocale()
|
||||
const router = useRouter()
|
||||
const [user, setUser] = useState<UserProfile | null>(null)
|
||||
const [groups, setGroups] = useState<Group[]>([])
|
||||
@@ -174,7 +177,7 @@ export default function DashboardClient() {
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(shareUrl)
|
||||
alert('Ссылка скопирована в буфер обмена!')
|
||||
alert(t('common.success') + ': ' + t('dashboard.shareUrl.copied'))
|
||||
} catch (err) {
|
||||
// Fallback для старых браузеров
|
||||
const textArea = document.createElement('textarea')
|
||||
@@ -183,7 +186,7 @@ export default function DashboardClient() {
|
||||
textArea.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(textArea)
|
||||
alert('Ссылка скопирована в буфер обмена!')
|
||||
alert(t('common.success') + ': ' + t('dashboard.shareUrl.copied'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,9 +224,9 @@ export default function DashboardClient() {
|
||||
fetch('/api/customization/settings/', { headers: { Authorization: `Bearer ${token}` } }),
|
||||
])
|
||||
.then(async ([uRes, gRes, lRes, dRes]) => {
|
||||
if (!uRes.ok) throw new Error('Не удалось получить профиль')
|
||||
if (!gRes.ok) throw new Error('Не удалось загрузить группы')
|
||||
if (!lRes.ok) throw new Error('Не удалось загрузить ссылки')
|
||||
if (!uRes.ok) throw new Error(t('common.error') + ': ' + t('auth.networkError'))
|
||||
if (!gRes.ok) throw new Error(t('common.error') + ': ' + t('dashboard.groups'))
|
||||
if (!lRes.ok) throw new Error(t('common.error') + ': ' + t('dashboard.links'))
|
||||
|
||||
const userData = await uRes.json()
|
||||
const groupsData = await gRes.json()
|
||||
@@ -318,7 +321,7 @@ export default function DashboardClient() {
|
||||
await reloadData()
|
||||
}
|
||||
async function handleDeleteGroup(grp: Group) {
|
||||
if (!confirm(`Удалить группу "${grp.name}"?`)) return
|
||||
if (!confirm(t('common.confirm') + ` ${t('group.delete')} "${grp.name}"?`)) return
|
||||
const token = localStorage.getItem('token')!
|
||||
await fetch(`${API}/api/groups/${grp.id}/`, {
|
||||
method: 'DELETE',
|
||||
@@ -372,7 +375,7 @@ export default function DashboardClient() {
|
||||
await reloadData()
|
||||
}
|
||||
async function handleDeleteLink(link: LinkItem) {
|
||||
if (!confirm(`Удалить ссылку "${link.title}"?`)) return
|
||||
if (!confirm(t('common.confirm') + ` ${t('link.delete')} "${link.title}"?`)) return
|
||||
const token = localStorage.getItem('token')!
|
||||
await fetch(`${API}/api/links/${link.id}/`, {
|
||||
method: 'DELETE',
|
||||
@@ -419,11 +422,11 @@ export default function DashboardClient() {
|
||||
setShowProfileModal(false)
|
||||
} else {
|
||||
const error = await res.json()
|
||||
alert('Ошибка: ' + JSON.stringify(error))
|
||||
alert(t('dashboard.error') + JSON.stringify(error))
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) return <div className="flex items-center justify-center h-screen">Загрузка...</div>
|
||||
if (loading) return <div className="flex items-center justify-center h-screen">{t('common.loading')}</div>
|
||||
if (error) return <div className="p-6 text-red-600 max-w-xl mx-auto">{error}</div>
|
||||
|
||||
// Функция расчета оптимального размера изображения для группы
|
||||
@@ -475,9 +478,9 @@ export default function DashboardClient() {
|
||||
const renderListLayout = () => (
|
||||
<div className="card">
|
||||
<div className="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 className="mb-0">Группы ссылок</h5>
|
||||
<h5 className="mb-0">{t('dashboard.groups')}</h5>
|
||||
<button className="btn btn-sm btn-success" onClick={openAddGroup}>
|
||||
<i className="bi bi-plus-lg"></i> Добавить группу
|
||||
<i className="bi bi-plus-lg"></i> {t('dashboard.addGroup')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="list-group list-group-flush">
|
||||
@@ -528,9 +531,9 @@ export default function DashboardClient() {
|
||||
const renderGridLayout = () => (
|
||||
<div>
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 className="mb-0">Группы ссылок</h5>
|
||||
<h5 className="mb-0">{t('dashboard.groups')}</h5>
|
||||
<button className="btn btn-sm btn-success" onClick={openAddGroup}>
|
||||
<i className="bi bi-plus-lg"></i> Добавить группу
|
||||
<i className="bi bi-plus-lg"></i> {t('dashboard.addGroup')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="row g-3">
|
||||
@@ -599,7 +602,7 @@ export default function DashboardClient() {
|
||||
const renderCompactLayout = () => (
|
||||
<div className="compact-layout">
|
||||
<div className="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 className="mb-0">Группы ссылок</h6>
|
||||
<h6 className="mb-0">{t('dashboard.groups')}</h6>
|
||||
<button className="btn btn-sm btn-success" onClick={openAddGroup}>
|
||||
<i className="bi bi-plus-lg"></i>
|
||||
</button>
|
||||
@@ -647,9 +650,9 @@ export default function DashboardClient() {
|
||||
const renderCardsLayout = () => (
|
||||
<div>
|
||||
<div className="d-flex justify-content-between align-items-center mb-4">
|
||||
<h5 className="mb-0">Группы ссылок</h5>
|
||||
<h5 className="mb-0">{t('dashboard.linkGroups')}</h5>
|
||||
<button className="btn btn-success" onClick={openAddGroup}>
|
||||
<i className="bi bi-plus-lg"></i> Добавить группу
|
||||
<i className="bi bi-plus-lg"></i> {t('dashboard.addGroup')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="row g-4">
|
||||
@@ -673,7 +676,7 @@ export default function DashboardClient() {
|
||||
<h5 className="mb-1" style={{ color: designSettings.group_text_color || designSettings.theme_color }}>
|
||||
{group.name}
|
||||
</h5>
|
||||
<small className="text-muted">{group.links.length} ссылок</small>
|
||||
<small className="text-muted">{t('dashboard.linksCount', { count: group.links.length })}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -738,7 +741,7 @@ export default function DashboardClient() {
|
||||
<div className="col-md-3">
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h6 className="mb-0">Группы</h6>
|
||||
<h6 className="mb-0">{t('dashboard.groups')}</h6>
|
||||
<button className="btn btn-sm btn-success mt-2 w-100" onClick={openAddGroup}>
|
||||
<i className="bi bi-plus-lg"></i> Добавить
|
||||
</button>
|
||||
@@ -830,9 +833,9 @@ export default function DashboardClient() {
|
||||
const renderMasonryLayout = () => (
|
||||
<div>
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 className="mb-0">Группы ссылок</h5>
|
||||
<h5 className="mb-0">{t('dashboard.groups')}</h5>
|
||||
<button className="btn btn-success" onClick={openAddGroup}>
|
||||
<i className="bi bi-plus-lg"></i> Добавить группу
|
||||
<i className="bi bi-plus-lg"></i> {t('dashboard.addGroup')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="row g-3">
|
||||
@@ -904,9 +907,9 @@ export default function DashboardClient() {
|
||||
const renderTimelineLayout = () => (
|
||||
<div>
|
||||
<div className="d-flex justify-content-between align-items-center mb-4">
|
||||
<h5 className="mb-0">Группы ссылок</h5>
|
||||
<h5 className="mb-0">{t('dashboard.groups')}</h5>
|
||||
<button className="btn btn-success" onClick={openAddGroup}>
|
||||
<i className="bi bi-plus-lg"></i> Добавить группу
|
||||
<i className="bi bi-plus-lg"></i> {t('dashboard.addGroup')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="timeline">
|
||||
@@ -929,7 +932,7 @@ export default function DashboardClient() {
|
||||
<h6 className="mb-0" style={{ color: designSettings.group_text_color || designSettings.theme_color }}>
|
||||
{group.name}
|
||||
</h6>
|
||||
<small className="text-muted">{group.links.length} ссылок</small>
|
||||
<small className="text-muted">{t('dashboard.linksCount', { count: group.links.length })}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -976,9 +979,9 @@ export default function DashboardClient() {
|
||||
const renderMagazineLayout = () => (
|
||||
<div>
|
||||
<div className="d-flex justify-content-between align-items-center mb-4">
|
||||
<h5 className="mb-0">Группы ссылок</h5>
|
||||
<h5 className="mb-0">{t('dashboard.groups')}</h5>
|
||||
<button className="btn btn-success" onClick={openAddGroup}>
|
||||
<i className="bi bi-plus-lg"></i> Добавить группу
|
||||
<i className="bi bi-plus-lg"></i> {t('dashboard.addGroup')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="magazine-layout">
|
||||
@@ -1006,7 +1009,7 @@ export default function DashboardClient() {
|
||||
{group.name}
|
||||
</h5>
|
||||
<p className="card-text text-muted">
|
||||
{group.links.length} ссылок в этой группе
|
||||
{t('dashboard.linksInGroup', { count: group.links.length })}
|
||||
</p>
|
||||
<div className="links-preview">
|
||||
{group.links.slice(0, 3).map(link => (
|
||||
@@ -1017,7 +1020,7 @@ export default function DashboardClient() {
|
||||
</div>
|
||||
))}
|
||||
{group.links.length > 3 && (
|
||||
<small className="text-muted">и еще {group.links.length - 3}...</small>
|
||||
<small className="text-muted">{t('dashboard.andMore', { count: group.links.length - 3 })}</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
@@ -1087,6 +1090,8 @@ export default function DashboardClient() {
|
||||
style={containerStyle}
|
||||
suppressHydrationWarning={true}
|
||||
>
|
||||
<Navbar />
|
||||
|
||||
{user && (
|
||||
<ProfileCard
|
||||
avatar={user.avatar_url || user.avatar}
|
||||
@@ -1102,20 +1107,20 @@ export default function DashboardClient() {
|
||||
|
||||
<div className="container my-4">
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2>Ваши ссылки</h2>
|
||||
<h2>{t('dashboard.title')}</h2>
|
||||
<div>
|
||||
<span className="me-2">Panel state: {showCustomizationPanel ? 'Open' : 'Closed'}</span>
|
||||
<span className="me-2">Panel state: {showCustomizationPanel ? t('dashboard.panelOpen') : t('dashboard.panelClosed')}</span>
|
||||
<button
|
||||
className="btn btn-outline-info me-2"
|
||||
onClick={openEditProfile}
|
||||
>
|
||||
<i className="bi bi-person-gear"></i> Профиль
|
||||
<i className="bi bi-person-gear"></i> {t('profile.edit')}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-success me-2"
|
||||
onClick={() => setShowShareModal(true)}
|
||||
>
|
||||
<i className="bi bi-share"></i> Поделиться
|
||||
<i className="bi bi-share"></i> {t('dashboard.share')}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-primary"
|
||||
@@ -1126,7 +1131,7 @@ export default function DashboardClient() {
|
||||
console.log('After setting showCustomizationPanel to true')
|
||||
}}
|
||||
>
|
||||
<i className="bi bi-gear"></i> Настройки
|
||||
<i className="bi bi-gear"></i> {t('dashboard.settings')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1141,18 +1146,18 @@ export default function DashboardClient() {
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">{groupModalMode === 'add' ? 'Добавить группу' : 'Редактировать группу'}</h5>
|
||||
<h5 className="modal-title">{groupModalMode === 'add' ? t('group.create') : t('group.edit')}</h5>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close"
|
||||
onClick={() => setShowGroupModal(false)}
|
||||
aria-label="Закрыть"
|
||||
title="Закрыть модальное окно"
|
||||
aria-label={t('common.close')}
|
||||
title={t('common.close')}
|
||||
/>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Название</label>
|
||||
<label className="form-label">{t('group.name')}</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
@@ -1161,17 +1166,17 @@ export default function DashboardClient() {
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Описание (опционально)</label>
|
||||
<label className="form-label">{t('group.description')} ({t('common.optional')})</label>
|
||||
<textarea
|
||||
className="form-control"
|
||||
rows={3}
|
||||
value={groupForm.description}
|
||||
onChange={e => setGroupForm(f => ({ ...f, description: e.target.value }))}
|
||||
placeholder="Краткое описание группы ссылок"
|
||||
placeholder={t('group.descriptionPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Цвет заголовка</label>
|
||||
<label className="form-label">{t('group.color')}</label>
|
||||
<input
|
||||
type="color"
|
||||
className="form-control form-control-color"
|
||||
@@ -1191,7 +1196,7 @@ export default function DashboardClient() {
|
||||
onChange={(e) => setGroupForm(prev => ({ ...prev, is_public: e.target.checked }))}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="groupPublic">
|
||||
Публичная
|
||||
{t('group.public')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1205,7 +1210,7 @@ export default function DashboardClient() {
|
||||
onChange={(e) => setGroupForm(prev => ({ ...prev, is_favorite: e.target.checked }))}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="groupFavorite">
|
||||
Избранная
|
||||
{t('group.favorite')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1219,20 +1224,20 @@ export default function DashboardClient() {
|
||||
onChange={(e) => setGroupForm(prev => ({ ...prev, is_expanded: e.target.checked }))}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="groupExpanded">
|
||||
Развернутая
|
||||
{t('group.expanded')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Иконка группы (опционально)</label>
|
||||
<label className="form-label">{t('group.icon')} ({t('common.optional')})</label>
|
||||
{editingGroup?.icon_url && (
|
||||
<div className="mb-2">
|
||||
<label className="form-label small">Текущая иконка:</label>
|
||||
<label className="form-label small">{t('group.currentIcon')}:</label>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<img
|
||||
src={editingGroup.icon_url}
|
||||
alt="Текущая иконка группы"
|
||||
alt={t('group.currentIcon')}
|
||||
className="img-thumbnail"
|
||||
style={{ width: '32px', height: '32px', objectFit: 'cover' }}
|
||||
/>
|
||||
@@ -1240,7 +1245,7 @@ export default function DashboardClient() {
|
||||
type="button"
|
||||
className="btn btn-outline-danger btn-sm"
|
||||
onClick={() => {
|
||||
if (confirm('Удалить текущую иконку группы?')) {
|
||||
if (confirm(t('group.confirmRemoveIcon'))) {
|
||||
// Удаляем иконку через API
|
||||
fetch(`/api/groups/${editingGroup.id}/`, {
|
||||
method: 'PATCH',
|
||||
@@ -1264,9 +1269,9 @@ export default function DashboardClient() {
|
||||
})
|
||||
}
|
||||
}}
|
||||
title="Убрать иконку группы"
|
||||
title={t('group.removeIcon')}
|
||||
>
|
||||
<i className="bi bi-trash"></i> Убрать иконку
|
||||
<i className="bi bi-trash"></i> {t('group.removeIcon')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1277,17 +1282,17 @@ export default function DashboardClient() {
|
||||
accept="image/*"
|
||||
onChange={e => setGroupForm(f => ({ ...f, iconFile: e.target.files?.[0] || null }))}
|
||||
/>
|
||||
<div className="form-text">Рекомендуемый размер: 32x32 пикселя</div>
|
||||
<div className="form-text">{t('group.iconSizeRecommendation')}</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Фоновое изображение (опционально)</label>
|
||||
<label className="form-label">{t('group.background')} ({t('common.optional')})</label>
|
||||
{editingGroup?.background_image_url && (
|
||||
<div className="mb-2">
|
||||
<label className="form-label small">Текущий фон:</label>
|
||||
<label className="form-label small">{t('group.currentBackground')}:</label>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<img
|
||||
src={editingGroup.background_image_url}
|
||||
alt="Текущий фон группы"
|
||||
alt={t('group.currentBackground')}
|
||||
className="img-thumbnail"
|
||||
style={{ maxWidth: '150px', maxHeight: '80px', objectFit: 'cover' }}
|
||||
/>
|
||||
@@ -1295,7 +1300,7 @@ export default function DashboardClient() {
|
||||
type="button"
|
||||
className="btn btn-outline-danger btn-sm"
|
||||
onClick={() => {
|
||||
if (confirm('Удалить текущий фон группы?')) {
|
||||
if (confirm(t('group.confirmRemoveBackground'))) {
|
||||
// Удаляем фон через API
|
||||
fetch(`/api/groups/${editingGroup.id}/`, {
|
||||
method: 'PATCH',
|
||||
@@ -1319,9 +1324,9 @@ export default function DashboardClient() {
|
||||
})
|
||||
}
|
||||
}}
|
||||
title="Убрать фон группы"
|
||||
title={t('group.removeBackground')}
|
||||
>
|
||||
<i className="bi bi-trash"></i> Убрать фон
|
||||
<i className="bi bi-trash"></i> {t('group.removeBackground')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1334,7 +1339,7 @@ export default function DashboardClient() {
|
||||
/>
|
||||
<div className="alert alert-info mt-2">
|
||||
<i className="bi bi-info-circle me-2"></i>
|
||||
<strong>Рекомендуемый размер изображения:</strong>
|
||||
<strong>{t('group.imageSizeRecommendation')}</strong>
|
||||
<br />
|
||||
{(() => {
|
||||
const linksCount = editingGroup ? editingGroup.links.length : 3 // по умолчанию для новых групп
|
||||
@@ -1350,16 +1355,16 @@ export default function DashboardClient() {
|
||||
</small>
|
||||
<br />
|
||||
<small className="text-muted">
|
||||
💡 <strong>Совет:</strong> Для групп с рамкой используйте изображения с отступами по краям (10-20px)
|
||||
💡 <strong>{t('group.tip')}</strong> {t('group.borderTip')}
|
||||
</small>
|
||||
</div>
|
||||
<div className="form-text">Изображение будет использовано как фон для содержимого группы</div>
|
||||
<div className="form-text">{t('group.backgroundDescription')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button className="btn btn-secondary" onClick={() => setShowGroupModal(false)}>Отмена</button>
|
||||
<button className="btn btn-secondary" onClick={() => setShowGroupModal(false)}>{t('common.cancel')}</button>
|
||||
<button className="btn btn-primary" onClick={handleGroupSubmit}>
|
||||
Сохранить
|
||||
{t('common.save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1371,55 +1376,55 @@ export default function DashboardClient() {
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">{linkModalMode === 'add' ? 'Добавить ссылку' : 'Редактировать ссылку'}</h5>
|
||||
<h5 className="modal-title">{linkModalMode === 'add' ? t('link.create') : t('link.edit')}</h5>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close"
|
||||
onClick={() => setShowLinkModal(false)}
|
||||
aria-label="Закрыть"
|
||||
title="Закрыть модальное окно"
|
||||
aria-label={t('common.close')}
|
||||
title={t('common.close')}
|
||||
/>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Заголовок</label>
|
||||
<label className="form-label">{t('link.title')}</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={linkForm.title}
|
||||
onChange={e => setLinkForm(f => ({ ...f, title: e.target.value }))}
|
||||
placeholder="Название ссылки"
|
||||
placeholder={t('link.titlePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">URL</label>
|
||||
<label className="form-label">{t('link.url')}</label>
|
||||
<input
|
||||
type="url"
|
||||
className="form-control"
|
||||
value={linkForm.url}
|
||||
onChange={e => setLinkForm(f => ({ ...f, url: e.target.value }))}
|
||||
placeholder="https://example.com"
|
||||
placeholder={t('link.urlPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Описание (опционально)</label>
|
||||
<label className="form-label">{t('link.description')} ({t('common.optional')})</label>
|
||||
<textarea
|
||||
className="form-control"
|
||||
rows={2}
|
||||
value={linkForm.description}
|
||||
onChange={e => setLinkForm(f => ({ ...f, description: e.target.value }))}
|
||||
placeholder="Краткое описание ссылки"
|
||||
placeholder={t('link.descriptionPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Иконка (опционально)</label>
|
||||
<label className="form-label">{t('link.icon')} ({t('common.optional')})</label>
|
||||
{editingLink?.icon_url && (
|
||||
<div className="mb-2">
|
||||
<label className="form-label small">Текущая иконка:</label>
|
||||
<label className="form-label small">{t('link.currentIcon')}:</label>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<img
|
||||
src={editingLink.icon_url}
|
||||
alt="Текущая иконка"
|
||||
alt={t('link.currentIcon')}
|
||||
className="img-thumbnail"
|
||||
style={{ width: '32px', height: '32px', objectFit: 'cover' }}
|
||||
/>
|
||||
@@ -1427,7 +1432,7 @@ export default function DashboardClient() {
|
||||
type="button"
|
||||
className="btn btn-outline-danger btn-sm"
|
||||
onClick={() => {
|
||||
if (confirm('Удалить текущую иконку ссылки?')) {
|
||||
if (confirm(t('link.confirmRemoveIcon'))) {
|
||||
// Удаляем иконку через API
|
||||
fetch(`/api/links/${editingLink.id}/`, {
|
||||
method: 'PATCH',
|
||||
@@ -1454,9 +1459,9 @@ export default function DashboardClient() {
|
||||
})
|
||||
}
|
||||
}}
|
||||
title="Убрать иконку ссылки"
|
||||
title={t('link.removeIcon')}
|
||||
>
|
||||
<i className="bi bi-trash"></i> Убрать иконку
|
||||
<i className="bi bi-trash"></i> {t('link.removeIcon')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1467,13 +1472,13 @@ export default function DashboardClient() {
|
||||
accept="image/*"
|
||||
onChange={e => setLinkForm(f => ({ ...f, iconFile: e.target.files?.[0] || null }))}
|
||||
/>
|
||||
<div className="form-text">Рекомендуемый размер: 24x24 пикселя</div>
|
||||
<div className="form-text">{t('link.iconSizeRecommendation')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button className="btn btn-secondary" onClick={() => setShowLinkModal(false)}>Отмена</button>
|
||||
<button className="btn btn-secondary" onClick={() => setShowLinkModal(false)}>{t('common.cancel')}</button>
|
||||
<button className="btn btn-primary" onClick={handleLinkSubmit}>
|
||||
Сохранить
|
||||
{t('common.save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1485,25 +1490,25 @@ export default function DashboardClient() {
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Поделиться страницей</h5>
|
||||
<h5 className="modal-title">{t('share.title')}</h5>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close"
|
||||
onClick={() => setShowShareModal(false)}
|
||||
aria-label="Закрыть"
|
||||
title="Закрыть модальное окно"
|
||||
aria-label={t('common.close')}
|
||||
title={t('common.closeModal')}
|
||||
/>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<p>Ваша публичная страница со ссылками доступна по адресу:</p>
|
||||
<p>{t('share.description')}</p>
|
||||
<div className="input-group mb-3">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={shareUrl || 'Загрузка...'}
|
||||
value={shareUrl || t('share.loading')}
|
||||
readOnly
|
||||
aria-label="URL публичной страницы"
|
||||
title="URL публичной страницы"
|
||||
aria-label={t('share.urlAriaLabel')}
|
||||
title={t('share.urlTitle')}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-outline-primary"
|
||||
@@ -1511,17 +1516,16 @@ export default function DashboardClient() {
|
||||
onClick={copyShareUrl}
|
||||
disabled={!shareUrl}
|
||||
>
|
||||
<i className="bi bi-clipboard"></i> Копировать
|
||||
<i className="bi bi-clipboard"></i> {t('share.copy')}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-muted small">
|
||||
На этой странице будут видны все ваши группы и ссылки.
|
||||
Она обновляется автоматически при изменении данных.
|
||||
{t('share.note')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button className="btn btn-secondary" onClick={() => setShowShareModal(false)}>
|
||||
Закрыть
|
||||
{t('common.close')}
|
||||
</button>
|
||||
{shareUrl && (
|
||||
<a
|
||||
@@ -1530,7 +1534,7 @@ export default function DashboardClient() {
|
||||
rel="noopener noreferrer"
|
||||
className="btn btn-primary"
|
||||
>
|
||||
<i className="bi bi-box-arrow-up-right"></i> Открыть страницу
|
||||
<i className="bi bi-box-arrow-up-right"></i> {t('share.openPage')}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
@@ -1543,20 +1547,20 @@ export default function DashboardClient() {
|
||||
<div className="modal-dialog modal-lg">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Редактировать профиль</h5>
|
||||
<h5 className="modal-title">{t('profile.edit')}</h5>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close"
|
||||
onClick={() => setShowProfileModal(false)}
|
||||
aria-label="Закрыть"
|
||||
title="Закрыть модальное окно"
|
||||
aria-label={t('common.close')}
|
||||
title={t('common.closeModal')}
|
||||
/>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Email</label>
|
||||
<label className="form-label">{t('profile.email')}</label>
|
||||
<input
|
||||
type="email"
|
||||
className="form-control"
|
||||
@@ -1565,7 +1569,7 @@ export default function DashboardClient() {
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Имя</label>
|
||||
<label className="form-label">{t('profile.firstName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
@@ -1574,7 +1578,7 @@ export default function DashboardClient() {
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Фамилия</label>
|
||||
<label className="form-label">{t('profile.lastName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
@@ -1583,7 +1587,7 @@ export default function DashboardClient() {
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Полное имя</label>
|
||||
<label className="form-label">{t('profile.fullName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
@@ -1594,7 +1598,7 @@ export default function DashboardClient() {
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Биография</label>
|
||||
<label className="form-label">{t('profile.bio')}</label>
|
||||
<textarea
|
||||
className="form-control"
|
||||
rows={4}
|
||||
@@ -1603,7 +1607,7 @@ export default function DashboardClient() {
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Аватар</label>
|
||||
<label className="form-label">{t('profile.avatar')}</label>
|
||||
<input
|
||||
type="file"
|
||||
className="form-control"
|
||||
@@ -1612,12 +1616,12 @@ export default function DashboardClient() {
|
||||
/>
|
||||
{user?.avatar && (
|
||||
<div className="mt-2">
|
||||
<img src={user.avatar_url || user.avatar} alt="Текущий аватар" className="img-thumbnail w-25" />
|
||||
<img src={user.avatar_url || user.avatar} alt={t('profile.currentAvatar')} className="img-thumbnail w-25" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Обложка</label>
|
||||
<label className="form-label">{t('profile.cover')}</label>
|
||||
<input
|
||||
type="file"
|
||||
className="form-control"
|
||||
@@ -1634,7 +1638,7 @@ export default function DashboardClient() {
|
||||
className="btn btn-secondary"
|
||||
onClick={() => setShowProfileModal(false)}
|
||||
>
|
||||
Отмена
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
'use client'
|
||||
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useLocale } from '../../contexts/LocaleContext'
|
||||
|
||||
// Динамический импорт клиентского компонента без SSR
|
||||
const DashboardClient = dynamic(() => import('./DashboardClient'), {
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<div className="d-flex justify-content-center align-items-center min-vh-100">
|
||||
<div className="text-center">
|
||||
<div className="spinner-border text-primary" role="status">
|
||||
<span className="visually-hidden">Загрузка...</span>
|
||||
loading: () => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { t } = useLocale()
|
||||
return (
|
||||
<div className="d-flex justify-content-center align-items-center min-vh-100">
|
||||
<div className="text-center">
|
||||
<div className="spinner-border text-primary" role="status">
|
||||
<span className="visually-hidden">{t('common.loading')}</span>
|
||||
</div>
|
||||
<p className="mt-3">{t('dashboard.title')} {t('common.loading')}...</p>
|
||||
</div>
|
||||
<p className="mt-3">Загрузка дашборда...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default function DashboardPage() {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useState, useEffect } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { useLocale } from '../../contexts/LocaleContext'
|
||||
|
||||
type FormData = { username: string; password: string }
|
||||
|
||||
@@ -12,6 +13,7 @@ export default function LoginPage() {
|
||||
const router = useRouter()
|
||||
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<FormData>()
|
||||
const [apiError, setApiError] = useState<string | null>(null)
|
||||
const { t } = useLocale()
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined' && localStorage.getItem('token')) {
|
||||
@@ -32,14 +34,14 @@ export default function LoginPage() {
|
||||
)
|
||||
if (!res.ok) {
|
||||
const json = await res.json()
|
||||
setApiError(json.detail || 'Ошибка входа')
|
||||
setApiError(json.detail || t('auth.loginError'))
|
||||
return
|
||||
}
|
||||
const { access } = await res.json()
|
||||
localStorage.setItem('token', access)
|
||||
router.push('/dashboard')
|
||||
} catch {
|
||||
setApiError('Сетевая ошибка')
|
||||
setApiError(t('auth.networkError'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,8 +60,8 @@ export default function LoginPage() {
|
||||
height="80"
|
||||
className="mb-3"
|
||||
/>
|
||||
<h2 className="fw-bold text-primary">Добро пожаловать!</h2>
|
||||
<p className="text-muted">Войдите в свой аккаунт CatLink</p>
|
||||
<h2 className="fw-bold text-primary">{t('auth.welcome')}</h2>
|
||||
<p className="text-muted">{t('auth.welcomeSubtitle')}</p>
|
||||
</div>
|
||||
|
||||
{apiError && (
|
||||
@@ -70,13 +72,13 @@ export default function LoginPage() {
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="username" className="form-label">Имя пользователя</label>
|
||||
<label htmlFor="username" className="form-label">{t('auth.usernameLabel')}</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
placeholder="Введите имя пользователя"
|
||||
placeholder={t('auth.usernamePlaceholder')}
|
||||
className={`form-control form-control-lg ${errors.username ? 'is-invalid' : ''}`}
|
||||
{...register('username', { required: 'Введите имя пользователя' })}
|
||||
{...register('username', { required: t('auth.usernameRequired') })}
|
||||
/>
|
||||
{errors.username && (
|
||||
<div className="invalid-feedback">{errors.username.message}</div>
|
||||
@@ -84,13 +86,13 @@ export default function LoginPage() {
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="password" className="form-label">Пароль</label>
|
||||
<label htmlFor="password" className="form-label">{t('auth.passwordLabel')}</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Введите пароль"
|
||||
placeholder={t('auth.passwordPlaceholder')}
|
||||
className={`form-control form-control-lg ${errors.password ? 'is-invalid' : ''}`}
|
||||
{...register('password', { required: 'Введите пароль' })}
|
||||
{...register('password', { required: t('auth.passwordRequired') })}
|
||||
/>
|
||||
{errors.password && (
|
||||
<div className="invalid-feedback">{errors.password.message}</div>
|
||||
@@ -105,19 +107,19 @@ export default function LoginPage() {
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<span className="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||
Входим...
|
||||
{t('auth.loggingIn')}
|
||||
</>
|
||||
) : (
|
||||
'Войти'
|
||||
t('auth.loginButton')
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-muted mb-0">
|
||||
Нет аккаунта?{' '}
|
||||
{t('auth.noAccount')}{' '}
|
||||
<Link href="/auth/register" className="text-primary text-decoration-none fw-bold">
|
||||
Зарегистрироваться
|
||||
{t('common.register')}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { useState, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Image from 'next/image'
|
||||
import { useLocale } from '../../contexts/LocaleContext'
|
||||
|
||||
export default function RegisterPage() {
|
||||
const [formData, setFormData] = useState({
|
||||
@@ -17,6 +18,7 @@ export default function RegisterPage() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const router = useRouter()
|
||||
const { t } = useLocale()
|
||||
|
||||
useEffect(() => {
|
||||
// Автозаполнение email из главной страницы
|
||||
@@ -40,7 +42,7 @@ export default function RegisterPage() {
|
||||
setError(null)
|
||||
|
||||
if (formData.password !== formData.password2) {
|
||||
setError('Пароли не совпадают')
|
||||
setError(t('auth.passwordMismatch'))
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
@@ -59,10 +61,10 @@ export default function RegisterPage() {
|
||||
router.push('/auth/login?message=registration_success')
|
||||
} else {
|
||||
const errorData = await response.json()
|
||||
setError(errorData.message || 'Ошибка регистрации')
|
||||
setError(errorData.message || t('auth.registrationError'))
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Ошибка соединения с сервером')
|
||||
setError(t('auth.connectionError'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -84,8 +86,8 @@ export default function RegisterPage() {
|
||||
height={64}
|
||||
className="mb-3"
|
||||
/>
|
||||
<h2 className="fw-bold text-primary">Создать аккаунт</h2>
|
||||
<p className="text-muted">Присоединяйтесь к CatLink сегодня</p>
|
||||
<h2 className="fw-bold text-primary">{t('auth.createAccount')}</h2>
|
||||
<p className="text-muted">{t('auth.createAccountSubtitle')}</p>
|
||||
</div>
|
||||
|
||||
{/* Форма регистрации */}
|
||||
@@ -99,7 +101,7 @@ export default function RegisterPage() {
|
||||
<div className="row">
|
||||
<div className="col-sm-6 mb-3">
|
||||
<label htmlFor="first_name" className="form-label">
|
||||
Имя
|
||||
{t('auth.firstNameLabel')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -113,7 +115,7 @@ export default function RegisterPage() {
|
||||
</div>
|
||||
<div className="col-sm-6 mb-3">
|
||||
<label htmlFor="last_name" className="form-label">
|
||||
Фамилия
|
||||
{t('auth.lastNameLabel')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -129,7 +131,7 @@ export default function RegisterPage() {
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="username" className="form-label">
|
||||
Имя пользователя
|
||||
{t('auth.usernameLabel')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -138,14 +140,14 @@ export default function RegisterPage() {
|
||||
name="username"
|
||||
value={formData.username}
|
||||
onChange={handleChange}
|
||||
placeholder="Только латинские буквы, цифры и _"
|
||||
placeholder={t('auth.usernameHelp')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="email" className="form-label">
|
||||
Email
|
||||
{t('auth.emailLabel')}
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
@@ -160,7 +162,7 @@ export default function RegisterPage() {
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="password" className="form-label">
|
||||
Пароль
|
||||
{t('auth.passwordLabel')}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
@@ -176,7 +178,7 @@ export default function RegisterPage() {
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="password2" className="form-label">
|
||||
Подтвердите пароль
|
||||
{t('auth.passwordConfirmLabel')}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
@@ -198,17 +200,17 @@ export default function RegisterPage() {
|
||||
{loading ? (
|
||||
<>
|
||||
<span className="spinner-border spinner-border-sm me-2"></span>
|
||||
Создание аккаунта...
|
||||
{t('auth.registering')}
|
||||
</>
|
||||
) : (
|
||||
'Создать аккаунт'
|
||||
t('auth.registerButton')
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="text-center">
|
||||
<span className="text-muted">Уже есть аккаунт? </span>
|
||||
<span className="text-muted">{t('auth.haveAccount')} </span>
|
||||
<Link href="/auth/login" className="text-decoration-none">
|
||||
Войти
|
||||
{t('common.login')}
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
@@ -218,13 +220,13 @@ export default function RegisterPage() {
|
||||
{/* Дополнительная информация */}
|
||||
<div className="text-center mt-4">
|
||||
<p className="text-muted small">
|
||||
Создавая аккаунт, вы соглашаетесь с{' '}
|
||||
{t('auth.termsAgreement')}{' '}
|
||||
<Link href="/terms" className="text-decoration-none">
|
||||
Условиями использования
|
||||
{t('auth.termsLink')}
|
||||
</Link>{' '}
|
||||
и{' '}
|
||||
{t('auth.and')}{' '}
|
||||
<Link href="/privacy" className="text-decoration-none">
|
||||
Политикой конфиденциальности
|
||||
{t('auth.privacyLink')}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -358,63 +358,63 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate, user, gr
|
||||
<div className="col-12 mb-4">
|
||||
<label className="form-label fs-5 mb-3">
|
||||
<i className="bi bi-layout-text-window-reverse me-2"></i>
|
||||
Стиль отображения групп и ссылок
|
||||
{t('customization.layout.style')}
|
||||
</label>
|
||||
<div className="row g-3">
|
||||
{[
|
||||
{
|
||||
value: 'list',
|
||||
label: 'Список',
|
||||
labelKey: 'customization.layout.list',
|
||||
icon: 'bi-list-ul',
|
||||
description: 'Классический вертикальный список'
|
||||
descriptionKey: 'customization.layout.listDescription'
|
||||
},
|
||||
{
|
||||
value: 'grid',
|
||||
label: 'Сетка',
|
||||
labelKey: 'customization.layout.grid',
|
||||
icon: 'bi-grid-3x3',
|
||||
description: 'Равномерная сетка карточек'
|
||||
descriptionKey: 'customization.layout.gridDescription'
|
||||
},
|
||||
{
|
||||
value: 'cards',
|
||||
label: 'Карточки',
|
||||
labelKey: 'customization.layout.cards',
|
||||
icon: 'bi-card-heading',
|
||||
description: 'Большие информативные карточки'
|
||||
descriptionKey: 'customization.layout.cardsDescription'
|
||||
},
|
||||
{
|
||||
value: 'compact',
|
||||
label: 'Компактный',
|
||||
labelKey: 'customization.layout.compact',
|
||||
icon: 'bi-layout-text-sidebar',
|
||||
description: 'Компактное отображение без отступов'
|
||||
descriptionKey: 'customization.layout.compactDescription'
|
||||
},
|
||||
{
|
||||
value: 'sidebar',
|
||||
label: 'Боковая панель',
|
||||
labelKey: 'customization.layout.sidebar',
|
||||
icon: 'bi-layout-sidebar',
|
||||
description: 'Навигация в боковой панели'
|
||||
descriptionKey: 'customization.layout.sidebarDescription'
|
||||
},
|
||||
{
|
||||
value: 'masonry',
|
||||
label: 'Кладка',
|
||||
labelKey: 'customization.layout.masonry',
|
||||
icon: 'bi-bricks',
|
||||
description: 'Динамическая сетка разной высоты'
|
||||
descriptionKey: 'customization.layout.masonryDescription'
|
||||
},
|
||||
{
|
||||
value: 'timeline',
|
||||
label: 'Лента времени',
|
||||
labelKey: 'customization.layout.timeline',
|
||||
icon: 'bi-clock-history',
|
||||
description: 'Хронологическое отображение'
|
||||
descriptionKey: 'customization.layout.timelineDescription'
|
||||
},
|
||||
{
|
||||
value: 'magazine',
|
||||
label: 'Журнальный',
|
||||
labelKey: 'customization.layout.magazine',
|
||||
icon: 'bi-newspaper',
|
||||
description: 'Стиль журнала с крупными изображениями'
|
||||
descriptionKey: 'customization.layout.magazineDescription'
|
||||
},
|
||||
{
|
||||
value: 'test-list',
|
||||
label: 'Тестовый список',
|
||||
labelKey: 'customization.layout.testList',
|
||||
icon: 'bi-list-check',
|
||||
description: 'Полный несворачиваемый список всех групп и ссылок'
|
||||
descriptionKey: 'customization.layout.testListDescription'
|
||||
}
|
||||
].map((layout) => (
|
||||
<div key={layout.value} className="col-md-6 col-lg-4">
|
||||
@@ -425,8 +425,8 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate, user, gr
|
||||
>
|
||||
<div className="card-body d-flex flex-column">
|
||||
<i className={`${layout.icon} fs-1 mb-3 text-primary`}></i>
|
||||
<h6 className="card-title mb-2">{layout.label}</h6>
|
||||
<p className="card-text small text-muted flex-grow-1">{layout.description}</p>
|
||||
<h6 className="card-title mb-2">{t(layout.labelKey)}</h6>
|
||||
<p className="card-text small text-muted flex-grow-1">{t(layout.descriptionKey)}</p>
|
||||
{settings.dashboard_layout === layout.value && (
|
||||
<div className="mt-2">
|
||||
<span className="badge bg-primary">
|
||||
@@ -458,7 +458,7 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate, user, gr
|
||||
<div className="tab-pane fade show active">
|
||||
<div className="row">
|
||||
<div className="col-md-6 mb-3">
|
||||
<label className="form-label">Основной цвет темы</label>
|
||||
<label className="form-label">{t('customization.colors.theme')}</label>
|
||||
<div className="input-group">
|
||||
<input
|
||||
type="color"
|
||||
@@ -475,7 +475,7 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate, user, gr
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6 mb-3">
|
||||
<label className="form-label">Цвет фона дашборда</label>
|
||||
<label className="form-label">{t('customization.colors.background')}</label>
|
||||
<div className="input-group">
|
||||
<input
|
||||
type="color"
|
||||
@@ -492,7 +492,7 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate, user, gr
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 mb-3">
|
||||
<label className="form-label">Фоновое изображение</label>
|
||||
<label className="form-label">{t('customization.colors.backgroundImage')}</label>
|
||||
<div className="mb-2">
|
||||
<input
|
||||
type="file"
|
||||
@@ -506,12 +506,12 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate, user, gr
|
||||
}}
|
||||
/>
|
||||
<div className="form-text">
|
||||
Выберите изображение для фона (JPG, PNG, GIF). Если не выбрано - текущее изображение останется без изменений.
|
||||
{t('customization.colors.backgroundImageHelp')}
|
||||
</div>
|
||||
</div>
|
||||
{settings.background_image_url && (
|
||||
<div className="mb-2">
|
||||
<label className="form-label small">Текущее изображение:</label>
|
||||
<label className="form-label small">{t('customization.colors.currentImage')}</label>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<img
|
||||
src={settings.background_image_url}
|
||||
@@ -525,16 +525,16 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate, user, gr
|
||||
onClick={() => {
|
||||
handleChange('background_image_url', '')
|
||||
}}
|
||||
title="Убрать фон"
|
||||
title={t('customization.colors.removeBackground')}
|
||||
>
|
||||
<i className="bi bi-trash"></i> Убрать фон
|
||||
<i className="bi bi-trash"></i> {t('customization.colors.removeBackground')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{backgroundImageFile && (
|
||||
<div className="mb-2">
|
||||
<label className="form-label small">Новое изображение (будет применено после сохранения):</label>
|
||||
<label className="form-label small">{t('customization.colors.newImage')}</label>
|
||||
<div className="text-success">
|
||||
<i className="bi bi-file-earmark-image me-1"></i>
|
||||
{backgroundImageFile.name}
|
||||
@@ -543,7 +543,7 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate, user, gr
|
||||
)}
|
||||
</div>
|
||||
<div className="col-md-4 mb-3">
|
||||
<label className="form-label">Цвет заголовков</label>
|
||||
<label className="form-label">{t('customization.colors.header')}</label>
|
||||
<div className="input-group">
|
||||
<input
|
||||
type="color"
|
||||
@@ -560,7 +560,7 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate, user, gr
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-4 mb-3">
|
||||
<label className="form-label">Цвет названий групп</label>
|
||||
<label className="form-label">{t('customization.colors.group')}</label>
|
||||
<div className="input-group">
|
||||
<input
|
||||
type="color"
|
||||
@@ -577,7 +577,7 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate, user, gr
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-4 mb-3">
|
||||
<label className="form-label">Цвет названий ссылок</label>
|
||||
<label className="form-label">{t('customization.colors.link')}</label>
|
||||
<div className="input-group">
|
||||
<input
|
||||
type="color"
|
||||
@@ -602,7 +602,7 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate, user, gr
|
||||
<div className="tab-pane fade show active">
|
||||
<div className="row">
|
||||
<div className="col-12 mb-3">
|
||||
<h6 className="text-muted">Настройки отображения групп</h6>
|
||||
<h6 className="text-muted">{t('customization.groups.displaySettings')}</h6>
|
||||
</div>
|
||||
<div className="col-12 mb-3">
|
||||
<div className="form-check form-switch">
|
||||
@@ -874,10 +874,10 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate, user, gr
|
||||
<div className="tab-pane fade show active">
|
||||
<div className="row">
|
||||
<div className="col-12 mb-4">
|
||||
<h6 className="text-muted">Настройки шрифтов</h6>
|
||||
<h6 className="text-muted">{t('customization.advanced.fontSettings')}</h6>
|
||||
</div>
|
||||
<div className="col-md-6 mb-3">
|
||||
<label className="form-label">Основной шрифт</label>
|
||||
<label className="form-label">{t('customization.advanced.mainFont')}</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={settings.font_family}
|
||||
@@ -900,7 +900,7 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate, user, gr
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-md-6 mb-3">
|
||||
<label className="form-label">Шрифт заголовков</label>
|
||||
<label className="form-label">{t('customization.advanced.headingFont')}</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={settings.heading_font_family || settings.font_family}
|
||||
@@ -929,7 +929,7 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate, user, gr
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-md-6 mb-3">
|
||||
<label className="form-label">Шрифт основного текста</label>
|
||||
<label className="form-label">{t('customization.advanced.bodyFont')}</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={settings.body_font_family || settings.font_family}
|
||||
@@ -963,11 +963,11 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate, user, gr
|
||||
|
||||
<div className="col-12 mb-4">
|
||||
<hr />
|
||||
<h6 className="text-muted">Дополнительные настройки</h6>
|
||||
<h6 className="text-muted">{t('customization.advanced.additionalSettings')}</h6>
|
||||
</div>
|
||||
|
||||
<div className="col-12 mb-3">
|
||||
<label className="form-label">Дополнительный CSS</label>
|
||||
<label className="form-label">{t('customization.advanced.customCSS')}</label>
|
||||
<textarea
|
||||
className="form-control font-monospace"
|
||||
rows={6}
|
||||
@@ -1183,7 +1183,7 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate, user, gr
|
||||
type="button"
|
||||
className="btn btn-outline-warning"
|
||||
onClick={() => {
|
||||
if (confirm('Вы уверены, что хотите сбросить все настройки интерфейса к значениям по умолчанию? Это действие нельзя отменить.')) {
|
||||
if (confirm(t('customization.advanced.resetConfirm'))) {
|
||||
// Сброс к дефолтным настройкам
|
||||
const defaultSettings = {
|
||||
theme_color: '#007bff',
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import React from 'react';
|
||||
"use client"
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useLocale, Locale } from '../contexts/LocaleContext';
|
||||
|
||||
const LanguageSelector: React.FC = () => {
|
||||
const { locale, setLocale, t } = useLocale();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const languages: Array<{ code: Locale; name: string; flag: string }> = [
|
||||
{ code: 'en', name: 'English', flag: '🇺🇸' },
|
||||
@@ -14,31 +17,43 @@ const LanguageSelector: React.FC = () => {
|
||||
|
||||
const currentLanguage = languages.find(lang => lang.code === locale);
|
||||
|
||||
const handleLanguageChange = (langCode: Locale) => {
|
||||
console.log('Changing language from', locale, 'to', langCode);
|
||||
setLocale(langCode);
|
||||
setIsOpen(false);
|
||||
console.log('Language change completed');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="dropdown">
|
||||
<div className="dropdown position-relative">
|
||||
<button
|
||||
className="btn btn-outline-secondary btn-sm dropdown-toggle d-flex align-items-center"
|
||||
className="btn btn-outline-secondary btn-sm d-flex align-items-center"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
title={t('language.select')}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
title="Выбрать язык"
|
||||
>
|
||||
<span className="me-1">{currentLanguage?.flag}</span>
|
||||
<span className="d-none d-lg-inline">{currentLanguage?.name}</span>
|
||||
<span className="d-none d-lg-inline me-1">{currentLanguage?.name}</span>
|
||||
<i className={`bi bi-chevron-${isOpen ? 'up' : 'down'}`} style={{ fontSize: '0.7em' }}></i>
|
||||
</button>
|
||||
<ul className="dropdown-menu">
|
||||
{languages.map((language) => (
|
||||
<li key={language.code}>
|
||||
<button
|
||||
className={`dropdown-item d-flex align-items-center ${locale === language.code ? 'active' : ''}`}
|
||||
onClick={() => setLocale(language.code)}
|
||||
>
|
||||
<span className="me-2">{language.flag}</span>
|
||||
{language.name}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{isOpen && (
|
||||
<ul
|
||||
className="dropdown-menu show position-absolute end-0"
|
||||
style={{ top: '100%', zIndex: 1000 }}
|
||||
>
|
||||
{languages.map((language) => (
|
||||
<li key={language.code}>
|
||||
<button
|
||||
className={`dropdown-item d-flex align-items-center ${locale === language.code ? 'active' : ''}`}
|
||||
onClick={() => handleLanguageChange(language.code)}
|
||||
>
|
||||
<span className="me-2">{language.flag}</span>
|
||||
{language.name}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,6 @@ import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import Script from 'next/script'
|
||||
import { useLocale } from '../contexts/LocaleContext'
|
||||
import { useTheme } from '../contexts/ThemeContext'
|
||||
import ThemeToggle from './ThemeToggle'
|
||||
import LanguageSelector from './LanguageSelector'
|
||||
import '../layout.css'
|
||||
@@ -23,38 +22,42 @@ interface User {
|
||||
export function LayoutWrapper({ children }: { children: ReactNode }) {
|
||||
const pathname = usePathname() || ''
|
||||
const isPublicUserPage = /^\/[^\/]+$/.test(pathname)
|
||||
const isDashboard = pathname === '/dashboard'
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const router = useRouter()
|
||||
const { t } = useLocale()
|
||||
|
||||
// При монтировании пробуем загрузить профиль
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
fetch('/api/auth/user', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error()
|
||||
return res.json()
|
||||
})
|
||||
.then(data => {
|
||||
// Заполняем полную информацию о пользователе
|
||||
setUser({
|
||||
id: data.id,
|
||||
username: data.username,
|
||||
email: data.email,
|
||||
full_name: data.full_name || '',
|
||||
avatar: data.avatar
|
||||
const checkAuth = async () => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
try {
|
||||
const response = await fetch('/api/auth/user', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
// сбросить некорректный токен
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setUser({
|
||||
id: data.id,
|
||||
username: data.username,
|
||||
email: data.email,
|
||||
full_name: data.full_name || '',
|
||||
avatar: data.avatar
|
||||
})
|
||||
} else {
|
||||
localStorage.removeItem('token')
|
||||
setUser(null)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth check failed:', error)
|
||||
localStorage.removeItem('token')
|
||||
setUser(null)
|
||||
})
|
||||
}
|
||||
}
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
checkAuth()
|
||||
}, [])
|
||||
|
||||
const handleLogout = () => {
|
||||
@@ -67,9 +70,8 @@ export function LayoutWrapper({ children }: { children: ReactNode }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Шапка не выводим на публичных страницах /[username] */}
|
||||
{!isPublicUserPage && (
|
||||
<nav className="navbar navbar-expand-lg theme-bg-secondary fixed-top shadow-sm border-bottom theme-border">
|
||||
<nav className="navbar navbar-expand-lg navbar-light bg-light fixed-top shadow-sm border-bottom">
|
||||
<div className="container">
|
||||
<Link href="/" className="navbar-brand d-flex align-items-center">
|
||||
<Image
|
||||
@@ -82,82 +84,86 @@ export function LayoutWrapper({ children }: { children: ReactNode }) {
|
||||
<span className="fw-bold">CatLink</span>
|
||||
</Link>
|
||||
|
||||
<button
|
||||
className="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navcol-1"
|
||||
title={t('common.menu')}
|
||||
>
|
||||
<span className="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div className="collapse navbar-collapse" id="navcol-1">
|
||||
{/* Убираем navbar-toggler и делаем всё всегда видимым */}
|
||||
<div className="d-flex justify-content-between align-items-center flex-grow-1">
|
||||
{/* Левое меню */}
|
||||
<ul className="navbar-nav me-auto">
|
||||
<ul className="navbar-nav d-flex flex-row me-auto">
|
||||
{user && (
|
||||
<li className="nav-item">
|
||||
<Link href="/dashboard" className="nav-link">
|
||||
<i className="fas fa-tachometer-alt me-1"></i>
|
||||
{t('dashboard.title')}
|
||||
</Link>
|
||||
</li>
|
||||
<>
|
||||
<li className="nav-item me-3">
|
||||
<Link href="/dashboard" className="nav-link">
|
||||
<i className="bi bi-speedometer2 me-1"></i>
|
||||
{t('dashboard.title')}
|
||||
</Link>
|
||||
</li>
|
||||
<li className="nav-item me-3">
|
||||
<Link href="/profile" className="nav-link">
|
||||
<i className="bi bi-person-gear me-1"></i>
|
||||
{t('common.profile')}
|
||||
</Link>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
{/* Правое меню */}
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
{/* Переключатели темы и языка всегда видны */}
|
||||
{/* Компоненты контекстов */}
|
||||
<ThemeToggle />
|
||||
<LanguageSelector />
|
||||
|
||||
{!user ? (
|
||||
{isLoading ? (
|
||||
<div className="spinner-border spinner-border-sm ms-2" role="status">
|
||||
<span className="visually-hidden">{t('common.loading')}</span>
|
||||
</div>
|
||||
) : !user ? (
|
||||
<div className="d-flex gap-2 ms-2">
|
||||
<Link href="/auth/login" className="btn btn-outline-primary btn-sm">
|
||||
<i className="fas fa-sign-in-alt me-1"></i>
|
||||
<i className="bi bi-box-arrow-in-right me-1"></i>
|
||||
<span className="d-none d-sm-inline">{t('common.login')}</span>
|
||||
</Link>
|
||||
<Link href="/auth/register" className="btn btn-primary btn-sm">
|
||||
<i className="fas fa-user-plus me-1"></i>
|
||||
<i className="bi bi-person-plus me-1"></i>
|
||||
<span className="d-none d-sm-inline">{t('common.register')}</span>
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="dropdown ms-2">
|
||||
<button
|
||||
className="btn btn-link text-decoration-none d-flex align-items-center dropdown-toggle"
|
||||
className="btn btn-outline-secondary dropdown-toggle d-flex align-items-center"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<Image
|
||||
src={
|
||||
user.avatar && user.avatar.startsWith('http')
|
||||
? user.avatar
|
||||
: user.avatar
|
||||
? `${process.env.NEXT_PUBLIC_API_URL || 'https://links.shareon.kr'}${user.avatar}`
|
||||
: '/assets/img/avatar-dhg.png'
|
||||
}
|
||||
alt="Avatar"
|
||||
width={32}
|
||||
height={32}
|
||||
className="rounded-circle me-2"
|
||||
/>
|
||||
<span className="text-dark fw-medium d-none d-md-inline">
|
||||
{user.avatar ? (
|
||||
<Image
|
||||
src={
|
||||
user.avatar.startsWith('http')
|
||||
? user.avatar
|
||||
: `http://localhost:8000${user.avatar}`
|
||||
}
|
||||
alt="Avatar"
|
||||
width={24}
|
||||
height={24}
|
||||
className="rounded-circle me-2"
|
||||
/>
|
||||
) : (
|
||||
<i className="bi bi-person-circle me-2"></i>
|
||||
)}
|
||||
<span className="d-none d-md-inline">
|
||||
{user.full_name?.trim() || user.username}
|
||||
</span>
|
||||
</button>
|
||||
<ul className="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<Link href="/profile" className="dropdown-item">
|
||||
<i className="fas fa-user me-2"></i>
|
||||
{t('profile.edit')}
|
||||
<Link href="/dashboard" className="dropdown-item">
|
||||
<i className="bi bi-speedometer2 me-2"></i>
|
||||
{t('dashboard.title')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/dashboard" className="dropdown-item">
|
||||
<i className="fas fa-tachometer-alt me-2"></i>
|
||||
{t('dashboard.title')}
|
||||
<Link href="/profile" className="dropdown-item">
|
||||
<i className="bi bi-person-gear me-2"></i>
|
||||
{t('common.profile')}
|
||||
</Link>
|
||||
</li>
|
||||
<li><hr className="dropdown-divider" /></li>
|
||||
@@ -166,7 +172,7 @@ export function LayoutWrapper({ children }: { children: ReactNode }) {
|
||||
onClick={handleLogout}
|
||||
className="dropdown-item text-danger"
|
||||
>
|
||||
<i className="fas fa-sign-out-alt me-2"></i>
|
||||
<i className="bi bi-box-arrow-right me-2"></i>
|
||||
{t('common.logout')}
|
||||
</button>
|
||||
</li>
|
||||
@@ -179,42 +185,27 @@ export function LayoutWrapper({ children }: { children: ReactNode }) {
|
||||
</nav>
|
||||
)}
|
||||
|
||||
{/* отступ, чтобы контент не прятался под фиксированным хедером */}
|
||||
{!isPublicUserPage && <div className="navbar-spacing" />}
|
||||
{!isPublicUserPage && <div style={{ height: '76px' }} />}
|
||||
|
||||
{children}
|
||||
|
||||
{/* Подвал не выводим на публичных страницах */}
|
||||
{!isPublicUserPage && (
|
||||
<footer className="bg-light footer border-top mt-5">
|
||||
<div className="container py-4">
|
||||
<div className="row">
|
||||
<div className="col-lg-6 text-center text-lg-start mb-2 mb-lg-0">
|
||||
<ul className="list-inline mb-1">
|
||||
<li className="list-inline-item"><Link href="#">About</Link></li>
|
||||
<li className="list-inline-item"><Link href="#">{t('footer.about')}</Link></li>
|
||||
<li className="list-inline-item">⋅</li>
|
||||
<li className="list-inline-item"><Link href="#">Contact</Link></li>
|
||||
<li className="list-inline-item">⋅</li>
|
||||
<li className="list-inline-item"><Link href="#">Terms of Use</Link></li>
|
||||
<li className="list-inline-item">⋅</li>
|
||||
<li className="list-inline-item"><Link href="#">Privacy Policy</Link></li>
|
||||
</ul>
|
||||
<p className="text-muted small mb-0">© CatLink 2025</p>
|
||||
</div>
|
||||
<div className="col-lg-6 text-center text-lg-end">
|
||||
<ul className="list-inline mb-0">
|
||||
<li className="list-inline-item"><Link href="#"><i className="fa fa-facebook fa-lg fa-fw"></i></Link></li>
|
||||
<li className="list-inline-item"><Link href="#"><i className="fa fa-twitter fa-lg fa-fw"></i></Link></li>
|
||||
<li className="list-inline-item"><Link href="#"><i className="fa fa-instagram fa-lg fa-fw"></i></Link></li>
|
||||
<li className="list-inline-item"><Link href="#">{t('footer.contact')}</Link></li>
|
||||
</ul>
|
||||
<p className="text-muted small mb-0">{t('footer.copyright')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)}
|
||||
|
||||
{/* Bootstrap JS */}
|
||||
{/* Bootstrap JS: load after React hydrates to avoid DOM mutations during hydration */}
|
||||
<Script
|
||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
||||
strategy="afterInteractive"
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
// src/components/LayoutWrapper.tsx
|
||||
'use client'
|
||||
|
||||
import React, { ReactNode, useEffect, useState } from 'react'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import Script from 'next/script'
|
||||
import { useLocale } from '../contexts/LocaleContext'
|
||||
import { useTheme } from '../contexts/ThemeContext'
|
||||
import ThemeToggle from './ThemeToggle'
|
||||
import LanguageSelector from './LanguageSelector'
|
||||
import '../layout.css'
|
||||
|
||||
interface User {
|
||||
id: number
|
||||
username: string
|
||||
email: string
|
||||
full_name: string
|
||||
avatar: string | null
|
||||
}
|
||||
|
||||
export function LayoutWrapper({ children }: { children: ReactNode }) {
|
||||
const pathname = usePathname() || ''
|
||||
const isPublicUserPage = /^\/[^\/]+$/.test(pathname)
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const router = useRouter()
|
||||
const { t } = useLocale()
|
||||
|
||||
// При монтировании пробуем загрузить профиль
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
try {
|
||||
const response = await fetch('/api/auth/user', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setUser({
|
||||
id: data.id,
|
||||
username: data.username,
|
||||
email: data.email,
|
||||
full_name: data.full_name || '',
|
||||
avatar: data.avatar
|
||||
})
|
||||
} else {
|
||||
// Токен недействителен
|
||||
localStorage.removeItem('token')
|
||||
setUser(null)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth check failed:', error)
|
||||
localStorage.removeItem('token')
|
||||
setUser(null)
|
||||
}
|
||||
}
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
checkAuth()
|
||||
}, [])
|
||||
|
||||
const handleLogout = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem('token')
|
||||
}
|
||||
setUser(null)
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Шапка отображается на всех страницах кроме публичных /[username] */}
|
||||
{!isPublicUserPage && (
|
||||
<nav className="navbar navbar-expand-lg navbar-light bg-light fixed-top shadow-sm border-bottom">
|
||||
<div className="container">
|
||||
<Link href="/" className="navbar-brand d-flex align-items-center">
|
||||
<Image
|
||||
src="/assets/img/CAT.png"
|
||||
alt="CatLink"
|
||||
width={32}
|
||||
height={32}
|
||||
className="me-2"
|
||||
/>
|
||||
<span className="fw-bold">CatLink</span>
|
||||
</Link>
|
||||
|
||||
<button
|
||||
className="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navcol-1"
|
||||
aria-controls="navcol-1"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<span className="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div className="collapse navbar-collapse" id="navcol-1">
|
||||
{/* Левое меню */}
|
||||
<ul className="navbar-nav me-auto">
|
||||
{user && (
|
||||
<>
|
||||
<li className="nav-item">
|
||||
<Link href="/dashboard" className="nav-link">
|
||||
<i className="bi bi-speedometer2 me-1"></i>
|
||||
Дашборд
|
||||
</Link>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<Link href="/profile" className="nav-link">
|
||||
<i className="bi bi-person-gear me-1"></i>
|
||||
Профиль
|
||||
</Link>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
{/* Правое меню - всегда отображается */}
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
{/* Переключатели темы и языка - всегда видны */}
|
||||
<ThemeToggle />
|
||||
<LanguageSelector />
|
||||
|
||||
{/* Блок авторизации */}
|
||||
{isLoading ? (
|
||||
<div className="spinner-border spinner-border-sm ms-2" role="status">
|
||||
<span className="visually-hidden">Загрузка...</span>
|
||||
</div>
|
||||
) : !user ? (
|
||||
<div className="d-flex gap-2 ms-2">
|
||||
<Link href="/auth/login" className="btn btn-outline-primary btn-sm">
|
||||
<i className="bi bi-box-arrow-in-right me-1"></i>
|
||||
<span className="d-none d-sm-inline">Вход</span>
|
||||
</Link>
|
||||
<Link href="/auth/register" className="btn btn-primary btn-sm">
|
||||
<i className="bi bi-person-plus me-1"></i>
|
||||
<span className="d-none d-sm-inline">Регистрация</span>
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="dropdown ms-2">
|
||||
<button
|
||||
className="btn btn-outline-secondary dropdown-toggle d-flex align-items-center"
|
||||
type="button"
|
||||
id="userDropdown"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
{user.avatar ? (
|
||||
<Image
|
||||
src={
|
||||
user.avatar.startsWith('http')
|
||||
? user.avatar
|
||||
: `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'}${user.avatar}`
|
||||
}
|
||||
alt="Avatar"
|
||||
width={24}
|
||||
height={24}
|
||||
className="rounded-circle me-2"
|
||||
/>
|
||||
) : (
|
||||
<i className="bi bi-person-circle me-2"></i>
|
||||
)}
|
||||
<span className="d-none d-md-inline">
|
||||
{user.full_name?.trim() || user.username}
|
||||
</span>
|
||||
</button>
|
||||
<ul className="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
|
||||
<li>
|
||||
<Link href="/dashboard" className="dropdown-item">
|
||||
<i className="bi bi-speedometer2 me-2"></i>
|
||||
Дашборд
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/profile" className="dropdown-item">
|
||||
<i className="bi bi-person-gear me-2"></i>
|
||||
Профиль
|
||||
</Link>
|
||||
</li>
|
||||
<li><hr className="dropdown-divider" /></li>
|
||||
<li>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="dropdown-item text-danger"
|
||||
>
|
||||
<i className="bi bi-box-arrow-right me-2"></i>
|
||||
Выход
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
)}
|
||||
|
||||
{/* Отступ для фиксированного навбара */}
|
||||
{!isPublicUserPage && <div style={{ height: '76px' }} />}
|
||||
aria-expanded="false"
|
||||
>
|
||||
<Image
|
||||
src={
|
||||
user.avatar && user.avatar.startsWith('http')
|
||||
? user.avatar
|
||||
: user.avatar
|
||||
? `${process.env.NEXT_PUBLIC_API_URL || 'https://links.shareon.kr'}${user.avatar}`
|
||||
: '/assets/img/avatar-dhg.png'
|
||||
}
|
||||
alt="Avatar"
|
||||
width={32}
|
||||
height={32}
|
||||
className="rounded-circle me-2"
|
||||
/>
|
||||
<span className="text-dark fw-medium d-none d-md-inline">
|
||||
{user.full_name?.trim() || user.username}
|
||||
</span>
|
||||
</button>
|
||||
<ul className="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<Link href="/profile" className="dropdown-item">
|
||||
<i className="fas fa-user me-2"></i>
|
||||
{t('profile.edit')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/dashboard" className="dropdown-item">
|
||||
<i className="fas fa-tachometer-alt me-2"></i>
|
||||
{t('dashboard.title')}
|
||||
</Link>
|
||||
</li>
|
||||
<li><hr className="dropdown-divider" /></li>
|
||||
<li>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="dropdown-item text-danger"
|
||||
>
|
||||
<i className="fas fa-sign-out-alt me-2"></i>
|
||||
{t('common.logout')}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
)}
|
||||
|
||||
{/* отступ, чтобы контент не прятался под фиксированным хедером */}
|
||||
{!isPublicUserPage && <div className="navbar-spacing" />}
|
||||
|
||||
{children}
|
||||
|
||||
{/* Подвал не выводим на публичных страницах */}
|
||||
{!isPublicUserPage && (
|
||||
<footer className="bg-light footer border-top mt-5">
|
||||
<div className="container py-4">
|
||||
<div className="row">
|
||||
<div className="col-lg-6 text-center text-lg-start mb-2 mb-lg-0">
|
||||
<ul className="list-inline mb-1">
|
||||
<li className="list-inline-item"><Link href="#">About</Link></li>
|
||||
<li className="list-inline-item">⋅</li>
|
||||
<li className="list-inline-item"><Link href="#">Contact</Link></li>
|
||||
<li className="list-inline-item">⋅</li>
|
||||
<li className="list-inline-item"><Link href="#">Terms of Use</Link></li>
|
||||
<li className="list-inline-item">⋅</li>
|
||||
<li className="list-inline-item"><Link href="#">Privacy Policy</Link></li>
|
||||
</ul>
|
||||
<p className="text-muted small mb-0">© CatLink 2025</p>
|
||||
</div>
|
||||
<div className="col-lg-6 text-center text-lg-end">
|
||||
<ul className="list-inline mb-0">
|
||||
<li className="list-inline-item"><Link href="#"><i className="fa fa-facebook fa-lg fa-fw"></i></Link></li>
|
||||
<li className="list-inline-item"><Link href="#"><i className="fa fa-twitter fa-lg fa-fw"></i></Link></li>
|
||||
<li className="list-inline-item"><Link href="#"><i className="fa fa-instagram fa-lg fa-fw"></i></Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)}
|
||||
|
||||
{/* Bootstrap JS */}
|
||||
{/* Bootstrap JS: load after React hydrates to avoid DOM mutations during hydration */}
|
||||
<Script
|
||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
<Script src="/assets/js/bs-init.js" strategy="afterInteractive" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
28
frontend/linktree-frontend/src/app/components/Navbar.tsx
Normal file
28
frontend/linktree-frontend/src/app/components/Navbar.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
|
||||
import { useLocale } from '../contexts/LocaleContext'
|
||||
import LanguageSelector from './LanguageSelector'
|
||||
import ThemeToggle from './ThemeToggle'
|
||||
|
||||
interface NavbarProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function Navbar({ className = '' }: NavbarProps) {
|
||||
const { t } = useLocale()
|
||||
|
||||
return (
|
||||
<nav className={`navbar navbar-expand-lg navbar-light bg-light border-bottom ${className}`}>
|
||||
<div className="container-fluid">
|
||||
<a className="navbar-brand fw-bold" href="/">
|
||||
🐱 CatLink
|
||||
</a>
|
||||
|
||||
<div className="navbar-nav ms-auto d-flex flex-row align-items-center gap-3">
|
||||
<LanguageSelector />
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import React from 'react';
|
||||
import { ThemeProvider } from '../contexts/ThemeContext';
|
||||
import { LocaleProvider } from '../contexts/LocaleContext';
|
||||
|
||||
123
frontend/linktree-frontend/src/app/components/ThemeToggle.css
Normal file
123
frontend/linktree-frontend/src/app/components/ThemeToggle.css
Normal file
@@ -0,0 +1,123 @@
|
||||
.theme-toggle {
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 30px;
|
||||
background-color: var(--bs-light);
|
||||
border: 2px solid var(--bs-border-color);
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.theme-toggle.dark {
|
||||
background-color: var(--bs-dark);
|
||||
border-color: var(--bs-secondary);
|
||||
}
|
||||
|
||||
.theme-toggle-slider {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
background: linear-gradient(45deg, #ffd700, #ffed4e);
|
||||
border-radius: 50%;
|
||||
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.theme-toggle.dark .theme-toggle-slider {
|
||||
transform: translateX(30px);
|
||||
background: linear-gradient(45deg, #4a5568, #718096);
|
||||
}
|
||||
|
||||
.theme-toggle-icon {
|
||||
font-size: 12px;
|
||||
transition: all 0.4s ease;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.theme-toggle .theme-toggle-icon {
|
||||
animation: iconSpin 0.4s ease-in-out;
|
||||
}
|
||||
|
||||
.theme-toggle.dark .theme-toggle-icon {
|
||||
animation: iconSpin 0.4s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes iconSpin {
|
||||
0% {
|
||||
transform: rotate(0deg) scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(180deg) scale(0.8);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Дополнительные эффекты при наведении */
|
||||
.theme-toggle:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.theme-toggle:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Светлая тема - эффект солнечных лучей */
|
||||
.theme-toggle:not(.dark)::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 13px;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
background: #ffd700;
|
||||
border-radius: 50%;
|
||||
box-shadow:
|
||||
0 -8px 0 #ffd700,
|
||||
6px -6px 0 #ffd700,
|
||||
8px 0 0 #ffd700,
|
||||
6px 6px 0 #ffd700,
|
||||
0 8px 0 #ffd700,
|
||||
-6px 6px 0 #ffd700,
|
||||
-8px 0 0 #ffd700,
|
||||
-6px -6px 0 #ffd700;
|
||||
animation: sunRays 2s linear infinite;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@keyframes sunRays {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Темная тема - звездочки */
|
||||
.theme-toggle.dark::after {
|
||||
content: '✨';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 8px;
|
||||
transform: translateY(-50%);
|
||||
font-size: 8px;
|
||||
animation: twinkle 1.5s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes twinkle {
|
||||
0% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,35 @@
|
||||
'use client'
|
||||
"use client"
|
||||
|
||||
import React from 'react'
|
||||
import { useTheme } from '../contexts/ThemeContext'
|
||||
import { useLocale } from '../contexts/LocaleContext'
|
||||
import React from 'react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { useLocale } from '../contexts/LocaleContext';
|
||||
import './ThemeToggle.css';
|
||||
|
||||
interface ThemeToggleProps {
|
||||
className?: string
|
||||
showLabel?: boolean
|
||||
}
|
||||
|
||||
export const ThemeToggle: React.FC<ThemeToggleProps> = ({
|
||||
className = '',
|
||||
showLabel = false
|
||||
}) => {
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
const { t } = useLocale()
|
||||
const ThemeToggle: React.FC = () => {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<button
|
||||
<div
|
||||
className={`theme-toggle ${theme === 'dark' ? 'dark' : ''}`}
|
||||
onClick={toggleTheme}
|
||||
className={`btn btn-outline-secondary btn-sm d-flex align-items-center ${className}`}
|
||||
title={theme === 'light' ? t('theme.dark') : t('theme.light')}
|
||||
title={t('theme.toggle')}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
toggleTheme();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i className={`fas ${theme === 'light' ? 'fa-moon' : 'fa-sun'} ${showLabel ? 'me-2' : ''}`}></i>
|
||||
{showLabel && (
|
||||
<span className="d-none d-lg-inline">
|
||||
{theme === 'light' ? t('theme.dark') : t('theme.light')}
|
||||
<div className="theme-toggle-slider">
|
||||
<span className="theme-toggle-icon">
|
||||
{theme === 'dark' ? '🌙' : '☀️'}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeToggle
|
||||
export default ThemeToggle;
|
||||
@@ -1,21 +1,23 @@
|
||||
import Link from 'next/link'
|
||||
import { useLocale } from '../contexts/LocaleContext'
|
||||
|
||||
export function Footer() {
|
||||
const { t } = useLocale()
|
||||
return (
|
||||
<footer className="bg-light footer py-5 border-top">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-lg-6 text-center text-lg-start mb-3 mb-lg-0">
|
||||
<ul className="list-inline mb-2">
|
||||
<li className="list-inline-item"><Link href="#">About</Link></li>
|
||||
<li className="list-inline-item"><Link href="#">{t('footer.about')}</Link></li>
|
||||
<li className="list-inline-item"><span>⋅</span></li>
|
||||
<li className="list-inline-item"><Link href="#">Contact</Link></li>
|
||||
<li className="list-inline-item"><Link href="#">{t('footer.contact')}</Link></li>
|
||||
<li className="list-inline-item"><span>⋅</span></li>
|
||||
<li className="list-inline-item"><Link href="#">Terms of Use</Link></li>
|
||||
<li className="list-inline-item"><Link href="#">{t('footer.terms')}</Link></li>
|
||||
<li className="list-inline-item"><span>⋅</span></li>
|
||||
<li className="list-inline-item"><Link href="#">Privacy Policy</Link></li>
|
||||
<li className="list-inline-item"><Link href="#">{t('footer.privacy')}</Link></li>
|
||||
</ul>
|
||||
<p className="text-muted small mb-0">© CatLink 2025. Все права защищены.</p>
|
||||
<p className="text-muted small mb-0">{t('footer.copyright')}</p>
|
||||
</div>
|
||||
<div className="col-lg-6 text-center text-lg-end">
|
||||
<ul className="list-inline mb-0">
|
||||
|
||||
@@ -27,6 +27,88 @@
|
||||
"common.yes": "Yes",
|
||||
"common.no": "No",
|
||||
"common.menu": "Menu",
|
||||
"common.optional": "optional",
|
||||
"common.closeModal": "Close modal",
|
||||
|
||||
"share.title": "Share Page",
|
||||
"share.description": "Your public page with links is available at:",
|
||||
"share.loading": "Loading...",
|
||||
"share.urlAriaLabel": "Public page URL",
|
||||
"share.urlTitle": "Public page URL",
|
||||
"share.copy": "Copy",
|
||||
"share.note": "All your groups and links will be visible on this page. It updates automatically when data changes.",
|
||||
"share.openPage": "Open Page",
|
||||
|
||||
"home.title": "Your links. Your style. Your CatLink.",
|
||||
"home.subtitle": "Create a beautiful personal page with all your important links in one place. Share professionally and stylishly!",
|
||||
"home.emailPlaceholder": "Enter your email",
|
||||
"home.startFree": "Start Free",
|
||||
"home.haveAccount": "Already have an account?",
|
||||
"home.signIn": "Sign In",
|
||||
|
||||
"home.features.title": "Why Choose CatLink?",
|
||||
"home.features.subtitle": "Simple and powerful tool for creating your digital presence",
|
||||
|
||||
"home.features.links.title": "One URL — All Links",
|
||||
"home.features.links.description": "Gather all important links in one place. Social media, portfolio, contacts — everything under one address.",
|
||||
|
||||
"home.features.customization.title": "Customization",
|
||||
"home.features.customization.description": "Customize colors, fonts, layouts. Create a unique style that reflects your personality or brand.",
|
||||
|
||||
"home.features.analytics.title": "Analytics",
|
||||
"home.features.analytics.description": "Track clicks, link popularity and visitor activity. Understand your audience better.",
|
||||
|
||||
"home.useCases.title": "For Everyone",
|
||||
"home.useCases.bloggers": "Bloggers",
|
||||
"home.useCases.bloggersDescription": "Gather all social media",
|
||||
"home.useCases.business": "Business",
|
||||
"home.useCases.businessDescription": "Show services and contacts",
|
||||
"home.useCases.musicians": "Musicians",
|
||||
"home.useCases.musiciansDescription": "Share your creativity",
|
||||
"home.useCases.photographers": "Photographers",
|
||||
"home.useCases.photographersDescription": "Show your portfolio",
|
||||
"home.useCases.exampleTitle": "Your personal page",
|
||||
"home.useCases.exampleSubtitle": "Example of your page",
|
||||
"home.useCases.personalSite": "Personal website",
|
||||
|
||||
"home.cta.title": "Ready to Start?",
|
||||
"home.cta.subtitle": "Join thousands of users who have already created their perfect link page",
|
||||
"home.cta.createFree": "Create Free Account",
|
||||
"home.cta.haveAccount": "I have an account",
|
||||
"home.cta.features": "Free forever • No limits • Quick setup",
|
||||
|
||||
"auth.welcome": "Welcome!",
|
||||
"auth.welcomeSubtitle": "Sign in to your CatLink account",
|
||||
"auth.createAccount": "Create Account",
|
||||
"auth.createAccountSubtitle": "Join CatLink today",
|
||||
"auth.usernameLabel": "Username",
|
||||
"auth.usernamePlaceholder": "Enter your username",
|
||||
"auth.usernameRequired": "Please enter username",
|
||||
"auth.usernameHelp": "Only Latin letters, numbers and _",
|
||||
"auth.passwordLabel": "Password",
|
||||
"auth.passwordPlaceholder": "Enter your password",
|
||||
"auth.passwordRequired": "Please enter password",
|
||||
"auth.passwordConfirmLabel": "Confirm Password",
|
||||
"auth.passwordConfirmRequired": "Please confirm password",
|
||||
"auth.passwordMismatch": "Passwords do not match",
|
||||
"auth.emailLabel": "Email",
|
||||
"auth.emailRequired": "Please enter email",
|
||||
"auth.firstNameLabel": "First Name",
|
||||
"auth.lastNameLabel": "Last Name",
|
||||
"auth.loginButton": "Sign In",
|
||||
"auth.registerButton": "Create Account",
|
||||
"auth.loggingIn": "Signing in...",
|
||||
"auth.registering": "Creating account...",
|
||||
"auth.noAccount": "Don't have an account?",
|
||||
"auth.haveAccount": "Already have an account?",
|
||||
"auth.loginError": "Login error",
|
||||
"auth.networkError": "Network error",
|
||||
"auth.registrationError": "Registration error",
|
||||
"auth.connectionError": "Server connection error",
|
||||
"auth.termsAgreement": "By creating an account, you agree to the",
|
||||
"auth.termsLink": "Terms of Service",
|
||||
"auth.privacyLink": "Privacy Policy",
|
||||
"auth.and": "and",
|
||||
|
||||
"auth.login.title": "Login",
|
||||
"auth.login.email": "Email",
|
||||
@@ -50,16 +132,26 @@
|
||||
"dashboard.welcome": "Welcome, {{name}}!",
|
||||
"dashboard.groups": "Groups",
|
||||
"dashboard.links": "Links",
|
||||
"dashboard.settings": "Settings",
|
||||
"dashboard.settings": "Settings",
|
||||
"dashboard.share": "Share",
|
||||
"dashboard.customize": "Customize",
|
||||
"dashboard.panelOpen": "Open",
|
||||
"dashboard.panelClosed": "Closed",
|
||||
"dashboard.error": "Error: ",
|
||||
"dashboard.linkGroups": "Link Groups",
|
||||
"dashboard.andMore": "and {{count}} more...",
|
||||
"dashboard.linksCount": "{{count}} links",
|
||||
"dashboard.linksInGroup": "{{count}} links in this group",
|
||||
"dashboard.addGroup": "Add Group",
|
||||
"dashboard.addLink": "Add Link",
|
||||
"dashboard.noGroups": "No groups yet",
|
||||
"dashboard.noLinks": "No links yet",
|
||||
"dashboard.createFirst": "Create your first",
|
||||
"dashboard.shareUrl.copied": "Link copied to clipboard",
|
||||
|
||||
"group.create": "Create Group",
|
||||
"group.edit": "Edit Group",
|
||||
"group.delete": "delete group",
|
||||
"group.name": "Group name",
|
||||
"group.description": "Description",
|
||||
"group.icon": "Icon",
|
||||
@@ -70,9 +162,20 @@
|
||||
"group.expanded": "Expanded by default",
|
||||
"group.removeIcon": "Remove icon",
|
||||
"group.removeBackground": "Remove background",
|
||||
"group.descriptionPlaceholder": "Brief description of link group",
|
||||
"group.currentIcon": "Current icon",
|
||||
"group.confirmRemoveIcon": "Remove current group icon?",
|
||||
"group.iconSizeRecommendation": "Recommended size: 32x32 pixels",
|
||||
"group.currentBackground": "Current background",
|
||||
"group.confirmRemoveBackground": "Remove current group background?",
|
||||
"group.imageSizeRecommendation": "Recommended image size:",
|
||||
"group.tip": "Tip:",
|
||||
"group.borderTip": "For groups with borders, use images with edge padding (10-20px)",
|
||||
"group.backgroundDescription": "Image will be used as background for group content",
|
||||
|
||||
"link.create": "Create Link",
|
||||
"link.edit": "Edit Link",
|
||||
"link.delete": "delete link",
|
||||
"link.title": "Link title",
|
||||
"link.url": "URL",
|
||||
"link.description": "Description",
|
||||
@@ -80,16 +183,23 @@
|
||||
"link.removeIcon": "Remove icon",
|
||||
"link.public": "Public",
|
||||
"link.featured": "Featured",
|
||||
"link.titlePlaceholder": "Link title",
|
||||
"link.descriptionPlaceholder": "Brief description of the link",
|
||||
"link.urlPlaceholder": "https://example.com",
|
||||
"link.currentIcon": "Current icon",
|
||||
"link.confirmRemoveIcon": "Remove current link icon?",
|
||||
"link.iconSizeRecommendation": "Recommended size: 24x24 pixels",
|
||||
|
||||
"profile.edit": "Edit Profile",
|
||||
"profile.username": "Username",
|
||||
"profile.email": "Email",
|
||||
"profile.firstName": "First name",
|
||||
"profile.lastName": "Last name",
|
||||
"profile.fullName": "Full name",
|
||||
"profile.bio": "Bio",
|
||||
"profile.firstName": "First Name",
|
||||
"profile.lastName": "Last Name",
|
||||
"profile.fullName": "Full Name",
|
||||
"profile.bio": "Biography",
|
||||
"profile.avatar": "Avatar",
|
||||
"profile.cover": "Cover image",
|
||||
"profile.cover": "Cover",
|
||||
"profile.currentAvatar": "Current avatar",
|
||||
"profile.removeAvatar": "Remove avatar",
|
||||
"profile.removeCover": "Remove cover",
|
||||
|
||||
@@ -103,17 +213,29 @@
|
||||
|
||||
"customization.layout.style": "Display style for groups and links",
|
||||
"customization.layout.list": "List",
|
||||
"customization.layout.grid": "Grid",
|
||||
"customization.layout.grid": "Grid",
|
||||
"customization.layout.cards": "Cards",
|
||||
"customization.layout.compact": "Compact",
|
||||
"customization.layout.masonry": "Masonry",
|
||||
"customization.layout.timeline": "Timeline",
|
||||
"customization.layout.magazine": "Magazine",
|
||||
|
||||
"customization.colors.theme": "Theme color",
|
||||
"customization.layout.sidebar": "Sidebar",
|
||||
"customization.layout.testList": "Test List",
|
||||
"customization.layout.listDescription": "Classic vertical list",
|
||||
"customization.layout.gridDescription": "Uniform grid of cards",
|
||||
"customization.layout.cardsDescription": "Large informative cards",
|
||||
"customization.layout.compactDescription": "Compact display without spacing",
|
||||
"customization.layout.sidebarDescription": "Navigation in sidebar",
|
||||
"customization.layout.masonryDescription": "Dynamic grid with varying heights",
|
||||
"customization.layout.timelineDescription": "Chronological display",
|
||||
"customization.layout.magazineDescription": "Magazine style with large images",
|
||||
"customization.layout.testListDescription": "Full non-collapsible list of all groups and links", "customization.colors.theme": "Theme color",
|
||||
"customization.colors.background": "Background color",
|
||||
"customization.colors.backgroundImage": "Background image",
|
||||
"customization.colors.removeBackground": "Remove background",
|
||||
"customization.colors.backgroundImageHelp": "Select background image (JPG, PNG, GIF). If not selected - current image will remain unchanged.",
|
||||
"customization.colors.currentImage": "Current image:",
|
||||
"customization.colors.newImage": "New image (will be applied after saving):",
|
||||
"customization.colors.header": "Header text color",
|
||||
"customization.colors.group": "Group text color",
|
||||
"customization.colors.link": "Link text color",
|
||||
@@ -121,9 +243,13 @@
|
||||
"customization.groups.showIcons": "Show group icons",
|
||||
"customization.groups.showLinks": "Show link icons",
|
||||
"customization.groups.defaultExpanded": "Groups expanded by default",
|
||||
"customization.groups.showTitle": "Show groups title",
|
||||
"customization.groups.showTitle": "Show group titles",
|
||||
"customization.groups.displaySettings": "Group display settings",
|
||||
|
||||
"customization.advanced.fonts": "Font settings",
|
||||
"customization.advanced.fontSettings": "Font settings",
|
||||
"customization.advanced.additionalSettings": "Additional settings",
|
||||
"customization.advanced.resetConfirm": "Are you sure you want to reset all interface settings to default values? This action cannot be undone.",
|
||||
"customization.advanced.mainFont": "Main font",
|
||||
"customization.advanced.headingFont": "Heading font",
|
||||
"customization.advanced.bodyFont": "Body font",
|
||||
@@ -191,5 +317,11 @@
|
||||
"language.ru": "Русский",
|
||||
"language.ko": "한국어",
|
||||
"language.zh": "中文",
|
||||
"language.ja": "日本語"
|
||||
"language.ja": "日本語",
|
||||
|
||||
"footer.about": "About",
|
||||
"footer.contact": "Contact",
|
||||
"footer.terms": "Terms of Service",
|
||||
"footer.privacy": "Privacy Policy",
|
||||
"footer.copyright": "© CatLink 2025. All rights reserved."
|
||||
}
|
||||
@@ -28,6 +28,77 @@
|
||||
"common.no": "いいえ",
|
||||
"common.menu": "メニュー",
|
||||
|
||||
"home.title": "あなたのリンク。あなたのスタイル。あなたのCatLink。",
|
||||
"home.subtitle": "大切なリンクをすべて一箇所に集めた美しい個人ページを作成しましょう。プロフェッショナルでスタイリッシュにシェア!",
|
||||
"home.emailPlaceholder": "メールアドレスを入力",
|
||||
"home.startFree": "無料で始める",
|
||||
"home.haveAccount": "既にアカウントをお持ちですか?",
|
||||
"home.signIn": "サインイン",
|
||||
|
||||
"home.features.title": "なぜCatLinkを選ぶのか?",
|
||||
"home.features.subtitle": "デジタルプレゼンスを作るためのシンプルで強力なツール",
|
||||
|
||||
"home.features.links.title": "一つのURL — すべてのリンク",
|
||||
"home.features.links.description": "重要なリンクをすべて一箇所に集めます。ソーシャルメディア、ポートフォリオ、連絡先 — すべてが一つのアドレスに。",
|
||||
|
||||
"home.features.customization.title": "カスタマイゼーション",
|
||||
"home.features.customization.description": "色、フォント、レイアウトをカスタマイズ。あなたの個性やブランドを反映するユニークなスタイルを作成しましょう。",
|
||||
|
||||
"home.features.analytics.title": "アナリティクス",
|
||||
"home.features.analytics.description": "クリック数、リンクの人気度、訪問者の活動を追跡。あなたのオーディエンスをより良く理解しましょう。",
|
||||
|
||||
"home.useCases.title": "すべての人のために",
|
||||
"home.useCases.bloggers": "ブロガー",
|
||||
"home.useCases.bloggersDescription": "すべてのソーシャルメディアを集約",
|
||||
"home.useCases.business": "ビジネス",
|
||||
"home.useCases.businessDescription": "サービスと連絡先を表示",
|
||||
"home.useCases.musicians": "ミュージシャン",
|
||||
"home.useCases.musiciansDescription": "あなたの作品をシェア",
|
||||
"home.useCases.photographers": "フォトグラファー",
|
||||
"home.useCases.photographersDescription": "ポートフォリオを表示",
|
||||
"home.useCases.exampleTitle": "個人ページ",
|
||||
"home.useCases.exampleSubtitle": "ページの例",
|
||||
"home.useCases.personalSite": "個人ウェブサイト",
|
||||
|
||||
"home.cta.title": "始める準備はできていますか?",
|
||||
"home.cta.subtitle": "完璧なリンクページを作成した数千人のユーザーに参加しましょう",
|
||||
"home.cta.createFree": "無料アカウントを作成",
|
||||
"home.cta.haveAccount": "アカウントを持っています",
|
||||
"home.cta.features": "永続無料 • 制限なし • 素早いセットアップ",
|
||||
|
||||
"auth.welcome": "ようこそ!",
|
||||
"auth.welcomeSubtitle": "CatLinkアカウントにサインイン",
|
||||
"auth.createAccount": "アカウント作成",
|
||||
"auth.createAccountSubtitle": "今日CatLinkに参加しましょう",
|
||||
"auth.usernameLabel": "ユーザー名",
|
||||
"auth.usernamePlaceholder": "ユーザー名を入力",
|
||||
"auth.usernameRequired": "ユーザー名を入力してください",
|
||||
"auth.usernameHelp": "ラテン文字、数字、_のみ",
|
||||
"auth.passwordLabel": "パスワード",
|
||||
"auth.passwordPlaceholder": "パスワードを入力",
|
||||
"auth.passwordRequired": "パスワードを入力してください",
|
||||
"auth.passwordConfirmLabel": "パスワード確認",
|
||||
"auth.passwordConfirmRequired": "パスワードを確認してください",
|
||||
"auth.passwordMismatch": "パスワードが一致しません",
|
||||
"auth.emailLabel": "メール",
|
||||
"auth.emailRequired": "メールを入力してください",
|
||||
"auth.firstNameLabel": "名前",
|
||||
"auth.lastNameLabel": "苗字",
|
||||
"auth.loginButton": "サインイン",
|
||||
"auth.registerButton": "アカウント作成",
|
||||
"auth.loggingIn": "サインイン中...",
|
||||
"auth.registering": "アカウント作成中...",
|
||||
"auth.noAccount": "アカウントをお持ちでない方",
|
||||
"auth.haveAccount": "既にアカウントをお持ちですか?",
|
||||
"auth.loginError": "ログインエラー",
|
||||
"auth.networkError": "ネットワークエラー",
|
||||
"auth.registrationError": "登録エラー",
|
||||
"auth.connectionError": "サーバー接続エラー",
|
||||
"auth.termsAgreement": "アカウントを作成することで、以下に同意したことになります",
|
||||
"auth.termsLink": "利用規約",
|
||||
"auth.privacyLink": "プライバシーポリシー",
|
||||
"auth.and": "および",
|
||||
|
||||
"auth.login.title": "ログイン",
|
||||
"auth.login.email": "メール",
|
||||
"auth.login.password": "パスワード",
|
||||
@@ -51,15 +122,17 @@
|
||||
"dashboard.groups": "グループ",
|
||||
"dashboard.links": "リンク",
|
||||
"dashboard.settings": "設定",
|
||||
"dashboard.customize": "カスタマイズ",
|
||||
"dashboard.customize": "シェア",
|
||||
"dashboard.addGroup": "グループ追加",
|
||||
"dashboard.addLink": "リンク追加",
|
||||
"dashboard.noGroups": "グループがありません",
|
||||
"dashboard.noLinks": "リンクがありません",
|
||||
"dashboard.noGroups": "まだグループがありません",
|
||||
"dashboard.noLinks": "まだリンクがありません",
|
||||
"dashboard.createFirst": "最初の作成",
|
||||
"dashboard.shareUrl.copied": "リンクがクリップボードにコピーされました",
|
||||
|
||||
"group.create": "グループ作成",
|
||||
"group.edit": "グループ編集",
|
||||
"group.delete": "グループ削除",
|
||||
"group.name": "グループ名",
|
||||
"group.description": "説明",
|
||||
"group.icon": "アイコン",
|
||||
@@ -73,6 +146,7 @@
|
||||
|
||||
"link.create": "リンク作成",
|
||||
"link.edit": "リンク編集",
|
||||
"link.delete": "リンク削除",
|
||||
"link.title": "リンクタイトル",
|
||||
"link.url": "URL",
|
||||
"link.description": "説明",
|
||||
@@ -191,5 +265,11 @@
|
||||
"language.ru": "Русский",
|
||||
"language.ko": "한국어",
|
||||
"language.zh": "中文",
|
||||
"language.ja": "日本語"
|
||||
"language.ja": "日本語",
|
||||
|
||||
"footer.about": "会社概要",
|
||||
"footer.contact": "お問い合わせ",
|
||||
"footer.terms": "利用規約",
|
||||
"footer.privacy": "プライバシーポリシー",
|
||||
"footer.copyright": "© CatLink 2025. 全ての権利を保有。"
|
||||
}
|
||||
@@ -28,6 +28,77 @@
|
||||
"common.no": "아니오",
|
||||
"common.menu": "메뉴",
|
||||
|
||||
"home.title": "당신의 링크. 당신의 스타일. 당신의 CatLink.",
|
||||
"home.subtitle": "모든 중요한 링크를 한 곳에서 관리할 수 있는 아름다운 개인 페이지를 만드세요. 전문적이고 스타일리시하게 공유하세요!",
|
||||
"home.emailPlaceholder": "이메일을 입력하세요",
|
||||
"home.startFree": "무료 시작",
|
||||
"home.haveAccount": "이미 계정이 있으신가요?",
|
||||
"home.signIn": "로그인",
|
||||
|
||||
"home.features.title": "왜 CatLink를 선택해야 할까요?",
|
||||
"home.features.subtitle": "디지털 존재감을 만들기 위한 간단하고 강력한 도구",
|
||||
|
||||
"home.features.links.title": "하나의 URL — 모든 링크",
|
||||
"home.features.links.description": "모든 중요한 링크를 한 곳에 모아보세요. 소셜 미디어, 포트폴리오, 연락처 — 모든 것이 하나의 주소에.",
|
||||
|
||||
"home.features.customization.title": "커스터마이징",
|
||||
"home.features.customization.description": "색상, 글꼴, 레이아웃을 맞춤 설정하세요. 당신의 개성이나 브랜드를 반영하는 독특한 스타일을 만드세요.",
|
||||
|
||||
"home.features.analytics.title": "분석",
|
||||
"home.features.analytics.description": "클릭 수, 링크 인기도, 방문자 활동을 추적하세요. 당신의 청중을 더 잘 이해하세요.",
|
||||
|
||||
"home.useCases.title": "모두를 위한",
|
||||
"home.useCases.bloggers": "블로거",
|
||||
"home.useCases.bloggersDescription": "모든 소셜 미디어 모으기",
|
||||
"home.useCases.business": "비즈니스",
|
||||
"home.useCases.businessDescription": "서비스와 연락처 보여주기",
|
||||
"home.useCases.musicians": "음악가",
|
||||
"home.useCases.musiciansDescription": "창작물 공유하기",
|
||||
"home.useCases.photographers": "사진가",
|
||||
"home.useCases.photographersDescription": "포트폴리오 보여주기",
|
||||
"home.useCases.exampleTitle": "개인 페이지",
|
||||
"home.useCases.exampleSubtitle": "페이지 예시",
|
||||
"home.useCases.personalSite": "개인 웹사이트",
|
||||
|
||||
"home.cta.title": "시작할 준비가 되셨나요?",
|
||||
"home.cta.subtitle": "완벽한 링크 페이지를 만든 수천 명의 사용자와 함께하세요",
|
||||
"home.cta.createFree": "무료 계정 만들기",
|
||||
"home.cta.haveAccount": "계정이 있습니다",
|
||||
"home.cta.features": "영원히 무료 • 제한 없음 • 빠른 설정",
|
||||
|
||||
"auth.welcome": "환영합니다!",
|
||||
"auth.welcomeSubtitle": "CatLink 계정에 로그인하세요",
|
||||
"auth.createAccount": "계정 만들기",
|
||||
"auth.createAccountSubtitle": "오늘 CatLink와 함께하세요",
|
||||
"auth.usernameLabel": "사용자명",
|
||||
"auth.usernamePlaceholder": "사용자명을 입력하세요",
|
||||
"auth.usernameRequired": "사용자명을 입력해주세요",
|
||||
"auth.usernameHelp": "라틴 문자, 숫자 및 _ 만",
|
||||
"auth.passwordLabel": "비밀번호",
|
||||
"auth.passwordPlaceholder": "비밀번호를 입력하세요",
|
||||
"auth.passwordRequired": "비밀번호를 입력해주세요",
|
||||
"auth.passwordConfirmLabel": "비밀번호 확인",
|
||||
"auth.passwordConfirmRequired": "비밀번호를 확인해주세요",
|
||||
"auth.passwordMismatch": "비밀번호가 일치하지 않습니다",
|
||||
"auth.emailLabel": "이메일",
|
||||
"auth.emailRequired": "이메일을 입력해주세요",
|
||||
"auth.firstNameLabel": "이름",
|
||||
"auth.lastNameLabel": "성",
|
||||
"auth.loginButton": "로그인",
|
||||
"auth.registerButton": "계정 만들기",
|
||||
"auth.loggingIn": "로그인 중...",
|
||||
"auth.registering": "계정 생성 중...",
|
||||
"auth.noAccount": "계정이 없으신가요?",
|
||||
"auth.haveAccount": "이미 계정이 있으신가요?",
|
||||
"auth.loginError": "로그인 오류",
|
||||
"auth.networkError": "네트워크 오류",
|
||||
"auth.registrationError": "등록 오류",
|
||||
"auth.connectionError": "서버 연결 오류",
|
||||
"auth.termsAgreement": "계정을 만들면 다음에 동의하는 것입니다",
|
||||
"auth.termsLink": "서비스 약관",
|
||||
"auth.privacyLink": "개인정보 보호정책",
|
||||
"auth.and": "및",
|
||||
|
||||
"auth.login.title": "로그인",
|
||||
"auth.login.email": "이메일",
|
||||
"auth.login.password": "비밀번호",
|
||||
@@ -51,15 +122,17 @@
|
||||
"dashboard.groups": "그룹",
|
||||
"dashboard.links": "링크",
|
||||
"dashboard.settings": "설정",
|
||||
"dashboard.customize": "커스터마이즈",
|
||||
"dashboard.customize": "공유",
|
||||
"dashboard.addGroup": "그룹 추가",
|
||||
"dashboard.addLink": "링크 추가",
|
||||
"dashboard.noGroups": "그룹이 없습니다",
|
||||
"dashboard.noLinks": "링크가 없습니다",
|
||||
"dashboard.createFirst": "첫 번째 생성하기",
|
||||
"dashboard.noGroups": "그룹이 아직 없습니다",
|
||||
"dashboard.noLinks": "링크가 아직 없습니다",
|
||||
"dashboard.createFirst": "첫 번째 만들기",
|
||||
"dashboard.shareUrl.copied": "링크가 클립보드에 복사되었습니다",
|
||||
|
||||
"group.create": "그룹 생성",
|
||||
"group.edit": "그룹 편집",
|
||||
"group.delete": "그룹 삭제",
|
||||
"group.name": "그룹 이름",
|
||||
"group.description": "설명",
|
||||
"group.icon": "아이콘",
|
||||
@@ -73,6 +146,7 @@
|
||||
|
||||
"link.create": "링크 생성",
|
||||
"link.edit": "링크 편집",
|
||||
"link.delete": "링크 삭제",
|
||||
"link.title": "링크 제목",
|
||||
"link.url": "URL",
|
||||
"link.description": "설명",
|
||||
@@ -191,5 +265,11 @@
|
||||
"language.ru": "Русский",
|
||||
"language.ko": "한국어",
|
||||
"language.zh": "中文",
|
||||
"language.ja": "日本語"
|
||||
"language.ja": "日本語",
|
||||
|
||||
"footer.about": "회사 소개",
|
||||
"footer.contact": "연락처",
|
||||
"footer.terms": "이용약관",
|
||||
"footer.privacy": "개인정보 보호정책",
|
||||
"footer.copyright": "© CatLink 2025. 모든 권리 보유."
|
||||
}
|
||||
@@ -27,6 +27,88 @@
|
||||
"common.yes": "Да",
|
||||
"common.no": "Нет",
|
||||
"common.menu": "Меню",
|
||||
"common.optional": "опционально",
|
||||
"common.closeModal": "Закрыть модальное окно",
|
||||
|
||||
"share.title": "Поделиться страницей",
|
||||
"share.description": "Ваша публичная страница со ссылками доступна по адресу:",
|
||||
"share.loading": "Загрузка...",
|
||||
"share.urlAriaLabel": "URL публичной страницы",
|
||||
"share.urlTitle": "URL публичной страницы",
|
||||
"share.copy": "Копировать",
|
||||
"share.note": "На этой странице будут видны все ваши группы и ссылки. Она обновляется автоматически при изменении данных.",
|
||||
"share.openPage": "Открыть страницу",
|
||||
|
||||
"home.title": "Ваши ссылки. Ваш стиль. Ваш CatLink.",
|
||||
"home.subtitle": "Создайте красивую персональную страницу со всеми важными ссылками в одном месте. Делитесь профессионально и стильно!",
|
||||
"home.emailPlaceholder": "Введите ваш email",
|
||||
"home.startFree": "Начать бесплатно",
|
||||
"home.haveAccount": "Уже есть аккаунт?",
|
||||
"home.signIn": "Войти",
|
||||
|
||||
"home.features.title": "Почему выбирают CatLink?",
|
||||
"home.features.subtitle": "Простой и мощный инструмент для создания вашего цифрового присутствия",
|
||||
|
||||
"home.features.links.title": "Один URL — все ссылки",
|
||||
"home.features.links.description": "Соберите все важные ссылки в одном месте. Социальные сети, портфолио, контакты — всё под одним адресом.",
|
||||
|
||||
"home.features.customization.title": "Персонализация",
|
||||
"home.features.customization.description": "Настройте цвета, шрифты, макеты. Создайте уникальный стиль, который отражает вашу личность или бренд.",
|
||||
|
||||
"home.features.analytics.title": "Аналитика",
|
||||
"home.features.analytics.description": "Отслеживайте клики, популярность ссылок и активность посетителей. Понимайте свою аудиторию лучше.",
|
||||
|
||||
"home.useCases.title": "Для всех и каждого",
|
||||
"home.useCases.bloggers": "Блогеры",
|
||||
"home.useCases.bloggersDescription": "Соберите все социальные сети",
|
||||
"home.useCases.business": "Бизнес",
|
||||
"home.useCases.businessDescription": "Покажите услуги и контакты",
|
||||
"home.useCases.musicians": "Музыканты",
|
||||
"home.useCases.musiciansDescription": "Поделитесь творчеством",
|
||||
"home.useCases.photographers": "Фотографы",
|
||||
"home.useCases.photographersDescription": "Покажите портфолио",
|
||||
"home.useCases.exampleTitle": "Ваша персональная страница",
|
||||
"home.useCases.exampleSubtitle": "Пример вашей страницы",
|
||||
"home.useCases.personalSite": "Личный сайт",
|
||||
|
||||
"home.cta.title": "Готовы начать?",
|
||||
"home.cta.subtitle": "Присоединяйтесь к тысячам пользователей, которые уже создали свою идеальную страницу ссылок",
|
||||
"home.cta.createFree": "Создать аккаунт бесплатно",
|
||||
"home.cta.haveAccount": "У меня есть аккаунт",
|
||||
"home.cta.features": "Бесплатно навсегда • Без ограничений • Быстрая настройка",
|
||||
|
||||
"auth.welcome": "Добро пожаловать!",
|
||||
"auth.welcomeSubtitle": "Войдите в свой аккаунт CatLink",
|
||||
"auth.createAccount": "Создать аккаунт",
|
||||
"auth.createAccountSubtitle": "Присоединяйтесь к CatLink сегодня",
|
||||
"auth.usernameLabel": "Имя пользователя",
|
||||
"auth.usernamePlaceholder": "Введите имя пользователя",
|
||||
"auth.usernameRequired": "Введите имя пользователя",
|
||||
"auth.usernameHelp": "Только латинские буквы, цифры и _",
|
||||
"auth.passwordLabel": "Пароль",
|
||||
"auth.passwordPlaceholder": "Введите пароль",
|
||||
"auth.passwordRequired": "Введите пароль",
|
||||
"auth.passwordConfirmLabel": "Подтвердите пароль",
|
||||
"auth.passwordConfirmRequired": "Подтвердите пароль",
|
||||
"auth.passwordMismatch": "Пароли не совпадают",
|
||||
"auth.emailLabel": "Email",
|
||||
"auth.emailRequired": "Введите email",
|
||||
"auth.firstNameLabel": "Имя",
|
||||
"auth.lastNameLabel": "Фамилия",
|
||||
"auth.loginButton": "Войти",
|
||||
"auth.registerButton": "Создать аккаунт",
|
||||
"auth.loggingIn": "Входим...",
|
||||
"auth.registering": "Создание аккаунта...",
|
||||
"auth.noAccount": "Нет аккаунта?",
|
||||
"auth.haveAccount": "Уже есть аккаунт?",
|
||||
"auth.loginError": "Ошибка входа",
|
||||
"auth.networkError": "Сетевая ошибка",
|
||||
"auth.registrationError": "Ошибка регистрации",
|
||||
"auth.connectionError": "Ошибка соединения с сервером",
|
||||
"auth.termsAgreement": "Создавая аккаунт, вы соглашаетесь с",
|
||||
"auth.termsLink": "Условиями использования",
|
||||
"auth.privacyLink": "Политикой конфиденциальности",
|
||||
"auth.and": "и",
|
||||
|
||||
"auth.login.title": "Вход",
|
||||
"auth.login.email": "Email",
|
||||
@@ -46,20 +128,30 @@
|
||||
"auth.register.haveAccount": "Уже есть аккаунт?",
|
||||
"auth.register.signIn": "Войти",
|
||||
|
||||
"dashboard.title": "Панель управления",
|
||||
"dashboard.title": "Панель управления",
|
||||
"dashboard.welcome": "Добро пожаловать, {{name}}!",
|
||||
"dashboard.groups": "Группы",
|
||||
"dashboard.links": "Ссылки",
|
||||
"dashboard.links": "Ссылки",
|
||||
"dashboard.settings": "Настройки",
|
||||
"dashboard.customize": "Настроить",
|
||||
"dashboard.share": "Поделиться",
|
||||
"dashboard.customize": "Персонализация",
|
||||
"dashboard.panelOpen": "Открыта",
|
||||
"dashboard.panelClosed": "Закрыта",
|
||||
"dashboard.error": "Ошибка: ",
|
||||
"dashboard.linkGroups": "Группы ссылок",
|
||||
"dashboard.andMore": "и еще {{count}}...",
|
||||
"dashboard.linksCount": "{{count}} ссылок",
|
||||
"dashboard.linksInGroup": "{{count}} ссылок в этой группе",
|
||||
"dashboard.addGroup": "Добавить группу",
|
||||
"dashboard.addLink": "Добавить ссылку",
|
||||
"dashboard.noGroups": "Пока нет групп",
|
||||
"dashboard.noLinks": "Пока нет ссылок",
|
||||
"dashboard.createFirst": "Создайте вашу первую",
|
||||
"dashboard.noGroups": "Групп пока нет",
|
||||
"dashboard.noLinks": "Ссылок пока нет",
|
||||
"dashboard.createFirst": "Создайте первую",
|
||||
"dashboard.shareUrl.copied": "Ссылка скопирована в буфер обмена",
|
||||
|
||||
"group.create": "Создать группу",
|
||||
"group.edit": "Редактировать группу",
|
||||
"group.delete": "удалить группу",
|
||||
"group.name": "Название группы",
|
||||
"group.description": "Описание",
|
||||
"group.icon": "Иконка",
|
||||
@@ -70,9 +162,20 @@
|
||||
"group.expanded": "Развернута по умолчанию",
|
||||
"group.removeIcon": "Убрать иконку",
|
||||
"group.removeBackground": "Убрать фон",
|
||||
"group.descriptionPlaceholder": "Краткое описание группы ссылок",
|
||||
"group.currentIcon": "Текущая иконка",
|
||||
"group.confirmRemoveIcon": "Удалить текущую иконку группы?",
|
||||
"group.iconSizeRecommendation": "Рекомендуемый размер: 32x32 пикселя",
|
||||
"group.currentBackground": "Текущий фон",
|
||||
"group.confirmRemoveBackground": "Удалить текущий фон группы?",
|
||||
"group.imageSizeRecommendation": "Рекомендуемый размер изображения:",
|
||||
"group.tip": "Совет:",
|
||||
"group.borderTip": "Для групп с рамкой используйте изображения с отступами по краям (10-20px)",
|
||||
"group.backgroundDescription": "Изображение будет использовано как фон для содержимого группы",
|
||||
|
||||
"link.create": "Создать ссылку",
|
||||
"link.edit": "Редактировать ссылку",
|
||||
"link.delete": "удалить ссылку",
|
||||
"link.title": "Название ссылки",
|
||||
"link.url": "URL",
|
||||
"link.description": "Описание",
|
||||
@@ -80,6 +183,12 @@
|
||||
"link.removeIcon": "Убрать иконку",
|
||||
"link.public": "Публичная",
|
||||
"link.featured": "Рекомендуемая",
|
||||
"link.titlePlaceholder": "Название ссылки",
|
||||
"link.descriptionPlaceholder": "Краткое описание ссылки",
|
||||
"link.urlPlaceholder": "https://example.com",
|
||||
"link.currentIcon": "Текущая иконка",
|
||||
"link.confirmRemoveIcon": "Удалить текущую иконку ссылки?",
|
||||
"link.iconSizeRecommendation": "Рекомендуемый размер: 24x24 пикселя",
|
||||
|
||||
"profile.edit": "Редактировать профиль",
|
||||
"profile.username": "Имя пользователя",
|
||||
@@ -87,9 +196,10 @@
|
||||
"profile.firstName": "Имя",
|
||||
"profile.lastName": "Фамилия",
|
||||
"profile.fullName": "Полное имя",
|
||||
"profile.bio": "О себе",
|
||||
"profile.bio": "Биография",
|
||||
"profile.avatar": "Аватар",
|
||||
"profile.cover": "Обложка",
|
||||
"profile.currentAvatar": "Текущий аватар",
|
||||
"profile.removeAvatar": "Убрать аватар",
|
||||
"profile.removeCover": "Убрать обложку",
|
||||
|
||||
@@ -103,17 +213,29 @@
|
||||
|
||||
"customization.layout.style": "Стиль отображения групп и ссылок",
|
||||
"customization.layout.list": "Список",
|
||||
"customization.layout.grid": "Сетка",
|
||||
"customization.layout.grid": "Сетка",
|
||||
"customization.layout.cards": "Карточки",
|
||||
"customization.layout.compact": "Компактный",
|
||||
"customization.layout.masonry": "Кирпичная кладка",
|
||||
"customization.layout.timeline": "Временная шкала",
|
||||
"customization.layout.magazine": "Журнал",
|
||||
|
||||
"customization.colors.theme": "Цвет темы",
|
||||
"customization.layout.sidebar": "Боковая панель",
|
||||
"customization.layout.testList": "Тестовый список",
|
||||
"customization.layout.listDescription": "Классический вертикальный список",
|
||||
"customization.layout.gridDescription": "Равномерная сетка карточек",
|
||||
"customization.layout.cardsDescription": "Большие информативные карточки",
|
||||
"customization.layout.compactDescription": "Компактное отображение без отступов",
|
||||
"customization.layout.sidebarDescription": "Навигация в боковой панели",
|
||||
"customization.layout.masonryDescription": "Динамическая сетка разной высоты",
|
||||
"customization.layout.timelineDescription": "Хронологическое отображение",
|
||||
"customization.layout.magazineDescription": "Стиль журнала с крупными изображениями",
|
||||
"customization.layout.testListDescription": "Полный несворачиваемый список всех групп и ссылок", "customization.colors.theme": "Цвет темы",
|
||||
"customization.colors.background": "Цвет фона",
|
||||
"customization.colors.backgroundImage": "Фоновое изображение",
|
||||
"customization.colors.removeBackground": "Убрать фон",
|
||||
"customization.colors.backgroundImageHelp": "Выберите изображение для фона (JPG, PNG, GIF). Если не выбрано - текущее изображение останется без изменений.",
|
||||
"customization.colors.currentImage": "Текущее изображение:",
|
||||
"customization.colors.newImage": "Новое изображение (будет применено после сохранения):",
|
||||
"customization.colors.header": "Цвет текста заголовков",
|
||||
"customization.colors.group": "Цвет текста групп",
|
||||
"customization.colors.link": "Цвет текста ссылок",
|
||||
@@ -122,8 +244,12 @@
|
||||
"customization.groups.showLinks": "Показывать иконки ссылок",
|
||||
"customization.groups.defaultExpanded": "Группы развернуты по умолчанию",
|
||||
"customization.groups.showTitle": "Показывать заголовки групп",
|
||||
"customization.groups.displaySettings": "Настройки отображения групп",
|
||||
|
||||
"customization.advanced.fonts": "Настройки шрифтов",
|
||||
"customization.advanced.fontSettings": "Настройки шрифтов",
|
||||
"customization.advanced.additionalSettings": "Дополнительные настройки",
|
||||
"customization.advanced.resetConfirm": "Вы уверены, что хотите сбросить все настройки интерфейса к значениям по умолчанию? Это действие нельзя отменить.",
|
||||
"customization.advanced.mainFont": "Основной шрифт",
|
||||
"customization.advanced.headingFont": "Шрифт заголовков",
|
||||
"customization.advanced.bodyFont": "Шрифт текста",
|
||||
@@ -186,10 +312,16 @@
|
||||
"theme.light": "Светлая тема",
|
||||
"theme.dark": "Темная тема",
|
||||
|
||||
"language.select": "Выберите язык",
|
||||
"language.select": "Выбрать язык",
|
||||
"language.en": "English",
|
||||
"language.ru": "Русский",
|
||||
"language.ko": "한국어",
|
||||
"language.zh": "中文",
|
||||
"language.ja": "日本語"
|
||||
"language.ja": "日本語",
|
||||
|
||||
"footer.about": "О нас",
|
||||
"footer.contact": "Контакты",
|
||||
"footer.terms": "Условия использования",
|
||||
"footer.privacy": "Политика конфиденциальности",
|
||||
"footer.copyright": "© CatLink 2025. Все права защищены."
|
||||
}
|
||||
@@ -28,6 +28,77 @@
|
||||
"common.no": "否",
|
||||
"common.menu": "菜单",
|
||||
|
||||
"home.title": "您的链接。您的风格。您的CatLink。",
|
||||
"home.subtitle": "创建一个美丽的个人页面,将所有重要链接集中在一处。专业且时尚地分享!",
|
||||
"home.emailPlaceholder": "输入您的邮箱",
|
||||
"home.startFree": "免费开始",
|
||||
"home.haveAccount": "已有账户?",
|
||||
"home.signIn": "登录",
|
||||
|
||||
"home.features.title": "为什么选择CatLink?",
|
||||
"home.features.subtitle": "创建数字存在感的简单而强大的工具",
|
||||
|
||||
"home.features.links.title": "一个网址 — 所有链接",
|
||||
"home.features.links.description": "将所有重要链接汇聚在一处。社交媒体、作品集、联系方式——全部在一个地址下。",
|
||||
|
||||
"home.features.customization.title": "个性化定制",
|
||||
"home.features.customization.description": "自定义颜色、字体、布局。创造反映您个性或品牌的独特风格。",
|
||||
|
||||
"home.features.analytics.title": "分析统计",
|
||||
"home.features.analytics.description": "追踪点击量、链接热度和访客活动。更好地了解您的受众。",
|
||||
|
||||
"home.useCases.title": "适合所有人",
|
||||
"home.useCases.bloggers": "博主",
|
||||
"home.useCases.bloggersDescription": "汇聚所有社交媒体",
|
||||
"home.useCases.business": "企业",
|
||||
"home.useCases.businessDescription": "展示服务和联系方式",
|
||||
"home.useCases.musicians": "音乐家",
|
||||
"home.useCases.musiciansDescription": "分享您的作品",
|
||||
"home.useCases.photographers": "摄影师",
|
||||
"home.useCases.photographersDescription": "展示您的作品集",
|
||||
"home.useCases.exampleTitle": "个人页面",
|
||||
"home.useCases.exampleSubtitle": "页面示例",
|
||||
"home.useCases.personalSite": "个人网站",
|
||||
|
||||
"home.cta.title": "准备开始了吗?",
|
||||
"home.cta.subtitle": "加入数千名用户,他们已经创建了完美的链接页面",
|
||||
"home.cta.createFree": "创建免费账户",
|
||||
"home.cta.haveAccount": "我有账户",
|
||||
"home.cta.features": "永久免费 • 无限制 • 快速设置",
|
||||
|
||||
"auth.welcome": "欢迎!",
|
||||
"auth.welcomeSubtitle": "登录您的CatLink账户",
|
||||
"auth.createAccount": "创建账户",
|
||||
"auth.createAccountSubtitle": "今天就加入CatLink",
|
||||
"auth.usernameLabel": "用户名",
|
||||
"auth.usernamePlaceholder": "输入您的用户名",
|
||||
"auth.usernameRequired": "请输入用户名",
|
||||
"auth.usernameHelp": "只能使用拉丁字母、数字和_",
|
||||
"auth.passwordLabel": "密码",
|
||||
"auth.passwordPlaceholder": "输入您的密码",
|
||||
"auth.passwordRequired": "请输入密码",
|
||||
"auth.passwordConfirmLabel": "确认密码",
|
||||
"auth.passwordConfirmRequired": "请确认密码",
|
||||
"auth.passwordMismatch": "密码不匹配",
|
||||
"auth.emailLabel": "邮箱",
|
||||
"auth.emailRequired": "请输入邮箱",
|
||||
"auth.firstNameLabel": "名字",
|
||||
"auth.lastNameLabel": "姓氏",
|
||||
"auth.loginButton": "登录",
|
||||
"auth.registerButton": "创建账户",
|
||||
"auth.loggingIn": "登录中...",
|
||||
"auth.registering": "创建账户中...",
|
||||
"auth.noAccount": "还没有账户?",
|
||||
"auth.haveAccount": "已有账户?",
|
||||
"auth.loginError": "登录错误",
|
||||
"auth.networkError": "网络错误",
|
||||
"auth.registrationError": "注册错误",
|
||||
"auth.connectionError": "服务器连接错误",
|
||||
"auth.termsAgreement": "创建账户即表示您同意",
|
||||
"auth.termsLink": "服务条款",
|
||||
"auth.privacyLink": "隐私政策",
|
||||
"auth.and": "和",
|
||||
|
||||
"auth.login.title": "登录",
|
||||
"auth.login.email": "邮箱",
|
||||
"auth.login.password": "密码",
|
||||
@@ -51,15 +122,17 @@
|
||||
"dashboard.groups": "分组",
|
||||
"dashboard.links": "链接",
|
||||
"dashboard.settings": "设置",
|
||||
"dashboard.customize": "自定义",
|
||||
"dashboard.customize": "分享",
|
||||
"dashboard.addGroup": "添加分组",
|
||||
"dashboard.addLink": "添加链接",
|
||||
"dashboard.noGroups": "暂无分组",
|
||||
"dashboard.noLinks": "暂无链接",
|
||||
"dashboard.noGroups": "还没有分组",
|
||||
"dashboard.noLinks": "还没有链接",
|
||||
"dashboard.createFirst": "创建您的第一个",
|
||||
"dashboard.shareUrl.copied": "链接已复制到剪贴板",
|
||||
|
||||
"group.create": "创建分组",
|
||||
"group.edit": "编辑分组",
|
||||
"group.delete": "删除分组",
|
||||
"group.name": "分组名称",
|
||||
"group.description": "描述",
|
||||
"group.icon": "图标",
|
||||
@@ -73,6 +146,7 @@
|
||||
|
||||
"link.create": "创建链接",
|
||||
"link.edit": "编辑链接",
|
||||
"link.delete": "删除链接",
|
||||
"link.title": "链接标题",
|
||||
"link.url": "网址",
|
||||
"link.description": "描述",
|
||||
@@ -191,5 +265,11 @@
|
||||
"language.ru": "Русский",
|
||||
"language.ko": "한국어",
|
||||
"language.zh": "中文",
|
||||
"language.ja": "日本語"
|
||||
"language.ja": "日本語",
|
||||
|
||||
"footer.about": "关于我们",
|
||||
"footer.contact": "联系我们",
|
||||
"footer.terms": "服务条款",
|
||||
"footer.privacy": "隐私政策",
|
||||
"footer.copyright": "© CatLink 2025. 保留所有权利。"
|
||||
}
|
||||
@@ -4,12 +4,13 @@
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Image from 'next/image'
|
||||
import { useLocale } from './contexts/LocaleContext'
|
||||
|
||||
export default function HomePage() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
const { t } = useLocale()
|
||||
|
||||
const handleQuickStart = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
@@ -17,10 +18,7 @@ export default function HomePage() {
|
||||
|
||||
setLoading(true)
|
||||
|
||||
// Сохраняем email в локальном хранилище для автозаполнения формы регистрации
|
||||
localStorage.setItem('quickStartEmail', email)
|
||||
|
||||
// Перенаправляем на страницу регистрации
|
||||
router.push('/auth/register')
|
||||
}
|
||||
|
||||
@@ -38,21 +36,11 @@ export default function HomePage() {
|
||||
<div className="container py-5">
|
||||
<div className="row">
|
||||
<div className="col-xl-10 mx-auto">
|
||||
<div className="mb-4">
|
||||
<Image
|
||||
src="/assets/img/CAT.png"
|
||||
alt="CatLink"
|
||||
width={120}
|
||||
height={120}
|
||||
className="mb-4"
|
||||
/>
|
||||
</div>
|
||||
<h1 className="display-4 fw-bold mb-4">
|
||||
Ваши ссылки. Ваш стиль. Ваш CatLink.
|
||||
{t('home.title')}
|
||||
</h1>
|
||||
<p className="lead mb-5 fs-4">
|
||||
Создайте красивую персональную страницу со всеми важными ссылками в одном месте.
|
||||
Делитесь профессионально и стильно!
|
||||
{t('home.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-md-10 col-lg-8 col-xl-6 mx-auto">
|
||||
@@ -60,7 +48,7 @@ export default function HomePage() {
|
||||
<input
|
||||
className="form-control form-control-lg"
|
||||
type="email"
|
||||
placeholder="Введите ваш email"
|
||||
placeholder={t('home.emailPlaceholder')}
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
@@ -74,180 +62,51 @@ export default function HomePage() {
|
||||
{loading ? (
|
||||
<span className="spinner-border spinner-border-sm"></span>
|
||||
) : (
|
||||
'Начать бесплатно'
|
||||
t('home.startFree')
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
<div className="mt-3">
|
||||
<small className="text-white-50">
|
||||
Уже есть аккаунт? <Link href="/auth/login" className="text-warning text-decoration-none fw-bold">Войти</Link>
|
||||
{t('home.haveAccount')} <Link href="/auth/login" className="text-warning text-decoration-none fw-bold">{t('home.signIn')}</Link>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Анимированная стрелка вниз */}
|
||||
<div className="position-absolute bottom-0 start-50 translate-middle-x mb-4">
|
||||
<div className="animate-bounce">
|
||||
<i className="bi bi-chevron-down text-white fs-3"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Преимущества */}
|
||||
<section className="py-5 bg-light">
|
||||
<div className="container">
|
||||
<div className="row text-center mb-5">
|
||||
<div className="col-lg-8 mx-auto">
|
||||
<h2 className="display-5 fw-bold mb-3">Почему выбирают CatLink?</h2>
|
||||
<h2 className="display-5 fw-bold mb-3">{t('home.features.title')}</h2>
|
||||
<p className="lead text-muted">
|
||||
Простой и мощный инструмент для создания вашего цифрового присутствия
|
||||
{t('home.features.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-lg-4 mb-4">
|
||||
<div className="text-center h-100">
|
||||
<div className="bg-primary rounded-circle d-inline-flex align-items-center justify-content-center mb-4" style={{ width: '80px', height: '80px' }}>
|
||||
<i className="bi bi-link-45deg text-white fs-1"></i>
|
||||
</div>
|
||||
<h4 className="fw-bold">Один URL — все ссылки</h4>
|
||||
<p className="text-muted">
|
||||
Соберите все важные ссылки в одном месте. Социальные сети, портфолио,
|
||||
контакты — всё под одним адресом.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-lg-4 mb-4">
|
||||
<div className="text-center h-100">
|
||||
<div className="bg-success rounded-circle d-inline-flex align-items-center justify-content-center mb-4" style={{ width: '80px', height: '80px' }}>
|
||||
<i className="bi bi-palette text-white fs-1"></i>
|
||||
</div>
|
||||
<h4 className="fw-bold">Персонализация</h4>
|
||||
<p className="text-muted">
|
||||
Настройте цвета, шрифты, макеты. Создайте уникальный стиль,
|
||||
который отражает вашу личность или бренд.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-lg-4 mb-4">
|
||||
<div className="text-center h-100">
|
||||
<div className="bg-warning rounded-circle d-inline-flex align-items-center justify-content-center mb-4" style={{ width: '80px', height: '80px' }}>
|
||||
<i className="bi bi-graph-up text-white fs-1"></i>
|
||||
</div>
|
||||
<h4 className="fw-bold">Аналитика</h4>
|
||||
<p className="text-muted">
|
||||
Отслеживайте клики, популярность ссылок и активность посетителей.
|
||||
Понимайте свою аудиторию лучше.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Примеры использования */}
|
||||
<section className="py-5">
|
||||
<div className="container">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-lg-6 mb-4">
|
||||
<h2 className="display-6 fw-bold mb-4">Для всех и каждого</h2>
|
||||
<div className="row">
|
||||
<div className="col-sm-6 mb-3">
|
||||
<div className="d-flex align-items-start">
|
||||
<div className="bg-primary rounded-circle me-3 d-flex align-items-center justify-content-center" style={{ width: '40px', height: '40px', minWidth: '40px' }}>
|
||||
<i className="bi bi-person text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="fw-bold mb-1">Блогеры</h6>
|
||||
<small className="text-muted">Соберите все социальные сети</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-6 mb-3">
|
||||
<div className="d-flex align-items-start">
|
||||
<div className="bg-success rounded-circle me-3 d-flex align-items-center justify-content-center" style={{ width: '40px', height: '40px', minWidth: '40px' }}>
|
||||
<i className="bi bi-briefcase text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="fw-bold mb-1">Бизнес</h6>
|
||||
<small className="text-muted">Покажите услуги и контакты</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-6 mb-3">
|
||||
<div className="d-flex align-items-start">
|
||||
<div className="bg-warning rounded-circle me-3 d-flex align-items-center justify-content-center" style={{ width: '40px', height: '40px', minWidth: '40px' }}>
|
||||
<i className="bi bi-music-note text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="fw-bold mb-1">Музыканты</h6>
|
||||
<small className="text-muted">Поделитесь творчеством</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-6 mb-3">
|
||||
<div className="d-flex align-items-start">
|
||||
<div className="bg-info rounded-circle me-3 d-flex align-items-center justify-content-center" style={{ width: '40px', height: '40px', minWidth: '40px' }}>
|
||||
<i className="bi bi-camera text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="fw-bold mb-1">Фотографы</h6>
|
||||
<small className="text-muted">Покажите портфолио</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-lg-6 text-center">
|
||||
<div className="bg-light rounded-3 p-4 shadow-sm">
|
||||
<div className="bg-white rounded-3 p-4 mb-3 border">
|
||||
<div className="d-flex align-items-center mb-3">
|
||||
<div className="bg-primary rounded-circle me-3" style={{ width: '40px', height: '40px' }}></div>
|
||||
<div>
|
||||
<h6 className="mb-0">@your_username</h6>
|
||||
<small className="text-muted">Ваша персональная страница</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-grid gap-2">
|
||||
<div className="bg-light rounded-2 p-2 text-start">
|
||||
<small><i className="bi bi-instagram text-danger me-2"></i>Instagram</small>
|
||||
</div>
|
||||
<div className="bg-light rounded-2 p-2 text-start">
|
||||
<small><i className="bi bi-youtube text-danger me-2"></i>YouTube</small>
|
||||
</div>
|
||||
<div className="bg-light rounded-2 p-2 text-start">
|
||||
<small><i className="bi bi-globe text-primary me-2"></i>Личный сайт</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<small className="text-muted">Пример вашей страницы</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA секция */}
|
||||
<section className="py-5 bg-primary text-white">
|
||||
<div className="container text-center">
|
||||
<div className="row">
|
||||
<div className="col-lg-8 mx-auto">
|
||||
<h2 className="display-6 fw-bold mb-4">Готовы начать?</h2>
|
||||
<h2 className="display-6 fw-bold mb-4">{t('home.cta.title')}</h2>
|
||||
<p className="lead mb-4">
|
||||
Присоединяйтесь к тысячам пользователей, которые уже создали свою идеальную страницу ссылок
|
||||
{t('home.cta.subtitle')}
|
||||
</p>
|
||||
<div className="d-flex flex-column flex-sm-row gap-3 justify-content-center">
|
||||
<Link href="/auth/register" className="btn btn-warning btn-lg px-4 fw-bold">
|
||||
Создать аккаунт бесплатно
|
||||
{t('home.cta.createFree')}
|
||||
</Link>
|
||||
<Link href="/auth/login" className="btn btn-outline-light btn-lg px-4">
|
||||
У меня есть аккаунт
|
||||
{t('home.cta.haveAccount')}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<small className="text-white-75">Бесплатно навсегда • Без ограничений • Быстрая настройка</small>
|
||||
<small className="text-white-75">{t('home.cta.features')}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user