refactor. pre-deploy
This commit is contained in:
19
frontend/linktree-frontend/Dockerfile
Normal file
19
frontend/linktree-frontend/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Копирование package.json и package-lock.json
|
||||
COPY package*.json ./
|
||||
|
||||
# Установка зависимостей
|
||||
RUN npm install
|
||||
|
||||
# Копирование исходного кода
|
||||
COPY . .
|
||||
|
||||
# Сборка приложения
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["npm", "start"]
|
||||
59
frontend/linktree-frontend/next.config.js
Normal file
59
frontend/linktree-frontend/next.config.js
Normal file
@@ -0,0 +1,59 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'http',
|
||||
hostname: 'web',
|
||||
port: '8000',
|
||||
pathname: '/storage/**',
|
||||
},
|
||||
{
|
||||
protocol: 'http',
|
||||
hostname: '127.0.0.1',
|
||||
port: '8000',
|
||||
pathname: '/storage/**',
|
||||
},
|
||||
{
|
||||
protocol: 'http',
|
||||
hostname: 'localhost',
|
||||
port: '8000',
|
||||
pathname: '/storage/**',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/api/:path*',
|
||||
destination: 'http://web:8000/api/:path*',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
|
||||
trailingSlash: false,
|
||||
skipTrailingSlashRedirect: true,
|
||||
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/api/:path*',
|
||||
headers: [
|
||||
{ key: 'Access-Control-Allow-Origin', value: '*' },
|
||||
{ key: 'Access-Control-Allow-Methods', value: 'GET, POST, PUT, DELETE, OPTIONS' },
|
||||
{ key: 'Access-Control-Allow-Headers', value: 'Content-Type, Authorization' },
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -2,36 +2,55 @@
|
||||
const nextConfig = {
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'http',
|
||||
hostname: 'web',
|
||||
port: '8000', // where Django serves media
|
||||
pathname: '/storage/**', // storage/avatars, images/link_groups, etc.
|
||||
},
|
||||
{
|
||||
protocol: 'http',
|
||||
hostname: '127.0.0.1',
|
||||
port: '8000', // where Django serves media
|
||||
pathname: '/storage/**', // storage/avatars, images/link_groups, etc.
|
||||
},
|
||||
{
|
||||
protocol: 'http',
|
||||
hostname: 'localhost',
|
||||
port: '8000', // where Django serves media
|
||||
pathname: '/storage/**', // storage/avatars, images/link_groups, etc.
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// proxy all /api/* calls to Django
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/api/:path*/',
|
||||
destination: 'http://web:8000/api/:path*/',
|
||||
},
|
||||
{
|
||||
source: '/api/:path*',
|
||||
destination: 'http://127.0.0.1:8000/api/:path*',
|
||||
destination: 'http://web:8000/api/:path*/',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
experimental: {
|
||||
// whitelist origins you’ll browse from in dev
|
||||
allowedDevOrigins: [
|
||||
'http://localhost:3001',
|
||||
'http://127.0.0.1:3001',
|
||||
'http://192.168.219.114:3001',
|
||||
'http://0.0.0.0:3001',
|
||||
'http://localhost:3000',
|
||||
'http://192.168.219.114:3000',
|
||||
],
|
||||
eslint: {
|
||||
// Предупреждение: это позволяет продакшн сборки завершаться успешно даже если
|
||||
// ваш проект имеет ошибки ESLint.
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
typescript: {
|
||||
// Опасно: это позволяет продакшн сборки завершаться успешно даже если
|
||||
// ваш проект имеет ошибки TypeScript.
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
|
||||
// Добавляем настройки для правильной работы в development
|
||||
trailingSlash: false,
|
||||
skipTrailingSlashRedirect: true,
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function LoginPage() {
|
||||
setApiError(null)
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/auth/login/`,
|
||||
'/api/auth/login',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function DashboardPage() {
|
||||
iconFile: null,
|
||||
})
|
||||
|
||||
const API = process.env.NEXT_PUBLIC_API_URL ?? ''
|
||||
const API = ''
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token')
|
||||
@@ -70,9 +70,9 @@ export default function DashboardPage() {
|
||||
}
|
||||
// загружаем профиль, группы и ссылки
|
||||
Promise.all([
|
||||
fetch(`${API}/api/auth/user/`, { headers: { Authorization: `Bearer ${token}` } }),
|
||||
fetch(`${API}/api/groups/`, { headers: { Authorization: `Bearer ${token}` } }),
|
||||
fetch(`${API}/api/links/`, { headers: { Authorization: `Bearer ${token}` } }),
|
||||
fetch('/api/auth/user', { headers: { Authorization: `Bearer ${token}` } }),
|
||||
fetch('/api/groups', { headers: { Authorization: `Bearer ${token}` } }),
|
||||
fetch('/api/links', { headers: { Authorization: `Bearer ${token}` } }),
|
||||
])
|
||||
.then(async ([uRes, gRes, lRes]) => {
|
||||
if (!uRes.ok) throw new Error('Не удалось получить профиль')
|
||||
@@ -98,8 +98,8 @@ export default function DashboardPage() {
|
||||
async function reloadData() {
|
||||
const token = localStorage.getItem('token')!
|
||||
const [gRes, lRes] = await Promise.all([
|
||||
fetch(`${API}/api/groups/`, { headers: { Authorization: `Bearer ${token}` } }),
|
||||
fetch(`${API}/api/links/`, { headers: { Authorization: `Bearer ${token}` } }),
|
||||
fetch('/api/groups', { headers: { Authorization: `Bearer ${token}` } }),
|
||||
fetch('/api/links', { headers: { Authorization: `Bearer ${token}` } }),
|
||||
])
|
||||
const groupsData = await gRes.json()
|
||||
const linksData = await lRes.json()
|
||||
@@ -128,8 +128,8 @@ export default function DashboardPage() {
|
||||
fd.append('name', groupForm.name)
|
||||
if (groupForm.iconFile) fd.append('icon', groupForm.iconFile)
|
||||
const url = groupModalMode === 'add'
|
||||
? `${API}/api/groups/`
|
||||
: `${API}/api/groups/${editingGroup?.id}/`
|
||||
? '/api/groups'
|
||||
: `/api/groups/${editingGroup?.id}`
|
||||
const method = groupModalMode === 'add' ? 'POST' : 'PATCH'
|
||||
await fetch(url, {
|
||||
method,
|
||||
@@ -142,7 +142,7 @@ export default function DashboardPage() {
|
||||
async function handleDeleteGroup(grp: Group) {
|
||||
if (!confirm(`Удалить группу "${grp.name}"?`)) return
|
||||
const token = localStorage.getItem('token')!
|
||||
await fetch(`${API}/api/groups/${grp.id}/`, {
|
||||
await fetch(`/api/groups/${grp.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
@@ -171,8 +171,8 @@ export default function DashboardPage() {
|
||||
if (linkForm.iconFile) fd.append('icon', linkForm.iconFile)
|
||||
fd.append('group', String(currentGroupIdForLink))
|
||||
const url = linkModalMode === 'add'
|
||||
? `${API}/api/links/`
|
||||
: `${API}/api/links/${editingLink?.id}/`
|
||||
? '/api/links'
|
||||
: `/api/links/${editingLink?.id}`
|
||||
const method = linkModalMode === 'add' ? 'POST' : 'PATCH'
|
||||
await fetch(url, {
|
||||
method,
|
||||
@@ -185,7 +185,7 @@ export default function DashboardPage() {
|
||||
async function handleDeleteLink(link: LinkItem) {
|
||||
if (!confirm(`Удалить ссылку "${link.title}"?`)) return
|
||||
const token = localStorage.getItem('token')!
|
||||
await fetch(`${API}/api/links/${link.id}/`, {
|
||||
await fetch(`/api/links/${link.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
|
||||
@@ -28,7 +28,7 @@ export default async function UserPage({
|
||||
params: Promise<{ username: string }>
|
||||
}) {
|
||||
const { username } = await params
|
||||
const API = process.env.NEXT_PUBLIC_API_URL
|
||||
const API = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'
|
||||
|
||||
const res = await fetch(`${API}/api/users/${username}/public`, {
|
||||
cache: 'no-store',
|
||||
|
||||
@@ -23,7 +23,7 @@ export function LayoutWrapper({ children }: { children: ReactNode }) {
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/auth/user/`, {
|
||||
fetch('/api/auth/user', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
.then(res => {
|
||||
@@ -82,7 +82,7 @@ export function LayoutWrapper({ children }: { children: ReactNode }) {
|
||||
src={
|
||||
user.avatar.startsWith('http')
|
||||
? user.avatar
|
||||
: `${process.env.NEXT_PUBLIC_API_URL}${user.avatar}`
|
||||
: `http://localhost:8000${user.avatar}`
|
||||
}
|
||||
alt="Avatar"
|
||||
width={32}
|
||||
|
||||
Reference in New Issue
Block a user