diff --git a/bun.lock b/bun.lock
index 4f739e3..09c4529 100644
--- a/bun.lock
+++ b/bun.lock
@@ -23,6 +23,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.15",
"@radix-ui/react-navigation-menu": "^1.2.14",
+ "@radix-ui/react-popover": "^1.1.23",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-select": "^2.3.7",
"@radix-ui/react-separator": "^1.1.8",
diff --git a/package.json b/package.json
index 885702d..60a9671 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.15",
"@radix-ui/react-navigation-menu": "^1.2.14",
+ "@radix-ui/react-popover": "^1.1.23",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-select": "^2.3.7",
"@radix-ui/react-separator": "^1.1.8",
diff --git a/public/images/ribbon-overlay.png b/public/images/ribbon-overlay.png
new file mode 100644
index 0000000..170ea96
Binary files /dev/null and b/public/images/ribbon-overlay.png differ
diff --git a/src/app/(frontend)/profile/[username]/page.tsx b/src/app/(frontend)/profile/[username]/page.tsx
index edf76a6..8f6034e 100644
--- a/src/app/(frontend)/profile/[username]/page.tsx
+++ b/src/app/(frontend)/profile/[username]/page.tsx
@@ -4,7 +4,6 @@ import { notFound } from "next/navigation";
import Link from "next/link";
import type { Assignment, Award, Media, Qualification, Rank, User } from "@/payload-types";
import { RichText } from "@payloadcms/richtext-lexical/react";
-import { convertLexicalToPlaintext } from "@payloadcms/richtext-lexical/plaintext";
import { resolveLevel } from "@/utils/xp/resolveLevel";
import {
ArrowLeftIcon,
@@ -32,6 +31,8 @@ import {
ItemMedia,
ItemTitle,
} from "@/components/ui/item";
+import AwardCard from "@/components/frontend/account/AwardCard";
+import { RibbonRack } from "@/components/frontend/account/RibbonRack";
interface ProfilePageProps {
params: Promise<{ username: string }>;
@@ -151,6 +152,7 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
|
@{user.username}
+
{
const awardImage =
award.image && typeof award.image === "object" ? (award.image as Media) : null;
- return (
-
-
-
-
- {awardImage?.url && (
-

- )}
-
- {award.name}
-
- {award.type ?? "commendation"}
-
-
-
-
-
-
- {award.description
- ? convertLexicalToPlaintext({ data: award.description }).slice(0, 120)
- : "No description."}
-
- {award.experiencePoints != null && (
-
- +{award.experiencePoints} XP
-
- )}
-
-
- );
+ return
;
})}
) : (
diff --git a/src/components/admin/awards/RibbonDesigner.tsx b/src/components/admin/awards/RibbonDesigner.tsx
index 696d274..ad964e4 100644
--- a/src/components/admin/awards/RibbonDesigner.tsx
+++ b/src/components/admin/awards/RibbonDesigner.tsx
@@ -19,7 +19,7 @@ type Stripe = {
width: number;
};
-type RibbonDesign = {
+export type RibbonDesign = {
stripes: Stripe[];
devices: Device[];
mirrored: boolean;
@@ -27,7 +27,7 @@ type RibbonDesign = {
// ─── Constants ──────────────────────────────────────────────────────
-const DEFAULT_RIBBON: RibbonDesign = {
+export const DEFAULT_RIBBON: RibbonDesign = {
stripes: [
{ color: "#b91c1c", width: 30 },
{ color: "#f5f5f4", width: 5 },
@@ -62,16 +62,20 @@ const MILITARY_COLORS: { name: string; hex: string }[] = [
{ name: "Black", hex: "#18181b" },
];
-const DEVICE_INFO: Record
= {
+export const RIBBON_DEVICE_INFO: Record = {
star: { label: "Star", symbol: "\u2605" },
oakLeaf: { label: "Oak Leaf", symbol: "\u{1F333}" },
arrowUp: { label: "Arrow", symbol: "\u2191" },
campaignClasp: { label: "Clasp", symbol: "\u2015" },
};
-const RIBBON_ASPECT = 3.667;
-const RIBBON_WIDTH = 366;
-const PREVIEW_HEIGHT = 80;
+export const RIBBON_CONSTANTS = {
+ RIBBON_ASPECT: 3.667,
+ RIBBON_WIDTH: 366,
+ PREVIEW_HEIGHT: 80,
+};
+
+const { RIBBON_ASPECT, RIBBON_WIDTH, PREVIEW_HEIGHT } = RIBBON_CONSTANTS;
// ─── Preview ────────────────────────────────────────────────────────
@@ -103,7 +107,7 @@ function RibbonPreview({ design }: { design: RibbonDesign }) {
})()}
{devices.map((device) => {
- const info = DEVICE_INFO[device.type];
+ const info = RIBBON_DEVICE_INFO[device.type];
const cx = (device.position / 100) * viewWidth;
const cy = (device.positionY / 100) * viewHeight;
const fontSize = Math.min(viewHeight * 0.45, 24);
@@ -218,8 +222,8 @@ function DeviceEditor({
Devices
- {(Object.keys(DEVICE_INFO) as DeviceType[]).map((type) => {
- const info = DEVICE_INFO[type];
+ {(Object.keys(RIBBON_DEVICE_INFO) as DeviceType[]).map((type) => {
+ const info = RIBBON_DEVICE_INFO[type];
return (