feat(awards): render backgroundImage as faded backdrop on medal cards
This commit is contained in:
parent
d8867a643a
commit
d952a33ee7
6 changed files with 119 additions and 29 deletions
|
|
@ -73,8 +73,17 @@ export default async function AwardsPage() {
|
|||
{section.items.map((award) => {
|
||||
const awardImage =
|
||||
typeof award.image === "object" && award.image ? (award.image as Media) : null;
|
||||
const backgroundImage =
|
||||
typeof award.backgroundImage === "object" && award.backgroundImage
|
||||
? (award.backgroundImage as Media)
|
||||
: null;
|
||||
return (
|
||||
<AwardWithGrantButton key={award.id} award={award} awardImage={awardImage} />
|
||||
<AwardWithGrantButton
|
||||
key={award.id}
|
||||
award={award}
|
||||
awardImage={awardImage}
|
||||
backgroundImage={backgroundImage}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -623,7 +623,18 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
|
|||
.map((award) => {
|
||||
const awardImage =
|
||||
award.image && typeof award.image === "object" ? (award.image as Media) : null;
|
||||
return <AwardCard key={award.id} awardImage={awardImage} award={award} />;
|
||||
const backgroundImage =
|
||||
award.backgroundImage && typeof award.backgroundImage === "object"
|
||||
? (award.backgroundImage as Media)
|
||||
: null;
|
||||
return (
|
||||
<AwardCard
|
||||
key={award.id}
|
||||
awardImage={awardImage}
|
||||
backgroundImage={backgroundImage}
|
||||
award={award}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -406,38 +406,69 @@
|
|||
|
||||
.medal-card::after {
|
||||
content: "";
|
||||
z-index: 2;
|
||||
background: radial-gradient(circle, transparent, rgb(253 224 71 / 0.1));
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
animation: pulse 5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.medal-card > .medal-card-bg,
|
||||
.medal-card > .medal-card-scrim {
|
||||
position: absolute !important;
|
||||
inset: 0;
|
||||
z-index: 0 !important;
|
||||
}
|
||||
|
||||
.medal-card > .medal-card-bg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.medal-card > .medal-card-scrim {
|
||||
border-radius: inherit;
|
||||
background-color: rgb(0 0 0 / 0.42);
|
||||
box-shadow: inset 0 0 2.25rem rgb(0 0 0 / 0.38);
|
||||
}
|
||||
|
||||
.medal-card.medal-card-overflow {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.award-xp-high {
|
||||
background: linear-gradient(110deg, rgb(245 158 11), rgb(254 249 195), rgb(250 204 21), rgb(245 158 11));
|
||||
background-size: 200% auto;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
filter: drop-shadow(0 0 0.3rem rgb(250 204 21 / 0.35));
|
||||
animation: award-xp-shimmer 2.5s linear infinite;
|
||||
background: linear-gradient(
|
||||
110deg,
|
||||
rgb(245 158 11),
|
||||
rgb(254 249 195),
|
||||
rgb(250 204 21),
|
||||
rgb(245 158 11)
|
||||
);
|
||||
background-size: 200% auto;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
filter: drop-shadow(0 0 0.3rem rgb(250 204 21 / 0.35));
|
||||
animation: award-xp-shimmer 2.5s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes award-xp-shimmer {
|
||||
to {
|
||||
background-position: 200% center;
|
||||
}
|
||||
to {
|
||||
background-position: 200% center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.award-xp-high {
|
||||
animation: none;
|
||||
}
|
||||
.award-xp-high {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.commendation-card {
|
||||
align-self: start;
|
||||
gap: 0.5rem;
|
||||
padding: 0.625rem 0.75rem;
|
||||
align-self: start;
|
||||
gap: 0.5rem;
|
||||
padding: 0.625rem 0.75rem;
|
||||
}
|
||||
|
||||
/* Scrollbars — styled to match the dark frontend theme */
|
||||
|
|
|
|||
|
|
@ -13,7 +13,11 @@ const getExperiencePointsClass = (experiencePoints: number) => {
|
|||
return "text-muted-foreground";
|
||||
};
|
||||
|
||||
export default function AwardCard(props: { awardImage: Media | null; award: Award }) {
|
||||
export default function AwardCard(props: {
|
||||
awardImage: Media | null;
|
||||
award: Award;
|
||||
backgroundImage?: Media | null;
|
||||
}) {
|
||||
const isCommendation = props.award.type === "commendation" || !props.award.type;
|
||||
const commendationColor =
|
||||
isCommendation &&
|
||||
|
|
@ -22,12 +26,14 @@ export default function AwardCard(props: { awardImage: Media | null; award: Awar
|
|||
? props.award.commendationColor
|
||||
: null;
|
||||
|
||||
const bgUrl = props.backgroundImage?.url;
|
||||
|
||||
return (
|
||||
<Item
|
||||
variant="outline"
|
||||
size={isCommendation ? "sm" : "default"}
|
||||
className={cn("text-center", {
|
||||
"medal-card": props.award.type === "medal",
|
||||
className={cn("relative overflow-hidden text-center", {
|
||||
"medal-card medal-card-overflow": props.award.type === "medal",
|
||||
"commendation-card self-start h-fit": isCommendation,
|
||||
})}
|
||||
style={
|
||||
|
|
@ -39,7 +45,21 @@ export default function AwardCard(props: { awardImage: Media | null; award: Awar
|
|||
: undefined
|
||||
}
|
||||
>
|
||||
<ItemHeader className="flex flex-col items-center">
|
||||
{bgUrl && (
|
||||
<>
|
||||
<img
|
||||
src={bgUrl}
|
||||
alt=""
|
||||
aria-hidden
|
||||
className="medal-card-bg absolute inset-0 w-full h-full object-cover pointer-events-none select-none"
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="medal-card-scrim absolute inset-0 pointer-events-none bg-gradient-to-b from-background/70 via-background/60 to-background/80"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<ItemHeader className="relative z-10 flex flex-col items-center">
|
||||
{props.award.ribbonDesign && (
|
||||
<RibbonViewer
|
||||
ribbonDesign={props.award.ribbonDesign as RibbonDesign}
|
||||
|
|
@ -69,7 +89,7 @@ export default function AwardCard(props: { awardImage: Media | null; award: Awar
|
|||
</div>
|
||||
</div>
|
||||
</ItemHeader>
|
||||
<ItemContent>
|
||||
<ItemContent className="relative z-10">
|
||||
<ItemDescription
|
||||
className={cn("text-center", isCommendation ? "line-clamp-none text-xs" : "line-clamp-3")}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { AwardIcon, LockIcon } from "lucide-react";
|
|||
interface AwardListingCardProps {
|
||||
award: Award;
|
||||
awardImage?: Media | null;
|
||||
backgroundImage?: Media | null;
|
||||
showGrantButton?: boolean;
|
||||
onGrantClick?: () => void;
|
||||
canGrant?: boolean | null;
|
||||
|
|
@ -29,6 +30,7 @@ interface AwardListingCardProps {
|
|||
export function AwardListingCard({
|
||||
award,
|
||||
awardImage,
|
||||
backgroundImage,
|
||||
showGrantButton = false,
|
||||
onGrantClick,
|
||||
canGrant,
|
||||
|
|
@ -42,16 +44,31 @@ export function AwardListingCard({
|
|||
typeof award.ribbonDesign === "object" &&
|
||||
Array.isArray((award.ribbonDesign as RibbonDesign).stripes);
|
||||
const hasArt = hasRibbonArt || Boolean(awardImage?.url);
|
||||
const bgUrl = backgroundImage?.url;
|
||||
|
||||
return (
|
||||
<Item
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"min-w-0 h-full flex flex-col flex-nowrap text-center",
|
||||
isMedal && "justify-center medal-card",
|
||||
"relative overflow-hidden min-w-0 h-full flex flex-col flex-nowrap text-center",
|
||||
isMedal && "justify-center medal-card medal-card-overflow",
|
||||
)}
|
||||
>
|
||||
<ItemHeader className={cn("flex flex-col items-center gap-2", isMedal && "justify-center")}>
|
||||
{bgUrl && (
|
||||
<>
|
||||
<img
|
||||
src={bgUrl}
|
||||
alt=""
|
||||
aria-hidden
|
||||
className="medal-card-bg absolute inset-0 w-full h-full object-cover pointer-events-none select-none"
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="medal-card-scrim absolute inset-0 pointer-events-none bg-gradient-to-b from-background/70 via-background/60 to-background/80"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<ItemHeader className={cn("relative z-10 flex flex-col items-center gap-2", isMedal && "justify-center")}>
|
||||
{hasArt && (
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -80,7 +97,7 @@ export function AwardListingCard({
|
|||
</span>
|
||||
</ItemHeader>
|
||||
|
||||
<ItemContent className="flex flex-col gap-2">
|
||||
<ItemContent className="relative z-10 flex flex-col gap-2">
|
||||
<ItemDescription className="min-w-0 break-words text-center text-sm leading-relaxed">
|
||||
{award.description
|
||||
? convertLexicalToPlaintext({ data: award.description })
|
||||
|
|
@ -94,7 +111,7 @@ export function AwardListingCard({
|
|||
</ItemContent>
|
||||
|
||||
{showGrantButton && (
|
||||
<div className="mt-auto w-full">
|
||||
<div className="relative z-10 mt-auto w-full">
|
||||
{canGrant === false ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
|
|||
|
|
@ -11,9 +11,10 @@ import { getGrantStats } from "@/app/(frontend)/awards/grant-context";
|
|||
interface AwardWithGrantButtonProps {
|
||||
award: Award;
|
||||
awardImage?: Media | null;
|
||||
backgroundImage?: Media | null;
|
||||
}
|
||||
|
||||
export function AwardWithGrantButton({ award, awardImage }: AwardWithGrantButtonProps) {
|
||||
export function AwardWithGrantButton({ award, awardImage, backgroundImage }: AwardWithGrantButtonProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [canGrant, setCanGrant] = useState<boolean | null>(null);
|
||||
const [remainingGrants, setRemainingGrants] = useState<number | null | undefined>(undefined);
|
||||
|
|
@ -40,6 +41,7 @@ export function AwardWithGrantButton({ award, awardImage }: AwardWithGrantButton
|
|||
<AwardListingCard
|
||||
award={award}
|
||||
awardImage={awardImage}
|
||||
backgroundImage={backgroundImage}
|
||||
showGrantButton
|
||||
onGrantClick={() => setOpen(true)}
|
||||
canGrant={canGrant}
|
||||
|
|
|
|||
Loading…
Reference in a new issue