feat: add XP display component and experience collection, integrate user XP in layout
This commit is contained in:
parent
773f6a29c1
commit
d04630ca65
11 changed files with 11407 additions and 16 deletions
3
bun.lock
3
bun.lock
|
|
@ -15,6 +15,7 @@
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||||
|
"@radix-ui/react-progress": "^1.1.8",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
|
@ -552,6 +553,8 @@
|
||||||
|
|
||||||
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "optionalDependencies": { "@types/react": "19.2.7", "@types/react-dom": "19.2.3" }, "peerDependencies": { "react": "19.2.3", "react-dom": "19.2.3" } }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "optionalDependencies": { "@types/react": "19.2.7", "@types/react-dom": "19.2.3" }, "peerDependencies": { "react": "19.2.3", "react-dom": "19.2.3" } }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.8", "", { "dependencies": { "@radix-ui/react-context": "1.1.3", "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA=="],
|
||||||
|
|
||||||
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "optionalDependencies": { "@types/react": "19.2.7", "@types/react-dom": "19.2.3" }, "peerDependencies": { "react": "19.2.3", "react-dom": "19.2.3" } }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
|
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "optionalDependencies": { "@types/react": "19.2.7", "@types/react-dom": "19.2.3" }, "peerDependencies": { "react": "19.2.3", "react-dom": "19.2.3" } }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
|
||||||
|
|
||||||
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "optionalDependencies": { "@types/react": "19.2.7", "@types/react-dom": "19.2.3" }, "peerDependencies": { "react": "19.2.3", "react-dom": "19.2.3" } }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="],
|
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "optionalDependencies": { "@types/react": "19.2.7", "@types/react-dom": "19.2.3" }, "peerDependencies": { "react": "19.2.3", "react-dom": "19.2.3" } }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="],
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||||
|
"@radix-ui/react-progress": "^1.1.8",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
|
|
||||||
11219
pnpm-lock.yaml
Normal file
11219
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -22,6 +22,26 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
|
||||||
const payload = await getPayload({ config });
|
const payload = await getPayload({ config });
|
||||||
const { user } = await payload.auth({ headers, canSetHeaders: false });
|
const { user } = await payload.auth({ headers, canSetHeaders: false });
|
||||||
|
|
||||||
|
let userXp = 0;
|
||||||
|
if (user) {
|
||||||
|
const profileRes = await payload.find({
|
||||||
|
collection: "profiles",
|
||||||
|
where: {
|
||||||
|
user: {
|
||||||
|
equals: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
progression: {
|
||||||
|
awards: true,
|
||||||
|
xp: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(profileRes);
|
||||||
|
userXp = profileRes.docs[0]?.progression?.xp ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en" className="bg-black leading-8 antialiased h-full dark">
|
<html lang="en" className="bg-black leading-8 antialiased h-full dark">
|
||||||
<body className="h-full font-sans m-0 text-white">
|
<body className="h-full font-sans m-0 text-white">
|
||||||
|
|
@ -31,6 +51,7 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
|
||||||
displayName: user?.payloadDisplayName ?? "Guest",
|
displayName: user?.payloadDisplayName ?? "Guest",
|
||||||
username: user?.username ?? "Not logged in",
|
username: user?.username ?? "Not logged in",
|
||||||
}}
|
}}
|
||||||
|
xp={userXp}
|
||||||
adminUrl={payloadConfig.routes.admin}
|
adminUrl={payloadConfig.routes.admin}
|
||||||
className="border-r"
|
className="border-r"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
37
src/collections/users/Experience.ts
Normal file
37
src/collections/users/Experience.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { CollectionConfig } from "payload";
|
||||||
|
import { isDeveloper } from "@/utils/access-control/isRole";
|
||||||
|
|
||||||
|
export const Experience: CollectionConfig = {
|
||||||
|
slug: "experience",
|
||||||
|
access: {
|
||||||
|
create: isDeveloper,
|
||||||
|
update: isDeveloper,
|
||||||
|
delete: isDeveloper,
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "Users",
|
||||||
|
useAsTitle: "name",
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
plural: "XP Levels",
|
||||||
|
singular: "XP Level",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "name",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "requiredPoints",
|
||||||
|
type: "number",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
type: "richText",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
@ -74,24 +74,28 @@ export const Profiles: CollectionConfig = {
|
||||||
async ({ req, siblingData }) => {
|
async ({ req, siblingData }) => {
|
||||||
let xpAcc = 0;
|
let xpAcc = 0;
|
||||||
|
|
||||||
await req.payload
|
const awardsList = (siblingData as { awards?: number[] })?.awards || [];
|
||||||
.find({
|
const scoreboard = (siblingData as { scoreboard?: Record<string, number | null> })?.scoreboard || {};
|
||||||
collection: "awards",
|
|
||||||
where: {
|
|
||||||
id: {
|
|
||||||
in: (siblingData as { awards: number[] }).awards,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
const awards: Award[] = res.docs;
|
|
||||||
xpAcc += awards.reduce((prev, cur) => prev + (cur.experiencePoints ?? 0), 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.entries(
|
if (awardsList.length > 0) {
|
||||||
(siblingData as { scoreboard: Record<string, number | null> }).scoreboard,
|
await req.payload
|
||||||
).forEach(([k, v]) => {
|
.find({
|
||||||
|
collection: "awards",
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: awardsList,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
const awards: Award[] = res.docs;
|
||||||
|
xpAcc += awards.reduce((prev, cur) => prev + (cur.experiencePoints ?? 0), 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(scoreboard).forEach(([k, v]) => {
|
||||||
if (!v) return;
|
if (!v) return;
|
||||||
|
|
||||||
switch (k) {
|
switch (k) {
|
||||||
case "vehicleKills":
|
case "vehicleKills":
|
||||||
xpAcc += v * 100;
|
xpAcc += v * 100;
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import {
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import XPDisplay from "@/components/frontend/blocks/XPDisplay";
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
navMain: [
|
navMain: [
|
||||||
|
|
@ -115,6 +116,7 @@ const data = {
|
||||||
|
|
||||||
export function AppSidebar({
|
export function AppSidebar({
|
||||||
user,
|
user,
|
||||||
|
xp,
|
||||||
adminUrl,
|
adminUrl,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -122,6 +124,7 @@ export function AppSidebar({
|
||||||
displayName: string;
|
displayName: string;
|
||||||
username: string;
|
username: string;
|
||||||
};
|
};
|
||||||
|
xp: number;
|
||||||
adminUrl?: string;
|
adminUrl?: string;
|
||||||
} & React.ComponentProps<typeof Sidebar>) {
|
} & React.ComponentProps<typeof Sidebar>) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -170,6 +173,7 @@ export function AppSidebar({
|
||||||
}
|
}
|
||||||
className="mt-auto"
|
className="mt-auto"
|
||||||
/>
|
/>
|
||||||
|
<XPDisplay xp={xp} />
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<NavUser
|
<NavUser
|
||||||
|
|
|
||||||
26
src/components/frontend/blocks/XPDisplay.tsx
Normal file
26
src/components/frontend/blocks/XPDisplay.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item";
|
||||||
|
import { Progress } from "@/components/ui/progress";
|
||||||
|
import { SparkleIcon } from "lucide-react";
|
||||||
|
|
||||||
|
const XPDisplay = ({ xp }: { xp: number }) => {
|
||||||
|
return (
|
||||||
|
<Item>
|
||||||
|
<ItemHeader>
|
||||||
|
<ItemTitle className="flex justify-between items-center w-full">
|
||||||
|
<SparkleIcon />
|
||||||
|
<div className="flex flex-col items-end">
|
||||||
|
<span className="uppercase">Rookie</span>
|
||||||
|
<span className="text-2xl font-bold uppercase">{xp ?? 0}</span>
|
||||||
|
</div>
|
||||||
|
</ItemTitle>
|
||||||
|
</ItemHeader>
|
||||||
|
<ItemContent>
|
||||||
|
<Progress value={33} />
|
||||||
|
</ItemContent>
|
||||||
|
</Item>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default XPDisplay;
|
||||||
31
src/components/ui/progress.tsx
Normal file
31
src/components/ui/progress.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Progress({
|
||||||
|
className,
|
||||||
|
value,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<ProgressPrimitive.Root
|
||||||
|
data-slot="progress"
|
||||||
|
className={cn(
|
||||||
|
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ProgressPrimitive.Indicator
|
||||||
|
data-slot="progress-indicator"
|
||||||
|
className="bg-primary h-full w-full flex-1 transition-all"
|
||||||
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
|
/>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Progress }
|
||||||
|
|
@ -76,6 +76,7 @@ export interface Config {
|
||||||
awards: Award;
|
awards: Award;
|
||||||
qualifications: Qualification;
|
qualifications: Qualification;
|
||||||
assignments: Assignment;
|
assignments: Assignment;
|
||||||
|
experience: Experience;
|
||||||
missions: Mission;
|
missions: Mission;
|
||||||
campaigns: Campaign;
|
campaigns: Campaign;
|
||||||
factions: Faction;
|
factions: Faction;
|
||||||
|
|
@ -111,6 +112,7 @@ export interface Config {
|
||||||
awards: AwardsSelect<false> | AwardsSelect<true>;
|
awards: AwardsSelect<false> | AwardsSelect<true>;
|
||||||
qualifications: QualificationsSelect<false> | QualificationsSelect<true>;
|
qualifications: QualificationsSelect<false> | QualificationsSelect<true>;
|
||||||
assignments: AssignmentsSelect<false> | AssignmentsSelect<true>;
|
assignments: AssignmentsSelect<false> | AssignmentsSelect<true>;
|
||||||
|
experience: ExperienceSelect<false> | ExperienceSelect<true>;
|
||||||
missions: MissionsSelect<false> | MissionsSelect<true>;
|
missions: MissionsSelect<false> | MissionsSelect<true>;
|
||||||
campaigns: CampaignsSelect<false> | CampaignsSelect<true>;
|
campaigns: CampaignsSelect<false> | CampaignsSelect<true>;
|
||||||
factions: FactionsSelect<false> | FactionsSelect<true>;
|
factions: FactionsSelect<false> | FactionsSelect<true>;
|
||||||
|
|
@ -1001,6 +1003,32 @@ export interface Assignment {
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "experience".
|
||||||
|
*/
|
||||||
|
export interface Experience {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
requiredPoints: number;
|
||||||
|
description: {
|
||||||
|
root: {
|
||||||
|
type: string;
|
||||||
|
children: {
|
||||||
|
type: any;
|
||||||
|
version: number;
|
||||||
|
[k: string]: unknown;
|
||||||
|
}[];
|
||||||
|
direction: ('ltr' | 'rtl') | null;
|
||||||
|
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||||
|
indent: number;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "missions".
|
* via the `definition` "missions".
|
||||||
|
|
@ -1375,6 +1403,10 @@ export interface PayloadLockedDocument {
|
||||||
relationTo: 'assignments';
|
relationTo: 'assignments';
|
||||||
value: number | Assignment;
|
value: number | Assignment;
|
||||||
} | null)
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'experience';
|
||||||
|
value: number | Experience;
|
||||||
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'missions';
|
relationTo: 'missions';
|
||||||
value: number | Mission;
|
value: number | Mission;
|
||||||
|
|
@ -1623,6 +1655,17 @@ export interface AssignmentsSelect<T extends boolean = true> {
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "experience_select".
|
||||||
|
*/
|
||||||
|
export interface ExperienceSelect<T extends boolean = true> {
|
||||||
|
name?: T;
|
||||||
|
requiredPoints?: T;
|
||||||
|
description?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "missions_select".
|
* via the `definition` "missions_select".
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import { Technologies } from "@/collections/intelligence/Technologies";
|
||||||
import { GameRules } from "@/collections/game/GameRules";
|
import { GameRules } from "@/collections/game/GameRules";
|
||||||
import { GameStructures } from "@/collections/game/GameStructures";
|
import { GameStructures } from "@/collections/game/GameStructures";
|
||||||
import { GameHardResources } from "@/collections/game/GameHardResources";
|
import { GameHardResources } from "@/collections/game/GameHardResources";
|
||||||
|
import { Experience } from "@/collections/users/Experience";
|
||||||
|
|
||||||
const filename = fileURLToPath(import.meta.url);
|
const filename = fileURLToPath(import.meta.url);
|
||||||
const dirname = path.dirname(filename);
|
const dirname = path.dirname(filename);
|
||||||
|
|
@ -73,6 +74,7 @@ export default buildConfig({
|
||||||
Awards,
|
Awards,
|
||||||
Qualifications,
|
Qualifications,
|
||||||
Assignments,
|
Assignments,
|
||||||
|
Experience,
|
||||||
|
|
||||||
// Intel
|
// Intel
|
||||||
Missions,
|
Missions,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue