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-dropdown-menu": "^2.1.16",
|
||||
"@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-slot": "^1.2.4",
|
||||
"@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-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-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-dropdown-menu": "^2.1.16",
|
||||
"@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-slot": "^1.2.4",
|
||||
"@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 { 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 (
|
||||
<html lang="en" className="bg-black leading-8 antialiased h-full dark">
|
||||
<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",
|
||||
username: user?.username ?? "Not logged in",
|
||||
}}
|
||||
xp={userXp}
|
||||
adminUrl={payloadConfig.routes.admin}
|
||||
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 }) => {
|
||||
let xpAcc = 0;
|
||||
|
||||
await req.payload
|
||||
.find({
|
||||
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);
|
||||
});
|
||||
const awardsList = (siblingData as { awards?: number[] })?.awards || [];
|
||||
const scoreboard = (siblingData as { scoreboard?: Record<string, number | null> })?.scoreboard || {};
|
||||
|
||||
Object.entries(
|
||||
(siblingData as { scoreboard: Record<string, number | null> }).scoreboard,
|
||||
).forEach(([k, v]) => {
|
||||
if (awardsList.length > 0) {
|
||||
await req.payload
|
||||
.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;
|
||||
|
||||
switch (k) {
|
||||
case "vehicleKills":
|
||||
xpAcc += v * 100;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import {
|
|||
} from "@/components/ui/sidebar";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import XPDisplay from "@/components/frontend/blocks/XPDisplay";
|
||||
|
||||
const data = {
|
||||
navMain: [
|
||||
|
|
@ -115,6 +116,7 @@ const data = {
|
|||
|
||||
export function AppSidebar({
|
||||
user,
|
||||
xp,
|
||||
adminUrl,
|
||||
...props
|
||||
}: {
|
||||
|
|
@ -122,6 +124,7 @@ export function AppSidebar({
|
|||
displayName: string;
|
||||
username: string;
|
||||
};
|
||||
xp: number;
|
||||
adminUrl?: string;
|
||||
} & React.ComponentProps<typeof Sidebar>) {
|
||||
return (
|
||||
|
|
@ -170,6 +173,7 @@ export function AppSidebar({
|
|||
}
|
||||
className="mt-auto"
|
||||
/>
|
||||
<XPDisplay xp={xp} />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<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;
|
||||
qualifications: Qualification;
|
||||
assignments: Assignment;
|
||||
experience: Experience;
|
||||
missions: Mission;
|
||||
campaigns: Campaign;
|
||||
factions: Faction;
|
||||
|
|
@ -111,6 +112,7 @@ export interface Config {
|
|||
awards: AwardsSelect<false> | AwardsSelect<true>;
|
||||
qualifications: QualificationsSelect<false> | QualificationsSelect<true>;
|
||||
assignments: AssignmentsSelect<false> | AssignmentsSelect<true>;
|
||||
experience: ExperienceSelect<false> | ExperienceSelect<true>;
|
||||
missions: MissionsSelect<false> | MissionsSelect<true>;
|
||||
campaigns: CampaignsSelect<false> | CampaignsSelect<true>;
|
||||
factions: FactionsSelect<false> | FactionsSelect<true>;
|
||||
|
|
@ -1001,6 +1003,32 @@ export interface Assignment {
|
|||
updatedAt: 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
|
||||
* via the `definition` "missions".
|
||||
|
|
@ -1375,6 +1403,10 @@ export interface PayloadLockedDocument {
|
|||
relationTo: 'assignments';
|
||||
value: number | Assignment;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'experience';
|
||||
value: number | Experience;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'missions';
|
||||
value: number | Mission;
|
||||
|
|
@ -1623,6 +1655,17 @@ export interface AssignmentsSelect<T extends boolean = true> {
|
|||
updatedAt?: 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
|
||||
* via the `definition` "missions_select".
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import { Technologies } from "@/collections/intelligence/Technologies";
|
|||
import { GameRules } from "@/collections/game/GameRules";
|
||||
import { GameStructures } from "@/collections/game/GameStructures";
|
||||
import { GameHardResources } from "@/collections/game/GameHardResources";
|
||||
import { Experience } from "@/collections/users/Experience";
|
||||
|
||||
const filename = fileURLToPath(import.meta.url);
|
||||
const dirname = path.dirname(filename);
|
||||
|
|
@ -73,6 +74,7 @@ export default buildConfig({
|
|||
Awards,
|
||||
Qualifications,
|
||||
Assignments,
|
||||
Experience,
|
||||
|
||||
// Intel
|
||||
Missions,
|
||||
|
|
|
|||
Loading…
Reference in a new issue