feat: Enhance quiz functionality and UI by adding correct answers and total questions to QuizAttempt model, updating related services, and refining star display across frontend components.

This commit is contained in:
NikitolProject 2025-09-17 22:58:43 +03:00
parent a96e123141
commit 46ffe69b57
13 changed files with 80 additions and 65 deletions

View File

@ -85,13 +85,16 @@ type Option struct {
} }
type QuizAttempt struct { type QuizAttempt struct {
ID int `json:"id"` ID int `json:"id"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
QuizID int `json:"quiz_id"` QuizID int `json:"quiz_id"`
Score int `json:"score"` Score int `json:"score"`
StarsEarned int `json:"stars_earned"` StarsEarned int `json:"stars_earned"`
CompletedAt time.Time `json:"completed_at"` CompletedAt time.Time `json:"completed_at"`
Answers []UserAnswer `json:"answers"` // Stored as JSONB Answers []UserAnswer `json:"answers"` // Stored as JSONB
// Additional fields for response
CorrectAnswers int `json:"correct_answers"`
TotalQuestions int `json:"total_questions"`
} }
type Reward struct { type Reward struct {

View File

@ -125,11 +125,13 @@ func (s *quizService) SubmitQuiz(ctx context.Context, userID int64, quizID int,
// --- Database Transaction --- // --- Database Transaction ---
attempt := &models.QuizAttempt{ attempt := &models.QuizAttempt{
UserID: userID, UserID: userID,
QuizID: quizID, QuizID: quizID,
Score: score, Score: score,
StarsEarned: starsEarned, StarsEarned: starsEarned,
Answers: submission.Answers, Answers: submission.Answers,
CorrectAnswers: score,
TotalQuestions: len(quiz.Questions),
} }
tx, err := s.db.Begin(ctx) tx, err := s.db.Begin(ctx)

View File

@ -75,7 +75,7 @@ export const DeepLinkHandler: React.FC<DeepLinkHandlerProps> = ({ onActionComple
setResult({ setResult({
type: 'reward', type: 'reward',
value: stars, value: stars,
message: response.data.message || `Вы получили ${stars}!` message: response.data.message || `Вы получили ${stars}!`
}); });
// Update user balance // Update user balance
@ -232,7 +232,7 @@ export const DeepLinkHandler: React.FC<DeepLinkHandlerProps> = ({ onActionComple
variant="body1" variant="body1"
sx={{ color: '#ffffff', mb: 1 }} sx={{ color: '#ffffff', mb: 1 }}
> >
Вы получили {result.value} Вы получили {result.value}
</Typography> </Typography>
<Typography <Typography
variant="body2" variant="body2"

View File

@ -155,7 +155,7 @@ export const CardQuiz: React.FC<CardQuizProps> = ({
}} }}
/> />
} }
label={`+${quiz.reward_stars}`} label={`+${quiz.reward_stars}`}
size="small" size="small"
sx={{ sx={{
backgroundColor: 'rgba(255, 215, 0, 0.15)', backgroundColor: 'rgba(255, 215, 0, 0.15)',

View File

@ -229,7 +229,7 @@ export const CardReward: React.FC<CardRewardProps> = ({
}} }}
/> />
} }
label={`${reward.price_stars.toLocaleString()}`} label={`${reward.price_stars.toLocaleString()}`}
size="small" size="small"
sx={{ sx={{
backgroundColor: canAfford backgroundColor: canAfford
@ -314,7 +314,7 @@ export const CardReward: React.FC<CardRewardProps> = ({
}} }}
> >
{!canAfford {!canAfford
? `Не хватает ${reward.price_stars - userStars} ` ? `Не хватает ${reward.price_stars - userStars} звёзд`
: !inStock : !inStock
? 'Нет в наличии' ? 'Нет в наличии'
: !isActive : !isActive

View File

@ -107,7 +107,7 @@ export const HeaderProfile: React.FC<HeaderProfileProps> = ({
fontSize: 20, fontSize: 20,
}} }}
> >
{starsBalance.toLocaleString()} {starsBalance.toLocaleString()}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>

View File

@ -625,7 +625,7 @@ export const AdminPage: React.FC = () => {
<TableRow key={quiz.id}> <TableRow key={quiz.id}>
<TableCell sx={{ color: '#ffffff' }}>{quiz.title}</TableCell> <TableCell sx={{ color: '#ffffff' }}>{quiz.title}</TableCell>
<TableCell sx={{ color: '#ffffff' }}>{quiz.questions?.length || 0}</TableCell> <TableCell sx={{ color: '#ffffff' }}>{quiz.questions?.length || 0}</TableCell>
<TableCell sx={{ color: '#ffffff' }}>{quiz.reward_stars} </TableCell> <TableCell sx={{ color: '#ffffff' }}>{quiz.reward_stars}</TableCell>
<TableCell> <TableCell>
<Chip <Chip
label={quiz.is_active ? 'Активна' : 'Неактивна'} label={quiz.is_active ? 'Активна' : 'Неактивна'}
@ -695,7 +695,7 @@ export const AdminPage: React.FC = () => {
{rewards.map((reward) => ( {rewards.map((reward) => (
<TableRow key={reward.id}> <TableRow key={reward.id}>
<TableCell sx={{ color: '#ffffff' }}>{reward.title}</TableCell> <TableCell sx={{ color: '#ffffff' }}>{reward.title}</TableCell>
<TableCell sx={{ color: '#ffffff' }}>{reward.price_stars} </TableCell> <TableCell sx={{ color: '#ffffff' }}>{reward.price_stars}</TableCell>
<TableCell sx={{ color: '#ffffff' }}> <TableCell sx={{ color: '#ffffff' }}>
{reward.stock === -1 ? '∞' : reward.stock} {reward.stock === -1 ? '∞' : reward.stock}
</TableCell> </TableCell>

View File

@ -94,10 +94,7 @@ export const ProfilePage: React.FC = () => {
}); });
}; };
const handleLogout = () => {
logout();
};
if (loading) { if (loading) {
return ( return (
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}> <Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
@ -193,7 +190,7 @@ export const ProfilePage: React.FC = () => {
sx={{ sx={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'center',
backgroundColor: 'rgba(255, 215, 0, 0.1)', backgroundColor: 'rgba(255, 215, 0, 0.1)',
p: 2, p: 2,
borderRadius: 1, borderRadius: 1,
@ -206,23 +203,9 @@ export const ProfilePage: React.FC = () => {
variant="h6" variant="h6"
sx={{ color: '#FFD700', fontWeight: 'bold' }} sx={{ color: '#FFD700', fontWeight: 'bold' }}
> >
{user?.stars_balance} {user?.stars_balance}
</Typography> </Typography>
</Box> </Box>
<Button
variant="outlined"
onClick={handleLogout}
sx={{
borderColor: '#f44336',
color: '#f44336',
'&:hover': {
borderColor: '#f44336',
backgroundColor: 'rgba(244, 67, 54, 0.1)',
},
}}
>
Выйти
</Button>
</Box> </Box>
</CardContent> </CardContent>
</Card> </Card>
@ -296,7 +279,7 @@ export const ProfilePage: React.FC = () => {
} }
/> />
<Chip <Chip
label={`${transaction.type === 'earned' ? '+' : ''}${transaction.amount}`} label={`${transaction.type === 'earned' ? '+' : ''}${transaction.amount}`}
size="small" size="small"
sx={{ sx={{
backgroundColor: backgroundColor:
@ -356,7 +339,7 @@ export const ProfilePage: React.FC = () => {
/> />
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Chip <Chip
label={`${purchase.stars_spent}`} label={`${purchase.stars_spent}`}
size="small" size="small"
sx={{ sx={{
backgroundColor: 'rgba(255, 215, 0, 0.2)', backgroundColor: 'rgba(255, 215, 0, 0.2)',

View File

@ -108,7 +108,7 @@ export const QRScannerPage: React.FC = () => {
// Handle different response structures // Handle different response structures
if (response.data.type === 'REWARD') { if (response.data.type === 'REWARD') {
transformedData.value = response.data.data.amount; transformedData.value = response.data.data.amount;
transformedData.message = `Вы получили ${response.data.data.amount}`; transformedData.message = `Вы получили ${response.data.data.amount}`;
} else if (response.data.type === 'OPEN_QUIZ') { } else if (response.data.type === 'OPEN_QUIZ') {
transformedData.value = response.data.data.id; transformedData.value = response.data.data.id;
transformedData.message = `Викторина: ${response.data.data.title}`; transformedData.message = `Викторина: ${response.data.data.title}`;
@ -375,7 +375,7 @@ export const QRScannerPage: React.FC = () => {
variant="body1" variant="body1"
sx={{ color: '#ffffff', mb: 1 }} sx={{ color: '#ffffff', mb: 1 }}
> >
Вы получили {result.value} Вы получили {result.value}
</Typography> </Typography>
<Typography <Typography
variant="body2" variant="body2"

View File

@ -113,7 +113,9 @@ export const QuizPage: React.FC = () => {
try { try {
const response = await apiService.submitQuiz(parseInt(id), { answers }); const response = await apiService.submitQuiz(parseInt(id), { answers });
console.log('Submit quiz response:', response);
if (response.success && response.data) { if (response.success && response.data) {
console.log('Quiz submission successful, data:', response.data);
// Update user balance with earned stars // Update user balance with earned stars
if (response.data.stars_earned > 0) { if (response.data.stars_earned > 0) {
updateUser({ updateUser({
@ -129,6 +131,7 @@ export const QuizPage: React.FC = () => {
} }
}); });
} else { } else {
console.log('Quiz submission failed:', response);
setError('Не удалось отправить ответы'); setError('Не удалось отправить ответы');
} }
} catch (err) { } catch (err) {

View File

@ -28,6 +28,10 @@ export const QuizResultPage: React.FC = () => {
const state = location.state as LocationState; const state = location.state as LocationState;
useEffect(() => {
console.log('QuizResultPage state:', state);
}, [state]);
useEffect(() => { useEffect(() => {
if (!state) { if (!state) {
navigate('/home'); navigate('/home');
@ -56,6 +60,14 @@ export const QuizResultPage: React.FC = () => {
? Math.round((result.correct_answers / result.total_questions) * 100) ? Math.round((result.correct_answers / result.total_questions) * 100)
: 0; : 0;
console.log('Result data:', {
result,
quizTitle,
percentage,
correctAnswers: result.correct_answers,
totalQuestions: result.total_questions
});
return ( return (
<Box> <Box>
{/* Result Header */} {/* Result Header */}
@ -96,31 +108,38 @@ export const QuizResultPage: React.FC = () => {
{/* Score Circle */} {/* Score Circle */}
<Box <Box
sx={{ sx={{
width: 120,
height: 120,
borderRadius: '50%',
backgroundColor: '#FFD700',
display: 'flex', display: 'flex',
alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
margin: '0 auto 3', mb: 2,
border: '4px solid #ffffff',
boxShadow: '0 0 20px rgba(255, 215, 0, 0.5)',
}} }}
> >
<Typography <Box
variant="h3"
sx={{ sx={{
color: '#000000', width: 80,
fontWeight: 'bold', height: 80,
borderRadius: '50%',
backgroundColor: '#FFD700',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '3px solid #ffffff',
boxShadow: '0 0 15px rgba(255, 215, 0, 0.5)',
}} }}
> >
{percentage}% <Typography
</Typography> variant="h4"
sx={{
color: '#000000',
fontWeight: 'bold',
}}
>
{percentage}%
</Typography>
</Box>
</Box> </Box>
{/* Score Details */} {/* Score Details */}
<Box sx={{ textAlign: 'center', mb: 3 }}> <Box sx={{ textAlign: 'center', mb: 3, mt: 2 }}>
<Typography <Typography
variant="h6" variant="h6"
sx={{ sx={{
@ -159,7 +178,7 @@ export const QuizResultPage: React.FC = () => {
fontWeight: 'bold', fontWeight: 'bold',
}} }}
> >
+{result.stars_earned} начислено! +{result.stars_earned} начислено!
</Typography> </Typography>
</Box> </Box>
</CardContent> </CardContent>

View File

@ -138,7 +138,7 @@ export const ShopPage: React.FC = () => {
gap: 1, gap: 1,
}} }}
> >
Ваш баланс: {user?.stars_balance} Ваш баланс: {user?.stars_balance}
</Typography> </Typography>
</Box> </Box>
@ -195,7 +195,7 @@ export const ShopPage: React.FC = () => {
{/* Price and Status */} {/* Price and Status */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
<Chip <Chip
label={`${reward.price_stars}`} label={`${reward.price_stars}`}
size="small" size="small"
sx={{ sx={{
backgroundColor: canAfford(reward.price_stars) backgroundColor: canAfford(reward.price_stars)
@ -251,7 +251,7 @@ export const ShopPage: React.FC = () => {
}} }}
> >
{!canAfford(reward.price_stars) {!canAfford(reward.price_stars)
? 'Недостаточно ' ? 'Недостаточно звёзд'
: !isInStock(reward.stock) : !isInStock(reward.stock)
? 'Нет в наличии' ? 'Нет в наличии'
: 'Купить' : 'Купить'
@ -315,7 +315,7 @@ export const ShopPage: React.FC = () => {
variant="body1" variant="body1"
sx={{ color: '#888', mb: 1 }} sx={{ color: '#888', mb: 1 }}
> >
Вы уверены, что хотите купить "{selectedReward?.title}" за {selectedReward?.price_stars} ? Вы уверены, что хотите купить "{selectedReward?.title}" за {selectedReward?.price_stars}?
</Typography> </Typography>
<Box sx={{ display: 'flex', gap: 2, mt: 3 }}> <Box sx={{ display: 'flex', gap: 2, mt: 3 }}>
<Button <Button

View File

@ -58,10 +58,15 @@ export interface SubmissionRequest {
} }
export interface SubmissionResponse { export interface SubmissionResponse {
id: number;
user_id: number;
quiz_id: number;
score: number; score: number;
total_questions: number;
stars_earned: number; stars_earned: number;
completed_at: string;
answers: UserAnswer[];
correct_answers: number; correct_answers: number;
total_questions: number;
} }
export interface CanRepeatResponse { export interface CanRepeatResponse {