diff --git a/public/images/main-bg.jpg b/public/images/main-bg.jpg new file mode 100644 index 0000000..219f84d Binary files /dev/null and b/public/images/main-bg.jpg differ diff --git a/public/images/main-bg.png b/public/images/main-bg.png new file mode 100644 index 0000000..535a196 Binary files /dev/null and b/public/images/main-bg.png differ diff --git a/src/components/AuthorsSection.tsx b/src/components/AuthorsSection.tsx new file mode 100644 index 0000000..157f344 --- /dev/null +++ b/src/components/AuthorsSection.tsx @@ -0,0 +1,80 @@ +import { Twitter, Instagram, Globe } from 'lucide-react'; +import {useEffect, useState} from "react"; +import axios from "axios"; +import { Author } from "../types/auth.ts"; +import { Link } from 'react-router-dom'; + + +export function AuthorsSection() { + const [authors, setAuthors] = useState([]); + + // Загрузка авторов + useEffect(() => { + const fetchAuthors = async () => { + try { + const response = await axios.get('/api/authors/'); + setAuthors(response.data); + } catch (error) { + console.error('Ошибка загрузки авторов:', error); + } + }; + + fetchAuthors(); + }, []); + + return ( +
+
+

Наши авторы

+

+ Познакомьтесь с талантливыми писателями и экспертами, работающими для вас +

+
+ +
+ {authors.map((author) => ( +
+
+
+ {author.displayName} +
+

{author.displayName}

+ +
+
+

{author.bio}

+ +
+
+ + {author.articlesCount} статей + + + Статьи автора → + +
+
+
+
+ ))} +
+
+ ); +} \ No newline at end of file diff --git a/src/components/CustomImageExtension.tsx b/src/components/CustomImageExtension.tsx new file mode 100644 index 0000000..dfb5900 --- /dev/null +++ b/src/components/CustomImageExtension.tsx @@ -0,0 +1,146 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Node, mergeAttributes } from '@tiptap/core'; +import {Editor, ReactNodeViewRenderer } from '@tiptap/react'; +import { ZoomIn, ZoomOut } from 'lucide-react'; +import { Node as ProseMirrorNode } from 'prosemirror-model'; + + +interface ImageNodeViewProps { + node: ProseMirrorNode; + updateAttributes: (attrs: Record) => void; + editor: Editor; + getPos: () => number; +} + +// React component for the image node view +const ImageNodeView: React.FC = ({ node, updateAttributes, editor, getPos }) => { + const [isSelected, setIsSelected] = useState(false); + const [scale, setScale] = useState(1); + const imageRef = useRef(null); + + // Update selection state when the editor selection changes + useEffect(() => { + const handleSelectionUpdate = ({ editor }: { editor: Editor }) => { + const { state } = editor; + const { selection } = state; + const nodePos = getPos(); + + // Check if this node is selected + const isNodeSelected = selection.$anchor.pos >= nodePos && + selection.$anchor.pos <= nodePos + node.nodeSize; + + setIsSelected(isNodeSelected); + }; + + editor.on('selectionUpdate', handleSelectionUpdate); + return () => { + editor.off('selectionUpdate', handleSelectionUpdate); + }; + }, [editor, getPos, node.nodeSize]); + + // Handle zoom in + const handleZoomIn = (e: React.MouseEvent) => { + e.stopPropagation(); + setScale(prev => Math.min(prev + 0.1, 2)); + updateAttributes({ scale: Math.min(scale + 0.1, 2) }); + }; + + // Handle zoom out + const handleZoomOut = (e: React.MouseEvent) => { + e.stopPropagation(); + setScale(prev => Math.max(prev - 0.1, 0.5)); + updateAttributes({ scale: Math.max(scale - 0.1, 0.5) }); + }; + + // Track current scale from attributes + useEffect(() => { + if (node.attrs.scale) { + setScale(node.attrs.scale); + } + }, [node.attrs.scale]); + + return ( +
+ {/* The actual image */} + {node.attrs.alt + + {/* Zoom controls - only visible when selected */} + {isSelected && ( +
+ + +
+ )} +
+ ); +}; + +// Custom Image extension for TipTap +export const CustomImage = Node.create({ + name: 'customImage', + group: 'block', + inline: false, + selectable: true, + draggable: true, + atom: true, + + addAttributes() { + return { + src: { + default: null, + }, + alt: { + default: null, + }, + title: { + default: null, + }, + scale: { + default: 1, + }, + }; + }, + + parseHTML() { + return [ + { + tag: 'img[src]', + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + const { scale, ...rest } = HTMLAttributes; + return [ + 'div', + { class: 'flex justify-center my-4' }, + ['img', mergeAttributes(rest, { style: `transform: scale(${scale})` })], + ]; + }, + + addNodeView() { + return ReactNodeViewRenderer(ImageNodeView); + }, +}); diff --git a/src/components/FeaturedSection.tsx b/src/components/FeaturedSection.tsx index b7270d9..1dc622e 100644 --- a/src/components/FeaturedSection.tsx +++ b/src/components/FeaturedSection.tsx @@ -2,18 +2,20 @@ import { useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { ArticleCard } from './ArticleCard'; import { Pagination } from './Pagination'; -import {Article, CategoryTitles, CityTitles} from '../types'; +import { Article, CategoryTitles, CityTitles } from '../types'; import axios from "axios"; +const ARTICLES_PER_PAGE = 6; + export function FeaturedSection() { -// const location = useLocation(); const [searchParams, setSearchParams] = useSearchParams(); const category = searchParams.get('category'); const city = searchParams.get('city'); const currentPage = Math.max(1, parseInt(searchParams.get('page') || '1', 10)); const [articles, setArticles] = useState([]); const [totalPages, setTotalPages] = useState(1); + const [totalArticles, setTotalArticles] = useState(0); const [error, setError] = useState(null); // Загрузка статей @@ -27,9 +29,10 @@ export function FeaturedSection() { cityId: city || undefined, }, }); - console.log('Загруженные данные:', response.data); + //console.log('Загруженные данные:', response.data); setArticles(response.data.articles); - setTotalPages(response.data.totalPages); // Берём totalPages из ответа + setTotalPages(response.data.totalPages); + setTotalArticles(response.data.total); } catch (error) { setError('Не удалось загрузить статьи'); console.error(error); @@ -67,8 +70,10 @@ export function FeaturedSection() { {city ? `${CityTitles[Number(city)]} ` : ''} {category ? `${CategoryTitles[Number(category)]} Статьи` : 'Тематические статьи'} -

