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 {
ID int `json:"id"`
UserID int64 `json:"user_id"`
QuizID int `json:"quiz_id"`
Score int `json:"score"`
StarsEarned int `json:"stars_earned"`
CompletedAt time.Time `json:"completed_at"`
Answers []UserAnswer `json:"answers"` // Stored as JSONB
ID int `json:"id"`
UserID int64 `json:"user_id"`
QuizID int `json:"quiz_id"`
Score int `json:"score"`
StarsEarned int `json:"stars_earned"`
CompletedAt time.Time `json:"completed_at"`
Answers []UserAnswer `json:"answers"` // Stored as JSONB
// Additional fields for response
CorrectAnswers int `json:"correct_answers"`
TotalQuestions int `json:"total_questions"`
}
type Reward struct {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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