Compare commits

..

3 Commits

Author SHA1 Message Date
laktionov-as 45f4c143c2 fix errors 2026-06-16 13:58:33 +03:00
laktionov-as da596fd2d8 add dockerfile 2026-06-15 17:50:00 +03:00
laktionov-as 4b15fb237b Docker ready deploy 2026-06-15 17:30:24 +03:00
6 changed files with 383 additions and 79 deletions
+6
View File
@@ -0,0 +1,6 @@
node_modules
build
.git
.gitignore
README.md
!index.html
+13
View File
@@ -0,0 +1,13 @@
# Stage 1: Build the React application
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Stage 2: Serve the application with Nginx
FROM nginx:1.25-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
-25
View File
@@ -264,31 +264,6 @@
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+40 -20
View File
@@ -1,4 +1,3 @@
// App.js
import React, { useState, useEffect } from 'react';
import QuizQuestion from './components/QuizQuestion';
import QuizResults from './components/QuizResults';
@@ -20,7 +19,7 @@ const App = () => {
const [currentAnswerResult, setCurrentAnswerResult] = useState(null);
const [otherPlayersResults, setOtherPlayersResults] = useState([]);
// Новые состояния для лобби
//состояния для лобби
const [showLobby, setShowLobby] = useState(true);
const [currentPlayer, setCurrentPlayer] = useState(null);
const [players, setPlayers] = useState([]);
@@ -35,22 +34,22 @@ const App = () => {
useEffect(() => {
const loadQuizData = async () => {
const mockQuizData = {
title: "Увлекательная викторина",
title: "Викторина",
description: "Проверьте свои знания! Сравнивайте свои результаты с другими игроками после каждого вопроса.",
timeLimit: 30,
questions: [
{
id: 1,
text: "Какая планета находится ближе всего к Солнцу?",
imageUrl: "https://example.com/solar-system.jpg",
imageUrl: "https://placehold.jp/150x150.png",
type: "single",
timeLimit: 30,
points: 100,
answers: [
{ id: "a", text: "Венера", imageUrl: "https://example.com/venus.jpg", isCorrect: false },
{ id: "b", text: "Меркурий", imageUrl: "https://example.com/mercury.jpg", isCorrect: true },
{ id: "c", text: "Марс", imageUrl: "https://example.com/mars.jpg", isCorrect: false },
{ id: "d", text: "Юпитер", imageUrl: "https://example.com/jupiter.jpg", isCorrect: false }
{ id: "a", text: "Венера", imageUrl: "https://placehold.jp/150x150.png", isCorrect: false },
{ id: "b", text: "Меркурий", imageUrl: "https://placehold.jp/150x150.png", isCorrect: true },
{ id: "c", text: "Марс", imageUrl: "https://placehold.jp/150x150.png", isCorrect: false },
{ id: "d", text: "Юпитер", imageUrl: "https://placehold.jp/150x150.png", isCorrect: false }
]
},
{
@@ -60,8 +59,8 @@ const App = () => {
timeLimit: 25,
points: 100,
answers: [
{ id: "a", text: "Python", imageUrl: "https://example.com/python.jpg", isCorrect: false },
{ id: "b", text: "Java", imageUrl: "https://example.com/java.jpg", isCorrect: false },
{ id: "a", text: "Python", imageUrl: "https://placehold.jp/150x150.png", isCorrect: false },
{ id: "b", text: "Java", imageUrl: "https://placehold.jp/150x150.png", isCorrect: false },
{ id: "c", text: "JavaScript", isCorrect: true },
{ id: "d", text: "C++", isCorrect: false }
]
@@ -77,15 +76,15 @@ const App = () => {
{
id: 4,
text: "Какое животное изображено на картинке?",
imageUrl: "https://example.com/animal.jpg",
imageUrl: "https://placehold.jp/150x150.png",
type: "single",
timeLimit: 20,
points: 100,
answers: [
{ id: "a", text: "Собака", imageUrl: "https://example.com/dog.jpg", isCorrect: false },
{ id: "b", text: "Кошка", imageUrl: "https://example.com/cat.jpg", isCorrect: true },
{ id: "c", text: "Птица", imageUrl: "https://example.com/bird.jpg", isCorrect: false },
{ id: "d", text: "Рыба", imageUrl: "https://example.com/fish.jpg", isCorrect: false }
{ id: "a", text: "Собака", imageUrl: "https://placehold.jp/150x150.png", isCorrect: false },
{ id: "b", text: "Кошка", imageUrl: "https://placehold.jp/150x150.png", isCorrect: true },
{ id: "c", text: "Птица", imageUrl: "https://placehold.jp/150x150.png", isCorrect: false },
{ id: "d", text: "Рыба", imageUrl: "https://placehold.jp/150x150.png", isCorrect: false }
]
}
]
@@ -382,10 +381,31 @@ const App = () => {
if (quizCompleted) {
const score = calculateScore();
// Формируем массив всех игроков для таблицы лидеров
const allPlayersScores = [
{ ...currentPlayer, isCurrent: true },
...players.filter(p => p.id !== currentPlayer.id)
].sort((a, b) => b.score - a.score);
{
...currentPlayer,
isCurrent: true,
totalAnswers: userAnswers.filter(a => a && !a.timeExpired).length,
correctAnswers: userAnswers.filter(a => {
if (!a || a.timeExpired) return false;
if (a.id) {
const question = quizData.questions[userAnswers.indexOf(a)];
return question.answers.find(ans => ans.id === a.id)?.isCorrect;
}
return false;
}).length
},
...players
.filter(p => p.id !== currentPlayer.id)
.map(p => ({
...p,
isCurrent: false,
totalAnswers: 0, // В реальном приложении здесь были бы данные
correctAnswers: 0
}))
].sort((a, b) => (b.score || 0) - (a.score || 0));
return (
<QuizResults
@@ -467,11 +487,11 @@ const App = () => {
)}
<div className="navigation-buttons">
{hasPrevious && !isTimeExpired && (
{/*hasPrevious && !isTimeExpired && (
<button onClick={handlePrevious} className="nav-button prev">
← Назад
</button>
)}
)*/}
</div>
</div>
);
+228
View File
@@ -131,3 +131,231 @@
font-size: 36px;
}
}
.leaderboard-section {
margin: 30px 0;
padding: 20px;
background: #f8f9fa;
border-radius: 15px;
}
.leaderboard-section h3 {
color: #333;
margin-bottom: 20px;
text-align: center;
font-size: 1.3rem;
}
.leaderboard {
display: flex;
flex-direction: column;
gap: 10px;
}
.leaderboard-item {
display: flex;
align-items: center;
padding: 15px 20px;
background: white;
border-radius: 12px;
transition: all 0.3s ease;
border: 2px solid transparent;
gap: 15px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.leaderboard-item:hover {
transform: translateX(5px);
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.leaderboard-item.current-player {
border-color: #667eea;
background: linear-gradient(90deg, #e8eaf6 0%, #ffffff 100%);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.2);
}
.leaderboard-item.first {
background: linear-gradient(90deg, #fff3cd 0%, #ffffff 100%);
border-color: #ffc107;
}
.leaderboard-item.second {
background: linear-gradient(90deg, #e8e8e8 0%, #ffffff 100%);
border-color: #c0c0c0;
}
.leaderboard-item.third {
background: linear-gradient(90deg, #ffe0b0 0%, #ffffff 100%);
border-color: #cd7f32;
}
.leaderboard-rank {
min-width: 50px;
font-size: 1.5rem;
text-align: center;
}
.rank-number {
display: inline-block;
}
.leaderboard-avatar {
font-size: 2.5rem;
width: 50px;
text-align: center;
}
.leaderboard-info {
flex: 1;
min-width: 0;
}
.leaderboard-name {
font-weight: bold;
color: #333;
font-size: 1.1rem;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.host-badge {
background: #ff9800;
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.7rem;
font-weight: normal;
}
.you-badge {
background: #667eea;
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.7rem;
font-weight: normal;
}
.leaderboard-stats {
display: flex;
gap: 15px;
margin-top: 4px;
flex-wrap: wrap;
}
.stat-item {
font-size: 0.8rem;
color: #666;
}
.leaderboard-score {
text-align: right;
min-width: 80px;
}
.score-value {
display: block;
font-size: 1.8rem;
font-weight: bold;
color: #333;
line-height: 1;
}
.leaderboard-item.first .score-value {
color: #ffc107;
}
.leaderboard-item.second .score-value {
color: #c0c0c0;
}
.leaderboard-item.third .score-value {
color: #cd7f32;
}
.leaderboard-item.current-player .score-value {
color: #667eea;
}
.score-label {
font-size: 0.7rem;
color: #999;
display: block;
margin-top: 2px;
}
/* Точки за ответ */
.points-earned {
margin-top: 5px;
color: #4caf50;
font-weight: 500;
}
.points-earned strong {
color: #333;
}
/* Адаптивность для таблицы лидеров */
@media (max-width: 768px) {
.leaderboard-item {
flex-wrap: wrap;
padding: 12px 15px;
}
.leaderboard-rank {
min-width: 35px;
font-size: 1.2rem;
}
.leaderboard-avatar {
font-size: 2rem;
width: 40px;
}
.leaderboard-info {
width: 100%;
order: 3;
margin-top: 5px;
}
.leaderboard-score {
margin-left: auto;
min-width: 60px;
}
.score-value {
font-size: 1.4rem;
}
.leaderboard-stats {
font-size: 0.7rem;
gap: 10px;
}
}
/* Анимация появления */
.leaderboard-item {
animation: slideInLeft 0.5s ease forwards;
opacity: 0;
}
.leaderboard-item:nth-child(1) { animation-delay: 0.1s; }
.leaderboard-item:nth-child(2) { animation-delay: 0.2s; }
.leaderboard-item:nth-child(3) { animation-delay: 0.3s; }
.leaderboard-item:nth-child(4) { animation-delay: 0.4s; }
.leaderboard-item:nth-child(5) { animation-delay: 0.5s; }
.leaderboard-item:nth-child(6) { animation-delay: 0.6s; }
@keyframes slideInLeft {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
+84 -22
View File
@@ -1,7 +1,16 @@
import React from 'react';
import './QuizResults.css';
const QuizResults = ({ score, totalQuestions, userAnswers, quizData, onRestart, timeExpiredQuestions }) => {
const QuizResults = ({
score,
totalQuestions,
userAnswers,
quizData,
onRestart,
timeExpiredQuestions,
allPlayers = [], // Добавляем allPlayers с значением по умолчанию
currentPlayer = null
}) => {
const percentage = (score / totalQuestions) * 100;
let resultMessage = "";
@@ -42,7 +51,7 @@ const QuizResults = ({ score, totalQuestions, userAnswers, quizData, onRestart,
};
const getAnswerStatus = (question, answer, index) => {
if (timeExpiredQuestions.includes(index)) {
if (timeExpiredQuestions?.includes(index)) {
return "expired";
}
if (question.type === "single" && answer && !answer.timeExpired) {
@@ -55,11 +64,28 @@ const QuizResults = ({ score, totalQuestions, userAnswers, quizData, onRestart,
return "unanswered";
};
// Функция для получения эмодзи медали
const getMedalEmoji = (index) => {
if (index === 0) return "🥇";
if (index === 1) return "🥈";
if (index === 2) return "🥉";
return `#${index + 1}`;
};
// Функция для получения класса места
const getRankClass = (index) => {
if (index === 0) return "first";
if (index === 1) return "second";
if (index === 2) return "third";
return "other";
};
return (
<div className="results-container">
<div className="results-card">
<h2>Ваши результаты</h2>
<h2>🏆 Результаты викторины</h2>
{/* Основной результат */}
<div className={`score-circle ${resultClass}`}>
<div className="score-number">{score}</div>
<div className="score-total">/{totalQuestions}</div>
@@ -68,17 +94,62 @@ const QuizResults = ({ score, totalQuestions, userAnswers, quizData, onRestart,
<div className="result-message">
<p>{resultMessage}</p>
<p className="percentage">Правильных ответов: {Math.round(percentage)}%</p>
{timeExpiredQuestions.length > 0 && (
{timeExpiredQuestions?.length > 0 && (
<p className="time-warning">
На {timeExpiredQuestions.length} вопрос{(timeExpiredQuestions.length > 1 ? 'ах' : 'е')} время истекло
</p>
)}
{currentPlayer && (
<p className="total-points">
🎯 Всего очков: <strong>{currentPlayer.score || 0}</strong>
</p>
)}
</div>
{/* Таблица лидеров */}
{allPlayers && allPlayers.length > 0 && (
<div className="leaderboard-section">
<h3>👑 Таблица лидеров</h3>
<div className="leaderboard">
{allPlayers.map((player, index) => (
<div
key={player.id || index}
className={`leaderboard-item ${getRankClass(index)} ${player.isCurrent ? 'current-player' : ''}`}
>
<div className="leaderboard-rank">
<span className="rank-number">{getMedalEmoji(index)}</span>
</div>
<div className="leaderboard-avatar">{player.avatar || "👤"}</div>
<div className="leaderboard-info">
<div className="leaderboard-name">
{player.name || "Игрок"}
{player.isHost && <span className="host-badge">👑 Хост</span>}
{player.isCurrent && <span className="you-badge">Вы</span>}
</div>
<div className="leaderboard-stats">
<span className="stat-item">
{player.correctAnswers || 0} правильных
</span>
<span className="stat-item">
📝 {player.totalAnswers || 0} ответов
</span>
</div>
</div>
<div className="leaderboard-score">
<span className="score-value">{player.score || 0}</span>
<span className="score-label">очков</span>
</div>
</div>
))}
</div>
</div>
)}
{/* Обзор ответов */}
<div className="answers-review">
<h3>Обзор ответов:</h3>
{quizData.questions.map((question, index) => {
const userAnswer = userAnswers[index];
<h3>📋 Обзор ваших ответов:</h3>
{quizData && quizData.questions && quizData.questions.map((question, index) => {
const userAnswer = userAnswers?.[index];
const status = getAnswerStatus(question, userAnswer, index);
return (
@@ -94,7 +165,7 @@ const QuizResults = ({ score, totalQuestions, userAnswers, quizData, onRestart,
<div className="user-answer">
<strong>Ваш ответ:</strong> {getUserAnswerText(question, userAnswer, index)}
</div>
{status !== "expired" && status !== "pending" && (
{status !== "expired" && status !== "pending" && status !== "unanswered" && (
<div className="correct-answer">
<strong>Правильный ответ:</strong> {getCorrectAnswerText(question, index)}
</div>
@@ -104,25 +175,16 @@ const QuizResults = ({ score, totalQuestions, userAnswers, quizData, onRestart,
<strong> Время на ответ:</strong> {userAnswer.timeSpent} сек
</div>
)}
{userAnswer?.pointsEarned > 0 && (
<div className="points-earned">
<strong>🎯 Очков получено:</strong> +{userAnswer.pointsEarned}
</div>
)}
</div>
</div>
);
})}
</div>
<div className="leaderboard">
<h3>🏆 Итоговая таблица лидеров</h3>
{allPlayers.map((player, index) => (
<div key={player.id} className={`leaderboard-item ${player.isCurrent ? 'current' : ''}`}>
<div className="leaderboard-rank">{index + 1}</div>
<div className="leaderboard-avatar">{player.avatar}</div>
<div className="leaderboard-name">
{player.name}
{player.isCurrent && <span className="you-badge">(Вы)</span>}
</div>
<div className="leaderboard-score">{player.score} очков</div>
</div>
))}
</div>
<button onClick={onRestart} className="restart-button">
Пройти викторину снова 🔄