Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ export function AchievementUnlockToast({ achievements, onDismiss }: AchievementU

{/* Close button */}
<button
type="button"
onClick={handleClose}
aria-label="Fermer"
className="absolute top-4 right-4 p-2 rounded-full text-white/50 hover:bg-white/10 hover:text-white transition-colors z-20"
Expand Down Expand Up @@ -235,9 +236,9 @@ export function AchievementUnlockToast({ achievements, onDismiss }: AchievementU
</motion.div>

{/* Floating Particles */}
{[...Array.from({ length: 6 })].map((_, i) => (
{[1, 2, 3, 4, 5, 6].map((key, i) => (
<motion.div
key={i}
key={key}
initial={{ opacity: 0, scale: 0, x: 0, y: 0 }}
animate={{
opacity: [0, 1, 0],
Expand Down Expand Up @@ -290,6 +291,7 @@ export function AchievementUnlockToast({ achievements, onDismiss }: AchievementU
>
{/* Action Button */}
<button
type="button"
onClick={handleNext}
className={cn(
'group w-full py-4 px-6 rounded-xl font-bold text-white shadow-lg transition-all',
Expand Down Expand Up @@ -320,9 +322,9 @@ export function AchievementUnlockToast({ achievements, onDismiss }: AchievementU
{/* Progress Dots */}
{achievements.length > 1 && (
<div className="flex justify-center gap-2 pt-2">
{achievements.map((_, i) => (
{achievements.map((achievement, i) => (
<div
key={i}
key={achievement.id}
className={cn(
'h-1.5 rounded-full transition-all duration-300',
i === currentIndex
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { motion } from 'motion/react'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { cn } from '@/lib/utils'

type ActivityStatus = 'active' | 'warning' | 'inactive'

interface ActivityStatusCardProps {
childName: string
childImage?: string
lastActiveAt: Date | null
status: ActivityStatus
className?: string
}

/**
* Activity Status Card
*
* Shows child's current activity status with color-coded badge:
* - 🟢 Active (today)
* - 🟡 Warning (2-3 days)
* - 🔴 Inactive (4+ days)
*/
export function ActivityStatusCard({
childName,
childImage,
lastActiveAt,
status,
className,
}: ActivityStatusCardProps) {
const getStatusConfig = (s: ActivityStatus) => {
switch (s) {
case 'active':
return {
label: 'Actif',
color: 'bg-emerald-500',
textColor: 'text-emerald-400',
bgColor: 'bg-emerald-500/10',
borderColor: 'border-emerald-500/20',
}
case 'warning':
return {
label: 'Inactif 2-3j',
color: 'bg-amber-500',
textColor: 'text-amber-400',
bgColor: 'bg-amber-500/10',
borderColor: 'border-amber-500/20',
}
case 'inactive':
return {
label: 'Inactif 4j+',
color: 'bg-red-500',
textColor: 'text-red-400',
bgColor: 'bg-red-500/10',
borderColor: 'border-red-500/20',
}
}
}

const statusConfig = getStatusConfig(status)

const getRelativeTime = (date: Date | null) => {
if (!date)
return 'Jamais connecté'

const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffMins = Math.floor(diffMs / 60000)
const diffHours = Math.floor(diffMs / 3600000)
const diffDays = Math.floor(diffMs / 86400000)

if (diffMins < 1)
return 'À l\'instant'
if (diffMins < 60)
return `Il y a ${diffMins} min`
if (diffHours < 24)
return `Il y a ${diffHours}h`
if (diffDays === 1)
return 'Hier'
return `Il y a ${diffDays} jours`
}

const getInitials = (name: string) => {
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
}

return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className={cn(
'relative overflow-hidden rounded-2xl border p-4 backdrop-blur-md',
statusConfig.bgColor,
statusConfig.borderColor,
className,
)}
>
<div className="flex items-center gap-4">
{/* Avatar with status ring */}
<div className="relative">
<Avatar className={cn('h-14 w-14 border-2', statusConfig.borderColor)}>
<AvatarImage src={childImage} />
<AvatarFallback className={cn('text-lg font-bold', statusConfig.bgColor, statusConfig.textColor)}>
{getInitials(childName)}
</AvatarFallback>
</Avatar>
{/* Status dot */}
<span className={cn(
'absolute bottom-0 right-0 h-4 w-4 rounded-full border-2 border-background',
statusConfig.color,
)}
/>
</div>

{/* Info */}
<div className="flex-1">
<h3 className="text-lg font-bold text-foreground">{childName}</h3>
<p className="text-sm text-muted-foreground">{getRelativeTime(lastActiveAt)}</p>
</div>

{/* Status badge */}
<span className={cn(
'px-3 py-1 rounded-full text-xs font-bold',
statusConfig.bgColor,
statusConfig.textColor,
)}
>
{statusConfig.label}
</span>
</div>
</motion.div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { Link, useRouterState } from '@tanstack/react-router'
import { BarChart3, Bell, Home, User } from 'lucide-react'
import { motion } from 'motion/react'
import { cn } from '@/lib/utils'

/**
* Parent Bottom Navigation Component
*
* Features:
* - Teal/Cyan color scheme to differentiate from student nav
* - Floating glassmorphism design matching app style
* - Smooth motion animations
* - 4 items: Accueil, Stats, Alertes, Profil
*/

const parentNavItems = [
{
icon: Home,
label: 'Accueil',
href: '/app/parent',
activeColor: 'text-teal-400',
glowColor: 'bg-teal-500/20',
},
{
icon: BarChart3,
label: 'Stats',
href: '/app/parent/stats',
activeColor: 'text-cyan-400',
glowColor: 'bg-cyan-500/20',
},
{
icon: Bell,
label: 'Alertes',
href: '/app/parent/alerts',
activeColor: 'text-amber-400',
glowColor: 'bg-amber-500/20',
},
{
icon: User,
label: 'Profil',
href: '/app/parent/profile',
activeColor: 'text-emerald-400',
glowColor: 'bg-emerald-500/20',
},
] as const

interface ParentBottomNavProps {
/** Number of unread alerts to show as badge */
alertCount?: number
}

export function ParentBottomNav({ alertCount = 0 }: ParentBottomNavProps) {
const router = useRouterState()
// Clean pathname to handle potential trailing slashes
const currentPath = router.location.pathname.replace(/\/$/, '')

return (
<div className="fixed bottom-0 left-0 right-0 z-50 px-4 pb-6 pt-2 pointer-events-none flex justify-center">
<nav className="pointer-events-auto w-full max-w-md rounded-2xl border border-border bg-card/80 backdrop-blur-xl shadow-2xl">
<ul className="flex items-center justify-between px-2 py-2">
{parentNavItems.map((item) => {
const Icon = item.icon

// Precise active state matching
// For 'Accueil' (/app/parent), only match exact path
// For others, allow partial matching
const isActive = item.href === '/app/parent'
? currentPath === '/app/parent' || currentPath === '/app/parent/'
: currentPath.startsWith(item.href)

const showBadge = item.label === 'Alertes' && alertCount > 0

return (
<li key={item.href} className="relative flex-1">
<Link
to={item.href}
className="group flex flex-col items-center justify-center gap-1 py-1 relative z-10 w-full outline-none"
>
<div className={cn(
'relative p-2 rounded-xl transition-all duration-300 group-active:scale-95',
isActive ? item.glowColor : 'bg-transparent hover:bg-accent',
)}
>
<Icon
className={cn(
'h-6 w-6 transition-all duration-300',
isActive ? cn('scale-110', item.activeColor) : 'text-muted-foreground group-hover:text-foreground',
)}
strokeWidth={isActive ? 2.5 : 2}
/>

{/* Alert badge */}
{showBadge && (
<span className="absolute -top-1 -right-1 min-w-[18px] h-[18px] px-1 flex items-center justify-center rounded-full bg-red-500 text-white text-[10px] font-bold shadow-lg">
{alertCount > 99 ? '99+' : alertCount}
</span>
)}

{/* Subtle glow effect for active state */}
{isActive && (
<motion.div
layoutId="parent-nav-glow"
className={cn('absolute inset-0 rounded-xl blur-md opacity-40', item.glowColor)}
transition={{ duration: 0.3 }}
/>
)}
</div>

{/* Label */}
<span className={cn(
'text-[10px] font-medium transition-colors duration-300',
isActive ? item.activeColor : 'text-muted-foreground',
)}
>
{item.label}
</span>

{/* Active Indicator Dot */}
<div className="h-1 w-full flex justify-center">
{isActive && (
<motion.span
layoutId="parent-nav-pill"
className={cn('h-1 w-1 rounded-full', item.activeColor.replace('text-', 'bg-'))}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
/>
)}
</div>
</Link>
</li>
)
})}
</ul>
</nav>
</div>
)
}
Loading
Loading