From 7fb5daf210f8571c799ec4b2034b7c987763ad55 Mon Sep 17 00:00:00 2001 From: anibilag Date: Tue, 11 Mar 2025 12:47:09 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=80=D0=B5=D0=B0=D0=BA=D1=86=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=81=D1=82=D0=B0=D1=82=D1=8C=D0=B8=20(like/dis?= =?UTF-8?q?like)=20=D0=B8=20URL=20=D0=B2=D1=8B=D0=BD=D0=B5=D1=81=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=B2=20.env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 4 +- src/components/GalleryManager.tsx | 220 ------------------ .../GalleryManager/EditImageModal.tsx | 2 +- src/components/GalleryManager/ImageForm.tsx | 12 +- src/components/GalleryManager/index.tsx | 11 +- .../ImageUpload/CoverImageUpload.tsx | 4 +- src/components/ImageUpload/ImageUploader.tsx | 7 +- src/components/PhotoGallery.tsx | 2 +- src/hooks/useGallery.ts | 33 ++- src/pages/AdminPage.tsx | 86 ++++--- src/pages/ArticlePage.tsx | 69 +++--- src/pages/SearchPage.tsx | 12 +- src/services/galleryService.ts | 8 +- src/services/imageService.ts | 37 +-- src/stores/authStore.ts | 4 +- src/utils/api.ts | 5 +- tsconfig.app.json | 3 +- tsconfig.node.json | 3 +- vite.config.ts | 30 +-- 19 files changed, 192 insertions(+), 360 deletions(-) delete mode 100644 src/components/GalleryManager.tsx diff --git a/.env b/.env index 1665982..882287e 100644 --- a/.env +++ b/.env @@ -1,3 +1 @@ -DATABASE_URL="postgresql://lenin:8D2v7A4s@max.anibilag.ru:5466/russcult" -JWT_SECRET="d131c955dce9f6709acb06f2680b2916ac641f91b814bb0bd28872f9b1edc949" -PORT=5000 \ No newline at end of file +VITE_API_URL="http://localhost:5000" diff --git a/src/components/GalleryManager.tsx b/src/components/GalleryManager.tsx deleted file mode 100644 index f062da9..0000000 --- a/src/components/GalleryManager.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import { useState } from 'react'; -import { X, Plus, Move, Pencil, Trash2 } from 'lucide-react'; -import { GalleryImage } from '../types'; - -interface GalleryManagerProps { - images: GalleryImage[]; - onChange: (images: GalleryImage[]) => void; -} - -export function GalleryManager({ images, onChange }: GalleryManagerProps) { - const [editingImage, setEditingImage] = useState(null); - const [newImageUrl, setNewImageUrl] = useState(''); - const [newImageCaption, setNewImageCaption] = useState(''); - const [newImageAlt, setNewImageAlt] = useState(''); - - const handleAddImage = () => { - if (!newImageUrl.trim()) return; - - const newImage: GalleryImage = { - id: Date.now().toString(), - url: newImageUrl, - caption: newImageCaption, - alt: newImageAlt || newImageCaption - }; - - onChange([...images, newImage]); - setNewImageUrl(''); - setNewImageCaption(''); - setNewImageAlt(''); - }; - - const handleUpdateImage = (updatedImage: GalleryImage) => { - onChange(images.map(img => img.id === updatedImage.id ? updatedImage : img)); - setEditingImage(null); - }; - - const handleRemoveImage = (id: string) => { - onChange(images.filter(img => img.id !== id)); - }; - - const handleReorder = (dragIndex: number, dropIndex: number) => { - const reorderedImages = [...images]; - const [draggedImage] = reorderedImages.splice(dragIndex, 1); - reorderedImages.splice(dropIndex, 0, draggedImage); - onChange(reorderedImages); - }; - - return ( -
- {/* Add New Image */} -
-

Добавить новое фото

-
-
- - setNewImageUrl(e.target.value)} - className="px-2 py-1 mt-1 block w-full rounded-lg border border-gray-300 shadow-sm hover:border-blue-300 focus:border-blue-500 focus:ring-4 focus:ring-blue-100 transition-all duration-200" - placeholder="https://example.com/image.jpg" - /> -
-
- - setNewImageCaption(e.target.value)} - className="px-2 py-1 mt-1 block w-full rounded-lg border border-gray-300 shadow-sm hover:border-blue-300 focus:border-blue-500 focus:ring-4 focus:ring-blue-100 transition-all duration-200" - /> -
-
- - setNewImageAlt(e.target.value)} - className="px-2 py-1 mt-1 block w-full rounded-lg border border-gray-300 shadow-sm hover:border-blue-300 focus:border-blue-500 focus:ring-4 focus:ring-blue-100 transition-all duration-200" - /> -
- -
-
- - {/* Gallery Preview */} -
-

