Skip to content

Commit

Permalink
feat: composition details (#218)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebald authored Jul 26, 2023
1 parent e0b67a9 commit 36a7922
Show file tree
Hide file tree
Showing 47 changed files with 1,979 additions and 649 deletions.
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.next
node_modules
pnpm-lock.yaml
7 changes: 7 additions & 0 deletions app/_components/mobile-navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ export const MobileNavigation = () => {
Pattern Analyzer
</NavLink>
<div className="flex flex-col gap-7 pl-2">
<NavLink
href="/"
close={close}
className="text-xl font-medium hover:text-primary-500"
>
Home
</NavLink>
{[...siteNavigation, ...secondaryNavigation].map(({ name, href }) => (
<NavLink
key={href}
Expand Down
121 changes: 121 additions & 0 deletions app/analyze/composition/[id]/_component/pilot-details.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
'use client';

import { Fragment, useState } from 'react';

import { type Ships, getPilotName } from '@/lib/get-value';
import type { SquadCompositionStats } from '@/lib/stats/details/composition';
import { toPercentage } from '@/lib/utils';
import { isStandardized, upgradesToList } from '@/lib/xws';

import { PilotImage, Detail, Card, Switch } from '@/ui';
import { Info } from '@/ui/icons';

// Props
// ---------------
export interface PilotDetailProps {
className?: string;
ships: Ships[];
value: SquadCompositionStats['pilot'];
}

// Components
// ---------------
export const PilotDetails = ({ className, ships, value }: PilotDetailProps) => {
const [grouped, setGrouped] = useState<boolean>(true);

const data = Object.entries(value);

if (grouped) {
data.sort(([, a], [, b]) => {
const result = ships.indexOf(a.ship) - ships.indexOf(b.ship);
return result !== 0 ? result : b.percentile - a.percentile;
});
}

return (
<Card className={className} inset="list">
<Card.Header>
<Card.Title>Pilots</Card.Title>
<Card.Actions className="px-4">
<Switch
size="small"
label="Group by Chassis"
checked={grouped}
onCheckedChange={setGrouped}
/>
</Card.Actions>
<Card.Body variant="enumeration">
{data.map(([pid, current]) => (
<div
key={pid}
className="grid gap-x-4 gap-y-6 px-4 py-5 md:grid-cols-[150px,auto] md:grid-rows-[auto,auto,auto] lg:grid-cols-[200px,auto]"
>
<PilotImage
className="row-span-full hidden rounded-md md:block"
pilot={pid}
type="art"
width={250}
height={250}
/>
<div className="text-2xl font-bold leading-none">
{getPilotName(pid)}
</div>
<div className="flex flex-wrap gap-x-8 gap-y-4">
<Detail
variant="secondary"
className="max-w-[150px]"
label="Percentile"
value={toPercentage(current.percentile)}
/>
<Detail
variant="secondary"
className="max-w-[150px]"
label="Deviation"
value={
current.deviation ? toPercentage(current.deviation) : '-'
}
/>
<Detail
variant="secondary"
className="max-w-[150px]"
label="Winrate"
value={current.winrate ? toPercentage(current.winrate) : '-'}
/>
<Detail
variant="secondary"
className="max-w-[150px]"
label="Frequency"
value={toPercentage(current.frequency)}
/>
</div>
<div>
<div className="text-sm font-medium text-secondary-400">
Loadout Performance
</div>
{isStandardized(pid) ? (
<div className="text-secondary-950 flex items-center gap-1 pt-2 text-sm italic">
<Info className="h-4 w-4" /> Standarized Pilot. No
variations.
</div>
) : (
<div className="grid grid-cols-[max-content,auto] gap-x-6 gap-y-3 pt-2">
{current.upgrades.map(({ id, list, count, percentile }) => (
<Fragment key={id}>
<div className="text-right text-sm leading-6 text-secondary-600">
{toPercentage(percentile)} ({count})
</div>
<div className="font-medium">
{upgradesToList(list)}
</div>
</Fragment>
))}
</div>
)}
</div>
</div>
))}
</Card.Body>
</Card.Header>
</Card>
);
};
76 changes: 76 additions & 0 deletions app/analyze/composition/[id]/_component/squad-groups.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
'use client';

import { Accordion, Badge, CopyButton, Detail, Squad, Timeline } from '@/ui';

import { getPilotName } from '@/lib/get-value';
import { type SquadCompositionStats } from '@/lib/stats/details/composition';
import { formatDate } from '@/lib/utils/date.utils';
import { toPercentage } from '@/lib/utils/math.utils';
import { Copy } from '@/ui/icons';

// Props
// ---------------
export interface SquadGroupsProps {
value: SquadCompositionStats['squads'];
}

// Component
// ---------------
export const SquadGroups = ({ value }: SquadGroupsProps) => {
const data = Object.entries(value);
data.sort(([, a], [, b]) => b.percentile - a.percentile);

return (
<Accordion type="multiple">
{Object.entries(value).map(([id, current]) => (
<Accordion.Item value={id} key={id}>
<Accordion.Trigger className="flex gap-4 text-lg">
<div className="w-14">
<Badge variant="light">{current.items.length}</Badge>
</div>
{id.split('.').map(getPilotName).join(', ')}
</Accordion.Trigger>
<Accordion.Content>
<div className="flex flex-col gap-8 pt-6">
<div className="flex flex-wrap gap-8 px-2">
<Detail
variant="secondary"
label="Percentile"
value={toPercentage(current.percentile)}
/>
<Detail
variant="secondary"
label="Deviation"
value={
current.deviation ? toPercentage(current.deviation) : '-'
}
/>
<Detail
variant="secondary"
label="Winrate"
value={current.winrate ? toPercentage(current.winrate) : '-'}
/>
</div>
<Timeline>
{current.items.map(({ date, player, xws }) => (
<Timeline.Item key={date + player}>
<Timeline.Header>
{formatDate(new Date(date))}
<Timeline.Caption>by {player}</Timeline.Caption>
</Timeline.Header>
<Timeline.Body className="flex flex-1 flex-col items-start justify-between gap-4 md:mt-[3px] md:gap-6 lg:flex-row">
<Squad variant="narrow" xws={xws} />
<CopyButton size="small" content={JSON.stringify(xws)}>
<Copy className="inline-block h-4 w-4" /> Copy XWS
</CopyButton>
</Timeline.Body>
</Timeline.Item>
))}
</Timeline>
</div>
</Accordion.Content>
</Accordion.Item>
))}
</Accordion>
);
};
74 changes: 74 additions & 0 deletions app/analyze/composition/[id]/_component/trend-curve.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
'use client';

import { toPercentage } from '@/lib/utils';
import { formatMonth } from '@/lib/utils/date.utils';
import { linearGradientDef } from '@nivo/core';
import { ResponsiveLine } from '@nivo/line';

// Props
// ---------------
export interface TrendCurveProps {
value: {
/**
* Date format YYYY-MM
*/
date: string;
count: number;
percentile: number;
}[];
}

// Components
// ---------------
export const TrendCurve = ({ value }: TrendCurveProps) => {
const data = value.map(({ date, percentile, count }) => ({
x: date,
y: percentile,
count,
}));

return (
<div className="grid h-64 auto-cols-fr">
<ResponsiveLine
data={[
{
id: 'squad trend',
data,
},
]}
curve="monotoneX"
axisBottom={{
format: formatMonth,
}}
yScale={{
type: 'linear',
min: 0,
max: 1,
}}
axisLeft={{
tickValues: 3,
format: toPercentage,
legend: <tspan style={{ fontWeight: 700 }}>Percentile</tspan>,
legendPosition: 'middle',
legendOffset: -45,
}}
pointLabel={({ y }) => toPercentage(y as number)}
enablePointLabel
enableArea
enableGridX={false}
pointSize={8}
defs={[
linearGradientDef('gradient', [
{ offset: 0, color: '#5155b1' },
{ offset: 100, color: '#96a6e3' },
]),
]}
colors="#5155b1"
fill={[{ match: '*', id: 'gradient' }]}
margin={{ top: 20, right: 30, bottom: 30, left: 60 }}
isInteractive={false}
animate={false}
/>
</div>
);
};
13 changes: 13 additions & 0 deletions app/analyze/composition/[id]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { ReactNode } from 'react';
import { Title } from '@/ui';

const Layout = ({ children }: { children: ReactNode }) => (
<>
<div className="pb-6">
<Title>Composition Details</Title>
</div>
<div>{children}</div>
</>
);

export default Layout;
Loading

1 comment on commit 36a7922

@vercel
Copy link

@vercel vercel bot commented on 36a7922 Jul 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.