beauty cards

This commit is contained in:
2025-04-04 00:11:53 +03:00
parent 4157cd574b
commit 5debb1ebe9
3 changed files with 150 additions and 256 deletions

View File

@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
import { User } from '../../__data__/model'
import { AddMissedButton } from './sstyle'
import { AddIcon } from '@chakra-ui/icons'
import { UserCard } from '../user-card'
interface StudentCardBackProps {
student: User & { present?: boolean; recentlyPresent?: boolean }
@@ -14,139 +15,33 @@ export const StudentCardBack: React.FC<StudentCardBackProps> = ({ student, onAdd
const { colorMode } = useColorMode()
const { t } = useTranslation()
// Determine attendance level based on student data
// We simulate different battery levels based on student presence
const getAttendanceLevel = () => {
// Using a scale of 0-5 for battery levels (6 segments total)
// In a real scenario, you would calculate this based on attendance history
// If student is present, show higher battery level
if (student.present) {
return student.recentlyPresent ? 4 : 5; // Full or almost full if present
}
// For absent students, randomly assign a lower battery level (0-3)
// In a real implementation, this would come from attendance history
// Веселые градиентные цвета
const colors = {
gradient: colorMode === 'light'
? 'linear-gradient(135deg, #F6FFDE, #E3F2C1, #C9DBB2)'
: 'linear-gradient(135deg, #0D1282, #2F58CD, #3795BD)',
text: colorMode === 'light' ? "#2E7D32" : "#81C784"
}
// Is the student marked as present?
const isPresent = !!student.present;
// Функция для генерации случайного, но стабильного положения пузырьков для каждого студента
const getBubbleStyle = (index: number) => {
// Используем ID студента для получения стабильных, но уникальных чисел
const id = student.sub || '';
// Use the student ID hash to deterministically assign a battery level
// This creates a pseudo-random but consistent level for each student
const idSum = id.split('').reduce((sum, char) => sum + char.charCodeAt(0), 0);
return Math.min(3, Math.floor((idSum % 100) / 25));
}
const batteryLevel = getAttendanceLevel();
// Check if student recently joined the class
const isRecentlyJoined = !!student.recentlyPresent;
// Get color scheme based on battery level
const getBatteryColors = () => {
// Colors for different battery levels
const colorSchemes = [
// Empty (0)
{
light: {
primary: "#E53E3E", // red.500
secondary: "#FED7D7", // red.100
accent: "#C53030", // red.700
text: "journal.pl.lesson.veryPoorAttendance"
},
dark: {
primary: "#F56565", // red.400
secondary: "#9B2C2C", // red.800
accent: "#FEB2B2", // red.200
text: "journal.pl.lesson.veryPoorAttendance"
}
},
// Very Low (1)
{
light: {
primary: "#DD6B20", // orange.500
secondary: "#FEEBC8", // orange.100
accent: "#C05621", // orange.700
text: "journal.pl.lesson.poorAttendance"
},
dark: {
primary: "#ED8936", // orange.400
secondary: "#9C4221", // orange.800
accent: "#FBD38D", // orange.200
text: "journal.pl.lesson.poorAttendance"
}
},
// Low (2)
{
light: {
primary: "#D69E2E", // yellow.500
secondary: "#FEFCBF", // yellow.100
accent: "#B7791F", // yellow.700
text: "journal.pl.lesson.lowAttendance"
},
dark: {
primary: "#ECC94B", // yellow.400
secondary: "#975A16", // yellow.800
accent: "#F6E05E", // yellow.200
text: "journal.pl.lesson.lowAttendance"
}
},
// Medium (3)
{
light: {
primary: "#38B2AC", // teal.500
secondary: "#B2F5EA", // teal.100
accent: "#285E61", // teal.700
text: "journal.pl.lesson.mediumAttendance"
},
dark: {
primary: "#4FD1C5", // teal.400
secondary: "#234E52", // teal.800
accent: "#81E6D9", // teal.200
text: "journal.pl.lesson.mediumAttendance"
}
},
// Good (4)
{
light: {
primary: "#3182CE", // blue.500
secondary: "#BEE3F8", // blue.100
accent: "#2C5282", // blue.700
text: "journal.pl.lesson.goodAttendance"
},
dark: {
primary: "#4299E1", // blue.400
secondary: "#2A4365", // blue.800
accent: "#90CDF4", // blue.200
text: "journal.pl.lesson.goodAttendance"
}
},
// Excellent (5)
{
light: {
primary: "#38A169", // green.500
secondary: "#C6F6D5", // green.100
accent: "#276749", // green.700
text: "journal.pl.lesson.excellentAttendance"
},
dark: {
primary: "#48BB78", // green.400
secondary: "#22543D", // green.800
accent: "#9AE6B4", // green.200
text: "journal.pl.lesson.excellentAttendance"
}
}
];
const charSum = id.split('').reduce((sum, char, i) => sum + char.charCodeAt(0) * (i + 1), 0);
const scheme = colorSchemes[batteryLevel];
return scheme[colorMode === 'light' ? 'light' : 'dark'];
}
const colors = getBatteryColors();
// Function to determine which battery segments to fill based on level
const getSegmentFill = (segmentIndex: number) => {
const isActive = segmentIndex <= batteryLevel;
// Разные значения для разных пузырей
const seed = (charSum + index * 137) % 100;
return isActive ? colors.primary : 'transparent';
}
return {
left: `${(seed % 80) + 10}%`,
top: `${((seed * 1.5) % 80) + 10}%`,
size: `${(seed % 15) + 10}px`,
duration: `${(seed % 4) + 8}s`
};
};
return (
<Flex
@@ -168,113 +63,112 @@ export const StudentCardBack: React.FC<StudentCardBackProps> = ({ student, onAdd
aspectRatio: "1"
}}
>
<AddMissedButton onClick={() => onAddUser(student)} aria-label={t('journal.pl.common.add')}>
{/* Add button */}
{!isPresent && (
<AddMissedButton
onClick={() => onAddUser?.(student)}
aria-label={t('journal.pl.common.add')}
>
<AddIcon boxSize={3} />
</AddMissedButton>
)}
{/* Веселый фон с градиентом */}
<Box
position="absolute"
top="0"
left="0"
right="0"
bottom="0"
opacity="0.15"
className="animated-bg"
sx={{
background: `linear-gradient(135deg,
${colors.secondary}, ${colors.primary}, ${colors.accent})`,
backgroundSize: "400% 400%",
animation: "gradientAnimation 8s ease infinite",
"@keyframes gradientAnimation": {
"0%": { backgroundPosition: "0% 50%" },
"50%": { backgroundPosition: "100% 50%" },
"100%": { backgroundPosition: "0% 50%" }
}
}}
background={colors.gradient}
opacity="0.2"
/>
<Box
position="relative"
textAlign="center"
{/* Декоративные пузырьки */}
{[...Array(6)].map((_, i) => {
const style = getBubbleStyle(i);
return (
<Box
key={i}
position="absolute"
left={style.left}
top={style.top}
width={style.size}
height={style.size}
borderRadius="full"
background={colors.gradient}
opacity="0.4"
sx={{
animation: `float ${style.duration} ease-in-out infinite`,
"@keyframes float": {
"0%, 100%": { transform: "translateY(0px)" },
"50%": { transform: "translateY(-10px)" }
}
}}
/>
);
})}
{/* Content */}
<Flex
direction="column"
align="center"
justify="center"
width="100%"
zIndex="1"
>
{/* Gravatar с весёлой анимацией */}
<Box
width="70px"
height="70px"
mx="auto"
mb={3}
position="relative"
width="80px"
height="80px"
borderRadius="full"
overflow="hidden"
mb={4}
boxShadow="0 0 15px rgba(255, 255, 255, 0.4)"
>
{isRecentlyJoined && (
<Box
position="absolute"
top="-5px"
left="-5px"
right="-5px"
bottom="-5px"
borderRadius="full"
zIndex="0"
sx={{
animation: "pulse 2s infinite",
background: `radial-gradient(circle, ${colors.accent} 0%, transparent 70%)`,
"@keyframes pulse": {
"0%": { opacity: 0.7, transform: "scale(0.95)" },
"70%": { opacity: 0, transform: "scale(1.2)" },
"100%": { opacity: 0, transform: "scale(1.5)" }
}
}}
{/* Using the UserCard for gravatar */}
<Box
position="absolute"
top="0"
left="0"
right="0"
bottom="0"
zIndex="1"
overflow="hidden"
>
<UserCard
wrapperAS="div"
student={student}
present={false}
width="100%"
/>
)}
{/* Battery icon with 6 segments */}
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
{/* Battery outline */}
<rect
x="2"
y="6"
width="18"
height="12"
rx="2"
stroke={colors.primary}
strokeWidth="1.5"
fill="transparent"
/>
{/* Battery cap */}
<path
d="M20 10H22V14H20V10Z"
fill={colors.primary}
/>
{/* Battery segments - from lowest to highest */}
<rect x="4" y="8" width="2.3" height="8" rx="1" fill={getSegmentFill(0)} />
<rect x="7" y="8" width="2.3" height="8" rx="1" fill={getSegmentFill(1)} />
<rect x="10" y="8" width="2.3" height="8" rx="1" fill={getSegmentFill(2)} />
<rect x="13" y="8" width="2.3" height="8" rx="1" fill={getSegmentFill(3)} />
<rect x="16" y="8" width="2.3" height="8" rx="1" fill={getSegmentFill(4)} />
{/* Lightning icon if recently joined or fully charged */}
{(isRecentlyJoined || batteryLevel === 5) && (
<path
d="M11.5 7.5L8 12H11L9.5 16.5L13 12H10L11.5 7.5Z"
fill={colors.accent}
stroke={colors.primary}
strokeWidth="0.3"
opacity="0.9"
/>
)}
</svg>
</Box>
</Box>
<Box fontSize="sm" fontWeight="medium" color={colorMode === "light" ? "gray.800" : "white"}>
{/* Student name */}
<Box
fontSize="sm"
fontWeight="medium"
textAlign="center"
color={colorMode === "light" ? "gray.800" : "white"}
mb={1}
>
{student.name || student.preferred_username}
</Box>
{/* Status с эмодзи */}
<Box
fontSize="xs"
mt={1}
color={colors.primary}
color={colors.text}
fontWeight="medium"
display="flex"
alignItems="center"
>
{isRecentlyJoined
? t('journal.pl.lesson.recentlyJoined')
: t(colors.text || 'journal.pl.lesson.attendance')}
<Box as="span" mr="1" fontSize="sm">🔔</Box>
{t('journal.pl.lesson.notMarked')}
</Box>
</Box>
</Flex>
</Flex>
)
}

View File

@@ -109,7 +109,7 @@ export const Wrapper = styled.div<{ warn?: boolean; width?: string | number; pos
props.warn
? css`
opacity: 0.7;
filter: grayscale(0.8);
filter: grayscale(0.3);
`
: ''}
`