All files / src/utils/components/class-card-banner ClassCardBanner.jsx

100% Statements 22/22
73.33% Branches 11/15
100% Functions 7/7
100% Lines 21/21

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126            59x   177x 177x           280x 280x 280x   55x                       40x                       71x                           59x         55x                   70x 70x 70x               70x 70x 70x 70x   70x                 280x                                   70x                      
import { useMemo } from "react";
import { Box, Chip } from "@mui/material";
import { alpha, useTheme } from "@mui/material/styles";
import { generateClassCardShapes } from "./generateClassCardShapes";
 
function trianglePoints(cx, cy, r) {
    return [-90, 30, 150]
        .map((deg) => {
            const rad = (deg * Math.PI) / 180;
            return `${cx + r * Math.cos(rad)},${cy + r * Math.sin(rad)}`;
        })
        .join(" ");
}
 
function BannerShape({ shape, color, fillOpacity, strokeOpacity }) {
    const { type, cx, cy, r, rotate } = shape;
    const transform = `rotate(${rotate} ${cx} ${cy})`;
    switch (type) {
        case "ring":
            return (
                <circle
                    cx={cx}
                    cy={cy}
                    r={r}
                    fill="none"
                    stroke={color}
                    strokeOpacity={strokeOpacity}
                    strokeWidth={1.5}
                />
            );
        case "square":
            return (
                <rect
                    x={cx - r * 0.75}
                    y={cy - r * 0.75}
                    width={r * 1.5}
                    height={r * 1.5}
                    fill={color}
                    opacity={fillOpacity}
                    transform={transform}
                />
            );
        case "squareOutline":
            return (
                <rect
                    x={cx - r * 0.75}
                    y={cy - r * 0.75}
                    width={r * 1.5}
                    height={r * 1.5}
                    fill="none"
                    stroke={color}
                    strokeOpacity={strokeOpacity}
                    strokeWidth={1.5}
                    transform={transform}
                />
            );
        case "triangle":
            return (
                <polygon points={trianglePoints(cx, cy, r)} fill={color} opacity={fillOpacity} transform={transform} />
            );
        case "circle":
        default:
            return <circle cx={cx} cy={cy} r={r} fill={color} opacity={fillOpacity} />;
    }
}
 
// No per-class cover image exists in the class model, so this banner is
// generated instead: a flat accent-color fill plus a crisp (no blur, no
// gradient) scatter of shapes computed continuously from `seed` — the same
// seed always renders the same layout, and distinct seeds render distinct
// layouts. Shared by the teacher and student class card grids.
export default function ClassCardBanner({ seed, accentMain, label, labelId, ariaLabel, height = 120 }) {
    const theme = useTheme();
    const isLight = theme.palette.mode !== "dark";
    const shapes = useMemo(() => generateClassCardShapes(seed), [seed]);
 
    // Light mode: a mostly-white banner — a ~20% tint of the accent color
    // (the rest reads as white, since that's what's behind this Box) instead
    // of a fully-saturated flat fill. The shape scatter switches from white
    // to the accent color itself so it stays visible against the now-pale
    // background. Dark mode is unaffected — a saturated fill with white
    // shapes already reads correctly there.
    const bannerBg = isLight ? alpha(accentMain, 0.2) : accentMain;
    const shapeColor = isLight ? accentMain : "#fff";
    const fillOpacity = isLight ? 0.4 : 0.16;
    const strokeOpacity = isLight ? 0.6 : 0.35;
 
    return (
        <Box role="img" aria-label={ariaLabel} sx={{ position: "relative", height, bgcolor: bannerBg }}>
            <Box
                component="svg"
                viewBox="0 0 100 100"
                preserveAspectRatio="none"
                sx={{ width: "100%", height: "100%" }}
            >
                {shapes.map((s, i) => (
                    <BannerShape
                        key={i}
                        shape={s}
                        color={shapeColor}
                        fillOpacity={fillOpacity}
                        strokeOpacity={strokeOpacity}
                    />
                ))}
            </Box>
 
            <Chip
                label={label}
                size="small"
                id={labelId}
                sx={{
                    position: "absolute",
                    top: 12,
                    right: 12,
                    bgcolor: (t) => alpha(t.palette.background.paper, 0.92),
                    color: "text.primary",
                    fontWeight: 700,
                    fontSize: "0.64rem",
                    height: 22,
                    backdropFilter: "blur(4px)",
                }}
            />
        </Box>
    );
}