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