diff --git a/src/app/(frontend)/profile/[username]/page.tsx b/src/app/(frontend)/profile/[username]/page.tsx index 8f6034e..b61400a 100644 --- a/src/app/(frontend)/profile/[username]/page.tsx +++ b/src/app/(frontend)/profile/[username]/page.tsx @@ -2,7 +2,7 @@ import config from "@payload-config"; import { getPayload } from "payload"; import { notFound } from "next/navigation"; import Link from "next/link"; -import type { Assignment, Award, Media, Qualification, Rank, User } from "@/payload-types"; +import type { Assignment, Media, Qualification, Rank, User } from "@/payload-types"; import { RichText } from "@payloadcms/richtext-lexical/react"; import { resolveLevel } from "@/utils/xp/resolveLevel"; import { @@ -13,6 +13,7 @@ import { CrosshairIcon, FileTextIcon, HashIcon, + HourglassIcon, PlaneIcon, ShieldCheck, ShieldIcon, @@ -70,9 +71,12 @@ export default async function ProfilePage({ params }: ProfilePageProps) { const profile = profileRes.docs[0]; const user = profile.user as User; const rank = profile.rank as Rank; - const awards = (profile.progression?.awards ?? []).filter( - (a): a is Award => typeof a === "object" && a !== null, - ); + const awards = (profile.progression?.awards ?? []).flatMap((entry) => { + if (typeof entry !== "object" || entry === null) return []; + + const award = entry.award; + return typeof award === "object" && award !== null ? [award] : []; + }); const qualifications = (profile.progression?.qualifications ?? []).filter( (q): q is Qualification => typeof q === "object" && q !== null, ); @@ -252,9 +256,16 @@ export default async function ProfilePage({ params }: ProfilePageProps) { Intelligence Record -
- Verified by the Intelligence Division -
+ {profile.dossier?.intelExcerpt ? ( +
+ Verified by the Intelligence Division +
+ ) : ( +
+ Undergoing review by the Intelligence Division{" "} + +
+ )}
{profile.dossier?.intelExcerpt ? ( diff --git a/src/app/(frontend)/styles.css b/src/app/(frontend)/styles.css index 23719ef..53614c7 100644 --- a/src/app/(frontend)/styles.css +++ b/src/app/(frontend)/styles.css @@ -299,20 +299,20 @@ } @keyframes xp-gain { - 0% { - opacity: 0; - transform: translateX(0.35rem); - } - 15% { - opacity: 1; - } - 75% { - opacity: 1; - } - 100% { - opacity: 0; - transform: translateX(-1.1rem); - } + 0% { + opacity: 0; + transform: translateX(0.35rem); + } + 15% { + opacity: 1; + } + 75% { + opacity: 1; + } + 100% { + opacity: 0; + transform: translateX(-1.1rem); + } } .locker-search-highlight { @@ -320,6 +320,62 @@ box-shadow: 0 0 0 3px rgb(251 191 36 / 0.9); } +@property --medal-border-angle { + syntax: ""; + inherits: false; + initial-value: 0deg; +} + +@keyframes border-spin-animate { + from { + --medal-border-angle: 0deg; + } + to { + --medal-border-angle: 360deg; + } +} + +.medal-card { + --medal-border-angle: 0deg; + background: rgb(193 161 79 / 0.28); + position: relative; + isolation: isolate; +} + +.medal-card::before { + content: ''; + position: absolute; + inset: -0.125rem; + z-index: 0; + border-radius: inherit; + border: 2px solid transparent; + background: linear-gradient(var(--background), var(--background)) padding-box, repeating-conic-gradient(from var(--medal-border-angle), transparent 0deg 150deg, peru 159deg, rgb(253 224 71) 168deg, white 178deg, transparent 178deg) border-box; + animation: border-spin-animate 10s linear infinite; + pointer-events: none; +} + +@keyframes border-spin-animate { + from { + --medal-border-angle: 0deg; + } + to { + --medal-border-angle: 360deg; + } +} + +.medal-card * { + position: relative; + z-index: 1; +} + +.medal-card::after { + content: ''; + 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; +} + /* Scrollbars — styled to match the dark frontend theme */ * { scrollbar-width: thin; diff --git a/src/components/frontend/account/AwardCard.tsx b/src/components/frontend/account/AwardCard.tsx index 52944d4..adb281f 100644 --- a/src/components/frontend/account/AwardCard.tsx +++ b/src/components/frontend/account/AwardCard.tsx @@ -3,14 +3,17 @@ import { Item, ItemContent, ItemDescription, ItemHeader, ItemTitle } from "@/com import { convertLexicalToPlaintext } from "@payloadcms/richtext-lexical/plaintext"; import { RibbonViewer } from "@/components/frontend/account/RibbonViewer"; import { RibbonDesign } from "@/components/admin/awards/RibbonDesigner"; +import { cn } from "@/lib/utils"; -export default function AwardCard(props: { - awardImage: Media | null; - award: Award; -}) { +export default function AwardCard(props: { awardImage: Media | null; award: Award }) { return ( - - + + {props.award.ribbonDesign && ( )}
- {props.award.name} + + {props.award.name} + {props.award.type ?? "commendation"} diff --git a/src/components/frontend/account/RibbonRack.tsx b/src/components/frontend/account/RibbonRack.tsx index 94dfa17..a7ff997 100644 --- a/src/components/frontend/account/RibbonRack.tsx +++ b/src/components/frontend/account/RibbonRack.tsx @@ -8,13 +8,13 @@ export const RibbonRack = ({ awards }: { awards: Award[] }) => { return (
{awards - .filter((award) => award.ribbonDesign !== null) + .filter((award) => award.type === "ribbon" && award.ribbonDesign != null) .sort((a, b) => (b.experiencePoints ?? 0) - (a.experiencePoints ?? 0)) .map((award, key) => { const ribbon = award.ribbonDesign as RibbonDesign; return ( - + diff --git a/src/components/frontend/locker/Wardrobe.tsx b/src/components/frontend/locker/Wardrobe.tsx index ba46406..6d53be1 100644 --- a/src/components/frontend/locker/Wardrobe.tsx +++ b/src/components/frontend/locker/Wardrobe.tsx @@ -115,8 +115,7 @@ export function Wardrobe({
{layout.label} diff --git a/src/components/static/VersionOverlay.tsx b/src/components/static/VersionOverlay.tsx index a109439..b0a2889 100644 --- a/src/components/static/VersionOverlay.tsx +++ b/src/components/static/VersionOverlay.tsx @@ -2,9 +2,6 @@ import { useEffect, useState } from "react"; import type { VersionInfo } from "@/lib/versionInfo"; -import { getPayload } from "payload"; -import config from "@payload-config"; -import { User } from "@/payload-types"; const EMPTY: VersionInfo = { version: null, diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx index f6b8dbc..c3a3f59 100644 --- a/src/components/ui/tooltip.tsx +++ b/src/components/ui/tooltip.tsx @@ -34,24 +34,29 @@ function TooltipContent({ className, sideOffset = 0, children, + arrowClassName, ...props -}: React.ComponentProps) { +}: React.ComponentProps & { arrowClassName?: string }) { return ( {children} - + - ) + ); } export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/src/payload-types.ts b/src/payload-types.ts index 91adecc..ca73a51 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -1610,7 +1610,24 @@ export interface Profile { }; progression?: { xp?: number | null; - awards?: (number | Award)[] | null; + awards?: + | { + award: number | Award; + /** + * Who issued the award. If no one, the award will be attributed as "on behalf of the unit." + */ + awardedBy?: (number | null) | Profile; + /** + * The issuing date of the award. + */ + awardedAt: string; + /** + * An optional reason for the award. + */ + awardReason?: string | null; + id?: string | null; + }[] + | null; qualifications?: (number | Qualification)[] | null; scoreboard?: { infantryKills?: number | null; @@ -3239,7 +3256,15 @@ export interface ProfilesSelect { | T | { xp?: T; - awards?: T; + awards?: + | T + | { + award?: T; + awardedBy?: T; + awardedAt?: T; + awardReason?: T; + id?: T; + }; qualifications?: T; scoreboard?: | T diff --git a/src/utils/access-control/hasIntelligenceQualification.ts b/src/utils/access-control/hasIntelligenceQualification.ts index 1f25172..ad96202 100644 --- a/src/utils/access-control/hasIntelligenceQualification.ts +++ b/src/utils/access-control/hasIntelligenceQualification.ts @@ -33,8 +33,15 @@ export async function hasIntelligenceQualification( const profile = (await payload.find({ collection: "profiles", where: { user: { equals: user.id as number } }, - depth: 4, + // Only qualifications are needed here. A deeper read recursively + // hydrates Profiles through progression.awards.awardedBy. + depth: 1, limit: 1, + select: { + progression: { + qualifications: true, + }, + }, })) as unknown as { docs: Array }; const userProfile = profile.docs[0];