Состав галереи

-
- {images.map((image, index) => ( -
e.dataTransfer.setData('text/plain', index.toString())} - onDragOver={(e) => e.preventDefault()} - onDrop={(e) => { - e.preventDefault(); - const dragIndex = parseInt(e.dataTransfer.getData('text/plain')); - handleReorder(dragIndex, index); - }} - > - {image.alt} -
-
-
- - - -
-
-
-
- {image.caption} -
-
- ))} -
-
- - {/* Edit Image Modal */} - {editingImage && ( -
-
-
-

Edit Image

- -
-
-
- - setEditingImage({ ...editingImage, url: e.target.value })} - className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500" - /> -
-
- - setEditingImage({ ...editingImage, caption: e.target.value })} - className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500" - /> -
-
- - setEditingImage({ ...editingImage, alt: e.target.value })} - className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500" - /> -
-
- - -
-
-
-
- )} -
- ); -} \ No newline at end of file diff --git a/src/components/GalleryManager/EditImageModal.tsx b/src/components/GalleryManager/EditImageModal.tsx index 052af6e..5d626bf 100644 --- a/src/components/GalleryManager/EditImageModal.tsx +++ b/src/components/GalleryManager/EditImageModal.tsx @@ -16,7 +16,7 @@ export function EditImageModal({ image, onClose, onSave }: EditImageModalProps)
-

Edit Image

+

Редактирование изображения

