diff --git a/src/app/(frontend)/profile/[username]/page.tsx b/src/app/(frontend)/profile/[username]/page.tsx
index 0e9a7c1..5d57d25 100644
--- a/src/app/(frontend)/profile/[username]/page.tsx
+++ b/src/app/(frontend)/profile/[username]/page.tsx
@@ -595,8 +595,15 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
{awards.length > 0 ? (
- {awards
- .sort((a, b) => (b.experiencePoints ?? 0) - (a.experiencePoints ?? 0))
+ {[...awards]
+ .sort((a, b) => {
+ const typeOrder = { medal: 0, ribbon: 1, commendation: 2 } as const;
+ const categoryOrder =
+ (typeOrder[a.type as keyof typeof typeOrder] ?? typeOrder.commendation) -
+ (typeOrder[b.type as keyof typeof typeOrder] ?? typeOrder.commendation);
+
+ return categoryOrder || (b.experiencePoints ?? 0) - (a.experiencePoints ?? 0);
+ })
.map((award) => {
const awardImage =
award.image && typeof award.image === "object" ? (award.image as Media) : null;
diff --git a/src/app/(frontend)/styles.css b/src/app/(frontend)/styles.css
index 53614c7..b451078 100644
--- a/src/app/(frontend)/styles.css
+++ b/src/app/(frontend)/styles.css
@@ -376,6 +376,28 @@
animation: pulse 5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
+.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;
+}
+
+@keyframes award-xp-shimmer {
+ to {
+ background-position: 200% center;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .award-xp-high {
+ animation: none;
+ }
+}
+
/* Scrollbars — styled to match the dark frontend theme */
* {
scrollbar-width: thin;
diff --git a/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js
index 704732c..ff5af9b 100644
--- a/src/app/(payload)/admin/importMap.js
+++ b/src/app/(payload)/admin/importMap.js
@@ -4,6 +4,7 @@ import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8c
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { LexicalEditorFeatureClient as LexicalEditorFeatureClient_4490b89d4413c1ffaecdacfe72efaf73 } from '@ai-stack/payloadcms/client'
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { default as default_d9c22b951ac6762f436db59b889da382 } from '@/components/admin/awards/CommendationColorField'
import { default as default_c3ea68702b5e5f8f2f98055e01c43c8d } from '@/components/admin/awards/RibbonDesigner'
import { default as default_2f2033acccb3be245f1a85f7b4271dac } from '@/components/admin/awards/MedalDesigner'
import { default as default_0edde40a6ccd30c893b1227360f69360 } from '@/components/static/TickDurationDescription'
@@ -28,6 +29,7 @@ export const importMap = {
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
"@ai-stack/payloadcms/client#LexicalEditorFeatureClient": LexicalEditorFeatureClient_4490b89d4413c1ffaecdacfe72efaf73,
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+ "@/components/admin/awards/CommendationColorField#default": default_d9c22b951ac6762f436db59b889da382,
"@/components/admin/awards/RibbonDesigner#default": default_c3ea68702b5e5f8f2f98055e01c43c8d,
"@/components/admin/awards/MedalDesigner#default": default_2f2033acccb3be245f1a85f7b4271dac,
"@/components/static/TickDurationDescription#default": default_0edde40a6ccd30c893b1227360f69360,
diff --git a/src/collections/users/Awards.ts b/src/collections/users/Awards.ts
index 39ef8ea..d6b96bd 100644
--- a/src/collections/users/Awards.ts
+++ b/src/collections/users/Awards.ts
@@ -43,6 +43,17 @@ export const Awards: CollectionConfig = {
],
defaultValue: "commendation",
},
+ {
+ name: "commendationColor",
+ type: "text",
+ defaultValue: "#92400e",
+ admin: {
+ components: {
+ Field: "@/components/admin/awards/CommendationColorField",
+ },
+ condition: (data) => data?.type === "commendation",
+ },
+ },
{
name: "image",
type: "upload",
diff --git a/src/components/admin/awards/CommendationColorField.tsx b/src/components/admin/awards/CommendationColorField.tsx
new file mode 100644
index 0000000..7f80c6c
--- /dev/null
+++ b/src/components/admin/awards/CommendationColorField.tsx
@@ -0,0 +1,40 @@
+"use client";
+
+import { useField } from "@payloadcms/ui";
+
+const DEFAULT_COLOR = "#92400e";
+
+export default function CommendationColorField() {
+ const { value, setValue } = useField
();
+ const color = typeof value === "string" && /^#[0-9a-f]{6}$/i.test(value) ? value : DEFAULT_COLOR;
+
+ return (
+
+
+
+ setValue(event.target.value)}
+ aria-label="Choose commendation color"
+ className="h-10 w-14 cursor-pointer rounded border border-border bg-transparent p-1"
+ />
+ setValue(event.target.value)}
+ placeholder={DEFAULT_COLOR}
+ pattern="^#[0-9a-fA-F]{6}$"
+ aria-describedby="commendation-color-description"
+ className="field-type__input"
+ />
+
+
+ Used for the border and background of this commendation on service records.
+
+
+ );
+}
diff --git a/src/components/frontend/account/AwardCard.tsx b/src/components/frontend/account/AwardCard.tsx
index adb281f..189d550 100644
--- a/src/components/frontend/account/AwardCard.tsx
+++ b/src/components/frontend/account/AwardCard.tsx
@@ -5,13 +5,39 @@ import { RibbonViewer } from "@/components/frontend/account/RibbonViewer";
import { RibbonDesign } from "@/components/admin/awards/RibbonDesigner";
import { cn } from "@/lib/utils";
+const getExperiencePointsClass = (experiencePoints: number) => {
+ if (experiencePoints >= 5000) return "award-xp-high";
+ if (experiencePoints >= 1000) return "text-emerald-400";
+ if (experiencePoints >= 501) return "text-lime-300";
+ if (experiencePoints >= 200) return "text-sky-300";
+ return "text-muted-foreground";
+};
+
export default function AwardCard(props: { awardImage: Media | null; award: Award }) {
+ const isCommendation = props.award.type === "commendation" || !props.award.type;
+ const commendationColor =
+ isCommendation &&
+ typeof props.award.commendationColor === "string" &&
+ /^#[0-9a-f]{6}$/i.test(props.award.commendationColor)
+ ? props.award.commendationColor
+ : null;
+
return (
-
{props.award.ribbonDesign && (
@@ -25,27 +51,40 @@ export default function AwardCard(props: { awardImage: Media | null; award: Awar
)}
-
+
{props.award.name}
-
+
{props.award.type ?? "commendation"}