diff --git a/.env.local b/.env.local new file mode 100644 index 0000000..d0e2f0f --- /dev/null +++ b/.env.local @@ -0,0 +1,71 @@ +# Django настройки для локальной разработки +DJANGO_SECRET_KEY=lskjflSDJHFdSFYU7TYOREIFLUDJKFBNKLJSDHFP9Q234856QT80OUAEIYDWSF9PQ28345701784QRTEOYAGWDFLSBAPWO9I485Y +DJANGO_DEBUG=True +DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,web +DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:8000,http://127.0.0.1:8000 + +# CORS настройки для локальной разработки +CORS_ALLOWED_ORIGINS=http://127.0.0.1:3000,http://localhost:3000 +CORS_ALLOW_ALL_ORIGINS=True +CORS_ALLOW_CREDENTIALS=True +CORS_ALLOW_HEADERS=accept,accept-encoding,authorization,content-type,dnt,origin,user-agent,x-csrftoken,x-requested-with + +# Локализация +DJANGO_LANGUAGE_CODE=ru-ru +DJANGO_TIME_ZONE=UTC +DJANGO_USE_I18N=True +DJANGO_USE_TZ=True + +# Статические файлы +DJANGO_STATIC_URL=/static/ +DJANGO_MEDIA_URL=/storage/ + +# API настройки +DJANGO_APPEND_SLASH=False + +# JWT настройки +JWT_ACCESS_TOKEN_LIFETIME_MINUTES=60 +JWT_REFRESH_TOKEN_LIFETIME_DAYS=1 + +# База данных PostgreSQL +DATABASE_ENGINE=django.db.backends.postgresql +DATABASE_NAME=links_db +DATABASE_USER=links_user +DATABASE_PASSWORD=links_OASDUIFH90324*ftye(guBJ;O234789SDgfu{ +DATABASE_HOST=db +DATABASE_PORT=5432 + +# PostgreSQL настройки для контейнера +POSTGRES_DB=links_db +POSTGRES_USER=links_user +POSTGRES_PASSWORD=links_OASDUIFH90324*ftye(guBJ;O234789SDgfu{ + +# Frontend настройки (для локальной разработки) +NEXT_PUBLIC_API_URL=http://localhost:8000 + +# URL настройки для Django backend (локальные) +DJANGO_BACKEND_URL=http://localhost:8000 +DJANGO_BACKEND_PROTOCOL=http +DJANGO_BACKEND_DOMAIN=localhost:8000 +DJANGO_MEDIA_BASE_URL=http://localhost:8000 + +# Безопасность (отключено для локальной разработки) +DJANGO_SECURE_SSL_REDIRECT=False +DJANGO_SECURE_HSTS_SECONDS=0 +DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS=False +DJANGO_SECURE_HSTS_PRELOAD=False +DJANGO_SECURE_CONTENT_TYPE_NOSNIFF=True +DJANGO_SECURE_BROWSER_XSS_FILTER=True +DJANGO_X_FRAME_OPTIONS=SAMEORIGIN + +# Email настройки (консоль для локальной разработки) +DJANGO_EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend +DJANGO_EMAIL_HOST= +DJANGO_EMAIL_PORT=587 +DJANGO_EMAIL_HOST_USER= +DJANGO_EMAIL_HOST_PASSWORD= +DJANGO_EMAIL_USE_TLS=False +DJANGO_EMAIL_USE_SSL=False +DJANGO_EMAIL_TIMEOUT=30 +DJANGO_DEFAULT_FROM_EMAIL= +DJANGO_SERVER_EMAIL= \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index e40416a..6589fec 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/frontend/linktree-frontend/.dockerignore b/frontend/linktree-frontend/.dockerignore index 1ff2662..5034fb3 100644 --- a/frontend/linktree-frontend/.dockerignore +++ b/frontend/linktree-frontend/.dockerignore @@ -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/ diff --git a/frontend/linktree-frontend/Dockerfile b/frontend/linktree-frontend/Dockerfile index 24b1300..6b60296 100644 --- a/frontend/linktree-frontend/Dockerfile +++ b/frontend/linktree-frontend/Dockerfile @@ -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"] \ No newline at end of file diff --git a/frontend/linktree-frontend/src/app/(protected)/dashboard/DashboardClient.tsx b/frontend/linktree-frontend/src/app/(protected)/dashboard/DashboardClient.tsx index 996b1ab..c19b4c8 100644 --- a/frontend/linktree-frontend/src/app/(protected)/dashboard/DashboardClient.tsx +++ b/frontend/linktree-frontend/src/app/(protected)/dashboard/DashboardClient.tsx @@ -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(null) const [groups, setGroups] = useState([]) @@ -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
Загрузка...
+ if (loading) return
{t('common.loading')}
if (error) return
{error}
// Функция расчета оптимального размера изображения для группы @@ -475,9 +478,9 @@ export default function DashboardClient() { const renderListLayout = () => (
-
Группы ссылок
+
{t('dashboard.groups')}
@@ -528,9 +531,9 @@ export default function DashboardClient() { const renderGridLayout = () => (
-
Группы ссылок
+
{t('dashboard.groups')}
@@ -599,7 +602,7 @@ export default function DashboardClient() { const renderCompactLayout = () => (
-
Группы ссылок
+
{t('dashboard.groups')}
@@ -647,9 +650,9 @@ export default function DashboardClient() { const renderCardsLayout = () => (
-
Группы ссылок
+
{t('dashboard.linkGroups')}
@@ -673,7 +676,7 @@ export default function DashboardClient() {
{group.name}
- {group.links.length} ссылок + {t('dashboard.linksCount', { count: group.links.length })}
@@ -738,7 +741,7 @@ export default function DashboardClient() {
-
Группы
+
{t('dashboard.groups')}
@@ -830,9 +833,9 @@ export default function DashboardClient() { const renderMasonryLayout = () => (
-
Группы ссылок
+
{t('dashboard.groups')}
@@ -904,9 +907,9 @@ export default function DashboardClient() { const renderTimelineLayout = () => (
-
Группы ссылок
+
{t('dashboard.groups')}
@@ -929,7 +932,7 @@ export default function DashboardClient() {
{group.name}
- {group.links.length} ссылок + {t('dashboard.linksCount', { count: group.links.length })}
@@ -976,9 +979,9 @@ export default function DashboardClient() { const renderMagazineLayout = () => (
-
Группы ссылок
+
{t('dashboard.groups')}
@@ -1006,7 +1009,7 @@ export default function DashboardClient() { {group.name}

- {group.links.length} ссылок в этой группе + {t('dashboard.linksInGroup', { count: group.links.length })}

{group.links.slice(0, 3).map(link => ( @@ -1017,7 +1020,7 @@ export default function DashboardClient() {
))} {group.links.length > 3 && ( - и еще {group.links.length - 3}... + {t('dashboard.andMore', { count: group.links.length - 3 })} )}
@@ -1087,6 +1090,8 @@ export default function DashboardClient() { style={containerStyle} suppressHydrationWarning={true} > + + {user && (
-

Ваши ссылки

+

{t('dashboard.title')}

- Panel state: {showCustomizationPanel ? 'Open' : 'Closed'} + Panel state: {showCustomizationPanel ? t('dashboard.panelOpen') : t('dashboard.panelClosed')}
@@ -1141,18 +1146,18 @@ export default function DashboardClient() {
-
{groupModalMode === 'add' ? 'Добавить группу' : 'Редактировать группу'}
+
{groupModalMode === 'add' ? t('group.create') : t('group.edit')}
- +
- +