{ - handleGalleryImageUpload(imageUrl); + setNewImageUrl(imageUrl); setShowGalleryUploader(false); - }} /> + }} + articleId={articleId} + />
)} diff --git a/src/pages/ArticlePage.tsx b/src/pages/ArticlePage.tsx index dd672d8..9aa898f 100644 --- a/src/pages/ArticlePage.tsx +++ b/src/pages/ArticlePage.tsx @@ -4,22 +4,16 @@ import { ArrowLeft, Clock, Share2, Bookmark } from 'lucide-react'; import { Header } from '../components/Header'; import { ReactionButtons } from '../components/ReactionButtons'; import { PhotoGallery } from '../components/PhotoGallery'; -//import { articles } from '../data/mock'; -import {Article, CategoryTitles} from '../types'; +import { Article, CategoryTitles } from '../types'; import { ArticleContent } from '../components/ArticleContent'; import MinutesWord from '../components/MinutesWord'; import axios from "axios"; +import api from "../utils/api"; export function ArticlePage() { const { id } = useParams(); const [articleData, setArticleData] = useState
(null); -// const [error, setError] = useState(null); - -/* - const [articleData, setArticleData] = useState
( - articles.find(a => a.id === id) - ); -*/ + const [error, setError] = useState(null); useEffect(() => { const fetchArticle = async () => { @@ -27,7 +21,7 @@ export function ArticlePage() { const response = await axios.get(`/api/articles/${id}`); setArticleData(response.data); } catch (error) { - //setError('Не удалось загрузить статью'); + setError('Не удалось загрузить статью'); console.error(error); } }; @@ -43,7 +37,7 @@ export function ArticlePage() { return (
-

Article not found

+

Статья не найдена

Назад на главную @@ -52,25 +46,37 @@ export function ArticlePage() { ); } - const handleReaction = (reaction: 'like' | 'dislike') => { - setArticleData(prev => { - if (!prev) return prev; + const handleReaction = async (reaction: 'like' | 'dislike') => { + if (!articleData || !id) return; - const newArticle = { ...prev }; - - if (prev.userReaction === 'like') newArticle.likes--; - if (prev.userReaction === 'dislike') newArticle.dislikes--; - - if (prev.userReaction !== reaction) { - if (reaction === 'like') newArticle.likes++; - if (reaction === 'dislike') newArticle.dislikes++; - newArticle.userReaction = reaction; - } else { - newArticle.userReaction = null; - } - - return newArticle; - }); + try { + const { data: updatedArticle } = await api.put(`/articles/react/${id}`, { + reaction: articleData.userReaction === reaction ? null : reaction, + likes: articleData.likes, + dislikes: articleData.dislikes, + }); + setArticleData(prev => { + if (!prev) return prev; + + const newArticle = { ...prev }; + + if (prev.userReaction === 'like') newArticle.likes--; + if (prev.userReaction === 'dislike') newArticle.dislikes--; + + if (prev.userReaction !== reaction) { + if (reaction === 'like') newArticle.likes = updatedArticle.likes; + if (reaction === 'dislike') newArticle.dislikes = updatedArticle.dislikes; + newArticle.userReaction = reaction; + } else { + newArticle.userReaction = null; + } + + return newArticle; + }); + } catch (error) { + setError('Ошибка оценки статьи. Попробуйте еще раз.'); + console.error('Ошибка оценки статьи:', error); + } }; return ( @@ -149,6 +155,11 @@ export function ArticlePage() { {/* Article Footer */}
+ {error && ( +
+ {error} +
+ )} { const fetchResults = async () => { if (!query && !authorId) return; @@ -36,14 +36,16 @@ export function SearchPage() { setArticles(response.data.articles); setTotalPages(response.data.totalPages); } catch (error) { - console.error('Search failed:', error); + console.error('Ошибка поиска:', error); } finally { setLoading(false); } }; + window.scrollTo({ top: 0, behavior: 'smooth' }); + fetchResults(); - }, [query, page]); + }, [query, authorId, page]); const handlePageChange = (newPage: number) => { setSearchParams({ q: query, page: newPage.toString() }); @@ -84,13 +86,13 @@ export function SearchPage() { /> )} - ) : query ? ( + ) : (query || authorId) ? (

Не найдено ни одной статьи

- Попытайтесь изменить сроку поиска или browse our categories instead + {authorId ? "Этот автор не опубликовал пока ни одной статьи" : "Ничего не найдено. Попытайтесь изменить сроку поиска"}

) : null} diff --git a/src/services/galleryService.ts b/src/services/galleryService.ts index 9e75144..78c0c9e 100644 --- a/src/services/galleryService.ts +++ b/src/services/galleryService.ts @@ -11,12 +11,12 @@ export const galleryService = { size: number; format: string }) => { - const { data } = await axios.post(`/api/gallery/article/${articleId}`, imageData); + const { data } = await axios.post(`/gallery/article/${articleId}`, imageData); return data; }, updateImage: async (id: string, updates: Partial) => { - const { data } = await axios.put(`/api/gallery/${id}`, updates); + const { data } = await axios.put(`/gallery/${id}`, updates); return data; }, @@ -25,11 +25,11 @@ export const galleryService = { }, reorderImages: async (articleId: string, imageIds: string[]) => { - await axios.post(`/api/gallery/article/${articleId}/reorder`, { imageIds }); + await axios.post(`/gallery/article/${articleId}/reorder`, { imageIds }); }, getArticleGallery: async (articleId: string) => { - const { data } = await axios.get(`/api/gallery/article/${articleId}`); + const { data } = await axios.get(`/gallery/article/${articleId}`); return data as GalleryImage[]; } }; \ No newline at end of file diff --git a/src/services/imageService.ts b/src/services/imageService.ts index 53d0b4c..00fa8d2 100644 --- a/src/services/imageService.ts +++ b/src/services/imageService.ts @@ -1,22 +1,18 @@ import axios from '../utils/api'; -import { ImageResolution, UploadedImage } from '../types/image'; +import {ImageResolution, UploadedImage} from '../types/image'; -export async function uploadImage( - file: File, - resolution: ImageResolution, - onProgress?: (progress: number) => void -): Promise { - // Get pre-signed URL for S3 upload - const { data: { uploadUrl, imageId } } = await axios.post('/images/upload-url', { - fileName: file.name, - fileType: file.type, - resolution: resolution.id - }); +export async function uploadImageToS3(file: File, resolution: ImageResolution, articleId: string, onProgress?: (progress: number) => void): Promise { - // Upload to S3 - await axios.put(uploadUrl, file, { + const formData = new FormData(); + formData.append('file', file); + formData.append('resolutionId', resolution.id); + formData.append('folder', 'articles/' + articleId + '/gallery'); + + // Загрузка файла изображения в хранилище S3 + const response = await axios.post('/images/upload-url', formData, { headers: { - 'Content-Type': file.type + 'Content-Type': 'multipart/form-data', + 'Authorization': `Bearer ${localStorage.getItem('token')}`, // Передача токена аутентификации }, onUploadProgress: (progressEvent) => { if (progressEvent.total) { @@ -26,7 +22,12 @@ export async function uploadImage( } }); - // Get the processed image details - const { data: image } = await axios.get(`/images/${imageId}`); - return image; + return { + id: '', + url: response.data?.fileUrl, + width: resolution.width, + height: resolution.height, + size: 102400, + format: 'webp' + }; } \ No newline at end of file diff --git a/src/stores/authStore.ts b/src/stores/authStore.ts index c8e5610..64a17d7 100644 --- a/src/stores/authStore.ts +++ b/src/stores/authStore.ts @@ -27,15 +27,13 @@ export const useAuthStore = create((set) => ({ return; } - const response = await axios.post('http://localhost:5000/api/auth/login', { + const response = await axios.post('/api/auth/login', { email, password }); const { user, token } = response.data; localStorage.setItem('token', token); set({ user }); - } catch (error) { - throw error; } finally { set({ loading: false }); } diff --git a/src/utils/api.ts b/src/utils/api.ts index f101fcc..5f5b60d 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -1,7 +1,10 @@ import axios from 'axios'; +// Берем baseURL из .env +const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000'; + const api = axios.create({ - baseURL: 'http://localhost:5000/api' + baseURL: `${API_URL}/api` }); api.interceptors.request.use((config) => { diff --git a/tsconfig.app.json b/tsconfig.app.json index f0a2350..8eef2b3 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -11,7 +11,8 @@ "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", - "noEmit": true, + "composite": true, + "noEmit": false, "jsx": "react-jsx", /* Linting */ diff --git a/tsconfig.node.json b/tsconfig.node.json index 0d3d714..bcb93bc 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -10,7 +10,8 @@ "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", - "noEmit": true, + "composite": true, + "noEmit": false, /* Linting */ "strict": true, diff --git a/vite.config.ts b/vite.config.ts index 8b7f910..9d1ee9a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,18 +1,22 @@ -import { defineConfig } from 'vite'; +import { defineConfig, loadEnv } from 'vite'; import react from '@vitejs/plugin-react'; -export default defineConfig({ - plugins: [react()], - optimizeDeps: { - exclude: ['@prisma/client', 'lucide-react'], - }, - server: { - host: true, - proxy: { - '/api': { - target: 'http://localhost:5000', - changeOrigin: true, +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd()); + + return { + plugins: [react()], + optimizeDeps: { + exclude: ['@prisma/client', 'lucide-react'], + }, + server: { + host: true, + proxy: { + '/api': { + target: env.VITE_API_URL || 'http://localhost:5000', + changeOrigin: true, + }, }, }, - }, + }; }); \ No newline at end of file