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
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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)',
|
||||
|
||||
@ -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
|
||||
|
||||
@ -107,7 +107,7 @@ export const HeaderProfile: React.FC<HeaderProfileProps> = ({
|
||||
fontSize: 20,
|
||||
}}
|
||||
>
|
||||
{starsBalance.toLocaleString()} ⭐
|
||||
{starsBalance.toLocaleString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -94,9 +94,6 @@ export const ProfilePage: React.FC = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@ -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)',
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user