Отображение тэгов

This commit is contained in:
2025-11-17 13:18:16 +04:00
parent 294699289c
commit d7f289234c
6 changed files with 1578 additions and 2 deletions

View File

@@ -33,7 +33,7 @@ function BookCardInfo() {
};
if (loading) return <div> Загрузка</div>;
if (error) return <div> Ошибка: {error}</div>;
if(!book) return <div> Книга не найдена</div>;
if (!book) return <div> Книга не найдена</div>;
return (
<body className="bg-dark text-light p-4">
{console.log(book.title)}
@@ -68,7 +68,24 @@ function BookCardInfo() {
<strong>Год издания:</strong> {book.year || testObject.year}
</p>
</div>
{book.tags && book.tags.length > 0 && (
<div className="mb-4">
<h5 className="text-accent mb-3" style={{ color: "#00adb5" }}>
Теги:
</h5>
<div className="d-flex flex-wrap gap-2">
{book.tags.map((tag, index) => (
<span
key={index}
className="badge bg-info text-dark px-3 py-2"
style={{ backgroundColor: "#00adb5", color: "#222831" }}
>
{tag.name}
</span>
))}
</div>
</div>
)}
<div className="mb-4">
<h5 className="text-accent mb-3" style={{ color: "#00adb5" }}>
Аннотация:

View File

@@ -0,0 +1,337 @@
import React, { useState, useEffect } from 'react';
import { Card, Badge, ProgressBar, ListGroup, Button } from 'react-bootstrap';
// Компонент популярных тем с расширенной функциональностью
const PopularTopics = ({ posts }) => {
const [trendingHashtags, setTrendingHashtags] = useState([]);
const [hotTopics, setHotTopics] = useState([]);
const [weeklyLeaderboard, setWeeklyLeaderboard] = useState([]);
// Анализируем данные для вычисления трендов
useEffect(() => {
// Анализ популярных хештегов
const hashtagCount = {};
posts.forEach(post => {
post.hashtags.forEach(tag => {
hashtagCount[tag] = (hashtagCount[tag] || 0) + 1 + post.likes * 0.1 + post.comments.length * 0.2;
});
});
const trending = Object.entries(hashtagCount)
.sort(([,a], [,b]) => b - a)
.slice(0, 8)
.map(([tag, score]) => ({ tag, score }));
setTrendingHashtags(trending);
// Горячие темы (посты с наибольшей активностью)
const hot = posts
.map(post => ({
...post,
activityScore: post.likes + post.comments.length * 2
}))
.sort((a, b) => b.activityScore - a.activityScore)
.slice(0, 5);
setHotTopics(hot);
// Топ пользователей (заглушка - в реальном приложении бралось бы из API)
setWeeklyLeaderboard([
{ username: "alex_dev", posts: 12, likes: 145 },
{ username: "web_designer", posts: 8, likes: 98 },
{ username: "react_fan", posts: 6, likes: 87 },
{ username: "code_master", posts: 5, likes: 76 },
{ username: "ui_guru", posts: 4, likes: 65 }
]);
}, [posts]);
const getTrendingLevel = (score, maxScore) => {
const percentage = (score / maxScore) * 100;
if (percentage > 80) return "danger";
if (percentage > 60) return "warning";
if (percentage > 40) return "info";
return "secondary";
};
const maxScore = trendingHashtags[0]?.score || 1;
return (
<div className="popular-topics-section">
{/* Трендовые хештеги */}
<Card className="mb-4" style={{ backgroundColor: 'var(--bg-medium)', color: 'var(--text-light)' }}>
<Card.Header style={{ borderBottom: '2px solid var(--accent)' }}>
<h5 className="mb-0">🚀 Трендовые хештеги</h5>
</Card.Header>
<Card.Body>
{trendingHashtags.map(({ tag, score }, index) => (
<div key={tag} className="mb-3">
<div className="d-flex justify-content-between align-items-center mb-1">
<Badge
bg={getTrendingLevel(score, maxScore)}
style={{ cursor: 'pointer', fontSize: '0.85rem' }}
>
{tag}
</Badge>
<small className="text-muted">{Math.round(score)} очков</small>
</div>
<ProgressBar
now={(score / maxScore) * 100}
variant={getTrendingLevel(score, maxScore)}
style={{ height: '4px' }}
/>
</div>
))}
</Card.Body>
</Card>
{/* Горячие обсуждения */}
<Card className="mb-4" style={{ backgroundColor: 'var(--bg-medium)', color: 'var(--text-light)' }}>
<Card.Header style={{ borderBottom: '2px solid var(--accent)' }}>
<h5 className="mb-0">🔥 Горячие обсуждения</h5>
</Card.Header>
<Card.Body>
<ListGroup variant="flush">
{hotTopics.map((topic, index) => (
<ListGroup.Item
key={topic.id}
style={{
backgroundColor: 'transparent',
borderColor: 'var(--bg-dark)',
color: 'var(--text-light)'
}}
className="px-0"
>
<div className="d-flex align-items-start">
<Badge
bg="accent"
className="me-2 mt-1"
style={{ backgroundColor: 'var(--accent)' }}
>
{index + 1}
</Badge>
<div className="flex-grow-1">
<div
className="fw-bold topic-title"
style={{
fontSize: '0.9rem',
cursor: 'pointer',
color: 'var(--text-light)'
}}
>
{topic.title}
</div>
<div className="d-flex justify-content-between mt-1">
<small className="text-muted">@{topic.author}</small>
<small className="text-muted">
{topic.likes} {topic.comments.length} 💬
</small>
</div>
</div>
</div>
</ListGroup.Item>
))}
</ListGroup>
</Card.Body>
</Card>
{/* Таблица лидеров */}
<Card className="mb-4" style={{ backgroundColor: 'var(--bg-medium)', color: 'var(--text-light)' }}>
<Card.Header style={{ borderBottom: '2px solid var(--accent)' }}>
<h5 className="mb-0">🏆 Топ авторов недели</h5>
</Card.Header>
<Card.Body>
<ListGroup variant="flush">
{weeklyLeaderboard.map((user, index) => (
<ListGroup.Item
key={user.username}
style={{
backgroundColor: 'transparent',
borderColor: 'var(--bg-dark)',
color: 'var(--text-light)'
}}
className="px-0"
>
<div className="d-flex align-items-center justify-content-between">
<div className="d-flex align-items-center">
<div
className="rank-badge me-2 d-flex align-items-center justify-content-center"
style={{
width: '24px',
height: '24px',
borderRadius: '50%',
backgroundColor: index === 0 ? '#ffd700' :
index === 1 ? '#c0c0c0' :
index === 2 ? '#cd7f32' : 'var(--accent)',
color: index < 3 ? 'var(--bg-dark)' : 'var(--text-light)',
fontSize: '0.7rem',
fontWeight: 'bold'
}}
>
{index + 1}
</div>
<span>@{user.username}</span>
</div>
<div className="text-end">
<div className="small text-muted">
{user.posts} постов
</div>
<div className="small" style={{ color: 'var(--accent)' }}>
{user.likes} лайков
</div>
</div>
</div>
</ListGroup.Item>
))}
</ListGroup>
</Card.Body>
</Card>
{/* Рекомендуемые темы */}
<Card style={{ backgroundColor: 'var(--bg-medium)', color: 'var(--text-light)' }}>
<Card.Header style={{ borderBottom: '2px solid var(--accent)' }}>
<h5 className="mb-0">💡 Рекомендуемые темы</h5>
</Card.Header>
<Card.Body>
<div className="d-flex flex-wrap gap-2 mb-3">
<Badge bg="secondary" style={{ cursor: 'pointer' }}>#react</Badge>
<Badge bg="secondary" style={{ cursor: 'pointer' }}>#javascript</Badge>
<Badge bg="secondary" style={{ cursor: 'pointer' }}>#webdev</Badge>
<Badge bg="secondary" style={{ cursor: 'pointer' }}>#css</Badge>
<Badge bg="secondary" style={{ cursor: 'pointer' }}>#beginners</Badge>
</div>
<Button
variant="outline-accent"
size="sm"
className="w-100"
style={{
borderColor: 'var(--accent)',
color: 'var(--accent)'
}}
>
Показать все темы
</Button>
</Card.Body>
</Card>
</div>
);
};
// Компонент статистики сообщества
const CommunityStats = ({ posts }) => {
const totalPosts = posts.length;
const totalComments = posts.reduce((sum, post) => sum + post.comments.length, 0);
const totalLikes = posts.reduce((sum, post) => sum + post.likes, 0);
// Собираем уникальных авторов
const uniqueAuthors = [...new Set(posts.map(post => post.author))];
return (
<Card className="mb-4" style={{ backgroundColor: 'var(--bg-medium)', color: 'var(--text-light)' }}>
<Card.Header style={{ borderBottom: '2px solid var(--accent)' }}>
<h5 className="mb-0">📊 Статистика сообщества</h5>
</Card.Header>
<Card.Body>
<div className="row text-center">
<div className="col-4">
<div style={{ color: 'var(--accent)', fontSize: '1.5rem', fontWeight: 'bold' }}>
{totalPosts}
</div>
<small className="text-muted">Постов</small>
</div>
<div className="col-4">
<div style={{ color: 'var(--accent)', fontSize: '1.5rem', fontWeight: 'bold' }}>
{totalComments}
</div>
<small className="text-muted">Комментариев</small>
</div>
<div className="col-4">
<div style={{ color: 'var(--accent)', fontSize: '1.5rem', fontWeight: 'bold' }}>
{totalLikes}
</div>
<small className="text-muted">Лайков</small>
</div>
</div>
<hr style={{ borderColor: 'var(--bg-dark)' }} />
<div className="text-center">
<small className="text-muted">
{uniqueAuthors.length} активных участников
</small>
</div>
</Card.Body>
</Card>
);
};
// Компонент событий и активностей
const RecentActivity = () => {
const activities = [
{ type: 'new_post', user: 'react_fan', topic: 'React Hooks', time: '5 мин назад' },
{ type: 'comment', user: 'web_designer', topic: 'Цветовые схемы', time: '12 мин назад' },
{ type: 'like', user: 'code_master', topic: 'Оптимизация', time: '25 мин назад' },
{ type: 'follow', user: 'ui_guru', topic: 'Новый участник', time: '1 час назад' }
];
const getActivityIcon = (type) => {
switch (type) {
case 'new_post': return '📝';
case 'comment': return '💬';
case 'like': return '❤️';
case 'follow': return '👤';
default: return '🔔';
}
};
return (
<Card style={{ backgroundColor: 'var(--bg-medium)', color: 'var(--text-light)' }}>
<Card.Header style={{ borderBottom: '2px solid var(--accent)' }}>
<h5 className="mb-0">🔔 Последняя активность</h5>
</Card.Header>
<Card.Body>
<ListGroup variant="flush">
{activities.map((activity, index) => (
<ListGroup.Item
key={index}
style={{
backgroundColor: 'transparent',
borderColor: 'var(--bg-dark)',
color: 'var(--text-light)'
}}
className="px-0"
>
<div className="d-flex align-items-start">
<span className="me-2" style={{ fontSize: '1.1rem' }}>
{getActivityIcon(activity.type)}
</span>
<div className="flex-grow-1">
<div className="small">
<strong>@{activity.user}</strong>
{activity.type === 'new_post' && ' создал пост'}
{activity.type === 'comment' && ' прокомментировал'}
{activity.type === 'like' && ' лайкнул'}
{activity.type === 'follow' && ' присоединился'}
</div>
<div className="small text-muted">
"{activity.topic}" {activity.time}
</div>
</div>
</div>
</ListGroup.Item>
))}
</ListGroup>
</Card.Body>
</Card>
);
};
// Обновленный компонент ForumPage с новыми блоками
const EnhancedForumPage = ({ posts }) => {
return (
<div className="enhanced-forum-sidebar">
<CommunityStats posts={posts} />
<PopularTopics posts={posts} />
<RecentActivity />
</div>
);
};
export default EnhancedForumPage;

View File

@@ -0,0 +1,249 @@
import React, { useState } from "react";
import { Container, Row, Col, Nav, Form, Button } from "react-bootstrap";
import "bootstrap/dist/css/bootstrap.min.css";
import Post from "./Post";
import NewPostModal from "./NewPostModal";
import EnhancedForumPage from "./EnhancedForumPage";
// Стили с использованием вашей цветовой схемы
const customStyles = `
:root {
--bg-dark: #222831;
--bg-medium: #393e46;
--accent: #00adb5;
--text-light: #eeeeee;
}
.forum-container {
background-color: var(--bg-dark);
color: var(--text-light);
min-height: 100vh;
padding: 20px 0;
}
.filter-menu, .popular-topics, .user-stats, .recent-activity {
background-color: var(--bg-medium);
border-radius: 10px;
padding: 15px;
margin-bottom: 20px;
}
.post-card {
background-color: var(--bg-medium);
border: none;
color: var(--text-light);
transition: transform 0.2s;
}
.post-card:hover {
transform: translateY(-2px);
}
.btn-accent {
background-color: var(--accent);
border: none;
color: var(--bg-dark);
font-weight: bold;
}
.btn-outline-accent {
border: 1px solid var(--accent);
color: var(--accent);
background: transparent;
}
.btn-outline-accent:hover {
background-color: var(--accent);
color: var(--bg-dark);
}
.like-btn:hover, .comment-toggle-btn:hover {
opacity: 0.8;
}
.hashtag-filter {
color: var(--accent);
cursor: pointer;
}
.comment-item {
transition: background-color 0.2s;
}
.comment-item:hover {
background-color: rgba(0, 173, 181, 0.1) !important;
}
`;
const ForumPage = () => {
const [posts, setPosts] = useState([
{
id: 1,
author: "alex_dev",
title: "Мой первый пост на React Bootstrap",
text: "Сегодня я начал изучать React Bootstrap и хочу поделиться своими впечатлениями. Очень удобная библиотека для быстрой разработки UI!",
images: [
"https://via.placeholder.com/600x300/00adb5/222831?text=React+Bootstrap",
],
likes: 24,
comments: [
"Отличный пост! Согласен с тобой полностью.",
"А есть какие-то конкретные примеры использования?",
],
hashtags: ["#react", "#bootstrap", "#webdev"],
commentsDisabled: false,
createdAt: "2024-01-15T10:30:00Z",
},
{
id: 2,
author: "web_designer",
title: "Советы по цветовым схемам",
text: "Хочу поделиться несколькими советами по выбору цветовых схем для веб-приложений. Контраст и читаемость - наше всё!",
images: null,
likes: 15,
comments: [],
hashtags: ["#design", "#ui", "#colors"],
commentsDisabled: false,
createdAt: "2024-01-14T16:45:00Z",
},
{
id: 2,
author: "web_designer",
title: "Советы по цветовым схемам",
text: "Хочу поделиться несколькими советами по выбору цветовых схем для веб-приложений. Контраст и читаемость - наше всё!",
images: null,
likes: 15,
comments: [],
hashtags: ["#design", "#ui", "#colors"],
commentsDisabled: true,
createdAt: "2024-01-14T16:45:00Z",
},
]);
const [showNewPostModal, setShowNewPostModal] = useState(false);
const [selectedHashtags, setSelectedHashtags] = useState([]);
const allHashtags = [...new Set(posts.flatMap((post) => post.hashtags))];
const filteredPosts =
selectedHashtags.length > 0
? posts.filter((post) =>
post.hashtags.some((tag) => selectedHashtags.includes(tag))
)
: posts;
const handleLike = (postId) => {
setPosts(
posts.map((post) =>
post.id === postId ? { ...post, likes: post.likes + 1 } : post
)
);
};
const handleAddComment = (postId, commentText) => {
setPosts(
posts.map((post) =>
post.id === postId
? { ...post, comments: [...post.comments, commentText] }
: post
)
);
};
const handleAddNewPost = (newPost) => {
setPosts([newPost, ...posts]);
};
const toggleHashtag = (hashtag) => {
setSelectedHashtags((prev) =>
prev.includes(hashtag)
? prev.filter((t) => t !== hashtag)
: [...prev, hashtag]
);
};
return (
<>
<style>{customStyles}</style>
<div className="forum-container">
<Container>
<Row>
{/* Левая колонка - меню фильтрации */}
<Col md={3}>
<div className="filter-menu">
<h5>Фильтр по хештегам</h5>
{allHashtags.map((tag) => (
<Form.Check
key={tag}
type="checkbox"
label={tag}
checked={selectedHashtags.includes(tag)}
onChange={() => toggleHashtag(tag)}
className="hashtag-filter"
/>
))}
</div>
<div className="popular-topics">
<h5>Популярные темы</h5>
<Nav className="flex-column">
<Nav.Link>React Components</Nav.Link>
<Nav.Link>Bootstrap Layouts</Nav.Link>
<Nav.Link>CSS Tricks</Nav.Link>
</Nav>
</div>
<div className="user-stats">
<h5>Статистика</h5>
<p>Постов: {posts.length}</p>
<p>
Комментариев:{" "}
{posts.reduce((acc, post) => acc + post.comments.length, 0)}
</p>
</div>
</Col>
{/* Центральная колонка - посты */}
<Col md={6}>
{filteredPosts.map((post) => (
<Post
key={post.id}
post={post}
onLike={handleLike}
onAddComment={handleAddComment}
/>
))}
</Col>
{/* Правая колонка - действия пользователя */}
<Col md={3}>
<div className="d-grid gap-2 mb-4">
<Button
variant="accent"
size="lg"
onClick={() => setShowNewPostModal(true)}
>
+ Новый пост
</Button>
<Button variant="outline-accent" size="lg">
👤 Мой профиль
</Button>
</div>
{/* Новые расширенные блоки */}
<EnhancedForumPage posts={posts} />
</Col>
</Row>
</Container>
</div>
<NewPostModal
show={showNewPostModal}
onHide={() => setShowNewPostModal(false)}
onAddPost={handleAddNewPost}
/>
</>
);
};
export default ForumPage;

View File

@@ -0,0 +1,292 @@
import React, { useState, useRef } from 'react';
import { Modal, Form, Button, Row, Col, Badge, Image } from 'react-bootstrap';
const NewPostModal = ({ show, onHide, onAddPost }) => {
const [newPost, setNewPost] = useState({
title: '',
text: '',
hashtags: [],
images: [],
commentsDisabled: false
});
const [currentHashtag, setCurrentHashtag] = useState('');
const fileInputRef = useRef(null);
const handleAddHashtag = () => {
if (currentHashtag.trim() && !newPost.hashtags.includes(currentHashtag)) {
setNewPost({
...newPost,
hashtags: [...newPost.hashtags, currentHashtag.trim()]
});
setCurrentHashtag('');
}
};
const handleRemoveHashtag = (hashtagToRemove) => {
setNewPost({
...newPost,
hashtags: newPost.hashtags.filter(hashtag => hashtag !== hashtagToRemove)
});
};
const handleImageUpload = (e) => {
const files = Array.from(e.target.files);
const imageUrls = files.map(file => URL.createObjectURL(file));
setNewPost({
...newPost,
images: [...newPost.images, ...imageUrls]
});
};
const handleRemoveImage = (imageToRemove) => {
setNewPost({
...newPost,
images: newPost.images.filter(image => image !== imageToRemove)
});
};
const handleSubmit = () => {
if (newPost.title.trim() && newPost.text.trim()) {
onAddPost({
...newPost,
id: Date.now(),
author: "Текущий пользователь",
likes: 0,
comments: [],
createdAt: new Date().toISOString()
});
// Сброс формы
setNewPost({
title: '',
text: '',
hashtags: [],
images: [],
commentsDisabled: false
});
setCurrentHashtag('');
onHide();
}
};
const handleKeyPress = (e) => {
if (e.key === 'Enter' && currentHashtag.trim()) {
e.preventDefault();
handleAddHashtag();
}
};
return (
<Modal
show={show}
onHide={onHide}
size="lg"
centered
style={{ fontFamily: 'Arial, sans-serif' }}
>
<Modal.Header
closeButton
style={{
backgroundColor: 'var(--bg-medium)',
color: 'var(--text-light)',
borderBottom: '1px solid var(--accent)'
}}
>
<Modal.Title>Создать новый пост</Modal.Title>
</Modal.Header>
<Modal.Body style={{ backgroundColor: 'var(--bg-medium)', color: 'var(--text-light)' }}>
<Form>
{/* Заголовок поста */}
<Form.Group className="mb-3">
<Form.Label style={{ color: 'var(--text-light)' }}>Заголовок поста *</Form.Label>
<Form.Control
type="text"
placeholder="Введите заголовок..."
value={newPost.title}
onChange={(e) => setNewPost({...newPost, title: e.target.value})}
style={{
backgroundColor: 'var(--bg-dark)',
border: '1px solid var(--accent)',
color: 'var(--text-light)'
}}
/>
</Form.Group>
{/* Хештеги */}
<Form.Group className="mb-3">
<Form.Label style={{ color: 'var(--text-light)' }}>Хештеги</Form.Label>
<div className="d-flex gap-2 mb-2">
<Form.Control
type="text"
placeholder="Добавьте хештег..."
value={currentHashtag}
onChange={(e) => setCurrentHashtag(e.target.value)}
onKeyPress={handleKeyPress}
style={{
backgroundColor: 'var(--bg-dark)',
border: '1px solid var(--accent)',
color: 'var(--text-light)'
}}
/>
<Button
variant="outline-accent"
onClick={handleAddHashtag}
disabled={!currentHashtag.trim()}
>
Добавить
</Button>
</div>
{/* Список добавленных хештегов */}
<div className="d-flex flex-wrap gap-2">
{newPost.hashtags.map((hashtag, index) => (
<Badge
key={index}
bg="accent"
style={{
cursor: 'pointer',
backgroundColor: 'var(--accent)',
fontSize: '0.9rem',
padding: '0.5rem 0.75rem'
}}
onClick={() => handleRemoveHashtag(hashtag)}
>
{hashtag} ×
</Badge>
))}
</div>
</Form.Group>
{/* Текст поста */}
<Form.Group className="mb-3">
<Form.Label style={{ color: 'var(--text-light)' }}>Текст поста *</Form.Label>
<Form.Control
as="textarea"
rows={4}
placeholder="Напишите ваш пост..."
value={newPost.text}
onChange={(e) => setNewPost({...newPost, text: e.target.value})}
style={{
backgroundColor: 'var(--bg-dark)',
border: '1px solid var(--accent)',
color: 'var(--text-light)',
resize: 'vertical'
}}
/>
</Form.Group>
{/* Загрузка изображений */}
<Form.Group className="mb-3">
<Form.Label style={{ color: 'var(--text-light)' }}>Изображения</Form.Label>
<Form.Control
ref={fileInputRef}
type="file"
multiple
accept="image/*"
onChange={handleImageUpload}
style={{
backgroundColor: 'var(--bg-dark)',
border: '1px solid var(--accent)',
color: 'var(--text-light)'
}}
/>
<Form.Text style={{ color: 'var(--text-light)' }}>
Вы можете загрузить несколько изображений
</Form.Text>
{/* Преview изображений */}
{newPost.images.length > 0 && (
<Row className="mt-3">
{newPost.images.map((image, index) => (
<Col xs={6} md={4} key={index} className="mb-2 position-relative">
<Image
src={image}
alt={`Preview ${index + 1}`}
fluid
rounded
style={{
border: '2px solid var(--accent)',
height: '100px',
objectFit: 'cover',
width: '100%'
}}
/>
<Button
variant="danger"
size="sm"
style={{
position: 'absolute',
top: '5px',
right: '5px',
borderRadius: '50%',
width: '25px',
height: '25px',
padding: 0,
fontSize: '12px'
}}
onClick={() => handleRemoveImage(image)}
>
×
</Button>
</Col>
))}
</Row>
)}
</Form.Group>
{/* Настройки комментариев */}
<Form.Group className="mb-3">
<Form.Check
type="checkbox"
label="Отключить комментарии"
checked={newPost.commentsDisabled}
onChange={(e) => setNewPost({
...newPost,
commentsDisabled: e.target.checked
})}
style={{ color: 'var(--text-light)' }}
/>
<Form.Text style={{ color: 'var(--text-light)' }}>
При отмеченной опции другие пользователи не смогут комментировать этот пост
</Form.Text>
</Form.Group>
</Form>
</Modal.Body>
<Modal.Footer style={{
backgroundColor: 'var(--bg-medium)',
borderTop: '1px solid var(--accent)'
}}>
<Button
variant="secondary"
onClick={onHide}
style={{
backgroundColor: 'var(--bg-dark)',
border: '1px solid var(--text-light)',
color: 'var(--text-light)'
}}
>
Отмена
</Button>
<Button
variant="accent"
onClick={handleSubmit}
disabled={!newPost.title.trim() || !newPost.text.trim()}
style={{
backgroundColor: 'var(--accent)',
border: 'none',
color: 'var(--bg-dark)',
fontWeight: 'bold'
}}
>
Опубликовать
</Button>
</Modal.Footer>
</Modal>
);
};
export default NewPostModal;

View File

@@ -0,0 +1,189 @@
import React, { useState } from 'react';
import { Card, Button, Form, InputGroup, Badge, Collapse } from 'react-bootstrap';
const Post = ({ post, onLike, onAddComment }) => {
const [showComments, setShowComments] = useState(false);
const [newComment, setNewComment] = useState('');
const handleSubmitComment = (e) => {
e.preventDefault();
if (newComment.trim()) {
onAddComment(post.id, newComment);
setNewComment('');
}
};
return (
<Card className="post-card mb-4">
<Card.Body>
{/* Заголовок и автор */}
<div className="d-flex justify-content-between align-items-start mb-3">
<div>
<Card.Title className="mb-1" style={{ color: 'var(--accent)' }}>
{post.title}
</Card.Title>
<Card.Subtitle className="text-muted small">
@{post.author} {new Date(post.createdAt).toLocaleDateString()}
</Card.Subtitle>
</div>
<div>
{post.commentsDisabled && (
<Badge bg="secondary" className="me-1">
Комментарии отключены
</Badge>
)}
</div>
</div>
{/* Хештеги */}
{post.hashtags.length > 0 && (
<div className="mb-3">
{post.hashtags.map((tag, index) => (
<Badge
key={index}
bg="secondary"
className="me-1 mb-1"
style={{
backgroundColor: 'var(--accent)',
cursor: 'pointer'
}}
>
{tag}
</Badge>
))}
</div>
)}
{/* Текст поста */}
<Card.Text className="mb-3" style={{ lineHeight: '1.5' }}>
{post.text}
</Card.Text>
{/* Изображения */}
{post.images && post.images.length > 0 && (
<div className="mb-3">
{post.images.map((image, index) => (
<img
key={index}
src={image}
alt={`Post image ${index + 1}`}
className="img-fluid rounded mb-2"
style={{
maxHeight: '400px',
width: '100%',
objectFit: 'cover'
}}
/>
))}
</div>
)}
{/* Статистика и действия */}
<div className="d-flex justify-content-between align-items-center border-top border-bottom py-2 mb-3">
<div className="d-flex align-items-center">
<Button
variant="link"
className="p-0 me-3 like-btn"
onClick={() => onLike(post.id)}
style={{
color: 'var(--text-light)',
textDecoration: 'none'
}}
>
<span style={{ color: 'var(--accent)', fontSize: '1.2rem' }}></span>
<span className="ms-1">{post.likes}</span>
</Button>
{!post.commentsDisabled && (
<Button
variant="link"
className="p-0 comment-toggle-btn"
onClick={() => setShowComments(!showComments)}
style={{
color: 'var(--text-light)',
textDecoration: 'none'
}}
>
<span style={{ fontSize: '1.2rem' }}>💬</span>
<span className="ms-1">{post.comments.length}</span>
</Button>
)}
</div>
{!post.commentsDisabled && (
<Button
variant="outline-accent"
size="sm"
onClick={() => setShowComments(!showComments)}
>
{showComments ? 'Скрыть комментарии' : 'Показать комментарии'}
</Button>
)}
</div>
{/* Комментарии с анимацией раскрытия */}
{!post.commentsDisabled && (
<Collapse in={showComments}>
<div>
{/* Список комментариев */}
{post.comments.length > 0 ? (
<div className="comments-section mb-3">
{post.comments.map((comment, index) => (
<div
key={index}
className="comment-item p-2 mb-2 rounded"
style={{
backgroundColor: 'var(--bg-dark)',
borderLeft: `3px solid var(--accent)`
}}
>
<div className="d-flex justify-content-between align-items-start">
<strong className="text-accent">User{index + 1}</strong>
<small className="text-muted">
{new Date().toLocaleTimeString()}
</small>
</div>
<p className="mb-0 mt-1">{comment}</p>
</div>
))}
</div>
) : (
<div
className="text-center py-3 text-muted"
style={{ backgroundColor: 'var(--bg-dark)', borderRadius: '8px' }}
>
Пока нет комментариев. Будьте первым!
</div>
)}
{/* Форма добавления комментария */}
<Form onSubmit={handleSubmitComment}>
<InputGroup>
<Form.Control
placeholder="Напишите комментарий..."
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
style={{
backgroundColor: 'var(--bg-dark)',
border: '1px solid var(--accent)',
color: 'var(--text-light)'
}}
/>
<Button
variant="accent"
type="submit"
disabled={!newComment.trim()}
>
Отправить
</Button>
</InputGroup>
</Form>
</div>
</Collapse>
)}
</Card.Body>
</Card>
);
};
export default Post;

View File

@@ -0,0 +1,492 @@
import React, { useState } from 'react';
import { Container, Row, Col, Card, Tab, Tabs, Badge, Button, Form, Modal, ProgressBar, ListGroup, Image } from 'react-bootstrap';
// Стили для страницы профиля
const profileStyles = `
.profile-container {
background-color: var(--bg-dark);
color: var(--text-light);
min-height: 100vh;
padding: 20px 0;
}
.profile-header {
background: linear-gradient(135deg, var(--bg-medium) 0%, var(--accent) 100%);
border-radius: 15px;
padding: 30px;
margin-bottom: 20px;
position: relative;
}
.profile-avatar {
width: 120px;
height: 120px;
border-radius: 50%;
border: 4px solid var(--text-light);
object-fit: cover;
}
.profile-stats {
background-color: var(--bg-medium);
border-radius: 10px;
padding: 20px;
}
.profile-card {
background-color: var(--bg-medium);
border: none;
color: var(--text-light);
margin-bottom: 20px;
}
.achievement-badge {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
margin: 0 auto 10px;
}
.edit-profile-btn {
position: absolute;
top: 20px;
right: 20px;
}
.tab-content {
background-color: transparent;
border: none;
}
.nav-tabs .nav-link {
background-color: var(--bg-medium);
color: var(--text-light);
border: none;
margin-right: 5px;
}
.nav-tabs .nav-link.active {
background-color: var(--accent);
color: var(--bg-dark);
border: none;
font-weight: bold;
}
.activity-item {
border-left: 3px solid var(--accent);
padding-left: 15px;
margin-bottom: 15px;
}
`;
const ProfilePage = () => {
const [user, setUser] = useState({
id: 1,
username: "alex_dev",
name: "Алексей Петров",
avatar: "https://via.placeholder.com/120/00adb5/222831?text=AP",
coverImage: "https://via.placeholder.com/1200/300/393e46/00adb5?text=Profile+Cover",
bio: "Frontend разработчик | React enthusiast | Люблю создавать красивые и функциональные интерфейсы",
joinDate: "2023-05-15",
location: "Москва, Россия",
website: "https://alexdev-portfolio.ru",
stats: {
posts: 24,
comments: 156,
likes: 842,
followers: 128,
following: 64
}
});
const [userPosts, setUserPosts] = useState([
{
id: 1,
title: "Мой первый пост на React Bootstrap",
content: "Сегодня я начал изучать React Bootstrap и хочу поделиться своими впечатлениями...",
likes: 24,
comments: 8,
hashtags: ["#react", "#bootstrap", "#webdev"],
date: "2024-01-15",
isPublished: true
},
{
id: 2,
title: "Советы по цветовым схемам",
content: "Хочу поделиться несколькими советами по выбору цветовых схем для веб-приложений...",
likes: 15,
comments: 12,
hashtags: ["#design", "#ui", "#colors"],
date: "2024-01-14",
isPublished: true
}
]);
const [achievements, setAchievements] = useState([
{ id: 1, name: "Первый пост", icon: "📝", description: "Опубликовал первый пост", earned: true },
{ id: 2, name: "Активный комментатор", icon: "💬", description: "Оставил 50 комментариев", earned: true },
{ id: 3, name: "Популярный автор", icon: "🔥", description: "Получил 100 лайков", earned: true },
{ id: 4, name: "Эксперт", icon: "🏆", description: "10 постов с более 20 лайками", earned: false },
{ id: 5, name: "Социальная бабочка", icon: "🦋", description: "50 подписчиков", earned: true },
{ id: 6, name: "Ветеран", icon: "🎖️", description: "На сайте более 6 месяцев", earned: false }
]);
const [recentActivity, setRecentActivity] = useState([
{ type: "post", content: "Опубликовал новый пост 'React Hooks Guide'", date: "2 часа назад" },
{ type: "comment", content: "Прокомментировал пост 'CSS Grid vs Flexbox'", date: "5 часов назад" },
{ type: "like", content: "Понравился пост 'JavaScript Best Practices'", date: "Вчера" },
{ type: "achievement", content: "Получено достижение 'Популярный автор'", date: "2 дня назад" }
]);
const [showEditModal, setShowEditModal] = useState(false);
const [editForm, setEditForm] = useState({
name: user.name,
bio: user.bio,
location: user.location,
website: user.website
});
const handleSaveProfile = () => {
setUser({
...user,
...editForm
});
setShowEditModal(false);
};
const getActivityIcon = (type) => {
switch (type) {
case 'post': return '📝';
case 'comment': return '💬';
case 'like': return '❤️';
case 'achievement': return '🏆';
default: return '🔔';
}
};
return (
<>
<style>{profileStyles}</style>
<div className="profile-container">
<Container>
{/* Шапка профиля */}
<Card className="profile-header">
<div className="edit-profile-btn">
<Button
variant="outline-light"
size="sm"
onClick={() => setShowEditModal(true)}
>
Редактировать профиль
</Button>
</div>
<Row className="align-items-end">
<Col md="auto">
<img
src={user.avatar}
alt="Avatar"
className="profile-avatar"
/>
</Col>
<Col>
<h1 className="mb-1">{user.name}</h1>
<p className="mb-1">@{user.username}</p>
<p className="mb-2">{user.bio}</p>
<div className="d-flex gap-3 text-sm">
<span>📍 {user.location}</span>
<span>🔗 {user.website}</span>
<span>📅 На сайте с {new Date(user.joinDate).toLocaleDateString('ru-RU')}</span>
</div>
</Col>
</Row>
</Card>
<Row>
{/* Левая колонка - статистика и достижения */}
<Col md={4}>
{/* Статистика */}
<Card className="profile-stats mb-4">
<h5 className="mb-3">📊 Статистика активности</h5>
<Row className="text-center">
<Col xs={4}>
<div style={{ color: 'var(--accent)', fontSize: '1.5rem', fontWeight: 'bold' }}>
{user.stats.posts}
</div>
<small className="text-muted">Постов</small>
</Col>
<Col xs={4}>
<div style={{ color: 'var(--accent)', fontSize: '1.5rem', fontWeight: 'bold' }}>
{user.stats.comments}
</div>
<small className="text-muted">Комментариев</small>
</Col>
<Col xs={4}>
<div style={{ color: 'var(--accent)', fontSize: '1.5rem', fontWeight: 'bold' }}>
{user.stats.likes}
</div>
<small className="text-muted">Лайков</small>
</Col>
</Row>
<hr />
<Row className="text-center">
<Col xs={6}>
<div style={{ color: 'var(--accent)', fontSize: '1.5rem', fontWeight: 'bold' }}>
{user.stats.followers}
</div>
<small className="text-muted">Подписчиков</small>
</Col>
<Col xs={6}>
<div style={{ color: 'var(--accent)', fontSize: '1.5rem', fontWeight: 'bold' }}>
{user.stats.following}
</div>
<small className="text-muted">Подписок</small>
</Col>
</Row>
</Card>
{/* Достижения */}
<Card className="profile-card">
<Card.Header>
<h5 className="mb-0">🏆 Достижения</h5>
</Card.Header>
<Card.Body>
<Row>
{achievements.map(achievement => (
<Col xs={6} key={achievement.id} className="text-center mb-3">
<div
className="achievement-badge"
style={{
backgroundColor: achievement.earned ? 'var(--accent)' : 'var(--bg-dark)',
opacity: achievement.earned ? 1 : 0.5
}}
>
{achievement.icon}
</div>
<div className="small">
<strong>{achievement.name}</strong>
<div className="text-muted" style={{ fontSize: '0.7rem' }}>
{achievement.description}
</div>
</div>
</Col>
))}
</Row>
<ProgressBar
now={achievements.filter(a => a.earned).length / achievements.length * 100}
variant="accent"
className="mt-2"
/>
<div className="text-center small text-muted mt-1">
{achievements.filter(a => a.earned).length} из {achievements.length} достижений
</div>
</Card.Body>
</Card>
</Col>
{/* Правая колонка - вкладки с контентом */}
<Col md={8}>
<Card className="profile-card">
<Card.Body className="p-0">
<Tabs defaultActiveKey="posts" className="px-3 pt-3">
{/* Вкладка с постами */}
<Tab eventKey="posts" title="📝 Мои посты">
<div className="p-3">
{userPosts.map(post => (
<Card key={post.id} className="mb-3" style={{ backgroundColor: 'var(--bg-dark)' }}>
<Card.Body>
<div className="d-flex justify-content-between align-items-start">
<div>
<h6>{post.title}</h6>
<p className="mb-2 text-muted small">{post.content}</p>
<div className="mb-2">
{post.hashtags.map(tag => (
<Badge key={tag} bg="secondary" className="me-1">
{tag}
</Badge>
))}
</div>
</div>
<div className="text-end">
<div className="small text-muted">
{new Date(post.date).toLocaleDateString('ru-RU')}
</div>
<div className="d-flex gap-2 mt-1">
<small className="text-muted"> {post.likes}</small>
<small className="text-muted">💬 {post.comments}</small>
</div>
</div>
</div>
<div className="d-flex gap-2 mt-2">
<Button variant="outline-accent" size="sm">Редактировать</Button>
<Button variant="outline-danger" size="sm">Удалить</Button>
{!post.isPublished && (
<Button variant="accent" size="sm">Опубликовать</Button>
)}
</div>
</Card.Body>
</Card>
))}
</div>
</Tab>
{/* Вкладка с активностью */}
<Tab eventKey="activity" title="🔔 Активность">
<div className="p-3">
<ListGroup variant="flush">
{recentActivity.map((activity, index) => (
<ListGroup.Item
key={index}
style={{
backgroundColor: 'transparent',
color: 'var(--text-light)',
borderColor: 'var(--bg-dark)'
}}
className="px-0"
>
<div className="activity-item">
<div className="d-flex align-items-center mb-1">
<span className="me-2">{getActivityIcon(activity.type)}</span>
<div className="flex-grow-1">
{activity.content}
</div>
<small className="text-muted">{activity.date}</small>
</div>
</div>
</ListGroup.Item>
))}
</ListGroup>
</div>
</Tab>
{/* Вкладка с настройками */}
<Tab eventKey="settings" title="⚙️ Настройки">
<div className="p-3">
<Form>
<Form.Group className="mb-3">
<Form.Label>Уведомления</Form.Label>
<Form.Check
type="switch"
label="Уведомления о новых комментариях"
defaultChecked
/>
<Form.Check
type="switch"
label="Уведомления о лайках"
defaultChecked
/>
<Form.Check
type="switch"
label="Email-рассылка"
/>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Конфиденциальность</Form.Label>
<Form.Check
type="switch"
label="Скрыть профиль от поисковых систем"
/>
<Form.Check
type="switch"
label="Только подписчики могут комментировать"
/>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Тема оформления</Form.Label>
<Form.Select>
<option>Темная (текущая)</option>
<option>Светлая</option>
<option>Авто</option>
</Form.Select>
</Form.Group>
<Button variant="accent">Сохранить настройки</Button>
</Form>
</div>
</Tab>
</Tabs>
</Card.Body>
</Card>
</Col>
</Row>
</Container>
</div>
{/* Модальное окно редактирования профиля */}
<Modal show={showEditModal} onHide={() => setShowEditModal(false)} centered>
<Modal.Header closeButton style={{ backgroundColor: 'var(--bg-medium)', color: 'var(--text-light)' }}>
<Modal.Title>Редактировать профиль</Modal.Title>
</Modal.Header>
<Modal.Body style={{ backgroundColor: 'var(--bg-medium)', color: 'var(--text-light)' }}>
<Form>
<Form.Group className="mb-3 text-center">
<Image
src={user.avatar}
roundedCircle
style={{ width: '100px', height: '100px', objectFit: 'cover' }}
/>
<div className="mt-2">
<Button variant="outline-accent" size="sm">
Сменить аватар
</Button>
</div>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Имя и фамилия</Form.Label>
<Form.Control
value={editForm.name}
onChange={(e) => setEditForm({...editForm, name: e.target.value})}
style={{ backgroundColor: 'var(--bg-dark)', color: 'var(--text-light)' }}
/>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>О себе</Form.Label>
<Form.Control
as="textarea"
rows={3}
value={editForm.bio}
onChange={(e) => setEditForm({...editForm, bio: e.target.value})}
style={{ backgroundColor: 'var(--bg-dark)', color: 'var(--text-light)' }}
/>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Местоположение</Form.Label>
<Form.Control
value={editForm.location}
onChange={(e) => setEditForm({...editForm, location: e.target.value})}
style={{ backgroundColor: 'var(--bg-dark)', color: 'var(--text-light)' }}
/>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Веб-сайт</Form.Label>
<Form.Control
value={editForm.website}
onChange={(e) => setEditForm({...editForm, website: e.target.value})}
style={{ backgroundColor: 'var(--bg-dark)', color: 'var(--text-light)' }}
/>
</Form.Group>
</Form>
</Modal.Body>
<Modal.Footer style={{ backgroundColor: 'var(--bg-medium)' }}>
<Button variant="secondary" onClick={() => setShowEditModal(false)}>
Отмена
</Button>
<Button variant="accent" onClick={handleSaveProfile}>
Сохранить изменения
</Button>
</Modal.Footer>
</Modal>
</>
);
};
export default ProfilePage;