Emojy reactions
This commit is contained in:
@@ -53,7 +53,7 @@ export const NameOverlay = styled.div`
|
||||
`
|
||||
|
||||
// Стили без интерполяций компонентов
|
||||
export const Wrapper = styled.div<{ warn?: boolean; width?: string | number }>`
|
||||
export const Wrapper = styled.div<{ warn?: boolean; width?: string | number; position?: string }>`
|
||||
list-style: none;
|
||||
position: relative;
|
||||
border-radius: 12px;
|
||||
@@ -98,6 +98,13 @@ export const Wrapper = styled.div<{ warn?: boolean; width?: string | number }>`
|
||||
`
|
||||
: ''}
|
||||
|
||||
${({ position }) =>
|
||||
position
|
||||
? css`
|
||||
position: ${position};
|
||||
`
|
||||
: ''}
|
||||
|
||||
${(props) =>
|
||||
props.warn
|
||||
? css`
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
import React from 'react'
|
||||
import { sha256 } from 'js-sha256'
|
||||
import { useState } from 'react'
|
||||
import { Box, useColorMode } from '@chakra-ui/react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { Box, useColorMode, Text } from '@chakra-ui/react'
|
||||
import { CheckCircleIcon, AddIcon } from '@chakra-ui/icons'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { User } from '../../__data__/model'
|
||||
import { User, Reaction } from '../../__data__/model'
|
||||
|
||||
import { AddMissedButton, Avatar, Wrapper, NameOverlay } from './style'
|
||||
|
||||
// Map of reaction types to emojis
|
||||
const REACTION_EMOJIS = {
|
||||
thumbs_up: '👍',
|
||||
heart: '❤️',
|
||||
laugh: '😂',
|
||||
wow: '😮',
|
||||
clap: '👏'
|
||||
}
|
||||
|
||||
export function getGravatarURL(email, user) {
|
||||
if (!email) return void 0
|
||||
const address = String(email).trim().toLowerCase()
|
||||
@@ -22,7 +33,8 @@ export const UserCard = ({
|
||||
onAddUser = undefined,
|
||||
wrapperAS = 'div',
|
||||
width,
|
||||
recentlyPresent = false
|
||||
recentlyPresent = false,
|
||||
reactions = []
|
||||
}: {
|
||||
student: User
|
||||
present: boolean
|
||||
@@ -30,9 +42,50 @@ export const UserCard = ({
|
||||
onAddUser?: (user: User) => void
|
||||
wrapperAS?: React.ElementType<any, keyof React.JSX.IntrinsicElements>;
|
||||
recentlyPresent?: boolean
|
||||
reactions?: Reaction[]
|
||||
}) => {
|
||||
const { colorMode } = useColorMode();
|
||||
const { t } = useTranslation();
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const [visibleReactions, setVisibleReactions] = useState<Reaction[]>([]);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Filter reactions to only show this student's reactions
|
||||
useEffect(() => {
|
||||
const studentReactions = reactions.filter(r => r.sub === student.sub);
|
||||
|
||||
if (studentReactions.length > 0) {
|
||||
// Check for new reactions
|
||||
const newReactions = studentReactions.filter(
|
||||
newReaction => !visibleReactions.some(
|
||||
existingReaction => existingReaction._id === newReaction._id
|
||||
)
|
||||
);
|
||||
|
||||
if (newReactions.length > 0) {
|
||||
// If there are new reactions, add them to visible reactions
|
||||
setVisibleReactions(prevReactions => [...prevReactions, ...newReactions]);
|
||||
|
||||
// Clear any existing timeout
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
// Set a new timeout
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setVisibleReactions([]);
|
||||
timeoutRef.current = null;
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up on unmount
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [reactions, student.sub, visibleReactions]);
|
||||
|
||||
return (
|
||||
<Wrapper
|
||||
@@ -40,6 +93,7 @@ export const UserCard = ({
|
||||
as={wrapperAS}
|
||||
width={width}
|
||||
className={!present ? 'warn' : recentlyPresent ? 'recent' : ''}
|
||||
position="relative"
|
||||
>
|
||||
<Avatar
|
||||
src={imageError ? getGravatarURL(student.email, null) : (student.picture || getGravatarURL(student.email, null))}
|
||||
@@ -58,10 +112,52 @@ export const UserCard = ({
|
||||
)}
|
||||
</NameOverlay>
|
||||
{onAddUser && !present && (
|
||||
<AddMissedButton onClick={() => onAddUser(student)} aria-label="Отметить присутствие">
|
||||
<AddMissedButton onClick={() => onAddUser(student)} aria-label={t('journal.pl.common.add')}>
|
||||
<AddIcon boxSize={3} />
|
||||
</AddMissedButton>
|
||||
)}
|
||||
|
||||
{/* Student reactions animation */}
|
||||
<AnimatePresence>
|
||||
{visibleReactions.map((reaction, index) => (
|
||||
<motion.div
|
||||
key={reaction._id || index}
|
||||
initial={{ opacity: 0, scale: 0.5, x: 0, y: 0 }}
|
||||
animate={{ opacity: 1, scale: 1, x: 0, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.5, y: -20 }}
|
||||
transition={{
|
||||
duration: 0.5,
|
||||
delay: index * 0.1
|
||||
}}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '10px', // Position at the top
|
||||
right: '10px', // Position at the right
|
||||
zIndex: 10,
|
||||
pointerEvents: 'none',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: colorMode === 'light' ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.6)',
|
||||
borderRadius: '50%',
|
||||
padding: '2px',
|
||||
boxShadow: '0 2px 5px rgba(0,0,0,0.2)'
|
||||
}}
|
||||
title={t(`journal.pl.reactions.${reaction.reaction}`)}
|
||||
>
|
||||
<Text
|
||||
fontSize="3xl" // Increased size
|
||||
sx={{
|
||||
filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.3))',
|
||||
transform: 'scale(1.2)', // Additional scaling
|
||||
display: 'flex'
|
||||
}}
|
||||
>
|
||||
{REACTION_EMOJIS[reaction.reaction] || reaction.reaction}
|
||||
</Text>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user