- Показано {articles.length} статей +

+ Статьи {Math.min((currentPage - 1) * ARTICLES_PER_PAGE + 1, totalArticles)} + - + {Math.min(currentPage * ARTICLES_PER_PAGE, totalArticles)} из {totalArticles}

diff --git a/src/components/TipTapEditor.tsx b/src/components/TipTapEditor.tsx index c3ec6c6..73171d7 100644 --- a/src/components/TipTapEditor.tsx +++ b/src/components/TipTapEditor.tsx @@ -20,11 +20,14 @@ import { ImagePlus } from 'lucide-react'; import { useEffect, useState } from "react"; +import { CustomImage } from "./CustomImageExtension"; +import TipTapImageUploadButton from "./TipTapImageUploadButton"; interface TipTapEditorProps { initialContent: string; onContentChange: (content: string) => void; + atricleId: string; } /* @@ -56,9 +59,10 @@ const ResizableImage = Image.extend({ }); */ -export function TipTapEditor({ initialContent, onContentChange }: TipTapEditorProps) { +export function TipTapEditor({ initialContent, onContentChange, atricleId }: TipTapEditorProps) { const [selectedImage, setSelectedImage] = useState(null); +/* const ResizableImage = Image.extend({ addAttributes() { return { @@ -85,6 +89,7 @@ export function TipTapEditor({ initialContent, onContentChange }: TipTapEditorPr }; }, }); +*/ const editor = useEditor({ extensions: [ @@ -99,7 +104,7 @@ export function TipTapEditor({ initialContent, onContentChange }: TipTapEditorPr TextAlign.configure({ types: ['heading', 'paragraph'], }), - ResizableImage, + CustomImage, Highlight, ], content: initialContent || '', @@ -438,10 +443,11 @@ export function TipTapEditor({ initialContent, onContentChange }: TipTapEditorPr editor?.chain().focus().setImage({ src: url }).run(); } }} - title="Вставить изображение" + title="Вставить изображение (старое)" > + + + + ); +}; + +export default TipTapImageUploadButton; diff --git a/src/pages/AdminPage.tsx b/src/pages/AdminPage.tsx index 4a15483..d9a844a 100644 --- a/src/pages/AdminPage.tsx +++ b/src/pages/AdminPage.tsx @@ -432,7 +432,7 @@ export function AdminPage() { - +
diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 53e579c..25378fd 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -1,6 +1,7 @@ import { useSearchParams } from 'react-router-dom'; import { Header } from '../components/Header'; import { FeaturedSection } from '../components/FeaturedSection'; +import { AuthorsSection } from '../components/AuthorsSection'; import { BackgroundImages } from '../hooks/useBackgroundImage'; import { CategoryDescription, CategoryText, CategoryTitles } from '../types'; @@ -30,6 +31,7 @@ export function HomePage() { return ( <> +
@@ -59,12 +61,18 @@ export function HomePage() {
-
+ ); } \ No newline at end of file diff --git a/src/pages/SearchPage.tsx b/src/pages/SearchPage.tsx index 5c47d0f..899e128 100644 --- a/src/pages/SearchPage.tsx +++ b/src/pages/SearchPage.tsx @@ -11,21 +11,24 @@ const ARTICLES_PER_PAGE = 9; export function SearchPage() { const [searchParams, setSearchParams] = useSearchParams(); const query = searchParams.get('q') || ''; + const authorId = searchParams.get('author'); const page = parseInt(searchParams.get('page') || '1', 10); const [articles, setArticles] = useState([]); const [totalPages, setTotalPages] = useState(1); const [loading, setLoading] = useState(false); + useEffect(() => { const fetchResults = async () => { - if (!query) return; + if (!query && !authorId) return; setLoading(true); try { const response = await api.get('/articles/search', { params: { q: query, + author: authorId, page, limit: ARTICLES_PER_PAGE } @@ -53,7 +56,7 @@ export function SearchPage() {

- {query ? `Результаты поиска "${query}"` : 'Search Articles'} + {query ? `Результаты поиска "${query}"` : 'Статьи автора'}

{articles.length > 0 && (

diff --git a/src/pages/UserManagementPage.tsx b/src/pages/UserManagementPage.tsx index 9e4bbbf..99877d6 100644 --- a/src/pages/UserManagementPage.tsx +++ b/src/pages/UserManagementPage.tsx @@ -7,7 +7,7 @@ import { ImagePlus, X, UserPlus, Pencil } from 'lucide-react'; import { imageResolutions } from '../config/imageResolutions'; import { CategoryIds, CategoryTitles, CityIds, CityTitles } from "../types"; import axios from "axios"; -import {useAuthStore} from "../stores/authStore.ts"; +import { useAuthStore } from "../stores/authStore"; const initialFormData: UserFormData = { @@ -15,6 +15,7 @@ const initialFormData: UserFormData = { email: '', password: '', displayName: '', + bio: '', avatarUrl: '' }; @@ -152,6 +153,7 @@ export function UserManagementPage() { email: user.email, password: '', displayName: user.displayName, + bio: user.bio, avatarUrl: user.avatarUrl }); setShowEditModal(true); @@ -328,7 +330,7 @@ export function UserManagementPage() { setFormData(prev => ({ ...prev, password: e.target.value }))} placeholder={showEditModal ? 'Оставьте пустым, чтобы не менять' : ''} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500" @@ -336,6 +338,18 @@ export function UserManagementPage() { />

+
+ +