Переход от монолитной Header к набору компонентов. Работает поиск статей по дате. Запоминание позиции скролинга при переходе к статье.

This commit is contained in:
anibilag 2025-06-17 23:26:27 +03:00
parent 933d82e3ac
commit 9ca069c49b
16 changed files with 256 additions and 140 deletions

9
Dockerfile Normal file
View File

@ -0,0 +1,9 @@
FROM node:20 AS build
WORKDIR /app
COPY . .
RUN npm install && npm run build
# Переход на stage со статикой
FROM nginx:alpine AS production
COPY --from=build /app/dist /usr/share/nginx/html

View File

@ -2,7 +2,7 @@ import { Clock, ThumbsUp, MapPin, ThumbsDown } from 'lucide-react';
import { Link, useLocation } from 'react-router-dom';
import { Article, CategoryTitles, CityTitles } from '../types';
import MinutesWord from './Words/MinutesWord';
import { useScrollStore } from '../stores/scrollStore';
interface ArticleCardProps {
article: Article;
@ -11,11 +11,19 @@ interface ArticleCardProps {
export function ArticleCard({ article, featured = false }: ArticleCardProps) {
const location = useLocation();
const setHomeScrollPosition = useScrollStore(state => state.setHomeScrollPosition);
const writerAuthors = article.authors
.filter(a => a.role === 'WRITER')
.sort((a, b) => (a.author.order ?? 0) - (b.author.order ?? 0));
const handleArticleClick = () => {
// Сохранить текущее положение скролинга при переходе к статье
if (location.pathname === '/') {
setHomeScrollPosition(window.scrollY);
}
};
return (
<article className={`overflow-hidden rounded-lg shadow-lg transition-transform hover:scale-[1.02] ${
featured ? 'sm:col-span-2 sm:row-span-2' : ''
@ -76,6 +84,7 @@ export function ArticleCard({ article, featured = false }: ArticleCardProps) {
<Link
to={`/article/${article.id}`}
state={{ from: location.pathname + location.search }}
onClick={handleArticleClick}
className="text-blue-600 font-medium hover:text-blue-800"
>
Читать

View File

@ -4,11 +4,12 @@ import { CoverImageUpload } from './ImageUpload/CoverImageUpload';
import { ImageUploader } from './ImageUpload/ImageUploader';
import { GalleryManager } from './GalleryManager';
import { useGallery } from '../hooks/useGallery';
import { ArticleData, Author, AuthorRole, CategoryTitles, CityTitles, GalleryImage } from '../types';
import { ArticleData, Author, AuthorLink, AuthorRole, CategoryTitles, CityTitles, GalleryImage } from '../types';
import { useAuthStore } from '../stores/authStore';
import ConfirmModal from './ConfirmModal';
import RolesWord from "./Words/RolesWord";
import { Trash2, UserPlus } from "lucide-react";
import isEqual from 'lodash/isEqual';
const ArticleAuthorRoleLabels: Record<AuthorRole, string> = {
@ -56,6 +57,7 @@ export function ArticleForm({
}: ArticleFormProps) {
const { user } = useAuthStore();
const isAdmin = user?.permissions.isAdmin || false;
const showGallery = false;
const [title, setTitle] = useState('');
const [excerpt, setExcerpt] = useState('');
@ -75,14 +77,15 @@ export function ArticleForm({
const [newRole, setNewRole] = useState('');
const [newAuthorId, setNewAuthorId] = useState('');
const [showAddAuthorModal, setShowAddAuthorModal] = useState(false);
const [formReady, setFormReady] = useState(false);
const { images: galleryImages, loading: galleryLoading, error: galleryError, addImage: addGalleryImage, updateImage: updateGalleryImage, deleteImage: deleteGalleryImage, reorderImages: reorderGalleryImages } = useGallery(editingId || '');
const [selectedAuthors, setSelectedAuthors] = useState<Array<{
authorId: string;
role: AuthorRole;
author: Author;
}>>([]);
const [selectedAuthors, setSelectedAuthors] = useState<AuthorLink[]>([]);
useEffect(() => {
if (initialFormState) setFormReady(true);
}, [initialFormState]);
// Добавляем обработку ошибок
useEffect(() => {
@ -135,12 +138,27 @@ export function ArticleForm({
}, [initialFormState]);
useEffect(() => {
if (!initialFormState) return;
if (!initialFormState || !formReady) return;
const currentState: FormState = {
title,
excerpt,
categoryId,
cityId,
coverImage,
readTime,
content,
authors: selectedAuthors,
galleryImages: showGallery ? displayedImages : initialFormState.galleryImages,
};
const currentState: FormState = { title, excerpt, categoryId, cityId, coverImage, readTime, content, authors: selectedAuthors, galleryImages: displayedImages };
const areRequiredFieldsFilled = title.trim() !== '' && excerpt.trim() !== '';
const hasFormChanges = Object.keys(initialFormState).some(key => {
if (!formReady) return false;
if (key === 'galleryImages') {
if (!showGallery) return false; // 💡 игнорировать при выключенной галерее
const isDifferent = JSON.stringify(currentState[key]) !== JSON.stringify(initialFormState[key]);
if (isInitialLoad && isDifferent) return false;
return isDifferent;
@ -150,7 +168,8 @@ export function ArticleForm({
}
const currentValue = typeof currentState[key as keyof FormState] === 'number' ? String(currentState[key as keyof FormState]) : currentState[key as keyof FormState];
const initialValue = typeof initialFormState[key as keyof FormState] === 'number' ? String(initialFormState[key as keyof FormState]) : initialFormState[key as keyof FormState];
return currentValue !== initialValue;
return !isEqual(currentValue, initialValue);
});
setHasChanges(hasFormChanges && areRequiredFieldsFilled);
@ -158,7 +177,7 @@ export function ArticleForm({
if (isInitialLoad) {
setIsInitialLoad(false);
}
}, [title, excerpt, categoryId, cityId, coverImage, readTime, content, selectedAuthors, displayedImages, initialFormState, isInitialLoad]);
}, [title, excerpt, categoryId, cityId, coverImage, readTime, content, selectedAuthors, displayedImages, initialFormState, isInitialLoad, formReady, showGallery]);
const filteredAuthors = authors.filter(
(a) =>
@ -195,10 +214,7 @@ export function ArticleForm({
content: content || '',
importId: 0,
isActive: false,
authors: selectedAuthors.map(a => ({
author: a.author,
role: a.role,
}))
authors: selectedAuthors,
};
try {
@ -391,51 +407,55 @@ export function ArticleForm({
articleId={articleId}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
<span className="italic font-bold">Галерея</span>
</label>
{editingId ? (
<>
<ImageUploader
onUploadComplete={(imageUrl) => {
setFormNewImageUrl(imageUrl);
}}
articleId={articleId}
/>
<GalleryManager
images={displayedImages}
imageUrl={formNewImageUrl}
setImageUrl={setFormNewImageUrl}
onAddImage={async (imageData) => {
console.log('Добавляем изображение в форме:', imageData);
const newImage = await addGalleryImage(imageData);
setFormNewImageUrl('');
setDisplayedImages(prev => [...prev, newImage]);
}}
onReorder={(images) => {
console.log('Переупорядочиваем изображения в форме:', images);
reorderGalleryImages(images.map(img => img.id));
setDisplayedImages(images);
}}
onDelete={(id) => {
console.log('Удаляем изображение в форме:', id);
deleteGalleryImage(id);
setDisplayedImages(prev => prev.filter(img => img.id !== id));
}}
onEdit={(image) => {
console.log('Редактируем изображение в форме:', image);
updateGalleryImage(image.id, { alt: image.alt }).catch(err => {
console.error('Ошибка при редактировании изображения в форме:', err);
setError('Не удалось обновить изображение');
});
}}
/>
</>
) : (
<p className="text-sm text-gray-500">Галерея доступна после создания статьи.</p>
)}
</div>
{showGallery && (
<div>
<label className="block text-sm font-medium text-gray-700">
<span className="italic font-bold">Галерея</span>
</label>
{editingId ? (
<>
<ImageUploader
onUploadComplete={(imageUrl) => {
setFormNewImageUrl(imageUrl);
}}
articleId={articleId}
/>
<GalleryManager
images={displayedImages}
imageUrl={formNewImageUrl}
setImageUrl={setFormNewImageUrl}
onAddImage={async (imageData) => {
console.log('Добавляем изображение в форме:', imageData);
const newImage = await addGalleryImage(imageData);
setFormNewImageUrl('');
setDisplayedImages(prev => [...prev, newImage]);
}}
onReorder={(images) => {
console.log('Переупорядочиваем изображения в форме:', images);
reorderGalleryImages(images.map(img => img.id));
setDisplayedImages(images);
}}
onDelete={(id) => {
console.log('Удаляем изображение в форме:', id);
deleteGalleryImage(id);
setDisplayedImages(prev => prev.filter(img => img.id !== id));
}}
onEdit={(image) => {
console.log('Редактируем изображение в форме:', image);
updateGalleryImage(image.id, { alt: image.alt }).catch(err => {
console.error('Ошибка при редактировании изображения в форме:', err);
setError('Не удалось обновить изображение');
});
}}
/>
</>
) : (
<p className="text-sm text-gray-500">Галерея доступна после создания статьи.</p>
)}
</div>
)}
<div className="flex justify-end gap-4">
<button
type="button"

View File

@ -25,23 +25,23 @@ export function CitySelector({ cities, currentCity }: CitySelectorProps) {
};
return (
<div className="relative">
<select
value={currentCity || ''}
onChange={handleCityChange}
className="appearance-none bg-white pl-3 pr-8 py-2 text-sm font-medium rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent cursor-pointer"
>
<option value="">All Cities</option>
{cities.map((city) => (
<option key={city} value={city}>
{CityTitles[city]}
</option>
))}
</select>
<ChevronDown
size={16}
className="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-400 pointer-events-none"
/>
</div>
<div className="relative">
<select
value={currentCity || ''}
onChange={handleCityChange}
className="appearance-none bg-white pl-3 pr-8 py-2 text-sm font-medium rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent cursor-pointer"
>
<option value="">Все столицы</option>
{cities.map((city) => (
<option key={city} value={city}>
{CityTitles[city]}
</option>
))}
</select>
<ChevronDown
size={16}
className="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-400 pointer-events-none"
/>
</div>
);
}

View File

@ -25,13 +25,13 @@ export function MobileMenu({
<div className="lg:hidden border-b border-gray-200">
<div className="px-2 pt-2 pb-3 space-y-1">
<div className="px-3 py-2">
<h3 className="text-sm font-medium text-gray-500 mb-2">City</h3>
<h3 className="text-sm font-medium text-gray-500 mb-2">Столицы</h3>
<select
value={currentCity || ''}
onChange={onCityChange}
className="w-full bg-white px-3 py-2 text-base font-medium rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="">All Cities</option>
<option value="">Все столицы</option>
{cities.map((city) => (
<option key={city} value={city}>
{CityTitles[city]}
@ -41,18 +41,8 @@ export function MobileMenu({
</div>
<div className="px-3 py-2">
<h3 className="text-sm font-medium text-gray-500 mb-2">Categories</h3>
<div className="space-y-1">
<Link
to={currentCity ? `/?city=${currentCity}` : '/'}
className={`block px-3 py-2 rounded-md text-base font-medium ${
!currentCategory
? 'bg-blue-50 text-blue-600'
: 'text-gray-600 hover:bg-gray-50'
}`}
>
All Categories
</Link>
<h3 className="text-sm font-medium text-gray-500 mb-2">Разделы</h3>
<div className="space-y-0">
{categories.map((category) => (
<Link
key={category}

View File

@ -10,25 +10,14 @@ interface NavigationProps {
export function Navigation({ categories, currentCategory, currentCity }: NavigationProps) {
return (
<div className="flex items-center space-x-4 overflow-x-auto">
<span className="text-sm font-medium text-gray-500"></span>
<Link
to={currentCity ? `/?city=${currentCity}` : '/'}
className={`px-3 py-2 text-sm font-medium transition-colors whitespace-nowrap ${
!currentCategory
? 'text-blue-600 hover:text-blue-800'
: 'text-gray-600 hover:text-gray-900'
}`}
>
All Categories
</Link>
{categories.map((category) => (
<Link
key={category}
to={`/?category=${category}${currentCity ? `&city=${currentCity}` : ''}`}
className={`px-3 py-2 text-sm font-medium transition-colors whitespace-nowrap ${
className={`px-3 py-2 font-medium transition-colors whitespace-nowrap ${
Number(currentCategory) === category
? 'text-blue-600 hover:text-blue-800'
: 'text-gray-600 hover:text-gray-900'
? 'text-base md:text-lg lg:text-xl font-semibold text-blue-600 hover:text-blue-800 bg-gray-200'
: 'text-sm md:text-base lg:text-lg font-medium text-gray-600 hover:text-gray-900'
}`}
>
{CategoryTitles[category]}

View File

@ -4,6 +4,7 @@ import { Search, Calendar } from 'lucide-react';
import { DayPicker } from 'react-day-picker';
import { format } from 'date-fns';
import 'react-day-picker/dist/style.css';
import { ru } from 'date-fns/locale';
export function SearchBar() {
@ -16,11 +17,6 @@ export function SearchBar() {
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
const params = new URLSearchParams();
if (selectedDate) {
params.set('date', format(selectedDate, 'yyyy-MM-dd'));
}
if (searchQuery.trim()) {
navigate(`/search?q=${encodeURIComponent(searchQuery.trim())}`);
@ -36,6 +32,15 @@ export function SearchBar() {
const handleDateSelect = (date: Date | undefined) => {
setSelectedDate(date);
setShowDatePicker(false);
if (date) {
const params = new URLSearchParams();
params.set('date', format(date, 'yyyy-MM-dd'));
if (searchQuery.trim()) {
params.set('q', searchQuery.trim());
}
navigate(`/search?${params.toString()}`);
}
};
// Close datepicker when clicking outside
@ -56,7 +61,7 @@ export function SearchBar() {
<div className="relative">
<input
type="text"
placeholder="Поиск###..."
placeholder="Поиск..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={handleSearchKeyPress}
@ -66,7 +71,7 @@ export function SearchBar() {
type="submit"
className="absolute left-3 top-2.5 text-gray-400 hover:text-gray-600"
>
<Search size={28} />
<Search size={18} />
</button>
</div>
@ -91,6 +96,7 @@ export function SearchBar() {
mode="single"
selected={selectedDate}
onSelect={handleDateSelect}
locale={ru}
className="p-3"
/>
</div>

View File

@ -33,11 +33,20 @@ export function Header() {
return (
<header className="sticky top-0 z-50 bg-white shadow-sm">
{/* Название блога над навигацией */}
<div className="bg-white border-b border-gray-100">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-2">
<h1 className="text-center text-xl sm:text-2xl font-semibold text-gray-800 tracking-tight bg-gradient-to-b from-white to-gray-100 border-b border-gray-100 pb-2">
Новая Культура Двух Столиц
</h1>
</div>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Main header row */}
<div className="flex justify-between items-center h-16">
<div className="flex items-center">
<button
className="p-2 rounded-md text-gray-500 lg:hidden hover:bg-gray-100"
className="p-2 rounded-md text-gray-500 xl:hidden hover:bg-gray-100"
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
>
{isMobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
@ -46,10 +55,10 @@ export function Header() {
<Logo />
</div>
</div>
<nav className="hidden lg:flex items-center space-x-8">
{/* Desktop navigation - single line for large screens */}
<nav className="hidden xl:flex items-center space-x-8">
<div className="flex items-center space-x-4">
<span className="text-sm font-medium text-gray-500"></span>
<CitySelector cities={CityIds} currentCity={currentCity} />
</div>
@ -77,6 +86,27 @@ export function Header() {
</Link>
</div>
</div>
{/* Tablet navigation - two lines for medium screens */}
<div className="hidden lg:block xl:hidden border-t border-gray-100">
<div className="py-3">
<div className="flex flex-col space-y-3">
{/* First line: City selector */}
<div className="flex justify-center">
<CitySelector cities={CityIds} currentCity={currentCity} />
</div>
{/* Second line: Categories navigation */}
<div className="flex justify-center">
<Navigation
categories={CategoryIds}
currentCategory={currentCategory}
currentCity={currentCity}
/>
</div>
</div>
</div>
</div>
</div>
<MobileMenu

View File

@ -6,7 +6,7 @@ import { CategoryIds, CategoryTitles, CityIds, CityTitles } from '../types';
import { useBookmarkStore } from "../stores/bookmarkStore";
export function Header() {
export function HeaderMono() {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const location = useLocation();

View File

@ -5,7 +5,7 @@ import { ArticleList } from '../components/ArticleList';
import { ArticleForm } from '../components/ArticleForm';
import { GalleryModal } from '../components/GalleryManager/GalleryModal';
import { ArticleDeleteModal } from '../components/ArticleDeleteModal';
import { Article, Author, GalleryImage, ArticleData, AuthorRole } from '../types';
import { Article, Author, GalleryImage, ArticleData, AuthorRole, AuthorLink } from '../types';
import { usePermissions } from '../hooks/usePermissions';
import { FileJson, Users, UserSquare2 } from "lucide-react";
import { Link } from 'react-router-dom';
@ -19,10 +19,7 @@ interface FormState {
coverImage: string;
readTime: number;
content: string;
authors: {
role: AuthorRole;
author: Author;
}[];
authors: AuthorLink[];
galleryImages: GalleryImage[];
}
@ -60,6 +57,14 @@ export function AdminPage() {
fetchAuthors();
}, []);
function normalizeAuthors(authors: AuthorLink[]): AuthorLink[] {
return authors.map(({ authorId, role, author }) => ({
authorId,
role,
author,
}));
}
const handleEdit = useCallback(
(id: string) => {
const article = articles.find(a => a.id === id);
@ -74,8 +79,8 @@ export function AdminPage() {
coverImage: article.coverImage,
readTime: article.readTime,
content: article.content,
authors: article.authors ?? [],
galleryImages: [],
authors: normalizeAuthors(article.authors) ?? [],
galleryImages: article.gallery ?? [],
});
setArticleId(article.id);
setShowForm(true);

View File

@ -1,11 +1,12 @@
import { useState, useEffect } from 'react';
import { useParams, Link, useLocation } from 'react-router-dom';
import { useParams, Link, useLocation, useNavigate } from 'react-router-dom';
import { ArrowLeft, Clock } from 'lucide-react';
import { Header } from '../components/Header';
import { ReactionButtons } from '../components/ReactionButtons';
import { PhotoGallery } from '../components/PhotoGallery';
import { Article, CategoryTitles, CityTitles } from '../types';
import { SEO } from '../components/SEO';
import { useScrollStore } from '../stores/scrollStore';
import { ArticleContent } from '../components/ArticleContent';
import { ShareButton } from '../components/ShareButton';
import { BookmarkButton } from '../components/BookmarkButton';
@ -16,13 +17,30 @@ import api from "../utils/api";
export function ArticlePage() {
const { id } = useParams();
const location = useLocation();
const navigate = useNavigate();
const [articleData, setArticleData] = useState<Article | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const restoreHomeScrollPosition = useScrollStore(state => state.restoreHomeScrollPosition);
// Получть предыдущее состояние местоположения или создать путь по умолчанию
const backTo = location.state?.from || '/';
const handleBackClick = (e: React.MouseEvent) => {
e.preventDefault();
// При возврате на главную страницу восстановливается положение скролинга
if (backTo === '/' || backTo.startsWith('/?')) {
navigate(backTo);
// Восстановление положения скролинга после навигации
setTimeout(() => {
restoreHomeScrollPosition();
}, 50);
} else {
navigate(backTo);
}
};
useEffect(() => {
const fetchArticle = async () => {
try {
@ -114,13 +132,13 @@ export function ArticlePage() {
/>
<Header />
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<Link
to={backTo}
className="inline-flex items-center text-gray-600 hover:text-gray-900 mb-8"
<button
onClick={handleBackClick}
className="inline-flex items-center text-gray-600 hover:text-gray-900 mb-8 transition-colors"
>
<ArrowLeft size={20} className="mr-2" />
К списку статей
</Link>
</button>
<article>
{/* Article Header */}

View File

@ -19,6 +19,7 @@ import { imageResolutions } from '../config/imageResolutions';
import { useAuthStore } from '../stores/authStore';
import RolesWord from "../components/Words/RolesWord";
import axios from "axios";
import {AuthorRole} from "../types";
const roleIcons: Record<string, JSX.Element> = {
@ -484,7 +485,7 @@ export function AuthorManagementPage() {
});
}}
/>
<span><RolesWord role={role} /></span>
<span><RolesWord role={role as AuthorRole} /></span>
</label>
))}
</div>

View File

@ -1,8 +1,10 @@
import { useEffect } from "react";
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 { useScrollStore } from '../stores/scrollStore';
import { CategoryDescription, CategoryText, CategoryTitles } from '../types';
import { SEO } from '../components/SEO';
import { MasterBio } from "../components/MasterBio";
@ -12,6 +14,16 @@ export function HomePage() {
const [searchParams] = useSearchParams();
const categoryId = searchParams.get('category');
const backgroundImage= BackgroundImages[Number(categoryId)];
const restoreHomeScrollPosition = useScrollStore(state => state.restoreHomeScrollPosition);
// Восстановление позиции скролинга, когда возвращаемся на главную страницу
useEffect(() => {
// Проверка, возвращаемся ли мы к статье (сохранено положение скролинга).
const hasStoredPosition = useScrollStore.getState().homeScrollPosition > 0;
if (hasStoredPosition) {
restoreHomeScrollPosition();
}
}, [restoreHomeScrollPosition]);
const getHeroTitle = () => {
if (categoryId) {

View File

@ -6,6 +6,7 @@ import { Pagination } from '../components/Pagination';
import { Article } from '../types';
import api from '../utils/api';
import { format, parseISO } from 'date-fns';
import { ru } from 'date-fns/locale';
const ARTICLES_PER_PAGE = 9;
@ -65,7 +66,7 @@ export function SearchPage() {
return `Статьи автора ${authorName}`;
}
if (date) {
return `Статьи за ${format(parseISO(date), 'MMMM d, yyyy')}`;
return `Статьи за ${format(parseISO(date), 'd MMMM, yyyy', { locale: ru })}`;
}
return query ? `Результаты поиска "${query}"` : 'Поиск статей';
};

26
src/stores/scrollStore.ts Normal file
View File

@ -0,0 +1,26 @@
import { create } from 'zustand';
interface ScrollState {
homeScrollPosition: number;
setHomeScrollPosition: (position: number) => void;
restoreHomeScrollPosition: () => void;
}
export const useScrollStore = create<ScrollState>((set, get) => ({
homeScrollPosition: 0,
setHomeScrollPosition: (position) => set({ homeScrollPosition: position }),
restoreHomeScrollPosition: () => {
const { homeScrollPosition } = get();
const tryScroll = () => {
if (document.body.scrollHeight >= homeScrollPosition + window.innerHeight) {
window.scrollTo({
top: homeScrollPosition,
behavior: 'instant'
});
} else {
requestAnimationFrame(tryScroll);
}
};
requestAnimationFrame(tryScroll);
}
}));

View File

@ -6,10 +6,7 @@ export interface Article {
content: string;
categoryId: number;
cityId: number;
authors: {
role: AuthorRole;
author: Author;
}[];
authors: AuthorLink[];
authorName: string;
coAuthorName: string;
photographerName: string;
@ -44,10 +41,7 @@ export interface ArticleData {
content: string;
importId: number;
isActive: boolean;
authors: {
role: AuthorRole;
author: Author;
}[];
authors: AuthorLink[];
}
// Структура ответа на список статей
@ -85,6 +79,12 @@ export interface Author {
roles: string;
}
export interface AuthorLink {
authorId: string;
role: AuthorRole;
author: Author;
}
export const CategoryIds: number[] = [1 , 2 , 3 , 4 , 5 , 6 , 7 , 8];
export const CityIds: number[] = [1 , 2];