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:
parent
a96e123141
commit
46ffe69b57
@ -92,6 +92,9 @@ type QuizAttempt struct {
|
|||||||
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 {
|
||||||
|
|||||||
@ -130,6 +130,8 @@ func (s *quizService) SubmitQuiz(ctx context.Context, userID int64, quizID int,
|
|||||||
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)
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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)',
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -107,7 +107,7 @@ export const HeaderProfile: React.FC<HeaderProfileProps> = ({
|
|||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{starsBalance.toLocaleString()} ⭐
|
{starsBalance.toLocaleString()}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -94,9 +94,6 @@ export const ProfilePage: React.FC = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
|
||||||
logout();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@ -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)',
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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,20 +108,26 @@ export const QuizResultPage: React.FC = () => {
|
|||||||
{/* Score Circle */}
|
{/* Score Circle */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
width: 120,
|
display: 'flex',
|
||||||
height: 120,
|
justifyContent: 'center',
|
||||||
|
mb: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
backgroundColor: '#FFD700',
|
backgroundColor: '#FFD700',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
margin: '0 auto 3',
|
border: '3px solid #ffffff',
|
||||||
border: '4px solid #ffffff',
|
boxShadow: '0 0 15px rgba(255, 215, 0, 0.5)',
|
||||||
boxShadow: '0 0 20px rgba(255, 215, 0, 0.5)',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
variant="h3"
|
variant="h4"
|
||||||
sx={{
|
sx={{
|
||||||
color: '#000000',
|
color: '#000000',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
@ -118,9 +136,10 @@ export const QuizResultPage: React.FC = () => {
|
|||||||
{percentage}%
|
{percentage}%
|
||||||
</Typography>
|
</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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user